SpringBoot高级篇-监控
今日目标
掌握SpringBoot自定义Starter
代码地址:
25-sprongboot-starter-test
1.什么是SpringBoot starter
Spring Boot Starter机制是Spring Boot框架提供的一种约定,用于简化依赖管理和配置的过程。它基于可插拔的自动配置原则,通过打包一组相关的依赖项和自动配置类,为特定的功能模块提供一键式集成和使用。
具体来说,Spring Boot Starter由以下几个关键组成部分组成:
Starter依赖:Starter依赖是一个聚合性的依赖,提供了一组相关的库和工具,以及必要的传递性依赖。它们通常以spring-boot-starter-*命名,如spring-boot-starter-web、spring-boot-starter-data-jpa等。引入这些Starter依赖,可以轻松地添加所需的功能模块到项目中。
自动配置:每个Starter依赖都包含一个或多个自动配置类。这些自动配置类会根据类路径上的条件(例如依赖的库是否存在)来检测需要自动配置的组件,并进行相应的配置。通过自动配置,开发者无需手动编写大量的配置代码,框架会自动完成许多常见的配置任务。
属性配置:Starter依赖通常还包含一些默认的属性配置,可以通过在应用程序的配置文件(如application.properties或application.yml)中进行覆盖。这些属性配置提供了对自动配置行为和功能的可定制性。
Spring Boot Starter机制的优点包括:
简化依赖管理:通过引入Starter依赖,开发者无需手动管理复杂的依赖关系,框架会自动处理依赖的版本和传递性依赖。 快速集成和使用:通过引入特定的Starter依赖,开发者可以快速集成和使用各种功能模块。 自动配置和约定优于配置:Spring Boot的自动配置机制可以根据类路径上的条件自动完成大部分配置工作,减少了繁琐的手动配置。 可定制性:通过覆盖默认的属性配置,开发者可以根据项目需求进行个性化定制。 总而言之,Spring Boot Starter机制提供了一种方便、高效的方式来集成和使用各种功能模块,大大简化了Spring Boot应用程序的开发和配置过程。
2.AOP方式统一服务日志
原先实现统一日志都是放到每个工程中以AOP方式实现,现在有了starter方式,就可以将公司的日志规范集中到这里来统一管理。
2.1创建Log的Starter项目
项目名称zbblog-spring-boot-starter
,结构如下:

2.1.1在Pom.xml中添加依赖
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.14</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.zbbmeta</groupId>
<artifactId>zbblog-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.35</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.20</version>
</dependency>
</dependencies>
</project>复制
2.1.2 添加自定义的Util类
添加HttpUtil用于Http请求操作
package com.zbbmeta.util;
import lombok.extern.slf4j.Slf4j;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
/**
* @author springboot葵花宝典
* @description: http请求操作类
* @link https://bbs.huaweicloud.com/blogs/104013
*/
@Slf4j
public class HttpUtil {
/**
* 发送GET请求
*
* @param requestUrl
* @return
*/
public static Object getRequest(String requestUrl, String charSetName) {
String res = "";
StringBuffer buffer = new StringBuffer();
try {
URL url = new URL(requestUrl);
HttpURLConnection urlCon = (HttpURLConnection) url.openConnection();
if (200 == urlCon.getResponseCode()) {
InputStream is = urlCon.getInputStream();
InputStreamReader isr = new InputStreamReader(is, charSetName);
BufferedReader br = new BufferedReader(isr);
String str = null;
while ((str = br.readLine()) != null) {
buffer.append(str);
}
br.close();
isr.close();
is.close();
res = buffer.toString();
return res;
} else {
throw new Exception("连接失败");
}
} catch (Exception e) {
log.error(e.getMessage());
}
return null;
}
/**
* 发送POST请求
*
* @param path
* @param post
* @return
*/
public static Object postRequest(String path, String post) {
URL url = null;
try {
url = new URL(path);
HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
// 提交模式
httpURLConnection.setRequestMethod("POST");
//连接超时 单位毫秒
httpURLConnection.setConnectTimeout(10000);
//读取超时 单位毫秒
httpURLConnection.setReadTimeout(2000);
// 发送POST请求必须设置如下两行
httpURLConnection.setDoOutput(true);
httpURLConnection.setDoInput(true);
// 获取URLConnection对象对应的输出流
PrintWriter printWriter = new PrintWriter(httpURLConnection.getOutputStream());
// 发送请求参数
//post的参数 xx=xx&yy=yy
printWriter.write(post);
// flush输出流的缓冲
printWriter.flush();
//开始获取数据
BufferedInputStream bis = new BufferedInputStream(httpURLConnection.getInputStream());
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int len;
byte[] arr = new byte[1024];
while ((len = bis.read(arr)) != -1) {
bos.write(arr, 0, len);
bos.flush();
}
bos.close();
return bos.toString("utf-8");
} catch (Exception e) {
log.error(e.getMessage());
}
return null;
}
}复制
添加IP工具类
package com.zbbmeta.util;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;
/**
* @author springboot葵花宝典
* @description: IP工具类
*/
@Slf4j
public class IPUtil {
private final static boolean ipLocal = false;
/**
* 根据ip获取详细地址
*/
public static String getCityInfo(String ip) {
if (ipLocal) {
//待开发
return null;
} else {
return getHttpCityInfo(ip);
}
}
/**
* 根据ip获取详细地址
* 临时使用,待调整
*/
public static String getHttpCityInfo(String ip) {
String api = String.format("http://whois.pconline.com.cn/ipJson.jsp?ip=%s&json=true", ip);
JSONObject object = JSON.parseObject((String) HttpUtil.getRequest(api, "gbk"));
return object.getString("addr");
}
public static void main(String[] args) {
System.err.println(getCityInfo("220.248.12.158"));
}
}复制
添加获取获取HttpServletRequest类
package com.zbbmeta.util;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Objects;
/**
* @author springboot葵花宝典
* @description: 获取HttpServletRequest类
*/
@Slf4j
public class RequestHolder {
private static final String UNKNOWN = "unknown";
/**
* 获取HttpServletRequest请求
*
* @return HttpServletRequest
*/
public static HttpServletRequest getHttpServletRequest() {
return ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
}
/**
* 获取请求IP
*
* @return String IP
*/
public static String getHttpServletRequestIpAddress() {
HttpServletRequest request = getHttpServletRequest();
return getHttpServletRequestIpAddress(request);
}
public static String getHttpServletRequestIpAddress(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
if (ip.contains(",")) {
ip = ip.split(",")[0];
}
return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
}
public static String getServerHttpRequestIpAddress(ServerHttpRequest request) {
HttpHeaders headers = request.getHeaders();
String ip = headers.getFirst("x-forwarded-for");
if (ip != null && ip.length() != 0 && !UNKNOWN.equalsIgnoreCase(ip)) {
if (ip.contains(StringPool.COMMA)) {
ip = ip.split(StringPool.COMMA)[0];
}
}
if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
ip = headers.getFirst("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
ip = headers.getFirst("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
ip = headers.getFirst("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
ip = headers.getFirst("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
ip = headers.getFirst("X-Real-IP");
}
if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
ip = Objects.requireNonNull(request.getRemoteAddress()).getAddress().getHostAddress();
}
return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
}
}复制
字符串操作
package com.zbbmeta.util;
/**
* @author springboot葵花宝典
* @description: https://java.sun.com/docs/books/jls/third_edition/html/lexical.html#3.10.5
*/
public interface StringPool {
String AMPERSAND = "&";
String AND = "and";
String AT = "@";
String ASTERISK = "*";
String STAR = ASTERISK;
String BACK_SLASH = "\\";
String COLON = ":";
String COMMA = ",";
String DASH = "-";
String DOLLAR = "$";
String DOT = ".";
String DOTDOT = "..";
String DOT_CLASS = ".class";
String DOT_JAVA = ".java";
String DOT_XML = ".xml";
String EMPTY = "";
String EQUALS = "=";
String FALSE = "false";
String SLASH = "/";
String HASH = "#";
String HAT = "^";
String LEFT_BRACE = "{";
String LEFT_BRACKET = "(";
String LEFT_CHEV = "<";
String DOT_NEWLINE = ",\n";
String NEWLINE = "\n";
String N = "n";
String NO = "no";
String NULL = "null";
String OFF = "off";
String ON = "on";
String PERCENT = "%";
String PIPE = "|";
String PLUS = "+";
String QUESTION_MARK = "?";
String EXCLAMATION_MARK = "!";
String QUOTE = "\"";
String RETURN = "\r";
String TAB = "\t";
String RIGHT_BRACE = "}";
String RIGHT_BRACKET = ")";
String RIGHT_CHEV = ">";
String SEMICOLON = ";";
String SINGLE_QUOTE = "'";
String BACKTICK = "`";
String SPACE = " ";
String TILDA = "~";
String LEFT_SQ_BRACKET = "[";
String RIGHT_SQ_BRACKET = "]";
String TRUE = "true";
String UNDERSCORE = "_";
String UTF_8 = "UTF-8";
String US_ASCII = "US-ASCII";
String ISO_8859_1 = "ISO-8859-1";
String Y = "y";
String YES = "yes";
String ONE = "1";
String ZERO = "0";
String DOLLAR_LEFT_BRACE = "${";
String HASH_LEFT_BRACE = "#{";
String CRLF = "\r\n";
String HTML_NBSP = " ";
String HTML_AMP = "&";
String HTML_QUOTE = """;
String HTML_LT = "<";
String HTML_GT = ">";
// ---------------------------------------------------------------- array
String[] EMPTY_ARRAY = new String[0];
byte[] BYTES_NEW_LINE = StringPool.NEWLINE.getBytes();
}复制
2.1.3 添加日志对象类CommonLog
package com.zbbmeta.dto;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* @author springboot葵花宝典
* @description: 日志对象
*/
@Data
@Accessors(chain = true)
public class CommonLog implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 日志类型
*/
private String type;
/**
* 跟踪ID
*/
private String traceId;
/**
* 日志标题
*/
private String title;
/**
* 操作内容
*/
private String operation;
/**
* 执行方法
*/
private String method;
/**
* 请求路径
*/
private String url;
/**
* 参数
*/
private String params;
/**
* ip地址
*/
private String ip;
/**
* 耗时
*/
private Long executeTime;
/**
* 地区
*/
private String location;
/**
* 创建人
*/
private String createBy;
/**
* 更新人
*/
private String updateBy;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
/**
* 删除标识
*/
private String isDeleted;
/**
* 租户ID
*/
private Integer tenantId;
/**
* 异常信息
*/
private String exception;
}复制
2.2 编写相关属性类LogProperties
/**
* @author springboot葵花宝典
* @description: TODO
*/
@ConfigurationProperties("zbbmeta.log")
@Data
public class LogProperties implements Serializable {
private Boolean enabled;
}复制
2.3 使用AOP功能编写Starter业务
package com.zbbmeta.aspect;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.zbbmeta.dto.CommonLog;
import com.zbbmeta.util.IPUtil;
import com.zbbmeta.util.RequestHolder;
import com.zbbmeta.util.StringPool;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.SynthesizingMethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
/**
* @author springboot葵花宝典
* @description: 日志拦截器
*/
@Slf4j
@Aspect
public class LogAspect {
// @Pointcut("@annotation(com.zbbmeta.annotation.Log)")
@Pointcut("execution(* *..*Controller.*(..))")
public void pointcut() {
}
/**
* 环绕通知,使用Pointcut()上注册的切入点
* @param point
* @return
*/
@Around("pointcut()")
public Object recordLog(ProceedingJoinPoint point) throws Throwable {
Object result = new Object();
// 获取request
HttpServletRequest request = RequestHolder.getHttpServletRequest();
// 判断为空则直接跳过执行
if (ObjectUtils.isEmpty(request)){
return point.proceed();
}
// 获取注解里的value值
Method targetMethod = resolveMethod(point);
// Log logAnn = targetMethod.getAnnotation(Log.class);
// 打印执行时间
long startTime = System.nanoTime();
// 请求方法
String method = request.getMethod();
String url = request.getRequestURI();
// 获取IP和地区
String ip = RequestHolder.getHttpServletRequestIpAddress();
String region = IPUtil.getCityInfo(ip);
//获取请求参数
//Map<String, Object> paramMap = logIngArgs(point);
// 参数
Object[] args = point.getArgs();
String requestParam = getArgs(args, request);
// 计算耗时
long tookTime = 0L;
try {
result = point.proceed();
} finally {
tookTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime);
}
// 如果是登录请求,则不获取用户信息
String userName = "springboot葵花宝典";
// 封装SysLog
CommonLog commonLog = new CommonLog();
commonLog.setIp(ip)
.setCreateBy(userName)
.setMethod(method)
.setUrl(url)
.setOperation(String.valueOf(result))
.setLocation(StringUtils.isEmpty(region) ? "本地" : region)
.setExecuteTime(tookTime)
// .setTitle(logAnn.value())
.setParams(JSON.toJSONString(requestParam));
log.info("Http Request: {}", JSONObject.toJSONString(commonLog));
return result;
}
/**
* 配置异常通知
*
* @param point join point for advice
* @param e exception
*/
@AfterThrowing(pointcut = "pointcut()", throwing = "e")
public void logAfterThrowing(JoinPoint point, Throwable e) {
// 打印执行时间
long startTime = System.nanoTime();
CommonLog commonLog = new CommonLog();
// 获取IP和地区
String ip = RequestHolder.getHttpServletRequestIpAddress();
String region = IPUtil.getCityInfo(ip);
// 获取request
HttpServletRequest request = RequestHolder.getHttpServletRequest();
// 请求方法
String method = request.getMethod();
String url = request.getRequestURI();
// 获取注解里的value值
Method targetMethod = resolveMethod((ProceedingJoinPoint) point);
// Log logAnn = targetMethod.getAnnotation(Log.class);
commonLog.setExecuteTime(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime))
.setIp(ip)
.setLocation(region)
.setMethod(method)
.setUrl(url)
.setType("2")
// .setTitle(logAnn.value())
.setException(getStackTrace(e));
// 发布事件
log.info("Error Result: {}", commonLog);
}
private Method resolveMethod(ProceedingJoinPoint point) {
MethodSignature signature = (MethodSignature) point.getSignature();
Class<?> targetClass = point.getTarget().getClass();
Method method = getDeclaredMethod(targetClass, signature.getName(),
signature.getMethod().getParameterTypes());
if (method == null) {
throw new IllegalStateException("无法解析目标方法: " + signature.getMethod().getName());
}
return method;
}
/**
* 获取堆栈信息
*/
public static String getStackTrace(Throwable throwable) {
StringWriter sw = new StringWriter();
try (PrintWriter pw = new PrintWriter(sw)) {
throwable.printStackTrace(pw);
return sw.toString();
}
}
private Method getDeclaredMethod(Class<?> clazz, String name, Class<?>... parameterTypes) {
try {
return clazz.getDeclaredMethod(name, parameterTypes);
} catch (NoSuchMethodException e) {
Class<?> superClass = clazz.getSuperclass();
if (superClass != null) {
return getDeclaredMethod(superClass, name, parameterTypes);
}
}
return null;
}
/**
* 获取请求参数
* @param args
* @param request
* @return
*/
private String getArgs(Object[] args, HttpServletRequest request) {
String strArgs = StringPool.EMPTY;
try {
if (!request.getContentType().contains("multipart/form-data")) {
strArgs = JSONObject.toJSONString(args);
}
} catch (Exception e) {
try {
strArgs = Arrays.toString(args);
} catch (Exception ex) {
log.warn("解析参数异常", ex);
}
}
return strArgs;
}
}复制
2.4.编写自动配置类AutoConfig
@ConditionalOnProperty(prefix = "zbbmeta.log" ,value = "enabled" ,matchIfMissing = true): matchIfMissing属性:默认情况下matchIfMissing为false,也就是说如果未进行属性配置,则自动配置不生效。如果matchIfMissing为true,则表示如果没有对应的属性配置,则自动配置默认生效
/**
* @author springboot葵花宝典
* @description: TODO
*/
@Configuration
@EnableConfigurationProperties({LogProperties.class})
@ConditionalOnProperty(prefix = "zbbmeta.log" ,value = "enabled" ,matchIfMissing = true)
public class LogAutoConfig {
@Bean
public LogAspect logAspect(){
return new LogAspect();
}
}复制
2.5 编写自动配置类
在resources
目录下创建MERA-INF.spring
文件夹并创建文件org.springframework.boot.autoconfigure.AutoConfiguration.imports
,进行配置
注意创建文件的时候是META-INF/spring
com.zbbmeta.config.LogAutoConfig
复制

3 创建SpringBoot项目进行测试
创建名称为day25-sprongboot-starter-test
项目进行测试自定义日志Satrter

<dependencies>
<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>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.zbbmeta</groupId>
<artifactId>zbblog-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>复制
3.1在application.yml中进行配置
zbbmeta:
log:
enabled: true # 表示开启自定义日志starter复制
3.2 创建Controller
/**
* @author springboot葵花宝典
* @description: TODO
*/
@RestController
@RequestMapping("/students")
public class StudentController {
@GetMapping
public String getAll(){
return "student log test";
}
}复制
启动测试
访问http://localhost:8080/students
显示结果如下:
[nio-8080-exec-1] com.zbbmeta.aspect.LogAspect : Http Request: {"createBy":"springboot葵花宝典","executeTime":657,"ip":"127.0.0.1","location":" 本机地址","method":"GET","operation":"student log test","params":"\"[]\"","url":"/students"}
复制
关闭日志starter
zbbmeta:
log:
enabled: false # 表示关闭自定义日志starter复制
重新启动项目,访问发现控制台就没有我们自定义的日志了。

如果您觉得本文不错,欢迎关注,点赞,收藏支持,您的关注是我坚持的动力!
原创不易,转载请注明出处,感谢支持!如果本文对您有用,欢迎转发分享!