準備 gossip 專案


Spring MVC 基於 Spring 核心等發展起來,為支援 Web 應用程式 MVC 架構的框架,功能強大而龐多,想要認識 Spring MVC 的話有幾種方式。

其一是將個別功能拆開來說,每個範例簡短易消化,然而功能這麼多,哪些該認識而哪些之後再瞭解,會陷入一個難題,而且個別功能如何彼此銜接出完整應用程式,還是要自行勾勒。

其二是發展一個 Web 應用程式,視應用程式必要的功能使用必要的 Spring MVC 元件,這是一個不錯的方式,然而最好對 Servlet 相關技術已經有一定的認識,才比較會知道相關設定之意義。

有不少開發者是直接認識 Spring MVC,雖說運用 Spring MVC 可以減少 Servlet API 等的運用,然而它是基於 Servlet 的框架,這個事實不容迴避,使用 Spring MVC 基本上還是要知道它與 Servlet 之間的關係,遇到一些問題才有個脈絡來思考如何解決。

有些開發者會希望,基於 Spring MVC 的 Web 應用程式,能夠儘量或甚至完全不使用 Servlet API 等資源,當然,Servlet API 等是有它本身的一些問題在,只不過,想要完全避開 Servlet API,意謂著你要認識與運用更多 Spring MVC,這件事本身並不容易,另一方面,心智模型也是個問題,一直想避開 Servlet API,然而心智模型上卻一直掛念著 Servlet API,反而會讓應用程式的發展產生問題。

Spring MVC 就是基於 Servlet,如果使用 Servlet 解決會比使用 Spring MVC 某元件來得簡易,該使用就使用,沒有必要為了 Spring MVC 而 Spring MVC。

因此,認識 Spring MVC 的第三種方式,就是有個基於 MVC 架構的 Servlet 應用程式,試著在最小運用 Spring MVC 功能的情況下,使之運行起來,再隨著對 Spring MVC 的認識越多,逐步重構應用程式套用相關元件或設定,這樣對兩者都會有足夠的認識。

接下來的文件,就是要使用第三種方式,基於一個 Servlet 發展而來的 gossip 專案,來逐一認識 Spring MVC,在這邊就是要先將這之準備好,它是個 Eclipse 專案,我開發的時侯是放在 C:\workspace 之下,基於 Apache Tomcat 9.0.2,使用 Gradle 來管理相依程式庫,資料庫的部份是 H2,基本上只要啟動 H2 Console,然後在 Eclipse 4.9 中匯入、Refresh Gradle Project、Run on Server 就可以操作應用程式了:

準備 gossip 專案

專案中的資料庫檔案 gossip.mv.db 中,已經有個使用者 caterpillar,密碼為 12345678,可以用它來登入玩玩看這個應用程式,如果要註冊新的使用者,註冊表單送出後,會使用 Java Mail 寄送啟用鏈結,Java Mail 的實作是基於 Gmail,你可以在 web.xml 中設定你的 Gmail 使用者名稱與密碼:

...
<context-param>
  <param-name>MAIL_USER</param-name>
  <param-value>yourname@gmail.com</param-value>
</context-param>
<context-param>
  <param-name>MAIL_PASSWORD</param-name>
  <param-value>yourpassword</param-value>
</context-param>
..

Gmail預設會限制一些「低安全性應用程式」的程式登入帳號,你必須至帳戶的「登入和安全性」中,將「允許安全性較低的應用程式」啟用,這個專案才能透過 Gmail 發送郵件:

準備 gossip 專案

這個應用程式是基於 MVC 架構發展,你應該試著摸清楚它的流程走向,記得,在套用任何框架之前,你本來就該清楚應用程式的流程。

web.xml 與 cc.openhome.web 中的 GossipInitializer 是你的起點:

package cc.openhome.web;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import javax.sql.DataSource;

import cc.openhome.model.AccountDAO;
import cc.openhome.model.AccountDAOJdbcImpl;
import cc.openhome.model.GmailService;
import cc.openhome.model.MessageDAO;
import cc.openhome.model.MessageDAOJdbcImpl;
import cc.openhome.model.UserService;

@WebListener
public class GossipInitializer implements ServletContextListener {
    public void contextInitialized(ServletContextEvent sce) {
        DataSource dataSource = dataSource();

        ServletContext context = sce.getServletContext();

        AccountDAO acctDAO = new AccountDAOJdbcImpl(dataSource);
        MessageDAO messageDAO = new MessageDAOJdbcImpl(dataSource);
        context.setAttribute("userService", new UserService(acctDAO, messageDAO));
        context.setAttribute("emailService", 
                new GmailService(
                    context.getInitParameter("MAIL_USER"), 
                    context.getInitParameter("MAIL_PASSWORD")
               )
        );
    }

    private DataSource dataSource() {
        try {
            Context initContext = new InitialContext();
            Context envContext = (Context) initContext.lookup("java:/comp/env");
           return (DataSource) envContext.lookup("jdbc/gossip");
        } catch (NamingException e) {
            throw new RuntimeException(e);
        }
    }
}

可以看到初始了 UserServiceGmailService 實例,並設定為 ServletContext 的屬性,在各控制器中,若需要這兩個實例,就是從 ServletContext 取得對應的屬性。

UserService 提供使用者註冊、登入、發表訊息等服務,在資料庫存取部份,會委託給 AccountDAOMessageDAO 來處理,在註冊或重設密碼的流程中,若需要發送郵件,則是由 GmailService 來處理。

資料庫檔案是專案中的 gossip.mv.db,表格使用底下的 SQL 建立:

CREATE TABLE t_account (
  name VARCHAR(15) NOT NULL,
  email VARCHAR(128) NOT NULL,
  password VARCHAR(32) NOT NULL,
  salt VARCHAR(256) NOT NULL,
  PRIMARY KEY (name)
);
CREATE TABLE t_message (
    name VARCHAR(15) NOT NULL,
    time BIGINT NOT NULL,
    blabla VARCHAR(512) NOT NULL,
    FOREIGN KEY (name) REFERENCES t_account(name) 
);
CREATE TABLE t_account_role (
    name VARCHAR(15) NOT NULL,
    role VARCHAR(15) NOT NULL,
    PRIMARY KEY (name, role)
);

這個專案使用了 OWASP 的 Java HTML Sanitizer 來過濾使用者的輸入訊息,這部份實作為過濾器 cc.openhome.web.HtmlSanitizer;在頁面權限控制上,使用 Java EE 安全管理,必要的安全設定撰寫在 web.xml。

畫面呈現技術是使用 JSP 搭配 JSTL,嗯?JSP 過時了?這邊不討論過不過時這話題,由於採用 MVC 架構,若想將 JSP 換成其他呈現技術,像是 Thymeleaf 模版,並不會是難事。

這個專案實際上改寫自《 Servlet & JSP技術手冊 - 從 Servlet 到 Spring Boot》第 11 章的成果,如果你想知道這個專案是怎麼發展起來的,可以參考該書,接下來的文件,只會說明怎麼將這個專案重構,以便套用 Spring MVC 框架。