Spring Boot Starter
简介
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丶端口丶客户端类型丶请求参数等。
自定义日志启动器
- 创建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>
- 自定义日志过滤器。
/**
* 分布式环境下为每条请求生成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;
}
}
- 定义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);
}
}
- 定义自动配置类将LogFilterRegistrantionBean注入到Spring上下文中
@Configuration
@ConditionalOnClass({LogFilterRegistrantionBean.class, LogFilter.class})
public class LogFilterConfiguration {
@Bean
@ConditionalOnMissingBean(LogFilterRegistrantionBean.class)
public LogFilterRegistrantionBean logFilterRegistrantionBean(){
return new LogFilterRegistrantionBean();
}
}
- 定义使自动配置类生效的注解。因为starter是通过Jar包的方式引入项目的,对应的class并不在项目的Spring扫描范围内,所以需要定义使自动配置类生效的注解引入我们对应的配置类LogFilterConfiguration
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import(LogFilterConfiguration.class)
public @interface EnableLogFilter {
}
- 使用
- 将我们的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代码已上传,审核中,等待后续更新。。。