项目统一处理
统一结果返回
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>
归档日志效果
注意
- 日志的环境即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 |
2 | boolean equals(Object obj)判断其他对象是否等于 Optional。 |
3 | 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 |
11 | static |
12 | T orElse(T other)如果存在该值,返回值, 否则返回 other。 |
13 | T orElseGet(Supplier<? extends T> other)如果存在该值,返回值, 否则触发 other,并返回 other 调用的结果。 |
14 | |
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;
});
}
采用这种链式编程,虽然代码优雅了。但是,逻辑性没那么明显,可读性有所降低。