在我们的日常工作中,经常会用到 Spring、Spring Boot、Spring Cloud、Struts、Mybatis、Hibernate 等开源框架,有了这些框架的诞生,平时的开发工作量也是变得越来越轻松,我们用 Spring Boot
分分钟可以新建一个 Web 项目。
记得自己刚开始工作的时候还是在用 Servlet
写Web
项目,自己写 数据库连接池 ,用原生JDBC
操作数据库,好了不发散了。回到这篇文章的主题,今天通过手写 Spring 框架,帮大家深入了解一下 Spring 的工作机制,文中涉及的代码只用来帮助大家理解 Spring,不会在线上使用,有不严谨的地方还请大家掠过。
<!– more –>
项目结构
框架部分实现
- 为了区分框架部分代码和业务部分代码,我们将这两部分分别划分在不同的包内
com.mars.demo
和com.mars.framework
,以便随后只扫描业务代码。 - 这里是自己手写 Spring 框架,所以不会引入任何 Spring 项目相关的包。
- 由于是一个 Web 项目,所有我们需要引入
servlet-api
包,仅供编译器使用,所有配置scope
为provided
。
新建一个 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>
- 首先配置了一个 servlet,名字是 marsmvc,类全路径是
com.mars.framework.servlet.MarsDispatcherServlet
。 - 设置了初始化参数名和值(这里的值是整个项目的配置文件)。
- 配置
load-on-startup
, 标记容器是否在启动的时候就加载这个 servlet(实例化并调用其 init()方法)。 - 配置
servlet-mapping
, 将所有请求转发到这个 servlet 处理。
配置 application.properties
scanPackage=com.mars.demo
这个比较好理解,仅配置了一项内容,意思是要扫描的包,随后我们会获取这个值去加载容器。
定义我们常用的注解
- MarsAutowired
- MarsController
- MarsRequestMapping
- MarsRequestParam
- 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 功能
先列出框架在初始化的时候都要做那些事情
- 加载配置文件
- 扫描所有相关联的类
- 初始化所有相关联的类,并且将其保存在 IOC 容器里面
- 执行依赖注入(把加了 @Autowired 注解的字段赋值)
- 构造 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
仓库地址
欢迎访问我的个人博客
关注公众号:JAVA 九点半课堂,这里有一批优秀的程序猿,加入我们,一起探讨技术,共同进步!回复“资料”获取 2T 行业最新资料!