前请提示

  1. 我们在项目中微服务之间是可以相互调用的,在调用过程中我们只需要将一个微服务名+接口路径+接口请求对象给到我们调用方,那么调用方便可以发起调用。那它这么一个过程是怎样实现的呢?<br /> 其实根据我们学到的zk知识其实就很容易联想到应该是这么一个调用过程。首先我们服务A给服务B提供服务,服务A一般是集群部署,然后将服务A所提供的所有接口服务暴露在zk,然后服务B去找zk中是否有自己所需的服务,存在呢可能就会通过nginx负载均衡到一台服务器A,然后服务器A会去分发找到对应的请求,并将结果返回给服务B。<br /> 我这里就手写和比较简单的BIO的请求,而且这里面还没写服务暴露发现,也没用到任何协议,也没有去更好的序列化传输机制,也没有使用更好的性能的通信框架NIO或者netty。仅仅是作为自己的笔记而已。<br />![屏幕快照 2020-03-29 下午1.09.41.png](https://cdn.nlark.com/yuque/0/2020/png/771792/1585459865539-f410aa12-74e1-4086-8510-4256e9b48515.png#crop=0&crop=0&crop=1&crop=1&height=261&id=rUiJv&name=%E5%B1%8F%E5%B9%95%E5%BF%AB%E7%85%A7%202020-03-29%20%E4%B8%8B%E5%8D%881.09.41.png&originHeight=261&originWidth=769&originalType=binary&ratio=1&rotation=0&showTitle=false&size=43537&status=done&style=none&title=&width=769)

代码演示

自定义注解仿照spring

  1. @Retention(RetentionPolicy.RUNTIME)
  2. @Target(ElementType.METHOD)
  3. public @interface GetMappingByHmm {
  4. String value();
  5. }
  6. @Retention(RetentionPolicy.RUNTIME)
  7. @Target(ElementType.TYPE)
  8. public @interface RequestMappingByHmm {
  9. String value();
  10. }

接口层

  1. @RequestMappingByHmm("/user")
  2. public class UserController {
  3. @GetMappingByHmm("/queryUserName")
  4. public String queryUserName(String id) {
  5. return "hmm:" + id;
  6. }
  7. @GetMappingByHmm("/queryUserXX")
  8. public String queryUserXX() {
  9. return "xxx";
  10. }
  11. }

请求实体类

  1. @Data
  2. public class ReqParamDto implements Serializable {
  3. private String applicationName;//微服务名 单台机器也可能部署了其它用用 客户端传的
  4. private String url;//接口地址
  5. private Object reqDto;//请求对象
  6. }

服务发现(没有暴露出去)

  1. /**
  2. * @author heian
  3. * @create 2020-03-29-10:44 上午
  4. * @description 加载所有的路径
  5. */
  6. public class ControllerLoader {
  7. //所有接口层{"/user"/queryUserName",controller层的类}
  8. static Map<String,DetailsReqDto> map = new HashMap<>();
  9. /**
  10. * 路径是唯一的可以作为map的key
  11. * @param pkgName com.example.customers.standard.rpc.api
  12. */
  13. public static Map<String,DetailsReqDto> getController(String pkgName) throws Exception {
  14. List<Class> classes = getClasses(pkgName);//com.example.customers.standard.rpc.api.UserController
  15. for(Class<?> cla : classes) {
  16. RequestMappingByHmm annotation = cla.getAnnotation(RequestMappingByHmm.class);
  17. String path1 = annotation.value();
  18. Method[] declaredMethods = cla.getDeclaredMethods();
  19. for (Method declaredMethod : declaredMethods) {
  20. GetMappingByHmm annotation1 = declaredMethod.getAnnotation(GetMappingByHmm.class);
  21. if (annotation1 != null){
  22. String path2 = annotation1.value();
  23. DetailsReqDto dto = new DetailsReqDto();
  24. dto.setAClass(cla);
  25. dto.setMethodName(declaredMethod.getName());
  26. map.put(path1+path2,dto);
  27. }
  28. }
  29. }
  30. //map 可以考虑存放在zk或者其它分布式配置中心
  31. System.out.println(map.toString());
  32. return map;
  33. }
  34. /**
  35. * @param pkgName
  36. * @return 返回 RequestMappingByHmm 注解下所有的类
  37. * @throws ClassNotFoundException
  38. */
  39. public static List<Class> getClasses(String pkgName) throws ClassNotFoundException{
  40. List<Class> totalClass = new ArrayList<>();
  41. ClassLoader cld = Thread.currentThread().getContextClassLoader();
  42. if (cld == null)
  43. throw new ClassNotFoundException("无法获取到ClassLoader");
  44. String path = pkgName.replace('.', '/');
  45. URL resource = cld.getResource(path); //会去target 下面寻找的编译器文件
  46. if (resource == null)
  47. throw new ClassNotFoundException("没有这样的资源:" + path);
  48. File directory = new File(resource.getFile());
  49. if (directory.exists()){
  50. String[] fileName = directory.list();//目录名
  51. File[] files = directory.listFiles();
  52. for (int i = 0; i < files.length; i++) {
  53. File file = files[i];
  54. //判断是不是比以class 后缀结尾的
  55. if (file.isFile() && file.getName().endsWith(".class")){
  56. String classPath = pkgName + "." + fileName[i].substring(0,fileName[i].length()-6);
  57. Class<?> aClass = Class.forName(classPath);
  58. RequestMappingByHmm annotation = aClass.getAnnotation(RequestMappingByHmm.class);
  59. if (annotation != null){
  60. totalClass.add(aClass);
  61. }
  62. }else {
  63. //目录则递归查找
  64. List<Class> classes1 = getClasses(pkgName + "." + file.getName());
  65. if (classes1 != null && !classes1.isEmpty()){
  66. totalClass.addAll(classes1);
  67. }
  68. }
  69. }
  70. }else {
  71. throw new ClassNotFoundException(pkgName + "不是一个有效的包");
  72. }
  73. return totalClass;
  74. }
  75. }

大致流程

  1. 扫描包下所有带 RequestMapping注解的类,拿到后对应的接口类
  2. 获取对应类请求的value1,然后再获取带有 GetMappingBy 注解的方法,获取对应的注解的value2
  3. 然后将value1+value2作为map的key,方法名和对应class(当然还有微服务名,这里暂时没用)整合成一个对象作为一个map的value存起来
  4. 服务分发:服务端开启,处于侦听状态,一旦接收到客户端的请求,拿到传来的参数,获取缓存map,有了类有了方法,直接method.invoke(类对象,传来的参数)这样就相等于调用了接口一样,这不过我这里是通过socket来简单实现一下。

服务分发

  1. /**
  2. * @author heian
  3. * @create 2020-03-29-12:49 上午
  4. * @description 找到接口请求过来的对象,解析->调用对应的方法
  5. */
  6. public class ServiceDispatch {
  7. public static Object dispatch(Object reqObj){
  8. ReqParamDto reqParamDto = (ReqParamDto) reqObj;
  9. //String applicationName = reqParamDto.getApplicationName();//暂时 没用到
  10. String url = reqParamDto.getUrl();
  11. Object reqDto = reqParamDto.getReqDto();//requestbody
  12. Object ret = null;
  13. //根据客户端发来的请求 地址存在则表明存在对应的方法
  14. if (ControllerLoader.map.get(url) != null){
  15. DetailsReqDto dto = ControllerLoader.map.get(url);
  16. Class aClass = dto.getAClass();
  17. String methodName = dto.getMethodName();
  18. try {
  19. Method method = aClass.getMethod(methodName, reqDto.getClass());
  20. System.out.println("服务端开始执行方法:" + methodName + ",参数类型:" + reqDto.getClass());
  21. ret = method.invoke(aClass.newInstance(),reqDto);
  22. System.out.println("执行结果:" + ret.toString());
  23. }catch (Exception e){
  24. e.printStackTrace();
  25. }
  26. }
  27. return ret;
  28. }
  29. }

服务段自身存储的实体类用来标识:接口与方法的映射

  1. /**
  2. * @author heian
  3. * @create 2020-03-29-1:34 上午
  4. * @description
  5. */
  6. public class BootStrap {
  7. public static void main(String[] args) throws Exception {
  8. //加载所有的接口
  9. ControllerLoader.getController("com.example.customers.standard.rpc.api");
  10. BioSocketServer.start(8888);
  11. }
  12. }

服务端启动类

  1. @Setter
  2. @Getter
  3. @ToString
  4. public class DetailsReqDto {
  5. //通过接口路径 找到对应的类 方法名
  6. private Class aClass;
  7. private String methodName;
  8. }
  1. /**
  2. * @author heian
  3. * @create 2020-03-29-12:37 上午
  4. * @description
  5. */
  6. public class BioSocketServer {
  7. private static ExecutorService executorService = Executors.newCachedThreadPool();
  8. public static void start(int port) throws IOException {
  9. ServerSocket serverSocket = new ServerSocket(port);
  10. System.out.println("服务端正在启动");
  11. while (true){
  12. Socket socket = serverSocket.accept();//阻塞等待客户端连接
  13. //每一个连接连接进来必须分配一个线程,防止因为某个客户端阻塞而导致无法接受新的请求
  14. executorService.submit(() -> {
  15. ObjectInputStream in = null;
  16. ObjectOutputStream out = null;
  17. System.out.println("收到客户端的连接进来");
  18. try {
  19. in = new ObjectInputStream(socket.getInputStream());
  20. //拿到客户端发来的对象
  21. Object reqObj = in.readObject();
  22. System.out.println("reqObj:" + reqObj.toString());
  23. //调用服务的分发 去查询对应服务
  24. Object retObj = ServiceDispatch.dispatch(reqObj);
  25. out = new ObjectOutputStream(socket.getOutputStream());
  26. //返回客户端接口
  27. out.writeObject(retObj);
  28. out.flush();
  29. }catch (Exception e){
  30. e.printStackTrace();
  31. }finally {
  32. try {
  33. if (in != null) in.close();
  34. if (out != null)out.close();
  35. }catch (Exception e){
  36. e.printStackTrace();
  37. }
  38. }
  39. });
  40. }
  41. }
  42. }

客户端启动类

  1. /**
  2. * @author heian
  3. * @create 2020-03-29-12:06 下午
  4. * @description
  5. */
  6. public class BioSocketClient {
  7. public static void main(String[] a) throws IOException {
  8. SocketAddress socketAddress = new InetSocketAddress("localhost",8888);
  9. Socket socket = new Socket();
  10. socket.connect(socketAddress);
  11. //调用服务
  12. ReqParamDto reqParamDto = new ReqParamDto();
  13. reqParamDto.setApplicationName("lcloud-ceres-rnp");
  14. reqParamDto.setUrl("/user/queryUserName");
  15. reqParamDto.setReqDto("1");
  16. ObjectInputStream in = null;
  17. ObjectOutputStream out = null;
  18. try {
  19. out = new ObjectOutputStream(socket.getOutputStream());
  20. out.writeObject(reqParamDto);
  21. out.flush();
  22. in = new ObjectInputStream(socket.getInputStream());
  23. Object retObj = in.readObject();
  24. System.out.println("收到服务段的返回信息" + retObj.toString());
  25. }catch (Exception e){
  26. e.printStackTrace();
  27. }finally {
  28. try {
  29. if (in != null) in.close();
  30. if (out != null)out.close();
  31. }catch (Exception e){
  32. e.printStackTrace();
  33. }
  34. }
  35. }
  36. }

屏幕快照 2020-03-29 下午1.27.00.png

测试

分别启动服务段和客户端
server

  1. 服务端正在启动
  2. 收到客户端的连接进来
  3. reqObj:ReqParamDto(applicationName=lcloud-ceres-rnp, url=/user/queryUserName, reqDto=1)
  4. 服务端开始执行方法:queryUserName,参数类型:class java.lang.String
  5. 执行结果:hmm:1

client

收到服务段的返回信息hmm:1