Day606.SpringAOP常编程错误案例① -Spring编程常见错误

Posted 阿昌喜欢吃黄桃

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Day606.SpringAOP常编程错误案例① -Spring编程常见错误相关的知识,希望对你有一定的参考价值。

SpringAOP常编程错误案例①

Spring AOP 是 Spring 中除了依赖注入外(DI)最为核心的功能。

顾名思义,AOP 即 Aspect Oriented Programming,翻译为面向切面编程

而 Spring AOP 则利用 CGlibJDK 动态代理等方式来实现运行期动态方法增强,其目的是将与业务无关的代码单独抽离出来,使其逻辑不再与业务代码耦合,从而降低系统的耦合性,提高程序的可重用性和开发效率。

追根溯源,我们之所以能无感知地在容器对象方法前后任意添加代码片段,那是由于 Spring 在运行期帮我们把切面中的代码逻辑动态“织入”到了容器对象方法内,所以说 AOP 本质上就是一个代理模式

下面记录两种AOP编程中会出现的编程错误案例!


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

假设我们系统,这个模块包含一个负责电费充值的类 ElectricService,它含有一个充电方法 charge():

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

在这个电费充值方法 charge() 中,我们会使用支付宝进行充值。

因此在这个方法中,我加入了 pay() 方法。为了模拟 pay() 方法调用耗时,代码执行了休眠 1 秒,并在 charge() 方法里使用 this.pay() 的方式调用这种支付方法。

但是因为支付服务是第三方接口,我们需要记录下接口调用时间。

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

这时候我们就引入了一个 @Around 的增强 ,分别记录在 pay() 方法执行前后的时间,并计算出执行 pay() 方法的耗时

@RestController
public class HelloWorldController 
    @Autowired
    ElectricService electricService;
    @RequestMapping(path = "charge", method = RequestMethod.GET)
    public void charge() throws Exception
          electricService.charge();
    ;

最后我们再通过定义一个 Controller 来提供暴露接口。

完成代码后,我们访问上述接口,会发现这段计算时间的切面并没有执行到,输出日志如下:

Electric charging …
Pay with alipay …

回溯之前的代码可知,在 @Around 的切面类中,我们很清晰地定义了切面对应的方法,但是却没有被执行到。

这说明了在类的内部,通过 this 方式调用的方法,是没有被 Spring AOP 增强的。这是为什么呢?


首先来设置个断点,调试看看 this 对应的对象是什么样的: 看到这个this就是Service本身

再看看在 Controller 层中自动装配的 ElectricService 对象是什么样:被 Spring 增强代理过的 Bean


首先Spring创建代理对象的过程。先来看下调用栈:

创建代理对象的时机就是创建一个 Bean 的时候,而创建的的关键工作其实是由 AnnotationAwareAspectJAutoProxyCreator 完成的。

它本质上是一种 BeanPostProcessor。所以它的执行是在完成原始 Bean 构建后的初始化 Bean(initializeBean)过程中。

而它到底完成了什么工作呢?我们可以看下它的 postProcessAfterInitialization 方法

public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) 
   if (bean != null) 
      Object cacheKey = getCacheKey(bean.getClass(), beanName);
      if (this.earlyProxyReferences.remove(cacheKey) != bean) 
      	//可以看到,他会进行判断,是否有必须要进行包装
         return wrapIfNecessary(bean, beanName, cacheKey);
      
   
   return bean;

具体到这个 wrap 过程,可参考下面的关键代码行:

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;
   
   // 省略非关键代码 

上述代码中,createProxy() 调用是创建代理对象的关键。

具体到执行过程,它首先会创建一个代理工厂,然后将通知器(advisors)、被代理对象等信息加入到代理工厂,最后通过这个代理工厂来获取代理对象。一些关键过程参考下面的方法:

protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
      @Nullable Object[] specificInterceptors, TargetSource targetSource) 
  // 省略非关键代码
  ProxyFactory proxyFactory = new ProxyFactory();
  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());

经过这样一个过程,一个代理对象就被创建出来了。

从 Spring 中获取到的对象都是这个代理对象,所以具有 AOP 功能。而之前直接使用 this 引用到的只是一个普通对象,自然也就没办法实现 AOP 的功能了。

所以从上面看出,只有引用的是被动态代理创建出来的对象,才会被 Spring 增强,具备 AOP 该有的功能。


那么解决方案这里提供两种:

  • 第一种:被 @Autowired 注解的,于是我们的代码可以改成这样,即通过 @Autowired 的方式,在类的内部,自己引用自己:
    @Service
    public class ElectricService 
        @Autowired
        ElectricService electricService;
        public void charge() throws Exception 
            System.out.println("Electric charging ...");
            //this.pay();
            electricService.pay();
        
        public void pay() throws Exception 
            System.out.println("Pay with alipay ...");
            Thread.sleep(1000);
        
    
    
  • 第二种:直接从 AopContext 获取当前的 Proxy,通过一个 ThreadLocal 来将 Proxy 和线程绑定起来,这样就可以随时拿出当前线程绑定的 Proxy。
    • @EnableAspectJAutoProxy 里加一个配置项 exposeProxy = true ,Spring默认是关闭的
    @SpringBootApplication
    @EnableAspectJAutoProxy(exposeProxy = true)
    public class Application 
        // 省略非关键代码
    
    
    • 通过AopContext.currentProxy(),获取当前代理对象
    @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);
        
    
    

这两种方法的效果其实是一样的,最终我们打印出了期待的日志,到这,问题顺利解决了。

Electric charging ...
Pay with alipay ...
Pay method time cost(ms): 1005

