Java项目重构之旧接口兼容的一个思路
重写所有接口,功能保持一致,完全按照新的项目模式来。 单独设置模块处理老接口的兼容,兼容老接口的请求参数和响应结果。 针对第二点,在头信息中指定 X-VERSION,值为 1.0.0 的时候表示老接口,新接口用 2.0.0,以后往上迭代。
整体思路
- 重写所有接口,功能保持一致,完全按照新的项目模式来。
- 单独设置模块处理老接口的兼容,兼容老接口的请求参数和响应结果。
针对第二点,在头信息中指定 X-VERSION
,值为 1.0.0 的时候表示老接口,新接口用 2.0.0,以后往上迭代。
请求参数转换
由于是针对接口的兼容,所以我们需要知道指定的控制器方法,以及针对该接口的参数转换。
遇到的问题
- 用拦截器方案,可以在拦截器中知道转发的控制器方法,但是无法改写inputStream。
- 用过滤器方案,可以改写请求参数,但是无法知道具体转发到的控制器方法。
最后利用spring的HandlerMethodArgumentResolver,可以自定义控制器参数的转换。
RequestParamResolver
首先我们定义一个特殊的参数注解,这个注解中包含一个参数handler,handler是我们最终处理参数转换的实体类。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
public @interface RequestParamResolver {
Class<?> handler();
}
然后在指定的控制器方法中给参数加上自定义的注解,并且指定一个对应的handler,表示我准备用这个handler去处理这个接口的参数兼容。
@PostMapping(value = "/ui")
public ResponseEntity<?> insert(HttpServletRequest request,
@RequestParamResolver(handler = AppUiBodyHandler.class) AppUiDTO appUiDTO)
throws EcarxException {
...
}
ParamsResolver
然后到我们的核心地带,实现一个HandlerMethodArgumentResolver去处理自定义的参数转换流程。
public class ParamsResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(RequestParamResolver.class);
}
@Override
@Nullable
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
RequestParamResolver resolvedRequestParam = parameter.getParameterAnnotation(RequestParamResolver.class);
Class<?> paramType = parameter.getParameterType();
String body = getRequestBody(webRequest);
if(resolvedRequestParam != null) {
RequestParamHandler handler = (RequestParamHandler) resolvedRequestParam.handler().newInstance();
JSONObject newBody = handler.doHandler(webRequest, body);
return newBody.toJavaObject(paramType);
}
return null;
}
private String getRequestBody(NativeWebRequest webRequest) {
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
String jsonBody = (String) servletRequest.getAttribute("JSON_REQUEST_BODY");
if (jsonBody == null) {
try {
jsonBody = IOUtils.toString(servletRequest.getInputStream());
servletRequest.setAttribute("JSON_REQUEST_BODY", jsonBody);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return jsonBody;
}
}
HandlerMethodArgumentResolver要实现2个方法,supportsParameter方法返回一个布尔值,表示这个控制器参数要不要被转换,resolveArgument方法就是你具体要怎么处置参数,返回一个对象,这个对象就是你接受的参数的值。
可以看到我们首先判断当前控制器参数有没有自定义注解,有,说明要做兼容处理。然后在处理参数的流程中,我们去抓取注解中的handler,执行handler的doHandle方法,最后把转换完的参数返回。
handler的简单示例:
public interface RequestParamHandler {
/**
* 处理
* @param request
* @param body
* @return
*/
JSONObject doHandler(WebRequest request, String body);
}
public class AppUiBodyHandler implements RequestParamHandler {
@Override
public JSONObject doHandler(WebRequest request, String body) {
String version = request.getHeader("X-VERSION");
JSONObject jsonBody = JSONObject.parseObject(body);
jsonBody.put("screen", "123456");
return jsonBody;
}
}
响应结果转换
响应结果转换的需求是希望在控制器返回结果后-输出返回结果前,针对结果做包装做处理。同时要允许每个接口,即控制器的方法,可以指定它们的返回结果处理方式。所以最佳的处理方式是实现 ResponseBodyAdvice
。
ResponseBodyAdvice
与之前的HandlerMethodArgumentResolver
很类似,
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter methodParameter,
Class<? extends HttpMessageConverter<?>> aClass) {
return methodParameter.hasMethodAnnotation(ReturnValueTool.class) || methodParameter.hasMethodAnnotation(GetMapping.class);
}
@Override
public Object beforeBodyWrite(Object o, MethodParameter methodParameter,
MediaType mediaType,
Class<? extends HttpMessageConverter<?>> aClass,
ServerHttpRequest serverHttpRequest,
ServerHttpResponse serverHttpResponse) {
ReturnValueTool returnValueTool = methodParameter.getMethodAnnotation(ReturnValueTool.class);
String version = serverHttpRequest.getHeaders().getFirst("X-VERSION");
if (version != null) {
return o;
}
if (returnValueTool != null) {
try {
ReturnValueHandler handler = (ReturnValueHandler) returnValueTool.value().newInstance();
return handler.doHandler(serverHttpRequest, (BaseResult) o);
} catch (Exception e) {
return o;
}
}
if(o instanceof PageResult) {
JSONObject response = PageHelper.compatible((PageResult) o);
response.put("data", ((PageResult) o).getData());
return response;
}
return o;
}
}
supports
方法决定控制器方式是否继续重写流程,beforeBodyWrite
方法就是继续重写的流程。在这个流程中需要注意的是,我们允许所有get请求的方法进入,因为这里为分页查询做了同统一的重写流程(原因是分页的结构新老接口不同)。
我们这里为需要重写返回结果的控制器方法添加一个注解 ReturnValueTool
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface ReturnValueTool {
Class<?> value();
}
同样的注解中只有一个参数,是一个实现了ReturnValueHandler接口的类,例如给我们的供应商列表接口带上一个返回结果处理的handler。
@ReturnValueTool(ManufacturerReturnHandler.class)
@GetMapping(value = "/manufacturer")
public ResponseEntity<?> list(HttpServletRequest request,
@RequestParam(required = false, value = "name") String name) throws EcarxException {
PageResult<List<ManufacturerDTO>> pageResult = manufacturerCallService.selectList(new HashMap<String, Object>(16) {{
put("store", GlobalVariable.STORE_NAME);
put("status", 1);
put("name", name);
}});
return Utils.OK(request, pageResult);
}
public interface ReturnValueHandler {
/**
* 处理
* @param request
* @param response
* @return
*/
JSONObject doHandler(ServerHttpRequest request, BaseResult response);
}
public class ManufacturerReturnHandler implements ReturnValueHandler {
@Override
public JSONObject doHandler(ServerHttpRequest request, BaseResult response) {
if(response instanceof PageResult) {
JSONObject newResponse = PageHelper.compatible((PageResult) response);
List<JSONObject> newData = new ArrayList<>();
List<ManufacturerDTO> data = (List<ManufacturerDTO>) response.getData();
data.forEach(manufacturerDTO -> {
newData.add(formatManufacturer(manufacturerDTO));
});
newResponse.put("manufacturers", newData);
return newResponse;
}
ManufacturerDTO data = (ManufacturerDTO) response.getData();
return formatManufacturer(data);
}
private JSONObject formatManufacturer(ManufacturerDTO manufacturerDTO) {
JSONObject manufacturer = new JSONObject();
manufacturer.put("count", manufacturerDTO.getProductsCount());
manufacturer.put("manufacturer_id", manufacturerDTO.getId());
manufacturer.put("name", manufacturerDTO.getName());
manufacturer.put("code", manufacturerDTO.getCode());
manufacturer.put("image", manufacturerDTO.getCover());
return manufacturer;
}
}
总结
总结来说,如果你遇到项目重构或者迁移改动量很大的情况,例如从非java项目迁移到java项目,或者对本身的java项目接口大规模重定义,本文提供了一个重构的思路:专注了写新的接口业务,针对老的部分兼容脱离于新内容之外,作为独立模块存在。