一次对Spring IoC作用的讨论

引言

这篇文章是用来记录和反思昨天晚上跟室友(我们俩说实话只是会用,但是不懂)的一次讨论的,所以说,谁知道对不对呢?

  1. 讨论的主题,IOC到底有什么用?
  2. 讨论的方式,因为我稍微读过一点Spring的源码,所以采用的是他问我答的方式,
  3. 讨论的思路,通过Spring帮我们创建对象和普通的new一个对象进行比较,再一点点弥补new的不足,剥离出IoC的某个作用。实际上,想到啥就讲啥,整理清楚了,再返回主线。。。

其中的Aop原理,对单例和原型的理解可能有点用哦, 降耦合这块真的很浅,感觉意义不大。

讨论过程

1. Spring实现了单例

我们一开始讨论基本就确定了Spring的这一作用,它能我们无需添加任何代码或者其他方式就能轻松地实现单例
首先我们先用一个图表示出new的一个弱势
在这里插入图片描述
new 是直接生成一个实例的,会导致本只需要一个实例的Dao2,却生成了两个实例,这里就需要使用的是单例模式。
单例模式实现的关键是,保存住单例的引用并且可以暴露给依赖的对象,而这个要求的实现方式最常见的就是禁止外部调用new方法,仅暴露一个静态方法,它来管理对象的创建和单例的引用。
而Spring采取的方式是用一个注册器来创建所有的单例和保存所有的单例引用。

对new的弥补:对所有只需要提供方法的类改写为单例类,当然通过对象创建者持有注册器的引用来取得依赖对象的单例来实现会更加高效,但是有抄Spring的嫌疑,太类似了,会让我们忽略很多,就不用了。

这时候我们又想到了一个新问题

什么情况下可以单例,什么情况下得用原型?

这是我最近学习Spring的思考重点之一,虽然这次讨论的不多,但还是放在一起吧
总的来说,对象的方法调用时需要保存线程隔离的信息的对象就必须得要使用原型。

让我们看整个SSM项目(我学SpingBoot学不下去,在补Spring,室友也是被我骗来学的)

  1. 我们主要编写的Controller,Service以及DispatcherServlet,其中DispatcherServlet作为一个Servlet,肯定是单例的,从tomcat开启初始化到最后关闭服务器都会一直存在,它主要去调用Controller提供的方法(当然没那么简单,但确实只是需要方法,而不需要其他内容),而Controller调用Service中的方法,这些都是完全只是调用方法,不需要线程隔离的信息,只需要单例就可以了。
  2. 然后就是我们编写的POJO,这些除了框架会用反射生成,基本都是我们自己直接new的,不会让Spring去管理
  3. Mapper(Dao)是我刚才先放着不管的,因为我不懂。。。也不是,Mapper的特点是它本是Mybatis的,需要我们将它和Spring整合的。
    1. 当我们单独使用Mybatis的时候
      SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputstream);
      // 这行代码被多个线程运行的时候,可以产生多个连接
      SqlSession sqlSession = sqlSessionFactory.openSession();
      // 由于多个线程中的sqlSession不同,生成的Mapper也不是一个对象
      Mapper mapper = sqlSession.getMapper(Mapper.class);
      // 这里的mapper和mapper2也不是同一个对象,不是单例的
      Mapper mapper2 = sqlSession.getMapper(Mapper.class);
      
      运行结果:
      System.out.println(mapper)
      org.apache.ibatis.binding.MapperProxy@1060b431
      System.out.println(mapper2)
      org.apache.ibatis.binding.MapperProxy@612679d6
      System.out.println(mapper == mapper2)
      false
      
    2. 当我们使用Spring整合Mybatis的时候
      // 线程1去取得archiveMapper,并打印
      Thread thread1 = new Thread(new Runnable() {
              
              
          @Override
          public void run() {
              
              
              Object archiveMapper = applicationContext.getBean("archiveMapper");
              System.out.println(archiveMapper);
          }
      });
      // 线程2也去取得archiveMapper,并打印
      Thread thread2 = new Thread(new Runnable() {
              
              
          @Override
          public void run() {
              
              
              Object archiveMapper = applicationContext.getBean("archiveMapper");
              System.out.println(archiveMapper);
          }
      });
      thread1.start();
      TimeUnit.SECONDS.sleep(2);
      thread2.start();
      
      运行结果
      org.apache.ibatis.binding.MapperProxy@550b986e
      org.apache.ibatis.binding.MapperProxy@550b986e
      
      我们可以看到,Spring是把Mapper做成单例了,你问我怎么做的啊,我不知道,这貌似跟事务管理和SpringTransactionManaager有关,我还没看,期末了,别到时候都挂了,还是等寒假再学吧

