用不到 300 行代码来描述 Spring IOC、DI、MVC 的精华设计思想,并保证基本功能完整。
Spring 的三个阶段,配置阶段、初始化阶段和运行阶段
image.png
配置阶段:主要是完成 application.xml 配置和 Annotation 配置。
初始化阶段:主要是加载并解析配置信息,然后,初始化 IOC 容器,完成容器的 DI 操作,已经完成 HandlerMapping 的初始化。
运行阶段:主要是完成 Spring 容器启动以后,完成用户请求的内部调度,并返回响应结果。
项目结构
image.png
一、配置阶段
1、pom.xml配置
我采用的是 maven 管理项目。先来看 pom.xml 文件中的配置,我只引用了 servlet-api 的依赖。
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
sz-spring
com.suzao.spring
1.0-SNAPSHOT
4.0.0
spring-demo-01
javax.servlet
servlet-api
2.5
war
webapp
org.apache.maven.plugins
maven-war-plugin
3.2.2
src/main/webapp/WEB-INF
WEB-INF
2、SZDispatcherServlet 类
然后,创建 SZDispatcherServlet 类并继承 HttpServlet,重写 init()、doGet() 和 doPost() 方法。
package com.suzao.mvcframework.servlet.v2;
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.IOException;
/**
* @ClassName SZDispatchServlet
* @Description: TODO
* @Author mc
* @Date 2020
* @Version V1.0
**/
public class SZDispatchServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doGet(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
}
}
3、web.xml配置
在 web.xml 文件中配置以下信息:
xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:javaee="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/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
version="2.4">
SuZao Web Application
szmvc
com.suzao.mvcframework.servlet.v2.SZDispatchServlet
contextConfigLocation
application.properties
1
szmvc
/*
4、application.properties配置
我们配置了一个初始化加载的 Spring 主配置文件路径,在原生框架中,我们应该配置的是 classpath:application.xml。在这里,我们为了简化操作,用 properties 文件代替 xml 文件。以下是 properties 文件中的内容:
scanPackage=com.suzao.demo
5、创建SZController 注解:
package com.suzao.mvcframework.annotation;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SZController {
String value() default "";
}
6、创建SZService 注解
package com.suzao.mvcframework.annotation;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SZService {
String value() default "";
}
7、创建SZRequestMapping注解
package com.suzao.mvcframework.annotation;
import java.lang.annotation.*;
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SZRequestMapping {
String value() default "";
}
8、创建SZRequestParam注解
package com.suzao.mvcframework.annotation;
import java.lang.annotation.*;
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SZRequestParam {
String value() default "";
}
9、创建SZAutowired 注解
package com.suzao.mvcframework.annotation;
import java.lang.annotation.*;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SZAutowired {
String value() default "";
}
10、使用自定义注解进行配置DemoAction
package com.suzao.demo.action;
import com.suzao.demo.service.IDemoService;
import com.suzao.mvcframework.annotation.SZAutowired;
import com.suzao.mvcframework.annotation.SZController;
import com.suzao.mvcframework.annotation.SZRequestMapping;
import com.suzao.mvcframework.annotation.SZRequestParam;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @ClassName DemoAction
* @Description: TODO
* @Author mc
* @Date 2020
* @Version V1.0
**/
@SZController
@SZRequestMapping("/demo")
public class DemoAction {
@SZAutowired
private IDemoService demoService;
@SZRequestMapping("query")
public void query(HttpServletRequest req, HttpServletResponse resp,
@SZRequestParam("name") String name){
String result = demoService.get(name);
try {
resp.getWriter().write(result);
}catch (Exception e){
e.printStackTrace();
}
}
@SZRequestMapping("add")
public void add(HttpServletRequest request , HttpServletResponse resp,
@SZRequestParam("a") Integer a, @SZRequestParam("b") Integer b){
try {
resp.getWriter().write(a+"+" +b +"=" +(a+b));
}catch (Exception e){
e.printStackTrace();
}
}
@SZRequestMapping("remove")
public void remove(HttpServletRequest req , HttpServletResponse resp,
@SZRequestParam("id") Integer id){
}
}
11、使用自定义注解进行配置DemoService
package com.suzao.demo.service;
import com.suzao.mvcframework.annotation.SZService;
/**
* @ClassName DemoService
* @Description: TODO
* @Author mc
* @Date 2020
* @Version V1.0
**/
@SZService
public class DemoService implements IDemoService {
@Override
public String get(String name) {
return "My name is "+ name;
}
}
12、IDemoService
package com.suzao.demo.service;
/**
* @ClassName IDemoService
* @Description: TODO
* @Author mc
* @Date 2020
* @Version V1.0
**/
public interface IDemoService {
String get(String name);
}
二、初始化阶段
1、先在 SZDispatcherServlet 中声明几个成员变量
//保存application.properties配置文件中的内容
private Properties contextConfig = new Properties();
//保存扫描的所有的类名
private List classNames = new ArrayList<>();
//IOC容器
private Map ioc = new HashMap<>();
//保存url和Method的对应关系
private Map handleMapping = new HashMap<>();
2、SZDispatcherServlet 的init()方法
当 Servlet 容器启动时,会调用 SZDispatcherServlet 的 init()方法,从 init 方法的参数中,我们可以拿到主配置文件的路径,从能够读取到配置文件中的信息。前面我们已经介绍了 Spring 的三个阶段,现在来完成初始化阶段的代码。在 init() 方法中,定义好执行步骤,如下:
@Override
public void init(ServletConfig config) throws ServletException {
//1.加载配置文件
doLoadConfig(config.getInitParameter("contextConfigLocation"));
//2.扫描相关的类
doScanner(contextConfig.getProperty("scanPackage"));
//3.初始化扫描到的类,并且放入到IOC容器中
doInstance();
//4.完成自动化的依赖注入
doAutowired();
//5.初始化HandlerMapping
doInitHandlerMapping();
System.out.println("SZ Spring framework is init.");
}
3、doLoadConfig() 方法
doLoadConfig() 方法的实现,将文件读取到 Properties 对象中
//加载配置文件
private void doLoadConfig(String contextConfigLocation) {
//直接从类路径下找到Spring主配置文件所在的路径
//并且将其读取出来放到Properties对象中
//相对于scanPackage=com.suzao.demo从文件中保存到内存中
InputStream is = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation);
try {
contextConfig.load(is);
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
4、doScanner() 方法
doScanner() 方法,递归扫描出所有的 Class 文件
//扫描出相关的类
private void doScanner(String scanPackage) {
URL url = this.getClass().getClassLoader().getResource("/"+scanPackage.replaceAll("\\.","/" ));
//scanPackage= com.suzao.demo 存储的包路径
//转换为文件路径,实际上就是把 .替换成/
//classpath下不仅有.class 文件, .xml文件 .properties文件
File classPath = new File(url.getFile());
for (File file :classPath.listFiles()){
if(file.isDirectory()){
doScanner(scanPackage + "." + file.getName());
}else {
//变成包名.类名
//Class.forname()
if(!file.getName().endsWith(".class")){
continue;
}
classNames.add(scanPackage + "." + file.getName().replace(".class","" ));
}
}
}
5、doInstance() 方法
doInstance() 方法,初始化所有相关的类,并放入到 IOC 容器之中。IOC 容器的 key 默认是类名首字母小写,如果是自己设置类名,则优先使用自定义的。因此,要先写一个针对类名首字母处理的工具方法
private String toLowerFirstCase(String simpleName) {
char[] chars = simpleName.toCharArray();
chars[0] += 32;
return String.valueOf(chars);
}
private void doInstance() {
if(classNames.isEmpty()){
return;
}
try {
for(String className : classNames){
Class> clazz = Class.forName(className);
//什么样的类才需要初始化呢
//加了注解的类,才初始化,怎么判断
//为了简化代码逻辑,主要体会设计思想,只举例@Controller @Service
if(clazz.isAnnotationPresent(SZController.class)){
Object instance = clazz.newInstance();
String beanName = toLowerFirstCase(clazz.getSimpleName());
ioc.put(beanName,instance );
} else if (clazz.isAnnotationPresent(SZService.class)) {
//1.默认根据beanName类名首字母小写
String beanName = toLowerFirstCase(clazz.getSimpleName());
//2.使用自定义的beanName
SZService service = clazz.getAnnotation(SZService.class);
if(!"".equals(service.value())){
beanName = service.value();
}
Object instance = clazz.newInstance();
ioc.put(beanName, instance);
//3.根据包名.类名作为beanName
for (Class> i : clazz.getInterfaces()){
if(ioc.containsKey(i.getName())){
throw new Exception("The beanName is exists!!");
}
//把接口的类型直接当成key了
ioc.put(i.getName(),instance );
}
}else {
continue;
}
}
}catch (Exception e){
e.printStackTrace();
}
}
doAutowired() 方法
doAutowired() 方法,将初始化到 IOC 容器中的类,需要赋值的字段进行赋值
private void doAutowired() {
if(ioc.isEmpty()){
return;
}
for (Map.Entry entry : ioc.entrySet()){
Field[] fields = entry.getValue().getClass().getDeclaredFields();
for (Field field : fields){
if(!field.isAnnotationPresent(SZAutowired.class)){
continue;
}
SZAutowired autowired = field.getAnnotation(SZAutowired.class);
//如果用户没有自定义beanname ,默认就根据类型注入
String beanName = autowired.value().trim();
if("".equals(beanName)){
beanName = field.getType().getName();
}
//如果是public以外的修饰符,只要加了@Autowired注解,都要强制赋值
//暴力访问
field.setAccessible(true);
//反射调用
//给entry.getValue()这个对象的field字段,赋ioc.get(beanName)的值
try {
field.set(entry.getValue(),ioc.get(beanName) );
} catch (IllegalAccessException e) {
e.printStackTrace();
continue;
}
}
}
}
6、doInitHandlerMapping() 方法
doInitHandlerMapping() 方法,将 SZRequestMapping 中配置的信息和 Method 进行关联,并保存这些关系。
//初始化url和method的一对一对应关系
private void doInitHandlerMapping() {
if(ioc.isEmpty()){
return;
}
for (Map.Entry entry : ioc.entrySet()){
Class> clazz = entry.getValue().getClass();
if(!clazz.isAnnotationPresent(SZController.class)){
continue;
}
//保存写在类上面的@GPRequestMapping("/demo")
String baseUrl = "";
if(clazz.isAnnotationPresent(SZRequestMapping.class)){
SZRequestMapping requestMapping = clazz.getAnnotation(SZRequestMapping.class);
baseUrl = requestMapping.value();
}
//默认获取所有的public方法
for(Method method : clazz.getMethods()){
if(!method.isAnnotationPresent(SZRequestMapping.class)){
continue;
}
SZRequestMapping requestMapping = method.getAnnotation(SZRequestMapping.class);
String url = ("/" + baseUrl + "/" + requestMapping.value()).replaceAll("/+","/" );
handleMapping.put(url,method );
System.out.println("Mapped " + url + "," + method);
}
}
}
到此,初始化阶段的所有代码全部写完。
三、运行阶段
1、doPost()方法
来到运行阶段,当用户发送请求被 Servlet 接受时,都会统一调用 doPost 方法,我先在 doPost 方法中再调用 doDispach() 方法
@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 {
try {
doDispatch(req,resp);
}catch (Exception e){
e.printStackTrace();
resp.getWriter().write("500 Exception " + Arrays.toString(e.getStackTrace()));
}
}
2、doDispatch() 方法
private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception{
String url = req.getRequestURI();
String contextPath = req.getContextPath();
url = url.replace(contextPath,"" ).replaceAll("/+","/" );
if(!this.handleMapping.containsKey(url)){
resp.getWriter().write("404 Not Found!");
return;
}
Method method = this.handleMapping.get(url);
Map paramsMap = req.getParameterMap();
//实参列表要根据形参列表才能决定,首先得拿到形参列表
Class>[] parameterTypes = method.getParameterTypes();
Object[] parameValus = new Object[parameterTypes.length];
for (int i = 0 ;i< parameterTypes.length ; i++){
Class paramterType = parameterTypes[i];
if(paramterType == HttpServletRequest.class){
parameValus[i] = req;
continue;
}else if(paramterType == HttpServletResponse.class){
parameValus[i] = resp;
continue;
}else if(paramterType == String.class){
Annotation[][] pa = method.getParameterAnnotations();
for (Annotation a : pa[i]){
if(a instanceof SZRequestParam){
String paramName = ((SZRequestParam) a).value();
if(!"".equals(paramName.trim())){
String value = Arrays.toString(paramsMap.get(paramName))
.replaceAll("\\[|\\]", "")
.replaceAll("\\s","," );
parameValus[i] = value;
}
}
}
/*for(int j =0 ;j < pa.length ;j++){
}*/
}
}
String beanName = toLowerFirstCase(method.getDeclaringClass().getSimpleName());
method.invoke(ioc.get(beanName),parameValus);
}
image.png
当然,真正的 Spring 要复杂很多,但核心设计思路基本如此。
4、SZDispatchServlet完整代码
package com.suzao.mvcframework.servlet.v2;
import com.suzao.mvcframework.annotation.*;
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.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.*;
/**
* @ClassName SZDispatchServlet
* @Description: TODO
* @Author mc
* @Date 2020
* @Version V1.0
**/
public class SZDispatchServlet extends HttpServlet {
//保存application.properties配置文件中的内容
private Properties contextConfig = new Properties();
//保存扫描的所有的类名
private List classNames = new ArrayList<>();
//IOC容器
private Map ioc = new HashMap<>();
//保存url和Method的对应关系
private Map handleMapping = new HashMap<>();
@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 {
try {
doDispatch(req,resp);
}catch (Exception e){
e.printStackTrace();
resp.getWriter().write("500 Exception " + Arrays.toString(e.getStackTrace()));
}
}
private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception{
String url = req.getRequestURI();
String contextPath = req.getContextPath();
url = url.replace(contextPath,"" ).replaceAll("/+","/" );
if(!this.handleMapping.containsKey(url)){
resp.getWriter().write("404 Not Found!");
return;
}
Method method = this.handleMapping.get(url);
Map paramsMap = req.getParameterMap();
//实参列表要根据形参列表才能决定,首先得拿到形参列表
Class>[] parameterTypes = method.getParameterTypes();
Object[] parameValus = new Object[parameterTypes.length];
for (int i = 0 ;i< parameterTypes.length ; i++){
Class paramterType = parameterTypes[i];
if(paramterType == HttpServletRequest.class){
parameValus[i] = req;
continue;
}else if(paramterType == HttpServletResponse.class){
parameValus[i] = resp;
continue;
}else if(paramterType == String.class){
Annotation[][] pa = method.getParameterAnnotations();
for (Annotation a : pa[i]){
if(a instanceof SZRequestParam){
String paramName = ((SZRequestParam) a).value();
if(!"".equals(paramName.trim())){
String value = Arrays.toString(paramsMap.get(paramName))
.replaceAll("\\[|\\]", "")
.replaceAll("\\s","," );
parameValus[i] = value;
}
}
}
/*for(int j =0 ;j < pa.length ;j++){
}*/
}
}
String beanName = toLowerFirstCase(method.getDeclaringClass().getSimpleName());
method.invoke(ioc.get(beanName),parameValus);
}
@Override
public void init(ServletConfig config) throws ServletException {
//1.加载配置文件
doLoadConfig(config.getInitParameter("contextConfigLocation"));
//2.扫描相关的类
doScanner(contextConfig.getProperty("scanPackage"));
//3.初始化扫描到的类,并且放入到IOC容器中
doInstance();
//4.完成自动化的依赖注入
doAutowired();
//5.初始化HandlerMapping
doInitHandlerMapping();
System.out.println("SZ Spring framework is init.");
}
//初始化url和method的一对一对应关系
private void doInitHandlerMapping() {
if(ioc.isEmpty()){
return;
}
for (Map.Entry entry : ioc.entrySet()){
Class> clazz = entry.getValue().getClass();
if(!clazz.isAnnotationPresent(SZController.class)){
continue;
}
//保存写在类上面的@GPRequestMapping("/demo")
String baseUrl = "";
if(clazz.isAnnotationPresent(SZRequestMapping.class)){
SZRequestMapping requestMapping = clazz.getAnnotation(SZRequestMapping.class);
baseUrl = requestMapping.value();
}
//默认获取所有的public方法
for(Method method : clazz.getMethods()){
if(!method.isAnnotationPresent(SZRequestMapping.class)){
continue;
}
SZRequestMapping requestMapping = method.getAnnotation(SZRequestMapping.class);
String url = ("/" + baseUrl + "/" + requestMapping.value()).replaceAll("/+","/" );
handleMapping.put(url,method );
System.out.println("Mapped " + url + "," + method);
}
}
}
private void doAutowired() {
if(ioc.isEmpty()){
return;
}
for (Map.Entry entry : ioc.entrySet()){
Field[] fields = entry.getValue().getClass().getDeclaredFields();
for (Field field : fields){
if(!field.isAnnotationPresent(SZAutowired.class)){
continue;
}
SZAutowired autowired = field.getAnnotation(SZAutowired.class);
//如果用户没有自定义beanname ,默认就根据类型注入
String beanName = autowired.value().trim();
if("".equals(beanName)){
beanName = field.getType().getName();
}
//如果是public以外的修饰符,只要加了@Autowired注解,都要强制赋值
//暴力访问
field.setAccessible(true);
//反射调用
//给entry.getValue()这个对象的field字段,赋ioc.get(beanName)的值
try {
field.set(entry.getValue(),ioc.get(beanName) );
} catch (IllegalAccessException e) {
e.printStackTrace();
continue;
}
}
}
}
private void doInstance() {
if(classNames.isEmpty()){
return;
}
try {
for(String className : classNames){
Class> clazz = Class.forName(className);
//什么样的类才需要初始化呢
//加了注解的类,才初始化,怎么判断
//为了简化代码逻辑,主要体会设计思想,只举例@Controller @Service
if(clazz.isAnnotationPresent(SZController.class)){
Object instance = clazz.newInstance();
String beanName = toLowerFirstCase(clazz.getSimpleName());
ioc.put(beanName,instance );
} else if (clazz.isAnnotationPresent(SZService.class)) {
//1.默认根据beanName类名首字母小写
String beanName = toLowerFirstCase(clazz.getSimpleName());
//2.使用自定义的beanName
SZService service = clazz.getAnnotation(SZService.class);
if(!"".equals(service.value())){
beanName = service.value();
}
Object instance = clazz.newInstance();
ioc.put(beanName, instance);
//3.根据包名.类名作为beanName
for (Class> i : clazz.getInterfaces()){
if(ioc.containsKey(i.getName())){
throw new Exception("The beanName is exists!!");
}
//把接口的类型直接当成key了
ioc.put(i.getName(),instance );
}
}else {
continue;
}
}
}catch (Exception e){
e.printStackTrace();
}
}
private String toLowerFirstCase(String simpleName) {
char[] chars = simpleName.toCharArray();
chars[0] += 32;
return String.valueOf(chars);
}
//扫描出相关的类
private void doScanner(String scanPackage) {
URL url = this.getClass().getClassLoader().getResource("/"+scanPackage.replaceAll("\\.","/" ));
//scanPackage= com.suzao.demo 存储的包路径
//转换为文件路径,实际上就是把 .替换成/
//classpath下不仅有.class 文件, .xml文件 .properties文件
File classPath = new File(url.getFile());
for (File file :classPath.listFiles()){
if(file.isDirectory()){
doScanner(scanPackage + "." + file.getName());
}else {
//变成包名.类名
//Class.forname()
if(!file.getName().endsWith(".class")){
continue;
}
classNames.add(scanPackage + "." + file.getName().replace(".class","" ));
}
}
}
//加载配置文件
private void doLoadConfig(String contextConfigLocation) {
//直接从类路径下找到Spring主配置文件所在的路径
//并且将其读取出来放到Properties对象中
//相对于scanPackage=com.suzao.demo从文件中保存到内存中
InputStream is = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation);
try {
contextConfig.load(is);
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}