生命太短暂,不要去做一些根本没有人想要的东西。本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈、MyBatis、JVM、中间件等小而美的专栏供以免费学习。关注公众号【BAT的乌托邦】逐个击破,深入掌握,拒绝浅尝辄止。

前言

各位小伙伴大家好,我是A哥。通过本专栏前两篇的学习,相信你对static关键字在Spring/Spring Boot里的应用有了全新的认识,能够解释工作中遇到的大多数问题/疑问了。本文继续来聊聊static关键字更为常见的一种case:使用@Autowired依赖注入静态成员(属性)。

在Java中,针对static静态成员,我们有一些最基本的常识:静态变量(成员)它是属于类的,而非属于实例对象的属性;同样的静态方法也是属于类的,普通方法(实例方法)才属于对象。而Spring容器管理的都是实例对象,包括它的@Autowired依赖注入的均是容器内的对象实例,所以对于static成员是不能直接使用@Autowired注入的。

这很容易理解:类成员的初始化较早,并不需要依赖实例的创建,所以这个时候Spring容器可能都还没“出生”,谈何依赖注入呢?

这个示例,你或许似曾相识:

@Component
public class SonHolder { @Autowired
private static Son son; public static Son getSon() {
return son;
}
}

然后“正常使用”这个组件:

@Autowired
private SonHolder sonHolder; @Transaction
public void method1(){
...
sonHolder.getSon().toString();
}

运行程序,结果抛错:

Exception in thread "main" java.lang.NullPointerException
...

很明显,getSon()得到的是一个null,所以给你扔了个NPE。


版本约定

本文内容若没做特殊说明,均基于以下版本:

  • JDK:1.8
  • Spring Framework:5.2.2.RELEASE

正文

说起@Autowired注解的作用,没有人不熟悉,自动装配嘛。根据此注解的定义,它似乎能使用在很多地方:

@Target({ElementType.CONSTRUCTOR, ElementType.METHOD,
ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
boolean required() default true;
}

本文我们重点关注它使用在FIELD成员属性上的case,标注在static静态属性上是本文讨论的中心。

说明:虽然Spring官方现在并不推荐字段/属性注入的方式,但它的便捷性仍无可取代,因此在做业务开发时它仍旧是主流的使用方式


场景描述

假如有这样一个场景需求:创建一个教室(Room),需要传入一批学生和一个老师,此时我需要对这些用户按照规则(如名字中含有test字样的示为测试帐号)进行数据合法性校验和过滤,然后才能正常走创建逻辑。此case还有以下特点:

  • 用户名字/详细信息,需要远程调用(如FeignClient方式)从UC中心获取

    • 因此很需要做桥接,提供防腐层
  • 该过滤规则功能性很强,工程内很多地方都有用到
    • 有点工具的意思有木有

阅读完“题目”感觉还是蛮简单的,很normal的一个业务需求case嘛,下面我来模拟一下它的实现。

从UC用户中心获取用户数据(使用本地数据模拟远程访问):

/**
* 模拟去远端用户中心,根据ids批量获取用户数据
*
* @author yourbatman
* @date 2020/6/5 7:16
*/
@Component
public class UCClient { /**
* 模拟远程调用的结果返回(有正常的,也有测试数据)
*/
public List<User> getByIds(List<Long> userIds) {
return userIds.stream().map(uId -> {
User user = new User();
user.setId(uId);
user.setName("YourBatman");
if (uId % 2 == 0) {
user.setName(user.getName() + "_test");
}
return user;
}).collect(Collectors.toList());
} }

说明:实际情况这里可能只是一个@FeignClient接口而已,本例就使用它mock喽

因为过滤测试用户的功能过于通用,并且规则也需要收口,须对它进行封装,因此有了我们的内部帮助类UserHelper

/**
* 工具方法:根据用户ids,按照一定的规则过滤掉测试用户后返回结果
*
* @author yourbatman
* @date 2020/6/5 7:43
*/
@Component
public class UserHelper { @Autowired
UCClient ucClient; public List<User> getAndFilterTest(List<Long> userIds) {
List<User> users = ucClient.getByIds(userIds);
return users.stream().filter(u -> {
Long id = u.getId();
String name = u.getName();
if (name.contains("test")) {
System.out.printf("id=%s name=%s是测试用户,已过滤\n", id, name);
return false;
}
return true;
}).collect(Collectors.toList());
} }

很明显,它内部需依赖于UCClient这个远程调用的结果。封装好后,我们的业务Service层任何组件就可以尽情的“享用”该工具啦,形如这样:

/**
* 业务服务:教室服务
*
* @author yourbatman
* @date 2020/6/5 7:29
*/
@Service
public class RoomService { @Autowired
UserHelper userHelper; public void create(List<Long> studentIds, Long teacherId) {
// 因为学生和老师统称为user 所以可以放在一起校验
List<Long> userIds = new ArrayList<>(studentIds);
userIds.add(teacherId);
List<User> users = userHelper.getAndFilterTest(userIds); // ... 排除掉测试数据后,执行创建逻辑
System.out.println("教室创建成功");
} }

书写个测试程序来模拟Service业务调用:

@ComponentScan
public class DemoTest { public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(DemoTest.class); // 模拟接口调用/单元测试
RoomService roomService = context.getBean(RoomService.class);
roomService.create(Arrays.asList(1L, 2L, 3L, 4L, 5L, 6L), 101L);
}
}

