从零开始实现一个简易的Java MVC框架(八)–制作Starter

spring-boot的Starter

一个项目总是要有一个启动的地方,当项目部署在tomcat中的时候,经常就会用tomcat的startup.sh(startup.bat)的启动脚本来启动web项目

而在spring-boot的web项目中基本会有类似于这样子的启动代码:

@SpringBootApplication public class SpringBootDemoApplication {     public static void main(String[] args) {         SpringApplication.run(SpringBootDemoApplication.class, args);     } }

这个方法实际上会调用spring-boot的SpringApplication类的一个run方法:

public ConfigurableApplicationContext run(String... args) {     StopWatch stopWatch = new StopWatch();     stopWatch.start();     ConfigurableApplicationContext context = null;     Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();     configureHeadlessProperty();     SpringApplicationRunListeners listeners = getRunListeners(args);     listeners.starting();     try {         // 1.加载环境变量、参数等         ApplicationArguments applicationArguments = new DefaultApplicationArguments(             args);         ConfigurableEnvironment environment = prepareEnvironment(listeners,                                                                  applicationArguments);         configureIgnoreBeanInfo(environment);         Banner printedBanner = printBanner(environment);         // 2.加载Bean(IOC、AOP)等         context = createApplicationContext();         exceptionReporters = getSpringFactoriesInstances(             SpringBootExceptionReporter.class,             new Class[] { ConfigurableApplicationContext.class }, context);         prepareContext(context, environment, listeners, applicationArguments,                        printedBanner);         //会调用一个AbstractApplicationContext@refresh()方法,主要就是在这里加载Bean,方法的最后还会启动服务器         refreshContext(context);         afterRefresh(context, applicationArguments);         stopWatch.stop();         if (this.logStartupInfo) {             new StartupInfoLogger(this.mainApplicationClass)                 .logStarted(getApplicationLog(), stopWatch);         }         listeners.started(context);         callRunners(context, applicationArguments);     }     catch (Throwable ex) {         handleRunFailure(context, ex, exceptionReporters, listeners);         throw new IllegalStateException(ex);     }      try {         listeners.running(context);     }     catch (Throwable ex) {         handleRunFailure(context, ex, exceptionReporters, null);         throw new IllegalStateException(ex);     }     return context; }

这段代码还是比较长的,不过实际上主要就做了两个事情:1.加载环境变量、参数等 2.加载Bean(IOC、AOP)等。3.如果获得的ApplicationContextServletWebServerApplicationContext,那么在refresh()之后会启动服务器,默认的就是tomcat服务器。

我觉得spring-boot启动器算是spring-boot中相对来说代码清晰易懂的,同时也非常容易了解到整个spring-boot的流程结构,建议大家能够去看一下。

实现Starter

了解到spring-boot的启动器的作用和原理之后,我们可以开始实现doodle的启动器了。

根据刚才提到的,启动器要做以下几件事

  1. 加载一些参数变量
  2. 加载Bean(IOC、AOP)等工作
  3. 启动服务器

Configuration保存变量

在com.zbw包下创建类Configuration用于保存一些全局变量,目前这个类只保存了现在实现的功能所需的变量。

package com.zbw; import ...  /**  * 服务器相关配置  */ @Builder @Getter public class Configuration {      /**      * 启动类      */     private Class<?> bootClass;      /**      * 资源目录      */     @Builder.Default     private String resourcePath = "src/main/resources/";      /**      * jsp目录      */     @Builder.Default     private String viewPath = "/templates/";      /**      * 静态文件目录      */     @Builder.Default     private String assetPath = "/static/";      /**      * 端口号      */     @Builder.Default     private int serverPort = 9090;      /**      * tomcat docBase目录      */     @Builder.Default     private String docBase = "";      /**      * tomcat contextPath目录      */     @Builder.Default     private String contextPath = ""; }

实现内嵌Tomcat服务器

在上一章文章从零开始实现一个简易的Java MVC框架(七)--实现MVC已经在pom.xml文件中引入了tomcat-embed依赖,所以这里就不用引用了。

先在com.zbw.mvc下创建一个包server,然后再server包下创建一个接口Server

package com.zbw.mvc.server;  /**  * 服务器 interface  */ public interface Server {     /**      * 启动服务器      */     void startServer() throws Exception;      /**      * 停止服务器      */     void stopServer() throws Exception; } 

因为服务器有很多种,虽然现在只用tomcat,但是为了方便扩展和修改,就先创建一个通用的server接口,每个服务器都要实现这个接口。

接下来就创建TomcatServer类,这个类实现Server

package com.zbw.mvc.server; import ...  /**  * Tomcat 服务器  */ @Slf4j public class TomcatServer implements Server {      private Tomcat tomcat;      public TomcatServer() {         new TomcatServer(Doodle.getConfiguration());     }      public TomcatServer(Configuration configuration) {         try {             this.tomcat = new Tomcat();             tomcat.setBaseDir(configuration.getDocBase());             tomcat.setPort(configuration.getServerPort());              File root = getRootFolder();             File webContentFolder = new File(root.getAbsolutePath(), configuration.getResourcePath());             if (!webContentFolder.exists()) {                 webContentFolder = Files.createTempDirectory("default-doc-base").toFile();             }              log.info("Tomcat:configuring app with basedir: [{}]", webContentFolder.getAbsolutePath());             StandardContext ctx = (StandardContext) tomcat.addWebapp(configuration.getContextPath(), webContentFolder.getAbsolutePath());             ctx.setParentClassLoader(this.getClass().getClassLoader());              WebResourceRoot resources = new StandardRoot(ctx);             ctx.setResources(resources);             // 添加jspServlet,defaultServlet和自己实现的dispatcherServlet             tomcat.addServlet("", "jspServlet", new JspServlet()).setLoadOnStartup(3);             tomcat.addServlet("", "defaultServlet", new DefaultServlet()).setLoadOnStartup(1);             tomcat.addServlet("", "dispatcherServlet", new DispatcherServlet()).setLoadOnStartup(0);             ctx.addServletMappingDecoded("/templates/" + "*", "jspServlet");             ctx.addServletMappingDecoded("/static/" + "*", "defaultServlet");             ctx.addServletMappingDecoded("/*", "dispatcherServlet");             ctx.addServletMappingDecoded("/*", "dispatcherServlet");         } catch (Exception e) {             log.error("初始化Tomcat失败", e);             throw new RuntimeException(e);         }     }      @Override     public void startServer() throws Exception {         tomcat.start();         String address = tomcat.getServer().getAddress();         int port = tomcat.getConnector().getPort();         log.info("local address: http://{}:{}", address, port);         tomcat.getServer().await();     }      @Override     public void stopServer() throws Exception {         tomcat.stop();     }      private File getRootFolder() {         try {             File root;             String runningJarPath = this.getClass().getProtectionDomain().getCodeSource().getLocation().toURI().getPath().replaceAll("\", "/");             int lastIndexOf = runningJarPath.lastIndexOf("/target/");             if (lastIndexOf < 0) {                 root = new File("");             } else {                 root = new File(runningJarPath.substring(0, lastIndexOf));             }             log.info("Tomcat:application resolved root folder: [{}]", root.getAbsolutePath());             return root;         } catch (URISyntaxException ex) {             throw new RuntimeException(ex);         }     } } 

这个类主要就是配置tomcat,和配置普通的外部tomcat有点类似只是这里是用代码的方式。注意的是在getRootFolder()方法中获取的是当前项目目录下的target文件夹,即idea默认的编译文件保存的位置,如果修改了编译文件保存位置,这里也要修改。

特别值得一提的是这部分代码:

// 添加jspServlet,defaultServlet和自己实现的dispatcherServlet tomcat.addServlet("", "jspServlet", new JspServlet()).setLoadOnStartup(3); tomcat.addServlet("", "defaultServlet", new DefaultServlet()).setLoadOnStartup(1); tomcat.addServlet("", "dispatcherServlet", new DispatcherServlet()).setLoadOnStartup(0); ctx.addServletMappingDecoded("/templates/" + "*", "jspServlet"); ctx.addServletMappingDecoded("/static/" + "*", "defaultServlet"); ctx.addServletMappingDecoded("/*", "dispatcherServlet"); ctx.addServletMappingDecoded("/*", "dispatcherServlet");

这部分代码就相当于原来的web.xml配置的文件,而且defaultServletjspServlet这两个servlet是tomcat内置的servlet,前者用于处理静态资源如css、js文件等,后者用于处理jsp。如果有安装tomcat可以去tomcat目录下的conf文件夹里有个web.xml文件,里面有几行就是配置defaultServletjspServlet

<servlet>     <servlet-name>default</servlet-name>     <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>     <init-param>         <param-name>debug</param-name>         <param-value>0</param-value>     </init-param>     <init-param>         <param-name>listings</param-name>         <param-value>false</param-value>     </init-param>     <load-on-startup>1</load-on-startup> </servlet> <servlet>     <servlet-name>jsp</servlet-name>     <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>     <init-param>         <param-name>fork</param-name>         <param-value>false</param-value>     </init-param>     <init-param>         <param-name>xpoweredBy</param-name>         <param-value>false</param-value>     </init-param>     <load-on-startup>3</load-on-startup> </servlet>

而dispatcherServlet就是从零开始实现一个简易的Java MVC框架(七)--实现MVC这一节中实现的分发器。这三个servlet都设置了LoadOnStartup,当这个值大于等于0时就会随tomcat启动也实例化。

实现启动器类

在com.zbw包下创建一个类作为启动器类,就是类似于SpringApplication这样的。这里起名叫做Doodle,因为这个框架就叫doodle嘛。

package com.zbw; import ...  /**  * Doodle Starter  */ @NoArgsConstructor(access = AccessLevel.PRIVATE) @Slf4j public final class Doodle {      /**      * 全局配置      */     @Getter     private static Configuration configuration = Configuration.builder().build();      /**      * 默认服务器      */     @Getter     private static Server server;      /**      * 启动      */     public static void run(Class<?> bootClass) {         run(Configuration.builder().bootClass(bootClass).build());     }      /**      * 启动      */     public static void run(Class<?> bootClass, int port) {         run(Configuration.builder().bootClass(bootClass).serverPort(port).build());     }      /**      * 启动      */     public static void run(Configuration configuration) {         new Doodle().start(configuration);     }      /**      * 初始化      */     private void start(Configuration configuration) {         try {             Doodle.configuration = configuration;             String basePackage = configuration.getBootClass().getPackage().getName();             BeanContainer.getInstance().loadBeans(basePackage);            //注意Aop必须在Ioc之前执行             new Aop().doAop();             new Ioc().doIoc();              server = new TomcatServer(configuration);             server.startServer();         } catch (Exception e) {             log.error("Doodle 启动失败", e);         }     } }

这个类中有三个启动方法都会调用Doodle@start()方法,在这个方法里做了三件事:

  1. 读取configuration中的配置
  2. BeanContainer扫描包并加载Bean
  3. 执行Aop
  4. 执行Ioc
  5. 启动Tomcat服务器

这里的执行是有顺序要求的,特别是Aop必须要在Ioc之前执行,不然注入到类中的属性都是没被代理的。

修改硬编码

在之前写mvc的时候有一处有个硬编码,现在有了启动器和全局配置,可以把之前的硬编码修改了

对在com.zbw.mvc包下的ResultRender类里的resultResolver()方法,当判断为跳转到jsp文件的时候跳转路径那一行代码修改:

try {     Doodle.getConfiguration().getResourcePath();     // req.getRequestDispatcher("/templates/" + path).forward(req, resp);     req.getRequestDispatcher(Doodle.getConfiguration().getResourcePath() + path).forward(req, resp); } catch (Exception e) {     log.error("转发请求失败", e);     // TODO: 异常统一处理,400等... }

启动和测试项目

现在doodle框架已经完成其功能了,我们可以简单的创建一个Controller来感受一下这个框架。

在com包下创建sample包,然后在com.sample包下创建启动类APP

package com.sample;  import com.zbw.Doodle; public class App {     public static void main(String[] args) {         Doodle.run(App.class);     } }

然后再创建一个ControllerDoodleController:

package com.sample; import com.zbw.core.annotation.Controller; import com.zbw.mvc.annotation.RequestMapping; import com.zbw.mvc.annotation.ResponseBody;  @Controller @RequestMapping public class DoodleController {     @RequestMapping     @ResponseBody     public String hello() {         return "hello doodle";     } }

接着再运行App的main方法,就能启动服务了。


源码地址:doodle

原文地址:从零开始实现一个简易的Java MVC框架(八)--制作Starter

脚本宝典为你提供优质服务
脚本宝典 » 从零开始实现一个简易的Java MVC框架(八)–制作Starter

发表评论

提供最优质的资源集合

立即查看 了解详情