[TOC]

框架中的代理模式

什么是代理模式?

代理模式: 为其他对象提供一种代理以控制对这个对象的访问。可以理解成中介、黄牛等

解决的问题:

在直接访问对象时带来的问题,比如说,要访问的对象在远程的机器上。在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层。说白了就是在你带卖钱插一段后面插一段

Java动态代理实现方式:

  1. JDK自带的动态代理
  2. Cglib动态代理
    1. JDK自带的动态代理

    以黄牛为例,黄牛刚了解到该人需求,该人将信息(要某某票三张)给予黄牛,黄牛给票。黄牛就是该买票人的代理。

1.1 People.java

接口

  1. public interface people {
  2. /**
  3. * 交谈
  4. */
  5. void speak();
  6. }

这个接口很简单,这就是一个讲话的功能,在接下来的HuangNiu这个类中,Proxy.newProxyInstance这个方法实现需要接口

1.2 HuangNiu.java

黄牛代理类,获取到People信息后调用Proxy来生成一个新的代理类,它必须实现InvocationHandler接口,这个接口中使得它可以通过invoke方法实现对真实角色(People)的代理访问。

  1. public class HuangNiu implements InvocationHandler {
  2. private People people;
  3. /**
  4. * 获取被代理对象信息
  5. */
  6. public Object getInstance(People people) {
  7. this.people = people;
  8. Class clazz = people.getClass();
  9. System.out.println("没生成代理之前的对象:" + clazz);
  10. return Proxy.newProxyInstance(clazz.getClassLoader(),clazz.getInterface(),this);
  11. }
  12. @Override
  13. public Object invoke(Object proxy,Method method,Object[] args) throw Throwable {
  14. System.out.println("代理中....");
  15. method.invoke(people);
  16. System.out.println("代理处理完毕,OK请查收");
  17. return null;
  18. }
  19. }

在实例化HuangNiu这个对象的时候,我们调用了Proxy的newProxyInstance方法:

  1. return Proxy.newProxyInstance(clazz.getClassLoader(),clazz.getInterface(),this);

其中clazz是People的class对象。再来看看newProxyInstance的源码实现:

  1. public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h) throws IllegalArgumentException {
  2. Objects.requireNonNull(h);
  3. final Class<?>[] intfs = interfaces.clone();
  4. final SecurityManager sm = System.getSecurityManager();
  5. if (sm != null) {
  6. checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
  7. }
  8. /*
  9. * Look up or generate the designated proxy class.
  10. */
  11. Class<?> cl = getProxyClass0(loader, intfs);
  12. /*
  13. * Invoke its constructor with the designated invocation handler.
  14. */
  15. try {
  16. if (sm != null) {
  17. checkNewProxyPermission(Reflection.getCallerClass(), cl);
  18. }
  19. //获取代理类的构造函数对象
  20. //参数constructorParams为常量值:private static final Class<?>[] constructorParams = { InvocationHandler.class };
  21. final Constructor<?> cons = cl.getConstructor(constructorParams);
  22. final InvocationHandler ih = h;
  23. if (!Modifier.isPublic(cl.getModifiers())) {
  24. AccessController.doPrivileged(new PrivilegedAction<Void>() {
  25. public Void run() {
  26. cons.setAccessible(true);
  27. return null;
  28. }
  29. });
  30. }
  31. //根据代理类的构造函数对象来创建代理类对象
  32. return cons.newInstance(new Object[]{h});
  33. } catch (IllegalAccessException|InstantiationException e) {
  34. throw new InternalError(e.toString(), e);
  35. } catch (InvocationTargetException e) {
  36. Throwable t = e.getCause();
  37. if (t instanceof RuntimeException) {
  38. throw (RuntimeException) t;
  39. } else {
  40. throw new InternalError(t.toString(), t);
  41. }
  42. } catch (NoSuchMethodException e) {
  43. throw new InternalError(e.toString(), e);
  44. }
  45. }

getProxyClass0方法源码:

  1. /**
  2. * 生成一个代理类。必须在调用此方法之前调用checkProxyAccess方法来执行权限检查
  3. */
  4. private static Class<?> getProxyClass0(ClassLoader loader,Class<?>... interfaces) {
  5. if (interfaces.length > 65535) {
  6. throw new IllegalArgumentException("interface limit exceeded");
  7. }
  8. // If the proxy class defined by the given loader implementing (如果代理类由给定加载器定义实现)
  9. // the given interfaces exists, this will simply return the cached copy; (给定的接口存在,将简单的返回缓存的副本)
  10. // otherwise, it will create the proxy class via the ProxyClassFactory (否则,他将通过ProxyClassFactory创建代理类)
  11. return proxyClassCache.get(loader, interfaces);
  12. }

如果缓存中有该代理类,则取缓存,如果没有,则通过ProxyClassFactory来创建代理类。可以了解到JDK的动态代理实现是依据接口来重新生成一个新的代理类。

什么是新的代理类?

通俗的讲就是综合和前后代理逻辑并重新生成一份.class文件来实现动态代理的类。

1.3 Me.java

被代理对象,实现people接口,给代理类提供需要的信息来实现被代理。

  1. public class Me implements People {
  2. private String name;
  3. private String title;
  4. public Me(String name, String title) {
  5. this.name = name;
  6. this.title = title;
  7. }
  8. @Override
  9. public void speak() {
  10. System.out.println("我叫"+name+", 我要一张"+title);
  11. }
  12. }

2. Cglib实现动态代理

Cglib动态代理的实现原理和jdk基本一样,但是也有不同点。

  • 不同点
    • jdk动态代理生成的代理类是继承自Proxy,实现你的被代理类所实现的接口,要求必须有接口;
    • cglib动态代理生成的代理类是被代理者子类,并且会重写父类的所有方法,要求该父类必须有空的构造方法,否则会报错:Superclass has no null constructors but no arguments were given,还有,private和final修饰的方法不会被子类重写;
  • 相同点
    • 都是生成了新的代理类(字节码重组)、

Bean加载原理

加载过程:通过ResourceLoader和其子类DefaultResourceLoader完成资源文件位置定位,实现从类路径,文件系统,url等方式定位功能,完成定位后得到Resource对象,再交给BeanDefinitionReader,它再委托给BeanDefinitionParserDelegate完成bean的解析并得到BeanDefinition对象,然后通过registerBeanDefinition方式进行注册,IOC容器内部维护了一个HashMap来保存该BeanDefinition对象,Spring中的BeanDefinition其实就是我们用的JavaBean。

什么是BeanDefinition对象

BeanDefinition是一个接口,描述了一个bean实例,它具有属性值,构造函数参数值以及具体实现提供更多信息。

bean.xml

一个普通的bean配置文件,这里是要强调它里面内容的格式,因为解析标签的时候会用到。它有<beans>``<bean>``<import>``<alias>等标签,下文会对他们进行解析并翻译成BeanDefinition对象。

  1. <beans>
  2. <!-- this definition could be inside one beanRefFactory.xml file -->
  3. <bean id="a.qualified.name.of.some.sort"
  4. class="org.springframework.context.support.ClassPathXmlApplicationContext">
  5. <property name="configLocation" value="org/springframework/web/context/beans1.xml"/>
  6. </bean>
  7. <!-- while the following two could be inside another, also on the classpath,
  8. perhaps coming from another component jar -->
  9. <bean id="another.qualified.name"
  10. class="org.springframework.context.support.ClassPathXmlApplicationContext">
  11. <property name="configLocation" value="org/springframework/web/context/beans1.xml"/>
  12. <property name="parent" ref="a.qualified.name.of.some.sort"/>
  13. </bean>
  14. <alias name="another.qualified.name" alias="a.qualified.name.which.is.an.alias"/>
  15. </beans>

ResourceLoader.java

加载资源的策略接口(策略模式)。DefaultResourceLoader is a standalone implementation that is usable outside an ApplicationContext, also used by ResourceEditor

An ApplicationContext is required to provide this functionality, plus extended ResourcePatternResolver support.

  1. public interface ResourceLoader {
  2. /** Pseudo URL prefix for loading from the class path: "classpath:". */
  3. String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX;
  4. /**
  5. * Return a Resource handle for the specified resource location.
  6. * <p>The handle should always be a reusable resource descriptor,
  7. * allowing for multiple {@link Resource#getInputStream()} calls.
  8. * <p><ul>
  9. * <li>Must support fully qualified URLs, e.g. "file:C:/test.dat".
  10. * <li>Must support classpath pseudo-URLs, e.g. "classpath:test.dat".
  11. * <li>Should support relative file paths, e.g. "WEB-INF/test.dat".
  12. * (This will be implementation-specific, typically provided by an
  13. * ApplicationContext implementation.)
  14. * </ul>
  15. * <p>Note that a Resource handle does not imply an existing resource;
  16. * you need to invoke {@link Resource#exists} to check for existence.
  17. * @param location the resource location
  18. * @return a corresponding Resource handle (never {@code null})
  19. * @see #CLASSPATH_URL_PREFIX
  20. * @see Resource#exists()
  21. * @see Resource#getInputStream()
  22. */
  23. Resource getResource(String location);
  24. /**
  25. * Expose the ClassLoader used by this ResourceLoader.
  26. * <p>Clients which need to access the ClassLoader directly can do so
  27. * in a uniform manner with the ResourceLoader, rather than relying
  28. * on the thread context ClassLoader.
  29. * @return the ClassLoader
  30. * (only {@code null} if even the system ClassLoader isn't accessible)
  31. * @see org.springframework.util.ClassUtils#getDefaultClassLoader()
  32. * @see org.springframework.util.ClassUtils#forName(String, ClassLoader)
  33. */
  34. @Nullable
  35. ClassLoader getClassLoader();
  36. }

