如何通过协议id找到(调用)方法入口
如果前端传来一个请求,服务端如何识别这个请求,找到对应的要执行的方法?
一直觉得这个效果很神奇,自己动手实现一下。
一、问题的简单分析
(1)servlet
最开始学习java web的时候,我用servlet写过一些小项目。一个servlet类extends HttpServlet父类后,可以实现doPost/doGet方法,接收http请求。画风大概是这个样子的:
Server.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
package test; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @WebServlet("/server") public class Server extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // do something } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // do something } } |
如果用post/get的方式访问/server这个路径,就会自动进到server类的doPost/doGet方法,执行相应的操作。
按照上面的规则,如果要实现登录接口,我就配一个/login路径,写一个login类。如果要实现注册接口,我就要配一个/register路径,写一个register类。
那么问题来了:如果我有一大堆请求,岂不是要写一大堆servlet类,配上一大堆路径?我的项目还要不要管理了…
于是我们想到了一个办法,只配一个/server路径,写一个server类。至于请求想调用哪个方法,由请求带进来的method参数来决定。
Server.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
package test; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @WebServlet("/server") public class server extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String method = req.getParameter("method"); resp.setContentType("text/html;charset=UTF-8"); if (method.equals("login")) { ...... } if(method.equals("getusername")){ ...... } if (method.equals("checkregistername")) { ...... } if (method.equals("checkregisteremail")) { ...... } } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String method = req.getParameter("method"); resp.setContentType("text/html;charset=UTF-8"); if (method.equals("yanzhen")) { ...... } if (method.equals("confirmpassword")) { ...... } } } |
如果请求是post方法并且method为login,就会匹配上doPost方法中的
1 |
if (method.equals("login")) |
执行登录方法。
看上去是解决了,但是问题又来了:如果写的api越来越多,把逻辑全写在这个Server.java里面,这个类岂不是很臃肿?
你可能会说:那我自己写几个service类总行了吧。Server.java只留下if else和方法调用,具体逻辑全搬到service类里面,不就得了吗。
但是,要是api很多很多,Server.java里面可能会堆积一大堆if else,这个类还不是一样臃肿吗?
你可能会说:你嫌Server.java臃肿是吧,我写多几个servlet类不就行了!我要写一个Server2.java,不够再加Server3.java、Server4.java…
不得不说这个思路确实不错,一定程度上能解决类过于臃肿的问题。
但是问题又来了:servlet的入口是可以自己定义的,一个不够我们可以写多几个。但是,如果使用Netty,入口类只有那一个Handler,我们怎么办?
(2)Netty中的Handler
拿这篇文章的一个Handler做个例子:
http://www.xie4ever.com/2017/07/22/%E4%BD%BF%E7%94%A8netty%E5%AE%9E%E7%8E%B0%E7%AE%80%E5%8D%95%E7%9A%84http%E6%9C%8D%E5%8A%A1%E5%99%A8/
HttpHelloWorldServerHandler.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
package com.xie.Netty.HelloWorld; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpHeaders.Values; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpVersion; public class HttpHelloWorldServerHandler extends ChannelInboundHandlerAdapter { @Override public void channelReadComplete(ChannelHandlerContext ctx) { ctx.flush(); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { // do something } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } } |
我们只能把逻辑都写在channelRead方法中。如果要处理的方法多了,这个HttpHelloWorldServerHandler类又会变得臃肿。但是这次,我们无法写多几个Handler帮助分担责任,上面的方法行不通了!
我们再想想,HttpHelloWorldServerHandler类的作用是什么?个人认为,比起处理请求,这个类更像是一个请求入口。对于请求的处理,我们应该写在另外一个Service层中,而不是写在Handler层中。换言之,Handler层中不应该出现具体业务的逻辑代码。
到这里其实就是设计模式的范畴了,我将把重点放在如何解决上面的问题。
(顺带一提,写代码要注意SOLID原则。个人认为,设计模式就是该原则的参考实现,用于帮助我们写出合理的代码。具体可以参考:https://www.cnblogs.com/lanxuezaipiao/archive/2013/06/09/3128665.html)
二、我的解决方案
(1)初步解决方案
用注解配合反射的方式,分离入口类的Service层的逻辑实现。
首先写上一个注解,用来修饰要调用的方法,方便反射找到。
FindMethod.java
1 2 3 4 5 6 7 8 9 10 11 12 13 |
package com.test.getMethodById; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.FIELD, ElementType.METHOD }) public @interface FindMethod { public int methodId() default 0; } |
写好要调用的方法,加上注解,约定方法的id(Service层)。
Methods.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
package com.test.getMethodById; public class Methods { @FindMethod(methodId = 1) public void testGetMethod1() { System.out.println("testGetMethod1"); } @FindMethod(methodId = 2) public void testGetMethod2() { System.out.println("testGetMethod2"); } } |
接下来要在入口类中写好反射,通过id对方法进行调用。我用一个main方法模拟一下入口类:
Main.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
package com.test.getMethodById; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class Main { public static void main(String[] args) { int id = 1; Class clazz = null; Object object = null; try { clazz = Class.forName("com.test.getMethodById.Methods"); object = clazz.newInstance(); Method[] methods = clazz.getMethods(); for (Method method : methods) { Annotation[] annotations = method.getAnnotations(); for (Annotation annotation : annotations) { if (annotation.annotationType().equals(FindMethod.class)) { FindMethod findMethod = (FindMethod) annotation; if (findMethod.methodId() == id) { method.invoke(object, null); } } } } } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (InstantiationException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IllegalAccessException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IllegalArgumentException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (InvocationTargetException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } |
现在id为1,即调用注解id为1的testGetMethod1方法,输出结果。运行结果为:
1 |
testGetMethod1 |
(2)拓展Service类
到了这里,你可能会问:这算什么优化嘛!如果要处理的方法多了,这个Methods类还不是会变得臃肿吗?
个人认为:写多一个Methods类不就好了吗…在反射的部分稍微改动一下就行了。
Main.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
package com.test.getMethodById; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class Main { public static void main(String[] args) { int id = 1; Class clazz1 = null; Class clazz2 = null; Object object = null; try { clazz1 = Class.forName("com.test.getMethodById.Methods1"); clazz2 = Class.forName("com.test.getMethodById.Methods2"); Class[] classes = new Class[] { clazz1, clazz2 }; boolean isFinded = false; for (Class clazz : classes) { object = clazz.newInstance(); Method[] methods = clazz.getMethods(); for (Method method : methods) { Annotation[] annotations = method.getAnnotations(); for (Annotation annotation : annotations) { if (annotation.annotationType().equals(FindMethod.class)) { FindMethod findMethod = (FindMethod) annotation; if (findMethod.methodId() == id) { method.invoke(object, null); isFinded = true; break; } } } if (isFinded == true) { break; } } if (isFinded == true) { break; } } } catch ( ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (InstantiationException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IllegalAccessException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IllegalArgumentException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (InvocationTargetException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } |
要注意的是:找到相应的方法就可以停止遍历,及时break结束循环。
(3)快速找到id对应的方法
上面的程序有个问题,如果Service类多了,可能要用反射遍历一大堆Service类。
举个例子:一共有10个service类(service1到service10),我要调用的方法在service10中,但是反射只能一个个遍历,从service1找到service10,最后才找到,效率太低了。
除此之外,反射的速度本来就比直接调用要慢很多,如果要遍历的方法很多,就会慢上加慢,速度将令人难以接受!
(至于反射为什么慢,可以参考我以前的总结《java反射为什么慢?》)
http://www.xie4ever.com/2016/12/09/java%E5%8F%8D%E5%B0%84%E4%B8%BA%E4%BB%80%E4%B9%88%E6%85%A2%EF%BC%9F/
其实这个问题特别容易解决。我想要通过调用的id找到该id所在的类,映射关系为:调用方法的id -> 方法所在的全类名。
那我加个缓存不就好了吗?只要访问过一次,下次访问速度就会显著变快。写个全局的ConcurrentHashMap应该就能解决问题了。
1.错误示范
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
package com.xie.Netty.HelloWorld; import java.util.concurrent.ConcurrentHashMap; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpHeaders.Values; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.QueryStringDecoder; public class HttpHelloWorldServerHandler extends ChannelInboundHandlerAdapter { private static final byte[] CONTENT = { 'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd' }; private ConcurrentHashMap<String, String> cache = new ConcurrentHashMap<>(); @Override public void channelReadComplete(ChannelHandlerContext ctx) { ctx.flush(); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { if (msg instanceof HttpRequest) { HttpRequest req = (HttpRequest) msg; if (req.getUri().equals("/favicon.ico")) { return; } QueryStringDecoder decoder = new QueryStringDecoder(req.uri()); decoder.parameters().entrySet().forEach(entry -> { cache.put(entry.getKey(), entry.getValue().get(0)); }); System.out.println("cache:" + cache.toString()); if (HttpHeaders.is100ContinueExpected(req)) { ctx.write(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE)); } boolean keepAlive = HttpHeaders.isKeepAlive(req); FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.wrappedBuffer(CONTENT)); response.headers().set("CONTENT_TYPE", "text/plain"); response.headers().set("CONTENT_LENGTH", response.content().readableBytes()); if (!keepAlive) { ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); } else { response.headers().set("CONNECTION", Values.KEEP_ALIVE); ctx.write(response); } } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } } |
这样写是不行的,Handler中的ConcurrentHashMap不是全局对象,只在当前线程内(体现为每条链接都有独立的缓存,没有缓存到一起)。还是要老老实实写个单例模式。
2.把缓存写成单例模式
Cache.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
package com.xie.Netty.HelloWorld; import java.util.concurrent.ConcurrentHashMap; public class Cache { private static ConcurrentHashMap<Integer, String> concurrentHashMap; private Cache() { // TODO Auto-generated constructor stub } static { concurrentHashMap = new ConcurrentHashMap<>(); } public static void setCache(Integer key, String value) { concurrentHashMap.put(key, value); } public static ConcurrentHashMap<Integer, String> getCache() { return concurrentHashMap; } } |
再使用QueryStringDecoder解析一下请求的参数内容:
HttpHelloWorldServerHandler.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
package com.xie.Netty.HelloWorld; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpHeaders.Values; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.QueryStringDecoder; public class HttpHelloWorldServerHandler extends ChannelInboundHandlerAdapter { private static final byte[] CONTENT = { 'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd' }; private ConcurrentHashMap<String, List<String>> cache = new ConcurrentHashMap<>(); @Override public void channelReadComplete(ChannelHandlerContext ctx) { ctx.flush(); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { if (msg instanceof HttpRequest) { HttpRequest req = (HttpRequest) msg; if (req.getUri().equals("/favicon.ico")) { return; } QueryStringDecoder decoder = new QueryStringDecoder(req.uri()); decoder.parameters().entrySet().forEach(entry -> { cache.put(entry.getKey() + entry.getValue().get(0), entry.getValue()); }); System.out.println("cache:" + cache.toString()); if (HttpHeaders.is100ContinueExpected(req)) { ctx.write(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE)); } boolean keepAlive = HttpHeaders.isKeepAlive(req); FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.wrappedBuffer(CONTENT)); response.headers().set("CONTENT_TYPE", "text/plain"); response.headers().set("CONTENT_LENGTH", response.content().readableBytes()); if (!keepAlive) { ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); } else { response.headers().set("CONNECTION", Values.KEEP_ALIVE); ctx.write(response); } } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } } |
这样我们就实现了一个全局的缓存。
这仅仅是一个示例,自行替换为:方法的id -> 方法所在的全类名即可。可以通过id快速找到方法所在的全类名,加快调用速度。
3.技巧
写一个类专门存放id的静态变量:
MsgHead.java
1 2 3 4 5 6 7 8 9 10 |
package com.xie.Netty.HelloWorld; public class MsgHead { public static final int MAIMAIMAI_ACTIVITY = 10000; private static final int CHONGCHONGCHONG_ACTIVITY = 10001; ...... } |
凡是涉及id的地方都替换为MAIMAIMAI_ACTIVITY之类的静态变量。在debug时,可以直接ctrl + alt + h这个静态变量,找到方法id相关的所有方法。
三、总结
算是java反射的小应用吧,属于技巧性的问题。