动态代理之自调用

问题描述

在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");
    }
}

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

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

相关推荐
BUG指挥官15 小时前
Sa-Token和Spring Security对比
java·后端·spring
geminigoth16 小时前
Spring AI Alibaba 入门开发一(备份)
java·人工智能·spring
xbgRS18 小时前
spring整合mybaits
java·spring·mybatis
ruleslol19 小时前
静态依赖注入 VS 动态依赖查询
spring
墨雨晨曦8821 小时前
Spring AI总结
java·人工智能·spring
用户3126874877201 天前
Spring Boot 异常处理到底怎么玩的?从 DispatcherServlet 到全局兜底的全链路拆解
spring
不才不才不不才1 天前
Spring 源码系列(17): HandlerMapping 与 HandlerAdapter 两大体系
java·后端·spring
(轻舟已过万重山)1 天前
第40章 Spring AI 实战:企业级 AI 应用架构
人工智能·spring·架构
堕落年代2 天前
Ollama CPU 推理大提示词优化实测报告(细致化数据版)
java·后端·spring
cfm_29142 天前
基于OAuth2.0实现微服务SSO单点登录
后端·spring·微服务·架构