程序员你为什么这么累【续】:编写简陋的接口调用框架 - 动态代理学习
前些时间写了很多编码习惯的帖子,今天写一点简单的技术贴。其实我个人觉得编码习惯是最主要的,比技术重要,但初学者还是喜欢看技术贴,今天就写一个小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这类框架,如果这些框架不满足需要自己写一个可以参考这个代码。我觉得他已经可以算一个乞丐版的框架了!技术简单,我觉得我的编码习惯比技术更有价值,嘿嘿。
由于个人技术水平有限,代码如有错误或者好的实现方式请大家务必指出,一起学习进步。大家有什么建议也请留言区留言,谢谢阅读。
导读: 程序员你为什么这么累?