最近在学习mvc模式,试着开发一个简单的mvc框架。
架构图:
开发步骤:
一、准备阶段:
1.开发注解,该注解用于给处理器的方法提供对应的uri
package base.annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
//上面的注解可以让我们开发的注解存活到被扫描到为止,如果不加默认存活到class文件
public @interface RequestMapping {
//这是注解的属性,可以把数据(比如uri)存在属性中,
//需要的时候可以通过反射技术读取出来
public String value();
}
2.准备配置文件,此配置文件中保存处理器的类名,供HandlerMapping使用
<?xml version="1.0" encoding="UTF-8"?>
<beans>
<!-- 配置处理器-->
<bean class="demo.controller.LoginController"/>
</beans>
二、正式开发:
1.首先,我们要将控制器和其对应的uri读取出来。我们在前端控制器初始化时从smartmvc.xml中读取已配置的控制器(如LoginController),然后交给映射处理器处理
public void init() throws ServletException {
/*
* 使用dom4j读取配置文件的内容
*/
SAXReader reader = new SAXReader();
InputStream ins = getClass().getClassLoader().getResourceAsStream("smartmvc.xml");
try {
// 解析配置文件
Document doc = reader.read(ins);
// 找到根节点
Element root = doc.getRootElement();
// 找到根节点下面的所有的子节点
List<Element> eles = root.elements();
// 遍历所有子节点
List beans = new ArrayList();
for (Element ele : eles) {
// 读取class属性值
String className = ele.attributeValue("class");
System.out.println("className: " + className);
// 将处理器实例化
Object bean = Class.forName(className).newInstance();
// 将处理器实例添加到集合里面。
beans.add(bean);
}
System.out.println("beans:" + beans);
// 将包含有处理器实例的集合给
// HandlerMapping来处理。
handlerMapping = new HandlerMapping();
//HandlerMapping会建立请求路径与处理器的对应关系,存在map里
handlerMapping.process(beans);
} catch (Exception e) {
e.printStackTrace();
throw new ServletException(e);
}
}
2.上面写了 HandlerMapping 类 的 process 方法,但还没有实现。下面我们来实现它,该方法用于建立请求路径与处理器的对应关系
package base.common;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import base.annotation.RequestMapping;
/**
* 映射处理器: 负责提供请求路径与处理器的对应关系
*/
public class HandlerMapping {
// 存放请求路径与处理器的对应关系。
private Map<String, Handler> handlerMap = new HashMap<String, Handler>();
/**
* 依据请求路径,返回Handler对象。
* 注: Handler对象包含了Method对象和处理器实例。
*/
public Handler getHandler(String path) {
return handlerMap.get(path);
}
/**
* process方法遍历整个集合,将处理器实例取出来,
* 通过java反射技术读取@RequestMapping
* 注解中的路径信息,建立请求路径与处理器的对应关系。
*/
public void process(List beans) {
for (Object bean : beans) {
// 找到该实例对应的class对象。
Class clazz = bean.getClass();
// 找出所有方法
Method[] methods = clazz.getDeclaredMethods();
// 遍历所有方法
for (Method mh : methods) {
// 获得方法前的@RequestMapping注解
RequestMapping rm = mh.getDeclaredAnnotation(RequestMapping.class);
// 获得路径信息
String path = rm.value();
System.out.println("path:" + path);
// 建立请求路径与处理器的对应关系(这里需要解释一下,请看步骤3)
handlerMap.put(path, new Handler(mh, bean));
}
}
System.out.println("handlerMap: " + handlerMap);
}
}
3.因为通过反射调用invoke方法时需要两个两个参数,是该类实例和要调用的方法实例,所以我们维护一个map集合,key是方法上@RequestMapping注解的value值,map集合的value里需要放入类实例和要调用的方法实例,所以我们要创建一个Handler类封装要存入集合的类实例和要调用的方法实例。
package base.common;
import java.lang.reflect.Method;
/**
* 为了方便利用java反射进行方法的调用,将处理器实例和方法对象进行了封装。
*/
public class Handler {
private Method mh;
private Object obj;
public Handler(Method mh, Object obj) {
this.mh = mh;
this.obj = obj;
}
public Method getMh() {
return mh;
}
public void setMh(Method mh) {
this.mh = mh;
}
public Object getObj() {
return obj;
}
public void setObj(Object obj) {
this.obj = obj;
}
}
4.现在前端控制器的初始化已经完成,所有的处理器和其对应的uri都已经储存在HandlerMapping的一个map集合里,并对外提供方法,可以通过uri查询对应的处理器实例和方法实例。下面我们可以编写前端控制器的请求转发了,转发请求时需要利用反射技术,调用处理器的方法来处理业务(此版本还可以根据方法需要的参数类型自动传入参数,但目前只支持HttpServletRequest和HttpServletResponse类型参数的传入),处理器会返回视图名(即jsp的名称),然后前端控制器会将请求转发|重定向给对应的jsp
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 获得请求资源路径
String uri = request.getRequestURI();
// 获得应用名
String contextPath = request.getContextPath();
// 截取请求资源路径的一部分
String path = uri.substring(contextPath.length());
System.out.println("path:" + path);
// 依据请求路径(path)找到对应的处理器来处理
Handler handler = handlerMapping.getHandler(path);
System.out.println("handler:" + handler);
// 获得要调用的Method对象
Method mh = handler.getMh();
// 获得处理器实例
Object bean = handler.getObj();
// returnVal是方法的返回值
Object returnVal = null;
try {
/*
* 调用处理器的方法: 需要查看处理器方法带不带参数,
* 如果带有参数,需要给参数赋值,然后才能调用。
*/
Class[] types = mh.getParameterTypes();
if (types.length > 0) {
// 带有参数的方法
Object[] params = new Object[types.length];
for (int i = 0; i < types.length; i++) {
if (types[i] == HttpServletRequest.class) {
params[i] = request;
}
if (types[i] == HttpServletResponse.class) {
params[i] = response;
}
}
// 调用带参数的方法
returnVal = mh.invoke(bean, params);
} else {
// 调用不带参的方法
returnVal = mh.invoke(bean);
}
System.out.println("returnVal:" + returnVal);
/*
* 看视图名是否以"redirect:"开头, 如果是,则重定向;否则转发。
*/
String viewName = returnVal.toString();
if (viewName.startsWith("redirect:")) {
// 重定向
// 生成重定向地址
String redirectPath = contextPath + "/" + viewName.substring("redirect:".length());
response.sendRedirect(redirectPath);
} else {
// 转发
// 将视图名转换成对应的jsp
String jspPath = "/WEB-INF/" + viewName + ".jsp";
request.getRequestDispatcher(jspPath).forward(request, response);
}
} catch (Exception e) {
e.printStackTrace();
throw new ServletException(e);
}
}
5.现在,前端处理器已经可以将请求转发/重定向到视图层(jsp)了,下面我们写一个处理器和一个jsp来测试一下。
处理器:
package demo.controller;
import javax.servlet.http.HttpServletRequest;
import base.annotation.RequestMapping;
public class LoginController {
@RequestMapping("/toLogin.do")
public String toLogin() {
System.out.println("LoginController的toLogin方法");
return "login";
}
@RequestMapping("/login.do")
public String login(HttpServletRequest request) {
System.out.println("处理登录请求");
String username = request.getParameter("username");
String pwd = request.getParameter("pwd");
System.out.println("username:" + username + " pwd:" + pwd);
//模拟数据库查询结果
if ("Tom".equals(username) && "test".equals(pwd)) {
// 登录成功,重定向到用户列表
// 视图名前面有"redirect:",表示
// 重定向到某个地址。
return "redirect:toList.do";
} else {
// 登录失败,转发到登录页面,并提示用户
request.setAttribute("login_failed", "用户名或密码错误");
return "login";
}
}
@RequestMapping("/toList.do")
public String toList() {
return "listUser";
}
}
jsp:
<%@ page contentType="text/html; charset=utf-8"
pageEncoding="utf-8"%>
<html>
<head>
<title>Insert title here</title>
</head>
<body style="font-size:30px;">
<form action="login.do" method="post">
<fieldset>
<legend>登录</legend>
用户名:<input name="username"/>
${login_failed}
<br/>
密码:<input type="password"
name="pwd"/><br/>
<input type="submit" value="确定"/>
</fieldset>
</form>
</body>
</html>
6.最后要记得配置下web.xml,将所有的请求交由前端控制器处理
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">
<servlet>
<servlet-name>DispatcherServlet</servlet-name>
<servlet-class>base.web.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>DispatcherServlet</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
</web-app>
完成!