【SpringBoot 教程】动态加载项目属性配置

 
愿你如阳光,明媚不忧伤。

 


1. 配置文件简介

配置文件(Configuration File)在我们编写Java程序时,有很多参数是会经常改变的。比如环境的配置,我们开发的时候是一套环境,测试使用的是另外一套环境,最后交付给用户的时候,用户用的又是另外一套环境,不能每次都重新编译,那样成本太高,所以对于这些参数往往不能直接写死在程序里,不然会非常麻烦,每次修改都需要重新运行一次程序,这时就需要借助配置文件来存储这些常变的参数,在程序中是读取配置文件中的变量的值,如果我们需要修改这些值,只需要修改配置文件即可。

本篇文章代码涉及日志记录,如有不明白请参考 → Java 日志记录 SLF4J

 


2. 少量配置信息的情形

举个例子,在微服务架构中,最常见的就是某个服务需要调用其他服务来获取其提供的相关信息,那么在该服务的配置文件中需要配置被调用的服务地址,比如在当前服务里,我们需要调用订单微服务获取订单相关的信息,假设订单服务的端口号是 8002,那我们可以做如下配置:

server:
  # 配置微服务的地址
  port: 8001
url:
  # 订单微服务的地址
  orderUrl: http://localhost:8002

然后在业务代码中如何获取到这个配置的订单服务地址呢?我们可以使用 @Value 注解来解决。在对应的类中加上一个属性,在属性上使用 @Value 注解即可获取到配置文件中的配置信息,如下:

  • ConfigController.java
@RestController
@RequestMapping("/config")
public class ConfigController extends BaseController {
    @Value("${url.orderUrl}")
    private String orderUrl;

    @RequestMapping("/less")
    public JsonResult<String> lessConfig() {
        log.info(startLog(Thread.currentThread().getStackTrace()[1].getMethodName()));
        log.info("获取的订单服务地址为:{}", orderUrl);
        log.info(endLog("lessConfig"));
        return new JsonResult<>(orderUrl, "获取订单服务地址!");
    }
}

浏览器访问 http://localhost:8001/config/less

在这里插入图片描述
控制台打印出订单服务的地址,我们成功获取到了配置文件中的数据,在实际项目中也是这么用的,后面如果因为服务器部署的原因,需要修改某个服务的地址,那么只要在配置文件中修改即可。

在这里插入图片描述

 


3. 多个配置信息的情形

随着业务复杂度的增加,一个项目中可能会有越来越多的微服务,某个模块可能
需要调用多个微服务获取不同的信息,那么就需要在配置文件中配置多个微服务的地址。可是,在需要调用这些微服务的代码中,如果这样一个个去使用 @Value 注解引入相应的微服务地址的话,太过于繁琐,也不科学。所以,在实际项目中,业务繁琐,逻辑复杂的情况下,需要考虑封装一个或多个配置类。

server:
  # 配置微服务的地址
  port: 8001
url:
  # 订单微服务的地址
  orderUrl: http://localhost:8002
  # 用户微服务的地址
  userUrl: http://localhost:8003
  # 购物车微服务的地址
  shoppingUrl: http://localhost:8004
  • MicroServiceUrl.java
    使用 @ConfigurationProperties 注解并且使用 prefix 来指定一个前缀,然后该类中的属性名就是配置中去掉前缀后的名字,一一对应即可。即:前缀名 + 属性名就是配置文件中定义的 key。同时,该类上面需要加上 @Component 注解,把该类作为组件放到Spring容器中,让 Spring 去管理,我们使用的时候直接注入即可。
@Component
@ConfigurationProperties(prefix = "url")
public class MicroServiceUrl {
    private String orderUrl;
    private String userUrl;
    private String shoppingUrl;
    // 省略get和set
}
  • pom.xml
    使用 @ConfigurationProperties 注解需要导入它的依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>
  • ConfigController.java
    使用@Resource 注解将刚刚写好配置类注入进来
@RestController
@RequestMapping("/config")
public class ConfigController extends BaseController {
    @Resource
    private MicroServiceUrl microServiceUrl;
    @Value("${url.orderUrl}")
    private String orderUrl;

    @RequestMapping("/less")
    public JsonResult<String> lessConfig() {
        log.info(startLog(Thread.currentThread().getStackTrace()[1].getMethodName()));
        log.info("获取的订单服务地址为:{}", orderUrl);
        log.info(endLog("lessConfig"));
        return new JsonResult<>(orderUrl);
    }

    @RequestMapping("/lot")
    public JsonResult<List<String>> lotConfig() {
        log.info(startLog(Thread.currentThread().getStackTrace()[1].getMethodName()));
        log.info("获取的订单服务地址为:{}", microServiceUrl.getOrderUrl());
        log.info("获取的用户服务地址为:{}", microServiceUrl.getUserUrl());
        log.info("获取的购物车服务地址为:{}", microServiceUrl.getShoppingUrl());
        List<String> list = new ArrayList<>();
        list.add(microServiceUrl.getOrderUrl());
        list.add(microServiceUrl.getUserUrl());
        list.add(microServiceUrl.getShoppingUrl());
        log.info(endLog("lotConfig"));
        return new JsonResult<>(list, orderUrl);
    }
}

浏览器访问 http://localhost:8001/config/lot

在这里插入图片描述

控制台打印出如下信息,说明配置文件生效,同时正确获取配置文件内容

在这里插入图片描述

3.1 指定项目配置文件

实际项目中,一般有两个环境:开发环境和生产环境。开发环境中的配置和生产环境中的配置往往不同,比如:环境、端口、数据库、相关地址等等。我们不可能在开发环境调试好之后,部署到生产环境后,又要将配置信息全部修改成生产环境上的配置,这样太麻烦,也不科学。
最好的解决方法就是开发环境和生产环境都有一套对应的配置信息,然后当我们在开发时,指定读取开发环境的配置,当我们将项目部署到服务器上之后,再指定去读取生产环境的配置。

  • application-dev.yml
# 开发环境配置文件
server:
  port: 80
  • application-pro.yml
# 生产环境配置文件
server:
  port: 443
  • application.yml
# 指定读取的配置文件
spring:
  profiles:
    active: 
      - dev

 


4. Spring 初始化 Bean

spring初始化bean有三种方式:

  • 方式一:使用@PostConstruct注解,被注解的方法,在对象加载完依赖注入后执行。
  • 方式二:实现InitializingBean接口,重写afterPropertiesSet方法。
  • 方式三:反射原理,通过xml配置<bean init-method="myInit" /bean>直接注入bean。
    执行顺序:Constructor > @Autowired > @PostConstruct > InitializingBean > init-method

