javaWeb的开发经历了多个阶段;
刚开始时,由于框架等不够成熟,主要使用serlvet,jsp等技术实现,如果需求较为简单,当然可以胜任,不过随着项目复杂度的增加,这种略显"混乱"的方式便有些力所不逮了。
之后SpringMVC的出现,使得开发可以将更多的工作量放在核心任务之上,虽然还是需要做一定的配置,但总的来说,已经很方便了。
这里对比JSP
的开发方式,来看一下SpringMVC
的代码逻辑。
出于易懂的初衷,一般使用很简单的代码来查看整个流程,暂时不考虑逻辑的合理性
在古老的servlet时代,后台都是用java代码将html数据一行行打印生成的,代码量极为庞大,这里以一个简单的servlet来看:
TestAServlet.java:
public class TestAServlet extends HttpServlet implements CommonI {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html");
PrintWriter out = resp.getWriter();
out.println("");
out.println("");
out.println("Hello World! ");
out.println("");
out.println("");
out.println("Hello World!
");
out.println("");
out.println("");
out.close();
/*var a =req.getRequestDispatcher("/gc/abc");ing
a.forward(req,resp );*/
}
}
该类继承自 HttpServlet
,CommonI
是自定义的用于获取 Logger
的接口,不必过多考虑,Servlet的继承结构图如下:
这也是Servlet
经典继承关系图:
Servlet
接口定义了模版方法:
public interface Servlet {
void init(ServletConfig var1) throws ServletException;
ServletConfig getServletConfig();
void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
String getServletInfo();
void destroy();
}
出于简单的原则,我们只看个大概就好:
init
方法用于初始化操作,这个从方法名字也可以看得出来,一般整个生命周期内只调用一次。destroy
方法在容器销毁时才会调用。service
方法在每次有客户端请求到来时都会调用,用于处理主要的业务逻辑。GenericServlet
是个抽象类,主要是使用代理模式
在类中保持一个ServletConfig
对象,用于获取各种参数或者配置信息(还提供了log方法)
HttpServlet
抽象类主要是根据Method
的不同,将请求路由到不同的方法中:例如**将 GET 请求路由到 doGet 方法 **,至于其他功能,处于简单的情况考虑,都不重要
因此,我们在TestAServlet
中重载了doGet
方法,就可以正确的处理GET请求,然后按照一贯的做法,在web.xml
中,我们配置一下拦截路径就可以了:
test
com.knowledge.mnlin.znd.TestAServlet
true
test
/servlet
这也是之前Servlet开发
的基本流程,此时在浏览器中访问:
http://localhost:8080/servlet
就可以获取返回信息:
Hello World!
Hello World!
从这里看到,html
显示的所有内容,包括一类的标签,都是通过
out.println()
直接写出来
的
但这种方式的弊端很容易发现,代码太过繁琐,可视性等都很差,如果再加上对于或者
的处理,那更是不堪重负,对于简单请求还好,其他的就算了。
不过通过Servlet,更容易去把控web框架是如何处理请求信息的。
结合上面的例子,即便我们没有去查看tomcat的源码,也很容易反向推测出被成为Web容器的tomcat,在这个过程中的作用:监听某个端口;解析处理Http协议
解释的说:建立socket
连接,因为http只是协议,无法直接监听某个端口(如8080),获取网络数据,因此需要在服务端建立socket-server
,当有客户端发送请求时,其实是向 8080 端口 发送了一段数据,该端数据满足 http 协议,经过 tomcat 处理后,转化为可以使用的对象,然后在 servlet 中进行处理。
我们抛却其中可能复杂的部分,以简单的逻辑示图:
JSP有九大内置对象,这个做开发时,应该都有了解,但对于JSP
文件生成的代码(class类),可能不会做太多的关注,这里以一个例子查看jsp
到底是怎么被jvm
使用的。
当前有一JSP文件:start.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
String path = request.getContextPath();
String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/";
%>
<%=basePath.substring(0, 1)%>
静止的首页信息:
<%=basePath%>
功能很简单:**获取请求的路径信息,并进行显示,比如请求路径为:
http://localhost:8080/start.jsp
那么,客户端将收到服务端返回的信息(假设路径能正确响应):
<html>
<head>
<title>h
title>
head>
<body>
静止的首页信息:
<br>
<br>
http://localhost:8080/
body>
html>
从上面的html
代码可以看到,框架对于jsp
的处理,就是将其中有效的以 <% .* %>
声明的部分,或者一些已定义好的标签,用真实的数据进行填充,然后将结果返回给客户端浏览器进行显示。
我们可以查看该jsp
对应生成的java/class
文件,该文件一般存放在tomcat目录
下,类似这样:
E:\apache-tomcat-9.0.12\work\Catalina\localhost\war_create\org\apache\jsp\start_jsp.java
E:\apache-tomcat-9.0.12\work\Catalina\localhost\war_create\org\apache\jsp\start_jsp.class
即:
tomcat安装目录\work\Catalina\localhost\项目名\org\apache\jsp\*
先查看 java
文件:start_jsp.java
public final class start_jsp extends org.apache.jasper.runtime.HttpJspBase
implements org.apache.jasper.runtime.JspSourceDependent,
org.apache.jasper.runtime.JspSourceImports {
private static final javax.servlet.jsp.JspFactory _jspxFactory =
javax.servlet.jsp.JspFactory.getDefaultFactory();
private static java.util.Map<java.lang.String,java.lang.Long> _jspx_dependants;
private static final java.util.Set<java.lang.String> _jspx_imports_packages;
private static final java.util.Set<java.lang.String> _jspx_imports_classes;
static {
_jspx_imports_packages = new java.util.HashSet<>();
_jspx_imports_packages.add("javax.servlet");
_jspx_imports_packages.add("javax.servlet.http");
_jspx_imports_packages.add("javax.servlet.jsp");
_jspx_imports_classes = null;
}
private volatile javax.el.ExpressionFactory _el_expressionfactory;
private volatile org.apache.tomcat.InstanceManager _jsp_instancemanager;
public java.util.Map<java.lang.String,java.lang.Long> getDependants() {
return _jspx_dependants;
}
public java.util.Set<java.lang.String> getPackageImports() {
return _jspx_imports_packages;
}
public java.util.Set<java.lang.String> getClassImports() {
return _jspx_imports_classes;
}
public javax.el.ExpressionFactory _jsp_getExpressionFactory() {
if (_el_expressionfactory == null) {
synchronized (this) {
if (_el_expressionfactory == null) {
_el_expressionfactory = _jspxFactory.getJspApplicationContext(getServletConfig().getServletContext()).getExpressionFactory();
}
}
}
return _el_expressionfactory;
}
public org.apache.tomcat.InstanceManager _jsp_getInstanceManager() {
if (_jsp_instancemanager == null) {
synchronized (this) {
if (_jsp_instancemanager == null) {
_jsp_instancemanager = org.apache.jasper.runtime.InstanceManagerFactory.getInstanceManager(getServletConfig());
}
}
}
return _jsp_instancemanager;
}
public void _jspInit() {
}
public void _jspDestroy() {
}
public void _jspService(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response)
throws java.io.IOException, javax.servlet.ServletException {
// 主代码部分,下面会详细介绍
}
}
这里粘贴出该类所有的代码方便解析。我们先从继承关系来看该jsp文件的结构
相比较于第一部分介绍Servlet时的 TestAServlet
:JspPage
、HttpJspPage
、HttpJspBase
所做的事情,只是提供了init
和destroy
生命周期时的模版方法,再就是将之前依据Method
分开的处理逻辑,统一又交还给了_jspService
方法。
_jspService
方法里面的逻辑层次很清晰:
首先判断请求的Method
,jsp 只支持四种方式的请求:GET, HEAD, POST, OPTIONS
,如果不是这四种,则直接抛出 405 错误码:METHOD_NOT_ALLOWED
if (!javax.servlet.DispatcherType.ERROR.equals(request.getDispatcherType())) {
final java.lang.String _jspx_method = request.getMethod();
if ("OPTIONS".equals(_jspx_method)) {
response.setHeader("Allow","GET, HEAD, POST, OPTIONS");
return;
}
if (!"GET".equals(_jspx_method) && !"POST".equals(_jspx_method) && !"HEAD".equals(_jspx_method)) {
response.setHeader("Allow","GET, HEAD, POST, OPTIONS");
response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "JSPs only permit GET, POST or HEAD. Jasper also permits OPTIONS");
return;
}
}
然后是对于 JSP 内置对象的处理:
final javax.servlet.jsp.PageContext pageContext;
javax.servlet.http.HttpSession session = null;
final javax.servlet.ServletContext application;
final javax.servlet.ServletConfig config;
javax.servlet.jsp.JspWriter out = null;
final java.lang.Object page = this;
javax.servlet.jsp.JspWriter _jspx_out = null;
javax.servlet.jsp.PageContext _jspx_page_context = null;
接下来是对于这些对象的赋值操作
response.setContentType("text/html;charset=UTF-8");
pageContext = _jspxFactory.getPageContext(this, request, response,
null, true, 8192, true);
_jspx_page_context = pageContext;
application = pageContext.getServletContext();
config = pageContext.getServletConfig();
session = pageContext.getSession();
out = pageContext.getOut();
_jspx_out = out;
除了不经常使用的 exception
对象,剩余的八个都在这里定义好了:
pageContext
属于 JSP 加入的,可以进行一些数据的保存读取等等。具体的功能可以参照实现类:org.apache.jasper.runtime.PageContextImpl
page
对应了该class类自身,也就是this
session
自不必说,项目开发中使用的很多,详细 创建过程可以参照源码:org.apache.catalina.connector.Request
与org.apache.catalina.connector.RequestFacade
application
和config
包含的信息比较多,详细功能可参照接口参数。request
和response
,使用的也很多,_jspService
方法参数传入。out
对象其实是从response
获取到的输出流对象,具体代码可参照org.apache.jasper.runtime.JspWriterImpl
;this.out = this.response.getWriter();
因此,在JSP中,我们才可以直接去使用这些对象,而不用去显式的创建,更根本的说:JSP开发和Servlet开发并无区别,只是开发效率上有了很大的提升。
好了,简单剖析了一下 JSP 和 Servlet ,可以看到,javaEE中最为麻烦的部分,其实 tomcat 已经帮我们处理好了,对于简单的业务需求,完成起来并会特别耗费力气。
但其中路径拦截处理是在很麻烦,需要一直去配置,更麻烦的是,随着业务逻辑的复杂性提高以及需求的变更,使用jsp来进行开发,着实不太方便。
使用SpringMVC
进行开发时,会发现配置文件很少,对于web.xml
,基本上不需要做什么大的配置,如果不进行国际化或者主题切换(已当前来看,这种需求前端可以自行完成),那么只需要完成两个文件的处理就好:
web.xml
这个配置是不可缺少的,毕竟tomcat容器依据的就是此文件;在该文件中,我们只需要配置一个Servlet,然后拦截所有的网络请求即可;该Servlet即为org.springframework.web.servlet.DispatcherServlet
。
<servlet>
<servlet-name>springmvcservlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServletservlet-class>
<init-param>
<param-name>contextConfigLocationparam-name>
<param-value>/WEB-INF/contextConfigLocation.xmlparam-value>
init-param>
servlet>
<servlet-mapping>
<servlet-name>springmvcservlet-name>
<url-pattern>/url-pattern>
servlet-mapping>
contextConfigLocation.xml
,该文件用于配置 SpringMVC
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<mvc:annotation-driven/>
<context:component-scan base-package="com.knowledge.mnlin" use-default-filters="true"/>
beans>
默认的配置信息很少,SpringMVC 本质就是单Servlet应用,在一个Servlet中完成所有的路由选择,请求处理等信息。
之后为了简写,使用 mvc 代替 SpringMVC
跟之前两部分一样,我们根据org.springframework.web.servlet.DispatcherServlet
的继承结构,来简单说明 mvc 如何工作的,不过限于篇幅以及mvck框架结构的复杂性,只做大概性的说明。
org.springframework.web.servlet.DispatcherServlet
继承关系图:
这里只看中间主线的继承关系,相比于Servlet开发模式,这里又多了三层:
HttpServletBean
主要是把xml中配置的servlet参数信息设置到类的属性中。
FrameworkServlet
初始化了WebApplicationContext
,WebApplicationContext
类的功能可以从其注释得到:
Interface to provide configuration for a web application. This is read-only while
the application is running, but may be reloaded if the implementation supports this.
FrameworkServlet
还使用LocaleContextHolder
保存了当前请求的Locale
信息,使用RequestContextHolder
保存了此次请求的request
信息,并在本次请求处理结束后进行了还原。
DispatcherServlet
做的事情虽然比较多,但层次很清晰;
在容器init
阶段,进行一些全局信息的初始化,主要是初始化 mvc 的几个内置对象
protected void initStrategies(ApplicationContext context) {
//处理包含文件上传的请求
initMultipartResolver(context);
//处理Locale,对于一些需要做国际化等等的项目,可能需要进行环境处理
initLocaleResolver(context);
//处理 theme,同locale一样,暂时不考虑这个
initThemeResolver(context);
//handler-mapping,根据request来确定使用哪个处理器处理网络请求
initHandlerMappings(context);
//当确定了用来处理本地请求的处理器时,需要找到对应的adapter,adapter用于指定处理器如何处理该请求
initHandlerAdapters(context);
// 当处理器模块出错时,会调用这个配置的对象去获取默认的视图信息
initHandlerExceptionResolvers(context);
//当View层没有值时,通过该方法获取一个ViewName
initRequestToViewNameTranslator(context);
//view 层获取到值时,通过解析来获取真正的 渲染的视图对象
initViewResolvers(context);
//在 重定向 时,获取第一次请求的参数信息
initFlashMapManager(context);
}
这些内置对象在下面还会做出解释,这里先看一个大概。
在 请求到来时,每次都会调用doService
方法,该方法会在真正处理请求前,将一些locale,flashMap,theme
等信息添加到request对象中,但这些对象一般我们并不用,所以直接略过,直接查看“真正”的方法doDispatch
doDispatch
会先检查是否为文件上传请求,是的话调用initMultipartResolver
方法配置的内置对象先处理一次;然后,根据initHandlerMappings
配置的 一些 hanlder-mapping,找到适合的处理器:handler
;然后根据initHandlerAdapters
配置好的数据,找到适合该处理器的适配器:adapter
;然后结合adapter
和handler
从本次request
得到 一个ModelAndView
,看这个名字就很清楚,包含了view
和model
两个模块,一个视图,一个数据,进行数据填充后就可以得到最终的返回数据(当然,这个填充数据的过程需要使用initViewResolvers
配置好的view-resovler
来获取可用代码处理的view
层)。
正常的逻辑基本就像上面所述,当然如果中间出现了异常,或者说view层为null,那就需要使用到initHandlerExceptionResolvers
以及initRequestToViewNameTranslator
配置的内置对象了。
好了,目前为止,已经浅析了 servlet 与 mvc 模式以及部分源码,关于两者之间可使用的“内置对象”,也做了简单的说明,不过,仅仅从源码,我们无法开出springmvc 开发的简便之处,这里可以使用一个例子来进行说明:
@Controller
@RequestMapping(value = "/gc")
public class GoController implements EnvironmentAware, CommonI {
/**
* 处理GET类型的"/index"和”/ind”请求
*
* 匹配 @RequestMapping 注解的是 RequestMappingHandlerMapping
*/
@RequestMapping(value = {"/index", "/ind"}, method = {RequestMethod.GET})
public String index(Model model) throws Exception {
getL().info("get请求:访问index或者ind");
model.addAttribute("msg", "Go Go Go!");
return "index.jsp";
}
}
相比于 servlet 的需要在 xml 中进行路径配置,mvc只需要使用注解
就可以实现同样的需求,并且结构更为简单。
这些注解
是由 mvc 框架自动解析配置的,无需进行“人工”干涉;之前 servlet 开发好像将访问路径拦截到了具体的HttpServlet子类
,mvc 像是根据访问路径先拦截到具体的标注了@Controller
的类,然后又拦截到了类中具体的方法
(事实上并非这么简单,只是在理解上可能会比较方便一些);
如果查看 mvc 具体负责请求处理的类源码,会很复杂,如果有兴趣可以参照看透springmvc源代码分析与实践;框架可能会在更迭中有所升级,不过核心原理一般不会变更。这里仅以 debug 模式
来查看一下,mvc 自带的 “内置对象” 是哪些类:
按照前面initStrategies方法
中内置对象出现的顺序,在初始化之后,我们可以查看到各个内置对象的值(有些是列表,有些是单个对象):
multipart-resolver
:mvc 框架内置默认没有处理文件上传,需要的话可以自己定义。locale-resolver
:语言环境处理,默认有实现,不过用的比较少。theme-resolver
:主题切换处理,默认有实现,用的较少flash-map
:重定向时使用,用session
实现,知道使用就好然后看剩余的几个:
request-to-view-name-translator
:默认实现是DefaultRequestToViewNameTranslator
类,当没有明确指定 view 时,会从 request
中取得请求路径,然后加上前后缀
,这里前后缀都是空串:""
,因此取值就是路径@Override
public String getViewName(HttpServletRequest request) {
String lookupPath = this.urlPathHelper.getLookupPathForRequest(request);
return (this.prefix + transformPath(lookupPath) + this.suffix);
}
view-resolver
:当返回的 ModelAndView
中 view 是字符串
时,我们需要解析对应的真正可使用的 view 层对象,view-resolver
是个 list
,因此可以存储多个实现类,它们之间的优先级是根据实现的 Ordered
接口来的,如果前面的view-resolver
不想进行解析,可以在resolveViewName
中直接返回null
,这样框架会调用第二个解析器去进行处理。这里有个默认的实现类:InternalResourceViewResolver
,可以返回InternalResourceView
,该 view 对应的就是 jsp 类型的视图。然后还剩下了最后三个对象,这是 mvc 框架最核心的部分,也是我们之前注解真正起作用的地方,这里会说明每个对象大概完成的功能,具体类的实现如果有兴趣可以参照源码进行比对:
handler-mapping
:list类型,有三个元素,用于寻找请求对应的处理器(controller):
handler-adapter
:list类型;之前找到了处理器,还需要知道如何使用该处理器去处理请求;系统同样默认了三个实例:
RequestMappingHandlerAdatper
:配合RequestMappingHandlerMapping
,mvc 框架中最核心的类,规范如何调用处理器,在处理器调用前做的初始化操作,参数赋值等等。HttpRequestHandlerAdapter
:处理实现了HttpRequestHandler
接口的处理器,框架会自动调用其handleRequest
方法处理本次请求。SimpleControllerHandlerAdapter
,处理实现了Controller
接口的处理器。handler-exception-resolver
:也是一个列表,这里默认实现有三个:
ExceptionHandlerExceptionResolver
:处理器为HandlerMethod
类型时,如果出现异常,则使用该对象进行处理;该对象一般会选择合适的注解了@ExceptionHandler
的方法。ResponseStatusExceptionResolver
:从名字便可以看出,主要处理@ResponseStatus
注解标注的方法产生的异常。DefaultHanlderExceptionResolver
:主要处理一些通用异常,如“METHOD”不支持
,handler未找到
等等。ok,经过上面的分析,应该有了一个 mvc 大概的处理流程,现在我们不妨彻底的“自定义”一把,把上面的九大组件都自己实现一次,然后查看效果;当然,这些组件需要先到contextConfigLocation.xml
中配置好;
自定义的组件都是以First***
开头,如果组件是单个对象,则会被替换,如果是List
列表,则会添加一条数据
,这样,我们就可以针对整个系统流程进行拦截。
以上就是对 mvc 架构的一些浅析,当然,设计到具体处理器的环境,肯定不止于此,不过了解这些至少可以在有需求时进行部分的定制。
这里附上:GITHUB:mvc-demo,可以查看文中出现的项目代码部分。
注:文中部分图片引用自他人blog:
BeanNameUrlHandlerMapping
与SimpleUrlHandlerMapping
解释图引用自:SpringMVC 配置式开发-BeanNameUrlHandlerMapping(七)