飞天班第18节:企业项目研发(二)后端微服务搭建

2020/04/05

1、MP代码生成工具

导入依赖

<!-- mybatis-plus -->
<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>mybatis-plus-boot-starter</artifactId>
  <version>3.0.5</version>
</dependency>
<dependency>
  <groupId>org.apache.velocity</groupId>
  <artifactId>velocity-engine-core</artifactId>
  <version>2.0</version>
</dependency>

参考官网教程:https://mp.baomidou.com/guide/generator.html#%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B

编写自己的代码生成类CodeGenerator

2、实战项目

项目分析

实战项目icoding-edu,是一个B2C模式的职业技能在线教育系统,分为前台用户系统和后台运营平台。

  • 前台系统包括课程、问答、文章三大部分。

  • 后台运营平台包括会员管理、讲师管理、课程管理、文章资讯、统计分析等。

技术栈

使用前后端分离架构。

  • 前端:Node.js+Vue.js+Nuxt

  • 后端:SpringBoot+SpringCloud+Mybatis-Plus+Mysql+Swagger2

    使用阿里云OSS、阿里云视频点播、微信登录、ECharts图表展示,POI解析Excel。

系统模块

系统架构

项目微服务搭建

1.首先要设计好数据库表

2.接着搭建基本的微服务架构

edu-parent是父依赖,只做依赖包版本管理。

<!-- 控制我们整个项目的所有的依赖! -->
<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>2.2.5.RELEASE</version>
  <relativePath/>
</parent>
<groupId>com.coding</groupId>
<artifactId>coding-edu-parent</artifactId>
<!-- 打包方式是pom文件! -->
<packaging>pom</packaging>
<properties>
  <java.version>1.8</java.version>
  <!-- 我们要使用的扩展的版本控制中心都在这里进行配置 -->
  <coding.version>0.0.1-SNAPSHOT</coding.version>
  <mybatis-plus.version>3.0.5</mybatis-plus.version>
  ...
</properties>
 <!-- dependencyManagement 只是管理并不会真正的下载!-->
<dependencyManagement>
  <dependencies>
    <!--公共业务模块版本管理-->
    <dependency>
      <groupId>com.coding</groupId>
      <artifactId>coding-edu-common</artifactId>
      <version>${coding.version}</version>
    </dependency>

    <!--mybatis-plus 持久层-->
    <dependency>
      <groupId>com.baomidou</groupId>
      <artifactId>mybatis-plus-boot-starter</artifactId>
      <version>${mybatis-plus.version}</version>
    </dependency>
   .....
<dependencyManagement>    

edu-commom是公共模块,所有的基本业务都依赖该模块

<!-- 父依赖 -->
<parent>
  <artifactId>coding-edu-parent</artifactId>
  <groupId>com.coding</groupId>
  <version>0.0.1-SNAPSHOT</version>
  <relativePath>../coding-edu-parent/pom.xml</relativePath>
</parent>

edu-edu是edu业务模块

这里的edu就是我们对应 coding_edu 这个数据库的,一个微服务对应一个数据库

<!-- 父依赖 -->
<parent>
  <artifactId>coding-edu-parent</artifactId>
  <groupId>com.coding</groupId>
  <version>0.0.1-SNAPSHOT</version>
  <relativePath>../coding-edu-parent/pom.xml</relativePath>
</parent>
...
<dependencies>
    <!-- 依赖公共模块 -->
    <dependency>
      <groupId>com.coding</groupId>
      <artifactId>coding-edu-common</artifactId>
    </dependency>
</dependencies>s  

3.edu-edu模块使用MP代码生成工具,连接数据库 coding_edu,生成CRUD业务代码

public class CodeGenerator {
    // 自动生成代码
    public static void main(String[] args) {

      // 模块名
      String moduleName = "edu";

      ....
      // 乐观锁
      strategy.setVersionFieldName("version");
      strategy.setRestControllerStyle(true); // restful api
      strategy.setControllerMappingHyphenStyle(true); //  /user/hello_name 使用_连接驼峰!

        mpg.setStrategy(strategy);
      
        // 2、执行代码生成器
        mpg.execute();

    }
}

注意:代码生成器里我们设置了去掉is_前缀

