Java Web 拾遺

莱布尼茨發表於2024-09-25

許是年紀大了,老是回憶起以前的點點滴滴。翻看當初的程式碼,如同偶遇多年未見的前女友,曾經一起深入交流的情誼在頷首之間消散,令人煩躁。

今天就來聊聊老生常談的 Java Web 開發。緣於一個簡單的Spring Boot專案改造,筆者看著一坨註解和配置,苦於拾掇記憶的痛苦,擇其一二記錄,紀念逝去的青春。

本文對新手有一定幫助,大家笑過勿噴。

JSP + JavaBean

筆者學生時代接觸了JSP,作為遠古產物,現在已難覓蹤跡,但與它一同出現的JavaBean,卻一直留傳了下來。

在任何開發模式下,都需要一套規範,JavaBean 就是符合這些規範的類/物件,比如:

  • 所有欄位為 private(不允許外部直接訪問,避免以後重新命名/刪除等操作引發依賴故障)
  • 提供預設構造方法(方便外部例項化)
  • 提供 getter 和 setter(自定義屬性的讀寫邏輯)
  • 實現 serializable 介面(序列化支援)

注意,JavaBean 不是 POJO,因為它需要方法、事件等處理和響應業務。它包含所有的資料和業務邏輯,開發時在 HTML 中嵌入後端程式碼呼叫它們,如下所示:

<%@ page language="java" import="java.util.*,com.cy.bean.*" pageEncoding="utf-8"%>
<%
String path = request.getContextPath();
%>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head>
    <base href="<%=path%>">
  </head>

  <body>
    <%CheckUserBean cub=new CheckUserBean(); %>
  <jsp:useBean id="user" class="com.cy.bean.UserBean" scope="request"></jsp:useBean>
  <jsp:getProperty property="name" name="user"/>
  <jsp:setProperty property="password" name="user"/>
  <%if(cub.checkUser(user)) {%>
  <jsp:forward page="success.jsp"></jsp:forward>
  <%}else{%>
  <jsp:forward page="fail.jsp"></jsp:forward>
  <%} %>
  </body>
</html>

上述有 UserBean 和 CheckUserBean 兩個 JavaBean,其中 UserBean 用於展示資料及接收使用者輸入,CheckUserBean 用於判斷使用者是否合法。

