# MySimpleSpring
**Repository Path**: mlming/my-simple-spring
## Basic Information
- **Project Name**: MySimpleSpring
- **Description**: 从0开始搭建一个具有IOC、AOP、Web等核心功能的简易版Spring自研框架
- **Primary Language**: Java
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2022-05-26
- **Last Updated**: 2022-06-08
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# DefaultAspectAspectListExecutorMySimpleSpring
## 介绍
从0开始搭建一个具有IOC、AOP、Web等核心功能的简易版Spring自研框架
## Spring核心模块整体感知:






## 自研框架的整体架构:

所以本自研框架是一个基于Servlet实现的**Web项目**
各组件作用如下:
IOC: 实现简易版的IOC容器
AOP: 实现简易版的AOP功能
Parser: 用于解析各个配置文件从而配置Bean
MVC: 基于Servlet实现的简易版的SpringMVC
## 自研框架雏形:
* 概述:
因为上面说了, 本项目是一个基于Servlet实现的**Web项目**
所以我们可以把项目构建为一个Web工程, 并引入Servlet依赖(同时为了更直观测试功能,我们引入JSP来展示成果) 来进行初始化
* 实现:
* 构建一个Maven的Web工程


* pom文件中引入Servlet与JSP的依赖:
```xml
javax.servlet
javax.servlet-api
3.1.0
javax.servlet.jsp
jsp-api
2.2.1-b03
dependency>
```
* 创建一个Servlet类以及一个jsp:
项目目录:

HelloServlet.java:
```java
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String name = "Hello SimpleSpring!";
// 设置属性
req.setAttribute("name",name);
// 转发到jsp页面
req.getRequestDispatcher("/WEB-INF/jsp/hello.jsp").forward(req,resp);
}
}
```
hello.jsp:
```java
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
Title
你好!
${name}
```
* IDEA配置运行环境:


* 启动项目, 浏览器访问localhost:8080/hello -> HelloServlet -> hello.jsp

* 至此,项目初始化成功!
## 业务系统架子的构建【自研框架的起源】
* 概述:
为了进一步明确我们自研框架的作用, 那我们就得用原生的脱离了Spring框架的JavaWeb方法去实现一个业务系统。
这样依赖,之后我们的自研框架就有了更多的出发点和落足点。
* 业务系统的需求分析与基本设计:
* 考虑到这只是一个启发式的小系统,所以我们无需太完善,比如链接数据库等操作可以直接省略,
我们只需要关心业务相关的**模块之间的交互 以及 类的管理**
* 需求:

* 由此需求,可以分析出只需要两个表:一个是头条表,一个是商铺表,同时也对应两个实体类。
由于我们此处不会去关心数据库层面的,所以只需要设计两个实体类即可:

