2

【基础系列】SpringContext.getBean()方法调用导致NPE?

 2 years ago
source link: https://spring.hhui.top/spring-blog/2021/11/18/211118-SpringBoot%E7%B3%BB%E5%88%97%E4%B9%8BBean%E6%96%B9%E6%B3%95%E8%B0%83%E7%94%A8%E5%AF%BC%E8%87%B4NPE/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
logo.jpg

【基础系列】SpringContext.getBean()方法调用导致NPE?

在实际的业务开发中,为了方便获取Spring容器中的Bean对象,一个常见的case就是创建一个SpringUtil类,内部持有SpringContext上下文,然后提供一个静态的方式获取bean对象,然而这种使用姿势,一个不小心可能导致npe

今天我们来看一下这个场景

1. 基础工程搭建

搭建一个基础的SpringBoot项目,具体的过程这里省略,下面标注关键的信息

本项目借助SpringBoot 2.2.1.RELEASE + maven 3.5.3 + IDEA进行开发

开一个web服务用于测试

<dependencies>
<!-- 邮件发送的核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

2. SpringUtil

构建一个基础的SpringUtil工具类,借助SpringContextAware来持有上下文

@Component
public class SpringUtil implements ApplicationContextAware, EnvironmentAware {
private static ApplicationContext applicationContext;
private static Environment environment;

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringUtil.applicationContext = applicationContext;
}

@Override
public void setEnvironment(Environment environment) {
SpringUtil.environment = environment;
}

public static <T> T getBean(Class<T> clz) {
return applicationContext.getBean(clz);
}

public static String getProperty(String key) {
return environment.getProperty(key);
}
}

3. 使用实例

首先构建一个简单的bean对象

@Component
public class TestDemo {
public String showCase() {
return UUID.randomUUID().toString();
}

public String testCase() {
return "test-" + Math.random();
}
}

接着是另外一个对象,依赖上面这个对象,对外提供的主要接口是 process,其内部实现是根据枚举类,来做的一个策略选择;

@Component
public class BasicDemo {
@Autowired
private TestDemo testDemo;

public String process(String data) {
return Data.process(data);
}

private String show() {
return testDemo.showCase();
}

String test() {
return testDemo.testCase();
}

public enum Data {
SHOW("show") {
@Override
String doProcess() {
return SpringUtil.getBean(BasicDemo.class).show();
}
},
CASE("test") {
@Override
String doProcess() {
return SpringUtil.getBean(BasicDemo.class).test();
}
};

private String data;

Data(String data) {
this.data = data;
}

abstract String doProcess();

static String process(String data) {
for (Data d: values()) {
if (d.data.equalsIgnoreCase(data)) {
return d.doProcess();
}
}
return null;
}
}
}

重点关注上面实现中的枚举类,在枚举类中,根据SpringUtil获取到BasicDemo对象,然后执行它的私有方法show()及包内方法test()

这种用法会有什么问题么?

4. 测试case

接下来写个简单接口测试一下

@Aspect
@RestController
@SpringBootApplication
public class Application implements WebMvcConfigurer {
public static void main(String[] args) {
SpringApplication.run(Application.class);
}

@Autowired
private BasicDemo basicDemo;

@GetMapping(path = "show")
public String show(String data) {
return basicDemo.process(data);
}
}

接下来访问看看会是怎样

what? 不是说会npe么?这不是很正常的返回了么!!!

接下来就是见证bug的时刻了,同样是上面的代码,就让它出现npe

5. bug复现

接下来我们添加一个切面,目的就是让通过SpringUtil.getBean获取到的对象是代理类

// 注意在这个方法所在类上,添加注解 @Aspect
@Around("execution(public * com.git.hui.boot.web.interceptor.server.BasicDemo.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
return joinPoint.proceed();
}

然后再重新请求一下上面的访问

在访问私有方法 show()这里抛了异常,从服务端的堆栈可以看到异常类型为NPE,主要原因就是 testDemo 为null

简单来讲就是访问代理类的私有方法时,内部若有注入bean对象,这个时候拿到的是null

这个就有点神奇了,那么我们再变一下,私有方法内部不直接使用注入的bean对象,改调用一个bean对象的共有方法,会怎样

将上面的show()方法重写一下

private String show() {
return show2();
}

public String show2() {
return testDemo.showCase();
}

再次测试,输出如

居然没有问题!!!

就这么神奇有木有,那么是什么原因呢?

  • 关键知识点:Spring代理类的生成逻辑

好像刚进入主体,结果到这里就结束了,真是过分😡,这里先小结一下这个问题出现的场景,至于具体原因有待下片博文介绍

当我们通过SpringContext获取到的bean对象时,不要直接访问它的私有方法,可能导致npe

100%必先的场景

  • 这个bean对象有代理类(如有切面拦截了它,如类内部有一些特定注解)
  • 私有方法内使用了注入对象

看到上面就会有个疑问,谁会去访问私有方法呢?我脑子又没坑😒,何况私有方法在外面也访问不了啊

这就涉及到一个相当常见的场景了,类内部方法A调用希望切面拦截的方法B,这时我们常这么做

public class A {
@Autowired
private A a;

public void test() {
a.testB();
}

@Point
public String testB() {
return "hello";
}
}

上面的test方法,访问testB方法就可以走切面逻辑,在上面这个类中,就有可能出现直接是用a.privetMethod()的场景了

此外就是反射执行某些逻辑的时候也有可能出现访问私有方法了,这里就不展开了;

欢迎有兴趣的小伙伴回复互动一下,也可以关注我的公众号:一灰灰blog

III. 不能错过的源码和相关知识点

1. 微信公众号: 一灰灰Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

一灰灰blog

打赏 如果觉得我的文章对您有帮助,请随意打赏。

分享到


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK