Spring学习(一)IOC、AOP、事务的使用

文章目录

一、Spring概述

1.1 什么是spring

  Spring是一个轻量级Java开发框架,目的是为了解决企业级应用开发的业务逻辑层和其他各层的耦合问题。它是一个分层的JavaSE/JavaEE full-stack(一站式)轻量级开源框架,为开发Java应用程序提供全面的基础架构支持。Spring负责基础架构,因此Java开发者可以专注于应用程序的开发。
  Spring最根本的使命是解决企业级应用开发的复杂性,即简化Java开发。
  Spring的功能底层都依赖于它的两个核心特性,也就是依赖注入(dependency injection,DI)和面向切面编程(aspect-oriented programming,AOP)。
  为了降低Java开发的复杂性,Spring采取了以下4种关键策略:

  1. 基于POJO的轻量级和最小侵入性编程
  2. 通过依赖注入和面向接口实现松耦合
  3. 基于切面和惯例进行声明式编程
  4. 通过切面和模板减少样板式代码

  Spring框架的核心:IoC容器和AOP模块。通过IoC容器管理POJO对象以及他们之间的耦合关系;通过AOP以动态非侵入的方式增强服务

1.2 Spring的优缺点

优点

  • 1、方便解耦,简化开发(IOC)
      Spring就是一个大工厂,可以将所有对象的创建和依赖关系的维护,交给Spring管理。
  • 2、AOP编程的支持
      Spring提供面向切面编程,可以方便的实现对程序进行权限拦截、运行监控等功能。
  • 3、声明式事务的支持
      只需要通过配置就可以完成对事务的管理,而无需手动编程。
  • 4、方便程序的测试
      Spring对Junit4支持,可以通过注解方便的测试Spring程序。
  • 5、方便集成各种优秀框架
      Spring不排斥各种优秀的开源框架,其内部提供了对各种优秀框架的直接支持(如:Struts、Hibernate、MyBatis等)。
  • 6、降低JavaEE API的使用难度
      Spring对JavaEE开发中非常难用的一些API(JDBC、JavaMail、远程调用等),都提供了封装,使这些API应用难度大大降低。

缺点:Spring依赖反射,反射影响性能。

1.3 Spring由哪些模块组成

  Spring 5 的模块结构图:

  Spring的较核心模块:

  • 1、Spring core(核心容器)
      Beans:负责Bean工厂中Bean的装配,所谓Bean工厂即是创建对象的工厂,Bean的装配也就是对象的创建工作;
      Core:这个模块即是负责IOC(控制反转)最基本的实现;
      Context:Spring的IOC容器,因大量调用Spring Core中的函数,整合了Spring的大部分功能。Bean创建好对象后,由Context负责建立Bean与Bean之间的关系并维护。所以也可以把Context看成是Bean关系的集合;
      SpEl:即Spring Expression Language(Spring表达式语言)。
  • 2、Data Access/Integration(数据访问/集成)
      JDBC:对JDBC的简单封装;
      ORM:支持数据集成框架的封装(如Mybatis,Hibernate);
      OXM:即Object XML Mapper,它的作用是在Java对象和XML文档之间来回转换;
      JMS:生产者和消费者的消息功能的实现;
      Transations:事务管理。
  • 3、Web
      WebSocket:提供Socket通信,web端的的推送功能;
      Servlet:Spring MVC框架的实现;
      Web:包含web应用开发用到Spring框架时所需的核心类,包括自动载入WebApplicationContext特性的类,Struts集成类、文件上传的支持类、Filter类和大量辅助工具类;
      Portlet:实现web模块功能的聚合。
  • 4、AOP(Spring面向切面编程)
      AOP把一个业务流程分成几部分,例如权限检查、业务处理、日志记录,每个部分单独处理,然后把它们组装成完整的业务流程。
  • 5、Aspects
      提供了与AspectJ的集成功能,AspectJ是一个功能强大且成熟的AOP框架。
  • 6、Instrumentation(设备)
      相当于一个检测器,提供对JVM以及对Tomcat的检测。
  • 7、Messaging(消息)
      Spring提供的对消息处理的功能。
  • 8、Test(测试)
      在做单元测试时,Spring会帮初始化一些测试过程当中需要用到的资源对象。

1.4 Spring框架中的设计模式

  1. 工厂模式
     BeanFactory就是简单工厂模式的体现,用来创建对象的实例;
  2. 单例模式
     Bean默认为单例模式;
  3. 代理模式
     Spring的AOP功能用到了JDK的动态代理和CGLIB字节码生成技术;
  4. 模板方法
     用来解决代码重复的问题。比如. RestTemplate, JmsTemplate, JpaTemplate。
  5. 观察者模式
     定义对象键一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知被制动更新,即spring 事件驱动模型。

1.5 Spring框架中有哪些不同类型的事件

  Spring的事件通知机制是一项很有用的功能,使用事件机制可以将相互耦合的代码解耦,从而方便功能的修改与添加。
  举个例子,假设有一个添加评论的方法,在评论添加成功之后需要进行修改redis缓存、给用户添加积分等等操作。在以前的代码中,可以使用观察者模式来解决这个问题。Spring中已经存在了一个升级版观察者模式的机制,这就是监听者模式。通过该机制,我们就可以发送接收任意的事件并处理。

  监听者模式包含了一个监听者Listener与之对应的事件Event,还有一个事件发布者EventPublish,过程就是EventPublish发布一个事件,被监听者捕获到,然后执行事件相应的方法。

  Spring 提供了以下5种标准的事件:

  • 1、上下文更新事件
     在调用ConfigurableApplicationContext 接口中的refresh()方法时被触发。
  • 2、上下文开始事件
     当容器调用ConfigurableApplicationContext的Start()方法开始/重新开始容器时触发该事件。
  • 3、上下文停止事件
     当容器调用ConfigurableApplicationContext的Stop()方法停止容器时触发该事件。
  • 4、上下文关闭事件
     当ApplicationContext被关闭(close()方法)时触发该事件。容器被关闭时,其管理的所有单例Bean都被销毁。
  • 5、请求处理事件
     在Web应用中,当一个http请求(request)结束触发该事件。

  如果一个bean实现了ApplicationListener接口,当一个ApplicationEvent 被发布以后,bean会自动被通知,其实就是接下来要说的自定义事件。
  除了上面介绍的五种事件以外,还可以通过扩展ApplicationEvent 类来开发自定义的事件。示例:

	public class CustomApplicationEvent extends ApplicationEvent{
    
    
	    public CustomApplicationEvent ( Object source, final String msg ){
    
    
	        super(source);
	        System.out.println("Created a Custom event");
	    }
	}

  为了监听这个事件,还需要创建一个监听器:

	public class CustomEventListener implements ApplicationListener < CustomApplicationEvent >{
    
    
	    @Override
	    public void onApplicationEvent(CustomApplicationEvent applicationEvent) {
    
    
	        //handle event
	    }
	}

  之后通过applicationContext接口的publishEvent()方法来发布自定义事件:

	CustomApplicationEvent customEvent = 
		new CustomApplicationEvent(applicationContext, "Test message");
	applicationContext.publishEvent(customEvent);

二、IOC

