针对第二点,在头信息中指定 X-VERSION
,值为 1.0.0
的时候表示老接口,新接口用 2.0.0
,以后往上迭代。
由于是针对接口的兼容,所以我们需要知道指定的控制器方法,以及针对该接口的参数转换。
最后利用spring的HandlerMethodArgumentResolver,可以自定义控制器参数的转换。
首先我们定义一个特殊的参数注解,这个注解中包含一个参数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 { ... }
然后到我们的核心地带,实现一个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项目接口大规模重定义,本文提供了一个重构的思路:专注了写新的接口业务,针对老的部分兼容脱离于新内容之外,作为独立模块存在。