写了两年半的代码了,一直没有很好的沉淀下来。这两年多基本上也是在工作中学习,有问题就上网查一下或者平时觉得好的文章就收藏一下,想想确实还是需要自己好好整理一下,不实践很快就忘了。
作为第一篇文章,就从jdk的动态代理说说吧。之前受到转转的一篇文章启发:转转支付网关之注解式HTTP客户端,最近也是开源了一个基于jdk动态代理的声明式http模块 slow-creator-http ,经验不足,写的不好,如果可以的话大佬们可以指点一下。
同时也非常感谢一些互联网大公司愿意分享他们自己在工作中遇到的问题、心得和技术,让我们这些人可以了解受到大公司考验的新技术,怎么用他们的经验、思路来处理我们的问题。
jdk动态代理
今天的jdk动态代理有一些地方会基于slow-creator-http来进行说明,首先我们有这么几个问题:
- 为什么需要代理模式?
- 为什么使用jdk动态代理?
- jdk动态代理可以不需要实现类吗?
- 使用动态代理可以节省哪些工作?
为什么需要动态代理
平常我们如果需要对接第三方的http请求接口的请求怎么对接呢? 大致有两种:
1、原生方式
//new一个URL
URL url = new URL('https://xxx.xxx.xxx/xxx');
// 打开链接
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
// 设置请求方式
connection.setRequestMethod("POST");
//设置超时时间
connection.setConnectTimeout(5000);
//设置请求头等等
connection.setRequestProperty("Content-Type", "application/json");
...
//读取流信息
InputStream is = connection.getInputStream();
//关闭流
is.close();
......
2、使用其他人封装好的http请求工具,如okhttp、hutool-http,然后基于这些工具封装一个符合自己业务的工具,把需要的请求方式、请求头、请求体、请求地址等信息传入
这两种方式最后都会落到封装工具类上,但是实际上即使封装的再完美也会有例外的情况,比如有些请求不需要请求头、不需要请求体、form-data格式请求、json格式请求,这样我们就会产生很多的重载方法。而且比如json返回,我们对接了很多第三方接口,需要解析,每个第三方成功状态码不一样(之前甚至还遇到一个外购系统对接,第一次请求调用令牌返回的token有三种类型,可能json可能字符串(直接就是一个token值没其他信息)也可能是html代码,他们给的解决方式是拿到返回值解析json,try-catch起来,到catch就是解析报错那么整串是token,再去请求如果失败就是html代码继续请求获取token,主要是为什么返回三种类型问他们也不知道就说这是底层他们改不了......)
如果我们可以在运行中动态获取方法参数那就可以避免许多的重载方法,而且可以动态的修改解析参数,有些接口用json解析,有些是xml解析,那么动态代理就是一个很好的选择。
jdk的动态代理需要实现InvocationHandler并且重写invoke方法,该方法会传入三个参数Object proxy, Method method, Object[] args,第一个参数是代理类的实例,第二个参数是执行的方法,第三个参数是方法参数,没错我们可以通过第三个参数去获取参数。并且和注解相结合我们可以减少很多的固定参数通过参数传入,例如请求方法、请求地址、请求超时时间等参数。
例如这样
@HttpClient(
desc = "测试GET",
url = "http://127.0.0.1:8080/annotationHttp/testGet",
failThrow = true,
throwException = HttpRequestException.class,
timeout = 10000
)
ScReturn testGet(@HttpHeader Map<String, String> header,
@HttpParams Map<String, String[]> params,
@HttpReturnHeader Set<String> returnHeader);
我们可以通过Method和args就可以轻松获取对应的参数,如果不需要请求头直接把@HttpHeader Map<String, String> header参数去掉就好,目前slow-creator-http设计是无特定类型传入需要带注解,如果使用的是指定的参数类型可以省略这个注解。
通过method.invoke(obj, args)执行原有逻辑就可以实现方法调用前对参数进行处理,以及调用后的处理。
所以我们可以通过调用前获取参数执行http请求,然后请求结束把请求后的参数解析后传入业务方法,这样子我们的接口实现类拿到结果直接进行业务处理就好不用关心请求过程与数据解析。 比如像下面这样子:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
HttpClient httpClient;
httpClient = method.getAnnotation(HttpClient.class);
if (httpClient == null) {
throw new NullPointerException("请求注解不能为空!");
}
try {
ScReturn scReturn = this.doInvoke(method, args, httpClient);
// 拿到调用的返回值 进行后置处理 如果设置的是请求失败抛出异常 failThrow = true,在这边抛出
// 否则返回的 ScReturn 里面会包含一个请求状态 设置状态判断请求是否成功
this.failThrowExceptionIfNecessary(scReturn, httpClient);
// 如果非接口 这边可以直接调用它的实现类 ScReturn 看实际情况传入args内
// return method.invoke(implClass, args);
return scReturn;
} finally {
// 有一些参数使用 ThreadLocal 传递,在这边移除一下
HttpContextManager.remove();
}
}
现在可以回答这个问题了,为什么需要动态代理?
动态代理可以让我们对原本的方法进行增强,而不需要修改原有的逻辑,我们还可每个方法参数进行动态设置,后续只需要按照正常逻辑去处理就好。同时我们也可以根据业务动态更改增强实现,比如json请求、webservice的soap(对接过这种接口,部门十几个人问过去就一两个人很多年之前搞过,现在也忘了该怎么做,太难了)而不需要调整原有逻辑功能。请求日志、spring事务、鉴权基本上都可以通过代理模式去实现。
为什么使用jdk动态代理 jdk动态代理需不需要实现类
首先我们知道jdk的动态代理是基于接口的,而cglib动态代理是通过生成代理类的子类进行代理的。最重要的是,我们一般只是对接口请求,拿到返回数据就行了,如果使用cglib我们就不能仅仅通过只写接口去实现这个功能。所以也可以回到下一个问题——jdk动态代理需不需要实现类?,答案是不需要的,mybatis就是这么做的,它让我们通过接口与xml绑定实现了与数据库的交互。因此使用jdk动态代理我们可以只写接口,不写实现类,也可以通过实现接口实现类,直接把返回结果放置到参数上,就像springmvc的@RequestParam、@RequestBody一样自动注入参数,实现类拿来就用就好(目前项目没有做这一部分,只实现了接口的代理)。
简单使用下jdk动态代理
某一天,你的PM过来和你说,我们的接口不太完善,有个优惠券的接口,你帮我调整一下,谁一天领了10张以上帮我记录一下。
你看了下原来的代码
public static void main(String[] args) {
ICoupon coupon = new CouponImpl();
coupon.getCoupon1("张三");
coupon.getCoupon2("张三");
// ......
coupon.getCoupon100("张三");
}
public interface ICoupon {
void getCoupon1(String user);
void getCoupon2(String user);
void getCoupon100(String user);
}
@Override
public void getCoupon1(String user) {
System.out.println("用户:" + user + ",领取了一号优惠卷.");
}
@Override
public void getCoupon2(String user) {
System.out.println("用户:" + user + ",领取了二号优惠卷.");
}
@Override
public void getCoupon100(String user) {
System.out.println("用户:" + user + ",领取了一百号优惠卷.");
}
// 查询获取数量方法
public static int getNumFromCacheOrDb(String user) {
Random random = new Random();
// 根据用户查询优惠券数量
return random.nextInt(30);
}
把这100个方法都修改成
@Override
public void getCoupon1(String user) {
System.out.println("用户:" + user + ",领取了一号优惠卷.");
int num = getNumFromCacheOrDb(user);
if (num > 10) {
System.out.println("用户:" + user + ",今日领了:" + num + "张优惠券");
}
}
@Override
public void getCoupon2(String user) {
System.out.println("用户:" + user + ",领取了二号优惠卷.");
int num = getNumFromCacheOrDb(user);
if (num > 10) {
System.out.println("用户:" + user + ",今日领了:" + num + "张优惠券");
}
}
@Override
public void getCoupon100(String user) {
System.out.println("用户:" + user + ",领取了一百号优惠卷.");
int num = getNumFromCacheOrDb(user);
if (num > 10) {
System.out.println("用户:" + user + ",今日领了:" + num + "张优惠券");
}
}
控制台输出(超过10张的记录)
用户:张三,领取了一号优惠卷.
用户:张三,领取了二号优惠卷.
用户:张三,今日领了:23张优惠券
用户:张三,领取了一百号优惠卷.
用户:张三,今日领了:16张优惠券
然后就可以和你的PM交工下班了,但是千万别被你领导code review的时候看到这个代码,否则可能会发生一些奇奇怪怪的事情。
第二天你的PM和你说10张太少了给改成20张,然后到达20张发条短信给用户,告诉他领了20张可以参与一次抽奖,你再次去修改这100个方法,你可能已经在骂PM了。
当你和朋友吐槽你的PM需求后,他告诉你一个叫做动态代理的设计模式,你实现类代码不用改变,只要像下面这么做
接口与实现类不需要调整,还是原来没加领取优惠券之前的代码。但是现在使用代理类去调用,这样你就不用修改100个方法了
public static void main(String[] args) {
ICoupon couponImpl = new CouponImpl();
ICoupon coupon = (ICoupon)Proxy.newProxyInstance(couponImpl.getClass().getClassLoader(),
couponImpl.getClass().getInterfaces(),
(proxy, method, args1) -> {
String user = (String) args1[0];
int num = CouponImpl.getNumFromCacheOrDb(user);
if (num > 20) {
System.out.println("用户:" + user + ",今日领了:" + num + "张优惠券");
System.out.println("通知用户:[" + user + "]去抽奖");
} else {
System.out.println("用户:" + user + ",今日领了:" + num + "张优惠券,未达到要求,不通知抽奖");
}
return method.invoke(couponImpl, args1);
});
coupon.getCoupon1("张三");
coupon.getCoupon2("张三");
// ......
coupon.getCoupon100("张三");
}
或许你也可以这样子操作一下
public static void main(String[] args) {
ICoupon couponImpl = new CouponImpl1();
ICoupon coupon = (ICoupon)Proxy.newProxyInstance(couponImpl.getClass().getClassLoader(),
couponImpl.getClass().getInterfaces(),
(proxy, method, args1) -> {
String user = (String) args1[0];
int num = CouponImpl1.getNumFromCacheOrDb(user);
if (num > 20) {
System.out.println("用户:" + user + ",今日领了:" + num + "张优惠券");
System.out.println("通知用户:[" + user + "]去抽奖");
} else {
System.out.println("用户:" + user + ",今日领了:" + num + "张优惠券,未达到要求,不通知抽奖");
}
// 这里把参数改为李四
args1[0] = "李四";
return method.invoke(couponImpl, args1);
});
coupon.getCoupon1("张三");
coupon.getCoupon2("张三");
// ......
coupon.getCoupon100("张三");
}
控制台输出
用户:张三,今日领了:25张优惠券
通知用户:[张三]去抽奖
// 实际实现类这边的用户被改为了李四
用户:李四,领取了一号优惠卷.
用户:张三,今日领了:23张优惠券
通知用户:[张三]去抽奖
用户:李四,领取了二号优惠卷.
用户:张三,今日领了:13张优惠券,未达到要求,不通知抽奖
用户:李四,领取了一百号优惠卷.
使用动态代理可以节省哪些工作?
可以看到我们不再需要对100个优惠券的方法代码进行修改,我们甚至不需要对接口以及实现类做任何的改动,并且对于修改领取数量我们也只是修改了一个地方,就做到了处处的修改,既避免了有些方法没改到的问题,也可以避免重复代码的出现。
不过我自己在日常工作中遇到的还是单例、策略模式、工厂模式、责任链和模板方法会用的多一点,状态模式都用的少。代理模式嘛,一般都是直接通过spring的aop,很少会在项目中自己去写这些,这个项目也是给了我一个很好的实践机会。
其实设计模式这个东西真的很神奇,就拿刚入行的第一年来说,我觉得这个东西很麻烦,我还要设计还要加接口或者抽象类再去实现或者继承,每个方法直接复制粘贴不是很方便。后面到代码多了,修改的地方也多了就知道痛苦了,用了设计模式后,后续的维护都变得简单了起来。比如之前PM经常对同一个功能加新方案,需要调整很多地方,现在通过策略模式每加一个方案我实现一个接口就好了,不会动到其他方法,也不用过多的对之前的代码进行测试。
最后聊聊自己为什么进入这个行业和这个大环境吧
作为一名理科生,大学学的是偏文科(毕业论文要求是按照文科类来才知道的)的经济管理类专业,当初也没有报这个专业,被调剂过去的,报的计算机、土木、机械......,第二年才知道分数都到了,不知道为啥把我调到这个专业,这专业还是第一年招生。转专业困难,只能呆了四年,期间自学了java、c++、数据结构,四年好像也没学到啥,没办法达到企业要求。
其实我还是算蛮幸运的吧,第一份工作是在公司的市场部做品牌推广和网站优化,公司也不大当时六七十人吧,通知入职的那天也正是我得了很久的荨麻疹第一天没出现的日子,非常开心。第一家公司从董事长到我的上级人都很好,公司不加班、没有现在常说的职场pua、没有画饼,对于我一个啥都不会的人也没有抛弃我,公众号消息推错了也没说啥就让撤回(后面也没扣绩效啥的,每个人都对我很好)。但是一直还是想着做一个程序员,这一年我开始用下班时间系统学习java,从基础到java web、mysql到ssm再到springboot。
然后找了一个月没找到就提离职了,想认真找找,虽然说遇到的这些人都很好,但是权衡了一下,为了自己这五年的梦想吧。没经验又不是这个专业真的好难找,离职后花了一个月就一个面试,是一家刚刚成立的小公司,只有十几二十个人,进去了。带我的是当时技术最强的架构师,架构师就坐我旁边,人也真的非常好,问啥都会告诉你要怎么做,有一些难的sql也会写完后分析给我听,有时候我自己都被自己烦到了,但是还是厚着脸皮去问。工作了刚好三个月公司资金链断了,部门全部都失业了。
最后又花了一个月时间拿了三个offer选择了现在的公司。上家公司那三个月真的是进步最快收获最多的三个月,真的非常感谢当时的架构师,以至于到现在的公司后,因为没包装就直说工作经历,他们都不相信我只有三个月经验都认为是一两年的。这两年呢,基本上上下班在地铁上看看掘金有啥新文章,然后看看贴吧,回家有空就去B站看看新技术视频(这一点做的不好,经常看一点忙起来后面就忘了看,视频在收藏夹吃灰,导致现在学到的还不是很多,这点接下来要改掉)。去年在公司也写过一个go项目,由于只有我一个人会go,有问题不太好维护,后面的项目也就放弃了go,还是继续java。
有时候真的不得不感慨这行发展真的好快,去年还在用vue2觉得好用,今年用了vue3后基本回不去了,不会想去维护vue2的项目了。用了springboot3和jdk17也是,springboot2都已经逐渐要结束它的时代了。自己也有两个准备开源的项目,但是日常工作基本上是crud,那两个项目不是crud的功能,不知道实际在外面看来质量咋样,也不太敢开源出来,怕被喷傻😀。
目前我还是个菜鸟,很想提升一下,虽然有时候觉得挺累的,到家都很晚了,但是还是要多学学,毕竟现在的环境太严峻了,而且发展又太快了。刚入行的时候大家和我说坚持开头三年,过了前三年就好了。入行三年之期马上到了今年这情况真的是太让人想象不到了,感觉一切都变了,大厂一个个都在精简配置,提升自己才是最重要的。
一晃毕业也快四年了,时间过的真的好快,每次看到掘金上一些大厂分享的训练营成果、应届生技术,自己三年经验都比不上他们,就会反思自己到底要怎么才能赶得上他们呢?不过现在的大环境也在告诉我们不抓紧跟上这个技术,我们会很容易被技术抛弃的,外面的人都很厉害,要跟上他们的脚步。期待这个大环境变好吧,让每个人都可以开心工作,而不是要为了生活天天担心工作。