大部分Java应用都是Web或网络应用,MVC框架在Java框架中有着举足轻重的地位,一开始的Web应用并不现在这样子的,一步一步走来,每一步都经历了无数的血和泪的教训,以史为镜可以知兴替。
1. 草莽时代
早期的Java服务端技术主要是Servlet和JSP,Servlet的灵感应该是来自于CGI(Common Gateway Interface),Java通过Servlet定义了一组接口和类,用来抽象这个过程,核心抽象就3个,分别是输入、处理、输出
- 输入,ServletRequest是客户端输入的抽象,提供请求的内容读取的工具方法
- 处理,Servlet定了模板方法,开发者可以覆盖service方法,使用ServletRequest读取请求,使用ServletResponse响应输出
- 输出,ServletResponse是客户端响应的抽象,提供响应内容的工具方法
1.1 Servlet
我们来看一下ServletRequest和ServletResponse的接口定义,我们讲一下核心的方法,不需要记忆,只要对它们的能力边界有一个概念即可
1. 输入
分组
方法
解释
服务端
getProtocol()
返回请求使用的协议名和版本号,通常是"HTTP/1.1"
isSecure()
是否是HTTPS的,true表示是,false表示否
getServerName()
接收请求的服务器主机名,应用服务器支持自定义,Tomcat用server.xml的Host.name来设置,默认取机器的主机名(hostname命令)
getServerPort()
接收请求的端口,服务器监听并等待客户端连接的端口
getLocalName()
一般等同于getServerName,复杂环境(负载均衡、集群时)可能有独立值
getLocalAddr()
当前请求绑定的服务器IP地址,TCP连接中对应的IP
getLocalPort()
一般等同于getServerPort,处理请求的服务器端口,请求被转发会有差异
客户端
getRemoteHost()
使用getRemoteAddr()返回的IP地址,通过DNS反解析,解析不到直接返回IP
getRemoteAddr()
请求的客户端IP地址,TCP连接的IP,局域网内是局域网IP,公网是公网IP
getRemotePort()
请求的客户端端口
元信息
getCharacterEncoding()
通过HTTP头浏览器设置请求编码,如Content-Type: application/json;charset=utf-8
Servlet容器会用这个编码解析请求体
getContentType()
数据的MIME类型,通过HTTP头Content-Type指定
getContentLength()
请求体的长度,只包含body部分,不包括url、Head、Cookie
getLocale()
客户端的区域设置,有语言代码+国家/地区代码组成,比如zh_CN表示中文_中国大陆,通过解析HTTP头Accept-Language获取
请求体
getInputStream()
请求体的输入流,只能被读取一次,返回ServletInputStream对象
getReader()
请求体的输入流,读取的是字符,使用Content-Type里指定的编码,如果没指定则使用容器设置的默认编码,没设置则默认iso-8859-1
getParameterNames()
请求参数的名称,可以是URL查询字符串参数或表单参数
getParameter(String pname)
获取指定参数名对应的参数值
getParameterValues(String pname)
获取指定参数名对应的参数值,参数值是一个数组,应对同一个参数有多个值的情况
getParameterMap()
获取参数名-参数值的Map,参数值是一个数组
新手需要注意的两个问题:
- 字符编码通过Content-Type设置,所以首先要能读取Content-Type,但读取Content-Type需要知道编码,本质上是一个先有鸡还是先有蛋的问题。主流的解决方案是解析Content-Type时使用Servlet容器(如Tomcat)默认的编码,默认值是ISO-8859-1。目前URL、HTTP头、Cookie都是通过默认编码解析的。
- ServletRequest的输入流只能读取一次,Servlet规范定义了HttpServletRequestWrapper,通过扩展Wrapper实现,如Spring就提供了ContentCachingRequestWrapper。
2. 输出
ServletReponse的定义要相对简单的多
分组
方法
解释
元信息
setContentType(String)
设置响应给客户端内容的MINE类型
setCharacterEncoding(String)
设置响应给客户端内容的编码
setLocal(Locale)
响应的区域设置
输出流
getOutputStream()
获取输出流,用于输出客户端内容,需要自己将响应内容转换为byte数组
getWriter()
获取输出流,直接输出字符,使用setCharacterEncoding指定的编码转换为字节数组
3. 处理
Servlet是整个Java Web服务的核心,它负责读取输入,处理业务逻辑,最终生成输出内容。我们来看一下Servlet的继承结构
- Servlet定义一个service方法,接收ServletRequest参数用来读取输入,通过ServletResponse向客户端响应数据。
- GenericServlet在Servlet的基础上,提供了ServletContext(Servlet上下文),Servlet配置信息。
- HttpServlet则进一步将service方法安装HTTP METHOD的值,拆分为GET、POST、PUT等等。
1.2 JSP
从Servlet的定义来看,一开始的设计还是相当的直观和简单的,只是定义了输入-处理-输出这3个实体,这也印证了架构是一个逐步演进的过程,需求才是架构的主要驱动力。HTML本身是比较复杂的,需要大量的模板代码,通过Java代码来拼接,代码会显得十分拖沓。JSP的目标就是让HTML的生成变得简单,可以认为它是现代模板(如Thymeleaf)的前身,通过将Java代码、自定义标签(JSTL)内嵌到HTML代码中来生成最终的响应内容。
当客户端请求JSP时,WebServer容器会调用JSP引擎(JSP Engine),将JSP编译为Servlet,再由Servlet引擎(Servlet Engine)执行Servlet,处理输入,响应输出,一起看起来都那么的顺利成章
我们来看一个简单的JSP示例,好让我们对JSP有一个直观的感受,可以说JSP已经具备所有必须的基本能力。
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <%-- 注释 --%> <% String name = "John Doe"; %> <%-- if语句 --%> <% if (age >= 18) { %>
This person is an adult.
<% } %> <%-- for循环 --%>- <% for (String hobby : hobbies) { %>
- <%= hobby %> <% } %>
Name is not provided.
- ${hobby}
从JSP这一小节我得到的启示:
- 技术选型的采用率 = 易用性 * 能力 ,易用意味着开发效率,从Servlet到JSP,从Spring到SpringBoot,从MapReduce到Hive,概莫不如是。
- 从第1点提取模式,价值 = 界面友好 * 内核强大,对软件工程师来说,你的技术能力就是你的内核,你的沟通表达、自我驱动力等软技能就是你的界面,强强结合才能最大化价值
- 了解一些编译原理,学习一个词法和语法分析生成器(如JavaCC),能够拓展软件工程师的能力边界
2. Model 1 & Model 2
使用Servlet和JSP已经完成功能开发了,Model 1和Model 2其实是使用Servlet和JSP过程中总结的最佳实践。随着业务的发展,业务的复杂性增长,JSP遇到了两个问题:
1. 内嵌大量的Java代码,业务逻辑和展现层逻辑混合,可读性差,不好理解
2. 代码复用困难,功能重复编写,可维护性差,后续升级困难
3. 调试困难,业务逻辑都在JSP中,所有的HTML、Javascript和Java都混在一起
Model 1中JSP负责接收用户请求,业务流程控制,组装展现层数据,返回响应结果。从Model 1开始已经萌发了将部分业务逻辑转移到JavaBean的观念。
Model 2往前再迈了一步,结合Servlet和JSP的有点,Servlet做为一个Controller,处理请求并构造JSP需要的数据;JSP做为表示层,不再处理业务逻辑.
3. Struts 1.x
Struts 1.x实际上是基于Model 2实现的一个MVC框架,通过Actionservlet接收用户的请求,详细的工作流程如下图
- 在应用启动时,Servlet容器会加载web.xml,初始化Struts的ActionServlet,并解析struts-config.xml
- 用户发起请求,Servlet容器根据web.xml,找到对应的servlet-mapping,如果是ActionServlet,将控制权转交
- ActionServlet根据请求url,查询struts-config.xml的配置,找到对应的ActionMapping
- 根据配置创建对应的ActionForm,填充数据并调用validate方法完成参数校验
- 根据ActionMapping将请求转发给对应的Action,并调用execute方法,返回ActionForward
- ActionServlet处理Action的返回,根据ActionForward展示对应的JSP试图
我们来看一下最简单的Struts 1.x的示例,首先需要在web.xml中配置ActionServlet,让所有待处理的请求经过ActionServlet转发,指定structs-config.xml的位置
acton org.apache.struts.action,ActionServlet config /WEB-INF/struts-config.xml action *.do struts-config.xml用来配置哪些路径是有哪个action来处理的,下面是一个极简的示例,主要关注的是form-beans、action-mappings
form-bean里配置的元素需要继承ActionForm,我们看一个极简的示例
public class LogonForm extends ActionForm { private String userName; private String password; public ActionErrors validate(ActionMapping mapping, HttpServletRequest request) { return null; // 数据验证,验证失败返回ActionErrors } public void reset(ActionMapping mapping, HttpServletRequest request) { // 数据初始化 } }
action可以直接配置JSP,也可以配置Action实现类,下面是一个Action的实现类
public class LoginAction extends Action { public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { LogonForm loginForm = (LogonForm) form; // 业务逻辑处理 } }
在当时Struts 1.x也是霸主级的存在,当初技术公司使用的技术栈基本都是SSH,而这里的第一个S就是代指Struts 1.x,它主要的价值是:
- 基于Model 2,是MVC架构的早期实现者之一,统一了开发规范
- 简化了Web开发,封装底层的HTTP请求和响应处理,提供了Struts标签库
4. Spring MVC
一开始人们都认为Struts 1.x是足够好的,直到Struts 2.x(WebWork)和Spring MVC出世,人们意识到了问题,Spring MVC的优势是:
- 代码无侵入设计,架构层层演进,模块化清晰,提供丰富的自定义和扩展能力
- 天然和Spring无缝集成
- 不强耦合Servlet API,不强耦合框架代码,方便单元测试编写
- 自主选择表示层技术,整合比JSP更具表现力、学习成本更低的模板
应该承认的是,在Spring MVC刚刚出现的时候,和Struts 1.x对比,不存在决定生死的优势。因为Spring本身的流行,加上Spring MVC确实在表现比Struts 1.x略好,慢慢Spring MVC开始流行了。下图是Spring MVC处理一个请求的流程,除了部分扩展点,整体上和Struts 1.x还是相似的:
- Struts里的ActionServlet对应DispatcherServlet;
- Struts里的structs-config.xml对应-servlet.xml
- Struts里的ActionMapping对应HandlerMapping
- Sturts里的Action对应Handler
- Struts里的JSP对应View
Spring MVC提供了额外扩展能力,让用户可以自定义部分逻辑:
- 自定义HandlerMapping来处理请求和Handler的关系
- 自定义HandlerAdapter调用自定义的Handler类型
- 自定义HttpMessageConverter自定义数据类型转换
- 自定义DataBinder完成数据转换、格式化、验证
- 自定义ViewResolver自定义视图名到View的转换
- 自定义View实现Model到向客户端输出内容的转换
5. Spring Boot
应该说对于一个没有去了解过动态语言(Python、Ruby等),没有了解过现代Web框架的人来说,Spring MVC已经够好了。然而即使是开发一个最简单的Web应用,你还是需要配置web.xml,确定HandlerMapping,打包部署,经历一整套复杂的流程。得益于内嵌式Servlet容器,Spring Boot提把Servlet容器整合到同一个fat jar中,让Spring Boot能够自启动。
--未完待续--
6. 未来展望