实验室的项目开发中,一位大佬应用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来实现预先数据的加载。