一、背景

在项目开发中,遇到有一个很特殊的需求:有一个系统服务要求必须不依赖Mysql,Redis等中间件来完成可以新增配置信息,删除配置信息,并且要求配置可以被程序感知到,完成不同的逻辑。
后来想了下决定使用定时任务,定时读取配置文件,然后将配置信息定时加载进程序中

二、代码实现

1. pom依赖

其中各个依赖版本跟随项目即可

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
    </dependency>
</dependencies>

2. 新加配置文件

2.1 access-system.properties

自定义配置文件,用于配置允许访问本系统的系统以及系统秘钥,该文件必须放在与jar包平级的config目录下,如需新增配置,将配置按照格式写入,即可自动加载

000001=MDAwMDAxQQ==
000002=MDAwMDAyQg==
000003=MDAwMDAzQw==

2.2 application.properties

springboot项目配置文件

server.port=8082

#设置自动加载配置信息的时间(每分钟执行一次)
read.access.system.schedule=0 * * * * ?

2.3 logback.xml

日志配置文件,非核心配置,随意即可

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="5 minutes" debug="false">
    <property name="APPNAME" value="task-read-config"/>"
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{80} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- 以天为单位生成日志文件-->
    <appender name="ROOT_APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>/app/logs/${APPNAME}/${APPNAME}.log</file>
        <!--每天生成一个日志文件,保存30天的日志文件 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--日志保存地址 -->
            <fileNamePattern>/app/logs/${APPNAME}/${APPNAME}-%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} [%file:%line] - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <!--错误信息日志-->
    <appender name="ERROR_APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <File>/app/logs/${APPNAME}/${APPNAME}-error.log</File>
        <!-- 只打印错误日志-->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>

        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>/app/logs/${APPNAME}/${APPNAME}-error-%d{yyyy-MM-dd}.log
            </fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>

        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} [%file:%line] - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>
    <logger name="com.htsc.boot" level="INFO" additivity="false">
        <appender-ref ref="ROOT_APPENDER"/>
        <appender-ref ref="ERROR_APPENDER"/>
    </logger>
    <root level="INFO">
        <appender-ref ref="STDOUT" additivity="false"/>
        <appender-ref ref="ROOT_APPENDER" additivity="false"/>
        <appender-ref ref="ERROR_APPENDER" additivity="false"/>
    </root>
</configuration>

3. 新建相关实体类

3.1 启动类

package com.task.read;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;


/**
 * 定时读取配置文件加载到程序中
 * @EnableScheduling表示对定时任务的支持
 * 
 * @author zhang
 */
@EnableScheduling
@SpringBootApplication
public class TaskReadConfigApplication {

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

3.1 新建配置文件对应实体类

package com.task.read.entity;

import java.io.Serializable;

/**
 * 配置信息的实体类
 *
 * @author zhang
 * @date 2021-09-03 23:02:43
 */
public class AccessSystem implements Serializable {
    private static final long serialVersionUID = 8333665889439802146L;

    private String systemId;

    private String secretKey;

    public AccessSystem() {
    }

    public AccessSystem(String systemId, String secretKey) {
        this.systemId = systemId;
        this.secretKey = secretKey;
    }

    public String getSystemId() {
        return systemId;
    }

    public void setSystemId(String systemId) {
        this.systemId = systemId;
    }

    public String getSecretKey() {
        return secretKey;
    }

    public void setSecretKey(String secretKey) {
        this.secretKey = secretKey;
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder();
        sb.append("{")
                .append("\"systemId\":").append(systemId)
                .append(", \"secretKey\":").append("******")
                .append('}');
        return sb.toString();
    }
}

3.2 新建config类用于存储所有配置信息

package com.task.read.config;

import com.task.read.entity.AccessSystem;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

/**
 * 配置了用于存储所有配置信息
 *
 * @author zhang
 * @date 2021-09-03 23:04:01
 */
@Component
public class AccessSystemConfig {

    private List<AccessSystem> systemList = new ArrayList<>();

    public List<AccessSystem> getSystemList() {
        return systemList;
    }

    public void setSystemList(List<AccessSystem> systemList) {
        this.systemList = systemList;
    }
}

3.3 定时任务类

package com.task.read.schedule;

import com.task.read.config.AccessSystemConfig;
import com.task.read.entity.AccessSystem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.stream.Collectors;

/**
 * 定时任务,定时读取所有accessSystem配置信息,加载到程序中
 *
 * @author zhang
 * @date 2021-09-03 23:19:09
 */
@Component
public class ScheduleTask implements ApplicationRunner {
    private static final Logger LOGGER = LoggerFactory.getLogger(ScheduleTask.class);
    private static String configLocation = null;