然后看一下DefaultResourceLoader对于getResource()方法的实现。

  1. @Override
  2. public Resource getResource(String location) {
  3. Assert.notNull(location, "Location must not be null");
  4. for (ProtocolResolver protocolResolver : getProtocolResolvers()) {
  5. Resource resource = protocolResolver.resolve(location, this);
  6. if (resource != null) {
  7. return resource;
  8. }
  9. }
  10. //如果location以/开头
  11. if (location.startsWith("/")) {
  12. return getResourceByPath(location);
  13. }
  14. // 如果location 以classpath: 开头
  15. else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
  16. return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
  17. }
  18. else {
  19. try {
  20. // Try to parse the location as a URL...
  21. URL url = new URL(location);
  22. return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
  23. }
  24. catch (MalformedURLException ex) {
  25. // No URL -> resolve as resource path.
  26. return getResourceByPath(location);
  27. }
  28. }
  29. }

可以看到,它判断了三种情况:/ classpath: url格式匹配,然后调用相应的处理方法,我只分析classpath:,因为这是最常用的。所以看一下ClassPathResource实现:

  1. /**
  2. * Create a new {@code ClassPathResource} for {@code ClassLoader} usage.
  3. * A leading slash will be removed, as the ClassLoader resource access
  4. * methods will not accept it.
  5. * @param path the absolute path within the classpath
  6. * @param classLoader the class loader to load the resource with,
  7. * or {@code null} for the thread context class loader
  8. * @see ClassLoader#getResourceAsStream(String)
  9. */
  10. public ClassPathResource(String path, @Nullable ClassLoader classLoader) {
  11. Assert.notNull(path, "Path must not be null");
  12. String pathToUse = StringUtils.cleanPath(path);
  13. if (pathToUse.startsWith("/")) {
  14. pathToUse = pathToUse.substring(1);
  15. }
  16. this.path = pathToUse;
  17. this.classLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader());
  18. }

看上面代码,意味着你配置静态资源文件路径的时候,不用纠结classpath:后面用不用写/,因为如果写了它会给你过滤掉。

跟踪getResourceByPath(location)方法:

  1. /**
  2. * Return a Resource handle for the resource at the given path.
  3. * <p>The default implementation supports class path locations. This should
  4. * be appropriate for standalone implementations but can be overridden,
  5. * e.g. for implementations targeted at a Servlet container.
  6. * @param path the path to the resource
  7. * @return the corresponding Resource handle
  8. * @see ClassPathResource
  9. * @see org.springframework.context.support.FileSystemXmlApplicationContext#getResourceByPath
  10. * @see org.springframework.web.context.support.XmlWebApplicationContext#getResourceByPath
  11. */
  12. protected Resource getResourceByPath(String path) {
  13. return new ClassPathContextResource(path, getClassLoader());
  14. }
  15. /**
  16. * ClassPathResource that explicitly expresses a context-relative path
  17. * through implementing the ContextResource interface.
  18. */
  19. protected static class ClassPathContextResource extends ClassPathResource implements ContextResource {
  20. public ClassPathContextResource(String path, @Nullable ClassLoader classLoader) {
  21. super(path, classLoader);
  22. }
  23. @Override
  24. public String getPathWithinContext() {
  25. return getPath();
  26. }
  27. @Override
  28. public Resource createRelative(String relativePath) {
  29. String pathToUse = StringUtils.applyRelativePath(getPath(), relativePath);
  30. return new ClassPathContextResource(pathToUse, getClassLoader());
  31. }
  32. }

上面getResourceByPath方法中实例化了对象ClassPathContextResource,则表明getRsourceByPath方法实际上实现内容为

  1. /**
  2. * Create a new {@code ClassPathResource} for {@code ClassLoader} usage.
  3. * A leading slash will be removed, as the ClassLoader resource access
  4. * methods will not accept it.
  5. * @param path the absolute path within the classpath
  6. * @param classLoader the class loader to load the resource with,
  7. * or {@code null} for the thread context class loader
  8. * @see ClassLoader#getResourceAsStream(String)
  9. */
  10. public ClassPathResource(String path, @Nullable ClassLoader classLoader) {
  11. Assert.notNull(path, "Path must not be null");
  12. String pathToUse = StringUtils.cleanPath(path);
  13. if (pathToUse.startsWith("/")) {
  14. pathToUse = pathToUse.substring(1);
  15. }
  16. this.path = pathToUse;
  17. this.classLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader());
  18. }

触发bean加载
在上面提到的使用spring手动加载bean.xml的时候用到:

  1. ApplicationContext applicationContext = new ClassPathXmlApplicationContext("bean.xml");

从ClassPathXmlApplicationContext开始:

ClassPathXmlApplicationContext.java

这个类里只有多个构造方法和一个getConfigResources()方法,构造方法最终都统一调用下面这个构造方法(spring源码经常这样,适配器模式):

  1. public ClassPathXmlApplicationContext(
  2. String[] configLocations, boolean refresh, @Nullable ApplicationContext parent)
  3. throws BeansException {
  4. //动态的确定用哪个加载器去加载配置文件
  5. super(parent);
  6. //告诉读取器 配置文件在哪里,定位加载配置文件
  7. setConfigLocations(configLocations);
  8. //刷新
  9. if (refresh) {
  10. //在创建IOC容器前,如果容器已经存在,则需要把已有的容器摧毁和关闭,以保证refresh
  11. //之后使用的是IOC容器
  12. refresh();
  13. }
  14. }

对上面方法的解析,认为它定义了一个xml加载bean的一个生命周期:

  1. super() 方法完成类加载器的指定;
  2. setConfigLocations(configLocations); 方法对配置文件进行定位和解析,拿到Resource对象;
  3. refresh() 方法对标签进行解析拿到BeanDefition对象,在通过校验后将其注册到IOC容器。

一、IOC

Ioc核心:通过反射机制创建对象,操作对象。Ioc容器会帮助我们创建对象并管理它,当我们需要使用的时候,直接通过Ioc容器获取即可,不用自己去new对象,解决对象之间的耦合问题。

ioc.png

BeanFactory和FactoryBean的区别

BeanFactory是一个工厂类,定义了IOC容器的最基本形式,并提供了IOC容器应遵守的最基本的接口,其职责包括:实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。

一般情况下,Spring通过反射机制实例化Bean,但在某些情况下,实例化Bean过程比较复杂,用户可以通过实现FactoryBean接口定制实例化Bean的逻辑。该接口中只有3个方法,分别为:

  • T getObject():返回创建Bean实例,如果isSingleton()返回true,则会将实例放到Spring容器的单实例化缓存池中。
  • boolean isSingleton():返回由FactoryBean创建的Bean实例的作用域是singleton还是prototype。
  • Class getObjectType():返回由FactoryBean创建的Bean类型。

BeanFactory是个Factory,也就是IOC容器或对象工厂,FactoryBean是个Bean。在Spring中,所有的Bean都是由BeanFactory(也就是IOC容器)来进行管理的。但对FactoryBean而言,这个Bean不是简单的Bean,而是一个能生产或者修饰对象生成的工厂Bean,它的实现与设计模式中的工厂模式和修饰器模式类似。

SpringIOC中有两种Bean:普通Bean和工厂Bean;FactoryBean就是一个工厂Bean。

BeanFactory是SpringIOC的基础容器;ApplicationContext是容器的高级接口,继承自BeanFactory,具备更多的功能。

Bean作用域

Spring框架中针对Bean的管理提供以下几种作用范围:

  • singleton:单例(默认)
  • prototype:原型模式(也叫多例模式)
  • request:每个Http请求都有自己的Bean实例,仅对Web应用有效
  • session:每个Http会话都有自己的Bean实例,仅对Web应用有效
  • application:作用于ServletContext的生命周期,仅对Web应用有效
  • websocket:作用于WebSocket的生命周期,仅对Web应用有效

单例模式下,当创建容器时,对象就被创建了(此处针对非延迟加载的Bean,如果是延迟加载的Bean,则会在首次使用时被创建并放至容器中);只要容器在,对象就一直活着;当容器销毁时,对象也随之被销毁。

多例模式下,当使用对象时,容器负责创建新的对象实例给调用者;只要对象在使用中,就一直活着;如果对象长时间不用,则会被GC回收掉(多例Bean,Spring只负责创建,不负责销毁)。

常用配置注解:

  • @Configuration:表明当前类是一个配置类
  • @ComponentScan:会在指定的扫描路径下找出标识了需要装配的类,并自动装配到Spring容器中。默认就会装配标识了@Controller,@Service,@Repository,@Component注解的类到spring容器中。如果指定多个路径时,需加上@Configuration注解,否则无效。
  • @PropertySource:引入外部属性配置文件,常配合@ConfigurationProperties和@Vlaue使用
  • @Import:引入其它配置类
  • @Value:对变量赋值,可直接赋值,也可使用${}读取配置文件中的信息
  • @ConfigurationProperties:将属性文件中的指定配置项与一个java类绑定
  • @Bean:将方法返回对象加入SpringIOC容器中

RequestContextListener

RequestContextListener实现了ServletRequestListener监听器接口,该监听器监听Http请求事件,Web服务器接收的每一次请求都会通知该监听器。

Spring容器中Bean的request、session作用域的实现,就必须获得Web容器的Http请求事件,以Http请求的事件驱动Bean作用域的控制逻辑。所以如果需要创建resquest或session作用域的Bean,那么就需要向Ioc容器中注入RequestContextListener。

ContextLoaderListener

ContextLoaderListener实现了ServletContextListener监听器接口,该接口负责监听Web容器的启动和关闭事件,借助该类可实现Spring容器的启动和关闭操作由Web容器的启动和关闭事件触发。

Bean对象创建流程

spring-bean-create.png

Spring单例对象的初始化可分为3步(可参考AbstractAutowireCapableBeanFactory中的doCreateBean方法):

  1. 实例化:createBeanInstance(),实际上就是调用对应的构造方法构造对象,此时只是调用了构造方法,属性并没有设置。
  2. 属性填充:populateBean(),完成属性填充
  3. 初始化:initializeBean(),调用后置处理器或者init方法

