即使有一天,我放弃了自己的身体,也请你,不要放弃我,我亲爱的灵魂.
上一章简单介绍了 SpringBoot自定义Starter(二十四),如果没有看过,请观看上一章
一. AOP 实现日志功能
关于 AOP 切面的知识, 可以看:
云深i不知处 前辈的文章: 切面AOP实现权限校验:实例演示与注解全解
我们在 上一章节的 StarterApply 项目中 添加 切面实现日志的功能
一.一 pom.xml 添加依赖
<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>top.yueshushu</groupId>
<artifactId>starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--添加aop 的依赖信息-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--添加json-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.68</version>
</dependency>
</dependencies>
一.二 HelloController 中添加方法
HelloApplication.java 是普通的启动类.
HelloController 添加三个简单的方法
@RestController
public class HelloController {
// 无参
@GetMapping("/")
public OutputResult toHello(){
return OutputResult.success("无参数响应");
}
//相加
@GetMapping("/add/{a}/{b}")
public OutputResult add(@PathVariable("a") int a, @PathVariable("b") int b){
System.out.println("进行添加");
return OutputResult.success(a+b);
}
//可能会出现异常的方法
@GetMapping("/div/{a}/{b}")
public OutputResult div(@PathVariable("a") int a, @PathVariable("b") int b){
return OutputResult.success(a/b);
}
}
方法可以正常的访问.
一.三 日志切面 LogAspect
package top.yueshushu.learn.aop;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.InputStreamSource;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
/**
* 可以使用
* @date 2021/10/25 10:53
* @author zk_yjl
*/
@Slf4j
@Aspect //定义切面的注解
@Component
@Order(1) // 顺序是第一个
public class LogAspect {
/**
* 异常,输出完整的stack trace
*/
private boolean printFullStackTraceForException = true;
/**
* (输入输出)参数最大输出长度. -1表示不限制
*/
private int paramMaxPrintLength = 20000;
//定义多个切点的位置, 用 || 分隔
@Pointcut("(execution(public * top.yueshushu.learn.controller.*.*(..))) " +
"|| (execution(public * top.yueshushu.learn.controller2.*.*(..)))")
public void log(){
}
@Before("log()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
}
@AfterReturning(value = "log()", returning = "ret")
public void doAfterReturning(Object ret) throws Throwable {
}
//主要是这一个
@Around("log()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
String invokeMethodFullPath = buildInvokeMethodFullPath(joinPoint);
String requestParams = buildRequestParams(joinPoint);
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
String method = Optional.ofNullable(attributes).map(attr -> attr.getRequest().getMethod()).orElse(null);
StringBuilder logInfo = new StringBuilder();
logInfo.append("request method: ").append(invokeMethodFullPath).append("; ");
logInfo.append("request type: ").append(method).append("; ");
logInfo.append("request param: ").append(requestParams).append("; ");
long startMs = System.currentTimeMillis();
try {
Object result = joinPoint.proceed();
long cost = System.currentTimeMillis() - startMs;
logInfo.insert(0, "cost(ms): " + cost + "; ");
logInfo.append(" ----- response: ").append(toJsonString(result));
log.info(logInfo.toString());
return result;
} catch (Throwable throwable) {
long cost = System.currentTimeMillis() - startMs;
logInfo.insert(0, "cost(ms): " + cost + "; ");
if (printFullStackTraceForException) {
log.error("error. " + logInfo.toString(), throwable);
} else {
log.error("error. " + throwable.getMessage() + "; " + logInfo.toString());
}
throw throwable;
}
}
private String toJsonString(Object result) {
String json = JSON.toJSONString(result);
if (paramMaxPrintLength <= 0) {
return json;
}
if (json.length() > paramMaxPrintLength) {
return json.substring(0, paramMaxPrintLength) + "...";
}
return json;
}
private String buildRequestParams(ProceedingJoinPoint point) {
try {
Map<String, Object> requestP = new LinkedHashMap<>();
Method m = ((MethodSignature) point.getSignature()).getMethod();
Parameter[] parameters = m.getParameters();
for (int i = 0, iLen = parameters.length; i < iLen; i++) {
//过滤Request、Response or InputStreamSource对象,防止序列化异常
Object arg = point.getArgs()[i];
if (null == arg) {
continue;
}
if (arg instanceof HttpServletRequest
|| arg instanceof HttpServletResponse
|| arg instanceof InputStreamSource
|| arg instanceof Errors) {
continue;
}
requestP.put(parameters[i].getName(), arg);
}
// 提前构造入参信息,防方法内修改入参对象,异常时再构造入参会不准
return toJsonString(requestP);
} catch (Exception e) {
log.warn("请求参数构造失败. error msg: " + e.getMessage());
return "build error";
}
}
private String buildInvokeMethodFullPath(ProceedingJoinPoint point) {
Signature signature = point.getSignature();
Class<?> targetClass = point.getTarget().getClass();
// 执行方法的路径
return targetClass.getSimpleName() + " " + signature.getName();
}
}
一.四 测试
输入网址: http://localhost:8081/Log/add/1/2
可以发现,日志输出打印了
输入网址: http://localhost:8081/Log/div/2/1
输入网址 : http://localhost:8081/Log/div/2/0
可以发现,切面日志是正常工作的.
接下来,将 切面日志做成 自定义Starter 的方式.
二. 自定义 日志Starter
一般都是采用 注解的方式, 哪个方法上添加了相应的注解,就对哪个方法进行日志处理.
二.一 注解 MyLog
MyLog.java
// 适用于方法上
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyLog {
String module() default "默认模块";
String optType() default "默认类型";
String description() default "默认说明";
}
二.二 日志展示信息 LogVo
package top.yueshushu.log;
import lombok.Data;
import java.io.Serializable;
/**
* @ClassName:LogVo
* @Description 自定义日志的输出展示对象
* @Author zk_yjl
* @Date 2021/10/25 10:22
* @Version 1.0
* @Since 1.0
**/
@Data
public class LogVo implements Serializable {
/**
@param className 请求的类
@param methodName 请求的方法名称
@param params 请求的参数
@param returnValue 返回值
@param model 模块 从Log 注解里面拿
@param optType 操作类型 从Log 注解里面拿
@param description 操作说明 从 Log 注解里面拿
@param reqUrl 请求的路径
@param reqIp 请求的ip地址
@param reqTime 请求的时间
@param execTime 执行的时长
@param excName 异常名称
@param excInfo 异常的信息
*/
private String className;
private String methodName;
private String params;
private String returnValue;
private String model;
private String optType;
private String description;
private String reqUrl;
private String reqIp;
private String reqTime;
private Long execTime;
private String excName;
private String excInfo;
// ... 其他后期扩展字段
@Override
public String toString() {
return "LogVo{" +
"className='" + className + '\'' +
", methodName='" + methodName + '\'' +
", params='" + params + '\'' +
", returnValue='" + returnValue + '\'' +
", model='" + model + '\'' +
", optType='" + optType + '\'' +
", description='" + description + '\'' +
", reqUrl='" + reqUrl + '\'' +
", reqIp='" + reqIp + '\'' +
", reqTime=" + reqTime +
", execTime=" + execTime +
", excName='" + excName + '\'' +
", excInfo='" + excInfo + '\'' +
'}';
}
}
接下来,就跟前面的自定义 Starter 差不多了.
二.三 自定义参数配置 MyLogProperties
MyLogProperties.java
package top.yueshushu.log;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @ClassName:MyLogProperties
* @Description 日志配置类
* @Author zk_yjl
* @Date 2021/10/25 17:07
* @Version 1.0
* @Since 1.0
**/
@ConfigurationProperties("mylog")
public class MyLogProperties {
/**
定义默认的信息
*/
public static final Long DEFAULT_RUNTIME=0L;
public static Boolean DEFAULT_EXC_FULL_SHOW=true;
public static Integer DEFAULT_RESULT_LENGTH=0;
/**
* @runTime 方法的运行时长 当方法的运行时间> 设置的值时,才记录。 默认为0
*/
private Long runTime=DEFAULT_RUNTIME;
/**
* @excFullShow 异常的信息 是否全部展示 保存
*/
private Boolean excFullShow=DEFAULT_EXC_FULL_SHOW;
/**
* @resultLength 输出结果的长度 0 表示全部输出
*/
private Integer resultLength=DEFAULT_RESULT_LENGTH;
// ...... 其他的默认的信息,后期可以补充其他的
// ... 构造方法和默认的 setter, gett方法
}
二.四 定义服务 Service
日志的处理,可以单独的打印到控制台,可以放置到数据库里面,也可以输出到文件里面。
这个 定义一个接口和 默认的实现
二.四.一 日志接口 LogService
public interface LogService {
/**
* 日志处理
* @date 2021/10/25 19:56
* @author zk_yjl
* @param
* @return void
*/
public void logHandler(LogVo logVo);
}
二.四.二 默认的日志接口实现 DefaultLogServiceImpl
打印到控制台
@Log4j2
public class DefaultLogServiceImpl implements LogService{
/**
* 默认的日志实现,打印到控制台
* @date 2021/10/29 17:53
* @author zk_yjl
* @param logVo
* @return void
*/
@Override
public void logHandler(LogVo logVo) {
log.info("默认处理日志:>>>"+logVo);
}
}
二.五 服务配置 LogConfiguration
@Configuration
@EnableConfigurationProperties(MyLogProperties.class)
public class LogConfiguration {
@Bean
public MyLogProperties myLogProperties(){
return new MyLogProperties();
}
/**
外界没有 LogService 的实现时,用默认的
*/
@Bean
@ConditionalOnMissingBean
public LogService getLogService(){
return new DefaultLogServiceImpl();
}
/**
* 创建切面
* @date 2021/10/29 17:57
* @author zk_yjl
* @param myLogProperties
* @param logService
* @return top.yueshushu.log.LogAspect
*/
@Bean
public LogAspect logAspect(MyLogProperties myLogProperties,LogService logService){
return new LogAspect(myLogProperties,logService);
}
}
二.六 切面配置 LogAspect
package top.yueshushu.log;
import com.alibaba.fastjson.JSONObject;
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.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
@Aspect
/**
* 自定义日志输出AOP切面,定义以添加了MyLog注解的所有方法作为连接点,
* 这些连接点触发时定义对应正常方法返回时通知以及异常发生时通知
*/
public class LogAspect{
private LogService logService;
private MyLogProperties mylogProperties;
public LogAspect(MyLogProperties myLogProperties,LogService logService){
this.logService=logService;
this.mylogProperties=myLogProperties;
}
/**
* 切点连接点:在MyLog注解的位置切入 和 controller 下面进行配置
*/
@Pointcut(value ="(@annotation(top.yueshushu.log.MyLog)) ||(execution(public * *..controller.*.*(..))))")
public void doMyLogCut() {
}
/**
* MyLog注解方法执行 Around 触发事件
* @param joinPoint
* @param
*/
@Around(value = "doMyLogCut()")
public Object logInvoke(ProceedingJoinPoint joinPoint) throws Throwable{
//记录一下时间,
long beginTime = System.currentTimeMillis();
Object keys = joinPoint.proceed();
long time = System.currentTimeMillis() - beginTime;
LogVo myLogVO = this.getMyLog(joinPoint, keys,null);
myLogVO.setExecTime(time);
/**
运行的时间长 才执行操作
*/
if(mylogProperties.getRunTime()<=time){
logService.logHandler(myLogVO);
}
return keys;
}
/**
* 异常发生时的通知
* @param joinPoint
* @param e
*/
@AfterThrowing(pointcut = "doMyLogCut()", throwing = "e")
public void doExceptionMyLog(JoinPoint joinPoint, Throwable e) {
LogVo myLogVO = this.getMyLog(joinPoint, null,e);
//出现异常,执行时间为 -1
myLogVO.setExecTime(-1L);
// 异常的,一直都进行操作.
logService.logHandler(myLogVO);
}
/**
* 获取输出日志实体
* @param joinPoint 触发的连接点
* @param e 异常对象
* @return MyLogVO
*/
private LogVo getMyLog(JoinPoint joinPoint,Object keys,Throwable e){
// 获取RequestAttributes
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
// 从获取RequestAttributes中获取HttpServletRequest的信息
HttpServletRequest request = (HttpServletRequest) requestAttributes
.resolveReference(RequestAttributes.REFERENCE_REQUEST);
// 输出日志VO
LogVo myLogVO = new LogVo();
try {
// 从切面织入点处通过反射机制获取织入点处的方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 获取切入点所在的方法
Method method = signature.getMethod();
// 获取操作
MyLog opLog = method.getAnnotation(MyLog.class);
if (opLog != null) {
myLogVO.setModel(opLog.module());
myLogVO.setOptType(opLog.optType());
myLogVO.setDescription(opLog.description());
}
// 获取请求的类名
String className = joinPoint.getTarget().getClass().getName();
myLogVO.setClassName(className);
// 获取请求的方法名
String methodName = method.getName();
myLogVO.setMethodName(methodName);
//请求uri
String uri = request.getRequestURI();
myLogVO.setReqUrl(uri);
myLogVO.setReqIp(getIpAddr(request));
//操作时间点
myLogVO.setReqTime(getNowDate());
//异常名称+异常信息
if(null != e){
myLogVO.setExcName(e.getClass().getName());
myLogVO.setExcInfo(stackTraceToString(e.getClass().getName(), e.getMessage(), e.getStackTrace()));
}
//请求的参数,参数所在的数组转换成json
String params = Arrays.toString(joinPoint.getArgs());
myLogVO.setParams(params);
//返回值
if(null != keys && Void.class.getName() != keys){
StringBuilder result =new StringBuilder( JSONObject.toJSONString(keys));
if(mylogProperties.getResultLength()==0){
//表示全部
myLogVO.setReturnValue(result.toString());
}else{
String tempResult=result.substring(0,mylogProperties.getResultLength());
myLogVO.setReturnValue(tempResult);
}
}
//输出日志
} catch (Exception ex) {
// ex.printStackTrace();
}
return myLogVO;
}
/**
* 转换异常信息为字符串
* @param exceptionName
* @param exceptionMessage
* @param elements
* @return
*/
private String stackTraceToString(String exceptionName, String exceptionMessage, StackTraceElement[] elements) {
StringBuffer strbuff = new StringBuffer();
if(mylogProperties.getExcFullShow()){
for (StackTraceElement stet : elements) {
strbuff.append(stet + "\n");
}
return exceptionName + ":" + exceptionMessage + "\n\t" + strbuff.toString();
}
return exceptionName+":"+exceptionMessage;
}
/**
* 获取当前的时间
* @date 2021/10/26 9:29
* @author zk_yjl
* @param
* @return java.lang.String
*/
private String getNowDate(){
Date now=new Date();
SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return simpleDateFormat.format(now);
}
/**
* 获取访问者的ip地址
* 注:要外网访问才能获取到外网地址,如果你在局域网甚至本机上访问,获得的是内网或者本机的ip
*/
public static String getIpAddr(HttpServletRequest request) {
String ipAddress = null;
try {
//X-Forwarded-For:Squid 服务代理
String ipAddresses = request.getHeader("X-Forwarded-For");
if (ipAddresses == null || ipAddresses.length() == 0 ||
"unknown".equalsIgnoreCase(ipAddresses)) {
//Proxy-Client-IP:apache 服务代理
ipAddresses = request.getHeader("Proxy-Client-IP");
}
if (ipAddresses == null || ipAddresses.length() == 0 ||
"unknown".equalsIgnoreCase(ipAddresses)) {
//WL-Proxy-Client-IP:weblogic 服务代理
ipAddresses = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddresses == null || ipAddresses.length() == 0 ||
"unknown".equalsIgnoreCase(ipAddresses)) {
//HTTP_CLIENT_IP:有些代理服务器
ipAddresses = request.getHeader("HTTP_CLIENT_IP");
}
if (ipAddresses == null || ipAddresses.length() == 0 ||
"unknown".equalsIgnoreCase(ipAddresses)) {
//X-Real-IP:nginx服务代理
ipAddresses = request.getHeader("X-Real-IP");
}
//有些网络通过多层代理,那么获取到的ip就会有多个,一般都是通过逗号(,)分割开来,并且第一个ip为客户端的真实IP
if (ipAddresses != null && ipAddresses.length() != 0) {
ipAddress = ipAddresses.split(",")[0];
}
//还是不能获取到,最后再通过request.getRemoteAddr();获取
if (ipAddress == null || ipAddress.length() == 0 ||
"unknown".equalsIgnoreCase(ipAddresses)) {
ipAddress = request.getRemoteAddr();
}
} catch (Exception e) {
ipAddress = "";
}
return ipAddress;
}
}
这上面就是一个简单的日志切面配置信息。
二.七 测试前准备
将 yjlLog 项目 通过 maven clean install 安装到本地仓库.
二.七.一 StarterApply 项目 pom.xml 添加依赖
<!--添加自定义日志的依赖信息-->
<dependency>
<groupId>top.yueshushu</groupId>
<artifactId>yjlLog</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
二.七.二 准备 controller验证 HelloController
package top.yueshushu.learn.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import top.yueshushu.learn.response.OutputResult;
import top.yueshushu.log.MyLog;
/**
* @ClassName:HelloController
* @Description 普通的Controller 方法
* @Author zk_yjl
* @Date 2021/4/9 20:55
* @Version 1.0
**/
//1. 添加了一个RestController的注解
@RestController
public class HelloController {
// 无参
@GetMapping("/")
public OutputResult toHello(){
return OutputResult.success("无参数响应");
}
//相加
@GetMapping("/add/{a}/{b}")
public OutputResult add(@PathVariable("a") int a, @PathVariable("b") int b){
System.out.println("进行添加");
return OutputResult.success(a+b);
}
//可能会出现异常的方法
@GetMapping("/div/{a}/{b}")
@MyLog(module = "测试Controller",optType = "测试",description = "可能有异常的情况")
public OutputResult div(@PathVariable("a") int a, @PathVariable("b") int b){
return OutputResult.success(a/b);
}
}
二.八 验证
二.八.一 默认值验证
输入网址: http://127.0.0.1:8081/Log/add/1/0
输入可能有异常的情况: http://127.0.0.1:8081/Log/div/2/1
输入异常信息: http://127.0.0.1:8081/Log/div/2/0
有异常了.
二.八.二 自定义值验证
在 application.yml 配置文件中,进行相应的配置
#配置日志
mylog:
run-time: 0 #运行时间为0,暂时不设置大值
result-length: 10 # 结果就返回10
exc-full-show: false # 不展示全异常
输入可能有异常的情况: http://127.0.0.1:8081/Log/div/2/1
但是实际返回值,是正常的.
输入异常信息: http://127.0.0.1:8081/Log/div/2/0
处理运行的时间
将 mylog.run-time 时间设置成 100 时,无任何日志打印输出
设置成 1时,有相应的日志打印输出
说明日志 starter 是成功的.
二.九 自定义日志处理方式 LogConfig
现在的日志处理方式,是默认的打印方式. 使用者可以进行自定义,如放置到数据库里面.
在 StarterApply 项目时,进行配置
package top.yueshushu.learn.config;
import org.springframework.context.annotation.Configuration;
import top.yueshushu.log.LogService;
import top.yueshushu.log.LogVo;
/**
* @ClassName:LogConfig
* @Description 自定义配置日志处理方式
* @Author zk_yjl
* @Date 2021/10/25 20:12
* @Version 1.0
* @Since 1.0
**/
@Configuration //添加注解
public class LogConfig implements LogService {
//注入数据 Mapper, 进行处理.
@Override
public void logHandler(LogVo logVo) {
//一个简单的输出,让自定义生效
System.out.println(">>>将其写入到数据库里面,内容是:"+logVo);
}
}
进行自定义接口验证 ( mylog.run-time 设置值为1)
输入网址: http://127.0.0.1:8081/Log/add/2/2
三. EnableXxx 形式处理 日志Starter
现在我们导入了 日志Starter, 但是我不想使用它,那么该如何处理呢? (现在是只要导入了日志依赖,就处理日志)。
SpringBoot 里面,有大量的 @EnableXxx 的注解形式,
@EnableCaching 添加了,就可以进行缓存 cache,不添加,不可以进行缓存。
我们可以使用到上一章节里面的第三步内容 , 实现动态的热插拔
- 动态的进行配置,热插拔效果。达到 我们拥有这个自定义starter,就拥有这些东西,没有自定义starter,就没有这些东西的效果。
这就用到了 @ConditionalOnClass 注解
均在 自定义的 LogStarter 里面进行处理.
三.一 定义标识类的类 LogMarkerConfiguration
LogMarkerConfiguration.java
package top.yueshushu.log;
/**
* @ClassName:LogMarkerConfiguration
* @Description TODO
* @Author zk_yjl
* @Date 2021/10/25 17:42
* @Version 1.0
* @Since 1.0
**/
public class LogMarkerConfiguration {
}
三.二 注解里面引入这个标识类 EnableMyLog
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(LogMarkerConfiguration.class) //添加了这一个
public @interface EnableMyLog {
}
三.三 Configuration 里面进行配置
@Configuration
@EnableConfigurationProperties(MyLogProperties.class)
@ConditionalOnBean(LogMarkerConfiguration.class) // 添加这一个
public class LogConfiguration {
// 后面均一样
}
三.四 测试运行
将 自定义的 starter 重新 maven clean install .
重新运行项目,访问: http://127.0.0.1:8081/Log/add/2/2
发现,并没有日志输出
在启动类上 添加 @EnableMyLog 注解
@SpringBootApplication
@EnableMyLog //添加注解
public class HelloApplication {
}
重新访问,这个时候,控制台便可以打印输出日志信息了
实现了动态的热插拔的效果.
本章节的代码放置在 github 上:
https://github.com/yuejianli/springboot/tree/develop/yjlLog
https://github.com/yuejianli/springboot/tree/develop/StarterApply
谢谢您的观看,如果喜欢,请关注我,再次感谢 !!!