Spring(二)延迟加载、生命周期、面向切面、事务

本系列文章:
  Spring(一)控制反转、两种IOC容器、自动装配、作用域
  Spring(二)延迟加载、生命周期、面向切面、事务
  Spring(三)父子容器、国际化、异步调用、定时器、缓存
  Spring(四)Spring MVC
  Spring(五)Spring Boot
  Spring(六)Spring Cloud

一、延迟加载

  Spring中默认定义的Bean都是单例的,在容器启动过程中会被创建好,然后放在spring容器中以供使用。这也是实际项目开发时常用的方式。这种方式的优点:

  1. 更早发下Bean定义的错误:实时初始化的Bean如果定义有问题,会在容器启动过程中会抛出异常,让开发者快速发现问题。
  2. 查找Bean更快:容器启动完毕之后,实时初始化的Bean已经完全创建好了,此时被缓存在Spring容器中,当我们需要使用的时候,容器直接返回就可以了,速度是非常快的。

  项目启动时就创建Bean的缺点是:如果程序中定义的Bean非常多,并且有些Bean创建的过程中比较耗时的时候,会导致系统消耗的资源比较多,并且会让整个启动时间比较长。
  针对这个问题,Spring提供的方案是:Bean延迟初始化。所谓延迟初始化,就是和实时初始化刚好相反,延迟初始化的bean在容器启动过程中不会创建,而是需要使用的时候才会去创建,先说一下Bean什么时候会被使用:

  1. 被其他Bean作为依赖进行注入的时候,比如通过property元素的ref属性进行引用,通过构造器注入、通过set注入、通过自动注入,这些都会导致被依赖Bean的创建
    2>. 开发者自己写代码向容器中查找Bean的时候,如调用容器的getBean方法获取Bean。

   IOC容器的初始化过程就是对Bean定义资源的定位、载入和注册,此时容器对Bean的依赖注入并没有发生,依赖注入主要是在应用程序第一次向容器索取Bean时,通过getBean()方法的调用完成
  当Bean定义资源的<Bean>元素中配置了lazy-init=false属性时,容器将会在初始化的时候对所配置的Bean进行预实例化,Bean的依赖注入在容器初始化的时候就已经完成。这样,当应用程序第一次向容器索取被管理的Bean时,就不用再初始化和对Bean进行依赖注入了,直接从容器中获取已经完成依赖注入的现成Bean,可以提高应用第一次向容器获取Bean的性能。
  总的来说,延迟初始化的Bean无法在程序启动过程中迅速发现Bean定义的问题,第一次获取的时候可能耗时会比较长,在实际工作中用的比较少。

1.1 延迟加载的使用

  ApplicationContext容器默认在启动服务器时,将所有单例Bean提前进行实例化。提前实例化意味着作为初始化过程的一部分,ApplicationContext实例会创建并配置所有的单例Bean。
  比如:

	<bean id="testBean" class="cn.lagou.LazyBean" />
	<!--该bean默认的设置为:-->
	<bean id="testBean" calss="cn.lagou.LazyBean" lazy-init="false" />

  lazy-init="false",立即加载,表示在Spring启动时,立刻进行实例化。
  如果不想让个单例Bean在ApplicationContext实现初始化时被提前实例化,那么可以将Bean设置为延迟实例化。示例:

	<bean id="testBean" calss="cn.lagou.LazyBean" lazy-init="true" />

  设置lazy-init为true的Bean将不会在ApplicationContext启动时提前被实例化,而是第一次向容器通过getBean索取Bean时实例化的
  如果一个设置了立即加载的bean1,引用了一个延迟加载的bean2,那么bean1在容器启动时被实例化,而bean2由于被bean1引用,所以也被实例化,这种情况也符合延时加载的Bean在第一次调用时才被实例化的规则。
  如果一个bean的scope属性为scope=“pototype” 时,即使设置了lazy-init=“false”,容器启动时也不会实例化Bean,而是调用getBean方法实例化的。
  开启延迟加载对应的注解是@Lazy

  • 延迟加载场景
      1)开启延迟加载一定程度提高容器启动和运转性能。
      2)对于不常使用的Bean设置延迟加载,这样偶尔使用的时候再加载,不必要从一开始就让该Bean占用资源。

1.2 延迟加载流程

  • 1、refresh()方法
      先从IOC容器的初始化过程开始。IOC容器读入已经定位的Bean定义资源是从refresh()方法开始的,因此首先从AbstractApplicationContext类的refresh()方法入手分析:
	//refresh方法主要为IOC容器Bean的生命周期管理提供条件,在获取了BeanFactory之后都是在向该容器
	//注册信息源和生命周期事件。在创建IOC容器前,如果已经有容器存在,需要把已有的容器销毁和关闭,
	//以保证在refresh()方法之后使用的是新创建的IOC容器。
	@Override
	public void refresh() throws BeansException, IllegalStateException {
		synchronized (this.startupShutdownMonitor) {
			// 调用容器准备刷新的方法,获取容器的当前时间,同时给容器设置同步标识
			prepareRefresh();

			// 获得beanFactory
			ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

			// beanFactory的预准备工作,对beanfactory配置容器特性,例如类加载器、事件处理器等
			prepareBeanFactory(beanFactory);

			try {
				//beanFactory准备工作完成之后要进行的后置处理(BeanPost事件处理)工作,留给子类扩展使用
				postProcessBeanFactory(beanFactory);
				//调用所有注册的BeanFactoryPostProcessor的Bean
				invokeBeanFactoryPostProcessors(beanFactory);
				//为BeanFactory注册BeanPost事件处理器
				//BeanPostProcessors是Bean后置处理器,用于监听容器触发的事件
				registerBeanPostProcessors(beanFactory);
				//初始化MessageSource信息源,即国际化处理、消息绑定、消息解析
				initMessageSource();
				//初始化容器事件传播器
				initApplicationEventMulticaster();
				//留给子类来初始化其他的bean
				onRefresh();
				//在所有注册的bean中查找ApplicationListener,为事件广播器注册事件监听器
				registerListeners();
				//初始化所有剩下的单实例bean
				finishBeanFactoryInitialization(beanFactory);
				//完成刷新过程,初始化容器的生命周期事件处理器,并发布容器的生命周期事件
				finishRefresh();
			}

			catch (BeansException ex) {
				if (logger.isWarnEnabled()) {
					logger.warn("Exception encountered during context initialization - " +
							"cancelling refresh attempt: " + ex);
				}

				// 销毁已经创建的Bean
				destroyBeans();
				// 取消刷新操作,重置容器的同步标识
				cancelRefresh(ex);

				throw ex;
			}

			finally {
				// 重置公共缓存
				resetCommonCaches();
			}
		}
	}

  在refresh()方法中ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();启动了Bean定义资源的载入、注册过程,而finishBeanFactoryInitialization方法是对注册后的Bean定义中的预实例化(lazy-init=false)的Bean进行处理的地方。

  • 2、finishBeanFactoryInitialization处理预实例化Bean
      当Bean定义资源被载入IOC容器之后,容器将Bean定义资源解析为容器内部的数据结构 BeanDefinition注册到容器中,AbstractApplicationContext类中的 finishBeanFactoryInitialization()方法对配置了预实例化属性的Bean进行预初始化过程:
	//对配置了lazy-init属性的bean进行实例化处理
	protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
		//这是Spring3之后增加的代码,为容器准备一个转换服务,在对某些Bean属性进行转换时使用
		if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) &&
				beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) {
			beanFactory.setConversionService(
					beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class));
		}

		if (!beanFactory.hasEmbeddedValueResolver()) {
			beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment().resolvePlaceholders(strVal));
		}

		String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false);
		for (String weaverAwareName : weaverAwareNames) {
			getBean(weaverAwareName);
		}

		//为了使类型匹配,停止使用临时的类加载器
		beanFactory.setTempClassLoader(null);

		//缓存容器中所有注册的BeanDefinition元数据,以防被修改
		beanFactory.freezeConfiguration();

		//对配置了lazy-init属性的单例模式的Bean进行预实例化处理
		beanFactory.preInstantiateSingletons();
	}

  ConfigurableListableBeanFactory是一个接口 , 其preInstantiateSingletons()方法由其子类DefaultListableBeanFactory提供。

  • 3、DefaultListableBeanFactory对配置lazy-init属性单例Bean的预实例化
	@Override
	public void preInstantiateSingletons() throws BeansException {
		if (logger.isTraceEnabled()) {
			logger.trace("Pre-instantiating singletons in " + this);
		}

		//获取容器中的所有bean,依次进行初始化和创建对象
		List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);

		for (String beanName : beanNames) {
			//获取bean的定义信息
			RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
			//bean不是抽象的、是单实例的、是懒加载的
			if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
				//判断是否是FactoryBean,是否是实现FactoryBean接口的bean
				if (isFactoryBean(beanName)) {
					Object bean = getBean(FACTORY_BEAN_PREFIX + beanName);
					if (bean instanceof FactoryBean) {
						final FactoryBean<?> factory = (FactoryBean<?>) bean;
						//标识是否需要实例化
						boolean isEagerInit;
						if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) {
							isEagerInit = AccessController.doPrivileged((PrivilegedAction<Boolean>)
											((SmartFactoryBean<?>) factory)::isEagerInit,
									getAccessControlContext());
						}
						else {
							isEagerInit = (factory instanceof SmartFactoryBean &&
									((SmartFactoryBean<?>) factory).isEagerInit());
						}
						if (isEagerInit) {
							getBean(beanName);
						}
					}
				}
				else {
					//不是工厂Bean,利用getBean创建对象
					getBean(beanName);
				}
			}
		}

		for (String beanName : beanNames) {
			Object singletonInstance = getSingleton(beanName);
			if (singletonInstance instanceof SmartInitializingSingleton) {
				final SmartInitializingSingleton smartSingleton = (SmartInitializingSingleton) singletonInstance;
				if (System.getSecurityManager() != null) {
					AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
						smartSingleton.afterSingletonsInstantiated();
						return null;
					}, getAccessControlContext());
				}
				else {
					smartSingleton.afterSingletonsInstantiated();
				}
			}
		}
	}

  可以看出,如果设置了lazy-init属性,则容器在完成Bean定义的注册之后,会通过getBean方法,触发对指定Bean的初始化和依赖注入过程,这样当应用第一次向容器索取所需的Bean时,容器不再需要对Bean进行初始化和依赖注入,直接从已经完成实例化和依赖注入的Bean中取一个现成的Bean,这样就提高了第一次获取Bean的性能。

