MVC - 总控层和Controller映射

引入DispatcherServlet

DispatcherServlet(以下简称DS)负责总管全体的Controller,并与前端页面对接;前端发来的各请求都最终被DS所拦截和处理,并根据具体的参数来调用Controller中对应的方法

DS的任务主要有以下三个:

  1. 获取调用方法所需的参数资源
  2. 调用Controller方法
  3. 控制视图的跳转

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,则登陆失败,页面重置

posted @ 2022-09-06 16:10  FICeN  阅读(59)  评论(0)    收藏  举报