实验室的项目开发中,一位大佬应用Java多态+工厂模式实现的服务调用,这里学习一下。
一、需求背景
本项目是基于微服务开发的,用户可以在本平台上进行服务注册,注册在平台上的服务可以被其他用户订阅和调用。因此平台上存在一个微服务模块service_management是负责做服务调用的,相当于对服务的请求不是直接发送给该服务,而是先请求本平台(的service_management模块),然后该模块再通过解析请求中的信息去远程调用该服务。类似于API网关,但是API网关只是简单地做请求的转发,而本场景的service_management模块还要对请求进行解析(包括路径、参数等信息),再根据解析后的信息重新构造请求进行远程调用。
二、源代码
本节直观地贴一下代码,欣赏一下大佬的精妙设计。
1. 抽象类Invoker.java
@Data @Slf4j public abstract class Invoker {     String invokeAddress;
      String path;
      HttpMethod method;
      Descriptor descriptor;
      List<Parameter> parameters;
      public String invokeWith(String requestBody, HttpServletRequest request) {         HttpHeaders httpHeaders = createHeaders(request);         String url = createUrl(request);         log.info("Request URL is: {}", url);         return doInvoke(url, httpHeaders, requestBody, request);     }
      HttpHeaders createHeaders(HttpServletRequest request) {         HttpHeaders httpHeaders = new HttpHeaders();         httpHeaders.setContentType(MediaType.valueOf(request.getContentType()));         if (parameters != null) {             parameters.stream()                     .filter(parameter -> parameter.getIn().equals(ParamIn.HEAD))                     .forEach(parameter -> {                         String keyName = parameter.getSchema().getKeyName();                         httpHeaders.set(keyName, Optional.ofNullable(request.getHeader(keyName))                                 .orElse(getDefaultValueOfParameter(parameter)));                     });         }         return httpHeaders;     }
      String createUrl(HttpServletRequest request) {         return this.invokeAddress + createPath(request) + createParameterList(request);     }
      String createPath(HttpServletRequest request) {         String res = this.path;         if (parameters != null) {             List<Parameter> inPathParameters = parameters                     .stream()                     .filter(parameter -> parameter.getIn().equals(ParamIn.PATH))                     .collect(Collectors.toList());             for (Parameter parameter : inPathParameters) {                 String keyName = parameter.getSchema().getKeyName();                 String value = Optional.ofNullable(request.getParameter(keyName))                         .orElse(getDefaultValueOfParameter(parameter));                 res = res.replace("{" + keyName + "}", value);             }         }         return res;     }
      String createParameterList(HttpServletRequest request) {         StringBuilder sb = new StringBuilder();         sb.append("?");         if (parameters != null) {             parameters.stream()                     .filter(parameter -> parameter.getIn().equals(ParamIn.QUERY))                     .forEach(parameter -> {                         String keyName = parameter.getSchema().getKeyName();                         String value = Optional.ofNullable(request.getParameter(keyName))                                 .orElse(getDefaultValueOfParameter(parameter));                         sb.append(keyName);                         sb.append("=");                         sb.append(value);                         sb.append("&");                     });         }         String res = sb.toString();         return res.substring(0, res.length() - 1);     }
      private String getDefaultValueOfParameter(Parameter parameter) {         return Optional.ofNullable(parameter)                 .map(Parameter::getSchema)                 .map(Descriptor::getDefaultValue)                 .map(Object::toString)                 .orElse("");     }
      
 
 
 
 
 
 
 
      protected abstract String doInvoke(String url, HttpHeaders httpHeaders, String requestBody, HttpServletRequest request); }
   | 
 
2. 派生类JsonInvoker.java(处理application/json格式的调用)
@InvokerType(contentType = MediaType.APPLICATION_JSON_VALUE) public class JsonInvoker extends Invoker {     @Override     protected String doInvoke(String url, HttpHeaders httpHeaders, String requestBody, HttpServletRequest request) {         JsonNode content = JsonUtil.readTree(requestBody);         String reqBody = descriptor.generateJsonNodeFromJson(content).toString();         HttpEntity<String> httpEntity = new HttpEntity<>(reqBody, httpHeaders);         ResponseEntity<String> res = ContextUtil.ctx.getBean(RestTemplate.class)                 .exchange(url, method, httpEntity, String.class);         return res.getBody();     } }
   | 
 
