目录
    
   
    一、需求说明
   
为了简化代码,提示可读性需要针对优先读缓存,如果缓存没有则插入数据库然后再将数据写入缓存并返回。
根据这个需求我们可以基于Spring AOP 去实现。但是有一个问题,缓存的key是一个动态值,对于不同入参它的key是不同的。我们可以使用SpringEL 表达式去解决这个问题。
预期的效果是:
@Cache(prefix = "test", value = "#bizId", expire = CacheTime.CACHE_EXP_DAY) public Object queryFromCache(Long bizId)
    二、Spring AOP
   
首先我们先了解一下Spring AOP的基础知识
    2.1 AOP切点指示器
   
| AspectJ 指示器 | 描述 | 
| args() | 限制连接点匹配参数为执行类型的执行方法 | 
| @args() | 限制连接点匹配参数由执行注解标注的执行方法 | 
| execution() | 匹配连接点的执行方法 | 
| this() | 限制连接点匹配AOP代理的Bean引用类型为指定类型的Bean | 
| target() | 限制连接点匹配目标对象为指定类型的类 | 
| @target() | 限制连接点匹配目标对象被指定的注解标注的类 | 
| within() | 限制连接点匹配匹配指定的类型 | 
| @within() | 限制连接点匹配指定注解标注的类型 | 
| @annotation | 限制匹配带有指定注解的连接点 | 
Spring AOP 中常用的是:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)
          throws-pattern?)
    2.2 AOP表达式
   
| 表达式 | 说明 | 
| execution(“* *.*(..)”) | 匹配所有 | 
| execution(“* *.set*(..)) | 匹配所有以set开头的方法 | 
| execution(“* com.david.biz.service.impl.*(..)) | 匹配指定包下所有的方法 | 
| execution(“* com.david..*(..)”) | 匹配指定包以及其子包下的所有方法 | 
| execution(“* com.david..*(java.lang.String)) | 匹配指定包以及其子包下 参数类型为String 的方法 | 
    2.3、Spring AOP通知类型
   
| 通知类型 | 说明 | 
| 前置通知(Before) | 在目标方法被调用之前调用通知功能。 | 
| 后置通知(After) | 在目标方法完成之后调用通知,此时不会关心方法的输出是什么 | 
| 返回通知(After-returning) | 在目标方法成功执行之后调用通知。 | 
| 异常通知(After-throwing) | 目标方法抛出异常后调用通知。 | 
| 环绕通知(Around) | 通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。 | 
    2.4、示例代码
   
/**
 * execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)
          throws-pattern?)
 *arg()  限制连接点匹配参数为指定类型的执行方法
 *@args() 限制连接点匹配参数由执行注解标注的执行
 *execution() 用于匹配连接点的执行方法
 *this() 限制连接点匹配AOP代理的Bean引用为执行类型的类
 *target() 限制连接点匹配目标对象为指定类型的类
 *@target() 限制连接点匹配特定的执行对象,这些对象应具备指定的注解类型
 *@annotation()限制匹配带有指定注解的连接点
 *
 *
 *
 * @author zhangwei_david
 * @version $Id: LogAspect.java, v 0.1 2014年11月29日 下午1:10:13 zhangwei_david Exp $
 */
