项目统一处理

统一结果返回

1、统一自定义返回状态码

/**
 * 统一自定义返回状态码
 * @author bigfairy
 * @date  2021-01-28
 */
@Getter
@AllArgsConstructor
public enum ResultEnum {
    /**
     * 自定义状态码
     */
    SUCCESS(1000, "操作成功"),
	FAILURE(1001,"操作失败"),
	UNKNOWN_ERROR(1002,"未知错误"),
    SERVER_ERROR(1003,"服务端异常"),
	NULL_POINT(1004,"空指针异常"),
    PARAM_ERROR(1005, "参数校验异常:%s");

    /** 响应码 */
    private final Integer code;
    /** 响应消息 */
    private final String msg;
}

2、统一封装返回对象

/**
 * 统一封装返回对象
 * @author bigfairy
 * @date  2021-01-28
 */
@Data
public class ResultVO implements Serializable {
    /** 响应码 */
    private Integer code;

    /** 响应消息 */
    private String msg;

    /** 响应内容 */
    private Object data;

   /**
    * 成功
    * @return result
    */
   public static ResultVO success() {
      return  createResult(null,ResultEnum.SUCCESS);
   }

   /**
    * 成功返回数据
    * @param object 响应内容
    * @return result
    */
   public static ResultVO success(Object object) {
      return  createResult(object,ResultEnum.SUCCESS);
   }
   /**
    * 失败返回未知错误
    * @return result
    */
   public static ResultVO fail() {
      return  createResult(null,ResultEnum.FAILURE);
   }

   /**
    * 失败返回未知错误
    * @param data 响应内容
    * @return result
    */
   public static ResultVO fail(Object data) {
      return  createResult(data,ResultEnum.FAILURE);
   }

   /**
    * 失败返回自定义错误信息
    * @param resultEnum  自定义枚举
    * @return result
    */
   public static ResultVO fail(ResultEnum resultEnum) {
      return  createResult(null,resultEnum);
   }

   /**
    * 失败返回自定义错误信息
    * @param data 响应内容
    * @param resultEnum  自定义枚举
    * @return result
    */
   public static ResultVO fail(Object data,ResultEnum resultEnum) {
      return  createResult(data,resultEnum);
   }

   /**
    * 使用链式编程 自定义响应消息
    * @param msg 自定义响应消息
    * @return result
    */
   public ResultVO msg(String msg) {
      this.setMsg(msg);
      return this;
   }

   /**
    *
    * @param data 响应内容
    * @param resultEnum 自定义枚举
    * @return result
    */
   private static ResultVO createResult(Object data,ResultEnum resultEnum){
      ResultVO result = new ResultVO();
      result.setData(data);
      result.setCode(resultEnum.getCode());
      result.setMsg(resultEnum.getMsg());
      return result;
   }
}

3、自定义响应消息

/**
 * 自定义响应消息
 * @author bigfairy
 * @date  2021-01-28
 */
public interface ResultMsg {
   String DEFAULT_MESSAGE = "自定义响应消息";
}

4、测试

/**
 * 全局统一返回测试
 */
@RestController
@RequestMapping("/results")
public class ResultController {

   @GetMapping("/test1")
   public ResultVO test1(){
      return ResultVO.success();
   }

   @GetMapping("/test2")
   public ResultVO test2(){
      UserVO userVO = new UserVO();
      userVO.setUserName("测试");
      userVO.setMobileNum("13141314520");
      userVO.setSex(1);
      userVO.setAge(23);
      userVO.setEmail("592188043@qq.com");
      return ResultVO.success(userVO);
   }

   @GetMapping("/test3")
   public ResultVO test3(){
      UserVO userVO = new UserVO();
      userVO.setUserName("测试");
      userVO.setMobileNum("13141314520");
      userVO.setSex(1);
      userVO.setAge(23);
      userVO.setEmail("592188043@qq.com");
      List<UserVO> userVOList = new ArrayList<>();
      for (int i = 0; i < 4; i++) {
         userVOList.add(userVO);
      }
      return ResultVO.success(userVOList);
   }

   @GetMapping("/test4")
   public ResultVO test4(){
      return ResultVO.fail();
   }

   @GetMapping("/test5")
   public ResultVO test5(){
      return ResultVO.fail(ResultEnum.SERVER_ERROR);
   }

