Spring Boot 入门(二)日志、RESTFUL Web服务、异常处理


代码在 https://github.com/betterGa/SpringBootDemo

一、Spring Boot 日志

    日志,通常不会在需求阶段作为一个功能单独提出来,也不会在产品方案中看到它的细节。但是,这丝毫不影响它在任何一个系统中的重要的地位。
    为了保证服务的高可用,发现问题一定要及时,解决问题一定要迅速,所以生产环境一旦出现问题,预警系统就会通过邮件、短信 甚至 电话 的方式实施多维轰炸模式,确保相关负责人不错过每一个可能的 bug。预警系统判断疑似 bug 大部分源于日志,比如 某个微服务接口 由于各种原因导致频繁调用出错,此时 调用端 会捕获这样的异常并打印ERROR 级别的日志,当该错误日志达到一定次数出现的时候,就会触发报警。而市面上常见的日志框架有很多,比如:JCL、SLF4J、Jboss-logging、jUL、log4j、log4j2、logback等
等,我们该如何选择呢?
    通常情况下,日志是由一个抽象层+实现层的组合来搭建的。
在这里插入图片描述
    而 SpringBoot 机智地选择了 SLF4J+Logback 的组合,这种实现 类似于 JDBC + 数据库驱动(统一接口+实现类),这个组合是当下比较合适的一组。slf4j 叫做日志门面,是一个统一的日志接口层,就是说,它是个日志标准,它只做两件事情:
提供日志接口 和 提供获取具体日志对象的方法,各种具体的日志实现都可以通过 slf4j 来实现,比如 logback 就是一个具体的日志门面的实现。

  • Spring Boot 默认日志系统
        Spring Boot 默认使用 LogBack 日志系统,如果不需要更改为其他日志系统如 Log4j2 等,则无需多余的配置,LogBack 默认将日志打印到控制台上。如果要使用 LogBack,原则上是需要添加 dependency 依赖的,但是因为新建的 Spring Boot 项目一般都会引用 spring-boot-starter 或者 spring-boot-starter- web ,而这两个起步依赖中都已经包含了对于 spring-boot-starter-logging 的依赖,所以,无需额外添加依赖。

  • 日志格式
    在这里插入图片描述
        信息依次是:提供日志的日期和时间、日志级别显示(有 DEBUG,INFO,ERROR 或WARN)、进程ID、 — 分隔符、方括号 [] 中是线程名称、记录器名称、显示源类名称 和 日志消息。

  • 日志级别
        TRACE < DEBUG < INFO 【Spring Boot 默认】< WARN < ERROR < FATAL( Logback 不支持 FATAL 级别日志。 它映射到 ERROR 级别日志) ,且级别是逐渐提高,如果日志级别设置为 INFO,则意味 TRACE 和 DEBUG 级别的日志都看不到。

  • 文件日志输出
        默认情况下,所有日志都将在控制台窗口中打印,而不是在文件中打印, 如果要在文件中打印日志,则需要在配置文件 application.yaml 中设置属性 logging.file.name 或 logging.file.path (当然控制台中还是会打印日志) :

logging:
  file:
    name: d:/mylog.log
    ## 这个path是为默认spring.log准备的
    path: d:/

注意 name 和 path 的用法:
(1)如果只配置 path,那么默认会在 path 下自动生成 spring.log 的文件用来记录日志。
(2)如果只配置 name,那么必须是文件的全路径名。
(3)如果同时配置,则 name 生效。

    还可以在 application.properties 中指定具体包具体类的日志属性:

logging.level.root=info
logging.level.com.jia.UserController=warn
  • 配置 Logback
        Logback 支持基于 XML 的配置来处理 Spring Boot Log 配置。在正式的生产环境中,可能是需要把日志详细信息在 logback.xml 文件中进行配置。

    
logback.xml 文件应放在 classpath 下 :

