利用 Spring Boot 设计风格良好的Restful API及错误响应

一、前言

网上经常会看到一些文章,旨在介绍如何使用Spring MVC或Spring Boot实现Restful接口,譬如:

 @RequestMapping(value = "/addUser", method = RequestMethod.POST)
    public boolean addUser( User user) {
        System.out.println("开始新增...");
        return userService.addUser(user);
    }
    
    @RequestMapping(value = "/updateUser", method = RequestMethod.PUT)
    public boolean updateUser( User user) {
        System.out.println("开始更新...");
        return userService.updateUser(user);
    }
    
    @RequestMapping(value = "/deleteUser", method = RequestMethod.DELETE)
    public boolean delete(@RequestParam(value = "userName", required = true) int userId) {
        System.out.println("开始删除...");
        return userService.deleteUser(userId);
    }
    
    @RequestMapping(value = "/userId", method = RequestMethod.GET)
    public User findByUserId(@RequestParam(value = "userId", required = true) int userId) {
        System.out.println("开始查询...");
        return userService.findUserById(userId);
    }

对于如上实现方式,本人着实不敢恭维。试问,这算哪门子的Restful?自认为其与Restful无丝毫关系,有误导众人之嫌。

对于RESTful 的相关概念,以及其API的设计方法,本人极力推荐阮一峰大神的文章《RESTful API 设计指南》,仅此一篇足够已。在此基础上,本人将小试牛刀,介绍如何在Spring Boot项目中设计风格良好的Restful API,以及如何实现Restful的错误响应。

文笔拙劣,并且水平有限,望各位看官不吝赐教,相互交流~

二、项目介绍

本项目IDE使用 intellij idea 2018, 构建工具使用Maven,JDK使用1.8。方便起见,我们可以使用maven的原型插件maven-archetype-quickstart快速建立一个Java 工程,在此基础上再进行功能开发。

2.1 目录结构

image.png

如上目录结构,了解Spring Boot或Spring MVC开发的朋友应该再熟悉不过了。这是一个较简单用户(User)服务,目前只实现了对用户模型基本的增删改查功能,尚未考虑多种异常情况。

2.2 项目依赖

在pom.xml中引入如下依赖:

<dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>1.5.14.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
            <version>1.5.14.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>

        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.46</version>
        </dependency>
    </dependencies>

由上可知,我们分别引入了如下依赖:

  • spring-boot-starter-web
    众所周知,这是使用spring boot做web开发的必备依赖
  • spring-boot-starter-data-jpa
    本项目使用JPA作为ORM框架
  • springfox-swagger2与springfox-swagger-ui
    swagger是个好东西,可以用来生成RESTFUL接口的在线文档,而且更牛逼的是可以直接在文档中进行接口测试,代替Postman。在Spring Boot工程中,可以引入这两个依赖实现swagger的众多功能。
  • mysql-connector-java
    不必多言,使用mysql必备

2.3 Spring Boot 配置文件

resources目录中定义配置文件application.properties:

spring.jpa.database=MySQL
spring.datasource.url=jdbc:mysql://*.*.*.*:3306/test
spring.datasource.username=root
spring.datasource.password=abc

server.port=8801

spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jackson.serialization.indent_output=true

logging.level.root=info

注意,除此之外,在pom.xml的build节点中,还需指定resources的路径:

        <resources>
            <resource>
                <directory>src/main/resources</directory>
            </resource>
        </resources>

2.4 模型定义

模型 User 相当简单,只有id、userName、age三个属性。其中,id我们不使用自增主键,直接利用JPA提供的UUID主键生成策略,如下所示:

//User.java
package com.mystudy.spring.domain;

import org.hibernate.annotations.GenericGenerator;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.validation.constraints.NotNull;

@GenericGenerator(name = "jpa-uuid", strategy = "uuid")
@Entity
public class User
{
    @Id
    @NotNull
    @GeneratedValue(generator = "jpa-uuid")
    private String id;

    @NotNull
    private String userName;

    private int age;

    public String getId()
    {
        return id;
    }

    public void setId(String id)
    {
        this.id = id;
    }

    public String getUserName()
    {
        return userName;
    }

    public void setUserName(String userName)
    {
        this.userName = userName;
    }

    public int getAge()
    {
        return age;
    }

    public void setAge(int age)
    {
        this.age = age;
    }
}

2.5 Repository 定义

Spring Data 为我们提供了很多Repository 接口,我们只需要简单的继承就可以快速实现领域对象(也就是前面提到的模型)的各种Dao层操作。若需要自定义操作,只需要按命名规范添加接口声明即可,具体参见官方文档

