首页
题库
公司真题
专项练习
面试题库
在线编程
面试
面试经验
AI 模拟面试
简历
求职
学习
基础学习课
实战项目课
求职辅导课
专栏&文章
竞赛
搜索
我要招人
发布职位
发布职位、邀约牛人
更多企业解决方案
AI面试、笔试、校招、雇品
HR免费试用AI面试
最新面试提效必备
登录
/
注册
何人听我楚狂声
字节跳动_抖音_后端开发工程师
关注
已关注
取消关注
#声哥今天更新了吗#
系列的最后一弹
卑微关注公众号:楚狂声哥
@何人听我楚狂声:
手撸一个 Spring —— 3. Web 框架
前言 这次实现的这个 SpringMVC,依赖于我们上一篇文章实现的基于注解的 IOC 容器框架,如果还没有看过上篇文章的同学可以去学习一下。 本文项目的完整代码在Github上,地址:https://github.com/CN-GuoZiyang/My-Spring-IOC SpringMVC 原理 要实现我们自己的框架,就必须对原版框架的处理流程了解得清晰透彻,简易总结如下: 用户发送请求至前端控制器 DispatcherServlet。 DispatcherServlet 收到请求调用 HandlerMapping 处理器映射器。 处理器映射器根据请求url找到具体的处理器,生成处理器对象及处理器拦截器(如果有则生成)一并返回给 DispatcherServlet。 DispatcherServlet 通过 HandlerAdapter 处理器适配器调用处理器。 执行处理器(Controller,也叫后端控制器)。 Controller 执行完成返回 ModelAndView。 HandlerAdapter 将 controller 执行结果 ModelAndView 返回给 DispatcherServlet。 DispatcherServlet 将 ModelAndView 传给 ViewReslover 视图解析器。 ViewReslover 解析后返回具体 View。 DispatcherServlet 对 View 进行渲染视图(即将模型数据填充至视图中)。 DispatcherServlet 响应用户。 整个框架的流程这么多,但是用户只需要编写 Controller 的业务代码即可,大大简化了开发。 这也算是八股文了,面试时问的 SpringMVC 的处理流程就是这个。 框架实现 我们都知道,SpringMVC 是基于 Java 的 Servelt 技术实现的,那么我们就需要导入 Servlet 的支持包,将这个项目改造成为一个 Web 项目。 在 Maven 中添加如下依赖: <dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency></dependencies> 我们模仿 web 项目的目录结构,在项目的根目录下建立一个 web 文件夹,再在 web 文件夹下建立 WEB-INF 文件夹,在其中新建 web.xml 文件。这就是这个 web 项目的配置文件,即 Servlet 的配置文件。内容如下: <?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" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0"> <servlet> <servlet-name>MySpringMVC</servlet-name> <servlet-class>top.guoziyang.springframework.web.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>application.properties</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>MySpringMVC</servlet-name> <url-pattern>/*</url-pattern> </servlet-mapping></web-app> 这里和 SpringMVC 的处理流程一样,就是新建了一个类 DispatcherServlet 注册为 Servlet,并且设置这个 Servlet 处理所有的 URL 请求。 在这里我们配置了一个参数 contextConfigLocation,参数的值为 application.properties 。这个文件作为我们的 Web 框架的配置文件,这个框架并不需要太多配置,只需要知道 Controller 的扫描路径就可以了。在 resources 文件夹下新建这个文件,里面我只写了一行: scanPackage=top.guoziyang.main.controller 表示我的所有的 Controller 都会放在 top.guoziyang.main.controller 包及其子包下,到时候启动时我们的框架会去扫描这个包。 接着我们去定义三个注解:@Controller、@RequestMapping 和 @RequestParam,用过 SpringMVC 的人应该都知道这三个注解是干嘛的,我就不多说了。 package top.guoziyang.springframework.annotation;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)public @interface Controller {} package top.guoziyang.springframework.annotation;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;@Target({ElementType.TYPE, ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)public @interface RequestMapping { String value() default "";} package top.guoziyang.springframework.annotation;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;@Target(ElementType.PARAMETER)@Retention(RetentionPolicy.RUNTIME)public @interface RequestParam { String value();} 注意这些注解都是 Runtime 的。 接着我们就需要实现在配置文件里写的 DispatcherServlet 类,这个类需要继承 HttpServlet 类,才是一个可被使用的Servlet。 这个类需要重写父类中的三个主要的方法,init()、doGet()和doPost()方法。 在 Servlet 实例化之后,Servlet 容器会调用 init() 方法,来初始化该对象,主要是为了让 Servlet 对象在处理客户请求前可以完成一些初始化的工作。方法如下: @Overridepublic void init(ServletConfig config) { try { xmlApplicationContext = new ClassPathXmlApplicationContext("application-annotation.xml"); } catch (Exception e) { e.printStackTrace(); } doLoadConfig(config.getInitParameter("contextConfigLocation")); doScanner(properties.getProperty("scanPackage")); doInstance(); initHandlerMapping();} init 方法传入的参数 ServletConfig 类,就是在 web.xml 文件中配置的信息,Servlet 启动时会自动解析 xml 文件封装成 ServletConfig 类。 init 方法首先初始化了一个 Spring 容器。其主要的功能就是读取配置文件,接着扫描目标包下所有的 Controller,最后实例化所有的 Controller,并且绑定 URL 路由。对应上面的 8、9、10 和 11 行。其中第八行解析 properties 文件的内容,并存储到成员变量 properties 中。第九行将将包中所有的类都扫描出来,并存储在 classNames 这个 List 里。 doInstance() 的实现很简单,如下: private void doInstance() { if (classNames.isEmpty()) { return; } for (String className : classNames) { try { //把类搞出来,反射来实例化(只有加 @Controller 需要实例化) Class clazz = Class.forName(className); if (clazz.isAnnotationPresent(Controller.class)) { classes.add(clazz); BeanDefinition definition = new BeanDefinition(); definition.setSingleton(true); definition.setBeanClassName(clazz.getName()); xmlApplicationContext.addNewBeanDefinition(clazz.getName(), definition); } } catch (Exception e) { e.printStackTrace(); } } try { xmlApplicationContext.refreshBeanFactory(); } catch (Exception e) { e.printStackTrace(); }} 主要就是把上一步中包下的所有类遍历一下,找到加上了 Controller 注解的类,添加到 Spring 容器里就行了。 这里有人就会问了,唉我 Spring 容器已经初始化完成了,怎么还能往里添加 Bean 呢?很简单,我们可以手动刷新一下。这里给 XmlApplicationContext 类添加了一个 refreshBeanFactory() 方法,手动刷新Bean的配置,如果遇到没有初始化的(刚添加进去的)就会初始化。方法实现非常简单: public void refreshBeanFactory() throws Exception { prepareBeanFactory((AbstractBeanFactory) beanFactory);} 注意这里我们还把符合条件的类(Controller)放在了 classes 里,这是一个 HashSet,后续在绑定 URL 的时候要用。 在 initHandlerMapping() 方法中,我们将扫描对应的 Controller,找出某个 URL 应当由哪个类的哪个方法进行处理。如下: private void initHandlerMapping() { if (classes.isEmpty()) return; try { for (Class<?> clazz : classes) { String baseUrl = ""; if (clazz.isAnnotationPresent(RequestMapping.class)) { baseUrl = clazz.getAnnotation(RequestMapping.class).value(); } Method[] methods = clazz.getMethods(); for (Method method : methods) { if (!method.isAnnotationPresent(RequestMapping.class)) continue; String url = method.getAnnotation(RequestMapping.class).value(); url = (baseUrl + "/" + url).replaceAll("/+", "/"); handlerMapping.put(url, method); controllerMap.put(url, xmlApplicationContext.getBean(clazz)); } } } catch (Exception e) { e.printStackTrace(); }} 由于我们已经把符合条件的 Controller 都放在了 classes 中,只要遍历这个 Set 就行了。对每个类遍历方法,获取 RequestMapping 这个注解的值,并且拼接出完整的 URL,将 URL 与方法的映射存储在 handlerMapping 这个 map 中,将 URL 与类的映射存储在 controllerMap 中。 那么最终,一个请求到来时,是到达 doGet() 和 doPost() 方法的。我们自己实现一个 doDispatch() 方法来进行自定义处理。 doDispatch() 方法首先需要分离出请求的 URL 和请求参数,找到对应的方法后通过反射调用。如下: public void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { if (handlerMapping.isEmpty()) return; String url = request.getRequestURI(); String contextPath = request.getContextPath(); url = url.replace(contextPath, "").replaceAll("/+", "/"); if (!handlerMapping.containsKey(url)) { response.getWriter().write("404 NOT FOUND!"); return; } Method method = handlerMapping.get(url); Class<?>[] parameterTypes = method.getParameterTypes(); Map<String, String[]> parameterMap = request.getParameterMap(); Object[] paramValues = new Object[parameterTypes.length]; for (int i = 0; i < parameterTypes.length; i++) { String requestParam = parameterTypes[i].getSimpleName(); if (requestParam.equals("HttpServletRequest")) { paramValues[i] = request; continue; } if (requestParam.equals("HttpServletResponse")) { paramValues[i] = response; continue; } if (requestParam.equals("String")) { for (Map.Entry<String, String[]> param : parameterMap.entrySet()) { String value = Arrays.toString(param.getValue()).replaceAll("\\[|\\]", "").replaceAll(",\\s", ","); paramValues[i] = value; } } } method.invoke(controllerMap.get(url), paramValues); } 反射调用方法传参的方式,是通过一个 Object 数组的方式传入参数的,按照方法定义参数的顺序,将值存放在数组中,在反射调用时将数组传入即可。在最后,将 request 域中获取到的参数作为方法参数存入 paramValues 数组。 而 doGet 和 doPost 方法,则直接调用 doDispatch: @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { doPost(req, resp); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { try { //处理请求 doDispatch(req, resp); } catch (Exception e) { resp.getWriter().write("500!! Server Exception"); } } 有兴趣的同学可以扩展 doPost 方法,使其可以接受 body 的数据。 测试 这里我们写了一个 Controller: @Controller@RequestMapping("/test")public class TestController { @Autowired private HelloWorldService helloWorldService; @RequestMapping("/test1") public void test1(HttpServletRequest request, HttpServletResponse response, @RequestParam("param") String param) { try { String text = helloWorldService.getString(); response.getWriter().write(text + " and the param is " + param); } catch (IOException e) { e.printStackTrace(); } }} 这里我们同时还注入了一个对象,HelloWorldService,来看一看和 Spring 的耦合是否成功。这个 test1 方法还需要传入一个参数 param,用于测试传参。 关于如何使用 tomcat 运行这个项目,可以参考 https://blog.csdn.net/fengchao2016/article/details/83023725 这篇教程。 当把项目通过 Tomcat 启动在 8080 端口后,访问http://localhost:8080/test1?param=abc,出现如下结果: Hello world and the param is abc成功!我们参数成功得传到了服务区,并且服务器的结果也成功地返回来了。
点赞 19
评论 8
声哥今天更新了吗
全部评论
推荐
最新
楼层
暂无评论,快来抢首评~
相关推荐
01-20 13:20
蚌埠坦克学院 嵌入式软件开发
嵌入式工资低?别急着下结论——你可能只是没碰到“好岗位”
很多人一听到“嵌入式”,第一反应就是:工资低、加班多、发展慢。尤其是在互联网行业的光环下,嵌入式常被贴上“落后、低薪、苦逼”的标签。但事实真的如此吗?并不完全。1. 嵌入式不是一个单一的行业,而是一条多分支的技术路线嵌入式并不是“写写单片机、做做小项目”这么简单。它覆盖的范围非常广:从最基础的单片机控制,到操作系统内核,再到通信协议、车载系统、工业自动化、智能硬件、物联网、边缘计算等。不同方向的岗位,技术难度和价值差异巨大。你可以在一些公司做“嵌入式外设驱动”,也可以在一些行业做“系统架构/内核/安全/通信”,两者的薪资差距往往超过想象。2. 工资高低,取决于行业的“价值密度”很多高薪行业都离...
点赞
评论
收藏
分享
01-22 14:02
飞鱼科技_美术设计部_角色原画(准入职员工)
飞鱼科技内推,飞鱼科技内推码
一面 35min经典自我介绍。长达25min的项目问题。(我PDF简历上贴了演示链接,但是面试官说打不开???)一个向量绕一个点怎么旋转?(这里我说我不会图形学,所以跳过了)那你了解点积和叉积吗,简单说说看看?应用场景?了解协程吗?协程是异步还是同步?项目里面有用到协程吗?你项目里面用到了接口吗?一个逻辑题:斗地主中的“飞机”怎么判断?你怎么设计数据结构来解决这个问题?那假设现在是有“癞子”的情况呢?(然后我BalaBala讲了一堆,看面试官最后的说法,大概说对了)。反问环节。请问贵公司对于鱼苗夏令营是一个怎么样的安排,参加夏令营的同学们会学习到什么知识?听说贵公司不止在研保卫萝卜,请问还有哪...
点赞
评论
收藏
分享
01-01 15:27
已编辑
合肥工业大学 前端工程师
24h一面到oc速通字节!挑战全网最快timeline
人生第一个offer,从此成为节孝子字节timeline:12.29 一面12.30 二面 hr面12.31 ocmomenta:12.25 一面12.30 二面 oc从刚开始面小厂被无情嘲讽,到一天拿下两个大厂offer,中间经历了好多好多困难和挫折,感谢一路上帮助的 人,感谢没有放弃的自己。再见2025,加油2026!
泪水打湿猪脚饭😭:
合工大依旧乱杀
我的OC时间线
点赞
评论
收藏
分享
2025-12-28 23:30
清华大学 视觉设计师
实习第一天,就被mt骂了,文档都不会看吗。这写的什么东西,我都教你多少遍了,从来没见过你这么差的实习生,这些名词都不认识吗?面试怎么进来的?有没有动脑子?
龙虾x:
工牌不戴了,戴学生证
工作中听到最受打击的一句...
点赞
评论
收藏
分享
01-20 10:28
蚌埠坦克学院 嵌入式软件开发
在上海实习生多少工资正常
在上海这样的一线城市,实习生的工资水平差异比较大,主要取决于公司规模、岗位性质和个人能力。总体上,大部分实习岗位的月薪通常落在 ¥2,000 – ¥6,000 左右。在一些普通企业或中小公司,实习生的月薪比较常见是 ¥2,000 – ¥4,000;在互联网、金融等热门行业,有经验或者技能强的实习生也可能拿到 ¥4,000 – ¥6,000 甚至更高。具体岗位如市场、产品类实习生平均每月大约 ¥3,000 – ¥4,000 左右。
实习生工资多少才算正常?
点赞
评论
收藏
分享
评论
点赞成功,聊一聊 >
4
收藏
分享
评论
提到的真题
返回内容
招聘动态
查看更多
牛客网申助</br>备战春招大杀器
27届寒假/转正实习汇总
全站热榜
更多
1
...
实习产出如何包装?
9328
2
...
32岁程序员猝死,底薪3千要24h待岗
2904
3
...
【官方活动】牛客新春计划:给陌生人的一封信
2636
4
...
实习生怎么快速融入团队
2368
5
...
后端从0开始来得及吗
1399
6
...
27届实习全时间全方位大体指南
1350
7
...
20多岁最痛苦的年纪
1277
8
...
绷不住了,找了一个月实习嵌入式还找不到
1231
9
...
25届工作半年,想辞职了
1131
10
...
在咖啡店、家里、公司走廊哪里都可以来一场面试
1061
创作者周榜
更多
正在热议
更多
#
牛客十周岁生日快乐
#
206786次浏览
1932人参与
#
你觉得什么岗位会被AI替代
#
34906次浏览
232人参与
#
我和mentor的爱恨情仇
#
101685次浏览
922人参与
#
一人一个landing小技巧
#
143132次浏览
1498人参与
#
如果工作一直消耗情绪还要继续做吗
#
18102次浏览
83人参与
#
四大天坑是哪四家?
#
101613次浏览
235人参与
#
互联网公司评价
#
479747次浏览
4091人参与
#
机械人春招想让哪家公司来捞你?
#
377777次浏览
3127人参与
#
聊聊你的被动加班经历
#
4351次浏览
80人参与
#
在国企工作的人,躺平了吗?
#
392078次浏览
3951人参与
#
我的求职精神状态
#
422480次浏览
3075人参与
#
华为工作体验
#
289409次浏览
1376人参与
#
实习吐槽大会
#
404957次浏览
2168人参与
#
工作压力大怎么缓解
#
138895次浏览
1260人参与
#
找工作以来,你最看不惯__
#
17465次浏览
352人参与
#
你的mentor是什么样的人?
#
49278次浏览
705人参与
#
第一次找实习,我建议__
#
69306次浏览
841人参与
#
实习教会我的事
#
52166次浏览
413人参与
#
实习怎么做才有更好的产出
#
13954次浏览
263人参与
#
AI coding的好用工具分享
#
21515次浏览
409人参与
牛客网
牛客网在线编程
牛客网题解
牛客企业服务