在doCreateBean方法中完成实例化之后,如果当前是一个单例Bean,并且正在创建过程中同时允许循环依赖,则会通过ObjectFactory提前把自己暴露出去,加入三级缓存中,用来解决循环依赖问题。

后置处理器

  • BeanFactoryPostProcessor:在BeanFactory初始化之后进行一些后置处理,针对整个Bean的工厂,在调用内部的方法时,bean还没有被初始化,此时bean刚被解析成BeanDefinition对象
  • BeanPostProcessor:在Bean对象实例化之后进行一些后置处理,针对具体的Bean

Bean注入方式及循环依赖

常见注入方式:filed注入、构造器注入、setter注入。

在XML配置时代,依赖注入主要有两种方式:构造器注入和set方法注入,在XML中定义Bean的同时通过上述方式完成注入。

在注解时代,推荐使用@Autowired,该注解是Spring提供的注解,用来完成自动装配(依赖注入),默认按照类型注入;如果一个类型有多个Bean的时候,再配合@Qualifier指定名称告诉Spring具体装配哪个对象。也可以使用@Resource(Java提供)完成注入,默认按照名称进行注入,不过该注解在jdk11中已被移除,如需使用,需要单独引入依赖。

构造器注入的好处:

  1. 保证依赖不可变(final修饰的属性)
  2. 保证依赖不为空(省去了检查)
  3. 保证返回调用端的代码是完全初始化的状态
  4. 避免了循环依赖,但解决不了循环依赖

在使用setter方法注入属性时,如果我们提供了有参的构造函数,那么也需要提供一个无参的构造函数,因为使用setter方法注入的时候会调用无参构造函数去实例化对象,而当我们提供有参构造函数时就不会默认生成无参构造函数,所以需要手动提供。

Spring无法解决构造器注入方式下的循环依赖问题,会直接抛异常;对于多例模式的Bean无论是构造器注入还是set方法注入,都会直接报错。

针对单例Bean,Spring通过set方法或者@Autowired解决循环依赖。理论依据是基于Java的引用传递,当获得对象的引用时,对象的属性是可以延后设置的,但是构造器必须是在获取引用之前。其实就是通过提前暴露一个ObjectFactory对象来完成的,简单来说就是类A在调用构造器完成对象实例化之后,在调用其setClassB方法之前就把类A实例化的对象通过ObjectFactory提前暴露到Spring容器中。

在解决循环依赖问题上,Spring使用了“三级缓存”,在DefaultSingletonBeanRegistry类中有针对“三级缓存”的定义,代码如下:

  1. /** Cache of singleton objects: bean name to bean instance. */
  2. // 单例对象的缓存(一级)
  3. private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
  4. /** Cache of singleton factories: bean name to ObjectFactory. */
  5. // 单例对象工厂的缓存(三级)
  6. private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
  7. /** Cache of early singleton objects: bean name to bean instance. */
  8. // 提前曝光的单例对象的缓存(二级)
  9. private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

该类的getSingleton方法在获取单例对象的时候,先从singletonObjects(一级缓存)中尝试获取,如果获取不到并且对象在创建中,则尝试从earlySingletonObjects(二级缓存)中获取,如果还是获取不到并且允许从singletonFactories通过getObject获取,则通过singletonFactory.getObject()(三级缓存)获取。如果获取到了则将singletonObject放入到earlySingletonObjects,也就是将三级缓存提升到二级缓存中。

A在实例化之后,并将自己提前曝光到三级缓存中,然后调用setB()时,先从容器中获取B对象,发现B还没有初始化,则先创建B对象,而在B实例化后调用setA()时,先从一级缓存中找A(肯定没有,因为A还没有初始化完成),再尝试从二级缓存中找(也没有),然后尝试三级缓存,由于A通过ObjectFactory将自己提前曝光了,所以B能够通过ObjectFactory.getObject()拿到A对象(此时A对象还是个半成品),B在拿到A对象后顺利完成初始化,之后将自己放入到一级缓存中。此时返回A中,A此时也拿到了B对象,顺利完成初始化并进入一级缓存。

二、事件监听机制

应用程序事件允许我们发送和接收特定事件,我们可以根据需要处理这些事件。事件用于在松散耦合的组件之间交换信息。由于发布者和订阅者之间没有直接耦合,因此可以在不影响发布者的情况下修改订阅者,反之亦然。在一个事件体系中,有以下几个重要的概念:

  1. 事件源:事件对象的产生者,任何一个事件都有一个来源
  2. 事件监听器注册表:当事件框架或组件收到一个事件后,需要通知所有相关的事件监听器来进行处理,这个时候就需要有个存储监听器的地方,也就是事件监听器注册表
  3. 事件广播器:事件广播器在整个事件机制中扮演一个中介的角色,当事件发布者发布一个事件后,就需要通过广播器来通知所有相关的监听器对该事件进行处理

事件监听机制类似设计模式中的观察者模式。

JDK事件机制

在Java中,通过java.util. EventObject来描述事件,通过java.util. EventListener来描述事件监听器。

  1. // 用户实体
  2. public class User {
  3. private Long id;
  4. public Long getId() {return id;}
  5. public void setId(Long id) {this.id = id;}
  6. public User(Long id) {this.id = id;}
  7. }
  8. // 用户事件
  9. public class UserEvent extends EventObject {
  10. public UserEvent(Object source) {super(source);}
  11. }
  12. // 邮件事件
  13. public class EmailEvent extends UserEvent{
  14. public EmailEvent(Object source) {super(source);}
  15. }
  16. // 用户监听器
  17. public interface UserListener extends EventListener {
  18. // 处理用户注册事件
  19. void onRegister(UserEvent event);
  20. }
  21. // 邮件监听器
  22. public class EmailListener implements UserListener {
  23. @Override
  24. public void onRegister(UserEvent event) {
  25. if (event instanceof EmailEvent){
  26. User user = (User) event.getSource();
  27. System.out.println("给User Id为: " + user.getId() + "的用户发送邮件");
  28. }
  29. }
  30. }
  31. // 用户服务
  32. public class UserService {
  33. // 存放所有监听器
  34. private final List<UserListener> listeners = new ArrayList<>();
  35. // 添加监听器
  36. public void addListener(UserListener userListener){
  37. this.listeners.add(userListener);
  38. }
  39. // 注册用户并发布用户注册事件
  40. public void register(User user){
  41. System.out.println("注册新用户,Id为【" + user.getId() + "】");
  42. publishEvent(new EmailEvent(user));
  43. }
  44. // 广播用户注册事件
  45. private void publishEvent(UserEvent event){
  46. for (UserListener listener : listeners) {
  47. listener.onRegister(event);
  48. }
  49. }
  50. public static void main(String[] args) {
  51. UserService userService = new UserService();
  52. User user = new User(1000L);
  53. userService.addListener(new EmailListener());
  54. userService.register(user);
  55. }
  56. }
  57. // ==输出结果==
  58. // 注册新用户,Id为【1000】
  59. // 给User Id为: 1000的用户发送邮件

Spring事件

在Spring中,事件机制采用观察者模式进行具体实现,设计类图如下:

spring-event.png

  • ApplicationEvent是Spring事件的顶层抽象类,代表事件本身,继承自JDK的EventObject。Spring提供了一些内置事件,如ContextClosedEvent、ContextStartedEvent、ContextRefreshedEvent、ContextStopedEvent等
  • ApplicationListener是Spring事件监听器顶层接口,所有的监听器都实现该接口,继承自JDK的EventListener。Spring提供了一些易扩展的接口,如SmartApplicationListener、GenericApplicationListener
  • ApplicationEventPublisher是Spring的事件发布接口,事件源通过该接口的publishEvent方法发布事件,所有的应用上下文都具备事件发布能力,因为ApplicationContext继承了该接口
  • ApplicationEventMulticaster是Spring事件机制中的事件广播器,它默认提供一个SimpleApplicationEventMulticaster实现,如果用户没有自定义广播器,则使用默认的(初始化逻辑见AbstractApplicationContext的refresh方法)。它通过父类AbstractApplicationEventMulticaster的getApplicationListeners方法从事件注册表中获取事件监听器,并且通过invokeListener方法执行监听器的具体逻辑。
    spring-listener1.png
    默认的广播器是同步调用监听器的执行逻辑,但是可以通过为广播器配置Executor实现监听器的异步执行。

在Spring中通常是ApplicationContext本身担任监听器注册表的角色,在其子类AbstractApplicationContext中就聚合了事件广播器ApplicationEventMulticaster和事件监听器ApplicationListnener,并且提供注册监听器的addApplicationListnener方法。

当一个事件源产生事件时,它通过事件发布器ApplicationEventPublisher发布事件,然后事件广播器ApplicationEventMulticaster会去事件注册表ApplicationContext中找到事件监听器ApplicationListnener,并且逐个执行监听器的onApplicationEvent方法,从而完成事件监听器的逻辑。

SpringBoot事件

SpringApplicationEvent是SpringBoot事件的顶层抽象类,且SpringBoot内置了7个事件:

springboot-event.png

  • ApplicationStartingEvent:SpringBoot启动开始的时候发布的事件
  • ApplicationEnvironmentPreparedEvent:SpringBoot对应Environment准备完毕时发布的事件,此时上下文还没有创建,该事件中可以获取配置信息。通过监听该事件可以修改默认的配置信息,配置中心就是借助该事件完成将远端配置放入应用中
  • ApplicationContextInitializedEvent:当准备好上下文,并且在加载任何Bean之前发布的事件。该事件可以获取上下文信息
  • ApplicationPreparedEvent:上下文创建完成时发布的事件,此时的Bean还没有完全加载完成,只是加载了部分特定Bean。该事件可以获取上下文信息,但是无法获取自定义Bean,应为还没有被加载
  • ApplicationStartedEvent:上下文刷新(完成所有Bean的加载)之后,但是还没有调用任何ApplicationRunner 和CommandLineRunner运行程序之前发布的事件。该事件同样可以获取上下文信息,并且可以获取自定义的Bean
  • ApplicationReadyEvent:完成ApplicationRunner 和CommandLineRunner运行程序调用之后发布的事件,此时的SpringBoot已全部启动完成。该事件同样可以获取上下文信息
  • ApplicationFailedEvent:SpringBoot启动异常时发布的事件。该事件可以获取上下文信息和异常信息,通过该事件可以友好地完成资源的回收

