关注:Java提升营,最新文章第一时间送达,10T 免费学习资料随时领取!!!

在我们的日常工作中,经常会用到Spring、Spring Boot、Spring Cloud、Struts、Mybatis、Hibernate等开源框架,有了这些框架的诞生,平时的开发工作量也是变得越来越轻松,我们用 Spring Boot 分分钟可以新建一个Web项目。

记得自己刚开始工作的时候还是在用ServletWeb项目,自己写数据库连接池,用原生JDBC操作数据库,好了不发散了。回到这篇文章的主题,今天通过手写Spring框架,帮大家深入了解一下Spring的工作机制,文中涉及的代码只用来帮助大家理解Spring,不会在线上使用,有不严谨的地方还请大家掠过。

项目结构

框架部分实现

  1. 为了区分框架部分代码和业务部分代码,我们将这两部分分别划分在不同的包内 com.mars.democom.mars.framework,以便随后只扫描业务代码。
  2. 这里是自己手写Spring框架,所以不会引入任何Spring项目相关的包。
  3. 由于是一个Web项目,所有我们需要引入 servlet-api 包,仅供编译器使用,所有配置 scopeprovided

新建一个Servlet

首先新建一个 HttpServlet 的实现类 MarsDispatcherServlet,用来接收请求。

public class MarsDispatcherServlet extends HttpServlet {

    @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 {
//6. 处理请求
} @Override
public void init(ServletConfig config) throws ServletException { }

配置web.xml

<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" > <web-app>
<display-name>Spring Mvc Education</display-name> <servlet>
<servlet-name>marsmvc</servlet-name>
<servlet-class>com.mars.framework.servlet.MarsDispatcherServlet</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>marsmvc</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
  1. 首先配置了一个 servlet, 名字是 marsmvc, 类全路径是 com.mars.framework.servlet.MarsDispatcherServlet
  2. 设置了初始化参数名和值(这里的值是整个项目的配置文件)。
  3. 配置 load-on-startup, 标记容器是否在启动的时候就加载这个servlet(实例化并调用其init()方法)。
  4. 配置 servlet-mapping, 将所有请求转发到这个servlet处理。

配置application.properties

scanPackage=com.mars.demo

这个比较好理解,仅配置了一项内容,意思是要扫描的包,随后我们会获取这个值去加载容器。

定义我们常用的注解

  1. MarsAutowired
  2. MarsController
  3. MarsRequestMapping
  4. MarsRequestParam
  5. MarsService

这里仅列举两个,其他都大同小异,需要源码的可以去我的代码仓库fork。

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

充实Servlet功能

先列出框架在初始化的时候都要做那些事情

  1. 加载配置文件
  2. 扫描所有相关联的类
  3. 初始化所有相关联的类,并且将其保存在IOC容器里面
  4. 执行依赖注入(把加了@Autowired注解的字段赋值)
  5. 构造HandlerMapping,将URL和Method进行关联

接下来我们一步步完成上面的操作

    @Override
public void init(ServletConfig config) throws ServletException {
System.out.println("===================");
//1.加载配置文件
doLoadConfig(config.getInitParameter("contextConfigLocation")); //2.扫描所有相关联的类
doScanner(contextConfig.getProperty("scanPackage")); //3.初始化所有相关联的类,并且将其保存在IOC容器里面
doInstance(); //4.执行依赖注入(把加了@Autowired注解的字段赋值)
doAutowired(); //Spring 和核心功能已经完成 IOC、DI //5.构造HandlerMapping,将URL和Method进行关联
initHandlerMapping(); System.out.println("Mars MVC framework initialized"); }

加载配置文件

    private Properties contextConfig = new Properties();

