动态代理之自调用

问题描述

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

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

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

相关推荐
与遨游于天地1 小时前
Spring解决循环依赖实际就是用了个递归
java·后端·spring
不会吃萝卜的兔子1 小时前
spring - 微服务授权 1
spring
程序定小飞5 小时前
基于springboot的web的音乐网站开发与设计
java·前端·数据库·vue.js·spring boot·后端·spring
Rover.x11 小时前
Spring国际化语言切换不生效
java·后端·spring
Mos_x12 小时前
【Spring Boot】Spring Boot解决循环依赖
java·spring boot·spring
ZHE|张恒12 小时前
深入理解 Spring 原理:IOC、AOP 与事务管理
java·后端·spring
♡喜欢做梦15 小时前
Spring MVC 响应处理:页面、数据与状态配置详解
java·javascript·spring·java-ee
L.EscaRC20 小时前
Spring Security的解析与应用
spring boot·spring
天若有情6731 天前
【java EE】IDEA 中创建或迁移 Spring 或 Java EE 项目的核心步骤和注意事项
后端·spring·java-ee·intellij-idea
钱多多_qdd1 天前
基础篇:IoC(三):Bean实例化策略InstantiationStrategy
java·spring