上述7个事件在容器启动的合适阶段进行发布,发布顺序自上而下,分别对应SpringApplicationRunListener的7个方法,源码如下:

  1. // 该接口规定了SpringBoot的生命周期,会在各个生命周期广播响应的事件,调用实际的监听器
  2. public interface SpringApplicationRunListener {
  3. // 发布ApplicationStartingEvent
  4. default void starting() {
  5. }
  6. // 发布ApplicationEnvironmentPreparedEvent
  7. default void environmentPrepared(ConfigurableEnvironment environment) {
  8. }
  9. // 发布ApplicationContextInitializedEvent
  10. default void contextPrepared(ConfigurableApplicationContext context) {
  11. }
  12. // 发布ApplicationPreparedEvent
  13. default void contextLoaded(ConfigurableApplicationContext context) {
  14. }
  15. // 发布ApplicationStartedEvent
  16. default void started(ConfigurableApplicationContext context) {
  17. }
  18. // 发布ApplicationReadyEvent
  19. default void running(ConfigurableApplicationContext context) {
  20. }
  21. // 发布ApplicationFailedEvent
  22. default void failed(ConfigurableApplicationContext context, Throwable exception) {
  23. }
  24. }

上述接口提供了唯一的实现类EventPublishingRunListener,该类聚合了SpringApplication和SimpleApplicationEventMulticaster,前4个事件的发布由广播器完成,后3个委托ApplicationContext完成发布,其实最终都是由广播器负责发布。

同样SpringBoot也内置了好多监听器,例如ConfigFileApplicationListener等等。

监听器使用方式

1、通过SPI机制配置

在resources目录下新建META-INF目录,并创建名为spring.factories的文件,在文件中声明以ApplicationListener接口全路径为键的配置,配置内容为ApplicationListener实现类的全路径,若有多个以逗号间隔,示例如下:

  1. org.springframework.context.ApplicationListener=\
  2. com.lyentech.bdc.tuya.listener.HunterApplicationContextInitializedListener,\
  3. com.lyentech.bdc.tuya.listener.HunterApplicationEnvironmentPreparedListener,\
  4. com.lyentech.bdc.tuya.listener.HunterApplicationFailedListener,\
  5. com.lyentech.bdc.tuya.listener.HunterApplicationPreparedListener,\
  6. com.lyentech.bdc.tuya.listener.HunterApplicationReadyListener,\
  7. com.lyentech.bdc.tuya.listener.HunterApplicationStartedListener,\
  8. com.lyentech.bdc.tuya.listener.HunterApplicationStartingListener

Spring在启动的时候会加载spring.factories中配置的监听器实现类,并在合适的阶段通过发布事件触发相应监听器的逻辑。此方式可以实现针对任何事件(SpringBoot内置7个事件都支持)监听器的成功加载。

2、启动类main方法手动添加

在引用启动类的main方法中先创建一个SpringApplication实例,然后调用addListeners方法添加自定义的监听器实现类,最后调用实例的run方法启动容器,示例如下:

  1. public class TuyaServiceApplication{
  2. public static void main(String[] args) {
  3. // 习惯写法
  4. //SpringApplication.run(TuyaServiceApplication.class, args);
  5. // 新写法:实例化对象、添加监听器、启动
  6. SpringApplication springApplication = new SpringApplication(TuyaServiceApplication.class);
  7. springApplication.addListeners(new HunterApplicationStartingListener());
  8. springApplication.addListeners(new HunterApplicationPreparedListener());
  9. springApplication.run(args);
  10. }
  11. }

这种方式也可以实现针对任何事件(SpringBoot内置7个事件都支持)监听器的成功加载。

3、使用@Component

在监听器实现类定义的时候适用@Component注解注释当前监听器,确保让Spring可以扫描到并完成加载,示例如下:

  1. // 监听ApplicationReadyEvent事件
  2. @Component
  3. public class HunterApplicationReadyListener implements ApplicationListener<ApplicationReadyEvent> {
  4. @Override
  5. public void onApplicationEvent(ApplicationReadyEvent event) {
  6. System.err.println("===execute 【ApplicationReadyEvent】 listener...");
  7. }
  8. }

这种方式只能实现对SpringBoot内置事件的后3个进行监听,因为容器启动在借助@Component加载Bean的时候,前4个事件已经发布了,所以不会生效。

4、使用@Component和@EventListener

这种方式可以减少监听器类的个数,示例如下:

  1. @Component
  2. public class HunterListener {
  3. @EventListener
  4. public void listener1(ApplicationStartingEvent event){
  5. System.err.println("使用@EventListener注册监听器,监听ApplicationStartingEvent事件");
  6. }
  7. @EventListener
  8. public void listener2(ApplicationStartedEvent event){
  9. System.err.println("使用@EventListener注册监听器,监听ApplicationStartedEvent事件");
  10. }
  11. @EventListener
  12. public void listener3(ApplicationReadyEvent event){
  13. System.err.println("使用@EventListener注册监听器,监听ApplicationReadyEvent事件");
  14. }
  15. }

这种方式配置的对前四个事件的监听不会生效,原因同第三种方式。

5、总结

前三种方式的监听器都需要通过实现ApplicationListener接口给出具体定义,如果监听器特别多,就会造成类的数量很大;第四种方式仅需一个类就可以完成多个监听器的定义,但是适用的事件有限。

如果多种方式共存,监听器会被执行多次。

如果需要指定同类事件监听器的执行顺序,可以通过@Order或者实现Ordered接口完成。

监听器默认同步执行,如果需要异步执行,一种方式是为广播器提供Executor,另一种就是在后两种使用方式上结合@Async注解(此时需要通过@EnableAsync开启容器对异步的支持)。

事件与事务

Spring允许将监听器绑定到当前事务执行的某个阶段,类似上述的第四种使用方式,结合@Component和@TransactionalEventListener完成定义,后者注解提供属性可以指定监听器执行的时机:

  • BEFORE_COMMIT:该事件将在事务提交之前进行处理。
  • AFTER_COMMIT:事务成功提交后,将处理该事件。如果事件监听器器仅在当前事务成功时才运行,则可以使用此方法。
  • AFTER_COMPLETION:事务提交或回滚时将处理该事件。例如,我们可以使用它在事务完成后执行清理。
  • AFTER_ROLLBACK:事务回滚后将处理该事件。

总之,事件监听机制在Spring框架中被广泛使用。

三、AOP

AOP本质:在不改变原有业务逻辑的情况下增强横切逻辑,横切逻辑代码往往是权限校验代码、日志代码、事务控制代码、性能监控代码。

相关术语

名词 解释
Pointcut(切入点) 指AOP增强代码要影响的具体方法,通过切入点表达式指定所要增强的方法,借助@Pointcut(“execution(方法签名)”)完成,该注解通常声明在一个空方法上
Joinpoint(连接点) 指定AOP增强代码在指定方法的执行时机,有前置通知(@Before)、后置通知(@After)、正常返回通知(@AfterReturning)、异常返回通知(@AfterThrowing)、环绕通知(@Around)五种类型;这些注解用于方法上,对应具体的增强代码
Advice(增强) 上述5个注解对应的方法就是增强逻辑
Target(目标对象) 指被代理对象
Proxy(代理) 指被AOP织入增强后,产生的代理对象
Weaving(织入) 指把增强应用到目标对象来创建新的代理对象的过程。Spring采用动态代理织入,AspectJ采用编译器织入和类装载期织入
Aspect(切面) 切入点、连接点、增强逻辑的综述,@Aspect表明当前类是一个切面,内部定义各种切入点和连接点及对应的增强代码

上述的概念,最终目的就是为了锁定要在哪个方法的什么时机插入什么样的横切逻辑代码。

Spring实现AOP使用的是动态代理技术。默认情况下,Spring会根据被代理对象是否实现接口来选择使用JDK还是CGLIB。如果被代理对象没有实现任何接口,Spring会选择CGLIB;如果实现了接口,Spring会选择JDK官方的代理技术。

SpringAOP代理对象的创建是在后置处理器的postProcessAfterInitialization方法中完成,具体源代码如下:

  1. // AbstractAutowireCapableBeanFactory类中的initializeBean方法
  2. protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) {
  3. // 1、完成各种Aware接口中方法的调用
  4. if (System.getSecurityManager() != null) {
  5. AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
  6. invokeAwareMethods(beanName, bean);
  7. return null;
  8. }, getAccessControlContext());
  9. }
  10. else {
  11. invokeAwareMethods(beanName, bean);
  12. }
  13. // 2、调用BeanPostProcessor中的postProcessBeforeInitialization方法
  14. Object wrappedBean = bean;
  15. if (mbd == null || !mbd.isSynthetic()) {
  16. wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
  17. }
  18. // 3、调用自定义的初始化方法
  19. try {
  20. invokeInitMethods(beanName, wrappedBean, mbd);
  21. }
  22. catch (Throwable ex) {
  23. throw new BeanCreationException(
  24. (mbd != null ? mbd.getResourceDescription() : null),
  25. beanName, "Invocation of init method failed", ex);
  26. }
  27. // 4、调用BeanPostProcessor中的postProcessAfterInitialization方法
  28. // AOP代理对象的创建就是在这一环节完成
  29. if (mbd == null || !mbd.isSynthetic()) {
  30. wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
  31. }
  32. return wrappedBean;
  33. }
  34. // applyBeanPostProcessorsAfterInitialization方法
  35. public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName) throws BeansException {
  36. Object result = existingBean;
  37. for (BeanPostProcessor processor : getBeanPostProcessors()) {
  38. // 遍历所有BeanPostProcessor执行postProcessAfterInitialization方法
  39. Object current = processor.postProcessAfterInitialization(result, beanName);
  40. if (current == null) {
  41. return result;
  42. }
  43. result = current;
  44. }
  45. return result;
  46. }