@Component
@Aspect
public class LogAspect {
    private static final Logger logger = LogManager.getLogger(LogAspect.class);
    /**
     * 匹配参数是任何类型,任何数量 且在com,david.biz包或子包下的方法
     */
    @Pointcut("args(..)&&within(com.david.biz..*)")
    public void arg() {
    }
    @Pointcut("@args(com.david.aop.LoggingRequired)")
    public void annotationArgs() {
    }
    @Pointcut("@annotation(com.david.aop.LoggingRequired)")
    public void logRequiredPointcut() {
    }
    @Pointcut("args(java.lang.String,*)")
    public void argsWithString() {
    }
    @Pointcut("target(com.david.biz.service.impl.BookServiceImpl)")
    public void targetPointcut() {
    }
    @Pointcut("@target(org.springframework.stereotype.Service)")
    public void targetAnnotation() {
    }
    //    @Around("execution(* org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter.handle(..))")
    //    public Object aa(ProceedingJoinPoint pjp) throws Throwable {
    //        try {
    //            Object retVal = pjp.proceed();
    //            System.out.println(retVal);
    //            return retVal;
    //        } catch (Exception e) {
    //            System.out.println("异常");
    //            return null;
    //        }
    //    }
    @Before(value = "logRequiredPointcut()")
    public void before(JoinPoint joinPoint) {
        LogUtils.info(logger,
            " 连接点表达式@annotation(com.david.aop.LoggingRequired) - method={0} has been visited",
            joinPoint.getSignature().getName());
    }
    @Before(value = "arg()")
    public void beforeArg(JoinPoint joinPoint) {
        LogUtils.info(logger,
            "连接点表达式:args(..)&&within(com.david.biz..*)  method ={0}, args ={1},target={2}",
            joinPoint.getSignature().getName(), ToStringBuilder.reflectionToString(
                joinPoint.getArgs(), ToStringStyle.SHORT_PREFIX_STYLE), joinPoint.getTarget()
                .getClass().getName());
    }
    @Before(value = "argsWithString()")
    public void beforeArgWithString(JoinPoint joinPoint) {
        LogUtils.info(logger, "连接点表达式:args(java.lang.String,*)  method={0} ,args ={1},target={2}",
            joinPoint.getSignature().getName(), ToStringBuilder.reflectionToString(
                joinPoint.getArgs(), ToStringStyle.SHORT_PREFIX_STYLE), joinPoint.getTarget()
                .getClass().getName());
    }
    @Before(value = "annotationArgs()")
    public void beforeAnnotationArgs(JoinPoint joinPoint) {
        LogUtils
            .info(
                logger,
                "连接点表达式:@args(com.david.annotation.validate.Length,*)  method={0} ,args ={1},target={2}",
                joinPoint.getSignature().getName(), ToStringBuilder.reflectionToString(
                    joinPoint.getArgs(), ToStringStyle.SHORT_PREFIX_STYLE), joinPoint.getTarget()
                    .getClass().getName());
    }
    @Before(value = "targetPointcut()")
    public void beforeTarget(JoinPoint joinPoint) {
        LogUtils
        .info(
            logger,
            "连接点表达式:target(com.david.biz.service.impl.BookServiceImpl)  method={0} ,args ={1},target={2}",
            joinPoint.getSignature().getName(), ToStringBuilder.reflectionToString(
                joinPoint.getArgs(), ToStringStyle.SHORT_PREFIX_STYLE), joinPoint.getTarget()
                .getClass().getName());
    }
    @Before(value = " targetAnnotation()")
    public void beforeTargetAnnotation(JoinPoint joinPoint) {
        LogUtils
            .info(
                logger,
                "连接点表达式:@target(org.springframework.stereotype.Service)  method={0} ,args ={1},target={2}",
                joinPoint.getSignature().getName(), ToStringBuilder.reflectionToString(
                    joinPoint.getArgs(), ToStringStyle.SHORT_PREFIX_STYLE), joinPoint.getTarget()
                    .getClass().getName());
    }
}
    三、需求实现
   
根据需求我们需要使用注解指示器,也就是基于注解实现。
    3.1、定义注解
   
作为一个缓存注解首先就是缓存key的构成,其次就是缓存的有效期。下面我们就看看源码
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Cache {
    /**
     * 前缀,默认为空
     *
     * @return
     */
    String prefix() default "";
    /**
     * key的构成数据
     *
     * @return
     */
    String[] value() default {};
    /**
     * 分割符
     *
     * @return
     */
    String separator() default "_";
    /**
     * 过期时间,单位为秒,默认为1天
     * @return
     */
    int expire() default CacheTime.CACHE_EXP_DAY;
}
    3.2、定义切面
   
    3.2.1获取方法参数名称
   
可以通过ASM提供的通过字节码获取方法的参数名称,spring已经集成了相关功能,我们可以方便的使用它。
第一步、声明一个成员并初始化变量LocalVariableTableParameterNameDiscoverer
private LocalVariableTableParameterNameDiscoverer nameDiscoverer = new LocalVariableTableParameterNameDiscoverer();第二步、通过反射方式获取方法参数类型
Class[] argTypes = ((MethodSignature) pjp.getSignature()).getMethod().getParameterTypes();第三步、根据方法名称和参数类型获取方法
 Method method = pjp.getTarget().getClass().getMethod(pjp.getSignature().getName(), argTypes);第四步、获取方法的参数名称
