Java多态+工厂模式实现服务调用

实验室的项目开发中,一位大佬应用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("");
}

/**
* 实现多态服务调用
*
* @param url 服务地址
* @param httpHeaders HttpHeaders
* @param requestBody 调用体
* @param request 调用请求
* @return String
*/
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();
}
}

3. 派生类MultiPartFormInvoker.java(处理multipart/form-data格式的调用)

@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();
}
}
}
}

6. 工厂配置类InvokerFactoryConfigure.java

@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;

/** HTTP方法 */
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来实现预先数据的加载。

文章作者: Moon Lou
文章链接: https://loumoon.github.io/2020/08/05/Java多态+工厂模式实现服务调用/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Moon's Blog