跟我猜spring-boot:简单的HttpServer

Posted IT老拐瘦

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了跟我猜spring-boot:简单的HttpServer相关的知识,希望对你有一定的参考价值。

引&目标

本篇是《跟我猜Spring-boot》系列文章的第三篇,本拐做事向来随机随意,三分热血,有始无终,希望《猜》系列文章可以成为本拐第一个正常完结的系列。

在这个系列里,笔者将会通过简单实现Spring里各种功能和特性,试着拆解这些功能特性背后的实现机理。

我们最终靠“猜"实现的源码,肯定会与真正的工程化的东西差十万八千里。

但是通过这些猜出来的源码,希望各位与笔者一样,对设计模式,语言特性会有一些深层的认知。

废话不多说(明明已经说了那么。。。),在上一篇里,我们已经实现了简单的bean注入。

那么我们这一篇,将去实现一个简单的HttpServer. 这其中,所有的源码的改造都基于: 

https://github.com/yfge/mini-boot/archive/'article-02'.tar.gz

既然要实现HttpServer,对照Spring的一些功能,我们需要标注Controller以达到:

  1. 说明它是一个restController

  2. 标注方法

这样,改造后的 SimpleController如下:

SimpleController.java

 
   
   
 
  1. package com.github.yfge.miniapp;

  2. //省去import ..

  3. @RestController

  4. @RequestMapping(path="/")

  5. public class SimpleController {

  6. @Autowired

  7. private SimpleService simpleService;


  8. public SimpleController(){

  9. System.out.println("the controller is created!");

  10. }

  11. @PostConstruct

  12. public void init(){

  13. System.out.println("the service Id is :"+this.simpleService.getServiceId());

  14. }


  15. @RequestMapping(path="/hello",method = RequestMethod.GET)

  16. public String getHello(){

  17. return simpleService.getHelloMessage();

  18. }

  19. }

那么对应的,我们需要在 SimpleService上加一个 getHelloMessage()的实现:

 
   
   
 
  1. package com.github.yfge.miniapp;

  2. //略去import

  3. @Service

  4. public class SimpleService {


  5. private String serviceId ;

  6. public SimpleService(){

  7. serviceId= UUID.randomUUID().toString();

  8. }

  9. public String getServiceId(){

  10. return this.serviceId;

  11. }

  12. public String getHelloMessage(){

  13. StringBuilder builder = new StringBuilder();

  14. builder.append("Hello ,the Service is :")

  15. .append(this.serviceId);

  16. return builder.toString();

  17. }

  18. }

需求分析

由我们的改造目标可以看到,我们需要做到:

  1. 实现一个简单的HttpServer

  2. 定义我们需要实现的注解( RequestMappingRespnseBodyRestController)并实现挂载逻辑。

实现简单的HttpServer

JDK本身已经有了一个HttpServer,使用起来非常简单粗暴,类似于这个样子:

 
   
   
 
  1. try {

  2. server = HttpServer.create(new InetSocketAddress(8080),0);

  3. server.createContext("/",new ServerHandler());

  4. server.start();

  5. } catch (IOException e) {

  6. e.printStackTrace();

  7. }

为了让这个server与我们的简单框架结合到一起,我们显然要把它加到 Application.loadBeans结尾。

在这段代码里, ServerHandler是一个 HttpHandler简单实现,类似于下面这样:

 
   
   
 
  1. public class ServerHandler implements HttpHandler{

  2. @Override

  3. public void handle(HttpExchange exchange) throws IOException {

  4. String response = "hello world";

  5. exchange.sendResponseHeaders(200, 0);

  6. OutputStream os = exchange.getResponseBody();

  7. os.write(response.getBytes());

  8. os.close();

  9. }

  10. }

在这个简单实现里,我们已经能看到一个HttpServer的基本的结构了,但有一个问题,就是这个HttpServer其实是单线程的。为了验证个问题,可以加一个sleep来测试,这里就不再缀述了。

为了能让他并发执行,我们简单的加一个并发的机制。即引入一个 ThreadPool来进行执行请求,同时将原有的处理逻辑抽象成 ServiceRunnable的类。ServiceRunnable这个类名字听起来或许有些不那么靠谱,但是我们目前为止似乎只看到了这些,所以,更改后的 ServerHandler变成了下面的样子:

ServerHandler.java

 
   
   
 
  1. public class ServerHandler implements HttpHandler {

  2. private ThreadPoolExecutor poolExecutor ;

  3. public ServerHandler(){

  4. poolExecutor= (ThreadPoolExecutor) Executors.newCachedThreadPool();

  5. }

  6. @Override

  7. public void handle(HttpExchange exchange) throws IOException {

  8. poolExecutor.execute(new ServiceRunnable(exchange));

  9. }


  10. }

我们这个简单的 ServiceRunnable就成了如下样子:ServiceRunnable.java

 
   
   
 
  1. public class ServiceRunnable implements Runnable {

  2. private final HttpExchange exchange;

  3. public ServiceRunnable(HttpExchange exchange) {

  4. this.exchange = exchange;

  5. }

  6. @Override

  7. public void run() {

  8. try {

  9. exchange.sendResponseHeaders(200, 0);

  10. exchange.getResponseBody().write("hello".getBytes());

  11. exchange.getResponseBody().flush();

  12. exchange.getResponseBody().close();

  13. } catch (IOException e) {

  14. e.printStackTrace();

  15. }

  16. }

  17. }

OK,做了以上的改动,我们已经实现了一个简单的HttpServer,虽然它目前只会输出一个Hello,但是通过我们的一些设计,他实现了:

  1. 端口监听和服务启动

  2. 我们将处理请求的逻辑移到了 ServiceRunnable这个类中,意味着对Http的一些扩展性操作我们只要和 ServiceRunnable打交道就可以了。

定义注解

在实现了HttpServer以后,我们需要照着Spring的方式定义一系列的注解:

RequestMapping.java

 
   
   
 
  1. @Retention(RetentionPolicy.RUNTIME)

  2. @Target({ElementType.TYPE,ElementType.METHOD})

  3. public @interface RequestMapping {

  4. String[] path() default {};

  5. RequestMethod[] method() default {};

  6. }

ResponseBody.java

 
   
   
 
  1. @Retention(RetentionPolicy.RUNTIME)

  2. @Target({ElementType.METHOD,ElementType.TYPE})

  3. public @interface ResponseBody {

  4. }

RestController.java

 
   
   
 
  1. @Target(ElementType.TYPE)

  2. @Retention(RetentionPolicy.RUNTIME)

  3. @Service

  4. public @interface RestController {

  5. }

那么像之前一样,我们需要将这些注解解析,并得创建相应的bean. 这里面,重要的一点,我们的 SimpleController已经没有Service的注解,而只有了RestController这个注解。并且为了标明RestController也是一个bean,也要创建,我们在 RestController上加了 @Service这就意味着我们的Bean创建逻辑要更改,即,从创建是否含有 @Service的注解,到循环判断这个类的注解上是否也有@Service注解! 有一些绕是吧,其实我们只是需要对 Application.loadBean方法中下面这段做更改:

 
   
   
 
  1. for (String name : classNames) {

  2. try {

  3. var classInfo = Class.forName(name);

  4. /**

  5. * 检查是否声明了@Service

  6. **/

  7. if (classInfo.getDeclaredAnnotation(Service.class) != null) { // 这里变得不适用了。

  8. /**

  9. * 得到默认构造函数

  10. */

  11. var constructor = classInfo.getConstructor();

  12. if (constructor != null) {

  13. /**

  14. * 创建实例

  15. */

  16. var obj = constructor.newInstance();

  17. /** 保存bean**/

  18. applicationContext.addBean(obj);

  19. }

  20. }

  21. } catch (Throwable e) {

  22. e.printStackTrace();

  23. }

  24. }

将其中的判断抽取成一个方法,然后进行调用,如下:

 
   
   
 
  1. public class Application {



  2. /**

  3. * 检查一个类是否需要创建(是否是bean)

  4. * @param beanClass

  5. * @return

  6. */

  7. private static boolean isNeedToCreate(Class beanClass) {

  8. var annotations = beanClass.getDeclaredAnnotations();

  9. if (annotations != null && annotations.length > 0) {

  10. for (var annotation : annotations) {

  11. if (annotation.annotationType() == Service.class) {

  12. return true;

  13. } else {

  14. if(annotation.annotationType()!= Target.class && annotation.annotationType()!= Retention.class) {

  15. return isNeedToCreate(annotation.annotationType());

  16. }

  17. }

  18. }

  19. return false;

  20. }

  21. return false;

  22. }


  23. /**

  24. * 加载相应的bean(Service)

  25. *

  26. * @param source

  27. */

  28. private static void LoadBeans(Class source) {

  29. ClassUtils util = new ClassUtils();

  30. List<String> classNames = util.loadClass(source);

  31. /** 实例化一个context **/

  32. ApplicationContext applicationContext = new ApplicationContext();

  33. for (String name : classNames) {

  34. try {

  35. var classInfo = Class.forName(name);

  36. /**

  37. * 检查是否声明了@Service

  38. **/

  39. if (classInfo.isAnnotation() == false && isNeedToCreate(classInfo)) {

  40. /**

  41. * 得到默认构造函数

  42. */

  43. var constructor = classInfo.getConstructor();

  44. if (constructor != null) {

  45. /**

  46. * 创建实例

  47. */

  48. var obj = constructor.newInstance();

  49. /** 保存bean**/

  50. applicationContext.addBean(obj);

  51. }

  52. }

  53. } catch (Throwable e) {

  54. e.printStackTrace();

  55. }

  56. }

  57. // other code

  58. }

  59. }

  1. 将标有 @RestController的Bean取出。

