讲道理,感受本身有点菜。Spring 源码看不懂,不想强行解释,等多积累些项目经验以后再看吧,可是 Spring 中的控制反转(IOC)和面向切面编程(AOP)思想很重要,为了更好的使用 Spring 框架,有必要理解这两个点,为此,我使用 JDK API 实现了一个玩具级的简陋 IOC/AOP 框架 mini-spring,话很少说,直接开干。html
所有代码已上传 GitHub:https://github.com/czwbig/mini-springjava
gradle build
命令;mini-spring\framework_use_test\build\libs\framework_use_test-1.0-SNAPSHOT.jar
,点击 Run,固然也能够直接使用 java -jar jarPath.jar
命令来运行此 jar 包;localhost:8080/rap
便可观察到显示 CXK 字母,同时 IDE 控制台会输出:first,singing <chicken is too beautiful>. and the chicken monster is dancing now. CXK rapping... oh! Don't forget my favorite basketball.
下面开始框架的讲解。git
本项目使用 Java API 以及内嵌 Tomcat 服务器写了一个玩具级 IOC/AOP web 框架。实现了 @Controller
、@AutoWired
、@Component
、@Pointcut
、@Aspect
、@Before
、@After
等 Spring 经常使用注解。可实现简单的访问 uri 映射,控制反转以及不侵入原代码的面向切面编程。github
讲解代码实现以前,假设读者已经掌握了基础的项目构建、反射、注解,以及 JDK 动态代理知识,项目精简,注释详细,而且总代码 + 注释不足 1000 行,适合用来学习。其中构建工具 Gradle 没用过也没关系,我也是第一次使用,当成没有 xml 的 Maven 来看就行,下面我会详细解读其构建配置文件。web
项目由两个模块组成,一个是框架自己的模块,实现了框架的 IOC/AOP 等功能,以下图:spring
类比较多,可是大部分都是代码不多的,特别是注解定义接口,不要怕。编程
aop
包中是 After
等注解的定义接口,以及动态代理辅助类;bean
包中是两个注解定义,以及 BeanFactory
这个 Bean 工厂,其中包含了类扫描和 Bean 的初始化的代码;core
包是一个 ClassScanner
类扫描工具类;starter
包是一个框架的启动与初始化类;web/handler
包中是 uri 请求的处理器的收集与管理,如查找 @Controller
注解修饰的类中的 @RequestMapping
注解修饰的方法,用来响应对应 uri 请求。web/mvc
包定义了与 webMVC 有关的三个注解;web/server
包中是一个嵌入式 Tomcat 服务器的初始化类;web/servlet
包中是一个请求分发器,重写的 service()
方法定义使用哪一个请求处理器来响应浏览器请求;另外一个模块是用来测试(使用)框架的模块,以下图:数组
就像咱们使用 Spring 框架同样,定义 Controller 等来响应请求,代码很简单,就不解释了。浏览器
根目录下有 setting.gradle
、build.gradle
项目构建文件,其中 setting.gradle
指定了项目名以及模块名。tomcat
rootProject.name = 'mini-spring' include 'framework' include 'framework_use_test'
build.gradle
是项目构建设置,主要代码以下:
plugins { id 'java' } group 'com.caozhihu.spring' version '1.0-SNAPSHOT' sourceCompatibility = 1.8 repositories { repositories { maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' } } // mavenCentral() } dependencies { testCompile group: 'junit', name: 'junit', version: '4.12' }
引入了 gradle 的 java 插件,由于 gradle 不只仅能够用于 java 项目,也能够用于其余项目,引入了 java 插件定义了项目的文件目录结构等。
而后就是项目的版本以及 java 源代码适配级别,这里是 JDK 1.8,在后面是指定了依赖仓库,gradle 能够直接使用 maven 仓库。
最后就是引入项目具体依赖,这里和 maven 同样。
每一个模块也有单独的 build.gradle
文件来指定模块的构建设置,这里以 framework_use_test
模块的 build.gradle
文件来讲明:
dependencies { // 只在单元测试时候引入此依赖 testCompile group: 'junit', name: 'junit', version: '4.12' // 项目依赖 compile(project(':framework')) } jar { manifest { attributes "Main-Class": "com.caozhihu.spring.Application" } // 固定打包句式 from { configurations.runtime.asFileTree.files.collect { zipTree(it) } } }
除去和项目根目录下构建文件相同部分,其余的构建代码如上,这里的 dependencies 除了添加 Junit 单元测试依赖以外,还指定了 framework
模块。
下面指定了 jar 包的打包设置,首先使用 manifest 设置主类,不然生成的 jar 包找不到主类清单,会没法运行。还使用了 from 语句来设置打包范围,这是固定句式,用来收集全部的 java 类文件。
以下图:
public void startServer() throws LifecycleException { tomcat = new Tomcat(); tomcat.setPort(8080); tomcat.start(); // new 一个标准的 context 容器并设置访问路径; // 同时为 context 设置生命周期监听器。 Context context = new StandardContext(); context.setPath(""); context.addLifecycleListener(new Tomcat.FixContextListener()); // 新建一个 DispatcherServlet 对象,这个是咱们本身写的 Servlet 接口的实现类, // 而后使用 `Tomcat.addServlet()` 方法为 context 设置指定名字的 Servlet 对象, // 并设置为支持异步。 DispatcherServlet servlet = new DispatcherServlet(); Tomcat.addServlet(context, "dispatcherServlet", servlet) .setAsyncSupported(true); // Tomcat 全部的线程都是守护线程, // 若是某一时刻全部的线程都是守护线程,那 JVM 会退出, // 所以,须要为 tomcat 新建一个非守护线程来保持存活, // 避免服务到这就 shutdown 了 context.addServletMappingDecoded("/", "dispatcherServlet"); tomcat.getHost().addChild(context); Thread tomcatAwaitThread = new Thread("tomcat_await_thread") { @Override public void run() { TomcatServer.this.tomcat.getServer().await(); } }; tomcatAwaitThread.setDaemon(false); tomcatAwaitThread.start(); }
这里看代码注释,结合下面这张 tomcat 架构图就能够理解了。
图片来自 http://click.aliyun.com/m/1000014411/
若是暂时不理解也不要紧,不影响框架学习,我只是为了玩一玩内嵌 tomcat,彻底能够本身实现一个乞丐版的网络服务器的。
这里使用的是咱们自定义的 Servlet 子类 DispatcherServlet 对象,该类重写了 service()
方法,代码以下:
@Override public void service(ServletRequest req, ServletResponse res) throws IOException { for (MappingHandler mappingHandler : HandlerManager.mappingHandlerList) { // 从全部的 MappingHandler 中逐一尝试处理请求, // 若是某个 handler 能够处理(返回true),则返回便可 try { if (mappingHandler.handle(req, res)) { return; } } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } res.getWriter().println("failed!"); }
HandlerManager 和 MappingHandler 处理器后面会讲,这里先不展开。至此,tomcat 服务器启动完成;
扫描类是经过这句代码完成的:
// 扫描类 List<Class<?>> classList = ClassScanner.scannerCLasses(cls.getPackage().getName());
ClassScanner.scannerCLasses
方法实现以下:
public static List<Class<?>> scannerCLasses(String packageName) throws IOException, ClassNotFoundException { List<Class<?>> classList = new ArrayList<>(); String path = packageName.replace(".", "/"); // 线程上下文类加载器默认是应用类加载器,即 ClassLoader.getSystemClassLoader(); ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); // 使用类加载器对象的 getResources(ResourceName) 方法获取资源集 // Enumeration 是古老的迭代器版本,可当成 Iterator 使用 Enumeration<URL> resources = classLoader.getResources(path); while (resources.hasMoreElements()) { URL url = resources.nextElement(); // 获取协议类型,判断是否为 jar 包 if (url.getProtocol().contains("jar")) { // 将打开的 url 返回的 URLConnection 转换成其子类 JarURLConnection 包链接 JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection(); String jarFilePath = jarURLConnection.getJarFile().getName(); // getClassesFromJar 工具类获取指定 Jar 包中指定资源名的类; classList.addAll(getClassesFromJar(jarFilePath, path)); } else { // 简单起见,咱们暂时仅实现扫描 jar 包中的类 // todo } } return classList; } private static List<Class<?>> getClassesFromJar(String jarFilePath, String path) throws IOException, ClassNotFoundException { // 为减小篇幅,这里完整代码就不放出来了 }
注释很详细,就很少废话了。
这部分是最重要的,IOC 和 AOP 都在这里实现。
代码请到在 BeanFactory
类中查看,GitHub 在线查看 BeanFactory
注释已经写的很是详细。这里简单说下处理逻辑。
首先经过遍历上一步类扫描得到类的 Class 对象集合,将被 @Aspect
注解的类保存起来,而后初始化其余被 @Component
和 @Controller
注解的类,并处理类中被 @AutoWired
注解的属性,将目标引用对象注入(设置属性的值)到类中,而后将初始化好的对象保存到 Bean 工厂。到这里,控制反转就实现好了。
接下来是处理被 @Aspect
注解的类,并解析他们中被 @Pointcut
、@Before
和 @After
注解的方法,使用 JDK 动态代理生成代理对象,并更新 Bean 工厂。
注意,在处理被 @Aspect
注解的类以前,Bean 工厂中的对象依赖已经设置过了就旧的 Bean,更新了 Bean 工厂中的对象后,须要通知依赖了被更新对象的对象从新初始化。
例如对象 A 依赖对象 B,即 A 的类中有一句
@AutoWired B b;
同时,一个切面类中的切点 @Pointcut
的值指向了 B 类对象,而后他像 Bean 工厂更新了 B 对象,但这时 A 中引用的 B 对象,仍是以前的旧 B 对象。
这里个人解决方式是,将带有 @AutoWired
属性的类保存起来,处理好 AOP 关系以后,再次初始化这些类,这样他们就能从 Bean 工厂得到新的已经被代理过的对象了。
至于如何使用 JDK 动态代理处理 AOP 关系的,请参考 GitHub ProxyDyna 类
中代码,总的来讲是,定义一个 ProxyDyna
类实现 InvocationHandler
接口,而后实现 invoke()
方法便可,在 invoke()
方法中处理代理加强逻辑。
而后获取对象的时候,使用 Proxy.newProxyInstance()
方法而不是直接 new,以下:
Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
HandlerManager 类中调用 parseHandlerFromController()
方法来遍历处理全部的已扫描到的类,来初始化 MappingHandler 对象,方法代码以下:
private static void parseHandlerFromController(Class<?> aClass) { Method[] methods = aClass.getDeclaredMethods(); // 只处理包含了 @RequestMapping 注解的方法 for (Method method : methods) { if (method.isAnnotationPresent(RequestMapping.class)) { // 获取赋值 @RequestMapping 注解的值,也就是客户端请求的路径,注意,不包括协议名和主机名 String uri = method.getDeclaredAnnotation(RequestMapping.class).value(); List<String> params = new ArrayList<>(); for (Parameter parameter : method.getParameters()) { if (parameter.isAnnotationPresent(RequestParam.class)) { params.add(parameter.getAnnotation(RequestParam.class).value()); } } // List.toArray() 方法传入与 List.size() 刚好同样大的数组,能够提升效率 String[] paramsStr = params.toArray(new String[params.size()]); MappingHandler mappingHandler = new MappingHandler(uri, aClass, method, paramsStr); HandlerManager.mappingHandlerList.add(mappingHandler); } } }
MappingHandler 对象表示如何处理一次请求,包括请求 uri,应该调用的类,应该调用的方法以及方法参数。
如此,在 MappingHandler 的 handle()
方法中处理请求,直接从 Bean 工厂获取指定类对象,从 response 对象中获取请求参数值,使用反射调用对应方法,并接收方法返回值输出给浏览器便可。
再回顾咱们启动 tomcat 服务器时指定运行的 servlet:
@Override public void service(ServletRequest req, ServletResponse res) throws IOException { for (MappingHandler mappingHandler : HandlerManager.mappingHandlerList) { // 从全部的 MappingHandler 中逐一尝试处理请求, // 若是某个 handler 能够处理(返回true),则返回便可 try { if (mappingHandler.handle(req, res)) { return; } } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } res.getWriter().println("failed!"); }
一目了然,其 service()
方法只是遍历全部的 MappingHandler 对象来处理请求而已。
测试使用 IOC 和 AOP 功能。这里以定义一个 /rap 路径举例,
1. 定义Controller
@Controller public class RapController { @AutoWired private Rap rapper; @RequestMapping("/rap") public String rap() { rapper.rap(); return "CXK"; } }
RapController 从 Bean 工厂获取一个 Rap 对象,访问 /rap 路径是,会先执行该对象的 rap()
方法,而后返回 "CXK" 给浏览器。
2. 定义 Rap 接口及其实现类
public interface Rap { void rap(); } // ----another file---- @Component public class Rapper implements Rap { public void rap() { System.out.println("CXK rapping..."); } }
接口必定要定义,不然没法使用 AOP,由于咱们使用的是 JDK 动态代理,只能代理实现了接口的类(原理是生成一个该接口的加强带向)。Spring 使用的是 JDK 动态代理和 CGLIB 两种方式,CGLIB 能够直接使用 ASM 等字节码生成框架,来生成一个被代理对象的加强子类。
使用浏览器访问 http://localhost:8080/rap
,便可看到 IDE 控制台输出 CXK rapping...
,能够看到,@AutoWired
注解成功注入了对象。
但若是咱们想在 rap 前面先 唱、跳,而且在 rap 后面打篮球,那么就须要定义织面类来面向切面编程。
定义一个 RapAspect
类以下:
@Aspect @Component public class RapAspect { // 定义切点,spring的实现中, // 此注解可使用表达式 execution() 通配符匹配切点, // 简单起见,咱们先实现明确到方法的切点 @Pointcut("com.caozhihu.spring.service.serviceImpl.Rapper.rap()") public void rapPoint() { } @Before("rapPoint()") public void singAndDance() { // 在 rap 以前要先唱、跳 System.out.println("first,singing <chicken is too beautiful>."); System.out.println("and the chicken monster is dancing now."); } @After("rapPoint()") public void basketball() { // 在 rap 以后别忘记了篮球 System.out.println("oh! Don't forget my favorite basketball."); } }
织面类 RapAspect 定义了切入点以及前置后置通知等,这样 RapController 中使用 @AutoWired
注解引入的 Rap 对象,会被替换为加强的 Rap 代理对象,如此,咱们无需改动 RapController 中任何一处代码,就实现了在 rap()
方法先后执行额外的代码(通知)。
增长 RapAspect 后,再次访问会在 IDE 控制台输出:
first,singing <chicken is too beautiful>. and the chicken monster is dancing now. CXK rapping... oh! Don't forget my favorite basketball.
没啥好说的,该说的,都说了,你懂得,就够了,怎么有某一种悲哀.... 哈哈哈哈
tomcat 使用与框架图:手写一个简化版Tomcat
gradle 配置与 DI 部分实现:慕课网
Spring 经常使用注解 how2j SPRING系列教材