2.1 IOC理论简单分析

  介绍IOC之前,先了解为什么要有IOC功能,先看个例子。

  • 1、先写一个UserDao接口
	public interface UserDao {
    
    
	    public void getUser();
	}
  • 2、Dao的实现类
	public class UserDaoImpl implements UserDao {
    
    
	    @Override
	    public void getUser() {
    
    
	        System.out.println("获取用户数据");
	    }
	}
  • 3、UserService的接口
	public interface UserService {
    
    
  	    public void getUser();
	}
  • 4、Service的实现类
	public class UserServiceImpl implements UserService {
    
    
	    private UserDao userDao = new UserDaoImpl();
	    @Override
	    public void getUser() {
    
    
	        userDao.getUser();
	    }
	}
  • 5、测试类
	@Test
	public void test(){
    
    
	    UserService service = new UserServiceImpl();
	    service.getUser();
	}
  • 6、假如此时要增加Userdao的实现类,则UserService的实现类也要对应增加
	public class UserDaoMySqlImpl implements UserDao {
    
    
	    @Override
	    public void getUser() {
    
    
	        System.out.println("MySql获取用户数据");
	    }
	}
	
	public class UserServiceImpl implements UserService {
    
    
	    private UserDao userDao = new UserDaoMySqlImpl();
	    @Override
	    public void getUser() {
    
    
	        userDao.getUser();
	    }
	}

  这种功能的实现方式,耦合性比较高。要降低耦合性,一种比较简单的方式是在UserService的实现类中,动态调用UserDao的不同实现类,示例:

	public class UserServiceImpl implements UserService {
    
    
	    private UserDao userDao;
	    // 利用set实现
	    public void setUserDao(UserDao userDao) {
    
    
	        this.userDao = userDao;
	    }
	    @Override
	    public void getUser() {
    
    
	        userDao.getUser();
	    }
	}

  测试类:

	@Test
	public void test(){
    
    
	    UserServiceImpl service = new UserServiceImpl();
	    service.setUserDao( new UserDaoMySqlImpl() );
	    service.getUser();
	    service.setUserDao( new UserDaoOracleImpl() );
	    service.getUser();
	}

  在未改进的代码中,每增加一个Dao的实现类,就要增加一个Service的实现类;在改进后的代码中,即使用户创建很多Dao的实现类,也不必再增加Service的实现类,就能直接调用新的Dao实现类中的方法。
  将这种思想再抽象一些:当开发者需要一个对象时,可以从一个地方(容器)直接取,而不用自己创建,这就是IOC的基本理解。

2.2 什么是Spring IOC 容器

  控制反转即IoC (Inversion of Control),它把传统上由程序代码直接操控的对象的调用权交给容器,通过容器来实现对象组件的装配和管理。所谓的“控制反转”概念就是对组件对象控制权的转移,从程序代码本身转移到了外部容器

  Spring IOC 负责创建对象,管理对象,装配对象,配置对象,并且管理这些对象的整个生命周期。即:IoC容器就是具有依赖注入功能的容器,IoC容器负责实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。应用程序无需直接在代码中创建相关的对象,应用程序由IoC容器进行组装。
  Spring IoC容器如何知道哪些是它管理的对象呢?这就需要配置文件,Spring IoC容器通过读取配置文件中的配置元数据,通过元数据对应用中的各个对象进行实例化及装配。由IoC容器管理的那些组成应用程序的对象我们就叫它Bean, Bean就是由Spring容器初始化、装配及管理的对象。
  IOC容器的职责是:

  1. 实例化Bean
  2. 把Bean关联在一起
  3. 配置Bean
  4. 管理Bean的整个生命周期

2.3 控制反转有什么作用

  1、管理对象的创建和依赖关系的维护;
  2、解耦。

2.4 通过无参构造方法来创建Bean【了解即可】

  Spring Bean是那些形成Spring应用的Java对象,它们被Spring IOC容器初始化、装配和管理。
  导入jar:

  	 <dependencies>
		<dependency>
		   <groupId>org.springframework</groupId>
		   <artifactId>spring-webmvc</artifactId>
		   <version>5.1.10.RELEASE</version>
		</dependency>
     </dependencies>

  写实体类:

public class Hello {
    
    
	private String name;
	public String getName() {
    
    
	    return name;
	}
	public void setName(String name) {
    
    
	    this.name = name;
	}
	public void show(){
    
    
	    System.out.println("Hello,"+ name );
	}
}

  Spring的配置文件,在src目录下,命名为beans.xml:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">

   <bean id="hello" class="com.spring.test.Hello"></bean>
</beans>

  测试类:

public class SpringTest {
    
    
	public static void main(String[] args) {
    
    
		test();
	}
	
	public static void test(){
    
    
	   //解析beans.xml文件 , 生成管理相应的Bean对象
	   ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
	   //getBean : 参数即为spring配置文件中bean的id
	   Hello hello = (Hello) context.getBean("hello");
	   //Hello,null
	   hello.show();
	}
}

2.5 通过有参构造方法来创建Bean【了解即可】

  通过有参构造方法来创建对象的方式有三种,示例:

<!-- 第一种根据index参数下标设置 -->
<bean id="user1" class="com.test.pojo.User">
   <!-- index指构造方法 , 下标从0开始 -->
   <constructor-arg index="0" value="zhangsan"/>
</bean>
<!-- 第二种根据参数名字设置 -->
<bean id="user2" class="com.test.pojo.UserT">
   <!-- name指参数名 -->
   <constructor-arg name="name" value="lisi"/>
</bean>
<!-- 第三种根据参数类型设置 -->
<bean id="user3" class="com.test.pojo.UserT">
   <constructor-arg type="java.lang.String" value="wangwu"/>
</bean>

2.6 通过set注入属性的方法来创建Bean【了解即可】

  在上面的两种创建Bean的方式中,要求Bean有对象的构造方法,那么有没有其他比较灵活的注入方式呢?当然是有的,就是set方法的注入方式,这种方式要求Bean有对应的setXXX方法。
  配置文件示例(只贴与之前不同的部分):

   <bean id="hello" class="com.spring.test.Hello">
       <property name="name" value="Spring"/>
   </bean>

  测试结果:

Hello,Spring

2.6 通过set方式注入不同类型数据

  在上一小结中,通过set的方式注入了最普通的字符串,来创建对象。其实,除了字符串,还可以通过set的放入注入其他数据,接下来一一举例。

  • 1、注入其他对象
 <bean id="addr" class="com.kuang.pojo.Address">
     <property name="address" value="重庆"/>
 </bean>
 
 <bean id="student" class="com.test.pojo.Student">
     <property name="name" value="小明"/>
     <property name="address" ref="addr"/>
 </bean>
  • 2、注入数组
 <bean id="student" class="com.kuang.pojo.Student">
     <property name="name" value="小明"/>
     <property name="address" ref="addr"/>
     <property name="books">
         <array>
             <value>英雄志</value>
             <value>沧浪之水</value>
             <value>梦的解析</value>
         </array>
     </property>
 </bean>
  • 3、注入List
 <property name="interest">
     <list>
         <value>听歌</value>
         <value>看电影</value>
         <value>爬山</value>
     </list>
 </property>

 <property name="empList">
     <list>
         <ref bean="emp1" />
         <ref bean="emp2"/>
     </list>
 </property>
  • 4、注入Map
 <property name="story">
     <map>
         <entry key="余华" value="活着"/>
         <entry key="东野圭吾" value="白夜行"/>
     </map>
 </property>

 <property name="empMap">
     <map>
         <entry key="1" value-ref="emp1" />
         <entry key="2" value-ref="emp2" />
     </map>
 </property>
  • 5、注入Set
 <property name="movie">
     <set>
         <value>兹山鱼谱</value>
         <value>浪客剑心</value>
     </set>
 </property>

 <property name="empSets">
     <set>
          <ref bean="emp1" />
          <ref bean="emp2"/>
     </set>
 </property>
  • 6、注入Null
      在Spring中,不仅可以注入一个Null值,也可以注入空字符串。
 <property name="wife">
	 <null/>
 </property>

 <property name="name">
      <null></null>
 </property>
  • 7、注入属性
      用于注入键值对,键和值都只能为String类型。
 <property name="info">
     <props>
         <prop key="学号">20190604</prop>
         <prop key="性别"></prop>
         <prop key="姓名">小明</prop>
     </props>
 </property>

2.7 构造器依赖注入和 Setter方法注入的区别【了解即可】

  • 1、Setter方法注入
     1、设值注入需要该Bean包含这些属性的setter方法
     2、对于复杂的依赖关系,如果采用构造注入,会导致构造器过于臃肿。Spring在创建Bean实例时,需要同时实例化器依赖的全部实例,因而导致性能下降。而使用设值注入,则能避免这些问题。
     3、尤其是在某些属性可选的情况下,多参数的构造器显得更加笨重。

  • 2、构造注入
     1、构造注入需要该Bean包含带有这些属性的构造器
     2、构造注入可以在构造器中决定依赖关系的注入顺序,优先依赖的优先注入。例如,组件中其他依赖关系的注入,常常要依赖于DataSrouce的注入。采用构造注入,可以在代码中清晰的决定注入顺序。
     3、对于依赖关系无需变化的Bean,构造注入更有用处。因为没有Setter方法,所有的依赖关系全部在构造器内设定。因此,无需担心后续的代码对依赖关系产生破坏。
     4、依赖关系只能在构造器中设定,则只有组件的创建者才能改变组件的依赖关系。对组件的调用者而言,组件内部的依赖关系完全透明,更符合高内聚的原则。

  在两种方式的选择上:最好的解决方案是用构造器参数实现强制依赖,setter方法实现可选依赖