看了下SSM整个项目,其实大部分内容都可以是单例的,但是在Mapper这层出现了分歧,使用原生的Mybatis会产生多例,而Spring却能实现单例,Spring到底做了什么,虽然我还没有看过内部,但是还是可以一定程度下进行猜测,之后我要是验证了,大概会来改的

我们简单地讨论一下可以发现,其实Mybatis中唯一需要线程隔离的其实只是数据库Connection对象,甚至整个Web项目中,对于处理每个请求的线程需要线程隔离的也就Connection罢了,其他对象(一般也就是POJO)往往仅会在方法中被直接引用,是天然线程隔离的。所以重点就是Connection对象。

Connection虽然确实可以在一个方法中取得并释放,保证线程隔离,但是在一些比较复杂的业务中,需要操作多个表并保证事务性,也就需要多个Mapper操作同一个connection,如果让mapper直接持有connection,要保证connection线程隔离,就无法保证mapper的单例。如果让service方法直接持有connection并操作,并传入给mapper,首先会导致大量重复代码,提高耦合,而且无法使用service方法之间调用。总的来说,事务管理必须要低侵入性,我们需要找到一个合适的方式去线程隔离地保存住connection

Mybatis的sqlsession本质也就是对Connection的封装,稍微看一下源码就可以看到,

  1. SqlSession封装了Executor,依靠executor来执行也写sql操作
  2. Executor封装了Transaction,为executor提供了事务支持和连接
  3. Transaction封装了Connection,根据connection的commit和rollback实现了事务的接口

我们使用原生的mybatis,是直接把connection(sqlSession)注入到mapper中,mapper对象本质上是我们编写的Mapper接口实现的动态代理对象在这里插入图片描述
而Spring中,它不再需要我们手动注入sqlsession,而是使用的是sqlSessionProxy
在这里插入图片描述
找到MapperFactoryBean和SqlSessionTemplate,稍微翻一下源码,可以看到,sqlSessionProxy是由TransactionSynchronizationManager.getResource(sessionFactory提供的),再看TransactionSynchronizationManager,第一眼就可以看到ThreadLocal,懂得都懂好吧,来了,是线程隔离,依靠ThreadLocal来保存connection,用于事务管理等,也能保证单例的mapper可以使用线程隔离的connection

其实我感觉我还是蛮聪明的哈,在翻Mybatis源码之前,就有尝试过用ThreadLocal来保存connection,来保证能用单例的dao持有线程隔离的connection。不开玩笑了,大概就是这样,其实就我个人感觉啊,将可以单例的和只能多例的部分进行拆分,是很多设计要用到的。

2. IoC 起到了降耦合的作用

前言:这是我们讨论的重头戏,毕竟我们也不是很懂耦合。。。我们判断依据就是,一旦一个类需要发生改变(如某个接口类属性要换一个实现类),那么其他类要改的越多那么耦合就越高。
我们不懂的一个大问题就是,我们写一个SSM项目,根本就不会有多个实现,直接用@Autowire就完事了,Autowire是根据类来自动注入依赖的,当有两个实现类时候就会出现问题,需要我们去设置id来保证一对一的匹配

先举个栗子
在这里插入图片描述
场景:有三个Controller类,都有一个接口ServiceA的属性,我们起初全部都使用的是ServiceAImpl1,但是由于业务改变,我们不再使用实现1,而使用实现2 ServiceAImpl2

解释下,我为什么举了这个例子,而且后面的都只用了这个栗子来证明降耦合。首先,我不懂耦合,我也不知道该从哪些方面说;其次,就比较合理的设计中,应该是少修改,只新增,这个例子是比较合理的

让我们来看看,使用new(现在已经加强为单例,但是被我们控制变量法了,它在这块还是原来的new,指需要具体的类名)和Spring的区别吧:

new

这里有两种解决方式

  1. 直接把实现1的代码全删了,换成实现2的代码换上去,这河里吗?这不合理,除非确保我们再也不会使用实现1,否则直接更换代码肯定是不合理的
  2. 找出Controller中每一个serviceA = new ServiceAImpl1() 改成 serviceA = new ServiceAImpl2(),这个相对合理一点,但是代价就是你得找出所有用了ServiceAImpl1的Controller,再去修改,项目一大,这就会变成一件很恶心人的事

spring

