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

Spring AOP常见错误(上)

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

问题

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

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

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

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

// 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;
}
// 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;
    }
	...
}
// 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
@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
@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);
    }

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

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

问题

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

// 包含用户的付款编号信息
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;
    }
}
@Service
public class AdminUserService {
    public final User adminUser = new User("202101166");
    // 用于登录系统
    public void login() {
        System.out.println("admin user login...");
    }
}

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

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

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

@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为例来看具体的流程)

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方式实例化对象,如失败则再尝试使用常规方式实例化对象

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方法,从内部访问获取变量:

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值