Spring 32 Spring REST - RESTController

Spring 32 Spring REST - RESTController

想要通过Spring返回JSON字符串作为HTTP的响应体,一个比较简便的方法是使用Spring特殊的REST控制器。 REST服务在HTTP中的使用常见于如下: HTTP请求类型 CRUD操作对应 POST Create GET Retrieve单个或者对象结果集合 PUT 更新一个存在的对象 D

想要通过Spring返回JSON字符串作为HTTP的响应体,一个比较简便的方法是使用Spring特殊的REST控制器。 REST服务在HTTP中的使用常见于如下:
HTTP请求类型 CRUD操作对应
POST Create
GET Retrieve单个或者对象结果集合
PUT 更新一个存在的对象
DELETE 删除一个已经存在的对象
在开始学习REST之前,由于从此之后就是前后端分离了,我们不再返回具体的页面(或者很少返回),所以实际上我们还需要编写前端页面,比如使用Vue等前端框架通过AJAX来发起请求。 不过这里我们只是后端,所以除了浏览器之外,还有一些第三方工具可以用来进行HTTP尤其是REST的测试。 这里涉及的一些Web基础知识就不再多说了,请求行,请求头,请求体,HTTP状态码,MIME等,这里介绍一个开发调试工具Postman

Postman使用

Postman的地址是https://www.getpostman.com/,下载Windows版本然后安装,用Google账户登录之后就可以进入界面了,使用起来也很方便,输入网址就可以方便的看到HTTP的具体信息。 可以通过其访问http://jsonplaceholder.typicode.com/,这是一个用于JSON开发和测试的网站,访问http://jsonplaceholder.typicode.com/users可以看到头部信息和返回的JSON字符串。 现在由于我们没有前端框架用于发送请求,所以就先用Postman当成我们的客户端。

简单的Rest控制器

实际上提供支持的是Spring Web MVC,通过一个注解@RestController来实现,这个注解继承自@Controller,所以也是一个Bean。被注解的控制器类用来处理REST请求和响应,会自动在POJO和JSON之间进行转换,只需要将Jackson包在classpath下或者通过Maven配置了Jackson依赖。 使用Maven配置的增删改查项目,配置好Jackson依赖,来添加新的控制器
package cc.conyli.controller;


import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/test")
public class DemoRestController {

    @GetMapping("/hello")
    public String sayHello() {
        return "第一个Spring REST响应";
    }
}
然后访问链接,可以发现@RestController修饰的类的方法,并没有被视图解析器解析成jsp路径,而是直接返回了字符串。通过Postman查看可以发现编码不是UTF-8,可以在XML文件中如下配置:
<mvc:annotation-driven>
    <mvc:message-converters register-defaults="true">
        <bean class="org.springframework.http.converter.StringHttpMessageConverter">
            <constructor-arg value="UTF-8" />
        </bean>
    </mvc:message-converters>
</mvc:annotation-driven>
既然可以使用REST用来直接返回字符串了,剩下就是如何返回JSON字符串了。

返回JSON字符串

这一次我们不再返回简单的字符串,而是要返回一个POJO对象对应的JSON字符串。 其实没有想象的复杂,只需要创建这个POJO对象,然后让控制器返回这个对象就可以了,之后看看到底是什么样的响应体。由于我们已经有了Customer类,就使用这个类,来返回一个Customer集合。 创建返回一个Customer集合对象的REST控制器:
package cc.conyli.controller;

import cc.conyli.entity.Customer;
import cc.conyli.service.CustomerService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/api")
public class CustomerRestController {

    private CustomerService customerService;

    @Autowired
    public CustomerRestController(CustomerService customerService) {
        this.customerService = customerService;
    }

    @GetMapping("/customers")
    public List<Customer> getCustomers() {
        return customerService.getCustomers();
    }
}

注入Bean之类的就不再多说了,因为我们有编写好的业务层和DAO层,可以直接使用。在没有引入REST控制器之前,控制器方法返回了一个Student集合对象,不符合要求。但是现在加上了注解之后,在Postman里访问,可以发现返回了一个由列表转换而成的JSON字符串,这就是Rest控制器的威力。 能返回客户列表了,如何返回单个客户呢,一般采用/api/customers/1的方式作为REST风格的地址,很显然,就是要获得最后一个数字,用于查询对应的客户。 这里我们使用@PathVariable注解来获取URL中可变的部分(path variable 路径变量)。这个变量也可以用在普通的非RestController上,这样就像Django一样有匹配。 给REST控制器添加一个新方法:
@GetMapping("/customers/{customerId}")
public Customer getCustomer(@PathVariable int customerId) {
    List<Customer> customers = customerService.getCustomers();
        return customers.get(customerId - 1);
}
这里现在URL中标出会变化的部分并且给一个名称,然后在参数中绑定该变量为int类型,像极了刚学Spring MVC时候的@ModelAttribute绑定。 这里我们使用了用户列表,然后通过索引-1去获取,这其实不是太好,应该通过已经编写好的业务层和DAO层按照ID或者对象的方式。不过这里是为了之后的异常处理。 如果直接调用编写好的方法,在id获取不到的时候,返回的就是空页面,观感不好。 尝试在Postman里访问,发现可以正常获取,但是当id超过列表索引的时候,会看到500错误:
Request processing failed; nested exception is java.lang.IndexOutOfBoundsException: Index: 4, Size: 4
很显然这是数组越界,一般如果出现这种运行时错误,应该向用户告知不存在,而不能简单的抛出错误给浏览器。

