Spring

Spring

Spring简介说明

1)Spring是一个开源框架,为简化企业级应用开发而生,Spring可以使用简单的JavaBean实现以前只有EJB才能实现的功能,Spring是一个IOC和AOP容器框架。

2)Spring容器是Spring的核心,一切Spring Bean都存储在Spring容器内,并由其通过IOC技术管理。Spring容器也就是一个Bean工厂(BeanFactory)。应用中Bean的实例化、获取、销毁等都是由这个Bean工厂管理的。

3)ApplicationContext接口用于完成容器的配置、初始化、管理bean。一个Spring容器就是某个实现了ApplicationContext接口的类的实例。也就是说,从代码层面,Spring容器其实就是一个ApplicationContext。在普通的JAVA工程中,我们可以通过代码显式new一个ClassPathXmlApplicationContext或者FileSystemXmlApplicationContext来初始化一个Spring容器。在Web工程中,我们一般是通过配置web.xml的方式来初始化Spring容器。

Spring特征说明

1)轻量:从大小与开销两方面而言Spring都是轻量的。完整的Spring框架可以在一个大小只有1MB多的JAR文件里发布,并且Spring所需的处理开销也是微不足道的。此外,Spring是非侵入式的,Spring应用中的对象不依赖于Spring的特定类。

2)IOC(控制反转):Spring通过一种称作控制反转的技术促进了低耦合。当应用了IOC,一个对象依赖的其它对象会通过被动的方式传递进来,而不是这个对象自己创建或者查找依赖对象。你可以认为IOC与JNDI相反,即不是对象从容器中查找依赖,而是容器在对象初始化时不等对象请求就主动将依赖传递给它。

3)AOP(面向切面):Spring提供了面向切面编程的丰富支持,允许通过分离应用的业务逻辑与系统级服务,例如审计和事务管理进行内聚性的开发。应用对象只实现业务逻辑仅此而已,它们并不负责其它的系统级关注点,例如日志或事务支持。

4)容器:Spring包含并管理应用对象的配置和生命周期,在这个意义上它是一种容器,基于一个可配置原型,你可以配置你的每个Bean如何被创建,你的Bean可以创建一个单独的实例或者每次需要时都生成一个新的实例,以及它们是如何相互关联的。然而,Spring不应该被混同于传统的重量级的EJB容器,它们经常是庞大与笨重的,难以使用。

5)框架:Spring可以将简单的组件配置、组合成为复杂的应用。在Spring中,应用对象被声明式地组合,典型地是在一个XML文件里进行配置。Spring也提供了很多基础功能,如事务管理、持久化框架集成等等,开发者只需要进行应用逻辑的开发。

6)MVC:Spring的作用是整合,但不仅仅限于整合,Spring框架可以被看做是一个企业解决方案级别的框架。客户端发送请求,前端控制器(由DispatcherServlet实现的)完成请求的转发,控制器调用一个用于映射的类HandlerMapping,该类用将请求映射到对应的处理器来处理请求。HandlerMapping将请求映射到对应的处理器Controller,在Spring当中如果写一些处理器组件,一般实现Controller接口,在Controller中就可以调用一些Service或DAO来进行数据操作,ModelAndView用于存放从DAO中取出的数据,还可以存放响应视图的一些数据。 如果想将处理结果返回给用户,那么在Spring框架中还提供一个视图组件ViewResolver,该组件根据Controller返回的标示,找到对应的视图,将响应response返回给用户。

Spring结构说明

image-20210818101218648

核心容器模块

包括Core、Beans、Context、EL模块
1)Core模块:封装了框架依赖的最底层部分,包括资源访问、类型转换及一些常用工具类。

2)Beans模块:提供了框架的基础部分,包括控制反转和依赖注入。其中Bean Factory是容器核心,本质是工厂设计模式的实现,而且无需编程实现单例设计模式,单例完全由容器控制,而且提倡面向接口编程,而非面向实现编程;所有应用程序对象及对象间关系由框架管理,从而真正把你从程序逻辑中把维护对象之间的依赖关系提取出来,所有这些依赖关系都由BeanFactory来维护。

3)Context模块:以Core和Beans为基础,集成Beans模块功能并添加资源绑定、数据验证、国际化、Java EE支持、容器生命周期、事件传播等;核心接口是ApplicationContext。

4)EL模块:提供强大的表达式语言支持,支持访问和修改属性值,方法调用,支持访问及修改数组、容器和索引器,命名变量,支持算数和逻辑运算,支持从Spring容器获取Bean,它也支持列表投影、选择和一般的列表聚合等。

AOP、Aspects模块

1)AOP模块: Spring AOP模块提供了符合AOP Alliance规范的面向切面的编程实现,提供比如日志记录、权限控制、性能统计等通用功能和业务逻辑分离的技术,并且能动态的把这些功能添加到需要的代码中。这样各专其职,降低业务逻辑和通用功能的耦合。

2)Aspects模块:提供了对AspectJ的集成,AspectJ提供了比Spring ASP更强大的功能。

数据访问/集成模块

该模块包括了JDBC、ORM、OXM、JMS和事务管理。
1)事务模块:该模块用于Spring管理事务,只要是Spring管理对象都能得到Spring管理事务的好处,无需在代码中进行事务控制了,而且支持编程和声明式的事务管理。

2)JDBC模块:提供了一个JBDC的样例模板,使用这些模板能消除传统冗长的JDBC编码还有必须的事务控制,而且能享受到Spring管理事务的好处。

3)ORM模块:提供与流行的“对象-关系”映射框架的无缝集成,包括Hibernate、JPA、MyBatis等。而且可以使用Spring事务管理,无需额外控制事务。

4)OXM模块:提供了一个对Object/XML映射实现,将java对象映射成XML数据,或者将XML数据映射成java对象,Object/XML映射实现包括JAXB、Castor、XMLBeans和XStream。

5)JMS模块:用于JMS(Java Messaging Service),提供一套“消息生产者、消息消费者”模板用于更加简单的使用JMS,JMS用于用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。

Web/Remoting模块

包含了Web、Web-Servlet、Web-Struts、Web-Porlet模块。
1)Web模块:提供了基础的web功能。例如多文件上传、集成IOC容器、远程过程访问(RMI、Hessian、Burlap)以及Web Service支持,并提供一个RestTemplate类来提供方便的Restful services访问。

2)Web-Servlet模块:提供了一个Spring MVC Web框架实现。Spring MVC框架提供了基于注解的请求资源注入、更简单的数据绑定、数据验证等及一套非常易用的JSP标签,完全无缝与Spring其他技术协作。

