对于传统的三层架构而言,Controller层负责接收请求,Service层负责处理与业务逻辑相关的请求,Dao层负责与数据库进行交互,配合Model模型对象承载业务数据,在请求上下文中传递,最终处理填充完毕数据后,交由View视图进行渲染:
但是在微服务场景下,Service层中难免会涉及到RPC远程调用请求,因此上面的流程图就变成了下面这样子:
上面是rpc远程调用的一种情况,还有一种情况如下所示:
在这种情况下,我们的Controller层代码可能如下所示:
@RestController
public class UserController {
// 响应码为成功时的值
public static final String SUCC = "000000";
// 定义访问下游查询用户服务的字段
@DubboReference
private UserQueryFacade userQueryFacade;
// 定义URL地址
@PostMapping("/queryUserInfo")
public String queryUserInfo(@RequestBody QueryUserInfoReq req){
// 将入参的req转为下游方法的入参对象,并发起远程调用
QueryUserInfoResp resp =
userQueryFacade.queryUserInfo(convertReq(req));
// 判断响应对象的响应码,不是成功的话,则组装失败响应
if(!SUCC.equals(resp.getRespCode())){
return RespUtils.fail(resp);
}
// 如果响应码为成功的话,则组装成功响应
return RespUtils.ok(resp);
}
}
在这种情况下,我们的Web服务器只是编写代码把数据做了一下包装,然后给到下游系统,等收到下游系统返回的内容后,啥也不做直接返回给前端,此时Web服务器本质是在做一些透传性质的事情。
这里我们只写了一个接口,但是如果有很多接口的Controller层逻辑都是简单的透传,那么能不能把这个逻辑抽取成通用逻辑呢?
我们先尝试使用反射将透传逻辑的公共部分抽取出来:
@RestController
public class UserController {
// 响应码为成功时的值
public static final String SUCC = "000000";
// 定义访问下游查询用户服务的字段
@DubboReference
private UserQueryFacade userQueryFacade;
// 定义URL地址
@PostMapping("/queryUserInfo")
public String queryUserInfo(@RequestBody QueryUserInfoReq req){
// 调用公共方法
return commonInvoke(userQueryFacade, "queryUserInfo", req);
}
/**
* 模拟公共的远程调用方法
* @param reqObj:下游的接口的实例对象,即通过 @DubboReference 得到的对象。
* @param mtdName:下游接口的方法名。
* @param reqParams:需要请求到下游的数据。
* @return 直接结果数据。
*/
public static String commonInvoke(Object reqObj, String mtdName, Object reqParams) throws InvocationTargetException, IllegalAccessException {
// 通过反射找到 reqObj(例:userQueryFacade) 中的 mtdName(例:queryUserInfo) 方法
Method reqMethod = ReflectionUtils.findMethod(reqObj.getClass(), mtdName);
// 并设置查找出来的方法可被访问
ReflectionUtils.makeAccessible(reqMethod);
// 通过序列化工具将 reqParams 序列化为字符串格式
String reqParamsStr = JSON.toJSONString(reqParams);
// 然后再将 reqParamsStr 反序列化为下游对象格式,并反射调用 invoke 方法
Object resp = reqMethod.invoke(reqObj, JSON.parseObject(reqParamsStr, reqMethod.getParameterTypes()[0]));
// 判断响应对象的响应码,不是成功的话,则组装失败响应
if(resp == null || !SUCC.equals(OgnlUtils.getValue(resp, "respCode"))){
return RespUtils.fail(resp);
}
// 如果响应码为成功的话,则组装成功响应
return RespUtils.ok(resp);
}
}
虽然我们通过反射抽取了远程方法调用流程的公共逻辑,使得单个controller接口内部的逻辑精简了许多,但是我们仍然需要定义很多类似于queryUserInfo这样的请求接口,那么有没有办法以一个统一的请求接口作为入口地址,统一处理所有透传式接口请求呢?
要以一个统一的请求接口作为入口地址,其实就类似于DispatchServlet统一拦截处理所有servlet请求的思路一样,然后再由DispatcherServlet按照路由规则派发给各个控制器进行请求处理:
我们这里的思路,就是编写一个统一的次级控制处理器,拦截所有请求,按照上面封装好的通用逻辑,发起RPC请求调用,然后返回远程调用结果即可。
上面的代码改造思路如下:
@RestController
public class CommonController {
// 响应码为成功时的值
public static final String SUCC = "000000";
// 定义URL地址
@PostMapping("/gateway/{className}/{mtdName}/request")
public String commonRequest(@PathVariable String className,
@PathVariable String mtdName,
@RequestBody String reqBody){
// 将入参的req转为下游方法的入参对象,并发起远程调用
return commonInvoke(className, mtdName, reqBody);
}
/**
* 模拟公共的远程调用方法
* @param className:下游的接口归属方法的全类名。
* @param mtdName:下游接口的方法名。
* @param reqParamsStr:需要请求到下游的数据。
* @return 直接返回下游的整个对象。
*/
public static String commonInvoke(String className,
String mtdName,
String reqParamsStr) throws InvocationTargetException, IllegalAccessException, ClassNotFoundException {
// 试图从类加载器中通过类名获取类信息对象
Class<?> clz = CommonController.class.getClassLoader().loadClass(className);
// 然后试图通过类信息对象想办法获取到该类对应的实例对象
Object reqObj = tryFindBean(clz.getClass());
// 通过反射找到 reqObj(例:userQueryFacade) 中的 mtdName(例:queryUserInfo) 方法
Method reqMethod = ReflectionUtils.findMethod(clz, mtdName);
// 并设置查找出来的方法可被访问
ReflectionUtils.makeAccessible(reqMethod);
// 将 reqParamsStr 反序列化为下游对象格式,并反射调用 invoke 方法
Object resp = reqMethod.invoke(reqObj, JSON.parseObject(reqParamsStr, reqMethod.getParameterTypes()[0]));
// 判断响应对象的响应码,不是成功的话,则组装失败响应
if(!SUCC.equals(OgnlUtils.getValue(resp, "respCode"))){
return RespUtils.fail(resp);
}
// 如果响应码为成功的话,则组装成功响应
return RespUtils.ok(resp);
}
}
现在的关键问题就是tryFindBean方法中,我们该通过什么样的办法拿到服务调用接口的实例对象呢?或者说,该怎么仿照@DubboReference注解,拿到服务调用的实例对象呢?
环境准备:
public interface HelloService {
String sayHello(String arg);
}
public class GenericImplOfHelloService implements GenericService,HelloService{
@Override
public Object $invoke(String method, String[] parameterTypes, Object[] args) throws GenericException {
System.out.println("进行泛化调用");
if(method.equals("sayHello")){
return "泛化调用结果: "+sayHello(args[0].toString());
}
return null;
}
@Override
public String sayHello(String arg) {
return "hello "+arg;
}
}
服务提供方使用API使用泛化调用的步骤:
ServiceConfig
时,使用setGeneric("true")
来开启泛化调用ServiceConfig
时,使用 setRef 指定实现类时,要设置一个 GenericService
的对象。而不是真正的服务实现类对象 @Test
void genericProviderTest() throws InterruptedException {
//创建ApplicationConfig
ApplicationConfig applicationConfig = new ApplicationConfig();
applicationConfig.setName("generic-impl-provider");
//创建注册中心配置
RegistryConfig registryConfig = new RegistryConfig();
registryConfig.setAddress("zookeeper://8.134.144.48:2181");
//新建服务实现类,注意要使用GenericService接收
GenericService helloService = new GenericImplOfHelloService();
//创建服务相关配置
ServiceConfig<GenericService> service = new ServiceConfig<>();
service.setApplication(applicationConfig);
service.setRegistry(registryConfig);
service.setInterface("dubbo.dubboSpi.HelloService");
service.setRef(helloService);
//重点:设置为泛化调用
//注:不再推荐使用参数为布尔值的setGeneric函数
//应该使用referenceConfig.setGeneric("true")代替
service.setGeneric("true");
service.export();
System.out.println("dubbo service started");
new CountDownLatch(1).await();
}
服务消费方使用API使用泛化调用的步骤:
ReferenceConfig
时,使用 setGeneric("true")
来开启泛化调用ReferenceConfig
后,使用 referenceConfig.get()
获取到 GenericService
类的实例$invoke
方法获取结果 @Test
void genericConsumerTest() throws InterruptedException {
//创建ApplicationConfig
ApplicationConfig applicationConfig = new ApplicationConfig();
applicationConfig.setName("generic-call-consumer");
//创建注册中心配置
RegistryConfig registryConfig = new RegistryConfig();
registryConfig.setAddress("zookeeper://8.134.144.48:2181");
//创建服务引用配置
ReferenceConfig<GenericService> referenceConfig = new ReferenceConfig<>();
//设置接口
referenceConfig.setInterface("dubbo.dubboSpi.HelloService");
applicationConfig.setRegistry(registryConfig);
referenceConfig.setApplication(applicationConfig);
//重点:设置为泛化调用
//注:不再推荐使用参数为布尔值的setGeneric函数
//应该使用referenceConfig.setGeneric("true")代替
referenceConfig.setGeneric(true);
//设置异步,不必须,根据业务而定。
referenceConfig.setAsync(true);
//设置超时时间
referenceConfig.setTimeout(7000);
//获取服务,由于是泛化调用,所以获取的一定是GenericService类型
GenericService genericService = referenceConfig.get();
//使用GenericService类对象的$invoke方法可以代替原方法使用
//第一个参数是需要调用的方法名
//第二个参数是需要调用的方法的参数类型数组,为String数组,里面存入参数的全类名。
//第三个参数是需要调用的方法的参数数组,为Object数组,里面存入需要的参数。
Object result = genericService.$invoke("sayHello", new String[]{"java.lang.String"}, new Object[]{"world"});
//使用CountDownLatch,如果使用同步调用则不需要这么做。
CountDownLatch latch = new CountDownLatch(1);
//获取结果
CompletableFuture<String> future = RpcContext.getContext().getCompletableFuture();
future.whenComplete((value, t) -> {
System.err.println("invokeSayHello(whenComplete): " + value);
latch.countDown();
});
//由于开启了异步模式,此处打印应该为null
System.err.println("invokeSayHello(return): " + result);
//打印结果
latch.await();
}
Spring 中服务暴露与服务发现有多种使用方式,如 xml,注解。这里以注解为例。 步骤:
@DubboReference
注解上指明interfaceClass和generic=true
public interface UserService {
User getUserById(String id);
}
public class UserServiceImpl implements UserService {
@Override
public User getUserById(String id) {
// Do something to get the user by id
return user;
}
}
@Service
public class UserServiceImpl implements UserService {
@DubboReference(interfaceClass = UserService.class, generic = true)
private GenericService genericService;
@Override
public User getUserById(String id) {
Object result = genericService.$invoke("getUserById", new String[]{"java.lang.String"}, new Object[]{id});
return (User) result;
}
}
Dubbo消费端发起远程调用的核心类是ReferenceConfig,接下来要做的就是拿到 referenceConfig#get 返回的泛化对象 GenericService,然后调用 GenericService#$invoke 方法进行远程调用。
@RestController
public class CommonController {
// 响应码为成功时的值
public static final String SUCC = "000000";
// 定义URL地址
@PostMapping("/gateway/{className}/{mtdName}/{parameterTypeName}/request")
public String commonRequest(@PathVariable String className,
@PathVariable String mtdName,
@PathVariable String parameterTypeName,
@RequestBody String reqBody){
// 将入参的req转为下游方法的入参对象,并发起远程调用
return commonInvoke(className, parameterTypeName, mtdName, reqBody);
}
/**
* 模拟公共的远程调用方法
* @param className:下游的接口归属方法的全类名。
* @param mtdName:下游接口的方法名。
* @param parameterTypeName:下游接口的方法入参的全类名。
* @param reqParamsStr:需要请求到下游的数据。
* @return 直接返回下游的整个对象。
*/
public static String commonInvoke(String className,
String mtdName,
String parameterTypeName,
String reqParamsStr) {
// 然后试图通过类信息对象想办法获取到该类对应的实例对象
ReferenceConfig<GenericService> referenceConfig = createReferenceConfig(className);
// 远程调用
GenericService genericService = referenceConfig.get();
Object resp = genericService.$invoke(
mtdName,
new String[]{parameterTypeName},
new Object[]{JSON.parseObject(reqParamsStr, Map.class)});
// 判断响应对象的响应码,不是成功的话,则组装失败响应
if(!SUCC.equals(OgnlUtils.getValue(resp, "respCode"))){
return RespUtils.fail(resp);
}
// 如果响应码为成功的话,则组装成功响应
return RespUtils.ok(resp);
}
private static ReferenceConfig<GenericService> createReferenceConfig(String className) {
DubboBootstrap dubboBootstrap = DubboBootstrap.getInstance();
// 设置应用服务名称
ApplicationConfig applicationConfig = new ApplicationConfig();
applicationConfig.setName(dubboBootstrap.getApplicationModel().getApplicationName());
// 设置注册中心的地址
String address = dubboBootstrap.getConfigManager().getRegistries().iterator().next().getAddress();
RegistryConfig registryConfig = new RegistryConfig(address);
ReferenceConfig<GenericService> referenceConfig = new ReferenceConfig<>();
referenceConfig.setApplication(applicationConfig);
referenceConfig.setRegistry(registryConfig);
referenceConfig.setInterface(className);
// 设置泛化调用形式
referenceConfig.setGeneric("true");
// 设置默认超时时间5秒
referenceConfig.setTimeout(5 * 1000);
return referenceConfig;
}
}
泛化调用是指在调用方没有服务方提供的 API(SDK)的情况下,对服务方进行调用,并且可以正常拿到调用结果。
泛化采用一种统一的方式来发起对任何服务方法的调用,至少我们知道是一种接口调用的方式,只是这种方式有一个比较独特的名字而已。
泛化调用有哪些应用场景呢?
泛化调用主要用于实现一个通用的远程服务 Mock 框架,可通过实现 GenericService 接口处理所有服务请求。比如如下场景:
详细可以阅读官方文档: 泛化调用(客户端泛化)
到此为止,我们已经利用了泛化调用将服务消费端改造为了一个通用的网关服务,但是服务提供方这边如何处理泛化请求呢?
如果要针对这个统一的入口进行编码实现,你会怎么写呢?
最容易想到的思路便是通过反射机制获取接口类名对应的类对象,然后利用类对象从IOC容器中拿到对应的bean,通过接口方法名和接口方法参数,来精准定位需要提供方接口服务中的哪个方法进行处理。
@RestController
public class CommonController {
// 定义统一的URL地址
@PostMapping("gateway/{className}/{mtdName}/{parameterTypeName}/request")
public String recvCommonRequest(@PathVariable String className,
@PathVariable String mtdName,
@PathVariable String parameterTypeName,
@RequestBody String reqBody) throws Exception {
// 统一的接收请求的入口
return commonInvoke(className, parameterTypeName, mtdName, reqBody);
}
/**
* 统一入口的核心逻辑
*
* @param className:接口归属方法的全类名。
* @param mtdName:接口的方法名。
* @param parameterTypeName:接口的方法入参的全类名。
* @param reqParamsStr:请求数据。
* @return 接口方法调用的返回信息。
*/
public static String commonInvoke(String className,
String mtdName,
String parameterTypeName,
String reqParamsStr) throws Exception {
// 通过反射机制可以获取接口类名对应的类对象
Class<?> clz = Class.forName(className);
// 接着通过类对象从IOC容器中定位对应的bean实例
Object cacheObj = SpringCtxUtils.getBean(clz);
// 通过反射找到方法对应的 Method 对象,并调用执行
Class<?> methodParamType = Class.forName(parameterTypeName);
Method method = clz.getDeclaredMethod(mtdName,methodParamType);
method.setAccessible(true);
return (String) method.invoke(cacheObj, JSON.parseObject(reqParamsStr,methodParamType));
}
}
通过反射调用实现起来很简单,但是问题在于反射调用比较耗时,dubbo作为一款追求高性能的调用框架,并没有采用反射来实现,性能最高的实现应该是直接调用目标对象的方法,如下所示:
// 来精准定位需要提供方接口服务中的哪个方法进行处理
if ("sayHello".equals(mtdName) && String.class.getName().equals(parameterTypeName)) {
// 真正的发起对源对象(被代理对象)的方法调用
return ((DemoFacade) cacheObj).sayHello(reqParamsStr);
} else if("say".equals(mtdName) && Void.class.getName().equals(parameterTypeName)){
// 真正的发起对源对象(被代理对象)的方法调用
return ((DemoFacade) cacheObj).say();
}
很显然,我们无法直接将这段逻辑移到commonInvoke方法内部,因为我们不可能在commonInvoke方法内部使用if…else硬编码出所有情况,这实在是不合理 !
解决办法是将上面的逻辑移动到对应的服务提供实现类中,即每个服务实现类继承Dubbo提供的GenericService接口:
public interface GenericService {
Object $invoke(String method, String[] parameterTypes, Object[] args) throws GenericException;
...
}
在接口的$invoke方法中硬编码列举所有可能的调用情况,如下所示:
public class GenericImplOfHelloService implements GenericService,HelloService{
@Override
public Object $invoke(String method, String[] parameterTypes, Object[] args) throws GenericException {
if(method.equals("sayHello")){
return "泛化调用结果: "+sayHello(args[0].toString());
}
return null;
}
@Override
public String sayHello(String arg) {
return "hello "+arg;
}
}
利用Dubbo提供的泛化接口改造上面的代码,结果如下:
@RestController
public class CommonController {
// 定义统一的URL地址
@PostMapping("gateway/{className}/{mtdName}/{parameterTypeName}/request")
public Object recvCommonRequest(@PathVariable String className,
@PathVariable String mtdName,
@PathVariable String parameterTypeName,
@RequestBody String reqBody) throws Exception {
// 统一的接收请求的入口
return commonInvoke(className, parameterTypeName, mtdName, reqBody);
}
/**
* 统一入口的核心逻辑
*
* @param className:接口归属方法的全类名。
* @param mtdName:接口的方法名。
* @param parameterTypeName:接口的方法入参的全类名。
* @param reqParamsStr:请求数据。
* @return 接口方法调用的返回信息。
*/
public static Object commonInvoke(String className,
String mtdName,
String parameterTypeName,
String reqParamsStr) throws Exception {
// 通过反射机制可以获取接口类名对应的类对象
Class<?> clz = Class.forName(className);
// 接着通过类对象的简称获取到对应的接口服务
GenericService genericService = SpringCtxUtils.getBean(clz);
// 调用泛化接口的invoke方法
return genericService.$invoke(mtdName,new String[]{parameterTypeName},new Object[]{JSON.parseObject(reqParamsStr,Class.forName(parameterTypeName))});
}
}
目前看上去一切都很完美,唯一不完美的地方在于服务提供方的每个服务实现类都需要实现GenericService接口,并重写invoke方法,并在方法内部硬编码好相关的调用逻辑。
其实我们可以利用动态代理来将上面硬编码的重复逻辑抽取出来,动态代理常用的有JDK动态代理和Cglib动态代理,这里首先排除JDK动态代理,因为JDK动态代理采用的也是反射调用。
而Cglib 的核心原理,是通过执行拦截器的回调方法(methodProxy.invokeSuper),从代理类的众多方法引用中匹配正确方法,并执行被代理类的方法。
Cglib 的这种方式,就像代理类的内部动态生成了一堆的 if…else 语句来调用被代理类的方法,避免了手工写各种 if…else 的硬编码逻辑,省去了不少硬编码的活。
但是Dubbo没有采用cglib实现动态代理,因为Cglib的核心实现是生成了各种 if…else 代码来调用被代理类的方法,但是这块生成代理的逻辑不够灵活,难以自主修改。
Dubbo在 Cglib 的思想之上采用自主实现,并且不使用反射机制, 打造一个简化版的迷你型 Cglib 代理工具,这样一来,就可以在自己的代理工具中做各种与框架密切相关的逻辑了。
既然要自己生成代理类,就得先按照一个代码模板来编码,我们来设计代码模板:
public interface HelloService {
String sayHello(String arg,String name);
String test();
}
public class HelloServiceImpl implements HelloService{
@Override
public String sayHello(String arg, String name) {
return "hello "+arg+" "+name;
}
@Override
public String test() {
return "test";
}
}
//代理类模板
public class $GenericImplOfHelloService_1 extends HelloServiceImpl implements GenericService {
public java.lang.Object $invoke(String method, String[] parameterTypes, Object[] args) throws GenericException {
if ("test".equals(method) && (parameterTypes == null || parameterTypes != null && parameterTypes.length == 0)) {
return test();
}
if ("sayHello".equals(method) && (parameterTypes != null && parameterTypes.length == 2 && parameterTypes[0].equals("java.lang.String") && parameterTypes[1].equals("java.lang.String"))) {
return sayHello((java.lang.String) args[0], (java.lang.String) args[1]);
}
throw new GenericException(new NoSuchMethodException("Method [" + method + "] not found."));
}
}
有了代码模板,我们对照着代码模板用 Java 语言编写生成出来:
package com.provider.wrapperDemo;
import org.apache.dubbo.rpc.service.GenericException;
import org.apache.dubbo.rpc.service.GenericService;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
public class CustomInvokerProxyUtils {
private static final String WRAPPER_PACKAGE_NAME = "dubbo.dubboSpi";
private static final AtomicInteger INC = new AtomicInteger();
/**
* 创建源对象(被代理对象)的代理对象
*/
public static GenericService newProxyInstance(Object sourceTarget) throws Exception {
// 代理类文件保存的磁盘路径
String filePath = getWrapperBasePath();
// 获取服务接口类
Class<?> targetClazz = sourceTarget.getClass().getInterfaces()[0];
// 生成的代理类名称: $GenericImplOfHelloService_1
String proxyClassName = "$GenericImplOf" + targetClazz.getSimpleName() + "_" + INC.incrementAndGet();
// 获取代理的字节码内容
String proxyByteCode = getProxyByteCode(proxyClassName, targetClazz, sourceTarget.getClass());
// 缓存至磁盘中
file2Disk(filePath, proxyClassName, proxyByteCode);
// 等刷盘稳定后
Thread.sleep(2000);
// 再编译java加载class至内存中,返回实例化的对象
return (GenericService) compileJava2Class(filePath, proxyClassName);
}
private static String getWrapperBasePath() {
return Objects.requireNonNull(CustomInvokerProxyUtils.class.getResource("/")).getPath()
+ CustomInvokerProxyUtils.class.getPackage().toString().substring("package ".length()).replaceAll("\\.", "/");
}
/**
* 生成代理的字节码内容,其实就是一段类代码的字符串
*/
private static String getProxyByteCode(String proxyClassName, Class<?> targetClazz, Class<?> sourceClass) {
StringBuilder sb = new StringBuilder();
// java文件第一行是package声明包路径
String pkgContent = "package " + CustomInvokerProxyUtils.WRAPPER_PACKAGE_NAME + ";";
//通过import导入代理类中可能会使用到的类
String importTargetClazzImpl = "import " + sourceClass.getName() + ";";
String importGenericService = "import " + GenericService.class.getName() + ";";
String importGenericException = "import " + GenericException.class.getName() + ";";
String importNoSuchMethodException="import "+ NoSuchMethodException.class.getName()+";";
// 代理类主体内容构建
String classHeadContent = "public class " + proxyClassName + " extends " + sourceClass.getSimpleName() + " implements " + GenericService.class.getSimpleName() + " {";
// 添加内容
sb.append(pkgContent).append(importTargetClazzImpl).append(importGenericService).append(importGenericException)
.append(importNoSuchMethodException).append(classHeadContent);
// 构建invoke方法
String invokeMethodHeadContent = "public " + Object.class.getName() + " $invoke" +
"(" + String.class.getSimpleName() + " method, "
+ String.class.getSimpleName() + "[] parameterTypes, "
+ Object.class.getSimpleName() + "[] args) throws " + GenericException.class.getSimpleName() + " {\n";
sb.append(invokeMethodHeadContent);
// 组装if...else...逻辑
for (Method method : targetClazz.getMethods()) {
String methodName = method.getName();
Class<?>[] parameterTypes = method.getParameterTypes();
String ifHead = "if (\"" + methodName + "\".equals(method)"+buildMethodParamEqual(parameterTypes)+") {\n";
//组装方法调用逻辑
String ifContent = buildMethodInvokeContent(methodName, parameterTypes);
String ifTail = "}\n";
sb.append(ifHead).append(ifContent).append(ifTail);
}
// throw new GenericException("Method [" + method + "] not found.");
String invokeMethodTailContent = "throw new " + GenericException.class.getSimpleName() + "(new NoSuchMethodException(\"Method [\" + method + \"] not found.\"));\n}\n";
sb.append(invokeMethodTailContent);
// 类的尾巴大括号
String classTailContent = " } ";
sb.append(classTailContent);
return sb.toString();
}
private static String buildMethodParamEqual(Class<?>[] parameterTypes) {
StringBuilder methodParamEqualContent=new StringBuilder();
methodParamEqualContent.append("&&(");
//方法参数为0,则可以传入null
if(parameterTypes.length==0){
methodParamEqualContent.append("parameterTypes==null ||");
}
//参数类型必须合法
methodParamEqualContent.append(" parameterTypes!=null&¶meterTypes.length==").append(parameterTypes.length);
for (int i = 0; i < parameterTypes.length; i++) {
methodParamEqualContent.append("&¶meterTypes[").append(i).append("].equals(\"").append(parameterTypes[i].getName()).append("\")");
}
methodParamEqualContent.append(")");
return methodParamEqualContent.toString();
}
private static String buildMethodInvokeContent(String methodName, Class<?>[] parameterTypes) {
if (parameterTypes.length == 0) {
return "return " + methodName + "();\n";
}
StringBuilder methodInvokeContent = new StringBuilder();
methodInvokeContent.append("return ").append(methodName).append("(");
for (int i = 0; i < parameterTypes.length; i++) {
methodInvokeContent.append("(").append(parameterTypes[i].getName()).append(")")
.append("args[").append(i).append("]").append(",");
}
//删除最后一个多余的,
methodInvokeContent.delete(methodInvokeContent.length()-1,methodInvokeContent.length());
methodInvokeContent.append(");\n");
return methodInvokeContent.toString();
}
private static void file2Disk(String filePath, String proxyClassName, String proxyByteCode) throws IOException {
File file = new File(filePath + File.separator + proxyClassName + ".java");
if (!file.exists()) {
file.createNewFile();
}
FileWriter fileWriter = new FileWriter(file);
fileWriter.write(proxyByteCode);
fileWriter.flush();
fileWriter.close();
}
private static Object compileJava2Class(String filePath, String proxyClassName) throws Exception {
// 编译 Java 文件
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
Iterable<? extends JavaFileObject> compilationUnits =
fileManager.getJavaFileObjects(new File(filePath + File.separator + proxyClassName + ".java"));
JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, null, null, compilationUnits);
task.call();
fileManager.close();
// 加载 class 文件
URL[] urls = new URL[]{new URL("file:" + filePath)};
URLClassLoader urlClassLoader = new URLClassLoader(urls);
Class<?> clazz = urlClassLoader.loadClass(CustomInvokerProxyUtils.WRAPPER_PACKAGE_NAME + "." + proxyClassName);
// 反射创建对象,并且实例化对象
Constructor<?> constructor = clazz.getConstructor();
return constructor.newInstance();
}
}
生成的代码主要有三个步骤:
注意: 如果抛出下面这个异常
Caused by: java.lang.ClassNotFoundException: com.sun.tools.javac.processing.JavacProcessingEnvironment
可能是因为缺少 tools.jar 这个依赖,可以在maven中引入相关依赖:
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8.0</version>
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>
请注意,system 作用域将使 Maven 不会从远程仓库中下载这个依赖,而是使用指定的 systemPath。你需要根据你的环境修改 systemPath 的值,确保它指向 tools.jar 的实际路径。
测试:
@Test
void wrapperDemoTest() throws Exception {
GenericService genericService = CustomInvokerProxyUtils.newProxyInstance(new HelloServiceImpl());
Object testMethodRes = genericService.$invoke("test", null, null);
System.out.println("test method invoke res: "+testMethodRes);
Object sayHelloRes = genericService.$invoke("sayHello", new String[]{String.class.getName(),String.class.getName()}, new Object[]{"参数", "大忽悠"});
System.out.println("sayHello method invoke res: "+sayHelloRes);
}
生成的java文件:
上面通过代理类实现GenericService泛化接口,是我根据Dubbo官方文档泛化调用(客户端泛化)一节,服务提供者端代码启发而作,dubbo官方实现思路类似,但是代理类并非实现GenericService接口,下面我们一起来看看。
dubbo创建的代理类继承了Wrapper接口,实现类比上面给出的GenericService:
// org.apache.dubbo.rpc.proxy.javassist.JavassistProxyFactory#getInvoker
// 创建一个 Invoker 的包装类
@Override
public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
// 这里就是生成 Wrapper 代理对象的核心一行代码
final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type);
// 包装一个 Invoker 对象
return new AbstractProxyInvoker<T>(proxy, type, url) {
@Override
protected Object doInvoke(T proxy, String methodName,
Class<?>[] parameterTypes,
Object[] arguments) throws Throwable {
// 使用 wrapper 代理对象调用自己的 invokeMethod 方法
// 以此来避免反射调用引起的性能开销
// 通过强转来实现统一方法调用
return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
}
};
}
使用Dubbo提供的Wrapper创建代理对象,并进行方法调用演示:
@Test
void dubboWrapperTest() throws InvocationTargetException {
HelloService helloService = new HelloServiceImpl();
final Wrapper wrapper = Wrapper.getWrapper(helloService.getClass());
Object testMethodRes = wrapper.invokeMethod(helloService, "test", new Class[]{},null);
System.out.println("test method invoke res: " + testMethodRes);
Object sayHelloRes = wrapper.invokeMethod(helloService,"sayHello",new Class[]{String.class, String.class}, new Object[]{"参数", "大忽悠"});
System.out.println("sayHello method invoke res: " + sayHelloRes);
}
我们把生成的 wrapper 代理类 class 文件反编译为 Java 代码,看看生成的内容到底长什么样的,这里需要借助提供的Arthas工具完成:
5. 查看对应代理类的完整代码
package dubbo.dubboSpi;
import dubbo.dubboSpi.HelloServiceImpl;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
import org.apache.dubbo.common.bytecode.ClassGenerator;
import org.apache.dubbo.common.bytecode.NoSuchMethodException;
import org.apache.dubbo.common.bytecode.NoSuchPropertyException;
import org.apache.dubbo.common.bytecode.Wrapper;
public class HelloServiceImplDubboWrap0 extends Wrapper implements ClassGenerator.DC {
public static String[] pns;
public static Map pts;
public static String[] mns;
public static String[] dmns;
public static Class[] mts0;
public static Class[] mts1;
@Override
public String[] getPropertyNames() {
return pns;
}
@Override
public boolean hasProperty(String string) {
return pts.containsKey(string);
}
public Class getPropertyType(String string) {
return (Class)pts.get(string);
}
@Override
public String[] getMethodNames() {
return mns;
}
@Override
public String[] getDeclaredMethodNames() {
return dmns;
}
@Override
public void setPropertyValue(Object object, String string, Object object2) {
try {
HelloServiceImpl helloServiceImpl = (HelloServiceImpl)object;
}
catch (Throwable throwable) {
throw new IllegalArgumentException(throwable);
}
throw new NoSuchPropertyException(new StringBuffer().append("Not found property \"").append(string).append("\" field or setter method in class dubbo.dubboSpi.HelloServiceImpl.").toString());
}
@Override
public Object getPropertyValue(Object object, String string) {
try {
HelloServiceImpl helloServiceImpl = (HelloServiceImpl)object;
}
catch (Throwable throwable) {
throw new IllegalArgumentException(throwable);
}
throw new NoSuchPropertyException(new StringBuffer().append("Not found property \"").append(string).append("\" field or getter method in class dubbo.dubboSpi.HelloServiceImpl.").toString());
}
public Object invokeMethod(Object object, String string, Class[] classArray, Object[] objectArray) throws InvocationTargetException {
HelloServiceImpl helloServiceImpl;
try {
helloServiceImpl = (HelloServiceImpl)object;
}
catch (Throwable throwable) {
throw new IllegalArgumentException(throwable);
}
try {
if ("sayHello".equals(string) && classArray.length == 2) {
return helloServiceImpl.sayHello((String)objectArray[0], (String)objectArray[1]);
}
if ("test".equals(string) && classArray.length == 0) {
return helloServiceImpl.test();
}
}
catch (Throwable throwable) {
throw new InvocationTargetException(throwable);
}
throw new NoSuchMethodException(new StringBuffer().append("Not found method \"").append(string).append("\" in class dubbo.dubboSpi.HelloServiceImpl.").toString());
}
}
上面dubbo生成的动态代理类中,我们暂时只需要重点关注invokeMethod方法逻辑即可,可以看出其实和我们上面所讲的逻辑是一致的。
wrapper类源码本文不做展开,后续章节再深入分析。
我们从服务提供方设计统一入口为题进行切入,从反射调用改造,到尝试硬编码提到性能,从而引出了自定义动态代理,虽然Cglib代理实现逻辑符合改造诉求,但是对于定制生成代理类的灵活需求,还得受Cglib库的限制。
因此,考虑上诉因素后,dubbo自定义了一个迷你型的Cglib代理工具,总体实现思路为:
本文参考Dubbo官网提供的源码解析部分,综合个人理解,如有错误,欢迎评论区指出。