Spring编程常见错误50例-Spring AOP常见错误(上)

Spring AOP常见错误(上)

this调用的当前类方法无法被拦截

问题

假设当前开发负责电费充值的类,同时记录下进行充值的时间(此时需要使用到AOP),并提供电费充值接口:

java 复制代码
@Service
public class ElectricService {
    public void charge() throws Exception {
        System.out.println("Electric charging ...");
        this.pay();

    public void pay() throws Exception {
        System.out.println("Pay with alipay ...");
        // 模拟支付耗时
        Thread.sleep(1000);
    }
}
java 复制代码
@Aspect
@Service
@Slf4j
public class AopConfig {
    @Around("execution(* com.spring.puzzle.class5.example1.ElectricService.pay()) ")
    public void recordPayPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        joinPoint.proceed();
        long end = System.currentTimeMillis();
        System.out.println("Pay method time cost(ms): " + (end - start));
    }
}
java 复制代码
@RestController
public class HelloWorldController {
    @Autowired
    ElectricService electricService;

    @RequestMapping(path = "charge", method = RequestMethod.GET)
    public void charge() throws Exception{
        electricService.charge();
    };
}

但是在访问接口后,计算时间的切面并没有被执行,即在类的内部通过this方式调用的方法没被AOP增强的

原因

通过Debug可知this对应的是普通的ElectricService对象,而在控制器类装配的electricService对象是被Spring增强后的Bean:

先补充关于Spring AOP的基础知识:

  • Spring AOP的实现

    • Spring AOP的底层是动态代理,而创建代理的方式有两种:

      • JDK动态代理只能对实现了接口的类生成代理,而不能针对普通类

      • CGLIB可针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法来实现代理对象

  • 如何使用Spring AOP

    • 添加依赖:
    xml 复制代码
    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-aop</artifactId>
    </dependency>	
    • 添加注解:对于非Spring Boot程序,除了添加依赖项外还常会使用@EnableAspectJAutoProxy来开启AOP功能

具体看下创建代理对象的过程:创建代理对象的关键由AnnotationAwareAspectJAutoProxyCreator完成的,它本质上是一种BeanPostProcessor,所以它的执行是在完成原始Bean构建后的初始化Bean中:

java 复制代码
// AbstractAutoProxyCreator#postProcessAfterInitialization
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
    if (bean != null) {
        Object cacheKey = getCacheKey(bean.getClass(), beanName);
        if (this.earlyProxyReferences.remove(cacheKey) != bean) {
            // *需使用AOP时,该方法把创建的原始的Bean对象wrap成代理对象作为Bean返回
            return wrapIfNecessary(bean, beanName, cacheKey);
        }
    }
    return bean;
}
java 复制代码
// AbstractAutoProxyCreator#wrapIfNecessary
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
    ...
    Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
    if (specificInterceptors != DO_NOT_PROXY) {
        this.advisedBeans.put(cacheKey, Boolean.TRUE);
        // *创建代理对象的关键
        Object proxy = createProxy(
            bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
        this.proxyTypes.put(cacheKey, proxy.getClass());
        return proxy;
    }
	...
}
java 复制代码
// AbstractAutoProxyCreator#createProxy
protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
                             @Nullable Object[] specificInterceptors, TargetSource targetSource) {
	...
    // 创建代理工厂
    ProxyFactory proxyFactory = new ProxyFactory();
    proxyFactory.copyFrom(this);
	// 将通知器(advisors)、被代理对象等信息加入到代理工厂
    if (!proxyFactory.isProxyTargetClass()) {
        if (shouldProxyTargetClass(beanClass, beanName)) {
            proxyFactory.setProxyTargetClass(true);
        }
        else {
            evaluateProxyInterfaces(beanClass, proxyFactory);
        }
    }

    Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
    proxyFactory.addAdvisors(advisors);
    proxyFactory.setTargetSource(targetSource);
    customizeProxyFactory(proxyFactory);
	...
    // 通过该代理工厂来获取代理对象
    return proxyFactory.getProxy(getProxyClassLoader());
}