   @GetMapping("/test6")
   public ResultVO test6(){
      return ResultVO.success().msg(ResultMsg.DEFAULT_MESSAGE);
   }
}

统一异常处理

1、全局异常处理

/**
 * 全局异常处理
 * @author bigfairy
 * @date  2021-01-28
 */
public class GlobalException extends RuntimeException{

    private static final long serialVersionUID = -4204734806795919275L;

    public GlobalException(ResultEnum resultEnum) {
        super(resultEnum.getMsg());
    }
    public GlobalException(String message) {
        super(message);
    }

    @Override
    public String toString() {
        return "GlobalException{message=" + this.getMessage() + '}';
    }
}

2、将异常信息写到日志文件中

/**
 * 将异常信息写到日志文件中
 * @author bigfairy
 * @date  2021-01-28
 */
@Slf4j
public class GlobalExceptionUtil {
    /**
     * 打印异常信息 基于JDK 1.7之后,实现正确关闭流资源方法
     * try - with - resource
     */
    public static String getMessage(Exception e) {
        String swStr = null;
        try (
         StringWriter sw = new StringWriter();
         PrintWriter pw = new PrintWriter(sw)
        ) {
            e.printStackTrace(pw);
            pw.flush();
            sw.flush();
            swStr = sw.toString();
            log.error(swStr);
        } catch (IOException ex) {
            ex.printStackTrace();
            log.error(ex.getMessage());
        }
        return swStr;
    }
}

3、全局异常处理返回信息

/**
 * 全局异常处理返回信息
 * @author bigfairy
 * @date  2021-01-28
 */
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**-------- 通用异常处理方法 --------**/
    @ExceptionHandler(Exception.class)
    public ResultVO error(Exception e) {
        String message = GlobalExceptionUtil.getMessage(e);
        return ResultVO.fail(message,ResultEnum.UNKNOWN_ERROR);
    }

    /**-------- 指定异常处理方法 --------**/
    @ExceptionHandler(NullPointerException.class)
    public ResultVO error(NullPointerException e) {
        String message = GlobalExceptionUtil.getMessage(e);
        return ResultVO.fail(message,ResultEnum.NULL_POINT);
    }

    /**-------- 自定义定异常处理方法 --------**/
    @ExceptionHandler(GlobalException.class)
    public ResultVO error(GlobalException e) {
        String message = GlobalExceptionUtil.getMessage(e);
        return ResultVO.fail(message).msg(e.getMessage());
    }

    /**--------  body raw参数校验处理方法 --------**/
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResultVO handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        return handleBodyException(e);
    }
    /**--------  body form-data参数校验处理方法 --------**/
    @ExceptionHandler(BindException.class)
    public ResultVO handleBindException(BindException e) {
        return handleBodyException(e);
    }

    private ResultVO handleBodyException(BindException e) {
        GlobalExceptionUtil.getMessage(e);
        List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
        Map<String,String> map = new HashMap<>();
        for (FieldError fieldError : fieldErrors) {
            map.put(fieldError.getField(),fieldError.getDefaultMessage());
        }
        return ResultVO.fail(map).msg("参数验证错误");
    }

    /**--------  Query Params参数校验处理方法 --------**/
    @ExceptionHandler(ConstraintViolationException.class)
    public ResultVO handleConstraintViolationException(ConstraintViolationException e) {
        GlobalExceptionUtil.getMessage(e);
        Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
        Map<String,String> map = new HashMap<>();
        for (ConstraintViolation<?> violation : violations) {
            String[] split = violation.getPropertyPath().toString().split("\.");
            String message = violation.getMessage();
            map.put(split[1],message);
        }
        return ResultVO.fail(map).msg("参数验证错误");
    }
}

4、测试

/**
 * 全局异常测试
 */
@RestController
@RequestMapping("/exceptions")
public class ExceptionController {
   @GetMapping("/test1")
   public ResultVO test1(){
      int a =  1/0;
      return ResultVO.success();
   }

   @GetMapping("/test2")
   public ResultVO test2(){
//    throw new GlobalException(ResultEnum.SERVER_ERROR);
      throw new GlobalException("自定义异常提示");
   }