@InvokerType(contentType = MediaType.MULTIPART_FORM_DATA_VALUE) public class MultiPartFormInvoker extends Invoker {     @Override     protected String doInvoke(String url, HttpHeaders httpHeaders, String requestBody, HttpServletRequest request) {         MultiValueMap<String, Object> parameterMap = createParameterMap(request);         HttpEntity<MultiValueMap<String, Object>> httpEntity = new HttpEntity<>(parameterMap, httpHeaders);         ResponseEntity<String> res = ContextUtil.ctx.getBean(RestTemplate.class)                 .exchange(url, method, httpEntity, String.class);         return res.getBody();     }
      private MultiValueMap<String, Object> createParameterMap(HttpServletRequest request) {         MultiValueMap<String, Object> parameterMap = new LinkedMultiValueMap<>();
          MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
          ObjectDescriptor objectDescriptor = (ObjectDescriptor) descriptor;         for (Descriptor child : objectDescriptor.getChildren()) {             if (child.getType().equals("file")) {                 parameterMap.add(child.getKeyName(), multipartRequest.getFile(child.getKeyName()));             } else {                 parameterMap.add(child.getKeyName(), multipartRequest.getParameter(child.getKeyName()));             }         }         return parameterMap;     }
  }
   | 
 
4. 注解类InvokerType.java
@Retention(RetentionPolicy.RUNTIME) @Documented public @interface InvokerType {     String contentType(); }
   | 
 
5. 工厂类InvokerFactory.java
public class InvokerFactory {     private static Map<String, Class<? extends Invoker>> registry = new HashMap<>();
      public static void register(String contentType, Class<? extends Invoker> clazz) {         registry.put(contentType, clazz);     }
      public static Invoker createInstance(String contentType, String invokeAddress, Api api) {         if (!registry.containsKey(contentType)) {             throw new ServiceException(ResultCode.NON_SUPPORT_CONTENT_TYPE);         }         else {             try {                 Invoker invoker = registry.get(contentType).newInstance();                 invoker.setInvokeAddress(invokeAddress);                 invoker.setPath(api.getPath());                 invoker.setMethod(api.getMethod());                 invoker.setParameters(api.getParameters());                 invoker.setDescriptor(api.getRequestBody().getContent().get(contentType));                 return invoker;             } catch (Exception e) {                 e.printStackTrace();                 throw new ServiceException();             }         }     } }
  | 
 
@Configuration public class InvokerFactoryConfigure {     @Bean     public CommandLineRunner invokeFactoryInitialization() {         return (args) -> {             Reflections reflections = new Reflections("cn.ist.msplatform.servicemanagement.model.invoke");             for (Class clazz : reflections.getTypesAnnotatedWith(InvokerType.class)) {                 InvokerType invokerType = (InvokerType) clazz.getAnnotation(InvokerType.class);                 InvokerFactory.register(invokerType.contentType(), clazz);             }         };     } }
   | 
 
7. 服务调用逻辑Api.java
@Data @EqualsAndHashCode(of = {"path", "method"}) @Builder @Slf4j public class Api {     private String id;
           private String path;
           private HttpMethod method;
           private String description;
           private RequestBody requestBody;
           private List<Parameter> parameters;
           private Map<String, Response> responses;
      public String invokeWith(Instance instance, String requestBody, HttpServletRequest request) {         String invokeAddress = instance.getInvokeAddress();         Invoker invoker = InvokerFactory.createInstance(request.getContentType(), invokeAddress, this);         return invoker.invokeWith(requestBody, request);     } }
   | 
 
