Spring Boot快速入门 -> 自定义一个Starter,实现在分布式系统环境下进行请求调用链路追踪(Spring Boot + log4j2 + 雪花算法 + 消息摘要算法整合)(四)

简介

Spring Boot项目的快速发展,很大程度依赖于Starter的出现。通过Starter,可以获取到所需相关技术的一站式支持,比如:相关依赖丶相关自动配置文件和相关Bean,无需通过代码和复制粘贴。例如当我们需要Spring Web支持的时候,我们只需要引入spring-boot-starter-web这个依赖即可,轻松快捷,而且它还内嵌了Tomcat,自动帮我们开启Spring Webmvc功能。省去了之前很多的配置操作。

那么如果我们想要自定义一个Starter需要做些什么呢?上一篇我们已经研究了其自动配置的原理,下面我就介绍下如何实现自己的SpringBoot-logfilter-starter。

自定义SpringBoot-logfilter-starter作用:过滤http请求,生成RequestId及SessionId(方便后续通过日志全链路查找问题),输出请求 日志,包含请求路径丶IP丶端口丶客户端类型丶请求参数等。

自定义日志启动器

  1. 创建starter项目丶导入依赖。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.springboot.starter</groupId>
    <artifactId>filter-log-starter</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.1.5.RELEASE</version>
        </dependency>
    </dependencies>
</project>
  1. 自定义日志过滤器。
/**
 * 分布式环境下为每条请求生成traceId
 */
public class LogFilter implements Filter {

    private static final Logger logger = LoggerFactory.getLogger(LogFilter.class);
    private static Snowflake snowflake = null;

    static {
    	//这里可以从配置中获取数据中心编号和机器实例编号
        snowflake = new Snowflake(1, 1);
    }

    public void init(FilterConfig filterConfig) throws ServletException {
        logger.info("init");
    }

    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        if (servletRequest instanceof HttpServletRequest){
            HttpServletRequest request = (HttpServletRequest) servletRequest;
            String requestID = (String) MDC.get("RequestID");
            String sessionID = (String) MDC.get("SessionID");
            putMDC(request);
            try {
                ClientRequestInfo clientRequestInfo =ClientInfoUtils.getRequestInfo(request);
                logger.info(clientRequestInfo.toString());
                filterChain.doFilter(servletRequest,servletResponse);
            }finally {
                MDC.clear();
                if(request != null){
                    MDC.put("RequestID",requestID);
                    MDC.put("SessionID",sessionID);
                }
            }
        }else{
            filterChain.doFilter(servletRequest,servletResponse);
        }
    }
    public void destroy() {

    }

    private static void putMDC(HttpServletRequest request) {
        //调用方需要在请求中生成Log4j-Request-ID
        String log4jRequestId = request.getHeader("Log4j-Request-ID");
        String log4jSessionId = request.getHeader("Log4j-Session-ID");
        if (log4jRequestId != null){
            MDC.put("RequestID",log4jRequestId);
            if(log4jSessionId != null){
                MDC.put("SessionID",log4jSessionId);
            }
        }else{
            MDC.put("RequestID",getKey());
            HttpSession session = request.getSession();
            if(session != null){
                MDC.put("SessionID",session.getId());
            }
        }
    }

    /**
     * 分布式场景下通过雪花算法获取唯一ID
     * @return
     * @throws NoSuchAlgorithmException
     */
    private static String getKey() {
        long id = snowflake.next();
        byte[] bytes = null;
        try {
            //Java消息摘要算法转换traceId
            bytes = MessageDigest.getInstance("MD5").digest(String.valueOf(id).getBytes());
        } catch (NoSuchAlgorithmException e) {
            logger.error("生成TraceID异常");
            e.printStackTrace();
        }
        return HexBin.encode(bytes);
    }
}
/**
 * 雪花算法
 */
public class Snowflake {
    private final static long oldstamp = 1594965350605l;

    private long machineId;    //机器标识
    private long datacenterId;  //数据中心
    private long sequence = 0L; //序列号
    private long lastStmp = -1L;//上一次时间戳
    /*
    每一部门占的位数
     */
    private final static long workmachine_bit = 5;
    private final static long datacenter_bit = 5;
    private final static long sequence_bit = 12;

    /*
    各个节点的组成
     */
    private final static long max_workmachine = -1 ^ (-1 << workmachine_bit);
    private final static long max_datacenter = -1 ^ (-1 << datacenter_bit);;
    private final static long max_sequence = -1 ^ (-1 << sequence_bit);

