SpringMVC执行原理
一. SpringMVC 执行过程如下图所示
执行过程如下:
图解:
①:前端控制器接受客户端浏览器的请求。
②:前端控制器调用HandlerMapping查找Handler,HandlderMapping返回一个执行链。
③:前端控制器调用HandlerAdapter。
④:HandlerAdapter执行Handler,并返回一个ModelAndView给前端控制器。
⑤:前端控制器调用视图解析器,将ModelAndView中的逻辑视图(加前缀加后缀)解析成真正的视图,并返回到View。
⑥:对得到的View进行视图渲染:将模型数据填充到request域中。
⑦:前端控制器响应请求,将View返回给客户端。
首先先清楚一点,如何通过传的/hello链接便能够找到自己对应的类并去执行它:在原生的Servlet中这样实现的
<servlet-name>default</servlet-name>
<servlet-class>cn.dglydrpy.study.smartmvc.controller.BizController</servlet-class>
<servlet-name>default</servlet-name>
<url-pattern>/hello</url-pattern>
通过在web.xml中配置servlet-mapping来映射该链接要执行的JAVA类中的某个方法,该类必须继承HttpServlet 类并且去实现doGet()和doPost()方法。过程繁琐复杂. 这种繁琐的工作由SpringMVC去解决,那SpringMVC是怎么解决映射关系的呢?
1.要想的第一点是,我们并没有指定要找的类,MVC是如何通过我们传入的链接找到的呢?
原理是:在web容器启动的时候,便将我们通过访问链接执行的类和方法全部加载到了容器中,这些方法上有和我们传入的链接一样的标识,这样便能定位到具体要执行的方法.代码如下:
public class ContextConfigListener implements ServletContextListener {
public HandlerMapping loadControlers(String xml) throws Exception {
SAXReader reader = new SAXReader();
InputStream in = ContextConfigListener.class.getClassLoader().getResourceAsStream(xml);
Document doc = reader.read(in);
in.close();
Element root = doc.getRootElement();//beans
List<Element> list=root.elements("bean");
HandlerMapping mapping = new HandlerMapping();
for(Element e:list) {
String className=e.attributeValue("class");
mapping.init(className);
}
return mapping;
}
public void contextInitialized(ServletContextEvent e) {
try {
ServletContext ctx = e.getServletContext();
HandlerMapping mapping = loadControlers("smartMVC.xml");
String path = ctx.getContextPath();
ctx.setAttribute("root", path);
ctx.setAttribute("handlerMapping", mapping);
}catch(Exception ex) {
ex.printStackTrace();
throw new RuntimeException(ex);
}
}
public void contextDestroyed(ServletContextEvent arg0) {}
}
ServletContextListener它能够监听 Web 应用的生命周期。当Servlet 容器启动或终止Web 应用时,会触发ServletContextEvent 事件,该事件由ServletContextListener 来处理。里面有两个方法contextInitialized(初始化)和 contextDestroyed (销毁). Servlet容器启动执行初始化方法,这时候需要利用初始化方法将我们需要的Handler加载进去,调用loadControlers()方法,传入用户配置的smartMVC.xml 如下:
<bean class="cn.dglydrpy.study.smartmvc.controller.BizController"></bean>
<bean class="cn.dglydrpy.study.smartmvc.controller.EmpController"></bean>
<bean class="cn.dglydrpy.study.smartmvc.controller.UserController"></bean>
将我们的Controller类(Handler)配置到xml中(相当于@Controller注解),然后解析xml.到了这一步仅是通过解析xml,读取xml的字符串,然后运用反射能够实例化配置的Handler。那么真正执行的方法是怎么标识的呢,来看下一步
2. 如何标识并加载方法的呢?
HandlerMapping mapping = new HandlerMapping();
for(Element e:list) {
String className=e.attributeValue("class");
mapping.init(className);
}
return mapping;
上述代码 只是提取了xml的标签内容,真正执行反射和加载在上面这一小段代码中.我们需要将方法加载到HandelMapping映射器,使用了init()方法代码如下:
public class HandlerMapping {
private Map<String, RequestHandler> handlerMap = new HashMap<>();
public void init(String className) throws Exception{
Class cls = Class.forName(className);
Method[] methods = cls.getDeclaredMethods();
Object controller = cls.newInstance();
for(Method method:methods) {
RequestMapping ann = method.getAnnotation(RequestMapping.class);
if(ann!=null) {
//找到注解上标注的路径
String path=ann.value();
//创建请求处理器对象,封装控制器对象和方法
RequestHandler handler=new RequestHandler(method,controller);
//请求路径和对应的“请求处理器”添加到map
handlerMap.put(path, handler);
}
}
}
}
为了方便利用java反射调用方法,我们把方法对象和配置的类(Handler)实例进行封装.
public class RequestHandler {
private Method method;
private Object controller;
public Method getMethod() {
return method;
}
public void setMethod(Method method) {
this.method = method;
}
public Object getController() {
return controller;
}
public void setController(Object controller) {
this.controller = controller;
}
public RequestHandler(Method method, Object controller) {
this.method = method;
this.controller = controller;
}
}
上述便是通过反射实例化类(都是java.lang.reflect中的方法,不懂可查看API文档,不再说明),然后获取方法数组并且循环方法,以此封装到Map集合中。这时候有个疑问,map集合中的value是封装的RequestHandlel类参数是方法和类,那key(path)是啥呢,便是RequestMapping注解上的路径(反射获取),注解代码如下:
@Retention(RetentionPolicy.RUNTIME) //运行期
@Target(ElementType.METHOD) //在方法上使用
public @interface RequestMapping {
public String value();//默认value属性
}
不懂可查看JDK 1.8API文档
@RequestMapping("/hello.do")
public String hello(){
System.out.println("Hello.do");
return "hello";//返回视图名
}
这时候就清晰明了了,注解的内容“/hello.do”作为key , 映射的value便是该注解的方法和类.这样便实现了一一映射关系。当我们启动web服务的时候,这些带有@Controller和@RequestMapping的类和方法便会加载到HandleMapping中,交给MVC来管理,初始化工作结束。
3. 之后的流程就简单了,每次访问链接,链接内容便是作为key,然后查找HandlerMapping中集合的key,如果在,便返回value(要执行的类和方法)。然后利用反射的invoke()方法动态代理去执行真正的方法,然后返回参数(ModelAndView).之后交给视图解析器,去渲染。有时间这些代码再写(因为很好理解了)。
注:很多文档都是写MVC的工作流程,困扰了我很久HandleMapping究竟是个啥,我便从GitHub上扒下了一个模拟MVC执行流程的代码。写下这篇文档希望能够帮助大家理解HandleMapping是怎么工作的.如何通过我们传入的链接便能执行要执行的方法,代码是在GitHub上找的模拟SpringMVC的执行。如果有需要的可以私聊我,我把代码发给你(免费)。