4.1 @PostConstruct

  • AbstractAutowireCapableBeanFactory.java#initializeBean
    源码位置:package org.springframework.beans.factory.support;
    这里主要包含了初始化前置处理、调用初始化方法、初始化后置处理。
    protected Object initializeBean(String beanName, Object bean, @Nullable RootBeanDefinition mbd) {
        if (System.getSecurityManager() != null) {
            AccessController.doPrivileged(() -> {
                this.invokeAwareMethods(beanName, bean);
                return null;
            }, this.getAccessControlContext());
        } else {
            this.invokeAwareMethods(beanName, bean);
        }

        Object wrappedBean = bean;
        if (mbd == null || !mbd.isSynthetic()) {
        	// 初始化前置处理
            wrappedBean = this.applyBeanPostProcessorsBeforeInitialization(bean, beanName);
        }

        try {
        	// 调用初始化方法
            this.invokeInitMethods(beanName, wrappedBean, mbd);
        } catch (Throwable var6) {
            throw new BeanCreationException(mbd != null ? mbd.getResourceDescription() : null, beanName, "Invocation of init method failed", var6);
        }

        if (mbd == null || !mbd.isSynthetic()) {
        	// 初始化后置处理
            wrappedBean = this.applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
        }

        return wrappedBean;
    }
  • AbstractAutowireCapableBeanFactory.java#applyBeanPostProcessorsBeforeInitialization
    跟进 applyBeanPostProcessorsBeforeInitialization 方法
    @PostConstruct注解在applyBeanPostProcessorsBeforeInitialization这个前置处理中起作用
    public Object applyBeanPostProcessorsBeforeInitialization(Object existingBean, String beanName) throws BeansException {
        Object result = existingBean;

        Object current;
        for(Iterator var4 = this.getBeanPostProcessors().iterator(); var4.hasNext(); result = current) {
            BeanPostProcessor processor = (BeanPostProcessor)var4.next();
            current = processor.postProcessBeforeInitialization(result, beanName);
            if (current == null) {
                return result;
            }
        }

        return result;
    }
  • InitDestroyAnnotationBeanPostProcessor.java#postProcessBeforeInitialization
    源码位置:package org.springframework.beans.factory.annotation;
    findLifecycleMetadata方法会解析元数据,所以@PostConstruct注解的初始化方法在这里找到了。
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
    	// findLifecycleMetadata方法会解析元数据
        InitDestroyAnnotationBeanPostProcessor.LifecycleMetadata metadata = this.findLifecycleMetadata(bean.getClass());

        try {
            metadata.invokeInitMethods(bean, beanName);
            return bean;
        } catch (InvocationTargetException var5) {
            throw new BeanCreationException(beanName, "Invocation of init method failed", var5.getTargetException());
        } catch (Throwable var6) {
            throw new BeanCreationException(beanName, "Failed to invoke init method", var6);
        }
    }
  • InitDestroyAnnotationBeanPostProcessor.java#findLifecycleMetadata
    跟进 findLifecycleMetadata 方法
    private InitDestroyAnnotationBeanPostProcessor.LifecycleMetadata findLifecycleMetadata(Class<?> clazz) {
        if (this.lifecycleMetadataCache == null) {
            return this.buildLifecycleMetadata(clazz);
        } else {
            InitDestroyAnnotationBeanPostProcessor.LifecycleMetadata metadata = (InitDestroyAnnotationBeanPostProcessor.LifecycleMetadata)this.lifecycleMetadataCache.get(clazz);
            if (metadata == null) {
                synchronized(this.lifecycleMetadataCache) {
                    metadata = (InitDestroyAnnotationBeanPostProcessor.LifecycleMetadata)this.lifecycleMetadataCache.get(clazz);
                    if (metadata == null) {
                        metadata = this.buildLifecycleMetadata(clazz);
                        this.lifecycleMetadataCache.put(clazz, metadata);
                    }

                    return metadata;
                }
            } else {
                return metadata;
            }
        }
    }
  • InitDestroyAnnotationBeanPostProcessor.java#buildLifecycleMetadata
    跟进 buildLifecycleMetadata 方法
    可以看到有个判断是否被 initAnnotationType 注解,然后添加到集合中。
    private InitDestroyAnnotationBeanPostProcessor.LifecycleMetadata buildLifecycleMetadata(Class<?> clazz) {
        if (!AnnotationUtils.isCandidateClass(clazz, Arrays.asList(this.initAnnotationType, this.destroyAnnotationType))) {
            return this.emptyLifecycleMetadata;
        } else {
            List<InitDestroyAnnotationBeanPostProcessor.LifecycleElement> initMethods = new ArrayList();
            List<InitDestroyAnnotationBeanPostProcessor.LifecycleElement> destroyMethods = new ArrayList();
            Class targetClass = clazz;

            do {
                List<InitDestroyAnnotationBeanPostProcessor.LifecycleElement> currInitMethods = new ArrayList();
                List<InitDestroyAnnotationBeanPostProcessor.LifecycleElement> currDestroyMethods = new ArrayList();
                ReflectionUtils.doWithLocalMethods(targetClass, (method) -> {
                	// 判断是否被 initAnnotationType 注解
                    if (this.initAnnotationType != null && method.isAnnotationPresent(this.initAnnotationType)) {
                        InitDestroyAnnotationBeanPostProcessor.LifecycleElement element = new InitDestroyAnnotationBeanPostProcessor.LifecycleElement(method);
                        currInitMethods.add(element);
                        if (this.logger.isTraceEnabled()) {
                            this.logger.trace("Found init method on class [" + clazz.getName() + "]: " + method);
                        }
                    }

                    if (this.destroyAnnotationType != null && method.isAnnotationPresent(this.destroyAnnotationType)) {
                        currDestroyMethods.add(new InitDestroyAnnotationBeanPostProcessor.LifecycleElement(method));
                        if (this.logger.isTraceEnabled()) {
                            this.logger.trace("Found destroy method on class [" + clazz.getName() + "]: " + method);
                        }
                    }

                });
                initMethods.addAll(0, currInitMethods);
                destroyMethods.addAll(currDestroyMethods);
                targetClass = targetClass.getSuperclass();
            } while(targetClass != null && targetClass != Object.class);

            return initMethods.isEmpty() && destroyMethods.isEmpty() ? this.emptyLifecycleMetadata : new InitDestroyAnnotationBeanPostProcessor.LifecycleMetadata(clazz, initMethods, destroyMethods);
        }
    }
  • CommonAnnotationBeanPostProcessor.java
    initAnnotationType位于CommonAnnotationBeanPostProcessor类中
    public CommonAnnotationBeanPostProcessor() {
        this.setOrder(2147483644);
        this.setInitAnnotationType(PostConstruct.class);
        this.setDestroyAnnotationType(PreDestroy.class);
        this.ignoreResourceType("javax.xml.ws.WebServiceContext");
    }