    /*
    每一部分向左移的位数
     */

    private final static long left_workmachine = sequence_bit;
    private final static long left_datacenter = sequence_bit+datacenter_bit;
    private final static long left_timestamp = left_datacenter + datacenter_bit;



    public Snowflake(long datacenter,long workmachine) {
        if(datacenter > max_datacenter || datacenter < 0){
            throw new IllegalArgumentException("datacenter");
        }
        if(workmachine > max_workmachine || workmachine < 0){
            throw new IllegalArgumentException("datacenter");
        }
        this.datacenterId=datacenter;
        this.machineId = workmachine;
    }

    public synchronized long next(){
        long currenttime = getTime();
        if(currenttime < lastStmp){
            throw new RuntimeException("时间回拨了...");
        }
        if(currenttime == lastStmp){
            sequence = (sequence + 1) & max_sequence;
            if(sequence == 0){
                currenttime = getNextTime();
            }
        }else{
            sequence = 0 ;
        }
        lastStmp = currenttime;
        return (currenttime - oldstamp) << left_timestamp |
                datacenterId << left_datacenter |
                machineId << left_workmachine |
                sequence;

    }

    private long getTime(){
        return System.currentTimeMillis();
    }

    private long getNextTime(){
        long current = getTime();
        if(current <= lastStmp){
            current = getTime();
        }
        return current;
    }
}
  1. 定义LogFilterRegistrantionBean将LogFilter过滤器封装成Spring Bean。
public class LogFilterRegistrantionBean extends FilterRegistrationBean<LogFilter> {

    public LogFilterRegistrantionBean() {
        super();
        this.setFilter(new LogFilter());
        this.addUrlPatterns("/*");
        this.setName("LogFilter");
        this.setOrder(1);
    }
}
  1. 定义自动配置类将LogFilterRegistrantionBean注入到Spring上下文中
@Configuration
@ConditionalOnClass({LogFilterRegistrantionBean.class, LogFilter.class})
public class LogFilterConfiguration {

    @Bean
    @ConditionalOnMissingBean(LogFilterRegistrantionBean.class)
    public LogFilterRegistrantionBean logFilterRegistrantionBean(){
        return new LogFilterRegistrantionBean();
    }

}
  1. 定义使自动配置类生效的注解。因为starter是通过Jar包的方式引入项目的,对应的class并不在项目的Spring扫描范围内,所以需要定义使自动配置类生效的注解引入我们对应的配置类LogFilterConfiguration
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import(LogFilterConfiguration.class)
public @interface EnableLogFilter {
}
  1. 使用
  • 将我们的starter通过maven打包进本地仓库,idea -> maven -> install
    在这里插入图片描述

整合spring boot + log4j2

可以借鉴下链接: springboot整合Log4j2(将日志输出到指定文件),修改PATTERN参数格式即可。

<Properties>
    <Property name="PATTERN">[%d{yyyy-MM-dd HH:mm:ss.SSS} %p] R[%X{RequestID},%X{SessionID}] [%F\:%L] - %m%n</Property>
</Properties>

测试

  • 在我们自己的spring boot项目中加入启动器依赖
    在这里插入图片描述
  • 在启动类或者controller中加入我们的自动配置注解
@SpringBootApplication
@EnableLogFilter
public class MySpringBootApplication {

    public static void main(String[] args) {

        SpringApplication application = new SpringApplication(MySpringBootApplication.class);

        ConfigurableApplicationContext context = application.run();
    }
}
@RestController
public class TestController {

    private static final Logger logger = LoggerFactory.getLogger(TestController.class);

//    @Autowired
//    MyDataSource dataSource;


    @RequestMapping("hello/{userName}")
    public String sayHello(@PathVariable String userName){
        logger.info("调用了sayHello...");
        return "welcome,"+userName+",this is springboot !";
    }

//    @RequestMapping("testYaml")
//    public String testYaml(){
//        return "welcome, this is yaml属性注入 !"+dataSource;
//    }
}
  • 简单整合spring boot + log4j2
  • 测试,启动spring boot项目,在浏览器中访问http://localhost:8081/hello/TeacherMa
    在这里插入图片描述
  • 查看控制台日志,启动器生效,日志正常监控
    在这里插入图片描述

代码链接

自定义starter代码已上传,审核中,等待后续更新。。。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值