搭建大型分布式服务(十九)面试官:你做过Spring框架功能拓展吗?

·  阅读 354

系列文章目录


@TOC


前言

群里有个小伙伴反馈说,前段时间去面试被面试官问到:你做过spring框架的哪些功能上的拓展呢?他说当时心里瞬间就凉了一截,满脑子浮现的是IOC和AOP,spring拓展实在想不起来是啥。为什么面试官都喜欢问spring拓展?为什么我们背过spring容器初始化流程还是回答不出来呢?其实这里主要考察候选人对spring框架的掌握程度,如果你能熟练运用spring的拓展点,也就间接证明了你对IOC、AOP理解还是比较好的。

Spring框架的强大很大程度得益于它提供了完备的拓展点,网上对拓展点的介绍资料已经很详细,这里简单解释为在环境(Environment)、容器(ApplicationContext)、类元数据(Meta)、实例(Bean)的初始化或实例化前、中、后增加一些自定义的操作,改变或者增强框架原有的逻辑。常见的拓展方式有实现xxxInitializer、xxxProcessor、xxxAware接口,如早期xml方式整合dubbo,还有注解或者基于SPI的META-INF/spring.factories,如springboot的starter。

为了加深小伙伴们的理解,我们假定一个业务场景:我们需要在application.properties/application.yml 中自定义配置变量来获取ip,用来生成不同的日志文件名称。其中random是springboot内置的,myVar是我们拓展的。

my.log.prefix=monitor_${myVar.ip}
#my.log.prefix=mopnitor_${myVar.yyyyMMddHHmmss}
#my.log.prefix=monitor _${random.int(10)}
复制代码

一、本文要点

接前文,我们整合了Apollo,并把配置文件放在Apollo远程服务器中托管了,细心的你肯定发现了,Apollo把远端配置注入springboot容器上下文何尝不就是一种Environment拓展呢?本文我们将模仿springboot内置random变量的处理逻辑,实现我们自定义配置变量的拓展。系列文章完整目录

  • springboot 拓展
  • springboot 自定义配置变量
  • springboot application配置文件获取服务IP
  • springboot application配置文件获取当前时间 + 格式化
  • EnvironmentPostProcessor 外置配置
  • logback 读取springboot配置

二、开发环境

  • jdk 1.8
  • maven 3.6.2
  • springboot 2.4.3
  • idea 2020

三、项目改造

1、在resources目录增加 META-INF/spring.factories 文件,使spring框架能感知到新增的SPI,配置如下:

org.springframework.boot.env.EnvironmentPostProcessor=\
com.mmc.lesson.envdemo.support.MyValuePropertySourceEnvironmentPostProcessor
复制代码

2、编写MyValuePropertySourceEnvironmentPostProcessor.java ,实现EnvironmentPostProcessor 、Order 接口,这样可以更高优先级在spring容器refresh前先实现我们自定义的配置的注入。

public class MyValuePropertySourceEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered {

    private Log logger;

    /**
     * empty.
     */
    public MyValuePropertySourceEnvironmentPostProcessor() {
        logger = LogFactory.getLog(MyValuePropertySourceEnvironmentPostProcessor.class);
    }

    /**
     * init。
     */
    public MyValuePropertySourceEnvironmentPostProcessor(Log logger) {
        this.logger = logger;
    }

    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {

        MyValuePropertySource.addToEnvironment(environment, this.logger);
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE + 2;
    }
}
复制代码

3、编写 MyValuePropertySource.java ,定义我们在application.yml / application.properties 的变量前缀 ${myVar} 。

public class MyValuePropertySource extends PropertySource<MyLogValue> {

    /**
     * Name of the random {@link PropertySource}.
     */
    public static final String MY_PROPERTY_SOURCE_NAME = "myVar";

    private static final String PREFIX = "myVar.";

    private static final Log logger = LogFactory.getLog(MyValuePropertySource.class);

    /**
     * MyValuePropertySource.
     */
    public MyValuePropertySource() {
        this(MY_PROPERTY_SOURCE_NAME);
    }

    /**
     * MyValuePropertySource.
     */
    public MyValuePropertySource(String name) {
        super(name, new MyLogValue());
    }

    /**
     * 增加自定义表达式到环境上下文.
     */
    public static void addToEnvironment(ConfigurableEnvironment environment, Log logger) {

        MutablePropertySources sources = environment.getPropertySources();
        PropertySource<?> existing = sources.get(MY_PROPERTY_SOURCE_NAME);
        if (existing != null) {
            logger.trace("RandomValuePropertySource already present");
            return;
        }
        MyValuePropertySource randomSource = new MyValuePropertySource(MY_PROPERTY_SOURCE_NAME);
        if (sources.get(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME) != null) {
            sources.addAfter(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, randomSource);
        } else {
            sources.addLast(randomSource);
        }
        logger.trace("MyValuePropertySource add to Environment");
    }