3)Web-Struts模块:提供了与Struts无缝集成,Struts1.x和Struts2.x都支持。

Test模块

Spring支持Junit和TestNG测试框架,而且还额外提供了一些基于Spring的测试功能,比如在测试Web框架时,模拟Http请求的功能。

Spring设计模式

1)代理模式:Spring的AOP功能用到了JDK的动态代理和CGLIB字节码生成技术。
2)单例模式:在Spring的配置文件中设置的bean默认为单例模式。
3)模板方法模式:用来解决代码重复的问题,比如:RestTemplate、JmsTemplate、JpaTemplate。
4)工厂模式:在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用同一个接口来指向新创建的对象,Spring中使用beanFactory来创建对象的实例。
5)观察者模式:定义对象键一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知被制动更新,如Spring中listener的实现ApplicationListener。

Spring核心思想

控制反转(Spring IOC)

1)IOC(Inversion of Control,控制反转),传统的Java开发模式中,当需要一个对象时,我们会自己使用new或者getInstance等直接或者间接调用构造方法创建一个对象。而在Spring开发模式中,Spring容器使用了工厂模式为我们创建了所需要的对象,不需要我们自己创建了,直接调用Spring提供的对象就可以了,这就是控制反转的思想。

2)IOC不是一种技术,只是一种思想,一个重要的面向对象编程的法则,它能指导我们如何设计出松耦合、更优良的程序。传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试。有了IOC容器后,把创建和查找依赖对象的控制权交给了容器,由容器进行注入、组合对象,所以对象与对象之间是松散耦合,这样也方便测试,利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活。

3)其实IOC对编程带来的最大改变不是从代码上,而是从思想上,发生了主从换位的变化。应用程序原本是老大,要获取什么资源都是主动出击,但是在IOC/DI思想中,应用程序就变成被动的了,被动的等待IOC容器来创建并注入它所需要的资源了。IOC很好的体现了面向对象设计法则之一,好莱坞法则:“别找我们,我们找你”,即由IOC容器帮对象找相应的依赖对象并注入,而不是由对象主动去找。

4)IOC是Spring的核心,贯穿始终。所谓IOC,对于Spring框架来说,就是由Spring来负责控制对象的生命周期和对象间的关系。Spring所倡导的开发方式就是如此,所有的类都会在Spring容器中登记,告诉Spring你是个什么东西,你需要什么东西,然后Spring会在系统运行到适当的时候,把你要的东西主动给你,同时也把你交给其他需要你的东西。所有的类的创建、销毁都由Spring来控制,也就是说控制对象生存周期的不再是引用它的对象,而是Spring。对于某个具体的对象而言,以前是它控制其他对象,现在是所有对象都被Spring控制,这就是控制反转。

依赖注入(Spring DI)

1)DI(Dependency Injection,依赖注入),Spring使用JavaBean对象的set方法或者带参数的构造方法为我们在创建所需对象时将其属性自动设置所需要的值的过程,就是依赖注入。

2)组件之间依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件复用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。

3)平常的Java开发中,程序员在某个类中需要依赖其它类的方法,则通常是new一个依赖类再调用类实例的方法,这种开发存在的问题是new的类实例不好统一管理,Spring提出了依赖注入的思想,即依赖类不由程序员实例化,而是通过Spring容器帮我们new指定实例并且将实例注入到需要该对象的类中。

4)依赖注入的另一种说法是控制反转,通俗的理解是:平常我们new一个实例,这个实例的控制权是我们程序员,而控制反转是指new实例工作不由我们程序员来做而是交给Spring容器来做。IOC的一个重点是在系统运行中,动态的向某个对象提供它所需要的其他对象。这一点是通过DI来实现的。依赖注入的方式有:Set注入、构造器注入、静态工厂的方法注入、实例工厂的方法注入等。

面向切面编程(Spring AOP)

1)AOP(Aspect Orient Programming,面向切面编程),在OOP面向对象编程思想中,我们将事物纵向抽成一个个的对象,而在面向切面编程中,我们将一个个的对象某些类似的方面横向抽成一个切面(纵向重复,横向抽取),对这个切面进行一些如权限控制、事物管理,记录日志等公用操作处理的过程就是面向切面编程。

2)AOP底层是动态代理,如果是接口采用JDK动态代理,如果是类采用CGLIB方式实现动态代理。AOP作为面向对象编程的一种补充,广泛应用于处理一些具有横切性质的系统级服务,如事务管理、安全检查、缓存、对象池管理等。

3)AOP实现的关键就在于AOP框架自动创建的AOP代理,AOP代理则可分为静态代理和动态代理两大类,其中静态代理是指使用AOP框架提供的命令进行编译,从而在编译阶段就可生成AOP代理类,因此也称为编译时增强;而动态代理则在运行时借助于JDK动态代理、CGLIB等在内存中临时生成AOP动态代理类,因此也被称为运行时增强。Spring的AOP代理由Spring的IOC容器负责生成、管理,其依赖关系也由IOC容器负责管理。因此,AOP代理可以直接使用容器中的其他Bean实例作为目标,这种关系可由IOC容器的依赖注入提供。

纵观AOP编程,其中需要程序员参与的只有3个部分:
一、定义普通业务组件。
二、定义切入点,一个切入点可能横切多个业务组件。
三、定义增强处理,增强处理就是在AOP框架为普通业务组件织入的处理动作。
上面3个部分的第一个部分是最平常不过的事情,无须额外说明。那么进行AOP编程的关键就是定义切入点和定义增强处理。一旦定义了合适的切入点和增强处理,AOP框架将会自动生成AOP代理。核心关注点(业务)是应用中一个模块的行为,一个核心关注点可能会被定义成一个我们想实现的一个功能。横切关注点(复用的,与业务无关)是一个关注点,此关注点是整个应用都会使用的功能,并影响整个应用,比如日志,安全和数据传输,几乎应用的每个模块都需要的功能,因此这些都属于横切关注点。

面向切面编程说明

AOP通知术语

1)切面(aspect):切入点+通知。在Spring AOP中,切面可以使用通用类(基于模式的风格) 或者在普通类中以@AspectJ注解来实现。
2)连接点(Joinpoint):目标对象中,所有可以增强的方法。连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。
3)通知/增强(Advice):增强的代码。在AOP术语中,切面的工作被称为通知。
4)切入点(Pointcut):目标对象,已经增强的方法。切点的定义会匹配通知所要织入的一个或多个连接点。我们通常使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。
5)目标对象(Target):被代理对象。
6)织入(Weaving):将通知应用到切入点的过程。
7)代理(Proxy):将通知织入到目标对象之后,形成代理对象。

