SmartMVC 框架开发

1. 将前端控制器与子控制器进行分离

将web控制逻辑封装到 DispatchServlet 中,将业务功能提取到子控制中。

1.1 前端控制器

前端控制器:

/**
 * 核心前端控制器,处理任何的 *.do 请求
 * 前端控制器处理全部的Web功能 
 **/
public class DispatchServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    protected void service(
            HttpServletRequest request, 
            HttpServletResponse response) throws ServletException, IOException {
        //创建子控制器
        //执行子控制器方法
        //根据子控制器的返回值,转发到JSP页面
        request.setCharacterEncoding("UTF-8");  
        try {
            //创建子控制器
            Controller controller=new Controller();
            //调用子控制器的方法(封装业务逻辑)
            String path = controller.execute(request);
            //   path=            "list"
            path = "/WEB-INF/jsp/"+path+".jsp";
            request.getRequestDispatcher(path)
            .forward(request, response);
        } catch (Exception e) {
            e.printStackTrace(); 
            response.setContentType(
                    "text/html; charset=UTF-8");
            PrintWriter out=response.getWriter();
            out.println("系统故障:"+e.getMessage()); 
        }
    }
}

将作为视图组件的JSP文件放在 /WEB-INF/JSP中, 避免用户直接访问。

配置前端控制器:将所有 *.do 请求转到前端控制器进行处理。

  <servlet>
    <description></description>
    <display-name>DispatchServlet</display-name>
    <servlet-name>DispatchServlet</servlet-name>
    <servlet-class>mvc.DispatchServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>DispatchServlet</servlet-name>
    <url-pattern>*.do</url-pattern>
  </servlet-mapping>

1.2 编写子控制器,封装业务功能

public class Controller {
    /**
     * 子控制器方法
     * @param request 用于在控制器和JSP直接传递数据
     * @return 转发的目标JSP页面
     * @throws Exception
     */
    @RequestMapping("/list.do") 
    public String execute(
            HttpServletRequest request) 
            throws Exception {
        UserDAO dao = new UserDAO();
        List<User> users = dao.findAll();
        //将users数据传递到JSP
        request.setAttribute("users", users); 
        return "list";
    }

    @RequestMapping("/add.do") 
    public String add(HttpServletRequest request) 
            throws Exception {
        return "add";
    }
}

1.3 编写视图组件 list.jsp

视图组件的JSP文件放在 /WEB-INF/JSP中:

<%@ page language="java" 
    contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" 
    uri="http://java.sun.com/jsp/jstl/core"%> 
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>用户列表</title>
</head>
<body>
    <h1>用户列表</h1>
    <table>
        <tr>
            <td>ID</td>
            <td>用户名</td>
            <td>密码</td>
            <td>邮箱</td>
            <td>操作</td>
        </tr>           
        <c:forEach items="${users}" var="user">
            <tr>
                <td>${user.id}</td> 
                <td>${user.uname}</td> 
                <td>${user.pwd}</td> 
                <td>${user.email}</td> 
                <td>删除</td> 
            </tr>
        </c:forEach>
    </table>
</body>
</html>

2. 利用注解映射用户请求到控制器方法

原理:

2.1 编写注解

@Retention(RetentionPolicy.RUNTIME) //注解一值保留到运行期
@Target(ElementType.METHOD) //此注解只能写在方法上
public @interface RequestMapping {
    String value();
}

2.2 编写Handler类用于封装控制器和控制器方法

/**
 * 处理者,处理器 
 */
public class Handler {
    /**
     * 子控制器对象
     */
    private Object controller;
    /**
     * 子控制的方法 
     */
    private Method method;

    public Handler() {
    }
    public Handler(Object controller, Method method) {
        this.controller = controller;
        this.method = method;
    }
    @Override
    public String toString() {
        return "Handler [controller=" + controller + ", method=" + method + "]";
    }

