程序员你为什么这么累【续】:编写简陋的接口调用框架 - 动态代理学习

导读:

前些时间写了很多编码习惯的帖子,今天写一点简单的技术贴。其实我个人觉得编码习惯是最主要的,比技术重要,但初学者还是喜欢看技术贴,今天就写一个小Demo,也加深自己的理解。

JDK的动态代理是非常重要的技术,使用的地方很多,用于代理接口,Spring的AOP也会用到。技术细节这里不贴了,我不是技术高手,大家可以上网搜索一下一大把,今天我们结合spring编写一个简陋的“框架”。

完整代码已经上传到GITHUB,地址在最后面。

最终效果

假设我们需要调用另外一个系统提供了的GET请求 http://localhost:8081/test/get2?key=somekey**

我们只需要定义一个接口:

import cn.xiaowenjie.myrestutil.http.GET;
import cn.xiaowenjie.myrestutil.http.Param;
import cn.xiaowenjie.myrestutil.http.Rest;
import cn.xiaowenjie.retrofitdemo.beans.ResultBean;

@Rest("http://localhost:8081/test")
public interface IRequestDemo {
  @GET
  ResultBean get1();

  @GET("/get2")
  ResultBean getWithKey(@Param("key") String key);
}

然后直接注入该接口即可:

@Service
public class TestService{
  @Autowired
  IRequestDemo demo;

  public void test() {
    // 调用接口,得到结果
    ResultBean get1 = demo.get1();
    ResultBean get2 = demo.getWithKey("key-------");
  }
}

这就是今天Demo的效果,看着还行,有点类似restfeign,当然离真正应用还有一段差距。我们这里主要是学习动态代理和基本的spring应用。

总共不到200行代码,很容易阅读,逐一说明实现过程。

定义注解

这里定义三个注解

  • Rest作用表示这是一个Rest的接口,主要属性是要调用的Rest服务器信息。
  • GET作用表示这个方法是GET方法,主要属性是调用的URL信息
  • Param作用是映射参数名称

定义Rest服务器信息Bean

扫描Rest注解后生成,这里包含了被调用的服务器的信息。Demo里面只有一个Host信息。

/**
 * 包装服务器信息类,目前只有host,其他自己配置即可。
 */
@Data
public class RestInfo {
  private String host;
}

定义请求信息的包装Bean

扫描GET请求生成,主要包括请求是URL,参数等。

/**
 * 请求信息包装类
 */
@Data
public class RequestInfo {
  private String url;

  private Class<?> returnType;

  private LinkedHashMap<String, String> params;
}

定义处理请求的接口

使用它来处理扫描生成的请求

/**
 * 处理网络请求接口
 */
public interface IRequestHandle {
  Object handle(RestInfo restInfo, RequestInfo request);
}

扫描所有Rest接口生成动态代理类

Spring启动的时候,扫描所有的带Rest注解的接口。如下这种

@Rest("http://localhost:8081/test")
public interface IRequestDemo

定义一个工具Bean,Bean注册的时候使用Reflections扫描工程里面属于带Rest注解的接口。

@Component
public class RestUtilInit {

  @Autowired
  IRequestHandle requestHandle;

  @PostConstruct
  public void init() {
    Set<Class<?>> requests = new Reflections("cn.xiaowenjie").getTypesAnnotatedWith(Rest.class);

    for (Class<?> cls : requests) {
      createProxyClass(cls);
    }
  }
}

创建动态代理实现如下:

private void createProxyClass(Class<?> cls) {
  System.err.println("\tcreate proxy for class:" + cls);

  // rest服务器相关信息
  final RestInfo restInfo = extractRestInfo(cls);

  InvocationHandler handler = new InvocationHandler() {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      RequestInfo request = extractRequestInfo(method, args);
      return requestHandle.handle(restInfo, request);
    }
  };

  // 创建动态代理类
  Object proxy = Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class<?>[] { cls }, handler);

  registerBean(cls.getName(), proxy);
}

