Spring 手撕IOC
提到spring 不可避免的就是两个核心组件 IOC 和 AOP 这里我们主要实现的是 IOC
那接触到这个实现ioc的文章 适合哪些人群一起学习呢
- 了解反射
- 至少 用过 spring 或多或少能感受到IOC给我们带来的好处
IOC (inverse of control)
简单介绍
IOC就是将创建对象的权限,从Java 程序员 交给 IOC容器来创建,就是将对象的创造全 给到了框架,
我们称为 控制反转
创建一个Maven项目
我们用maven 项目来演示和编写spring 的ioc 容器代码
1、配置依赖
pom.xml
<dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> </dependency> </dependencies>
2、编写三层架构
模拟数据 DAO
public class hellloDaoImpl implements helloDao { @Override public List<String> findAll() { return Arrays.asList("1", "2", "3"); } }
连接视图和数据层: service
public class helloServiceImpl implements helloservice { private helloDao helloDao = new hellloDaoImpl(); @Override public List<String> findAll() { return helloDao.findAll(); } }
控制层 : servlet/controller
@WebServlet("/hello") public class helloservlet extends HttpServlet { helloservice helloservice = new helloServiceImpl(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.getWriter().write("spring" + helloservice.findAll()); } }
3、配置Tomcat 部署
需要解决的问题
不同实现需要重写接口代码
DAO 操作的是数据源, 数据源(数据库) 如果此时项目变更,比如数据库变成 Oracle 那么我们的DAO代码可能就需要推翻重写
如何解决呢?
设计模式
使用静态工厂来创建特定的类, 不再把代码写死在service 中,通过工厂的方式来实现
public class beanfactory { public static helloDao getDao() { return new hellloDaoImpl(); } }
service
代码
private helloDao helloDao = beanfactory.getDao();
上述方法提供的思路 还是不能解决我们的问题,需求发生改变的时候 , 仍然需要改变代码 ,这个时候我们就要用到反射
如果不修改java 代码 如何实现类的切换呢?
使用 : 外部配置文件的方式 将具体的类写到配置文件中, Java 程序只需要读取配置文件即可
编写外部文件
properties , yml , xml 等配置文件的方式来读取需要根绝什么数据源来使用什么工厂、这里我们使用 properties
,
1、定义外部配置文件
hellDao=com.hyc.Dao.Impl.hellloDaoImpl
2、Java程序读取这个配置文件
private static Properties properties; static { properties = new Properties(); try { properties.load(beanfactory.class.getClassLoader().getResourceAsStream("factory.properties")); } catch (IOException e) { e.printStackTrace(); } } public static Object getDao() { String helloDao = properties.getProperty("helloDao"); //利用反射机制创建对象 try { Class clazz = Class.forName(helloDao); Object object = clazz.getConstructor(null).newInstance(); return object; } catch (ClassNotFoundException | NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } }
3、修改service
private helloDao helloDao = (helloDao) beanfactory.getDao();
测试
正常来说 我们访问 hello 会返回 123 (再dao中模拟的数据),这个时候我们需要切换数据源只需要修改外部配置文件即可
此时我们修改外部配置文件
helloDao=com.hyc.Dao.Impl.hellloDaoImpl2
hellloDaoImpl2 中模拟的数据是 7,8,9 ,重启项目
到这里我们就使用外部配置+工厂的方法解决了代码不能切换的问题,
创建出来的对象并非单例
这个问题会出现什么问题呢 重复使用对象的话会出现相同的对象创造很多个无用实例的问题
这里我们就提出用缓存的思路来保证单例,
- 用Map 来存放创建的对象,避免重复创建
- 双重判断保证线程安全
修改工厂生成对象的代码
public static Object getDao(String beanName) { if (!cache.containsKey(beanName)) { synchronized (beanfactory.class) { if (!cache.containsKey(beanName)) { //利用反射机制创建对象 //将对象放入我们的缓存里(map) try { String helloDao = properties.getProperty("helloDao"); Class clazz = Class.forName(helloDao); Object object = clazz.getConstructor(null).newInstance(); cache.put(beanName, object); } catch (ClassNotFoundException | NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } } } return cache.get(beanName); }
测试
public static void main(String[] args) { for (int i = 0; i < 10; i++) { helloDao helloDao = (helloDao) beanfactory.getDao("helloDao"); System.out.println(helloDao); } }
这样我们就实现了对工厂生成的实例复用,而并非每次都重新创建一个
两种方式的区别
private helloDao hellodao = new hellodaoimpl(); private helloDao hellodao = (helloDao) BeanFactory.getDao("helloDao");
- 强依赖/紧耦合 编译之后无法修改 没有拓展性
- 弱依赖/松耦合 编译之后可以通过修改配置文件来变动,让程序具有更好的拓展性
我们自己放弃了创造对象的权限,将创建对象的权限交给了BeanFactory 这种将控制权交给别人或者是对象的思想
就是 : IOC 控制反转
IOC基于注解的执行原理和实现
手写基于注解的Ioc思路
- 自定义 MyAnnotationConfigApplicationContext ,构造器中传入要扫描的包
- 获取包下所有类
- 遍历这些类找到添加了
@Component
注解的类,获取它的class 和对应的beanName 封装成beanDefinition 存入Set集合 这个机会就是IOC自动装载的原材料 - 遍历Set集合 通过反射机制创建对象 同时检测属性有没有添加 @ Value 注解 如果有给属性赋值,再将这些动态创建的对象以K-V的形式 放入缓存区
- 提供 getBean 方法 通过 beanName 取出对应的bean即可
根据上述的思路我们实现代码
注解
自动装配注解
@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface Autowired { }
组件注解
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface Component { String value() default ""; }
指定Name注入注解
@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface Qualifer { String value(); }
赋值注解
@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface Value { String value(); }
核心上下文
扫描包中组件类
public class MyTools { public static Set<Class<?>> getClasses(String pack) { // 第一个class类的集合 Set<Class<?>> classes = new LinkedHashSet<Class<?>>(); // 是否循环迭代 boolean recursive = true; // 获取包的名字 并进行替换 String packageName = pack; String packageDirName = packageName.replace('.', '/'); // 定义一个枚举的集合 并进行循环来处理这个目录下的things Enumeration<URL> dirs; try { dirs = Thread.currentThread().getContextClassLoader().getResources(packageDirName); // 循环迭代下去 while (dirs.hasMoreElements()) { // 获取下一个元素 URL url = dirs.nextElement(); // 得到协议的名称 String protocol = url.getProtocol(); // 如果是以文件的形式保存在服务器上 if ("file".equals(protocol)) { // 获取包的物理路径 String filePath = URLDecoder.decode(url.getFile(), "UTF-8"); // 以文件的方式扫描整个包下的文件 并添加到集合中 findClassesInPackageByFile(packageName, filePath, recursive, classes); } else if ("jar".equals(protocol)) { // 如果是jar包文件 // 定义一个JarFile System.out.println("jar类型的扫描"); JarFile jar; try { // 获取jar jar = ((JarURLConnection) url.openConnection()).getJarFile(); // 从此jar包 得到一个枚举类 Enumeration<JarEntry> entries = jar.entries(); findClassesInPackageByJar(packageName, entries, packageDirName, recursive, classes); } catch (IOException e) { // log.error("在扫描用户定义视图时从jar包获取文件出错"); e.printStackTrace(); } } } } catch (IOException e) { e.printStackTrace(); } return classes; } private static void findClassesInPackageByJar(String packageName, Enumeration<JarEntry> entries, String packageDirName, final boolean recursive, Set<Class<?>> classes) { // 同样的进行循环迭代 while (entries.hasMoreElements()) { // 获取jar里的一个实体 可以是目录 和一些jar包里的其他文件 如META-INF等文件 JarEntry entry = entries.nextElement(); String name = entry.getName(); // 如果是以/开头的 if (name.charAt(0) == '/') { // 获取后面的字符串 name = name.substring(1); } // 如果前半部分和定义的包名相同 if (name.startsWith(packageDirName)) { int idx = name.lastIndexOf('/'); // 如果以"/"结尾 是一个包 if (idx != -1) { // 获取包名 把"/"替换成"." packageName = name.substring(0, idx).replace('/', '.'); } // 如果可以迭代下去 并且是一个包 if ((idx != -1) || recursive) { // 如果是一个.class文件 而且不是目录 if (name.endsWith(".class") && !entry.isDirectory()) { // 去掉后面的".class" 获取真正的类名 String className = name.substring(packageName.length() + 1, name.length() - 6); try { // 添加到classes classes.add(Class.forName(packageName + '.' + className)); } catch (ClassNotFoundException e) { // .error("添加用户自定义视图类错误 找不到此类的.class文件"); e.printStackTrace(); } } } } } } private static void findClassesInPackageByFile(String packageName, String packagePath, final boolean recursive, Set<Class<?>> classes) { // 获取此包的目录 建立一个File File dir = new File(packagePath); // 如果不存在或者 也不是目录就直接返回 if (!dir.exists() || !dir.isDirectory()) { // log.warn("用户定义包名 " + packageName + " 下没有任何文件"); return; } // 如果存在 就获取包下的所有文件 包括目录 File[] dirfiles = dir.listFiles(new FileFilter() { // 自定义过滤规则 如果可以循环(包含子目录) 或则是以.class结尾的文件(编译好的java类文件) @Override public boolean accept(File file) { return (recursive && file.isDirectory()) || (file.getName().endsWith(".class")); } }); // 循环所有文件 for (File file : dirfiles) { // 如果是目录 则继续扫描 if (file.isDirectory()) { findClassesInPackageByFile(packageName + "." + file.getName(), file.getAbsolutePath(), recursive, classes); } else { // 如果是java类文件 去掉后面的.class 只留下类名 String className = file.getName().substring(0, file.getName().length() - 6); try { // 添加到集合中去 // classes.add(Class.forName(packageName + '.' + // className)); // 经过回复同学的提醒,这里用forName有一些不好,会触发static方法,没有使用classLoader的load干净 classes.add(Thread.currentThread().getContextClassLoader().loadClass(packageName + '.' + className)); } catch (ClassNotFoundException e) { // log.error("添加用户自定义视图类错误 找不到此类的.class文件"); e.printStackTrace(); } } } } }
容器上下文代码
- findBeanDefinitions 扫描包中符合条件的类
- createObject 根据上个方法获取到的类的元数据信息创造对象,加入缓存
- autowireObject 是否有需要自动注入的对象,判断注解存在决定ByName或者ByType
/** * @projectName: SpringIOC * @package: com.hyc.MySpring * @className: MyAnnotationConfigApplicationContext * @author: 冷环渊 doomwatcher * @description: TODO * @date: 2022/3/24 18:53 * @version: 1.0 */ public class MyAnnotationConfigApplicationContext { private Map<String, Object> ioc = new HashMap<>(); public MyAnnotationConfigApplicationContext(String pack) { //扫描包中的目标类 Set<BeanDefinition> beanDefinitions = findBeanDefinitions(pack); //根据原材料 创建bean createObject(beanDefinitions); // 自动装载 autowireObject(beanDefinitions); } public void autowireObject(Set<BeanDefinition> beanDefinitions) { //获取注入的集合 Iterator<BeanDefinition> iterator = beanDefinitions.iterator(); while (iterator.hasNext()) { //拿到bean 信息 BeanDefinition beanDefinition = iterator.next(); //获取当前需要自动装配的对象 Class clazz = beanDefinition.getBeanClass(); //拿到全部属性 Field[] declaredFields = clazz.getDeclaredFields(); for (Field field : declaredFields) { Autowired annotation = field.getAnnotation(Autowired.class); if (annotation != null) { Qualifer qualifer = field.getAnnotation(Qualifer.class); if (qualifer != null) { try { //byName String beanName = qualifer.value(); Object bean = getBean(beanName); String fieldName = field.getName(); String mothedName = "set" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1); Method method = clazz.getMethod(mothedName, field.getType()); Object object = getBean(beanDefinition.getBeanName()); System.out.println(bean); method.invoke(object, bean); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } else { //byType } } } } } public Object getBean(String beanName) { return ioc.get(beanName); } public void createObject(Set<BeanDefinition> beanDefinitions) { Iterator<BeanDefinition> iterator = beanDefinitions.iterator(); while (iterator.hasNext()) { BeanDefinition beanDefinition = iterator.next(); Class clazz = beanDefinition.getBeanClass(); String beanName = beanDefinition.getBeanName(); try { //创造对象 Object objects = clazz.getConstructor().newInstance(); //完成属性的赋值 Field[] declaredFields = clazz.getDeclaredFields(); for (Field declaredField : declaredFields) { Value valueAnnotation = declaredField.getAnnotation(Value.class); if (valueAnnotation != null) { String value = valueAnnotation.value(); String fieldName = declaredField.getName(); String methodName = "set" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1); Method method = clazz.getMethod(methodName, declaredField.getType()); //数据类型转换 Object val = null; //可能会有很多类型 篇幅过长 switch (declaredField.getType().getName()) { case "java.lang.Integer": val = Integer.parseInt(value); break; case "java.lang.String": val = value; break; case "java.lang.Float": val = Float.parseFloat(value); break; } method.invoke(objects, val); } } ioc.put(beanName, objects); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } } } public Set<BeanDefinition> findBeanDefinitions(String pack) { // 1 获取包下的所有类 Set<Class<?>> classes = MyTools.getClasses(pack); Iterator<Class<?>> iterator = classes.iterator(); Set<BeanDefinition> BeanDefinitions = new HashSet<>(); while (iterator.hasNext()) { // 2,遍历这些类 找到添加了注解的类 Class<?> clazz = iterator.next(); Component annotation = clazz.getAnnotation(Component.class); if (annotation != null) { // 获取Compoent的值 String beanName = annotation.value(); if ("".equals(beanName)) { // 获取类名首字母小写、 String className = clazz.getSimpleName(); beanName = className.substring(0, 1).toLowerCase() + className.substring(1, className.length()); } BeanDefinition beanDefinition = new BeanDefinition(beanName, clazz); BeanDefinitions.add(beanDefinition); } } // 3. 将这些类封装成BeanDefinition 装载到集合中 return BeanDefinitions; } }
元数据
@Data @AllArgsConstructor public class BeanDefinition { private String BeanName; private Class BeanClass; }
测试对象
Account
@Component() @Data public class Account { public Account() { } @Value("1") private Integer id; @Value("张三") private String name; @Value("12") private Integer age; @Autowired @Qualifer("myOrder") private Order myorder; }
Orderj
@Component("myOrder") @Data public class Order { @Value("xxx123") private String orderId; @Value("1000.5") private Float price; }
总结
手动实现基于注解Ioc 这篇文章,对于理解spring的装配过程 ,Bean的声明周期 有着非常好的启发作用
手动实现过之后会才会知道为什么要解决这个问题,怎么解决这个问题
得到的成果
- 一个自己编写的IOC容器
- 反射技术的实战
- spring原理的加深理解