使用Spring实现@Value注入静态字段

 更新时间:2024年05月24日 15:25:16   作者:NaTook  
这篇文章主要介绍了使用Spring实现@Value注入静态字段方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教

1. 前言

在开发 spring 应用时,不可避免会有读取配置文件,注入到静态变量或者常量字段的场景。

我们最常用的是 @Value 注解,但是 @Value 不支持静态字段的注入。

本文搜索了常见的解决方案,发现或多或少都有一定的限制。

于是结合自己对 spring 的了解,增强 @Value 的功能,实现静态字段的直接注入。

代码实现没有经过严格测试,有问题请批评指正。

2. 注入静态变量常规方案

2.1. @Value 标记 set 方法

示例代码如下:

  • 类必须是 spring bean
  • @Value 标记在 set 方法上,方法名没有要求
@Component
public class TestConfig {

    private static String name;

    @Value("${test.name}")
    public void inject(String s) {
        name = s;
    }
}

2.2. @ConfigurationProperties 结合 set 方法

示例代码如下:

  • 类必须是 spring bean
  • set 方法名必须符合 spring 命名规范
@ConfigurationProperties(prefix = "test")
@Component
public class TestConfig {

    private static String name;

    public void setName(String n) {
        name = n;
    }
}

2.3. @Value 结合 @PostConstruct 间接注入

示例代码如下:

@Component
public class TestConfig {

    @Value("${test.name}")
    private String name;
    private static String staticName;

    @PostConstruct
    public void init() {
        staticName = name;
        System.out.println("staticName = " + staticName);
    }
}

3. 扩展 @Value 实现静态字段的注入

前面几种常规方案,都不太方便,而且有一定的限制。

所以笔者考虑扩展 @Value 注解,实现以下功能:

  • 可以注入静态字段,包括变量和常量
  • 所在类不一定是 spring bean,没有限制
  • 仍然支持 spel 表达式
  • 作用的类范围是指定包及其子包下的所有类

比如可以直接这么使用

public class Foo {

    @Value("${test.string}")
    private static String s;
}

接下来介绍如何实现

3.1. 自定义标识注解

首先自定义一个注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface InjectStaticField {
}

这个注解没有实际功能,只是给需要注入静态字段的类加上标识,使用示例如下:

@InjectStaticField
public class Foo {

    @Value("${test.string}")
    private static String s;
}

为什么要声明一个没有实际功能的注解呢?这里简单解释一下。

要实现 @Value 注入静态变量,不可避免要加载 class,然后遍历所有静态字段

这种方式会导致项目在初始化时,就把所有的 class 都加载一遍,不管你实际有没有用到。

加载类的时候,静态代码段、静态属性初始化都会被执行,这可能会导致意料不到的后果。

但这还是存在一样的问题,判断一个类是否标识该自定义注解,还是得加载这个类再进行判断,这不是多此一举吗?

这里有个小细节,Spring 提供了一种机制,可以在不加载类的前提下,读取类的元信息,包括注解信息。

所以我们可以利用这个机制,避免加载不必要的类

3.2. 在 Spring 应用启动时实现注入

这里通过实现 SpringApplicationRunListener 接口

在 Spring 应用启动时实现注入静态变量的逻辑

@Slf4j
public class RunListener implements SpringApplicationRunListener {

    private static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";

    /**
     * 根据 SpringApplicationRunListener 规范,必须要定义以下构造方法
     */
    public RunListener(SpringApplication application, String[] args) {

    }

    @Override
    public void contextPrepared(ConfigurableApplicationContext context) {
        initInjectStaticField(context);
    }

    /**
     * 注入静态字段
     */
    private static void initInjectStaticField(ConfigurableApplicationContext context) {
        ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();

        BeanExpressionResolver expressionResolver = beanFactory.getBeanExpressionResolver();
        if (expressionResolver == null) {
            expressionResolver = new StandardBeanExpressionResolver(beanFactory.getBeanClassLoader());
        }

        ConfigurableEnvironment env = context.getEnvironment();

        TypeConverter converter = beanFactory.getTypeConverter();
        CachingMetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(context);

        // 获取启动类所在包路径
        String packagePath = ClassUtils.classPackageAsResourcePath(App.class);
        // 配置扫描包 pattern
        String searchPattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
                packagePath + '/' + DEFAULT_RESOURCE_PATTERN;

        try {
            Field modifiersField = Field.class.getDeclaredField("modifiers");
            modifiersField.setAccessible(true);

            // 扫描包
            Resource[] resources = context.getResources(searchPattern);
            for (Resource resource : resources) {
                // 获取类的元信息,这里不会触发类的加载
                MetadataReader reader = readerFactory.getMetadataReader(resource);
                AnnotationMetadata metadata = reader.getAnnotationMetadata();

                // 只有声明指定注解的类,才支持静态注入
                if (!metadata.isAnnotated(InjectStaticField.class.getName())) {
                    continue;
                }

                // 加载类
                Class<?> clazz = Class.forName(metadata.getClassName());
                for (Field field : clazz.getDeclaredFields()) {
                    // 只注入静态字段
                    if (!Modifier.isStatic(field.getModifiers())) {
                        continue;
                    }

                    // 获取 Value 注解
                    Value anno = field.getDeclaredAnnotation(Value.class);
                    if (anno == null) {
                        continue;
                    }

                    // 读取配置文件数据
                    String strValue = env.resolveRequiredPlaceholders(anno.value());
                    // 解析 spel
                    Object value = expressionResolver.evaluate(strValue,
                            new BeanExpressionContext(beanFactory, null));

                    Class<?> type = field.getType();
                    TypeDescriptor descriptor = new TypeDescriptor(field);
                    // 类型转换
                    Object result = converter.convertIfNecessary(value, type, descriptor);

                    // 确保 private 和 final 修饰的静态字段也可以注入
                    field.setAccessible(true);
                    if (Modifier.isFinal(field.getModifiers())) {
                        modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
                    }
                    // 注入值
                    field.set(null, result);
                }
            }
        } catch (Exception ex) {
            log.error("Inject static field failed", ex);
            throw new RuntimeException(ex);
        }
        log.info("Inject static field done");
    }
}

