美文网首页
Spring AOP简单样例

Spring AOP简单样例

作者: 一块自由的砖 | 来源:发表于2019-07-31 19:31 被阅读0次

    简介

    AOP (Aspect Oriented Programming) 即 面向切面编程,是一种编程典范,它通过分离横切关注点来增加程序的模块化。通俗说就是 AOP 可以在不修改现有代码的情况下对现有代码增加一些功能,那么这就是 AOP 最强大的功能。

    目前最受欢迎的 AOP 库有两个,一个是 AspectJ, 另外一个是 Spring AOP。

    核心概念

    • Aspect:即切面,切面一般定义为一个 Java 类, 每个切面连接点所采用的处理逻辑,也就是向连接点注入的代码, AOP在特定的切入点上执行的增强处理。
      1. @Before: 标识一个前置增强方法,相当于BeforeAdvice的功能.
      2. @After: final增强,不管是抛出异常或者正常退出都会执行.
      3. @AfterReturning: 后置增强,似于AfterReturningAdvice, 方法正常退出时执行.
      4. @AfterThrowing: 异常抛出增强,相当于ThrowsAdvice.
      5. @Around: 环绕增强,相当于MethodInterceptor.
    • Joinpoint:即连接点,程序执行的某个点,比如方法执行。构造函数调用或者字段赋值等。
    • Pointcut:即切点,一个匹配连接点的正则表达式。指明Advice要在什么样的条件下才能被触发,当一个连接点匹配到切点时,一个关联到这个切点的特定的 (Advice) 会被执行。
    • Advisor(增强): 是PointCut和Advice的综合体,完整描述了一个advice将会在Pointcut所定义的位置被触发。
    • AOP Proxy:AOP框架创建的对象,代理就是目标对象的加强。Spring中的AOP代理可以使JDK动态代理,也可以是CGLIB代理,前者基于接口,后者基于子类。

    使用实践

    按照整体规划,这里需要给项目增加一个基于自定义注解的日志切面程序。实现目标:任何请求都要记录到日志,目前不引入数据库,先组织成对应的格式,在控制台输出。
    目录组织形式:


    image.png

    1 引入Spring boot AOP的包

    maven文件增加依赖

            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-aop</artifactId>
            </dependency>
    

    2 定义切入点要干啥

    定义记录那些信息Log类(Log.java)

    package com.springboot.action.saas.common.logging.domain;
    
    import lombok.Data;
    
    import java.sql.Timestamp;
    
    @Data
    public class Log {
        //描述
        private String description;
        //方法名
        private String method;
        //参数
        private String params;
        //日志类型
        private String logType;
        //请求ip
        private String requestIp;
        //请求耗时
        private Long time;
        //异常详细
        private String exceptionDetail;
        //请求实践
        private Timestamp createTime;
        //构造函数(代参)
        public Log(String logType, Long time) {
            this.logType = logType;
            this.time = time;
        }
    }
    

    日志记录业务接口(LogService.java)

    package com.springboot.action.saas.common.logging.service;
    
    import com.springboot.action.saas.common.logging.domain.Log;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.springframework.scheduling.annotation.Async;
    
    /*
    * 日志记录业务
    *
    */
    public interface LogService {
        /*
        * 记录日志,异步执行,不影响正常业务流程
        * 参数 joinPoint 切面方法的信息,当前切入点各种信息
        *     log 要记录的日志信息有那些
        * */
        @Async
        void save(ProceedingJoinPoint joinPoint, Log log);
    }
    

    日志记录业务接口实现(LogServiceImpl.java)

    package com.springboot.action.saas.common.logging.service.impl;
    
    import com.springboot.action.saas.common.logging.domain.Log;
    import com.springboot.action.saas.common.logging.service.LogService;
    import com.springboot.action.saas.common.utils.StringUtils;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.springframework.stereotype.Service;
    import org.springframework.web.context.request.RequestContextHolder;
    import org.springframework.web.context.request.ServletRequestAttributes;
    
    import javax.servlet.http.HttpServletRequest;
    import java.lang.reflect.Method;
    
    @Service
    public class LogServiceImpl implements LogService {
        /*
        * 记录日志接口实现
        **/
        @Override
        public void save(ProceedingJoinPoint joinPoint, Log log) {
            //获取request 请求对象
            HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes())
                    .getRequest();
            //getSignature获取切面相关信息,比如方法名、目标方法参数等信息
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            //获取抽象类(代理对象)方法
            Method method = signature.getMethod();
            //返回该元素的指定类型的注释,这里是Log注解
            com.springboot.action.saas.common.logging.annotation.Log aopLog = method.getAnnotation(com.springboot.action.saas.common.logging.annotation.Log.class);
            //获取注解传递的参数
            if (log != null) {
                log.setDescription(aopLog.value());
            }
            //通过最笨的反射方法,获取方法路径
            String methodName = joinPoint.getTarget().getClass().getName()+"."+signature.getName()+"()";
            log.setMethod(methodName);
            //参数处理
            //获取参数值
            Object[] argValues = joinPoint.getArgs();
            //获取参数名
            String[] argNames = ((MethodSignature)joinPoint.getSignature()).getParameterNames();
            //组织参数列表
            String params = "{";
            if(argValues != null){
                for (int i = 0; i < argValues.length; i++) {
                    params += " " + argNames[i] + ": " + argValues[i];
                }
            }
            log.setParams(params + " }");
            //获取IP地址
            log.setRequestIp(StringUtils.getIP(request));
            //输出下日志到控制台
            System.out.println(log.toString());
        }
    }
    

    3 定义切入点以及切入点具体动作

    切点的表达式以指示器指定的类型,配合类似正则表达式的用来告诉 Spring AOP 如何匹配连接点,Spring AOP 提供了以下几种指示器:

    • execution
    • within
    • this和target
    • args
    • @within
    • @target
    • @args
    • @annotation

    下面依次说明指示器的作用

    execution

    该指示器用来匹配方法执行连接点,即匹配哪个方法执行,如

    @Pointcut("execution(public String aaric.springaopdemo.UserDao.findById(Long))")
    

    上面这个切点会匹配在 UserDao 类中 findById 方法的调用,并且需要该方法是 public 的,返回值类型为 String,只有一个 Long 的参数。
    切点的表达式同时还支持宽字符匹配,如

    @Pointcut("execution(* aaric.springaopdemo.UserDao.*(..))")
    

    上面的表达式中,第一个宽字符 * 匹配 任何返回类型,第二个宽字符 * 匹配 任何方法名,最后的参数 (..) 表达式匹配 任意数量任意类型 的参数,也就是说该切点会匹配类中所有方法的调用。

    within

    如果要匹配一个类中所有方法的调用,便可以使用 within 指示器

    @Pointcut("within(aaric.springaopdemo.UserDao)")
    

    这样便可以匹配该类中所有方法的调用了。同时,我们还可以匹配某个包下面的所有类的所有方法调用,如下面的例子
    @Pointcut("within(aaric.springaopdemo..*)")

    this 和 target

    如果目标对象实现了任何接口,Spring AOP 会创建基于CGLIB 的动态代理,这时候需要使用 target 指示器
    如果目标对象没有实现任何接口,Spring AOP 会创建基于JDK的动态代理,这时候需要使用 this 指示器

    @Pointcut("target(aaric.springaopdemo.A)") A 实现了某个接口
    @Pointcut("this(aaric.springaopdemo.B)") B 没有实现任何一个接口
    

    args

    该指示器用来匹配具体的方法参数

    @Pointcut("execution(* *..find*(Long))")
    

    这个切点会匹配任何以 find 开头并且只有一个 Long 类型的参数的方法。
    如果我们想匹配一个以 Long 类型开始的参数,后面的参数类型不做限制,我们可以使用如下的表达式

    @Pointcut("execution(* *..find*(Long,..))")
    

    @target
    该指示器不要和 target 指示器混淆,该指示器用于匹配连接点所在的类是否拥有指定类型的注解,如

    @Pointcut("@target(org.springframework.stereotype.Repository)")
    

    @annotation

    该指示器用于匹配连接点的方法是否有某个注解

    @Pointcut("@annotation(org.springframework.scheduling.annotation.Async)")
    

    配置切点和切点对应的动作(好多文档都说是通知)
    LogAspect.java

    package com.springboot.action.saas.common.logging.aspect;
    
    import com.springboot.action.saas.common.logging.domain.Log;
    import com.springboot.action.saas.common.logging.service.LogService;
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.AfterThrowing;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    
    @Component
    @Aspect
    public class LogAspect {
        //日志业务
        @Autowired
        private LogService logService;
        //当前时间
        private long currentTime = 0L;
    
        /**
         * 配置切入点, 匹配连接点的方法是否有Log注解,定义切点
         */
        @Pointcut("@annotation(com.springboot.action.saas.common.logging.annotation.Log)")
        public void logPointcut() {
            // 该方法无方法体,主要为了让同类中其他方法使用此切入点
        }
    
        /**
         * 配置环绕通知,使用在方法logPointcut()上注册的切入点,具体要通知在什么条件下执行和执行什么动作
         *
         * @param joinPoint join point for advice
         */
        @Around("logPointcut()")
        public Object logAround(ProceedingJoinPoint joinPoint){
            //返回值
            Object result = null;
            //获取当前时间
            currentTime = System.currentTimeMillis();
            try {
                //执行目标方法
                result = joinPoint.proceed();
            } catch (Throwable e) {
                //抛异常
                throw new RuntimeException(e.getMessage());
            }
            //创建日志对象
            Log log = new Log("INFO",System.currentTimeMillis() - currentTime);
            //记录日志
            logService.save(joinPoint, log);
            //返回目标方法的返回值
            return result;
        }
    
        /**
         * 配置异常通知,异常的日志也要记录
         *
         * @param joinPoint join point for advice
         * @param e exception
         */
        @AfterThrowing(pointcut = "logPointcut()", throwing = "e")
        public void logAfterThrowing(JoinPoint joinPoint, Throwable e) {
            //创建日志对象
            Log log = new Log("ERROR",System.currentTimeMillis() - currentTime);
            //异常设置
            log.setExceptionDetail(e.getMessage());
            //记录异常
            logService.save((ProceedingJoinPoint)joinPoint, log);
        }
    }
    

    相关文章

      网友评论

          本文标题:Spring AOP简单样例

          本文链接:https://www.haomeiwen.com/subject/mphnrctx.html