2.8 Bean的作用域

  当定义一个Bean在Spring里,我们还能给这个bean声明一个作用域。它可以通过bean 定义中的scope属性来定义。Spring框架支持以下五种bean的作用域:

类别 说明 注解
singleton 默认值 , 容器初始时创建 bean 实例, 在整个容器的生命周期内只创建这一个bean @Scope(“singleton”)
prototype 容器初始化时不创建bean的实例,而在每次请求时都创建一个新的Bean的实例 @Scope(“prototype”)
request 在每一次http请求时会创建一个实例,该实例仅在当前http request有效 @Scope(“request”)
session 在每一次http请求时会创建一个实例,该实例仅在当前http session有效 @Scope(“session”)
globalSession 全局Session,类似于标准的HTTP Session作用域,不过它仅仅在基于portlet的web应用中才有意义。 @SessionScope

   spring4.x的版本中包含两种作用域:request和session作用域,不过这两种作用域几乎不用,因此在5版本的时候被淘汰了。
  在这几类作用域中,最常用的是Singleton和Prototype

  • 1、Singleton
      当一个Bean的作用域为Singleton,那么Spring IoC容器中只会存在一个共享的Bean实例,并且所有对Bean的请求,只要id与该bean定义相匹配,则只会返回Bean的同一实例。
      Singleton是单例类型,就是在创建起容器时就同时自动创建了一个Bean的对象,每次获取到的对象都是同一个对象。示例:
   <bean id="hello" class="com.spring.test.Hello" scope="singleton">
       <property name="name" value="Spring"/>
   </bean>

  测试:

	public static void test(){
    
    
	   //解析beans.xml文件 , 生成管理相应的Bean对象
	   ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
	   //getBean : 参数即为spring配置文件中bean的id
	   Hello hello1 = (Hello) context.getBean("hello");
	   Hello hello2 = (Hello) context.getBean("hello");
	   //true
	   System.out.println(hello1==hello2);
	}
  • 2、Prototype
      当一个Bean的作用域为Prototype,表示一个Bean定义对应多个对象实例。Prototype作用域的Bean会导致在每次对该Bean请求(将其注入到另一个bean中,或者以程序的方式调用容器的getBean()方法)时都会创建一个新的Bean实例。
      Prototype是原型类型,它在我们创建容器的时候并没有实例化,而是当我们获取Bean的时候才会去创建一个对象,而且我们每次获取到的对象都不是同一个对象。示例:
   <bean id="hello" class="com.spring.test.Hello" scope="prototype">
       <property name="name" value="Spring"/>
   </bean>

  测试:

	public static void test(){
    
    
	   //解析beans.xml文件 , 生成管理相应的Bean对象
	   ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
	   //getBean : 参数即为spring配置文件中bean的id
	   Hello hello1 = (Hello) context.getBean("hello");
	   Hello hello2 = (Hello) context.getBean("hello");
	   //false
	   System.out.println(hello1==hello2);
	}
  • 3、Request
      当一个Bean的作用域为Request,表示在一次HTTP请求中,一个Bean定义对应一个实例;即每个HTTP请求都会有各自的bean实例,它们依据某个Bean定义创建而成。该作用域仅在基于Web的Spring ApplicationContext情形下有效。示例:
	<bean id="loginAction" class="cn.csdn.LoginAction" scope="request"/>
  • 4、Session
      当一个Bean的作用域为Session,表示在一个HTTP Session中,一个Bean定义对应一个实例。该作用域仅在基于Web的Spring ApplicationContext情形下有效。
	<bean id="userPreferences" class="com.foo.UserPreferences" scope="session"/>
  • 5、Global Session
      当一个bean的作用域为Global Session,表示在一个全局的HTTP Session中,一个bean定义对应一个实例。典型情况下,仅在使用portlet context的时候有效。该作用域仅在基于Web的Spring ApplicationContext情形下有效。

  默认的Spring Bean 的作用域是Singleton。使用Prototype作用域需要慎重的思考,因为频繁创建和销毁Bean会带来很大的性能开销。

2.9 Spring框架中的单例Bean是线程安全的吗

  不是,Spring框架中的单例bean不是线程安全的
  Spring中的Bean默认是单例模式,Spring框架并没有对单例Bean进行多线程的封装处理。
  如果单例Bean是一个无状态Bean,也就是线程中的操作不会对Bean的成员执行查询以外的操作,那么这个单例Bean是线程安全的。比如Spring MVC的 Controller、Service、Dao等,这些Bean大多是无状态的,只关注于方法本身。
  对于有状态的Bean,Spring官方提供的Bean,一般提供了通过ThreadLocal去解决线程安全的方法,比如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等。

  • 有状态对象
      就是有实例变量的对象,可以保存数据,是非线程安全的。每个用户有自己特有的一个实例,在用户的生存期内,bean保持了用户的信息,即“有状态”。一旦用户灭亡(调用结束或实例结束),bean的生命期也告结束。
  • 无状态对象
      就是没有实例变量的对象,不能保存数据,是不变类,是线程安全的。Bean一旦实例化就被加进会话池中,各个用户都可以共用。即使用户已经消亡,Bean的生命期也不一定结束,它可能依然存在于会话池中,供其他用户调用。由于没有特定的用户,那么也就不能保持某一用户的状态,所以叫无状态Bean。

2.10 Spring如何处理线程并发问题

  在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为Singleton作用域,Spring对一些Bean中非线程安全状态采用ThreadLocal进行处理,解决线程安全问题
  ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。同步机制采用了“时间换空间”的方式,仅提供一份变量,不同的线程在访问前需要获取锁,没获得锁的线程则需要排队。而ThreadLocal采用了“空间换时间”的方式。
  ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。

2.11 BeanFactory 和 ApplicationContext的区别

  在上面的示例代码中,用到了ClassPathXmlApplicationContext这个类,用于加载Spring的配置文件,继而创建Bean对象。
  BeanFactory和ApplicationContext是Spring的两大核心接口,都可以当做Spring的容器。ApplicationContext是BeanFactory的子接口

  BeanFactory 简单粗暴,可以理解为就是个 HashMap,Key 是 BeanName,Value 是 Bean 实例。通常只提供注册(put),获取(get)这两个功能。可以称之为 “低级容器”。
  ApplicationContext 可以称之为 “高级容器”,因为比 BeanFactory 多了更多的功能。

  BeanFactory和ApplicationContext 接口及其子类图:

  • 1、依赖关系
      BeanFactory:是Spring里面最顶级的接口,包含了各种Bean的定义,读取bean配置文档,管理bean的加载、实例化,控制bean的生命周期,维护bean之间的依赖关系。
      ApplicationContext接口作为BeanFactory的派生,除了提供BeanFactory所具有的功能外,还提供了更完整的框架功能
  1. 继承MessageSource,因此支持国际化
  2. 统一的资源文件访问方式
  3. 提供在监听器中注册bean的事件
  4. 同时加载多个配置文件。
  5. 载入多个(有继承关系)上下文 ,使得每一个上下文都专注于一个特定的层次,比如应用的web层。
  • 2、加载方式
      BeanFactroy采用的是延迟加载形式来注入Bean的,即只有在使用到某个Bean时(调用getBean()),才对该Bean进行加载实例化
      ApplicationContext,它是在容器启动时,一次性创建了所有的Bean
      相对于基本的BeanFactory,ApplicationContext 唯一的不足是占用内存空间。当应用程序配置Bean较多时,程序启动较慢。
  • 3、底层资源的访问
      ApplicationContext扩展了ResourceLoader(资源加载器)接口,从而可以用来加载多个Resource,而BeanFactory是没有扩展ResourceLoader

  绝大多数情况下建议使用ApplicationContext,因为ApplicationContext包含BeanFactory的所有功能,因此通常建议优先于BeanFactory使用它。

2.12 ApplicationContext的实现类