运行程序,结果输出:

id=2 name=YourBatman_test是测试用户,已过滤
id=4 name=YourBatman_test是测试用户,已过滤
id=6 name=YourBatman_test是测试用户,已过滤
教室创建成功

一切都这么美好,相安无事的,那为何还会有本文指出的问题存在呢?正所谓“不作死不会死”,总有那么一些“追求极致”的选手就喜欢玩花,下面姑且让我猜猜你为何想要依赖注入static成员属性呢?


帮你猜猜你为何有如此需求?

从上面示例类的命名中,我或许能猜出你的用意。UserHelper它被命名为一个工具类,而一般我们对工具类的理解是:

  1. 方法均为static工具方法
  2. 使用越便捷越好
    1. 很明显,static方法使用是最便捷的嘛

现状是:使用UserHelper去处理用户信息还得先@Autowired注入它的实例,实属不便。因此你想方设法的想把getAndFilterTest()这个方法变为静态方法,这样通过类名便可直接调用而并不再依赖于注入UserHelper实例了,so你想当然的这么“优化”:

@Component
public class UserHelper { @Autowired
static UCClient ucClient; public static List<User> getAndFilterTest(List<Long> userIds) {
... // 处理逻辑完全同上
}
}

属性和方法都添加上static修饰,这样使用方通过类名便可直接访问(无需注入):

@Service
public class RoomService { public void create(List<Long> studentIds, Long teacherId) {
...
// 通过类名直接调用其静态方法
List<User> users = UserHelper.getAndFilterTest(userIds);
...
}
}

运行程序,结果输出:

07:22:49.359 [main] INFO org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor - Autowired annotation is not supported on static fields: static cn.yourbatman.temp.component.UCClient cn.yourbatman.temp.component.UserHelper.ucClient
07:22:49.359 [main] INFO org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor - Autowired annotation is not supported on static fields: static cn.yourbatman.temp.component.UCClient cn.yourbatman.temp.component.UserHelper.ucClient
...
Exception in thread "main" java.lang.NullPointerException
at cn.yourbatman.temp.component.UserHelper.getAndFilterTest(UserHelper.java:23)
at cn.yourbatman.temp.component.RoomService.create(RoomService.java:26)
at cn.yourbatman.temp.DemoTest.main(DemoTest.java:19)

以为天衣无缝,可结果并不完美,抛异常了。我特意多粘贴了两句info日志,它们告诉了你为何抛出NPE异常的原因:@Autowired不支持标注在static字段/属性上


为什么@Autowired不能注入static成员属性

静态变量是属于类本身的信息,当类加载器加载静态变量时,Spring的上下文环境还没有被加载,所以不可能为静态变量绑定值(这只是最表象原因,并不准确)。同时,Spring也不鼓励为静态变量注入值(言外之意:并不是不能注入),因为它认为这会增加了耦合度,对测试不友好。

这些都是表象,那么实际上Spring是如何“操作”的呢?我们沿着AutowiredAnnotationBeanPostProcessor输出的这句info日志,倒着找原因,这句日志的输出在这:

AutowiredAnnotationBeanPostProcessor:

