NiceLeeのBlog 用爱发电 bilibili~

一个弱鸡应用的诞生(2)-处理都在Controller的弱鸡服务器

2019-09-09
nIceLee

阅读:


前面已经分析了,要实现的是一个Java Web应用。
但由于是dalvikvm环境,服务器需要自己去搭建。
再加上想向可拓展方面靠,于是仿SpringBoot,一个处理都在Controller的弱鸡服务器诞生了(其实是想吹爆的,可惜不够强。

前言

做到了什么

  • 举个例子 Hello World
    @Controller(path = "/test", note = "在线设备状态相关")
    public class ControllerHelloWorld {
      @Controller(path = "/hello", note="传参+返回结果测试")
      public String hello(@Value(key = "param") String param, @Value(key = "_t") String time) {
          System.out.println(param);
          System.out.println(time);
          return "Hello World";
      }    
      @Controller(path = "/hello2", note="返回结果测试2")
      public String hello(BufferedWriter out) {
          out.write("Hello World");
          return null;
      }    
      @Controller(path = "/hello3", note="返回结果测试3")
      public String hello(BufferedWriter out) {
          out.write("<html>");
          out.write("<head><title>Index</title></head>");
          out.write("<body><h2>Hello World</h2></body>");
          out.write("</html>");
          return null;
      }
    }
    
    • 假设要处理/test/hello路径的请求,我们可以仿照SpringBoot创建相应类+方法,注意@Controller注解
    • 对于传参问题,需要加上@Value注解,处理时会自动赋值。若path里面不含对应参数,默认指向null
        http://192.168.0.101:8888/test/hello?param=test  hello方法里,param=="test",time==null; 
        http://192.168.0.101:8888/test/hello?param=test&_t=123 hello方法里,param=="test",time=="123";
      
    • 对于返回结果,举例
        /test/hello ==> "Hello World"
        /test/hello2 ==> "Hello World"
        即浏览器直接访问会收到并显示文本Hello World
        /test/hello3 ==> 浏览器直接访问会显示Hello World
      

怎么做到的

SocketServer监听TCP端口

HTTP基于TCP协议,所以直接监听TCP端口就行了,重要的是怎么处理

public class SocketServer {	
	int portServerListening; // 监听端口
	boolean isRun = true;
	ExecutorService httpThreadPool; // Socket处理线程池
	ServerSocket serverSocket;	
	public SocketServer(int portServerListening) {
		this.portServerListening = portServerListening;
		httpThreadPool = Executors.newFixedThreadPool(20);
	}	
	/**
	 *  关闭服务器
	 */
	public void stopServer() {
		try {
			serverSocket.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
		System.out.println("正在关闭 SocketServer: 服务器... ");
	}	
	/**
	 *  打开服务器
	 */
	public void startServer() {
		Socket socket = null;
		System.out.println("SocketServer: 服务器监听开始... ");
		try {
			serverSocket = new ServerSocket(portServerListening);
            while (isRun) {
				try {
					socket = serverSocket.accept();
				}catch (SocketTimeoutException e) {
					continue;
				}catch (SocketException e) {
					break;
				}
				//System.out.println("收到新连接: " + socket.getInetAddress() + ":" + socket.getPort());
				SocketDealer dealer = new SocketDealer(socket);
				httpThreadPool.execute(dealer);
			}
			httpThreadPool.shutdownNow();
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			...
		}
		System.out.println("SocketServer: 服务器已经关闭... ");
	}
}

对于Socket连接的处理

其实就是利用反射,不用写死,便于拓展。

  • 获取Socket的io流in/out
  • 参考HTTP协议,通过输入in获取URL,解析出path、params参数字符串(URL?号后面那一坨)
  • 遍历@Controller注解类的@Controller注解方法,获得匹配path的class和method
  • 根据Method的相关参数注解,配合params参数字符串生成方法的各种参数
  • 实例化匹配的class对象,并传入参数,执行method方法,并得到结果
  • 根据结果输出out

以下是Socket线程处理的run方法

public void run() {
    String path = null, param = null;
    try {
        in = new BufferedReader(new InputStreamReader(socketClient.getInputStream()));
        out = new BufferedWriter(new OutputStreamWriter(socketClient.getOutputStream()));
        
        // 读取url请求, 得到path + param
        String line = null;
        while ((line = in.readLine()) != null) {
            Pattern urlPattern = Pattern.compile("^GET ([^ \\?]+)\\??([^ \\?]*) HTTP.*$");
            Matcher matcher = urlPattern.matcher(line);
            if(path == null && matcher.find()) {
                System.out.println("正在处理请求: " + line);
                path = matcher.group(1);
                param = matcher.group(2);
            }
            if(line.length() == 0)
                break;
        }
        
        // 返回结果        
        out.write("HTTP/1.1 200 OK\r\n");
        out.write("Content-Type: text/html; charset=UTF-8\r\n");
        out.write("\r\n");
        // 处理请求并返回内容
        // 遍历Controller类,得到和Path匹配的处理方法, 目前仅一个Class
        Method currentMethod = null;
        Class<?> klass = Class.forName("nicelee.server.controller.ControllerOnlinerFinder");
        Controller preAnno = klass.getAnnotation(Controller.class);
        String pathPrefix = preAnno.path();
        for(Method method: klass.getMethods()) {
            Controller controller = method.getAnnotation(Controller.class);
            if(controller!=null && (pathPrefix + controller.path()).equals(path)) {
                currentMethod = method;
                break;
            }
        }
        // 找到Method方法后,根据param给Method变量赋值
        if(currentMethod != null) {
            dealWithPathKnown(param, currentMethod, klass);
        }else {
            dealWithPathUnknown(klass);
        }
        out.write("\r\n");
        out.flush();
        
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        ...
    }
}

上接run方法,自动生成参数、实例化对象并调用处理方法:

private void dealWithPathKnown(String param, Method currentMethod, Class<?> klass) throws Exception {
    Annotation[][] paramAnnos = currentMethod.getParameterAnnotations();
    Class<?>[] paramTypes = currentMethod.getParameterTypes();
    Object[] values = new Object[paramTypes.length];
    for(int i=0; i<paramTypes.length; i++) {
        // 如果是BufferedWriter,那么直接赋值,否则从params中找
        if(paramTypes[i] == BufferedWriter.class) {
            values[i] = out;
        }else {
            if(paramAnnos[i].length > 0) {
                Value value = (Value) paramAnnos[i][0];
                values[i] = getValue(param, value.key());
            }
        }
    }
    // 实例化Controller,并执行该方法
    String result = (String) currentMethod.invoke(klass.newInstance(), values);
    if(result!=null) {
        out.write(result);
    }
}
/**
 * 从参数字符串中取出值 "key1=value1&key2=value2 ..."
 * 
 * @param param
 * @param key
 * @return
 */
private static String getValue(String param, String key) {
    Pattern pattern = Pattern.compile(key + "=([^&]*)");
    Matcher matcher = pattern.matcher(param);
    if (matcher.find()) {
        return matcher.group(1);
    }
    return null;
}

有哪些缺陷

  • 逻辑比较简单,没能考虑复杂的情况,比如:
    • 只考虑简单的GET,没有考虑POST、PUT等,全都当作GET对待
    • 方法参数自动赋值目前只考虑String类型
      其它也不是不能自动适配,但暂时没这需求…
    • 这里方法的@Controller和SpringBoot的返回json的@RequestBody有点类似。
      缺少返回return作为模板引擎路径,然后自动渲染结果的实现
    • 由于dalvikvm环境下包扫描机制没想好咋解决,目前只能一个个Class.forName(“nicelee.xxx”)来加载。
      这样一来需要人工维护一张class的信息表,不过工作量不大

内容
隐藏