strategy.setLogicDeleteFieldName("is_delete");  // 逻辑删除字段
strategy.setEntityBooleanColumnRemoveIsPrefix(true); // 去掉布尔值的列的is_前缀

导致属性名与列名不一致,需要指定列名

public class Teacher implements Serializable {
	...
    @ApiModelProperty(value = "逻辑删除 1(true)已删除, 0(false)未删除")
    @TableField(value = "is_deleted")
    @TableLogic
    private Boolean deleted;
  ...
}

4.配置Swagger

   @Configuration
   @EnableSwagger2
   public class Swagger2Config {
       @Bean
       public Docket webApiConfig(){
           //过滤掉 admin 下的请求
           return new Docket(DocumentationType.SWAGGER_2)
                   .groupName("webApi")
                   .apiInfo(webApiInfo())
                   .select()
                   .paths(Predicates.not(PathSelectors.regex("/admin/.*")))
                   .paths(Predicates.not(PathSelectors.regex("/error.*")))
                   .build();
       }
   
       @Bean
       public Docket adminApiConfig(){
           //只选取 /admin 下的请求
           return new Docket(DocumentationType.SWAGGER_2)
                   .groupName("adminApi")
                   .apiInfo(adminApiInfo())
                   .select()
                   .paths(Predicates.and(PathSelectors.regex("/admin/.*")))
                   .build();
       }
   
       private ApiInfo webApiInfo(){
           return new ApiInfoBuilder()
                   .title("网站-课程中心API文档")
                   .description("本文档描述了课程中心微服务接口定义")
                   .version("1.0")
                   .contact(new Contact("Coding", "http://icodingedu.com", "24736743@qq.com"))
                   .build();
       }
   
       private ApiInfo adminApiInfo(){
           return new ApiInfoBuilder()
                   .title("后台管理系统-课程中心API文档")
                   .description("本文档描述了后台管理系统课程中心微服务接口定义")
                   .version("1.0")
                   .contact(new Contact("Coding", "http://icodingedu.com", "24736743@qq.com"))
                   .build();
       }
   }

统一返回结果

真正的微服务项目是提供给多端使用的,web\app\小程序,所以需要返回的数据格式是统一的。

我们的系统要求返回的基本数据格式如下:

  • 列表

    {
      "success": true,
      "code": 20000,
      "message":"成功",
      "data":{
        "item":[
          {
            "id":1,
            "name": "coding"
          }
          ...
        ]
      }
    }
    
  • 分页

    {
      "success":true,
      "code": 20000,
      "message":"成功",
       "data":{
         "total":17,
         "rows":[
           ....
         ]
       }
    }
    
  • 没有返回数据

    {
      "success":true,
      "code":20000,
      "message":"成功",
      "data":{}
    }
    
  • 失败

    {
      "success": false,
      "code": 20001,
      "message": "失败",
      "data":{}
    }
    

因此,我们定义统一返回结果

{
  "success": 布尔,	// 是否成功
  "code": 数字,	// 响应码
  "message": 字符串,// 返回消息
  "data": HashMap //返回数据,键值对
}

使用枚举定义不同的返回码

package com.coding.common.constants;

import lombok.Getter;

@Getter
public enum ResultCodeEnum {
    // 未来在这里会有十分多的状态码!方便管理
    SUCCESS(true,20000,"成功"),
    UNKNOW_REASON(false,20001,"未知错误"),
    BAD_SQL_GRAMMAR(false,21001,"sql语法错误"),
    JSON_PARSE_ERROR(false,21002,"json 解析错误"),
    PARAM_ERROR(false,21003,"参数不正确");

    private Boolean success; // 是否响应成功
    private Integer code;    // 响应的状态码
    private String message;  // 响应的消息

    ResultCodeEnum(Boolean success, Integer code, String message) {
        this.success = success;
        this.code = code;
        this.message = message;
    }
}

统一返回结果类

// 无论什么接口,返回值永远是R!
@Data
@ApiModel(value = "全局的统一返回结果")
public class R {

    @ApiModelProperty(value = "是否成功")
    private Boolean success;
    @ApiModelProperty(value = "返回状态码")
    private Integer code;
    @ApiModelProperty(value = "返回消息")
    private String message;
    @ApiModelProperty(value = "返回的数据!")
    private Map<String,Object> data = new HashMap<>();

