图源:
本篇文章会在中最终代码的基础上进行修改。你可以从下面的链接获取对应的示例代码:
为了说明之前设计的一些缺陷,这里为这个示例代码添加上Swagger文档支持,具体方法见,这里不再详细说明。
因为这个示例程序使用Shiro做权限验证,所以需要将Swagger相关路径添加为例外,不经过Shiro验证:
// 配置不会被拦截的链接 顺序判断
map.put("/swagger-ui/**", "anon");
map.put("/swagger-resources/**", "anon");
map.put("/v3/api-docs/**", "anon");
map.put("/static/**", "anon");
关于Shiro可以阅读。
现在查看Swagger你就会发现一个问题:
所有接口的返回值都是String
类型,所以无法通过Swagger
文档查看具体的返回值格式,也就谈不上前后端分离开发时让前端通过Swagger文档来协同开发。
之所以这样,是因为我们在Controller
中返回的是String
类型:
value = {"guest", "manager"}, logical = Logical.OR)
( "/book")
( "获取所有图书列表")
( public String listAllBooks() {
Result result = new Result();
List<Book> books = bookService.list();
result.setData(books);
return result.toString();
}
而Swagger
正是通过展示Controller
中方法的返回值类型来阐述具体接口的返回值格式。
所以,我们需要让接口能直接返回具体类型而非String
。
但是这样会产生另一个问题,如果返回的是具体的业务类型,那如何将其封装为统一的返回值格式?
其实可以利用Spring的拦截器来实现这点。
处理返回值
我们先看新封装的统一返回值类型:
package cn.icexmoon.demo.books.system.result;
import com.alibaba.fastjson.JSONObject;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
public class Result {
public enum ErrorCode {
SUCCESS(200, "成功"),
DB_NO_DATA(500, "数据库数据缺失"),
PARAM_CHECK(400, "没有通过参数校验"),
LOGIN_ERROR(400, "登录失败"),
NO_PRIORITY(401, "缺少操作的相关权限"),
REQUEST_PARSE(402, "HTTP请求解析错误"),
TIME_PARSE(403, "时间解析出错"),
NEED_LOGIN(404, "需要登录"),
DEFAULT_ERROR(501, "默认错误"),
NETWORK_ERROR(502, "网络错误");
private Integer code;
private String desc;
private ErrorCode(Integer code, String desc) {
this.code = code;
this.desc = desc;
}
public Integer getCode() {
return code;
}
public String getDesc() {
return desc;
}
public String toString() {
return this.code.toString();
}
}
"调用成功/失败")
( private boolean success = true;
"调用失败后的提示信息")
( private String msg = "";
"接口返回的数据")
( private Object data = null;
"错误码")
( private ErrorCode code = ErrorCode.SUCCESS;
private static final Result SUCCESS_RESULT = new Result();
/**
* 获取成功Result
*
* @return
*/
public static Result success() {
return SUCCESS_RESULT;
}
/**
* 获取成功的Result
*
* @param data
* @return
*/
public static Result success(Object data) {
Result result = new Result();
result.setData(data);
return result;
}
/**
* 获取失败Result
*
* @param errorCode 错误码
* @param msg 错误信息
* @return
*/
public static Result fail(ErrorCode errorCode, String msg) {
Result result = new Result();
result.setSuccess(false);
result.setCode(errorCode);
result.setData(null);
result.setMsg(msg);
return result;
}
/**
* 获取失败Result
*
* @param exp 产生错误的异常
* @return
*/
public static Result fail(ResultException exp) {
Result result = new Result();
result.setSuccess(false);
result.setCode(exp.getCode());
result.setData(null);
result.setMsg(exp.getMessage());
return result;
}
public String toString() {
JSONObject jsonObject = new JSONObject();
jsonObject.put("success", success);
jsonObject.put("msg", msg);
jsonObject.put("data", data);
jsonObject.put("code", code.getCode());
return jsonObject.toString();
}
}
这个新的Result
类型除了包含基本的data
和success
等,更便利的是使用内嵌的枚举封装了系统错误类型,这样在编程的时候就无需使用类属性或者字符串来指定错误,并且枚举自带desc
属性可以对错误进行说明。
就像前边说的,因为之后Controller
层的方法会直接返回具体业务类型,所以我们需要添加拦截器来对具体类型统一封装:
package cn.icexmoon.demo.books.system;
import cn.icexmoon.demo.books.system.result.IResult;
import cn.icexmoon.demo.books.system.result.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
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;
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
private ObjectMapper objectMapper;
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
return true;
}
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
if (o instanceof IResult) {
return Result.success(o);
} else if (o == null) {
return Result.success(null);
} else {
;
}
return o;
}
}
这里拦截了所有HTTP请求的返回值,这其中包含一些静态资源,如果对其进行封装显然会造成系统错误。所以这里我使用了一个标记接口IResult
来识别是否是Controller
层返回的需要封装的对象,如果是,再进行封装。
package cn.icexmoon.demo.books.system.result;
/**
* 标记需要被包装为Result的返回类型
*/
public interface IResult {
}
现在我们修改Controller
中的接口方法,让其返回具体类型:
package cn.icexmoon.demo.books.book.controller;
import cn.icexmoon.demo.books.book.entity.Book;
import cn.icexmoon.demo.books.book.service.IBookService;
import cn.icexmoon.demo.books.system.result.IResultArrayList;
import cn.icexmoon.demo.books.system.result.Result;
import cn.icexmoon.demo.books.user.entity.User;
import cn.icexmoon.demo.books.user.service.IUserService;
import io.swagger.annotations.ApiOperation;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* <p>
* 前端控制器
* </p>
*
* @author icexmoon
* @since 2022-05-06
*/
public class BookController {
private IBookService bookService;
private IUserService userService;
value = {"guest", "manager"}, logical = Logical.OR)
( "/book")
( "获取所有图书列表")
( public List<Book> listAllBooks() {
Result result = new Result();
List<Book> books = new IResultArrayList<>();
books.addAll(bookService.list());
return books;
}
"manager")
( "/book/add")
( "添加书籍")
( public Result addBook( Book book) {
//添加图书
Subject subject = SecurityUtils.getSubject();
String name = (String) subject.getPrincipal();
User user = userService.getUserByName(name);
book.setUserId(user.getId());
bookService.save(book);
return Result.success();
}
}
这里为了能封装返回的List
,我定义了一个实现了IResult
的IResultArrayList
:
package cn.icexmoon.demo.books.system.result;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
public class IResultArrayList<T> extends ArrayList<T> implements IResult {
public <E> IResultArrayList() {
super();
}
public <E> IResultArrayList(List<E> items, Function<E, T> mapFunc) {
this();
for (E item : items) {
add(mapFunc.apply(item));
}
}
}
实际上也可以在拦截器中用反射检查对象是否为List
的子类型来决定是否封装。
现在再看Swagger中的类型:
这样就可以愉快地让前端自己去看返回值格式了。
处理异常
最后还要重构下对错误的处理,定义一个统一的系统异常:
package com.sjcx.ccsp.system.result;
public class ResultException extends RuntimeException {
private Result.ErrorCode code;
public ResultException(Result.ErrorCode code, String msg) {
super(msg);
this.code = code;
}
public Result.ErrorCode getCode() {
return code;
}
}
在全局异常拦截器中拦截该类型的异常并包装成Result
类型返回:
/**
* 拦截并处理ResultException异常
*
* @param exp
* @param model
* @return
*/
ResultException.class)
( public Result doHandleResultException(ResultException exp, Model model) {
return Result.fail(exp);
}
现在可以将之前产生错误并返回Result
的地方全部改成简单的抛出异常:
package cn.icexmoon.demo.books.system;
import cn.icexmoon.demo.books.system.result.Result;
import cn.icexmoon.demo.books.system.result.ResultException;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
public class Login {
/**
* 根据用户名和密码检查身份并登录
*
* @param name
* @param password
* @return
*/
public Result checkAndLogin(String name, String password) {
Result result = new Result();
if (ObjectUtils.isEmpty(name) || ObjectUtils.isEmpty(password)) {
throw new ResultException(Result.ErrorCode.LOGIN_ERROR, "用户名或密码为空。");
}
//用户认证信息
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(
name,
password
);
try{
subject.login(usernamePasswordToken);
}
catch (UnknownAccountException e){
throw new ResultException(Result.ErrorCode.LOGIN_ERROR, "账户不存在");
}
catch (AuthenticationException e){
throw new ResultException(Result.ErrorCode.LOGIN_ERROR, "账号或密码错误");
}
catch (AuthorizationException e){
throw new ResultException(Result.ErrorCode.DEFAULT_ERROR, "没有权限");
}
result.setData(subject.getSession().getId());
return result;
}
}
故意输错密码试一下:
可以看到拦截器很好地发挥了作用,将异常封装成了统一返回值。
好了,这篇文章就到这里了,谢谢阅读。
本篇文章的最终代码见。
文章评论