Java项目重构之旧接口兼容的一个思路

重写所有接口,功能保持一致,完全按照新的项目模式来。 单独设置模块处理老接口的兼容,兼容老接口的请求参数和响应结果。 针对第二点,在头信息中指定 X-VERSION,值为 1.0.0 的时候表示老接口,新接口用 2.0.0,以后往上迭代。

整体思路

  1. 重写所有接口,功能保持一致,完全按照新的项目模式来。
  2. 单独设置模块处理老接口的兼容,兼容老接口的请求参数和响应结果。

针对第二点,在头信息中指定 X-VERSION,值为 1.0.0 的时候表示老接口,新接口用 2.0.0,以后往上迭代。

请求参数转换

由于是针对接口的兼容,所以我们需要知道指定的控制器方法,以及针对该接口的参数转换。

遇到的问题

  1. 用拦截器方案,可以在拦截器中知道转发的控制器方法,但是无法改写inputStream。
  2. 用过滤器方案,可以改写请求参数,但是无法知道具体转发到的控制器方法。

最后利用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项目接口大规模重定义,本文提供了一个重构的思路:专注了写新的接口业务,针对老的部分兼容脱离于新内容之外,作为独立模块存在。