经过上面两篇文章,spring原理应该都大致懂了吧(代码不重要,重点看思路)。
spring实现了,那就不得不提MVC架构了。
MVC架构
如何设计一个程序的结构,这是一门专门的学问,叫做“架构模式”(architectural pattern),属于编程的方法论。MVC模式就是架构模式的一种:
Model(业务模型):数据和业务规则,一个M能给多个V提供数据,复用M减少代码重复性。
View(前端页面):用户看到并与之交互的界面。
Controller(业务控制器):接收用户的输入并调用M和V去完成用户的需求。
这三个应该不陌生吧。今天就来手写实现一下吧。
MVC手写简单实现
1.预先准备
1.1 代码结构变化(接上节)
1.2 需要自己配置tomcat启动,要依赖servlet,这里贴一下pom文件
<groupId>com.deno</groupId>
<artifactId>springmvc-demo</artifactId>
<packaging>war</packaging>
<version>1.0</version>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.4</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.10</version>
</dependency>
</dependencies>
1.3 了解tomcat的肯定都知道web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<display-name>test web spring</display-name>
<servlet>
//servlet名称
<servlet-name>TestSpringMvc</servlet-name>
//servlet实现类,这里我们自己实现
<servlet-class>com.xxx.framework.springmvc.TDispatcherServlet</servlet-class>
//init方法和参数
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:application.properties</param-value>
</init-param>
//启动加载次数
<load-on-startup>1</load-on-startup>
</servlet>
//拦截路径配置
<servlet-mapping>
<servlet-name>TestSpringMvc</servlet-name>
//拦截请求路径正则
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
1.4 TRequestMapping、TRequestParam注解
直接copy,这里不贴
1.5 配置文件以及 layouts目录
- 配置文件新加
#MVC 前端页面目录
htmlRootTemplate=layouts
- 404 和 500 页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>页面去火星了</title>
</head>
<body>
<font size='30' color='red'>404 Not Found</font>
</body>
</html>
//500页面,使用#{}替代${}取值
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>服务器出错了</title>
</head>
<body>
<font style="font-size: 18px" color='blue'>500 服务器好像有点累了,需要休息一下<br/>
msg: #{msg}<br/>
error: #{error}<br/>
</font>
</body>
</html>
- index.html 使用#{}替代${}取值
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/html">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<font color="red" style="font-size: 30px">
welcome to #{name}<br>
当前时间: #{date}<br>
token: #{token}<br>
</font>
</body>
</html>
2. servlet初始化阶段TDispatcherServlet
现在开始一步一步分析。既然是继承了HttpServlet,那肯定由该类重写init()了。
2.1 init()配置解析
public class TDispatcherServlet extends HttpServlet {
//上下文
TestApplicationContext context;
//请求路径对应的Controller spring 实际是list 这里简化用map
Map<String, Handler> handlerMapping = new HashMap<String, Handler>();
TView view;
@Override
public void init(ServletConfig config) throws ServletException {
//上下文IOC DI阶段 前面的文章里已经写好了 这里直接调用
this.context = new TestApplicationContext(
config.getInitParameter("contextConfigLocation"));
//springmvc实际上这里是初始化9大组件,后面揭晓
//初始化HandlerMapping & 参数解析
initHandlerMapping();
//初始化视图转换器 传入配置的htmlRootPath
initViewResolvers(context.getContextConfig().getProperty("htmlRootTemplate"));
System.out.println("spring mvc is init");
}
}
2.2 初始化mapping initHandlerMapping
private void initHandlerMapping() {
//加载Controller层
for (Map.Entry<String, Object> entry : this.context.getIoc().entrySet()) {
Object instance = entry.getValue();
Class<?> clazz = instance.getClass();
if (!clazz.isAnnotationPresent(TController.class)) continue;
//获取根路径
String baseUrl = "";
if (clazz.isAnnotationPresent(TRequestMapping.class)) {
TRequestMapping annotation = clazz.getAnnotation(TRequestMapping.class);
baseUrl = annotation.value();
}
//获取所有方法上的路径
Method[] methods = clazz.getMethods();
for (Method method : methods) {
if (!method.isAnnotationPresent(TRequestMapping.class)) continue;
TRequestMapping annotation = method.getAnnotation(TRequestMapping.class);
String reqUrl = (baseUrl + "/" + annotation.value()).replaceAll("/+", "/");
if (this.handlerMapping.containsKey(reqUrl)) {
throw new RuntimeException("[" + reqUrl + "] the handlerMapping is exists");
}
//保存到map中
this.handlerMapping.put(reqUrl, new Handler(instance, method, reqUrl));
System.out.println("mapping添加: " + reqUrl);
}
}
}
Handler
记录类信息和参数信息
@Data
public class Handler {
Object target; //实例类
Method method; //方法
String url; //请求路径
Class<?>[] parameterTypes; //参数类型
Map<String,Integer> mapIndex; //参数位置
public Handler(Object target, Method method, String reqUrl) {
this.target = target;
this.method = method;
this.url = reqUrl;
this.parameterTypes = method.getParameterTypes();
doHanlerParam(method);
}
//初始化参数map 记录参数key和位置
private void doHanlerParam(Method method) {
this.mapIndex = new HashMap<String, Integer>();
//先处理 req 和resp的位置
for (int i = 0; i < this.parameterTypes.length; i++) {
Class<?> type = this.parameterTypes[i];
if (type == HttpServletRequest.class || type == HttpServletResponse.class) {
this.mapIndex.put(type.getName(),i);
}
}
//处理参数的位置(根据注解)
Annotation[][] pas = method.getParameterAnnotations();
for (int k = 0; k < pas.length; k++) { //如果用forEach 需要用Map存储参数位置
Annotation[] pa = pas[k];
for (int i = 0; i < pa.length; i++) {
Annotation annotation = pa[i];
if (annotation instanceof TRequestParam) {
String key = ((TRequestParam) annotation).name();
this. mapIndex.put(key,k);
}
}
}
}
}
2.3 初始化视图转换器
private void initViewResolvers(String htmlRootTemplate) {
//加载html页面目录
URL url = this.getClass().getClassLoader().getResource(htmlRootTemplate);
//封装成view 在view中提供渲染方法
this.view = new TView(url.getFile());
}
View
保存htmlRootPath并提供渲染方法render()
@Data
public class TView {
String htmlPath;
public TView(String htmlPath) {
this.htmlPath = htmlPath;
}
public void render(Map<String, Object> modelMap, String htmlName, HttpServletResponse resp) throws Exception {
StringBuilder builder = new StringBuilder();
//获取html的路径
File viewFile = new File(this.htmlPath + "/" + htmlName
+ (htmlName.endsWith(".html") ? "" : ".html"));
//读行
RandomAccessFile accessFile = new RandomAccessFile(viewFile, "r");
String line;
while (null != (line = accessFile.readLine())) {
line = new String(line.getBytes("ISO-8859-1"), "utf-8");
//正则匹配#{}的字段
Pattern pattern = Pattern.compile("#\\{[^\\}]+\\}", Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(line);
while (matcher.find()) {
String paramName = matcher.group();
//#{name}
paramName = paramName.replaceAll("#\\{|\\}", "");
//从map中取出并替换
Object paramValue = modelMap.get(paramName);
if (paramValue != null) {
line = matcher.replaceFirst(makeStringForRegExp(paramValue.toString()));
} else {
line = matcher.replaceFirst("null");
}
matcher = pattern.matcher(line);
}
builder.append(line);
}
resp.setCharacterEncoding("utf-8");
resp.getWriter().write(builder.toString());
}
//处理特殊字符
public static String makeStringForRegExp(String str) {
return str.replace("\\", "\\\\").replace("*", "\\*")
.replace("+", "\\+").replace("|", "\\|")
.replace("{", "\\{").replace("}", "\\}")
.replace("(", "\\(").replace(")", "\\)")
.replace("^", "\\^").replace("$", "\\$")
.replace("[", "\\[").replace("]", "\\]")
.replace("?", "\\?").replace(",", "\\,")
.replace(".", "\\.").replace("&", "\\&");
}
}
3.servlet调用阶段TDispatcherServlet
使用过servlet的时候,对doGet
和doPost
肯定不陌生吧
3.1 重写方法doGet和doPost
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
//调用
doDispatcher(req, resp);
} catch (Exception e) {
try {
if ("404".equals(e.getMessage())) {
this.view.render(null, "404", resp);
} else {
Map<String, Object> modelMap = new HashMap<String, Object>();
modelMap.put("msg", e.getMessage());
modelMap.put("error", Arrays.toString(e.getStackTrace()));
this.view.render(modelMap, "500", resp);
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
3.2 调用处理doDispatcher
private void doDispatcher(HttpServletRequest req, HttpServletResponse resp) throws Exception {
//根据请求路径 得到存储Controller类的handler
Handler handler = getHandler(req, resp);
//解析参数
Object[] objects = getParam(req, resp, handler);
//反射调用
Object result = handler.getMethod().invoke(handler.getTarget(), objects);
if (null == result || result instanceof Void) return;
//返回页面的
if (result instanceof TModelAndView) {
TModelAndView mv = (TModelAndView) result;
this.view.render(mv.getModel(), mv.getHtmlName(), resp);
} else {
//解决前端页面显示中文乱码
resp.setHeader("Content-Type", "text/html; charset=utf-8");
resp.getWriter().write(result.toString());
}
}
- getHandler 获取Handler
private Handler getHandler(HttpServletRequest req, HttpServletResponse resp) {
String uri = req.getRequestURI();
//从mapping map中获取 不存在返回404
if (this.handlerMapping.containsKey(uri)) {
return this.handlerMapping.get(uri);
}
throw new RuntimeException("404");
}
3.3 参数处理
private Object[] getParam(HttpServletRequest req, HttpServletResponse resp, Handler handler) {
//参数类型和参数的下标位置
Class<?>[] parameterTypes = handler.getParameterTypes();
Map<String, Integer> mapIndex = handler.getMapIndex();
//传入参数
Map<String, String[]> parameterMap = req.getParameterMap();
Object[] params = new Object[mapIndex.size()];
//根据下标位置获取参数
for (Map.Entry<String, Integer> entry : mapIndex.entrySet()) {
String key = entry.getKey();
Integer index = entry.getValue();
if (key.equals(HttpServletRequest.class.getName())) {
params[index] = req;
}
if (key.equals(HttpServletResponse.class.getName())) {
params[index] = resp;
}
//同名参数这版只考虑一个
if (parameterMap.containsKey(key)) {
String value = Arrays.toString(parameterMap.get(key)).replaceAll("\\[|\\]", "");
//根据参数类型的数组 转换参数
params[index] = converter(value, parameterTypes[index]);
}
}
return params;
}
private Object converter(String value, Class<?> type) {
if (type == Integer.class) {
return Integer.valueOf(value);
} else if (type == Double.class) {
return Double.valueOf(value);
}
//其它不写了
return value;
}
4. 调用测试
打包,配置tomcat,HelloAction添加@TRequestMapping("/mvc")
启动日志输出
application is init
mapping添加: /mvc/index.html
mapping添加: /mvc/hello1
mapping添加: /mvc/hello2
spring mvc is init
4.1 正常response write返回
@TRequestMapping("/hello1")
public void hello1(@TRequestParam(name = "name") String name, HttpServletResponse response) {
try {
response.setHeader("Content-Type","text/html; charset=utf-8");
response.getWriter().write(helloService.sayHello(name));
} catch (Exception e) {
e.printStackTrace();
}
}
请求 http://localhost:8080//mvc/hello1?name=王二麻子
my name is 王二麻子
4.2 直接return字符串
@TRequestMapping("/hello2")
public String hello2(@TRequestParam(name = "a") Integer a, @TRequestParam(name = "b") Integer b) {
return "a + b = " + (a + b);
}
请求http://localhost:8080/mvc/hello2?a=28&b=232
a + b = 260
4.3 返回html页面
@TRequestMapping("/index.html")
public TModelAndView index(@TRequestParam(name = "name") String name) {
Map<String, Object> modelMap;
try {
String time = helloService.getDataTime(name);
modelMap = new HashMap<String, Object>();
modelMap.put("name", name);
modelMap.put("date", time);
modelMap.put("token", UUID.randomUUID());
return new TModelAndView("index", modelMap);
} catch (Exception e) {
modelMap = new HashMap<String, Object>();
modelMap.put("msg", "服务器请求异常");
modelMap.put("error", Arrays.toString(e.getStackTrace()));
return new TModelAndView("500", modelMap);
}
}
请求http://localhost:8080/mvc/index.html?name=北京
当前时间: 2021-12-14 11:30:33
token: af3efd56-7a13-4dd4-8ce1-a1327ab80fc4
4.4 service异常切面日志以及500页面
在service新加报错代码
public String getDataTime(String name) {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String date = format.format(new Date());
System.out.println("获取当前时间:" + date + ",name=" + name);
int i =1/0; //测试切面异常和返回异常model
return date;
}
请求http://localhost:8080/mvc/index.html?name=北京
- 先看html页面返回
- Java 控制台日志打印
application is init
mapping添加: /mvc/index.html
mapping添加: /mvc/hello2
mapping添加: /mvc/hello1
spring mvc is init
[2021-12-14 11:34:17,868] Artifact springmvc-demo:war: Artifact is deployed successfully
[2021-12-14 11:34:17,868] Artifact springmvc-demo:war: Deploy took 698 milliseconds
方法之前调用:JoinPoint(target=null, method=null, args=[北京], result=null, throwName=null)
获取当前时间:2021-12-14 11:34:22,name=北京
出现异常调用:JoinPoint(target=null, method=null, args=[北京], result=null, throwName=/ by zero)
SpringMvc
手写内容到这里就全部完成了,基本上也算是实现了主体功能,当然仅供参考思路哈。下篇文章开始源码的分析。
本文为学习记录,不支持转载
上一篇:SpringMvc手写简单实现篇 - AOP切换编程篇
下一篇:Spring源码分析第一弹 - IOC控制反转分析
黑发不知勤学早,白发方悔读书迟