手动开发-实现SpringMVC底层机制--小试牛刀

文章目录

    • 前端控制器
    • Controller注解
    • RequestMapping注解
    • 自定义容器LingWebApplicationContext
    • 设计handlerList
    • 完成分发请求
    • Service注解和AutoWired注解
    • RequestParam注解
    • 完整代码

在这里说的底层机制的实现主要是指:前端控制器、Controller、Service注入容器、对象自动装配、控制器方法获取参数、视图解析、返回json数据。

前端控制器

前端控制器就是核心控制器。在这里我们可以设计一个Servlet来充当核心控制器:LingDispatcherServlet.java.这个控制器的作用主要是接收响应前端过来的Http请求和Response响应。一开始需要在web.xml中配置好控制器的 请求路径,还要配置好SpringMVC的xml文件: lingspringmvc.xml.

lingspringmvc.xml

<?xml version="1.0" encoding="UTF-8" ?>
<beans><component-scan base-package="com.linghu.controller"></component-scan>
</beans>

有了这个xml文件就好办了,里面写了Controller层的类路径,只需要通过dom4j技术将类路径读出来,就可以轻松的将该文件下的类文件进行读取遍历,再去分析他们是否加了我们设计的注解,如果加了就保留类路径,甚至加入到我们自己设计的ioc容器中。在读取xml文件里的内容的时候,可以单独写一个工具类XMLParser.java

package com.linghu.springmvc.xml;import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;import java.io.InputStream;/*** @author linghu* @date 2023/9/11 14:22*/
public class XMLParser {public static String getBasePackage(String xmlFile){SAXReader saxReader = new SAXReader();InputStream resourceAsStream =XMLParser.class.getClassLoader().getResourceAsStream(xmlFile);try {Document document = saxReader.read(resourceAsStream);Element rootElement = document.getRootElement();Element componentScanElement =rootElement.element("component-scan");Attribute attribute = componentScanElement.attribute("base-package");String basePackage = attribute.getText();System.out.println("basePackage="+basePackage);return basePackage;} catch (DocumentException e) {e.printStackTrace();}return null;}
}

Controller注解

在Controller层,我们会在类上标注Controller注解,容器就会将这个类路径扫描加入到我们的容器中。在这里我们将自己设计一个自己的注解Controller注解。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Controller {String value() default "";
}

RequestMapping注解

前端发出请求的时候,请求地址为:IP+端口+URI。RequestMapping注解可以规定我们请求的URI。同样我们需要自己设计一个属于自己的RequestMapping注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestMapping {String value() default "";
}

前端发出请求的时候,如果携带了参数,后端接收的时候就要标注接收的参数字段,会用到注解 RequestMapping注解。

自定义容器LingWebApplicationContext

需要自己设计一个容器,将我们从lingspringmvc.xml中读取到的类路径下的.class文件的路径列表全部存起来。具体来说 就是:

<?xml version="1.0" encoding="UTF-8" ?>
<beans><component-scan base-package="com.linghu.controller"></component-scan>
</beans>

需要将上面com.linghu.controller这个路径下的类文件的路径进行存储。以后这个扫描的包不光有controller,还会有service,dao等等。

这个容器里最重要的就是要保存我们扫描包和子包下的全类路径,所以我们定义了一个 classFullPathList集合用来存储。

private ArrayList<String> classFullPathList=new ArrayList<>();

接下来就是利用工具类 XMLParser去读取springmvc.xml配置文件里的包路径,接着利用文件和IO的知识去扫描这个包路径下的文件和目录,将这个路径下的所有.class文件的后缀.class裁剪掉,将路径名和文件名进行拼接就得到了类文件的类路径了,我们将他们存储到classFullPathList集合中即可。如下:

img

这个类路径就对应下图:

img

这里目前只讨论controller注解的类,因为springmvc.xml扫描的包只写了controller这个包。