* 同时,因为是业务系统,所以我们尽量采取MVC架构来进行开发(其实更多指的是三层架构):
* Controller: 接收请求,并调用service层进行业务处理,并响应给前端
* Service:业务处理
* Dao:持久层的相关操作
* Entity:实体类,一般对应数据库表
* 代码实现:
* pom.xml文件中引入 slf4j 和 lombok依赖:
```xml
org.slf4j
slf4j-log4j12
1.7.28
org.projectlombok
lombok
1.18.10
```
* entity包下创建两个实体类:
店铺实体类:
```java
/**
* 店铺实体类
*/
@Data
public class ShopCategory {
private Long shopCategoryId;
private String shopCategoryName;
private String shopCategoryDesc;
private String shopCategoryImg;
private Integer priority;
private Date createTime;
private Date lastEditTime;
private ShopCategory parent;
}
```
头条实体类:
```java
/**
* 头条实体类
*/
@Data
public class HeadLine {
private Long lineId;
private String lineName;
private String lineLink;
private String lineImg;
private Integer priority;
private Integer enableStatus;
private Date createTime;
private Date lastEditTime;
}
```
* dto包下,创建用于响应给前端的响应信息的通用类:
```java
@Data
public class Result {
//本次请求结果的状态码,200表示成功
private int code;
//本次请求结果的详情
private String msg;
//本次请求返回的结果集
private T data;
}
```
* dao层: 由于本业务系统不考虑数据库,所以为了省事,就不考虑dao层了
* service包下,对应着业务需求, 创建业务处理对应的Service:
头条相关业务处理的Service:
```java
public interface HeadLineService {
Result addHeadLine(HeadLine headLine);
Result removeHeadLine(int headLineId);
Result modifyHeadLine(HeadLine headLine);
Result queryHeadLineById(int headLineId);
Result>queryHeadLine(HeadLine headLineCondition, int pageIndex, int pageSize);
}
```
店铺相关业务处理的Service:
```java
public interface ShopCategoryService {
Result addShopCategory(ShopCategory shopCategory);
Result removeShopCategory(int shopCategoryId);
Result modifyShopCategory(ShopCategory shopCategory);
Result queryShopCategoryById(int shopCategoryId);
Result> queryShopCategory(ShopCategory shopCategoryCondition, int pageIndex, int pageSize);
}
```
合并两个Service,用于实现返回首页相关信息的Service:
```java
public interface HeadLineShopCategoryCombineService {
Result getMainPageInfo();
}
```
再次强调, 由于本项目只关心模块之间的联系以及类的处理, 所以并不会具体实现这些方法, 所以这里就不一一展示对应的impl实现类了,
这里只贴其中一个实现类:
HeadLineShopCategoryCombineService的实现类:
```java
public class HeadLineShopCategoryCombineServiceImpl implements HeadLineShopCategoryCombineService {
private HeadLineService headLineService; // 头条Service
private ShopCategoryService shopCategoryService;// 店铺Service
@Override
public Result getMainPageInfo() {
// 1.获取头条列表
HeadLine headLineCondition = new HeadLine();
headLineCondition.setEnableStatus(1);
Result> HeadLineResult = headLineService.queryHeadLine(headLineCondition, 1, 4);
// 2.获取店铺类别列表
ShopCategory shopCategoryCondition = new ShopCategory();
Result> shopCategoryResult =shopCategoryService.queryShopCategory(shopCategoryCondition, 1, 100);
// 3.合并两者并返回
Result result = mergeMainPageInfoResult(HeadLineResult, shopCategoryResult);
return result;
}
private Result mergeMainPageInfoResult(Result> headLineResult, Result> shopCategoryResult) {
return null;
}
}
```
**此处其实就能发现, 我们出现了Service之间的包含关系,即: 一个类有其他类作为其成员变量 **
**此时我们肯定要对他们进行初始化的, 否则就是null, 无法使用,这里由于这个系统的业务简单, 嵌套关系并不深, 只不过是new一下就能解决 **
**如果在稍微大型的项目里面,一个Service用到了多个Service, 一个Service下面可能还有多个dao成员, 那么此时手动去初始化就会变得非常非常复杂**
**此刻我们是否需要一个可以帮我们实现自动化注入(初始化), 来帮我们自动管理类与类之间关系 的工具呢? **
* controller层:
**此时有一个特别需要注意的点:**
**之前开发我们一直用Spring封装好的Controller类, 我们可以随意在Controller里面通过@RequestMapping给各个controller方法指定其对应的请求方法和请求路径, 这样就很容易实现 一个Controller类对应其模块的所有请求 --- 例如: StudentController可以在这个类里面一下子设置所有的请求操作: 请求学生列表,请求单个学生信息, 修改学生信息等等, 我们只要设置好@RequestMapping就可以随意在一个Controller类里面放入各种请求处理**
* **但是,此时我们是原生Servlet, 如果照葫芦画瓢, 我们直接用Servlet充当Controller类 来 处理对应的一个模块 行不行? **
**答案是不行! 因为一个Servlet只能对应一个路径, 一个路径下只有 doGet/doPost/doPut...的方法区别. 我们无法实现像Controller类那样的随便往里面放入请求处理, 例如: 学生模块需要两个Get方法: 一个是请求学生列表, 一个是请求单个学生信息, 这两个方法, 如果用一个StudentServlet来处理, 很明显是不能处理的, 因为StudentServlet只能有一个路径, 且只有一个doGet方法, 所以顶多只能实现其中的一个需求, 所以: 直接用Servlet来处理一个模块是行不通的**
* **但是Servlet有doGet,doPost,doPut等方法, 不是刚好对应了一个实体的增删改查操作吗? 那我们改一下思路: 一个Servlet对应处理一个实体类, 行不行? **
**答案也是不行的! 首先当一个项目大起来, 可能会有一堆表, 按照一个表对应一个实体的一般原则, 那么就会有一堆的实体类, 那么就会有一堆的Servlet, 这样肯定是不行的, 其次还是回到刚刚那个需求, 我们如果要请求学生列表怎么办呢? 这种思想也是很难实现的**
* **那Servlet对应一个页面的请求处理行不行? 也不行, 还是刚刚那个例子: 一个页面上可能会要求查询单个学生, 也会要求查询所有学生列表, 那么这种情况Servlet也是无法处理的**
* (写到这的时候, 我突然兴起, 去看了以前大二学JavaWeb时期的项目, 居然是: 一个Servlet对应一个请求然后对应一个Service!!??, 我差点蚌埠住了, 包括我刚刚想了这么多种方案选型, 都根本想不到这么离谱的做法哈哈哈, 只能说过了一年了, 我果然是在进步的哈哈哈 )
总结上述这几种Servlet无法应对的情况, 归根结底就两个原因:
* Servlet太多不好
* Servlet只能对应一个路径, 且只有一个doGet,一个doPost...无法实现Controller类的那种模块化统一请求处理
那么, 我们应该怎么样实现 **Controller类的那种模块化统一请求处理** 呢?
**既然都是Servlet的错, 那我们干脆不用Servlet不就行了吗? **
**羊毛出在羊身上, 我们参考一下SpringMVC的做法 -- 用一个Servlet类DispatcherServlet类, 然后用多个普通Java类作为Controller类, 由DispatcherServlet类接收所有的请求, 根据请求的路径以及方法, 对应着调用普通Controller类来进行处理。**
**这样一来,我们尽管往Controller类里面添加方法,因为他没有了Servlet的限制, 这样就能实现一个模块的请求全部放在一个类里面处理了, 同时因为Controller类只是一个普普通通的Java类, 那么就算添加再多Controller类, 也不会有Servlet太多的顾虑**
我们来定义几个Controller类:
首页模块对应的Controller类:
```java
public class MainPageController {
private HeadLineShopCategoryCombineService headLineShopCategoryCombineService;
public Result getMainPageInfo(HttpServletRequest req, HttpServletResponse resp){
return headLineShopCategoryCombineService.getMainPageInfo();
}
public void throwException(){
throw new RuntimeException("抛出异常测试");
}
}
```
头条操作模块对应的:
```java
public class HeadLineOperationController {
private HeadLineService headLineService;
public Result addHeadLine(HttpServletRequest req,HttpServletResponse resp) {
//TODO:参数校验以及请求参数转化
HeadLine headLine = new HeadLine();
Result result = headLineService.addHeadLine(headLine);
return result;
}
public void removeHeadLine(){
System.out.println("删除HeadLine");
}
public Result modifyHeadLine(HttpServletRequest req, HttpServletResponse resp){
//TODO:参数校验以及请求参数转化
return headLineService.modifyHeadLine(new HeadLine());
}
public Result queryHeadLineById(HttpServletRequest req, HttpServletResponse resp){
//TODO:参数校验以及请求参数转化
return headLineService.queryHeadLineById(1);
}
public Result>queryHeadLine(){
return headLineService.queryHeadLine(null, 1, 100);
}
}
```
店铺操作模块对应的:
```java
public class ShopCategoryOperationController {
private ShopCategoryService shopCategoryService;
public Result addShopCategory(HttpServletRequest req, HttpServletResponse resp){
//TODO:参数校验以及请求参数转化
return shopCategoryService.addShopCategory(new ShopCategory());
}
public Result removeShopCategory(HttpServletRequest req, HttpServletResponse resp){
//TODO:参数校验以及请求参数转化
return shopCategoryService.removeShopCategory(1);
}
public Result modifyShopCategory(HttpServletRequest req, HttpServletResponse resp){
//TODO:参数校验以及请求参数转化
return shopCategoryService.modifyShopCategory(new ShopCategory());
}
public Result queryShopCategoryById(HttpServletRequest req, HttpServletResponse resp){
//TODO:参数校验以及请求参数转化
return shopCategoryService.queryShopCategoryById(1);
}
public Result> queryShopCategory(HttpServletRequest req, HttpServletResponse resp){
//TODO:参数校验以及请求参数转化
return shopCategoryService.queryShopCategory(null, 1, 100);
}
}
```
**DispatcherServlet: 接收所有的请求, 然后根据请求路径以及请求方法, 调用对应的Controller类中的方法进行处理**
```java
/**
* 请求分发器
*/
@WebServlet("/") // 接收所有的请求
public class DispatcherServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 1) 获得请求路径以及请求方法
String requestPath = req.getServletPath();
String requestMethod = req.getMethod();
// 2) 根据请求路径以及请求方法, 调用对应的Controller类进行
if("frontend/getmainpageinfo".equals(requestPath) && "GET".equals(requestMethod)) {
new MainPageController().getMainPageInfo(req,resp);
} else if("superadmin/addheadline".equals(requestPath) && "POST".equals(requestMethod)) {
new HeadLineOperationController().addHeadLine(req,resp);
}
}
}
```
**此时我们发现, 上面的代码都是写死的! 当项目大起来, 会有以下缺点:**
* **每一次新增一个模块对应的Controller类时, 我们都必须得手动去DispatcherServlet的if-else链上面添加一个判断, 长期下去, 非常不利于维护与扩展**
* **Controller中也包含了Service类成员变量, 这个问题在service层时就探讨过了, 总之就是手动初始化(注入)会很麻烦**
* **Controller过于原生, 我们发现Controller里面传入的都是req,resp, 我们无论是对参数的获取还是响应参数, 都必须使用最原生的api手动去实现, 非常繁琐, 浪费精力**
**我们是否需要一个可以直接把新增的Controller自动注册到DispatcherServlet中的工具呢? 是否需要一个自动从req获取参数, 同时更方便我们响应结果的工具呢? **
* 从业务系统的开发中发现的问题总结:
* **我们是否需要一个可以帮我们实现自动化注入(初始化), 来帮我们自动管理类与类之间关系 的工具呢? **
* **我们是否需要一个可以直接把新增的Controller自动注册到DispatcherServlet中的工具呢?**
* **我们的Controller类是否需要一个自动从req获取参数, 同时更方便我们响应结果的工具呢? **
......
**我们的自研框架, 能不能实现上述以及更多的需求, 使得我们的项目开发更简单呢?**
## 自研框架IOC实现前奏[实现自研框架IOC核心功能的前置知识]
### 工厂设计模式的思想:
我们从上面通过原生JavaWeb实现业务系统的过程中,提出的需求是:**需要一个自动管理类与类之间关系、自动初始化(注入)的工具**
当提及这样的需求时, 我们很容易想到一个设计模式:**工厂模式**,
**因为工厂模式最大的特点就是:可以忽略对象创建的具体细节,由工厂负责创建并返回需要的对象**
但是普通的工厂模式也有好几个,所以,我们先来分析一下,有什么普通的工厂模式符合我们的需求的:
(下列代码均在demo.factory包下)
* 简单工厂模式:
* 概念:定义一个工厂类以及多个具有共同父类或接口的产品类,客户端**通过工厂类,传入不同的参数,并得到对应的产品类:**
* 大致代码思想如下:
工厂类:

测试:

* 缺点:
* 所有的产品类对象的创建均在一个工厂类中,如果各个对象的创建都比较复杂,该工厂类就会显得特别臃肿,违背了单一职责原则
* 我们会发现,该工厂类是通过类似于if-else等方式进行判断从而创建不同的产品类对象,
如果我们此时要多加一个产品类,那么就必须手动修改工厂类的代码才能实现新功能的添加,毫无疑问违背了开闭原则。
* 工厂方法模式:
* 概念:定义多个具有共同父类或接口的工厂类 以及 多个具有共同父类或接口的产品类,
**每一个工厂类对应一个产品类,客户端通过调用不同的工厂类获得不同的产品类对象**
* 大致代码思想如下:
工厂类:



测试:

* 优点:
* 把简单工厂中的单一工厂创建所有的产品类的职责下沉并分散给各个子类,一个工厂类只负责一个产品类的创建,符合单一职责
* 当新增一个产品类时,只需要新增对应的一个工厂类,然后客户端调用新工厂类即可,无需修改其他原有的代码,符合开闭原则
* 缺点:
* 一个产品类对应一个工厂类,产品过多时,类会太多
* 同一类工厂类,只能创建同一类的产品类,不能创建多种产品类
* 抽象工厂模式:
* 概念:上述的无论是简单工厂还是工厂方法模式,都是:一种工厂类对应一种产品类,一种工厂类无法创建多种产品类
**而抽象工厂模式,可以实现 一个工厂类创建一系列相关的产品族, 可以认为是可以生产多种相关的产品的工厂方法模式。**
* 大致代码思想如下:
工厂类:



测试:

* 优点:
* 在工程模式基础上,实现了一个工厂类可以创建一系列的产品类
至此,我们已经把普通的工厂模式相关的都说完了,那么哪个工厂模式可以实现我们的需求呢?
答案是:光靠工厂模式我们是无法实现我们的需求的
仔细想想我们的需求:我们**自动管理**类与类的关系,要**自动**实现初始化,
但是我们的**这些普通的工厂模式里,都是手动写死地去new**,毫无疑问是无法实现我们要求的**自动**的需求的,因为当我们新增一个类时,必须又得去手写实现,
注意,我们这里说的无法实现,只是**特指上述我们所说的普通的工厂模式(即:简单工厂,工厂方法,抽象工厂)无法实现我们的需求,**
但是,毫无疑问**工厂模式**也将是**我们要实现的功能的一个必要的设计思想**:**客户端只需要从工厂里面取出想要的对象,而对象的创建由工厂内部完成**
只不过因为普通的工厂模式的内部都是写死new来创建对象,不符合我们的自动需求罢了,
那么:**我们Java有什么技术可以不写死地用new,而是动态地来创建对象呢?**
我们应该第一时间想到:**反射!**
**如果我们可以通过反射实现动态地创建对象,那么我们新增一个类时,就无需手写来实现其实例对象的创建,也就离我们的自动需求更近一步了!**
### **反射**:
* 概念:反射机制就有点像**获得一份class字节码的抽象**,我们可以通过三种方式(.class, getClass, Class.forName)获得Class对象,
并通过Class对象获得这个类其中的Field成员变量,Method成员方法,Constructor构造方法,
**从而动态地去实例化对象,设置变量值,调用方法等**
* 作用:
* **一切都可以在运行时再进行,而非编译期就写死!**
* 在运行时,实例化一个类的对象
* 在运行时,访问一个对象的成员变量、成员方法等等
* **可以实现配置化动态创建对象,即:让代码更有通用性,我们可以把一些变动的内容(例如类的全限定名称等)写在配置文件/注解等处,**
**通过动态地获取配置信息,从而动态地确定要被创建的对象是什么,要调用的方法是什么等等,可以非常灵活,而原代码并未发生任何变动**
* 具体的反射API就不在此诸多赘述了。
* 对实现我们要求的需求的作用:
* 我们的要求是可以**自动管理**类与类的关系,要**自动**实现初始化(注入)等
* 而有了**反射**这一可以**动态创建对象并访问其成员** 并且 **还可以实现配置化** 的利器,
我们不就可以通过**提前配置好类相关的信息**,然后交由**工厂** 去**读取这些配置,然后通过反射创建对象**
这样一来,我们的客户端就可以完全不关心这些对象的创建过程,**也不需要自己手动去写new,**
只需要交由 结合了 **工厂模式 + 反射**的工厂,就可以获得这些对象了。
不就实现了我们要求的**自动**了吗?
至此,又出现了一个新的问题,那我们**如何去配置类相关的信息呢?**
借鉴一下Spring,我们可以通过类似于 **xml文件 这种集中式配置的方式**, 也可以 通过 **注解 这种分散式配置的方式**
两种其实都可以,我们这里暂时只采用了 **注解** 的方式去配置,在之后有余力,可以实现一下XML的配置方式。
(关于注解的相关知识,也不在此过多赘述了)
* 我们一直都在说:工厂内部里要管理类与类之间的关系,**那么工厂在其内部创建对象时,应该按怎么样的思想和方式去创建类对象,实现类与类的关系呢?**
例如下述例子:

* 上层依赖下层的方式:
* 概念:
我们很容易想到,既然行李箱依赖箱体,箱体依赖底盘,底盘依赖轮子,那么我们在构造方法把依赖的下层去new出来不就行了吗?
这就是上层依赖下层的创建方式。

* 缺点:不可扩展,难以维护
如果此时我们需要修改轮子的相关信息,那么就会引起一系列的连锁反应,导致项目难以维护:
例如:我们要把Trie的构造函数改动了,那么上面使用该构造函数来new该成员变量对象的类都得改变代码:

### **依赖倒置原则 -> 依赖注入 -> IOC思想 -> IOC容器:**
* 原本一般是上层依赖下层,所以:在上层的构造函数中把依赖的下层对象new出来
* **依赖倒置**的所谓倒置, 就是变为:下层依赖上层,具体实现上:把 **依赖的下层对象,注入到 上层对象中**
这种实现方式,我们称为**依赖注入DI: 就是把一个类的成员变量(即:依赖的对象)通过 外部注入而非内部创建 的方式来实现赋值**
由此方法,改造上面我们的那个例子:


此时你会发现,无论下层的怎么变,只需要改动一下该变动的对象的创建方式即可,其他的均不影响,这就解决了之前的问题。
* 同时,你会发现,通过**依赖注入**改造后的代码中,
**本来一个类对其成员变量的创建权,已经不在该类手上了,即:一个类的成员变量对象的创建,并非由其自己去实现**
**在此案例代码的右边,我们可以看到:对于这些对象的创建,已经统一放在了这些类的外部了**
**这种 把类在其内部程序本身创建对象的控制权,交由别的地方进行统一管理的设计思想,我们称为:控制反转IOC**
* 而,**在此控制反转IOC的设计思想下,这个统一管理这些创建对象的地方,我们称之为:IOC容器**
* 至此,我们一一介绍了依赖倒置、依赖注入、控制反转IOC思想,IOC容器的概念,他们的关系如下:

* 在介绍完上述概念后,回答提出的问题:工厂内部要管理类与类之间的关系,那么**工厂在内部创建对象时,应该按怎么样的思想和方式去创建类对象呢?**
答案就应该是:**在依赖倒置原则 以及 控制反转设计思想的指导下,通过IOC容器,在其内部通过依赖注入的方式,实现类对象的创建**


### 总结:

我们依次介绍了:
* 工厂模式 -> 结论:我们要实现的**自研框架要像一个工厂一样,客户端可以从该工厂里面获取对象,而无需关注内部的对象创建过程。**
* 反射机制 -> 结论:我们要通过**反射这一以实现配置化动态创建对象的特性,实现我们的自动需求**
* 注解 -> 结论:我们要通过**注解这一分散式配置方式,来为反射机制配置与提供类的相关信息。**
* IOC容器 -> 结论:我们的**自研框架内部创建对象的方式,应该是个IOC容器**
总结来看,要实现我们提出的**需要一个自动管理类与类之间关系、自动初始化(注入)的工具**的需求,
那**我们的自研框架在这方面的实现方式**就应该是:
* **用户可以通过 注解 来对 类的相关信息 进行配置;**
* **而自研框架提供一个IOC容器,其内部的对象创建过程是:通过 读取程序中注解的配置信息+反射机制 动态地创建出对象,同时通过依赖注入的方式,实现类与类之间的依赖关系;**
* **从而IOC容器就如同一个工厂一样,用户可以忽略上述的内部的对象创建过程,直接从该IOC容器中获得需要的对象**
* **此过程中,用户只需要通过注解进行配置,然后就可以直接通过IOC容器获得对象,从头到尾没写过一行创建对象的代码,这就是我们的需求:“自动管理类与类之间关系、自动初始化(注入)” 的实现**
## 自研框架IoC容器(功能)的实现
### IOC容器(功能)的实现流程:
* 思路:
前面我们已经介绍过我们自研框架在实现上述需求时的实现方式了,其实该实现方式,就是**IOC容器(功能)的实现**!
据上面的分析,我们可以把框架的作用或功能分为以下几点:
* 解析**配置**:
* **定位到目标**同时**注册目标的实例对象到IOC容器**内
* 实现**依赖注入**
* 提供对外的api来对IOC容器进行操作
我们要通过**以下的技术选型**来实现上述功能:
* **注解**:用于给用户在特定的地方做标记进行配置,从而自研框架可以去识别并进行相应处理 => **创建注解 => 用于配置**
* **类加载器**:之前学反射的时候说过,类加载器可以用于在在任何环境下获得指定范围(用户给定包名)下的所有的文件 => **用于从指定范围下获得所有的class文件,从而通过反射获得对应的Class对象**
* **反射**:用于通过全限定名称获得Class对象,从而获得Constructor对象进行对象实例化,获得Field对象进行注入,同时可以通过isAnnotationPresent方 法来判断是否被某注解标注 => **获得目标的对象、以及成员变量对应的Field,从而可以实现注册与注入功能**
* **ConcurrentHashMap**:用于作为IOC容器的载体,以Class对象作为Key(因为Class对象在JVM中只会有一个),以Class对象的实例对象作为Value =>
**实现IOC容器的载体,并被包装为一个IOC容器类,对外提供相关操作**
* 综上,实现流程为:
* 创建注解
* 定位与提取目标
* 实现容器
* 依赖注入
### 创建注解:
* 此处我们先来创建用于修饰类的注解 : Controller、Service、Repository、Component
```java
/**
* 要被注册的controller对象
*/
@Target(ElementType.TYPE) // 用于修饰类
@Retention(RetentionPolicy.RUNTIME) // 要用反射进行判断,所以必须保留到Runtime时期
public @interface Controller {
}
```
* 用于修饰之前我们业务系统里的类:

### 定位与提取目标:
* 上面我们已经把创建好的 **标识 该类的对象是要被注册到容器的类 的注解** 放置到该放置的类上面了,那,**我们怎么去定位并提取这些目标呢?**
* **毫无疑问,要定位并提取这些目标,我们首先要实现的,就是要把用户指定范围下所有的类都给获取并创建出来!先以此得到 全部的Class对象 ,再逐一遍历去过滤从而实现定位并提取这些目标**
* 既然要实现 **指定的全限定名称 -> 类 -> 对象** ,必然是用 **反射机制**,那么问题来了,当用户使用该框架提供的IOC容器时,只会传入一个限定范围
例如:com.mlming 包下的所有类都要被获取,但是用户只会传一个“com.mlming”路径进来,这种情况下,我们怎么得到这个包下所有类的全限定名称呢?
在之前学反射时,我们学过,**通过类加载器 是可以实现在任何环境下面都能获取到绝对路径的方法!**
```java
之前学Java,我们都说过绝对路径不好,因为换个机子就不对了
但是,我们知道,相对路径是为了无论什么环境都能移植
但是Java不是如此,不同开发工具其的相对路径的默认相对的那个路径是不一样的
这就是失去了移植性
所以其实Java中一般也不用相对路径
在学Node.js时,我们其实面临过一样的问题,
我们最后采取的方式是: 动态获取当前文件的绝对路径
这样就能无论放在那里都能动态获取到绝对路径,从而实现了通用,可移植
那么在Java中,如何实现**动态获取绝对路径呢?**
以下代码是基本固定的,以后Java使用路径,基本上都是用这种方式,所以最好背会:
实现: Thread.currentThread().getContextClassLoader().getResource(文件相当于src的相对路径).getPath()
注意: 此种写法必须要求该文件处于**类路径下,即src目录下**
**从而getSource()里面填的是文件相当于src目录下的相对路径**
解析: Thread.currentThread()获取当前线程对象
getContextClassLoader()是线程对象的一个方法,获取到当前线程的类加载器对象
getResource() 是类加载器对象的一个方法,可以到当前类所在的根路径下加载资源
**当前类所在的根路径 => 类路径 => src**
所以这种方式要求文件必须位于src下,是getSource()的执行问题
因为类的根目录肯定是src,所以类所在根路径就是src,所以资源即文件必须放在src下
getPath() 获取当前资源绝对路径
```
因此,**我们就可以获得传入的指定路径的绝对路径了,获得绝对路径后,我们可以通过穷举的方式把里面的.class文件一一的获得,从而通过字符串分割等方式获得全限定名称部门,从而利用反射进行Class对象的创建,**这样一来,就实现 **获取指定范围下所有的Class对象** 的需求了:
因为该功能是通用的,所以可以抽象成一个**ClassUtil中的extractPackageClasses方法**来进行实现:
```java
@Slf4j
public class ClassUtil {
/**
* 获取包下类集合
*
* @parampackageName包名
* @return 类集合
*/
public static Set<Class<?>> extractPackageClasses(String packageName) {
// 1) 获得类加载器:
ClassLoader classLoader = getClassLoader();
// 2) 通过类加载器加载指定路径的资源,获得其对应的绝对URL
URL url = classLoader.getResource(packageName.replace(".","/"));
if(url == null) {
log.warn(FrameWorkConst.PACKAGE_NOT_FOUND_ERROR_MSG + packageName);
return null;
}
// 3) 依据不同的资源类型,采用不同的方式获取资源的集合
Set<Class<?>> classSet = null;
// 过滤出文件类型的资源
if (url.getProtocol().equalsIgnoreCase(FrameWorkConst.FILE_PROTOCOL)){
classSet = new HashSet<Class<?>>();
File packageDirectory = new File(url.getPath());
extractClassFile(classSet, packageDirectory, packageName);
}
// TODO: 可以处理别的资源类型
return classSet;
}
/**
* 递归获取目标package里面的所有class文件(包括子package里的class文件)
*
* @param classSet 装载目标类的集合
* @param packageDirectory 文件或者目录
* @param packageName 包名
* @return 类集合
*/
private static void extractClassFile(Set<Class<?>> classSet, File packageDirectory, String packageName) {
// 如果该packageDirectory是文件,则直接return不处理
if(!packageDirectory.isDirectory()) return;
// 1) 获得目录下的所有的File,并对其进行过滤:
// 如果是目录,则还需要进一步的递归调用, 则加入到files数组, 然后之后遍历调用该函数进行穷举
// 如果是文件,且是.class文件,那么可以直接对其进行路径切割获得全限定名称,从而通过反射创建Class对象并加入set中
File[] files = packageDirectory.listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
if(file.isDirectory()) return true;
// 如果是.class文件:
String absoluteFilePath = file.getAbsolutePath();
if(absoluteFilePath.endsWith(".class")) {
// 进行路径切割,获得全限定名称:
// 因为包名是.分割,而路径是/, 所以先替换
absoluteFilePath = absoluteFilePath.replace(File.separator,".");
// 从包名处截断, 一直到最后一个.的位置, 从而就可以得到包名.类名, 也就是全限定名称
String className = absoluteFilePath.substring(absoluteFilePath.indexOf(packageName),absoluteFilePath.lastIndexOf("."));
// 反射获得Class对象, 并把该Class对象加入到set中去
Class<?> clazz = loadClass(className);
classSet.add(clazz);
}
return false;
}
});
// 2) 把files数组里面存的这些目录, 进一步递归调用
if(!ValidationUtil.isEmpty(files)) {
for (File file : files) {
extractClassFile(classSet,file,packageName);
}
}
}
/**
* 获得类加载器
* @return
*/
public static ClassLoader getClassLoader() {
return Thread.currentThread().getContextClassLoader();
}
/**
* 获取Class对象
*
* @param className class全名=package + 类名
* @return Class
*/
public static Class<?> loadClass(String className){
try {
return Class.forName(className);
} catch (ClassNotFoundException e) {
log.error(FrameWorkConst.LOAD_CLASS_ERROR_MSG, e);
throw new RuntimeException(e);
}
}
}
```
* 上一步,我们通过ClassUtil中的extractPackageClasses方法就可以获得指定包下面的所有Class对象了,现在我们要做的,就是要定位并提取
但是这个提取目标,是和IOC容器本身的实现有关的,所以我们把这一步放到容器实现时去实现。
### 实现容器:实现IOC容器的 “容器”
* 要求:
* 因为一个框架只能有一个IOC容器,所以该容器类必须是**单例模式**实现的
因为 **普通的饿汉模式** 以及 **双重检查锁的懒汉模式** 都会被 **反射** 破坏其单例性!
所以,此处我们选用 **枚举饿汉模式** 来实现单例, **由于枚举的性质,可以帮我们抵御反射以及序列化的破坏**。
**(之所以是懒汉模式,是因为IOC容器应该是项目启动时就被创建的,符合懒汉模式)**
* **容器的数据结构(载体):**
因为在IOC容器里面,我们要得到一个实例对象,怎么获得?很明显需要一个唯一标识来获取,也就是说这应该是Key-Value形式
**以Class对象作为Key(因为一个类的Class对象在JVM中只有一个),以实例对象作为Value。**
因此,**毫无疑问应该是一个Map数据结构,考虑到多线程安全问题,我们可以使用ConcurrentHashMap**
* 实现流程:
* 定义容器类:通过 **枚举饿汉模式** 来定义 IOC容器类,其中以 **ConcurrentHashMap**作为数据载体
* 实现容器的加载:即**1.3后面定位与提取目标的那一个流程**
* 通过1.3中的extractPackageClasses方法来获得指定范围下所有的类的集合
* 通过遍历该集合,并通过isAnnotationPresent判断其是否是被 我们创建的注解 所标记的,
如果是,那这就是目标,因此要把其实例化,把Class对象以及实例化对象放入ConcurrentHashMap中
* 对外提供一些操作供外界操作该容器
- **增加、删除操作**
- **根据Class获取对应实例**
- **获取多有的Class和实例**
- **通过注解来获取被注解标注的Class**
- **通过超类获取对应的子类Class**
- **获取容器载体保存Class的数量**
* 具体实现:
BeanContainer类:
```java
/**
* 容器类
*/
public class BeanContainer {
/**
* 存放所有被配置标记的目标对象的Map
*/
private final ConcurrentHashMap,Object> beanMap = new ConcurrentHashMap<>();
/**
* 加载bean的注解列表
*/
private static final List> BEAN_ANNOTATIONS
= Arrays.asList(Component.class, Controller.class, Service.class, Repository.class);
/**
* 枚举懒汉单例模式 实现 BeanContainer的获取
*/
public static BeanContainer getInstance() {
return ContainerHolder.HOLDER.instance;
}
private enum ContainerHolder {
HOLDER;
private BeanContainer instance;
// 懒汉模式:
ContainerHolder() {
instance = new BeanContainer();
}
}
// 加载标记: 一个容器只会被加载一次
private boolean loaded = false;
public boolean isLoaded() { return loaded; }
/**
* 加载指定范围下面的被注解标记的目标Bean
*
* 因为只能加载一次,所以直接用synchronized实现比较方便,当然也能用双重验证锁
* @param packageName
*/
public synchronized void loadBeans(String packageName) {
// 1) 如果加载过了, 则直接return
if(loaded) return;
// 2) 通过ClassUtil.extractPackageClasses()获得指定包下所有类对象
Set> classes = ClassUtil.extractPackageClasses(packageName);
// 3) 遍历set, 对每一个Class判断其是否被 我们创建的注解修饰, 如果是, 则要注册到容器中
for (Class> clazz : classes) {
for (Class extends Annotation> annotation : BEAN_ANNOTATIONS) {
if(clazz.isAnnotationPresent(annotation)) {
beanMap.put(clazz,ClassUtil.newInstance(clazz,true));
}
}
}
// 4) 加载完毕,设置为true
loaded = true;
}
/**
* 添加一个class对象及其Bean实例
*
* @param clazz Class对象
* @param bean Bean实例
* @return 原有的Bean实例, 没有则返回null
*/
public Object addBean(Class> clazz, Object bean) {
return beanMap.put(clazz, bean);
}
/**
* 移除一个IOC容器管理的对象
*
* @param clazz Class对象
* @return 删除的Bean实例, 没有则返回null
*/
public Object removeBean(Class> clazz) {
return beanMap.remove(clazz);
}
/**
* 根据Class对象获取Bean实例
*
* @param clazz Class对象
* @return Bean实例
*/
public Object getBean(Class> clazz) {
return beanMap.get(clazz);
}
/**
* 获取容器管理的所有Class对象集合
*
* @return Class集合
*/
public Set> getClasses(){
return beanMap.keySet();
}
/**
* 获取所有Bean集合
*
* @return Bean集合
*/
public Set