AOP通知类型

1)前置通知(Before):在目标方法被调用之前调用通知功能
2)返回通知(After-returning):如果出现异常不会调用,在目标方法完成之后调用通知,此时不会关心方法的输出是什么
3)环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为
4)异常通知(After-throwing):在目标方法抛出异常后调用通知
5)后置通知(After):无论是否出现异常都会调用,在目标方法成功执行之后调用通知

AOP通知方式

1)通过配置文件设置通知,需要定义一个通知类MyAdviceAspect

<!-- AOP注入通知 -->
<!-- 配置目标对象 -->
<bean name="userServiceTarget" class="com.item.spring.service.UserServiceImpl"/>
<!-- 配置通知对象 -->
<bean name="myAdvice" class="com.item.spring.aspect.MyAdviceAspect"/>
<!-- 将通知织入目标对象 -->
<!-- 配置AOP, proxy-target-class用于设置是否强制使用cglib进行动态代理
         true表示强制使用,false则表示spring会根据目标对象有无实现接口来决定
         使用jdk动态代理还是cglib代理。默认值就是false-->
<aop:config>
    <!-- 配置切入点,id属性给切入点定义一个唯一标识,expression用于编写切入点表达式-->
    <!-- execution表达式的使用:切入范围是在方法级别-->
    <!-- 表达式语法[访问修饰符] 返回值类型 [完整类名].方法名(参数)-->
    <!-- *号表示通配所有,方法的参数可以使用".."来代表任意个数和类型的参数-->
    <!-- 配置切入点,即需要加强功能的方法;下面是简化过程
         public void com.item.ssh.service.UserServiceImpl.save()
         void com.item.ssh.service.UserServiceImpl.save()
         * com.item.ssh.service.UserServiceImpl.save()
         * com.item.ssh.service.UserServiceImpl.*
         * com.item.ssh.service.UserServiceImpl.*(..)
         * com.item.ssh.service.*ServiceImpl.*(..)-->
    <aop:pointcut id="pc" expression="execution(* com.item.spring.service.*ServiceImpl.*(..))"/>

    <!-- within表达式的使用:切入范围是在类级别-->
    <!--<aop:pointcut id="myCut" expression="within(edu.nf.ch11.service.impl.*)"/>-->

    <!-- 引用上面装配的切面类的id -->
    <aop:aspect ref="myAdvice">
        <!-- 配置具体的通知方法,method指定通知的方法名
            pointcut-ref引用上面定的切入点的id,也可以通过pointcut来编写相应的切入点表达式 -->
        <!-- 将myAdvice的before切入到UserServiceImpl.save() -->
        <!-- 前置通知 -->
        <aop:before method="before" pointcut-ref="pc"/>
        <!-- 后置通知,returning指定后置通知的参数名,用于获取目标方法的返回值-->
        <aop:after-returning method="afterReturning" pointcut-ref="pc"/>
        <!-- 环绕通知-->
        <aop:around method="around" pointcut-ref="pc"/>
        <!-- 异常通知,如果要获取目标方法抛出的异常对象,需要指定throwing属性,value对应异常通知的参数名-->
        <!-- <aop:after-throwing method="throwAdvice" pointcut-ref="myCut" throwing="e"/>-->
        <aop:after-throwing method="afterException" pointcut-ref="pc"/>
        <!-- 最终通知-->
        <aop:after method="after" pointcut-ref="pc"/>
    </aop:aspect>
</aop:config>

2)利用注解实现通知

@Aspect
public class MyAdviceAnno {
    /**
     * 方便管理切入点
     */
    @Pointcut("execution(* com.item.spring.service.*ServiceImpl.*(..))")
    public void pc() {
    }

    /**
     * 配置通知,并指定织入到哪去
     * 前置通知:目标方法运行之前
     */
    @Before("com.item.spring.aspect.MyAdviceAnno.pc()")
    public void before() {
        System.out.println("前置通知");
    }

    /**
     * 后置通知(如果出现异常不会调用):之后
     */
    @AfterReturning("execution(* com.item.spring.service.*ServiceImpl.*(..))")
    public void afterReturning() {
        System.out.println("后置通知(如果出现异常不会调用)");
    }

    /**
     * 环绕通知:之前之后
     *
     * @param pjp
     * @return
     * @throws Throwable
     */
    @Around("execution(* com.item.spring.service.*ServiceImpl.*(..))")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("环绕通知之前部分");
        //调用目标方法
        Object proceed = pjp.proceed();
        System.out.println("环绕通知之后部分");
        return proceed;
    }

    /**
     * 异常拦截通知:出现异常调用
     */
    @AfterThrowing("execution(* com.item.spring.service.*ServiceImpl.*(..))")
    public void afterException() {
        System.out.println("出现异常调用");
    }

    /**
     * 后置通知(无论是否出现异常都会调用):之后
     */
    @After("execution(* com.item.spring.service.*ServiceImpl.*(..))")
    public void after() {
        System.out.println("后置通知(无论是否出现异常都会调用)");
    }
}

核心容器(BeanFactory、ApplicationContext)

a6aae165c3dddab385cfa1849c9c16da

BeanFactory可以理解为就是个HashMap,Key是BeanName,Value是Bean实例。通常只提供注册、获取这两个功能,称之为低级容器。

ApplicationContext可以称之为高级容器,他继承了多个接口,因此具备了更多的功能,例如资源的获取,支持多种消息(如JSP tag的支持),比BeanFactory多了工具级别的支持等等。他的名字已经不是BeanFactory之类的工厂了,而是应用上下文,代表着整个大容器的所有功能。该接口定义了一个refresh方法,此方法是所有阅读Spring源码的人的最熟悉的方法,用于刷新整个容器,即重新加载/刷新所有的bean。

常用实现类

1)BeanFactory实现类

BeanFactory是工厂模式的一个实现,提供了控制反转功能,用来把应用的配置和依赖从正真的应用代码中分离。常用的BeanFactory实现有DefaultListableBeanFactory、XmlBeanFactory 、ApplicationContext等。最常用的就是XmlBeanFactory ,它根据XML文件中的定义加载beans,该容器从XML文件读取配置元数据并用它去创建一个完全配置的系统或应用。

2)ApplicationContext实现类