利用文件和IO的知识去扫描这个包路径下的文件和目录是重点,在扫描以后,我们要将这个路径下的所有.class文件的后缀.class裁剪掉,将路径名和文件名进行拼接。这两步是最重要的。scanPackage()函数可以完成这两个重要的东西。

  public void init(){String basePackage = XMLParser.getBasePackage("lingspringmvc.xml");String[] packages = basePackage.split(",");if (packages.length>0){for (String pack :packages) {scanPackage(pack);}}System.out.println("classFullPathList="+classFullPathList);}public void scanPackage(String pack){URL url = this.getClass().getClassLoader().getResource("/" + pack.replaceAll("\\.", "/"));String path = url.getFile();//获取所有文件的目录File dir = new File(path);for (File f :dir.listFiles()) {if (f.isDirectory()){scanPackage(pack+"."+f.getName());}else {String classFullPath=pack+"."+f.getName().replaceAll(".class","");classFullPathList.add(classFullPath);}}}

将classFullPathList存放的扫描的全类路径文件的类名提出来,将第一个字母小写,作为bean对象的名称,类似于以前xml配置bean对象的时候,声明的bean的id。然后将类路径进行反射创建对象,同时将beanName和反射创建好的对象放到ioc容器中。如下:

img

 public void executeInstance(){if (classFullPathList.size()==0){return;}try {for (String  classFullPath:classFullPathList) {Class<?> clazz = Class.forName(classFullPath);if (clazz.isAnnotationPresent(Controller.class)){String beanName=clazz.getSimpleName().substring(0,1).toLowerCase()+clazz.getSimpleName().substring(1);//同时将beanName和反射创建好的对象放到ioc容器中ioc.put(beanName,clazz.newInstance());}}} catch (Exception e) {e.printStackTrace();}}

设计handlerList

Controller层会写很多接口,规范化请求的地址,用注解RequestMapping进行了标识,RequestMapping的Value值其实就是请求的地址,我们把它取出来单独命名成url,在把RequestMapping标识的方法的方法名取出来命名成method,最后把当前方法所在的类,也就是Controller标识的这个类的对象命名成controller。我们现在有了url,controller,method。其实就已经拿到了url和method的映射关系了,现在将其封装保存到handerList集合中。这样做的好处是,当前端发来请求的时候,我们可以取出请求的url,通过url在handerList中找到对应的调用的方法名,实现调用的映射。

先设计一个hander,用来封装url,controller,method:

package com.linghu.springmvc.handler;import java.lang.reflect.Method;/*** @author linghu* @date 2023/9/12 9:23*/
public class LingHandler {private String url;private Object controller;private Method method;public LingHandler() {}public LingHandler(String url, Object controller, Method method) {this.url = url;this.controller = controller;this.method = method;}@Overridepublic String toString() {return "LingHandler{" +"url='" + url + '\'' +", controller=" + controller +", method=" + method +'}';}public String getUrl() {return url;}public void setUrl(String url) {this.url = url;}public Object getController() {return controller;}public void setController(Object controller) {this.controller = controller;}public Method getMethod() {return method;}public void setMethod(Method method) {this.method = method;}
}
  private void initHandlerMapping(){if (lingWebApplicationContext.ioc.isEmpty()){return;}for (Map.Entry<String,Object> entry :lingWebApplicationContext.ioc.entrySet()) {Class<?> clazz = entry.getValue().getClass();if (clazz.isAnnotationPresent(Controller.class)){Method[] declaredMethods = clazz.getDeclaredMethods();for (Method method :declaredMethods) {if (method.isAnnotationPresent(RequestMapping.class)){RequestMapping requestMappingAnnotation =method.getAnnotation(RequestMapping.class);String url = requestMappingAnnotation.value();LingHandler lingHandler = new LingHandler(url,entry.getValue(),method);handlerList.add(lingHandler);}}}}}

initHandlerMapping()会将容器中的对象,也就是之前 我们通过类路径反射创建的对象进行遍历,遍历过程中筛选出被Controller注解标识过的类,将类里的方法再全部进行遍历,遍历过程中将被RequestMapping注解标识的方法选出来,将这些方法的方法名命名成method,注解RequestMapping的value值取出来命名成url,将当前类命名成controller,最后将url,controller,method放到LingHandler对象中,在将LingHandler对象放到handerList集合中。

img

img

完成分发请求

前端发送一个请求过来,无论是get,post请求都要经过我的servlet,这个时候可以取出请求request信息里的uri,这样我就得到了前端想要请求的Controller层的具体方法,其实拿到这个方法我们就可以利用反射进行调用了。所以完成分发请求的还是我们的servlet,也就是文章一开篇就说的前端控制器,核心控制器,它就是我们的整个大脑核心,负责接收请求,完成请求分发,分发到具体要执行的Controller层的方法去。

前端过来的请求,我们可以让它统一都走post请求。

LingDispatcherServlet.java:

   @Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {doPost(req, resp);}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {System.out.println("LingDispatcherServlet doPost");executeDispatch(req, resp);}

executeDispatch(req, resp)便是携带着前端request请求的具体执行分发请求的方法。

 public void executeDispatch(HttpServletRequest request,HttpServletResponse response){LingHandler lingHandler = getLingHandler(request);try {if (lingHandler==null){response.getWriter().print("<h1>404</h1>");}else {lingHandler.getMethod().invoke(lingHandler.getController(),request,response);}} catch (Exception e) {e.printStackTrace();}}

lingHandler.getMethod().invoke(lingHandler.getController(),request,response);便是反射调用,是完成分发的核心。在这里lingHandler.getMethod()本身就是Method类型的对象,所以可以进行调用。getLingHandler(request)的具体过程如下:

 public LingHandler getLingHandler(HttpServletRequest request){String requestURI = request.getRequestURI();for (LingHandler lingHandler :handlerList) {if (requestURI.equals(lingHandler.getUrl())){return lingHandler;}}return null;}

getLingHandler完成的便是查看handlerList集合中有没有前端请求的方法url,有的话就直接把handler对象返回,handler对象里保存着前端请求url和调用后端方法名的映射关系,通过映射关系我们可以查到具体要调用的方法是谁。

Service注解和AutoWired注解

这两个注解的实现其实和Controller注解差不多的流程三板斧。先通过元注解定义好这两个注解,在扫描全类路径的时候,去判断有没有Service注解,有的话就获取文件下的所有接口名,对接口名字首字母变小写,然后拼接得到新的beanName,最后通过反射创建对象,将beanName和对象加入到ioc中。

 } else if (clazz.isAnnotationPresent(Service.class)) {Service serviceAnnotation = clazz.getAnnotation(Service.class);String beanName = serviceAnnotation.value();if ("".equals(beanName)){Class<?>[] interfaces = clazz.getInterfaces();for (Class<?> anInterface :interfaces) {String beanName2=anInterface.getSimpleName().substring(0,1).toLowerCase()+anInterface.getSimpleName().substring(1);ioc.put(beanName2,clazz.newInstance());}}else {ioc.put(beanName,clazz.newInstance());}}

设计Controller注解的时候,我们是直接获取的 类名,在这里设计Service注解的时候我们获取的是接口名字,是因为接口内部装满了所有实现类,而我们的Service注解又是写在这些实现类上面 的,我们通过获取接口,就有机会遍历到这些实现类,如果不通过获取接口,直接获取实现类代价要大点。

AutoWired注解的作用是将dao层和业务层对象注入到ioc中,方便业务层或者控制层调用dao层和业务层。扫描什么的其实不难,就是全文扫描带AutoWired注解的属性,将其加入到容器中。加入的这个过程放在一个函数executeAutoWired()中。

    public void executeAutoWired(){if (ioc.isEmpty()){throw new RuntimeException("容器中没有可以装配的bean");}for (Map.Entry<String,Object> entry :ioc.entrySet()) {String key = entry.getKey();Object bean = entry.getValue();Field[] declaredFields = bean.getClass().getDeclaredFields();for (Field declaredField :declaredFields) {if (declaredField.isAnnotationPresent(AutoWired.class)){AutoWired autoWiredAnnotation =declaredField.getAnnotation(AutoWired.class);String beanName = autoWiredAnnotation.value();if ("".equals(beanName)){Class<?> type = declaredField.getType();beanName= type.getSimpleName().substring(0,1).toLowerCase()+type.getSimpleName().substring(1);}declaredField.setAccessible(true);try {if (ioc.get(beanName)==null){throw new RuntimeException("容器中没有注入该bean");}declaredField.set(bean,ioc.get(beanName));} catch (IllegalAccessException e) {e.printStackTrace();}}}}}

RequestParam注解

前端在发送请求的时候,会携带一些参数过来,这个时候后端要接收请求的时候处理好参数字段的对应关系。我们可以用requestparam注解标识前端对应过来的参数字段。

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestParam {String value() default "";
}
 public void executeDispatch(HttpServletRequest request,HttpServletResponse response){LingHandler lingHandler = getLingHandler(request);try {if (lingHandler==null){response.getWriter().print("<h1>404</h1>");}else {
//                lingHandler.getMethod().invoke(lingHandler.getController(),request,response);Class<?>[] parameterTypes =lingHandler.getMethod().getParameterTypes();Object [] params = new Object[parameterTypes.length];for (int i=0;i< parameterTypes.length;i++){Class<?> parameterType=parameterTypes[i];if ("HttpServletRequest".equals(parameterType.getSimpleName())){params[i]=request;} else if ("HttpServletResponse".equals(parameterType.getSimpleName())) {params[i]=response;}}Map<String,String[]> parameterMap =request.getParameterMap();for (Map.Entry<String,String[]> entry :parameterMap.entrySet()) {String name = entry.getKey();String value = entry.getValue()[0];System.out.println("请求的参数:"+name+"----"+value);int indexRequestParamIndex=getIndexRequestParamIndex(lingHandler.getMethod(),name);if (indexRequestParamIndex!=-1){params[indexRequestParamIndex]=value;}else {}}lingHandler.getMethod().invoke(lingHandler.getController(),params);}} catch (Exception e) {e.printStackTrace();}}public int getIndexRequestParamIndex(Method method,String name){Parameter[] parameters = method.getParameters();for (int i=0;i<parameters.length;i++){Parameter parameter=parameters[i];boolean annotationPresent =parameter.isAnnotationPresent(RequestParam.class);if (annotationPresent){RequestParam requestParamAnnotation =parameter.getAnnotation(RequestParam.class);String value = requestParamAnnotation.value();if (name.equals(value)){return i;}}}return -1;}

完整代码

《实现SpringMVC底层机制》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/131517.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

go并发处理业务

引言 实际上&#xff0c;在服务端程序开发和爬虫程序开发时&#xff0c;我们的大多数业务都是IO密集型业务&#xff0c;什么是IO密集型业务&#xff0c;通俗地说就是CPU运行时间只占整个业务执行时间的一小部分&#xff0c;而剩余的大部分时间都在等待IO操作。 IO操作包括htt…

反编译小程序详细教程,处理各种异常报错

文章目录 一、准备工作 &#xff08;一&#xff09;安装Nodejs &#xff08;二&#xff09;解密和逆向工具 二、小程序缓存文件解密 &#xff08;一&#xff09;定位小程序缓存路径 &#xff08;二&#xff09;源码解密 &#xff08;三&#xff09;源码反编译 三、小结 四、异常…

Go 异常处理

代码在执行的过程中可能因为一些逻辑上的问题而出现错误 func test1(a, b int) int {result : a / breturn result } func main() {resut : test1(10, 0)fmt.Println(resut) }panic: runtime error: integer divide by zero goroutine 1 [running]: …

华为云arm架构的linux系统中通过docker部署python环境

背景 有时候需要在无互联网的环境安装部署python环境,虽然可以在linux系统中直接安装python环境,但是比较复杂乱,特别是环境多的时候,其实可以通过docker打包安装的方式来实现 1、租用华为云arm加载的服务器 https://www.huaweicloud.com/product/ecs.html 2、安装doc…

请明星出席品牌周年庆活动:巧妙策略与成功之道

品牌的周年庆典是一次展示实力、感谢客户和吸引更多关注的机会。在这个特殊时刻&#xff0c;让明星出席活动演出无疑是让庆典更加引人注目和难忘的方式。明星的存在不仅能增加活动的知名度&#xff0c;还能为品牌增色不少。然而&#xff0c;邀请明星出席活动是一项复杂的任务&a…

phpcmsV9.6.0sql注入漏洞分析

目录 前言 环境准备 漏洞点 看一看parse_str函数 看一看sys_auth函数 看一看get_one函数 全局搜索sys_auth($a_k, ENCODE) 查看哪里调用了 set_cookie 查看safe_replace函数 判断登录绕过 index的业务 加载modules/wap/index.php 加载modules/attachment/attachme…

免费版Photoshop2024智能人像磨皮插件

Portraiture是一款智能磨皮插件&#xff0c;为Photoshop和Lightroom添加一键磨皮美化功能&#xff0c;快速对照片中皮肤、头发、眉毛等部位进行美化&#xff0c;无需手动调整&#xff0c;大大提高P图效率。全新4版本&#xff0c;升级AI算法&#xff0c;并独家支持多人及全身模式…

vue手写提示组件弹窗

1、弹框展示 2、message组件 新建一个message.vue <template><div class"wrapper" v-if"isShow" :class"showContent ? fadein : fadeout">{{ text }}</div> </template> <script></script> <style s…

微信怎么定时发圈?

定时发圈的妙用 在合适的时间点发布新的产品、促销活动&#xff0c;不仅能够及时提醒用户品牌的存在&#xff0c;还可以引发用户的兴趣&#xff0c;增加品牌的曝光率。 选择最佳的发朋友圈时间段&#xff0c;以确保推广内容得到最大的曝光和关注&#xff0c;提高广告投放的效果…

python元组

元组tumple tumple_python1.元组的创建2.元组是不可变序列3.元组的遍历 tumple_python -元组(tuple)是是一个有序、不可变的序列。元组用小括号 () 表示&#xff0c;其中的元素可以是任意类型&#xff0c;并且可以通过索引访问。 不可变序列与可变序列 1>不可变序列&#x…

MySQL中分区与分表的区别

MySQL中分区与分表的区别 一、分区与分表的区别 分区和分表是在处理大规模数据时的两种技术手段&#xff0c;尽管它们的目标都是提升系统的性能和数据管理的效率&#xff0c;但它们的实现方式和应用场景略有不同。 1. 分区 分区是将一个大表分割为多个更小的子表&#xff0c…

【WEB3】区块链开发入门项目之「简易NFT交易市场」

参考文章 教程英文版&#xff1a;https://docs.alchemy.com/docs/how-to-build-an-nft-marketplace-from-scratch 教程中文版&#xff1a;https://zhuanlan.zhihu.com/p/557479922?utm_id0 以太坊测试币领取&#xff1a;https://goerlifaucet.com/ 入门项目地址&#xff1…

【C++杂货铺】优先级队列的使用指南与模拟实现

文章目录 一、priority_queue的介绍二、priority_queue的使用2.1 数组中的第k个最大元素 三、priority_queue模拟实现3.1 仿函数3.2 成员变量3.3 成员函数3.3.1 构造函数3.3.2 AdjustDown3.3.3 push3.3.4 AdjustUp3.3.5 pop3.3.6 empty3.3.7 size 四、结语 一、priority_queue的…

苏州融资融券(两融)开户利率最低能做到多少?5%!

两融利率是指投资者在证券交易所通过融资买入和融券卖出股票时所支付的利息费用。两融包括融资和融券两部分&#xff0c;而融资和融券则是两种常见的金融杠杆运作方式。融资是指投资者通过向券商借入资金来买入股票。在融资中&#xff0c;投资者需要支付给券商一定的利息费用。…

面了一个测试工程师要求月薪26K,总感觉他背了很多面试题...

最近有朋友去华为面试&#xff0c;面试前后进行了20天左右&#xff0c;包含4轮电话面试、1轮笔试、1轮主管视频面试、1轮hr视频面试。 据他所说&#xff0c;80%的人都会栽在第一轮面试&#xff0c;要不是他面试前做足准备&#xff0c;估计都坚持不完后面几轮面试。 其实&…

Linux系统离线安装RabbitMQ

安装rabbitmq 1、下载安装包 首先进入官网进行安装包的下载&#xff0c;在下载时一定要注意erlong版本和rabbitmq-server版本匹配 rabbitmq版本对应关系&#xff1a;传送门 Erlong下载地址:传送门 rabbitmq-server下载地址:传送门 socat 不同版本 centos7:传送门 cent…

【Unity3D】UI Toolkit数据动态绑定

1 前言 本文将实现 cvs 表格数据与 UI Toolkit 元素的动态绑定。 如果读者对 UI Toolkit 不是太了解&#xff0c;可以参考以下内容。 UI Toolkit简介UI Toolkit容器UI Toolkit元素UI Toolkit样式选择器UI Toolkit自定义元素 本文完整资源见→UI Toolkit数据动态绑定。 2 数据…

【办公软件】微信占用C盘空间如何释放

C盘又满了&#xff0c;如果微信QQ用得多的话&#xff0c;很可能微信就占用了很多空间。当然就算安装目录选择到了其他盘&#xff0c;但是软件的缓存也有可能占用C盘。 因为微信或是其他常用软件的缓存路径都会默认保存在C盘&#xff0c;随着传输文件较多&#xff0c;每次都会自…

编码转换流

同理&#xff0c;创建f1和f2方法&#xff0c;分别测试OutputStreamWriter和InputStreamReader 也是主要分三步&#xff0c;即1创建流 2使用流 3关流 OutputStreamWriter f1方法 因为要操作流&#xff0c;所以先创建一个try-catch-finally结构&#xff0c;创建流对象Out…

ARTS 打卡 第一周,初试ARTS

前言 认识三掌柜的想必都知道&#xff0c;我持续创作技术博客已经有6年时间了&#xff0c;固定每个月发布不少于6篇博文。同时&#xff0c;自己作为一名热爱分享的开发者&#xff0c;像ARTS这样的活动自然少不了我。由于我是打算挤在一起分享&#xff0c;之前都是做了本地文档记…