要记得在 META-INF/spring.factories 声明 SpringApplicationRunListener 的实现类,否则不会生效

org.springframework.boot.SpringApplicationRunListener=demo.spring.listener.RunListener

3.3. 测试

要注入的类

@InjectStaticField
public class Foo {

    @Value("${test.string}")
    public static String string;
    @Value("${test.int:100}")
    public static Integer i;
    @Value("#{${test.map}}")
    public static final Map<Object, Object> MAP = null;
}

application.properties

test.name=jack
test.map={key1: 'value1', key2: 'value2'}

测试打印

@SpringBootApplication
public class App {

    @PostConstruct
    public void init() {
        System.out.println("Foo.string = " + Foo.string);
        System.out.println("Foo.i = " + Foo.i);
        System.out.println("Foo.MAP = " + Foo.MAP);
    }

    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • 解决Maven项目pom.xml导入了Junit包还是用不了@Test注解问题

    解决Maven项目pom.xml导入了Junit包还是用不了@Test注解问题

    在Maven项目中,如果在非test目录下使用@Test注解,可能会因为pom.xml中<scope>test</scope>的设置而无法使用,正确做法是将测试代码放在src/test/java目录下,或去除<scope>test</scope>限制,这样可以确保Junit依赖正确加载并应用于适当的代码部分
    2024-10-10
  • SpringBoot解决跨域问题的五种方案

    SpringBoot解决跨域问题的五种方案

    跨域问题指的是不同站点之间,使用 ajax 无法相互调用的问题,跨域问题本质是浏览器的一种保护机制,它的初衷是为了保证用户的安全,防止恶意网站窃取数据,那怎么解决这个问题呢?接下来我们一起来看,需要的朋友可以参考下
    2024-07-07
  • 一文详解如何排查定位Java中的死锁

    一文详解如何排查定位Java中的死锁

    在当今数字化时代,微服务架构凭借其高可扩展性、灵活性和易于维护等优势,成为了众多企业构建大型应用系统的首选架构模式,当我们将微服务部署在 Linux 服务器上时,有时会遭遇令人头疼的死锁问题,本位给大家介绍了如何排查定位Java中的死锁,需要的朋友可以参考下
    2025-02-02
  • 解决遇到Cannot resolve ch.qos.logback:logback-classic:1.2.3错误的问题

    解决遇到Cannot resolve ch.qos.logback:logback-classic:

    当使用Maven配置项目依赖时,可能会遇到无法解析特定版本的错误,例如,logback-classic版本1.2.3可能无法在配置的仓库中找到,解决方法包括检查仓库是否包含所需版本,或更新到其他可用版本,可通过Maven官网搜索并找到适用的版本,替换依赖配置中的版本信息
    2024-09-09
  • java编程实现邮件定时发送的方法

    java编程实现邮件定时发送的方法

    这篇文章主要介绍了java编程实现邮件定时发送的方法,涉及Java基于定时器实现计划任务的相关技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-11-11
  • SpringCloud OpenFeign超时控制示例详解

    SpringCloud OpenFeign超时控制示例详解

    在Spring Cloud中使用OpenFeign时,可以通过配置来控制请求的超时时间,这篇文章主要介绍了SpringCloud OpenFeign超时控制,需要的朋友可以参考下
    2024-05-05
  • 基于 SpringBoot 实现 MySQL 读写分离的问题

    基于 SpringBoot 实现 MySQL 读写分离的问题

    这篇文章主要介绍了基于 SpringBoot 实现 MySQL 读写分离的问题,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-02-02
  • Spring基于xml实现自动装配流程详解

    Spring基于xml实现自动装配流程详解

    自动装配是使用spring满足bean依赖的一种方法,spring会在应用上下文中为某个bean寻找其依赖的bean,Spring中bean有三种装配机制,分别是:在xml中显式配置、在java中显式配置、隐式的bean发现机制和自动装配
    2023-01-01
  • Druid简单实现数据库的增删改查方式

    Druid简单实现数据库的增删改查方式

    这篇文章主要介绍了Druid简单实现数据库的增删改查方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-07-07
  • java实现微信扫码支付功能

    java实现微信扫码支付功能

    这篇文章主要为大家详细介绍了java实现微信扫码支付功能,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-07-07

最新评论