    public String execute(HttpServletRequest req) 
        throws Exception {
        //利用反射执行方法
        return (String)method.invoke(controller, req);
    }

}

2.3 创建HandlerMapping用于封装URL与控制器方法的映射关系

HandlerMapping的内部封装了一个Map,利用Map优化查询效率,提高性能。

/**
 * 用于管理,URL与子控制器方法的映射关系
 * 如:
 *   /list.do -> Handler(controller, execute())
 *   /add.do -> Handler(controller, add())
 */
public class HandlerMapping {
    private Map<String, Handler> mapping=
            new HashMap<String, Handler>();

    public HandlerMapping() {
    }

    /**
     * 将 一个控制器类解析并且添加到map中
     * 1. 动态加载类到内存
     * 2. 找到全部的方法,并且查找方法上是否有
     *    RequestMapping 注解
     * 3. 如果有注解,就将注解URL,和控制器以及方法添加到
     *    map中
     * @param className 一个控制器类名
     */
    public void parseController(String className) 
        throws Exception {
        //加载类到内存中
        Class cls = Class.forName(className);
        //创建控制器对象
        Object controller = cls.newInstance();
        //找到全部方法
        Method[] methods = cls.getDeclaredMethods();
        for (Method method : methods) {
            //在方法上查找 RequestMapping 注解
            RequestMapping ann=
              method.getAnnotation(RequestMapping.class);
            //如果ann不为空,则方法上包含RequestMapping注解
            if(ann!=null) {
                String url=ann.value();
                //将找到的方法添加到map中
                mapping.put(url,  
                    new Handler(controller, method));
            }
        }
    }


    @Override
    public String toString() {
        return "HandlerMapping [mapping=" + mapping + "]";
    }
    /**
     * 根据请求的路径url,找到Handler对象
     * @param url
     * @return 
     */
    public Handler get(String url) {
        return mapping.get(url);
    }

    public static void main(String[] args) 
        throws Exception {
        HandlerMapping handlerMapping=
                new HandlerMapping();
        handlerMapping.parseController("test.Demo");
        System.out.println(handlerMapping.mapping); 
    }
}

注意:务必利用main方法对HandlerMapping进行单元测试!

测试素材:

public class Demo {

    @RequestMapping("/list.do")
    public String list() {
        return "list";
    }

    @RequestMapping("/add.do")
    private String add() {
        return "add";
    }

}

2.4 重构 DispatchServlet 利用init方法读取mvc.xml 加载控制器类

mvc.xml: 配置控制器其类

<?xml version="1.0" encoding="UTF-8"?>
<beans>
    <bean class="mvc.Controller"/>
</beans>

DispatchServlet:

private HandlerMapping handlerMapping;

@Override
public void init() throws ServletException {
    try {
        handlerMapping = new HandlerMapping();
        //handlerMapping.parseController(
        //  "mvc.Controller");
        SAXReader reader = new SAXReader();

        //读取web.xml中的Servlet配置参数
        String file=getInitParameter("config");
        System.out.println("file:"+file); 

        InputStream in = getClass()
            .getClassLoader()
            .getResourceAsStream(file);
        Document doc = reader.read(in);
        Element root=doc.getRootElement();
        //找到全部 bean 元素
        List<Element> list=root.elements("bean");
        for (Element element : list) {
            //获取bean元素上的class属性
            String className=
                    element.attributeValue("class");
            System.out.println("Controller:"+className); 
            //将得到的类名交给 handlerMapping 
            handlerMapping.parseController(className);
        }
        in.close();
        System.out.println(handlerMapping); 
    }catch (Exception e) {
        e.printStackTrace();
        throw new ServletException(
                "控制器解析错误", e);
    }
}

