4 手写实现SpringMVC,第四节:匹配用户请求、执行映射方法

本文主要是介绍4 手写实现SpringMVC,第四节:匹配用户请求、执行映射方法,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在上一篇我们已经完成了配置的url到方法的映射,并且完成了method的各参数的注解、参数名、类型等的映射配置。

这一篇就很简单了,就是通过获取request的请求地址和参数,和已经加载好的映射进行比对,如果匹配上了就执行对应的方法。

直接上代码:

@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {//根据请求的URL去查找对应的methodtry {boolean isMatcher = pattern(req, resp);if (!isMatcher) {out(resp,"404 not found");}} catch (Exception ex) {ByteArrayOutputStream buf = new java.io.ByteArrayOutputStream();ex.printStackTrace(new java.io.PrintWriter(buf, true));String expMessage = buf.toString();buf.close();out(resp, "500 Exception" + "\n" + expMessage);}}

private boolean pattern(HttpServletRequest request, HttpServletResponse response) throws Exception {if (handlerMapping.isEmpty()) {return false;}//用户请求地址String requestUri = request.getRequestURI();String contextPath = request.getContextPath();//用户写了多个"///",只保留一个requestUri = requestUri.replace(contextPath, "").replaceAll("/+", "/");//遍历HandlerMapping,寻找url匹配的for (Map.Entry<String, HandlerModel> entry : handlerMapping.entrySet()) {if (entry.getKey().equals(requestUri)) {//取出对应的HandlerModelHandlerModel handlerModel = entry.getValue();Map<String, Integer> paramIndexMap = handlerModel.paramMap;//定义一个数组来保存应该给method的所有参数赋值的数组Object[] paramValues = new Object[paramIndexMap.size()];Class<?>[] types = handlerModel.method.getParameterTypes();//遍历一个方法的所有参数[name->0,addr->1,HttpServletRequest->2]for (Map.Entry<String, Integer> param : paramIndexMap.entrySet()) {String key = param.getKey();if (key.equals(HttpServletRequest.class.getName())) {paramValues[param.getValue()] = request;} else if (key.equals(HttpServletResponse.class.getName())) {paramValues[param.getValue()] = response;} else {//如果用户传了参数,譬如 name= "wolf",做一下参数类型转换,将用户传来的值转为方法中参数的类型String parameter = request.getParameter(key);if (parameter != null) {paramValues[param.getValue()] = convert(parameter.trim(), types[param.getValue()]);}}}//激活该方法handlerModel.method.invoke(handlerModel.controller, paramValues);return true;}}return false;}

由于用户传来的都是String,我们需要根据参数的具体类型,进行转换

/*** 将用户传来的参数转换为方法需要的参数类型*/private Object convert(String parameter, Class<?> targetType) {if (targetType == String.class) {return parameter;} else if (targetType == Integer.class || targetType == int.class) {return Integer.valueOf(parameter);} else if (targetType == Long.class || targetType == long.class) {return Long.valueOf(parameter);} else if (targetType == Boolean.class || targetType == boolean.class) {if (parameter.toLowerCase().equals("true") || parameter.equals("1")) {return true;} else if (parameter.toLowerCase().equals("false") || parameter.equals("0")) {return false;}throw new RuntimeException("不支持的参数");}else {//TODO 还有很多其他的类型,char、double之类的依次类推,也可以做List<>, Array, Map之类的转化return null;}}

以上就OK了。

下面还是贴个完整代码吧

package com.tianyalei.mvc;import com.tianyalei.mvc.annotation.*;
import com.tianyalei.mvc.util.Play;import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;/*** Created by wuwf on 17/6/28.* 入口Sevlet*/
public class DispatcherServlet extends HttpServlet {private List<String> classNames = new ArrayList<>();private Map<String, Object> instanceMapping = new HashMap<>();private Map<String, HandlerModel> handlerMapping = new HashMap<>();@Overridepublic void init(ServletConfig config) throws ServletException {System.out.println("我是初始化方法");scanPackage(config.getInitParameter("scanPackage"));doInstance();//注入值doAutoWired();doHandlerMapping();System.out.println("初始化完毕");}/*** 扫描包下的所有类*/private void scanPackage(String pkgName) {//获取指定的包的实际路径url,将com.tianyalei.mvc变成目录结构com/tianyalei/mvcURL url = getClass().getClassLoader().getResource("/" + pkgName.replaceAll("\\.", "/"));//转化成file对象File dir = new File(url.getFile());//递归查询所有的class文件for (File file : dir.listFiles()) {//如果是目录,就递归目录的下一层,如com.tianyalei.mvc.controllerif (file.isDirectory()) {scanPackage(pkgName + "." + file.getName());} else {//如果是class文件,并且是需要被spring托管的if (!file.getName().endsWith(".class")) {continue;}//举例,className = com.tianyalei.mvc.controller.WebControllerString className = pkgName + "." + file.getName().replace(".class", "");//判断是否被Controller或者Service注解了,如果没注解,那么我们就不管它,譬如annotation包和DispatcherServlet类我们就不处理try {Class<?> clazz = Class.forName(className);if (clazz.isAnnotationPresent(Controller.class) || clazz.isAnnotationPresent(Service.class)) {classNames.add(className);}} catch (ClassNotFoundException e) {e.printStackTrace();}}}}/*** 实例化*/private void doInstance() {if (classNames.size() == 0) {return;}//遍历所有的被托管的类,并且实例化for (String className : classNames) {try {Class<?> clazz = Class.forName(className);//如果是Controllerif (clazz.isAnnotationPresent(Controller.class)) {//举例:webController -> new WebControllerinstanceMapping.put(lowerFirstChar(clazz.getSimpleName()), clazz.newInstance());} else if (clazz.isAnnotationPresent(Service.class)) {//获取注解上的值Service service = clazz.getAnnotation(Service.class);//举例:QueryServiceImpl上的@Service("myQueryService")String value = service.value();//如果有值,就以该值为keyif (!"".equals(value.trim())) {instanceMapping.put(value.trim(), clazz.newInstance());} else {//没值时就用接口的名字首字母小写//获取它的接口Class[] inters = clazz.getInterfaces();//此处简单处理了,假定ServiceImpl只实现了一个接口for (Class c : inters) {//举例 modifyService->new ModifyServiceImpl()instanceMapping.put(lowerFirstChar(c.getSimpleName()), clazz.newInstance());break;}}}} catch (Exception e) {e.printStackTrace();}}}/*** 给被AutoWired注解的属性注入值*/private void doAutoWired() {if (instanceMapping.isEmpty()) {return;}//遍历所有被托管的对象for (Map.Entry<String, Object> entry : instanceMapping.entrySet()) {//查找所有被Autowired注解的属性// getFields()获得某个类的所有的公共(public)的字段,包括父类;// getDeclaredFields()获得某个类的所有申明的字段,即包括public、private和proteced,但是不包括父类的申明字段。Field[] fields = entry.getValue().getClass().getDeclaredFields();for (Field field : fields) {//没加autowired的不需要注值if (!field.isAnnotationPresent(Autowired.class)) {continue;}String beanName;//获取AutoWired上面写的值,譬如@Autowired("abc")Autowired autowired = field.getAnnotation(Autowired.class);if ("".equals(autowired.value())) {//例 searchService。注意,此处是获取属性的类名的首字母小写,与属性名无关,可以定义@Autowired SearchService abc都可以。beanName = lowerFirstChar(field.getType().getSimpleName());} else {beanName = autowired.value();}//将私有化的属性设为true,不然访问不到field.setAccessible(true);//去映射中找是否存在该beanName对应的实例对象if (instanceMapping.get(beanName) != null) {try {field.set(entry.getValue(), instanceMapping.get(beanName));} catch (IllegalAccessException e) {e.printStackTrace();}}}}}/*** 建立url到方法的映射*/private void doHandlerMapping() {if (instanceMapping.isEmpty()) {return;}//遍历托管的对象,寻找Controllerfor (Map.Entry<String, Object> entry : instanceMapping.entrySet()) {Class<?> clazz = entry.getValue().getClass();//只处理Controller的,只有Controller有RequestMappingif (!clazz.isAnnotationPresent(Controller.class)) {continue;}//定义urlString url = "/";//取到Controller上的RequestMapping值if (clazz.isAnnotationPresent(RequestMapping.class)) {RequestMapping requestMapping = clazz.getAnnotation(RequestMapping.class);url += requestMapping.value();}//获取方法上的RequestMappingMethod[] methods = clazz.getMethods();//只处理带RequestMapping的方法for (Method method : methods) {if (!method.isAnnotationPresent(RequestMapping.class)) {continue;}RequestMapping methodMapping = method.getAnnotation(RequestMapping.class);//requestMapping.value()即是在requestMapping上注解的请求地址,不管用户写不写"/",我们都给他补上String realUrl = url + "/" + methodMapping.value();//替换掉多余的"/",因为有的用户在RequestMapping上写"/xxx/xx",有的不写,所以我们处理掉多余的"/"realUrl = realUrl.replaceAll("/+", "/");//获取所有的参数的注解,有几个参数就有几个annotation[],为毛是数组呢,因为一个参数可以有多个注解……Annotation[][] annotations = method.getParameterAnnotations();//由于后面的Method的invoke时,需要传入所有参数的值的数组,所以需要保存各参数的位置/*以Search方法的这几个参数为例 @RequestParam("name") String name, HttpServletRequest request, HttpServletResponse response未来在invoke时,需要传入类似这样的一个数组["abc", request, response]。"abc"即是在Post方法中通过request.getParameter("name")来获取Request和response这个简单,在post方法中直接就有。所以我们需要保存@RequestParam上的value值,和它的位置。譬如 name->0,只有拿到了这两个值,才能将post中通过request.getParameter("name")得到的值放在参数数组的第0个位置。同理,也需要保存request的位置1,response的位置2*/Map<String, Integer> paramMap = new HashMap<>();//获取方法里的所有参数的参数名(注意:此处使用了ASM.jar 版本为asm-3.3.1,需要在web-inf下建lib文件夹,引入asm-3.3.1.jar,自行下载)//如Controller的add方法,将得到如下数组["name", "addr", "request", "response"]String[] paramNames = Play.getMethodParameterNamesByAsm4(clazz, method);//获取所有参数的类型,提取Request和Response的索引Class<?>[] paramTypes = method.getParameterTypes();for (int i = 0; i < annotations.length; i++) {//获取每个参数上的所有注解Annotation[] anns = annotations[i];if (anns.length == 0) {//如果没有注解,则是如String abc,Request request这种,没写注解的//如果没被RequestParam注解// 如果是Request或者Response,就直接用类名作key;如果是普通属性,就用属性名Class<?> type = paramTypes[i];if (type == HttpServletRequest.class || type == HttpServletResponse.class) {paramMap.put(type.getName(), i);} else {//参数没写@RequestParam注解,只写了String name,那么通过java是无法获取到name这个属性名的//通过上面asm获取的paramNames来映射paramMap.put(paramNames[i], i);}continue;}//有注解,就遍历每个参数上的所有注解for (Annotation ans : anns) {//找到被RequestParam注解的参数,并取value值if (ans.annotationType() == RequestParam.class) {//也就是@RequestParam("name")上的"name"String paramName = ((RequestParam) ans).value();//如果@RequestParam("name")这里面if (!"".equals(paramName.trim())) {paramMap.put(paramName, i);}}}}HandlerModel model = new HandlerModel(method, entry.getValue(), paramMap);handlerMapping.put(realUrl, model);}}}@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {doPost(req, resp);}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {//根据请求的URL去查找对应的methodtry {boolean isMatcher = pattern(req, resp);if (!isMatcher) {out(resp,"404 not found");}} catch (Exception ex) {ByteArrayOutputStream buf = new java.io.ByteArrayOutputStream();ex.printStackTrace(new java.io.PrintWriter(buf, true));String expMessage = buf.toString();buf.close();out(resp, "500 Exception" + "\n" + expMessage);}}private boolean pattern(HttpServletRequest request, HttpServletResponse response) throws Exception {if (handlerMapping.isEmpty()) {return false;}//用户请求地址String requestUri = request.getRequestURI();String contextPath = request.getContextPath();//用户写了多个"///",只保留一个requestUri = requestUri.replace(contextPath, "").replaceAll("/+", "/");//遍历HandlerMapping,寻找url匹配的for (Map.Entry<String, HandlerModel> entry : handlerMapping.entrySet()) {if (entry.getKey().equals(requestUri)) {//取出对应的HandlerModelHandlerModel handlerModel = entry.getValue();Map<String, Integer> paramIndexMap = handlerModel.paramMap;//定义一个数组来保存应该给method的所有参数赋值的数组Object[] paramValues = new Object[paramIndexMap.size()];Class<?>[] types = handlerModel.method.getParameterTypes();//遍历一个方法的所有参数[name->0,addr->1,HttpServletRequest->2]for (Map.Entry<String, Integer> param : paramIndexMap.entrySet()) {String key = param.getKey();if (key.equals(HttpServletRequest.class.getName())) {paramValues[param.getValue()] = request;} else if (key.equals(HttpServletResponse.class.getName())) {paramValues[param.getValue()] = response;} else {//如果用户传了参数,譬如 name= "wolf",做一下参数类型转换,将用户传来的值转为方法中参数的类型String parameter = request.getParameter(key);if (parameter != null) {paramValues[param.getValue()] = convert(parameter.trim(), types[param.getValue()]);}}}//激活该方法handlerModel.method.invoke(handlerModel.controller, paramValues);return true;}}return false;}/*** 将用户传来的参数转换为方法需要的参数类型*/private Object convert(String parameter, Class<?> targetType) {if (targetType == String.class) {return parameter;} else if (targetType == Integer.class || targetType == int.class) {return Integer.valueOf(parameter);} else if (targetType == Long.class || targetType == long.class) {return Long.valueOf(parameter);} else if (targetType == Boolean.class || targetType == boolean.class) {if (parameter.toLowerCase().equals("true") || parameter.equals("1")) {return true;} else if (parameter.toLowerCase().equals("false") || parameter.equals("0")) {return false;}throw new RuntimeException("不支持的参数");}else {//TODO 还有很多其他的类型,char、double之类的依次类推,也可以做List<>, Array, Map之类的转化return null;}}private void out(HttpServletResponse response, String str) {try {response.setContentType("application/json;charset=utf-8");response.getWriter().print(str);} catch (IOException e) {e.printStackTrace();}}private class HandlerModel {Method method;Object controller;Map<String, Integer> paramMap;public HandlerModel(Method method, Object controller, Map<String, Integer> paramMap) {this.method = method;this.controller = controller;this.paramMap = paramMap;}}private String lowerFirstChar(String className) {char[] chars = className.toCharArray();chars[0] += 32;return String.valueOf(chars);}}

我再对Controller修改一下,加个不带RequestParam注解的方法

package com.tianyalei.mvc.controller;import com.tianyalei.mvc.annotation.*;
import com.tianyalei.mvc.service.ModifyService;
import com.tianyalei.mvc.service.QueryService;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;/*** Created by wuwf on 17/6/28.*/
@Controller
@RequestMapping("/web")
public class WebController {@Autowired("myQueryService")private QueryService queryService;@Autowiredprivate ModifyService modifyService;@RequestMapping("/search")public void search(@RequestParam("name") String name, HttpServletRequest request, HttpServletResponse response) {String result = queryService.search(name);out(response, result);}@RequestMapping("/add")public void add(@RequestParam("name") String name,@RequestParam("addr") String addr,HttpServletRequest request, HttpServletResponse response) {String result = modifyService.add(name, addr);out(response, result);}@RequestMapping("/update")public void update(String name, boolean flag,HttpServletRequest request, HttpServletResponse response) {out(response, "我是name:" + name + "flag为:" + flag);}@RequestMapping("/remove")public void remove(@RequestParam("name") Integer id,HttpServletRequest request, HttpServletResponse response) {String result = modifyService.remove(id);out(response, result);}private void out(HttpServletResponse response, String str) {try {response.setContentType("application/json;charset=utf-8");response.getWriter().print(str);} catch (IOException e) {e.printStackTrace();}}
}


重启Tomcat,测试一下。

把里面的方法都试一下,发现基本已经OK了,只要参数传对,整个流程是能走通的。

还有一些遗留问题,譬如flag不传值时,注入时默认为null,而方法中定义的是boolean,所以会报错。这里就牵扯到一个require的问题了,就是说该参数是否是必传的,还有是否需要我们赋默认值的问题。

当然了,扩展起来还是很简单的,譬如SpringMVC在遇到小写的boolean或者int时,而用户又不传值时会赋默认值,做法应该就是遍历参数值数组,将为null的赋初值。如果是大写的Boolean就不赋值。如果在RequestParam上加了require为true,那么当为null时,我们应该直接抛出异常给用户。

还有一些比较难点的扩展,譬如/web/query/{userId},@PathVariable, @ModelAttribute,还有正则匹配/web/*,ModelMap,ModelAndView还有参数校验Hibernate Valider等等,SpringMVC非常强大,但是原理基本就是这样。在这个基础上,我们也是可以完成上面那些扩展的。





这篇关于4 手写实现SpringMVC,第四节:匹配用户请求、执行映射方法的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/879947

相关文章

Spring Security简介、使用与最佳实践

《SpringSecurity简介、使用与最佳实践》SpringSecurity是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架,本文给大家介绍SpringSec... 目录一、如何理解 Spring Security?—— 核心思想二、如何在 Java 项目中使用?——

SpringBoot+RustFS 实现文件切片极速上传的实例代码

《SpringBoot+RustFS实现文件切片极速上传的实例代码》本文介绍利用SpringBoot和RustFS构建高性能文件切片上传系统,实现大文件秒传、断点续传和分片上传等功能,具有一定的参考... 目录一、为什么选择 RustFS + SpringBoot?二、环境准备与部署2.1 安装 RustF

Nginx部署HTTP/3的实现步骤

《Nginx部署HTTP/3的实现步骤》本文介绍了在Nginx中部署HTTP/3的详细步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学... 目录前提条件第一步:安装必要的依赖库第二步:获取并构建 BoringSSL第三步:获取 Nginx

springboot中使用okhttp3的小结

《springboot中使用okhttp3的小结》OkHttp3是一个JavaHTTP客户端,可以处理各种请求类型,比如GET、POST、PUT等,并且支持高效的HTTP连接池、请求和响应缓存、以及异... 在 Spring Boot 项目中使用 OkHttp3 进行 HTTP 请求是一个高效且流行的方式。

java.sql.SQLTransientConnectionException连接超时异常原因及解决方案

《java.sql.SQLTransientConnectionException连接超时异常原因及解决方案》:本文主要介绍java.sql.SQLTransientConnectionExcep... 目录一、引言二、异常信息分析三、可能的原因3.1 连接池配置不合理3.2 数据库负载过高3.3 连接泄漏

MyBatis Plus实现时间字段自动填充的完整方案

《MyBatisPlus实现时间字段自动填充的完整方案》在日常开发中,我们经常需要记录数据的创建时间和更新时间,传统的做法是在每次插入或更新操作时手动设置这些时间字段,这种方式不仅繁琐,还容易遗漏,... 目录前言解决目标技术栈实现步骤1. 实体类注解配置2. 创建元数据处理器3. 服务层代码优化填充机制详

Python实现Excel批量样式修改器(附完整代码)

《Python实现Excel批量样式修改器(附完整代码)》这篇文章主要为大家详细介绍了如何使用Python实现一个Excel批量样式修改器,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一... 目录前言功能特性核心功能界面特性系统要求安装说明使用指南基本操作流程高级功能技术实现核心技术栈关键函

javacv依赖太大导致jar包也大的解决办法

《javacv依赖太大导致jar包也大的解决办法》随着项目的复杂度和依赖关系的增加,打包后的JAR包可能会变得很大,:本文主要介绍javacv依赖太大导致jar包也大的解决办法,文中通过代码介绍的... 目录前言1.检查依赖2.更改依赖3.检查副依赖总结 前言最近在写项目时,用到了Javacv里的获取视频

Java实现字节字符转bcd编码

《Java实现字节字符转bcd编码》BCD是一种将十进制数字编码为二进制的表示方式,常用于数字显示和存储,本文将介绍如何在Java中实现字节字符转BCD码的过程,需要的小伙伴可以了解下... 目录前言BCD码是什么Java实现字节转bcd编码方法补充总结前言BCD码(Binary-Coded Decima

python获取指定名字的程序的文件路径的两种方法

《python获取指定名字的程序的文件路径的两种方法》本文主要介绍了python获取指定名字的程序的文件路径的两种方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要... 最近在做项目,需要用到给定一个程序名字就可以自动获取到这个程序在Windows系统下的绝对路径,以下