面向切面编程--Spring AOP

568 阅读6分钟

前言

概念

学习JAVA刚开始都会接触到OOP(封装 继承 多态),这些概念将对象公共的行为抽离出来,使代码变得简洁易维护。但如果要将不同的对象引入一个公共的行为时,OOP就比较无能为力了。比较典型的就是项目中的日志功能,这个功能散布于各个不同的对象的每个层次中(执行前,执行后,异常),如果在每个需要日志功能的地方都添加一个,将会出现大量冗余的代码,且这些代码本身和业务并无太大联系,也使得业务更难理解,甚至还要加上更多处理才能让这块代码不影响到核心业务的执行。这时就引入了AOP的概念,它将不同对象的不同层次定义为切面,AOP也是面向切面编程,将核心业务行为和公共通用行为分离开,这就是它的目的。

术语

通知/增强(Advice)

需要加入的功能,比如上面提到的日志。

连接点(JoinPoint)

项目执行中允许你通知的地方。

切入点(Pointcut)

切入点是在连接点的基础上定义的,比如一个类中有10个连接点,你只想通知其中5个,那这5个连接点你就可以定义为切入点。

切面(Aspect)

切入点和通知的结合,切入点表示“在什么地方做”,通知表示“做什么”和“什么时候做”,两个结合表明“在什么时候什么地方做什么”,这就是一个完整的切面定义。

引入(introduction)

向现有的类添加方法属性。就是将切面在目标类中使用。

织入(weaving)

将切面引入目标对象创建一个新的代理类的过程。

提示

文章示例使用JDK1.8+SpringBoot框架进行讲解。小伙伴们要有一定的SpringBoot使用经验。

准备工作

1.aop

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

2.junit

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.11</version>
    <scope>test</scope>
</dependency>

正文

测试代码

@Service
public class IAopServiceImpl implements IAopService {

    @Override
    public String test1(String param) {
        System.out.println("我是test1");
        return "test1";
    }

    @Override
    public String test2() {
        System.out.println("我是test2");
        return "test2";
    }

    @Override
    public String test3() {
        System.out.println("我是test3");
        return "test3";
    }

}
@RunWith(SpringRunner.class)
@SpringBootTest
public class AopApplicationTests {

    @Autowired
    IAopService iAopService;

    @Test
    public void test() {
        iAopService.test1("参数1");
        iAopService.test2();
        iAopService.test3();
    }

}

前置增强

在目标方法执行前织入增强处理。 下面的这段就是在测试代码执行前获得请求的参数和IP地址,并打印日志。

@Aspect
@Component
@Slf4j
public class LogAspect {
    
    /**
     * [定义切入点] service包下所有函数
     * @author pengyu
     * @param 
     * @return void
     * @date 2020/12/23 11:18
     */
    @Pointcut("execution(public * com.pengyu.aop.service..*.*(..))")
    public void testLog(){}
    
    /**
     * [前置通知,方法执行前]
     * @author pengyu
     * @param joinPoint
     * @return void
     * @date 2020/12/23 11:18
     */
    @Before("testLog()")
    public void doBefore(JoinPoint joinPoint){
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
        //获得请求参数  转成map
        Map<String, Object> map = getNameAndValue(joinPoint);
        //获得请求ip
        String ip=getIpAddr(request);
        log.info("这是前置增强,请求参数-->{},ip-->{}",map.toString(),ip);
    }

    private static Map<String, Object> getNameAndValue(JoinPoint joinPoint) {
        Map<String, Object> param = new HashMap<>();
        Object[] paramValues = joinPoint.getArgs();
        String[] paramNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
        for (int i = 0; i < paramNames.length; i++) {
            param.put(paramNames[i], paramValues[i]);
        }
        return param;
    }

    public static String getIpAddr(HttpServletRequest request) {
        String ip = request.getHeader("x-forwarded-for");
        if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
            // 多次反向代理后会有多个ip值,第一个ip才是真实ip
            if (ip.indexOf(",") != -1) {
                ip = ip.split(",")[0];
            }
        }
        if (ip != null || "127.0.0.1".equals(ip)) {
            ip = null;
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Real-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}

执行后,控制台打印:

2020-12-23 14:34:23.453  INFO 14388 --- [           main] com.pengyu.aop.core.LogAspect            : 这是前置增强,请求参数-->{param=参数1},ip-->127.0.0.1
我是test1
2020-12-23 14:34:23.460  INFO 14388 --- [           main] com.pengyu.aop.core.LogAspect            : 这是前置增强,请求参数-->{},ip-->127.0.0.1
我是test2
2020-12-23 14:34:23.460  INFO 14388 --- [           main] com.pengyu.aop.core.LogAspect            : 这是前置增强,请求参数-->{},ip-->127.0.0.1
我是test3

可以看到,在每个方法执行前,都打印了一条日志,这就是前置增强。

后置增强

在目标方法执行后织入增强处理。 这里有两个注解需要注意以下,@After和@AfterReturning,@AfterReturning如果方法异常或其他原因中断了,是通知不到的,@After是任何情况都能通知得到。我们这里用@AfterReturning。

@Aspect
@Component
@Slf4j
public class LogAspect {
    
    /**
     * [定义切入点] service包下所有函数
     * @author pengyu
     * @param 
     * @return void
     * @date 2020/12/23 11:18
     */
    @Pointcut("execution(public * com.pengyu.aop.service..*.*(..))")
    public void testLog(){}
    
