概述
自研框架IoC容器的使用
框架具备的最基本功能
◆解析配置
◆定位与注册对象, 定位就是标记(注解),定位到对象之后,需要将对象注册到容器中
◆注入对象, 在用户需要时将对象精确的返回给用户
◆提供通用的工具类, 方便用户和框架根据自身需求进行调用
IoC容器的实现
对象:泛指被标记的东西,通常指类。将类名以及类以键值对存入到容器当中。
存贮的并不是类实例本身,而是类得class对象,xml属性信息或者注解,将他们组成beanFactory存储在容器里。之后再决定是否在容器初始化的时候直接创建经过目标包装好的实例,或者在首次使用到目标对象得地方在创建经过目标包装好得实例。
实现容器之后,解决IoC的输出和依赖注入的问题。
一、创建注解
先创建常用注解@Controller(需要将该类被spring容器管理起来,方便对前端请求进行管理)、
@Service(表示service层得实现类)、
@Repository(进行数据库得操作)
@Component(通用得需spring容器管理起来)
有了componet为什么还要有上面三个不同的注解?
能够在逻辑上划分不同的模块,针对不同的使用场景,采用特定功能化的注解主键,并在需要获取例如获取Controller类时能依据特定的标签筛选出来
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Controller {
}
二、提取被注解标记的对象
并将他们注入到容器当中,
遍历范围:需要遍历指定范围得类
实现思路
◆指定范围,获取范围内的所有类
◆遍历所有类,获取被注解标记的类并加载进容器里
将第一步的是实现抽取出来,整成工具类。(根据用户传入得业务项目得package,去扫面该package里面的所有类)
根据传入得package获取下面得所有类
extractPacakgeClass里面需要完成的事情
◆获取到类的加载器
◆通过类加载器获取到加载的资源信息
◆依据不同的资源类型,采用不同的方式获取资源的集合
获取项目类加载器的目的
获取项目发布的实际路径
com.imooc,我们是无法定位到它得具体路径,所以必须要先得到具体路径,才能拿到该路径下的所有class文件。
为什么不让用户传入绝对路径
◆不够友好:不同机器之间的路径可能不相同
◆如果打的是war包或者jar包,根本找不到路径
◆因此通用的做法是通过项目的类加载器来获取
类加载器ClassLoader
◆根据一个指定的类的名称,找到或者生成其对应的字节码
◆加载Java应用所需的资源(图片文件、配置文件、资源目录等)
程序都是线程来执行得,获得当前执行的线程所属的加载器就可。
/**
* 获取类加载器
* @return 当前ClassLoader
*/
public static ClassLoader getClassLoader(){
return Thread.currentThread().getContextClassLoader();
}
怎么通过类加载器获得加载的资源?
统一资源定位符URL
某个资源的唯一地址
◆通过获取java.net.URL实例获取协议名、资源名路径等信息
过滤协议为小写的file的资源。
路径是绝对路径,如果是文件的话,文件名也会被包含在里面。
获取资源的分隔符是以 / 为标识分割的,但是包名是以 . 分割的,所以要先处理packageName
判断ur空值,空值记录日志
当找到文件时递归停止,及判断不是目录就终止。如果是文件夹,就罗列当前文件夹下的所有文件和文件夹(不包含子文件夹)
我们只关注文件夹和class的文件,return true过滤出来。检测到class文件后,将类实例加载到到集合中
递归过滤文件夹下的目录,将其class文件的类实例加载到集合
为什么还要判断file为空?因为不判断在foreach循环时会报异常。即在foreach循环时要先空值判断。
内部类可以使用外部类的属性值,外部类有packageName,内部类又可以获取路径,可以得到类的路径全路径名
将获取Class对象封装为通用的方法,
最终ClassUtil提取被注解标记的对象代码:
package org.simpleframework.util;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.FileFilter;
import java.net.URL;
import java.util.HashSet;
import java.util.Set;
@Slf4j
public class ClassUtil {
public static final String FILE_PROTOCOL = "file";
/**
*获取包下类集合
* @param packageName
* @return 该包下的所有类的集合
*/
public static Set<Class<?>> extractPackageClass(String packageName){
//1、获取类加载器
ClassLoader classLoader = getClassLoader();
//2、通过类加载器获取需要加载的资源
//获取资源的分隔符是以 / 为标识分割的,但是包名是以 . 分割的,所以要先处理packageName
URL url = classLoader.getResource(packageName.replace(".", "/"));
if(url==null){
log.warn("unable to retrieve anything from package:" +packageName);
return null;
}
//3、依据不同的资源类型,采用不同的方式获取资源的集合
Set<Class<?>> classSet=null;
//过滤出文件类型的资源 getProtocol()获得协议名
if(url.getProtocol().equalsIgnoreCase(FILE_PROTOCOL )){
//创建集合实例
classSet=new HashSet<Class<?>>();
//获得资源的实际路径
File packageDirectory = new File(url.getPath());
//遍历目录以及子目录获得class文件
extractClassFile(classSet,packageDirectory,packageName);
}
//TODO 此处可以加入针对其他类型资源的处理,例如打成jar包后,protocol就是jar
return classSet;
}
/**
*
* @param emptyClassSet 装载目标类的集合
* @param fileSource 文件或者目录
* @param packageName 包名
*/
private static void extractClassFile(Set<Class<?>> emptyClassSet, File fileSource, String packageName) {
if(!fileSource.isDirectory()){//当找到文件时递归停止,及判断不是目录就终止。
return;
}
//如果是文件夹,就罗列当前文件夹下的所有文件和文件夹(不包含子文件夹)
File[] files = fileSource.listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
if(file.isDirectory()){//是文件夹就通过
return true;
}else {//文件需要判断是否是class文件
//获取文件的绝对值路径
String absolutePath = file.getAbsolutePath();
if(absolutePath.endsWith(".class")){
//若是class文件,则直接加载
addToClassSet(absolutePath);
}
return false;
}
}
//根据class文件的绝对值路径,获取并生成class对象,并放入classSet中
private void addToClassSet(String absolutePath) {
//1、从class文件的绝对值路径里提取出包含了package的类名
//将绝对路径中的分隔符转为.
absolutePath =absolutePath.replace(File.separator,".");
String className = absolutePath.substring(absolutePath.indexOf(packageName));
//将className包含的.class后缀去掉
className=className.substring(0,className.lastIndexOf("."));
//2、通过反射机制获取对应的Class对象并加入到classSet里
Class targetClass = loadClass(className);
emptyClassSet.add(targetClass);
}
});
if(files!=null){//先判断得到的文件们是否为空,在遍历进行递归调用
for (File f : files) {
//递归调用
extractClassFile(emptyClassSet, f, packageName);
}
}
}
/**
* 获取Class对象
* @param className=package+类名
* @return Class
*/
public static Class<?> loadClass(String className){
try {
return Class.forName(className);
} catch (ClassNotFoundException e) {
log.error("load class error:",e);
throw new RuntimeException(e);
}
}
/**
* 获取类加载器
* @return 当前ClassLoader
*/
public static ClassLoader getClassLoader(){
return Thread.currentThread().getContextClassLoader();
}
}
测试
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.8.1</version>
<scope>test</scope>
</dependency>
在test里面创建一样的目录
@Test
public void extractPackageClassTest(){
Set<Class<?>> classSet = ClassUtil.extractPackageClass("com.imooc.entity");
System.out.println(classSet);
Assertions.assertEquals(4,classSet.size());
}
单例模式Singleton Pattern
解决了实例的创建之后,再来创建容器,因为需要同一个容器将所有需要管理的对象给管理起来,所以容器需要用单例来实现。
确保一个类只有一个实例,并对外提供统一访问方式。客户端不需要同时也无法实例化该类的对象。
单例线程安全:
◆饿汉模式:
类被加载的时候就立即初始化并创建唯一实例
package demo.pattern.singleton;
public class StarvingSingleton {
private static final StarvingSingleton starvingSingleton=new StarvingSingleton();
private StarvingSingleton(){}
public static StarvingSingleton getInstance(){
return starvingSingleton;
}
public static void main(String[] args) {
System.out.println(StarvingSingleton.getInstance());
System.out.println(StarvingSingleton.getInstance());
}
}
构造函数设置为私有,为了防止外界new创建实例。设置为static保证唯一,设置private外界没法通过类名。的方式调用。设置为final,一经初始化不能再改变。只能通过我们提供的方法去获得早已创建好的实例对象。
◆懒汉模式:
在被客户端首次调用的时候才创建唯一实例
◆加入双重检查锁机制的懒汉模式能确保线程安全
package demo.pattern.singleton;
public class LazyDoubleCheckSingleton {
//加volatile防止重排序,以免出现还没初始化好,已经给对象分配好内存空间的情况
private volatile static LazyDoubleCheckSingleton instance;
private LazyDoubleCheckSingleton(){}
public static LazyDoubleCheckSingleton getInstance(){
//第一次检测
if(instance==null){
//加锁
synchronized (LazyDoubleCheckSingleton.class){
//第二次判断
if(instance==null){
//这条指令分为三步,加上volatile关键字使其严格按照顺序执行,不会出现132情况
/*
* 1、分配内存空间
* 2、初始化对象
* 3、设置instance指向刚分配的内存地址,此时instance对象!null*/
instance=new LazyDoubleCheckSingleton();
}
}
}
return instance;
}
}
利用反射可以获取私有的构造方法,将其private修饰符置为无效,从而创建实例,这说明单例模式并不绝对安全
类无法防御反射的攻击,给饿汉装备枚举无视反射的攻击。
定义私有的枚举,定义枚举的成员变量和实例。枚举类型的构造函数本身就是私有的,别人无法创建,所以加不加private修饰符都一样。枚举类型的实例在初始化的时候就会被创建,也属于饿汉式。
通过枚举将实例保护起来。
package demo.pattern.singleton;
public class EnumStarvingSingleton {
private EnumStarvingSingleton(){}
public static EnumStarvingSingleton getInstance(){
return ContainerHolder.HOLDER.instance;
}
private enum ContainerHolder{
HOLDER;
private EnumStarvingSingleton instance;
ContainerHolder(){
instance=new EnumStarvingSingleton();
}
}
}
测试发现,通过方法获得的实例对象和通过类对象反射创建对象获得的实例是一个
枚举的私有函数是可以低于反射的攻击的,可以通过这种方式来创建较为安全的容器。
反编译:javap 编译:javac
装备了枚举的饿汉模式能抵御反射与序列化的进攻4满足容器需求
通过序列化将创建的单例写到文件里,再通过该文件逆序列化创建单例,很有可能会出现两个不同的实例。
通过class的newInstance方法创建的对象被称为bean,因此将Ioc容器命名为BeanContainer
没有参数的构造方法,并设置修饰符。
设置私有的枚举的成员变量。HOLDER用来盛放BeanContainer实例。在定义私有的成员变量,定义枚举的私有构造函数,在里面new实例。
解决单例输出,方便外界获取单例对象。
package org.simpleframework.core;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE) //无参构造函数,可以包含私有的
public class BeanContainer {
/**
* 获取Bean容器实例
* @return
*/
public static BeanContainer getInstance(){
return ContainerHolder.HOLDER.instance;
}
private enum ContainerHolder{//设置私有的枚举的成员变量
HOLDER;//用来盛放BeanContainer实例
private BeanContainer instance;//定义私有的成员变量
//枚举的私有构造函数,在里面new实例
ContainerHolder{
instance=new BeanContainer();
}
}
}
实现容器
容器的组成部分
◆保存Class对象及其实例的载体
◆容器的加载 (需要定义配置获取并过滤目标对象的方法)◆容器的操作方式
并非所有class的对象都是容器管理的对象 ,而是从中选取配置里指定的class对象,也就是注解标记的对象,才将其存入载体并管理。
1、将类对象以及对应的实例以键值对的形式存入map里面
实现容器的加载
实现思路
◆配置的管理与获取(如何将注解管理起来,随时读取目标注解,得到被它标记的类)
◆获取指定范围内的Class对象◆依据配置提取Class对象,连同实例一-并存入容器
解决配置的问题:创建一个配置的载体用来保存相关的配置。
利用asList去转换成相应的list实例
//存放所有被配置标记的目标对象的Map,类对象以及对应的实例
private final Map<Class<?>,Object> beanMap=new ConcurrentHashMap<>();
/*
* 加载bean的注解列表
* */
private static final List<Class<? extends Annotation>> BEAN_ANNOTATION
= Arrays.asList(Component.class, Controller.class, Service.class, Repository.class);
当被这些注解修饰后就需要被beancontainer容器管理
加载bean和获取容器的分离,所以不需要返回值。传入加载的范围,返回该范围内的bean。
1、获取package所有的class
2、判断集合空值,日志记录,并返回。得到了就循环遍历classset根据定义好的注解得到对应的class对象
/**
* 扫描加载所有Bean
* @param packageName 包名
*/
public void loadBeans(String packageName){
//1、获取package所有的class
Set<Class<?>> classSet = ClassUtil.extractPackageClass(packageName);
//2、判断集合空值,日志记录,并返回。得到了就循环遍历classset根据定义好的注解得到对应的class对象
if(classSet==null || classSet.isEmpty()){
log.warn("extract nothing from packageName "+packageName);
return;
}
for (Class<?> clazz : classSet) {
for (Class<? extends Annotation> annotation : BEAN_ANNOTATION) {
//该类上面标记了我们定义的注解
if(clazz.isAnnotationPresent(annotation)){
//将目标类本身作为key,目标类实例作为v,放到beanMap中
beanMap.put(clazz,ClassUtil.newInstance(clazz,true));
}
}
}
}
创建class对象实例通用的方法,让用户来决定是否生成修饰符为private的实例
/**
* 获取类的实例化对象
* @param clazz Class
* @param accessible 是否支持创建出私有class对象的实例
* @param <T> class的类型
* @return 类的实例化
*/
public static <T> T newInstance(Class<?> clazz, boolean accessible) {
try {
Constructor<?> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(accessible);
return (T) constructor.newInstance();
} catch (Exception e) {
log.error("newInstance error ",e);
throw new RuntimeException(e);
}
}
再将上面判断集合是否为空得逻辑抽取出来作为工具类
还有判断字符串、数组、Map是否为空
package org.simpleframework.util;
import java.util.Collection;
import java.util.Map;
public class ValidationUtil {
/**
* Collection是否为null或size为0
* @param obj 集合Collection
* @return 是否为空
*/
public static boolean isEmpty(Collection<?> obj){
return obj==null || obj.isEmpty();
}
public static boolean isEmpty(String obj){
return obj==null || "".equals(obj);
}
public static boolean isEmpty(Object[] obj){
return obj==null || obj.length==0;
}
public static boolean isEmpty(Map<?,?> obj){
return obj==null || obj.isEmpty();
}
}
改造
if(ValidationUtil.isEmpty(classSet)){
log.warn("extract nothing from packageName "+packageName);
return;
}
由于容器的加载是一个比较耗时的过程,为了避免重复加载,我们可以定义i一个私有的布尔类型的成员变量用来判断容器是否被加载过。
/*
* 容器是否已经加载过bean
* */
private boolean loaded=false;
public boolean isLoaded(){
return loaded;
}
重构
在容器加载完成后,修改变量的值。
为了线程安全(两个线程修改变量的值),将方法定义为同步的
/**
* 扫描加载所有Bean
* @param packageName 包名
*/
public synchronized void loadBeans(String packageName){
//判断bean容器是否已经加载过
if(isLoaded()){
log.warn("BeanContainer has been loaded");
return;
}
//1、获取package所有的class
Set<Class<?>> classSet = ClassUtil.extractPackageClass(packageName);
//2、判断集合空值,日志记录,并返回。得到了就循环遍历classset根据定义好的注解得到对应的class对象
if(ValidationUtil.isEmpty(classSet)){
log.warn("extract nothing from packageName "+packageName);
return;
}
for (Class<?> clazz : classSet) {
for (Class<? extends Annotation> annotation : BEAN_ANNOTATION) {
//该类上面标记了我们定义的注解
if(clazz.isAnnotationPresent(annotation)){
//将目标类本身作为key,目标类实例作为v,放到beanMap中
beanMap.put(clazz,ClassUtil.newInstance(clazz,true));
}
}
}
loaded=true;
}
单元测试,创建一样的目录
@BeforeAll:只进行一次初始化,在所有测试开始之前进行的初始化。并不是每执行一次测试方法都初始化一次。
先判断是否被加载过。
将所有被core下面的annotation修饰的了类找出来并创建该类对应的实例,将其转载到容器里面。
给之前的类加上对应的标签,注意式给service的实现类加上service标签
定义共有的方法获取beanmap的size
/*
* 获得Bean实例数量
* */
public int size(){
return beanMap.size();
}
在测试整个类上执行
package org.simpleframework.core;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
public class BeanContainerTest {
private static BeanContainer beanContainer;
@BeforeAll //@BeforeAll:只进行一次初始化
static void init(){
beanContainer=BeanContainer.getInstance();
}
@Test
public void loadBeansTest(){
//先判断是否初始化
Assertions.assertEquals(false,beanContainer.isLoaded());
beanContainer.loadBeans("com.imooc");
Assertions.assertEquals(6,beanContainer.size());
Assertions.assertEquals(true,beanContainer.isLoaded());
}
}
整体的容器加载代码:
package org.simpleframework.core;
@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE) //无参构造函数,可以包含私有的
public class BeanContainer {
//存放所有被配置标记的目标对象的Map,类对象以及对应的实例
private final Map<Class<?>,Object> beanMap=new ConcurrentHashMap<>();
/*
* 加载bean的注解列表
* */
private static final List<Class<? extends Annotation>> BEAN_ANNOTATION
= Arrays.asList(Component.class, Controller.class, Service.class, Repository.class);
/*
* 容器是否已经加载过bean
* */
private boolean loaded=false;
public boolean isLoaded(){
return loaded;
}
/**
* 获取Bean容器实例
* @return
*/
public static BeanContainer getInstance(){
return ContainerHolder.HOLDER.instance;
}
/*
* 获得Bean实例数量
* */
public int size(){
return beanMap.size();
}
/**
* 扫描加载所有Bean
* @param packageName 包名
*/
public synchronized void loadBeans(String packageName){
//判断bean容器是否已经加载过
if(isLoaded()){
log.warn("BeanContainer has been loaded");
return;
}
//1、获取package所有的class
Set<Class<?>> classSet = ClassUtil.extractPackageClass(packageName);
//2、判断集合空值,日志记录,并返回。得到了就循环遍历classset根据定义好的注解得到对应的class对象
if(ValidationUtil.isEmpty(classSet)){
log.warn("extract nothing from packageName "+packageName);
return;
}
for (Class<?> clazz : classSet) {
for (Class<? extends Annotation> annotation : BEAN_ANNOTATION) {
//该类上面标记了我们定义的注解
if(clazz.isAnnotationPresent(annotation)){
//将目标类本身作为key,目标类实例作为v,放到beanMap中
beanMap.put(clazz,ClassUtil.newInstance(clazz,true));
}
}
}
loaded=true;
}
private enum ContainerHolder{//设置私有的枚举的成员变量
HOLDER;//用来盛放BeanContainer实例
private BeanContainer instance;//定义私有的成员变量
//枚举的私有构造函数,在里面new实例
ContainerHolder(){
instance=new BeanContainer();
}
}
}
容器的操作方式
根据某个service接口来获取该接口的实现类
实现容器的操作方式
涉及到容器的增删改查
◆增加、删除操作
◆通过注解来获取被注解标注的Cla
◆根据Class获取对应实例◆通过超类获取对应的子类Class
◆获取所有的Class和实例◆获取容器载体保存Class的数量
增加bean
/**
* 添加一个Class对象和其对应的Bean实例到容器
* @param clazz
* @param Bean
* @return
*/
public Object addBean(Class<?> clazz,Object Bean){
return beanMap.put(clazz, Bean);
}
删除:通过key删除
/**
* 删除容器中管理的对象
* @param clazz
* @return
*/
public Object removeBean(Class<?> clazz){
return beanMap.remove(clazz);
}
获取bean实例
/**
* 根据Class对象获得Bean实例
* @param clazz
* @return
*/
public Object getBean(Class<?> clazz){
return beanMap.get(clazz);
}
获取所有bean管理的对象,即key的集合
/**
* 获得容器管理的所有Class对象集合
* @return Class集合
*/
public Set<Class<?>> getClasses(){
return beanMap.keySet();
}
获取所有bean实例,即v的集合,和key保持一致强转为set
/**
* 获得所有Bean集合
* @return
*/
public Set<Object> getBeans(){
return (Set<Object>) beanMap.values();
}
根据注解获得容器里所有Class对象,即所有的key,对返回的classSet集合进行空值判断(null或者为空),统一返回结果
/**
* 根据注解获得容器里所有Class对象,即所有的key
* @param annotation
* @return
*/
public Set<Class<?>> getClassesByAnnotation(Class< ? extends Annotation> annotation){
//1、获取beanMap的所有class对象
Set<Class<?>> keySet = getClasses();
if(ValidationUtil.isEmpty(keySet)){
log.warn("nothing in beanMap");
return null;
}
//2、通过注解筛选被注解标记的class对象,并添加到classSet里
Set<Class<?>> classSet=new HashSet<>();
for (Class<?> clazz : keySet) {
//类是否有相关的注解标记
if(clazz.isAnnotationPresent(annotation)){
classSet.add(clazz);
}
}
return classSet.size()>0 ? classSet : null ;
}
通过接口或者父类获取实现类或者子类的Class集合,不包括本身
/**
* 通过接口或者父类获取实现类或者子类的Class集合,不包括本身
*/
public Set<Class<?>> getClassesBySuper(Class<?> interfaceOrClass){
//1、获取beanMap的所有class对象
Set<Class<?>> keySet = getClasses();
if(ValidationUtil.isEmpty(keySet)){
log.warn("nothing in beanMap");
return null;
}
//2、判断集合里的元素是否是传入的接口或者类的子类,并添加到classSet里
Set<Class<?>> classSet=new HashSet<>();
for (Class<?> clazz : keySet) {
//判断类是否传入参数的子类,并去除本身
if(interfaceOrClass.isAssignableFrom(clazz) && !clazz.equals(interfaceOrClass)){
classSet.add(clazz);
}
}
return classSet.size()>0 ? classSet : null ;
}
A.isAssignableFrom(B); //判断A是不是B的父类,还可以看AB两者是否是本身一样的或者有继承关系,不仅局限于父子类、实现类、还有祖孙
父类.isAssignableFrom(子类);
接口.isAssignableFrom(实现类);
测试
对容器的操作都是基于容器被加载之后,所以测试时要指定顺序
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
用instanceof来判断controller实例是否是MainPageController创建出来的
dispatcherServlet没有加注解没有被bean容器管理,所以使用bean容器获得的实例对象应该为null
package org.simpleframework.core;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class BeanContainerTest {
private static BeanContainer beanContainer;
@BeforeAll //@BeforeAll:只进行一次初始化
static void init(){
beanContainer=BeanContainer.getInstance();
}
@DisplayName("加载目标类及其实例到容器里")
@Order(1)
@Test
public void loadBeansTest(){
//先判断是否初始化
Assertions.assertEquals(false,beanContainer.isLoaded());
beanContainer.loadBeans("com.imooc");
Assertions.assertEquals(6,beanContainer.size());
Assertions.assertEquals(true,beanContainer.isLoaded());
}
@DisplayName("根据类获取其实例")
@Order(2)
@Test
public void getBeanTest(){
MainPageController bean = (MainPageController) beanContainer.getBean(MainPageController.class);
//用instanceof来判断controller实例是否是MainPageController创建出来的
Assertions.assertEquals(true,(bean instanceof MainPageController));
//dispatcherServlet没有加注解没有被bean容器管理,所以使用bean容器获得的实例对象应该为null
DispatcherServlet bean1 = (DispatcherServlet) beanContainer.getBean(DispatcherServlet.class);
Assertions.assertEquals(null,bean1);
}
@DisplayName("根据注解获取对应的实例")
@Order(3)
@Test
public void getClassesByAnnotationTest(){
Assertions.assertEquals(true,beanContainer.isLoaded());
Assertions.assertEquals(3,beanContainer.getClassesByAnnotation(Controller.class).size());
}
@DisplayName("根据接口获取实现类")
@Order(4)
@Test
public void getClassesBySuperTest(){
Assertions.assertEquals(true,beanContainer.isLoaded());
Assertions.assertEquals(true,beanContainer.getClassesBySuper(HeadLineService.class).contains(HeadLineServiceImpl.class));
}
}
容器管理的Bean实例
皆为单例
Spring框架有多种作用域
◆singleton
◆prototype :每次通过容器的getBean方法都会获得一个新的实例。
◆request:对于每次http请求使用request定义的bean都会产生一个新的bean实例(即每次http请求都会产生不同的bean实例),只有在web应用中使用spring时该作用域才有效。
◆session :对于每次http请求使用session定义的bean都会产生一个新的bean实例(即在session有效的时候返回的bean是一个,失效再去访问返回的就是新的),只有在web应用中使用spring时该作用域才有效。
◆globalsession:每个全局的http session,使用session定义的bean都会产生一个新实例。
实现容器的依赖注入
目前容器里面管理的Bean实例仍可能是不完备的
导读:
上面虽然将bean实例交给了容器管理,但是我们从容器中获取bean对象没有注入到相对应的成员变量对象上,此时我们如果调用对象封装的方法,一定会报空指针异常(相当于取到的对象没有赋值给对应的对象变量),即接下来我们需要解决把取到的对象给到对应的主人,让主人充实起来,不是null,就是依赖注入。
实现思路
◆定义相关的注解标签
◆实现创建被注解标记的成员变量实例,并将其注入到成员变量里◆依赖注入的使用(我们只支持成员变量级别的注入,不像spring框架还支持构造函数、工厂注入)
1、定义相关的注解标签
@Target(ElementType.TYPE) //仅支持在成员变量上使用
@Retention(RetentionPolicy.RUNTIME)
public @interface Autowired {
}
提供依赖注入的服务,
需要先获取容器中的实例,
为了在DependencyInjector实例被创建时就能获得beanContainer实例,可以在构造方法里面编写获得容器实例的逻辑
/*
* 执行IoC
* */
public void doIoc(){
//判空
if(ValidationUtil.isEmpty(beanContainer.getClasses())){
log.warn("bean is nothing class");
return;
}
//1、遍历Bean容器中所有的Class对象
for (Class<?> clazz : beanContainer.getClasses()) {
//2、遍历Class对象的所有成员变量
Field[] fields = clazz.getDeclaredFields();
//判空
if(ValidationUtil.isEmpty(fields)){
continue;
}
for (Field field : fields) {
//3、找出被Autowired标记的成员变量
if(field.isAnnotationPresent(Autowired.class)){
//4、获取这些成员变量的类型
Class<?> fieldClass = field.getType();
//5、获取这些成员变量的类型在容器里对应的实例
Object fieldValue=getFileInstance(fieldClass);
if(fieldValue==null){
throw new RuntimeException("unable to inject relevant type,target fieldClass is:"+fieldClass.getName());
}else {
//6、通过反射将对应的成员变量实例注入到成员变量所在类的实例里
Object targetBean = beanContainer.getBean(clazz);
ClassUtil.setField(field,targetBean,fieldValue,true);
}
}
}
}
}
先判空
给实例变量赋值是一个通用的方法
/**
* 设置类的属性值
* @param field 成员变量
* @param targetBean 类实例
* @param value 成员变量的值
* @param accessible 是否允许设置私有属性
*/
public static void setField(Field field, Object targetBean, Object value, boolean accessible) {
field.setAccessible(accessible);
try {
field.set(targetBean,value);
} catch (IllegalAccessException e) {
log.error("setField error",e);
throw new RuntimeException(e);
}
}
得到的bean实例不为空就说明在容器中直接找到了(说明Class是类),直接返回。如果为空,需要判断是不是接口(返回对应的实现类)。找他对应的实现类后,再在容器里找实现类,有实现类就返回。没有就返回null
/*
* 根据Class在beanContainer里获取其实例或者实现类
* */
private Object getFileInstance(Class<?> fieldClass) {
//从容器中获得bean
Object fieldValue = beanContainer.getBean(fieldClass);
//判空
if(fieldValue!=null){
return fieldValue;//直接找到,说明是类
}else {
//在容器中没有直接找到bean,有两种情况1、传入的是接口,bean里面存的是实现类 2、就是没有
Class<?> implementedClass= getImplmentClass(fieldClass);//根据接口找实现类
if(implementedClass!=null){
return beanContainer.getBean(implementedClass);
}else {
return null;
}
}
}
因为Autowired是依据成员变量的类型注入的,多个实现类的情况,增加了Qualifier,指定具体的实现类名称。
我们采用最简单的实现方式,在Autowired里面加上value属性
@Target(ElementType.TYPE) //仅支持在成员变量上使用
@Retention(RetentionPolicy.RUNTIME)
public @interface Autowired {
String value() default "";
}
重构注入ioc方法(先去获得Auyowired实例,再去获取其value)、根据Class在beanContainer里获取其实例或者实现类(就传递个参数)、
首先判断autowiredValue的值是否为默认值,为空表示用户没有告诉框架返回那个具体的实现类,需要处理两种情况:只有一个实现类2、多个实现类,抛出异常
如果value设置上值,需要去遍历classSet,class的名字是去除掉package的名字,所以用getSimpleName
/*
* 获取接口的实现类
* */
private Class<?> getImplmentedClass(Class<?> fieldClass, String autowiredValue) {
Set<Class<?>> classSet = beanContainer.getClassesBySuper(fieldClass);
//判空
if(!ValidationUtil.isEmpty(classSet)){
//首先判断autowiredValue的值是否为默认值(空)
if(ValidationUtil.isEmpty(autowiredValue)){
//再判断实现类有几个
if(classSet.size()==1){
return classSet.iterator().next();//一个实现类没有歧义直接返回
}else {
//多个实现类又没有指定具体的实现类名称,报错
throw new RuntimeException("multiple implemented classes for" +fieldClass.getName()+ " please set @Autowired's value to pick one");
}
}else {//指定了实现类
//遍历classSet,寻找并返回
for (Class<?> clazz : classSet) {
//得到类的简写名称getSimpleName()只有一个类名
if(autowiredValue.equals(clazz.getSimpleName())){
return clazz;
}
}
}
}
return null;
}
整体依赖注入代码:
@Slf4j
public class DependencyInjector {
//需要先获取容器中的实例,
private BeanContainer beanContainer;
//为了在DependencyInjector实例被创建时就能获得beanContainer实例,
// 可以在构造方法里面编写获得容器实例的逻辑
public DependencyInjector(){
beanContainer=BeanContainer.getInstance();
}
/*
* 执行IoC
* */
public void doIoc(){
//判空
if(ValidationUtil.isEmpty(beanContainer.getClasses())){
log.warn("bean is nothing class");
return;
}
//1、遍历Bean容器中所有的Class对象
for (Class<?> clazz : beanContainer.getClasses()) {
//2、遍历Class对象的所有成员变量
Field[] fields = clazz.getDeclaredFields();
//判空
if(ValidationUtil.isEmpty(fields)){
continue;
}
for (Field field : fields) {
//3、找出被Autowired标记的成员变量
if(field.isAnnotationPresent(Autowired.class)){
//先去获得Autowired注解实例,再去获取注解里面的属性
Autowired autowired = field.getAnnotation(Autowired.class);
String autowiredValue = autowired.value();
//4、获取这些成员变量的类型
Class<?> fieldClass = field.getType();
//5、获取这些成员变量的类型在容器里对应的实例
Object fieldValue=getFileInstance(fieldClass,autowiredValue);
if(fieldValue==null){
throw new RuntimeException("unable to inject relevant type,target fieldClass is:"+fieldClass.getName());
}else {
//6、通过反射将对应的成员变量实例注入到成员变量所在类的实例里
Object targetBean = beanContainer.getBean(clazz);
ClassUtil.setField(field,targetBean,fieldValue,true);
}
}
}
}
}
/*
* 根据Class在beanContainer里获取其实例或者实现类
* */
private Object getFileInstance(Class<?> fieldClass, String autowiredValue) {
//从容器中获得bean
Object fieldValue = beanContainer.getBean(fieldClass);
//判空
if(fieldValue!=null){
return fieldValue;//直接找到,说明是类
}else {
//在容器中没有直接找到bean,有两种情况1、传入的是接口,bean里面存的是实现类 2、就是没有
Class<?> implementedClass= getImplmentedClass(fieldClass,autowiredValue);//根据接口找实现类
if(implementedClass!=null){
return beanContainer.getBean(implementedClass);
}else {
return null;
}
}
}
/*
* 获取接口的实现类
* */
private Class<?> getImplmentedClass(Class<?> fieldClass, String autowiredValue) {
Set<Class<?>> classSet = beanContainer.getClassesBySuper(fieldClass);
//判空
if(!ValidationUtil.isEmpty(classSet)){
//首先判断autowiredValue的值是否为默认值(空)
if(ValidationUtil.isEmpty(autowiredValue)){
//再判断实现类有几个
if(classSet.size()==1){
return classSet.iterator().next();//一个实现类没有歧义直接返回
}else {
//多个实现类又没有指定具体的实现类名称,报错
throw new RuntimeException("multiple implemented classes for" +fieldClass.getName()+ " please set @Autowired's value to pick one");
}
}else {//指定了实现类
//遍历classSet,寻找并返回
for (Class<?> clazz : classSet) {
//得到类的简写名称getSimpleName()只有一个类名
if(autowiredValue.equals(clazz.getSimpleName())){
return clazz;
}
}
}
}
return null;
}
}
标记类中的成员变量,使其注入实例
测试
创建同样的目录
1、获取容器实例
2、指定范围将被注解标记的类交给容器管理加载
3、先判断容器是否被加载,加载后就可以获得bean实例
4、判断是否是MainPageController创建的实例
5、给controller设置get方法获得私有成员变量
6、因为没调用doIoc,所以成员变量的实例应该为null
7、调用doIoc,进行依赖注入
package org.simpleframework.inject;
public class DependencyInjectorTest {
@Test
@DisplayName("依赖注入doIoc")
public void doIocTest(){
//1、获取容器实例
BeanContainer beanContainer = BeanContainer.getInstance();
//2、指定范围将被注解标记的类交给容器管理加载
beanContainer.loadBeans("com.imooc");
//3、先判断容器是否被加载,加载后就可以获得bean实例
Assertions.assertEquals(true,beanContainer.isLoaded());
//4、判断是否是MainPageController创建的实例
MainPageController bean = (MainPageController) beanContainer.getBean(MainPageController.class);
Assertions.assertEquals(true,(bean instanceof MainPageController));
//6、因为没调用doIoc,所以成员变量的实例应该为null
Assertions.assertEquals(null,bean.getHeadLineShopCategoryCombineService());
//7、调用doIoc,进行依赖注入
new DependencyInjector().doIoc();
Assertions.assertNotEquals(null,bean.getHeadLineShopCategoryCombineService());
}
}
验证同一个接口有多个实现类,再创建一个头条实现类,这时MainPageController里面的成员变量就有两个实现类,如果Autowired不指明value是谁,就会抛出异常
public class DependencyInjectorTest {
@Test
@DisplayName("依赖注入doIoc")
public void doIocTest(){
//1、获取容器实例
BeanContainer beanContainer = BeanContainer.getInstance();
//2、指定范围将被注解标记的类交给容器管理加载
beanContainer.loadBeans("com.imooc");
//3、先判断容器是否被加载,加载后就可以获得bean实例
Assertions.assertEquals(true,beanContainer.isLoaded());
//4、判断是否是MainPageController创建的实例
MainPageController bean = (MainPageController) beanContainer.getBean(MainPageController.class);
Assertions.assertEquals(true,(bean instanceof MainPageController));
//6、因为没调用doIoc,所以成员变量的实例应该为null
Assertions.assertEquals(null,bean.getHeadLineShopCategoryCombineService());
//7、调用doIoc,进行依赖注入
new DependencyInjector().doIoc();
Assertions.assertNotEquals(null,bean.getHeadLineShopCategoryCombineService());
Assertions.assertEquals(true,bean.getHeadLineShopCategoryCombineService() instanceof HeadLineShopCategoryCombineServiceImpl);
Assertions.assertEquals(false,bean.getHeadLineShopCategoryCombineService() instanceof HeadLineShopCategoryServiceImpl2);
}
}
本章小结
最后
以上就是凶狠月饼为你收集整理的【自研框架一】自研框架IoC容器及单例模式的全部内容,希望文章能够帮你解决【自研框架一】自研框架IoC容器及单例模式所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复