   @GetMapping("/test3")
   public ResultVO test3(){
      throw new NullPointerException();
   }
}

统一参数校验

1、添加依赖

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
</dependency>

2、注解介绍

validator内置注解

注解 详细信息
@Null 被注释的元素必须为 null
@NotNull 被注释的元素必须不为 null
@AssertTrue 被注释的元素必须为 true
@AssertFalse 被注释的元素必须为 false
@Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Size(max, min) 被注释的元素的大小必须在指定的范围内
@Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past 被注释的元素必须是一个过去的日期
@Future 被注释的元素必须是一个将来的日期
@Pattern(value) 被注释的元素必须符合指定的正则表达式

Hibernate Validator 附加的 constraint

注解 详细信息
@Email 被注释的元素必须是电子邮箱地址
@Length 被注释的字符串的大小必须在指定的范围内
@NotEmpty 被注释的字符串的必须非空
@NotBlank 被验证字符串非null,且长度必须大于0
@Range 被注释的元素必须在合适的范围内

注意

  • @NotNull 适用于任何类型被注解的元素必须不能与NULL
  • @NotEmpty 适用于String Map或者数组不能为Null且长度必须大于0
  • @NotBlank 只能用于String上面 不能为null,调用trim()后,长度必须大于0

3、全局异常处理

如果校验失败,会抛出MethodArgumentNotValidException或者ConstraintViolationException异常。在实际项目开发中,通常会用统一异常处理来返回一个更友好的提示。代码如下

/**--------  body raw参数校验处理方法 --------**/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResultVO handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
    return handleBodyException(e);
}
/**--------  body form-data参数校验处理方法 --------**/
@ExceptionHandler(BindException.class)
public ResultVO handleBindException(BindException e) {
    return handleBodyException(e);
}

private ResultVO handleBodyException(BindException e) {
    GlobalExceptionUtil.getMessage(e);
    List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
    Map<String,String> map = new HashMap<>();
    for (FieldError fieldError : fieldErrors) {
        map.put(fieldError.getField(),fieldError.getDefaultMessage());
    }
    return ResultVO.fail(map).msg("参数验证错误");
}

/**--------  Query Params参数校验处理方法 --------**/
@ExceptionHandler(ConstraintViolationException.class)
public ResultVO handleConstraintViolationException(ConstraintViolationException e) {
    GlobalExceptionUtil.getMessage(e);
    Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
    Map<String,String> map = new HashMap<>();
    for (ConstraintViolation<?> violation : violations) {
        String[] split = violation.getPropertyPath().toString().split("\.");
        String message = violation.getMessage();
        map.put(split[1],message);
    }
    return ResultVO.fail(map).msg("参数验证错误");
}

4、常用校验

通常有这三种方式获取参数,所以对这些形式进行验证

1、请求路径参数

  • @PathVariable(不能为空,不能设置默认值,因为null对于url是无意义的)

    获取路径参数。即url/{id}这种形式。

  • @RequestParam

    获取查询参数。即url?name=这种形式

2、Body参数

  • @PostMapping组合@RequestBody注解

  • @PostMapping无@RequestBody注解

3、请求头参数以及Cookie

  • @RequestHeader
  • @CookieValue

测试demo如下:

@Getter
@Setter
public class UserVO {

   @NotBlank(message = "用户姓名不能为空1")
   @NotNull(message = "用户姓名不能为空2")
   private String userName;

   @NotBlank(message = "手机号不能为空1")
   @NotNull(message = "手机号不能为空2")
   private String mobileNum;

   @NotNull(message = "性别不能为空")
   private Integer sex;

   @NotNull(message = "年龄不能为空")
   private Integer age;

   @NotBlank(message = "邮箱不能为空")
   @NotNull(message = "邮箱不能为空")
   @Email(message = "邮箱格式错误")
   private String email;
}
/**
 * 全局参数校验测试
 */
@Log4j2
@Validated
@RestController
@RequestMapping("/validators")
public class ValidatorController {
	/**
	 * 验证@PostMapping+@RequestBody形式
	 * @param userVO
	 * @return
	 */
	@PostMapping("/test1")
	public ResultVO test1(@RequestBody @Validated UserVO userVO){
		return ResultVO.success();
	}

