网站首页 > 技术文章 正文
在实际的业务开发中,为了方便获取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代理类的生成逻辑
6. 小结
好像刚进入主体,结果到这里就结束了,真是过分,这里先小结一下这个问题出现的场景,至于具体原因有待下片博文介绍
当我们通过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()的场景了
此外就是反射执行某些逻辑的时候也有可能出现访问私有方法了,比如当前bean注入了A,结果我现在想访问A的私有方法,通过反射的方式来访问,是否也会有一样的问题呢?
欢迎有兴趣的小伙伴回复互动一下,也可以关注我的公众号:一灰灰blog
III. 不能错过的源码和相关知识点
0. 项目
- 工程:https://github.com/liuyueyi/spring-boot-demo
- 源码:https://github.com/liuyueyi/spring-boot-demo/tree/master/spring-boot/
1. 微信公众号: 一灰灰Blog
尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激
下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
- 一灰灰Blog个人博客 https://blog.hhui.top
- 一灰灰Blog-Spring专题博客 http://spring.hhui.top
猜你喜欢
- 2024-10-13 谈谈springboot 获取前端json数据几种方法
- 2024-10-13 在Spring Boot中如何获取到Request对象?
- 2024-10-13 SpringBoot:如何优雅地进行响应数据封装、异常处理
- 2024-10-13 SpringBoot实现接口防抖的几种方案,杜绝重复提交
- 2024-10-13 @PostMapping @GetMapping注解 postmapping注解接收参数
- 2024-10-13 如何在SpringBoot中动态过滤JSON响应正文
- 2024-10-13 WebSocket 集群解决方案 websocket500
- 2024-10-13 SpringBoot跨系统调用接口方案 springboot跨越设置
- 2024-10-13 SpringBoot如何优雅的进行参数校验(一)
- 2024-10-13 IntelliJ IDEA必装插件以及SpringBoot使用小技巧合集
你 发表评论:
欢迎- 最近发表
- 标签列表
-
- oraclesql优化 (66)
- 类的加载机制 (75)
- feignclient (62)
- 一致性hash算法 (71)
- dockfile (66)
- 锁机制 (57)
- javaresponse (60)
- 查看hive版本 (59)
- phpworkerman (57)
- spark算子 (58)
- vue双向绑定的原理 (68)
- springbootget请求 (58)
- docker网络三种模式 (67)
- spring控制反转 (71)
- data:image/jpeg (69)
- base64 (69)
- java分页 (64)
- kibanadocker (60)
- qabstracttablemodel (62)
- java生成pdf文件 (69)
- deletelater (62)
- com.aspose.words (58)
- android.mk (62)
- qopengl (73)
- epoch_millis (61)
本文暂时没有评论,来添加一个吧(●'◡'●)