ApplicationContext常用实现类 作用
AnnotationConfigApplicationContext(常用) 从一个或多个基于Java的配置类中加载上下文定义,适用于Java注解的方式
ClassPathXmlApplicationContext(常用) 从类路径下的一个或多个xml配置文件中加载上下文定义,适用于xml配置的方式 ,需要正确设置classpath因为这个容器将在classpath里找bean配置
FileSystemXmlApplicationContext (常用) 从文件系统下的一个或多个xml配置文件中加载上下文定义,也就是说系统盘符中加载xml配置文件,XML Bean 配置文件的全路径名必须提供给它的构造函数
AnnotationConfigWebApplicationContext 专门为web应用准备的,适用于注解方式
XmlWebApplicationContext 从Web应用下的一个或多个xml配置文件加载上下文定义,适用于xml配置方式

  ClassPathXmlApplicationContext和FileSystemXmlApplicationContext的路径加载区别:

  1. ClassPathXmlApplicationContext默认文件路径是src下那一级;
  2. FileSystemXmlApplicationContext 默认获取的是项目路径,默认文件路径是项目名下一级,与src同级。

2.13 实例化Bean的三种方式【了解即可】

  • 1、构造器实例化bean
      假设有一个Person类,有name、age两个属性,通过构造器的方法的配置方式为示例:
	<bean id="person" class="com.spring.demo.Person">
	        <constructor-arg name="name" value="等风的草"/>
	        <constructor-arg name="age" value="21"/>
	</bean>
  • 2、静态工厂实例化Bean
      当采用静态工厂方法创建bean时,除了需要指定class属性外,还需要通过factory-method属性来指定创建bean实例的工厂方法。
      假设有这样的静态工厂PersonStaticFactory类:
	public static Person createInstance() {
    
    
	    return new Person();
	}

  配置文件(class:指定静态工厂类;factory-method:指定哪个方法是工厂方法):

	<bean id="person" class="com.spring.demo.PersonStaticFactory" 
	    factory-method="createInstance">
		    <constructor-arg name="name" value="等风的草"/>
		    <constructor-arg name="age" value="21"/>
	</bean>

  这种方式获取Bean的方式也和通过构造器方法创建Bean的方式类似,示例:

        ApplicationContext applicationContext = 
        	new ClassPathXmlApplicationContext(xmlPath);
        System.out.println(applicationContext .getBean("person"));
  • 3、实例工厂实例化bean
      示例:
	public class InstanceFactory {
    
    
	    public Person createInstance() {
    
    
	        return new Person();
	    }
	}

  配置文件示例(factory-bean:指定使用哪个工厂实例;factory-method:指定使用哪个工厂实例的方法):

	<bean id="instancefactory" class="com.spring.demo.InstanceFactory"/>
	<bean id="personInstance" factory-bean="instancefactory" factory-method="createInstance"/>

2.14 Bean的自动装配

  Bean装配是指在Spring容器中,把Bean组装到一起,前提是容器需要知道Bean的依赖关系,如何通过依赖注入来把它们装配到一起。
  在Spring框架中,在配置文件中设定Bean的依赖关系是一个很好的机制,Spring容器能够自动装配相互合作的Bean。这就是Bean的自动装配。
  在spring中,对象无需自己查找或创建与其关联的其他对象,由容器负责把需要相互协作的对象引用赋予各个对象,使用autowire来配置自动装配模式。

  在Spring中,有以下几种可以装配Bean的属性的方式。先准备几个实体类:

	public class Cat {
    
    
		public void shout() {
    
    
	        System.out.println("喵~");
		}
	}

	public class Dog {
    
    
		public void shout() {
    
    
		    System.out.println("汪~");
		}
	}

public class User {
    
    
	private Cat cat;
	private Dog dog;
	private String str;
	public Cat getCat() {
    
    
		return cat;
	}
	public void setCat(Cat cat) {
    
    
		this.cat = cat;
	}
	public Dog getDog() {
    
    
		return dog;
	}
	public void setDog(Dog dog) {
    
    
		this.dog = dog;
	}
	public String getStr() {
    
    
		return str;
	}
	public void setStr(String str) {
    
    
		this.str = str;
	}
}
  • 1、no
      默认的方式是不进行自动装配的,通过手工设置ref属性来进行装配bean。示例:
   <bean id="dog" class="com.spring.test.Dog"/>
   <bean id="cat" class="com.spring.test.Cat"/>

   <bean id="user" class="com.spring.test.User">
       <property name="cat" ref="cat"/>
       <property name="dog" ref="dog"/>
       <property name="str" value="test"/>
   </bean>

  测试代码:

	public static void test(){
    
    
	   ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
       User user = (User) context.getBean("user");
       //喵~
       user.getCat().shout();
       //汪~
       user.getDog().shout();
	}
  • 2、byName
      通过Bean的名称进行自动装配,如果一个Bean的property与另一个Bean的name相同,就进行自动装配
      示例:
   <bean id="user" class="com.spring.test.User" autowire="byName">
   	    <property name="str" value="test"/>
   </bean>

  测试代码:

	public static void test(){
    
    
	   ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
       User user = (User) context.getBean("user");
       //test
       System.out.println(user.getStr());
	}

  该模式表示根据Property的Name自动装配,如果一个Bean的name,和另一个Bean中的Property的name相同,则自动装配这个Bean到Property中。
  也就是说,当一个Bean节点带有autowire="byName"的属性时,将查找其类中所有的set方法名,获得将set去掉并且首字母小写的字符串,然后去spring容器中寻找是否有此字符串名称id的对象。如果有,就取出注入;如果没有,就报空指针异常。

  • 3、byType
      通过参数的数据类型进行自动装配。使用autowire="byType"时,首先需要保证:同一类型的对象,在Spring容器中唯一。如果不唯一,会报不唯一的异常(NoUniqueBeanDefinitionException)。
      如果照下面的配置,就会报异常,因为有两个同类型的Bean:
   <bean id="dog" class="com.spring.test.Dog"/>
   <bean id="cat" class="com.spring.test.Cat"/>
   <bean id="cat2" class="com.spring.test.Cat"/>

   <bean id="user" class="com.spring.test.User" autowire="byType">
   	    <property name="str" value="test"/>
   </bean>

  删掉id为cat或cat2的Bean,则代码可以正常运行,示例:

	public static void test(){
    
    
	   ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
       User user = (User) context.getBean("user");
       //test
       System.out.println(user.getStr());
	}
  • 4、constructor
      利用构造函数进行装配,并且构造函数的参数通过byType进行装配。先在User实体类中加入一个构造方法:
	public User(Cat cat) {
    
    
		this.cat = cat;
	}

  然后配置文件改为:

    <bean id="cat" class="com.spring.test.Cat"/>
    <bean id="user" class="com.spring.test.User" autowire="constructor"></bean>

  测试代码:

	public static void test(){
    
    
	   ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
       User user = (User) context.getBean("user");
       //喵~
       user.getCat().shout();
	}

2.15 使用注解实现Bean的自动装配

  Jdk1.5开始支持注解,Spring2.5开始全面支持注解。
  要使用注解,需要在配置文件里加上如下配置,用来开启注解支持:

	<context:annotation-config/>

  配置文件中关于Bean的关键配置:

	<context:annotation-config/>

	<bean id="dog" class="com.spring.test.Dog"/>
	<bean id="cat" class="com.spring.test.Cat"/>
	<bean id="user" class="com.spring.test.User"/>

  此时就可以在代码中使用@Autowired注解了。示例:

	public class User {
    
    
		@Autowired
		private Cat cat;
		@Autowired
		private Dog dog;
		private String str;
		//其他代码
	}

  通过@Autowired就可以自动获取到对应的对象了,这也是实际项目中常见的做法了。测试代码示例:

	public static void test(){
    
    
	   ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
       User user = (User) context.getBean("user");
       //喵~
       user.getCat().shout();
	}