二、通过代理类访问被代理类的成员属性抛空指针异常

在系统中,我们使用了 charge() 方法进行支付。

在统一结算的时候我们会用到一个管理员用户付款编号,这时候就用到了几个新的类。User 类,包含用户的付款编号信息:

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;
    

AdminUserService 类,包含一个管理员用户(User),其付款编号为 2022050393;

另外,这个服务类有一个 login() 方法,用来登录系统。

@Service
public class AdminUserService 
    public final User adminUser = new User("2022050393");
    
    public void login() 
        System.out.println("admin user login...");
    

我们需要修改 ElectricService 类实现这个需求:

在电费充值时,需要管理员登录并使用其编号进行结算。完整代码如下:

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

代码完成后,执行 charge() 操作,一切正常:

Electric charging …
admin user login…
User pay num : 2022050393
Pay with alipay …

这时候,由于安全需要,就需要管理员在登录时,记录一行日志以便于以后审计管理员操作。

所以我们添加一个 AOP 相关配置类,具体如下:

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

添加这段代码后,我们执行 charge() 操作,发现不仅没有相关日志,而且在执行下面这一行代码的时候直接抛出了 NullPointerException:

String payNum = dminUserService.user.getPayNum();

那为什么会出现这个问题呢???


可以看出上面Spring用的是Cglib的方式去创建代理对象的方式。

CGLIB 中 AOP 的实现是基于 org.springframework.cglib.proxy 包中 EnhancerMethodInterceptor 两个接口来实现的。

整个过程,我们可以概括为三个步骤

  • 定义自定义的 MethodInterceptor 负责委托方法执行;
  • 创建 Enhance 并设置 Callback 为上述 MethodInterceptor;
  • enhancer.create() 创建代理。

那Cglib的方式,在最后Spring 会默认尝试使用 objenesis 方式实例化对象,如果失败则再次尝试使用常规方式实例化对象

protected Object createProxyClassAndInstance(Enhancer enhancer, Callback[] callbacks) 
   //创建代理类Class
   Class<?> proxyClass = enhancer.createClass();
   Object proxyInstance = null;
   //spring.objenesis.ignore默认为false
   //所以objenesis.isWorthTrying()一般为true
   if (objenesis.isWorthTrying()) 
      try 
      	//先尝试使用objenesis的方式实例化对象
         // 创建实例
         proxyInstance = objenesis.newInstance(proxyClass, enhancer.getUseCache());
      
      catch (Throwable ex) 
          // 省略非关键代码
      
   
       //objenesis的方式实例化对象失败后,就在下面
    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 方式最后使用了 JDK 的 ReflectionFactory.newConstructorForSerialization() 完成了代理对象的实例化。

而如果你稍微研究下这个方法,你会惊讶地发现,这种方式创建出来的对象是不会初始化类成员变量的。

这个案例的核心是代理类实例的默认构建方式很特别。


在这里,我们可以总结和对比下通过反射来实例化对象的方式,包括:

  • java.lang.Class.newInsance()
  • java.lang.reflect.Constructor.newInstance()
  • sun.reflect.ReflectionFactory.newConstructorForSerialization().newInstance()

前两种初始化方式都会同时初始化类成员变量,但是最后一种通过 ReflectionFactory.newConstructorForSerialization().newInstance() 实例化类则不会初始化类成员变量,这就是当前问题的最终答案了。

那知道了为什么,我们无法通过代理类去访问被代理类的成员变量的问题的原因是Spring使用Cglib方式去生成代理对象。其中他最后使用到了objenesis方式去生成代理对象,

这种方式中会去使用sun.reflect.ReflectionFactory.newConstructorForSerialization().newInstance()这种方式去生成对应代理对象。但是这种方式不会去初始化被代理类的成员变量给代理类,这就是为什么。


那提供的这里提供两种解决方案
我们不可能去直接改写Spring的源码让他换对应使用的生成代理类的方式。

  • 通过类方法去获取被代理类的成员变量

    public User getUser() 
        return user;
    
    

    在 ElectricService 里通过 getUser() 获取 User 对象:

    // 原来出错的方式:
    //String payNum = = adminUserService.adminUser.getPayNum();
    // 修改后的方式:
    String payNum = adminUserService.getAdminUser().getPayNum();

  • 修改启动参数 spring.objenesis.ignore ,告诉Spring,让他初始化代理类中的被代理类的成员属性


三、总结

  • 使用 AOP,实际上就是让 Spring 自动为我们创建一个 Proxy,使得调用者能无感知地调用指定方法。而 Spring 有助于我们在运行期里动态织入其它逻辑,因此,AOP 本质上就是一个动态代理。

  • 我们只有访问这些代理对象的方法,才能获得 AOP 实现的功能,所以通过 this 引用是无法正确使用 AOP 功能的。在不能改变代码结果前提下,我们可以通过 @Autowired、AopContext.currentProxy() 等方式获取相应的代理对象来实现所需的功能。

  • 我们一般不能直接从代理类中去拿被代理类的属性,这是因为除非我们显示设置 spring.objenesis.ignore 为 true,否则代理类的属性是不会被 Spring 初始化的,我们可以通过在被代理类中增加一个方法来间接获取其属性。

以上是关于Day606.SpringAOP常编程错误案例① -Spring编程常见错误的主要内容,如果未能解决你的问题,请参考以下文章

Day616.SpringException常见错误 -Spring常见编程错误

go基础编程 day-1

Day629.思考题解答① -Java业务开发常见错误

Day661.分析定位Java问题工具① -Java业务开发常见错误

go基础编程 day-2

Starting Day One