String[] paraNameArr = nameDiscoverer.getParameterNames(method);
    3.2.2  构建缓存Key
   
第一步、声明并初始化成员变量ExpressionParser
    private ExpressionParser parser = new SpelExpressionParser();
第二步、构建SpringEL上下文
从需求可以知道上下文中就是参数名称和参数值的映射关系
String[] paraNameArr = nameDiscoverer.getParameterNames(method);
        Object[] args = pjp.getArgs();
        //SPEL上下文
        StandardEvaluationContext context = new StandardEvaluationContext();            //把方法参数放入SPEL上下文中
        for (int i = 0; i < paraNameArr.length; i++) {
            context.setVariable(paraNameArr[i], args[i]);
        }第三步、解析构建缓存key
    StringBuilder sb = new StringBuilder();
        for (String expressionString : soloCache.value()) {
            sb.append(cache.separator());
            Object temp = parser.parseExpression(expressionString).getValue(context);
            String str = temp == null ? "null" : temp.toString();
            sb.append(str);
        }
        return sb.toString(); 
    3.2.3环绕切面处理
   
环绕切面处理核心流程就是首先从缓存中获取数据,如果有就直接返回,如果没有就执行原始方法并有效数据写入到缓存中。
        Object object = cacheClient.get(cache.prefix(), key);
        
        if (object != null) {
            return object;
        } else {
            Object value = pjp.proceed();
            if (value != null) {
               
             cacheClient.set(soloCache.prefix(), key, value, soloCache.expire());
                
            }
            return value;
        }
    四、源码
   
@Aspect
@Component
public class SoloCacheAdvice {
    private ExpressionParser parser = new SpelExpressionParser();
    @Resource
    private CacheClient cacheClient;
    private LocalVariableTableParameterNameDiscoverer nameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
    @Around(value = "@annotation(cache)")
    public Object invoke(ProceedingJoinPoint pjp, Cache cache) throws Throwable {
        Method method = getMethod(pjp);
        //获取被拦截方法参数名列表(使用Spring支持类库)
        String[] paraNameArr = nameDiscoverer.getParameterNames(method);
        Object[] args = pjp.getArgs();
        //SPEL上下文
        StandardEvaluationContext context = new StandardEvaluationContext();            //把方法参数放入SPEL上下文中
        for (int i = 0; i < paraNameArr.length; i++) {
            context.setVariable(paraNameArr[i], args[i]);
        }
        String key = createKey(cache, context);
        Object object = cacheClient.get(soloCache.prefix(), key);
        if (object != null) {
            return object;
        } else {
            Object value = pjp.proceed();
            if (value != null) {
                try {
                    cacheClient.set(cache.prefix(), key, value, cache.expire());
                } catch (Exception e) {
                    //error Log
                }
            }
            return value;
        }
    }
    
    private String createKey(Cache cache, StandardEvaluationContext context) {
        StringBuilder sb = new StringBuilder();
        for (String expressionString : cache.value()) {
            sb.append(cache.separator());
            Object temp = parser.parseExpression(expressionString).getValue(context);
            String str = temp == null ? "null" : temp.toString();
            sb.append(str);
        }
        return sb.toString();
    }
   
    public Method getMethod(ProceedingJoinPoint pjp) {
        //获取参数的类型
        Class[] argTypes = ((MethodSignature) pjp.getSignature()).getMethod().getParameterTypes();
        Method method = null;
        try {
            method = pjp.getTarget().getClass().getMethod(pjp.getSignature().getName(), argTypes);
        } catch (NoSuchMethodException | SecurityException e) {
           //error Log
        }
        return method;
    }
}
    五、其他问题
   
    5.1为什么无法获取到参数名称
   
首先检查一下maven配置是否配置了<parameters>true</parameters>
        <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                    <encoding>${project.build.sourceEncoding}</encoding>
                    <showWarnings>false</showWarnings>
                    <showDeprecation>false</showDeprecation>
                    <parameters>true</parameters>
                </configuration>
            </plugin> 
