Aop的使用(通知,日志存储等)

AOP是面向切面的横向的编程,在不影响原来业务的基础上,实现动态的增强。

通知顺序:

当方法符合切点规则不符合环绕通知的规则时候,执行的顺序如下

@Before→@After→@AfterRunning(如果有异常→@AfterThrowing)

当方法符合切点规则并且符合环绕通知的规则时候,执行的顺序如下

@Around→@Before→@After→@Around执行 ProceedingJoinPoint.proceed() 之后的操作→@AfterRunning(如果有异常→@AfterThrowing)

先环绕放行前,前置,环绕放行后,后置@After,最终@AfterReturning(对方法的结果做处理)

一、实现AOP方式:

1.使用原生的spring API接口。
2.自定义类,把这个类作为切面,其中的方法在切点前或后执行。

二、具体操作

1.首先,要使用AOP需要导入包
二选一


<!--  1.原始的aop依赖-->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.4</version>
</dependency>


<!-- 2.springboot的AOP依赖 -->
      	<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
   </dependency>

2.具体三种方式的实现。

第一种:使用原生的spring API接口。

public class Log implements MethodBeforeAdvice {
@Override
    public void before(Method method, Object[] objects, Object o) throws Throwable {
        System.out.println(o.getClass() + "的" + method.getName() + "方法被调用了");
    }
}

第二种:自定义切面类

可以定义多个注解,但是切入面只能有一个,可以有很多个不同切入点的环绕通知

切入点的定义:https://blog.csdn.net/guo_guo_cai/article/details/78332099
在这里插入图片描述
将这个类作为切面。其中的方法作为切点前或后执行的方法。

/**
 * Aspect 切面
 * 日志切面
 */
@Aspect
@Component
public class LogAspect {

    /**
     * slf4j日志
     */
    private final static Logger logger = LoggerFactory.getLogger(LogAspect.class);

    /**
     * Pointcut 切入点
     * 匹配cn.controller包下面的所有方法
     */
    @Pointcut("execution(public * cn.controller.*.*(..))")
    public void webLog(){}

    /**
     * 环绕通知
     */
    @Around(value = "webLog()")
    public Object arround(ProceedingJoinPoint pjp) {
        try {
            logger.info("1、Around:方法环绕开始.....");
            Object o =  pjp.proceed();
            logger.info("3、Around:方法环绕结束,结果是 :" + o);
            return o;
        } catch (Throwable e) {
            logger.error(pjp.getSignature() + " 出现异常: ", e);
            return Result.of(null, false, "出现异常:" + e.getMessage());
        }
    }

    /**
     * 方法执行前
     */
    @Before(value = "webLog()")
    public void before(JoinPoint joinPoint){
        logger.info("2、Before:方法执行开始...");
        // 接收到请求,记录请求内容
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        assert attributes != null;
        HttpServletRequest request = attributes.getRequest();
        // 记录下请求内容
        logger.info("URL : " + request.getRequestURL().toString());
        logger.info("HTTP_METHOD : " + request.getMethod());
        logger.info("IP : " + request.getRemoteAddr());
        logger.info("CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
        logger.info("ARGS : " + Arrays.toString(joinPoint.getArgs()));

    }

    /**
     * 方法执行结束,不管是抛出异常或者正常退出都会执行
     */
    @After(value = "webLog()")
    public void after(JoinPoint joinPoint){
        logger.info("4、After:方法最后执行.....");
    }

    /**
     * 方法执行结束,增强处理
     */
    @AfterReturning(returning = "ret", pointcut = "webLog()")
    public void doAfterReturning(Object ret){
        // 处理完请求,返回内容
        logger.info("5、AfterReturning:方法的返回值 : " + ret);
    }

    /**
     * 后置异常通知
     */
    @AfterThrowing(value = "webLog()")
    public void throwss(JoinPoint joinPoint){
        logger.error("AfterThrowing:方法异常时执行.....");
    }
}

第三种:配合注解使用(常用的)

package com.example.exceptiondemo.aop;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 控制器参数判断注解
 *
 * @author lc
 * @version 1.0
 * @date 2021/11/19 18:16
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ParameterJudgeAnnotation {
    boolean isVaild() default true;
}

package com.example.exceptiondemo.aop;

import com.example.exceptiondemo.exception.ResultCodeEnum;
import com.example.exceptiondemo.exception.ServiceException;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.Optional;
import java.util.stream.Collectors;

/**
 * 参数判断你的aop
 *
 * @author lc
 * @version 1.0
 * @date 2021/11/19 18:18
 */
@Aspect
@Component
@Slf4j
public class ParameterJudgeAop {

