惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Cloudbric
Cloudbric
酷 壳 – CoolShell
酷 壳 – CoolShell
人人都是产品经理
人人都是产品经理
G
Google Developers Blog
美团技术团队
MongoDB | Blog
MongoDB | Blog
博客园 - 聂微东
博客园 - Franky
罗磊的独立博客
雷峰网
雷峰网
F
Fortinet All Blogs
Stack Overflow Blog
Stack Overflow Blog
T
The Blog of Author Tim Ferriss
宝玉的分享
宝玉的分享
H
Help Net Security
Vercel News
Vercel News
Last Week in AI
Last Week in AI
Microsoft Security Blog
Microsoft Security Blog
B
Blog
Microsoft Azure Blog
Microsoft Azure Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 叶小钗
Apple Machine Learning Research
Apple Machine Learning Research
L
LangChain Blog
量子位
F
Full Disclosure
Hugging Face - Blog
Hugging Face - Blog
C
Check Point Blog
WordPress大学
WordPress大学
IT之家
IT之家
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Recorded Future
Recorded Future
D
Docker
T
Tailwind CSS Blog
博客园 - 司徒正美
博客园 - 三生石上(FineUI控件)
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Blog — PlanetScale
Blog — PlanetScale
阮一峰的网络日志
阮一峰的网络日志
GbyAI
GbyAI
Y
Y Combinator Blog
月光博客
月光博客
aimingoo的专栏
aimingoo的专栏
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Jina AI
Jina AI
Recent Announcements
Recent Announcements
V
Visual Studio Blog
The Cloudflare Blog
P
Proofpoint News Feed

Moon's Blog

Java内存 | Moon's Blog Spring Data JPA | Moon's Blog Spring Boot的Java Config | Moon's Blog Java知识点 | Moon's Blog Spring Boot知识点 | Moon's Blog Java异常机制 | Moon's Blog Java的类加载器 | Moon's Blog JVM类加载机制 | Moon's Blog 单体架构与微服务架构 | Moon's Blog 424.替换后的最长重复字符 | Moon's Blog 888.公平的糖果棒交换 | Moon's Blog 839.相似字符串组 | Moon's Blog 778.水位上升的泳池中游泳 | Moon's Blog 1631.最小体力消耗路径 | Moon's Blog 724.寻找数组的中心索引 | Moon's Blog 使用Jackson库实现日期序列化 | Moon's Blog 使用Jackson库实现Java多态解析 | Moon's Blog Spring Boot+InfluxDB实现日志管理 | Moon's Blog 为Spring Boot项目生成OpenAPI3.0文档 | Moon's Blog
Java多态+工厂模式实现服务调用 | Moon's Blog
Moon Lou · 2020-08-05 · via Moon's Blog

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

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;


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来实现预先数据的加载。