FileSystemXmlApplicationContext:此容器从一个XML文件中加载beans的定义,XML Bean配置文件的全路径名必须提供给它的构造函数。

ClassPathXmlApplicationContext:此容器也从一个XML文件中加载beans的定义,这里,你需要正确设置classpath,因为这个容器将在classpath里找bean配置。

WebXmlApplicationContext:此容器加载一个XML文件,此文件定义了一个WEB应用的所有bean。

两者的区别

1)功能区别:BeanFactory是Spring里面最底层的接口,包含了各种Bean的定义,读取Bean配置文档,管理Bean的加载、实例化,控制Bean的生命周期,维护Bean之间的依赖关系。ApplicationContext接口作为BeanFactory的派生,除了提供BeanFactory所具有的功能外,还提供了更完整的框架功能:继承MessageSource,因此支持国际化、统一的资源文件访问方式、提供在监听器中注册bean的事件、同时加载多个配置文件、载入多个(有继承关系)上下文 ,使得每一个上下文都专注于一个特定的层次,比如应用的web层。

2)加载方式:BeanFactroy采用的是延迟加载形式来注入Bean的,即只有在使用到某个Bean时调用getBean(),才对该Bean进行加载实例化。这样,我们就不能发现一些存在的Spring的配置问题。如果Bean的某一个属性没有注入,BeanFacotry加载后,直至第一次使用调用getBean方法才会抛出异常。ApplicationContext,它是在容器启动时,一次性创建了所有的Bean。这样,在容器启动时,我们就可以发现Spring中存在的配置错误,这样有利于检查所依赖属性是否注入。ApplicationContext启动后预载入所有的单实例Bean,通过预载入单实例bean,确保当你需要的时候,你就不用等待,因为它们已经创建好了。相对于基本的BeanFactory,ApplicationContext唯一的不足是占用内存空间。当应用程序配置Bean较多时,程序启动较慢。

3)创建方式:BeanFactory通常以编程的方式被创建,ApplicationContext还能以声明的方式创建,如使用ContextLoader。

4)注册方式:BeanFactory和ApplicationContext都支持BeanPostProcessor、BeanFactoryPostProcessor的使用,但两者之间的区别是:BeanFactory需要手动注册,而ApplicationContext则是自动注册。

Spring Bean相关

Spring Bean说明

SpringBean是那些形成Spring应用的主干的Java对象。它们被IOC容器初始化,装配,和管理。这些beans通过容器中配置的元数据创建,比如以XML文件中<bean/>的形式定义。一个SpringBean的定义包含容器必知的所有配置元数据,包括如何创建一个Bean,它的生命周期详情及它的依赖。当定义一个Bean在Spring里,我们还能给这个Bean声明一个作用域。它可以通过Bean定义中的scope属性来定义。如当Spring要在需要的时候每次生产一个新的Bean实例,Bean的scope属性被指定为prototype。另一方面,一个Bean每次使用的时候必须返回同一个实例,这个Bean的scope属性必须设为singleton。

Spring Bean作用域

1)singleton:Bean在每个IOC容器中只有一个实例,默认的SpringBean的作用域是singleton
2)prototype:一个Bean的定义可以有多个实例
3)request:每次http请求都会创建一个Bean,该作用域仅在基于web的SpringApplicationContext情形下有效
4)session:在一个HttpSession中,一个Bean定义对应一个实例。该作用域仅在基于web的SpringApplicationContext情形下有效
5)global-session:在一个全局的HttpSession中,一个Bean定义对应一个实例。该作用域仅在基于web的SpringApplicationContext情形下有效

Spring Bean生命周期

249748bc2b49e857

Bean实例化的时机也分为两种,BeanFactory管理的Bean是在使用到Bean的时候才会实例化Bean,ApplicantContext管理的Bean在容器初始化的时候就回完成Bean实例化。

流程说明:
1)Bean定义:Bean容器在配置文件中找到Spring Bean的定义。在配置文件里面用<bean></bean>来进行定义。
2)Bean容器使用Java Reflection API创建Bean的实例。
3)如果声明了任何属性,声明的属性会被设置。如果属性本身是Bean,则将对其进行解析和设置。
4)如果Bean类实现BeanNameAware接口,则将通过传递Bean的名称来调用setBeanName()方法。
5)如果Bean类实现BeanClassLoaderAware接口,则将通过传递加载此Bean的ClassLoader对象的实例来调用setBeanClassLoader()方法。
6)如果Bean类实现BeanFactoryAware接口,则将通过传递BeanFactory对象的实例来调用setBeanFactory()方法。
7)如果有任何与BeanFactory关联的BeanPostProcessors对象已加载Bean,则将在设置Bean属性之前调用postProcessBeforeInitialization()方法。
8)如果Bean类实现了InitializingBean接口,则在设置了配置文件中定义的所有Bean属性后,将调用afterPropertiesSet()方法。
9)Bean初始化:如果配置文件中的Bean定义包含init-method属性,则该属性的值将解析为Bean类中的方法名称,并将调用该方法。初始化有两种方式,1个是在配置文件中通过指定init-method属性来完成,另一个是实现InitializingBean接口。
10)如果为Bean Factory对象附加了任何Bean后置处理器,则将调用postProcessAfterInitialization()方法。
11)如果Bean类实现DisposableBean接口,则当Application不再需要Bean引用时,将调用destroy()方法。
12)Bean销毁:如果配置文件中的Bean定义包含destroy-method属性,那么将调用Bean类中的相应方法定义。销毁有两种方式,1种是使用配置文件指定的destroy-method属性,另一种是实现DisposeableBean接口。

总结:
对于Spring Bean的生命周期来说,可以分为四个阶段,其中初始化完成之后,就代表这个Bean可以使用了:实例化Instantiation、属性赋值Populate、初始化Initialization、销毁 Destruction。

相关内容推荐:
https://juejin.cn/post/7075168883744718856?searchId=2023101915170592B41F1DB1635B60A790

Spring Bean线程安全问题

1)Spring框架中的单例Bean不是线程安全的。Spring中的Bean默认是单例模式,Spring框架并没有对单例Bean进行多线程的封装处理。实际上大部分时候Bean是无状态的(比如dao类),所以某种程度上来说Bean也是安全的,但如果Bean有状态的话(比如view model对象,有状态就是有数据存储功能,无状态就是不会保存数据),那就要开发者自己去保证线程安全了,最简单的就是改变Bean的作用域,把singleton变更为prototype,这样请求Bean相当于new Bean()了,所以就可以保证线程安全。

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

