生命太短暂,不要去做一些基本没有人想要的货色。本文已被 https://www.yourbatman.cn 收录,外面一并有Spring技术栈、MyBatis、JVM、中间件等小而美的专栏供以收费学习。关注公众号【BAT的乌托邦】一一击破,深刻把握,回绝浅尝辄止。
前言
各位小伙伴大家好,我是A哥。通过本专栏前两篇的学习,置信你对static关键字在Spring/Spring Boot里的利用有了全新的意识,可能解释工作中遇到的大多数问题/疑难了。本文持续来聊聊static关键字更为常见的一种case:应用@Autowired
依赖注入动态成员(属性)。
在Java中,针对static动态成员,咱们有一些最根本的常识:动态变量(成员)它是属于类的,而非属于实例对象的属性;同样的静态方法也是属于类的,一般办法(实例办法)才属于对象。而Spring容器治理的都是实例对象,包含它的@Autowired
依赖注入的均是容器内的对象实例,所以对于static成员是不能间接应用@Autowired
注入的。
这很容易了解:类成员的初始化较早,并不需要依赖实例的创立,所以这个时候Spring容器可能都还没“出世”,谈何依赖注入呢?
这个示例,你或者似曾相识:
@Componentpublic class SonHolder { @Autowired private static Son son; public static Son getSon() { return son; }}
而后“失常应用”这个组件:
@Autowiredprivate SonHolder sonHolder;@Transactionpublic 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)@Documentedpublic @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 */@Componentpublic 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 */@Componentpublic 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 */@Servicepublic 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业务调用:
@ComponentScanpublic 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
它被命名为一个工具类,而个别咱们对工具类的了解是:
- 办法均为static工具办法
应用越便捷越好
- 很显著,static办法应用是最便捷的嘛
现状是:应用UserHelper
去解决用户信息还得先@Autowired
注入它的实例,实属不便。因而你千方百计的想把getAndFilterTest()
这个办法变为静态方法,这样通过类名便可间接调用而并不再依赖于注入UserHelper实例了,so你想当然的这么“优化”:
@Componentpublic class UserHelper { @Autowired static UCClient ucClient; public static List<User> getAndFilterTest(List<Long> userIds) { ... // 解决逻辑齐全同上 }}
属性和办法都增加上static润饰,这样应用方通过类名便可间接拜访(无需注入):
@Servicepublic 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.ucClient07: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:@Overridepublic void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) { InjectionMetadata metadata = findAutowiringMetadata(beanName, beanType, null); metadata.checkConfigMembers(beanDefinition);}
调用处二:在InstantiationAwareBeanPostProcessor
也就是实例创立好后,给属性赋值阶段(也就是populateBean()
阶段)执行。所以它也是会作用于每个bd的,上例中2句info日志的第二句就是从这输入的
AutowiredAnnotationBeanPostProcessor:@Overridepublic 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动态成员的赋值
@Componentpublic class UserHelper { static UCClient ucClient; @Autowired public void setUcClient(UCClient ucClient) { UserHelper.ucClient = ucClient; }}
形式二:应用@PostConstruct
注解,在外面为static动态成员赋值
@Componentpublic 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)的实现形式供以你学习和参考:
@Componentpublic 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.ucClientException 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日志也不会输入喽)。这么解决的益处我感觉有如下三点:
- 手动治理这种case的依赖注入,更可控。而非交给Spring容器去主动解决
- 工具类自身并不需要退出到Spring容器内,这对于有大量这种case的话,是能够节约开销的
- 略显高级,装x神器(可别小看装x,这是个中意词,你的加薪往往来来自于装x胜利)
当然,你也能够这么玩:
@Componentpublic class AutowireStaticSmartInitializingSingleton implements SmartInitializingSingleton { @Autowired private AutowiredAnnotationBeanPostProcessor autowiredAnnotationBeanPostProcessor; @Override public void afterSingletonsInstantiated() { autowiredAnnotationBeanPostProcessor.processInjection(new UserHelper()); }}
仍旧能够失常work。这不正是下面介绍的调用处三麽,马上就学以致用了有木有,开心吧????。
应用倡议
有这种应用需要的小伙伴须要清晰什么才叫真正的util工具类?若你的工具类存在内部依赖,依赖于Spring容器内的实例,那么它就称不上是工具类,就请不要把它当做static来用,容易玩坏的。你当初可能这么用恰好是得益于Spring治理的实例默认都是单例,所以你赋值一次即可,假使某天真变成多例了呢(即便可能性极小)?
强行这么撸,是有隐患的。同时也突破了优先级关系、生命周期关系,容易让“初学者”感到迷糊。当然若你保持这么应用也未尝不可,那么请做好相干标准/归约,比方应用下面我举荐的高(zhuang)级(bi)应用形式是一种较好的抉择,这个时候手动治理往往比主动来得更平安,升高前期可能的保护老本。
思考题
- 在解析类的
@Autowired
注入元数据的时候,Spring工厂/容器明明曾经筹备好了,实践上曾经齐全具备帮你实现注入/赋值的能力,既然这样,为何Spring还偏要“回绝”这么干呢?可间接注入static成员不香吗? - 既然
@Autowired
不能注入static属性,那么static办法呢?@Value注解呢?
总结
本文介绍了Spring依赖注入和static的关系,从应用背景到起因剖析都做了相应的论述,A哥感觉还是蛮香的,对你帮忙应该不小吧。
最初,我想对小伙伴说:依赖注入的次要目标,是让容器去产生一个对象的实例而后治理它的生命周期,而后在生命周期中应用他们,这会让单元测试工作更加容易(什么?不写单元测试,那你应该关注我喽,下下下个专栏会专门讲单元测试)。而如果你应用动态变量/类变量就扩充了应用范畴,使得不可控了。这种static field是隐含共享的,并且是一种global全局状态,Spring并不举荐你去这么做,因而应用起来务必当心~
关注A哥
Author | A哥(YourBatman) |
---|---|
集体站点 | www.yourbatman.cn |
yourbatman@qq.com | |
微 信 | fsx641385712 |
沉闷平台 | |
公众号 | BAT的乌托邦(ID:BAT-utopia) |
常识星球 | BAT的乌托邦 |
每日文章举荐 | 每日文章举荐 |