只有通过上述工厂才创建出一个代理对象,而之前直接使用this使用的还是普通对象

解决方式

方法的核心在于引用被动态代理创建出来的对象,有以下两种方式:

  • 使用被@Autowired注解的对象替换this
java 复制代码
@Service
public class ElectricService {
	@Autowired
    ElectricService electricService;

    public void charge() throws Exception {
        System.out.println("Electric charging ...");
        electric.pay();
    }

    public void pay() throws Exception {
        System.out.println("Pay with alipay ...");
        Thread.sleep(1000);
    }
}
  • 直接从AopContext获取当前的ProxyAopContext是通过一个ThreadLocalProxy和线程绑定,这样就可随时拿出当前线程绑定的Proxy(前提是在 @EnableAspectJAutoProxy里加配置项exposeProxy = true,表示将代理对象放入到ThreadLocal
java 复制代码
@Service
public class ElectricService {
    public void charge() throws Exception {
        System.out.println("Electric charging ...");
        ElectricService electric = ((ElectricService) AopContext.currentProxy());
        electric.pay();

    }
    public void pay() throws Exception {
        System.out.println("Pay with alipay ...");
        Thread.sleep(1000);
    }

}
java 复制代码
@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true)
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

直接访问被拦截类的属性抛空指针异常

问题

在使用charge方法进行支付时会用到一个管理员用户付款编号,此时新增几个类:

java 复制代码
// 包含用户的付款编号信息
public class User {
    private String payNum;

    public User(String payNum) {
        this.payNum = payNum;
    }
    public String getPayNum() {
        return payNum;
    }
    public void setPayNum(String payNum) {
        this.payNum = payNum;
    }
}
java 复制代码
@Service
public class AdminUserService {
    public final User adminUser = new User("202101166");
    // 用于登录系统
    public void login() {
        System.out.println("admin user login...");
    }
}

在电费充值时需管理员登录并使用其编号进行结算:

java 复制代码
@Service
public class ElectricService {
    @Autowired
    private AdminUserService adminUserService;

    public void charge() throws Exception {
        System.out.println("Electric charging ...");
        this.pay();
    }


    public void pay() throws Exception {
        adminUserService.login();
        String payNum = adminUserService.adminUser.getPayNum();
        System.out.println("User pay num : " + payNum);
        System.out.println("Pay with alipay ...");
        Thread.sleep(1000);
    }
}

由于安全需管理员在登录时记录一行日志以便于以后审计管理员操作:

java 复制代码
@Aspect
@Service
@Slf4j
public class AopConfig {
    @Before("execution(* com.spring.puzzle.class5.example2.AdminUserService.login(..)) ")
    public void logAdminLogin(JoinPoint pjp) throws Throwable {
        System.out.println("! admin login ...");
    }
}

结果在执行到接口中的electricService.charge()时不仅没打印日志,还执行String payNum = adminUserService.adminUser.getPayNum()NPE,对pay方法进行分析后发现加入AOPadminUserService对象已经是代理对象了,但是它的adminUser属性是null

原因

增强后的类实际是AdminUserService的子类,它会重写所有publicprotected方法,并在内部将调用委托给原始的AdminUserService实例(以CGLIBProxy的实现类CglibAopProxy为例来看具体的流程)

java 复制代码
public Object getProxy(@Nullable ClassLoader classLoader) {
    ...
        // ①创建并配置enhancer
        Enhancer enhancer = createEnhancer();
        ...
        // ②获取Callback:包含DynamicAdvisedInterceptor,即MethodInterceptor
        Callback[] callbacks = getCallbacks(rootClass);
    	...
        // ③生成代理对象并创建代理,即设置enhancer的callback值
        return createProxyClassAndInstance(enhancer, callbacks);
    }
    ...
}

第三步会执行到CglibAopProxy子类ObjenesisCglibAopProxycreateProxyClassAndInstance方法中:Spring会默认尝试使用objenesis方式实例化对象,如失败则再尝试使用常规方式实例化对象