    /**
     * 环绕通知处理
     *
     * @param pjp
     * @return
     * @throws Throwable
     */
    @Around("@annotation(com.example.exceptiondemo.aop.ParameterJudgeAnnotation)")
    public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
        String reg = "^[\u4e00-\u9fa5a-zA-Z0-9-]+$"; // 只允许中文数字英文和负号
        // 获取方法签名
        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
        Object[] args = pjp.getArgs();
		// 对参数进行处理
        args = Arrays.stream(args).map(arg -> {
            Optional<Object> argOptional = Optional.ofNullable(arg);
            if (!argOptional.isPresent() || !arg.toString().matches(reg)) {
                throw new ServiceException(ResultCodeEnum.BAD_REQUEST);
            }
            if (arg.toString().contains("省")) {
                String s = this.filterPro(arg.toString());
                arg = s;
            }
            return arg;
        }).toArray(Object[]::new);
        // 放行
        return pjp.proceed(args);
    }

    public String filterPro(String province) {
        if (province.contains("省")) {
            return province.substring(0, province.length() - 1);
        }
        return province;
    }
}

三、aop已实现的一些参数校验注解 - validation

<dependency>
      <groupId>javax.validation</groupId>
      <artifactId>validation-api</artifactId>
      <version>1.1.0.Final</version>
  </dependency>

在这里插入图片描述

在这里插入图片描述

3.1 使用

3.1.1 第一种方式,pojo作为传参的形式

Pojo 定义验证规则
以pojo作为参数传参
(1)User pojo类 定义校验规则


@Data
class User{
    	
    	@NotNull(message = "用户id不能为空", groups = UpdateUser.class)
    	private Integer id;
   
        @NotNull(message = "姓名不能为空" ,groups = {CreateUser.class, UpdateUser.class})
        private String name;
    
        @NotNull(message = "性别不能为空")
        private String sex;
    
        @Max(value = 20 ,message = "最大长度为20")
        private String address;
    
        @Email(message = "不满足邮箱格式")
        private String email;
    
        @AssertTrue(message = "字段为true才能通过")
        private boolean isAuth;
    
        @NotBlank(message = "手机号不能为空")
        @Pattern(regexp = "^[1][3,4,5,6,7,8,9][0-9]{9}$", message = "手机号格式有误")
        private String mobile;
    
        @Future(message = "时间在当前时间之后才可以通过")
        private Date date;
}

(2)让校验规则生效。在Controller类的时候,我们只需要利用 @Validated 注解来实现pojo的校验

@RequestMapping("users")
public ResponseDTO saveUser( @RequestBody @Validated User user){
    
}

(3)捕捉验证异常。如果参数校验通过,那么就直接执行接口方法,但是如果失败了,我们如何进行处理

MethodArgumentNotValidException是springBoot中进行绑定参数校验时的异常,需要在springBoot中处理

其他需要处理 ConstraintViolationException异常进行处理

一般像异常捕捉的,可以自定义一个异常捕捉Handler,实现异常捕捉后的数据返回

import com.dto.ResponseDTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.NoHandlerFoundException;

import javax.validation.ConstraintViolationException;
import javax.validation.ValidationException;

@RestControllerAdvice
public class GlobalExceptionHandler {

    private Logger logger = LoggerFactory.getLogger(getClass());

    private static int DUPLICATE_KEY_CODE = 1001;
    private static int PARAM_FAIL_CODE = 1002;
    private static int VALIDATION_CODE = 1003;

    /**
     * 处理自定义异常
     */
    @ExceptionHandler(BizException.class)
    public ResponseDTO handleRRException(BizException e) {
        logger.error(e.getMessage(), e);
        return new ResponseDTO(e.getCode(), e.getMessage());
    }

    /**
     * 方法参数校验
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseDTO handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        logger.error(e.getMessage(), e);
        return new ResponseDTO(PARAM_FAIL_CODE, e.getBindingResult().getFieldError().getDefaultMessage());
    }

    /**
     * ValidationException
     */
    @ExceptionHandler(ValidationException.class)
    public ResponseDTO handleValidationException(ValidationException e) {
        logger.error(e.getMessage(), e);
        return new ResponseDTO(VALIDATION_CODE, e.getCause().getMessage());
    }