2.16 @Autowired注解的使用说明

  @Autowired注解,用来自动装配指定的bean。在使用@Autowired注解之前需要在Spring配置文件进行配置:

	<context:annotation-config />

  @Autowired注解原理:在启动spring IoC时,容器自动装载了一个AutowiredAnnotationBeanPostProcessor后置处理器,当容器扫描到@Autowied、@Resource或@Inject时,就会在IoC容器自动查找需要的bean,并装配给该对象的属性。在使用@Autowired时,首先在容器中查询对应类型的bean:

  1. 如果查询结果刚好为一个,就将该bean装配给@Autowired指定的数据;
  2. 如果查询的结果不止一个,那么@Autowired会根据名称来查找;
  3. 如果上述查找的结果为空,那么会抛出异常。解决方法时,使用required=false。 示例:@Autowired(required=false)
      @Autowired(required=false) 使用说明:当值为false时,对象可以为null;当值为true时,对象必须存对象,不能为null。

  @Autowired可用于:构造函数、成员变量、Setter方法。@Autowired的作用是自动注入Bean。最常见的使用示例:

public class UserService {
    
    
	//相当于直接给userDao变量实例化
    @Autowired
    private UserDao userDao; 
}

  如上代码所示,Spring容器会找到类型为UserDao的类,然后将其注入进来。这样会产生一个问题,当一个类型有多个Bean值的时候,会造成无法选择具体注入哪一个的情况,Spring容器在启动时也会抛出BeanCreationException。这个时候可以配合着@Qualifier使用,@Qualifier告诉spring具体去装配哪个对象。
  当使用@AutoWired注解的时候,自动装配的时候是根据类型(在上面的例子中,即类型为UserDao的类)来装配的:

  1. 如果只找到一个UserDao类型的类,则直接进行赋值;
  2. 如果没有找到UserDao类型的类,则直接抛出异常;
  3. 如果找到多个UserDao类型的类,那么会按照变量名(此处的变量名为类名首字母小写)作为id继续匹配:
     1)匹配上直接进行装配;
     2)如果匹配不上则直接报异常。
  4. 还可以不使用变量名,使用@Qualifier注解来指定id的名称,当使用@Qualifier注解的时候也会有两种情况:
     1)找到,则直接装配;
     2)找不到,就会报错。

  @Qualifier和@Autowired结合使用的示例:

public class UserService {
    
    
    @Autowired
    @Qualifier(name="userDao1")    
    private UserDao userDao; 
}

  在上面的介绍中,找不到对应的类,都会报错,如果想不报错,可以使用@Autowired(required=false)。该注解表示忽略当前要注入的bean,如果有直接注入,没有跳过,不会报错。

  @AutoWired添加到方法上的时候,此方法在创建对象的时候会默认调用,同时方法中的参数会进行自动装配。@Qualifier注解也可以定义在方法的参数列表中,可以指定当前属性的id名称,示例:

    @Autowired
    public void test(@Qualifier("personDao") PersonDao personDao123) {
    
    
        System.out.println("test");
        personDao123.update();
    }

2.17 使用@Resource完成自动注入

  @Resource注解也可以完成自动注入,其自动注入规律:

  1. @Resource如有指定的name属性,先按该属性进行byName方式查找装配;
  2. 其次再进行默认的byName方式进行装配;
  3. 如果以上都不成功,则按byType的方式自动装配;
  4. 都不成功,则报异常。

  @Resource的使用示例:

public class User {
    
    
   //优先注入name为"cat2"的Bean
   @Resource(name = "cat2")
   private Cat cat;
   @Resource
   private Dog dog;
   private String str;
   //其他代码
}

2.18 @Autowired和@Resource之间的区别

  • 1、@Autowired
      @Autowired为Spring提供的注解
      @Autowired默认按类型装配,默认情况下必须要求依赖对象必须存在,如果要允许null 值,可以设置它的required属性为false,示例:
public class UserService {
    
    
    @Autowired(required=false)
    private UserDao userDao; 
}
  • 2、@Resource
      @Resourc是jdk提供的功能,需要导入包javax.annotation.Resource。
      @Resource默认按照ByName自动注入。示例:
public class UserService {
    
    
    @Resource  
    private UserDao userDao; 
    @Resource(name="studentDao")  
    private StudentDao studentDao; 
    @Resource(type="TeacherDao")  
    private TeacherDao teacherDao; 
    @Resource(name="manDao",type="ManDao")  
    private ManDao manDao; 
}  

  @Resource注解自动装配的流程:

  1. 如果同时指定了name(变量名)和type(类型),则从Spring上下文中按照先name再type,找唯一匹配的bean进行装配,找不到则抛出异常。
  2. 如果指定了name,则从上下文中查找名称(id)匹配的bean进行装配,找不到则抛出异常。
  3. 如果指定了type,则从上下文中找到类似匹配的唯一bean进行装配,找不到或是找到多个,都会抛出异常。
  4. 如果既没有指定name,又没有指定type,则自动按照byName方式进行装配;如果没有匹配,那就按类型就行匹配。

总结:

  1. @AutoWired是spring中提供的注解@Resource是jdk中定义的注解,依靠的是java的标准;
  2. @AutoWired默认是按照类型进行装配,默认情况下要求依赖的对象必须存在@Resource默认是按照名字进行匹配的,同时可以指定name属性;
  3. @AutoWired只适合spring框架,而@Resource扩展性更好

2.19 @Component和@Value的简单使用

  配置文件中需要添加context:component-scan配置,示例:

	<context:component-scan base-package="com.spring.test"/>
  • @Component
      @Component的使用简单示例:
@Component("user")
//相当于配置文件中 <bean id="user" class="当前注解的类"/>
public class User {
    
    
	 String name = "自己";
}

  测试代码:

	public static void test(){
    
    
	   ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
	   User user = (User) context.getBean("user");
	   //自己
	   System.out.println(user.name);
	}
  • @Value
      可以不用提供set方法,直接在直接名上添加@value(“值”),示例:
@Component("user")
// 相当于配置文件中 <bean id="user" class="当前注解的类"/>
public class User {
    
    
   @Value("自己")
   // 相当于配置文件中 <property name="name" value="自己"/>
   public String name;
}

  如果提供了set方法,在set方法上添加@value(“值”),示例:

@Component("user")
public class User {
    
    
   public String name;
   @Value("自己")
   public void setName(String name) {
    
    
       this.name = name;
  }
}

2.20 Spring 中循环注入的方式

  什么是循环注入?举个列子有一个类A,A有一个构造器里面的参数是类B;类B里面有个构造器参数是类C,类C里面有个构造器参数是类A。其实引用循环了A里面有B的引用,B里面有C的引用,C里面又有A的引用。
  当用Spring加载A的时候Spring的流程是这样的:

  1. Spring创建类A的实例对象,首先去当前创建池中去查找当前类A的实例对象是否在创建,如果发明没有创建则准备其构造器需要的参数B,然后把创建A的标识放入当前创建池中。
  2. Spring 创建B首先去当前创建池中去查找当前B是否在创建,如果发现没有创建则准备其构造器需要的参数C,然后把创建B的标识放入当前创建池中。
  3. Spring 创建C首先去当前创建池中去查找当前C是否在创建,如果发现没有创建则准备其构造器需要的参数A,然后把创建C的标识放入当前创建池中。
  4. Spring 创建C需要的A,这个时候会发现在当前创建池中已经有A的标识,A正在创建中则抛出 BeanCurrentlyInCreationException。

  构造器的循环注入是没有办法解决的,所以只能避免。因此,不要使用基于构造函数的依赖注入,可以通过以下方式解决:

  1. 在字段上使用@Autowired注解,让Spring决定在合适的时机注入;
  2. 用基于setter方法的依赖注入。

2.21 Spring的依赖注入的三种形式

  依赖注入(DI)和控制反转(IOC)是同一个概念。所谓依赖注入, 是指程序运行过程中,如果需要调用另一个对象协助时,无须在代码中创建被调用者,而是 依赖于外部的 注入
  依赖注入是时下最流行的IoC实现方式,依赖注入分为接口注入,Setter方法注入和构造器注入三种方式。其中接口注入由于在灵活性和易用性比较差,从Spring4开始已被废弃。

  • 1、 构造器依赖注入
    构造器依赖注入通过容器触发一个类的构造器来实现的,该类有一系列参数,每个参数代表一个对其他类的依赖。

Bean下面的子标签是constructor-arg开头。

    <bean id="userId1" class="com.cc.study.di.User" lazy-init="true">
        <constructor-arg  type="java.lang.Integer" value="1"></constructor-arg>
        <constructor-arg type="java.lang.String" value="bbb"></constructor-arg>
    </bean>
  • 2、Setter方法注入
    Setter方法注入是方法实例化bean之后,调用该bean的setter方法,即实现了基于setter的依赖注入。

