基础服务源码阅读笔记
一、基础服务RPC实现源码分析
1、基础服务的使用方法
基础服务是一个组件化项目,除了base-client是必须引入的包以外,其余的组件包按需引入。所以,要想使用基础服务,分几个步骤:
- Pom引入base-client,同时指定base-client-config.properties配置文件,在配置文件中要配置上基础服务域名(分为预发和正式),网关的信息(如果使用),代理类的全限定类名(这是来自基础服务其他组件的)。 。。。
- Pom引入要使用到的基础服务组件,例如DID组件,我们要提前在基础服务把自己用到的内容开发并发布更新,然后才能在自己的服务中调用成功。
- 代码中的使用,可以直接通过BaseClientFactory获取代理类的实例,如下:
private BaseJdVerifyService baseJdVerifyService = BaseClientFactory.get(BaseJdVerifyService.class);
总结一下,就是引入相关依赖,修改配置文件,通过BaseClientFactory获得实例,就像本地方法那样去调用即可。总体上来讲,基础服务的集成是RPC的一种实现方法。
2、入口BaseClientFactory
BaseClientFactory,客户端工厂,主要用来构造客户端服务对象。
2.1 成员变量
成员变量都是static的,因此不需要getset方法。整个BaseClientFactory的内容包括成员变量和成员函数都是静态的,这是在Class文件编译期就生成的,在程序运行期之前就准备得当。
public static final String SLASH = "/";
private static Properties properties;
private static Map<Class, Object> apis = new HashMap();
private static ProxyFactory proxyFactory;
4个成员:
- SLASH
- Properties
- Map类型的apis
- ProxyFactory
在继续程序运行的分析之前,我们先分别研究一下这几个变量类型。SLASH是斜线字符串。apis是一个Map集合,key是Class类,value是一个对象,这块后面使用时注意它的组成方式。
2.2 Properties
Properties类的全限定类名是: java.util.Properties,在Jdk类库runtime包rt.jar中。
Properties类表示一组持久的属性集合。它可以被保存为一个流或者从一个流中被加载。它的所有key和响应的value都是字符串类型。
Properties类本身也是一个容器,继承了Hashtable<Object,Object>,经常被用来读取配置文件的值到内存中。
2.3 ProxyFactory
这是基础服务一个比较重要的类,代理工厂。它有3个成员变量:
private RestTemplateClient restTemplateClient;
private static final Logger log = LoggerFactory.getLogger(ProxyFactory.class);
private Set<String> ignoreMethods = new HashSet<String>();
log是日志打印工具。另外还有一个元素是String类型名称为ignoreMethods的集合,看名字应该是用来保存被忽略的方法名。RestTemplateClient也是基础服务的另一个类,下面再展开。
构造函数:
public ProxyFactory(RestTemplateClient restTemplateClient) {
for (Method method : Object.class.getDeclaredMethods()) {
ignoreMethods.add(method.getName());
}
this.restTemplateClient = restTemplateClient;
}
所以,ProxyFactory的构造函数只需要一个RestTemplateClient对象。在对象构造期间,就把ignoreMethods初始化了,可以看到保存的都是Object类的一些方法,等于在这里都被忽略集合保存下来了。
接下来是两个重要的函数:create和createDynamicApi。
2.3.1 create 代理
首先来看create函数。
public <T> T create(final String url, Class<T> clazz, final boolean passGateway, final boolean encryption) {
return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
log.debug("调用基础服务-方法:{},URL:{}", method.getName(), url + method.getName());
if (ignoreMethods.contains(method.getName())) {
return method.invoke(this, args);
}
return restTemplateClient.postJson(url + method.getName(), args[0], method.getGenericReturnType(),passGateway,encryption);
}
});
}
这是一个泛型方法,因为方法名前使用了<T>做了声明,说明该方法中持有一个类型T,具体在哪里使用到这个T了呢?哦,是返回值,可以看到返回值类型就是T。还有方法的参数里面第二个参数传入了类clazz,这是个Class<T>类型,它是在java.lang包下:
public final class Class<T> implements java.io.Serializable,
GenericDeclaration,
Type,
AnnotatedElement {}
它的类型说明是:
Class对象建模了该class的类型。例如,String.class的类型是Class。如果该class的类型的建模者是未知,则使用Class<?>。
所以该方法的泛型部分可以用一句话来说明就是:创建函数的参数中传入了一个对象的类,那么它的返回值就是该类的对象。有点绕,在后面使用的时候再做分析。
接着看方法体,直接是一个返回值return,但是返回的内容比较多,我们逐一来分析。
1、首先是一个类型的强转(T)。
2、调用了Proxy的newProxyInstance函数。(*后面有机会要系统学习一下 反射 )
Proxy 是在java.lang.reflect包下,是java反射机制中的代理类。Proxy提供了静态方法,用来创建动态代理类和实例。同时,它也是所有由这些方法所创建出来的动态代理类的父类。Proxy的创建有一个简单的使用方法:
Foo f = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(),
new Class<?>[] { Foo.class },
handler);本例中正是使用了这个方法。
3、第一个参数获取类加载器clazz.getClassLoader(),这里的类加载器应该是AppClassLoader,第二参数传入对象数组 new Class[]{clazz},这里只有一个。
4、重点看第三个参数,是一个handler,匿名内部类new InvocationHandler(){},体中强制重写invoke方法。InvocationHandler是一个接口,也是在java.lang.reflect反射包里。该接口只有一个方法invoke,主要作用就是在代理实例中执行一个方法的调用并返回结果。invoke方法的声明如下:
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
传入代理实例,方法对象,以及方法执行的参数。这个接口的实现类简直不要太多,还是留作后续再学习。基础服务这里对invoke方法做了自己的实现,继续看源码。
1、首先是打印日志,输出方法名。
2、判断如果是ignoreMethods集合中的方法名,则直接调用该方法。如果不是,则调用restTemplateClient对象的postJson方法,去执行远程的http请求,返回响应。
问题1:ignoreMethods集合,为什么要把Object对象的方法忽略掉?
回答:rpc调用只允许远程调用业务方法 不然create方法会报错 jdk proxy不能代理原生方法。
2.3.2 createDynamicApi 创建动态API代理
这里与create唯一不同的是,restTemplateClient调用的是postForm方法。
2.3.3 RestTemplateClient
这是一个rest客户端,已开发好支持网关的安全策略。成员包括:
public static final String MEDIA_APPLICATION_JSON = "application/json;charset=utf-8";
public static final String MEDIA_X_WWW_FORM = "application/x-www-form-urlencoded";
public static final int OK = 200;
private GatewayInterceptor securityManager;
private OkHttpClient httpClient;
网关的内容不展开了,之前研究过。这里是通过成员GatewayInterceptor来支持的网关。它的网络请求是通过OkHttpClient工具。(注意不是org.springframework.web.client.RestTemplate,注意区分不要搞混。这里用的是okhttp,作者在选型时应该倾向于okhttp是更佳的。)
构造函数,传入了网关支持对象,以及okhttp的网络请求工具对象。
public RestTemplateClient(GatewayInterceptor securityManager, OkHttpClient httpClient) {
this.securityManager = securityManager;
this.httpClient = httpClient;
}
接下来主要看方法,有三个方法doPost、postJson和postForm,其中doPost是私有的,用来支撑其他两个方法。在我们上面分析ProxyFactory的时候,注意到它是用到了postJson和postForm这两个公开方法。
post传输
postJson和postForm的区别主要是请求头里面的Content-Type的值分别是:MEDIA_APPLICATION_JSON和MEDIA_X_WWW_FORM,上面成员的部分粘贴了。然后另一个不同时,form支持多个请求,所以这里增加了对于多个请求的组装代码:
FormBody.Builder builder = new FormBody.Builder();
int idx = 1;
if (reqs != null) {
for (Object req : reqs) {
builder.add(String.valueOf(idx), JSON.toJSONString(req));
idx++;
}
}
2.3.4 doPost
这是上面postJson和postForm的返回值都要调用的私有方法。
private <T> T doPost(Type respType, Request request,boolean passGateway,boolean encryption) {
try {
if (passGateway) {
request = securityManager.intercept(request, encryption);
}
Response response = httpClient.newCall(request).execute();
if (passGateway) {
response = securityManager.intercept(response);
}
if (response.code() != OK) {
String message = new StringBuilder("请求失败,http响应码为:")
.append(response.code())
.append(",原因:")
.append(response.body().string()).toString();
throw new RuntimeException(message);
}
ResponseBody body = response.body();
if (body == null) {
throw new RuntimeException("响应为空");
}
BufferedSource buffer = Okio.buffer(body.source());
byte[] bytes = buffer.readByteArray();
return JSON.parseObject(bytes, respType);
} catch (Exception e) {
throw new RuntimeException("调用基础服务接口失败", e);
}
}
这里会使用到几个判定,
- 是否使用网关passGateway,如果使用,则需要给request增加一层网关的处理,包括请求头、加密信息等,同时响应的时候,也要有相同的网关的处理。
- 是否加密encryption,这是配合网关使用的,如果不使用网关,这项就忽略了,如果使用,可以在网关配置中选择是否对数据进行加密传输。
接着是对响应内容的处理。如果响应失败,则通过报错的方式打印出拼接的错误信息。如果成功,则进一步去拆包,然后通过okio转为字节码,最后,通过JSON将字节码转型为方法的返回值类型(这里也是用到了reflect反射包中的Type类用来描述类的方法的返回值类型)。
2.3.5 ProxyFactory返回值
前面讲到了restTemplateClient的postJson和postForm的返回值都是doPost的返回值,最终是由JSON包装的实际方法的返回值。那么这个实际方法的返回值会被定义到Proxy初始化时InvocationHandler接口的invoke方法的返回值中。而当前这个组装好的Proxy代理实例将会被强制转型为T,即传入的Class<T>泛型类型。
2.4 static静态代码块
static {
loadConfig();
OkHttpClient httpClient = initHttpClient();
proxyFactory = initProxy(initRestTemplateClient(initGatewayInterceptor(httpClient), httpClient));
initApi(proxyFactory);
}
静态代码块是在程序运行期之前就要执行的。因此BaseClientFactory把一些准备工作放在了这里。
2.5 loadConfig()
private static void loadConfig() {
try {
InputStream is = BaseClientFactory.class.getClassLoader().getResourceAsStream("base-client-config.properties");
properties = new Properties();
properties.load(is);
} catch (Exception e) {
throw new RuntimeException("无法加载配置文件:base-client-config.properties");
}
}
本质上就是把classpath中的配置文件base-client-config.properties读到properties内存成员中。
2.6 initHttpClient()
private static OkHttpClient initHttpClient() {
OkHttpClient okClient = null;
try {
long connectTimeout = Long.parseLong(BaseClientFactory.properties.getProperty("base.http.connectTimeout", "5000"));
long readTimeout = Long.parseLong(BaseClientFactory.properties.getProperty("base.http.readTimeout", "30000"));
long writeTimeout = Long.parseLong(BaseClientFactory.properties.getProperty("base.http.writeTimeout", "30000"));
okClient = new OkHttpClient.Builder()
.connectTimeout(connectTimeout, TimeUnit.MILLISECONDS)
.readTimeout(readTimeout, TimeUnit.MILLISECONDS)
.writeTimeout(writeTimeout, TimeUnit.MILLISECONDS)
.build();
} catch (Exception e) {
throw new RuntimeException("base.http.connectTimeout,base.http.readTimeout,base.http.writeTimeout 参数不存在或格式错误");
}
return okClient;
}
初始化okhttp的实例。OkHttpClient的初始化非常简洁易用。通过builder设定连接超时时间、读取超时时间、写入超时时间,即可创出来一个可用的健壮的okClient实例。
2.7 proxyFactory初始化
首先先看initGatewayInterceptor(httpClient)。
2.7.1 initGatewayInterceptor()
private static GatewayInterceptor initGatewayInterceptor(OkHttpClient httpClient) {
String appId = properties.getProperty("base.gateway.appId");
BaseAssert.notNull(appId, "base.gateway.appId 未找到该变量");
String hexSecretKey = properties.getProperty("base.gateway.appSecret");
BaseAssert.notNull(hexSecretKey, "base.gateway.appSecret 未找到该变量");
String getTokenUrl = properties.getProperty("base.gateway.getTokenUrl");
BaseAssert.notNull(getTokenUrl, "base.gateway.getTokenUrl 未找到该变量");
String publicKeyB64 = properties.getProperty("base.gateway.publicKey");
BaseAssert.notNull(publicKeyB64, "base.gateway.publicKey 未找到该变量");
return new GatewayInterceptor(appId, publicKeyB64, hexSecretKey, getTokenUrl, httpClient);
}
初始化网关所需要的数据,然后创建网关连接工具,包括安全信道策略。
2.7.2 initRestTemplateClient()
private static RestTemplateClient initRestTemplateClient(GatewayInterceptor gatewayInterceptor, OkHttpClient httpClient) {
return new RestTemplateClient(gatewayInterceptor, httpClient);
}
初始化RestTemplateClient,见1.3.3。
2.7.3 initProxy()
private static ProxyFactory initProxy(RestTemplateClient restTemplateClient) {
return new ProxyFactory(restTemplateClient);
}
传入网络请求实例restTemplateClient,初始化ProxyFactory,见1.3。该方法执行完毕,我们在BaseClientFactory类中就获得了proxyFactory实例。
2.8 initApi()
传入ProxyFactory实例,该方法针对外部代理请求的方法进行处理,封装了增加网关的网络请求内容。这里应该是整个基础服务最为重要的一个方法。
2.8.1 基本准备
从配置文件中获取passGateway,即是否使用网关。以及encryption,即是否加密。还有请求域名的地址。
boolean passGateway = Boolean.parseBoolean(properties.getProperty("base.gateway.enable", "true"));
boolean encryption = Boolean.parseBoolean(properties.getProperty("base.gateway.encryption", "true"));
String hostPrefix = properties.getProperty("base.host");
2.8.2 遍历properties的属性
Enumeration<Object> keys = properties.keys();
while (keys.hasMoreElements()) {
Object element = keys.nextElement();
if (element instanceof String) {
String key = (String) element;
if (key.startsWith("base.api")) {
...
}
if (key.startsWith("base.dynamic.api")) {
...
}
}
}
将properties中的配置值转换成枚举类型,然后通过枚举类型的方法hasMoreElements去逐个读取并判断。
2.8.3 不同的处理
前面谈到了create和createDynamicApis两种方法分别对应这postJson和postForm的网络请求。那么在这里通过对于配置文件的配置key的判断,来进行区分到底使用哪种代理的create方法。
boolean gateWayEnable = passGateway;
boolean gatewayEncryption = encryption;
Class<?> aClass = Class.forName(key.replace("base.api.", ""));
1、首先把大作用域的判定条件转给小作用域,到if判定里面。
2、然后读取配置文件的全限定类名的字符串,通过Class.forName的反射能力,去载入该类,后面会作为创建代理的参数,这个前面提到过。
2.8.4 单独配置某类
String gateWayEnableStr = properties.getProperty(key.replace("base.api.", "") + ".gateway.enable");
if (gateWayEnableStr!=null){
gateWayEnable = Boolean.parseBoolean(gateWayEnableStr);
}
String gatewayEncryptionStr = properties.getProperty(key.replace("base.api.", "") + ".gateway.encryption");
if (gatewayEncryptionStr!=null){
gatewayEncryption = Boolean.parseBoolean(gatewayEncryptionStr);
}
这里可以单独给某个类配置是否使用网关以及是否加密,但是这个功能应该是没有使用起来。
2.8.5 apis载入内存
代理创建好了以后,前面ProxyFactory讲到了,会返回这个类的实例,该实例作为apis的value值,配置文件读出的那个Class作为key。
那么,到这就完成了整个BaseClientFactory的工作,内存中也准备好了apis,里面包括了所有从配置文件中定义的,需要远程调用的类以及他们的方法调用,其中也封装好了对于网关的处理。
3、一些问答
我们前面已经准备好了对于类和方法的网络请求的代理。那么在运行期的业务代码中,只有定义从BaseClientFactory中的apis中获取到对应的类的实例。即可完成远程的服务调用。
这里有一个思考。
问题2:基础服务是什么定位?它是服务端还是客户端?
回答:首先基础服务是一个代理服务,它既不是服务的真实的服务端,也不是业务方真正的客户端,它只是一个桥梁,用来方便业务方和真实服务端的通信。
问题3:基础服务是如何让业务方爽的?
回答:业务方首先需要配置好自己的base-client-config.properties文件,以及引入包含BaseClientFactory以及相关类的jar包。使用时需要从BaseClientFactory中获取到自己要请求的远程类的实例,然后就像本地调用方法那样完成对真实服务端的调用。
问题4:系统间的调用要么是通过jar包集成的方式,要么是通过网络请求的方式,那么基础服务属于哪一种方式呢?
回答:基础服务属于jar包集成的方式,看似是本地jar的集成,实际上jar包内部的BaseClientFactory包括了对于网络请求的一系列封装。因此你不能说基础服务是通过网络请求的方式,至少跟我们传统的controller暴露的网络接口不一样。我总感觉这就是一种rpc的调用方式。
二、JSF转Http服务的实现源码分析
前面谈到了基础服务除了base-client完成了RPC的调用方式,其他的一些组件大多是业务方自己开发的需要被代理的服务。但是也有一些组件包含了特有技术能力的实现,例如service2http服务。
1、BaseMain
BaseMain是基础服务的启动主类。它包含很多的注解(基础服务工程深入使用了Spring注解功能),以及监控和全局异常捕获。
@EnableHrcSupport
@EnableDid
@EnableFace
@EnableAccessLog
@EnablePdfSign
@EnableService2Http
@EnableValidate
@EnableDiscern
@EnableMail
@EnableSms
@EnableJdAccount
// 有数据库依赖,可以注解掉,同时注释掉main pom中的数据库相关依赖
@EnableAsyncTask
@EnablePayment
@Import(GlobalConfiguration.class)
@EnableAspectJAutoProxy(proxyTargetClass = true)
@SpringBootConfiguration
@EnableAutoConfiguration()
@ImportResource({"classpath:cache-config-${spring.profiles.active}.xml", "classpath:jsf-config.xml","classpath:jmq-consumer.xml"})
@PropertySource("classpath:important.properties")
public class BaseMain {
public static final String BASE_SERVICE_JVM = "base-service-jvm";
public static void main(String[] args) {
Profiler.registerJVMInfo(BASE_SERVICE_JVM);
SpringApplication.run(BaseMain.class, args);
}
@Bean
public GlobalExceptionResolver exceptionResolver() {
return new GlobalExceptionResolver();
}
}
1.1 全局异常捕获
通过在主启动类注册bean:GlobalExceptionResolver到Spring运行时容器中来实现。下面来看一下GlobalExceptionResolver类。
@ControllerAdvice
public class GlobalExceptionResolver {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionResolver.class);
@ExceptionHandler(Exception.class)
@ResponseBody
public BpResp handleException(Exception e) {
log.error("全局异常捕获", e);
if (e instanceof MethodArgumentNotValidException) {
MethodArgumentNotValidException be = (MethodArgumentNotValidException) e;
BpResp baseResp = new BpResp();
baseResp.setResponseEnum(BpRespCode.PARAMS_ERROR);
return baseResp;
} else if (e instanceof RespException){
RespException be = (RespException) e;
return be.toBpResp();
}else {
BpResp baseResp = new BpResp();
baseResp.setResponseEnum(BpRespCode.INTER_FAIL);
baseResp.setMsg(e.getMessage());
return baseResp;
}
}
}
这里通过注解@ControllerAdvice和@ExceptionHandler配合实现了全局异常捕获。代码中规定了3中异常:
1、MethodArgumentNotValidException,这是Spring-web在方法参数有问题时抛出的异常。这里对该异常进行捕获并统一处理返回体。
2、RespException,这是自己实现的异常类,继承了RuntimeException类。其中做了内外部响应code的转换操作。本身也是一种响应体结构,在自己工程组件中,可以对错误码和错误信息进行抛出异常,然后在这里统一处理。
3、其他的异常情况全部捕获并按照“内部系统错误”统一处理。
这几种捕捉的异常都会通过ResponseBody注释统一返回响应。
1.1.1 @ControllerAdvice注解
ControllerAdvice注解可以全局扩展Controller的功能,是Controller的增强器。通过ControllerAdvice注解的类,内部可以使用@ExceptionHandler、@InitBinder、@ModelAttribute这三个注解来实现具体的功能,这三个注解都会做作用到被RequestMapping注解的方法上。
- 基础服务BaseMain这里是用到了@ExceptionHandler注解,用来全局捕获异常:
@ExceptionHandler(Exception.class)注解到方法上。 - @ModelAttribute注解的方法,参数是Model类型对象,可以把值绑定到Model中,是全局@RequestMapping注解的方法都可以获取到该值。实现了统一增加全局变量的功能。非本文重点
- InitBiner用来做数据绑定,例如类型转换、映射对象。它有很多的方法,可以作用到RequestMapping的全局中去,可以慢慢研究。非本文重点
1.2 监控
BaseMain中给自己的JVM起了一个字符串名字,然后使用Profiler注册到JVMinfo。Profiler是一个JVM层级的服务监控组件,我们每一个业务代码都需要按照要求引用。Profiler组件值得研究一下源码。后面有机会再说。
1.3 类注解(其他组件以@EnableDid为例)
基础服务的注解使用机制。BaseMain类上方增加注解@EnableDid。进入该注解查看源码:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import({DidAutoConfiguration.class})
public @interface EnableDid {
}
1.3.1 @Target
@Target,设定该注解可以声明在哪些目标元素之前。后面括号内的值是ElementType.TYPE,这段说明就是该注解只能声明在一个类前。所以我们把EnableDid注解声明在类BaseMain上面。
1.3.2 @Retention
@Retention,告诉编译程序如何处理类的生命周期。后面括号内的值是RetentionPolicy.RUNTIME,这段说明是该注解不仅被保存在class文件中,jvm加载class文件之后,仍然存在。
1.3.3 DidAutoConfiguration
@Import,只能用在类上,把实例快速导入Spring的IOC容器中。后面括号内的值是{DidAutoConfiguration.class},它是一个数组,可以引用多个,但这里只有一个元素。我们跳转进去查看一下源码。这里只研究一下它的类声明的头的部分。
@Configuration
@EnableConfigurationProperties({ Jd2cConfig.class, Jd2bConfig.class, JderpConfig.class, JdAuthConfig.class, })
@ComponentScan("com.jd.bt.base.did.controller")
public class DidAutoConfiguration {
- @Configuration,该注解用于定义配置类,可替换xml配置文件,被注解的类内部包含一个或多个@Bean注解的方法,这些方法会被AnnotationConfigApplicationContext或AnnotationConfigWebApplicationContext类进行扫描,并用于构建bean,初始化到Spring容器。
- @EnableConfigurationProperties,用来读取配置文件的业务参数值,并映射到一个对象初始化到Spring容器,值是一个数组,可以读取多个。可指定一个prefix前缀,用于区分其他业务项。
- @ComponentScan,扫描指定注解的类注册到IOC容器中,用于类或接口上主要是指定扫描路径,Spring会把指定路径下带有指定注解的类注册到IOC容器中。这里设定把咱们自己业务DID的入口类Controller扫描进去Spring容器。
下面说一下类内部的实现,因为与业务耦合较深,就不粘贴代码了,这里只谈论技术方面的内容。
- @ConditionalOnClass,(类的前提条件)只有它的值中设定的类在classpath中,当前类才会被构建。那么这里的值是JSFRegistry.class,就是说明没有JSF的注册点类,咱们这个方法没有用,因为那是前提条件。
具体的业务实现,其实也是一种RPC的业务使用方法,我是RPC的客户端,引入相关依赖,即可本地调用方法使用。那么基础服务这里就是要将这种业务RPC转为http服务。
所以,基础服务是先把局域网RPC转为http服务,然后再将该http服务转为外网RPC服务,供外网客户端可以直接触达局域网的RPC。本质上基础服务就是干了这么一件事儿。
2、@EnableService2Http
该注解背后的组件完成了RPC转Http的过程。
2.1 Service2HttpAutoConfiguration
先贴源码(这种与业务完全没干系的最纯粹,可以去研究技术的实现。)
@Configuration
public class Service2HttpAutoConfiguration {
private static final Logger LOG = LoggerFactory.getLogger(Service2HttpAutoConfiguration.class);
private Map<String,ServiceMetaInfo> config;
@Autowired
private ServiceMgr serviceMgr;
@Autowired
private ControllerMgr controllerMgr;
@PostConstruct
public void initContext() throws IOException {
config = new Yaml().loadAs(new ClassPathResource("service2http.yaml").getInputStream(), Map.class);
serviceMgr.load(config);
controllerMgr.load(config);
}
@Bean
public ServiceMgr jsfConsumerMgr(RegistryConfig registryConfig) {
return new ServiceMgr(registryConfig);
}
@Bean
public ControllerMgr controllerMgr(RequestMappingHandlerMapping handlerMapping, ServiceMgr consumerMgr) {
return new ControllerMgr(handlerMapping, consumerMgr);
}
}
- ServiceMetaInfo,这是一个结构体,包含了一个JSF接口的所有字段。
- config,这是一个可装载多个JSF配置连接的Map,在初始化时赋值。
- ServiceMgr,JSF的本地服务管理器,初始化时根据JSF配置文件赋值。
- ControllerMgr,这是Http服务的Controller管理器。
- initContext,初始化上下文,被@PostConstruct注解声明,被要求在类的构造方法之后执行。从JSF配置文件中读取一个或多个JSF接口连接,然后加载到JSF本地管理器以及Controller管理器。而这两个管理器会将JSF接口服务转换为Http服务。
- 加载一个Bean方法,可以通过JSF的注册对象获得管理器实例。
- 加载一个Bean方法,可以通过RequestMappingHandlerMapping和ServiceMgr对象,获得一个新的Controller管理器实例。
2.2 ServiceMgr
JSF服务,本地服务管理器。包括3个容器:
- serviceTable,用来存储服务对象,key是类,value是对象。
- typeTable,储存类对象,key是接口名,value是类。
- registryConfig,JSF配置对象,在构造方法中传入。
load方法
传入的是JSF配置文件内容,遍历JSF配置对象,通过类型转换获得JSF消费者服务对象(深入JSF实现源码中学习)。然后分别保存到serviceTable和typeTable两个Map中去。
2.3 ControllerMgr
通过构造方法初始化:
- RequestMappingHandlerMapping类型,它的作用有两个:
- 通过request查找对应的HandlerMethod,即当前request具体是由Controller中的哪个⽅法进⾏处理
- 查找当前系统中的Interceptor,将其与HandlerMethod封装为⼀个HandlerExecutionChain。
- ServiceMgr,JSF消费者服务管理器。
load方法
通过JSF接口名称获得JSF服务类型类,通过JSF服务类型类获得JSF消费者服务对象。
然后利用反射得到JSF服务类型类的所有方法并进行遍历。通过比对JSF配置的方法集合和读取的类的反射出来的方法集合,如果相同,则开始发布接口:即从JSF RPC的接口发布为Http接口。
2.3.1 发布http接口
①获得Controller类
- 获得方法参数个数。
- findControllerType方法,查找匹配的controller类型,通过上面的方法参数个数适配模板,得到一个包含对应JSF接口的类:
private Class findControllerType(int parameterCount) {
Class controllerTemplate = null;
try {
controllerTemplate = Class.forName("com.jd.bt.base.service2http.controller.template.A" + parameterCount);
} catch (Exception e) {
throw new RuntimeException("不支持该数量参数的JSF接口", e);
}
return controllerTemplate;
}
这里第4行,是一个字符串拼接,通过参数个数拼接一个类的全限定类名,然后通过Class.forName获得类的实体。模板的内容,在包com.jd.bt.base.service2http.controller.template下,包括:A0到A10共11个接口,每个接口的方法参数从0到10,分别对应这接口名字中的数字。
② 生成Controller实例
通过上面获得的Controller类,以及JSF消费者服务对象,通过反射和配置比对的接口方法,生成Controller实例。
private Object genControllerObject(Class controllerType, Object service, Method serviceMethod) {
try {
return Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{controllerType}, new MethodInvocationHandler(service, serviceMethod));
} catch (Exception e) {
throw new RuntimeException("生成代理对象失败:" + controllerType.getSimpleName(), e);
}
}
主要是通过动态代理去获得对象。
思考:Class反射直接newInstance()和动态代理的newProxyInstance的区别?
回答:newProxyInstance可以修改对象创建的过程,以改造对象生成以后的状态。而newInstance不可以。这部分可以看2.4 MethodInvocationHandler。换句话说,newProxyInstance可以实现自定义对象创建逻辑。
③ 获得Controller方法对象
这个直接从类的反射中去拿,实际上就是模板对应方法参数个数生成的那个方法。
④ 生成Controller映射对象
注意,不是生成Controller代码,而是直接生成Spring所需要的Controller的映射对象,它与直接编写出来的Controller在启动时映射到Spring容器中的对象是一致的。
/**
* 生成controller映射对象
*/
String publishPath = prefixPath + "/" + serviceMethod.getName();
RequestMappingInfo mappingInfo = RequestMappingInfo
.paths(publishPath)
.methods(RequestMethod.POST)
.consumes(MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.produces(MediaType.APPLICATION_JSON_VALUE)
.build();
handlerMapping.registerMapping(mappingInfo, controller, controllerMethod);
这就是通过RequestMappingHandlerMapping这个类来完成的。
2.4 MethodInvocationHandler
Method执行处理器,用来做动态代理,生成对象时的方法部分的处理。这部分很重要,是构造controller类对象的重要的部分,也是基础服务在service4http组件中自己实现的。
- 构造方法就传入了JSF服务对象和服务对象的当前方法。
2.4.1 invoke方法
/**
* controller方法的入参都定义为String JSON,通过service方法的类型,转换JSON到对象
* 在调用service的方法,执行的结果转换为String JSON 返回到controller
*
* @param proxy
* @param method
* @param args
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (!"m".equals(method.getName())) {
return method.invoke(this, args);
}
Object[] serviceArgs = convertToExplicitArgs(args);
Object invokeResult = serviceMethod.invoke(serviceObj, serviceArgs);
LOG.info("动态调用出参:" + invokeResult);
return convertToJsonResult(invokeResult);
}
MethodInvocationHandler实现了InvocationHandler接口,重写方法的invoke内容。
- 首先是方法名的校验,只处理咱们自己声明的m方法。其他方法的invoke逻辑并不走重写的逻辑。
- 方法入参全部转为json。
- 方法出参也转为json。
三、Http与RPC的处理(衔接一和二)
按照本文的研究顺序,真实的基础服务网络协议的转换实际上是先二后一。也就是说先把局域网中的RPC,即JSF服务转换为Http服务,这就是本文第二大章节研究的内容。然后再把刚刚发布的http服务(基于Controller实现),转换为公网的带网关安全策略的RPC服务,这是本文第一大章节研究的内容。
不过在实际运用中,基础服务的使用却产生了两种方法:
第一、有些用户直接在基础服务中通过组件的方式实现了通过JSF RPC调用的业务代码,然后直接转为带网关的RPC服务,跑到他的业务工程中使用。
第二、通过启动基础服务时在配置文件service2htp.yaml中定义的JSF服务配置进行转换的。可以让终端客户不需要自行编写基础服务的业务组件,但是必须在基础服务端的配置文件中配置上自己要用的杰夫配置。
以上两种方式,无疑第二种是更加解耦合的,不需要代码侵入,基础服务可以维持一个纯技术工具的定位。而配置文件,我们知道线上发布是可以手动覆盖的,所以在配置文件中配置新的JSF配置对象,并不是一个复杂的事儿,至少相对于在基础服务中去编写一个业务模块要简单的多。
四、后记
本文还是有很多不足,例如第一和第二种方式应该是完全独立的两种方式,都是为了解决内网JSF公网调用的问题。只不过一种是代码侵入性高,但客户端使用RPC,比较方便。而另一种是直接通过配置文件将JSF转为Http,直接转为公网API了。
最早的基础服务定位:
现在宙斯网关已经能干这事了 当时咱们一是因为混合云部署 需要跨公网调用 二是成本核算 将一个总账号给n个外部渠道使用 需要记录流水
宙斯网关:内网接口,发布公网API。取代基础服务的内容了。
总之,从技术角度,感谢基础服务的作者mudi(笛哥)的智慧。

浙公网安备 33010602011771号