项目统一处理

统一结果返回

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; }); }

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


__EOF__

本文作者柳小白
本文链接https://www.cnblogs.com/bigfairy/p/14373457.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   柳小白  阅读(165)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
点击右上角即可分享
微信分享提示