AbstractAutoProxyCreator类是一个BeanPostProcessor,其内部的postProcessAfterInitialization方法如下:

  1. public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
  2. if (bean != null) {
  3. Object cacheKey = this.getCacheKey(bean.getClass(), beanName);
  4. if (this.earlyProxyReferences.remove(cacheKey) != bean) {
  5. return this.wrapIfNecessary(bean, beanName, cacheKey);
  6. }
  7. }
  8. return bean;
  9. }
  10. // 1、wrapIfNecessary方法调用内部的createProxy方法
  11. // 2、createProxy方法调用ProxyFactory.getProxy方法
  12. // 3、通过DefaultAopProxyFactory创建具体的AopProxy(JDK动态代理或者CGLIB)
  13. // 4、然后通过具体的AopProxy创建代理对象

四、事务

Spring提供了很好的事务管理机制,主要分为编程式事务和声明式事务:

  • 编程式事务:在代码中手动的管理事务的提交、回滚等操作,代码侵入性比较强,实际开发中不建议
  • 声明式事务:基于AOP进行实现,将具体业务和事务处理部分解耦,代码侵入性很低,实际开发中常用

@Transactional可以作用在接口、类和类方法上;作用于类上时,表示该类中所有public方法都配置了相同的事务属性信息;作用于方法上时,方法的事务会覆盖类上的事务配置信息;不建议在接口上使用该注解。

PlatformTransactionManager是Spring事务管理器的核心接口,该接口提供3个方法:

  1. getTransaction(TransactionDefinition):获取事务信息
  2. commit(TransactionStatus):提交事务
  3. rollback(TransactionStatus):回滚事务

Spring本身并不支持事务实现,只是负责提供标准,应用底层支持什么样的事务,需要提供具体实现类(策略模式的应用)。Spring框架也为我们内置了一些具体策略,如DataSourceTransactionManager(针对Mybatis、Spring的JdbcTemplate);这些具体策略中归根结底是横切逻辑代码,声明式事务要做的就是使用AOP(动态代理)来将事务控制逻辑织入到业务代码

Spring需要在配置类上添加@EnableTransactionManagement开启注解事务的支持。该注解使用@Import引入了TransactionManagementConfigurationSelector类,这个类又向容器中导入了两个类:AutoProxyRegistrar和ProxyTransactioManagementConfiguration,前者又会注册一个InfrastructureAdvisorAutoProxyCreator类型的Bean到容器中,这个类是一个后置处理器。

ProxyTransactioManagementConfiguration是一个配置类,注册了事务增强器(AOP的一种体现),内部又注册了事务属性解析器(TransactionAnnotationParser)和事务拦截器(TransactionInterceptor),事务解析器的作用之一就是解析@Transactional注解,事务拦截器实现了MethodInterceptor接口,在invoke方法中通过invokeWithinTransaction方法对拦截方法进行事务增强。

@Transactional属性信息

  • propagation:表示事务的传播行为,默认为Propagation.REQUIRED。
    • Propagation.REQUIRED:如果当前存在事务,则加入该事务,如果当前不存在事务,则创建一个新的事务。(也就是说如果A方法和B方法都添加了注解,在默认传播模式下,A方法内部调用B方法,会把两个方法的事务合并为一个事务 )
    • Propagation.SUPPORTS:如果当前存在事务,则加入该事务;如果当前不存在事务,则以非事务的方式继续运行。适合查询方法,首先当前方法的执行可以不需要事务的支持;如果被其它有事务的方法调用,那么该传播行为下的此方法就能进入当前事务并洞察当前事务对数据所做的更新。
    • Propagation.MANDATORY:如果当前存在事务,则加入该事务;如果当前不存在事务,则抛出异常。针对自身需要事务支持,但是又不管理事务,也就是自己的执行前提是必须有一个事务存在,此类方法适合该传播行为。
    • Propagation.REQUIRES_NEW:重新创建一个新的事务,如果当前存在事务,暂停当前的事务。(当类A中的 a 方法用默认Propagation.REQUIRED模式,类B中的 b方法加上采用 Propagation.REQUIRES_NEW模式,然后在 a 方法中调用 b方法操作数据库,然而 a方法抛出异常后,b方法并没有进行回滚,因为Propagation.REQUIRES_NEW会暂停 a方法的事务,同样b的回滚也不会影响到a)
    • Propagation.NOT_SUPPORTED:以非事务的方式运行,如果当前存在事务,暂停当前的事务。
    • Propagation.NEVER:以非事务的方式运行,如果当前存在事务,则抛出异常。方法的执行前提是不能存在事务,如果存在则抛异常。
    • Propagation.NESTED :如果当前存在事务,则在嵌套事务内执行。如果没有事务,则创建一个新的事务执行。嵌套事务是外部事务的一部分,只有外部事务结束后它才会被提交,外部事务回滚,它也会被回滚。
  • isolation:表示事务的隔离级别,默认为Isolation.DEFAULT。
    • Isolation.DEFAULT:使用底层数据库默认的隔离级别。下面另外四种隔离级别分别对应数据库的隔离级别
    • Isolation.READ_UNCOMMITTED:读未提交。允许其他事务可以看到该事物未提交的数据,这种隔离级别会产生脏读、不可重复读和幻读;
    • Isolation.READ_COMMITTED:读已提交。该隔离级别允许其他事务只能看到该事务以提交的数据;
    • Isolation.REPEATABLE_READ:可重复读。这种隔离级别可以防止脏读和不可重复读,但可能会出现幻读。保证一个事务不能读取另外一个事务未提交的数据外,还保证了避免不可重复读。
    • Isolation.SERIALIZABLE:序列化。事务被处理为顺序执行,防止脏读、不可重复读和幻读。
  • timeout:事务的超时时间,默认为-1,不限时。如果超过该时间限制但事务还没有完成,则自动回滚事务。
  • readOnly:表示事务是否为只读事务,默认为false。针对读方法可以设置为true。
  • rollbackFor:用于指定能够触发事务回滚的异常类型,可指定多个。默认值是RuntimeException和Error,详见DefaultTransactionAttribute.rollbackOn()方法。
  • noRollbackFor:抛出指定的异常类型,不回滚事务,可以指定多个。

什么是脏读、不可重复读、幻读?

  • 脏读:指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。因为这个数据是没有提交到数据库的数据,那么另外一个事物读到的这个数据是脏数据,依据脏数据做的操作是不正确的。
  • 不可重复读:指在一个事物内,多次读同一个数据。在数据还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中两次读数据之间,由于第二个事务的修改,那么第一个数据两次读到的数据可能是不一样的。这样就发生了在同一个事务内两次读到的数据是不一样的,因此称为不可重复读。
  • 幻读:指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一条数据,那么以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好像发生了幻觉一样。

@Transactional失效场景

  1. 注解应用在非public修饰的方法上。因为Spring AOP在代理时,会间接调用到AbstractFallbackTransactionAttributeSource的computeTransactionAttribute方法,该方法会检查目标方法是否为public,如果不是则不会获取注解的配置信息。
  2. 属性propagation或rollbackFor设置错误。
  3. 同一个类中,没有使用注解的方法A调用使用注解的方法B,当外部调用方法A时,会造成方法B的事务失效。因为只有当事务方法被当前类以外的代码调用时,才会由Spring生成的带对象来管理,否则就直接调用。
  4. 方法内部catch了触发回滚的异常,导致该事务不能正常回滚。
  5. 数据库引擎不支持事务。因为Spring的事务最终还是由数据库的事务来保证的,通过AOP绑定同一个Connection到方法调用链中,以此来保证事务。
  6. 该方法上同时使用了@Async注解,且该方法被外部的事务方法调用,那么这两个事务方法分别在两个事务中而不是同一个事务。

针对上述的第三种情况,做如下分析。因为Spring的声明式事务是基于动态代理实现的,所以当外部类调用目标类的方法A时,代理对象中的方法A中并没有进行事务增强,方法A中调用方法B就是在目标类中的普通调用,所以会造成方法B的事务注解失效,调用链如图中的红色部分;但是当外部类直接调用方法B时,代理对象中针对方法B进行了事务增强,所以可以得到事务的保证,调用链如图中的绿色部分。

spring-tran.png