这里,我们定义接口UserRepository,继承JpaRepository<User, String>接口:

package com.mystudy.spring.repository;

import com.mystudy.spring.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, String>
{
}

2.6 启动类

这再简单不过了:

package com.mystudy.spring;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class UserApplication
{
    public static void main(String[] args)
    {
        SpringApplication.run(UserApplication.class, args);
    }
}

三、Restful 接口设计

请注意,本节是核心内容。

3.1 Controller 设计

在Spring MVC 中,Restful API的定义对应为Controller层。根据Restful的接口定义规范:

GET(SELECT):从服务器取出资源(一项或多项)。
POST(CREATE):在服务器新建一个资源。
PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
DELETE(DELETE):从服务器删除资源。

我们设计接口如下所示:

//UserController.java
package com.mystudy.spring.api;

import com.mystudy.spring.domain.User;
import com.mystudy.spring.service.UserService;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/user")
public class UserController
{
    @Autowired
    private UserService userService;

    @ApiOperation(value="获取用户列表", notes="获取用户列表")
    @GetMapping(value = "/users")
    @ResponseStatus(HttpStatus.OK)
    public List<User> getUserList()
    {
        return userService.getUserList();
    }

    @ApiOperation(value="添加用户", notes="添加用户")
    @PostMapping(value = "/users")
    @ResponseStatus(HttpStatus.CREATED)
    public Object addUser(@RequestBody User user){
        return userService.addUser(user);
    }

    @ApiOperation(value="获取用户信息", notes="根据id获取用户信息")
    @GetMapping(value = "/users/{id}")
    @ResponseStatus(HttpStatus.OK)
    public Object getUser(@PathVariable("id") String id) throws NotFoundException
    {
        return userService.getUser(id);
    }

    @ApiOperation(value="删除用户", notes="根据id删除用户")
    @DeleteMapping(value = "/users/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void deleteUser(@PathVariable("id") String id)
    {
        userService.deleteUser(id);
    }

    @ApiOperation(value="更新用户", notes="更新用户")
    @PatchMapping(value = "/users/{id}")
    @ResponseStatus(HttpStatus.CREATED)
    public User updateUser(@PathVariable("id") String id, @RequestBody User user)
    {
        return userService.update(id, user);
    }


    @ApiOperation(value="测试")
    @GetMapping(value = "/test")
    @ResponseStatus(HttpStatus.OK)
    public String test()
    {
        return "test ok!";
    }
}

如上,各个接口只是简单的将JSON请求进行映射,并转发到对应的Service层,Service层负责具体的业务处理。

在这些接口上,我们使用了如下注解:

  • @GetMapping
  • @PostMapping
  • @DeleteMapping
  • @PutMapping

它们在Spring 4.3中引进,旨在简化常用的HTTP方法的映射,并可以更好地表达被注解方法的语义。如@GetMapping实际上是一个组合注解,可以直接代替@RequestMapping(method = RequestMethod.GET),我个人更推荐这种写法。

并且,在每个接口定义上,可以看到注解@ApiOperation,这就是我们前面的提到的swagger的应用。如果要为某个接口生成在线文档,只要在映射上添加该注解即可。似乎是侵入了代码,但是这点代价是值得的。在该注解中,value的值为接口说明,notes可以作为接口的简单描述。对应swagger的使用,文章后部分将会介绍。

同时,我们在每个接口上显示得使用了注解@ResponseStatus,用来标识接口正常返回时的HTTP状态码。另外,我们还需要注意每个接口的返回结果,除了删除用户,其他每个接口都有返回值。这是因为Restful 规范中提到:

GET /collection:返回资源对象的列表(数组)
GET /collection/resource:返回单个资源对象
POST /collection:返回新生成的资源对象
PUT /collection/resource:返回完整的资源对象
PATCH /collection/resource:返回完整的资源对象
DELETE /collection/resource:返回一个空文档

3.2 Service 设计

Service层负责具体的业务逻辑,其封装了Dao层的操作,如下:

//UserService.java
package com.mystudy.spring.service;

import com.mystudy.spring.domain.User;
import com.mystudy.spring.repository.UserRepository;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

import static com.mystudy.spring.util.Util.getNullPropertyNames;

@Service
public class UserService
{
    @Autowired
    private UserRepository userRepository;

    public User addUser(User user)
    {
        return userRepository.save(user);
    }

    public List<User> getUserList()
    {
        return userRepository.findAll();
    }