    /**
     * ConstraintViolationException
     */
    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseDTO handleConstraintViolationException(ConstraintViolationException e) {
        logger.error(e.getMessage(), e);
        return new ResponseDTO(PARAM_FAIL_CODE, e.getMessage());
    }
	/**
	* 路径异常
	*/
    @ExceptionHandler(NoHandlerFoundException.class)
    public ResponseDTO handlerNoFoundException(Exception e) {
        logger.error(e.getMessage(), e);
        return new ResponseDTO(404, "不好意思,路径不存在,请检查路径是否正确");
    }
	/**
	* 所有其他异常捕捉
	*/
    @ExceptionHandler(Exception.class)
    public ResponseDTO handleException(Exception e) {
        logger.error(e.getMessage(), e);
        return new ResponseDTO(500, "不好意思,系统繁忙,请稍后再试");
    }
}

3.1.2 第二种方式,restful风格

Controller 上加上@Validated
参数上加注解

@RestController
@RequestMapping("user/")
@Validated
public class UserController{
    @RequestMapping("users)
    public ResponseDTO getUser(@RequestParam("userId") @NotNull(message = "用户id不能为空") Long userId){

    }
}

四、自定义注解

如果我们想自定义一个验证的注解,那么需要怎么做呢?

定义一个注解
编写一个验证类
使用

(1)我们首先定义一个像上面的@Null 这样的注解

@Documented
// 这里标注该注解是使用在filed和参数上的
@Target({ElementType.PARAMETER, ElementType.FIELD})
// 运行时生效
@Retention(RetentionPolicy.RUNTIME)
// 指定验证的类是哪个   MyValidator 就是第二步做的事情
@Constraint(validatedBy = MyValidator.class)
public @interface MyValid {
	// 提供自定义异常的信息,可以指定默认值
    String message() default "参数不合法";
    // 分组,详细看 上面的介绍 
    Class<?>[] groups() default {};
	// 针对bean的,使用不多
    Class<? extends Payload>[] payload() default {};
}

(2)编写一个验证类

我们需要编写一个 实现 ConstraintValidator 类实现类,来指定我们的校验规则

如果不指定特定注解的情况下,直接使用

// 这个是Max的指定的验证规则源码
public abstract class AbstractMaxValidator<T> implements ConstraintValidator<Max, T> {
    protected long maxValue;

    public AbstractMaxValidator() {
    }
    public void initialize(Max maxValue) {
        this.maxValue = maxValue.value();
    }
    public boolean isValid(T value, ConstraintValidatorContext constraintValidatorContext) {
        if (value == null) {
            return true;
        } else {
            return this.compare(value) <= 0;
        }
    }
    protected abstract int compare(T var1);
}

// 自定义的验证类
public class MyValidator implements ConstraintValidator {
    @Override
    public void initialize(Annotation constraintAnnotation) {
		// 可以获取注解的值 ,一般写在该方法中
    }

    @Override
    public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
        // 编写自己属性验证的规则,o 则是待验证的值
        return false;
    }
}


如果在指定特定注解的情况下,那么我们就可特定 注解


// 自定义的验证类
public class MyValidator implements ConstraintValidator<MyValid , Object> {
    @Override
    public void initialize(MyValid myValid) {
		// 可以获取注解的值 ,一般写在该方法中
    }

    @Override
    public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
        // 编写自己属性验证的规则,o 则是待验证的值
        return false;
    }
}

(3)使用。就跟正常的那些注解一样使用即可。

@Data
public Class Example{
    @NotBlank(message = "姓名不能为空")
    @MyValid(message = "姓名有误,请核对后提交")
    private String name;
}

五、实际操作

5.1 操作日志和错误日志的存储

package net.facelib.eam.plancenter.webController.config.logInfo;

import com.fasterxml.jackson.databind.ObjectMapper;
import net.facelib.eam.EamErrorType;
import net.facelib.eam.exception.PlanException;
import net.facelib.eam.plancenter.common.MyPlanConstant;
import net.facelib.eam.plancenter.webController.config.PermissionAspect;
import net.facelib.eam.plancenter.webController.mapper.LogMapper;
import net.facelib.eam.plancenter.webController.utils.ExceptionLogService;
import net.facelib.eam.plancenter.webController.utils.MyCommonUtil;
import net.faceliib.eam.plancenter.common.entity.PcLog;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.Date;