其实就是把请求包装成RestInfo和RequestInfo,然后创建 一个InvocationHandler实现接口代理,在里面调用IRequestHandle接口处理请求。不复杂,就几行代码。

把代理类注册到Spring容器

注册到Spring容器后,然后就可以在Controll等出直接注入使用。

@Autowired
ApplicationContext ctx;

public void registerBean(String name, Object obj) {
  // 获取BeanFactory
  DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) ctx
      .getAutowireCapableBeanFactory();

  // 动态注册bean.
  defaultListableBeanFactory.registerSingleton(name, obj);
}

定义默认的RestTemplate处理请求类

需要先在容器里面注册自己的RestTemplate。然后实现IRequestHandle

/**
 * 实现了IRequestHandle,基于resttemplate处理rest请求。
 * 需要在spring容器中注册RestTemplate。
 */
@Component
public class RestTemplateRequestHandle implements IRequestHandle {
  @Autowired
  RestTemplate rest;

  @Override
  public Object handle(RestInfo restInfo, RequestInfo request) {
    System.err.println("\n\n\thandle request,  restInfo=" + restInfo);
    System.err.println("\thandle request,  request=" + request);

    String url = extractUrl(restInfo, request);
    System.err.println("\thandle url:" + url);

    //TODO 目前只写了get请求,需要支持post等在这里增加
    //TODO 需要在这里增加异常处理,如登录失败,链接不上

    Object result = rest.getForObject(url, request.getReturnType());

    return result;
  }

  /**
   * 生成真实的url
   * 
   * @param restInfo
   * @param request
   * @return
   */
  private String extractUrl(RestInfo restInfo, RequestInfo request) {
    String url = restInfo.getHost() + request.getUrl();

    if (request.getParams() == null) {
      return url;
    }

    Set<Entry<String, String>> entrySet = request.getParams().entrySet();

    String params = "";

    for (Iterator<Entry<String, String>> iterator = entrySet.iterator(); iterator.hasNext();) {
      Entry<String, String> entry = iterator.next();
      params += entry.getKey() + '=' + entry.getValue() + '&';
    }

    return url + '?' + params.substring(0, params.length() - 1);
  }
}

JUNIT简单测试

先启动被调用的服务。然后跑junit。直接注入IRequestDemo接口。

@RunWith(SpringRunner.class)
@SpringBootTest(classes = MyRestUtilApplication.class)
public class MyRestUtilApplicationTests {

  @Autowired
  IRequestDemo demo;

  @Test
  public void test() {
    ResultBean get1 = demo.get1();
    System.out.println(get1);
  }

  @Test
  public void test2() {
    ResultBean get2 = demo.getWithKey("2332323");
    System.out.println(get2);
  }
}

测试正常,接口调用正常。

如何加入认证

举例最简单的HttpBasic认证,可以在RestTemplate设置

@ComponentScan("cn.xiaowenjie")
@SpringBootApplication
public class MyRestUtilApplication {

  public static void main(String[] args) {
    SpringApplication.run(MyRestUtilApplication.class, args);
  }

  @Autowired(required = false)
  List<ClientHttpRequestInterceptor> interceptors;

  @Bean
  public RestTemplate restTemplate() {
    System.out.println("-------restTemplate-------");

    RestTemplate restTemplate = new RestTemplate();

    // 设置拦截器,用于http basic的认证等
    restTemplate.setInterceptors(interceptors);

    return restTemplate;
  }
}

拦截器把认证信息放在头里面。

@Component
public class HttpBasicRequestInterceptor implements ClientHttpRequestInterceptor {

  @Override
  public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
      throws IOException {

    // TODO 需要得到当前用户
    System.out.println("---------需要得到当前用户,然后设置账号密码-----------");

    String plainCreds = "xiaowenjie:admin";
    byte[] plainCredsBytes = plainCreds.getBytes();
    byte[] base64CredsBytes = Base64.encodeBase64(plainCredsBytes);

    String headerValue = new String(base64CredsBytes);

    HttpRequestWrapper requestWrapper = new HttpRequestWrapper(request);
    requestWrapper.getHeaders().add("Authorization", "Basic " + headerValue);

    return execution.execute(requestWrapper, body);
  }
}