    @Override
    public Object getProperty(String name) {
        if (!name.startsWith(PREFIX)) {
            return null;
        }
        logger.trace(LogMessage.format("Generating property for '%s'", name));
        return getValue(name.substring(PREFIX.length()));
    }

    /**
     * 目前仅支持ip.
     */
    private Object getValue(String type) {

        if (type.equalsIgnoreCase("ip")) {

            return getSource().getIp();
        }
        return null;
    }
}
复制代码

4、编写MyLogValue.java ,实现 ${myVar.ip} 的取值逻辑。

@Data
public class MyLogValue {

    /**
     * 获取本机Ip.
     */
    public String getIp() {

        return IpUtil.getLocalIP();
    }
}
class IpUtil {

    /**
     * 获取本机IP,只返回一个.
     */
    static String getLocalIP() {
        String sIP = "";
        InetAddress ip = null;
        try {
            // 如果是Windows操作系统
            if (isWindowsOS()) {
                ip = InetAddress.getLocalHost();
                // 如果是Linux操作系统
            } else {
                boolean bFindIP = false;
                Enumeration<NetworkInterface> netInterfaces = NetworkInterface.getNetworkInterfaces();
                while (netInterfaces.hasMoreElements()) {
                    if (bFindIP) {
                        break;
                    }
                    NetworkInterface ni = netInterfaces.nextElement();
                    if (ni.isLoopback() || ni.isVirtual() || !ni.isUp()) {
                        continue;
                    }
                    // ----------特定情况,可以考虑用ni.getName判断
                    // 遍历所有ip
                    Enumeration<InetAddress> ips = ni.getInetAddresses();
                    while (ips.hasMoreElements()) {
                        ip = ips.nextElement();
                        if (ip.isSiteLocalAddress() && !ip.isLoopbackAddress() // 127.开头的都是lookback地址
                                && !ip.getHostAddress().contains(":")) {
                            bFindIP = true;
                            break;
                        }
                    }

                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        if (null != ip) {
            sIP = ip.getHostAddress();
        }

        return sIP;
    }

    /**
     * 判断是否为Windows系统.
     */
    private static boolean isWindowsOS() {
        boolean isWindowsOS = false;
        String osName = System.getProperty("os.name");
        if (osName.toLowerCase().contains("windows")) {
            isWindowsOS = true;
        }
        return isWindowsOS;
    }
}

复制代码

四、运行一下

1、修改 logback-spring.xml 配置。

    <springProperty scope="context" name="log.path" source="logging.file.path"/>
    <springProperty scope="context" name="monitor.file.prefix" source="my.log.prefix"/>

 <appender name="MONITOR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文件的路径及文件名 -->
        <file>${log.path}/${monitor.file.prefix}.log</file>
        <!--日志文件输出格式-->
        <encoder>
            <pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%thread] [%level] [%logger{50}:%L] - %msg%n
            </pattern>
            <charset>UTF-8</charset>
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 每天日志归档路径以及格式 -->
            <fileNamePattern>${log.path}/monitor/monitor-%d{yyyy-MM-dd}.%i.zip</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy
                    class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>500MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <maxHistory>7</maxHistory>
            <totalSizeCap>7GB</totalSizeCap>
            <cleanHistoryOnStart>true</cleanHistoryOnStart>
        </rollingPolicy>
    </appender>
复制代码

2、重点来了,修改application.properties,增加我们自定义的变量myVar.ip

# 日志路径
logging.file.path=./logs
# 日志名称前缀
my.log.prefix=monitor_${myVar.ip}

复制代码

3、编写测试用例。

@SpringBootTest
class EnvDemoApplicationTests {

    @Value("${my.log.prefix}")
    private String prefix;

    @Test
    void contextLoads() {

        System.out.println("-----------------------------");
        System.out.println(prefix);
    }

}
复制代码

4、运行一下,和预期一致!可以正常获取myVar.ip 的值。 在这里插入图片描述

五、小结

至此,我们就简单实现了拓展spring框架的功能啦。课后作业,小伙伴可以自行去实现前文提到的 ${myVar.time.yyyyMMdd} 变量哦,有兴趣的小伙伴还可以学习下Apollo注入远程配置的原理。下一篇《搭建大型分布式服务(二十)Springboot 拓展-定制日志组件

加我一起交流学习!

分类:
代码人生
标签:
收藏成功!
已添加到「」, 点击更改