了解更多 → Spring 框架中 @PostConstruct 注解详解

4.2 实现InitializingBean接口afterPropertiesSet方法

  • AbstractAutowireCapableBeanFactory.java#initializeBean
    源码位置:package org.springframework.beans.factory.support;
    这里主要包含了初始化前置处理、调用初始化方法、初始化后置处理。
    继续initializeBean方法,这次来看invokeInitMethods方法
    protected Object initializeBean(String beanName, Object bean, @Nullable RootBeanDefinition mbd) {
        if (System.getSecurityManager() != null) {
            AccessController.doPrivileged(() -> {
                this.invokeAwareMethods(beanName, bean);
                return null;
            }, this.getAccessControlContext());
        } else {
            this.invokeAwareMethods(beanName, bean);
        }

        Object wrappedBean = bean;
        if (mbd == null || !mbd.isSynthetic()) {
        	// 初始化前置处理
            wrappedBean = this.applyBeanPostProcessorsBeforeInitialization(bean, beanName);
        }

        try {
        	// 调用初始化方法
            this.invokeInitMethods(beanName, wrappedBean, mbd);
        } catch (Throwable var6) {
            throw new BeanCreationException(mbd != null ? mbd.getResourceDescription() : null, beanName, "Invocation of init method failed", var6);
        }

        if (mbd == null || !mbd.isSynthetic()) {
        	// 初始化后置处理
            wrappedBean = this.applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
        }

        return wrappedBean;
    }
  • AbstractAutowireCapableBeanFactory.java#initializeBean
    可以看出首先会检查是否是 InitializingBean(判断bean是否实现了InitializingBean接口),如果是调用afterPropertiesSet()方法,如果不是则执行 init-method 配置的内容
    protected void invokeInitMethods(String beanName, Object bean, @Nullable RootBeanDefinition mbd) throws Throwable {
        boolean isInitializingBean = bean instanceof InitializingBean;
        if (isInitializingBean && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) {
            if (this.logger.isTraceEnabled()) {
                this.logger.trace("Invoking afterPropertiesSet() on bean with name '" + beanName + "'");
            }

            if (System.getSecurityManager() != null) {
                try {
                    AccessController.doPrivileged(() -> {
                        ((InitializingBean)bean).afterPropertiesSet();
                        return null;
                    }, this.getAccessControlContext());
                } catch (PrivilegedActionException var6) {
                    throw var6.getException();
                }
            } else {
                ((InitializingBean)bean).afterPropertiesSet();
            }
        }

        if (mbd != null && bean.getClass() != NullBean.class) {
            String initMethodName = mbd.getInitMethodName();
            if (StringUtils.hasLength(initMethodName) && (!isInitializingBean || !"afterPropertiesSet".equals(initMethodName)) && !mbd.isExternallyManagedInitMethod(initMethodName)) {
                this.invokeCustomInitMethod(beanName, bean, mbd);
            }
        }

    }

