SpringAOP详解,使用SpringAop实现统一日志处理,异常处理
SpringAOP详解,使用SpringAop实现统一日志处理,异常处理
阅读本文内容之前需要先了解java动态代理的实现。java动态代理实例
以下内容为本人原创>>>>>>>>>>>
Spring AOP的主要概念:
目标代码(或称目标方法、被代理类、被代理方法):原本的业务代码。
切点表达式(或称切面表达式):切点表达式过滤的规则,筛选范围,即哪些目标代码才需要切。
切点代码(或称切面代码、连接点、代理方法):在原本的业务代码上包裹的切面逻辑。
Spring AOP注解
@Aspect
切面声明,标注在类、接口(包括注解类型)或枚举上,表示我这个类要切别人,或者我这个类是要代理别人的,要准备搞事了。
例子:
@Aspect//我要开始搞事了
public class MyAspect {
@Around("@target(org.springframework.stereotype.Controller)")
public void cutCode(ProceedingJoinPoint pjp){
//...我真正搞事的代码
}
}
@Pointcut
切点声明,可以理解为就是一个包含切点表达式的变量,为了避免@Before、@After等真实切点代码重复定义切点表达式。只是图个方便,不是必需品。
例子:
@Aspect//我要开始搞事了
public class MyAspect {
@Pointcut("@target(org.springframework.stereotype.Controller)")
public void myPoincut(){
//这是只是喊口号,具体事务啥也不干
}
//@Around("@target(org.springframework.stereotype.Controller)")
@Around(value="myPoincut()")//响应myPoincut方法的口号
public void cutCode(ProceedingJoinPoint pjp){
//...我真正搞事的代码
}
}
@Before
前置通知,在目标方法(切入点)执行之前执行,接收JoinPoint参数。
value属性绑定通知的切入点表达式,可以关联切点声明(@Pointcut(切点表达式) myPoincut(){}
,@Before(value="myPoincut()") myBefore(JoinPoint jp){}
),也可以直接设置切入点表达式(@Before("切点表达式")
)。
注意:如果在此回调方法中抛出异常,则目标方法不会再执行,会继续执行后置通知和异常通知。
例子:
@Aspect//我要开始搞事了
public class MyAspect {
@Pointcut("@target(org.springframework.stereotype.Controller)")
public void myPoincut(){
//这是只是喊口号,具体事务啥也不干
}
//@Before("@target(org.springframework.stereotype.Controller)")
@Before(value="myPoincut()")//响应myPoincut方法的口号
public void myBefore(JoinPoint jp){
//...我真正搞事的代码,如果此处发生异常,那么目标方法不会再执行
}
}
@After
后置通知,在目标方法执行之后执行,接收JoinPoint参数。
value属性绑定通知的切入点表达式,可以关联切点声明(@Pointcut(切点表达式) myPoincut(){}
,@After(value="myPoincut()") myAfter(JoinPoint jp){}
),也可以直接设置切入点表达式(@After("切点表达式")
)。
例子:
@Aspect//我要开始搞事了
public class MyAspect {
@Pointcut("@target(org.springframework.stereotype.Controller)")
public void myPoincut(){
//这是只是喊口号,具体事务啥也不干
}
//@After("@target(org.springframework.stereotype.Controller)")
@After(value="myPoincut()")//响应myPoincut方法的口号
public void myAfter(JoinPoint jp){
//...我真正搞事的代码
}
}
@AfterReturning
返回通知,在目标方法返回结果之后,并且在@After
切点执行之后执行。
该注解有四个属性pointcut
,value
,returning
,argNames
,pointcut和value属性都是绑定通知的切入点表达式,不过pointcut优先级高于value。returning属性则是使用了命名绑定模式(下文有介绍),定义返回值类型并接收返回值。argNames属性使用了命名绑定模式,定义参数类型、个数和顺序,和args
(下文有介绍)效果一样,只是argNames优先级高于args。
注意:如果目标方法返回原生类型,则会报错,SpringAop不会自动给返回值装箱。
例子:
@Aspect//我要开始搞事了
public class MyAspect {
@Pointcut("@target(org.springframework.stereotype.Controller)")
public void myPoincut(){
//这是只是喊口号,具体事务啥也不干
}
//@AfterReturning(returning = "result",value="@target(org.springframework.stereotype.Controller)")
@AfterReturning(returning = "result",pointcut="myPoincut()")//响应myPoincut方法的口号
public void myAfterReturning(JoinPoint jp,Object result){//result是目标方法的返回值,如果目标方法返回原生类型,则会报错,SpringAop不会自动给返回值装箱。
//...我真正搞事的代码
}
}
@AfterThrowing
异常通知,在目标方法抛出异常之后执行,意味着如果此通知被执行,则@AfterReturning
不会被执行。
此注解有一个throwing属性,使用了命名绑定模式(下文有介绍),定义异常类型并接收异常对象。
注意:
1、如果目标方法自己try- catch了异常,而没有继续往外抛,则不会进入此通知。
2、@AfterThrowing虽然处理异常,但它不会阻止异常传播到上一级调用者,如果没有catch,则会导致jvm终止。
例子:
@Aspect//我要开始搞事了
public class MyAspect {
@Pointcut("@target(org.springframework.stereotype.Controller)")
public void myPoincut(){
//这是只是喊口号,具体事务啥也不干
}
//@AfterThrowing(throwing = "e",value="@target(org.springframework.stereotype.Controller)")
@AfterThrowing(throwing = "e",pointcut="myPoincut()")//响应myPoincut方法的口号
public void myAfterThrowing(JoinPoint jp,Throwable e){//e是目标方法的真实发生异常后抛出的异常对象。
//...我真正搞事的代码
}
}
@Around
环绕通知:目标方法执行前后分别执行一些代码,接收ProceedingJoinPoint参数,可以控制目标方法是否继续执行。通常用于统计方法耗时,参数校验等操作。
环绕通知早于前置通知,晚于返回通知。
例子:
@Aspect//我要开始搞事了
public class MyAspect {
@Pointcut("@target(org.springframework.stereotype.Controller)")
public void myPoincut(){
//这是只是喊口号,具体事务啥也不干
}
//@Around("@target(ctr)")//可以使用“命名绑定模式”定义切点表达式
//public void cutCode(ProceedingJoinPoint pjp,org.springframework.stereotype.Controller ctr){//可以使用“命名绑定模式”定义切点表达式
@Around(value="myPoincut()")//响应myPoincut方法的口号
public Object cutCode(ProceedingJoinPoint pjp){
//...我真正搞事的代码,前部分,这部分代码在@Before之前执行
Object result = pjp.proceed();//调用目标方法
//...我真正搞事的代码,后部分,这部分代码在@After之后执行
return result;
}
}
顺序总结
真实方法无异常:
@Around=>@Before=>真实方法=>@After=>@AfterReturning=>@Around
真实方法有异常:
@Around=>@Before=>真实方法=>@After=>@AfterThrowing=>@Around
以上内容为本人原创>>>>>>>>>>>
切点表达式
以下内容抄录自:最全 SpringAOP 切面表达式,内容我有细微改动和补充。
概述
切点表达式即PCD(pointcut designators ),SpringAOP的PCD是完全兼容AspectJ,一共有10种。
通配符
*
任意,不限制。
..
0个或多个项,主要用于类名匹配式
和参数匹配式
中,如果用于类名匹配式中,则表示匹配当前包及其子包,如果用于参数匹配式中,则表示匹配0个或多个参数。
运算符
切面表达式支持&&
、||
、!
这种逻辑操作,表示将多个表达式按照逻辑与、逻辑非、逻辑或的规则拼接起来。
&&
左右两个表达式同时满足(不是短路与)。
||
左右两个表达式任意满足一个(不是短路或)。
!
非,取反。
execution(* com.xxx.spring.demo..*.login(java.lang.String,..))&&execution(* com.xxx.spring.demo2..*.test(java.lang.String,..))
命名绑定模式
命名绑定模式,就是在表达式中写上变量名,在方法上对变量名的类型进行限定。
@Around("within(per.aop.*) && args(str)")//在表达式中写上变量名str
public Object logAspect(ProceedingJoinPoint pjp, String str) {//在方法上对变量名str的类型进行限定,这里限定为String类型
...
}
如上"within(per.aop.*) && args(str)"
,str必须是String类型或其子类,且方法入参只能有一个。
命名绑定模式只支持target、this、args三种PCD表达式。
execution
execution是最常用的PCD。它的匹配式模板如下展示:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
execution(修饰符匹配式? 返回类型匹配式 类名匹配式? 方法名匹配式(参数匹配式) 异常匹配式?)
代码块中带?符号的匹配式都是可选的,对于execution PCD必不可少的只有三个:
返回值匹配值、方法名匹配式、参数匹配式
举例分析: execution(public * ServiceDemo.*(..))
匹配public修饰符,返回值是*,即任意返回值类型都行,ServiceDemo是类名匹配式不一定要全路径,只要全局依可见性唯一就行,.*是方法名匹配式,匹配所有方法,…是参数匹配式,匹配任意数量、任意类型参数的方法。
栗子:
//匹配com.xyz.service及其子包下的任意方法
execution(* com.xyz.service..*.*(..))
//匹配任意名字为joke的方法,且其动态入参是是Object类型或该类的子类
execution(* joke(Object+)))
//匹配任意名字为joke的方法,该方法 一个入参为String(不可以为子类),后面可以有任意个入参且入参类型不限
execution(* joke(String,..))
//匹配指定包下find开头的方法
execution(* com..*.*Dao.find*(..))
//匹配com.baobaotao包下Waiter及其子类的所有方法
execution(* com.baobaotao.Waiter+.*(..))
//以下示例摘录自:https://blog.csdn.net/u012156858/article/details/108429285
//匹配使用public修饰,返回值为任意类型,并且是com.xxx.spring.demo.LoginService类中名称为login的方法,方法包含两个参数,都是String类型。
execution(public * com.xxx.spring.demo.LoginService.login(java.lang.String,java.lang.String))
//对任何类的任何返回值的任何方法都有效
execution( * *.*(..))
//匹配使用public修饰,返回值为任意类型,并且是com.xxx.spring.demo包下任意类中名称为login的方法,方法包含两个参数,第一个参数类型是String 第二个参数任意
execution(public * com.xxx.spring.demo.*.login(java.lang.String,*))
//匹配使用public修饰,返回值为任意类型,并且是com.xxx.spring.demo包下任意类中名称为login的方法,方法包含多个参数,第一个参数类型是String 后面的参数任意
execution(public * com.xxx.spring.demo.*.login(java.lang.String,..))
//匹配使用public修饰,返回值为任意类型,并且是com.xxx.spring.demo包及其子包下任意类中名称为login的方法,方法包含多个参数,第一个参数类型是String 后面的参数任意
execution(public * com.xxx.spring.demo..*.login(java.lang.String,..))
//与上面示例完全一样 权限修饰符可写可不写 默认就是public
execution(* com.xxx.spring.demo..*.login(java.lang.String,..))
//以上示例摘录自:https://blog.csdn.net/u012156858/article/details/108429285
within
筛选出某包下的所有类,注意要带有*
。
@Pointcut("within(com.abc.service.*)")//com.abc.service包下的所有类,不包括子包下的类。
public void myPointcut1()
{
}
@Pointcut("within(com.xyz.service..*)")//com.xyz.service包下及其子包下的类
public void myPointcut2()
{
}
target
target作用于目标对象,即被代理对象(请先了解代理模式中代理对象和被代理对象)需要实现哪些接口,可以通过target来定义。常用于命名绑定模式,对被代理对象的类型进行过滤筛选。this和target的实际作用非常相似。
@Pointcut("target(mys)")//被代理类是MyService接口的实现
public void myPointcut1(MyService mys)
{
}
@Pointcut("target(mys)")//被代理类是MyServiceImpl类或者是MyServiceImpl的子类
public void myPointcut2(MyServiceImpl mys)
{
}
this
this作用于代理对象,即生成的代理对象(请先了解代理模式中代理对象和被代理对象)需要实现哪些接口,可以通过this来定义。常用于命名绑定模式,对被代理对象(我没写错,它和target的真实目的都是过滤被代理对象)的类型进行过滤筛选。this和target的实际作用非常相似。
如果目标类是基于接口实现的,则this()中可以填该接口的全路径名,目标类是基于CGLIB实现的,则this中可以填写目标类的全路径名。
使用@EnableAspectJAutoProxy(proxyTargetClass = true)
可以强制使用CGLIB。否则默认首先使用jdk动态代理,jdk代理不了才会用CGLIB。
@Pointcut("this(mys)")//代理类是MyService接口的实现(也就是说明被代理类也必须这样,绕了一层后,其实最终目的还是为了筛选被代理类)
public void myPointcut1(MyService mys)
{
}
@Pointcut("this(mys)")//代理类是MyServiceImpl类或者是MyServiceImpl的子类(也就是说明被代理类也必须这样,绕了一层后,其实最终目的还是为了筛选被代理类)
public void myPointcut2(MyServiceImpl mys)
{
}
args
常用于对目标方法的参数匹配。一般不单独使用,而是配合其他PCD来使用。args可以使用命名绑定模式,如下举例:
@Aspect // 切面声明@Component // 注入IOC
@Slf4jclass
AspectDemo {
@Around("within(per.aop.*) && args(str)") // 在per.aop包下,且被代理方法的只有一个参数,参数类是String或者其子类
@SneakyThrowspublic
Object logAspect(ProceedingJoinPoint pjp, String str) {
String signature = pjp.getSignature().toString();
log.info("{} start,param={}", signature, pjp.getArgs());
Object res = pjp.proceed();
log.info("{} end", signature);return res;
}
}
1.如果args中是参数名,则配合切面(advice)
方法的使用来确定要匹配的方法参数类型。
2.如果args中是类型,例如@Around("within(per.aop.*) && args(String)")
,则可以不必使用切面方法来确定类型,但此时也不能使用参数绑定了见下文了。
虽然args()支持+符号,但本身args()就支持子类通配。
和带参数匹配execution区别
举个例子: args(com.xgj.Waiter)
等价于 execution(* *(com.xgj.Waiter+))
。而且execution不能支持带参数的advice。
@annotation
@annotation
属于方法名匹配式,指示筛选指定注解的方法作为被代理方法。
例子:
@Aspect//我要开始搞事了
public class MyAspect {
@Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)")//方法上有@RequestMapping注解的需要代理
public void myPoincut(){
}
}
@target
@target
属于类名匹配式,指示筛选指定注解的类作为被代理类。
例子:
@Aspect//我要开始搞事了
public class MyAspect {
@Pointcut("@target(org.springframework.stereotype.Controller)")//类上有@Controller注解的需要代理
public void myPoincut(){
}
}
@args
@args
属于参数匹配式,指示筛选指定注解的参数类型作为被代理方法。是方法参数的类上有指定注解,不是方法参数上带注解。
例子:
@Aspect//我要开始搞事了
public class MyAspect {
@Pointcut("@args(io.swagger.annotations.ApiModel)")//匹配1个参数,参数的类上运行时具有@ApiModel注解的需要被代理,不是方法参数有注解@ApiModel
public void myPoincut1(){
}
@Pointcut("@args(io.swagger.annotations.ApiModel,..)")//匹配一个或多个参数,第一个参数的类上运行时具有@ApiModel注解的需要被代理
public void myPoincut2(){
}
@Pointcut("@args(io.swagger.annotations.ApiModel,io.swagger.annotations.MyModel)")//匹配两个参数,第一个参数的类上运行时具有@ApiModel注解并且第二个参数的类上运行时具有@MyModel注解的需要被代理
public void myPoincut3(){
}
}
@within
非运行时类型的@target。
@target关注的是被调用的对象,@within关注的是调用的方法所在的类。
@target 和 @within 的不同点:
@target(注解A):判断被调用的目标对象中是否声明了注解A,如果有,会被拦截
@within(注解A): 判断被调用的方法所属的类中是否声明了注解A,如果有,会被拦截
bean
根据Spring Bean名称来匹配。支持*通配符。
bean(*Service) // 匹配所有Service结尾的Spring容器内对象
argNames
观察源码可以发现,所有的Advice注解都带有argNames字段,例如@Around:
@Around(value = "execution(* TestBean.paramArgs(..)) && args(decimal,str,..)&& target(bean)", argNames = "pjp,str,decimal,bean")@SneakyThrows // proceed会抛受检异常Object aroundArgs(ProceedingJoinPoint pjp,/*使用命名绑定模式*/ String str, BigDecimal decimal, Object bean) {// 在方法执行前做一些操作return pjp.proceed();
}
argnames 必须要和args、target、this标签一起使用。虽然实际操作中可以不带,但官方建议所有带参数的都带,原因如下:
因此如果‘ argernames’属性没有指定,那么 Spring AOP 将查看类的调试信息,并尝试从局部变量表中确定参数名。只要使用调试信息(至少是‘-g: vars’)编译了类,就会出现此信息。使用这个标志编译的结果是:
(1)你的代码将会更容易被反向工程)
(2)类文件大小将会非常大(通常是无关紧要的)
(3)删除未使用的局部变量的优化将不会被编译器应用。
此外,如果编译的代码没有必要的调试信息,那么 Spring AOP 将尝试推断绑定变量与参数的配对。如果变量的绑定在可用信息下是不明确的,那么一个 AmbiguousBindingException 就会被抛出。如果上面的策略都失败了,那么就会抛出一个 IllegalArgumentException。
建议所有的advice注解里都带argNames,反正idea也会提醒。
以上内容抄录自:最全 SpringAOP 切面表达式,内容我有细微改动和补充。
实战
以下内容为本人原创>>>>>>>>>>>
使用SpringAOP实现全局日志处理
pom.xml
<dependency>
<groupId>xxbs</groupId>
<artifactId>xxbs-util</artifactId>
<version>${xxbs.version}</version>
</dependency>
<dependency>
<groupId>xxbs</groupId>
<artifactId>xxbs-jwt</artifactId>
<version>${xxbs.version}</version>
</dependency>
<dependency>
<groupId>xxbs</groupId>
<artifactId>xxbs-bean</artifactId>
<version>${xxbs.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<scope>provided</scope>
</dependency>
application.properties
aopLog.enabled=true
AutoConfiguration.java
package tang.zhiyin.log.conf;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import tang.zhiyin.log.aspect.AopLogAspect;
@Configuration
@ConditionalOnProperty(name = "aopLog.enabled", havingValue = "true", matchIfMissing = false)
public class AutoConfiguration {
@Bean
public AopLogAspect sysLogAspect() {
return new AopLogAspect();
}
}
AopLogAspect.java
package tang.zhiyin.log.aspect;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.fileupload.FileItem;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.web.multipart.MultipartFile;
import tang.zhiyin.base.bean.result.Result;
import tang.zhiyin.base.bean.result.ResultCodeEnum;
import tang.zhiyin.base.util.IpUtil;
import tang.zhiyin.base.util.SpringRequestUtil;
import tang.zhiyin.jwt.util.JwtUtil;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* AOP日志处理
*/
@Slf4j
@Aspect
public class AopLogAspect {
/**
* 1、在tang.zhiyin.*.controller包下
* 2、有RestController或者Controller注解的类
* 3、有RequestMapping或者PostMapping或者GetMapping注解的方法
*/
private static final String execution = "execution(* tang.zhiyin.*.controller.*.*(..))" +
"&&(@target(org.springframework.web.bind.annotation.RestController)" +
"||@target(org.springframework.stereotype.Controller))" +
"&&(@annotation(org.springframework.web.bind.annotation.RequestMapping)" +
"||@annotation(org.springframework.web.bind.annotation.PostMapping)" +
"||@annotation(org.springframework.web.bind.annotation.GetMapping))";
@Around(value = execution)
public Object cutCode(ProceedingJoinPoint pjp) {
Map<String, String> logString = new LinkedHashMap<>(20);
logString.put("用户ID", JwtUtil.getUserId() + "");
logString.put("用户名", JwtUtil.getUserName());
logString.put("Token", JwtUtil.getJwtToken());
logString.put("URL", SpringRequestUtil.getRequest().getRequestURI());
logString.put("HTTP方法", SpringRequestUtil.getRequest().getMethod());
logString.put("客户IP", IpUtil.getIp(SpringRequestUtil.getRequest()));
logString.put("服务器IP", IpUtil.getLocalIp());
logString.put("线程", Thread.currentThread().getId() + "");
logString.put("类", pjp.getTarget().getClass().getName());
logString.put("方法", pjp.getSignature().getName());
logString.put("接口参数", getParam(pjp.getArgs()));
logString.put("BodyStream", getParams(SpringRequestUtil.getRequest()));
logString.put("QueryString", SpringRequestUtil.getRequest().getQueryString());
logString.put("User-Agent", SpringRequestUtil.getRequest().getHeader("User-Agent"));
LocalDateTime startTime = LocalDateTime.now();
Object result = null;
try {
result = pjp.proceed();//调用目标方法
logString.put("返回值", JSONObject.toJSONString(result));
} catch (Throwable e) {
logString.put("异常信息", e.getMessage());
logString.put("堆栈信息", StrUtil.sub(getStackTrace(e), 0, 65535));
e.printStackTrace();
// throw new RuntimeException(e);
result = Result.build(ResultCodeEnum.SERVICE_ERROR.getCode(), "系统错误:" + e.getMessage());
} finally {
logString.put("用时", Duration.between(startTime, LocalDateTime.now()).toMillis() + "秒");
if (logString.containsKey("异常信息")) {
log.error(JSONObject.toJSONString(logString));
} else {
log.info(JSONObject.toJSONString(logString));
}
}
return result;
}
public static String getStackTrace(Throwable throwable) {
StringWriter sw = new StringWriter();
try (PrintWriter pw = new PrintWriter(sw)) {
throwable.printStackTrace(pw);
return sw.toString();
}
}
public static String getParams(HttpServletRequest request) {
String param = "";
try {
BufferedReader br = new BufferedReader(new InputStreamReader(request.getInputStream(), "UTF-8"));//这流不能关闭
String line = null;
StringBuilder sb = new StringBuilder();
while ((line = br.readLine()) != null) {
sb.append(line);
}
param = sb.toString();
} catch (Exception e) {
e.printStackTrace();
return "";
}
return param;
}
public static String getParam(Object[] args) {
List<Object> params = new ArrayList<>(args.length);
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof ServletRequest || args[i] instanceof ServletResponse) {
continue;
}
if (args[i] instanceof FileItem) {
FileItem file = (FileItem) args[i];
Map<String, String> fileInfo = new LinkedHashMap<>();
fileInfo.put("文件名", file.getName());
fileInfo.put("文件大小", file.getSize() + "字节");
params.add("文件流参数:" + JSONObject.toJSONString(fileInfo));
} else if (args[i] instanceof MultipartFile) {
MultipartFile file = (MultipartFile) args[i];
Map<String, String> fileInfo = new LinkedHashMap<>();
fileInfo.put("文件名", file.getOriginalFilename());
fileInfo.put("文件大小", file.getSize() + "字节");
params.add("文件流参数:" + JSONObject.toJSONString(fileInfo));
} else {
params.add(JSONObject.toJSONString(args[i]));
}
}
return JSONObject.toJSONString(params);
}
}
spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
tang.zhiyin.log.conf.AutoConfiguration
以上内容为本人原创>>>>>>>>>>>
更多推荐
所有评论(0)