	/**
	 * 验证@PostMapping无@RequestBody形式
	 * @param userVO
	 * @return
	 */
	@PostMapping("/test2")
	public ResultVO test2(@Validated UserVO userVO){
		return ResultVO.success();
	}

	/**
	 * 验证@RequestParam接收参数形式
	 * @param userId
	 * @return
	 */
	@PostMapping("/test3")
	public ResultVO test3(@NotBlank(message = "用户ID不能为空") @RequestParam("userId") String userId){
		log.info(userId);
		return ResultVO.success();
	}

	/**
	 * 验证@RequestParam接收参数形式
	 * @param userId
	 * @return
	 */
	@GetMapping("/test4")
	public ResultVO test4(@NotBlank(message = "用户ID不能为空") @RequestParam("userId")  String userId){
		log.info(userId);
		return ResultVO.success();
	}

	/**
	 * 验证@RequestParam接收多个参数形式
	 * @param userId
	 * @return
	 */
	@GetMapping("/test5")
	public ResultVO test5(@NotBlank(message = "用户ID不能为空") @RequestParam("userId") String userId,
						  @NotBlank(message = "用户名字不能为空") @RequestParam("userName") String userName){
		log.info(userId);
		return ResultVO.success();
	}


	/**
	 * 验证@PathVariable接收参数形式
	 * @param userId
	 * @return
	 */
	@GetMapping("/test6/{userId}")
	public ResultVO test6(@Length(min = 6, max = 20,message = "用户Id长度需要在6和20之间")  @PathVariable String userId){
		log.info(userId);
		return ResultVO.success();
	}

	/**
	 * 验证@PathVariable接收参数形式
	 * @param userId
	 * @return
	 */
	@GetMapping("/test7/{userId}/{mobilePhone}")
	public ResultVO test7(@Length(min = 8,message = "userId长度最短为8位") @PathVariable String userId,
						  @Length(min = 11, max = 11, message = "手机号只能为11位") @PathVariable String mobilePhone){
		log.info("用户id:{},用户电话:{}",userId,mobilePhone);
		return ResultVO.success();
	}
}

注意:

对@PostMapping请求验证参数,直接在参数上添加@Validated或者@Valid

对@RequestParam验证参数,直接在参数上添加@Validated是无效的,使用@Valid也无效,需要在类上添加@Validated,然后在参数上使用注解验证。

5、分组校验

在实际项目中,可能多个方法需要使用同一个对象来接收参数,而不同方法的校验规则很可能是不一样的。这个时候,简单地在对象的字段上加约束注解无法解决这个问题。因此,spring-validation支持了分组校验的功能,专门用来解决这类问题。比如保存User的时候,UserId是可空的,但是更新User的时候,UserId的值必须>=10000000000000000L;其它字段的校验规则在两种情况下一样。

1、约束注解上声明适用的分组信息groups

@Getter
@Setter
public class UserVO {
   @Email(message = "邮箱格式错误", groups = {Save.class, Update.class})
   private String email;

   /**
    * 保存的时候校验分组
    */
   public interface Save {
   }

   /**
    * 更新的时候校验分组
    */
   public interface Update {
   }
}

2、@Validated注解上指定校验分组

/**
 * 分组校验测试
 * @param userVO
 * @return
 */
@PostMapping("/test8")
public ResultVO test8(@RequestBody @Validated(Save.class) UserVO userVO){
   return ResultVO.success();
}

/**
 * 分组校验测试
 * @param userVO
 * @return
 */
@PutMapping("/test9")
public ResultVO test9(@RequestBody @Validated(Update.class) UserVO userVO){
   return ResultVO.success();
}

6、嵌套校验

在嵌套的类上必须标记@Valid注解,如要对UserVO的属性CarVO的carNum校验,UserVO引入CarV0须标记@Valid注解

@Getter
@Setter
public class CarVO {
	@NotBlank(message = "车牌号",groups = Save.class)
	private String carNum;
}
@Getter
@Setter
public class UserVO {

   @NotBlank(message = "用户姓名不能为空1")
   @NotNull(message = "用户姓名不能为空2")
   private String userName;

   @Valid
   @NotNull(message = "car不能为空" ,groups = Save.class)
   private CarVO car;
}
/**
 * 嵌套校验测试
 * @param userList
 * @return
 */