Spring Bean循环依赖问题

问题描述:Spring的循环依赖过程,跟bean的生命周期密切相关,在实例化bean的时候,会完成依赖注入。所以就会出现:实例化A -> 属性填充注入B -> B还没有实例化,需要先进行实例化B(A等待) -> 实例化B -> 注入A -> A实例化未完成,无法注入 -> 实例化B失败 -> 实例化A失败。这个问题类似于我们常见的死锁。

Spring解决问题基本思路:就是在实例化过程中,提前把bean暴露出来,虽然此时还是个半成品(属性未填充),但是我们允许先注入,这样确实能解决问题。我们梳理一下: 实例化A -> 暴露a对象(半成品)-> 属性填充注入B -> B还没有实例化,需要先进行实例化B(A等待) -> 实例化B -> 注入A(半成品) -> 实例化B成功 -> 实例化A成功。通过提前把半成品的对象暴露出来,支持别的bean注入,确实可以解决循环依赖的问题。

Spring的具体处理方式:Spring是通过三级缓存来解决循环依赖的,提前暴露的对象存放在三级缓存中,二级缓存存放过渡bean,一级缓存存放最终形态的bean。其实这就是Spring解决循环依赖的流程,其核心思路就是:先将bean提前暴露到三级缓存中,后续有依赖注入的话,先将这个半成品的bean进行注入。之所以说这个bean是半成品,是因为暴露在三级缓存和二级缓存中的bean虽然已经创建成功,但是属性还没有进行填充,Aware回调等流程也没有执行,所以说它是一个不完整的bean对象。

三级缓存(singletonFactories):其存放的对象为ObjectFactory类型,主要作用是产生bean对象。Spring在这里存放的是一个匿名内部类,调用getObject()最终调用的是getEarlyBeanReference()。该方法的主要作用是:如果有需要,产生代理对象。如果bean被AOP切面代理,返回代理bean对象;如果未被代理,就返回原始的bean对象。getEarlyBeanReference()调用的SmartInstantiationAwareBeanPostProcessor,其实是Spring留的拓展点,本质是通过BeanPostProcessor定制bean的产生过程。绝大多数AOP(比如@Transactional)单例对象的产生,都是在这里进行了拓展,进而实现单例对象的生成。

二级缓存(earlySingletonObjects):主要存放过渡bean,也就是三级缓存中ObjectFactory产生的对象。主要作用是防止bean被AOP切面代理时,重复通过三级缓存对象ObjectFactory创建对象。被代理情况下,每次调用ObjectFactory#getObject()都是会产生新的代理对象的。这明显不满足spring单例的原则,所以需要二级缓存进行缓存。同时需要注意:二级缓存中存放的bean也是半成品的,此时属性未填充。

一级缓存(singletonObjects):也被称为单例池, 主要存放最终形态的bean(如果存在代理,存放的代理后的bean)。 一般情况我们获取bean都是从这里获取的,但是并不是所有的bean都在单例池里面,一些特殊的,比如原型的bean就不在里面。

是否一定要三级缓存:在没有AOP的情况下,二级缓存没有实际作用,只通过三级缓存和一级缓存就可以搞定。那如果bean被AOP代理了,情况就会大不一样,最核心的区别点:就是每次调用ObjectFactory#getObject()都会产生一个新的代理对象。比如A被加上事务,则会创建一个代理对象。但是A是单例的,也就是要保证,在Spring中,使用到该bean的地方,都是同一个bean才行。但是每次执行singletonFactory.getObject()都会产生新的代理对象。假设只有一级和三级缓存,每次从三级缓存中获取代理对象,都会产生新的代理对象,忽略性能不说,是不符合单例原则的。所以这里我们要借助二级缓存来解决这个问题,将singleFactory.getObject()产生的对象放到二级缓存中去,后面直接从二级缓存中拿,保证始终只有一个代理对象。

不支持循环依赖的情况
1)非单例的bean无法支持循环依赖
2)constructor注入导致无法支持循环依赖,这种情况下,A实例创建时 -> 构造注入B -> 查找B,容器中不存在,先实例化创建B -> 构造注入A -> 容器中不存在A(此时A还没有添加到三级缓存中) -> 异常UnsatisfiedDependencyException。因为暴露对象放入三级缓存的过程在实例创建之后,通过构造方法注入时,还没有放入三级缓存呢,所以无法支持构造器注入类型的循环依赖。
3)@Async导致无法支持循环依赖,Spring管理的bean是单例的,所以Spring默认要保证使用该bean的地方,指向的都是一个地址,也就是都是最终版本的bean。像带有@Async的循环依赖,会导致在b中注入的a和最终放到容器的a不一致,所以Spring提供了这样的自检机制,防止这种问题的发生。

Spring常用注解

常用注解说明

@Component:这将Java类标记为Bean。它是任何Spring管理组件的通用构造型。Spring的组件扫描机制现在可以将其拾取并将其拉入应用程序环境中。

@RequestMapping:@RequestMapping注解用于将特定HTTP请求方法映射到将处理相应请求的控制器中的特定类或方法。此注释可应用于两个级别:类级别,映射请求的URL;方法级别,映射URL以及HTTP请求方法。

@Controller:这将一个类标记为SpringMvc控制器。标有它的Bean会自动导入到IOC容器中。

@Service:此注解是组件注解的特化。它不会对@Component注解提供任何其他行为。您可以在服务层类中使用@Service而不是@Component,因为它以更好的方式指定了意图。

@Repository:这个注解是具有类似用途和功能的@Component注解的特化。它为DAO提供了额外的好处。它将DAO导入IOC容器,并使未经检查的异常有资格转换为Spring DataAccessException。

@Required:应用于Bean属性setter方法。此注解仅指示必须在配置时使用Bean定义中的显式属性值或使用自动装配填充受影响的Bean属性。如果尚未填充受影响的Bean属性,则容器将抛出BeanInitializationException。

@Autowired:可以更准确地控制应该在何处以及如何进行自动装配。此注解用于在setter方法、构造函数,具有任意名称或多个参数的属性或方法上进行自动装配Bean。默认情况下,它是类型驱动的注入。@Autowired默认是按照类型装配注入的,默认情况下它要求依赖对象必须存在(可以设置它required属性为false)。

@Qualififier:当您创建多个相同类型的Bean并希望仅使用属性装配其中一个Bean时,您可以使用@Qualififier注解和@Autowired通过指定应该装配哪个确切的Bean来消除歧义。

