likes
comments
collection
share

动态代理之自调用

作者站长头像
站长
· 阅读数 1

问题描述

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

验证

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

1. 添加依赖

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2. 创建测试类

controller
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
package com.example.demo.service;

public interface TestService {

    void test1();

    void test2();
}
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. 自定义一个日志注解

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. 切面逻辑

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里,可以在先获取自己的代理类。要注意先开启注解:

@EnableAspectJAutoProxy(exposeProxy=true)

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

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

第三个方案

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

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

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

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