// 构建@Autowired注入元数据方法
// 简单的说就是找到该Class类下有哪些是需要做依赖注入的
private InjectionMetadata buildAutowiringMetadata(final Class<?> clazz) {
...
// 循环递归,因为父类的也要管上
do {
// 遍历所有的字段(包括静态字段)
ReflectionUtils.doWithLocalFields(targetClass, field -> {
if (Modifier.isStatic(field.getModifiers())) {
logger.info("Autowired annotation is not supported on static fields: " + field);
}
return;
...
});
// 遍历所有的方法(包括静态方法)
ReflectionUtils.doWithLocalMethods(targetClass, method -> {
if (Modifier.isStatic(method.getModifiers())) {
logger.info("Autowired annotation is not supported on static methods: " + method);
}
return;
...
});
...
targetClass = targetClass.getSuperclass();
} while (targetClass != null && targetClass != Object.class);
...
}

这几句代码道出了Spring为何不给static静态字段/静态方法执行@Autowired注入的最真实原因:扫描Class类需要注入的元数据的时候,直接选择忽略掉了static成员(包括属性和方法)。

那么这个处理的入口在哪儿呢?是否在这个阶段时Spring真的无法给static成员完成赋值而选择忽略掉它呢,我们继续最终此方法的调用处。此方法唯一调用处是findAutowiringMetadata()方法,而它被调用的地方有三个:

调用处一:执行时机较早,在MergedBeanDefinitionPostProcessor处理bd合并期间就会解析出需要注入的元数据,然后做check。它会作用于每个bd身上,所以上例中的2句info日志第一句就是从这输出的

AutowiredAnnotationBeanPostProcessor:

@Override
public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
InjectionMetadata metadata = findAutowiringMetadata(beanName, beanType, null);
metadata.checkConfigMembers(beanDefinition);
}

调用处二:在InstantiationAwareBeanPostProcessor也就是实例创建好后,给属性赋值阶段(也就是populateBean()阶段)执行。所以它也是会作用于每个bd的,上例中2句info日志的第二句就是从这输出的

AutowiredAnnotationBeanPostProcessor:

@Override
public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);
try {
metadata.inject(bean, beanName, pvs);
}
...
return pvs;
}

调用处三:这个方法比较特殊,它表示对于带有任意目标实例(已经不仅是Class,而是实例本身)直接调用的“本地”处理方法实行注入。这是Spring提供给“外部”使用/注入的一个public公共方法,比如给容器外的实例注入属性,还是比较实用的,本文下面会介绍它的使用办法

说明:此方法Spring自己并不会主动调用,所以不会自动输出日志(这也是为何调用处有3处,但日志只有2条的原因)

AutowiredAnnotationBeanPostProcessor:

public void processInjection(Object bean) throws BeanCreationException {
Class<?> clazz = bean.getClass();
InjectionMetadata metadata = findAutowiringMetadata(clazz.getName(), clazz, null);
try {
metadata.inject(bean, null, null);
}
...
}

通过这部分源码,从底层诠释了Spring为何不让你@Autowired注入static成员的原因。既然这样,难道就没有办法满足我的“诉求”了吗?答案是有的,接着往下看。


间接实现static成员注入的N种方式

虽然Spring会忽略掉你直接使用@Autowired + static成员注入,但还是有很多方法来绕过这些限制,实现对静态变量注入值。下面A哥介绍2种方式,供以参考:

方式一:以set方法作为跳板,在里面实现对static静态成员的赋值

@Component
public class UserHelper { static UCClient ucClient; @Autowired
public void setUcClient(UCClient ucClient) {
UserHelper.ucClient = ucClient;
}
}

方式二:使用@PostConstruct注解,在里面为static静态成员赋值

@Component
public class UserHelper { static UCClient ucClient; @Autowired
ApplicationContext applicationContext;
@PostConstruct
public void init() {
UserHelper.ucClient = applicationContext.getBean(UCClient.class);
}
}

虽然称作是2种方式,但其实我认为思想只是一个:延迟为static成员属性赋值。因此,基于此思想确切的说会有N种实现方案(只需要保证你在使用它之前给其赋值上即可),各位可自行思考,A哥就没必要一一举例了。


高级实现方式

作为福利,A哥在这里提供一种更为高(zhuang)级(bi)的实现方式供以你学习和参考:

@Component
public class AutowireStaticSmartInitializingSingleton implements SmartInitializingSingleton { @Autowired
private AutowireCapableBeanFactory beanFactory; /**
* 当所有的单例Bena初始化完成后,对static静态成员进行赋值
*/
@Override
public void afterSingletonsInstantiated() {
// 因为是给static静态属性赋值,因此这里new一个实例做注入是可行的
beanFactory.autowireBean(new UserHelper());
}
}

