一个项目总是要有一个启动的地方,当项目部署在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.如果获得的 ApplicationContext
为 ServletWebServerApplicationContext
,那么在 refresh()
之后会启动服务器,默认的就是tomcat服务器。
我觉得spring-boot启动器算是spring-boot中相对来说代码清晰易懂的,同时也非常容易了解到整个spring-boot的流程结构,建议大家能够去看一下。
了解到spring-boot的启动器的作用和原理之后,我们可以开始实现 doodle 的启动器了。
根据刚才提到的,启动器要做以下几件事
在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 = ""; } 复制代码
在上一章文章 从零开始实现一个简易的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 (ContentFolder.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配置的文件,而且 defaultServlet
和 jspServlet
这两个servlet是tomcat内置的servlet,前者用于处理静态资源如css、js文件等,后者用于处理jsp。如果有安装tomcat可以去tomcat目录下的conf文件夹里有个web.xml文件,里面有几行就是配置 defaultServlet
和 jspServlet
<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()
方法,在这个方法里做了三件事:
configuration
这里的执行是有顺序要求的,特别是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); } } 复制代码
然后再创建一个Controller DoodleController
:
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