如果想让方法A在调用方法B时,让方法B的事务生效,有以下两种方式可以实现,其本质都是先获取代理对象,然后在调用事务方法:

  • 在类上加上@EnableAspectJAutoProxy(exposeProxy = true)来暴露AOP的代理对象,同时需要添加spring-boot-starter-aop的依赖。 java public void A(){ ((YourClassName) AopContext.currentProxy()).B(); }

  • 给该类注入ApplicationContext,然后获取代理对象```java @Autowired private ApplicationContext applicationContext;

public void A(){ applicationContext.getBean(YourClassName.class).B(); }

  1. 针对上述的第六种情况,因为事务切面和异步切面都是基于AOP动态代理的,并且异步切面会先于事务切面执行,所以事务注解会失效。即使事务切面先执行,但由于Spring事务管理器依赖的是ThreadLocal,所以开启的异步线程是感知不到外部事务方法增强的事务,因此@Transactinal的作用并没有生效(我们想要将该方法加入之前的事务中)。
  2. <a name="94c63375"></a>
  3. # 五、异步[@Async ](/Async )
  4. <a name="1e079232"></a>
  5. ## 基本原理
  6. Spring容器在启动初始化Bean的时候,会判断类中是否使用了@Async注解,创建切入点和切入点处理器,根据切入点创建代理,在调用@Async注解标注的方法时,会调用代理执行切入点处理器的invoke方法,将方法的执行提交给线程池,实现异步执行。核心也是通过AOP动态代理实现。
  7. 所以当一个类中的普通方法A内部调用该类中使用了@Async注解的方法B时,B时不会异步执行的,原因与事务的失效原理一样,解决方式也同理。
  8. <a name="ec09647d"></a>
  9. ## 使用方法
  10. 基于注解的使用方法:
  11. 1. 在启动类上添加@EnableAsync开启应用程序对异步任务的支持
  12. 2. 配置类中完成异步线程池TaskExecutor的创建
  13. 3. 在需要异步执行的方法上添加@Aysnc注解
  14. TaskExecutor继承自JDKExecutor,只是在Spring框架中进行了重新定义,其实现类与接口层级图如下:
  15. ![spring-async-task.png](https://cdn.nlark.com/yuque/0/2020/png/1617241/1605336095193-7beba36a-ff17-485d-a3c7-aba05407dbd8.png#align=left&display=inline&height=704&margin=%5Bobject%20Object%5D&name=spring-async-task.png&originHeight=704&originWidth=551&size=73858&status=done&style=none&width=551)
  16. 最常用的是ThreadPoolTaskExecutor,其实质是对JUCThreadPoolExecutor的包装。TaskExecutor配置示例代码如下:
  17. ```java
  18. @Bean("asyncServiceExecutor")
  19. public Executor asyncServiceExecutor() {
  20. ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
  21. //配置核心线程数
  22. executor.setCorePoolSize(5);
  23. //配置最大线程数
  24. executor.setMaxPoolSize(20);
  25. //配置队列大小
  26. executor.setQueueCapacity(400);
  27. //配置线程池中的线程的名称前缀
  28. executor.setThreadNamePrefix("headThread-");
  29. // rejection-policy:当pool已经达到max size的时候,如何处理新任务
  30. // CALLER_RUNS:不在新线程中执行任务,而是有调用者所在的线程来执行
  31. executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
  32. //执行初始化
  33. executor.initialize();
  34. return executor;
  35. }

可以针对不同的异步任务建立不同的线程池,为不同的线程池指定不同的BeanName,然后在@Async注解上通过设置value可实现将该异步任务提交至对应的异步线程池中。

如果Class1的事务方法A中调用Class2中的异步方法B,正常情况下可能会出现方法A的事务还没有提交方法B就执行了,可以通过注册事务同步器实现当方法A的事务提交后再执行异步方法B,修改A中对B的调用如下:

  1. @Transactional()
  2. public void A(){
  3. // 方法A的具体事务操作
  4. // doSomething......
  5. // 对方法B的调用改为下方写法
  6. TransactionSynchronizationManager.registerSynchronization(
  7. new TransactionSynchronizationAdapter() {
  8. @Override
  9. public void afterCommit() {
  10. // 事务提交完毕时,触发方法B
  11. Class2.B();
  12. }
  13. }
  14. );
  15. }

六、SpringBoot配置

配置文件默认存放位置及读取顺序

SpringBoot配置文件可以使用yml和properties格式,分别的默认命名为:application.yml、application.properties。

配置文件放到以下目录中,可被自动读取到(从ConfigFileApplicationListener类中可以看出):

  • 项目根目录下
  • 项目根目录中config目录下
  • 项目resources目录下
  • 项目resources目录的config目录下

如果在不同目录下存在多个配置文件,读取顺序如下:

  1. config/application.properties(项目根目录中config目录下)
  2. config/application.yml
  3. application.properties(项目根目录下)
  4. application.yml
  5. resources/config/application.properties(项目resources目录中config目录下)
  6. resources/config/application.yml
  7. resources/application.properties(项目的resources目录下)
  8. resources/application.yml

注意:

  • 如果同一个目录下,有application.yml也有application.properties,默认先读取application.properties
  • 如果同一个配置属性,在多个配置文件都配置了,默认使用第1个读取到的,后面读取的不覆盖前面读取到的

配置中心原理

针对Spring项目的配置文件,之前习惯于和代码放置在一起,然后打包部署。当我们仅仅修改配置的时候,就需要重新打包部署,效率很低,因此配置中心应运而生。

简易配置中心落地过程:

  1. 首先开发一个WEB系统,用于管理所有系统的配置信息,并入库保存,同时将配置数据写入ZK集群的某个path下
  2. 提供一个springboot-starter,用于业务系统接入配置中心。starter中通过监听SpringBoot事件,完成配置信息的远端拉取及加载到容器中,下方为实现细节
    • 监听ApplicationStartingEvent事件,完成从ZK集群拉取配置信息,并存到本地
    • 监听ApplicationEnvironmentPreparedEvent事件,将本地配置文件解析后加载到容器,并禁用默认配置
    • 监听ApplicationReadyEvent事件,当容器启动完成,监听ZK指定path,当配置变更的时候通过ZK的Watcher机制触发代码,重启容器即可实现新配置的加载

在处理ApplicationEnvironmentPreparedEvent事件的时候,需要关闭Spring对默认配置文件目录的扫描,以免最终配置信息被污染。具体办法如下:

  1. public class CustomListener implements ApplicationListener<ApplicationEnvironmentPreparedEvent> {
  2. public CustomListener() {
  3. }
  4. @Override
  5. public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
  6. ConfigurableEnvironment environment = event.getEnvironment();
  7. MutablePropertySources propertySources = environment.getPropertySources();
  8. // 存放配置信息来源
  9. ResourcePropertySource propertySource = null;
  10. // 加载本地文件并解析放入propertySource中
  11. // ......
  12. // 向容器中添加配置信息
  13. propertySources.addFirst(propertySource);
  14. // 关闭Spring对默认配置文件目录的扫描
  15. Properties properties = new Properties();
  16. properties.put("spring.config.location", "");
  17. propertySources.addFirst(new PropertiesPropertySource("disableDefaultProperties", properties));
  18. }
  19. }
  20. }

针对最后三行代码如何实现关闭Spring对默认配置文件目录的扫描,下面通过ConfigFileApplicationListener源码进行分析。

ConfigFileApplicationListener是一个监听器,同时也是一个EnvironmentPostProcessor,读取配置文件并将它们添加到引用上下文中。

  1. public class ConfigFileApplicationListener implements EnvironmentPostProcessor, SmartApplicationListener, Ordered {
  2. private static final String DEFAULT_PROPERTIES = "defaultProperties";
  3. // 当spring.config.location缺省配置的情况下,会从以下路径中读取配置文件,优先级从后向前
  4. private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/";
  5. // 配置项,用于指定配置文件的搜索位置
  6. public static final String CONFIG_LOCATION_PROPERTY = "spring.config.location";
  7. // 配置项,用于指定配置文件的名称
  8. public static final String CONFIG_NAME_PROPERTY = "spring.config.name";
  9. // 配置项
  10. public static final String CONFIG_ADDITIONAL_LOCATION_PROPERTY = "spring.config.additional-location";
  11. @Override
  12. public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
  13. // 表明该监听器监听的ApplicationEnvironmentPreparedEvent和ApplicationPreparedEvent事件
  14. return ApplicationEnvironmentPreparedEvent.class.isAssignableFrom(eventType)
  15. || ApplicationPreparedEvent.class.isAssignableFrom(eventType);
  16. }
  17. // 当ApplicationEnvironmentPreparedEvent事件发生时,该类将SpringBoot内置配置的其他EnvironmentPostProcessor和自己放到一起,排序,然后读取配置文件并添加到上下文环境中
  18. private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
  19. List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
  20. postProcessors.add(this);
  21. AnnotationAwareOrderComparator.sort(postProcessors);
  22. for (EnvironmentPostProcessor postProcessor : postProcessors) {
  23. postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
  24. }
  25. }
  26. private class Loader {
  27. // 获取配置文件的搜索位置
  28. private Set<String> getSearchLocations() {
  29. // 如果spring.config.location配置项有值,就从指定位置加载配置文件
  30. if (this.environment.containsProperty(CONFIG_LOCATION_PROPERTY)) {
  31. return getSearchLocations(CONFIG_LOCATION_PROPERTY);
  32. }
  33. // 否则获取spring.config.additional-location配置项指定的位置
  34. Set<String> locations = getSearchLocations(CONFIG_ADDITIONAL_LOCATION_PROPERTY);
  35. // 同时在searchLocations不为空的前提下优先使用其表示的搜索位置,否则使用默认位置
  36. locations.addAll(asResolvedSet(ConfigFileApplicationListener.this.searchLocations, DEFAULT_SEARCH_LOCATIONS));
  37. return locations;
  38. }
  39. // 此方法表明,当spring.config.location配置项有值时,就不会从内置的搜索位置扫描配置文件
  40. }
  41. }

上述方式实现的配置中心主要通过监听容器启动过程中的事件完成容器配置文件的替换及配置变更之后容器的重启,当远程数据库信息变更的时候,容器会重启使新的配置生效,这种方式节约了打包时间(如果只是修改配置而非源代码)。

EnvironmentPostProcessor

