上篇文章:
目录
1 数据返回格式
前后端分离的开发模式下,如果后端各种接口返回的数据不一致,就会导致前端开发很困难。并且如果后端返回的数据需要扩展,直接返回基本类型就会导致修改时很麻烦。因此通常后端返回自定义数据格式:
public enum ResultStatus {
SUCCESS(200),
UNLOGIN(-1),
FAIL(-2);
private Integer code;
ResultStatus(int code) {
this.code = code;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
}
@Data
public class Result {
private ResultStatus status;
private String errorMessage;
private Object data;
/**
* 业务执行成功时返回的方法
*
* @param data
* @return
*/
public static Result success(Object data) {
Result result = new Result();
result.setStatus(ResultStatus.SUCCESS);
result.setErrorMessage("");
result.setData(data);
return result;
}
/**
* 业务执行失败时返回的方法
*
* @param
* @return
*/
public static Result fail(String msg) {
Result result = new Result();
result.setStatus(ResultStatus.FAIL);
result.setErrorMessage(msg);
result.setData("");
return result;
}
/**
* 用户未登录时返回的方法
*
* @param
* @return
*/
public static Result unlogin() {
Result result = new Result();
result.setStatus(ResultStatus.UNLOGIN);
result.setErrorMessage("用户未登录");
result.setData(null);
return result;
}
}
通过Result类来统一对接口返回的数据进行包装,比如登录接口返回:
@RequestMapping("/login")
public Result login(String username, String password, HttpSession session) {
//账号或密码为空
if (!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
return Result.fail("账号或密码为空!");
}
//模拟验证数据, 账号密码正确
if ("admin".equals(username) && "123456".equals(password)) {
session.setAttribute("userName", username);
return Result.success(true);
}
//账号密码错误
return Result.fail("账号或密码错误!");
}
2 统一数据返回格式
但是实际中,每个方法返回的数据很灵活,有可能这个方法使用的Result来封装数据,但是其他方法可能没有使用Result封装数据,那我们统一数据返回格式的目的就没有达到。SpringBoot的统一数据返回格式使用方法如下:
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Autowired
public ObjectMapper objectMapper;
@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) {
// 防止数据过度封装(接口中的某些方法可能已经使用了Result类来封装数据)
if(body instanceof Result){
return body;
}
// 如果返回的是String类型,使用Result来封装会报错
if(body instanceof String){
return objectMapper.writeValueAsString(Result.success(body));
}
return Result.success(body);
}
}
定义的统一数据返回格式的类必须实现ResponseBodyAdvice接口,同时加上@ControllerAdvice注解(@Component的子类)来交给Spring管理。
supports()方法:判断是否要执行beforeBodyWrite方法,true为执行,false不执行。通过该方法可以选择哪些类或哪些方法的response要进行处理,其他的不进行处理。
beforeBodyWrite方法:对response方法进行具体操作处理。@SneakyThrows注解可以统一处理方法中的异常。
注意:如果返回的是String类型,使用Result来封装会报错。原因是SpringMVC默认会注册一些自带的HttpMessageConverter(消息转换器,理解为用来转换数据格式的),从先后顺序排列分别为ByteArrayHttpMessageConverter、StringHttpMessageConverter、SourceHttpMessageConverter、AllEncompassingFormHttpMessageConverter(内部根据依赖的包添加其他类型的转换器,有json格式的转化器)。下述为RequestMappingHandlerAdapter类的构造方法:
public RequestMappingHandlerAdapter() {
this.messageConverters.add(new ByteArrayHttpMessageConverter());
this.messageConverters.add(new StringHttpMessageConverter());
if (!shouldIgnoreXml) {
try {
this.messageConverters.add(new SourceHttpMessageConverter());
} catch (Error var2) {
}
}
this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
}
当返回类型是String类型时,会先匹配到StringHttpMessageConverter转化器;如果引入了json的依赖,就会匹配json格式的转化器。下述为AllEncompassingFormHttpMessageConverter类的构造方法:
public AllEncompassingFormHttpMessageConverter() {
if (!shouldIgnoreXml) {
try {
this.addPartConverter(new SourceHttpMessageConverter());
} catch (Error var2) {
}
if (jaxb2Present && !jackson2XmlPresent) {
this.addPartConverter(new Jaxb2RootElementHttpMessageConverter());
}
}
if (kotlinSerializationJsonPresent) {
this.addPartConverter(new KotlinSerializationJsonHttpMessageConverter());
}
if (jackson2Present) {
this.addPartConverter(new MappingJackson2HttpMessageConverter());
} else if (gsonPresent) {
this.addPartConverter(new GsonHttpMessageConverter());
} else if (jsonbPresent) {
this.addPartConverter(new JsonbHttpMessageConverter());
}
if (jackson2XmlPresent && !shouldIgnoreXml) {
this.addPartConverter(new MappingJackson2XmlHttpMessageConverter());
}
if (jackson2SmilePresent) {
this.addPartConverter(new MappingJackson2SmileHttpMessageConverter());
}
}
在StringHttpMessageConverter转化器中,重写了父类AbstractMessageConverter的addDefaultHeaders()方法,方法的参数是String类型的数据(String s)。
protected void addDefaultHeaders(HttpHeaders headers, String s, @Nullable MediaType type) throws IOException {
if (headers.getContentType() == null && type != null && type.isConcrete() && (type.isCompatibleWith(MediaType.APPLICATION_JSON) || type.isCompatibleWith(APPLICATION_PLUS_JSON))) {
headers.setContentType(type);
}
super.addDefaultHeaders(headers, s, type);
}
而在AbstractMessageConverterMethodProcessor类中执行了writeWithMessageConverters()方法负责数据格式的转化,在该方法中先执行beforeBodyWrite()方法把数据转化为Result,接着又执行了write()方法。write()方法是HttpMessageConverter类的方法,是空方法,由子类实现。在其StringHttpMessageConverter子类的实现中,内部调用addDefaultHeaders()方法,把已经是Result类型的数据作为参数传入到String类型参数的addDefaultHeaders()方法,参数类型不匹配,因此报错。
protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType, ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
// 该方法源码很长, 这里仅展示关键部分
body = this.getAdvice().beforeBodyWrite(body, returnType, selectedMediaType, converter.getClass(), inputMessage, outputMessage);
if (body != null) {
LogFormatUtils.traceDebug(this.logger, (traceOn) -> {
return "Writing [" + LogFormatUtils.formatValue(body, !traceOn) + "]";
});
this.addContentDispositionHeader(inputMessage, outputMessage);
if (genericConverter != null) {
genericConverter.write(body, (Type)targetType, selectedMediaType, outputMessage);
} else {
converter.write(body, selectedMediaType, outputMessage);
}
} else if (this.logger.isDebugEnabled()) {
this.logger.debug("Nothing to write: null body");
}
}
下篇文章: