最近读完了Servlet与JSP学习指南,想结合以前对反射和AOP的学习,以及对SpringMVC目前的了解,进行一次总结。
下面的内容基于Servlet3.0。且不包括理论部分知识,纯粹讲述JDK所提供的接口及特性,和它们可以用于实现什么。
Servlet应用中,有3大类比较重要的接口,包括Servlet,Filter,Listener。
Servlet是JAVA中的一个用于WEB开发的接口,是JAVA EE的开发规范中最基础的API模块之一(另外两个最基础的在下认为还包括JDBC,JSP),其本身需要实现以下方法:
init(ServletConfig) Servlet实现类初始化时需要执行的操作,其中通过ServletConfig类可以获得①初始化参数(web.xml中配置的context-param元素),Servlet的名称以及context;
getServletConfig() 获得ServletConfig对象;
getServletInfo() 返回自定义字符串,可以输出Servlet的描述;
destroy() Servlet销毁时所需要执行的操作;
service(ServletRequest, ServletResponse) Servlet实现类最常用的方法,用于处理所有发送到这个Servlet中的请求;
目前的servlet-api.jar中对原始的Servlet接口进行了扩展,在javax.servlet.http包下增加了一个HttpServlet抽象类,初步对于HTTP方法进行了处理,但仍需要开发者自己重写doGet,doPost等方法。
Servlet的代码编写完成后,有两种配置方法,一种是编程式配置,通过使用@WebServlet注解,关键属性有name,urlPatterns,也可以在此处增加初始化参数initParams;
二是通过部署描述符在web.xml文件中进行部署,每个servlet必须要有一个
load-on-startup元素(必须为>=0的整数)表示servlet会在应用启动时就进行初始化,数字表示优先级。若有多个servlet优先级相同,则无法预知初始化的顺序。
Servlet可以支持异步处理。Servlet容器是会根据服务器的性能极限设置相应的线程限制的,每一个请求都会占用一个线程,所以若是某有请求是需要执行某个需要长时间运行的任务时,当这一类请求数量多了,会导致线程队列拥挤,后续进入的线程由于超出数量限制而无法得到处理。
此时可以在Servlet配置中添加async-supported元素并设置成true,然后通过request.startAsync()得到一个AsyncContext对象,该对象可以设置timeout,并调用start(Runnable)方法启动异步线程。在线程结束时调用AsyncContext.complete()方法停止异步,否则该对象会等待timout后才会触发complete事件。
Filter拦截器会在请求数据进入Servlet之前先对请求进行拦截,可以对请求进行验证,过滤,包装等等预处理。Filter可以配置多个,多个Filter可以形成一个Filter Chain,方便在不修改原有Filter的基础上进行扩展。
Filter 接口有以下方法
init(FilterConfig),Filter的初始化方法;
doFilter(ServletRequest,ServletResponse,FilterChain),Filter的执行方法,可以对request和response进行处理;注意方法的末端需要执行FilterChain.doFilter()方法,否则在这个方法结束后不会继续往下执行;
destroy()销毁时会执行的方法。
与Servlet类似,Filter也有编程式和部署描述符两种配置方法。最大的区别在于,使用部署描述符在web.xml中配置可以控制Filter执行的先后顺序(根据配置的从上到下),而编程式配置无法控制。
编程式配置使用@WebFilter注解,有filterName,urlPatterns,initParams等属性;
部署描述符使用
Listener是基于事件驱动的接口,配置并开发好对应的Listener之后,当相应的事件触发时,系统会自动调用对应的方法。Servlet应用中的Listener包括有对ServletContext,Session,Request这3个不同层次的对象的监听,在这个基础上,对每个对象都有对应的生命周期与属性变化的的监听。
ServletContext:
① ServletContextListener 监听ServletContext各生命周期(init,destroy)
② ServletContextAttributeListener监听ServletContext 属性的add,remove,replace操作
Session:
① HttpSessionListener 监听生命周期
② HttpSessionAttribute 监听Session内属性的add,remove,replace操作
③ HttpSessionActivationListener 当Server内存较低时,Servlet容器可能会将当前暂时不使用但未销毁的Session序列化存储到存储设备中,该监听器会监听这个过程中,Session被激活/钝化(相当于休眠)的事件;
④ HttpSessionBindingListenersession变量绑定listener,包含bound和unbound两种事件;当某一个实体类实现了该listener接口,那么当该类的实例化对象通过setAttribute方法添加进Session内进行保存时,会触发该实体类的bound方法,同理,解绑时触发unbound,例如:
public class TestObjimplements HttpSessionBindingListener {
private String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
@Override
public void valueBound(HttpSessionBindingEventhttpSessionBindingEvent) {
System.out.println("TestString is binding to Session");
}
@Override
public void valueUnbound(HttpSessionBindingEventhttpSessionBindingEvent) {
System.out.println("TestString is unbinding to Session");
}
}
当TestObj的实例化对象被添加到Session时,如
httpSession.setAttribute(“key”,testObj);
则会执行TestObj的valueBound方法,输出TestString is binding to Session
⑤ HttpSessionIdListener 查看javax.servlet.http包发现的,通过查阅到的相关资料,是Servlet3.1新增的特性,当开发者通过HttpServletRequest.changeSessionId()方法强制修改当前Session的id时,会触发该listener,暂时不确定该特性有什么用,以及在什么情境下可以用;
Request:
① ServletRequestListener: 生命周期的监听器
② ServletRequestAttributeListener:request属性操作监听器
③ AsyncListener:当Servlet支持异步处理的时候,可以用这个监听器监听异步线程的生命周期,包括开始(onStartAsync),完成(onComplete),超时(onTimeOut),出错(onError)、
Listener配置比较简单,只需要在web.xml中添加
从Servlet3.0开始增加了动态注册特性,可以通过ServletContext动态注册Servlet,Filter,Listener,配合ClassLoader和反射,使web app可以更加方便地以插件的方式进行模块化开发,以及热更新。
接下来谈SpringMVC。
首先谈谈Spring最核心的两个特性,动态注入和事务管理,所使用到的两种思想就是IOC(Inversion Of Control控制反转)和AOP(Aspect-Oriented Programming面向横切面编程)。
IOC,控制反转,顾名思义就是将对象的控制权,学习过JAVA高级特性的应该都知道JAVA里的反射,通过 类名.class,或者静态方法Class.forName方法 获取类的class对象,获取到的对象中包含有对应类的信息,可以使用这个对象对类进行实例化,也可以对应的获取其成员变量(Field)以及方法(Method)的对象。通过以上的类以及所提供的API,就可以结合配置文件,以及配置文件对应的解析器,编写一个通用的对象的“控制器”,接管类的实例化以及各方面的控制,开发者不需要再针对一个个的类编写特定的实例化代码和执行方法的代码,而可以专注于功能模块的开发,实现了底层与高层的解耦,这是工厂模式思想的体现。
Spring 中就是利用了工厂模式,通过配置文件对bean进行统一配置,使用工厂类将应用内的业务模型实例化并存入到内存中(单例模式),在使用时直接从内存中获取业务对象,一则将业务类的使用与实例化进行了解耦,二则节省了大量的对象使用时实例化,用完后又销毁等等重复操作的时间。
AOP,面向横切面编程,其目的就是为了在程序运行的某个横切面注入自己的代码,以达到某种目的,例如记录运行时日志,管理事务等。AOP的具体实现方式是代理,氛围静态代理与动态代理,静态代理由于使用范围小而且比较死板,这里不讨论。
JDK内部有提供动态代理相关的API,同时各大开源社区也有他人所开发的动态代理的工具包,各有不同的优缺点,有兴趣的都可以去了解,这里只讨论JDK提供的代理类。
JDK动态代理的大致流程如下:
JDK动态代理→InvocationHandler,Proxy→Class,Method→JavaCompiler(ToolPrivider),ClassLoader
下面我们将按照这个步骤一步一步由表到里分析这个过程。
首先最表面的,JDK中动态代理的应用,需要使用InvocationHandler接口和Proxy类。它们属于java.lang.reflect包中,也就是说,它们是实现反射特性的一部分。
首先需要编写一个类实现InvocationHandler接口,实现其invoke方法,该方法会传入3个参数(Object proxy,Method method, Object[] args),第一个参数是JDK生成出来的实例化对象,大部分时间并没有什么用,先跳过;第二个参数是Mechod对象,即方法对象,可以理解为指向所代理的类里面所调用的某个方法的引用(个人觉得可以简单理解为C语言中的方法指针类似的东西);第三个是一个Object[]数组,即执行方法所需要的参数列表;在这个invoke方法中,可以编写在执行方法 前/后 所需要注入代码(写执行日志等),然后执行方法,最后返回。
Proxy类,提供了一个静态方法newProxyInstance(ClassLoader loader, Class[] interfaces,InvocationHandler h)来实例化代理对象,该方法传入3个参数:ClassLoader,用于实例化用的ClassLoader,一般将需要代理的类的ClassLoader传入;Class[]数组,需要代理的类所实现的interface数组;InvocationHandler,即自己所编写的handler。具体例子如下:
public interface If {
public void function();
}
public class Cl implements If{
@Override
public void function(){
System.out.println("helloworld");
}
}
public class Handler implements InvocationHandler { private Object obj; public Handler(Object obj){ this.obj=obj; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("start to run "+method.getName()+" method begin."); Object returnObj = method.invoke(obj ,args); System.out.println("start to run "+method.getName()+" method end."); return returnObj; } public static void main(String[] args) { InvocationHandler handler = new Handler(); If proxyIf = (If) Proxy.newProxyInstance(Cl.class.getClassLoader(),new Class[]{If.class},handler); } }
如上所示,就是一个简单的动态代理的实现。从上面的例子中可以引出几个问题:①为什么创建代理需要让类实现一个接口?②为什么创建代理类的过程中需要传入ClassLoader?③handler中invoke方法里的Object proxy到底是什么?
想要解答以上问题,需要从代理的建立过程中看。
进入到Proxy.newProxyInstance方法里读源码(网上也有很多对此的分析文章),发现里面最关键的过程其实就是两步:①生成一个类,使这个类继承Proxy类和If接口,并实现If接口的方法,在每个代理的方法体内调用handler的invoke方法,最后将生成的class的二进制码返回;②用ClassLoader载入这个class,利用反射实例化这个类,并返回;
要实现上述逻辑其实并不难,最简单的方法是自己自动化生成出源代码,然后使用JavaCompiler接口(可通过ToolProvider获得实例化对象)进行编译获得二进制码,再用ClassLoader载入,有兴趣的也可以研究JDK中ProxyGenerator的源码是如何实现的。
Spring 的核心原理大概就这样了,接下来来分析下SpringMvc。
首先要理解SpringMVC的话,肯定离不开MVC模式,MVC即Model/View/Controller,其中Model指的是模型,模型包括业务逻辑中的数据结构的抽象化,以及具体的数据处理方法,负担业务逻辑的具体实现;View视图,可以简单理解是可视化的客户端操作界面,既可以将模型数据以可视化的方式表达,也可以让用户用于与模型之间的互动;Controller控制器,连接起了View与Model,负责对用户发出的请求进行处理,并分派转发到对应的Model中,然后对Model返回的数据返回到对应视图用于显示。SpringMVC具体的工作步骤如下:
1. 用户操作视图发出请求
2. 后台的入口servlet(SpringMVC自带的dispatcherServlet)接受请求
3. servlet调用处理器映射器HandlerMapping→
4. 处理器映射器找到具体的处理器,获得处理器对象并返回→
5. servlet调用handlerAdapter处理器适配器→
6. adapter调用处理器(controller)→
7. 处理器执行完毕,返回ModelAndView→
8. adapter将ModelAndView返回到servlet→
9. Servlet将ModelAndView返回到视图解析器ViewResolver→
10. 解析器处理完毕后返回具体view→
11. Servlet将模型数据渲染填充到view中→
12. 响应请求
到了这里已经可以将前面说的每一个点串联起来了,让我们从Spring应用的启动,到SpringMVC的工作步骤,按顺序来理清它们的实现方式。
1. 配置应用容器,在web.xml中通过context-param写入spring需要读取的xml配置文件的相对路径,配置springMVC的入口servlet(dispatcherServlet)
2. 根据开发者自身需要,配置其他的Filter和Listener
3. 应用启动,Spring读取配置文件,在工厂类中读取到配置文件中的bean的类路径,通过反射和ClassLoader读取class文件并实例化。这里面的实例化分两种:
① 不需要AOP管理的controller类,通过一般的方式实例化即可;
② 需要AOP管理的业务service类,需要使用代理类和接口实例化;
根据配置文件中配置的bean name与具体的实例化后对象匹配,存入一个集合类容器中(Map),然后将该容器存入到HandlerMapping,并放入ServletContext(也可以在Servlet中以静态对象的方式存在),等待调用
4. 应用启动成功,等待请求到来
5. 用户端发送请求,应用容器接收到之后映射到SpringMVC应用入口的Servlet
6. Servlet分析请求信息,根据请求中包含的处理器name,调用HandlerMapping并获得返回的controller
7. Servlet获得请求中包含的方法名或方法信息,调用HandlerAdapter,adapter通过Method类调用方法
8. 方法执行完毕,返回包装好的ModelAndView对象
9. 对ModelAndView对象进行处理并响应给用户
基础的思路整理基本上就如上面那样,当然在下所研读过的代码只有一部分,并不是通读,所以在对框架的分析上面有部分属于自己的猜想,但所猜想的部分也是根据JDK所提供的特性而进行的合乎逻辑的推导,所以应该不算太离谱。
SpringMVC中还有很多其他的特性,例如注解的应用,例如线程安全,等等,还涉及到其他JAVA的高级特性以及并发编程的知识,暂时不在这里展开,留待以后再整理。