在web.xml 中添加 load-on-startup 在容器启动时候初始化 Servlet 测试init方法

  <servlet>
    <description></description>
    <display-name>DispatchServlet</display-name>
    <servlet-name>DispatchServlet</servlet-name>
    <servlet-class>mvc.DispatchServlet</servlet-class>
    <init-param>
      <param-name>config</param-name>
      <param-value>mvc.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>DispatchServlet</servlet-name>
    <url-pattern>*.do</url-pattern>
  </servlet-mapping>

2.5 重构 DispatchServlet 实现根据URL执行控制器方法

  1. 重构service()方法,根据用户请求的URL执行对应的控制器方法
  2. 重构service()方法,支持重定向功能:

    1. 控制返回值以redirect:为开头是重定向功能
      • 如果返回http为开头的就是跳转到其他网站
      • 否则是本网站的地址,拼接 ContextPath以后再重定向。
    2. 其他情况认为是转发到JSP页面

    protected void service( HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //创建子控制器 //执行子控制器方法 //根据子控制器的返回值,转发到JSP页面 request.setCharacterEncoding("UTF-8"); try {

        //获取用户请求的路径 
        String path = request.getRequestURI();
        System.out.println(path);
        String contextPath=request.getContextPath();
        System.out.println(contextPath);
        path = path.substring(contextPath.length());
        System.out.println(path);
        //根据请求的URL找到Handler
        Handler handler=handlerMapping.get(path);
        //执行控制器方法(利用反射执行方法)
        path = handler.execute(request);
        //创建子控制器
        //Controller controller=new Controller();
        //调用子控制器的方法(封装业务逻辑)
        //String path = controller.execute(request);
    
        if(path.startsWith("redirect:")) {
            //支持重定向功能
            path = path.substring(
                "redirect:".length());
            if(path.startsWith("http")) {
                //如果是http开头的就直接重定向
                response.sendRedirect(path);
            } else {
                // 否则就拼接绝对路径
                //  /servler6-4/list.do
                path = contextPath+path;
                response.sendRedirect(path);
            }
        } else {
            //转发到JSP
            path = "/WEB-INF/jsp/"+path+".jsp";
            request.getRequestDispatcher(path)
            .forward(request, response);
        }
    } catch (Exception e) {
        e.printStackTrace(); 
        response.setContentType(
                "text/html; charset=UTF-8");
        PrintWriter out=response.getWriter();
        out.println("系统故障:"+e.getMessage()); 
    }
    

    }

3. 实现添加和删除功能

3.1 添加add.jsp页面

<%@ page language="java" 
    contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" 
    uri="http://java.sun.com/jsp/jstl/core"%> 
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>添加用户</title>
</head>
<body>
    <h1>添加用户</h1>
    <form action="save.do" method="post">
        <div>
            <label>用户名:</label>
            <input type="text" name="uname" 
            value="${uname}"> 
            <span>${unameError}</span>
        </div>
        <div>
            <label>密码:</label>
            <input type="password" name="pwd"> 
        </div>
        <div>
            <label>e-mail:</label>
            <input type="text" name="email"
                value="${email}">
        </div>
        <div>
            <input type="submit" value="保存">  
        </div>
    </form>
</body>
</html>

3.2 添加控制器Controller方法:

public class Controller {
    /**
     * 子控制器方法
     * @param request 用于在控制器和JSP直接传递数据
     * @return 转发的目标JSP页面
     * @throws Exception
     */
    @RequestMapping("/list.do") 
    public String execute(
            HttpServletRequest request) 
            throws Exception {
        UserDAO dao = new UserDAO();
        List<User> users = dao.findAll();
        //将users数据传递到JSP
        request.setAttribute("users", users); 
        return "list";
    }

    @RequestMapping("/add.do") 
    public String add(HttpServletRequest request) 
            throws Exception {
        return "add"; //add.jsp
    }