首先我们得分成xml依赖注入和注解依赖注入

让我们先考虑xml
在这里插入图片描述
xml的配置方式绝对是理论上耦合度最低的,它全权负责了所有的依赖注入,所有依赖的改变都只需要修改这一个文件,而不需要去关心和修改java代码。但为什么SpringBoot中就只用纯注解,而我们一般也用注解来注入依赖呢?

  1. 首先,你那么多依赖注入写起来不累么
  2. 确实只需要修改xml这一个文件,但是随着依赖增多,文件变大,对其的修改也会变得更麻烦,也有点耦合了的味道

但是我室友非常钟意xml这个完全的降耦合的优点,并且因为注解配置法需要耦合到java代码中,而对注解配置法的降耦合表示了怀疑,不过被我一定程度上地说服了。

注解配置我们使用的是@Autowire,但是如果有两个实现类的时候就必须再使用@Qualified(没用@Primary,这个可能更方便)来给字段添加上要匹配的beanname。通过这个,我们可以通过给新的实现类原来的beanname,就可以不去修改每个Controller了。下面的图中,可以简单地切换实现。

在这里插入图片描述
总的来说,spring通过多加了一层beanname,或者依靠类,可以省去在代码中省去了直接注入完全的类名的强依赖,而是使用依赖比较弱的beanname,或者根据实现或继承关系来注入。

对new的弥补:降耦合的方式太多,性质也不一样,不好找,也是我们最不懂的

3. IoC 给出了很多实例化期间的额外操作

首先,IoC能为所有的对象提供统一的额外操作,有一个大前提,那就是Spring完全接管所有被管理对象的实例化工作,此外,为了能在统一操作的情况下对各个bean进行特殊化操作,还将所有要管理的对象的信息和配置抽象成了BeanDefinition。

然后,让我们看一下spring中bean的生命周期:
在这里插入图片描述
在这里插入图片描述
这张图来自:http://www.cnblogs.com/zrtqsk/p/3735273.html
这张图对我Spring源码的学习提供了很大的帮助,总之谢谢这位老哥整理的bean生命周期。

这里所有注册入BeanPostProcessor的都会在bean对应的生命周期中按一定顺序被调用,如果好奇具体是如何被调用的,大家可以从applicationContext.getBean(beanName)这步为起点,一点点往深处看Spring的源码哦。然后再从new AnnotationConfigApplicationContext()往深处看,基本就能知道spring在创建对象时为我们做了什么。

我再稍微介绍一下BeanPostProcessor,它主要用来在bean被实例化后初始化前后的拦截操作,一般会把实例传入,强化后的实例返回

//BeanPostProcessor.java
public interface BeanPostProcessor{
    
    
	default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
    
    
		return bean;
	}

	default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
    
    
		return bean;
	}
}

它还有子类InstantiationAwareBeanPostProcessor,DestructionAwareBeanPostProcessor,相对来说没那么重要。
而BeanFactoryPostProcessor不是其子类,是给BeanFactory用的,是个很牛批的接口,spring容器的初始化基本靠它的实现类完成的。

我们的项目中(但愿不是只是因为我菜),一般是不会编写BeanPostProcessor,但是它地位很高,举个例子就是,spring的aop就是靠它来实现的。
我最近扫了一遍spring aop的源码,于是拿来跟室友装杯了,算是体现一下BeanPostProcessor有多厉害吧。

Spring Aop 的一些实现原理和如何与IoC配合使用

开讲之前,还是先说一句,虽然Aop的核心是动态代理,但是我的重点是Aop如何与IoC合作,所以我不会多扯动态代理的事,动态代理的技术也选择大家比较熟悉的JDK原生的动态代理,这里我跟室友吹*的时候也大概是这样讲的。

首先,我们先让无敌的the world帮我们去找下我们的对象啥时候被代理了,不过要注意得去找我们自己写的带@Aspect的类,如果是其他的类被代理可能不是我们要的。

先AbstractApplicationContext的refresh()方法中的每个步骤打上断点,refresh方法是启动spring容器的关键,这时候会做很多事情,调用后基本就表示所有初始化工作都完毕了,我们要用的单例全部生成完毕,原型也会有它对应的工厂被创建。
显然会在finishBeanFactoryInitialization(beanFactory)这一步中,看到我们自己写的单例在此时被生成,往里面一看,发现其实就是在调用getBean方法,这边有一些比较深的递归,但是比较容易看懂,慢慢调试,总能找到的。

让我给出一下这个方法栈

