随着不断地使用Spring,以及后续的Boot、cloud,不断的体会到这个拯救Java的生态体系的强大,也使我对于这个框架有了极大的好奇心,以至于产生了我为什么不能写一个这样的框架的思考。
通过自学及参考谭勇德(Tom)老师的《Spring 5核心原理》这本书,决定记录我手写Spring的过程,记录此系列博客 。
愿每个想探究Spring原理的人,学习道路一帆风顺
Spring最初的时候,其功能远远不如现在强大,甚至我在看Spring最初版本的时候有种这就是所谓的Spring?的疑问,但随后便恍然大悟,我是站立在历史的下游,用后人的眼光去看前人的作品,当然有种站在制高点俯视的感觉,当我一步一步深入学习Spring的设计思想设计理念以及实现方式的时候,无不为前人那惊天地泣鬼神的思想所震撼。
话不多说进入主题:
正常的创建一个web项目就好
1 准备阶段——自定义配置
1.1 配置application.properties
为了解析方便,我们用application.properties来作为配置文件,内容很简单,如下:
scanPackage=com.gupaoedu.demo
1.2 配置web.xml文件
大家都知道,所有依赖于Web容器的项目都是从读取web.xml文件开始的。
<?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/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">
<display-name>XiaoZhao Web Application</display-name>
<servlet>
<servlet-name>zhao mvc</servlet-name>
<servlet-class>com.xiaoZhao666.mvcframework.v1.servlet.DispatchServlet</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>zhao mvc</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
其中DispatchServlet
是模拟Spring实现的核心功能类
1.3 自定义注解
做就做全套,我们连注解也给他模拟了,在自己包下创建annotation包,下面用的注解都是咱们自己创建的
1.3.1 @Service
@Target({
ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Service {
String value() default "";
}
1.3.2 @Autowired
@Target({
ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
String value() default "";
}
1.3.3 @Controller
@Target({
ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Controller {
String value() default "";
}
1.3.4 @RequestMapping
@Target({
ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestMapping {
String value() default "";
}
1.3.5 @RequestParam
@Target({
ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestParam {
String value() default "";
}
1.4 配置注解
配置业务实现类,此时文件结构如下
接口:
public interface IDemoService {
String get(String name);
}
实现类:
/**
* 核心业务逻辑
*/
@Service
public class DemoService implements IDemoService{
@Override
public String get(String name) {
return "My name is " + name + ",from service.";
}
}
配置请求入口DemoAction:
@Controller
@RequestMapping("/demo")
public class DemoAction {
@Autowired
private IDemoService demoService;
@RequestMapping("/query")
public void query(HttpServletRequest req, HttpServletResponse resp,
@RequestParam("name") String name){
String result = demoService.get(name);
try {
resp.getWriter().write(result);
} catch (IOException e) {
e.printStackTrace();
}
}
@RequestMapping("/add")
public void add(HttpServletRequest req, HttpServletResponse resp,
@RequestParam("a") Integer a, @RequestParam("b") Integer b){
try {
resp.getWriter().write(a + "+" + b + "=" + (a + b));
} catch (IOException e) {
e.printStackTrace();
}
}
@RequestMapping("/remove")
public String remove(@RequestParam("id") Integer id){
return "" + id;
}
}
至此,我们的所有配置就算完成了。
2 容器初始化
2.1 实现Spring 1.0版本
1.0版本只是有了一些简单的逻辑,对于以前写Servlet的老同学来说,看着会无比亲切,这一块没啥好说的,Spring的底层就是Servlet嘛。
核心逻辑都在init
方法里了,让我们迅速过度到下一阶段2.0版本
public class DispatcherServlet extends HttpServlet {
private Map<String,Object> mapping = new HashMap<String, Object>();
@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) {
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.mapping.containsKey(url)){
resp.getWriter().write("404 Not Found!!");return;}
Method method = (Method) this.mapping.get(url);
Map<String,String[]> params = req.getParameterMap();
method.invoke(this.mapping.get(method.getDeclaringClass().getName()),new Object[]{
req,resp,params.get("name")[0]});
}
//当我晕车的时候,我就不去看源码了
//init方法肯定干得的初始化的工作
//inti首先我得初始化所有的相关的类,IOC容器、servletBean
@Override
public void init(ServletConfig config) throws ServletException {
InputStream is = null;
try{
Properties configContext = new Properties();
is = this.getClass().getClassLoader().getResourceAsStream(config.getInitParameter("contextConfigLocation"));
configContext.load(is);
String scanPackage = configContext.getProperty("scanPackage");
doScanner(scanPackage);
for (String className : mapping.keySet()) {
if(!className.contains(".")){
continue;}
Class<?> clazz = Class.forName(className);
if(clazz.isAnnotationPresent(Controller.class)){
mapping.put(className,clazz.newInstance());
String baseUrl = "";
if (clazz.isAnnotationPresent(RequestMapping.class)) {
RequestMapping requestMapping = clazz.getAnnotation(RequestMapping.class);
baseUrl = requestMapping.value();
}
Method[] methods = clazz.getMethods();
for (Method method : methods) {
if (!method.isAnnotationPresent(RequestMapping.class)) {
continue; }
RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
String url = (baseUrl + "/" + requestMapping.value()).replaceAll("/+", "/");
mapping.put(url, method);
System.out.println("Mapped " + url + "," + method);
}
}else if(clazz.isAnnotationPresent(Service.class)){
Service service = clazz.getAnnotation(Service.class);
String beanName = service.value();
if("".equals(beanName)){
beanName = clazz.getName();}
Object instance = clazz.newInstance();
mapping.put(beanName,instance);
for (Class<?> i : clazz.getInterfaces()) {
mapping.put(i.getName(),instance);
}
}else {
continue;}
}
for (Object object : mapping.values()) {
if(object == null){
continue;}
Class clazz = object.getClass();
if(clazz.isAnnotationPresent(Controller.class)){
Field [] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if(!field.isAnnotationPresent(Autowired.class)){
continue; }
Autowired autowired = field.getAnnotation(Autowired.class);
String beanName = autowired.value();
if("".equals(beanName)){
beanName = field.getType().getName();}
field.setAccessible(true);
try {
field.set(mapping.get(clazz.getName()),mapping.get(beanName));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
} catch (Exception e) {
}finally {
if(is != null){
try {
is.close();} catch (IOException e) {
e.printStackTrace();
}
}
}
System.out.print("MVC Framework is init");
}
private void doScanner(String scanPackage) {
URL url = this.getClass().getClassLoader().getResource("/" + scanPackage.replaceAll("\\.","/"));
File classDir = new File(url.getFile());
for (File file : classDir.listFiles()) {
if(file.isDirectory()){
doScanner(scanPackage + "." + file.getName());}else {
if(!file.getName().endsWith(".class")){
continue;}
String clazzName = (scanPackage + "." + file.getName().replace(".class",""));
mapping.put(clazzName,null);
}
}
}
}
2.2 实现Spring 2.0版本
让我们迅速过度到2.0版本,改造1.0版本的DispatchServlet。
我们在1.0的版本上进行优化,加入Spring中使用的设计模式(工厂模式,单例模式,委派模式,策略模式),将init()方法中的代码进行封装。按照Spring框架的实现思路,先搭基础框架,再“填肉注血”,具体代码如下:
2.2.1 将init()方法中的代码进行改造
@Override
public void init(ServletConfig config) throws ServletException {
//1、加载配置文件
doLoadConfig(config.getInitParameter("contextConfigLocation"));
//2、扫描相关的类
doScanner(contextConfig.getProperty("scanPackage"));
//==============IoC部分==============
//3、初始化IoC容器,将扫描到的相关的类实例化,保存到IcC容器中
doInstance();
//AOP,新生成的代理对象
//==============DI部分==============
//4、完成依赖注入
doAutowired();
//==============MVC部分==============
//5、初始化HandlerMapping
doInitHandlerMapping();
System.out.println("GP Spring framework is init.");
}
然后声明全局成员变量,其中IOC容器就是注册时单例的具体案例
2.2.2 在类的开头声明变量
//保存application.properties配置文件中的内容
private Properties contextConfig = new Properties();
//享元模式,缓存
//保存扫描的所有的类名
private List<String> classNames = new ArrayList<String>();
//这就是传说中的IOC容器
//为了简化程序,先不考虑ConcurrentHashMap,主要还是关注设计思想和原理
//key默认是类名首字母小写,value就是对应的实例对象
private Map<String,Object> ioc = new HashMap<String,Object>();
//保存url和Method的对应关系
private Map<String,Method> handlerMapping = new HashMap<String, Method>();
按照init方法的步骤,实现doLoadConfig()方法:
2.2.3 实现doLoadConfig()方法
//加载配置文件
private void doLoadConfig(String contextConfigLocation) {
//直接通过类路径找到Spring主配置文件所在的路径,并且将其读取出来放在Properties对象中
InputStream is = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation);
try {
contextConfig.load(is);
} catch (IOException e) {
e.printStackTrace();
}finally {
if(null != is){
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
2.2.4 实现doScanner()方法
//扫描相关的类
private void doScanner(String scanPackage) {
//jar 、 war 、zip 、rar
//转换为文件路径,实际上就是把 . 替换为 /
URL url = this.getClass().getClassLoader().getResource("/" + scanPackage.replaceAll("\\.","/"));
File classPath = new File(url.getFile());
//当成是一个ClassPath文件夹
for (File file : classPath.listFiles()) {
if(file.isDirectory()){
doScanner(scanPackage + "." + file.getName());
}else {
if(!file.getName().endsWith(".class")){
continue;}
//全类名 = 包名.类名
String className = (scanPackage + "." + file.getName().replace(".class", ""));
//Class.forName(className);
classNames.add(className);
}
}
}
2.2.5 实现doInstance()方法
doInstance()方法就是工厂模式的具体实现:
private void doInstance() {
//初始化,为DI做准备
if(classNames.isEmpty()){
return;}
try {
for (String className : classNames) {
Class<?> clazz = Class.forName(className);
//什么样的类才需要初始化呢?———
// 加了注解的类才初始化>>>模拟Spring框架中的注解开发——
// 只用@Controller和@Service举例
if(clazz.isAnnotationPresent(Controller.class)) {
//key提取出来了,把value也搞出来
//Spring默认类名首字母小写
String beanName = toLowerFirstCase(clazz.getSimpleName());
Object instance = clazz.newInstance();
ioc.put(beanName, instance);
}else if(clazz.isAnnotationPresent(Service.class)){
//1、在多个包下出现相同的类名,只能寄几(自己)起一个全局唯一的名字
//自定义命名
String beanName = clazz.getAnnotation(Service.class).value();
if("".equals(beanName.trim())){
beanName = toLowerFirstCase(clazz.getSimpleName());
}
//2、默认的类名首字母小写
Object instance = clazz.newInstance();
ioc.put(beanName, instance);
//3、如果是接口————投机取巧一下,嘿嘿
//判断有多少个实现类,如果只有一个,默认就选择这个实现类
//如果有多个,只能抛异常
for (Class<?> i : clazz.getInterfaces()) {
if(ioc.containsKey(i.getName())){
throw new Exception("The " + i.getName() + " is exists!!");
}
//直接把接口的类型当成key
ioc.put(i.getName(),instance);
}
}else{
continue;
}
}
}catch (Exception e){
e.printStackTrace();
}
}
这里为了处理方便,自己实现了toLowerFirstCase()方法,来实现类名首字母小写:
//自己写,自己用
private String toLowerFirstCase(String simpleName) {
char [] chars = simpleName.toCharArray();
//之所以要加法,是因为大小写字母的ASCII码相差32
chars[0] += 32;
return String.valueOf(chars);
}
2.2.6 实现doAutowired()方法
.实现依赖注入:
private void doAutowired() {
if(ioc.isEmpty()){
return;}
for (Map.Entry<String,Object> entry : ioc.entrySet()) {
//把所有的包括private/protected/default/public 修饰字段都取出来
for (Field field : entry.getValue().getClass().getDeclaredFields()) {
if(!field.isAnnotationPresent(Autowired.class)){
continue; }
Autowired autowired = field.getAnnotation(Autowired.class);
//如果用户没有自定义的beanName,就默认根据类型注入
String beanName = autowired.value().trim();
if("".equals(beanName)){
//field.getType().getName() 获取字段的类型
beanName = field.getType().getName();
}
//暴力访问
field.setAccessible(true);
try {
//用反射机制动态给字段赋值
field.set(entry.getValue(),ioc.get(beanName));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
2.2.7 实现doInitHandlerMapping()方法
这一步其实就到了部分MVC的部分了。另外HandlerMapping是策略模式的案例
//初始化url和Method的一对一关系
private void doInitHandlerMapping() {
if(ioc.isEmpty()){
return;}
for (Map.Entry<String,Object> entry : ioc.entrySet()) {
Class<?> clazz = entry.getValue().getClass();
if(!clazz.isAnnotationPresent(Controller.class)){
continue; }
//相当于提取 class上配置的url
//也就是@RequestMapping上的路径
String baseUrl = "";
if(clazz.isAnnotationPresent(RequestMapping.class)){
RequestMapping requestMapping = clazz.getAnnotation(RequestMapping.class);
baseUrl = requestMapping.value();
}
//只获取public的方法
for (Method method : clazz.getMethods()) {
if(!method.isAnnotationPresent(RequestMapping.class)){
continue;}
//提取每个方法上面配置的url
RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
// //demo//query
String url = ("/" + baseUrl + "/" + requestMapping.value()).replaceAll("/+","/");
handlerMapping.put(url,method);
System.out.println("Mapped : " + url + "," + method);
}
}
}
到这里容器初始化的部分就完成了,接下来只要完成运行时的处理逻辑就行了,一起来写doGet和doPost叭
2.2.8 doGet和doPost的doDispatch
@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 {
//记得刚才init方法的5步吗,这是第六步
//6、委派,根据URL去找到一个对应的Method并通过response返回
try {
doDispatch(req,resp);
} catch (Exception e) {
e.printStackTrace();
resp.getWriter().write("500 Exception,Detail : " + Arrays.toString(e.getStackTrace()));
}
}
这里doDispatch用到了委派模式,代码如下:
private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
String url = req.getRequestURI();
String contextPath = req.getContextPath();
url = url.replaceAll(contextPath,"").replaceAll("/+","/");
if(!this.handlerMapping.containsKey(url)){
resp.getWriter().write("404 Not Found!!!");
return;
}
Method method = this.handlerMapping.get(url);
//第一个参数:方法所在的实例
//第二个参数:调用时所需要的实参
//保存请求的url参数列表
Map<String,String[]> params = req.getParameterMap();
//获取形参列表
Class<?> [] parameterTypes = method.getParameterTypes();
//保存赋值参数的位置
Object [] paramValues = new Object[parameterTypes.length];
//根据参数位置动态赋值
for (int i = 0; i < parameterTypes.length; i++) {
Class paramterType = parameterTypes[i];
if(paramterType == HttpServletRequest.class){
paramValues[i] = req;
}else if(paramterType == HttpServletResponse.class){
paramValues[i] = resp;
}else if(paramterType == String.class){
//通过运行时的状态去拿到你
Annotation[] [] pa = method.getParameterAnnotations();
for (int j = 0; j < pa.length ; j ++) {
for(Annotation a : pa[i]){
if(a instanceof RequestParam){
String paramName = ((RequestParam) a).value();
if(!"".equals(paramName.trim())){
String value = Arrays.toString(params.get(paramName))
.replaceAll("\\[|\\]","")
.replaceAll("\\s+",",");
paramValues[i] = value;
}
}
}
}
}
}
//暂时硬编码
String beanName = toLowerFirstCase(method.getDeclaringClass().getSimpleName());
//赋值实参列表
method.invoke(ioc.get(beanName),paramValues);
}
2.3 实现Spring 3.0版本
在2.0版本中,基本功能已经实现,但是代码还不够优雅,比如HandlerMapping还不能像真正的Spring一样支持正则,url参数还不能支持强制类型转换,在反射调用前还需要重新获取beanName,我们来继续优化
首先改造HandlerMapping,在真实的Spring源码中,HandlerMapping其实是一个List而不是一个Map。List中的元素是自定义类型的。现在我们来仿写这一段,定义一个内部类Handler:
2.3.1 HandlerMapping内部类Handler
//保存一个url和一个Method的关系
public class Handler {
//必须把url放到HandlerMapping才好理解吧
private Pattern pattern; //正则
private Method method;//保存映射的方法
private Object controller;//保存方法对应的实例
private Class<?> [] paramTypes;
//形参列表
//参数的名字作为key,参数的顺序,位置作为值
private Map<String,Integer> paramIndexMapping;
public Pattern getPattern() {
return pattern;
}
public Method getMethod() {
return method;
}
public Object getController() {
return controller;
}
public Class<?>[] getParamTypes() {
return paramTypes;
}
public Handler(Pattern pattern, Object controller, Method method) {
this.pattern = pattern;
this.method = method;
this.controller = controller;
paramTypes = method.getParameterTypes();
paramIndexMapping = new HashMap<String, Integer>();
putParamIndexMapping(method);
}
private void putParamIndexMapping(Method method){
//提取方法中加了注解的参数
//把方法上的注解拿到,得到的是一个二维数组
//因为一个参数可以有多个注解,而一个方法又有多个参数
Annotation [] [] pa = method.getParameterAnnotations();
for (int i = 0; i < pa.length ; i ++) {
for(Annotation a : pa[i]){
if(a instanceof RequestParam){
String paramName = ((RequestParam) a).value();
if(!"".equals(paramName.trim())){
paramIndexMapping.put(paramName, i);
}
}
}
}
//提取方法中的request和response参数
Class<?> [] paramsTypes = method.getParameterTypes();
for (int i = 0; i < paramsTypes.length ; i ++) {
Class<?> type = paramsTypes[i];
if(type == HttpServletRequest.class ||
type == HttpServletResponse.class){
paramIndexMapping.put(type.getName(),i);
}
}
}
}
然后优化HandlerMapping的结构:
//思考:为什么不用Map
//你用Map的话,key,只能是url
//Handler 本身的功能就是把url和method对应关系,已经具备了Map的功能
//根据设计原则:冗余的感觉了,单一职责,最少知道原则,帮助我们更好的理解
private List<Handler> handlerMapping = new ArrayList<Handler>();
2.3.2修改doDispatch()方法
private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
Handler handler = getHandler(req);
if(handler == null){
// if(!this.handlerMapping.containsKey(url)){
resp.getWriter().write("404 Not Found!!!");
return;
}
//获得方法的形参列表
Class<?> [] paramTypes = handler.getParamTypes();
Object [] paramValues = new Object[paramTypes.length];
Map<String,String[]> params = req.getParameterMap();
for (Map.Entry<String, String[]> parm : params.entrySet()) {
String value = Arrays.toString(parm.getValue()).replaceAll("\\[|\\]","")
.replaceAll("\\s",",");
if(!handler.paramIndexMapping.containsKey(parm.getKey())){
continue;}
int index = handler.paramIndexMapping.get(parm.getKey());
paramValues[index] = convert(paramTypes[index],value);
}
if(handler.paramIndexMapping.containsKey(HttpServletRequest.class.getName())) {
int reqIndex = handler.paramIndexMapping.get(HttpServletRequest.class.getName());
paramValues[reqIndex] = req;
}
if(handler.paramIndexMapping.containsKey(HttpServletResponse.class.getName())) {
int respIndex = handler.paramIndexMapping.get(HttpServletResponse.class.getName());
paramValues[respIndex] = resp;
}
Object returnValue = handler.method.invoke(handler.controller,paramValues);
if(returnValue == null || returnValue instanceof Void){
return; }
resp.getWriter().write(returnValue.toString());
}
private Handler getHandler(HttpServletRequest req) {
if(handlerMapping.isEmpty()){
return null;}
//绝对路径
String url = req.getRequestURI();
//处理成相对路径
String contextPath = req.getContextPath();
url = url.replaceAll(contextPath,"").replaceAll("/+","/");
for (Handler handler : this.handlerMapping) {
Matcher matcher = handler.getPattern().matcher(url);
if(!matcher.matches()){
continue;}
return handler;
}
return null;
}
//url传过来的参数都是String类型的,HTTP是基于字符串协议
//只需要把String转换为任意类型就好
private Object convert(Class<?> type,String value){
//如果是int
if(Integer.class == type){
return Integer.valueOf(value);
}
else if(Double.class == type){
return Double.valueOf(value);
}
//如果还有double或者其他类型,继续加if
//这时候,我们应该想到策略模式了
//在这里暂时不实现,希望小伙伴自己来实现
return value;
}
3 至此,运行项目
在浏览器中输入:
localhost:8080/demo/query.json?name=XiaoZhao666
就会得到:
当然真正的Spring要复杂很多,这里只是手写了解一下Spring的基本设计思路和设计模式的应用