1. 基础代码
1.1. 封装代码
代码使用了Lombok
注解。
首先提供一个枚举,用于封装返回的提示码和提示信息。
java
package com.example.demo.common.result;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author wangbo
* @date 2021/05/12
*/
@Getter
@AllArgsConstructor
public enum ResultCode {
//成功提示码
SUCCESS(20000, "成功"),
//自定义失败信息
FAILURE(50000, "失败"),
//通用错误码 50001~50099
PROGRAM_INSIDE_EXCEPTION(50001, "程序内部异常"),
REQUEST_PARAM_ERROR(50002, "请求参数错误");
//用户模块错误码 50100~50199
//商品模块错误码 50200~50299
//订单模块错误码 50300~50399
private final Integer code;
private final String message;
}
接下来提供一个返回类型,这里使用了泛型,用于对返回数据进行统一包装。
java
package com.example.demo.common.result;
import lombok.Data;
/**
* @author wangbo
* @date 2021/05/12
*/
@Data
public class Result<T> {
private Integer code;
private String message;
private T data;
/**
* 成功
*/
public static Result<Void> success() {
Result<Void> result = new Result<>();
result.setCode(ResultCode.SUCCESS.getCode());
result.setMessage(ResultCode.SUCCESS.getMessage());
return result;
}
/**
* 成功,有返回数据
*/
public static <V> Result<V> success(V data) {
Result<V> result = new Result<>();
result.code = ResultCode.SUCCESS.getCode();
result.message = ResultCode.SUCCESS.getMessage();
result.data = data;
return result;
}
/**
* 失败
*/
public static Result<Void> failure() {
Result<Void> result = new Result<>();
result.setCode(ResultCode.FAILURE.getCode());
result.setMessage(ResultCode.FAILURE.getMessage());
return result;
}
/**
* 失败,自定义失败信息
*/
public static Result<Void> failure(String message) {
Result<Void> result = new Result<>();
result.setCode(ResultCode.FAILURE.getCode());
result.setMessage(message);
return result;
}
/**
* 失败,使用已定义枚举
*/
public static Result<Void> failure(ResultCode resultCode) {
Result<Void> result = new Result<>();
result.setCode(resultCode.getCode());
result.setMessage(resultCode.getMessage());
return result;
}
}
1.2. 使用示例
java
package com.example.demo.controller;
import com.example.demo.entity.User;
import com.example.demo.result.Result;
import com.example.demo.result.ResultCode;
import com.example.demo.service.UserService;
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;
/**
* @auther wangbo
* @date 2021-01-10 17:48
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/list")
public Result<List<User>> list(){
List<User> list = userService.list();
return Result.success(list);
}
@GetMapping("/success")
public Result<Void> success(){
return Result.success();
}
@GetMapping("/failure")
public Result<Void> failure(){
return Result.failure("测试自定义失败信息");
}
@GetMapping("/failure1")
public Result<Void> failure1(){
return Result.failure(ResultCode.REQUEST_PARAM_ERROR);
}
}
1.3. 结果示例
http://localhost:8080/user/list
json
{
"code":20000,
"message":"成功",
"data":[
{
"id":1,
"name":"张三",
"age":32,
"email":"jxd@baomidou",
"managerId":3,
"createTime":"2020-11-04T19:04:22",
"remark":null
},
{
"id":2,
"name":"李四",
"age":32,
"email":"hb@baomidou",
"managerId":3,
"createTime":"2020-11-04T19:18:14",
"remark":null
},
{
"id":3,
"name":"王五",
"age":32,
"email":"@baomidou",
"managerId":2,
"createTime":"2020-11-04T19:20:12",
"remark":null
}
]
}
http://localhost:8080/user/success
json
{
"code":20000,
"message":"成功",
"data":null
}
http://localhost:8080/user/failure
json
{
"code":50000,
"message":"测试自定义失败信息",
"data":null
}
http://localhost:8080/user/failure1
json
{
"code":50002,
"message":"请求参数错误",
"data":null
}
2. 代码优化
对于上面的使用示例,可以继续做如下修改:
- 对于没有返回数据的接口可以继续使用上面的写法,比如增加,删除,更新接口,统一包装类不会再进行重复包装处理。
- 对于有返回数据的接口,比如查询接口,可以把返回值进行统一包装处理,这样每个查询接口返回的就是业务对象,比如
List<User>
,而不包装对象Result<T>
了。统一包装处理会把这些接口的返回值包装为Result<T>
格式。 - 对于一些第三方回调之类的接口,可能要求我们的接口响应格式符合第三方的规定,所以这些接口不需要进行统一包装,我们可以通过在这些接口上添加一个自定义注解来绕过统一包装类的处理。
注意:如果你的项目使用Swagger
的话,其实这样修改会让生成的接口文档中的响应格式不完整。比如对于本文中的/user/list
查询接口,文档中显示的返回结果将会是List<User>
格式,而不是Result<List<User>>
格式。
这种修改将代码复杂化了,非必要情况下不需要这么做!
2.1. 返回值包装
这里使用到了@RestControllerAdvice
注解,该注解既可以全局拦截异常也可拦截正常的返回值,并且可以对返回值进行修改。添加一个接口返回值包装类:
java
package com.example.demo.advice;
import com.example.demo.annotation.NotResultWrap;
import com.example.demo.result.Result;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
/**
* 接口返回值包装
* @auther wangbo
* @date 2021-01-13 15:55
*/
@Slf4j
@RestControllerAdvice(basePackages = "com.example.demo.controller")
public class ResponseControllerAdvice implements ResponseBodyAdvice<Object> {
/**
* 判断哪些接口需要进行返回值包装
* 返回 true 才会执行 beforeBodyWrite 方法;返回 false 则不执行。
*/
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
log.info("判断是否需要进行返回值包装");
//如果接口方法返回 Result 不需要再次包装
//如果接口方法使用了 @NotResultWrap 注解,表示不需要包装了
//只对成功的请求进行返回包装,异常情况统一放在全局异常中进行处理
return !(returnType.getParameterType().equals(Result.class)
|| returnType.hasMethodAnnotation(NotResultWrap.class));
}
/**
* 进行接口返回值包装
*/
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
log.info("进行返回值包装");
Result<Object> result = new Result<>();
//String 类型不能直接包装,需要进行特殊处理
if (returnType.getParameterType().equals(String.class)){
ObjectMapper objectMapper = new ObjectMapper();
try {
//使用 jackson 将返回数据转换为 json
return objectMapper.writeValueAsString(result.success(body));
} catch (JsonProcessingException e) {
//这里会走统一异常处理
throw new RuntimeException("String类型返回值包装异常");
}
}
return result.success(body);
}
}
写这个类的时候有以下几点注意事项:
(1) @RestControllerAdvice(basePackages = "com.example.demo.controller")
该注解必须写basePackages
属性,用来指定该返回值包装类所处理的接口包。如果不指定这个属性的话,该包装类会拦截所有的响应。比如404
之类的响应,不属于系统异常,不会被全局异常处理类处理,但是会被返回值包装类拦截,并且会被包装,但是这些异常响应其实是不需要也不应该进行包装的。
未指定basePackages
属性,404
的响应,明显是不合理的:
json
{
"code": 20000,
"message": "成功",
"data": {
"timestamp": "2021-01-17T10:42:04.406+00:00",
"status": 404,
"error": "Not Found",
"message": "",
"path": "/user_info/add3"
}
}
指定了basePackages
属性,404
的响应:
json
{
"timestamp": "2021-01-17T10:26:00.865+00:00",
"status": 404,
"error": "Not Found",
"message": "",
"path": "/user_info/add3"
}
(2)对于返回值为String
的接口,需要单独处理,如果没有进行单独处理,程序会抛出一个类转换异常。
SpringMVC
内部定义了九个不同的MessageConverter
用来处理不同的返回值。在AbstractMessageConverterMethodProcessor
类下面的writeWithMessageConverters
方法可以看出来,每个MessageConverer
是根据返回类型和媒体类型来选择处理的MessageConverter
的。
Controller
层中返回的类型是String
,但是在ResponseBodyAdvice
实现类中,我们把响应的类型修改成了Result
。这就导致了Spring
在处理MessageConverter
的时候,依旧根据之前的String
类型选择对应String
类型的StringMessageConverter
。而在StringMessageConverter
类型,它只接受String
类型的返回值,所以我们在ResponseBodyAdvice
中将返回值从String
类型改成Result
类型之后,调用StringMessageConverter
方法发生类型强转。Result
无法转换成String
,发生类型转换异常。所以需要单独处理。
当然,也可以选择对String
返回类型的接口不进行包装处理,直接返回原生String
,这种直接在supports
方法中进行拦截就可以了。
2.2. 自定义枚举
用于标志接口方法不需要进行统一返回值包装的自定义注解:
java
package com.example.demo.annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* 不进行返回值包装注解
* @auther wangbo
* @date 2021-01-13 20:55
*/
@Target({METHOD})
@Retention(RUNTIME)
public @interface NotResultWrap {
}
2.3. 接口修改示例
接口方法可以修改如下,前四个接口的返回结果和上面的示例是一样的:
建议:有返回数据的走包装,无数据返回的直接返回Result,提高性能。
java
package com.example.demo.controller;
import com.example.demo.annotation.NotResultWrap;
import com.example.demo.entity.User;
import com.example.demo.result.Result;
import com.example.demo.result.ResultCode;
import com.example.demo.service.UserService;
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;
/**
* @auther wangbo
* @date 2021-01-10 17:48
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
//只有这个方法进行了返回值改变,去掉了外层的Result包装,在接口返回值包装类中进行统一包装
@GetMapping("/list")
public List<User> list(){
return userService.list();
}
@GetMapping("/success")
public Result<Void> success(){
return Result.success();
}
@GetMapping("/failure")
public Result<Void> failure(){
return Result.failure("测试自定义失败信息");
}
@GetMapping("/failure1")
public Result<Void> failure1(){
return Result.failure(ResultCode.REQUEST_PARAM_ERROR);
}
//测试字符串的包装
@GetMapping("/string")
public String string(){
return "测试字符串包装";
}
//测试不进行返回值包装注解的效果
@NotResultWrap
@GetMapping("/not_wrap")
public String notWrap(){
return "测试不包装注解";
}
}
后面新加的两个接口的返回结果示例:
http://localhost:8080/user/string
这个接口返回的其实是String
格式,只不过这是个JSON
格式的字符串。
json
{"code":20000,"message":"成功","data":"测试字符串包装"}
http://localhost:8080/user/not_wrap
json
测试不包装注解