OK 按照我们的思路,第一步很容易,第二步呢,我们需要一个Map来进行存储。这个Map应该是一个K-V结构。即 URL-HTTP方法-类方法这里面,为了方便管理,我们直接将三类信息封装成一个类 UrlMappingInfo,同时,为了保证方法可以正常的调用,我们需要把对应的bean也传入。

UrlMappingInfo.java

 
   
   
 
  1. public class UrlMappingInfo {

  2. private String url;

  3. private RequestMethod[] requestMethods;

  4. private Method method;

  5. private Object bean;

  6. public UrlMappingInfo(String url,RequestMethod[] requestMethods ,Method method,Object bean){

  7. this.url=url;

  8. this.requestMethods = requestMethods;

  9. this.method = method;

  10. this.bean=bean;

  11. }


  12. public RequestMethod[] getRequestMethods() {

  13. return requestMethods;

  14. }

  15. public Method getMethod(){

  16. return method;

  17. }

  18. public String getUrl(){

  19. return url;

  20. }

  21. public Object getBean(){

  22. return bean;

  23. }

  24. }

 
   
   
 
  1. public class Application {

  2. /**

  3. * 加载相应的bean(Service)

  4. *

  5. * @param source

  6. */

  7. private static void LoadBeans(Class source) {

  8. // other code

  9. Map<String ,UrlMappingInfo> urlMappingInfoMap = new LinkedHashMap<>();

  10. /** 实例化一个context **/

  11. /** 注入bean **/

  12. /** 执行初始化方法 **/

  13. for(Object ob :applicationContext.getAllBeans()){

  14. var classInfo = ob.getClass();

  15. if(classInfo.getDeclaredAnnotation(RestController.class)!=null){

  16. var requestMapping = classInfo.getDeclaredAnnotation(RequestMapping.class);

  17. String[] baseUrl={};

  18. if(requestMapping!=null){

  19. baseUrl = requestMapping.path();

  20. }

  21. for(var method:classInfo.getDeclaredMethods()){

  22. var methodRequestMapping = method.getDeclaredAnnotation(RequestMapping.class);

  23. if(methodRequestMapping!=null){

  24. String[] subUrl = methodRequestMapping.path();

  25. for (var base: baseUrl

  26. ) {

  27. for(var sub:subUrl){

  28. String url = base+sub;

  29. urlMappingInfoMap.put(url,new UrlMappingInfo(url,methodRequestMapping.method(), method,ob));

  30. }

  31. }

  32. }

  33. }

  34. }

  35. }

  36. //

  37. HttpServer server = null;

  38. try {

  39. server = HttpServer.create(new InetSocketAddress(8080), 0);

  40. server.createContext("/", new ServerHandler(urlMappingInfoMap));

  41. server.start();

  42. } catch (IOException e) {

  43. e.printStackTrace();

  44. }


  45. }

  46. }

ServerHandler.java

 
   
   
 
  1. public class ServerHandler implements HttpHandler {

  2. private final Map<String, UrlMappingInfo> urlMappingInfoMap;

  3. private ThreadPoolExecutor poolExecutor ;

  4. public ServerHandler(Map<String, UrlMappingInfo> urlMappingInfoMap){

  5. this.urlMappingInfoMap = urlMappingInfoMap;

  6. poolExecutor= (ThreadPoolExecutor) Executors.newCachedThreadPool();

  7. }

  8. @Override

  9. public void handle(HttpExchange exchange) throws IOException {

  10. poolExecutor.execute(new ServerRunnable(exchange,urlMappingInfoMap));

  11. }

  12. }