<?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 scan="true" scanPeriod="10 seconds">

    <!--<include resource="org/springframework/boot/logging/logback/base.xml" />-->

    <contextName>logback</contextName>
    <!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义变量后,可以使“${}”来使用变量。 -->
    <property name="log.path" value="D:/mylog/mymodule"/>

    <conversionRule conversionWord="ip" converterClass="com.jia.SpringBootDemo.Component.LogUtil"/>
    <!-- 彩色日志 -->
    <!-- 彩色日志依赖的渲染类 -->
    <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"/>
    <!-- 彩色日志格式 -->
    <property name="CONSOLE_LOG_PATTERN"
              value="${CONSOLE_LOG_PATTERN:-%ip-%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}}"/>

    <!--输出到控制台-->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
        <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为 DEBUG 日志 -->
    <appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文件的路径及文件名 -->
        <file>${log.path}/log_debug.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}/debug/log-debug-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!-- 此日志文件只记录debug级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>debug</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </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>

    <!--开发环境:打印控制台-->
    <springProfile name="dev">
        <logger name="com.jia.UserController" level="debug"/>
        <root level="info">
            <appender-ref ref="CONSOLE"/>
        </root>
    </springProfile>


    <!--生产环境:输出到文件-->
    <springProfile name="prod">
        <root level="info">
            <appender-ref ref="DEBUG_FILE"/>
            <appender-ref ref="INFO_FILE"/>
            <appender-ref ref="ERROR_FILE"/>
            <appender-ref ref="WARN_FILE"/>
        </root>
    </springProfile>

</configuration>

注意到其中有一段 :