    public User getUser(String id)
    {
        return userRepository.findOne(id);
    }

    public void deleteUser(String id)
    {
        userRepository.delete(id);
    }

    public User update(String id, User user)
    {
        User currentInstance = userRepository.findOne(id);

        //支持部分更新
        String[] nullPropertyNames = getNullPropertyNames(user);
        BeanUtils.copyProperties(user, currentInstance, nullPropertyNames);

        return userRepository.save(currentInstance);
    }
}

可以看到,Service的大部分方法只是简单的调用了Repository的接口。这里,我们需要重点关注update方法。

根据Restful的思想,我们知道更新操作可以分为全部更新和部分更新。结合HTTP语义,可以表示为:

PUT /zoos/ID:更新某个指定动物园的信息(提供该动物园的全部信息)
PATCH /zoos/ID:更新某个指定动物园的信息(提供该动物园的部分信息)

但实际上,PATCH语义的应用并不广泛。所以,为了方便,我将两个接口合在一起,同时支持全部和部分更新,HTTP动词使用PUT,仅供参考,大家酌情而定。

在实现部分更新时,有个问题需要注意。举例说明,若我们只需要更新User的age字段,前端提供JSON形如:

{
    "id": "8a8194e5645f53a101645f6048470000",
    "age": 12
  }

其旨在更新age为12,但若请求直接映射到User:

@PutMapping(value = "/users/{id}")
    public User updateUser(@PathVariable("id") String id, @RequestBody User user)
    {
        return userService.update(id, user);
    }

则对象user的userName属性会自动映射为null,这样会导致数据库中对应的userName字段被置为空,这是无法接受的。

如何解决?很容易想到,通过id查询出已存在的User对象(如A),然后将传入的User对象(如B)的非空属性全部拷贝给A即可。

但是,难不成我们还要以一个一个的判断每个属性是否为空?大可不必,我们可以引入Spring提供的BeanUtils.copyProperties方法,该方法可以将一个对象的属性值拷贝给另一个对象,并可以忽略指定的属性。因此,我们只需要获得所有的空值属性,然后传递给BeanUtils.copyProperties即可。

如何获取空值属性?参考stackoverflow中的方案,如下所示:

//Util.java
package com.mystudy.spring.util;

import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;

import java.beans.FeatureDescriptor;
import java.util.stream.Stream;

public class Util
{
    public static String[] getNullPropertyNames(Object source) {
        final BeanWrapper wrappedSource = new BeanWrapperImpl(source);
        return Stream.of(wrappedSource.getPropertyDescriptors())
                .map(FeatureDescriptor::getName)
                .filter(propertyName -> wrappedSource.getPropertyValue(propertyName) == null)
                .toArray(String[]::new);
    }
}

但是,该方法其实有个大bug,当请求中不填写age时,User对象中的age会被映射为0(这是必定的,因为基本类型默认值为0),但只要我们将基本类型替换为引用类型(默认值为null)即可解决该问题,也就是修改age的类型为Integer。当然,如果你不想使用模型进行映射,也可以使用Map等方式。

综上,通过这种方法,我们实现了模型的全部更新和部分更新功能,前端只需要通过一个接口,传递模型的全部字段或部分字段即可。这样,就可以避免出现类似updateByName,updateByAge, updaeByXX等啰嗦、多余的更新接口。

3.3 Swagger

没错,酷炫的东西到了。除了前面提到的注解@ApiOperation,我们还需要实现一个配置类:

//Swagger2.java
package com.mystudy.spring.util;

import com.google.common.collect.Sets;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2
public class Swagger2 {
    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .protocols(Sets.newHashSet("http")) //协议,http或https
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.mystudy.spring.api")) //一定要写对,会在这个路径下扫描controller定义
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("REST接口定义")
                .version("1.0") 
                .description("用于测试RESTful API")
                .build();
    }
}

如上只使用了一些最基本的功能,还有很多个性化的配置大家可以自行发掘。

前面提到过,本项目的服务端口是8801,启动项目后,访问http://127.0.0.1:8801/swagger-ui.html,即可出现在线文档:

image.png

如上,每个接口都可以看到详细的参数,并可直接进行请求测试。如在添加用户接口中,我们先点击按钮“try out”,接着填写相关参数:


image.png

点击按钮“Exceute”即可执行,结果显示为成功:


image.png

综上,可以发现,结合Spring Boot时Swagger比Postman使用更加便捷。当然,这里只介绍了Swagger最基本的应用,更多特性请谷歌之。

四、Restful 错误响应