/**
 * 日志切面
 *
 * @author lc
 * @version 1.0
 * @date 2022/7/8 16:43
 */
@Aspect
@Component
@Order(1)
public class LogAspect {
    private Logger logger = LoggerFactory.getLogger(LogAspect.class);
    ThreadLocal<Long> threadLocal = new ThreadLocal<>();
    @Autowired
    private LogMapper logMapper;
    @Autowired
    private MyCommonUtil myCommonUtil;

    @Autowired
    private ExceptionLogService exceptionLogService;


    @Pointcut("execution(* net.facelib.eam.plancenter.webController.service..*.*(..))")
    public void performance() {
    }

    @Around("@annotation(net.facelib.eam.plancenter.webController.config.logInfo.SaveLog)")
    public Object pcLog(ProceedingJoinPoint joinPoint) throws Throwable {
        threadLocal.set(System.currentTimeMillis()); // 接口调用时间
        // 使用ServletRequestAttributrs请求上线文获取方法信息
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        String className = joinPoint.getSignature().getDeclaringTypeName();
        String methodName = joinPoint.getSignature().getName();
        Object[] paramArgs = joinPoint.getArgs();
        ObjectMapper mapper = new ObjectMapper();
        String ip = request.getRemoteAddr();

        // 执行函数前打印日志
        // mapper.writeValueAsString(paramArgs) 会读取response,所以当需要在逻辑里面使用response的时候(比如导出)需要注释掉这个
        logger.info("调用前:{}: {},传递的参数为:{}",className,methodName,mapper.writeValueAsString(paramArgs));
        //logger.info("调用前:{}: {}",className,methodName);
        logger.info("URL:{}",request.getRequestURL().toString());
        logger.info("IP地址:{}", ip);
        Object proceed = joinPoint.proceed();// 放行

        logger.info("成功调用,耗时:{}ms", System.currentTimeMillis() - threadLocal.get());
        PcLog pcLog = new PcLog();
        this.insertPcLog(methodName, paramArgs, ip, pcLog);
        return proceed;
    }



    @AfterThrowing(pointcut = "performance()" , throwing = "ex")
    public void ErrorLog(JoinPoint joinPoint, Exception ex) {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        System.out.println("========进入异常=======");
        String methodName = joinPoint.getSignature().getName();
        Object[] paramArgs = joinPoint.getArgs();
        ObjectMapper mapper = new ObjectMapper();
        String ip = request.getRemoteAddr();

        PcLog pcLog = new PcLog();
        if (ex instanceof PlanException) {
            Integer type = ((PlanException) ex).getType();
            pcLog.setLogCode(type);
            EamErrorType eamErrorType = EamErrorType.fromValue(type);
            pcLog.setCodeName(eamErrorType.name());
        } else {
            pcLog.setLogCode(-1);
            pcLog.setCodeName(ex.getClass().getName());
        }

        try {
            if (ex.getMessage() == null) {
                pcLog.setErrorMessage(ex.getClass().getName());
            } else {
                pcLog.setErrorMessage(ex.getMessage().length() > 500?ex.getMessage().substring(0,500): ex.getMessage());
            }
            StackTraceElement[] stackTrace = ex.getStackTrace();
            StringBuilder sb = new StringBuilder();
            for (StackTraceElement element : stackTrace) {
                sb.append(element.toString());
                sb.append("\n");
            }
            pcLog.setTrace(sb.toString());
        } catch (Exception e) {
            String errMsg = MyPlanConstant.LOGINFO + MyPlanConstant.DATAPK;
            logger.error(errMsg);
            throw new PlanException(EamErrorType.PC_SQL_OPERATE_ERROR.getValue(), errMsg);
        }
        this.insertPcLog(methodName, paramArgs, ip, pcLog);
        System.out.println();
    }

    public void insertPcLog(String methodName, Object[] paramArgs, String ip, PcLog pcLog) {
        System.out.println("=======存储错误日志=====");
        try {
            Long userId = PermissionAspect.getUserId();
            pcLog.setAccessorId(userId); // 调用者
            pcLog.setAccessorName(myCommonUtil.getUserById(userId.toString()).getString("nickName"));
        } catch (Exception e) {
            logger.error(e.getMessage() , e);
        }
        try {
            pcLog.setAccessorType("USER");
            pcLog.setPortName(methodName);
            pcLog.setIp(ip);
            pcLog.setCreateTime(new Date());
            pcLog.setProps(Arrays.toString(paramArgs));
            logMapper.insert(pcLog);
        } catch (Exception e) {
            String errMsg = MyPlanConstant.PC_LOG_INSERT_ERROR;
            logger.error(errMsg);
            throw new PlanException(EamErrorType.PC_LOG_INSERT_ERROR.getValue(), errMsg);
        }
    }
}