    @RequestMapping("/save.do") 
    public String save(HttpServletRequest request) 
            throws Exception {
        String uname=request.getParameter("uname");
        String pwd=request.getParameter("pwd");
        String email=request.getParameter("email");

        UserDAO dao = new UserDAO();
        //检查用户名是否是同名的
        User u = dao.find(uname);
        if(u!=null) {
            //返回到添加页面,继续输入信息
            request.setAttribute("uname", uname);
            request.setAttribute("email", email);
            request.setAttribute("unameError", 
                    "用户名重复");
            return "add";
        }

        User user = new User();
        user.setUname(uname);
        user.setPwd(pwd);
        user.setEmail(email);       
        dao.save(user);
        request.setAttribute("message", "成功"); 
        //return "success"; //success.jsp
        return "redirect:/list.do";
        //return "redirect:http://tmooc.cn";
    }

    @RequestMapping("/delete.do")
    public String delete(HttpServletRequest request)
        throws Exception{

        String str = request.getParameter("id");
        int id = Integer.parseInt(str);
        UserDAO dao = new UserDAO();
        dao.delete(id);

        return "redirect:/list.do";
    }

}

测试 列表,添加,删除功能

4. 添加登录功能

4.1 添加登录页面

<%@ page language="java" 
    contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" 
    uri="http://java.sun.com/jsp/jstl/core"%> 
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
    <h1>登录</h1>
    <form action="login.do" method="post">
        <div>
            <label>用户名:</label>
            <input type="text" name="uname" 
            value="${uname}"> 
            <span>${unameError}</span>
        </div>
        <div>
            <label>密码:</label>
            <input type="password" name="pwd"> 
            <span>${pwdError}</span>
        </div>
        <div>
            <input type="submit" value="登录">  
        </div>
    </form>
</body>
</html>

4.2 添加登录控制器方法

public class LoginController {

    @RequestMapping("/login-form.do")
    public String form(HttpServletRequest request)
        throws Exception{
        return "login";
    }

    @RequestMapping("/login.do")
    public String login(HttpServletRequest request) 
        throws Exception{
        //检查用户名和密码是否正确
        String uname = request.getParameter("uname");
        String pwd = request.getParameter("pwd");
        UserDAO dao=new UserDAO();
        User user = dao.find(uname);
        //数据中没有用户信息,拒绝登录返回 "login" 
        if(user==null) {
            request.setAttribute("unameError",
                    "用户名或密码错误");
            request.setAttribute("uname", uname);
            return "login";
        }
        if(user.getPwd().equals(pwd)) {
            //密码也一样则登录成功!
            request.getSession().setAttribute(
                    "loginUser", user);
            return "redirect:/list.do";
        }//密码不同,返回登录页面继续登录
        request.setAttribute("pwdError",
                "用户名或密码错误");
        request.setAttribute("uname", uname);
        return "login";
    }
}

登录成功以后需要将登录信息保存到session中

4.3 利用过滤器解决权限问题

过滤器:

public class AccessFilter implements Filter {

    public void destroy() {
    }

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest)request;
        HttpServletResponse res = (HttpServletResponse)response;

        //如果当前请求是 /login 开始的,就放过
        String path = req.getRequestURI();
        String ctxPath = req.getContextPath();
        path = path.substring(ctxPath.length());
        if(path.startsWith("/login")) {
            chain.doFilter(req, res);
            return;
        }

        HttpSession session = req.getSession();
        //检查登录用户信息
        User user=(User)session.getAttribute("loginUser");
        if(user==null) {
            //没有登录过,重定向到登录页面
            res.sendRedirect(req.getContextPath()+ 
                    "/login-form.do"); 
        }else {
            //如果登录了就放过
            chain.doFilter(req, res);
        }

    }

    public void init(FilterConfig fConfig) throws ServletException {
    }
}

配置过滤器 web.xml

  <filter>
    <display-name>AccessFilter</display-name>
    <filter-name>AccessFilter</filter-name>
    <filter-class>mvc.AccessFilter</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>AccessFilter</filter-name>
    <url-pattern>*.do</url-pattern>
  </filter-mapping>

测试。