Spring AOP项目实战

  • Post author:
  • Post category:其他



目录


一、需求说明


二、Spring AOP


2.1 AOP切点指示器


2.2 AOP表达式


2.3、Spring AOP通知类型


2.4、示例代码


三、需求实现


3.1、定义注解


3.2、定义切面


3.2.1获取方法参数名称


3.2.2  构建缓存Key


3.2.3环绕切面处理


四、源码


五、其他问题


5.1为什么无法获取到参数名称


一、需求说明

为了简化代码,提示可读性需要针对优先读缓存,如果缓存没有则插入数据库然后再将数据写入缓存并返回。

根据这个需求我们可以基于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>



版权声明:本文为zhangwei_david原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。