Bean下面的子标签是property开头。

<!-- Spring IOC注入方式: Setter方法注入 -->
    <bean id="injectionService" class="com.service.InjectionServiceImpl">
    	<property name="injectionDAO" ref="injectionDAO"></property>
    </bean>
    
    <bean id="injectionDAO" class="com.dao.InjectionDAOImpl"></bean>
  • 3、自动装配
     即用@Autowired来自动装配对象。Java代码示例:
@Component  
public class Programmer {
    
      
    @Autowired  
    Computer computer;  
}

@Component  
public class Computer {
    
     
}

 配置文件示例:

	<!--表示扫描整个com.spring.test包 -->
    <context:component-scan base-pakage="com.spring.test"> 

2.22 Spring有几种配置方式

  有三种:基于xml的配置、基于注解的配置、基于Java的配置。

  • 1、XML配置文件
      示例:
    <bean id="jackma" class="com.test.User">
        <property name="name" value="jackma" />
        <property name="age" value="55" />
        <property name="dog" ref="jm" />
     </bean>
  • 2、基于注解的配置
      可以理解为写在类上的注解。示例:
//控制层
@Controller
public class UserController {
    
    
}

//服务层
@Service
public interface UserService {
    
    
}
  • 3、基于Java的配置
      Spring3.0以后,提供了Java配置的能力,Spring4.x和SpringBoot都推荐使用Java配置,可以理解为写在方法和对象上的注解。示例:
@Configuration
public class DemoConfig {
    
    
    @Bean
    public User jackma(){
    
    
        return new User();
    }
    @Bean
    public Dog dog(){
    
    
        return  new Dog();
    }
}

@Component("jackma")
public class User {
    
    
    private String name;
    private int age;
    private Dog dog;

  //get,set方法略
}

2.23 Spring IoC 的实现机制

  Spring 中的 IoC 的实现原理就是工厂模式加反射机制。图示:

2.24 Spring框架中Bean的生命周期

  • 1、Spring对bean进行实例化
  • 2、Spring将值和bean的引用注入到bean对应的属性中
  • 3、如果bean实现了BeanNameAware接口,Spring将bean的ID传递给setBean-Name()方法;
  • 4、如果bean实现了BeanFactoryAware接口,Spring将调用setBeanFactory()方法,将BeanFactory容器实例传入;
  • 5、如果bean实现了ApplicationContextAware接口,Spring将调用setApplicationContext()方法,将bean所在的应用上下文的引用传入进来;
  • 6、如果bean实现了BeanPostProcessor接口,Spring将调用它们的post-ProcessBeforeInitialization()方法;
  • 7、如果bean实现了InitializingBean接口,Spring将调用它们的after-PropertiesSet()方法。类似地,如果bean使用initmethod声明了初始化方法,该方法也会被调用;
  • 8、如果bean实现了BeanPostProcessor接口,Spring将调用它们的post-ProcessAfterInitialization()方法;
  • 9、此时,bean已经准备就绪,可以被应用程序使用了,它们将一直驻留在应用上下文中,直到该应用上下文被销毁
  • 10、如果bean实现了DisposableBean接口,Spring将调用它的destroy()接口方法。同样,如果bean使用destroy-method声明了销毁方法,该方法也会被调用。

2.25 用xml配置的方式重写init和destroy方法

  bean生命周期表示bean的创建到销毁。

  1. 如果bean是单例,容器在启动的时候会创建好,关闭的时候会销毁创建的bean;
  2. 如果bean是多例,获取的时候创建对象,销毁的时候不会有任何的调用。

  在创建对象的时候,我们可以根据需要调用初始化和销毁的方法:

    <bean id="address" class="com.test.bean.User" 
    	init-method="init" destroy-method="destory"></bean>

  在具体的实体类里即可以实现上面的两个方法,示例:

    public void init(){
    
    
        System.out.println("对象被初始化");
    }
    
    public void destory(){
    
    
        System.out.println("对象被销毁");
    }

三、Spring事务

3.1 Spring事务的实现方式和实现原理

  Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。真正的数据库层的事务提交和回滚是通过binlog或者redo log实现的。
  对于纯JDBC操作数据库,想要用到事务,可以按照以下步骤进行(了解即可):

  1. 获取连接 Connection con = DriverManager.getConnection();
  2. 开启事务con.setAutoCommit(true/false);
  3. 执行CRUD;
  4. 提交事务/回滚事务 con.commit() / con.rollback();
  5. 关闭连接 conn.close()。

  使用Spring的事务管理功能后,可以不再写步骤2和4的代码,Spirng会自动完成。那么Spring是如何在我们书写的 CRUD 之前和之后开启事务和关闭事务的呢?
  Spring在启动的时候会去解析生成相关的Bean,这时候会查看拥有相关注解的类和方法,并且为这些类和方法生成代理,并根据@Transaction的相关参数进行相关配置注入,这样就在代理中为把相关的事务处理掉了(开启正常提交事务,异常回滚事务)。
  真正的数据库层的事务提交和回滚是通过bin log或者redo log实现的。

  • 1、配置文件开启注解驱动
      示例:
xmlns:tx="http://www.springframework.org/schema/tx" 

http://www.springframework.org/schema/tx
       http://www.springframework.org/schema/tx/spring-tx.xsd

    <!--配置事务管理器的bean对象-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"></property>
    </bean>
    <!--开启基于注解的事务管理器的配置-->
    <tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
  • 2、在相关的类和方法上通过注解@Transactional标识
      然后在代码里就可以加入@Transactional注解,就可以事务控制了。
      @Transactional注解的属性:

isolation:设置事务的隔离级别;
propagation:事务的传播行为;
noRollbackFor:那些异常事务可以不回滚;
noRollbackForClassName:填写的参数是全类名;
rollbackFor:哪些异常事务需要回滚;
rollbackForClassName:填写的参数是全类名;
readOnly:设置事务是否为只读事务;
timeout:事务超出指定执行时长后自动终止并回滚,单位是秒。

  @Transactional注解简单使用的示例:

	@Transactional(timeout = 3)

  如果一次执行单条查询语句,则没有必要启用事务支持,数据库默认支持SQL执行期间的读一致性;
  如果你一次执行多条查询语句,例如统计查询,报表查询,在这种场景下,多条查询SQL必须保证整体的读一致性,否则,在前条SQL查询之后,后条SQL查询之前,数据被其他用户改变,则该次整体的统计查询将会出现读数据不一致的状态,此时,应该启用事务支持。
  对于只读查询,可以指定事务类型为readonly,即只读事务。由于只读事务不存在数据的修改,因此数据库将会为只读事务提供一些优化手段。示例:

	@Transactional(readOnly = true)

  运行时异常默认回滚,编译时异常默认不回滚,可以手动指定哪些异常不会滚:

	@Transactional(noRollbackFor = {
    
    ArithmeticException.class,NullPointerException.class})

  设置哪些异常回滚:

	@Transactional(rollbackFor = {
    
    FileNotFoundException.class})

  设置隔离级别:

	@Transactional(isolation = Isolation.READ_COMMITTED)

3.2 Spring的事务传播行为

3.2.1 事务传播行为分类

  spring事务的传播行为说的是,当多个事务同时存在(当一个事务方法被另一个事务方法调用)的时候,Spring如何处理这些事务的行为。 事务的传播行为,默认值为 Propagation.REQUIRED。其七个分类:

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

  使用示例:

	@Transactional(propagation = Propagation.REQUIRED)

  展开来说:

  • 1、REQUIRED
      假如当前正要运行的事务不在另外一个事务里,那么就起一个新的事务 比方说,ServiceB.methodB的事务级别定义PROPAGATION_REQUIRED, 那么因为执行ServiceA.methodA的时候,ServiceA.methodA已经起了事务。这时调用ServiceB.methodB,ServiceB.methodB看到自己已经执行在ServiceA.methodA的事务内部。就不再起新的事务。
      而假如ServiceA.methodA执行的时候发现自己没有在事务中,他就会为自己分配一个事务。这样,在ServiceA.methodA或者在ServiceB.methodB内的不论什么地方出现异常。事务都会被回滚。即使ServiceB.methodB的事务已经被提交,可是ServiceA.methodA在接下来fail要回滚,ServiceB.methodB也要回滚。
  • 2、REQUIRES_NEW
      假如ServiceA.methodA的事务级别为PROPAGATION_REQUIRED,ServiceB.methodB的事务级别为PROPAGATION_REQUIRES_NEW。那么当运行到ServiceB.methodB的时候,ServiceA.methodA所在的事务就会挂起。ServiceB.methodB会起一个新的事务。等待ServiceB.methodB的事务完毕以后,ServiceA.methodA才继续运行。
      他与PROPAGATION_REQUIRED 的事务差别在于事务的回滚程度了。由于ServiceB.methodB是新起一个事务,那么就是存在两个不同的事务。假设ServiceB.methodB已经提交,那么ServiceA.methodA失败回滚。ServiceB.methodB是不会回滚的。假设ServiceB.methodB失败回滚,假设他抛出的异常被ServiceA.methodA捕获,ServiceA.methodA事务仍然可能提交。
  • 3、SUPPORTS
      假设当前在事务中,就以事务的形式执行。假设当前不在一个事务中,那么就以非事务的形式执行。
  • 4、MANDATORY
      必须在一个事务中执行。也就是说,他仅仅能被一个父事务调用。否则,他就要抛出异常。
  • 5、NOT_SUPPORTED
      当前不支持事务。比方ServiceA.methodA的事务级别是PROPAGATION_REQUIRED 。而ServiceB.methodB的事务级别是PROPAGATION_NOT_SUPPORTED ,那么当执行到ServiceB.methodB时。ServiceA.methodA的事务挂起。而他以非事务的状态执行完,再继续ServiceA.methodA的事务。
  • 6、NEVER
      不能在事务中执行。
      如果ServiceA.methodA的事务级别是PROPAGATION_REQUIRED。 而ServiceB.methodB的事务级别是PROPAGATION_NEVER ,那么ServiceB.methodB就要抛出异常了。
  • 7、NESTED
      如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。
      PROPAGATION_NESTED 开始一个 “嵌套的” 事务, 它是已经存在事务的一个真正的子事务. 嵌套事务开始执行时, 它将取得一个 savepoint. 如果这个嵌套事务失败, 我们将回滚到此 savepoint.嵌套事务是外部事务的一部分, 只有外部事务结束后它才会被提交.
      由此可见, PROPAGATION_REQUIRES_NEW 和 PROPAGATION_NESTED 的最大区别在于, PROPAGATION_REQUIRES_NEW 完全是一个新的事务, 而 PROPAGATION_NESTED 则是外部事务的子事务, 如果外部事务 commit, 嵌套事务也会被 commit, 这个规则同样适用于 roll back。

3.2.2 事务的配置

  配置方式有两种:配置文件的方式和注解的方式。

  • 1、配置文件的方式
      示例:
<tx:advice id="txAdvice" transaction-manager="txManager"> 
      <tx:attributes>  
      <!--设置所有匹配的方法,然后设置传播级别和事务隔离-->
           <tx:method name="save*" propagation="REQUIRED" /> 
           <tx:method name="add*" propagation="REQUIRED" /> 
           <tx:method name="create*" propagation="REQUIRED" /> 
           <tx:method name="insert*" propagation="REQUIRED" /> 
           <tx:method name="update*" propagation="REQUIRED" /> 
           <tx:method name="merge*" propagation="REQUIRED" /> 
           <tx:method name="del*" propagation="REQUIRED" /> 
           <tx:method name="remove*" propagation="REQUIRED" /> 
           <tx:method name="put*" propagation="REQUIRED" /> 
           <tx:method name="get*" propagation="SUPPORTS" read-only="true" /> 
           <tx:method name="count*" propagation="SUPPORTS" read-only="true" /> 
          <tx:method name="find*" propagation="SUPPORTS" read-only="true" /> 
          <tx:method name="list*" propagation="SUPPORTS" read-only="true" /> 
          <tx:method name="*" propagation="SUPPORTS" read-only="true" /> 
     </tx:attributes> 
</tx:advice> 
  • 2、注解的方式
	<!--开启注解的方式--> 
	<tx:annotation-driven transaction-manager="transactioManager" />

  Java代码使用示例:

	//如果有事务, 那么加入事务, 没有的话新建一个(默认情况)
	@Transactional(propagation=Propagation.REQUIRED)
	
	//容器不为这个方法开启事务
	@Transactional(propagation=Propagation.NOT_SUPPORTED)

	//不管是否存在事务,都创建一个新的事务,原来的挂起,新的执行完毕,继续执行老的事务
	@Transactional(propagation=Propagation.REQUIRES_NEW)

	//必须在一个已有的事务中执行,否则抛出异常
	@Transactional(propagation=Propagation.MANDATORY)
	
	//必须在一个没有的事务中执行,否则抛出异常(与Propagation.MANDATORY相反)
	@Transactional(propagation=Propagation.NEVER)

	//如果其他bean调用这个方法,在其他bean中声明事务,那就用事务;
	//如果其他bean没有声明事务,那就不用事务
	@Transactional(propagation=Propagation.SUPPORTS)

3.3 声明式事务和编程式事务

  Spring事务有两种:编程式事务和声明式事务。

  • 1、声明式事务
      大多数Spring框架的用户选择声明式事务管理,因为声明式事务管理要优于编程式事务管理,这正是spring倡导的非侵入式的开发方式。声明式事务管理要优于编程式事务管理,虽然比编程式事务管理(这种方式通过代码控制事务)少了一点灵活性。唯一不足地方是,最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。

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

  • 2、编程式事务
      编程式事务管理使用TransactionTemplate或者直接使用底层的PlatformTransactionManager。对于编程式事务管理,Spring推荐使用TransactionTemplate。

四、AOP

  AOP,一般称为面向切面编程,作为面向对象的一种补充,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块。这个模块被命名为“切面”(Aspect),这样就减少了系统中的重复代码,降低了模块间的耦合度,同时提高了系统的可维护性。
  AOP可用于日志管理、权限认证、安全检查、事务控制等。

