上一篇讲到完Netty服务端和客户端之间的一个通讯,这次继续接住上篇写的代码继续拓展。本期主要是把 Netty 远程调用 + 动态代理给弄出来。
从零开始写 RPC 框架 - 01
我们的RPC说到底说到底就是远程调用方法,远程我们实现了,现在就是调用,调用的话我们用的是动态代理。
在上一期创建传输的报文中就已经定义好了需要调用的接口和方法名,这里就先把他们晾一边,先把动态代理基础弄好。
代理模式
代理模式主要就分开两个,一个静态代理 另一个动态代proxy。
静态的就先不讲了,比较固定,基本上是写死那种,没太大意义。
动态代理
Java 动态代理的话其中两种,一个是JDKProxy,另外一个是CGLIB。
JDK 动态代理有一个最致命的问题是其只能代理实现了接口的类。
CGLIB
由于各种主客观因素,所以我们这里选择使用CBLIB。
首先我们加坐标。
<!-- cglib --><dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.3.0</version></dependency>然后创建一个自定义MethodInterceptor。
public interface MethodInterceptor extends Callback {
/** * 拦截被代理类中的方法 * @param obj 用于调用原始方法 * @param method 方法名 * @param args 参数 * @param proxy 用于调用原始方法 * @return 调用返回 * @throws Throwable e */ Object intercept(Object obj, Method method, Object[] args,MethodProxy proxy) throws Throwable;
}
Method是java.lang.reflect.Method的
然后我们写一个准备去被调用的类,这个比较简单,随意输出点东西就好了。
public interface ICallService { /** * 测试代理 * @param arg1 参数 * @return 返回值 */ String callTest(String arg1);}----------------------------------------------public class CallServiceImpl implements ICallService { @Override public String callTest(String arg1) { log.info("callTest->{}",arg1); return arg1; }}下一步,我们去自定义一个方法拦截器。
@Slf4jpublic class MethodInterceptorImpl implements MethodInterceptor { @Override @SneakyThrows public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) { log.info("执行方法前-->{}",method.getName()); Object o = proxy.invokeSuper(obj, args); log.info("执行方法后-->{}",method.getName()); return o; }}然后定义一个CGLIB工厂类,用于获取代理类。
public class CglibProxyFactory {
public static Object getProxy(Class<?> clazz) { // 创建动态代理增强类 Enhancer enhancer = new Enhancer(); // 设置类加载器 enhancer.setClassLoader(clazz.getClassLoader()); // 设置被代理类 enhancer.setSuperclass(clazz); // 设置方法拦截器 enhancer.setCallback(new MethodInterceptorImpl()); // 创建代理类 return enhancer.create(); }}然后我们就可以简单测试一下代理效果。
@Test public void testCGLIB(){ ICallService callService= (ICallService) CglibProxyFactory.getProxy(CallServiceImpl.class); System.out.println(callService.callTest("sb")); }执行方法前-->callTestcallTest->sb执行方法后-->callTestsb然后我们的CGLIB简单实现就可以了。下一步我们把Netty联动起来。
联动Netty
这里的话我们就去实现一个小功能,客户端发送接口名和方法名,然后服务端返回执行的效果。
发送我们展示就不需要改造了,但是之前NettyServerHandler里面返回值是写死的,现在要改成动态的。
首先我们创建一个处理类。
创建处理类RpcRequestHandler
这个处理类主要就是用来代理,通过方法名、类名等信息获取对应的Method及代理类。
public class RpcRequestHandler {
/** * 用作处理 */ @SneakyThrows public Object handler(RpcRequest request) { return invokeTargetMethod(request,CglibProxyFactory.getProxy(Class.forName(request.getInterfaceName()))); }
/** * 用作方法调用 */ @SneakyThrows private Object invokeTargetMethod(RpcRequest request, Object service) { Method method=service.getClass().getMethod(request.getMethodName(),request.getParameterTypes()); return method.invoke(service,request.getArgs()); }
}修改NettyServerHandler返回值
这个比较简单,就是把之前写死的改成RpcRequestHandler调用就好了。
@Slf4jpublic class NettyServerHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { super.channelRead(ctx, msg); try { RpcRequest rpcRequest = (RpcRequest) msg; log.info("服务端返回信息: [{}] ", rpcRequest); RpcResponse messageFromServer = RpcResponse.builder().message(String.valueOf(new RpcRequestHandler().handler(rpcRequest))).build(); ChannelFuture f = ctx.writeAndFlush(messageFromServer); f.addListener(ChannelFutureListener.CLOSE); } finally { ReferenceCountUtil.release(msg); } }测试
改造完我们就可以测试了。把上期写的测试类改一下。
@Test @SneakyThrows public void testSend(){ String methodName="callTest"; Method method = ICallService.class.getMethod(methodName, String.class); RpcRequest build = RpcRequest.builder() .interfaceName(ICallService.class.getName()) .methodName(method.getName()) .args(new String[]{"sb"}) .parameterTypes(method.getParameterTypes()) .build(); System.out.println(new NettyClient("localhost", 6666).send(build)); }把服务端和客户端按顺序跑,就会看到一下结果。
客户端并没有任何东西返回,但是服务端缺报错了。
java.lang.reflect.InvocationTargetExceptionCaused by: java.lang.NoSuchMethodError: java.lang.Object.callTest(Ljava/lang/String;)Ljava/lang/String;简单来说就是找不到方法。
Q
class里面拿类名,从class里面拿Method的名怎么还会报错呢?A
?
好,我们第一次测试就这样炸了。接口类是不能直接反射调用方法的,只能去获取它的实现类。
方法的话我想到的有两种。
- 是通过各种反射拿到它的实现类
- 通过
Spring拿到实现类
第一种的话我在下垃圾的技术暂时没找到很好的实现方法。所以我们只能借助Spring。
改造服务端
废话不多说,直接上坐标:
在最大的 pom 加上版本号及版本控制
<properties> <spring.boot.version>2.3.3.RELEASE</spring.boot.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring.boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>然后在adouge-rpc-server加上 web
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>添加启动类
@SpringBootApplication(scanBasePackages = {"com.adouge","cn.hutool.extra.spring"})public class ServerApplication {
public static void main(String[] args) { SpringApplication.run(ServerApplication.class, args); }}修改RpcRequestHandler
我们这里就简单的借助Spring IOC 通过IOC去获取它的实现类,在生成实现类的代理类
@SneakyThrows public Object handler(RpcRequest request) { Object bean = SpringUtil.getBean(Class.forName(request.getInterfaceName())); return invokeTargetMethod(request,bean); }记得在实现类加上 @Service
我们这里用到的SpringUtil是hutool里面的, 这里就不详细说明了。
测试
然后我们再去运行服务端和客户端。就可以看到:
11:04:42.093 [nioEventLoopGroup-2-1] INFO com.adouge.rpc.core.tool.netty.NettyClientHandler - 服务端返回数据: [RpcResponse(message=sb)]RpcResponse(message=sb)其实到这块这部分就已经完全可以走通了,但是绕了一套圈弄出来的CGLIB就没用上。既然是一个学习性质的项目,肯定是不能直接调用已封装好的东西。hutool 不算
结合Spring获取实现类
我们这里借助BeanPostProcessor去实现这个东西。先说下原理,就是在 bean 初始化的时候记录这个 bean 和他所实现的接口。
Spring在加载bean的时候肯定会加载各种各样的bean,但这些 bean 都不一定是我们RPC调用的时候所用到的,所以需要找个东西区分开来,我这里使用注解去区分开来。
创建 RPC 服务用的注解
在adouge-rpc-tool里面创建一个注解用来区分RPC服务和普通服务的。
@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.TYPE})@Inherited@Componentpublic @interface RpcService {}服务提供者
首先我们需要定义一个 Map,里面用来存放接口以及他的实现类。所以我们创建一个ServiceProvider以及他的实现类。
public interface ServiceProvider { /** * 添加服务类 * @param bean bean * @param interfaceName 接口名 */ void addService(Object bean,String interfaceName);
/** * 获取服务类 * @param interfaceName 接口名 * @return 服务嗲你勒 */ Object getService(String interfaceName);}@Slf4j@Componentpublic class ServiceProviderImpl implements ServiceProvider{ private final Map<String, Object> serviceMap= new ConcurrentHashMap<>();
@Override public void addService(Object bean,String interfaceName) { serviceMap.put(interfaceName,bean); } @Override @SneakyThrows public Object getService(String interfaceName) { Object service = serviceMap.get(interfaceName); if (null == service) { throw new Exception("找不到对应的Bean"); } return service; }}在BeanPostProcessor里面区分
实现BeanPostProcessor里面的前置方法postProcessBeforeInitialization
@Slf4j@Component@RequiredArgsConstructorpublic class RpcBeanPostProcessor implements BeanPostProcessor { private final ServiceProvider serviceProvider;
@Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { Class<?> cls = bean.getClass(); if (cls.isAnnotationPresent(RpcService.class)) { Class<?>[] interfaces = cls.getInterfaces(); for (Class<?> anInterface : interfaces) { log.info("【{}】添加到服务实现类---", anInterface.getName(), bean); serviceProvider.addService(bean, anInterface.getName()); } } return bean; }}再次修改RpcRequestHandler
同样的单例获取ServiceProvider,然后调用getService获取实现类。
@Component@RequiredArgsConstructorpublic class RpcRequestHandler { private final ServiceProvider serviceProvider;
/** * 用作处理 */ @SneakyThrows public Object handler(RpcRequest request) { Object service = serviceProvider.getService(request.getInterfaceName()); return invokeTargetMethod(request, service); }}RpcRequestHandler修改完后NettyServerHandler就会报错,这个简单,简单改下就好了。
RpcRequestHandler bean = SpringUtil.getBean(RpcRequestHandler.class);Object handler = bean.handler(rpcRequest);再一次测试
惯例,先运行服务端,在运行客户端就会出现真正成功的效果了。
11:57:38.224 [nioEventLoopGroup-2-1] INFO com.adouge.rpc.core.tool.netty.NettyClientHandler - 服务端返回数据: [RpcResponse(message=sb123)]RpcResponse(message=sb123)这篇有点长了,剩下部分分到下期吧。