4.3 使用 InitializingBean 自定义初始化

  • InitializingBean.java
    InitializingBean接口为bean提供了初始化方法的方式,它只包括afterPropertiesSet方法,凡是实现该接口的类,在初始化bean的时候都会执行该方法。
public interface InitializingBean {
    void afterPropertiesSet() throws Exception;
}
  • SpringInitializingBean.java
    自定义初始化类 SpringInitializingBean ,它实现了 InitializingBean 接口,并实现了接口的 afterPropertiesSet 方法,该方法调用系统配置类 SysConfig 中的 init 方法。
@Component
public class SpringInitializingBean extends LogBean implements InitializingBean {

    @Override
    public void afterPropertiesSet() {
        SysConfig.init(ItGodRoadApplication.server, ItGodRoadApplication.CALLBACK_DOMAIN);
    }

}
  • SysConfig.java
    自定义的系统配置类,封装了系统需要使用的相关配置,通过 getSettingsByServerType 方法,根据传递的参数动态实例化 BaseSettings。
public class SysConfig {
    protected static String CALLBACK_DOMAIN = null;
    protected static String SERVER_URL = null;
    protected static String GATEWAY_SERVER_URL = null;
    public static String[] GATEWAY_SERVER_URL_IP = null;
    public static String MCH_CODE = null;
    public static String API_KEY = null;
    public static boolean certUse = false;
    public static String certPW = null;
    public static String certFilePath = null;

    public static void init(String serverType, String callbackUrl) {

        /**
         * callback用url
         */
        SysConfig.CALLBACK_DOMAIN = callbackUrl;

        BaseSettings settings = getSettingsByServerType(serverType);

        /**
         * 访问
         */
        SysConfig.SERVER_URL = settings.SERVER_URL;
        SysConfig.GATEWAY_SERVER_URL = settings.GATEWAY_SERVER_URL;
        SysConfig.GATEWAY_SERVER_URL_IP = settings.GATEWAY_SERVER_URL_IP;

        /**
         * 业务
         */
        SysConfig.MCH_CODE = settings.MCH_CODE;
        SysConfig.API_KEY = settings.API_KEY;

        /**
         * 证书
         */
        SysConfig.certUse = settings.certUse;
        SysConfig.certPW = settings.certPW;
        SysConfig.certFilePath = settings.certFilePath;
    }

    private static BaseSettings getSettingsByServerType(String server) {

        BaseSettings settings = null;

        /**
         * 测试环境
         */
        if (Server.test_localhost.equals(server)) {
            settings = new Test_LocalSettings();
        } else if (Server.test_old.equals(server)) {
            settings = new Test_OldSettings();
        } else if (Server.test_new.equals(server)) {
            settings = new Test_NewSettings();
        } else if (Server.test_release.equals(server)) {
            settings = new Test_ReleaseSettings();
        }

        /**
         * 生产环境
         */
        else if (Server.product_release.equals(server)) {
            settings = new Product_ReleaseSettings();
        } else if (Server.product_gateway.equals(server)) {
            settings = new Product_GatewaySettings();
        } else if (Server.product_freeGateway.equals(server)) {
            settings = new Product_FreeGatewaySettings();
        }

        return settings;
    }

}
  • ItGodRoadApplication.java
    Springboot启动类,该类在项目根目录下,所有类都可以直接访问。在这里指定启动的服务环境和回调地址,项目启动后就会自动装配好我们设定的属性配置了。