二、生命周期

2.1 Bean的生命周期*

  • 1、Spring对Bean进行实例化【实例化Bean】;
  • 2、Spring将值和Bean的引用注入到Bean对应的属性中【依赖注入】;
  • 3、如果Bean实现了BeanNameAware接口,Spring调用setBeanName()方法,参数为将bean的ID或name【处理Aware接口1】;
      实现BeanNameAware接口的Bean能够在初始化时知道自己在BeanFactory中对应的名字。该接口只有一个方法:
	//让Bean获取自己在BeanFactory配置中的名字(根据情况是id或者name)
	void setBeanName(String name)
  • 4、如果bean实现了BeanFactoryAware接口,Spring将调用setBeanFactory()方法,将BeanFactory容器实例传入【处理Aware接口2】;
      实现BeanFactoryAware接口的Bean能够在初始化时知道自己所在的BeanFactory的名字。该接口只有一个方法:
	//让Bean获取配置他们的BeanFactory的引用
	void setBeanFactory(BeanFactory beanFactory) throws BeansException*
  • 5、如果bean实现了ApplicationContextAware接口,Spring将调用setApplicationContext()方法,将bean所在的应用上下文的引用传入进来【处理Aware接口3】;
      一个类实现了ApplicationContextAware之后,这个类就可以方便获得ApplicationContext中的所有Bean。ApplicationContextAware接口有一个方法:
	void setApplicationContext(ApplicationContext applicationContext) throws BeansException
  • 6、如果bean实现了BeanPostProcessor接口,Spring将调用它们的postProcessBeforeInitialization(预初始化)方法,在Bean初始化前对其进行处理【BeanPostProcessor1】;
      由于这个方法是在Bean初始化结束时调用的,所以可以被应用于内存或缓存技术。
      BeanPostProcessor接口中的方法 :
	// 在Bean初始化之前执行
	Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;
  • 7、如果bean实现了InitializingBean接口,Spring将调用它们的afterPropertiesSet()方法,然后调用xml定义的init-method方法(或Java代码中的@Bean(initMethod = "初始化的方法")),两个方法作用类似,都是在初始化bean的时候执行。该方法会在Bean的属性被初始化后调用【InitializingBean与init-method】。

  通过实现该接口,可以在afterPropertiesSet中读取一些数据,存到该对象的static属性中,相当于在应用启动时,把一下配置类信息存入了缓存中。这样,再读取这些配置信息时,就不需要实时去表里查了,直接读取缓存数据即可。

  • 8、如果bean实现了BeanPostProcessor接口,Spring将调用它们的postProcessAfterInitialization方法,该方法在Bean初始化后调用【BeanPostProcessor2】;
      以上几个步骤完成后,Bean就已经被正确创建了,可以被应用程序使用了,它们将一直驻留在应用上下文中,直到该应用上下文被销毁
      BeanPostProcessor接口中的方法:
	// 在Bean初始化之后执行
	@Nullable
    default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    @Nullable
    default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }
  • 9、如果bean实现了DisposableBean接口,Spring将在销毁Bean之前调用该接口中的destroy()接口方法。同样,如果bean使用destroy-method声明了销毁方法,该方法也会被调用。实现该接口时,可以进行一些Bean销毁相关的工作,比如将各种池关闭【DisposableBean】。
      该接口中的方法:
	void destroy() throws Exception;
  • 10、最后,如果这个Bean的Spring配置中配置了destroy-method属性,会自动调用其配置的销毁方法【destroy-method】,或代码中的@Bean(destroyMethod = "自定义销毁方法")

  在一个类如果同时实现了BeanFactoryPostProcessor和ApplicationContextAware,就可以通过beanFactory来获取容器中的Bean,这种功能可以用来做成获取Bean的工具类。

  • 从源码看Bean的生命周期

2.2 不同作用域Bean生命周期的区别*

2.2.1 单例对象*

  当scope=”singleton”,即默认情况下,会在启动容器时(即实例化容器时)时实例化。也可以指定Bean节点的lazy-init=”true”来延迟初始化Bean,这时候,只有在第一次获取Bean时才会初始化Bean,即第一次请求该Bean时才初始化。

    <bean id="ServiceImpl" class="cn.csdn.service.ServiceImpl" lazy-init="true"/>

  默认情况下,Spring在读取xml文件的时候,就会创建对象。在创建对象的时候先调用构造器,然后调用init-method属性值中所指定的方法。对象在被销毁的时候,会调用destroy-method属性值中所指定的方法。

2.2.2 非单例对象*

  当scope=”prototype”时,容器也会延迟初始化Bean,Spring读取xml文件的时候,并不会立刻创建对象,而是在第一次请求该Bean时才初始化(如调用getBean方法时)。在第一次请求每一个prototype的Bean时,Spring容器都会调用其构造器创建这个对象,然后调用init-method属性值中所指定的方法。对象销毁的时候,Spring容器不会帮我们调用任何方法,因为是非单例,这个类型的对象有很多个,Spring容器一旦把这个对象交给你之后,就不再管理这个对象了。
  Spring容器可以管理singleton作用域下Bean的生命周期。在此作用域下,Spring能够精确地知道Bean何时被创建、何时初始化完成,以及何时被销毁。而对于prototype作用域的Bean,Spring只负责创建,当容器创建了Bean的实例后,Bean的实例就交给了客户端的代码管理,Spring容器将不再跟踪其生命周期,并且不会管理那些被配置成prototype作用域的Bean的生命周期。

2.3 Spring容器注入bean的几种方法

2.3.1 @ComponentScan+@Component*

  @ComponentScan可以放在启动类上,指定要扫描的包路径;该包路径下被@Component修饰的类,都会被注入到Spring容器中。

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
@ComponentScan(basePackages = "com.gs.beanRegister")
public class BootStrap {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = 
        new AnnotationConfigApplicationContext(BootStrap.class);
        A bean = context.getBean(A.class);
        bean.say();
    }
}

  com.gs.beanRegister包下:

import org.springframework.stereotype.Component;
@Component
public class A {
    public void say() {
        System.out.println("这是a");
    }
}

  在SpringBoot中,由于其自动装配的特性,所以@ComponentScan可以不加,只要@Component修饰的类和启动类在同一包下或者在启动类所在包的子包下。

2.3.2 @Configuration+@Bean*

  @Configuration用来声明一个配置类,如果它的方法被@Bean修饰,那么该方法返回的对象也会被注入到Spring容器中。
  BootStrap类不动,A类的@Component去掉,com.gs.beanRegister包下建个配置类:

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Bean;
@Configuration
public class MyConfiguration {
    @Bean
    public A a() {
        return new A();
    }
}
2.3.3 通过@Import注解*
  • 1、直接导入类的class
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
@Import(A.class)
public class BootStrap {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = 
        new AnnotationConfigApplicationContext(BootStrap.class);
        A bean = context.getBean(A.class);
        //B bean = context.getBean(B.class);
        bean.say();
    }
}

//A类不用添加任何注解
public class A {
    public void say() {
        System.out.println("这是a");
    }
}
  • 2、导入配置类
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Import;
@Import(MyConfiguration.class)
public class BootStrap {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = 
        new AnnotationConfigApplicationContext(BootStrap.class);
        A bean = context.getBean(A.class);
        bean.say();
    }
}

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
// 使用@Import导入配置类时,@Configuration可以不加
//@Configuration
public class MyConfiguration {
    @Bean
    public A a() {
        return new A();
    }
}
  • 3、导入ImportSelector的实现类
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Import;
@Import(MyImportSelector.class)
public class BootStrap {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = 
        new AnnotationConfigApplicationContext(BootStrap.class);
        A bean = context.getBean(A.class);
        bean.say();
    }
}

import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;
public class MyImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata metadata) {
        // 返回要注入的bean的全路径,A类不用任何注解修饰
        // SpringBoot的自动装配,就用到了这种方式:
        // 返回配置类的全路径,配置类的@Bean方法返回的对象也能注入到容器中
        return new String[] { A.class.getName() };
    }
}
  • 4、导入ImportBeanDefinitionRegistrar的实现类
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Import;
@Import(MyImportBeanDefinitionRegistrar.class)
public class BootStrap {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = 
        new AnnotationConfigApplicationContext(BootStrap.class);
        A bean = context.getBean(A.class);
        bean.say();
    }
}

import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;
public class MyImportBeanDefinitionRegistrar implements 
             ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata, 
                BeanDefinitionRegistry registry) {
        // 构建bean的元数据,A类不用任何注解修饰
        // spring-mybatis扫描mapper接口,生成代理类,就是用的这种方式
        BeanDefinition definition = new RootBeanDefinition(A.class);
        registry.registerBeanDefinition("a", definition);
    }
}
2.3.4 借助FactoryBean接口

  实现FactoryBean接口的类,除了本身会被注入外,getObject方法返回的对象也会被注入到Spring容器中。

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Import;
@Import(MyFactoryBean.class)
public class BootStrap {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = 
        new AnnotationConfigApplicationContext(BootStrap.class);
        A bean = context.getBean(A.class);
        bean.say();
    }
}

import org.springframework.beans.factory.FactoryBean;
public class MyFactoryBean implements FactoryBean {
    @Override
    public Object getObject() throws Exception {
        return new A();
    }
    @Override
    public Class<?> getObjectType() {
        return A.class;
    }
}
2.3.5 借助BeanDefinitionRegistryPostProcessor接口

  在Spring容器启动时,会调用该接口的postProcessBeanDefinitionRegistry方法,大概意思是等BeanDefinition(上面提到的bean的元数据)加载完成后,再对它进行后置处理。所以可以在此调整BeanDefinition,从而把对应的bean注入。

import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class BootStrap {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = 
        new AnnotationConfigApplicationContext();
        BeanDefinitionRegistryPostProcessor postProcessor = 
        new MyBeanDefinitionRegistryPostProcessor();
        context.addBeanFactoryPostProcessor(postProcessor);
        context.refresh();
        A a = context.getBean(A.class);
        a.say();
    }
}

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.RootBeanDefinition;
public class MyBeanDefinitionRegistryPostProcessor implements 
             BeanDefinitionRegistryPostProcessor {
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry 
                registry) throws BeansException {
        BeanDefinition definition = new RootBeanDefinition(A.class);
        registry.registerBeanDefinition("a", definition);
    }
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory 
                beanFactory) throws BeansException {
    }
}

三、面向切面

  AOP:面向切面编程,作为面向对象的一种补充,将公共逻辑(事务管理、日志、缓存等)封装成切面,跟业务代码进行分离,可以减少系统的重复代码和降低模块之间的耦合度。
  切面就是那些与业务无关,但所有业务模块都会调用的公共逻辑。
  在不改变原有业务逻辑的情况下增强横切逻辑,横切逻辑代码往往用来实现这类功能:事务管理、安全检查、权限控制、数据校验、缓存、对象池管理等。
  Spring中提供的一些非常牛逼的功能都是通过AOP实现的:

  1. spring事务管理:@Transactional
  2. spring异步处理:@EnableAsync
  3. spring缓存技术的使用:@EnableCaching
  4. spring中各种拦截器:@EnableAspectJAutoProxy

3.1 初识AOP

  AOP(面向切面)是OOP(面向对象)的延续(或完善)
  OOP编程思想可以解决大多数的代码重复问题,但是有一些情况是处理不了的,比如权限校验、日志追踪等非业务性代码会多次重复出现。这种代码可以简单理解为横切逻辑代码

  横切逻辑代码存在的问题:

  1. 横切代码重复问题。
  2. 横切逻辑代码和业务代码混杂在一起,代码臃肿,维护不方便。

  AOP的做法,是将横切逻辑代码和业务逻辑代码分离:

  因此AOP解决的问题是:在不改变原有业务逻辑情况下,增强横切逻辑代码,根本上解耦合,避免横切逻辑代码重复。

3.2 AOP使用

3.2.1 AOP概念
  • 1、切面(Aspect)------>通知和切入点的结合
      指关注点模块化,这个关注点可能会横切多个对象。在Spring AOP中,切面可以使用通用类(基于模式的风格) 或者在普通类中以@Aspect注解来实现。
  • 2、连接点(Join point)
      在程序执行过程中某个特定的点,例如某个方法调用的时间点或者处理异常的时间点。在Spring AOP中,一个连接点总是代表一个方法的执行。
  • 3、切入点(Pointcut)------>添加公共功能的地方/方法
      我们通常使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些拦截的方法。
  • 4、通知(Advice)------>要添加的公共功能
      在切面的某个特定的连接点上执行的动作。通知类型包括(Around、Before、After、AfterReturning、AfterThrowing)。
  • 5、目标对象(Target)------>被添加公共功能的原始对象
       被一个或者多个切面所通知的对象。也被称作被通知对象。SpringAOP是通过运行时代理实现的,那么这个对象永远是一个被代理的对象。
  • 6、引入(Introduction)
      在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段。
  • 7、织入(Weaving)------>在目标对象上使用切面
      织入是把切面应用到目标对象并创建新的代理对象的过程。Spring是在运行时完成
    织入。在目标对象的生命周期里有多个点可以进行织入:
  1. 编译期:切面在目标类编译时被织入。AspectJ的织入编译器是以这种方式织入切面的。
  2. 类加载期:切面在目标类加载到JVM时被织入。需要特殊的类加载器,它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ5的加载时织入就支持以这种方式织入切面。
  3. 运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。SpringAOP就是以这种方式织入切面。
3.2.2 AOP例子

  声明一个切面类:

package com.xu.spring.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.util.Arrays;

@Aspect
@Component
@Order
//切面
public class TestAspect {
    //切入点
    @Pointcut("execution(* com.xu.spring.aop.TestCalculate.*(..))")
    public void pointCut() {
    }

    //前置通知
    @Before(value = "pointCut()")
    public void methodBefore(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        System.out.printf("执行目标方法【%s】的前置通知,入参是--->%s%n", methodName, Arrays.asList(joinPoint.getArgs()));
    }

    //后置通知
    @After(value = "pointCut()")
    public void methodAfter(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        System.out.printf("执行目标方法【%s】的后置通知,入参是--->%s%n", methodName, Arrays.asList(joinPoint.getArgs()));
    }

    //后置返回通知
    @AfterReturning(value = "pointCut()", returning = "resultObj")
    public void methodAfterReturning(JoinPoint joinPoint, Object resultObj) {
        String methodName = joinPoint.getSignature().getName();
        System.out.printf("执行目标方法【%s】的后置返回通知,入参是--->%s,后置通知返回的参数---->%s%n", methodName, Arrays.asList(joinPoint.getArgs()), resultObj);
    }

    //异常通知
    @AfterThrowing(value = "pointCut()")
    public void methodAfterThrowing(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        System.out.printf("执行目标方法【%s】的异常通知,入参是--->%s%n", methodName, Arrays.asList(joinPoint.getArgs()));
    }
}

  一个要被代理的类:

package com.xu.spring.aop;

import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Component;

@Component
public class TestCalculate implements Calculate {
    @Override
    public int add(int numA, int numB) {
        System.out.println("执行目标方法:add");
        return numA + numB;
    }

    @Override
    public int sub(int numA, int numB) {
        System.out.println("执行目标方法:sub");
        return numA - numB;
    }

    @Override
    public int div(int numA, int numB) {
        System.out.println("执行目标方法:div");
        return numA / numB;
    }

    @Override
    public int multi(int numA, int numB) {
        System.out.println("执行目标方法:multi");
        return numA * numB;
    }

    @Override
    public int mod(int numA, int numB) {
        System.out.println("执行目标方法:mod");
        Calculate calculate = (Calculate) AopContext.currentProxy();
        int add = calculate.add(numA, numB);
        return add;
    }
}

  一个配置类。

package com.xu.spring.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@ComponentScan(basePackages = {"com.xu.spring.aop"})
@EnableAspectJAutoProxy
public class MainConfig {
}

  测试类:

public class AopTest {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig.class);
        Calculate testCalculate = (Calculate) context.getBean("testCalculate");
        int add = testCalculate.div(12, 1);
        System.out.println(add);
    }
}

  结果:

执行目标方法【div】的前置通知,入参是—>[12, 1]
执行目标方法:div
执行目标方法【div】的后置通知,入参是—>[12, 1]
执行目标方法【div】的后置返回通知,入参是—>[12, 1],后置通知返回的参数---->12
12

3.3.3 通知中获取被调方法信息*

  通知中如果想获取被调用方法的信息,分2种情况:

  1. 非环绕通知,可以将JoinPoint作为通知方法的第1个参数,通过这个参数获取被调用方法的信息。
  2. 如果是环绕通知,可以将ProceedingJoinPoint作为方法的第1个参数,通过这个参数获取被调用方法的信息。

  JoinPoint:连接点信息,提供访问当前被通知方法的目标对象、代理对象、方法参数等数据:

public interface JoinPoint { 
    String toString();         //连接点所在位置的相关信息 
    String toShortString();     //连接点所在位置的简短相关信息 
    String toLongString();     //连接点所在位置的全部相关信息 
    Object getThis();         //返回AOP代理对象
    Object getTarget();       //返回目标对象 
    Object[] getArgs();       //返回被通知方法参数列表,也就是目前调用目标方法传入的参数 
    Signature getSignature();  //返回当前连接点签名,这个可以用来获取目标方法的详细信息,如方法Method对象等
    SourceLocation getSourceLocation();//返回连接点方法所在类文件中的位置 
    String getKind();        //连接点类型 
    StaticPart getStaticPart(); //返回连接点静态部分 
}

  ProceedingJoinPoint:环绕通知连接点信息,用于环绕通知,内部主要关注2个方法,一个有参的,一个无参的,用来继续执行拦截器链上的下一个通知。

public interface ProceedingJoinPoint extends JoinPoint {
  /**
  * 继续执行下一个通知或者目标方法的调用
  */
  public Object proceed() throws Throwable;
  /**
  * 继续执行下一个通知或者目标方法的调用
  */
  public Object proceed(Object[] args) throws Throwable;
}

  Signature:连接点签名信息。Signature有个子接口MethodSignature,JoinPoint中的getSignature方法返回值都可以转换转换为MethodSignature类型,然后可以通过这个接口提供的一些方法来获取被调用的方法的详细信息。

3.3.4 5种通知*

  Spring切面有5种类型的通知:

  • 1、前置通知(Before advice)
     在目标方法被调用之前调用通知功能。使用@Before注解。使用注意事项:
  1. 类上需要使用@Aspect标注。
  2. 任意方法上使用@Before标注,将这个方法作为前置通知,目标方法被调用之前,会自动回调这个方法。
  3. 被@Before标注的方法参数可以为空,或者为JoinPoint类型,当为JoinPoint类型时,必须为第一个参数。
  4. 被@Before标注的方法名称可以随意命名,符合Java规范就可以,其他通知也类似。
  • 2、返回后通知(After returning advice)
     在目标方法成功执行之后调用通知。使用@AfterReturning注解。
  • 3、抛出异常后通知(After throwing advice)
     在目标方法抛出异常后调用通知。使用@AfterThrowing注解。
  • 4、后置通知(After (finally) advice)
     在目标方法完成之后调用通知(不论是正常返回还是异常退出)。使用@After注解。用法和前置通知类似,特点:
  1. 这种通知无法获取方法返回值。
  2. 可以使用JoinPoint作为方法的第一个参数,用来获取连接点的信息。
  • 5、环绕通知(Around Advice)
     通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的逻辑。使用@Around注解。用起来和@Before类似,但是有2点不一样:
  1. 若需要获取目标方法的信息,需要将ProceedingJoinPoint作为第一个参数。
  2. 通常使用Object类型作为方法的返回值,返回值也可以为void。

  五种通知的常见使用场景:

前置通知---->记录日志(方法将被调用)
环绕通知---->控制事务、权限控制
后置通知---->记录日志(方法已经成功调用)
异常通知---->异常处理、控制事务
最终通知---->记录日志(方法已经调用,但不一定成功)

  环绕通知的执行顺序是优于普通通知的
  同一个Aspect,不同advice的执行顺序:

  • 1、没有异常情况下的执行顺序

around before advice
before advice
目标方法执行
around after advice
after advice
afterReturning

  如果出现异常的时候,在环绕通知中解决了,那么普通通知是接受不到的。如果想让普通通知接收到,需要在环绕通知中进行抛出异常。

  • 2、有异常情况下的执行顺序

around before advice
before advice
目标方法执行
around after advice
after advice
afterThrowing:异常发生
java.lang.RuntimeException: 异常发生

  • 几种通知对比
通知类型执行时间点可获取返回值目标方法异常时是否会执行
@Before方法执行之前
@Around环绕方法执行自己控制
@After方法执行后
@AfterReturning方法执行后
@AfterThrowing方法发生异常后

3.3.5 多个切面类的执行顺序*

  按照切面类的名称的首字母进行排序操作。比如有两个切面类:LogUtil、SecurityUtil,就是LogUtil先执行。
  如果需要人为地规定顺序,可以在切面类上添加@Order注解同时可以添加具体的值,值越小,越优先。示例:

	@Aspect
	@Component
	@Order(100)
	public class SecurityUtil {
	}

	@Aspect
	@Component
	@Order(200)
	public class LogUtil {
	}

  此时就是SecurityUtil切面先执行。

3.5 两种代理*

  AOP实现的关键在于代理模式,AOP代理主要分为静态代理和动态代理。静态代理的代表为AspectJ;动态代理则以Spring AOP为代表。

  • 静态代理和动态代理
      AspectJ:AspectJ是静态代理的增强,所谓静态代理,就是AOP框架会在编译阶段生成AOP代理类,因此也称为编译时增强,他会在编译阶段将AspectJ(切面)织入到Java字节码中,运行的时候就是增强之后的AOP对象。
      Spring AOP:Spring AOP采用的是动态代理,在运行期间对业务方法进行增强,所以不会生成新类,对于动态代理技术,Spring AOP提供了对JDK动态代理的支持以及CGLib的支持。JDK动态代理只能为接口创建动态代理实例,而不能对类创建动态代理。需要获得被目标类的接口信息(应用Java的反射技术),生成一个实现了代理接口的动态代理类(字节码),再通过反射机制获得动态代理类的构造函数,利用构造函数生成动态代理类的实例对象,在调用具体方法前调用invokeHandler方法来处理。
      CGLib动态代理需要依赖asm包,把被代理对象类的class文件加载进来,修改其字节码生成子类。

  • Spring AOP和AspectJ AOP的区别
      1、Spring AOP属于运行时增强,而AspectJ是编译时增强。
      2、Spring AOP基于代理,而AspectJ 基于字节码操作。
      3、Spring AOP 基于动态代理方式实现;AspectJ 基于静态代理方式实现。
      4、SpringAOP 仅支持方法级别的PointCut;AspectJ 提供了完全的 AOP 支持,它还支持属性级别的 PointCut。

  • Spring AOP的两种动态代理
      Spring提供了两种方式来生成代理对象: JDK动态代理和Cglib动态代理。
      JDK动态代理:利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。
      Cglib动态代理:利用asm开源包,对代理对象类的class文件加载进来,通过修改其(代理对象类的class文件)字节码生成子类来处理。

3.5.1 JDK动态代理和CGLIB动态代理*

  Spring会自动在JDK动态代理和CGLIB之间转换。具体规则为:

  1. 如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP。
  2. 如果目标对象实现了接口,可以强制使用CGLIB实现AOP。
  3. 如果目标对象没有实现接口,必须采用CGLIB。

  强制使用CGLIB实现AOP的方式:

  1. 添加CGLIB库(aspectjrt-xxx.jar、aspectjweaver-xxx.jar、cglib-nodep-xxx.jar);
  2. 在Spring配置文件中加入<aop:aspectj-autoproxy proxy-target-class="true"/>
3.5.2 JDK动态代理

  JDK动态代理的应用非常广泛,例如在Spring、MyBatis以及Feign等很多框架中动态代理都被大量的使用。
  JDK动态代理主要涉及到java.lang.reflect包中的两个类:Proxy和InvocationHandler。
  Proxy中的静态方法:

	/**
	 * 为指定的接口创建代理类,返回代理类的Class对象
	 * loader:定义代理类的类加载器
	 * interfaces:指定需要实现的接口列表,创建的代理默认会按顺序实现interfaces指定的接口
	 */
	public static Class<?> getProxyClass(ClassLoader loader,
                    Class<?>... interfaces)

	/**
	 * 创建代理类的实例对象
	 */
	public static Object newProxyInstance(ClassLoader loader,
                     Class<?>[] interfaces,
                     InvocationHandler h)

  newProxyInstance方法先为指定的接口创建代理类,然后会生成代理类的一个实例。最后一个参数是InvocationHandler类型的,InvocationHandler接口中有invoke方法,该方法主要实现代理的逻辑:

	public Object invoke(Object proxy, Method method, Object[] args)
    	throws Throwable;

  JDK动态代理创建方式1:

  1)调用Proxy.getProxyClass方法获取代理类的Class对象。
  2)使用InvocationHandler接口创建代理类的处理器。
  3)通过代理类和InvocationHandler创建代理对象。
  4)上面已经创建好代理对象了,接着我们就可以使用代理对象了。

  JDK动态代理创建方式2:

  1)使用InvocationHandler接口创建代理类的处理器。
  2)使用Proxy类的静态方法newProxyInstance直接创建代理对象。
  3)使用代理对象。

  • 为什么JDK动态代理只能用在接口/接口实现类上
      JDK动态代理的实现方式是让JVM去调用InvocationHandler中的invoke()方法。JDK动态代理处理这个问题的方式是选择继承父类Proxy,并把InvocationHandler保存在父类的对象中:
public class Proxy implements java.io.Serializable {
    protected InvocationHandler h;
    
    protected Proxy(InvocationHandler h) {
        Objects.requireNonNull(h);
        this.h = h;
    }
    
    // ......
}

  通过父类Proxy的构造方法,保存了创建代理对象过程中传进来的 InvocationHandler的实例,使用protected修饰保证了它可以在子类中被访问和使用。同时,因为Java是单继承的,因此在代理类$Proxy0继承了Proxy后,其只能通过实现目标接口的方式来实现方法的扩展,达到我们增强目标方法逻辑的目的。

3.5.2 CGLIB动态代理

  cglib是一个强大、高性能的字节码生成库,它用于在运行时扩展Java类和实现接口;本质上它是通过动态的生成一个子类去覆盖所要代理的类(非final修饰的类和方法)。Enhancer可能是CGLIB中最常用的一个类,和jdk中的Proxy不同的是,Enhancer既能够代理普通的class,也能够代理接口。Enhancer创建一个被代理对象的子类并且拦截所有的方法调用(包括从Object中继承的toString和hashCode方法)。Enhancer不能够拦截final方法,例如Object.getClass()方法,这是由于Java final方法语义决定的。基于同样的道理,Enhancer也不能对final类进行代理操作。
  CGLIB底层使用了ASM(一个短小精悍的字节码操作框架)来操作字节码生成新的类。Spring已将第三方cglib jar包中所有的类集成到spring自己的jar包中。

  • 使用CGlib代理接口的示例代码
      添加相应的依赖:
<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>

  创建一个接口Foo和一个实现该接口的类FooImpl:

public interface Foo {
    void bar();
}

public class FooImpl implements Foo {
    @Override
    public void bar() {
        System.out.println("Original bar() method");
    }
}

  创建一个MethodInterceptor的实现类,用于在代理方法执行前后添加额外的逻辑:

import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class FooInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("Before invoking method: " + method.getName());
        Object result = proxy.invokeSuper(obj, args);
        System.out.println("After invoking method: " + method.getName());
        return result;
    }
}

  创建一个测试类,演示如何使用CGlib代理接口:

import net.sf.cglib.proxy.Enhancer;

public class Main {
    public static void main(String[] args) {
        FooInterceptor interceptor = new FooInterceptor();

        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(FooImpl.class);
        enhancer.setCallback(interceptor);

        Foo proxy = (Foo) enhancer.create();
        proxy.bar();
    }
}

  在上述示例中,我们通过创建Enhancer对象来设置要代理的类和回调(即MethodInterceptor实现类)。然后使用enhancer.create()方法创建代理对象,最终调用代理对象的bar()方法。在代理方法执行前后,FooInterceptor中的逻辑会被执行。
  这就是使用CGlib方式对接口进行代理的示例代码。通过CGlib,我们可以在运行时动态地创建代理类,实现对接口方法的代理,并且添加额外的逻辑。

3.5.3 两者的区别*

  Spring AOP中的动态代理主要有两种方式:JDK动态代理和CGLIB动态代理。

  • JDK动态代理
      如果目标类实现了接口,Spring AOP会选择使用JDK动态代理目标类。代理类根据目标类实现的接口动态生成,不需要自己编写,生成的动态代理类和目标类都实现相同的接口。JDK动态代理的核心是InvocationHandler接口和Proxy类。
      JDK动态代理的缺点:目标类必须有实现的接口。如果某个类没有实现接口,那么这个类就不能用JDK动态代理。
  • CGLIB动态代理
      通过继承实现。如果目标类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB可以在运行时动态生成类的字节码,动态创建目标类的子类对象,在子类对象中增强目标类。
      CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final ,那么它是无法使用CGLIB做动态代理的。
      优点:目标类不需要实现特定的接口,更加灵活。

  具体区别:

  • 1、 JDK动态代理只能对接口实现类生成代理,而不能针对类
  • 2、CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法因为是继承,所以该类或方法最好不要声明成final ;
  • 3、两者的性能差别:在早期的时候,JDK动态代理性能弱于CGLIB动态代理,随着JDK的更新,两者性能以及差别不大(JDK1.6和JDK1.7的时候,CGLib更快;1.8的时候,JDK动态代理更快)。
  • 4、CGLib在创建对象的时候所花费的时间却比JDK动态代理多。
  • 5、单例的代理对象或者具有实例池的代理,因为无需频繁的创建代理对象,所以比较适合采用CGLib动态代理,反之,则适合用JDK动态代理。
  • 6、JDK生成的代理类类型是Proxy(因为继承的是Proxy),CGLIB生成的代理类类型是Enhancer类型。
  • 7、如果要被代理的对象是个实现类,那么Spring会默认使用JDK动态代理来完成操作;如果要被代理的对象不是实现类,那么Spring会强制使用CGLib来实现动态代理。
  • 8、 jdk动态代理使用jdk中的类Proxy来创建代理对象,它使用反射技术来实现,不需要导入其他依赖。cglib需要引入相关依赖:asm.jar,它使用字节码增强技术来实现。

3.6 AOP相关注解*

  • 1、@Pointcut
      切入点,常见的方式:采用表达式批量添加切入点。如下方法表示AopController下的所有public方法都添加该切面:
	@Pointcut(value = "execution(public * com.train.aop.controller.AopController.*(..))")
	public void pointCut(){
	
	}

  以execution(* com.itcodai.course09.controller..*.*(..)))表达式为例,语法:

execution() :表达式主体;
第一个 * 号的位置:表示返回值类型, * 表示所有类型;
包名:表示需要拦截的包名,后面的两个句点表示当前包和当前包的所有子包, com.itcodai.course09.controller 包、子包下所有类的方法;
第二个 * 号的位置:表示类名, * 表示所有类;
*(..) :这个星号表示方法名, * 表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数。

  • 2、@Before
      前置方法,在目标方法执行前触发。@Before注解指定的方法在切面切入目标方法之前执行,可以做一些log处理,也可以做一些信息的统计。
  • 3、@After
      后置方法,与@Before相反,在目标方法执行完毕后执行,也可以做一些完成某方法之后的log处理。
  • 4、@AfterReturning
      后置通知,在将返回值返回时执行。@AfterReturning注解和@After有些类似,区别在于@AfterReturning注解可以用来捕获切入方法执行完之后的返回值,对返回值进行业务逻辑上的增强处理。
  • 5、@AfterThrowing
      @AfterThrowing注解是当被切方法执行时抛出异常时,会进@AfterThrowing 注解的方法中执行,在该方法中可以做一些异常的处理逻辑。要注意的是throwing属性的值必须要和参数一致,否则会报错。该方法中的第二个入参即为抛出的异常。示例:
	@AfterThrowing(pointcut = "pointCut()", throwing = "ex")
	public void afterThrowing(JoinPoint joinPoint, Throwable ex)
  • 6、@Around
      环绕通知,可以说是使用最频繁的方式,会将整个方法包围起来。
  • 7、@EnableAspectJAutoProxy
      该注解表示开启AOP代理自动配置,一般加在配置类上。
  • 8、@Aspect
      定义一个切面类。

四、事务

  Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,Spring是无法提供事务功能的。Spring事务是在代码层面利用AOP实现,其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,执行完目标方法之后根据执行的情况提交或者回滚。

  事务能否生效数据库引擎是否支持事务是关键。比如常用的Mysql数据库默认使用支持事务的innodb引擎。但如果把数据库引擎变为myisam ,那么程序也就不再支持事务了。

  开启事务开关后,在代码里就可以使用@Transactional注解,就可以使用事务,这也是事务的一般使用方式。
  如果一次执行单条查询语句,则没有必要启用事务支持,数据库默认支持SQL执行期间的读一致性
  如果你一次执行多条查询语句,例如统计查询、报表查询。在这种场景下,多条查询SQL必须保证整体的读一致性。否则,在前条SQL查询之后,后条SQL查询之前,数据被其他用户改变,则该次整体的统计查询将会出现读数据不一致的状态。此时,应该启用事务支持。

4.1 声明式事务和编程式事务*

  Spring事务在具体使用时,可以分为两类:编程式事务和声明式事务。

  • 1、声明式事务
      大多数Spring框架的用户选择声明式事务管理,因为声明式事务管理要优于编程式事务管理,这正是spring倡导的非侵入式的开发方式。声明式事务唯一不足地方是,最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。
      Spring声明式事务使得我们无需要去处理获得连接、关闭连接、事务提交和回滚等这些操作。使用@Transactional注解开启声明式事务。

  声明式事务管理建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。

  • 2、编程式事务
      这种方式带来了很大的灵活性,但很难维护。
      这种方式通过TransactionTemplate或者TransactionManager手动管理事务,实际应用中很少使用(如使用,Spring推荐使用TransactionTemplate),但是对于理解Spring事务管理原理有帮助。
//使用TransactionTemplate进行编程式事务管理的示例
@Autowired
private TransactionTemplate transactionTemplate;
public void testTransaction() {
    transactionTemplate.execute(new TransactionCallbackWithoutResult() {
        @Override
        protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
            try {
               // .... 业务代码
            } catch (Exception e){
               //回滚
               transactionStatus.setRollbackOnly();
            }
         }
    });
}

//使用TransactionManager进行编程式事务管理示例
@Autowired
private PlatformTransactionManager transactionManager;
public void testTransaction() {
    TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
    try {
        // .... 业务代码
        transactionManager.commit(status);
    } catch (Exception e) {
        transactionManager.rollback(status);
    }
}

4.2 Spring事务管理接口

  Spring框架中,事务管理相关最重要的3个接口:

  PlatformTransactionManager:事务管理器,Spring事务策略的核心。
  TransactionDefinition:事务定义信息(事务隔离级别、传播行为、超时、只读、回滚规则)。
  TransactionStatus : 事务运行状态。

  可以把PlatformTransactionManager接口可以被看作是事务上层的管理者,TransactionDefinition和TransactionStatus这两个接口可以看作是事务的描述。PlatformTransactionManager会根据TransactionDefinition的定义,比如事务超时时间、隔离界别、传播行为等来进行事务管理 ,而TransactionStatus接口则提供了一些方法来获取事务相应的状态比如是否新事务、是否可以回滚等等。

4.2.1 PlatformTransactionManager

  Spring并不直接管理事务,而是提供了事务管理器接口PlatformTransactionManager 。
  通过这个接口,Spring为各个平台如JDBC( DataSourceTransactionManager )、Hibernate( HibernateTransactionManager )、JPA( JpaTransactionManager )等都提供了对应的事务管理器。
  PlatformTransactionManager接口中定义了三个方法:

public interface PlatformTransactionManager {
    //获得事务
    TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;
    //提交事务
    void commit(TransactionStatus var1) throws TransactionException;
    //回滚事务
    void rollback(TransactionStatus var1) throws TransactionException;
}
4.2.2 TransactionDefinition

  PlatformTransactionManager通过getTransaction(TransactionDefinition definition)方法来得到一个事务,这个方法里面的参数是TransactionDefinition,这个类就定义了一些基本的事务属性。
  事务属性可以理解成事务的一些基本配置,描述了事务策略如何应用到方法上。事务属性包含了 5 个方面:

  TransactionDefinition接口中定义了5个方法,以及一些表示事务属性的常量比如隔离级别、传播行为等等。

public interface TransactionDefinition {
    int PROPAGATION_REQUIRED = 0;
    int PROPAGATION_SUPPORTS = 1;
    int PROPAGATION_MANDATORY = 2;
    int PROPAGATION_REQUIRES_NEW = 3;
    int PROPAGATION_NOT_SUPPORTED = 4;
    int PROPAGATION_NEVER = 5;
    int PROPAGATION_NESTED = 6;
    int ISOLATION_DEFAULT = -1;
    int ISOLATION_READ_UNCOMMITTED = 1;
    int ISOLATION_READ_COMMITTED = 2;
    int ISOLATION_REPEATABLE_READ = 4;
    int ISOLATION_SERIALIZABLE = 8;
    int TIMEOUT_DEFAULT = -1;
    // 返回事务的传播行为,默认值为 REQUIRED。
    int getPropagationBehavior();
    //返回事务的隔离级别,默认值是 DEFAULT
    int getIsolationLevel();
    // 返回事务的超时时间,默认值为-1。如果超过该时间限制但事务还没有完成,则自动回滚事务。
    int getTimeout();
    // 返回是否为只读事务,默认值为 false
    boolean isReadOnly();
    @Nullable
    String getName();
}
4.2.3 TransactionStatus

  TransactionStatus接口用来记录事务的状态。该接口定义了一组方法,用来获取或判断事务的相应状态信息。
  TransactionStatus接口内容:

public interface TransactionStatus{
    boolean isNewTransaction(); // 是否是新的事物
    boolean hasSavepoint(); // 是否有恢复点
    void setRollbackOnly();  // 设置为只回滚
    boolean isRollbackOnly(); // 是否为只回滚
    boolean isCompleted(); // 是否已完成
}
4.2.4 事务管理器接口的实现

  Spring并不直接管理事务,而是提供了多种事务管理器,他们将事务管理的职责委托给Hibernate或者JTA等持久化机制所提供的相关平台框架的事务来实现。Spring事务管理器的接口是PlatformTransactionManager,通过这个接口,Spring为各个平台如JDBC、Hibernate等都提供了对应的事务管理器,但是具体的实现就是各个平台自己的事情了。

  • JDBC事务
      如果应用程序中直接使用JDBC来进行持久化,DataSourceTransactionManager会处理事务。为了使用DataSourceTransactionManager,你需要使用如下的XML将其装配到应用程序的上下文定义中:
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource" />
</bean>

  实际上,DataSourceTransactionManager是通过调用java.sql.Connection来管理事务,而后者是通过DataSource获取到的。通过调用连接的commit()方法来提交事务。同样,事务失败则通过调用rollback()方法进行回滚。

  • Hibernate事务
      如果应用程序的持久化是通过Hibernate实现的,那么你需要使用HibernateTransactionManager。对于Hibernate3,需要在Spring上下文定义中添加如下的 < bean > 声明:
<bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
    <property name="sessionFactory" ref="sessionFactory" />
</bean>

  sessionFactory属性需要装配一个Hibernate的session工厂,HibernateTransactionManager的实现细节是它将事务管理的职责委托给org.hibernate.Transaction对象,而后者是从Hibernate Session中获取到的。当事务成功完成时,HibernateTransactionManager将会调用Transaction对象的commit()方法,反之,将会调用rollback()方法。

  • Java持久化API事务(JPA)
      Hibernate多年来一直是事实上的Java持久化标准,但是现在Java持久化API作为真正的Java持久化标准进入大家的视野。如果你计划使用JPA的话,那你需要使用Spring的JpaTransactionManager来处理事务。你需要在Spring中这样配置JpaTransactionManager:
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
    <property name="sessionFactory" ref="sessionFactory" />
</bean>

  JpaTransactionManager只需要装配一个JPA实体管理工厂(EntityManagerFactory接口的任意实现)。JpaTransactionManager将与由工厂所产生的JPA EntityManager合作来构建事务。

  • Java原生API事务
      如果你没有使用以上所述的事务管理,或者是跨越了多个事务管理源(比如两个或者是多个不同的数据源),你就需要使用JtaTransactionManager:
<bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager">
    <property name="transactionManagerName" value="java:/TransactionManager"/>
</bean>

  JtaTransactionManager将事务管理的责任委托给UserTransaction和
TransactionManager对象,其中事务成功完成通过UserTransaction.commit()方法
提交,事务失败通过UserTransaction.rollback()方法回滚。

4.3 事务的5个属性

4.3.1 传播行为*

  Spring事务的传播行为:当多个事务同时存在(当一个事务方法被另一个事务方法调用)的时候,Spring如何处理这些事务的行为。 事务的传播行为,默认值为 Propagation.REQUIRED
  在 TransactionDefinition 定义中包括了如下几个表示传播行为的常量:

public interface TransactionDefinition {
    int PROPAGATION_REQUIRED = 0;
    int PROPAGATION_SUPPORTS = 1;
    int PROPAGATION_MANDATORY = 2;
    int PROPAGATION_REQUIRES_NEW = 3;
    int PROPAGATION_NOT_SUPPORTED = 4;
    int PROPAGATION_NEVER = 5;
    int PROPAGATION_NESTED = 6;
   //......
}

  为了方便使用,Spring会相应地定义了一个枚举类Propagation:

public enum Propagation {
    REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),
    SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),
    MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY),
    REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),
    NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED),
    NEVER(TransactionDefinition.PROPAGATION_NEVER),
    NESTED(TransactionDefinition.PROPAGATION_NESTED);
    private final int value;
    Propagation(int value) {
        this.value = value;
    }
    public int value() {
       return this.value;
    }
}

  七个分类:

事务传播行为类型说明
REQUIRED (重要)如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。
REQUIRES_NEW(重要)当前的方法必须启动新事务,并且在它自己的事务内运行,如果有事务正在运行,应该将它挂起。
SUPPORTS如果有事务在运行,当前的方法就在这个事务内运行,否则它可以不运行在事务中
NOT_SUPPORTED当前方法不应该运行在事务中,如果当前存在事务,就把当前事务挂起。
NEVER当前方法不应该运行在事务中,如果当前存在事务,则抛出异常。
MANDATORY当前的方法必须运行在事务内部,如果当前没有事务,就抛出异常。
NESTED(重要)如果有事务在运行,当前的方法就应该在这个事务的嵌套事务内运行,否则就启动一个新事物,并在它自己的事务内运行

  使用示例:

	@Transactional(propagation = Propagation.REQUIRED)
  • 1、TransactionDefinition.PROPAGATION_REQUIRED
      经常使用的@Transactional注解默认使用就是这个事务传播行为。如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。也就是说:

  1、如果外部方法没有开启事务的话,Propagation.REQUIRED修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
  2、如果外部方法开启事务并且被Propagation.REQUIRED的话,所有 Propagation.REQUIRED 修饰的内部方法和外部方法均属于同一事务 ,只要一个方法回滚,整个事务均回滚。

  比如aMethod()和bMethod()使用的都是PROPAGATION_REQUIRED传播行为的话,两者使用的就是同一个事务,只要其中一个方法回滚,整个事务均回滚。

Class A {
    @Transactional(propagation=propagation.PROPAGATION_REQUIRED)
    public void aMethod() {
        //do something
        B b = new B();
        b.bMethod();
   }
}
Class B {
    @Transactional(propagation=propagation.PROPAGATION_REQUIRED)
    public void bMethod() {
        //do something
   }
}
  • 2、TransactionDefinition.PROPAGATION_REQUIRES_NEW
      创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
      如果bMethod()使用PROPAGATION_REQUIRES_NEW事务传播行为修饰,aMethod还是用PROPAGATION_REQUIRED修饰的话。如果aMethod()发生异常回滚,bMethod()不会跟着回滚,因为bMethod()开启了独立的事务。但是,如果bMethod()抛出了未被捕获的异常并且这个异常满足事务回滚规则的话, aMethod()同样也会回滚,因为这个异常被aMethod()的事务管理机制检测到了。
Class A {
    @Transactional(propagation=propagation.PROPAGATION_REQUIRED)
    public void aMethod() {
        //do something
        B b = new B();
        b.bMethod();
   }
}
Class B {
    @Transactional(propagation=propagation.REQUIRES_NEW)
    public void bMethod() {
        //do something
   }
}
  • 3、TransactionDefinition.PROPAGATION_NESTED
      如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。也就是说:

  1、在外部方法未开启事务的情况下,Propagation.NESTED和Propagation.REQUIRED作用相同,修饰的内部方法都会新开启自己的事务,且开启的事务相互独立,互不干扰。
  2、如果外部方法开启事务的话,Propagation.NESTED修饰的内部方法属于外部事务的子事务。外部主事务回滚的话,子事务也会回滚,而内部子事务可以单独回滚而不影响外部主事务和其他子事务。

  如果aMethod()回滚的话,bMethod()和bMethod2()都要回滚,而bMethod()回滚的话,并不会造成aMethod()和bMethod2()回滚。

Class A {
    @Transactional(propagation=propagation.PROPAGATION_REQUIRED)
    public void aMethod {
        //do something
        B b = new B();
        b.bMethod();
        b.bMethod2();
   }
}
Class B {
    @Transactional(propagation=propagation.PROPAGATION_NESTED)
    public void bMethod {
       //do something
 }
    @Transactional(propagation=propagation.PROPAGATION_NESTED)
    public void bMethod2 {
      //do something
    }
}
  • 4、 TransactionDefinition.PROPAGATION_MANDATORY
      必须在一个事务中执行。如果当前存在事务,则加入该事务;如果当前没有事务,则抛出IllegalTransactionStateException异常。【使用较少】

  • 5、剩下的3种事务传播行为,事务将不会发生回滚
      TransactionDefinition.PROPAGATION_SUPPORTS : 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
      TransactionDefinition.PROPAGATION_NOT_SUPPORTED : 以非事务方式运行,如果当前存在事务,则把当前事务挂起。
      TransactionDefinition.PROPAGATION_NEVER : 以非事务方式运行,如果当前存在事务,则抛出异常。

  • 传播行为配置示例

	<!--开启注解的方式--> 
	<tx:annotation-driven transaction-manager="transactioManager" />

  Java代码中使用@Transactional注解:

	@Transactional(propagation=Propagation.REQUIRED)
4.3.2 隔离级别*

  TransactionDefinition接口中定义了五个表示隔离级别的常量:

public interface TransactionDefinition {
    //......
    int ISOLATION_DEFAULT = -1;
    int ISOLATION_READ_UNCOMMITTED = 1;
    int ISOLATION_READ_COMMITTED = 2;
    int ISOLATION_REPEATABLE_READ = 4;
    int ISOLATION_SERIALIZABLE = 8;
    //......
}

  为了方便使用,Spring 也相应地定义了一个枚举类: Isolation:

public enum Isolation {
    DEFAULT(TransactionDefinition.ISOLATION_DEFAULT),
    READ_UNCOMMITTED(TransactionDefinition.ISOLATION_READ_UNCOMMITTED),
    READ_COMMITTED(TransactionDefinition.ISOLATION_READ_COMMITTED),
    REPEATABLE_READ(TransactionDefinition.ISOLATION_REPEATABLE_READ),
    SERIALIZABLE(TransactionDefinition.ISOLATION_SERIALIZABLE); 
    private final int value;
    Isolation(int value) {
        this.value = value;
    }
    public int value() {
        return this.value;
    }
}
  • TransactionDefinition.ISOLATION_DEFAULT
       :使用后端数据库默认的隔离级别,MySQL默认采用REPEATABLE_READ隔离级别。
  • TransactionDefinition.ISOLATION_READ_UNCOMMITTED
       读未提交,最低的隔离级别,使用这个隔离级别很少,因为它允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
  • TransactionDefinition.ISOLATION_READ_COMMITTED
       读已提交,允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
  • TransactionDefinition.ISOLATION_REPEATABLE_READ
       可重复读,对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
  • TransactionDefinition.ISOLATION_SERIALIZABLE
      串行,最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能,通常情况下也不会用到该级别。
  • 隔离级别配置示例
    @Transactional(isolation = Isolation.READ_COMMITTED)
4.3.3 回滚规则*

  当@Transactional注解作用于类上时,该类的所有public方法将都具有该类型的事务属性。同时,我们也可以在方法级别使用该标注来覆盖类级别的定义。如果类或者方法加了这个注解,那么这个类里面的方法抛出异常,就会回滚,数据库里面的数据也会回滚。
  在@Transactional注解中如果不配置rollbackFor属性,那么事务只会在遇到RuntimeException和Error的时候才会回滚。针对非运行时异常,如果要进行事务回滚的话,可以在@Transactional注解中使用rollbackFor属性来指定异常,比如 @Transactional(rollbackFor =Exception.class),这样就没有问题了,所以在实际项目中,一定要指定异常。

  • 回滚规则配置示例
    @Transactional(rollbackFor = Exception.class)
4.3.4 只读属性

  对于只有读取数据查询的事务,可以指定事务类型为readonly,即只读事务。只读事务不涉及数据的修改,数据库会提供一些优化手段,适合用在有多条数据库查询操作的方法中。
  MySQL默认对每一个新建立的连接都启用了autocommit模式。在该模式下,每一个发送到MySQL服务器的sql语句都会在一个单独的事务中进行处理,执行结束后会自动提交事务,并开启一个新的事务。
  但是,如果你给方法加上了Transactional注解的话,这个方法执行的所有sql会被放在一个事务中。如果声明了只读事务的话,数据库就会去优化它的执行,并不会带来其他的什么收益。如果不加Transactional,每条sql会开启一个单独的事务,中间被其它事务改了数据,都会实时读取到最新值。

  • 只读属性配置示例
    @Transactional(readOnly = true) 
4.3.5 超时参数

  所谓事务超时,就是指一个事务所允许执行的最长时间。如果超过该时间限制但事务还没有完成,则自动回滚事务。在TransactionDefinition中以int的值来表示超时时间,其单位是秒,默认值为-1。

  • 只读属性配置示例
    //超时时间是30秒
    @Transactional(timeout=30) 

4.4 事务的相关问题

4.4.1 使用@Transactional的注意事项*
  • 1、@Transactional 只能被应用到public方法上
      对于其它非public的方法,如果标记了@Transactional也不会报错,但方法没有事务功能。
      只有@Transactional注解应用到 public 方法,才能进行事务管理。这是因为在使用Spring AOP代理时,Spring在调用TransactionInterceptor在目标方法执行前后进行拦截之前,DynamicAdvisedInterceptor(CglibAopProxy的内部类)的intercept方法或 JdkDynamicAopProxy的invoke方法会间接调用 AbstractFallbackTransactionAttributeSource(Spring通过这个类获取@Transactional注解的事务属性配置属性信息)的computeTransactionAttribute方法:
     protected TransactionAttribute computeTransactionAttribute(Method method,Class<?> targetClass) {
         if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
              return null;
         }
         //...
     }
  • 2、@Transactional被用在具体的类上时才会生效
      @Transactional注解的出现不足于开启事务行为,它仅仅是一种元数据,能够被可以识别@Transactional注解和上述的配置适当的具有事务行为的Beans所使用。
      Spring团队的建议是你在具体的类(或类的方法)上使用 @Transactional 注解,而不要使用在类所要实现的任何接口上。注解是不能继承的,这就意味着如果你正在使用基于类的代理时,那么事务的设置将不能被基于类的代理所识别,而且对象也将不会被事务代理所包装。
4.4.2 Spring AOP自调用问题

  若同一类中的其他没有@Transactional注解的方法内部调用有@Transactional注解的方法,有@Transactional 注解的方法的事务会失效。这是由于Spring AOP代理的原因造成的,因为只有当@Transactional注解的方法在类以外被调用
的时候,Spring事务管理才生效。
  如:MyService类中的method1()调用method2(),就会导致method2()的事务失效。

@Service
public class MyService {
    private void method1() {
        method2();
        //......
    }
    @Transactional
    public void method2() {
       //......
    }
}

  解决办法就是避免同一类中自调用,或者使用AspectJ取代Spring AOP代理。

4.4.3 如果在一个事务中,代码业务流程很长,会有什么问题吗*

  可能出现事务超时。可以使用@Transactional(timeout = 60)这样的注解。
  如果用这个注解描述一个方法的话,线程已经跑到方法里面,如果已经过去60秒了还没跑完这个方法,并且线程在这个方法中的后面还有涉及到对数据库的增删改查操作时会报事务超时错误(会回滚)。如果已经过去60秒了还没跑完,但是后面已经没有涉及到对数据库的增删改查操作,那么这时不会报事务超时错误(不会回滚)。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值