Autowired与Resource

@Resource和@Autowired都是做bean的注入时使用,其实@Resource并不是Spring的注解,它的包是javax.annotation.Resource,需要导入,但是Spring支持该注解的注入。共同点:两者都可以写在字段和setter方法上,两者如果都写在字段上,那么就不需要再写setter方法。不同点:

1)@Autowired为Spring提供的注解,需要导入包org.springframework.beans.factory.annotation.Autowired,只按照byType注入。

// 下面两种@Autowired只要使用一种即可 
public class TestServiceImpl {
    // 用于字段上
    @Autowired 
    private UserDao userDao; 
    
    // 用于属性的方法上 
    @Autowired 
    public void setUserDao(UserDao userDao) { 
        this.userDao = userDao; 
    } 
}

@Autowired注解是按照类型(byType)装配依赖对象,默认情况下它要求依赖对象必须存在,如果允许null值,可以设置它的required属性为false。如果我们想使用按照名称(byName)来装配,可以结合@Qualififier注解一起使用。如下:

public class TestServiceImpl { 
    @Autowired 
    @Qualifier("userDao") 
    private UserDao userDao; 
}

2)@Resource默认按照ByName自动注入,由J2EE提供,需要导入包javax.annotation.Resource。@Resource有两个重要的属性:name和type,而Spring将@Resource注解的name属性解析为Bean的名字,而type属性则解析为Bean的类型。所以,如果使用name属性,则使用byName的自动注入策略,而使用type属性时则使用byType自动注入策略。如果既不制定name也不制定type属性,这时将通过反射机制使用byName自动注入策略。

// 下面两种@Resource只要使用一种即可
public class TestServiceImpl { 
    // 用于字段上
    @Resource(name="userDao") 
    private UserDao userDao; 
    
    // 用于属性的setter方法上
    @Resource(name="userDao") 
    public void setUserDao(UserDao userDao) {  
        this.userDao = userDao; 
    } 
}

注:最好是将@Resource放在setter方法上,因为这样更符合面向对象的思想,通过set、get去操作属性,而不是直接去操作属性。

Resource装配顺序

1)如果同时指定了name和type,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则抛出异常。
2)如果指定了name,则从上下文中查找名称或id匹配的Bean进行装配,找不到则抛出异常。
3)如果指定了type,则从上下文中找到类似匹配的唯一Bean进行装配,找不到或是找到多个,都会抛出异常。
4)如果既没有指定name,又没有指定type,则自动按照byName方式进行装配,如果没有匹配,则回退为一个原始类型进行匹配,如果匹配成功则自动装配。

Spring事务相关

事务四大特征

事务是并发控制的单位,是用户定义的一个操作序列。这些操作要么都做,要么都不做,是一个不可分割的工作单位。

事务四大特征(ACID):
原子性:表示事务内操作不可分割,要么都成功、要么都是失败。
一致性:要么都成功,要么都是失败,后面的失败了要对前面的操作进行回滚。
隔离性:一个事务开始后,不能被其他事务干扰。
持久性/持续性:表示事务开始了,就不能终止。

编程式事务

在代码中显式挪用beginTransaction()、commit()、rollback()等事务治理相关的方法,这就是编程式事务管理。Spring对事务的编程式管理有基于底层API的编程式管理和基于TransactionTemplate的编程式事务管理两种方式。

1)基于底层API的编程式管理:凭证PlatformTransactionManager 、TransactionDefinition和TransactionStatus三个焦点接口,来实现编程式事务管理。

Public class BankServiceImpl implements BancService{ 
    Private BanckDao bankDao; 
    private TransactionDefinition txDefinition; 
    private PlatformTransactionManager txManager; 
    ...... 
    public boolean transfer(Long fromId, Long toId, double amount) { 
        //获取事务
        TransactionStatus txStatus = txManager.getTransaction(txDefinition); 
        boolean result = false; 
        try { 
            result = bankDao.transfer(fromId, toId, amount); 
            //提交事务
            txManager.commit(txStatus); 
        } catch (Exception e) { 
            result = false; 
            //回滚事务
            txManager.rollback(txStatus); 
            System.out.println("Transfer Error!"); 
        } 
        return result; 
    }
}

2)基于TransactionTemplate的编程式事务管理:为了不损坏代码原有的条理性,避免出现每一个方法中都包括相同的启动事务、提交、回滚事物样板代码的现象,Spring提供了TransactionTemplate模板来实现编程式事务管理。

public class BankServiceImpl implements BankService { 
    private BankDao bankDao; 
    private TransactionTemplate transactionTemplate; 
    ...... 
    //模板过程(1.打开事务 2.调用doInTransaction方法 3.提交事务)
    public boolean transfer(final Long fromId, final Long toId, final double amount) { 
        return (Boolean) transactionTemplate.execute(new TransactionCallback(){ 
            //实现对事务的管理
            public Object doInTransaction(TransactionStatus status) { 
                Object result; 
                try { 
                    result = bankDao.transfer(fromId, toId, amount); 
                } catch (Exception e) { 
                    status.setRollbackOnly(); 
                    result = false; 
                    System.out.println("Transfer Error!"); 
                } 
                return result; 
            } 
        }); 
    } 
} 

声明式事务

用在Spring配置文件中声明式的处理事务来代替代码式的处理事务。这样的好处是,事务管理不侵入开发的组件,具体来说,业务逻辑对象就不会意识到正在事务管理之中,事实上也应该如此,因为事务管理是属于系统层面的服务,而不是业务逻辑的一部分,如果想要改变事务管理策划的话,也只需要在定义文件中重新配置即可,这样维护起来极其方便。

1)基于TransactionInterceptor的声明式事务管理:两个次要的属性,一个transactionManager,用来指定一个事务治理器,并将具体事务相关的操作委托给它;另外一个是Properties类型的transactionAttributes属性,该属性的每一个键值对中,键指定的是方法名,方法名可以使用通配符,而值就是表现呼应方法的所运用的事务属性。

<bean id="transactionInterceptor" 
      class="org.springframework.transaction.interceptor.TransactionInterceptor"> 
    <property name="transactionManager" ref="transactionManager"/> 
    <property name="transactionAttributes"> 
        <props> 
            <prop key="transfer">PROPAGATION_REQUIRED</prop> 
        </props> 
    </property> 
</bean> 
<bean id="bankServiceTarget" 
      class="footmark.spring.core.tx.declare.origin.BankServiceImpl"> 
    <property name="bankDao" ref="bankDao"/> 
