从零开始实现一个简易的 Java MVC 框架(八)--制作 Starter
doodle
2018-07-27
3512
10
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. 如果获得的ApplicationContext
为ServletWebServerApplicationContext
, 那么在refresh()
之后会启动服务器,默认的就是 tomcat 服务器。
我觉得 spring-boot 启动器算是 spring-boot 中相对来说代码清晰易懂的,同时也非常容易了解到整个 spring-boot 的流程结构,建议大家能够去看一下。
实现 Starter
了解到 spring-boot 的启动器的作用和原理之后,我们可以开始实现* doodle *的启动器了。
根据刚才提到的,启动器要做以下几件事
- 加载一些参数变量
- 加载 Bean(IOC、AOP) 等工作
- 启动服务器
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");
} 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");
这部分代码就相当于原来的 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
中的配置 - BeanContainer 扫描包并加载 Bean
- 执行 Aop
- 执行 Ioc
- 启动 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 方法,就能启动服务了。
- 从零开始实现一个简易的 Java MVC 框架(一)--前言
- 从零开始实现一个简易的 Java MVC 框架(二)--实现 Bean 容器
- 从零开始实现一个简易的 Java MVC 框架(三)--实现 IOC
- 从零开始实现一个简易的 Java MVC 框架(四)--实现 AOP
- 从零开始实现一个简易的 Java MVC 框架(五)--引入 aspectj 实现 AOP 切点
- 从零开始实现一个简易的 Java MVC 框架(六)--加强 AOP 功能
- 从零开始实现一个简易的 Java MVC 框架(七)--实现 MVC
- 从零开始实现一个简易的 Java MVC 框架(八)--制作 Starter
- 从零开始实现一个简易的 Java MVC 框架(九)--优化 MVC 代码
源码地址:doodle