從零開始實現一個簡易的Java MVC框架(八)--製做Starter

spring-boot的Starter

一個項目老是要有一個啓動的地方,當項目部署在tomcat中的時候,常常就會用tomcat的startup.sh(startup.bat)的啓動腳原本啓動web項目css

而在spring-boot的web項目中基本會有相似於這樣子的啓動代碼:java

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

這個方法實際上會調用spring-boot的SpringApplication類的一個run方法:git

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服務器。github

我以爲spring-boot啓動器算是spring-boot中相對來講代碼清晰易懂的,同時也很是容易瞭解到整個spring-boot的流程結構,建議你們可以去看一下。web

實現Starter

瞭解到spring-boot的啓動器的做用和原理以後,咱們能夠開始實現doodle的啓動器了。spring

根據剛纔提到的,啓動器要作如下幾件事apache

  1. 加載一些參數變量
  2. 加載Bean(IOC、AOP)等工做
  3. 啓動服務器

Configuration保存變量

在com.zbw包下建立類Configuration用於保存一些全局變量,目前這個類只保存瞭如今實現的功能所需的變量。tomcat

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包下建立一個接口Servermvc

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

相關文章
相關標籤/搜索