目录
一、统一数据返回格式
在Spring框架中实现统一数据返回格式是构建规范化API的核心需求,它能让所有接口的响应保持一致的JSON结构,方便前端处理。
1.引入统一数据返回格式
- 添加统一数据返回格式之前:
不同的接口返回的格式不一样,杂乱无章,很混乱,对于前端来说不方便处理(上图只是其中一个接口)。
- 添加统一数据返回格式之后:
可以看到,添加之后, 不同接口的返回数据的格式变得更加规范,方便前端处理。
2.学习使用统一数据返回格式
统一的数据返回格式使用 @ControllerAdvice 和 ResponseBodyAdvice 的方式实现 @ControllerAdvice 表示控制器通知类。添加 ResponseAdvice 来实现 ResponseBodyAdvice 接口,并在类上添加 @ControllerAdvice 注解。
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType,
MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest
request, ServerHttpResponse response) {
return Result.success(body);
}
}
support方法
该方法是判断是否要执行 beforeBodyWrite 方法,true为执行,false不执行,通过该方法可以选择哪些类或哪些方法的response要进行处理,其他的不进行处理。
beforeBodyWrite方法
对响应数据结果进行统一处理
统一数据返回格式具体逻辑
以下面的代码为例:
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
return Result.SUCCESS(body);
}
补充:Result是一个实体类,封装了对应格式的属性(status,message...)以及静态方法,其中SUCCESS就是其中一个静态方法。
当controller的接口方法返回 不同类型的值时,会把该接口方法的返回值作为body参数,然后再调用Result.SUCCESS(body),让该接口方法的返回值被Result.SUCCESS(body)的值替代。总结就是:通过 ResponseBodyAdvice
将 Controller 方法的返回值统一包装为 Result
对象。
- 比如:
当接口返回类型boolean且返回true时,true作为body,然后再调用Result.SUCCESS(body),最后该接口返回值得到了封装后的结果。
使用统一数据返回格式存在的问题
当接口返回值类型为String时,会抛出异常:
异常表示为:Result类型的不能转换为String类型。其中只有返回结果为String类型时才会有这种错误发生。
原因:当接口返回的数据类型是String时,Spring会选择 StringHttpMessageConverter,该转换器只能处理 String 类型的返回值,又因为使用了统一数据返回格式,该接口实际返回的是Result类型的对象,而 StringHttpMessageConverter无法将 Result 对象转换为String,导致抛出异常:“Result cannot be cast to java.lang.String”。
解决方法:
- 可以手动序列化Result对象为JSON字符串,最终返回的是一个String类型的JSON字符串,与控制器中声明的String返回类型兼容。
- 同时我们还可以进行优化:把接口返回值类型为Result的进行单独处理,避免出现嵌套的情况。
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
ObjectMapper mapper = new ObjectMapper();
//supports方法是判断是否要执行beforeBodyWrite方法,true为执行,false不执行
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
@SneakyThrows
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
//body为其他接口的返回结果
//返回结果更加灵活,避免出现嵌套情况
if (body instanceof Result){
return body;
}
//如果返回结果为String类型,使用SpringBoot内置提供的Jackson来实现信息的序列化
if (body instanceof String){
return mapper.writeValueAsString(Result.SUCCESS(body));
}
//如果返回结果为其他类型,把返回结果作为参数
return Result.SUCCESS(body);
}
}
统一数据返回格式的优点
- 方便前端程序员能更好的接收和解析后端数据接口返回的数据;
- 降低前端程序员和后端程序员的沟通成本,按照某个格式实现就可以了,因为所有接口都是这样返回的;
- 有利于项目统一数据的维护和修改;
- 有利于后端技术部门的统一规范的标准制定,不会出现奇怪的返回内容。
统一数据返回格式代码实现(包含了拦截器):
- 常量层
public class Constants {
public static final String SUCCESS = "SUCCESS";
public static final String FAILURE = "FAILURE";
}
- 实现统一功能类:ResponseAdvice
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
ObjectMapper mapper = new ObjectMapper();
//supports方法是判断是否要执行beforeBodyWrite方法,true为执行,false不执行
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
@SneakyThrows
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
//body为其他接口的返回结果
//返回结果更加灵活
if (body instanceof Result){
return body;
}
//如果返回结果为String类型,使用SpringBoot内置提供的Jackson来实现信息的序列化
if (body instanceof String){
return mapper.writeValueAsString(Result.SUCCESS(body));
}
//如果返回结果为其他类型,把返回结果作为参数
return Result.SUCCESS(body);
}
}
- 实体层User
@Data
public class User {
private String username;
private String password;
}
- 实体层Result
@Data
public class Result<T> {
private String status;
private T message;
public static <T>Result<T> SUCCESS(T message) {
Result<T> result = new Result<>();
result.setStatus(Constants.SUCCESS);
result.setMessage(message);
return result;
}
public static <T>Result<T> FAIL(T message) {
Result<T> result = new Result<>();
result.setStatus(Constants.FAILURE);
result.setMessage(message);
return result;
}
}
- 控制层
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
GetUser getUser;
@RequestMapping("/login")
public Result login(String username, String password, HttpServletRequest request) {
User user = getUser.getUser(username, password);
if (user == null) {
return Result.FAIL("账号或密码错误");
}
request.getSession().setAttribute("user", user);
return Result.SUCCESS("账号密码正确,登录成功!!");
}
@RequestMapping("/enter")
public String Enter(){
return "进入enter成功哈哈";
}
}
- Service层
@Service
public class GetUser {
public User getUser(String username, String password) {
if (username == null || password == null) {
return null;
}
if (!"zhangsan".equals(username) ||!"123".equals(password)) {
return null;
}
User user = new User();
user.setUsername("zhangsan");
user.setPassword("123");
return user;
}
}
- 注册拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {
//自定义拦截器对象
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
//排除拦截的页面,登录页
.excludePathPatterns("/user/login");
}
}
- 定义拦截器
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
log.info("拦截开始");
HttpSession session = request.getSession();
User user = (User) session.getAttribute( "user");
if (user == null) {
log.info("被拦截");
return false;
}else {
log.info("不拦截,放行");
return true;
}
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response,
Object handler, ModelAndView modelAndView) throws Exception {
log.info("enter方法执行后");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("LoginInterceptor 视图渲染完毕后执⾏,最后执⾏");
}
}
二、统一异常处理
统一异常处理使用的是 @ControllerAdvice + @ExceptionHandler 来实现的,@ControllerAdvice 表示控制器通知类,@ExceptionHandler是异常处理器,两个结合表示出现异常的时候执行某个通知,也就是执行某个方法事件。
@ControllerAdvice
@ResponseBody
public class ErrorAdvice {
@ExceptionHandler
public Object handler(Exception e) {
return Result.fail(e.getMessage());
}
}
类名,方法名和返回值可以自定义,重要的是注解
使用@ResponseBody 是为了处理返回数据的接口,让返回的结果为序列化的数据(JSON/XML),而不是视图(HTML页面)。
以上代码表示,如果代码出现Exception异常(包括Exception的子类),就返回一个Result的对象,Result对象的设置参考 Result.fail(e.getMessage())。
public static <T>Result<T> FAIL(T message) {
Result<T> result = new Result<>();
result.setStatus(Constants.FAILURE);
result.setMessage(message);
return result;
}
在统一数据返回格式的代码基础上,我们可以针对不同的异常,返回不同的结果。
@ControllerAdvice
@ResponseBody
public class ErrorAdvice {
@ExceptionHandler
public Object handler(Exception e) {
return Result.FAIL(e.getMessage());
}
@ExceptionHandler
public Object handler(ArithmeticException e) {
return Result.FAIL("发生ArithmeticException异常,异常信息:"+e.getMessage());
}
@ExceptionHandler
public Object handler(NullPointerException e) {
return Result.FAIL("发生NullPointerException异常:"+e.getMessage());
}
}
创建控制类模拟制造异常:
@RestController
@RequestMapping("/test")
public class TestController {
@RequestMapping("/t1")
public Integer t1(){
String a = null;
return a.length();
}
@RequestMapping("/t2")
public Integer t2(){
int a = 10/0;
return a;
}
}
运行结果:
当有多个异常通知时,匹配顺序为当前类及其子类向上一次匹配 。