    //获取配置文件所在目录
    static {

        //获取当前类所在的目录,即jar包目录
        String property = System.getProperty("user.dir");

        //获取自定义配置文件的路径
        configLocation = property + "/config/access-system.properties";

        LOGGER.info("access-system.properties location:[{}]", configLocation);
    }

	/**
     * 项目启动后,会调用该方法
     */
    @Autowired
    private AccessSystemConfig accessSystemConfig;

    @Override
    public void run(ApplicationArguments args) {
        LOGGER.info("======================================================================");
        LOGGER.info("=== The task for reading the accessSystemConfig has been started ===");
        LOGGER.info("======================================================================");
        readConfig();
    }

    /**
     * 定时加载 accessSystem
     */
    @Scheduled(cron = "${read.access.system.schedule}")
    public void scheduleTask() {
        readConfig();
    }

    /**
     * 读取配置,将配置加载到程序中
     */
    private void readConfig() {
        LOGGER.info("============= parse accessSystemConfig is start =============");

        long startTime = System.currentTimeMillis();

        //加载文件
        try (FileInputStream inputStream = new FileInputStream(configLocation);
             Reader reader = new InputStreamReader(inputStream);
             BufferedReader br = new BufferedReader(reader)) {

            //存放本次读取出的配置
            List<AccessSystem> newSystemList = new ArrayList<>();

            //将文件映射成properties对象
            Properties properties = new Properties();
            properties.load(br);

            //解析properties对象,存入newSystemList中
            properties.forEach((k, v) -> {

                //解析秘钥,并保存
                AccessSystem accessSystem = new AccessSystem(k.toString(), v.toString());
                newSystemList.add(accessSystem);
            });

            //清空原有数据,并保存新获取到的数据(注这里可能存在并发问题,实际使用中需要处理)
    		accessSystemConfig.getSystemList().clear();
            accessSystemConfig.setSystemList(newSystemList);

        } catch (Exception e) {
            LOGGER.warn("read accessSystemConfig is fail, cause by:", e);

        } finally {
            //打印出已加载的系统信息
            List<String> systemNoList = accessSystemConfig.getSystemList().stream().map(AccessSystem::getSystemId).collect(Collectors.toList());
            LOGGER.info("Loaded systemNoList:{}", systemNoList);

            LOGGER.info("======= parse accessSystemConfig is complete, cost:{}ms ======", System.currentTimeMillis() - startTime);
        }
    }
}

3.4 Controller类

package com.task.read.controller;

import com.alibaba.fastjson.JSON;
import com.task.read.config.AccessSystemConfig;
import com.task.read.entity.AccessSystem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 控制器层,简单模拟下
 *
 * @author zhang
 * @date 2021-09-03 23:13:59
 */
@RestController
@RequestMapping
public class AdminController {
    private static final Logger LOGGER = LoggerFactory.getLogger(AdminController.class);

    @Autowired
    private AccessSystemConfig accessSystemConfig;


    @GetMapping("/getConfig")
    public Map<String, Object> getConfig() {
        Map<String, Object> result = new HashMap<>();

        //这里模拟根据配置 完成不同的逻辑
        List<AccessSystem> systemList = accessSystemConfig.getSystemList();
        systemList.forEach(accessSystem -> {

            result.put(accessSystem.getSystemId(), accessSystem.getSecretKey());
            LOGGER.info("accessSystem:{}", JSON.toJSONString(accessSystem));

        });

        return result;
    }
}

三、 功能测试

1. 系统启动

系统启动后,看下启动日志,如果成功启动,会打印如下信息
在这里插入图片描述

2. postman请求/getConfig接口

请求完成,看具体返回值,是否正常返回
在这里插入图片描述

3. 修改access-system.properties文件

修改配置文件中的内容后,定时任务执行时,就会重新加载配置,这时候再去请求/getConfig接口,应该会随着你的修改而产生变化

四、总结

因为领导催的很急,所以只是做了利用了定时任务来简单的实现,也基本满足了需求,但是如果硬扣的话,这么做是无法做到准实时的,并且肯定还有其他方式来实现,但是我觉得这是比较简单快速的实现了,就这样吧,完事收工

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