    public R() {
    }

    // ok
    public static R ok(){
        R r = new R();
        r.setSuccess(ResultCodeEnum.SUCCESS.getSuccess());
        r.setCode(ResultCodeEnum.SUCCESS.getCode());
        r.setMessage(ResultCodeEnum.SUCCESS.getMessage());
        return r;
    }

    // error
    public static R error(){
        R r = new R();
        r.setSuccess(ResultCodeEnum.UNKNOW_REASON.getSuccess());
        r.setCode(ResultCodeEnum.UNKNOW_REASON.getCode());
        r.setMessage(ResultCodeEnum.UNKNOW_REASON.getMessage());
        return r;
    }

    // setResult 自定义错误码!
    public static R setResult(ResultCodeEnum resultCodeEnum){
        R r = new R();
        r.setSuccess(resultCodeEnum.getSuccess());
        r.setCode(resultCodeEnum.getCode());
        r.setMessage(resultCodeEnum.getMessage());
        return r;
    }

    // 这些是为了我们方便链式编程
    public R success(Boolean success){
        this.setSuccess(success);
        return this;
    }
    public R message(String message){
        this.setMessage(message);
        return this;
    }
    public R code(Integer code){
        this.setCode(code);
        return this;
    }
    public R data(String key,Object value){
        this.data.put(key,value);
        return this;
    }

    public R data(Map<String,Object> map){
        this.setData(map);
        return this;
    }
}

测试接口返回结果

@ApiOperation(value = "获取讲师列表")
@GetMapping
public R list(){
  List<Teacher> list = teacherService.list(null);
  return R.ok().data("items",list);
}

@ApiOperation(value = "根据id删除讲师")
@DeleteMapping("{id}")
public R removeById(
  @ApiParam(name = "id",value = "讲师id",required = true)
  @PathVariable String id){
  teacherService.removeById(id);
  return R.ok();
}

自动填充

实体类中我们都添加了gmtCreate创建时间,gmtModified修改时间,

@ApiModelProperty(value = "创建时间",example = "2020-04-05 00:00:00")
@TableField(fill = FieldFill.INSERT)
private Date gmtCreate;

@ApiModelProperty(value = "更新时间")
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date gmtModified;

在插入和更新数据时可以自动填充时间,在前面mybatis-plus的课中有学习过,新建一个MyMetaObjectHandler类放到edu-commom模块下

// MetaObjectHandler 元对象处理
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {

    // 插入的策略
    @Override
    public void insertFill(MetaObject metaObject) {
        // this.setFieldValByName()设置当前字段的值!
        // String fieldName, Object fieldVal, MetaObject metaObject
        // 以后只要是插入操作就会自动控制
        // createTime updateTime 使用 new Date() 进行填充

        this.setFieldValByName("gmtCreate",new Date(),metaObject);
        this.setFieldValByName("gmtModified",new Date(),metaObject);
    }

    // 更新策略
    @Override
    public void updateFill(MetaObject metaObject) {
        this.setFieldValByName("gmtModified",new Date(),metaObject);
    }
}

同时edu-edu模块的启动类需要增加扫描commom包

@SpringBootApplication
@ComponentScan(basePackages = {"com.coding.edu","com.coding.common"})
public class EduApplication {
    public static void main(String[] args) {
        SpringApplication.run(EduApplication.class,args);
    }
}

统一异常处理

介绍

异常的处理可以分为两类,分别是局部异常处理 全局异常处理 。

  • 局部异常处理 :

    @ExceptionHandler 和 @Controller 注解搭配使用,只有指定的controller层出现了异常才会被 @ExceptionHandler 捕获到,实际生产中怕是有成百上千个controller了吧,显然这种方式不合适

  • 全局异常处理:

    @ControllerAdvice 这个注解的横空出世了, @ControllerAdvice 搭配 @ExceptionHandler 彻底解决了全局统一异常处理。当然后面还出现了 @RestControllerAdvice 这个注解,其实就是 @ControllerAdvice 和 @ResponseBody 结晶。

是按照 controller 进行分类,分为 进入controller前的异常 和 业务层的异常,如下图:

进入controller之前异常一般是 javax.servlet.ServletException 类型的异常,因此在全局异常处理的时候需要统一处理。几个常见的异常如下:

  1. NoHandlerFoundException :客户端的请求没有找到对应的controller,将会抛出 404 异常。

  2. HttpRequestMethodNotSupportedException :若匹配到了(匹配结果是一个列表,不同的是http方法不同,如:Get、Post等),则尝试将请求的http方法与列表的控制器做匹配,若没有对应http方法的控制器,则抛该异常

  3. HttpMediaTypeNotSupportedException :然后再对请求头与控制器支持的做比较,比如 content- type 请求头,若控制器的参数签名包含注解 @RequestBody ,但是请求的 content-type 请求头的值没有包含 application/json ,那么会抛该异常(当然,不止这种情况会抛这个异常)

  4. MissingPathVariableException :未检测到路径参数。比如url为:/user/{userId},参数签名包含@PathVariable(“userId”) ,当请求的url为/user,在没有明确定义url为/user的情况下,会被判定为:缺少路径参数

统一结果返回的形式,按照上面的介绍就可以了,统一异常处理很简单,这里以前后端分离的项目为例,步骤如下

  1. 新建一个统一异常处理的一个类

  2. 类上标注 @RestControllerAdvice 这一个注解,或者同时标注 @ControllerAdvice 和 @ResponseBody这两个注解。

  3. 在方法上标注 @ExceptionHandler 注解,并且指定需要捕获的异常,可以同时捕获多个。

/*** 全局统一的异常处理,简单的配置下,根据自己的业务要求详细配置 */ 
@RestControllerAdvice 
@Slf4j 
public class GlobalExceptionHandler { 
  /*** 重复请求的异常 
  * @param ex 
  * @return */ 
  @ExceptionHandler(RepeatSubmitException.class) 
  public ResultResponse onException(RepeatSubmitException ex){ 
    //打印日志 
    log.error(ex.getMessage()); 
    //todo 日志入库等等操作 
    //统一结果返回 
    return new ResultResponse(ResultCodeEnum.CODE_NOT_REPEAT_SUBMIT); 
  }
  
  /*** 自定义的业务上的异常 */ 
  @ExceptionHandler(ServiceException.class) 
  public ResultResponse onException(ServiceException ex){ 
    //打印日志 
    log.error(ex.getMessage()); 
    //todo 日志入库等等操作 
    //统一结果返回 
    return new ResultResponse(ResultCodeEnum.CODE_SERVICE_FAIL); 
  }
  
  /*** 捕获一些进入controller之前的异常,有些4xx的状态码统一设置为200
  * @param ex 
  * @return 
  */ 
  @ExceptionHandler({HttpRequestMethodNotSupportedException.class, HttpMediaTypeNotSupportedException.class, HttpMediaTypeNotAcceptableException.class, MissingPathVariableException.class, MissingServletRequestParameterException.class, ServletRequestBindingException.class, ConversionNotSupportedException.class, TypeMismatchException.class, HttpMessageNotReadableException.class, HttpMessageNotWritableException.class, MissingServletRequestPartException.class, BindException.class, NoHandlerFoundException.class, AsyncRequestTimeoutException.class}) 
  public ResultResponse onException(Exception ex){ 
    //打印日志 
    log.error(ex.getMessage()); 
    //todo 日志入库等等操作
    //统一结果返回 
    return new ResultResponse(ResultCodeEnum.CODE_FAIL); 
  } 
}

注意上面的只是一个例子,实际开发中还有许多的异常需要捕获,比如 TOKEN失效 过期 等等异常,如果整合了其他的框架,还要注意这些框架抛出的异常,比如 Shiro Spring Security 等等框架。

异常匹配的顺序

精准匹配

源码org.springframework.web.method.annotation.ExceptionHandlerMethodResolver#getMappedMethod ,如下:

@Nullable 
private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
  List<Class<? extends Throwable>> matches = new ArrayList<>(); 
  //遍历异常处理器中定义的异常类型 
  for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) { 
    //是否是抛出异常的父类,如果是添加到集合中 
    if (mappedException.isAssignableFrom(exceptionType)) { 
      //添加到集合中 
      matches.add(mappedException); 
    } 
  } 
  //如果集合不为空,则按照规则进行排序 
  if (!matches.isEmpty()) { 
    matches.sort(new ExceptionDepthComparator(exceptionType)); 
    //取第一个 
    return this.mappedMethods.get(matches.get(0)); 
  }else {
    return null; 
  }
}

在初次异常处理的时候会执行上述的代码找到最匹配的那个异常处理器方法,后续都是直接从缓存中(一个 Map 结构, key 是异常类型, value 是异常处理器方法)。

别着急,上面代码最精华的地方就是对 matches 进行排序的代码了,我们来看看

ExceptionDepthComparator 这个比较器的关键代码,如下:

//递归调用,获取深度,depth值越小越精准匹配 
private int getDepth(Class<?> declaredException, Class<?> exceptionToMatch, int depth) {
  //如果匹配了,返回 
  if (exceptionToMatch.equals(declaredException)) {
    // Found it! 
    return depth; 
  }
  // 递归结束的条件,最大限度了 
  if (exceptionToMatch == Throwable.class) { 
    return Integer.MAX_VALUE; 
  } 
  //继续匹配父类 
  return getDepth(declaredException, exceptionToMatch.getSuperclass(), depth + 1); 
}

精髓全在这里了,一个递归搞定,计算深度, depth 初始值为0。值越小,匹配度越高越精准。

实战

如果没有统一异常处理,使用swagger2 测试删除数据

处理一下,统一异常处理类

@ControllerAdvice
public class GlobalExceptionHandler {

	// 处理所有的异常
	@ExceptionHandler(Exception.class)
	@ResponseBody
	public R error(Exception e){
		e.printStackTrace();
		return R.error();
	}

}

我们有时候需要处理一些特性的异常,抓住精确的异常,如果需要指定异常,可以定义处理具体的异常,添加

// 优先匹配精确异常
@ExceptionHandler(BadSqlGrammarException.class)
@ResponseBody
public R error(BadSqlGrammarException e){
  e.printStackTrace();
  return R.setResult(ResultCodeEnum.BAD_SQL_GRAMMAR);
}

我们也可以自定义异常

@Data
@ApiModel(value = "全局异常")
public class JudeException extends RuntimeException{

	@ApiModelProperty(value = "状态码")
	private Integer code;

	/**
	 * 接收枚举类型参数
	 */
	public JudeException(ResultCodeEnum resultCodeEnum){
		super(resultCodeEnum.getMessage());
		this.code = resultCodeEnum.getCode();
	}
}

处理自己写的统一异常JudeException

// 处理自定义异常
@ExceptionHandler(JudeException.class)
public R error(JudeException e){
  e.printStackTrace();
  return R.error().code(e.getCode()).message(e.getMessage());
}

在ResultCodeEnum添加状态码

PARAM_ERROR(false,21003,"参数不正确");

在分页查询中,我们需要判断参数是否规范,并且抛出自定义异常

// 统一异常的好处,所有开发人员协同的时候,可以保证整个错误结构是一致
if(page<=0 || limit<=0){
  throw new JudeException(ResultCodeEnum.PARAM_ERROR);
}

page=-3测试:

今后,如果遇到异常抛出,就全部使用自己的异常即可,把所有错误信息,都放到我们的枚举类中。

统一日志处理

配置logback日志

Springboot 内部默认使用logback来作为日志实现的框架。

Idea 安装彩色日志插件:

