基于注解@Aspect实现Spring AOP

摘要:基于注解@Aspect实现Spring AOP切面编程。

基于注解@Aspect实现Spring AOP

  Spring AOP使用的是动态代理,所谓的动态代理就是说AOP框架不会去修改字节码,而是在内存中临时为方法生成一个AOP对象,这个AOP对象在特定的切点做了增强处理,包含了目标对象的全部方法,并且回调原对象的方法。
     
  Spring AOP中的动态代理主要有两种方式,JDK动态代理和CGLIB动态代理。JDK动态代理通过反射来接收被代理的类,并且要求被代理的类实现一个接口,她的核心是InvocationHandler接口和Proxy类。如果目标类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成某个类的子类。注意,CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。

  如果想要实现拦截private方法,可以使用原生 AspectJ 编译时织入。Spring使用纯Spring AOP(只能拦截public/protected/包)都是无法被拦截private方法的,因为子类无法覆盖;包级别能被拦截的原因是,如果子类和父类在同一个包中是能覆盖的。cglib动态代理可以拦截 public/protected/包级别方法(即这些方法都是能代理的)。因为AOP底层是动态代理,jdk动态代理是代理接口,私有方法必然不会存在在接口里,所以就不会被拦截;cglib是生成被代理类的子类,private的方法照样不会出现在子类里,也不能被拦截。关于Spring AOP的更多基本概念请参考《Spring AOP 面向切面编程之AOP是什么》。

  AspectJ是一个面向切面的框架,它扩展了java语言,定义了AOP语法,能够在编译期提供代码的织入。这里创建一个基于注解@Aspect的demo,实现AOP切面。定义测试API getUserById:

    @Autowired
    private UserService userService;
    
    @GetMapping(value ="/getUserById", produces = "application/json; charset=utf-8")
    public User getUserById(Long userId) {
        User user = userService.getUserById(userId);      
        return user;
    }

  编写实现类:

package com.eg.wiener.service;

import com.eg.wiener.dto.User;

public interface UserService {
    User getUserById(Long userId);
}

 === 我是分割线 === 
 
package com.eg.wiener.service.impl;

import com.eg.wiener.dto.User;
import com.eg.wiener.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import java.util.UUID;

@Service
public class UserServiceImpl implements UserService {
    private static Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);

    @Override
    public User getUserById(Long userId) {
        logger.info("--------**测试bean增强**-----------");
        User user = new User();
        user.setId(userId);
        user.setAddress("测试地址是 " + UUID.randomUUID().toString());
        logger.info("类信息: {}", user.getClass());
        return user;
    }
}

  在切面UserServiceAspect中使用@Pointcut注解声明切点表达式,命名为userPointcut(),然后在各类通知类型中引用此切点表达式。

package com.eg.wiener.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class UserServiceAspect {
    private static Logger logger = LoggerFactory.getLogger(UserServiceAspect.class);
    private final String POINT_CUT = "execution(* com.eg.wiener.service.UserService.getUserById(..))";
    
    @Pointcut(POINT_CUT)
    public void userPointcut() {
    }

    @Before("userPointcut()")
    public void beforeTest() {
        System.out.println("before ...");
    }

    @After("userPointcut()")
    public void afterTest() {
        System.out.println("After ...");
    }

    @AfterReturning("userPointcut()")
    public void afterReturningTest() {
        System.out.println("AfterReturning ...");
    }

    @Around("userPointcut()")
    public Object aroundTest(ProceedingJoinPoint pj) {
        long start = System.currentTimeMillis();
        Object obj = new Object();
        try {
            System.out.println("Around start ...");
            obj = pj.proceed(pj.getArgs());
            System.out.println("Around end ...耗时:" + 
(System.currentTimeMillis() - start));
        } catch (Exception e) {
            logger.error("------ e -----,", e);
        } catch (Throwable throwable) {
            logger.error("----- throwable ------,", throwable);
        }
        return obj;
    }
}

  POINT_CUT具体到了函数getUserById,表示切入点就是userService中的函数getUserById,不拦截其它接口。org.aspectj.lang.ProceedingJoinPoint为JoinPoint的子类,重点看一下其中的如下两个方法:

public Object proceed() throws Throwable;
public Object proceed(Object[] args) throws Throwable;

执行proceed() 意为调用下一个advice或者执行目标方法,返回结果为目标方法返回值,因此,如有必要,可以在这里修改方法的返回值。proceed(Object[] args)中的args为目标方法的参数,可以通过修改参数改变方法入参。

  在编写环绕通知的时候,如果被增强函数有返回值,我们需要返回执行结果;否则,API无法返回我们的业务数据,导致竹篮打水一场空。启动项目,请求getUserById,控制台打印结果如下:

Around start ...
before ...
2021-02-17 10:55:21.259 INFO 9000 --- [nio-8087-exec-4] c.e.wiener.service.impl.UserServiceImpl : --------测试bean增强-----------
2021-02-17 10:55:21.259 INFO 9000 --- [nio-8087-exec-4] c.e.wiener.service.impl.UserServiceImpl : 类信息: class com.eg.wiener.dto.User
AfterReturning ...
After ...
Around end ...耗时:1

  可以看到五种通知类型和具体实现类的执行顺序为:around、before、实现类、afterReturning、after和around。

  使用环绕通知监控API执行时间。在环绕通知中记录API执行完毕所耗费的时间,可以方便的监控和统计哪些API性能低,需要重构,以保证系统稳健运行。

  既然AOP已经生效了,那么,问题来了,究竟AspectJ是如何在没有修改UserServiceImpl类的情况下为UserServiceImpl类增加新功能的呢?这是注解@AspectJ在内存中临时为方法生成一个AOP对象,这个AOP对象在特定的切点做了增强处理,包含了目标对象的全部方法,并且回调原对象的方法,即运行的就是经过增强之后的AOP对象。

小结

  AspectJ在编译时就增强了目标对象,Spring AOP的动态代理则是在每次运行时动态的增强,生成AOP代理对象,区别在于生成AOP代理对象的时机不同,相对来说AspectJ的静态代理方式具有更好的性能,但是AspectJ需要特定的编译器进行处理,而Spring AOP则无需特定的编译器处理。

Reference

posted @ 2021-02-26 21:30  楼兰胡杨  阅读(618)  评论(0编辑  收藏  举报