异常处理

异常处理的逻辑是:
  1. 编写自定义的错误类和POJO对象
  2. 添加@ExceptionHandler来编写一个处理错误的方法
  3. 返回POJO对象转换而成的错误信息
由于我们依然要返回一个JSON作为错误信息,所以需要先编写一个类用于转换成JSON:
package cc.conyli.errorhandler;

public class CustomerErrorResponse {

    private int status;
    private String message;
    private long timeStamp;

    public CustomerErrorResponse(int status, String message, long timeStamp) {
        this.status = status;
        this.message = message;
        this.timeStamp = timeStamp;
    }

    public CustomerErrorResponse() {
    }

    public int getStatus() {
        return status;
    }

    public void setStatus(int status) {
        this.status = status;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public long getTimeStamp() {
        return timeStamp;
    }

    public void setTimeStamp(long timeStamp) {
        this.timeStamp = timeStamp;
    }

    @Override
    public String toString() {
        return "CustomerErrorResponse{" +
                "status=" + status +
                ", message='" + message + '\'' +
                ", timeStamp=" + timeStamp +
                '}';
    }
}
然后编写一个自定义的错误类,也很简单:
package cc.conyli.errorhandler;

public class CustomerNotfoundError extends RuntimeException {

    public CustomerNotfoundError(String message) {
        super(message);
    }
}
之后来编写错误处理方法,先看几个理论要点:
  1. 错误处理方法用@ExceptionHandler注解
  2. 错误处理方法要求固定返回ResponseEntity<T>对象,其中的泛型T是我们刚才编写那个转换成JSON的类。ResponseEntity是一个包装了Http响应的类,这个类可以去控制状态码,响应头和体等具体内容,非常方便。
  3. 错误处理方法的名称可以是任意名称,但是参数必须是要处理的异常类型,也就是尝试catch的异常类型
来在控制器内编写错误处理方法:
@ExceptionHandler
public ResponseEntity<CustomerErrorResponse> handleCustomerNotfoundError(CustomerNotfoundError customerNotfoundError) {
    CustomerErrorResponse customerErrorResponse = new CustomerErrorResponse();

    customerErrorResponse.setStatus(HttpStatus.NOT_FOUND.value());
    customerErrorResponse.setMessage(customerNotfoundError.getMessage());
    customerErrorResponse.setTimeStamp(System.currentTimeMillis());

    return new ResponseEntity<>(customerErrorResponse, HttpStatus.NOT_FOUND);
}
这个方法名称可以任意起,传入的参数是我们编写的继承自运行时异常的异常类。而返回的ResponseEntity来自于导入类,其中的泛型是我们编写的转换成JSON的错误信息类。 在方法内部,设置了这个JSON的各个参数,其中的错误信息来自于异常类的.getMessage()方法,还使用了Spring的工具类的状态码信息和值。 最后返回ResponseEntity对象,构建参数是我们的异常信息类和状态码对象,这里的异常信息类就是响应体,而后边的状态码对象,就是让响应的状态码变为404。 然后需要修改刚才的控制器方法,在id不符合要求的时候抛出错误。
@GetMapping("/customers/{customerId}")
public Customer getCustomer(@PathVariable int customerId) {
    Customer customer = customerService.getCustomer(customerId);
    if (customer == null) {
        throw new CustomerNotfoundError("Customer with id " + customerId + " NOT FOUND!");
    }
    return customer;
}
理论上讲此时如果查询超过范围的id,取不到对象,就会返回JSON字符串,其中的内容就是刚才的类转换而成的错误信息。 试着访问一下:http://localhost:8080/api/customers/10,得到响应:
{
    "status": 404,
    "message": "Customer with id 14 NOT FOUND!",
    "timeStamp": 1553500503641
}
成功的出现了错误对象,这里如果尝试输入一个较长的数字,会发现依然有提示,这是因为错误不再是我们自定义的错误,而是转换INT的时候出现的错误,因此可以再添加一个直接抓任何的Exception的方法:
@ExceptionHandler
public ResponseEntity<CustomerErrorResponse> handleNormalException(Exception ex) {
    CustomerErrorResponse customerErrorResponse = new CustomerErrorResponse();

    customerErrorResponse.setStatus(HttpStatus.BAD_REQUEST.value());
    customerErrorResponse.setMessage(ex.getMessage());
    customerErrorResponse.setTimeStamp(System.currentTimeMillis());

    return new ResponseEntity<>(customerErrorResponse, HttpStatus.BAD_REQUEST);
}
错误会优先匹配子类,然后是父类,所以如果错误是找不到对象的错误,那就会显示JSON格式的404错误,如果是其他错误,就是JSON格式的400错误,尝试访问http://localhost:8080/api/customers/1000000000000000,会得到如下:
{
    "status": 400,
    "message": "Failed to convert value of type 'java.lang.String' to required type 'int'; nested exception is java.lang.NumberFormatException: For input string: \"1000000000000000\"",
    "timeStamp": 1553501045234
}
这样就完成了错误处理,如果我们的前端有处理错误JSON字符串的JS程序,就可以将错误信息显示在页面上。 后记:这里最好将Jackson的版本升级到2.9.8或者之后,Github提示低于这个版本的Jackson有安全风险,可以使用如下方式配置:
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>[2.9.8,)</version>
</dependency>
LICENSED UNDER CC BY-NC-SA 4.0
Comment