另外一个重点来了,也是一个难点。目前,我们只介绍了Restful 最简单的正常使用场景,没有介绍Restful 的错误响应的处理方式。在实际的前后端分离的开发中,服务端的多种错误情况都必须要反馈给前端进行处理。

4.1 错误响应风格

对于错误响应,结合阮一峰的文章,我个人倾向的风格是:

  1. 正常的响应应该直接返回需要的数据,而无需嵌套或添加任何额外信息。此时HTTP的返回码可以为:
200 OK - [GET]:服务器成功返回用户请求的数据,该操作是幂等的(Idempotent)。
201 CREATED - [POST/PUT/PATCH]:用户新建或修改数据成功。
202 Accepted - [*]:表示一个请求已经进入后台排队(异步任务)
204 NO CONTENT - [DELETE]:用户删除数据成功。
  1. 如果状态码是4xx,就应该向用户返回出错信息。一般来说,返回的信息中将error作为键名,出错信息作为键值即可:
{
      error: "error message"
}

此时,HTTP的状态码就是相应的业务错误码,一般无需额外定义业务错误码。但很多时候,仅有的20多个HTTP状态码不足以表达服务端的所有异常,此时,我们就需要额外定义错误码,而HTTP状态码仅表示错误的大类型。例如,若查找的指定用户信息不存在,HTTP状态码依旧为404,响应可以返回:

{
  "code": 40401,
  "error": "user 11 not found!"
}

其中,code为自定义错误码40401,error为其对应的错误内容。40401的前缀404表示资源不存在,01可以表示具体表示user这种资源不存在。

当然,Restful 的错误响应风格并不局限于此,大家可以根据实际情况和使用习惯酌情考虑,我唯一建议的就是——合理利用HTTP错误码而不是完全弃之不顾。

4.2 Spring 统一异常处理

介绍完错误响应风格后,我们考虑如何在Spring Boot中实现之。很多人的的做法是将各种错误转化为错误响应对象进行返回。首先,我们定义一个表示错误响应的对象:

public class Result
{
    /**
     * 错误内容
     */
    private String error;

    /**
     * 自定义错误码
     */
    private int code;


    public Result(String error, int code)
    {
        this.error = error;
        this.code = code;
    }

    public String getError()
    {
        return error;
    }

    public void setError(String error)
    {
        this.error = error;
    }

    public int getCode()
    {
        return code;
    }

    public void setCode(int code)
    {
        this.code = code;
    }


    public enum ErrorCode{
        /**
         * 用户不存在
         */
        USER_NOT_FOUND(40401),

        /**
         * 用户已存在
         */
        USER_ALREADY_EXIST(40001),
        ;

        private int code;

        public int getCode()
        {
            return code;
        }

        ErrorCode(int code)
        {
            this.code = code;
        }
    }
}

可以看到,该对象中还定义了错误码的枚举类。接着,我们修改返回接口的返回类型为Object:

    @GetMapping(value = "/users/{id}")
    public Object getUser(@PathVariable("id") String id)
    {
        return userService.getUser(id);
    }

我们考虑被添加的用户已存在的错误情况,修改Service:

    public Object getUser(String id)
    {
        User currentInstance = userRepository.findOne(id);
        if (currentInstance == null)
        {
            return new Result("user " + id + "is exist!",
                    Result.ErrorCode.USER_ALREADY_EXIST.getCode());
        }
        return userRepository.findOne(id);
    }

好了,我们现在使用Swagger进行测试,查找一个不存在的用户abc

image.png

返回结果如我们所料,但是HTTP的响应码却还是200,应该是404。所以,紧靠这些无法满足我们的需求。当然,我们可以自定义拦截器实现响应码修改。这里,有一个更好的解决方案——Spring 全局异常处理机制。我们可以通过使用@ControllerAdvice注解定义全局统一的异常处理类来完成需求。

也即是说,在处理错误时,我们不再直接返回Result对象,而采用异常机制。其实,我个人也觉得代码中到处返回Result对象真是一个bad smell。在Java中,错误得情况难道还有比异常更好的表现方式么?

好吧,废话太多了,开始实现。新建一个全局异常GlobalException ,其作为众多自定义异常的父类:

public class GlobalException extends Exception {

    private int code;

    public GlobalException(String message)
    {
        super(message);
    }

    public GlobalException(String message, int code)
    {
        super(message);
        this.code = code;
    }

    public void setCode(int code)
    {
        this.code = code;
    }

    public int getCode()
    {
        return code;
    }
}

新建一个自定义异常NotFoundException,该异常专门用来表示各种类型资源不存在的异常情况:

public class NotFoundException extends GlobalException
{
    public NotFoundException(String message, int code)
    {
        super(message, code);
    }
}

新建类RestExceptionHandler,使用注解@ControllerAdvice,如下:

@ControllerAdvice
public class RestExceptionHandler
{
    private static Logger logger = LoggerFactory.getLogger(RestExceptionHandler.class);

    @ExceptionHandler(value = NotFoundException.class)
    @ResponseBody
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public Result handleResourceNotFoundException(NotFoundException e)
    {
        logger.error(e.getMessage(), e);
        return new Result(e.getMessage(), e.getCode());
    }
}

如上,通过使用注解@ControllerAdvice,类RestExceptionHandler就可以实现全局异常的拦截处理功能。自定义的方法handleResourceNotFoundException旨在拦截NotFoundException异常,一旦拦截成功后,我们可以进行各种处理操作,并且返回自己想要的结果。

其中,注解@ExceptionHandler表示要拦截的异常;注解@ResponseStatus可以指定HTTP响应的状态码;当然,注解@ResponseBody也必不可少。

OK,让我们先修改之前的用户查找接口,并且抛出异常:

    public Object getUser(String id) throws NotFoundException
    {
        User currentInstance = userRepository.findOne(id);
        if (currentInstance == null)
        {
            throw new NotFoundException("user " + id + " is not exist!", Result.ErrorCode.USER_NOT_FOUND.getCode());
        }
        return userRepository.findOne(id);
    }

当然,Controller也要抛出异常:

    @GetMapping(value = "/users/{id}")
    public Object getUser(@PathVariable("id") String id) throws NotFoundException
    {
        return userService.getUser(id);
    }

OK,重新请求一个不存在的用户尝试一下:


image.png

如上,如我们所愿,HTTP响应码也返回了,同时查看服务控制台:

com.mystudy.spring.exception.NotFoundException: user abc is not exist!
    at com.mystudy.spring.service.UserService.getUser(UserService.java:36) ~[classes/:na]
    at com.mystudy.spring.api.UserController.getUser(UserController.java:36) ~[classes/:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
    at java.base/java.lang.reflect.Method.invoke(Method.java:564) ~[na:na]
    at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205) ~[spring-web-4.3.18.RELEASE.jar:4.3.18.RELEASE]
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:133) ~[spring-web-4.3.18.RELEASE.jar:4.3.18.RELEASE]
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:97) ~[spring-webmvc-4.3.18.RELEASE.jar:4.3.18.RELEASE]
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:827) ~[spring-webmvc-4.3.18.RELEASE.jar:4.3.18.RELEASE]
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:738) ~[spring-webmvc-4.3.18.RELEASE.jar:4.3.18.RELEASE]
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85) ~[spring-webmvc-4.3.18.RELEASE.jar:4.3.18.RELEASE]
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:967) ~[spring-webmvc-4.3.18.RELEASE.jar:4.3.18.RELEASE]
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:901) ~[spring-webmvc-4.3.18.RELEASE.jar:4.3.18.RELEASE]
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:970) [spring-webmvc-4.3.18.RELEASE.jar:4.3.18.RELEASE]
    at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:861) [spring-webmvc-4.3.18.RELEASE.jar:4.3.18.RELEASE]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:635) [tomcat-embed-core-8.5.31.jar:8.5.31]
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846) [spring-webmvc-4.3.18.RELEASE.jar:4.3.18.RELEASE]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:742) [tomcat-embed-core-8.5.31.jar:8.5.31]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231) [tomcat-embed-core-8.5.31.jar:8.5.31]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-8.5.31.jar:8.5.31]
省略

没错,这才打开的正确方式!一旦接口抛出异常,Spring 马上拦截并进行处理,最后返回自定义的错误对象。当然,若接口一切正常,还是按正常逻辑返回模型对象。

同理,我们还可以新建多种其他异常,比如表示非法参数、权限不足等。需要注意的是,请不要为每种资源都新建异常,比如你不需要创建UserNotFoundExceptionBookNotFoundException等,否则会显得多么繁琐。

五、后语

至此,本文的目标已经达成,首先介绍了如何使用Spring Boot设计Restful API,然后介绍了常用的Restful 错误响应风格,最后利用Spring Boot的全局异常处理机制实现了Restful 的错误响应功能。

项目源码请戳:https://gitee.com/haoranjunzi/study-restful

本人水平有限,难免有错误或遗漏之处,望大家指正和谅解,欢迎评论留言。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容