文章目录

  • 前言
  • 参考目录
  • 问题描述
  • 问题重现
  • 1、没有加入 Spring AOP 测试
  • 1.1、Controller
  • 1.2、Service & ServiceImpl
  • 1.3、请求结果
  • 1.3.1、`public` 修饰方法
  • 1.3.2、`protected` 修饰方法
  • 1.3.3、`private` 修饰方法
  • 2、加入 Spring AOP 测试
  • 2.1、自定义切面 `LogAspects`
  • 2.2、AOP 配置类 `MyAopConfig`
  • 2.3、请求结果
  • 2.3.1、`public` 修饰方法
  • 2.3.2、`protected` 修饰方法
  • 2.3.3、`private` 修饰方法
  • 2.3.4、`private` 修饰方法,使用 `getBean` 方法获取
  • 问题分析
  • 类方法的过滤


前言

最近在写代码的时候犯了一个低级错误,但是要搞清楚问题背后的原理属实不太好找,这篇博客简单记录一下。

参考目录

问题描述

问题非常简单,我把 controller 里面的方法写成了 private 私有方法。导致调用 service 方法时报了 NullPointerException

一开始还没看到是修饰符的问题,而且容器中存在 service Bean,用 SpringUtil.getBean 方式可以获取到并且可以调用方法。后面发现是修饰符问题,真的是一整个无语住了。

问题重现

在一开始查询问题的时候,有看到同样遇到这个问题的朋友,大多是改为 public 修饰符即可,无意中看到一个评论说这是因为 Spring AOP 导致的,所以围绕这个问题,我写了一个 demo 来进行验证。

1、没有加入 Spring AOP 测试

1.1、Controller

分别用三种修饰符 publicprotectedprivate 调用 service 方法。

@RequiredArgsConstructor
@RestController
public class TestDemoController {

    private final ITestDemoService iTestDemoService;

    @GetMapping("/demo/public")
    public String demo1() {
        return iTestDemoService.demo();
    }

    @GetMapping("/demo/protected")
    protected String demo2() {
        return iTestDemoService.demo();
    }

    @GetMapping("/demo/private")
    private String demo3() {
        return iTestDemoService.demo();
    }

}

1.2、Service & ServiceImpl

public interface ITestDemoService {

    String demo();

}

@Service
public class TestDemoServiceImpl implements ITestDemoService {

    @Override
    public String demo() {
        return "success";
    }

}

1.3、请求结果

1.3.1、public 修饰方法

java中service注入失败 service注入失败空指针_修饰符

1.3.2、protected 修饰方法

java中service注入失败 service注入失败空指针_修饰符_02

1.3.3、private 修饰方法

java中service注入失败 service注入失败空指针_AOP_03


由此可知,默认情况下,三种修饰符都能够调用 service 方法。

2、加入 Spring AOP 测试

Controller、Service、ServiceImpl 不变。

2.1、自定义切面 LogAspects

@Aspect
public class LogAspects {

    // 抽取公共的切入点表达式
    // 1、本类引用
    // 2、其他的切面引用
    @Pointcut("execution(String space.zlyx.zlyxdemo.controller.TestDemoController.*(..))")
    public void pointCut() {
    }

    // @Before在目标方法之前切入;切入点表达式(指定在哪个方法切入)
    @Before("pointCut()")
    public void logStart(JoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();
        System.out.println("" + joinPoint.getSignature().getName()
                + " 运行。。。@Before:参数列表是:{" + Arrays.asList(args) + "}");
    }

}

2.2、AOP 配置类 MyAopConfig

@EnableAspectJAutoProxy
@Configuration
public class MyAopConfig {

    @Bean
    public LogAspects logAspects() {
        return new LogAspects();
    }

}

2.3、请求结果

2.3.1、public 修饰方法

java中service注入失败 service注入失败空指针_修饰符_04


java中service注入失败 service注入失败空指针_java_05

2.3.2、protected 修饰方法

java中service注入失败 service注入失败空指针_java_06


java中service注入失败 service注入失败空指针_spring aop_07

2.3.3、private 修饰方法

java中service注入失败 service注入失败空指针_java中service注入失败_08


查看控制台报错(空指针异常):

java中service注入失败 service注入失败空指针_java_09

2.3.4、private 修饰方法,使用 getBean 方法获取

@GetMapping("/demo/private")
private String demo3() {
    ITestDemoService iTestDemoService = SpringUtil.getBean(ITestDemoService.class);
    return iTestDemoService.demo();
}

请求成功:

java中service注入失败 service注入失败空指针_java_10


但是控制台没有打印切面方法。

问题分析

在之前学习 Spring AOP 的源码时,有提及到:在容器初始化时,有一个很重要方法:AbstractApplicationContext#refresh,里面有一步是需要实例化所有剩余的单实例 Bean。

java中service注入失败 service注入失败空指针_修饰符_11


从这里面深入,需要经过创建 Bean 实例,创建后置处理器等操作,然后再创建代理对象实例:

java中service注入失败 service注入失败空指针_spring aop_12


直到 Enhancer 类:

java中service注入失败 service注入失败空指针_spring aop_13

类方法的过滤

org.springframework.cglib.proxy.Enhancer#getMethods

java中service注入失败 service注入失败空指针_java_14


org.springframework.cglib.core.CollectionUtils#filter

java中service注入失败 service注入失败空指针_spring aop_15


org.springframework.cglib.core.VisibilityPredicate#evaluate

java中service注入失败 service注入失败空指针_java中service注入失败_16


在这里可以看到 private 修饰的方法被移除了,没有生成代理对象,因此调用方法时(实际上是代理对象调用方法)会报空指针异常。