class ReadThroughMultiCacheAdvice extends MultiCacheAdvice {

    @Around("getMulti()")
    @SuppressWarnings("unchecked")
    public Object cacheMulti(final ProceedingJoinPoint pjp) throws Throwable {
        if (isDisabled()) {
            getLogger().info("Cache disabled");
            return pjp.proceed();
        }

        // This is injected caching. If anything goes wrong in the caching, LOG
        // the crap outta it, but do not let it surface up past the AOP injection itself.
        final ReadThroughMultiCache annotation;
        final MultiCacheCoordinator coord;
        final AnnotationData data;
        final SerializationType serializationType;

        Object[] args = pjp.getArgs();
        try {
            // Get the target method being invoked, and make sure it returns the correct info.
            final Method methodToCache = getCacheBase().getMethodToCache(pjp, ReadThroughMultiCache.class);
            getCacheBase().verifyReturnTypeIsList(methodToCache, ReadThroughMultiCache.class);

            // Get the annotation associated with this method, and make sure the values are valid.
            annotation = methodToCache.getAnnotation(ReadThroughMultiCache.class);
            serializationType = getCacheBase().getSerializationType(methodToCache);

            data = AnnotationDataBuilder.buildAnnotationData(annotation, ReadThroughMultiCache.class, methodToCache);
            coord = new MultiCacheCoordinator(methodToCache, data);
            setMultiCacheOptions(coord, annotation.option());

            // Create key->object and object->key mappings.
            coord.setHolder(createObjectIdCacheKeyMapping(data, args, coord.getMethod()));

            List<Object> listKeyObjects = (List<Object>) Utils.getMethodArg(data.getListIndexInMethodArgs(), args, coord.getMethod()
                    .toString());
            coord.setListKeyObjects(listKeyObjects);

            // Get the full list of cache keys and ask the cache for the corresponding values.
            coord.setInitialKey2Result(getCacheBase().getCache(data).getBulk(coord.getKey2Obj().keySet(), serializationType));

            // We've gotten all positive cache results back, so build up a results list and return it.
            if (coord.getMissedObjects().isEmpty()) {
                return coord.generateResultList();
            }

            // Create the new list of arguments with a subset of the key objects that aren't in the cache. Do not modify
            // directly argument array from join point!
            args = coord.createModifiedArgumentList(args);
        } catch (Exception ex) {
            warn(ex, "Caching on %s aborted due to an error.", pjp.toShortString());
            return pjp.proceed();
        }

        /*
         * Call the target method with the new subset of arguments. We are calling this outside of the try/catch block
         * in case there are some 'not our fault' problems with the target method. (Connection issues, etc...) Though,
         * this decision could go either way, really.
         */
        final List<Object> results = (List<Object>) pjp.proceed(args);

        try {
            // there are no results
            if (results == null || results.isEmpty()) {
                if (coord.isAddNullsToCache()) {
                    addNullValues(coord.getMissedObjects(), coord, serializationType);
                }
                return coord.generatePartialResultList();
            }

            if (coord.isGenerateKeysFromResult()) {
                return generateByKeysFromResult(results, coord, serializationType);
            } else {
                return generateByKeysProviders(results, coord, serializationType);
            }
        } catch (Exception ex) {
            warn(ex, "Caching on %s aborted due to an error. The underlying method will be called twice.", pjp.toShortString());
            // invoke underlying method again using unmodified arguments array
            return pjp.proceed(pjp.getArgs());
        }
    }

}