一、前言
在平时Spring Boot项目开发过程中,我们进行远程服务调用大都采用@RestController
+ @RequestMapping
相关注解发布接口,使用OpenFeign
组件进行微服务之间调用。这套技术架构已经足够完善了,当然没有什么问题,但是作为一个开发者,老是用一套框架天天写代码,不免有点无聊,那么今天我们就从零开始,不使用@RestController
+ @RequestMapping
相关注解发布接口,不使用OpenFeign
组件进行远程调用,靠自己,快速从零开始实现一个属于自己的RPC框架
,炫一下吧!
二、开发过程
1. 新建Spring Boot项目,导入相关依赖
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency><dependency><groupId>io.springfox</groupId><artifactId>springfox-boot-starter</artifactId><version>3.0.0</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><dependency><groupId>com.squareup.okhttp3</groupId><artifactId>okhttp</artifactId></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.83</version></dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId><version>2021.0.5.0</version></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</artifactId><version>3.1.5</version></dependency></dependencies>
2. 自定义注解
既然我们不使用Spring提供的原生相关注解了,那我们为了简化开发,还是自定义相关注解
吧。
2.1 定义标识微服务发布RPC接口注解
此注解的作用于接口上,被该注解标识的接口可以被自定义框架把接口中的所有方法自动发布成URL
,简化我们代码开发,不用每个方法单独发布,示例代码:
import java.lang.annotation.*;/*** 通过该注解标识接口发布的应用信息*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CustomRpcApp {/*** 发布接口的应用名称 ** @return*/String appName() default "";/*** 应用的contentPath,通过server.servlet.context-path 属性配置** @return*/String contentPath() default "";
}
2.2 定义可以限制接口发布的注解
在【步骤2.1】
中,我们定义了一个接口服务发布URL的注解,但是接口中,有些方法我们不想发布出去,或者有些方法发布出去的url我们想自己定义另外一个名称,不直接使用方法名称,那么为了自定义的RPC逻辑更加灵活,我们增加一个注解,示例代码如下:
import java.lang.annotation.*;/*** RPC方法发布配置注解*/
@Target({ElementType.METHOD}) // 作用于方法上
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CustomRpcMethodConfig {/*** 发布方法取别名** @return*/String alias() default "";/*** 标识该方法是否禁止发布** @return*/boolean isForbidden() default false;}
2.3定义RPC服务提供方注解
接口服务被发布以后,还是需要具体实现类提供服务,我们也需要在实现类
上去标识一下这是一个RPC服务的提供方,示例代码如下:
/*** 声明服务提供方注解*/
@Target({ElementType.TYPE}) // 作用于类上, 发布整个类的接口
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CustomRpcProvider {
}
2.4 定义RPC消费者注解
我们提供好RPC服务,是需要提供给第三方调用的,为了让第三方调用时,标识自己需要进行RPC调用,我们也可以定义一个注解,示例代码:
/*** 声明一个消费者注解*/
@Target(ElementType.FIELD) // 作用于字段上
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CustomRpcConsumer {
}
3. 开发服务发布功能
定义好接口以后,我们就可以开始进行服务发布的逻辑开发了,我们大概思路是,通过解析自定义注解标识的服务接口,进行URL发布。
3.1 定义全局的服务发布信息保存类
我们可以定义一个RPC服务发布信息的保存类,在服务启动的时候解析一次进行保存
,这样方便我们在后续使用的时候直接获取,不用进行二次解析,示例代码:
import lombok.Data;import java.lang.reflect.Method;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;public class RpcProviderHolder {/*** 保存RpcProvider提供者的信息*/public static final ConcurrentMap<String, RpcProviderInfo> RPC_PROVIDER_MAP = new ConcurrentHashMap<>();@Datapublic static class RpcProviderInfo {/*** rpc服务提供者应用名称*/private String appName;/*** rpc发布服务前缀,默认是 /beanName*/private String urlPrefix;/*** rpc发布服务url核心部分,默认是接口方法名称*/private List<RpcMethod> urlCoreMethod;/*** rpc服务注册到spring容器中的beanName*/private String rpcBeanName;/*** rpc服务注册到spring容器中的bean*/private Object rpcBean;}@Datapublic static class RpcMethod {/*** 方法对象*/private Method method;/*** 方法别名*/private String alias;}
}
3.2 定义发布url统一的请求处理器
这里URL发布逻辑,之前在另一篇博客Spring Boot项目中不使用@RequestMapping相关注解,如何动态发布自定义URL路径,已经有描述,这里我们不在赘述,直接上代码:
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.util.UriComponentsBuilder;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;public class CustomCommonHandlerUrl {public static final Method HANDLE_CUSTOM_URL_METHOD;static {// 提前准备方法对象Method tempMethod = null;try {tempMethod = CustomCommonHandlerUrl.class.getMethod("handlerCustomUrl", HttpServletRequest.class, HttpServletResponse.class);} catch (NoSuchMethodException e) {e.printStackTrace();}HANDLE_CUSTOM_URL_METHOD = tempMethod;}@ResponseBody/*** 拦截自定义请求的url,可以做成统一的处理器*/public Object handlerCustomUrl(HttpServletRequest request, HttpServletResponse response) throws Exception {// 解析请求urlList<String> pathSegments = UriComponentsBuilder.fromUriString(request.getRequestURI()).build().getPathSegments();String rpcService = null;String methodName = null;// url默认格式是 接口名称/方法名称if (pathSegments.size() == 2) {rpcService = pathSegments.get(0);methodName = pathSegments.get(1);} else if (pathSegments.size() == 3) { // 可能配置了contentpath,这里偷懒简单判断一下rpcService = pathSegments.get(1);methodName = pathSegments.get(2);}// 获取请求体String requestBodyJsonString = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8);// 解析参数Object[] params = resolveParams(requestBodyJsonString, rpcService, methodName);// 执行方法return execute(rpcService, methodName, params);}/*** 执行方法** @param rpcService* @param methodName* @param params* @return* @throws InvocationTargetException* @throws IllegalAccessException*/private Object execute(String rpcService, String methodName, Object[] params) throws InvocationTargetException, IllegalAccessException {// 获取RpcProvider的相关信息RpcProviderHolder.RpcProviderInfo rpcProviderInfo