## SmartMVC 自定义框架
MVC 模式: 解决用户界面问题的标准模式(套路)!
M Model 模型,封装业务逻辑
V View 视图,代表显示界面
C Controller 控制器,是用于连接整合 M和V
Sun给出web用户界面的建议:
1. 使用Java Bean 作为Model,处理业务逻辑
2. 使用JSP 作为视图,显示数据
3. 使用Servlet作为控制器,整合JSP和JavaBean
一般情况将如上建议封装为框架(工具)减少后续编程量
框架实现步骤:
### 1. 拆分控制器为 前端控制器和子控制器
拆分的目的是将Web编程逻辑与业务处理逻辑进行剥离,将Web编程逻辑封装到前端控制器,达到简化子控制器逻辑,复用前端控制器的目的。
原理:
### 2. 利用request参数将控制器中的数据共享到JSP
为了避免用户直接访问JSP视图组件,将JSP保存到 /WEB-INF/jsp 文件夹中。
### 3. 利用注解将用户请求URL映射到子控制器的方法上
## 注解
1. Java 5 提供了一种代码标注功能。
2. 利用注解可以实现动态代码执行功能。
3. 在软件运行期间动态解析注解动态执行方法。
4. 使用注解:利用反射API解析使用注解。
案例:
@Retention(RUNTIME) //注解一值保留到运行期
@Target(METHOD) //此注解只能写在方法上
public @interface RequestMapping {
String value(); //利用value()可以给注解添加默认参数
}
标注注解:
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";
}
}
## 反射API
1. Java 提供的核心API
1. java.lang.reflect 包中的系列API
2. 提供功能:
1. 动态解析对象的类型和内部结构,解析注解
2. 动态加载类型
3. 动态创建对象
4. 动态执行方法
案例: 解析对象的内部结构
public class Test3 {
public static void main(String[] args) {
/**
* 利用反射检查对象的类型和内部结构
*/
test("ABC");
test(5);
ArrayList list = new ArrayList();
Iterator i = list.iterator();
test(i);
}
public static void test(Object obj) {
//obj 引用对象的类型,以及内部结构是什么?
//利用反射就可以动态检查对象的类型和内部结构
// getClass() 获取当对象的实际类型
Class cls = obj.getClass();
System.out.println(cls);
//检查类型的内部结构
// Declared 声明的 Field 字段、属性
Field[] fields=cls.getDeclaredFields();
for (Field field : fields) {
System.out.println(field);
}
//Method: 方法 检查类型中声明的方法
Method[] methods = cls.getDeclaredMethods();
for (Method method : methods) {
System.out.println(method);
}
}
}
案例: 利用反射API解析方法的注解
public class Demo {
@RequestMapping("/list.do")
public String list() {
return "list";
}
@RequestMapping("/add.do")
public String add() {
return "add";
}
}
public class Test4 {
public static void main(String[] args)
throws Exception{
/**
* 动态解析类方法上标注的注解
*/
Scanner in= new Scanner(System.in);
System.out.print("输入类名:");
String className=in.nextLine();
//获取Demo的类型信息
//Class cls = Demo.class;
//forName() 可以动态加载一个类到内存中
Class cls = Class.forName(className);
//获取类型上全部的方法
Method[] methods=cls.getDeclaredMethods();
for (Method method : methods) {
System.out.println(method);
//Annotation:注解
//method.getAnnotations() 获取在方法上
//标注的注解。此注解必须是 RUNTIME 注解
Annotation[] anns=method.getAnnotations();
//方法上只标注了一个注解,获取第一个注解
Annotation ann = anns[0];
System.out.println(ann);
//读取注解上的value值
if(ann instanceof RequestMapping) {
RequestMapping rm=(RequestMapping)ann;
System.out.println(rm.value());
}
}
}
}
利用反射调用方法:
1. 加载类
2. 在类找到要执行的方法
3. 创建对象
4. 在对象上执行方法
案例:
public class Test5 {
public static void main(String[] args)
throws Exception{
/*
* 利用反射执行方法
1. 加载类
2. 在类找到要执行的方法
3. 创建对象
4. 在对象上执行方法
*/
Scanner in = new Scanner(System.in);
System.out.print("输入类名:");
String className = in.nextLine();
//Class cls 是反射API的使用入口
//动态加载类
Class cls = Class.forName(className);
//输入方法名
System.out.print("输入方法名:");
String name = in.nextLine();
//动态找到类上声明的方法,如果找不到就抛出异常!
Method method = cls.getDeclaredMethod(name);
//创建对象:动态创建对象, Instance实例
//newInstance的使用前提是类型必须包含无参构造器
Object obj = cls.newInstance();
//在对象obj上调用method方法:
//如果参数错误,或者对象上没有方法,或者方法执行
//期间出现故障,都会抛出异常。
Object val = method.invoke(obj);
System.out.println(val);
}
}
# 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>