Java Web 拾遺
許是年紀(jì)大了,老是回憶起以前的點(diǎn)點(diǎn)滴滴。翻看當(dāng)初的代碼,如同偶遇多年未見(jiàn)的前女友,曾經(jīng)一起深入交流的情誼在頷首之間消散,令人煩躁。
今天就來(lái)聊聊老生常談的 Java Web 開(kāi)發(fā)。緣于一個(gè)簡(jiǎn)單的Spring Boot項(xiàng)目改造,筆者看著一坨注解和配置,苦于拾掇記憶的痛苦,擇其一二記錄,紀(jì)念逝去的青春。
本文對(duì)新手有一定幫助,大家笑過(guò)勿噴。
JSP + JavaBean
筆者學(xué)生時(shí)代接觸了JSP,作為遠(yuǎn)古產(chǎn)物,現(xiàn)在已難覓蹤跡,但與它一同出現(xiàn)的JavaBean,卻一直留傳了下來(lái)。
在任何開(kāi)發(fā)模式下,都需要一套規(guī)范,JavaBean 就是符合這些規(guī)范的類(lèi)/對(duì)象,比如:
- 所有字段為 private(不允許外部直接訪(fǎng)問(wèn),避免以后重命名/刪除等操作引發(fā)依賴(lài)故障)
- 提供默認(rèn)構(gòu)造方法(方便外部實(shí)例化)
- 提供 getter 和 setter(自定義屬性的讀寫(xiě)邏輯)
- 實(shí)現(xiàn) serializable 接口(序列化支持)
注意,JavaBean 不是 POJO,因?yàn)樗枰椒ā⑹录忍幚砗晚憫?yīng)業(yè)務(wù)。它包含所有的數(shù)據(jù)和業(yè)務(wù)邏輯,開(kāi)發(fā)時(shí)在 HTML 中嵌入后端代碼調(diào)用它們,如下所示:
<%@ 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 兩個(gè) JavaBean,其中 UserBean 用于展示數(shù)據(jù)及接收用戶(hù)輸入,CheckUserBean 用于判斷用戶(hù)是否合法。
后來(lái),JavaBean 的一些特征被開(kāi)發(fā)人員沿用下來(lái),同時(shí)概念簡(jiǎn)化為Bean,推廣至更多的框架。對(duì)大部分后起的語(yǔ)言(比如 C#)來(lái)說(shuō),因?yàn)橛?Java 幫忙踩的坑,它們往往在語(yǔ)言設(shè)計(jì)之初就提供了語(yǔ)言特性來(lái)更方便自然地貼合這些規(guī)范。
Servlet
JSP + JavaBean 的模式有一個(gè)明顯的缺點(diǎn),即隱性的頁(yè)面跳轉(zhuǎn)(數(shù)據(jù)流轉(zhuǎn)),提高了開(kāi)發(fā)過(guò)程中的出錯(cuò)概率,比如同一個(gè)頁(yè)面可能由多個(gè)不同頁(yè)面跳轉(zhuǎn)過(guò)來(lái),而相應(yīng)的數(shù)據(jù)結(jié)構(gòu)并不相同,開(kāi)發(fā)人員要考慮所有可能的情況,并提供相應(yīng)的 JavaBean 承接這些數(shù)據(jù)。同樣隨著業(yè)務(wù)發(fā)展,這種跳轉(zhuǎn)或數(shù)據(jù)結(jié)構(gòu)都會(huì)經(jīng)常發(fā)生變更,開(kāi)發(fā)維護(hù)成本極高。
于是增加了Servlet(一般繼承自HttpServlet,該類(lèi)定義了幾個(gè)簡(jiǎn)單明了的方法,此處不贅述)來(lái)處理請(qǐng)求、填充 JavaBean/調(diào)用 JavaBean 方法、選擇返回哪個(gè)視圖等,并且加上了路由的配置,形成了基礎(chǔ)的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>
值得一提的是,出現(xiàn)了Filter(過(guò)濾器)的概念,即在 servlet 處理請(qǐng)求之前和返回響應(yīng)之后的中間處理器,可以提供與業(yè)務(wù)無(wú)關(guān)的通用功能,比如身份校驗(yàn)、限流、異常處理等。這種 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 須運(yùn)行于 Servlet 容器(如Tomcat)中。
Struts
為了提高開(kāi)發(fā)效率,在 Servlet 基礎(chǔ)上,提供了一些通用模塊和工具,制定一套規(guī)范,形成一個(gè)框架,最知名的當(dāng)屬Struts,它有 1、2 兩個(gè)版本。這兩個(gè)版本并非簡(jiǎn)單的升級(jí),而是整個(gè)設(shè)計(jì)的更替。
Struts1
Struts1 使用一個(gè)單例核心ActionServlet接收所有請(qǐng)求,請(qǐng)求數(shù)據(jù)轉(zhuǎn)化為ActionForm,然后依據(jù)配置(struts-config.xml中的ActionMapping)分發(fā)給不同的Action。Action 一般只包含一個(gè) excute 方法用于處理業(yè)務(wù)。
Struts1 很明顯的缺點(diǎn)導(dǎo)致現(xiàn)在基本沒(méi)人會(huì)去用:
- 配置繁瑣
- ActionServlet 單例模式,須考慮線(xiàn)程安全
- 依賴(lài) Web 容器,單元測(cè)試不方便
Struts2
于是Struts2被推出。
它使用Interceptor(攔截器) + Controller(即 Struts1 中的 Action)的模式,使得整個(gè)處理流程擴(kuò)展性大大提高了。
同時(shí)它擯棄了單例模式,每次都會(huì)實(shí)例化新的 Controller 處理請(qǐng)求(其中可包含任意多的方法用以執(zhí)行不同業(yè)務(wù)),不用擔(dān)心線(xiàn)程安全問(wèn)題,缺點(diǎn)是并發(fā)量高的時(shí)候?qū)ο髮?shí)例激增內(nèi)存吃緊。
框架借助本身的攔截機(jī)制,將請(qǐng)求和響應(yīng)數(shù)據(jù)映射為 POJO,實(shí)現(xiàn)了 Controller 對(duì)HttpServletRequest和HttpServletResponse這樣的原生 Servlet 對(duì)象的剝離,即 Controller 不依賴(lài)于 Web 容器,可以方便地單元測(cè)試了。
還記得上面 Servlet 的過(guò)濾器嗎,Struts2 攔截器和它的原理一樣,只不過(guò)前者面對(duì)所有請(qǐng)求,后者針對(duì)的是某個(gè)具體的 Controller。當(dāng)然,Struts2 同時(shí)使用了兩者。
相比 Struts1,Struts2 有了質(zhì)的飛躍,然而沒(méi)過(guò)幾年,它的榮光也被后起之秀所掩蓋。
Spring MVC
說(shuō)起Spring MVC,不得不先說(shuō)說(shuō)Spring。
Spring
Spring是 Java 平臺(tái)流行的 IOC 和 AOP 框架,雖然它本身不針對(duì)特定的使用場(chǎng)景,但是 Java 平臺(tái)的 Web 基因一開(kāi)始就影響著它,所以我們慣常使用它來(lái)開(kāi)發(fā)后端服務(wù)。Spring 官方有專(zhuān)門(mén)的子項(xiàng)目Spring Web,Spring MVC 就是 Spring Web 的子模塊。Spring Web 包含很多其它模塊,如Spring WebFlux、Spring Web Service、Spring WebSocket等。
Java 后半程在移動(dòng)端大放異彩,有另一個(gè) IOC 框架Dagger在背后默默支持,可參看筆者寫(xiě)的
從零開(kāi)始擼一個(gè)App-Dagger2 ,此處不贅述。
IOC
我們可以通過(guò)在 XML 文件(使用ClassPathXmlApplicationContext加載)中配置 Bean,然后在代碼中使用@Autowired或@Resource(來(lái)自 JSR-250,JDK 內(nèi)置)注入 Bean 實(shí)例(作用域可通過(guò)scope設(shè)置,默認(rèn)是單例)。
XML 配置稍顯繁瑣,Sping2.5 開(kāi)始支持注解注入,只要在 XML 中配置<context:component-scan>(對(duì)應(yīng)的有@ComponentScan注解),Spring 便會(huì)自動(dòng)掃描指定包中的所有類(lèi),查找如@Component,@Service,@Repository,@Controller等注解修飾的類(lèi),并創(chuàng)建相應(yīng)的 Bean。當(dāng)然,這種方式只能配置本項(xiàng)目?jī)?nèi)的類(lèi)。
為了使注解方式可以注入第三方類(lèi),從 3.0 開(kāi)始,Spring 引入了@Configuration。使用 @Configuration 注解修飾的類(lèi)(使用AnnotationConfigApplicationContext加載)中,可使用@Bean注解修飾返回 Bean 的方法。我們?nèi)粢獜?fù)用它處定義的配置類(lèi),可使用@Import注解,它的作用類(lèi)似于將多個(gè) XML 配置文件導(dǎo)入到單個(gè)文件。
XML 配置和注解配置也可以混用,比如使用@ImportResource注解引入 XML 文件。
AOP
Spring 還是提供了 AOP 功能。
AOP 分為靜態(tài) AOP 和動(dòng)態(tài) AOP。靜態(tài) AOP 是將切面代碼直接編譯到源代碼中,如 Java 平臺(tái)的AspectJ實(shí)現(xiàn);動(dòng)態(tài) AOP 是指將切面代碼運(yùn)行時(shí)動(dòng)態(tài)織入。Spring 的 AOP 為動(dòng)態(tài) AOP,實(shí)現(xiàn)的技術(shù)為 JDK 提供的動(dòng)態(tài)代理技術(shù)和CGLIB(動(dòng)態(tài)字節(jié)碼增強(qiáng)技術(shù)),兩者區(qū)別如下:
- JDK 動(dòng)態(tài)代理利用攔截器(必須實(shí)現(xiàn) InvocationHandler)加上反射機(jī)制生成一個(gè)代理接口的匿名類(lèi),在調(diào)用具體方法前調(diào)用 InvokeHandler 來(lái)處理;CGLIB 利用
ASM框架,將目標(biāo)類(lèi)生成的 class 文件加載進(jìn)來(lái),通過(guò)修改其字節(jié)碼生成子類(lèi)來(lái)處理。 - JDK 動(dòng)態(tài)代理的目標(biāo)類(lèi)必須實(shí)現(xiàn)某個(gè)接口,只有接口中的方法才能夠被代理;CGLIB 無(wú)此限制,但是因?yàn)椴捎玫氖抢^承模式,所以目標(biāo)類(lèi)或方法不能為 final。
- 在 Java1.8 之后,大部分場(chǎng)景下,JDK 動(dòng)態(tài)代理的效率都要優(yōu)于 CGLIB。
兩者盡管實(shí)現(xiàn)技術(shù)不一樣,但都是基于代理模式,都是生成一個(gè)代理對(duì)象。
Spring 會(huì)根據(jù)目標(biāo)類(lèi)是否實(shí)現(xiàn)接口來(lái)決定使用 JDK 動(dòng)態(tài)代理還是 CGLIB,當(dāng)然在符合條件時(shí)也可以強(qiáng)制使用 CGLIB(<aop:aspectj-autoproxy proxyt-target-class="true"/>)。
Spring AOP 涉及到的注解包括@Aspect、@Pointcut、@Before、@After、@AfterReturning、@AfterThrowing、@Around、@EnableAspectJAutoProxy等,此處不詳述。
Spring MVC 同樣是基于 Servlet,像是 IOC 版的 Struts2,當(dāng)然由于 IOC 的引入,兩者的概念和組件大相徑庭,但是處理請(qǐng)求的主干是一致的。
Spring MVC 支持的頁(yè)面渲染實(shí)現(xiàn),并不包含 JSP。而是Thymeleaf、Freemarker等。
Spring Boot
最后來(lái)談?wù)?Spring Boot,它是建立在 Spring 之上的一個(gè)快速開(kāi)發(fā)框架,旨在簡(jiǎn)化 Spring 應(yīng)用的初始搭建以及開(kāi)發(fā)過(guò)程。它通過(guò)提供默認(rèn)配置、Starter dependencies等特性,極大地減少了項(xiàng)目的配置工作。
同樣的,它不獨(dú)屬于 Web 開(kāi)發(fā),但我們主要還是在 Web 領(lǐng)域使用它。
@ConfigurationProperties
在 Spring Boot 項(xiàng)目中,我們常將大量的參數(shù)配置在 application.properties(Spring) 或 application.yml 文件中,然后通過(guò)@Value取值,如下:
@Value("${db.userName}")
private String userName;
其實(shí)通過(guò)@ConfigurationProperties注解,我們可以更清爽地獲取這些參數(shù)值:
//@Component 注入
@ConfigurationProperties(prefix="db")
public class DbConfiguration{
public String userName;
}
@ConfigurationProperties 并不表示成為 Spring Bean,除非配置類(lèi)同時(shí)標(biāo)注 @Component 之類(lèi)的注解,或者在使用方標(biāo)注@EnableConfigurationProperties注解(建議后者,即按需索取,而非全局可見(jiàn)):
@EnableConfigurationProperties(DbConfiguration.class)
public class Invoker{
@Autowired
DbConfiguration dbConfiguration;
}
spring.factories
如果你正在編寫(xiě)一個(gè)基于 Spring 的類(lèi)庫(kù),其中很多對(duì)象都是以 Bean 的形式注入使用的,所以你當(dāng)然希望使用這個(gè)類(lèi)庫(kù)的第三方項(xiàng)目可以將這些對(duì)象事先加載到容器中。
你可以在 ReadMe 中寫(xiě)明“XX 類(lèi)及 XXX 類(lèi) 及……必須在項(xiàng)目啟動(dòng)時(shí)實(shí)例化到容器中”,如此使用方知道他必須采用 XML 或 @Configuration 等方式寫(xiě)上一大段和業(yè)務(wù)無(wú)關(guān)的配置代碼。
或者你可以使用 spring.factories 方案。spring.factories 其實(shí)是 Spring boot 提供的SPI機(jī)制,使用方的項(xiàng)目(需要在入口類(lèi)中標(biāo)注@EnableAutoConfiguration注解)會(huì)基于SpringFactoriesLoader檢索ClassLoader中所有 jar(包括ClassPath下的所有模塊)引入的META-INF/spring.factories文件,基于文件中的接口自動(dòng)加載對(duì)應(yīng)的 @Configuration 修飾的類(lèi)并且注冊(cè)到容器中。
spring.factories 為模塊化、配置化提供了基石,我們經(jīng)常引用的諸如“xxx-spring-boot-starter”的類(lèi)庫(kù),基本上就是使用了該方案。
ps:自 Spring Boot 3.0 始,由META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports替代 META-INF/spring.factories,內(nèi)容格式有所變化,原理不變。
Spring Boot 3.0 是一個(gè)比較大的改版,影響最大的改動(dòng)是必須使用 JDK17 及以上版本。
由于我們常將 @ComponentScan、@SpringBootConfiguration(同 @Configuration)、@EnableAutoConfiguration 一起使用,Spring Boot 干脆出了一個(gè)@SpringBootApplication注解,將三者合一。
Spring Boot 對(duì) AOP 的使用進(jìn)行了一些改動(dòng),此處不贅述。
內(nèi)置常見(jiàn)的服務(wù)器(如 Tomcat、Jetty),無(wú)需單獨(dú)部署。
Spring Boot 雖然是一個(gè)非常成熟的拆箱即用框架,但在微服務(wù)場(chǎng)景下就顯得過(guò)于笨重了。后續(xù)有緣的話(huà)筆者會(huì)再來(lái)聊聊 Java 平臺(tái)更適合微服務(wù)運(yùn)行的幾個(gè)框架。

浙公網(wǎng)安備 33010602011771號(hào)