阅读本文内容之前需要先了解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切点执行之后执行。
该注解有四个属性pointcutvaluereturningargNames,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

以上内容为本人原创>>>>>>>>>>>

Logo

长江两岸老火锅,共聚山城开发者!We Want You!

更多推荐