可以看到,这里,我们已经同时将urlMappingInfoMap这个结构传到了ServerRunnable中,那么更改ServerRunnable就变得简单起来。ServerRunnable.java

 
   
   
 
  1. public class ServerRunnable implements Runnable {


  2. private final HttpExchange exchange;

  3. private final Map<String, UrlMappingInfo> urlMappingInfoMap;


  4. public ServerRunnable(HttpExchange exchange, Map<String, UrlMappingInfo> urlMappingInfoMap) {

  5. this.urlMappingInfoMap = urlMappingInfoMap;

  6. this.exchange = exchange;

  7. }


  8. @Override

  9. public void run() {

  10. try {

  11. String url = this.exchange.getRequestURI().getPath();

  12. String method = this.exchange.getRequestMethod();

  13. System.out.println(method + " " + url);


  14. try {

  15. var mappingInfo = this.urlMappingInfoMap.getOrDefault(url, null);

  16. if (mappingInfo == null) {

  17. exchange.sendResponseHeaders(404, 0);

  18. exchange.getResponseBody().write("Error Not Found.".getBytes());

  19. } else {

  20. boolean isAllowed = false;

  21. for (var allowedMethod : mappingInfo.getRequestMethods()) {

  22. if (allowedMethod.toString().equals(method)) {

  23. var ob = mappingInfo.getMethod().invoke(mappingInfo.getBean());

  24. exchange.sendResponseHeaders(200, 0);

  25. exchange.getResponseBody().write(ob.toString().getBytes());

  26. isAllowed=true;

  27. break;

  28. }

  29. }

  30. if(isAllowed ==false){

  31. exchange.sendResponseHeaders(405,0);

  32. exchange.getResponseBody().write("not allowed!".getBytes());

  33. }

  34. }

  35. }catch (Throwable e){

  36. exchange.sendResponseHeaders(500,0);

  37. exchange.getResponseBody().write("internal error".getBytes());

  38. }

  39. exchange.getResponseBody().flush();

  40. exchange.getResponseBody().close();

  41. } catch (IOException e) {

  42. e.printStackTrace();

  43. }

  44. }

  45. }

现在编译运行整个程序. 你可以看到如下输出:

 
   
   
 
  1. the controller is created!

  2. the service :fa68fc94-933b-4ae6-b65b-4f2814dad76a is created!

  3. /hello is Mapping

  4. The Mini-Boot Application Is Run! The Name is Hello

做一些测试

 
   
   
 
  1. curl localhost:8080/hello

  2. Hello ,the Service is :fa68fc94-933b-4ae6-b65b-4f2814dad76a


  3. curl localhost:8080/hello1

  4. Error Not Found.


  5. curl -X POST localhost:8080/hello

  6. not allowed!

到现在为止,我们已经:

  1. 实现了简单的HttpServer

  2. 成功模拟了RestController和RequestMapping的功能

但是,我们代码,总是感觉有一些不对劲?

  1. Application.loadBean太复杂,太长了,重复的代码很多!

  2. 我已经有了ApplicationContext来管理Bean,为什么还要把bean包裹在UrlMap里一层一层传递?

  3. ServerRunnable那个类实现的实在丑了(丑出天际!!)有没有办法优雅一点?

OK,那么针这些问题,我们将在下一篇文章,对代码进行第一次重构和优化,顺带检测我们在设计和思考的过程中遗漏的地方。

其他

不给源码的分享都是耍流氓!

本来计划今天直接把servlet撸定。。但是似乎。。有点深,所以servlet的东西会单独放到一篇去写。

另外,由于面向功能的代码写起来实在是太丑了,所以准备先单独写一篇文档,去重构一些东西,以理清一些思路,算是我自己的一个整理吧。希望对您也是有用的。


参考

  1. 使用Java内置的Http Server构建Web应用 :


关于老拐瘦

  • 散养程序猿,野生架构狮

  • 二流搬砖工,三流摄影师

  • 假正经真逗比,装文艺实二逼

啥也不说,扫码关注吧




以上是关于跟我猜spring-boot:简单的HttpServer的主要内容,如果未能解决你的问题,请参考以下文章

rest-assured : Restful API 测试利器 - 真正的黑盒单元测试(跟Spring-Boot更配哦)

spring-boot之简单定时任务

Spring-boot简单的理解

明哥报错簿之 "javax.servlet.http.HttpServlet" was not found on the Java Build Path || HttpSer

spring-boot框架下的websocket服务

使用 Spring-Boot 创建一个简单的 JAR 而不是可执行的 JAR