UserHelper类不再需要标注@Component注解,也就是说它不再需要被Spirng容器管理(static工具类确实不需要交给容器管理嘛,毕竟我们不需要用到它的实例),这从某种程度上也是节约开销的表现。

public class UserHelper {

    @Autowired
static UCClient ucClient;
...
}

运行程序,结果输出:

08:50:15.765 [main] INFO org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor - Autowired annotation is not supported on static fields: static cn.yourbatman.temp.component.UCClient cn.yourbatman.temp.component.UserHelper.ucClient
Exception in thread "main" java.lang.NullPointerException
at cn.yourbatman.temp.component.UserHelper.getAndFilterTest(UserHelper.java:26)
at cn.yourbatman.temp.component.RoomService.create(RoomService.java:26)
at cn.yourbatman.temp.DemoTest.main(DemoTest.java:19)

报错。当然喽,这是我故意的,虽然抛异常了,但是看到我们的进步了没:info日志只打印一句了(自行想想啥原因哈)。不卖关子了,正确的姿势还得这么写:

public class UserHelper {

    static UCClient ucClient;
@Autowired
public void setUcClient(UCClient ucClient) {
UserHelper.ucClient = ucClient;
}
}

再次运行程序,一切正常(info日志也不会输出喽)。这么处理的好处我觉得有如下三点:

  1. 手动管理这种case的依赖注入,更可控。而非交给Spring容器去自动处理
  2. 工具类本身并不需要加入到Spring容器内,这对于有大量这种case的话,是可以节约开销的
  3. 略显高级,装x神器(可别小看装x,这是个中意词,你的加薪往往来来自于装x成功)

当然,你也可以这么玩:

@Component
public class AutowireStaticSmartInitializingSingleton implements SmartInitializingSingleton { @Autowired
private AutowiredAnnotationBeanPostProcessor autowiredAnnotationBeanPostProcessor;
@Override
public void afterSingletonsInstantiated() {
autowiredAnnotationBeanPostProcessor.processInjection(new UserHelper());
}
}

依旧可以正常work。这不正是上面介绍的调用处三麽,马上就学以致用了有木有,开心吧。


使用建议

有这种使用需求的小伙伴需要明晰什么才叫真正的util工具类?若你的工具类存在外部依赖,依赖于Spring容器内的实例,那么它就称不上是工具类,就请不要把它当做static来用,容易玩坏的。你现在能够这么用恰好是得益于Spring管理的实例默认都是单例,所以你赋值一次即可,倘若某天真变成多例了呢(即使可能性极小)?

强行这么撸,是有隐患的。同时也打破了优先级关系、生命周期关系,容易让“初学者”感到迷糊。当然若你坚持这么使用也未尝不可,那么请做好相关规范/归约,比如使用上面我推荐的高(zhuang)级(bi)使用方式是一种较好的选择,这个时候手动管理往往比自动来得更安全,降低后期可能的维护成本。


思考题

  1. 在解析类的@Autowired注入元数据的时候,Spring工厂/容器明明已经准备好了,理论上已经完全具备帮你完成注入/赋值的能力,既然这样,为何Spring还偏要“拒绝”这么干呢?可直接注入static成员不香吗?
  2. 既然@Autowired不能注入static属性,那么static方法呢?@Value注解呢?

总结

本文介绍了Spring依赖注入和static的关系,从使用背景到原因分析都做了相应的阐述,A哥觉得还是蛮香的,对你帮助应该不小吧。

最后,我想对小伙伴说:依赖注入的主要目的,是让容器去产生一个对象的实例然后管理它的生命周期,然后在生命周期中使用他们,这会让单元测试工作更加容易(什么?不写单元测试,那你应该关注我喽,下下下个专栏会专门讲单元测试)。而如果你使用静态变量/类变量就扩大了使用范围,使得不可控了。这种static field是隐含共享的,并且是一种global全局状态,Spring并不推荐你去这么做,因此使用起来务必当心~

