基础服务源码阅读笔记

一、基础服务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(笛哥)的智慧。

posted @ 2022-06-06 22:17  一面千人  阅读(33)  评论(0)    收藏  举报