MVC - 总控层和Controller映射
引入DispatcherServlet
DispatcherServlet(以下简称DS)负责总管全体的Controller,并与前端页面对接;前端发来的各请求都最终被DS所拦截和处理,并根据具体的参数来调用Controller中对应的方法
DS的任务主要有以下三个:
- 获取调用方法所需的参数资源
- 调用Controller方法
- 控制视图的跳转
Controller映射
DS首先需要对请求进行拦截,这里我选择拦截的标准格式为“*.do”,只要是由“.do”结尾的请求全部被拦截下来,DS会根据“*”中具体的内容按照Controller映射表找到与之对应的Controller
因为我当前只写了一个UserController,因此我将“*”的内容固定为“Login”表示登录,为了将“Login”内容映射为具体的UserController,我需要新建一个applicationContext.xml文件并进行配置(这个文件在之后的控制反转中也有用处):
<?xml version="1.0" encoding="UTF-8" ?>
<beans>
<bean id="Login" class="com.Controller.UserController"></bean>
</beans>
完成了映射关系的声明之后,就需要让DS能够读懂这个xml文件
首先,DS需要在初始化时就先读取这个xml文件,并将这个关系转化为泛型是<String,Object>的哈希表,存在自己的属性当中,每当接收到请求,直接拿着获取到的“*”去哈希表中取出对应对象即可
//在方法外定义一个beanMap
private Map<String,Object> beanMap = new HashMap<String,Object>();
//在init()中
InputStream ins = getClass().getClassLoader().getResourceAsStream("applicationContext.xml");
DocumentBuilderFactory DBF = DocumentBuilderFactory.newInstance();
DocumentBuilder DB = null;
DB = DBF.newDocumentBuilder();
Document doc = DB.parse(ins); //得到Document对象
NodeList beanNodeList = doc.getElementsByTagName("bean");//获取所有bean的列表
for(int i=0;i<beanNodeList.getLength();i++){
Node beanNode = beanNodeList.item(i);
if(beanNode.getNodeType() == Node.ELEMENT_NODE){ //必须是元素节点,而非文本节点或注释节点
Element beanElement = (Element)beanNode;
String beanID = beanElement.getAttribute("id");
String className = beanElement.getAttribute("class");
//通过className获取其实例,以便存入Map
Object beanObj = Class.forName(className).getConstructor().newInstance();
beanMap.put(beanID, beanObj);
}
}
如此一来,HashMap就构建完成了
接下来,DS需要在service()中读懂请求类型,明白需要调用哪个一个Controller的哪一个方法,再进行invoke()调用,但在这里有一个问题:Controller中的方法都不能直接从前端拿取数据,那么调用Service、DAO层的方法所需的参数该由谁来获取?
仍然是DS负责
参数获取
首先明确:DS是后端的前哨站,所有的前端数据DS都可以拿到,而这些数据是由POST提交表单或GET而来的,可以使用getParameter()方法从传来的表单中获取数据
但有一个问题:要由getParameter()获取资源并交给方法,需要得知该方法所需要的所有参数的名称才能得到参数值,最后传入。但在此时,DS已不能直接根据名字和参数查到方法了,因为方法的参数已经各不相同,所以,需要用一一对比来找方法,找到之后,获取它的所有参数名称,再去getParameter()获取即可
//service()中
//首先分析行为
req.setCharacterEncoding("UTF-8");
String path = req.getServletPath(); //获取请求名,如/Login.do
path = path.substring(1,path.length()-3);
Object ControllerObj = beanMap.get(path); //拿到对应请求的Controller实例
String op = req.getParameter("operation"); //获取方法名,如login
if(StringUtils.isEmpty(op)) op = "login"; //如果没有动作备注,则动作为转到login.html页面
//进行方法的寻找和方法参数的获取
Method[] methods = ControllerObj.getClass().getDeclaredMethods();
for(Method m : methods){
String name = m.getName();
if(op.equals(name)){ //找到方法
//获取参数
Parameter[] paras = m.getParameters();
Object[] paraValues = new Object[paras.length];
for(int i=0;i< paras.length;i++){ //遍历此方法所有的参数
Parameter para = paras[i];
String paraName = para.getName(); //获取到具体的某个参数名
//考虑特殊参数名
if(paraName.equals("req")) {
paraValues[i] = req;
continue;
}
else if(paraName.equals("resp")) {
paraValues[i] = resp;
continue;
}
else if(paraName.equals("session")) {
paraValues[i] = req.getSession();
continue;
}
String paraValue = req.getParameter(paraName);
//由于获取到的一定是String,但传入可能是Integer,所以需要判断转换:
Object paraObj = paraValue; //不管什么类型,都最终给它,最后把它赋给Obj数组
String type = para.getType().getName(); //获取参数的type名称
if(paraObj != null){
if(type.equals("java.lang.Integer"))
paraObj = Integer.parseInt(paraValue);
}
paraValues[i] = paraObj; //一个个拿到参数,组成数组
}
//未完,还有两层括号
需要注意的是,在IDEA中需要在Java编译器的附加命令行添加“-parameters”,para.getName()方法才能获取到真实的参数名称,而非“arg1、arg2”这样的化名
另外,要保证POST上来的表单中的name与Controller方法的参数名一一对应。若不同,则getParameter()传入的名称是方法参数名,而表单中的name与之不同,导致找不到表单数据
在构建参数数组完成之后,就可以进行方法的调用了,注意Controller中的每个方法都会返回一个String,所以在调用的同时,还需要临时用一个String接住,然后直接拿着这个String来进行视图的跳转
方法调用与视图跳转控制
DS要想进行视图跳转的总控,就需要借助Thymeleaf库,通过使用ViewBaseServlet中的processTemplate()方法对新的页面进行重构渲染,或是直接对页面进行重定向
ViewBaseServlet类的代码是固定的,这里给出:
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.WebContext;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.ServletContextTemplateResolver;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class ViewBaseServlet extends HttpServlet {
private TemplateEngine templateEngine;
@Override
public void init() throws ServletException {
// 1.获取ServletContext对象
ServletContext servletContext = this.getServletContext();
// 2.创建Thymeleaf解析器对象
ServletContextTemplateResolver templateResolver = new ServletContextTemplateResolver(servletContext);
// 3.给解析器对象设置参数
// ①HTML是默认模式,明确设置是为了代码更容易理解
templateResolver.setTemplateMode(TemplateMode.HTML);
// ②设置前缀
String viewPrefix = servletContext.getInitParameter("view-prefix");
templateResolver.setPrefix(viewPrefix);
// ③设置后缀
String viewSuffix = servletContext.getInitParameter("view-suffix");
templateResolver.setSuffix(viewSuffix);
// ④设置缓存过期时间(毫秒)
templateResolver.setCacheTTLMs(60000L);
// ⑤设置是否缓存
templateResolver.setCacheable(true);
// ⑥设置服务器端编码方式
templateResolver.setCharacterEncoding("utf-8");
// 4.创建模板引擎对象
templateEngine = new TemplateEngine();
// 5.给模板引擎对象设置模板解析器
templateEngine.setTemplateResolver(templateResolver);
}
protected void processTemplate(String templateName, HttpServletRequest req, HttpServletResponse resp) throws IOException {
// 1.设置响应体内容类型和字符集
resp.setContentType("text/html;charset=UTF-8");
// 2.创建WebContext对象
WebContext webContext = new WebContext(req, resp, getServletContext());
// 3.处理模板数据
templateEngine.process(templateName, webContext, resp.getWriter());
}
}
DS类首先需要继承ViewBaseServlet类,并在初始化时首先执行父类的init()方法来初始化渲染所需的各种属性
上文已经完成了方法参数数组的获取,接下来一次性完成方法的调用和视图的控制:
//方法调用和页面跳转
m.setAccessible(true);
try {
String returnMes = (String)m.invoke(ControllerObj,paraValues);
if(returnMes.startsWith("redirect:")){ //重定向
String redirectMes = returnMes.substring(9);
resp.sendRedirect(redirectMes);
} else { //重构(内部转发)
super.processTemplate(returnMes,req,resp);
}
break; //执行完毕,没有必要再找方法
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
//最后两层括号
}
}
测试
由于已经是Web项目,所以不用编写测试类,直接启动Tomcat测试,调试配置如下:

这里选择访问Login.do,为的是能够被DS捕获,接着由于没有operation参数,所以DS会给一个默认值“login”,接着向下执行,最后执行了Controller中的login()方法,由于传入的参数都为空,所以程序认为用户输入的账号密码错误,从而返回了“构建login页面”的信息,所以我们一开始就看到了login.html页面

启动Tomcat后,默认已经输入u03,003,点击登录,成功进入页面,将u03修改为u04,则登陆失败,页面重置

浙公网安备 33010602011771号