三、总结
1. Java多态设计
首先根据OpenAPI 3.0规范,HTTP调用的media type有多种,比如:
>   text/plain; charset=utf-8 >   application/json >   application/vnd.github+json >   application/vnd.github.v3+json >   application/vnd.github.v3.raw+json >   application/vnd.github.v3.text+json >   application/vnd.github.v3.html+json >   application/vnd.github.v3.full+json >   application/vnd.github.v3.diff >   application/vnd.github.v3.patch >
   | 
 
最常用的种类是application/json,即requestbody是json格式,能cover掉大部分的服务调用场景。但是当需要调用文件参数时,就需要传输表单数据,对应的media type是multipart/form-data。目前需求的主要就是这两个调用模式,未来可能还会增加对新的media type的调用模式。
对于以上的需求场景,最naive的实现方法就是写一个switch-case结构,通过检查HttpServletRequest的content type(对应OpenAPI规范的media type),来选择对应的调用模式。这样实现的程序扩展型差,每当有新的media type调用需求时,就需要新增case,并为其编写相应的调用逻辑。
相比之下,应用了Java多态可以更优雅地实现以上需求场景,将所有的调用逻辑都抽象到一个抽象基类Invoker中,并在其中留了一个抽象方法doInvoke,对不同的调用模式有两个对应的派生类JsonInvoker和MultiPartFormInvoker去继承Invoker,并且按照各自的调用逻辑重写(override)基类的doInvoke方法,当然后续根据需求的扩充,可以增加对应的派生类。在Api.java中服务调用的时候,只需要面对抽象基类Invoker编程, 派生类的功能可以被基类的方法或引用变量所调用(代码29行,invoker.invokeWith()调用的实际上是某个派生类的invokeWith()方法,而非基类的),这叫向后兼容,提高程序可扩充性和可维护性。考虑面向接口编程,而非面向实现编程的Java设计原则,这里是面向抽象基类编程,而非面向派生类编程。这样就实现了一个多态服务调用。
2. 工厂模式设计
根据上面的多态设计,在服务调用的时候需要根据调用模式创建对应的派生类的实例,去执行调用过程。
最naive的做法依然是写一个switch-case结构,通过检查HttpServletRequest的content type,去创建对应的派生类实例,但这样的实现方式依然存在上述扩展型差的缺陷。
相比之下,采用工厂风格的设计模式可以非常优雅地实现这个需求场景。如Api.java的代码28行所示,实例的创建逻辑并不会直接暴露给调用者(即Api.java),而是封装到工厂中执行,让工厂去决定应该创建什么类型的派生类实例。调用者只需要关心这个创建实例的接口(工厂提供),而不需要关心实现逻辑。
但上面的设计,仅仅是把switch-case结构从调用者里移到了工厂里,也就是工厂在决定创建什么样的派生类实例的时候,依然需要通过检查HttpServletRequest的content type。下面又是一个优雅的解决方案:注册表。
见InvokerFactory.java的代码第2行,Map<String, Class<? extends Invoker>> registry是一个注册表,把所有的派生类及其对应的content type注册于其中,这样创建派生类实例的时候只需要1行代码就能搞定(第14行),相比于冗长的switch-case结构有了很大的优化。
但是又有一个新问题:这个注册表要怎么初始化呢?目前只有两个派生类,那么初始化的时候就注册这两个派生类就行,但是如果后续要增加新的派生类,初始化的时候又要新增一条map.put操作,并且这个操作是写死的,这样的代码适应性比较差,不够灵活。为了解决这个问题,下面又是一个优雅的设计:自定义注解类。
如InvokerType.java所示,这个自定义的注解类只有一个contentType字段,对应注册表registry中的key,注解在派生类上,对应registry中的value。这样就通过注解把contentType和派生类关联在了一起,初始化注册表的时候只需要使用Java提供的Reflections反射框架,扫描指定路径下所有被InvokerType注解的派生类,并将关联关系写到注册表中即可(见InvokerFactoryConfig.java)。而且后续有新增的派生类,只需要添加对应的注解即可,这样的代码设计更加灵活,适应性强。当然,扫描所有派生类并注册这一过程是工厂注册表的初始化,应该在项目启动的时候进行。SpringBoot提供了一个简单的方式CommandLineRunner来实现预先数据的加载。