@PostMapping("/test11")
public ResultVO test11(@RequestBody @Validated(Save.class) ValidationList<UserVO> userList) {
   // 校验通过,才会执行业务逻辑处理
   return ResultVO.success();
}

7、集合校验

如果请求体直接传递了json数组给后台,并希望对数组中的每一项都进行参数校验。此时,如果我们直接使用java.util.Collection下的list或者set来接收数据,参数校验并不会生效!我们可以使用自定义list集合来接收参数:

包装List类型,并声明@Valid注解

/**
 * 集合校验
 * @param <E>
 */
@Getter
@Setter
public class ValidationList<E> implements List<E> {

	@Delegate
	@Valid
	public List<E> list = new ArrayList<>();
}
/**
 * 集合校验测试
 * @param userList
 * @return
 */
@PostMapping("/test10")
public ResultVO test10(@RequestBody @Validated(Save.class) ValidationList<UserVO> userList) {
   // 校验通过,才会执行业务逻辑处理
   return ResultVO.success();
}

8、自定义校验

Hibernate Validator提供的校验无法满足当前需求时,可以使用Validator自定义注解进行特殊校验。

1、自定义电话校验

@Documented
@Target({ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValid.class)
public @interface Phone {

    String message() default "电话格式错误";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

这个注解是作用在Field字段上,运行时生效,触发的是PhoneValid这个验证类。

  • message 定制化的提示信息,主要是从ValidationMessages.properties里提取,也可以依据实际情况进行定制
  • groups 这里主要进行将validator进行分类,不同的类group中会执行不同的validator操作
  • payload 主要是针对bean的,使用不多。

2、电话真正进行验证的逻辑代码

public class PhoneValid implements ConstraintValidator<Phone,String> {

    @Override
    public void initialize(Phone phone) {

    }
    @Override
    public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
        if(StringUtils.isNotEmpty(value)) {
            return isMobile(value);
        }else{
            return false;
        }
    }
    public static boolean isMobile(String str) {
        Pattern p = null;
        Matcher m = null;
        boolean b = false;
        String s2="^[1](([3|5|8][\d])|([4][4,5,6,7,8,9])|([6][2,5,6,7])|([7][^9])|([9][1,8,9]))[\d]{8}$";// 验证手机号
        if(StringUtils.isNotEmpty(str)){
            p = Pattern.compile(s2);
            m = p.matcher(str);
            b = m.matches();
        }
        return b;
    }
}

要对异常MethodArgumentNotValidException进行拦截处理。

3、测试

/**
 * 自定义校验测试
 * @param userVO
 * @return
 */
@PostMapping("/test12")
public ResultVO test12(@RequestBody @Validated UserVO userVO) {
   // 校验通过,才会执行业务逻辑处理
   return ResultVO.success();
}

补充身份证验证

@Documented
@Target({ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = IdCardValid.class)
public @interface IdCard {

    String message() default "身份证号码不合法";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

}
/**
 * 身份证校验
 */
@Log4j2
public class IdCardValid implements ConstraintValidator<IdCard, String> {
    @Override
    public void initialize(IdCard idCard) {
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
      log.info("验证的身份证号码:{}",value);
      // 使用HuTool身份证验证工具
      return IdcardUtil.isValidCard(value);
    }
}

统一日志处理

日志的框架比较丰富,spring boot集成了logback,因此推荐使用logback在项目中使用。

配置

在resources里创建日志配置文件logback-spring.xml,配置内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<!--
	日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出
	scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true。
	scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。
	debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。
-->
<configuration debug="true" scanPeriod="60 seconds" scan="false">
	<springProperty scop="context" name="spring.application.name" source="spring.application.name" defaultValue=""/>
	<!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义后,可以使“${}”来使用变量。 -->
	<property name="log.path" value="/DevEnvironment/workspace/IdeaProjects/demo/logs/${spring.application.name}"/>
	<!-- 彩色日志格式 -->
	<property name="CONSOLE_LOG_PATTERN"
			  value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
	<!-- 彩色日志依赖的渲染类 -->
	<conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
	<conversionRule conversionWord="wex"
					converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
	<conversionRule conversionWord="wEx"
					converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>
	<!-- 1、控制台日志输出 -->
	<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
		<!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
		<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
			<level>debug</level>
		</filter>
		<encoder>
			<pattern>${CONSOLE_LOG_PATTERN}</pattern>
			<charset>UTF-8</charset> <!-- 此处设置字符集 -->
		</encoder>
	</appender>
	<!-- 2、输出到文档 -->
	<!-- 2.1 level为 DEBUG 日志,时间滚动输出  -->
	<appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
		<!-- 记录日志文件的路径及文件名 -->
		<file>${log.path}/debug.log</file>
		<!--日志文档输出格式-->
		<encoder>
			<pattern>%date [%thread] %-5level [%logger{50}] %file:%line - %msg%n</pattern>
			<charset>UTF-8</charset> <!-- 设置字符集 -->
		</encoder>
		<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
		<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
			<!--
			   file节点指定当前写日志文件的路径
			   fileNamePattern节点指定归档的日志文件的路径
			   从而将当前日志文件或归档日志文件置不同的目录。
			   %d{yyyy-MM, aux}每天的日志按月份划分存储在不同的路径下,aux说明是并非主要的滚动参数,主要是用来做文件夹分割。
			   %d{yyyy-MM-dd}指定日期格式,
			   %i指定索引
			   下面表示每天的日志按照月份进行归档,超过10M,日志文件会以索引0开始分开归档
           -->
			<fileNamePattern>${log.path}/%d{yyyy-MM, aux}/debug.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
			<!--
               日志文件不能超过10M,若超过10M,日志文件会以索引0开始,
               命名日志文件,例如log-error-2019-06-03.0.log
           -->
			<maxFileSize>10MB</maxFileSize>
			<!--日志文件保留天数-->
			<maxHistory>15</maxHistory>
		</rollingPolicy>
		<!-- 此日志文档只记录debug级别的 -->
		<filter class="ch.qos.logback.classic.filter.LevelFilter">
			<level>debug</level>
			<onMatch>ACCEPT</onMatch>
			<onMismatch>DENY</onMismatch>
		</filter>
	</appender>

	<!-- 2.2 level为 INFO 日志,时间滚动输出  -->
	<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
		<!-- 正在记录的日志文档的路径及文档名 -->
		<file>${log.path}/info.log</file>
		<!--日志文档输出格式-->
		<encoder>
			<pattern>%date [%thread] %-5level [%logger{50}] %file:%line - %msg%n</pattern>
			<charset>UTF-8</charset>
		</encoder>
		<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
		<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
			<!-- 每天日志归档路径以及格式 -->
			<fileNamePattern>${log.path}/%d{yyyy-MM, aux}/info.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
			<maxFileSize>10MB</maxFileSize>
			<!--日志文档保留天数-->
			<maxHistory>15</maxHistory>
		</rollingPolicy>
		<!-- 此日志文档只记录info级别的 -->
		<filter class="ch.qos.logback.classic.filter.LevelFilter">
			<level>info</level>
			<onMatch>ACCEPT</onMatch>
			<onMismatch>DENY</onMismatch>
		</filter>
	</appender>

	<!-- 2.3 level为 WARN 日志,时间滚动输出  -->
	<appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
		<!-- 正在记录的日志文档的路径及文档名 -->
		<file>${log.path}/warn.log</file>
		<!--日志文档输出格式-->
		<encoder>
			<pattern>%date [%thread] %-5level [%logger{50}] %file:%line - %msg%n</pattern>
			<charset>UTF-8</charset> <!-- 此处设置字符集 -->
		</encoder>
		<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
		<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
			<!-- 每天日志归档路径以及格式 -->
			<fileNamePattern>${log.path}/%d{yyyy-MM, aux}/warn.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
			<maxFileSize>10MB</maxFileSize>
			<!--日志文档保留天数-->
			<maxHistory>15</maxHistory>
		</rollingPolicy>
		<!-- 此日志文档只记录warn级别的 -->
		<filter class="ch.qos.logback.classic.filter.LevelFilter">
			<level>warn</level>
			<onMatch>ACCEPT</onMatch>
			<onMismatch>DENY</onMismatch>
		</filter>
	</appender>

	<!-- 2.4 level为 ERROR 日志,时间滚动输出  -->
	<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
		<!-- 正在记录的日志文档的路径及文档名 -->
		<file>${log.path}/error.log</file>
		<!--日志文档输出格式-->
		<encoder>
			<pattern>%date [%thread] %-5level [%logger{50}] %file:%line - %msg%n</pattern>
			<charset>UTF-8</charset> <!-- 此处设置字符集 -->
		</encoder>
		<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
		<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
			<!-- 每天日志归档路径以及格式 -->
			<fileNamePattern>${log.path}/%d{yyyy-MM, aux}/error.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
			<maxFileSize>100MB</maxFileSize>
			<!--日志文档保留天数-->
			<maxHistory>15</maxHistory>
		</rollingPolicy>
		<!-- 此日志文档只记录ERROR级别的 -->
		<filter class="ch.qos.logback.classic.filter.LevelFilter">
			<level>ERROR</level>
			<onMatch>ACCEPT</onMatch>
			<onMismatch>DENY</onMismatch>
		</filter>
	</appender>
	<!--
            <logger>用来设置某一个包或者具体的某一个类的日志打印级别、
            以及指定<appender>。<logger>仅有一个name属性,
            一个可选的level和一个可选的addtivity属性。
            name:用来指定受此logger约束的某一个包或者具体的某一个类。
            level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
                  还有一个特俗值INHERITED或者同义词NULL,代表强制执行上级的级别。
                  如果未设置此属性,那么当前logger将会继承上级的级别。
            addtivity:是否向上级logger传递打印信息。默认是true。
            <logger name="org.springframework.web" level="info"/>
            <logger name="org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor" level="INFO"/>
        -->

	<!--
        使用mybatis的时候,sql语句是debug下才会打印,而这里我们只配置了info,所以想要查看sql语句的话,有以下两种操作:
        第一种把<root level="info">改成<root level="DEBUG">这样就会打印sql,不过这样日志那边会出现很多其他消息
        第二种就是单独给dao下目录配置debug模式,代码如下,这样配置sql语句会打印,其他还是正常info级别:
        【logging.level.org.mybatis=debug logging.level.dao=debug】
     -->

	<!--
        root节点是必选节点,用来指定最基础的日志输出级别,只有一个level属性
        level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
        不能设置为INHERITED或者同义词NULL。默认是DEBUG
        可以包含零个或多个元素,标识这个appender将会添加到这个logger。
    -->
	<!-- 4. 最终的策略 -->
	<!-- 4.1 开发环境:打印控制台-->
	<springProfile name="dev">
		<logger name="com.example.demo" level="info"/>
		<root level="debug">
			<appender-ref ref="CONSOLE" />
			<appender-ref ref="DEBUG_FILE" />
			<appender-ref ref="INFO_FILE" />
			<appender-ref ref="WARN_FILE" />
			<appender-ref ref="ERROR_FILE" />
		</root>
	</springProfile>

	<!-- 4.2 生产环境:输出到文档-->
	<springProfile name="pro">
		<logger name="com.example.demo" level="warn"/>
		<root level="info">
			<appender-ref ref="ERROR_FILE" />
			<appender-ref ref="WARN_FILE" />
		</root>
	</springProfile>
</configuration>

归档日志效果

alt

注意

  • 日志的环境即spring.profiles.acticve,跟随项目启动;
  • 启动后,即可到自定目录查找到生成的日志文件;
  • 本地idea调试时,推荐Grep Console插件可实现控制台的自定义颜色输出,插件地址:https://plugins.jetbrains.com/plugin/7125-grep-console

统一处理Web请求日志

统一处理Web请求的日志,方便排查问题

/**
 * AOP统一处理Web请求日志
 */
@Log4j2
@Aspect
@Order(1)
@Component
public class WebLogAspect {
   // 记录请求执行的开始时间
   ThreadLocal<Long> startTime = new ThreadLocal<>();

    @Pointcut("execution(public * com.bigfairy.springboot.common.controller.*.*(..))")
    public void webLogPoint(){}

   /**
    * 记录Web请求日志
    * @param joinPoint
    */
    @Before("webLogPoint()")
    public void doBefore(JoinPoint joinPoint){
      startTime.set(System.currentTimeMillis());
        // 接收到请求,记录请求内容
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
      Optional.ofNullable(attributes).ifPresent(a->{
         HttpServletRequest request = attributes.getRequest();
         // 记录下请求内容
         log.info("URL : {}",request.getRequestURL().toString());
         log.info("HTTP_METHOD : {}",request.getMethod());
         log.info("IP : {}",request.getRemoteAddr());
         log.info("CLASS_METHOD : {}",joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
         log.info("ARGS : {}",JSONUtil.toJsonStr(joinPoint.getArgs()));
      });
    }
    
   /**
    * 记录Web响应日志
    * @param result 返回结果
    */
    @AfterReturning(returning = "result", pointcut = "webLogPoint()")
    public void doAfterReturning(Object result){
        // 处理完请求,返回内容
        log.info("RESPONSE : {} ",JSONUtil.toJsonStr(result));
      log.info("SPEND_TIME : {}ms",System.currentTimeMillis() - startTime.get());
    }
}

统一空指针处理

Optional 类是一个可以为null的容器对象。如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象。

Optional 是个容器:它可以保存类型T的值,或者仅仅保存null。Optional提供很多有用的方法,这样我们就不用显式进行空值检测。

Optional 类的引入很好的解决空指针异常。

类方法

序号 方法 & 描述
1 static Optional empty()返回空的 Optional 实例。
2 boolean equals(Object obj)判断其他对象是否等于 Optional。
3 Optional filter(Predicate<? super predicate)如果值存在,并且这个值匹配给定的 predicate,返回一个Optional用以描述这个值,否则返回一个空的Optional。
4 Optional flatMap(Function<? super T,Optional> mapper)如果值存在,返回基于Optional包含的映射方法的值,否则返回一个空的Optional
5 T get()如果在这个Optional中包含这个值,返回值,否则抛出异常:NoSuchElementException
6 int hashCode()返回存在值的哈希码,如果值不存在 返回 0。
7 void ifPresent(Consumer<? super T> consumer)如果值存在则使用该值调用 consumer , 否则不做任何事情。
8 boolean isPresent()如果值存在则方法会返回true,否则返回 false。
9 Optional map(Function<? super T,? extends U> mapper)如果有值,则对其执行调用映射函数得到返回值。如果返回值不为 null,则创建包含映射返回值的Optional作为map方法返回值,否则返回空Optional。
10 static Optional of(T value)返回一个指定非null值的Optional。
11 static Optional ofNullable(T value)如果为非空,返回 Optional 描述的指定值,否则返回空的 Optional。
12 T orElse(T other)如果存在该值,返回值, 否则返回 other。
13 T orElseGet(Supplier<? extends T> other)如果存在该值,返回值, 否则触发 other,并返回 other 调用的结果。
14 T orElseThrow(Supplier<? extends X> exceptionSupplier)如果存在该值,返回包含的值,否则抛出由 Supplier 继承的异常
15 String toString()返回一个Optional的非空字符串,用来调试

实战案例

demo1

public String getCity(User user)  throws Exception{
    if(user!=null){
        if(user.getAddress()!=null){
            Address address = user.getAddress();
            if(address.getCity()!=null){
            	return address.getCity();
            }
        }
    }
    throw new Excpetion("取值错误"); 
}
public String getCity(User user) throws Exception{
    return Optional.ofNullable(user)
                   .map(u-> u.getAddress())
                   .map(a->a.getCity())
                   .orElseThrow(()->new Exception("取指错误"));
}

demo2

if(user!=null){
    dosomething(user);
}
 Optional.ofNullable(user)
    .ifPresent(u->{
        dosomething(u);
});

demo3

public User getUser(User user) throws Exception{
    if(user!=null){
        String name = user.getName();
        if("zhangsan".equals(name)){
            return user;
        }
    }else{
        user = new User();
        user.setName("zhangsan");
        return user;
    }
}
public User getUser(User user) {
    return Optional.ofNullable(user)
                   .filter(u->"zhangsan".equals(u.getName()))
                   .orElseGet(()-> {
                        User user1 = new User();
                        user1.setName("zhangsan");
                        return user1;
                   });
}

采用这种链式编程,虽然代码优雅了。但是,逻辑性没那么明显,可读性有所降低。

原文地址:https://www.cnblogs.com/bigfairy/p/14373457.html