getBean(beanName);				// AbstractBeanFactory
doGetBean(beanName);			// AbstractBeanFactory
getSingleton(beanName, ObjectFactory);	// AbstractBeanFactory, 这段代码使用匿名内部类的,可能比较难看懂,我在下面解释
createBean(beanName, mbd, args);		// AbstractAutowireCapableBeanFactory
doCreateBean(beanName, mbd, args);		// AbstractAutowireCapableBeanFactory
initializeBean(beanName, exposedObject, mbd)	// AbstractAutowireCapableBeanFactory
applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName)	// AbstractAutowireCapableBeanFactory
// 源码
sharedInstance = getSingleton(beanName, () -> {
    
    
						try {
    
    
							return createBean(beanName, mbd, args);
						}
						catch (BeansException ex) {
    
    
							destroySingleton(beanName);
							throw ex;
						}
					});
// 解释
getSingleton(String beanName, ObjectFactory<T> objectFactory)		// DefaultSingletonBeanRegistry
这里用了一个lambda实现的匿名对象,其本质是一个ObjectFactory,其getObject()实现方式是调用AbstractBeanFactory的createBean()
createBean()是一个抽象方法,具体实现在AbstractAutowireCapableBeanFactory

在这里插入图片描述
我们成功在applyBeanPostProcessorsAfterInitialization方法中找到了Aop的入口,其具体的类就是AnnotationAwareAspectJAutoProxyCreator

public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
    
    
	if (bean != null) {
    
    
		Object cacheKey = getCacheKey(bean.getClass(), beanName);
		if (this.earlyProxyReferences.remove(cacheKey) != bean) {
    
    
			return wrapIfNecessary(bean, beanName, cacheKey);
		}
	}
	return bean;
}

这是它在IoC中被调用的方法,实现在其父类的AbstractAutoProxyCreator,关键是调用wrapIfNecessary(),这里面的东西看起来稍微有点累,但是逻辑蛮简单的,我这下不多讲源码了。

我们要思考一些东西:
1. 我们如何配置动态代理,提供了哪些信息
2. 我们如何调用代理对象的方法,提供了哪些信息
3. 12中的那些信息是静态的,哪些是动态的
4. 以jdk为例,其加强方法如何被调用
5. aop创建代理的过程
6. 哪些内容依赖IoC

  1. 我们如何配置动态代理,提供了哪些信息

首先,让我们思考下,我们注解配置一个Aspect时提供了哪些信息

@Aspect
@Component
public class MyAspect {
    
    

   @Pointcut("execution(public * com.wisecoach.service.MyServiceImpl.*(..))")
   public void aspect(){
    
    }

   @Around("aspect()")
   public Object around(ProceedingJoinPoint point){
    
    
       Object[] args = point.getArgs();
       try {
    
    
           Object obj = point.proceed(args);
           System.out.println(obj);
           return obj;
       } catch (Throwable throwable) {
    
    
           throwable.printStackTrace();
       }
       return null;
   }
}

我们关注的重点应该是,我们可以从一个Aspect类中提取出多个用于匹配的pointcut和它对应的加强方法around组成的<Expression, MethodInterceptor>对,Expression将决定该方法是否使用加强方法MethodInterceptor

  1. 我们如何调用代理对象的方法,提供了哪些信息

然后我们再看我们如何调用被代理对象的方法

service.func(args);

这样很简单地调用后,我们可以看到它提供了被代理对象service,被调用方法func(),方法参数args

  1. 12中的那些信息是静态的,哪些是动态的

我们前两步提供的信息中,以调用一次被代理方法为例

  • 静态信息:所有的<Expression, MethodInterceptor>
  • 动态信息:被代理对象service,被调用方法func(),方法参数args

就像我之前说的,很多设计中,会将可以单例的和只能原型的区分开来,静态信息可以单例,动态信息只能原型,这里也会有两个类去封装这两类信息

  • MethodInterceptor:它封装了加强方法,其方法Object invoke(MethodInvocation invocation)用于调用加强方法和需要被加强的方法
  • ProxyMethodInvocation:它封装了被加强方法,被调用对象,方法参数,提供方法Object proceed()用来调用
  1. 以jdk为例,其加强方法如何被调用