java 复制代码
protected Object createProxyClassAndInstance(Enhancer enhancer, Callback[] callbacks) {
    // 创建代理类class
    Class<?> proxyClass = enhancer.createClass();
    Object proxyInstance = null;
	// 一般为true
    if (objenesis.isWorthTrying()) {
        try {
            // 创建实例
            proxyInstance = objenesis.newInstance(proxyClass, enhancer.getUseCache());
        }
        ...
    }

    if (proxyInstance == null) {
        // 尝试普通反射方式创建实例
        try {
            Constructor<?> ctor = (this.constructorArgs != null ?
                                   proxyClass.getDeclaredConstructor(this.constructorArgTypes) :
                                   proxyClass.getDeclaredConstructor());
            ReflectionUtils.makeAccessible(ctor);
            proxyInstance = (this.constructorArgs != null ?
                             ctor.newInstance(this.constructorArgs) : ctor.newInstance());
        }
        ...
    }

    ((Factory) proxyInstance).setCallbacks(callbacks);
    return proxyInstance;
}

objenesis方式最后使用了JDKReflectionFactory.newConstructorForSerialization方法完成代理对象的实例化,这种方式创建出来的对象不会初始化类成员变量

解决方式

AdminUserService里写getAdminUser方法,从内部访问获取变量:

java 复制代码
@Service
public class AdminUserService {
    public final User adminUser = new User("202101166");

    public User getAdminUser() {
        return adminUser;
    }

    public void login() {
        System.out.println("admin user login...");
    }
}
java 复制代码
@Service
public class ElectricService {
    @Autowired
    private AdminUserService adminUserService;

    public void charge() throws Exception {
        System.out.println("Electric charging ...");
        this.pay();
    }

    public void pay() throws Exception {
        adminUserService.login();
        String payNum = adminUserService.getAdminUser().getPayNum();  // 原来该步骤处报NPE
        System.out.println("User pay num : " + payNum);
        System.out.println("Pay with alipay ...");
        Thread.sleep(1000);
    }
}

既然代理类的类属性不会被初始化,为啥可通过AdminUserServicegetUser方法获取到代理类实例的属性?当代理类方法被调用后会被Spring拦截,进入到DynamicAdvisedInterceptor#intercept方法,在此方法中获取被代理的原始对象(原始对象的类属性是被实例化过且存在的)

根据原因分析,还可以有另一种解决方式:修改启动参数spring.objenesis.ignore=true

参考

极客时间-Spring 编程常见错误 50 例

https://github.com/jiafu1115/springissue/tree/master/src/main/java/com/spring/puzzle/class5

相关推荐
冷琴19963 分钟前
基于java+springboot的酒店预定网站、酒店客房管理系统
java·开发语言·spring boot
daiyang123...29 分钟前
IT 行业的就业情况
java
Nightselfhurt41 分钟前
Spring cloud 中gateway原理
spring·spring cloud·gateway
爬山算法1 小时前
Maven(6)如何使用Maven进行项目构建?
java·maven
.生产的驴1 小时前
Electron Vue框架环境搭建 Vue3环境搭建
java·前端·vue.js·spring boot·后端·electron·ecmascript
爱学的小涛1 小时前
【NIO基础】基于 NIO 中的组件实现对文件的操作(文件编程),FileChannel 详解
java·开发语言·笔记·后端·nio
吹老师个人app编程教学1 小时前
详解Java中的BIO、NIO、AIO
java·开发语言·nio
爱学的小涛1 小时前
【NIO基础】NIO(非阻塞 I/O)和 IO(传统 I/O)的区别,以及 NIO 的三大组件详解
java·开发语言·笔记·后端·nio
北极无雪1 小时前
Spring源码学习:SpringMVC(4)DispatcherServlet请求入口分析
java·开发语言·后端·学习·spring
琴智冰1 小时前
SpringBoot
java·数据库·spring boot