该接口是SpringBoot为了支持动态的读取文件留下的扩展接口。该接口允许在refresh上下文之前定制环境信息。该接口实现类的加载通过SPI机制实现,该接口实现类可以通过@Order或者实现Ordered接口指定被调用的顺序。Nacos配置中心和SpringBoot的集成就是通过扩展该接口实现的。

  1. // NacosConfigEnvironmentProcessor源码
  2. public class NacosConfigEnvironmentProcessor implements EnvironmentPostProcessor, Ordered {
  3. @Override
  4. public void postProcessEnvironment(ConfigurableEnvironment environment,
  5. SpringApplication application) {
  6. application.addInitializers(new NacosConfigApplicationContextInitializer(this));
  7. nacosConfigProperties = NacosConfigPropertiesUtils.buildNacosConfigProperties(environment);
  8. if (enable()) {
  9. // 开启预加载,才能触发配置文件自动加载
  10. // 即nacos.config.bootstrap.enable=true、nacos.config.bootstrap.log-enable=true
  11. System.out.println("[Nacos Config Boot] : The preload log configuration is enabled");
  12. loadConfig(environment);
  13. }
  14. }
  15. boolean enable() {
  16. return nacosConfigProperties != null
  17. && nacosConfigProperties.getBootstrap().isLogEnable();
  18. }
  19. private void loadConfig(ConfigurableEnvironment environment) {
  20. NacosConfigUtils configUtils = new NacosConfigUtils(nacosConfigProperties, environment, builder);
  21. // 加载配置
  22. configUtils.loadConfig();
  23. // set defer NacosPropertySource
  24. deferPropertySources.addAll(configUtils.getNacosPropertySources());
  25. }
  26. }
  27. // NacosConfigUtils源码
  28. public class NacosConfigUtils {
  29. public void loadConfig() {
  30. Properties globalProperties = buildGlobalNacosProperties();
  31. MutablePropertySources mutablePropertySources = environment.getPropertySources();
  32. // 加载配置
  33. List<NacosPropertySource> sources = reqGlobalNacosConfig(globalProperties, nacosConfigProperties.getType());
  34. for (NacosConfigProperties.Config config : nacosConfigProperties.getExtConfig()) {
  35. List<NacosPropertySource> elements = reqSubNacosConfig(config, globalProperties, config.getType());
  36. sources.addAll(elements);
  37. }
  38. // 添加到应用上下文中
  39. for (NacosPropertySource propertySource : sources) {
  40. mutablePropertySources.addLast(propertySource);
  41. }
  42. }
  43. }

作为配置中心,Nacos有自己的实现机制,客户端会通过长轮询调用服务端的接口,配合MD5检查远程配置是否有变化,如果发生变化,会拉取最新的配置并替换本地缓存的配置文件。Nacos通过nacos-spring-context项目和Spring进行集成,基于Spring的事件监听机制进行扩展,提供了一系列自己的事件,并在合适的时机通过EventPublishingConfigService和DelegatingEventPublishingListener进行事件的发布,应用程序可以通过监听相应的事件作出对应的调整。

Nacos的客户端工作类ClientWorker在通过LongPollingRunnable进行长轮询时,一旦检测到配置信息CacheData有变化(md5发生变化),就对通知响应的监听器(Nacos在启动的时候根据是否配置自动刷新借助ApplicationContextInitializer和后置处理器添加自己的监听器),CacheData源码如下:

nacos1.png

CacheData最终会调用DelegatingEventPublishingListener的receiveConfigInfo方法将配置信息变更通过回调的方式告诉关注者,并发布事件,源码如下:
nacos2.png
当和Spring集成时,为了实现自动刷新,需要在配置变更时动态替换Spring的Environment中由Nacos负责添加的配置,NacosPropertySourcePostProcessor负责这一动作的完成。
nacos3.png
至此,Nacos集成Spring,并且也可以实现自动刷新,虽然变更了配置信息,但是例如数据库Bean的信息并不会变化(也就是说项目启动时连接的是数据库A,之后变更配置改为B,代码操作的数据库仍旧为A),此时需要监听NacosConfigReceivedEvent事件,并判断数据库相关信息是否有变化,如果发生变化,销毁IOC中的数据库Bean,然后重新使用新的配置信息创建并注册到IOC中(实现上有难度)。

七、WebMvcConfigurationSupport

在SpringBoot2.0后,当我们需要对WebMvc进行一些个性化配置时,例如将Jackson换成Fastjson等,可以通过实现上述接口来类进行扩展。但是会引发一些问题,先看下WebMvc自动配置类WebMvcAutoConfiguration的定义:

webmvc.png

从上图可以看出,只有当WebMvcConfigurationSupport类不存在的时候才会生效WebMvc自动化配置。所以当我们在实现上述接口的同时,需要注意WebMvc自动化配置的失效所带来的问题,如:

  1. 无法解析视图。因为WebMvc的自动化配置会创建一个视图解析器。如果此时需要一个视图解析器,那么就重写WebMvcConfigurationSupport类下的configureViewResolvers方法,并新建一个视图解析器添加到ViewResolverRegistry中即可。
  2. WebMvcProperties和ResourceProperties失效。因为这两个配置类中的属性都在WebMvcAutoConfiguration中使用。
  3. HttpMessageConverter失效。所以如果需要更换序列化框架,一般都要重写configureMessageConverters方法。

八、SpringBoot启动流程

该启动流程主要结合源码分析,首先查看重点类SpringApplication的run方法,该方法的核心逻辑如下:

  1. // SpringApplication类中的核心启动方法
  2. public ConfigurableApplicationContext run(String... args) {
  3. // 新建计时器,用于统计启动用时
  4. StopWatch stopWatch = new StopWatch();
  5. stopWatch.start();
  6. // 存放应用上下文
  7. ConfigurableApplicationContext context = null;
  8. // 存放SPI机制加载的SpringBootExceptionReporter的实现类,用于收集错误,向用户报告错误原因
  9. Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
  10. // 配置激活Headless模式
  11. configureHeadlessProperty();
  12. // 通过SPI机制获取spring.factories文件中配置的监听器实现类
  13. SpringApplicationRunListeners listeners = getRunListeners(args);
  14. // 发布容器启动事件ApplicationStartingEvent
  15. listeners.starting();
  16. try {
  17. // 创建默认运行参数对象
  18. ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
  19. // 根据应用类型创建对应环境变量、处理命令行参数、发布环境准备完成事件ApplicationEnvironmentPreparedEvent、同时完成环境和当前对象的绑定
  20. ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
  21. // 根据spring.beaninfo.ignore配置,决定是否跳过对BeanInfo类的搜索
  22. configureIgnoreBeanInfo(environment);
  23. // 打印Banner
  24. Banner printedBanner = printBanner(environment);
  25. // 根据应用类型创建应用上下文
  26. context = createApplicationContext();
  27. // 通过SPI机制加载的SpringBootExceptionReporter的实现类
  28. exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
  29. new Class[] { ConfigurableApplicationContext.class }, context);
  30. // 应用上下文前置处理,下文具体讨论
  31. prepareContext(context, environment, listeners, applicationArguments, printedBanner);
  32. // 刷新应用上下文【核心方法】,下文具体讨论
  33. refreshContext(context);
  34. // 留给第三方扩展
  35. afterRefresh(context, applicationArguments);
  36. // 此时容器启动完成,停止计时,并根据日志级别打印启动用时
  37. stopWatch.stop();
  38. if (this.logStartupInfo) {
  39. new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
  40. }
  41. // 发布ApplicationStartedEvent事件,此时还没有调用任何ApplicationRunner和CommandLineRunner的实现类
  42. listeners.started(context);
  43. // 完成ApplicationRunner和CommandLineRunner实现类的调用
  44. callRunners(context, applicationArguments);
  45. }
  46. catch (Throwable ex) {
  47. // 容器启动失败,发布ApplicationFailedEvent事件、收集错误并反馈、关闭容器
  48. handleRunFailure(context, ex, exceptionReporters, listeners);
  49. throw new IllegalStateException(ex);
  50. }
  51. try {
  52. // 发布ApplicationReadyEvent事件,表明SpringBoot已经加载完成
  53. listeners.running(context);
  54. }
  55. catch (Throwable ex) {
  56. // 如果ApplicationReadyEvent事件发布失败,则收集错误反馈、并关闭容器,此时不再发布ApplicationFailedEvent事件
  57. handleRunFailure(context, ex, exceptionReporters, null);
  58. throw new IllegalStateException(ex);
  59. }
  60. // 完成启动,返回应用上下文
  61. return context;
  62. }
  63. // prepareContext方法源码
  64. private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment,
  65. SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
  66. // 设置environment到应用上下文
  67. context.setEnvironment(environment);
  68. // 后置处理,默认不做处理,留后子类扩展
  69. postProcessApplicationContext(context);
  70. // 在刷新上下文之前,应用所有的ApplicationContextInitializer
  71. applyInitializers(context);
  72. // 发布ApplicationContextInitializedEvent事件(此时还没有加载任何Bean)
  73. listeners.contextPrepared(context);
  74. if (this.logStartupInfo) {
  75. logStartupInfo(context.getParent() == null);
  76. logStartupProfileInfo(context);
  77. }
  78. // 部分特定Bean的加载
  79. ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
  80. beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
  81. if (printedBanner != null) {
  82. beanFactory.registerSingleton("springBootBanner", printedBanner);
  83. }
  84. if (beanFactory instanceof DefaultListableBeanFactory) {
  85. ((DefaultListableBeanFactory) beanFactory)
  86. .setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
  87. }
  88. if (this.lazyInitialization) {
  89. context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
  90. }
  91. // 加载Bean到应用上下文,sources代表应用启动类
  92. Set<Object> sources = getAllSources();
  93. Assert.notEmpty(sources, "Sources must not be empty");
  94. // 将启动类加载到spring容器的beanDefinitionMap中,为后续springBoot自动化配置奠定基础
  95. load(context, sources.toArray(new Object[0]));
  96. // 发布ApplicationPreparedEvent事件(此时Bean还没有完全加载完成)
  97. listeners.contextLoaded(context);
  98. }

