一、自定义返回HTTP状态码
当浏览器输入一个URL地址时,浏览器会向服务器发出请求,在浏览器接收和显示响应内容之前,服务器会返回一个包含HTTP状态码的响应头,响应浏览器的请求。动态码是一个标识,标识当前响应的状态成功或者失败或者需要进行进行其他操作。
常见的HTTP状态码有200、302、404、500等
HTTP状态码有以下五种类型,HTTP状态码的第一位表示状态码的类型:
- 1xx:服务器收到客户端的请求,需要客户端继续执行操作
- 2xx:请求成功
- 3xx:重定向,需要进一步的操作完成请求
- 4xx:客户端出错,请求出错
- 5xx:服务区错误,请求处理发生错误
而我们在编写基于Spring MVC的程序时并没有定义响应的状态码,这是因为Spring MVC已经在框架中定义好了这些响应码,不需要在编写业务代码时再去定义响应码,当然Spring MVC也支持自定义状态码
需要自定义返回状态码的场景有以下几种
- 针对不容的错误类型发送特定的错误码
- 客户端的定制化需求
Spring MVC中自定义返回状态码的方式有以下几种:
- 使用ResponseEntity表示状态码、头部信息、响应体
- Controller类或者异常类上使用@ResponseStatus注解标识响应码,当方法抛出该异常时返回设置的响应码
- 使用@ControllerAdvice或者@RestControllerAdvice标识一个异常处理类,@ExceptionHanlder标识一个异常处理方法,方法中定义异常类的返回码及响应体等内容
新建一个项目spring_mvc_traps,添加maven依赖
<modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.6.3</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.citi</groupId> <artifactId>spring-mvc-traps</artifactId> <version>0.0.1-SNAPSHOT</version> <name>spring-traps</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> 复制代码
添加启动应用主程序
@SpringBootApplication public class TrapsApplication { public static void main(String[] args) { SpringApplication.run(TrapsApplication.class,args); } } 复制代码
ResponseEntity实现自定义HTTP状态码
@RestController @RequestMapping("/tesla") public class TeslaController { @GetMapping("/first") public ResponseEntity<CommonResponse<String>> cyber(){ CommonResponse<String> result = new CommonResponse<>(0,""); result.setData("Cyber"); // 自定义HTTP响应码 return new ResponseEntity<>(result, HttpStatus.BAD_REQUEST); } } 复制代码
使用IDEA的插件REST Client发起HTTP请求,在resources目录下新建spring_mvc_traps.http 增加http请求
### GET http://localhost:8080/tesla/first Accept: application/json 复制代码
启动该服务,点击spring_mvc_traps.http文件左边的启动按钮,发起HTTP请求
响应头为设置的400,即BAD_REQUEST的枚举值。
@ResponseStatus注解
先看@ResponseStatus注解源码
@ResponseStatus注解可以标注在类上也可以标注在方法上,有三个属性,value和code都表示HTTP状态,默认时INTERAL_SERVER_ERROR,即500。reason属性表示原因,默认为空
新建common包,增加一个CommonException
@ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "请求错误") public class CommonException extends RuntimeException{ } 复制代码
在TeslaController中新增方法
@GetMapping("/second") public CommonResponse<String> model3(){ throw new CommonException(); } 复制代码
在spring_mvc_traps.http中增加请求方法
GET http://localhost:8080/tesla/second Accept: application/json 复制代码
重新启动SpringTrapsApplication程序,并发送HTTP请求
还可以将@ResponseStatus标注在方法上
@GetMapping("/third") @ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "请求地址不存在") public void response404(){ } 复制代码
在spring_mvc_traps.http增加请求
GET http://localhost:8080/tesla/third Accept: application/json 复制代码
@ControllerAdvice或者@RestControllerAdvice及@ExceptionHanlder注解
新增advice包,增加GlobalExceptionAdvice
@RestControllerAdvice public class GlobalExceptionAdvice { @ExceptionHandler(value = CustException.class) public ResponseEntity<CommonResponse> handleCustException(HttpServletRequest request, CustException ex){ CommonResponse<String> result = new CommonResponse<>(0,""); result.setData(ex.getMessage()); return new ResponseEntity<>(result, HttpStatus.BAD_REQUEST); } } 复制代码
在TeslaController中增加方法
@RequestMapping("/fourth") public CommonResponse<String> fourth() throws CustException{ throw new CustException("Some error"); } 复制代码
重启启动应用,在spring_mvc_traps.http增加请求
### GET http://localhost:8080/tesla/fourth Accept: application/json 复制代码
点击发送该请求
二、时间序列化和反序列化中的“陷阱”
新增一个entity包,增加UserInfo实体类
@Data @NoArgsConstructor @AllArgsConstructor public class UserInfo { private long id; private String name; @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") private Date createTime; } 复制代码
定义一个Controller,UserController;增加GET和POST请求
@RestController public class UserController { @GetMapping("/get") public Map<String, Long> getDateByGet(@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") Date date){ Map<String, Long> result = new HashMap<>(); result.put("timestamp", date.getTime()); return result; } @PostMapping("/post") public Map<String, String> getDataByPost(@RequestBody UserInfo userInfo){ Map<String, String> result = new HashMap<>(); result.put("id", userInfo.getId().toString()); result.put("name", userInfo.getName()); result.put("createTime", userInfo.getCreateTime().toString()); return result; } } 复制代码
在resource目录下新增一个spring_mvc_traps_date_transfer.http,定义GET和POST请求发起
### GET http://localhost:8080/get?date=2022-02-01 23:43:00 Accept: application/json ### POST http://localhost:8080/post Content-Type: application/json { "id": "1", "name": "stark", "createTime": "2022-02-01 23:43:00" } 复制代码
发送GET请求
发送POST请求
POST请求中的参数是在请求的BODY中,请求的参数的属性并不会触发 @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")定义的格式,所以会出发JSON转义错误,如何解决这类错误?
使用JsonFormat注解
在UserInfo实体类中的createTime属性增加注解
@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") 复制代码
重新启动应用,发送POST请求
使用自定义格式转换器@JsonDeserialize
@Slf4j public class DateJacksonConverter extends JsonDeserializer<Date> { private static final String[] pattern = new String[] { "yyyy-MM-dd HH:mm:ss", "yyyy/MM/dd" }; @Override public Date deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException, JsonProcessingException { Date targetDate = null; String originDate = jsonParser.getText(); if (StringUtils.isNotEmpty(originDate)) { try { long longDate = Long.parseLong(originDate.trim()); targetDate = new Date(longDate); } catch (NumberFormatException pe) { try { targetDate = DateUtils.parseDate( originDate, DateJacksonConverter.pattern ); } catch (ParseException ex) { log.error("parse error: {}", ex.getMessage()); throw new IOException("parse error"); } } } return targetDate; } } 复制代码
修改UserInfo实体类中createTime属性,将@JsonFormat注解注释,增加@JsonDeserialize(using = DateJacksonConverter.class)注解
修改POST请求传入参数中createTime的格式,再次发起POST请求
POST http://localhost:8080/post Content-Type: application/json { "id": "1", "name": "stark", "createTime": "2022/02/01" } 复制代码
仍然可以转化成功
时间格式的局部处理即对需要时间转换的属性上增加@JsonDeserialize注解,这种方式代码可维护性比较差
全局处理Date格式转换
增加时间格式处理的全局配置类,增加@Configuration及在方法上标注@Bean注解,将该类交个Spring容器管理。
@Configuration public class DateConverterConfig { @Bean public DateJacksonConverter dateJacksonConverter() { return new DateJacksonConverter(); } @Bean public Jackson2ObjectMapperFactoryBean jackson2ObjectMapperFactoryBean( @Autowired DateJacksonConverter dateJacksonConverter) { Jackson2ObjectMapperFactoryBean jackson2ObjectMapperFactoryBean = new Jackson2ObjectMapperFactoryBean(); jackson2ObjectMapperFactoryBean.setDeserializers(dateJacksonConverter); return jackson2ObjectMapperFactoryBean; } } 复制代码
在DateJacksonConverter类中重写handleType()方法,指定针对所有Date类型的属性进行反序列化
@Override public Class<?> handledType() { return Date.class; } 复制代码
将UserInfo实体类中createTime属性上的@JsonDeserialize注解注释掉,重新启动应用,再次发起POST请求
同样可以实现时间格式的转换。