@SpringBootApplication
public class ItGodRoadApplication extends SpringBootServletInitializer {

    /**
     * 环境
     */
    public static String server = Server.test_localhost;

    /**
     * callback用url domain
     */
    public static String CALLBACK_DOMAIN = "https://ITGodRoad.CallBack.com";

    public static void main(String[] args) {
        SpringApplication.run(ItGodRoadApplication.class, args);
    }
  • BaseSettings.java
    封装所有的配置属性
public class BaseSettings {

    /**
     * 访问
     */
    public String SERVER_URL = null;
    public String GATEWAY_SERVER_URL = null;
    public String[] GATEWAY_SERVER_URL_IP = null;

    /**
     * 业务
     */
    public String MCH_CODE = null;
    public String API_KEY = null;

    /**
     * 证书
     */
    public boolean certUse = false;
    public String certPW = null;
    public String certFilePath = null;

}
  • Test_LocalSettings.java
    不同环境下的配置类,继承BaseSettings,保证了配置类的通用性。
public class Test_LocalSettings extends BaseSettings {
    public Test_LocalSettings() {
        /**
         * 访问
         */
        this.SERVER_URL = "http://localhost:8080";
        this.GATEWAY_SERVER_URL = "http://localhost:8080";
        this.GATEWAY_SERVER_URL_IP = new String[]{};

        /**
         * 业务
         */
        this.MCH_CODE = "A1000010006";
        this.API_KEY = "random_123";

        /**
         * 证书
         */
        this.certUse = false;
        this.certPW = "ITGodRoad";
        this.certFilePath = "D:\\myCA.cer";
    }
}
  • 需要多少个配置环境就创建多少个对应的配置类

在这里插入图片描述

 


【每日一面】

jar 包和 war 包的区别

JAR(Java ARchive)Java 归档文件。是与平台无关的文件格式,它允许将许多文件组合成一个压缩文件。JAR 文件格式以流行的 ZIP 文件格式为基础。与 ZIP 文件不同的是,JAR 文件不仅用于压缩和发布,而且还用于部署和封装库、组件和插件程序,并可被像编译器和 JVM 这样的工具直接使用。在 JAR 中包含特殊的文件,如 manifests 和部署描述符,用来指示工具如何处理特定的 JAR。
启动方式:执行SpringBootApplication的run方法,启动IOC容器,然后创建嵌入式Servlet容器
WAR(Web ARchive)是一个可以直接运行的web模块,通常用于网站,打成包部署到容器中。以Tomcat来说,将war包放置在其\webapps\目录下,然后启动Tomcat,这个包就会自动解压,就相当于发布了。
先是启动Servlet服务器,服务器启动Springboot应用(springBootServletInitizer),然后启动IOC容器。
启动方式:先是启动Servlet服务器,服务器启动Springboot应用(springBootServletInitizer),然后启动IOC容器
JAR是类的归档文件,WAR包是JavaWeb程序打的包,是一个可以直接发布的Web应用程序

  • WAR包目录结构
    在这里插入图片描述

  • IDEA 打war包
     
    Ctrl+Shift+Alt+S 打开Project Structure,选择 Artifacts 点击 + 选择 Web Application Archive
    使用 Maven 命令 mvn pakage 打包更快捷

在这里插入图片描述
修改打包的war包名字和存放路径,默认是项目路径 \out\artifacts ,添加部署依赖。

在这里插入图片描述
应用,保存

在这里插入图片描述
回到首页中,点击 Build 选择 Build Artifacts 进行打包项目成 war

在这里插入图片描述
Buid 完成后,war 包显示在项目结构中

在这里插入图片描述
Ctrl+Alt+F12 进入系统目录查看

在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值