大家都知道,前后分離之后,后端響應最好以統一的格式的響應.
譬如:
名稱 | 描述 |
---|---|
status | 狀態碼,標識請求成功與否,如 [1:成功;-1:失敗] |
errorCode | 錯誤碼,給出明確錯誤碼,更好的應對業務異常;請求成功該值可為空 |
errorMsg | 錯誤消息,與錯誤碼相對應,更具體的描述異常信息 |
resultBody | 返回結果,通常是 Bean 對象對應的 JSON 數據, 通常為了應對不同返回值類型,將其聲明為泛型類型 |
話不多說,直接上代碼
1. 定義一個統一響應結果類CommonResult<T>
import lombok.Data; @Data public final class CommonResult<T> { private int status = 1; private String errorCode = ""; private String errorMsg = ""; private T resultBody; public CommonResult() { } public CommonResult(T resultBody) { this.resultBody = resultBody; } }
2. 自定義一個ResponseBodyAdvice類
import org.springframework.context.annotation.Configuration; 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.config.annotation.EnableWebMvc; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; import qinfeng.zheng.data.common.CommonResult; @EnableWebMvc @Configuration @RestControllerAdvice(basePackages = "qinfeng.zheng.data.api") public class CommonResultResponseAdvice implements ResponseBodyAdvice<Object> { @Override public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { return true; } @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { if (body instanceof CommonResult) { return body; } return new CommonResult<>(body); } }
3. 因為springmvc默認的message converter是Jackson框架,這個框架有點渣, 所以我們改成Fastjson
import com.alibaba.fastjson.serializer.SerializerFeature; import com.alibaba.fastjson.support.config.FastJsonConfig; import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter; import org.springframework.context.annotation.Configuration; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; @Configuration @EnableWebMvc public class WebConfig implements WebMvcConfigurer { @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { FastJsonHttpMessageConverter fastJsonConverter = new FastJsonHttpMessageConverter(); FastJsonConfig config = new FastJsonConfig(); config.setCharset(Charset.forName("UTF-8")); config.setDateFormat("yyyyMMdd HH:mm:ssS"); //設置允許返回為null的屬性 config.setSerializerFeatures(SerializerFeature.WriteMapNullValue); fastJsonConverter.setFastJsonConfig(config); List<MediaType> list = new ArrayList<>(); list.add(MediaType.APPLICATION_JSON_UTF8); fastJsonConverter.setSupportedMediaTypes(list); converters.add(fastJsonConverter); } }
4. 寫一個Vo類和一個Controller類做測試,但是需要注解Controller類的包路徑一定要與RestControllerAdvice注解中定義的package路徑一致
import lombok.Data; import lombok.experimental.Accessors; @Accessors(chain = true) @Data(staticConstructor = "of") public class UserVo { private Integer id; private String name; private Integer age; }
package qinfeng.zheng.data.api; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import qinfeng.zheng.data.entity.UserVo; import java.util.ArrayList; import java.util.List; @RestController public class TestController { @GetMapping("/index") public String index() { return "index"; } @GetMapping("/list") public List<UserVo> list() { List<UserVo> list = new ArrayList<>(); UserVo userVo1 = UserVo.of().setId(1).setName("admin").setAge(12); UserVo userVo2 = UserVo.of().setId(2).setName("root").setAge(100); list.add(userVo1); list.add(userVo2); return list; } @GetMapping("/entity") public UserVo entity() { UserVo userVo1 = UserVo.of().setId(1).setName("admin").setAge(12); return userVo1; } @GetMapping("/responseEntity") public ResponseEntity responseEntity() { return new ResponseEntity(UserVo.of().setId(1).setName("大象").setAge(18), HttpStatus.OK); } }
5. 啟動springboot項目,測試即可
測試應該是沒有問題.,現在來看看源碼,先看ResponseBodyAdvice接口
/** * Allows customizing the response after the execution of an {@code @ResponseBody} * or a {@code ResponseEntity} controller method but before the body is written * with an {@code HttpMessageConverter}. * * <p>Implementations may be registered directly with * {@code RequestMappingHandlerAdapter} and {@code ExceptionHandlerExceptionResolver} * or more likely annotated with {@code @ControllerAdvice} in which case they * will be auto-detected by both. * * @author Rossen Stoyanchev * @since 4.1 * @param <T> the body type */ public interface ResponseBodyAdvice<T> { /** * Whether this component supports the given controller method return type * and the selected {@code HttpMessageConverter} type. * @param returnType the return type * @param converterType the selected converter type * @return {@code true} if {@link #beforeBodyWrite} should be invoked; * {@code false} otherwise */ boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType); /** * Invoked after an {@code HttpMessageConverter} is selected and just before * its write method is invoked. * @param body the body to be written * @param returnType the return type of the controller method * @param selectedContentType the content type selected through content negotiation * @param selectedConverterType the converter type selected to write to the response * @param request the current request * @param response the current response * @return the body that was passed in or a modified (possibly new) instance */ @Nullable T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response); }
注釋說的很明白,該接口方法是在Controller方法執行之后, 且HttpMessageConverter執行之前調用,所以,我們完全可以在這兒對Controller方法返回的結果進行統一格式處理,然后再說消息轉換器進行轉換,響應到視圖層.
下面再來分析一下 CommonResultResponseAdvice 類是如何加載到spring的上下文環境中的.
通過@EnableWebMvc注解 ----> DelegatingWebMvcConfiguration.class ----> WebMvcConfigurationSupport.class
WebMvcConfigurationSupport這個類就很關鍵了......
在這個類中,有這樣一段代碼
/** * Returns a {@link RequestMappingHandlerAdapter} for processing requests * through annotated controller methods. Consider overriding one of these * other more fine-grained methods: * <ul> * <li>{@link #addArgumentResolvers} for adding custom argument resolvers. * <li>{@link #addReturnValueHandlers} for adding custom return value handlers. * <li>{@link #configureMessageConverters} for adding custom message converters. * </ul> */ @Bean public RequestMappingHandlerAdapter requestMappingHandlerAdapter( @Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager, @Qualifier("mvcConversionService") FormattingConversionService conversionService, @Qualifier("mvcValidator") Validator validator) { RequestMappingHandlerAdapter adapter = createRequestMappingHandlerAdapter(); adapter.setContentNegotiationManager(contentNegotiationManager); adapter.setMessageConverters(getMessageConverters()); adapter.setWebBindingInitializer(getConfigurableWebBindingInitializer(conversionService, validator)); adapter.setCustomArgumentResolvers(getArgumentResolvers()); adapter.setCustomReturnValueHandlers(getReturnValueHandlers()); if (jackson2Present) { adapter.setRequestBodyAdvice(Collections.singletonList(new JsonViewRequestBodyAdvice())); adapter.setResponseBodyAdvice(Collections.singletonList(new JsonViewResponseBodyAdvice())); } AsyncSupportConfigurer configurer = new AsyncSupportConfigurer(); configureAsyncSupport(configurer); if (configurer.getTaskExecutor() != null) { adapter.setTaskExecutor(configurer.getTaskExecutor()); } if (configurer.getTimeout() != null) { adapter.setAsyncRequestTimeout(configurer.getTimeout()); } adapter.setCallableInterceptors(configurer.getCallableInterceptors()); adapter.setDeferredResultInterceptors(configurer.getDeferredResultInterceptors()); return adapter; }
其它都無所謂,至少我們知道這兒會將 RequestMappingHandlerAdapter注入到spring的上下文環境中去. 而RequestMappingHandlerAdapter又是 InitializingBean 接口的一個實現. 看到InitializingBean 接口我們肯定會看看它的實現方法 afterPropertiesSet, 不巧的是RequestMappingHandlerAdapter真的在afterPropertiesSet方法中干了不少的事.
@Override public void afterPropertiesSet() { // Do this first, it may add ResponseBody advice beans initControllerAdviceCache(); if (this.argumentResolvers == null) { List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers(); this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers); } if (this.initBinderArgumentResolvers == null) { List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers(); this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers); } if (this.returnValueHandlers == null) { List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers(); this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers); } }
看initControllerAdviceCache方法, 就會看到下面這段代碼
public static List<ControllerAdviceBean> findAnnotatedBeans(ApplicationContext context) { List<ControllerAdviceBean> adviceBeans = new ArrayList<>(); for (String name : BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, Object.class)) { if (!ScopedProxyUtils.isScopedTarget(name)) { ControllerAdvice controllerAdvice = context.findAnnotationOnBean(name, ControllerAdvice.class); if (controllerAdvice != null) { // Use the @ControllerAdvice annotation found by findAnnotationOnBean() // in order to avoid a subsequent lookup of the same annotation. adviceBeans.add(new ControllerAdviceBean(name, context, controllerAdvice)); } } } OrderComparator.sort(adviceBeans); return adviceBeans; }
好吧,前面這一坨其實都是對數據格式的統一的封裝, 而如何將java bean轉成json格式數據,這塊功能其實都是靠HttpMessageConverter來實現的.