4.1 JDK动态代理和CGLIB动态代理

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

  • 1、Java动态代理
      利用反射机制生成一个实现代理接口的匿名类`,在调用具体方法前调用InvokeHandler来处理。
  • 2、Cglib动态代理
      是利用asm开源包,对代理对象类的class文件加载进来,通过修改其(代理对象类的class文件)字节码生成子类来处理。

4.1.1 JDK动态代理和CGLIB动态代理的使用选择

  1. 如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP;
  2. 如果目标对象实现了接口,可以强制使用CGLIB实现AOP;
      如何强制使用CGLIB实现AOP?
  1. 添加CGLIB库;
  2. 在Spring配置文件中加入<aop:aspectj-autoproxy proxy-target-class="true"/>
  1. 如果目标对象没有实现了接口,必须采用CGLIB库,Spring会自动在JDK动态代理和CGLIB之间转换。

4.1.2 JDK动态代理和CGLIB动态代理的区别

  • 1、 JDK动态代理只能对实现了接口的类生成代理,而不能针对类
  • 2、CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法因为是继承,所以该类或方法最好不要声明成final ;
  • 3、两者的性能差别:在早期的时候,JDK动态代理性能弱于CGLIB动态代理,随着JDK的更新,两者性能以及差别不大。

4.2 AOP里面的几个名词

  • 1、切面(Aspect)------>通知和切入点的结合
      通知和切入点共同定义了切面的全部内容【AOP核心1:可以简单理解为要实现切面功能的那个类】
  • 2、连接点(Join point)
      指方法,在Spring AOP中,一个连接点总是代表一个方法的执行。 应用可能有数以千计的时机应用通知。这些时机被称为连接点。连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。
  • 3、通知(Advice)------>要添加的公共功能
      在AOP术语中,切面的工作被称为通知【AOP核心2:需要在连接点实现的具体功能,有五种通知类型】
  • 4、切入点(Pointcut)------>添加公共功能的地方/方法
      切点的定义会匹配通知所要织入的一个或多个连接点【AOP核心3:定义一个方法,和原有业务代码中需要实现AOP功能的相应方法关联起来】。我们通常使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。
  • 5、引入(Introduction)
      引入允许我们向现有类添加新方法或属性。
  • 6、目标对象(Target Object)------>被添加公共功能的原始对象
       被一个或者多个切面(aspect)所通知(advise)的对象。它通常是一个代理对象。也有人把它叫做 被通知(adviced) 对象。 既然Spring AOP是通过运行时代理实现的,这个对象永远是一个 被代理(proxied) 对象。
  • 7、织入(Weaving)------>在目标对象上使用切面
      织入是把切面应用到目标对象并创建新的代理对象的过程。在目标对象的生命周期里有多少个点可以进行织入:
  1. 编译期
      切面在目标类编译时被织入。AspectJ的织入编译器是以这种方式织入切面的。
  2. 类加载期
      切面在目标类加载到JVM时被织入。需要特殊的类加载器,它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ5的加载时织入就支持以这种方式织入切面。
  3. 运行期
      切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。SpringAOP就是以这种方式织入切面。

4.3 Spring在运行时通知对象

  通过在代理类中包裹切面,Spring在运行期把切面织入到Spring管理的Bean中。代理封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标bean。当代理拦截到方法调用时,在调用目标Bean方法之前,会执行切面逻辑
  直到应用需要被代理的bean时,Spring才创建代理对象。如果使用的是ApplicationContext的话,在ApplicationContext从BeanFactory中加载所有Bean的时候,Spring才会创建被代理的对象。因为Spring运行时才创建代理对象,所以我们不需要特殊的编译器来织入Spring AOP的切面。

4.4 Spring通知类型

  在AOP术语中,切面的工作被称为通知,实际上是程序执行时要通过Spring AOP框架触发的代码段。
  Spring切面可以应用5种类型的通知:

  • 1、前置通知(Before advice)
     在某连接点(join point)之前执行的通知,但这个通知不能阻止连接点前的执行(除非它抛出一个异常)。
  • 2、返回后通知(After returning advice)
     在某连接点(join point)正常完成后执行的通知:例如,一个方法没有抛出任何异常,正常返回。
  • 3、抛出异常后通知(After throwing advice)
     在方法抛出异常退出时执行的通知。
  • 4、后通知(After (finally) advice)
     当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)。
  • 5、环绕通知(Around Advice)
     包围一个连接点(join point)的通知,如方法调用。这是最强大的一种通知类型。环绕通知可以在方法调用前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它们自己的返回值或抛出异常来结束执行。环绕通知是最常用的一种通知类型。大部分基于拦截的AOP框架,例如Nanning和JBoss4,都只提供环绕通知。

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

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

  环绕通知的执行顺序是优于普通通知的
  同一个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: 异常发生

4.5 多个切面类的执行顺序

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

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

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

  此时就是SecurityUtil切面先执行。

4.6 AOP的使用示例

  先导入aspectj依赖:

		<dependency>
   			<groupId>org.aspectj</groupId>
   			<artifactId>aspectjweaver</artifactId>
   			<version>1.9.4</version>
		</dependency>
  • 1、AOP实现方式一:使用Spring的API接口
      定义UserService业务接口和UserServiceImpl实现类:
public interface UserService {
    
    
    public void add();
    public void delete();
    public void update();
    public void select();
}

public class UserServiceImpl implements UserService {
    
    
    public void add() {
    
    
        System.out.println("增加了一个用户!");
    }

    public void delete() {
    
    
        System.out.println("删除了一个用户!");
    }

    public void update() {
    
    
        System.out.println("更新了一个用户!");
    }

    public void select() {
    
    
        System.out.println("查询了一个用户!");
    }
}

  一个BeforeLog前置增强和一个AfterLog后置增强类:

public class BeforeLog implements MethodBeforeAdvice {
    
    

	 //method: 要执行的目标对象的方法
	 //args:参数
	 //target:目标对象
	 public void before(Method method, Object[] agrs, Object target) throws Throwable {
    
    
	     System.out.println(target.getClass().getName()+"的"+method.getName()+"被执行了");
	 }
}

public class AfterLog implements AfterReturningAdvice {
    
    

    //returnValue: 返回值
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
    
    
        System.out.println("执行了"+method.getName()+"方法,返回结果为:"+returnValue);
    }
}

  在配置文件中实现aop切入实现:

    <!--注册bean-->
    <bean id="userService" class="com.spring.test.UserServiceImpl"/>
    <bean id="beforeLog" class="com.spring.test.BeforeLog"/>
    <bean id="afterLog" class="com.spring.test.AfterLog"/>

    <!--方式一:使用原生Spring API接口-->
    <!--配置aop:需要导入aop的约束-->
    <aop:config>
        <!--切入点:expression:表达式,execution(要执行的位置!* * * * *)-->
        <aop:pointcut id="pointcut" expression="execution(* com.spring.test.UserServiceImpl.*(..))"/>

        <!--执行环绕增加!-->
        <aop:advisor advice-ref="beforeLog" pointcut-ref="pointcut"/>
        <aop:advisor advice-ref="afterLog" pointcut-ref="pointcut"/>
    </aop:config>

  测试代码:

	public static void test(){
    
    
	   ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
       UserService userService = (UserService) context.getBean("userService");
       userService.add();
	}

  测试结果:

com.spring.test.UserServiceImpl的add被执行了
增加了一个用户!
执行了add方法,返回结果为:null

  • 2、AOP实现方式二: 自定义类来实现AOP
      定义自己的DiyPointCut切入类:
public class DiyPointCut {
    
    
    public void before(){
    
    
        System.out.println("====== 方法执行前 ======");
    }

    public void after(){
    
    
        System.out.println("====== 方法执行后 ======");
    }
}

  配置文件中增加配置:

    <!--注册bean-->
    <bean id="userService" class="com.spring.test.UserServiceImpl"/>
    <bean id="beforeLog" class="com.spring.test.BeforeLog"/>
    <bean id="afterLog" class="com.spring.test.AfterLog"/>

    <!--方式二:自定义类-->
    <bean id="diy" class="com.spring.test.DiyPointCut"/>

    <aop:config>
        <!--自定义切面,ref 要引用的类-->
        <aop:aspect ref="diy">
            <!--切入点-->
            <aop:pointcut id="point" expression="execution(* com.spring.test.UserServiceImpl.*(..))"/>
            <!--通知-->
            <aop:before method="before" pointcut-ref="point"/>
            <aop:after method="after" pointcut-ref="point"/>
        </aop:aspect>
    </aop:config>

  测试结果:

====== 方法执行前 ======
增加了一个用户!
====== 方法执行后 ======

  • 3、AOP实现方式三: 使用注解实现
      定义注解实现的AnnotationPointCut增强类:
@Aspect //标注这个类是一个切面
public class AnnotationPointCut {
    
    

  @Before("execution(* com.spring.test.UserServiceImpl.*(..))")
  public void before(){
    
    
      System.out.println("====方法执行前====");
  }

  @After("execution(* com.spring.test.UserServiceImpl.*(..))")
  public void after(){
    
    
      System.out.println("====方法执行后====");
  }

  //在环绕增强中,我们可以给定一个参数,代表我们要获取处理切入的点;
  @Around("execution(* com.spring.test.UserServiceImpl.*(..))")
  public void around(ProceedingJoinPoint jp) throws Throwable{
    
    
      System.out.println("环绕前");

      Signature signature = jp.getSignature();// 获得签名
      System.out.println("signature:"+signature);

      Object proceed = jp.proceed(); //执行方法

      System.out.println("环绕后");

      System.out.println(proceed);
  }
}

  配置文件中增加对注解的配置:

    <!--注册bean-->
    <bean id="userService" class="com.spring.test.UserServiceImpl"/>
    <bean id="beforeLog" class="com.spring.test.BeforeLog"/>
    <bean id="afterLog" class="com.spring.test.AfterLog"/>

    <!--方式三:使用注解-->
    <bean id="annotationPointCut" class="com.spring.test.AnnotationPointCut"/>
    <!--开启注解支持! JDK(默认是 proxy-target-class="false")  cglib(proxy-target-class="true")-->
    <aop:aspectj-autoproxy/>

  测试结果:

环绕前
signature:void com.spring.test.UserService.add()
方法执行前
增加了一个用户!
环绕后
null
方法执行后

猜你喜欢

转载自blog.csdn.net/m0_37741420/article/details/121342866