8.SpringBoot AOP方式统一数据格式返回

1,196 阅读2分钟

image.png

使用 Java Bean 来体现统一返回数据这个结构,可以解决接口返回格式一致性,但也有几个新问题诞生了:

  • 接口返回值不明显,不能一眼看出来该接口的返回值
  • 每一个接口都需要增加额外的代码量
  • 不利于swagger对接口的返回类型的支持

所幸Spring Boot已经为我们提供了更好的解决办法,只需要在项目中加上以下代码,就可以无感知的为我们统一全局返回值。


package com.crownboot.web.global.response;

import java.lang.reflect.Type;
import java.net.URI;
import java.util.Objects;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpStatus;
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.stereotype.Controller;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import com.crownboot.web.properties.CrownBoot;

@ControllerAdvice(annotations = { RestController.class, Controller.class })
public class GlobalResponseHandleAdvice implements RequestBodyAdvice, ResponseBodyAdvice<Object> {

	@Autowired
	private CrownBoot crownBoot;

	@Override
	public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
		Controller controller = returnType.getDeclaringClass().getAnnotation(Controller.class);
		RestController restController = returnType.getDeclaringClass().getAnnotation(RestController.class);
		ResponseBody responseBody = returnType.getMethodAnnotation(ResponseBody.class);
		boolean wrapper = true;
		UnifiedReturn unifiedReturn = returnType.getMethodAnnotation(UnifiedReturn.class);
		if (Objects.nonNull(unifiedReturn)) {
			wrapper = unifiedReturn.wrapper();
		}
		return (Objects.nonNull(restController) || (Objects.nonNull(controller) && Objects.nonNull(responseBody)))
				&& wrapper;
	}

	@Override
	public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
			Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,
			ServerHttpResponse response) {
		UnifiedReturn unifiedReturn = returnType.getExecutable().getDeclaringClass().getAnnotation(UnifiedReturn.class);
		if (Objects.isNull(unifiedReturn)) {
			unifiedReturn = returnType.getMethodAnnotation(UnifiedReturn.class);
		}
		HttpStatus status = HttpStatus.OK;
		if (Objects.nonNull(unifiedReturn)) {
			status = unifiedReturn.status();
		}
		// ResponseStatus
		ResponseStatus responseStatus = returnType.getMethodAnnotation(ResponseStatus.class);
		if (Objects.nonNull(responseStatus)) {
			status = responseStatus.code();
		}
		if (Objects.nonNull(unifiedReturn) && !unifiedReturn.wrapper()) {
			return body;
		}
		// swagger
		URI uri = request.getURI();
		if (Objects.nonNull(uri) && StringUtils.containsAny(uri.getPath(), "swagger-resources", "api-docs")) {
			return body;
		}
		return body instanceof ApiResponse ? body : ApiResponse.<Object>success(response, status.value(), body).build();
	}

	@Override
	public boolean supports(MethodParameter methodParameter, Type targetType,
			Class<? extends HttpMessageConverter<?>> converterType) {
		return true;
	}

	@Override
	public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
			Class<? extends HttpMessageConverter<?>> converterType) {
		return inputMessage;
	}

	@Override
	public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
			Class<? extends HttpMessageConverter<?>> converterType) {
		// RequestUtils.getRequest().setAttribute(APICons.API_REQUEST_BODY, body);
		return body;
	}

	@Override
	public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
			Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
		return body;
	}

}

有时针对特殊应用场景,无需对返回数据进行包装,可以自定义一个注解进行标识

package com.crownboot.web.global.response;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.http.HttpStatus;

/**
 * 包装API返回注解
 *
 * @author luopeng
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface UnifiedReturn {

    /**
     * 是否包装返回
     */
    boolean wrapper() default false;

    /**
     * 正常返回httpcode码
     */
    HttpStatus status() default HttpStatus.OK;
}

此时请求 hello接口,会报ApiResponses不能转为string的异常

{
  "timestamp": "2019-12-09T12:22:53.776+0000",
  "status": 500,
  "error": "Internal Server Error",
  "message": "com.gitee.web.api.ApiResponses cannot be cast to java.lang.String",
  "path": "/users"
}
java.lang.ClassCastException: com.gitee.web.api.ApiResponses cannot be cast to java.lang.String
	at org.springframework.http.converter.StringHttpMessageConverter.getContentLength(StringHttpMessageConverter.java:44) ~[spring-web-5.2.1.RELEASE.jar:5.2.1.RELEASE]
	at org.springframework.http.converter.AbstractHttpMessageConverter.addDefaultHeaders(AbstractHttpMessageConverter.java:260) ~[spring-web-5.2.1.RELEASE.jar:5.2.1.RELEASE]
	at org.springframework.http.converter.AbstractHttpMessageConverter.write(AbstractHttpMessageConverter.java:211) 

解决

package com.gitee.config;

import java.util.List;

import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class MyConfig implements WebMvcConfigurer{

	@Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.set(0, new MappingJackson2HttpMessageConverter());
    }
}

重写WebMvcConfigurerAdapter,覆盖了原有的HttpMessageConverters,此处采用:Jackson

epom-web工程pom添加jackson依赖

<dependency>
	<groupId>com.fasterxml.jackson.core</groupId>
	<artifactId>jackson-databind</artifactId>
</dependency>

<dependency>
	<groupId>com.fasterxml.jackson.datatype</groupId>
	<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>

效果