• spring aop聊点不一样的东西


    前言

    前几篇文章本打算写spring aop的,但是强忍着没有写(旁白:也有可能是没想好该怎么写😝),就是为了今天整个专题,因为它是spring中最核心的技术之一,实在太重要了。

    关于spring aop的文章网上一搜一大堆,但我想写点不一样的东西,尝试一种全新的写作风格,希望您会喜欢。

    96c444fffc7f44f1980f2ec7b438b4df.png 

    从实战出发

    很多文章讲spring aop的时候,一开始就整一堆概念,等我们看得差不多要晕的时候,才真正进入主题。。。

    我却相反,没错,先从实战出发。

    在spring aop还没出现之前,想要在目标方法之前先后加上日志打印的功能,我们一般是这样做的:

    1. @Service
    2. public class TestService {
    3. public void doSomething1() {
    4. beforeLog();
    5. System.out.println("==doSomething1==");
    6. afterLog();
    7. }
    8. public void doSomething2() {
    9. beforeLog();
    10. System.out.println("==doSomething1==");
    11. afterLog();
    12. }
    13. public void doSomething3() {
    14. beforeLog();
    15. System.out.println("==doSomething1==");
    16. afterLog();
    17. }
    18. public void beforeLog() {
    19. System.out.println("打印请求日志");
    20. }
    21. public void afterLog() {
    22. System.out.println("打印响应日志");
    23. }
    24. }

    如果加了新doSomethingXXX方法,就需要在新方法前后手动加beforeLog和afterLog方法。

    原本相安无事的,但长此以往,总有会出现几个刺头青。

    A说:每加一个新方法,都需要加两行重复的代码,是不是很麻烦?

    B说:业务代码和公共代码是不是耦合在一起了?

    A说:如果有几千个类中加了公共代码,而有一天我需要删除,是不是要疯了?

    spring大师们说:我们提供一套spring的aop机制,你们可以闭嘴了。

    下面看看用spring aop(偷偷说一句,还用了aspectj)是如何打印日志的:

    1. @Service
    2. public class TestService {
    3. public void doSomething1() {
    4. System.out.println("==doSomething1==");
    5. }
    6. public void doSomething2() {
    7. System.out.println("==doSomething1==");
    8. }
    9. public void doSomething3() {
    10. System.out.println("==doSomething1==");
    11. }
    12. }
    1. @Component
    2. @Aspect
    3. public class LogAspect {
    4. @Pointcut("execution(public * com.sue.cache.service.*.*(..))")
    5. public void pointcut() {
    6. }
    7. @Before("pointcut()")
    8. public void beforeLog() {
    9. System.out.println("打印请求日志");
    10. }
    11. @After("pointcut()")
    12. public void afterLog() {
    13. System.out.println("打印响应日志");
    14. }
    15. }

    增加了LogAspect类,在类上加了@Aspect注解。先在类中使用@Pointcut注解定义了pointcut方法,然后将beforeLog和afterLog方法移到这个类中,分别加上@Before@After注解。

    改造后,业务方法在TestService类中,而公共方法在LogAspect类中,是分离的。如果要新加一个业务方法,直接加就好,LogAspect类不用改任何代码,新加的业务方法就自动拥有打印日志的功能,是不是很神奇?

    a90ff5ebf6f34f11abcc91394073c09e.png 

    spring aop其实是一种横切的思想,通过动态代理技术将公共代码织入到业务方法中。

    这里出于5毛钱的友情,有必要温馨提醒一下。aop是一种思想,不是spring独有的,目前市面上比较出名的有:

    • aspectj

    • spring aop

    • jboss aop

    我们现在主流的做法是将spring aop和aspectj结合使用,spring借鉴了AspectJ的切面,以提供注解驱动的AOP。

    此时,一个黑影一闪而过。

    旁边兄弟问:你说的“横切”,“动态代理”,“织入” 是什么东东?

    几个重要的概念

    根据上面spring aop的代码,用一张图聊聊几个重要的概念:

    7bfd0d20c21c499ab3dd60700ee69d67.png 

    • 连接点(Joinpoint) 程序执行的某个特定位置,如某个方法调用前,调用后,方法抛出异常后,这些代码中的特定点称为连接点。

    • 切点(Pointcut) 每个程序的连接点有多个,如何定位到某个感兴趣的连接点,就需要通过切点来定位。

    • 通知(Advice) 增强是织入到目标类连接点上的一段程序代码。

    • 切面(Aspect) 切面由切点和通知组成,它既包括了横切逻辑的定义,也包括了连接点的定义,SpringAOP就是将切面所定义的横切逻辑织入到切面所制定的连接点中。

    • 目标对象(Target) 需要被增强的业务对象

    • 代理类(Proxy) 一个类被AOP织入增强后,就产生了一个代理类。

    • 织入(Weaving) 织入就是将增强添加到对目标类具体连接点上的过程。

    还是那个旁边兄弟说(旁边:这位仁兄比较好学):spring aop概念弄明白了,挺简单的。@Pointcut注解的execution表达式刚刚看得我一脸懵逼,可以再说说吗,我请你吃饭?

    切入点表达式

    @Pointcut注解的execution切入点表达,看似简单,里面还是有些内容的。为了更直观一些,还是用张图来总结一下:

    0a837d447439419a83c8849fabb899d4.png
    该表达式的含义是:匹配访问权限是public,任意返回值,包名为:com.sue.cache.service,下面的所有类所有方法和所有参数类型。图中所有用*表示,比如图中类名用.*表示的是所有类。如果具体匹配某个类,比如:TestService,则表达式可以换成: 

    @Pointcut("execution(public * com.sue.cache.service.TestService.*(..))")
    

    其实spring支持9种表达式,execution只是其中一种。

    1a9ba78cf9c2469c82ed3439aaf2b291.png 

    有哪些入口?

    先说说我为什么会问这样一个问题?

    spring aop有哪些入口?说人话就是在问:spring中有哪些场景需要调用aop生成代理对象,难道你不好奇吗?

    入口1

    AbstractAutowireCapableBeanFactory类的createBean方法中,有这样一段代码:

    ba56090134244aea8e453873fbbbf04a.png 

    它通过BeanPostProcessor提供了一个生成代理对象的机会。具体逻辑在AbstractAutoProxyCreator类的postProcessBeforeInstantiation方法中:

    8dacfd44c25240d6a54227bd8ff1d0a6.png 

    说白了,需要实现TargetSource才有可能会生成代理对象。该接口是对Target目标对象的封装,通过该接口可以获取到目标对象的实例。

    不出意外,这时,又会冒出一个黑影。

    旁边兄弟说:这里生成代理对象有什么用呢?

    有时我们想自己控制bean的创建和初始化,而不需要通过spring容器,这时就可以通过实现TargetSource满足要求。只是创建单纯的实例还好,如果我们想使用代理该怎么办呢?这时候,入口1的作用就体现出来了。

    入口2

    AbstractAutowireCapableBeanFactory类的doCreateBean方法中,有这样一段代码:

    e014583709144674bc3cae652270d2fe.png 

    它主要作用是为了解决对象的循环依赖问题,核心思路是提前暴露singletonFactory到缓存中。

    通过getEarlyBeanReference方法生成代理对象:

    f79ea16f85b54e19b2e3e6e60c5ef370.png 

    它又会调用wrapIfNecessary方法:

    b6ea6d00776d4230a339a0ed547e7fd6.png 

    这里有你想看到的生成代理的逻辑。

    这时。。。。,你猜错了。

    入口3

    AbstractAutowireCapableBeanFactory类的initializeBean方法中,有这样一段代码:

    77c8a13e610246edb7ced8b6c72b2b3b.png 

    它会调用到AbstractAutoProxyCreator类postProcessAfterInitialization方法:

    8d5b4492392a4a43960cf2a85e5536ce.png 

    该方法中能看到我们熟悉的面孔:wrapIfNecessary方法。从上面得知该方法里面包含了真正生成代理对象的逻辑。

    这个入口,是为了给普通bean能够生成代理用的,是spring最常见并且使用最多的入口。

    下面为了加深印象,用一张图总结一下:

    3cfdd46f08f34652983d3f51a1bbe6c4.png 

    jdk动态代理 vs cglib

    我猜你们对jdk动态代理和cglib是知道的(即使猜错了也不会少块肉😃),但为了照顾一下新朋友,还是有必要把这两种生成代理的方式拿出来说说。

    jdk动态代理

    jdk动态代理是通过反射技术实现的,生成代理的代码如下:

    1. public interface IUser {
    2. void add();
    3. }
    4. public class User implements IUser{
    5. @Override
    6. public void add() {
    7. System.out.println("===add===");
    8. }
    9. }
    10. public class JdkProxy implements InvocationHandler {
    11. private Object target;
    12. public Object getProxy(Object target) {
    13. this.target = target;
    14. return Proxy.newProxyInstance(this.getClass().getClassLoader(),target.getClass().getInterfaces(),this);
    15. }
    16. @Override
    17. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    18. before();
    19. Object result = method.invoke(target,args);
    20. after();
    21. return result;
    22. }
    23. private void before() {
    24. System.out.println("===before===");
    25. }
    26. private void after() {
    27. System.out.println("===after===");
    28. }
    29. }
    30. public class Test {
    31. public static void main(String[] args) {
    32. User user = new User();
    33. JdkProxy jdkProxy = new JdkProxy();
    34. IUser proxy = (IUser)jdkProxy.getProxy(user);
    35. proxy.add();
    36. }
    37. }

    首先要定义一个接口IUser,然后定义接口实现类User,再定义类JdkProxy实现InvocationHandler接口,重写invoke方法,该方法中实现额外的逻辑。当然,别忘了在getProxy方法中,用Proxy.newProxyInstance方法创建一个代理对象。

    jdk动态代理三个要素:

    • 定义一个接口

    • 实现InvocationHandler接口

    • 使用Proxy创建代理对象

    cglib

    cglib底层是通过asm字节码技术实现的,生成代理的代码如下:

    1. public class User {
    2. public void add() {
    3. System.out.println("===add===");
    4. }
    5. }
    6. public class CglibProxy implements MethodInterceptor {
    7. private Object target;
    8. public Object getProxy(Object target) {
    9. this.target = target;
    10. Enhancer enhancer = new Enhancer();
    11. enhancer.setSuperclass(target.getClass());
    12. enhancer.setCallback(this);
    13. return enhancer.create();
    14. }
    15. @Override
    16. public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
    17. before();
    18. Object result = method.invoke(target,objects);
    19. after();
    20. return result;
    21. }
    22. private void before() {
    23. System.out.println("===before===");
    24. }
    25. private void after() {
    26. System.out.println("===after===");
    27. }
    28. }
    29. public class Test {
    30. public static void main(String[] args) {
    31. User user = new User();
    32. CglibProxy cglibProxy = new CglibProxy();
    33. IUser proxy = (IUser)cglibProxy.getProxy(user);
    34. proxy.add();
    35. }
    36. }

    这里不需要定义接口,直接定义目标类User,然后实现MethodInterceptor接口,重写intercept方法,该方法中实现额外的逻辑。当然,别忘了在getProxy方法中,通过Enhancer创建代理对象。

    cglib两个要素:

    • 实现MethodInterceptor接口

    • 使用Enhancer创建代理对象

    spring中如何用的?

    DefaultAopProxyFactory类的createAopProxy方法中,有这样一段代码:

    4e88abed633a4d2592460a5770c3572a.png 

    它里面包含:

    • JdkDynamicAopProxy jdk动态代理生成类

    • ObjenesisCglibAopProxy cglib代理生成类

    JdkDynamicAopProxy类的invoke方法生成的代理对象。而ObjenesisCglibAopProxy类的父类:CglibAopProxy,它的getProxy方法生成的代理对象。

    哪个更好?

    我猜,不是旁边的兄弟,是你,可能会来自灵魂深处的一问:jdk动态代理和cglib哪个更好?

    其实这个问题没有标准答案,要看具体的业务场景:

    1. 没有定义接口,只能使用cglib,不说它好不行。

    2. 定义了接口,需要创建单例或少量对象,调用多次时,可以使用jdk动态代理,因为它创建时更耗时,但调用时速度更快。

    3. 定义了接口,需要创建多个对象时,可以使用cglib,因为它创建速度更快。

    随着jdk版本不断迭代更新,jdk动态代理创建耗时不断被优化,8以上的版本中,跟cglib已经差不多。所以spring官方默认推荐使用jdk动态代理,因为它调用速度更快。

    免费赠送一条有用经验:如果要强制使用cglib,可以通过以下两种方式:

    • spring.aop.proxy-target-class=true

    • @EnableAspectJAutoProxy(proxyTargetClass = true)

    五种通知

    spring默认提供了五种通知:

    f940fac17b324ca6aedf17b7519dd1b3.png 

    按照国际惯例,不,按照我个人习惯,先看看他们是怎么用的。

    前置通知

    该通知在方法执行之前执行,只需在公共方法上加@Before注解,就能定义前置通知:

    1. @Before("pointcut()")
    2. public void beforeLog(JoinPoint joinPoint) {
    3. System.out.println("打印请求日志");
    4. }

    后置通知

    该通知在方法执行之后执行,只需在公共方法上加@After注解,就能定义后置通知:

    1. @After("pointcut()")
    2. public void afterLog(JoinPoint joinPoint) {
    3. System.out.println("打印响应日志");
    4. }

    环绕通知

    该通知在方法执行前后执行,只需在公共方法上加@Round注解,就能定义环绕通知:

    1. @Around("pointcut()")
    2. public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    3. System.out.println("打印请求日志");
    4. Object result = joinPoint.proceed();
    5. System.out.println("打印响应日志");
    6. return result;
    7. }

    结果通知

    该通知在方法结束后执行,能够获取方法返回结果,只需在公共方法上加@AfterReturning注解,就能定义结果通知:

    1. @AfterReturning(pointcut = "pointcut()",returning = "retVal")
    2. public void afterReturning(JoinPoint joinPoint, Object retVal) {
    3. System.out.println("获取结果:"+retVal);
    4. }

    异常通知

    该通知在方法抛出异常之后执行,只需在公共方法上加@AfterThrowing注解,就能定义异常通知:

    1. @AfterThrowing(pointcut = "pointcut()", throwing = "e")
    2. public void afterThrowing(JoinPoint joinPoint, Throwable e) {
    3. System.out.println("异常:"+e);
    4. }

    spring aop给这五种通知,分别分配了一个xxxAdvice类。在ReflectiveAspectJAdvisorFactory类的getAdvice方法中可以看得到:

    2ef11a9da84e46c8869d81f66bb0acc4.png 

    下面用一张图总结一下对应关系:

    f0a52b00b0874d779ed30f2c08aa193e.png 

    这五种xxxAdvice类都实现了Advice接口,但是有些差异。

    下面三个xxxAdvice类实现了MethodInterceptor接口:

    a99257ef04784c5993c740be299e8af2.png 

    而另外两个类:AspectJMethodBeforeAdvice 和 AspectJAfterReturningAdvice 没有实现上面的接口,这是为什么?

    (这里留点悬念,后面的文章会揭晓谜题,敬请期待。)

    一个猝不及防,旁边的兄弟放下碗冲过来问了句:这五种通知的执行顺序是怎么样的?

    单个切面正常情况

    b2e654dfe6314bb893789e86fb833818.png 

    单个切面异常情况

    3a021669e63d4a20a0d4d6b14f9fbc27.png 

    多个切面正常情况

    d51fec18ee954131a5f3abe06c86e19a.png 

    多个切面异常情况

    4471f2e2450a484da82b66827abfe452.png 

    当有多有切面时,按照可以通过@Order(n)指定执行顺序,n值越小越先执行。

    为什么使用链式调用?

    这个问题没人问,是我自己想聊聊。

    先看看spring是如何使用链式调用的,在ReflectiveMethodInvocation的proceed方法中,有这样一段代码:

    abfc877554fc43a79791a90d7a46fdcc.png 

    下面用一张图捋一捋上面的逻辑:

    286f23e019904663b746cd85c769c9f3.png 

    图中包含了一个递归的链式调用,为什么要这样设计呢?

    假如不这样设计,我们代码中是不是需要写很多if...else,根据不同的切面和通知单独处理?

    而spring巧妙的使用责任链模式消除了原本需要大量的if...else判断,让代码的扩展性更好,很好的体现了开闭原则:对扩展开放,对修改关闭。

    缓存中存的原始还是代理对象?

    我们知道spring中为了性能考虑是有缓存的,通常说包含了三级缓存:

    38a69b519c5b4e93a288335ccb0239f9.png 

    说时迟那时快,旁边的兄弟忍不住赶过来问了句:缓存中存的原始还是代理对象?

    我竟然被问得一时语塞,仔细捋了捋,要从三个方面回答:

    singletonFactories(三级缓存)

    AbstractAutowireCapableBeanFactory类的doCreateBean方法中,有这样一段代码:

    17bd4fa058224a359ea4af31f1991ba2.png 

    其实之前已经说过,它是为了解决循环依赖问题。这次要说的是addSingletonFactory方法:

    c7fd0712d39b46dcaff974bde61fd19f.png 

    它里面保存的是singletonFactory对象,所以是原始对象。

    earlySingletonObjects(二级缓存)

    AbstractBeanFactory类的doGetBean方法中,有这样一段代码:

    a4c11c43a3c84062a39c167658730133.png 

    在调用getBean方法获取bean实例时,会调用getSingleton尝试先从缓存中看能否获取到,如果能获取到则直接返回。

    fcd1054e5e3f4e5daee2700ea2072fb9.png 

    这段代码会先从一级缓存中获取bean,如果没有再从二级缓存中获取,如果还是没有则从三级缓存中获取singletonFactory,通过getObject方法获取实例,将该实例放入到二级缓存中。

    答案的谜底就聚焦在getObject方法中,而这个方法又是在哪来定义的呢?

    其实就是上面的getEarlyBeanReference方法,我们知道这个方法生成的是代理对象,所以二级缓存中存的是代理对象。

    singletonObjects(一级缓存)

    DefaultSingletonBeanRegistry类的getSingleton方法中,有这样一段代码:

    22e86c841508445a9cb4ab0a4a37eb33.png 

    此时的bean创建、注入和初始化完成了,判断是如果新的单例对象,则会加入到一级缓存中,具体代码如下:

    ac5d0f7f3b3d44edac3917375aa785d4.png 

    出于一块钱的友谊,有必要温馨提醒一下:这里是DefaultSingletonBeanRegistry类的getSingleton方法,跟上面说的AbstractBeanFactory类getSingleton方法不一样。

    几个常见的坑

    我是一个乐于分享的人,虽说有时话比较少(旁边:属于人狠话不多的角色,别惹我)。为了表现我的share精神,给大家总结几个我之前使用spring aop遇过的坑。

    我们几乎每天都在用spring aop。

    “什么?我怎么不知道?” 你可能会问。

    如果你每天在用spring事务的话,就是每天在用spring aop,因为spring事务的底层就用到了spring aop。

    坑1:直接方法调用

    使用spring事务时,直接方法调用:

    1. @Service
    2. public class UserService {
    3. @Autowired
    4. private UserMapper userMapper;
    5. public void add(UserModel userModel) {
    6. userMapper.queryUser(userModel);
    7. save(userModel);
    8. }
    9. @Transactional
    10. public void save(UserModel userModel) {
    11. System.out.println("保存数据");
    12. }
    13. }

    这种情况直接方法调用spring aop无法生成代理对象,事务会失效。这个问题的解决办法有很多:

    1. 使用TransactionTemplate手动开启事务

    2. 将事务方法save放到新加的类UserSaveService中,通过userSaveService.save调用事务方法。

    3. UserService类中@Autowired注入自己的实例userService,通过userService.save调用事务方法。

    4. 通过AopContext类获取代理对象:((UserService)AopContext.currentProxy()).save(user);

    坑2:访问权限错误

    1. @Service
    2. public class UserService {
    3. @Autowired
    4. private UserService userService;
    5. @Autowired
    6. private UserMapper userMapper;
    7. public void add(UserModel userModel) {
    8. userMapper.queryUser(userModel);
    9. userService.save(userModel);
    10. }
    11. @Transactional
    12. private void save(UserModel userModel) {
    13. System.out.println("保存数据");
    14. }
    15. }

    上面用 UserService类中@Autowired注入自己的实例userService的方式解决事务失效问题,如果不出意外的话,是可以的。

    但是恰恰出现了意外,save方法被定义成了private的,这时也无法生成代理对象,事务同样会失效。

    所以,我们应该拿个小本本记一下,目标方法一定不能定义成private的。

    坑3:目标类用final修饰

    1. @Service
    2. public class UserService {
    3. @Autowired
    4. private UserService userService;
    5. @Autowired
    6. private UserMapper userMapper;
    7. public void add(UserModel userModel) {
    8. userMapper.queryUser(userModel);
    9. userService.save(userModel);
    10. }
    11. @Transactional
    12. public final void save(UserModel userModel) {
    13. System.out.println("保存数据");
    14. }
    15. }

    这种情况spring aop生成代理对象,重写save方法时,发现的final的,重写不了,也会导致事务失效。

    小本本需要再加一条,目标方法一定不能定义成final的。

    坑4:循环依赖问题

    在使用@Async注解开启异步功能的场景,它会通过AOP自动生成代理对象。

    1. @Service
    2. public class TestService1 {
    3. @Autowired
    4. private TestService2 testService2;
    5. @Async
    6. public void test1() {
    7. }
    8. }
    9. @Service
    10. public class TestService2 {
    11. @Autowired
    12. private TestService1 testService1;
    13. public void test2() {
    14. }
    15. }

    启动服务会报错:

    org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'testService1': Bean with name 'testService1' has been injected into other beans [testService2] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.
    

    至于为什么会报错,我在这里不过多解释了。

    最后说一句(求关注,别白嫖我)

    您看到的这篇文章,我写出来可能需要花费一周左右的时间。原创真心不易,画图不易,构思不易,码字不易。如果您通过这篇文章有些收获,那是我最开心的事情。如果恰好您能随手点个 在看,或者点 个赞,我会开心一整天。如果您觉得文章有些价值,分享给朋友,或者转发到朋友圈,我会对您感激不尽。

  • 相关阅读:
    CSS详解(二)
    java中如何将嵌套循环性能提高500倍
    医学之细胞组织基因(完整)
    linux rsyslog日志采集格式设定三
    【ROS】机械人开发四--ROS常用概念与Launch文件
    全网最全最深:web前端架构师面试题+缜密全面的学习笔记
    【1day】用友移动管理系统任意文件读取漏洞学习
    JAVA【Apache-DBUtils实现CRUD】
    分布式一致性算法 Raft
    React从脚手架开始搭建项目
  • 原文地址:https://blog.csdn.net/m0_72088858/article/details/126757329