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");
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
中的配置 - 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方法,就能啟動服務了。
原始碼地址:doodle