</bean> 
<bean id="bankService" 
      class="org.springframework.aop.framework.ProxyFactoryBean"> 
    <property name="target" ref="bankServiceTarget"/> 
    <property name="interceptorNames"> 
        <list> 
            <idref bean="transactionInterceptor"/> 
        </list> 
    </property> 
</bean> 

2)基于TransactionProxyFactoryBean的声明式事务管理:设置配置文件与先前比照简化了许多。我们把这类设置配置文件格式称为Spring经典的声明式事务治理。

<bean id="bankServiceTarget" 
      class="footmark.spring.core.tx.declare.classic.BankServiceImpl"> 
    <property name="bankDao" ref="bankDao"/> 
</bean> 
<bean id="bankService"                      class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"> 
    <property name="target" ref="bankServiceTarget"/> 
    <property name="transactionManager" ref="transactionManager"/> 
    <property name="transactionAttributes"> 
        <props> 
            <prop key="transfer">PROPAGATION_REQUIRED</prop> 
        </props> 
    </property> 
</bean> 

3)基于tx命名空间的声明式事务治理:在前两种方法的基础上,Spring 2.x引入了tx命名空间,带给开发人员设置配备声明式事务的全新体验。

<!-- xml配置;配置事务通知 -->
<tx:advice transaction-manager="transactionManager" id="txAdvice">
    <!-- 配置事务的传播属性 -->
    <tx:attributes>
        <!--
                name指定要参与事务的方法,可以使用*号通配
                propagation指定事务的传播级别
                read-only设置当前事务设置为只读,
                rollback-for属性设置当遇到什么异常就执行回滚,
                默认是只要遇到任何的运行时异常就进行回滚,也可以指定为其他的检查时异常
            -->
        <!-- isolation:隔离级别;propagation:传播行为 ;read-only:只读-->
        <!-- 以方法名为单位进行配置 -->
        <tx:method name="transfer" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="false"/>
        <!-- 通常都会加通配符进行配置 -->
        <tx:method name="save*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="false"/>
        <tx:method name="persist*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="false"/>
        <tx:method name="update*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="false"/>
        <tx:method name="modify*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="false"/>
        <tx:method name="delete*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="false"/>
        <tx:method name="remove*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="false"/>
        <tx:method name="get*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="true"/>
        <tx:method name="find*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="true"/>
    </tx:attributes>
</tx:advice>
<!-- 配置织入 -->
<aop:config>
    <!-- 目标对象 -->
    <aop:pointcut id="txPc" expression="execution(* com.item.spring.service.*ServiceImpl.*(..))"/>
    <!-- 配置切面:advice-ref:通知;pointcut-ref:切入点 -->
    <!-- 配置事务切面(这里表示为一个事务通知器)
        advice-ref引用上面的通知,pointcut指定一个切入点表达式
        这个表达式用于描述哪些类上的哪些方法需要参与事务管理-->
    <aop:advisor advice-ref="txAdvice" pointcut-ref="txPc"/>
</aop:config>

4)基于@Transactional的声明式事务管理:Spring 2.x还引入了基于Annotation的体式格式,@Transactional可以浸染于接口、接口方法、类和类方法上。作用于类上时,该类的一切public方法将都具有该类型的事务属性。

@Transactional(propagation = Propagation.REQUIRED) 
public boolean transfer(Long fromId, Long toId, double amount) { 
    return bankDao.transfer(fromId, toId, amount); 
}

事务传播行为

Spring事务的传播行为说的是,当多个事务同时存在的时候,Spring如何处理这些事务的行为。

1)PROPAGATION_REQUIRED:如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,该设置是最常用的设置。

2)PROPAGATION_SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。

3)PROPAGATION_MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。

4)PROPAGATION_REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事务。

5)PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。

6)PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。

7)PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则按REQUIRED属性执行。

事务隔离级别

脏读:表示一个事务能够读取另一个事务中还未提交的数据。比如,某个事务尝试插入记录A,此时该事务还未提交,然后另一个事务尝试读取到了记录A。

不可重复读:是指在一个事务内,多次读同一数据。

幻读:指同一个事务内多次查询返回的结果集不一样。比如同一个事务A第一次查询时候有n条记录,但是第二次同等条件下查询却有 n+1 条记录,这就好像产生了幻觉。发生幻读的原因也是另外一个事务新增或者删除或者修改了第一个事务结果集里面的数据,同一个记录的数据内容被修改了,所有数据行的记录就变多或者变少了。

Spring有五大隔离级别,默认值为ISOLATION_DEFAULT,其他四个隔离级别和数据库的隔离级别一致:

1)ISOLATION_DEFAULT:用底层数据库的设置隔离级别,数据库设置的是什么就用什么。

2)ISOLATION_READ_UNCOMMITTED:未提交读,最低隔离级别、事务未提交前,就可被其他事务读取(会出现幻读、脏读、不可重复读)。

3)ISOLATION_READ_COMMITTED:提交读,一个事务提交后才能被其他事务读取到(会造成幻读、不可重复读),SQL Server的默认级别。

4)ISOLATION_REPEATABLE_READ:可重复读,保证多次读取同一个数据时,其值都和事务开始时候的内容是一致,禁止读取到别的事务未提交的数据(会造成幻读),MySQL的默认级别。

5)ISOLATION_SERIALIZABLE:序列化,代价最高最可靠的隔离级别,该隔离级别能防止脏读、不可重复读、幻读。

Spring事务失效场景

抛出检查异常导致事务不能正确回滚

@Service
public class Service1 {
    @Autowired
    private AccountMapper accountMapper;

    @Transactional
    public void transfer(int from, int to, int amount) throws FileNotFoundException {
        int fromBalance = accountMapper.findBalanceBy(from);
        if (fromBalance - amount >= 0) {
            accountMapper.update(from, -1 * amount);
            new FileInputStream("aaa");
            accountMapper.update(to, amount);
        }
    }
}

原因:Spring默认只会回滚非检查异常
解法:可以配置rollbackFor属性解决
@Transactional(rollbackFor = Exception.class)

业务方法内自己try-catch异常导致事务不能正确回滚

@Service
public class Service2 {
    @Autowired
    private AccountMapper accountMapper;