总结

过程比较简单,步骤如下

  • 使用 org.reflections.Reflections 得到所有配置了 @Rest 的接口列表
  • 根据 @Rest 得到服务器配置信息 RestInfo
  • 使用 Proxy.newProxyInstance 生成接口的代理类,invoke 方法中根据 @GET 得到该方法请求信息 RequestInfo,调用 IRequestHandle 接口处理请求,。
  • 把生成的代理类注入到spring容器中。

DEMO GITHUB地址

源码在这里:xwjie/MyRestUtil**,欢迎加星。框架代码在单独的 MyRestUtil\myrestutil\restutil 目录中,主要逻辑都在 RestUtilInit 上,代码非常精简,一看就明白,总共200行左右吧。

后话

本示例只实现了GET请求,也没有支持REST风格的很多特性,工作中一般使用RestFeign这类框架,如果这些框架不满足需要自己写一个可以参考这个代码。我觉得他已经可以算一个乞丐版的框架了!技术简单,我觉得我的编码习惯比技术更有价值,嘿嘿。

由于个人技术水平有限,代码如有错误或者好的实现方式请大家务必指出,一起学习进步。大家有什么建议也请留言区留言,谢谢阅读。

原文发布于微信公众号 - 程序猿DD(didispace)

原文发表时间:2017-09-18

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏芋道源码1024

面试问烂的 Spring AOP 原理

来源:https://www.jianshu.com/p/e18fd44964eb

2114
来自专栏芋道源码1024

注册中心 Eureka 源码解析 —— EndPoint 与 解析器

目前有多种 Eureka-Server 访问地址的配置方式,本文只分享 Eureka 1.x 的配置,不包含 Eureka 1.x 对 Eureka 2.x 的...

1220
来自专栏Jack-Cui

Linux应用层系统时间写入RTC时钟的方法

Linux内核版本:linux-3.0.35 开发板:i.MX6S MY-IMX6-EK200 系统:Ubuntu12 前言:之前写过一篇关于如...

2150
来自专栏开源优测

[接口测试 - http.client篇] 16 基于http.client之POM实战一下

概述 关注公众号回复: http.client_pom_demo 获取本文示例源码 你需要了解以下知识和技术,以便掌握后续的实例代码: http.client常...

3578
来自专栏java学习

关于Spring面试题讲解2

依赖注入,是IOC的一个方面,是个通常的概念,它有多种解释。这概念是说你不用创建对象,而只需要描述它如何被创建。你不在代码里直接组装你的组件和服务,但是要在配置...

842
来自专栏Java帮帮-微信公众号-技术文章全总结

Web-第十一天 JSP学习

JSP全名是Java Server Pages,它是建立在Servlet规范之上的动态网页开发技术。在JSP文件中,HTML代码与Java代码共同存在,其中,H...

1523
来自专栏技术墨客

Spring核心——资源数据管理 原

在Profile管理环境一文中介绍了环境的概念以及Spring Profile特性控制Bean的添加。本文将进一步介绍Spring管理和控制操作系统变量、JVM...

934
来自专栏架构之路

SpringMVC + Mybatis bug调试 SQL正确,查数据库却返回NULL

今天碰到个bug,有点意思 背景是SpringMVC + Mybatis的一个项目,mapper文件里写了一条sql 大概相当于 select a from t...

3727
来自专栏Java后端生活

JavaWeb(六)JSP-1

2003
来自专栏GreenLeaves

WebService 之 身份验证

  在项目开发,我们经常会使用WebService,但在使用WebService时我们经常会考虑到了WebService是安全问题,很容易想到通过一组用户名与密...

3507

扫码关注云+社区

领取腾讯云代金券