生命太短暂,不要去做一些根本没有人想要的东西。本文已被 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. cb02a_c++_数据结构_顺序容器_STL_list类_双向链表

    /*cb02a_c++_数据结构_顺序容器_STL_list类_双向链表实例化std::list对象在list开头插入元素在list末尾插入元素在list中间插入元素,插入时间恒定,非常快.数组:中间 ...

  2. 手把手教你使用Python抓取QQ音乐数据(第一弹)

    [一.项目目标] 获取 QQ 音乐指定歌手单曲排行指定页数的歌曲的歌名.专辑名.播放链接. 由浅入深,层层递进,非常适合刚入门的同学练手. [二.需要的库] 主要涉及的库有:requests.json ...

  3. 第一章:开始启程-你的第一行Android代码

    Android 系统为开发者提供了什么? 四大组件 活动(Activity):界面 服务(Service):后台默默运行 广播接收器(Broadcast Receiver):接收.发送广播消息 内容提 ...

  4. mac Pycharm 导入jieba报错解决

    背景 新买的mac 用的还不是太熟,在用jieba做分词的时候,pycharm import说是没有安装,那我就直接点击安装呗,结果失败,说是当前渠道不能识别,ok,那我就开始一步一步来解决了. 正文 ...

  5. vue全家桶(1)

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

  6. RocketMQ入门到入土(二)事务消息&顺序消息

    接上一篇:RocketMQ入门到入土(一)新手也能看懂的原理和实战! 一.事务消息的由来 1.案例 引用官方的购物案例: 小明购买一个100元的东西,账户扣款100元的同时需要保证在下游的积分系统给小 ...

  7. 一文读懂 Redis 分布式部署方案

    为什么要分布式 Redis是一款开源的基于内存的K-V型数据库,因为内存访问速度快,一般被用来做系统的缓存. Redis作为单机部署能够支持业务简单,数据量不大的系统需求,但在实际应用中,一旦系统规模 ...

  8. 第二部分用户交互程序开发,通过paramiko记录ssh会话记录

    需求及任务:实现一个给用户登录的界面(通过ssh登到堡垒机上,然后给它展现一个命令行的页面,然后他选择登哪台机器,一选择就连上去且把日志也记录下来). 先在admin创建几条组数据并与用户关联如下图: ...

  9. C#实现快速查找(递归,非递归)

    原文件: http://pan.baidu.com/share/link?shareid=2838344856&uk=3912660076 我英语很烂...哎,我正在努力... 效果图:

  10. Spring中AOP相关的API及源码解析

    Spring中AOP相关的API及源码解析 本系列文章: 读源码,我们可以从第一行读起 你知道Spring是怎么解析配置类的吗? 配置类为什么要添加@Configuration注解? 谈谈Spring ...