<conversionRule conversionWord="ip"  
converterClass="com.jia.SpringBootDemo.Component.LogUtil"/>`

是想在分布式日志的场景下,区分多台服务器,在日志之前打印 IP 地址,对应的 工具类:

 public class LogUtil extends ClassicConverter {
    Logger logger= LoggerFactory.getLogger(LogUtil.class);

    @Override
    public String convert(ILoggingEvent event) {
        String ip=null;
        try {
        	// 通过本机名获取本机 IP
            ip=InetAddress.getLocalHost().getHostAddress();
        } catch (UnknownHostException e) {
            e.printStackTrace();
        }
        return ip;
    }
}

因为: value="${CONSOLE_LOG_PATTERN:-%ip-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS})… 也就是在 dev 环境中,把 IP 地址作为在控制台的输出信息的前缀:

在这里插入图片描述
在 prod 配置下,运行起来就会看到在指路径里生成的日志文件。
在这里插入图片描述
    

二、Spring Boot 构建 RESTful Web 服务

1、什么是 RESTful API

    Rest 是一种风格,符合 Rest 的 Api 就是 Rest Api。简单的说就是 可联网设备利用 HTTP 协议通过 GET、POST、DELETE、PUT、PATCH 来操作具有 URI 标识的服务器资源,返回统一格式的资源信息,包括 JSON、XML、CSV、ProtoBuf 或者 其他格式(都是和语言无关的)。
    

2、RESTful API 设计规范

在这里插入图片描述
    可以看到,Json、xml、protobuf、text、csv 这些都是跨语言的.
    比如,xml 是 可扩展标记语言,是用来传递数据的标签:

<note>
<to>Jia</to>
<from>Ting</from>
<heading>Reminder</heading>
<body>Don't forget the meeting!</body>
</note>

    比如,Json 是取代 xml 的以一种数据结构:{"name":"myname","password":"123456"},JSON规范规定,如果是字符串,那不管是 键 或 值 ,最好都用双引号引起来。
    protobuf 是由 google 公司用于数据交换的序列结构化数据格式,text 是 纯文本形式,csv 是以纯文本形式 存储 表格数据。

传统风格与 RESTFUL 风格简单对比
在这里插入图片描述
    可以看到,传统风格的 URL,比如说之前 SpringMVC 实现的接口,基本上只看 URL 就能知道方法的功能,但是 REST 风格,只看 URL 看不出来功能的。
    
特点:
(1)URL 描述资源
(2)使用 HTTP 方法描述行为,使用 HTTP 状态码来表示不同的结果
(3)使用 json 交互数据
(4) RESTful 只是一种风格,并不是强制的标准
(5)在 RESTful 架构中,每个网址代表一种资源(resource),所以网址中不能有动词,只能有名词,而且所用的名词往往与数据库的表名对应。常见动词就是

  • GET (SELECT):从服务器取出资源(一项或多项)
  • POST (CREATE):在服务器新建一个资源
  • PUT (UPDATE):在服务器更新资源(客户端提供改变后的完整资源)
  • PATCH (UPDATE):在服务器更新资源(客户端提供改变的属性)
        
    常见状态码:
    在这里插入图片描述
3、相关注解
  • Rest 控制器
    @RestController 注释用于定义 RESTful Web 服务,它提供 JSON,XML 和自定义响应。比如:
@RestController 
public class UserController { 
}
  • 请求映射
    @RequestMapping 注解用于定义访问 REST 端点的 Request URI,可以定义 Request 方法来使用和生成对象,默认请求方法是: GET 。
@RequestMapping(value = "/users") 
public ResponseEntity<Object> getUsers() {
 }
  • 请求主体
    @RequestBody 注解用于定义请求正文内容类型。
public ResponseEntity<Object> createUser(@RequestBody User user) { 
}
  • 路径变量
    @PathVariable 注解用于定义 自定义 或 动态请求 URI。请求 URI 中的 Path 变量定义为花括号 {},比如说,以下方法对应的路径可能是 @RequestMapping(value = “/user/{id}”, method = RequestMethod.GET),访问的时候输入 localhost/user/1 即可:
public ResponseEntity<Object> updateUser(@PathVariable("id") String id) { 
} 
  • 请求参数
        @RequestParam 注释用于从请求 URL 读取请求参数。默认情况下,它是必需参数。还可以为请求参数设置默认值:
public ResponseEntity<Object> getUser(@RequestParam(value = "name", required = false, defaultValue = "maxuan") String name) { 
}
4、API

(1)GET API

@RestController
public class UserController {
    @RequestMapping(value = "/user", method = RequestMethod.GET)
    
    public List<Person> queryUser(String name) {
        // 不写sql查询语句啦,简单构造一下
        ArrayList<Person> list = new ArrayList<>();
        list.add(new Person("1", "jia1"));
        list.add(new Person("2", "jia2"));
        list.add(new Person("3", "jia3"));
        if (name == null) {
            return list;
        }
        ArrayList<Person> ret = new ArrayList<>();
        for (Person person : list) {
            if (name.equals(person.getName())) {
                ret.add(person);
            }
        }
        return ret;
    }
}

写个单元测试:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = App.class)
public class UserControllerTest extends TestCase {
    @Autowired
    private WebApplicationContext wac;
    private MockMvc mockMvc;

    @Before
    public void setup() {
        mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
    }

    @Test
    public void queryUser() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders
                .get("/user")
                .param("name", "jia1")
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andDo(MockMvcResultHandlers.print())
                .andReturn().getResponse().getContentAsString();
    }
}

这个是以单元测试的方式,接下来的章节中还会专门讲到单元测试。
运行结果:
在这里插入图片描述
(2)POST API
把 list 设置成静态属性,初始化放到静态块里:

static ArrayList<Person> list = new ArrayList<>();

    static {
        list.add(new Person("1", "jia1"));
        list.add(new Person("2", "jia2"));
        list.add(new Person("3", "jia3"));
    }

POST API:

// @RequestMapping(value = "/user",method = RequestMethod.POST)
    @PostMapping(value="/user")
    public List<Person> createUser(@RequestBody Person person){
        person.setId("4");
        list.add(person);
        return list;
    }

使用 postman 进行访问:
在这里插入图片描述
查看结果:
在这里插入图片描述
(3)PUT API

	@PutMapping(value = "/user/{id}")
    public List<Person> updateUser(@PathVariable("id") String id){
        if(id==null) return list;
        for(Person person:list){
            if(id.equals(person.getId())){
                person.setName("jiaUpdate");
            }
        }
        return list;
    }

运行结果:
在这里插入图片描述
(4)DELETE API

	@DeleteMapping("/user/{id}")
    public List<Person> deleteUser(@PathVariable("id") String id){
        if(id==null) return list;
        for(Person person:list){
            if(id.equals(person.getId())){
                list.remove(person);
            }
        }
        return list;
    }

运行结果:
在这里插入图片描述
    

三、Spring Boot 异常处理

    针对代码中的异常,常规有两种处理方式:通过 throws 直接抛出;通过 try…catch 机制捕获。
在这里插入图片描述
    可以看到,之前的流向是从前端到 controller 层,controller 层调用 service 层,service 层到 dao 层,dao 通过 MyBatis 到数据库获取数据,这个过程有可能存在人为逻辑的异常,我们想要取得异常的详情,或是保证程序 在遇到异常时 继续向下执行,会采用 try…catch 机制处理。但是,代码中每一处异常都来捕获,会使代码什么冗余且不利于维护,现在 我们采用 全局处理方式来做,使用 @ControllerAdvice 注解,定义一个全局异常处理类 ,它会捕获从 Service 层抛出的异常,返回统一规范的异常信息,先判定是否会出现异常,再执行后续具体的业务。
    这样,程序分两路,正常执行的从 dao 到 service ,再到 controller,到前端;而异常的通过全局异常处理类从 Service 返回前端。

异常处理类的返回类型 ResponseResult:

public class ResponseResult {

    // 无参构造
    public ResponseResult() {
    }
    
    // 响应码
    private String code;

    // 响应信息,包括正常的和异常的信息
    private String message;

    // 响应结果
    private Object result;
    

BaseErrorInfo 接口:

public interface BaseErrorInfo {
    String getResultCode();
    String getResultMsg();
}

实现 BaseErrorInfo 接口,使用枚举的方式,让 resultCode 和 resultMsg 对应起来:

public enum CommonEnum implements BaseErrorInfo{

    // 数据操作错误信息定义
    SUCCESS("200","成功"),
    BODY_NOT_MATCH("400","请求数据格式不符"),
    NOT_FOUND("404","未找到该资源"),
    INTERNAL_ERROR("500","服务器内部错误");

    private String resultCode;
    private String resultMsg;

    CommonEnum(String resultCode, String resultMsg) {
        this.resultCode = resultCode;
        this.resultMsg = resultMsg;
    }

    @Override
    public String getResultCode() {
        return resultCode;
    }

    @Override
    public String getResultMsg() {
        return resultMsg;
    }
}

在 ResponseResult 类中提供相应的方法:

     public ResponseResult success(){
        return success(null);
    }

    // 成功信息
    public static ResponseResult success(Object data){
        ResponseResult responseResult=new ResponseResult();
        responseResult.setCode(CommonEnum.SUCCESS.getResultCode());
        responseResult.setMessage(CommonEnum.SUCCESS.getResultMsg());
        responseResult.setResult(data);
        return responseResult;
    }

    // 错误信息
    public static ResponseResult error(String code,String message){
    ResponseResult responseResult=new ResponseResult();
    responseResult.setCode(code);
    responseResult.setMessage(message);
    responseResult.setResult(null);
    return responseResult;
    }

    // 重载,不传错误码
    public static ResponseResult error(String message){
        ResponseResult responseResult=new ResponseResult();
        responseResult.setCode("-1");
        responseResult.setMessage(message);
        responseResult.setResult(null);
        return responseResult;
    }
    // 重载,传 BaseErrorInfo 实现类
    public static ResponseResult error(BaseErrorInfo baseErrorInfo){
        ResponseResult responseResult=new ResponseResult();
        responseResult.setCode(baseErrorInfo.getResultCode());
        responseResult.setMessage(baseErrorInfo.getResultMsg());
        responseResult.setResult(null);
        return responseResult;
    }    

提供业务异常类:

// 业务异常类
public class BizException extends RuntimeException {
    private String errorCode;
    private String errorMessage;

    public BizException() {
        super();
    }

    public BizException(BaseErrorInfo baseErrorInfo) {
        super(baseErrorInfo.getResultMsg());
        errorCode = baseErrorInfo.getResultCode();
        errorMessage = baseErrorInfo.getResultMsg();
    }

  public BizException(String errMsg){
        super(errMsg);
        errorMessage=errMsg;
    }
    
    @Override
    public synchronized Throwable fillInStackTrace() {
        return this;
    }

	/*其他构造方法、getter、setter方法省略*/

💎 异常处理类:

@ControllerAdvice
public class BizExceptionHandler {

    Logger logger = LoggerFactory.getLogger(BizExceptionHandler.class);

    @ExceptionHandler(value = BizException.class)
    // 以 Json 格式返回
    @ResponseBody
    public ResponseResult bizExceptionHandler(HttpServletRequest request, BizException bizException) {
        //{} 是占位符
        logger.error("发生业务异常:{}", bizException.getErrorMessage());
        return ResponseResult.error(bizException.getErrorCode(),bizException.getErrorMessage());
    }
}

@ExceptionHandler注解的功能是 自动捕获 controller层出现的指定类型异常,并对该异常进行相应的异常处理.。
   
    接下来,在 Service 层抛出异常:

@Service
public class PersonServiceImpl implements PersonService {
    // 不写sql查询语句啦,简单构造一下
    static ArrayList<Person> list = new ArrayList<>();

    static {
        list.add(new Person("1", "jia1"));
        list.add(new Person("2", "jia2"));
        list.add(new Person("3", "jia3"));
    }

    public int addPerson(Person person) {
    
        // 其实 postman 是模拟 HTTP 客户端的,HTTP1.0构造的请求是基于文本的。
        //  只有空字符串的概念而没有 null的概念,这里改成 person.id==null 更合适些。
        if (person == null) {
            throw new BizException("入参为空");
        }
        for (Person person1 : list) {
            if (person1.getId().equals(person.getId())) {
            
                // (1) 如果数据库中已经有这个 ID 了,需要抛出异常
                throw new BizException("-111", "用户已存在!");
            }
        }
        // 模拟数据插入
        return list.add(person) ? 1 : 0;
       // return 0;
    }
}

(1)处对应的构造方法:

   public BizException(String errorCode, String errorMessage) {
            this.errorCode = errorCode;
            this.errorMessage = errorMessage;
        }

由 Controller 层调用 Service 层:

@RestController
public class PersonController {

    @Autowired
    private PersonServiceImpl personService;

    @GetMapping("/addPerson")
    public ResponseResult addPerson(@RequestBody Person person){
        int ret= personService.addPerson(person);
       return ret == 1 ? ResponseResult.success(null) : ResponseResult.error("新增用户失败");
    }
}

运行结果:
在这里插入图片描述
    可以看到,返回了成功信息。 再次提交请求:
在这里插入图片描述
    可以看到,返回的是错误信息“用户已存在”,这是因为 在 Service 层,throw 了 BizException 对象,然后 addPerson 方法就会立刻退出,并不返回任何值, 所以说 这个方法里下一步 return list.add(person) ? 1 : 0; 也不会执行,也就是说这个对象没有插入到 list 中。然后 调用这个方法的代码 也就是 Controller 层 也无法继续执行,所以我们看到响应的结果是这样的。 而如果把 Service 层 addPerson 方法末尾改成直接 return 0,再提交同样的数据:
在这里插入图片描述
    这时 list 中只有 id 为1、2、3 的 person 对象,就不会抛出异常,Service 层 addPerson 方法返回 0,那么在 Controller 层就会调用 ResponseResult.error("新增用户失败"); 作为返回值,返回类型是 json,所以我们看到响应的结果是这样的。
    

四、总结

(1)日志通常是由一个抽象层 + 实现层 的组合搭建的,Spring Boot 用 slf4j + logback,日志级别:TRACE < DEBUG < INFO 【Spring Boot 默认】< WARN < ERROR < FATAL。( Logback 不支持 FATAL 级别日志。 它映射到 ERROR 级别日志) ,如果日志级别设置为 INFO,则意味 TRACE 和 DEBUG 级别的日志都看不到。logBack 默认将日志打印到控制台上,如果在不同的环境中对日志的级别要求不同,可以把日志详细信息在 logback.xml 文件中进行配置。
    
(2)Rest 是一种风格,符合 Rest 的 Api 就是 Rest Api。简单的说就是 可联网设备利用 HTTP 协议通过 GET、POST、DELETE、PUT、PATCH 这几个方法来操作具有 URI 标识的服务器资源,返回统一格式的资源信息,包括 JSON、XML、CSV、ProtoBuf 或者 其他格式(都是和语言无关的)。
    和 以往 Spring MVC 不同的是,从 URL 看不出功能、使用 json 交互数据。

  • 示例:
  // @RequestMapping(value = "/user",method = RequestMethod.POST)
    @PostMapping(value="/user")
    public List<Person> createUser(@RequestBody Person person){
        person.setId("4");
        list.add(person);
        return list;
    }

    
(3)在 Spring Boot 中,我们采用 全局处理方式来做异常处理,使用 @ControllerAdvice注解定义一个全局异常处理,它会捕获从 Service 层抛出的异常,返回统一规范的异常信息给前端。
    本文中的异常处理类和异常类的关系梳理:

在这里插入图片描述
    

🎉补充:@RestController 和 @ Controller 注解的区别

    @RestController 在 Spring MVC 中就是 @Controller 和 @ResponseBody 注解的集合。
    @RestController 注解是从 Spring 4.0 版本开始添加进来的,主要用于更加方便的构建 RESTful Web 服务。
    @ResponseBody 用于将 Controller 的方法返回的对象,通过适当的HttpMessageConverter转换为指定格式后,写入到 Response 对象的 body 数据区。使用此注解此次请求将不再走视图处理器,而是直接将此响应结果写入到输入流中,其效果等同于使用response对象输出指定格式的数据。
    在 RESTful 的服务中,我们大部分情况是使用 JSON 为返回数据, 所以 可以直接使用 @RestController 替换掉 @Controller 和 @ResponseBody。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值