    private void doLoadConfig(String location) {
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(location); try {
contextConfig.load(inputStream);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

扫描所有相关联的类

    private void doScanner(String basePackage) {
//获取要扫描包的url
URL url = this.getClass().getClassLoader().getResource("/" + basePackage.replaceAll("\\.", "/")); File dir = new File(url.getFile());
//遍历包下面所有文件
for(File file: dir.listFiles()) {
if(file.isDirectory()){
//递归扫描
doScanner(basePackage + "." + file.getName());
} else {
String className = basePackage + "." + file.getName().replace(".class", ""); classNames.add(className); System.out.println(className);
}
} }

初始化所有相关联的类,并且将其保存在IOC容器里面

private void doInstance() {

        if(classNames.isEmpty()) return;

        for(String className: classNames) {

            try {
Class<?> clazz = Class.forName(className); if(clazz.isAnnotationPresent(MarsController.class)) { Object instance = clazz.newInstance();
String beanName = lowerFirstCase(clazz.getSimpleName());
ioc.put(beanName, instance); } else if (clazz.isAnnotationPresent(MarsService.class)) { MarsService service = clazz.getAnnotation(MarsService.class); //2.优先使用自定义命名
String beanName = service.value(); if("".equals(beanName.trim())) {
//1.默认使用类名首字母小写
beanName = lowerFirstCase(clazz.getSimpleName());
} Object instance = clazz.newInstance(); ioc.put(beanName, instance); //3.自动类型匹配(例如:将实现类赋值给接口) Class<?> [] interfaces = clazz.getInterfaces(); for(Class<?> inter: interfaces) {
ioc.put(inter.getName(), instance);
} } } catch (Exception e) {
e.printStackTrace();
}
} } //利用ASCII码的差值
private String lowerFirstCase(String str) {
char[] chars = str.toCharArray();
chars[0] += 32;
return String.valueOf(chars);
}

执行依赖注入(把加了@Autowired注解的字段赋值)

private void doAutowired() {

        if(ioc.isEmpty()) return;

        for(Map.Entry<String, Object> entry: ioc.entrySet()) {
//注入的意思就是把所有的IOC容器中加了@Autowired注解的字段赋值
//包含私有字段
Field[] fields = entry.getValue().getClass().getDeclaredFields(); for(Field field : fields) { //判断是否加了@Autowired注解
if(!field.isAnnotationPresent(MarsAutowired.class)) continue; MarsAutowired autowired = field.getAnnotation(MarsAutowired.class); String beanName = autowired.value(); if("".equals(beanName)) {
beanName = field.getType().getName();
} //如果这个字段是私有字段的话,那么要强制访问
field.setAccessible(true);
try {
field.set(entry.getValue(), ioc.get(beanName));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}

构造HandlerMapping,将URL和Method进行关联

private void initHandlerMapping() {
if(ioc.isEmpty()) return; for(Map.Entry<String, Object> entry : ioc.entrySet()) {
Class<?> clazz = entry.getValue().getClass(); if(!clazz.isAnnotationPresent(MarsController.class)) continue; String baseUrl = ""; if(clazz.isAnnotationPresent(MarsRequestMapping.class)) {
MarsRequestMapping requestMapping = clazz.getAnnotation(MarsRequestMapping.class);
baseUrl = requestMapping.value();
} Method[] methods = clazz.getMethods(); for(Method method : methods) { if(!method.isAnnotationPresent(MarsRequestMapping.class)) continue; MarsRequestMapping requestMapping = method.getAnnotation(MarsRequestMapping.class); String regex = requestMapping.value(); regex = (baseUrl + regex).replaceAll("/+", "/"); Pattern pattern = Pattern.compile(regex);
handlerMapping.add(new Handler(entry.getValue(), method, pattern)); System.out.println("Mapping: " + regex + "," + method.getName());
}
} }

编写业务代码

新建一个Controller

@MarsController
@MarsRequestMapping("/demo")
public class DemoApi { @MarsAutowired
private DemoService demoService; @MarsRequestMapping("/query")
public void query(HttpServletRequest req,
HttpServletResponse resp,
@MarsRequestParam("name") String name) {
System.out.println("name: " + name);
String result = demoService.get(name); try{
resp.getWriter().write(result);
} catch (IOException e) {
e.printStackTrace();
}
} @MarsRequestMapping("/add")
public void add(HttpServletRequest req,
HttpServletResponse resp,
@MarsRequestParam("a") Integer a,
@MarsRequestParam("b") Integer b) {
try {
resp.getWriter().write(String.format("%d+%d=%d", a, b, (a+b)));
} catch (IOException e) {
e.printStackTrace();
}
}
}

提供两个接口,一个通过请求名称返回响应的介绍内容,另一个将请求的两个Integer相加并返回。

创建一个Service

public interface DemoService {
String get(String name);
} @MarsService
public class DemoServiceImpl implements DemoService {
public String get(String name) {
return String.format("My name is %s.", name);
}
}

添加Jetty插件

我们的项目运行在Jetty中,所以添加相关插件以及配置:

<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<version>7.1.6.v20100715</version>
<configuration>
<stopPort>9988</stopPort>
<stopKey>foo</stopKey>
<scanIntervalSeconds>5</scanIntervalSeconds>
<connectors>
<connector implementation="org.eclipse.jetty.server.nio.SelectChannelConnector">
<port>8080</port>
<maxIdleTime>60000</maxIdleTime>
</connector>
</connectors>
<webAppConfig>
<contextPath>/</contextPath>
</webAppConfig>
</configuration>
</plugin>

运行

点击 jetty:run 运行项目

浏览器访问: http://localhost:8080/demo/query?name=Mars

浏览器访问:http://localhost:8080/demo/add?a=10&b=20

仓库地址

欢迎访问我的个人博客

手写Spring框架,加深对Spring工作机制的理解!的更多相关文章

  1. Spring 08: AOP面向切面编程 + 手写AOP框架

    核心解读 AOP:Aspect Oriented Programming,面向切面编程 核心1:将公共的,通用的,重复的代码单独开发,在需要时反织回去 核心2:面向接口编程,即设置接口类型的变量,传入 ...

  2. 手写SpringMVC 框架

    手写SpringMVC框架 细嗅蔷薇 心有猛虎 背景:Spring 想必大家都听说过,可能现在更多流行的是Spring Boot 和Spring Cloud 框架:但是SpringMVC 作为一款实现 ...

  3. 手写RPC框架指北另送贴心注释代码一套

    Angular8正式发布了,Java13再过几个月也要发布了,技术迭代这么快,框架的复杂度越来越大,但是原理是基本不变的.所以沉下心看清代码本质很重要,这次给大家带来的是手写RPC框架. 完整代码以及 ...

  4. 手写SpringMVC框架(三)-------具体方法的实现

    续接前文 手写SpringMVC框架(二)结构开发设计 本节我们来开始具体方法的代码实现. doLoadConfig()方法的开发 思路:我们需要将contextConfigLocation路径读取过 ...

  5. Spring框架系列(9) - Spring AOP实现原理详解之AOP切面的实现

    前文,我们分析了Spring IOC的初始化过程和Bean的生命周期等,而Spring AOP也是基于IOC的Bean加载来实现的.本文主要介绍Spring AOP原理解析的切面实现过程(将切面类的所 ...

  6. Spring框架系列(11) - Spring AOP实现原理详解之Cglib代理实现

    我们在前文中已经介绍了SpringAOP的切面实现和创建动态代理的过程,那么动态代理是如何工作的呢?本文主要介绍Cglib动态代理的案例和SpringAOP实现的原理.@pdai Spring框架系列 ...

  7. Spring框架系列(12) - Spring AOP实现原理详解之JDK代理实现

    上文我们学习了SpringAOP Cglib动态代理的实现,本文主要是SpringAOP JDK动态代理的案例和实现部分.@pdai Spring框架系列(12) - Spring AOP实现原理详解 ...

  8. Spring框架学习03——Spring Bean 的详解

    1.Bean 的配置 Spring可以看做一个大型工厂,用于生产和管理Spring容器中的Bean,Spring框架支持XML和Properties两种格式的配置文件,在实际开发中常用XML格式的配置 ...

  9. 手写DAO框架(一)-从“1”开始

    背景: 很久(4年)之前写了一个DAO框架-zxdata(https://github.com/shuimutong/zxdata),这是我写的第一个框架.因为没有使用文档,我现在如果要用的话,得从头 ...

  10. Spring框架系列(2) - Spring简单例子引入Spring要点

    上文中我们简单介绍了Spring和Spring Framework的组件,那么这些Spring Framework组件是如何配合工作的呢?本文主要承接上文,向你展示Spring Framework组件 ...

随机推荐

  1. 浅析MVC Pattern

    一.前言 最近做CAD插件相关的工作,用到了一些模式,解决对应场景的问题. 比如插件的运行实例上使用Singleton.实例内部使用了MVC(Strategy and Observer ). 针对CA ...

  2. Java11月9日的动手动脑

    Java动手动脑 请自行编写代码测试以下特性(动手动脑): 在子类中,若要调用父类中被覆盖的方法,可以使用super关键字. 程序源代码: package yanzhengjicheng; class ...

  3. OFD电子文档阅读器功能说明(采用WPF开发,永久免费)

    特别说明 ofd阅读器开发语言为c#,具有完全自主产权,没有使用第三方ofd开发包.可以根据你的需求快速定制开发.本阅读器还在开发完善阶段,如有任何问题,可以联系我QQ:13712486.博客:htt ...

  4. mac下编译openjdk8?so easy!

    写在最前面的话:建议第一次编译jdk的朋友,查阅任何博客都不如认真读一遍源码中的README-builds.html 环境:macOS High Sierra(10.13.5) 编译jdk:openj ...

  5. 用阿里官网提供的plupload oss的web直传,视频上传进行前端验证它的时长,尺寸,大小等。替换上一个不需要的单个视频

    accessid = '' accesskey = '' host = '' policyBase64 = '' signature = '' callbackbody = '' filename = ...

  6. Jenkins邮件收发(qq邮箱)

    首先确认QQ邮箱SMTP服务器的地址和端口号.如下图所示,请谨记,JENKINS全局邮箱配置需要使用: 步骤1:开启QQ邮箱的smtp服务:登陆QQ邮箱-设置-账户-开启POP3/SMTP服务-完成“ ...

  7. Java学习笔记之Object常用方法

    Object:万类之祖   == : 比较的是是否是同一个对象,比较的是地址   equals: 是Object里面的方法,默认的是==,比较的是地址,但在String类型里重写为比较内容 一般我们在 ...

  8. ubuntu16.04 gcc升级到7.3

    下载gcc- wget https://mirrors.ustc.edu.cn/gnu/gcc/gcc-7.3.0/gcc-7.3.0.tar.gz 运行 download_prerequisites ...

  9. C#窗体练习:带历史信息的菜单

    在开发图纸管理软件时,要求在菜单上记录用户最近打开的档案或图纸,以方便下次使用.单击“文件”菜单下的“打开文件”子菜单,打开需要查阅的图纸.下次运行该软件时,上次打开的文件名记录到“文件”菜单的历史菜 ...

  10. JVM 启动调优总结

    启动命令 格式: java -jar 命令行参数 jar包路径 .示例如下 java -Dfile.encoding=utf-8 -jar -XX:MetaspaceSize=128m -XX:Max ...