动态代理之自调用

问题描述

在spring容器中,当一个bean调用自己的类中的方法时,该被调用的方法上的注解会失效,如@Transactional。

验证

利用spring官网创建一个web项目。官网创建地址

1. 添加依赖

xml 复制代码
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2. 创建测试类

controller
kotlin 复制代码
package com.example.demo.controller;

##### import com.example.demo.service.TestService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
@RequestMapping("/test")
public class TestController {

    @Autowired
    TestService testService;

    @GetMapping("/test")
    public String test(){
        testService.test1();
        return "yes";
    }
}
service
csharp 复制代码
package com.example.demo.service;

public interface TestService {

    void test1();

    void test2();
}
typescript 复制代码
package com.example.demo.service.impl;

import com.example.demo.aop.MyLog;
import com.example.demo.service.TestService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class TestServiceImpl implements TestService {

    @Override
    @MyLog("this is test1 log")
    public void test1() {
        log.info("this is test1");
        this.test2();
    }

    @Override
    @MyLog("this is test2 log")
    public void test2() {
        log.info("this is test2");
    }
}

3. 自定义一个日志注解

java 复制代码
package com.example.demo.aop;

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyLog {

    /**
     * 描述
     * @return {String}
     */
    String value() default "";

}

4. 切面逻辑

java 复制代码
package com.example.demo.aop;

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

@Aspect
@Slf4j
@Component
public class SysLogAspect {

    @Pointcut("@annotation(com.example.demo.aop.MyLog)")
    public void getLog() {
    }

    @Around("getLog()")
    @SneakyThrows
    public void around(ProceedingJoinPoint point) {
        MethodSignature signature = (MethodSignature)  point.getSignature();
        MyLog myLog = signature.getMethod().getAnnotation(MyLog.class);
        String value = myLog.value();
        log.info(value);
        point.proceed();
    }

}

5. 测试

按照预想,controller调用service的test1方法时会出现日志"this is test1 log",test1方法继续调用test2方法时会出现日志"this is test2 log"。

但是调用完成后的打印内容如下,并未出现test2的前置日志。

验证了"当一个bean调用自己类的方法时,该被调用的方法上的注解会失效。"

原理

很长一段时间,aop对我来说都很抽象,很多教程会详细说明面向切面的原理,讲切面是什么,讲spring aop的注解怎么用,我总是看了一遍忘一遍,过段日子再复习一遍。

直到我打断点时看到一直忽略的一行

controller调用的是一个cglib的代理类对象里面的test1,它是TestServiceImpl的代理类对象,这个代理类再调用真正的TestServiceImpl的test1。

忽略所有的理论和名词,这就是面向切面的核心,生成一个代理类(不论是谁生成的),代理类里面存在你想要的前置和后置步骤,而别的对象也不管它真正的内部逻辑,只需要调用这个代理类,在spring容器里,这个代理类的对象才是TestService的bean

因此自调用的注解失效也就可以理解,TestServiceImpl的test1里面调用的是TestServiceImpl的test2,想要注解生效,需要调用cglib的代理类对象的test2才对。

解决

既然知道问题在哪,就知道该怎么避免问题。

第一个方案

避免在同一个bean里调用,把test2的内容写在别的bean里。

第二个方案

如果非要写在同一个bean里,可以在先获取自己的代理类。要注意先开启注解:

ini 复制代码
@EnableAspectJAutoProxy(exposeProxy=true)

然后通过aop上下文获取代理类

less 复制代码
@Override
@MyLog("this is test1 log")
public void test1() {
    log.info("this is test1");
    //获取自己的代理类
    TestService testService = (TestService) AopContext.currentProxy();
    testService.test2();
}

第三个方案

自装配,即TestServiceImpl里面再装一个TestService

typescript 复制代码
package com.example.demo.service.impl;

import com.example.demo.aop.MyLog;
import com.example.demo.service.TestService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class TestServiceImpl implements TestService {

    @Autowired
    TestService testService;

    @Override
    @MyLog("this is test1 log")
    public void test1() {
        log.info("this is test1");
        testService.test2();
    }

    @Override
    @MyLog("this is test2 log")
    public void test2() {
        log.info("this is test2");
    }
}

不过我个人不建议这种方法,这种方法会陷入另一个问题,循环依赖。虽然网上解决循环依赖的帖子很泛滥,但我仍然觉得循环依赖的问题不是解决的,而是应该避免的。这种循环既不符合设计原理也没有可读性,实在是防御性编程,好的设计不应该出现这种循环。

以上是个人理解,如有误解欢迎指正。

相关推荐
x***010619 小时前
springboot中配置logback-spring.xml
spring boot·spring·logback
q***25121 小时前
Spring容器的开启与关闭
java·后端·spring
0***m82221 小时前
Maven Spring框架依赖包
java·spring·maven
K***430621 小时前
三大框架-Spring
java·spring·rpc
后端小张21 小时前
【JAVA 进阶】深入探秘Netty之Reactor模型:从理论到实战
java·开发语言·网络·spring boot·spring·reactor·netty
2501_941886861 天前
边缘计算崛起:推动万物互联时代高效运算的新引擎
spring
yihuiComeOn1 天前
[源码系列:手写Spring] AOP第二节:JDK动态代理 - 当AOP遇见动态代理的浪漫邂逅
java·后端·spring
程序猿小蒜1 天前
基于springboot的的学生干部管理系统开发与设计
java·前端·spring boot·后端·spring
q***56381 天前
Spring容器初始化扩展点:ApplicationContextInitializer
java·后端·spring
q***46521 天前
Spring中使用Async进行异步功能开发实战-以大文件上传为例
java·后端·spring