    @Transactional(rollbackFor = Exception.class)
    public void transfer(int from, int to, int amount)  {
        try {
            int fromBalance = accountMapper.findBalanceBy(from);
            if (fromBalance - amount >= 0) {
                accountMapper.update(from, -1 * amount);
                new FileInputStream("aaa");
                accountMapper.update(to, amount);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }
}

原因:事务通知只有捉到了目标抛出的异常,才能进行后续的回滚处理,如果目标自己处理掉异常,事务通知无法知悉。
解法1:异常原样抛出
在catch块添加throw new RuntimeException(e);
解法2:手动设置TransactionStatus.setRollbackOnly()
在catch块添加TransactionInterceptor.currentTransactionStatus().setRollbackOnly();

AOP切面顺序导致导致事务不能正确回滚

@Service
public class Service3 {
    @Autowired
    private AccountMapper accountMapper;

    @Transactional(rollbackFor = Exception.class)
    public void transfer(int from, int to, int amount) throws FileNotFoundException {
        int fromBalance = accountMapper.findBalanceBy(from);
        if (fromBalance - amount >= 0) {
            accountMapper.update(from, -1 * amount);
            new FileInputStream("aaa");
            accountMapper.update(to, amount);
        }
    }
}

@Aspect
public class MyAspect {
    @Around("execution(* transfer(..))")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        LoggerUtils.get().debug("log:{}", pjp.getTarget());
        try {
            return pjp.proceed();
        } catch (Throwable e) {
            e.printStackTrace();
            return null;
        }
    }
}

原因:事务切面优先级最低,但如果自定义的切面优先级和他一样,则还是自定义切面在内层,这时若自定义切面没有正确抛出异常。
解法1:异常原样抛出
在catch块添加throw new RuntimeException(e);
解法2:手动设置TransactionStatus.setRollbackOnly()
在catch块添加TransactionInterceptor.currentTransactionStatus().setRollbackOnly();
解法3:调整切面顺序,在MyAspect上添加@Order(Ordered.LOWEST_PRECEDENCE - 1)(不推荐)

非Public方法导致的事务失效

@Service
public class Service4 {
    @Autowired
    private AccountMapper accountMapper;

    @Transactional
    void transfer(int from, int to, int amount) throws FileNotFoundException {
        int fromBalance = accountMapper.findBalanceBy(from);
        if (fromBalance - amount >= 0) {
            accountMapper.update(from, -1 * amount);
            accountMapper.update(to, amount);
        }
    }
}

原因:Spring为方法创建代理、添加事务通知、前提条件都是该方法是public的。如果我们自定义的事务方法(即目标方法),它的访问权限不是public,而是private、default或protected的话,spring则不会提供事务功能。
解法1:改为public方法
解法2:添加bean配置如下(不推荐)
@Bean
public TransactionAttributeSource transactionAttributeSource() {
    return new AnnotationTransactionAttributeSource(false);
}

方法用static、final修饰

@Service
public class UserService {
    @Transactional
    public final void add(UserModel userModel){
        saveData(userModel);
        updateData(userModel);
    }
}

Spring事务底层使用了aop,也就是通过jdk动态代理或者cglib,帮我们生成了代理类,在代理类中实现的事务功能。但如果某个方法用final修饰了,那么在它的代理类中,就无法重写该方法,而添加事务功能。

调用本类方法导致传播行为失效

@Service
public class Service6 {
    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public void foo() throws FileNotFoundException {
        LoggerUtils.get().debug("foo");
        bar();
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
    public void bar() throws FileNotFoundException {
        LoggerUtils.get().debug("bar");
    }
}

原因:本类方法调用不经过代理,因此无法增强
解法1:依赖注入自己(代理)来调用
@Service
public class Service6 {
	@Autowired
	private Service6 proxy; // 本质上是一种循环依赖

    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public void foo() throws FileNotFoundException {
         LoggerUtils.get().debug("foo");
		System.out.println(proxy.getClass());
		proxy.bar();
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
    public void bar() throws FileNotFoundException {
        LoggerUtils.get().debug("bar");
    }
}

解法2:通过AopContext拿到代理对象,来调用
解法2,还需要在AppConfig上添加@EnableAspectJAutoProxy(exposeProxy = true)
@Service
public class Service6 {
    
    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public void foo() throws FileNotFoundException {
        LoggerUtils.get().debug("foo");
        ((Service6) AopContext.currentProxy()).bar();
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
    public void bar() throws FileNotFoundException {
        LoggerUtils.get().debug("bar");
    }
}

解法3:通过CTW,LTW实现功能增强

未被Spring管理

//@Service
public class UserService {
    @Transactional
    public void add(UserModel userModel) {
         saveData(userModel);
         updateData(userModel);
    }    
}

UserService类没有加@Service注解,那么该类不会交给Spring管理,所以它的add方法也不会生成事务。

多线程调用

@Service
public class Service7 {
    private static final Logger logger = LoggerFactory.getLogger(Service7.class);

    @Autowired
    private AccountMapper accountMapper;

    @Transactional(rollbackFor = Exception.class)
    public void transfer(int from, int to, int amount) {
        int fromBalance = accountMapper.findBalanceBy(from);
        logger.debug("更新前查询余额为: {}", fromBalance);
        if (fromBalance - amount >= 0) {
            accountMapper.update(from, -1 * amount);
            accountMapper.update(to, amount);
        }
    }

    public int findBalance(int accountNo) {
        return accountMapper.findBalanceBy(accountNo);
    }
}

上面的代码实际上是有bug的,假设from余额为1000,两个线程都来转账1000,可能会出现扣减为负数的情况。原因:事务的原子性仅涵盖insert、update、delete、select … for update语句,select方法并不阻塞

针对上面第7个问题,能否在方法上加synchronized锁来解决呢?
答案是不行,原因如下:synchronized保证的仅是目标方法的原子性,环绕目标方法的还有commit等操作,它们并未处于sync块内。
解法1:synchronized范围应扩大至代理方法调用
解法2:使用select … for update替换select

@Service
public class Service7 {
    private static final Logger logger = LoggerFactory.getLogger(Service7.class);

    @Autowired
    private AccountMapper accountMapper;

    @Transactional(rollbackFor = Exception.class)
    public synchronized void transfer(int from, int to, int amount) {
        int fromBalance = accountMapper.findBalanceBy(from);
        logger.debug("更新前查询余额为: {}", fromBalance);
        if (fromBalance - amount >= 0) {
            accountMapper.update(from, -1 * amount);
            accountMapper.update(to, amount);
        }
    }

    public int findBalance(int accountNo) {
        return accountMapper.findBalanceBy(accountNo);
    }
}
posted @ 2019-03-06 19:28  肖德子裕  阅读(450)  评论(0编辑  收藏  举报