想用Autowired注入static静态成员官方不推荐你却还偏要这么做

61次阅读

共计 10511 个字符,预计需要花费 27 分钟才能阅读完成。

生命太短暂,不要去做一些基本没有人想要的货色。本文已被 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 并不举荐你去这么做,因而应用起来务必当心~


关注 A 哥

Author A 哥(YourBatman)
集体站点 www.yourbatman.cn
E-mail yourbatman@qq.com
微 信 fsx641385712
沉闷平台
公众号 BAT 的乌托邦(ID:BAT-utopia)
常识星球 BAT 的乌托邦
每日文章举荐 每日文章举荐

正文完
 0