Java Web 拾遗

许是年纪大了,老是回忆起以前的点点滴滴。翻看当初的代码,如同偶遇多年未见的前女友,曾经一起深入交流的情谊在颔首之间消散,令人烦躁。

今天就来聊聊老生常谈的 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 平台更适合微服务运行的几个框架。