後來,JavaBean 的一些特徵被開發人員沿用下來,同時概念簡化為Bean,推廣至更多的框架。對大部分後起的語言(比如 C#)來說,因為有 Java 幫忙踩的坑,它們往往在語言設計之初就提供了語言特性來更方便自然地貼合這些規範。

Servlet

JSP + JavaBean 的模式有一個明顯的缺點,即隱性的頁面跳轉(資料流轉),提高了開發過程中的出錯機率,比如同一個頁面可能由多個不同頁面跳轉過來,而相應的資料結構並不相同,開發人員要考慮所有可能的情況,並提供相應的 JavaBean 承接這些資料。同樣隨著業務發展,這種跳轉或資料結構都會經常發生變更,開發維護成本極高。

於是增加了Servlet(一般繼承自HttpServlet,該類定義了幾個簡單明瞭的方法,此處不贅述)來處理請求、填充 JavaBean/呼叫 JavaBean 方法、選擇返回哪個檢視等,並且加上了路由的配置,形成了基礎的MVC模式。

路由的配置在web.xml中,如下:

<?xml version="1.0" encoding="UTF-8"?>
<web-app>
    <!-- other configrations -->

    <!-- 宣告 servlet -->
    <servlet>
        <servlet-name>login</servlet-name>
        <servlet-class>com.cy.servlet.LoginServlet</servlet-class>
    </servlet>
    <!-- 路由配置 -->
    <servlet-mapping>
        <servlet-name>login</servlet-name>
        <url-pattern>/login</url-pattern>
    </servlet-mapping>

</web-app>

值得一提的是,出現了Filter(過濾器)的概念,即在 servlet 處理請求之前和返回響應之後的中間處理器,可以提供與業務無關的通用功能,比如身份校驗、限流、異常處理等。這種 AOP 理念非常好,也一直保留至今。

同樣, Filter 也需要配置,如下:

<web-app>
    <!-- other configrations -->

     <filter>
         <filter-name>jsp</filter-name>
         <filter-class>com.cy.filter.DemoFilter</filter-class>
     </filter>
     <filter-mapping>
         <filter-name>jsp</filter-name>
         <url-pattern>/*</url-pattern>
     </filter-mapping>

</web-app>

注意,Servlet 須執行於 Servlet 容器(如Tomcat)中。

Struts

為了提高開發效率,在 Servlet 基礎上,提供了一些通用模組和工具,制定一套規範,形成一個框架,最知名的當屬Struts,它有 1、2 兩個版本。這兩個版本並非簡單的升級,而是整個設計的更替。

Struts1

Struts1 使用一個單例核心ActionServlet接收所有請求,請求資料轉化為ActionForm,然後依據配置(struts-config.xml中的ActionMapping)分發給不同的Action。Action 一般只包含一個 excute 方法用於處理業務。

Struts1 很明顯的缺點導致現在基本沒人會去用:

  • 配置繁瑣
  • ActionServlet 單例模式,須考慮執行緒安全
  • 依賴 Web 容器,單元測試不方便

Struts2

於是Struts2被推出。

它使用Interceptor(攔截器) + Controller(即 Struts1 中的 Action)的模式,使得整個處理流程擴充套件性大大提高了。

同時它擯棄了單例模式,每次都會例項化新的 Controller 處理請求(其中可包含任意多的方法用以執行不同業務),不用擔心執行緒安全問題,缺點是併發量高的時候物件例項激增記憶體吃緊。

框架藉助本身的攔截機制,將請求和響應資料對映為 POJO,實現了 Controller 對HttpServletRequestHttpServletResponse這樣的原生 Servlet 物件的剝離,即 Controller 不依賴於 Web 容器,可以方便地單元測試了。

還記得上面 Servlet 的過濾器嗎,Struts2 攔截器和它的原理一樣,只不過前者面對所有請求,後者針對的是某個具體的 Controller。當然,Struts2 同時使用了兩者。

相比 Struts1,Struts2 有了質的飛躍,然而沒過幾年,它的榮光也被後起之秀所掩蓋。

Spring MVC

說起Spring MVC,不得不先說說Spring

Spring

Spring是 Java 平臺流行的 IOC 和 AOP 框架,雖然它本身不針對特定的使用場景,但是 Java 平臺的 Web 基因一開始就影響著它,所以我們慣常使用它來開發後端服務。Spring 官方有專門的子專案Spring Web,Spring MVC 就是 Spring Web 的子模組。Spring Web 包含很多其它模組,如Spring WebFlux、Spring Web Service、Spring WebSocket等

Java 後半程在移動端大放異彩,有另一個 IOC 框架Dagger在背後默默支援,可參看筆者寫的
從零開始擼一個App-Dagger2
,此處不贅述。

IOC

我們可以透過在 XML 檔案(使用ClassPathXmlApplicationContext載入)中配置 Bean,然後在程式碼中使用@Autowired@Resource(來自 JSR-250,JDK 內建)注入 Bean 例項(作用域可透過scope設定,預設是單例)。

XML 配置稍顯繁瑣,Sping2.5 開始支援註解注入,只要在 XML 中配置<context:component-scan>(對應的有@ComponentScan註解),Spring 便會自動掃描指定包中的所有類,查詢如@Component,@Service,@Repository,@Controller等註解修飾的類,並建立相應的 Bean。當然,這種方式只能配置本專案內的類。

為了使註解方式可以注入第三方類,從 3.0 開始,Spring 引入了@Configuration。使用 @Configuration 註解修飾的類(使用AnnotationConfigApplicationContext載入)中,可使用@Bean註解修飾返回 Bean 的方法。我們若要複用它處定義的配置類,可使用@Import註解,它的作用類似於將多個 XML 配置檔案匯入到單個檔案。

XML 配置和註解配置也可以混用,比如使用@ImportResource註解引入 XML 檔案。

AOP

Spring 還是提供了 AOP 功能。

AOP 分為靜態 AOP 和動態 AOP。靜態 AOP 是將切面程式碼直接編譯到原始碼中,如 Java 平臺的AspectJ實現;動態 AOP 是指將切面程式碼執行時動態織入。Spring 的 AOP 為動態 AOP,實現的技術為 JDK 提供的動態代理技術CGLIB(動態位元組碼增強技術),兩者區別如下:

  • JDK 動態代理利用攔截器(必須實現 InvocationHandler)加上反射機制生成一個代理介面的匿名類,在呼叫具體方法前呼叫 InvokeHandler 來處理;CGLIB 利用ASM框架,將目標類生成的 class 檔案載入進來,透過修改其位元組碼生成子類來處理。
  • JDK 動態代理的目標類必須實現某個介面,只有介面中的方法才能夠被代理;CGLIB 無此限制,但是因為採用的是繼承模式,所以目標類或方法不能為 final。
  • 在 Java1.8 之後,大部分場景下,JDK 動態代理的效率都要優於 CGLIB。

兩者儘管實現技術不一樣,但都是基於代理模式,都是生成一個代理物件。

Spring 會根據目標類是否實現介面來決定使用 JDK 動態代理還是 CGLIB,當然在符合條件時也可以強制使用 CGLIB(<aop:aspectj-autoproxy proxyt-target-class="true"/>)。

Spring AOP 涉及到的註解包括@Aspect、@Pointcut、@Before、@After、@AfterReturning、@AfterThrowing、@Around、@EnableAspectJAutoProxy等,此處不詳述。


Spring MVC 同樣是基於 Servlet,像是 IOC 版的 Struts2,當然由於 IOC 的引入,兩者的概念和元件大相徑庭,但是處理請求的主幹是一致的。

Spring MVC 支援的頁面渲染實現,並不包含 JSP。而是ThymeleafFreemarker等。

Spring Boot

最後來談談 Spring Boot,它是建立在 Spring 之上的一個快速開發框架,旨在簡化 Spring 應用的初始搭建以及開發過程。它透過提供預設配置、Starter dependencies等特性,極大地減少了專案的配置工作。

同樣的,它不獨屬於 Web 開發,但我們主要還是在 Web 領域使用它。

@ConfigurationProperties

在 Spring Boot 專案中,我們常將大量的引數配置在 application.properties(Spring) 或 application.yml 檔案中,然後透過@Value取值,如下:

@Value("${db.userName}")
private String userName;

其實透過@ConfigurationProperties註解,我們可以更清爽地獲取這些引數值:

//@Component 注入
@ConfigurationProperties(prefix="db")
public class DbConfiguration{
  public String userName;
}

@ConfigurationProperties 並不表示成為 Spring Bean,除非配置類同時標註 @Component 之類的註解,或者在使用方標註@EnableConfigurationProperties註解(建議後者,即按需索取,而非全域性可見):

@EnableConfigurationProperties(DbConfiguration.class)
public class Invoker{

    @Autowired
    DbConfiguration dbConfiguration;
}

spring.factories

如果你正在編寫一個基於 Spring 的類庫,其中很多物件都是以 Bean 的形式注入使用的,所以你當然希望使用這個類庫的第三方專案可以將這些物件事先載入到容器中。

你可以在 ReadMe 中寫明“XX 類及 XXX 類 及……必須在專案啟動時例項化到容器中”,如此使用方知道他必須採用 XML 或 @Configuration 等方式寫上一大段和業務無關的配置程式碼。

或者你可以使用 spring.factories 方案。spring.factories 其實是 Spring boot 提供的SPI機制,使用方的專案(需要在入口類中標註@EnableAutoConfiguration註解)會基於SpringFactoriesLoader檢索ClassLoader中所有 jar(包括ClassPath下的所有模組)引入的META-INF/spring.factories檔案,基於檔案中的介面自動載入對應的 @Configuration 修飾的類並且註冊到容器中。

spring.factories 為模組化、配置化提供了基石,我們經常引用的諸如“xxx-spring-boot-starter”的類庫,基本上就是使用了該方案。

ps:自 Spring Boot 3.0 始,由META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports替代 META-INF/spring.factories,內容格式有所變化,原理不變。

Spring Boot 3.0 是一個比較大的改版,影響最大的改動是必須使用 JDK17 及以上版本。


由於我們常將 @ComponentScan、@SpringBootConfiguration(同 @Configuration)、@EnableAutoConfiguration 一起使用,Spring Boot 乾脆出了一個@SpringBootApplication註解,將三者合一。


Spring Boot 對 AOP 的使用進行了一些改動,此處不贅述。

內建常見的伺服器(如 Tomcat、Jetty),無需單獨部署。


Spring Boot 雖然是一個非常成熟的拆箱即用框架,但在微服務場景下就顯得過於笨重了。後續有緣的話筆者會再來聊聊 Java 平臺更適合微服務執行的幾個框架。

相關文章