在三的介绍下,我们基本可以猜到它是如何实现的了
以JDK的动态代理为例,其InvocationHandler的invoke方法中,首先必须要先取得MethodInterceptor获得它需要的加强方法,然后再把其三个参数封装入ProxyMethodInvocation,用来给MethodInterceptor调用(其实这是错的)。
但是加强方法往往是多个,怎么处理多个加强方法是个问题。
有个错误的想法是很多脑补AOP实现的同学会犯的:AOP通过递归地代理一个对象来实现能有多个加强方法
这个错误想法应该不少人有吧,我和我室友都犯了。
它其实是可以实现的,但是它代表着需要创建大量的类文件(cglib会创建类文件,JDK会在1.8以后如果被调用超过一定次数也会生成操作字节码)

为什么我们就不能把所有方法放一个数组里面挨个调用呢?完全是可以的,只是它的挨个指的是挨个递归调用
由于是递归调用,我们需要一个额外空间去保存我们当前递归的进度,一种是作为方法的参数传递,但是不合适;二是作为寻找一个对象去保存这个值,由于方法调用是异步的,我们这个对象还必须是线程隔离的。
我们自然而然地找到了这里唯一保存了动态信息的ProxyMethodInvocation,同时,也把所有加强方法也传入给它。
这个ProxyMethodInvocation是ReflectiveMethodInvocation,它的设计真的很漂亮,完美区分了动态和静态,区分了需要线程隔离的和不需要的,可以单例的和只能原型的。

public class ReflectiveMethodInvocation implements ProxyMethodInvocation, Cloneable {
    
    
	// 代理对象
	protected final Object proxy;
	// 被代理对象
    protected final Object target;
    // 被调用的方法
    protected final Method method;
    // 方法的参数
    protected Object[] arguments;
    // 刚刚我们的说的<Expression, MethodInterceptor>对的列表
    protected final List<?> interceptorsAndDynamicMethodMatchers;
    // 当前递归进度
    private int currentInterceptorIndex = -1;
    
	public Object proceed() throws Throwable {
    
    
		// 如果已经递归到最深处
        if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
    
    
            return this.invokeJoinpoint();		//就是在调用被代理对象对应的方法
        } else {
    
    
        	// 取出当前递归层数的<Expression, MethodInterceptor>对
            Object interceptorOrInterceptionAdvice = this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
            if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
    
    
                InterceptorAndDynamicMethodMatcher dm = (InterceptorAndDynamicMethodMatcher)interceptorOrInterceptionAdvice;
                Class<?> targetClass = this.targetClass != null ? this.targetClass : this.method.getDeclaringClass();
                // 如果匹配Expression,那就调用它的加强方法,被加强的方法就是这个proceed方法,这是最漂亮的地方,不理解多看几遍,如果不匹配,就直接递归
                return dm.methodMatcher.matches(this.method, targetClass, this.arguments) ? dm.interceptor.invoke(this) : this.proceed();
            } else {
    
    
                return ((MethodInterceptor)interceptorOrInterceptionAdvice).invoke(this);
            }
        }
    }
}

回头去看InvocationHandler作为一个单例,它的invoke方法要做的事就是取出所有的Matcher(即<Expression, MethodInterceptor>),然后将参数,方法等动态的信息和Matcher一起注入到ReflectiveMethodInvocation并调用。

5.aop创建代理的过程

  1. 它的入口是getBean时创建完实例,初始化后调用postProcessAfterInitialization()
  2. 从IOC容器中找出所有带有@Aspect注解的Bean,从中解析出所有的Matcher,并放入缓存
  3. 使用JDK或者CGLib为传入的实例创建动态代理

就这三步,蛮简单的

6.哪些内容依赖IoC

  • 它需要从IOC容器中找出Matcher
  • 它的入口是由IoC来调用的

总结

那个,谁还记得我们在将IoC。。。
我们的重点是去理解IoC是如何提供加强创建实例的接口的,它的关键是统一管理所有被管理对象的创建,从而能统一地进行加强。

最后的总结

我对IoC的理解还不够,大家看看算了。。。
我最大的收获还是对单例和原型的理解,我个人对框架设计还是挺感兴趣的

写完后的后话

看到这的人还记得这是个讨论记录不。。。
这次的讨论对我帮助真挺大的,一是重新整体地去考虑Spring,二是回顾最近学的Spring源码,算是一种举一反三吧
这里面蛮多东西还停留在猜测之类的,有些我成功通过看源码证实了,我个人感觉还是不错的。
不过这终究只是个记录,稍微再夹带点私活,我们俩的思维也很扩散,虽然最后还是能回到主题,但是大家看起来应该还是挺累的,还是那句,就图个乐吧,能涨见识最好。

猜你喜欢

转载自blog.csdn.net/weixin_44815852/article/details/111877354