上述源码中提到的refreshContext(context),主要完成Bean的加载操作,核心逻辑见AbstractApplicationContext的refresh方法:

  1. public void refresh() throws BeansException, IllegalStateException {
  2. synchronized (this.startupShutdownMonitor) {
  3. // 记录容器的启动时间、标记容器“已启动”状态、检查环境变量
  4. prepareRefresh();
  5. // 告诉子类刷新BeanFactory、并由子类返回BeanFactory容器
  6. ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
  7. // 设置BeanFactory的类加载器、添加几个BeanPostProcessor、手动注册几个特殊的Bean
  8. prepareBeanFactory(beanFactory);
  9. try {
  10. // 扩展点
  11. postProcessBeanFactory(beanFactory);
  12. // 调用BeanFactoryPostProcessor的各个实现类
  13. invokeBeanFactoryPostProcessors(beanFactory);
  14. // 注册BeanPostProcessor的实现类
  15. registerBeanPostProcessors(beanFactory);
  16. // 初始化MessageSource
  17. initMessageSource();
  18. // 初始化事件广播器,用于后续发布事件
  19. initApplicationEventMulticaster();
  20. // 扩展点
  21. onRefresh();
  22. // 注册事件监听器(此处的监听器是代码中配置的监听器,而不是通过SPI机制引入的),并发布早期事件
  23. registerListeners();
  24. // 初始化所有的非延迟加载的单例Bean
  25. finishBeanFactoryInitialization(beanFactory);
  26. // 广播事件
  27. finishRefresh();
  28. }
  29. catch (BeansException ex) {
  30. if (logger.isWarnEnabled()) {
  31. logger.warn("Exception encountered during context initialization - " +
  32. "cancelling refresh attempt: " + ex);
  33. }
  34. // 销魂已经初始化的Bean
  35. destroyBeans();
  36. // 设置“active”状态
  37. cancelRefresh(ex);
  38. throw ex;
  39. }
  40. finally {
  41. // 清除缓存
  42. resetCommonCaches();
  43. }
  44. }
  45. }

九、Spring扩展点

1、CommandLineRunner和ApplicationRunner

在SpringBoot项目启动时候,可以通过实现上述两个接口进行一些初始化操作,调用这些实现类的时候,容器已经加载完成。

  1. // 两者定义
  2. @FunctionalInterface
  3. public interface CommandLineRunner {
  4. void run(String... args) throws Exception;
  5. }
  6. @FunctionalInterface
  7. public interface ApplicationRunner {
  8. void run(ApplicationArguments args) throws Exception;
  9. }

两者的作用是相同的,唯一的区别就是入参的不同,前者接收原始参数,后者接收的则是对原始参数进行封装之后的对象。

两者均可以通过使用@Order注解或实现Ordered接口,确定执行顺序。

两者的执行时机是在容器启动的流程(SpringApplication#run方法中的callRunners调用)中,如果同步执行抛出异常,会造成容器启动失败,但如果是异步执行,则不会影响容器的正常启动。

2、ApplicationContextInitializer

  1. public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext> {
  2. void initialize(C applicationContext);
  3. }

该接口是Spring容器刷新之前执行的一个回调函数,作用是对之前创建的应用上下文做进一步的处理。实现类通过SPI机制进行指定,在SpringApplicaiton构造函数中完成加载,在prepareContext方法中完成调用。可以通过使用@Order注解或实现Ordered接口,确定执行顺序。

3、各种Aware

Aware接口族的目的就是为了让Bean获取Spring容器提供的一系列服务,使用了Aware之后,你的Bean将会和Spring框架耦合。常见接口:

aware1.png

上述接口在执行过程中,有三个较为特殊(被红框圈中的),这三个接口的调用(初始化Bean的过程中被调用)见下图源码:

aware2.png

剩余的Aware接口,都会提供一个实现BeanPostProcessor中postProcessBeforeInitialization方法的实现类,借助BeanPostProcessor完成方法调用。下图中这六个Aware均是在ApplicationContextAwareProcessor中完成调用。

aware3.png

4、BeanPostProcessor

  1. public interface BeanPostProcessor {
  2. // 在Bean的初始化方法(InitializingBean接口或init-method方法)执行前执行
  3. default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
  4. return bean;
  5. }
  6. // 在Bean的初始化方法(InitializingBean接口或init-method方法)执行后执行
  7. default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
  8. return bean;
  9. }
  10. }

该接口针对Bean实例进行扩展,分别在Bean初始化方法执行的前后执行。执行顺序见AbstractAutowireCapableBeanFactory#initializeBean方法。可以通过使用@Order注解或实现Ordered接口,确定执行顺序。

5、BeanFactoryPostProcessor

  1. @FunctionalInterface
  2. public interface BeanFactoryPostProcessor {
  3. // 通过入参的getBeanDefinition方法可以获取相关Bean的定义信息,然后根据需要进行修改
  4. void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException;
  5. }

该接口是针对BeanDefinition进行操作。实现该接口,可以在Spring的Bean创建之前,修改Bean的定义属性。Spring允许该接口在容器实例化任何其它Bean之前读取配置元数据,并可以根据需要进行修改,例如可以修改Bean的作用域等。

该接口还有一个子接口BeanDefinitionRegistryPostProcessor,其方法的入参是BeanDefinitionRegistry,通过这个参数可以更为方便的去做一些自定义Bean的操作。这两个接口的实现类,在最终被调用的时候,BeanDefinitionRegistryPostProcessor会优先执行。可以通过使用@Order注解或实现Ordered接口,确定执行顺序。

6、FactoryBean

十、SPI机制

SPI全称Service Provider Interface,是一种服务发现机制。SPI的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。通过该机制可以动态为接口提供实现类,提供扩展功能。

Java SPI

  1. // 定义接口
  2. public interface Hunter {
  3. void hello();
  4. }
  5. // 实现类
  6. public class HunterImpl implements Hunter {
  7. @Override
  8. public void hello() {
  9. System.out.println("Hello World");
  10. }
  11. public static void main(String[] args) {
  12. // 使用ServiceLoader加载接口的实现类
  13. ServiceLoader<Hunter> load = ServiceLoader.load(Hunter.class);
  14. // 循环实现类并调用hello方法
  15. load.forEach(Hunter::hello);
  16. }
  17. }

然后在META-INF/services文件夹下创建一个文件,名称为Hunter接口的全限定名com.hunter.spi.Hunter。文件内容为该接口实现类的全限定类名,如下:

  1. # 如果有多个实现类,每行一个即可
  2. com.hunter.spi.HunterImpl

然后运行main方法,控制台打印Hello World,说明接口实现类被正常加载。

ServiceLoader服务加载工具类内部是通过反射的方式获取具体实现类的实例,我们只是通过SPI定义的方式将实现类在指定文件下声明而已。

JDBC驱动类的加载就是通过SPI机制实现的。这样个大数据库厂商自己去实现数据库驱动,JDK本身不关心怎么实现,是提供规范。

jdbc-spi.png

缺点:JDK启动的时候会一次性加载全部的实现类,如果有的实现类初始化很耗时或者有的并没有用到,就会很浪费资源。同时获取实现类的方式不够灵活,只能通过迭代器获取,不能根据某个参数来获取对应的实现类。

Spring SPI

Spring中负载加载服务的类是SpringFactoriesLoader,Spring规定将相关配置写在META-INF/spring.factories文件中,内容以键值对的形式出现,示例如下:

  1. # 以借口的全限定名称为Key,实现类的全限定名称为Value(若有多个,以逗号间隔),中间以逗号连接
  2. org.springframework.context.ApplicationListener=\
  3. com.lyentech.bdc.tuya.listener.HunterApplicationStartedListener,\
  4. com.lyentech.bdc.tuya.listener.HunterApplicationStartingListener

最终实现类的初始化同样使用的是反射机制。SpringBoot的自动配置都是借助SPI机制完成的。

Dubbo SPI

JDK的SPI会对SPI文件中的所有实现类进行实例化,Dubbo则在其基础上进行了扩展,这也导致了Dubbo无法直接使用JDK的ServiceLoader。Dubbo中负责加载服务的类是ExtensionLoader,默认依次扫描META-INF/dubbo/internal/、META-INF/dubbo/、META-INF/services/三个ClassPath目录下的配置文件,配置文件以借口的全限定名命名,文件内容以键值对的形式存在,Key为具体实现类的扩展名,Value为具体借口的实现类。实例如下:

dubbo-spi.png

Dubbo可以根据按需加载指定实现类(而非全部加载),通过接口类名和Key值获取指定实现。示例如下:

  1. // 定义接口,通过@SPI注解表明该接口是Dubbo的可扩展接口,value值表明默认实现的Key
  2. @SPI("hunter1")
  3. public interface Hunter {
  4. void hello();
  5. }
  6. // 第一个实现类
  7. public class HunterImpl1 implements Hunter {
  8. @Override
  9. public void hello() {
  10. System.out.println("HunterImpl1 ... Hello World");
  11. }
  12. }
  13. // 第二个实现类
  14. public class HunterImpl2 implements Hunter {
  15. @Override
  16. public void hello() {
  17. System.out.println("HunterImpl2 ... Hello World");
  18. }
  19. }
  20. // 启动类
  21. public class Start{
  22. public static void main(String[] args) {
  23. // 打印HunterImpl1 ... Hello World
  24. ExtensionLoader.getExtensionLoader(Hunter.class).getExtension("hunter1").hello();
  25. // 打印HunterImpl2 ... Hello World
  26. ExtensionLoader.getExtensionLoader(Hunter.class).getExtension("hunter2").hello();
  27. // 打印HunterImpl1 ... Hello World
  28. ExtensionLoader.getExtensionLoader(Hunter.class).getDefaultExtension().hello();
  29. // 打印hunter1
  30. System.out.println(ExtensionLoader.getExtensionLoader(Hunter.class).getDefaultExtensionName());
  31. }
  32. }

创建META-INF/dubbo/目录,并创建名为com.hunter.spi.Hunter的文件,文件内容如下:

  1. hunter1=github.javaguide.spi.HunterImpl1
  2. hunter2=github.javaguide.spi.HunterImpl2