    /**
     * [后置通知]
     * @author pengyu
     * @param joinPoint
     * @param result 方法返回值
     * @return void
     * @date 2020/12/23 15:00
     */
    @AfterReturning(value = "testLog()",returning = "result")
    public void doBefore(JoinPoint joinPoint,Object result){
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
        //获得请求参数  转成map
        Map<String, Object> map = getNameAndValue(joinPoint);
        //获得请求ip
        String ip=getIpAddr(request);
        log.info("这是后置增强,请求参数-->{},ip-->{},返回值-->{}",map.toString(),ip,result.toString());
    }

    private static Map<String, Object> getNameAndValue(JoinPoint joinPoint) {
        Map<String, Object> param = new HashMap<>();
        Object[] paramValues = joinPoint.getArgs();
        String[] paramNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
        for (int i = 0; i < paramNames.length; i++) {
            param.put(paramNames[i], paramValues[i]);
        }
        return param;
    }

    public static String getIpAddr(HttpServletRequest request) {
        String ip = request.getHeader("x-forwarded-for");
        if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
            // 多次反向代理后会有多个ip值,第一个ip才是真实ip
            if (ip.indexOf(",") != -1) {
                ip = ip.split(",")[0];
            }
        }
        if (ip != null || "127.0.0.1".equals(ip)) {
            ip = null;
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Real-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}

后置增强可以拿到方法返回值,所以我们在日志输出里增加了个返回值,运行效果如下:

我是test1
2020-12-23 15:04:11.870  INFO 26348 --- [           main] com.pengyu.aop.core.LogAspect            : 这是后置增强,请求参数-->{param=参数1},ip-->127.0.0.1,返回值-->test1
我是test2
2020-12-23 15:04:11.872  INFO 26348 --- [           main] com.pengyu.aop.core.LogAspect            : 这是后置增强,请求参数-->{},ip-->127.0.0.1,返回值-->test2
我是test3
2020-12-23 15:04:11.872  INFO 26348 --- [           main] com.pengyu.aop.core.LogAspect            : 这是后置增强,请求参数-->{},ip-->127.0.0.1,返回值-->test3

后置增强相对于前置增强的位置变了,前置增强是在目标方法执行前,而后置增强是在目标方法执行后,并且可以获得返回值。

环绕增强

在目标方法执行前后织入增强处理。 环绕增强从名字就可以看出,可以针对目标方法前后都进行增强处理。 举个栗子,我们要在方法执行前日志打印参数,方法执行后打印返回值,就可以这么写:

@Aspect
@Component
@Slf4j
public class LogAspect {
    
    /**
     * [定义切入点] service包下所有函数
     * @author pengyu
     * @param 
     * @return void
     * @date 2020/12/23 11:18
     */
    @Pointcut("execution(public * com.pengyu.aop.service..*.*(..))")
    public void testLog(){}
    
    /**
     * [环绕通知]
     * @author pengyu
     * @param joinPoint
     * @return void
     * @date 2020/12/23 15:10
     */
    @Around("testLog()")
    public void doBefore(ProceedingJoinPoint joinPoint) throws Throwable {
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        //获得请求参数  转成map
        Map<String, Object> map = getNameAndValue(joinPoint);
        log.info("这是环绕增强,方法执行前,参数-->{}",map.toString());
        Object result=joinPoint.proceed();
        log.info("这是环绕增强,方法执行后,返回值-->{}",result);
    }

    private static Map<String, Object> getNameAndValue(JoinPoint joinPoint) {
        Map<String, Object> param = new HashMap<>();
        Object[] paramValues = joinPoint.getArgs();
        String[] paramNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
        for (int i = 0; i < paramNames.length; i++) {
            param.put(paramNames[i], paramValues[i]);
        }
        return param;
    }

    public static String getIpAddr(HttpServletRequest request) {
        String ip = request.getHeader("x-forwarded-for");
        if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
            // 多次反向代理后会有多个ip值,第一个ip才是真实ip
            if (ip.indexOf(",") != -1) {
                ip = ip.split(",")[0];
            }
        }
        if (ip != null || "127.0.0.1".equals(ip)) {
            ip = null;
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Real-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}

结果如下:

2020-12-23 15:17:52.731  INFO 22380 --- [           main] com.pengyu.aop.core.LogAspect            : 这是环绕增强,方法执行前,参数-->{param=参数1}
我是test1
2020-12-23 15:17:52.739  INFO 22380 --- [           main] com.pengyu.aop.core.LogAspect            : 这是环绕增强,方法执行后,返回值-->test1
2020-12-23 15:17:52.739  INFO 22380 --- [           main] com.pengyu.aop.core.LogAspect            : 这是环绕增强,方法执行前,参数-->{}
我是test2
2020-12-23 15:17:52.739  INFO 22380 --- [           main] com.pengyu.aop.core.LogAspect            : 这是环绕增强,方法执行后,返回值-->test2
2020-12-23 15:17:52.740  INFO 22380 --- [           main] com.pengyu.aop.core.LogAspect            : 这是环绕增强,方法执行前,参数-->{}
我是test3
2020-12-23 15:17:52.740  INFO 22380 --- [           main] com.pengyu.aop.core.LogAspect            : 这是环绕增强,方法执行后,返回值-->test3

不知道大家有没有注意到,环绕增强的入参由JoinPoint变成了ProceedingJoinPoint,点进去可以看到ProceedingJoinPoint继承了JoinPoint并且在原本的接口上暴露了proceed()方法。这个方法就是让目标方法执行,这样才能支持环绕通知。

结尾

上面主要讲到了前置增强,后置增强和环绕增强这三种常用的,还有其他的比如异常增强等其他增强下篇文章再讨论。如果发现错误或者我理解错了的地方,欢迎大家及时指正。