想用@Autowired注入static静态成员?官方不推荐你却还偏要这么做的更多相关文章

  1. Spring不能直接@autowired注入Static变量/ 关于SpringBoot的@Autowired 静态变量注入

    昨天在编写JavaMail工具类的时候,静态方法调用静态变量,这是很正常的操作,当时也没多想,直接静态注入. @Component public class JavaMailUtil { @Autow ...

  2. Spring不能直接@autowired注入Static变量

    一.业务场景 spring框架应用中有些静态方法需要依赖被容器管理的类,就像这样: @Component public class Test { @Autowired private static U ...

  3. static类型autowired 注入失败

    原代码:注入commonService对象失败 @Autowired private static CommonService commonService; public static List< ...

  4. Spring 注入static变量

    一般我们我想注入一个static的变量,如下: @Autowired    private static String str; 不过,这样最终结果为null. 1.使用配置文件的方式注入 priva ...

  5. Netty handler处理类无法使用@Autowired注入bean的解决方法

    问题由来: 公司有个项目用到netty作为websocket的实现,最近打算部署双机,这使得原来在内存中的保存Channel信息的方案不再可行,需要转移到redis中,改造过程中发现通过@Autowi ...

  6. Spring @Autowired 注入为 null

    原因 配置缺失,比如为开启注解扫描驱动.注入组件为注册: 使用 new 关键字创建的对象不受spring容器管理,无法注入: 注入静态变量, 静态变量/类变量不是对象的属性,而是一个类的属性,spri ...

  7. spring为什么不能注入static变量

    Spring 依赖注入 是依赖 set方法 set方法是 是普通的对象方法 static变量是类的属性 @Autowired private static JdbcTemplate jdbcTempl ...

  8. C++学习10 static静态成员变量和静态成员函数

    一般情况下,如果有N个同类的对象,那么每一个对象都分别有自己的成员变量,不同对象的成员变量各自有值,互不相干.但是有时我们希望有某一个或几个成员变量为所有对象共有,这样可以实现数据共享. 可以使用全局 ...

  9. Spring它不支持依赖注入static静态变量

    在springframework在,我们不能@Autowired静态变量,制作spring bean,例如,没有那么: @Autowired private static YourClass your ...

随机推荐

  1. CRC16冗余循环检测计算器-好用。modbus RTU

    开始使用 http://cht.nahua.com.tw/index.php?url=http://cht.nahua.com.tw/software/crc16/&key=Modbus,%2 ...

  2. 【uni-app】uni.request二次封装,更好的管理api接口和使用

    前言 之前写了一个Vue.js的axios二次封装(点击跳转),这次是uni-app,uni-app是基于vue.js框架的,我觉得是很好用的一个框架,而且一套代码编译那么多平台,非常节省成本,当然, ...

  3. v-on事件修饰符

    .stop阻止冒泡 .prevent阻止默认行为 .capture实现捕获触发事件的机制(从大到小) .self实现只有点击当前元素时,才能出发事件处理函数 .once只触发一次事件函数 .stop和 ...

  4. FreeSql.Generator命令行代码生成器是如何实现的

    目录 FreeSql介绍 FreeSql.Generator RazorEngine.NetCore 源码解析 FreeSql.Tools FreeSql FreeSql 是功能强大的对象关系映射技术 ...

  5. Django REST Framework(一) Serializer和ModelSerializer

    REST Framework概述 Django REST framework是一套基于Django的REST框架,是一个用于构建Web API的功能强大且灵活的工具包. 1.RESTful 简述Rep ...

  6. robot framework使用小结(三)

    robot framework采用行为驱动 新建测试案例baidu04,添加Library:Selenium2Library 右键项目名robotProject-->New Resource-- ...

  7. vue全家桶(1)

    1.环境搭建 1.1.脚手架搭建 1.1.1什么是脚手架 百度搜索一下脚手架长什么样子,它们是这样的: 从百度百科抄过来一段话: 脚手架是为了保证各施工过程顺利进行而搭设的工作平台.如果明白了脚手架在 ...

  8. 《UNIX环境高级编程》(APUE) 笔记第八章 - 进程控制

    8 - 进程控制 Github 地址 1. 进程标识 每个进程都有一个非负整型表示的 唯一进程 ID .进程 ID 是可复用的(延迟复用算法). ID 为 \(0\) 的进程通常是调度进程,常常被称为 ...

  9. 安装pymysql模块及使用

    安装pymysql模块: https://www.cnblogs.com/Eva-J/articles/9772614.html file--settings for New Projects---P ...

  10. Selenium WebDriver使用

    目录 介绍 selenium webdriver chromedriver下载安装 1.下载 2.使用 3.测试 WebDriver常用操作 1.浏览器操作 2.窗口和弹框操作 3.cookies 操 ...