在项目的resource目录下添加logback-spring.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!-- 以后这个文件只需要改几个地方! -->
<configuration  scan="true" scanPeriod="10 seconds">
    <!-- 日志级别从低到高分为TRACE < DEBUG(开发) < INFO(SpringBoot默认的!) < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 -->
    <!-- scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true -->
    <!-- scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
    <!-- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->

    <contextName>logback</contextName>
    <!--输出文件的位置!-->
    <!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义变量后,可以使“${}”来使用变量。 -->
    <property name="log.path" value="/Users/xjw/Documents/log/jude-edu-edu" />

    <!-- 彩色日志 -->
    <!-- 配置格式变量:CONSOLE_LOG_PATTERN 彩色日志格式 -->
    <!-- magenta:洋红 -->
    <!-- boldMagenta:粗红-->
    <!-- cyan:青色 -->
    <!-- white:白色 -->
    <!-- magenta:洋红 -->
    <property name="CONSOLE_LOG_PATTERN"
              value="%yellow(%date{yyyy-MM-dd HH:mm:ss}) |%highlight(%-5level) |%blue(%thread) |%blue(%file:%line) |%green(%logger) |%cyan(%msg%n)"/>


    <!--输出到控制台-->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
        <!-- 例如:如果此处配置了INFO级别,则后面其他位置即使配置了DEBUG级别的日志,也不会被输出 -->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>INFO</level>
        </filter>
        <encoder>
            <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
            <!-- 设置字符集 -->
            <charset>UTF-8</charset>
        </encoder>
    </appender>


    <!--输出到文件-->

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


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


    <!-- 时间滚动输出 level为 ERROR 日志 -->
    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文件的路径及文件名 -->
        <file>${log.path}/log_error.log</file>
        <!--日志文件输出格式-->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset> <!-- 此处设置字符集 -->
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${log.path}/error/log-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <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,
              如果未设置此属性,那么当前logger将会继承上级的级别。
    -->
    <!--
        使用mybatis的时候,sql语句是debug下才会打印,而这里我们只配置了info,所以想要查看sql语句的话,有以下两种操作:
        第一种把<root level="INFO">改成<root level="DEBUG">这样就会打印sql,不过这样日志那边会出现很多其他消息
        第二种就是单独给mapper下目录配置DEBUG模式,代码如下,这样配置sql语句会打印,其他还是正常DEBUG级别:
     -->
    <!--开发环境:打印控制台-->
    <!-- 和我们项目的环境对应一定要! -->
    <springProfile name="dev">
        <!--可以输出项目中的debug日志,包括mybatis的sql日志-->
        <logger name="com.jude" level="INFO" />
        <!--
            root节点是必选节点,用来指定最基础的日志输出级别,只有一个level属性
            level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,默认是DEBUG
            可以包含零个或多个appender元素。
        -->
        <root level="INFO">
            <appender-ref ref="CONSOLE" />
            <appender-ref ref="INFO_FILE" />
            <appender-ref ref="WARN_FILE" />
            <appender-ref ref="ERROR_FILE" />
        </root>
    </springProfile>


    <!-- 日志在生成环境中的级别一定要提高,保证效率! -->
    <!--生产环境:输出到文件-->
    <springProfile name="pro">

        <!--可以输出项目中的debug日志,包括mybatis的sql日志-->
        <logger name="com.jude" level="WARN" />

        <root level="INFO">
            <appender-ref ref="ERROR_FILE" />
            <appender-ref ref="WARN_FILE" />
        </root>
    </springProfile>

</configuration>

启动测试

但发现异常没有具体到类和行数,不够精确,编写自己的异常输出类

// 打印异常的堆栈信息
public class ExceptionUtil {
    public static String getMessage(Exception e){
        // 流
        StringWriter sw = null;
        PrintWriter pw = null;
        try {
            // 将出错的信息输出到 PrintWriter!
            sw = new StringWriter();
            pw = new PrintWriter(sw);
            e.printStackTrace(pw);
            pw.flush();
            sw.flush();
        } catch (Exception e1) {
            e1.printStackTrace();
        } finally {
            if (sw!=null){
                try {
                    sw.close();
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            }
            if (pw!=null){
                pw.close();
            }
        }
        return sw.toString();
    }
}

自定义全局异常类JudeException重写toString方法

@Override
public String toString(){
  return "JudeException{" +
    "message=" + this.getMessage() +
    "cod=" + this.code +
    "}";
}

统一异常处理类GlobalExceptionHandler修改对JudeException的处理

// 处理自定义异常
@ExceptionHandler(JudeException.class)
public R error(JudeException e){
  //e.printStackTrace();
  //log.error(e.getMessage());
  log.error(ExceptionUtil.getMessage(e));
  return R.error().code(e.getCode()).message(e.getMessage());
}

重启测试,控制台打印详细的堆栈信息

日志文件log_err.log 也同样记录的清楚。

Post Directory