注意

1.此时的ip获取可能会有问题,可以参考这篇文章进行修改
2.接口的参数是,实体类的时候需要实现toString方法,要不然参数会存储对象地址而不是对象的值

5.2 参数不为空的判断

  1. 先定义一个注解
  2. 切入点为注解的切面
package net.facelib.eam.plancenter.webController.config.param;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 参数注解
 *
 * @author lc
 * @version 1.0
 * @date 2022/8/18 16:19
 */

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequestRequire {
    /**
     * 请求当前接口所需要的参数,多个以小写的逗号隔开
     * @return
     */
    public String require() default "";

    /**
     *传递参数的对象类型
     */
    public Class parameter() default Object.class;
}

package net.facelib.eam.plancenter.webController.config.param;

import cn.hutool.core.util.ObjectUtil;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.lang.reflect.Method;

/**
 * 参数校验切面
 *
 * @author lc
 * @version 1.0
 * @date 2022/8/18 16:16
 */
@Aspect
@Component
public class ParamAspect {

    @Around("@annotation(net.facelib.eam.plancenter.webController.config.param.RequestRequire)")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        //获取注解的方法参数列表
        Object[] args = pjp.getArgs();
        //获取被注解的方法
        MethodInvocationProceedingJoinPoint mjp = (MethodInvocationProceedingJoinPoint) pjp;
        MethodSignature signature = (MethodSignature) mjp.getSignature();
        Method method = signature.getMethod();
        //获取方法上的注解
        RequestRequire require = method.getAnnotation(RequestRequire.class);
        //以防万一,将中文的逗号替换成英文的逗号
        String fieldNames = require.require().replace(",", ",");
        //从参数列表中获取参数对象
        Object parameter = null;
        for (Object pa : args) {
            //class相等表示是同一个对象
            if (pa.getClass() == require.parameter()) {
                parameter = pa;
            }
        }
        //通过反射去和指定的属性值判断是否非空
        Class cl = parameter.getClass();
        for (String fieldName : fieldNames.split(",")) {
            //根据属性名获取属性对象
            Field field = cl.getDeclaredField(fieldName);
            //设置可读写权限
            field.setAccessible(true);
            //获取参数值,因为我的参数都是String型所以直接强转
            Object value = field.get(parameter);
            //非空判断
            if (!ObjectUtil.isNotEmpty(value)) {
                throw new RuntimeException("参数:" + fieldName + "不允许为空");
            }
        }
        //如果没有报错,放行
        return pjp.proceed();
    }

}


/**
     * 测试
     */
    @GetMapping("/test")
    @ApiOperation(value = "test")
    @RequestRequire(require = "applyName,connectId", parameter = ApplyDto.class)
    public Response test(ApplyDto applyDto) {
        Response response = responseFactory.createResponse();
        response.onComplete("just test");
        return response;
    }

六、注意事项

6.1 切面的执行顺序问题

spring的很多功能的实现都是切面aop实现的,所以,假如自己要定义一个切面的时候,可能最好设置执行顺序;
比如,我们自定义一个@AfterThrowing异常通知来存储存储错误日志,假如我们不定义顺序的话,可能没有插入数据表中,插入日志的代码也没报任何错误;
因为,当我们自己写aop拦截的时候,会遇到跟spring的事务aop执行的先后顺序问题,比如说动态切换数据源的问题,如果事务在前,数据源切换在后,会导致数据源切换失效,所以就用到了Order(排序)这个关键字.我们可以通过在@AspectJ的方法中实现org.springframework.core.Ordered 这个接口来定义order的顺序,order 的值越小,说明越先被执行。

@Order 可以作用作用在类上
注解@Order或者接口Ordered的作用是定义Spring IOC容器中Bean的执行顺序的优先级,而不是定义Bean的加载顺序,Bean的加载顺序不受@Order或Ordered接口的影响


版权声明:本文为LC_Liangchao原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。