(面试必备)超详细Spring IOC、AOP、事务解析及其案例

10,552 阅读29分钟

Spring优点

  • 方便解耦,简化开发 Spring就是一个大工厂,可以将所有对象的创建和依赖关系的维护交给Spring管理
  • 方便集成各种框架 Spring可以整合很多框架,比如Mybatis
  • 方便程序的测试 Spring支持JUnit4,可以通过注解很方便的测试Spring程序
  • 支持AOP(面向切面编程) Spring提供了面向切面编程
  • 声明式事务 只需要配置就可以完成对事务的管理,无需手动编程

使用Spring需要导入的jar

  • Spring-core:包含Spring框架基本的核心工具类,Spring的其他组件都要用到这个包中的类,是其他组件的基本核心
  • Spring-beans:包含访问配置文件、创建和管理bean以及进行IOC或者DI操作相关的所有类
  • Spring-context:Spring提供在基础IOC功能上的扩展服务,此外还提供许多企业级服务的支持,例如邮件服务、任务调度等
  • Spring-expression:定义了Spring的表达式语句
  • commons-logging:处理日志信息
  • 使用SPring框架,只需要倒入Spring的四个基础jar和处理日志信息的commons-logging即可

IOC

IOC是指在程序开发过程中,对象实例的创建不再由调用者管理,而是由Spring容器创建,Spring容器会负责控制程序之间的关系,而不是由代码直接控制,因此,控制权由程序代码转移到了Spring容器,控制权发生了反转,即控制反转。 Spring IOC提供了两种IOC容器,分别是BeanFactory和ApplicationContext。

BeanFactory

BeanFactory是基础的IOC容器,是一个接口,提供了完整的IOC服务,BeanFactory是一个管理Bean的工厂,他主要负责初始化各种bean,并调用它们的生命周期方法 BeanFactory接口有多个实现类,最常见的是XmlBeanFactory,它根据Xml配置文件中的定义装配Bean。 创建XmlBeanFactory对象,传入xml文件

BeanFactory beanFactory = new XmlBeanFactory(newFileSystemResource("D://applicationContext.xml"));

ApplicationContext

ApplicationContext是BeanFactory的字接口,也被称为应用上下文,不仅提供了BeanFactory的所有功能,还添加了对国际化、资源访问、事件传播等方面的支持。

ApplicationContext提供了两个实现类

  • ClassPathXmlApplicationContext 该类从类路径ClassPath中寻找制定的xml文件
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(String configLocation);

configLocation参数用来指定Spring配置文件的名称和位置

  • FileSystemXmlApplicationContext 该类从指定的文件系统路径中寻找指定的xml文件
* ApplicationContext applicationContext = new FileSystemXmlApplicationContext(String configLocation);

ClassPathXmlApplicationContext和FileSystemXmlApplicationContext的区别 在读取Spring的配置文件时,ClassPathXmlApplicationContext从ClassPath中读取配置文件,而FileSystemXmlApplicationContext通过configLocated参数的值从文件系统中读取xml文件。

在Java项目中,通常使用ClassPathXmlApplicationContext类实例化ApplicationContext容器,而在web项目中,ApplicationContext容器的实例化工作交给web服务器完成,web服务器实例化ApplicationContext容器通常使用基于ContextLoaderListener实现的方式,只需要编辑web。xml文件即可

<!--指定Spring配置文件的位置,有多个配置文件时,以逗号分隔-->
<context-param>
    <param-name>contextConfigLocation</param-name>
    <!--spring将加载spring目录下的applicationContext.xml文件-->
    <param-value>
        classpath:spring/applicationContext.xml
    </param-value>
</context-param>
<!--指定以ContextLoaderListener方式启动Spring容器-->
<listener>
    <listener-class>
        org.springframework.web.context.ContextLoaderListener
    </listener-class>
</listener>

BeanFactory和ApplicationContext的区别 如果Bean的某一个属性没有注入,则使用BeanFactory加载后,在第一次调用getBean方法时会抛出异常,而ApplicationContext会在初始化时检查,有利于检查所依赖的属性是否注入。

依赖注入(DI)

依赖注入和控制反转含义相同,他们是从两个角度描述同一个概念。 当某个对象实例需要另外一个对象实例时,传统的方法是由调用者创建被调用者的实例,比如使用new,而使用Spring框架后,被调用者的实例不再有调用者创建,而是交给了Spring容器,者称为控制反转。 Spring容器在创建被调用实例时,会自动将调用者需要的对象实例注入为调用者,这样,通过 Spring容器获得被调用者实例,成为依赖注入。

依赖注入的实现方式

  • 属性setter注入
//创建类A
public class A {
    public A() {
        System.out.println("正在初始化类A,调用无参构造器A。。。");
    }
    public void print() {
        System.out.println("调用了类A的print方法。。。");
    }
}
//创建类B
public class B {
    private A a;
    public void setA(A a) {
        this.a = a;
    }
    public void print() {
        a.print();
    }
}
//配置Beans.xml
    <bean id="a" class="com.ssm.ioc.A"/>
    <bean id="b" class="com.ssm.ioc.B">
        <property name="a" ref="a"/>
    </bean>
//测试
    @Test
    public void test() {
        B b = (B) context.getBean("b");
        b.print();
    }
  • 构造构造器注入
//配置Beans.xml
    <bean id="a" class="com.ssm.ioc.A"/>
    <bean id="b" class="com.ssm.ioc.B">
        <constructor-arg name="a" ref="a"/>
    </bean>
//测试
    @Test
    public void test() {
        B b = (B) context.getBean("b");
        b.print();
    }
  • 静态工厂方式实例化
//创建工厂类
public class MyBeanFactory {
    public static A createCoffer() {
        return new A();
    }
}
//配置Beans.xml
<bean id="a" class="com.ssm.ioc.MyBeanFactory" factory-method="createA"/>
//测试
    @Test
    public void test() {
        A a = (A) context.getBean("a");
    }
  • 实例工厂实例化
//创建工厂类
public class MyBeanFactory {
    public MyBeanFactory() {
        System.out.println("A工厂实例化中。。。");
    }
    public A createBean() {
        return new A();
    }
}
//配置Beans.xml
    <bean id="myBeanFactory" class="com.ssm.ioc.MyBeanFactory"/>
    <bean id="a" factory-bean="myBeanFactory" factory-method="createBean"/>
//测试
    @Test
    public void test() {
        A a = (A) context.getBean("a");
    }

Bean的会话状态

  • 有状态会话的Bean 每个用户有自己特有的一个实例,在用户的生存期内,bean保持可用户的信息,即"有状态",一旦用户灭亡(调用结束或实例结束),bean的生命周期也会结束。
  • 无状态会话的Bean bean一旦被实例化就被加到会话池中,各个用户可以公用,即使用户死亡,bean的生命周期也不一定结束,他可能依然存在于会话池中,供其他用户使用,由于没有特定的用户,也就没办法保存用户的状态,所以叫无状态Bean,但无状态Bean并非没有状态,如果它有自己的属性,那么这些属性就会受到所有调用它的用户的影响。

Servlet的线程安全问题

Servlet体系结构是建立在Java多线程机制上的,它的生命周期是由web容器负责的,一个Servlet类Application中只有一个实例存在,也就是有多个线程在使用这个实例,这是单例模式的使用。Servlet本身是无状态的,无状态的单例是线程安全的,但是如果在Servlet中使用了实例变量,那么就会变成有状态了,是非线程安全的。

如何解决Servlet的线程安全问题

  • 避免使用实例变量
  • 避免使用非线程安全集合
  • 在多个Servlet中对某个外部对象的修改进行加锁操作

Spring中Bean的作用域

  • 在Spring配置文件中,使用的scope属性设置Bean的作用域
  • singleton 单例模式,使用singleton定义的Bean在Spring容器中只有一个实例,这也是Bean的默认作用域,所有的Bean请求,只要id与该Bean定义相匹配,就只会返回Bean的同一个实例。适用于无回话状态的Bean,例如(DAO层、Service层)。
  • prototype 原型模式,每次通过Spring容器获取prototype定义的Bean时,容器都会创建一个新的Bean实例,适用于需要需要保持会话状态的Bean(比如Struts2的Action类)。
  • request 在一次HTTP请求中,容器会返回该Bean的同一个实例,而对于不同的HTTP请求,会返回不同的实例,该作用域仅在当前HttpRequest内有效
  • session 在一次HttpSession中,容器会返回该Bean的同一个实例,而对于不同的HTTP请求,会返回不同的实例,该作用域仅在当前HttpSession内有效
  • global Session 在一个全局的session中,容器会返回该Bean的同一个实例,该作用域仅在使用portlet context时有效。

Spring Bean的生命周期

  • Spring容器可以管理singleton作用域Bean的生命周期,在此作用域下,Spring能够精确的知道该Bean何时被创建,何时初始化完成,以及何时被销毁。
  • 而对于prototype作用域的Bean,Spring只负责创建,当容器创建了Bean的实例后,Bean的实例就交给客户端代码管理,Spring容器不再跟踪其生命周期,每次客户端请求prototype作用域的bean时,Spring容器都会创建一个新的Bean,并且不会管那些被配置成prototype作用域的Bean的生命周期。

当一个Bean被加载到Spring容器时,就具有了生命周期,而Spring容器在保证一个Bean能够使用之前,会进行很多工作

Bean的生命周期的执行过程
  • 根据配置文件的配置调用Bean的构造方法或者工厂方法实例化Bean
  • 利用依赖注入完成Bean中所有属性值的配置注入
  • 如果Bean实现了BeanNameAware接口,则Spring调用Bean的setBeanName方法传入当前Bean的id值
  • 如果Bean实现了BeanFactoryAware接口,则Spring调用setBeanFactory方法传入当前工厂实例的引用
  • 如果Bean实现了ApplicationContextAware接口,则Spring调用setApplicationContext方法传入当前ApplicationContext实例的引用
  • 如果BeanPostProcessor和Bean关联,则Spring将调用该接口的预初始化方法postProcessBeforeInitialization对Bean进行加工,Spring AOP就是利用它实现的
  • 如果Bean实现了InitializingBean接口,则Spring调用afterPropertiesSet方法
  • 如果在配置文件中通过init-method属性指定了初始化方法,则调用该初始化方法
  • 如果BeanPostProcessor 和Bean关联,则Spring将调用该接口的初始化方法postPrecessAfterInitialization,此时,Bean已经可以使用
  • 如果在中指定了该Bean的作用范围为scope="singleton",则将该Bean放入Spring IOC的缓存池中,将触发Spring对该Bean的生命周期管理,若在中指定了该Bean的作用范围为scope="prototype",则将该Bean交给调用者,调用者管理该Bean的生命周期,Spring不再管理该Bean
  • 如果Bean实现了DisposableBean接口,则Spring会调用destroy方法将Spring中的Bean销毁,如果在配置文件中通过destroy-method属性指定了Bean的销毁方法,则Spring将调用该方法对Bean进行销毁

Spring基于XML装配Bean

设值注入和构造器注入

在实例化Bean的过程中,首先调用默认的构造方法实例Bean对象,然后通过java的反射机制调用setter方法对属性进行注入,因此设置注入要求一个Bean的对应类必须满足以下两点要求

  • 必须提供一个默认的无参构造方法
  • 必须为需要注入的属性提供setter方法

使用设值注入时,在Spring配置文件中,需要使用元素的子元素元素为每个属性注入值,而使用构造注入时,在配置文件中,主要使用标签定义构造方法的参数,可以使用value属性设置该参数的值。

Annotation(注解)注入

JDK1.5之后,提供了Annotation功能,Spring也提供了对Annotation的全面支持,Spring3中定义了一系列注解

  • @Component:可以使用此注解描述Spring中的Bean,但它是一个泛化的概念,仅仅表示一个组件(Bean),并且可以用在任何层次
  • @Repository:用于将数据访问层(DAO层)的类标示为Spring中的Bean
  • @Service:通常用作在业务层(Service层),用于将业务层的类标示为Spring中的Bean
  • @Controlle:通常作用在控制层,用于将控制层的类标示为Spring中的Bean
  • @Autowired:用于Bean的属性变量、属性的Set方法及构造函数进行标注,配合对应的注解处理器完成Bean的自动配置工作,默认按照Bean的类型进行装配
  • @Resource:作用与Autowired一样,区别是@Autowired默认按照Bean的类型装配,@而Resource默认按照Bean的实例类型进行装配,@Resource有name、type属性,Spring将name属性解析为Bean实例名称,将type属性解析为Bean的梳理类型,如果指定name属性,则按照实例名称进行装配,如果指定type属性,按照Bean类型进行装配,如果都不指定,则先按照Bean实例名称装配,如果不能装配,则按照Bean的类型进行装配,如果都不能匹配,抛出NoSuchBeanDefinitionException异常
  • @Qualifier:与@Autowired配合使用,会将默认的按照Bean配型装配修改为按Bean的实例名称装配,Bean的实例名称由@qualifier注解的参数指定

@Resource和@Autowired的区别

  • @Resource和@Autowired都是做bean的注入时使用
  • @Resource不是Spring中的注解,但是Spring支持该注解,而@Autowired是Spring的注解
  • @Autowired是按照类型(byType)来装配Bean的,不回去匹配name,默认情况下他要求依赖对象必须存在,如果需允许null,可以设置它的required属性为false,如果想让@Autowired按照名称(byName)来装配,则需要配合@Qualifier一起使用

@Resource注解装配步骤

  • 如果同时指定了name和type属性,则从Spring上下文中找到唯一匹配的Bean进行装配,找不到抛出异常
  • 如果指定了name,则从上下文中查找名称(id)匹配的Bean进行装配,找不到抛出异常
  • 如果指定了type,则从上下文查找类型匹配的唯一Bean进行装载,找不到或者是找到了多个,抛出异常
  • 如果即没有指定name,也没有指定type,则默认按照byName进行查找并装载,如果没有匹配,则按照byType进行匹配并装载

自动装配

自动装配是指Spring容器可以自动装配(autowire)相互协作的Bean之间的关系,将一个Bean注入其他Bean的Property中。 要使用自动装配,就需要配置元素的autowire值,autowire属性有五个

  • byName:根据Property的name自动装配,如果一个Bean的name和另外一个Bean中的Property相同,则会自动装配这个Bean到Property中
  • byType:根据Property的数据类型自动装配,如果一个Bean的数据类型兼容另外一个Bean中Property的数据类型,则自动装配
  • constructor:根据构造方法的参数的数据类型,进行byType模式的自动装配
  • autodetect:如果发现默认的构造方法,则用constructor,否则有byType
  • no:默认情况下,不实用自动装配,Bean依赖必须通过ref元素定义

AOP面向切面编程

AOP概述

  • AOP和OOP类似,也是一种编程模式,Spring AOP是基于AOP编程模式的一个框架,它的使用有效减少了系统间的重复代码,达到了模块间解耦的作用。
  • AOP的全程是"Aspect Oriented Programming",即面向切面编程,它将业务逻辑的各部分进行隔离,使开发人员在编写业务逻辑时可以专心核心业务,从而提高开发基础。
  • AOP采取横向抽取机制,取代了传统的纵向继承体系,其应用主要体现在事务处理、日志管理、权限控制、异常处理等方面。
  • 目前最流行的AOP有Spring AOP和AspectJ
  • Spring AOP使用纯Java实现,不需要专门的编译过程和类加载器,在运行期间通过代理方式向目标类植入增强的代码。
  • AspectJ是一个基于Java语言的AOP框架,从Spring2.0开始,Spring AOP引入了对AspectJ的支持。

AOP相关术语

名称 说明
Joinpoint(连接点) 指那些被拦截到的点,在Spring中,可以被动态代理拦截目标类的方法
Pointcut(切入点) 指要对哪些Joinpoint进行拦截,即被拦截的连接点
Advice(通知) 指拦截到Joinpoint之后要做的事情,即对切入点增强的内容
Target(目标) 指代理的目标对象
Weaving(植入) 指把增强代码应用到目标上,生成代理对象的过程
Proxy(代理) 指生成的代理对象
Aspect(切面) 切入点和通知的结合

JDK动态代理是通过JDK中的Java.lang.reflect.Proxy实现的

使用JDK中的Proxy实现动态代理

//创建接口
public interface CustomerDao {
    public void add(); // 添加
    public void update(); // 修改
    public void delete(); // 删除
    public void find(); // 查询
}

//创建接口实现类
public class CustomerDaoImpl implements CustomerDao {
    public void add() {
        System.out.println("添加客户...");
    }
    public void update() {
        System.out.println("修改客户...");
    }
    public void delete() {
        System.out.println("删除客户...");
    }
    public void find() {
        System.out.println("修改客户...");
    }
}

//创建切面类
public class MyAspect {
    public void myBefore() {
        System.out.println("方法执行之前");
    }
    public void myAfter() {
        System.out.println("方法执行之后");
    }
}

//创建代理类
import com.mengma.dao.CustomerDao;
import com.mengma.dao.CustomerDaoImpl;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class MyBeanFactory {
    public static CustomerDao getBean() {
        //准备目标类
        final CustomerDao customerDao = new CustomerDaoImpl();
        //创建切面类实例
        final MyAspect myAspect = new MyAspect();
        //使用代理类,进行增强
        return (CustomerDao) Proxy.newProxyInstance(
                //当前类加载器
                MyBeanFactory.class.getClassLoader(),
                //所创建实例的实现类接口
                new Class[]{CustomerDao.class},
                //需要增强的方法
                new InvocationHandler() {
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        myAspect.myBefore();
                        Object obj = method.invoke(customerDao, args);
                        myAspect.myBefore();
                        return obj;
                    }
                }
        );
    }
}

//创建测试类
import com.mengma.dao.CustomerDao;
import com.mengma.jdk.MyBeanFactory;
import org.junit.Test;
public class TestAOP {
    @Test
    public void test() {
        CustomerDao customerDao = MyBeanFactory.getBean();
        customerDao.add();
        customerDao.delete();
        customerDao.find();
        customerDao.update();
    }
}

输出结果 evernotecid://73D3823C-134C-4C14-AE11-0F871EA0D1DE/appyinxiangcom/27476071/ENResource/p4

在调用目标类的方法前后,成功调用了增强的代码

使用JDK自带的Proxy实现动态代理的局限性

  • JDK动态代理必须要实现一个或多个接口,如果不希望实现接口,可以使用CGLIB代理
  • CGLIB代理底层是通过使用一个小而快的字节码处理框架ASM(Java 字节码操控框架)转换字节码并生成新的类。因此CGLIB要依赖于ASM的jar包
  • Spring的core核心包中已经集成了CGLIB所需要的包

CGLIB实现动态代理

//创建目标类
public class GoodsDao {
    public void add() {
        System.out.println("添加商品...");
    }
    public void update() {
        System.out.println("修改商品...");
    }
    public void delete() {
        System.out.println("删除商品...");
    }
    public void find() {
        System.out.println("修改商品...");
    }
}

//创建切面类
public class MyAspect {
    public void myBefore() {
        System.out.println("方法执行之前");
    }
    public void myAfter() {
        System.out.println("方法执行之后");
    }
}

//创建代理类
import com.mengma.dao.GoodsDao;
import com.mengma.jdk.MyAspect;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class MyBeanFactory {
    public static GoodsDao getBean() {
        //创建目标类
        final GoodsDao goodsDao = new GoodsDao();
        //创建切面类实例
        final MyAspect myAspect = new MyAspect();
        //生成代理类,CGLIB在运行时,生成指定对象的子类,增强
        Enhancer enhancer = new Enhancer();
        //确定需要增强的类
        enhancer.setSuperclass(goodsDao.getClass());
        //添加回调函数
        enhancer.setCallback(new MethodInterceptor() {
            //intercept相当于jdk invoke
            public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                myAspect.myBefore();
                Object obj = method.invoke(goodsDao, args);
                myAspect.myAfter();
                return obj;
            }
        });
        GoodsDao goodsDaoProxy = (GoodsDao) enhancer.create();
        return goodsDaoProxy;
    }
}

//创建测试类
    @Test
    public void testCGLIB() {
        GoodsDao goodsDao = com.mengma.cglib.MyBeanFactory.getBean();
        goodsDao.add();
        goodsDao.delete();
        goodsDao.find();
        goodsDao.update();
    }

运行结果

Spring通知类型

通知(Advice)是对目标切入点进行增强的内容。Spring为通知(Advice)提供了Advice接口,Spring通知按照在目标类方法的连接点位置,可分为五种类型

名称 说明
MethodBeforeAdvice(前置通知) 在方法之前自动执行的通知成为前置通知,可以应用于权限管理功能
AfterReturningAdvice(后置通知) 在方法执行之后自动执行的通知成为后置通知,可用于关闭流、上传文件、删除临时文件等功能
MethodInterceptor(环绕通道) 在方法前后自动执行的通知成为环绕通知,可以应用于日志、事务管理等功能
ThrowsAdvice(异常通知) 在方法抛出异常时自动执行的通知成为异常通知,可以应用于处理异常记录日志等功能
IntroductionInterceptor(引介通知) 在目标类中添加一些新的方法和属性,可以应用于修改旧版本程序(增强类)

生命Spring AOP

Spring创建了一个AOP代理的基本方法是使用ProxyFactoryBean,这个类对应的切入点和通知提供了完整的控制能力,并可以生成指定的内容

ProxyFactoryBean类中的常用可配置属性
属性名称 描述
target 代理的目标对象
proxyInterfaces 代理要实现的接口,如果有多个接口,则可以使用list标签赋值
proxyTargetClass 是否对类代理而不是接口,设置为true时,使用CGLIB代理
interceptorNames 需要植入目标的Advice
singleton 返回的代理是否为单例,默认为true(单例)
optimize 设置为true时,强制使用CGLIB

环绕通知案例

//倒入Spring-aop依赖
        <!-- https://mvnrepository.com/artifact/org.springframework/spring-aop -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>5.1.5.RELEASE</version>
        </dependency>
//创建切面类
//需要实现接口,确定哪个通知,及告诉Spring应该执行哪个方法
public class MyAspect implements MethodInterceptor {
    public Object invoke(MethodInvocation mi) throws Throwable {
        System.out.println("方法执行之前");
        // 执行目标方法
        Object obj = mi.proceed();
        System.out.println("方法执行之后");
        return obj;
    }
}

在上述代码中,MyAspect实现了MethodInterceptor接口,并重写了invoke方法,MethodInterceptor是Spring AOP的jar包提供的,而invoke方法用于确定目标方法mi,并告诉Spring要在目标方法前后执行哪些方法。

//创建Spring配置文件
<?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="customerDao" class="com.mengma.dao.CustomerDaoImpl"/>
    <!-- 通知 advice -->
    <bean id="myAspect" class="com.mengma.factorybean.MyAspect"/>
    <!--生成代理对象 -->
    <bean id="customerDaoProxy"
          class="org.springframework.aop.framework.ProxyFactoryBean">
        <!--代理实现的接口 -->
        <property name="proxyInterfaces" value="com.mengma.dao.CustomerDao"/>
        <!--代理的目标对象 -->
        <property name="target" ref="customerDao"/>
        <!--用通知增强目标 -->
        <property name="interceptorNames" value="myAspect"/>
        <!-- 如何生成代理,true:使用cglib; false :使用jdk动态代理 -->
        <property name="proxyTargetClass" value="true"/>
    </bean>
</beans>

首先配置了目标类和通知,然后使用ProxyFactoryBean类生成代理对象。

//创建测试类
    @Test
    public void testAOP() {
        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
        CustomerDao customerDao = (CustomerDao) applicationContext.getBean("customerDaoProxy");
        customerDao.update();
        customerDao.find();
        customerDao.delete();
        customerDao.add();
    }

执行结果

Spring使用AspectJ开发AOP,基于XML和Annotation

使用AspectJ开发AOP的两种方式
  • 基于XML的声明式
  • 基于Annotation的声明式
基于XML的声明式开发

基于XML的声明式是指通过Spring配置文件的方式定义切面、切入点及声明通知,而所有的切面和通知都必须定义在aop:config元素中。

//倒入spring-aspects依赖
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>5.1.5.RELEASE</version>
        </dependency>
//创建切面类
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
//切面类
public class MyAspect {
    // 前置通知
    public void myBefore(JoinPoint joinPoint) {
        System.out.print("前置通知,目标:");
        System.out.print(joinPoint.getTarget() + "方法名称:");
        System.out.println(joinPoint.getSignature().getName());
    }
    // 后置通知
    public void myAfterReturning(JoinPoint joinPoint) {
        System.out.print("后置通知,方法名称:" + joinPoint.getSignature().getName());
    }
    // 环绕通知
    public Object myAround(ProceedingJoinPoint proceedingJoinPoint)
            throws Throwable {
        System.out.println("环绕开始"); // 开始
        Object obj = proceedingJoinPoint.proceed(); // 执行当前目标方法
        System.out.println("环绕结束"); // 结束
        return obj;
    }
    // 异常通知
    public void myAfterThrowing(JoinPoint joinPoint, Throwable e) {
        System.out.println("异常通知" + "出错了" + e.getMessage());
    }
    // 最终通知
    public void myAfter() {
        System.out.println("最终通知");
    }
}
//创建Spring配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="
            http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
            http://www.springframework.org/schema/aop
            http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">
    <!--目标类 -->
    <bean id="customerDao" class="com.mengma.dao.CustomerDaoImpl"/>
    <!--切面类 -->
    <bean id="myAspect" class="com.mengma.aspectJ.xml.MyAspect"/>
    <!--AOP 编程 -->
    <aop:config>
        <aop:aspect ref="myAspect">
            <!--配置切入点,通知最后增强哪些方法-->
            <aop:pointcut expression="execution ( * com.mengma.dao.*.* (..))" id="myPointCut"/>
            <!--前置通知,关联通知Advice和切入点PointCut-->
            <aop:before method="myBefore" pointcut-ref="myPointCut"/>
            <!--后置通知,在方法返回后执行,就可以获得返回值returning属性-->
            <aop:after-returning method="myAfterReturning" pointcut-ref="myPointCut" returning="joinPoint"/>
            <!--环绕通知-->
            <aop:around method="myAround" pointcut-ref="myPointCut"/>
            <!--抛出通知:用于处理程序发生异常,可以接收当前方法产生的异常 -->
            <!-- *注意:如果程序没有异常,则不会执行增强 -->
            <!-- * throwing属性:用于设置通知第二个参数的名称,类型Throwable -->
            <aop:after-throwing method="myAfterThrowing" pointcut-ref="myPointCut" throwing="e"/>
            <!--最终通知:无论程序发生任何事情,都将执行 -->
            <aop:after method="myAfter" pointcut-ref="myPointCut"/>
        </aop:aspect>
    </aop:config>
</beans>
//创建测试类
    @Test
    public void testAspectJForXml() {
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
        CustomerDao customerBean = (CustomerDao) applicationContext.getBean("customerDao");
        customerBean.add();
    }

执行结果

基于Annotation的声明式

AspectJ允许使用注解定义切面、切入点和增强处理,而Spring框架可以识别并根据注解生成AOP代理

名称 说明
@Aspect 用于定义一个切面
@Before 用于定义前置通知,相当于BeforeAdvice
@AfterReturning 用于定义后置通知,相当于AfterReturningAdvice
@Around 用于定义环绕通知,相当于MethodInterceptor
@AfterThrowing 用于定义抛出异常,相当于ThrowAdvice
@After 用于定义最终final通知,不管是否异常,该通知都会执行
DeclareParents 用于定义引介通知,相当于IntroductionInterceptor
开发案例
//创建切面类
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
//切面类
@Aspect
@Component
public class MyAspect {
    // 用于取代:<aop:pointcut
    // expression="execution(*com.mengma.dao..*.*(..))" id="myPointCut"/>
    // 要求:方法必须是private,没有值,名称自定义,没有参数
    @Pointcut("execution(*com.mengma.dao..*.*(..))")
    private void myPointCut() {
    }
    // 前置通知
    @Before("myPointCut()")
    public void myBefore(JoinPoint joinPoint) {
        System.out.print("前置通知,目标:");
        System.out.print(joinPoint.getTarget() + "方法名称:");
        System.out.println(joinPoint.getSignature().getName());
    }
    // 后置通知
    @AfterReturning(value = "myPointCut()")
    public void myAfterReturning(JoinPoint joinPoint) {
        System.out.print("后置通知,方法名称:" + joinPoint.getSignature().getName());
    }
    // 环绕通知
    @Around("myPointCut()")
    public Object myAround(ProceedingJoinPoint proceedingJoinPoint)
            throws Throwable {
        System.out.println("环绕开始"); // 开始
        Object obj = proceedingJoinPoint.proceed(); // 执行当前目标方法
        System.out.println("环绕结束"); // 结束
        return obj;
    }
    // 异常通知
    @AfterThrowing(value = "myPointCut()", throwing = "e")
    public void myAfterThrowing(JoinPoint joinPoint, Throwable e) {
        System.out.println("异常通知" + "出错了" + e.getMessage());
    }
    // 最终通知
    @After("myPointCut()")
    public void myAfter() {
        System.out.println("最终通知");
    }
}
//在CustomerDaoImpl目标类中添加注解@Repository("customerDao")
//创建ApplicationContext.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"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
            http://www.springframework.org/schema/aop
            http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
            http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context.xsd">
    <!--扫描含com.mengma包下的所有注解-->
    <context:component-scan base-package="com.mengma"/>
    <!-- 使切面开启自动代理 -->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
/。创建测试类
    @Test
    public void testAspectJForAnnottion() {
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
        CustomerDao customerDao = (CustomerDao) applicationContext.getBean("customerDao");
        customerDao.add();
    }

执行结果

Spring JDBCTemplate

Spring JDBCTemplate概述

  • Spring框架针对数据库开发中的应用提供了JDBCTemplate类,该类是Spring对JDBC支持的核心,提供了对所有数据库操作功能的支持。
  • Spring框架提供的JDBC支持主要由四个包组成,分别是core(核心包)、object(对象包)、dataSource(数据源包)和support(支持包),JdbcTemplate类在核心包中,作为Spring JDBC的核心,JdbcTemplate类包含了所有数据库操作的基本方法。
  • JdbcTemplate类继承自抽象类JdbcAccessor,同时实现了JdbcOperations接口,直接父类JdbcAccessor为子类提供了一些访问数据库时使用的公共属性

JdbcAccessor提供的公共属性

  • DataSource:获取数据库连接,具体实现时还可以引入对数据库连接的缓冲池和分布式事务的支持,它可以作为访问数据库资源的标准接口
  • SQLExceptionTranslator:SQLExceptiontranslator接口负责对SQLException进行转译工作,通过必要的设置或者获取SQLExceptionTranslator中的方法,可以使JdbcTemplate在需要处理SQLException时,委托SQLExceptionTranslator的实现类完成相关的转译工作。
  • JdbcOperations接口定义了在JdbcTemplate类中可以使用的操作集合,包括添加、修改、查询和删除工作

Spring中JDBC的相关配置

<?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="dataSource" class="org.springframework.jdbc.dataSource.DriverManagerDataSource">
        <!--数据库驱动-->
        <property name="driverClassName" value="com.mysql.jdbc.Driver" /> 
        <!--连接数据库的url-->
        <property name= "url" value="jdbc:mysql://localhost/spring" />
        <!--连接数据库的用户名-->
        <property name="username" value="root" />
        <!--连接数据库的密码-->
        <property name="password" value="root" />
    </bean>
    <!--配置JDBC模板-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.jdbcTemplate">
        <!--默认必须使用数据源-->
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <!--配置注入类-->
    <bean id="xxx" class="xxx">
        <property name="jdbcTemplate" ref="jdbcTemplate"/>
    </bean>
    ...
</beans>

Spring事务管理

Spring的事务管理是基于AOP实现的,而AOP是以方法为单位的,Spring的事务属性分别为传播行为、隔离界别、只读和超时属性,这些属性提供了事务的方法和描述策略

Spring事务管理的三个核心接口
  • PlatformTransactionManager:Spring提供的平台事务管理器,用于管理事务,提供了三个方法:getTransaction用于获取事务状态信息,commit用于提交事务,rollback用于事务的回滚,在项目中,Spring将xml中配置的事务详细信息封装到对象TransactionDefinition中,然后通过事务管理器的getTransaction方法获取事务的状态(TransactionStatus),并对事务进行下一步的操作
  • TransactionDefinition接口是事务定义的接口,提供了获取事务相关信息的方法
方法 描述
getName 获取事务对象名称
getIsolationLevel 获取事务的隔离级别
getPropagationBehavior 获取事务的传播行为
getTimeout 获取事务的超时时间
isReadOnly 获取事务是否只读

事务的传播行为是指在同一个方法中,不同操作前后所使用的事务

属性名称 描述
PROPAGATION_REQUIRED required 支持当前事务。如果 A 方法已经在事务中,则 B 事务将直接使用。否则将创建新事务
PROPAGATION_SUPPORTS supports 支持当前事务。如果 A 方法已经在事务中,则 B 事务将直接使用。否则将以非事务状态执行
PROPAGATION_MANDATORY mandatory 支持当前事务。如果 A 方法没有事务,则抛出异常
PROPAGATION_REQUIRES_NEW requires_new 将创建新的事务,如果 A 方法已经在事务中,则将 A 事务挂起
PROPAGATION_NOT_SUPPORTED not_supported 不支持当前事务,总是以非事务状态执行。如果 A 方法已经在事务中,则将其挂起
PROPAGATION_NEVER never 不支持当前事务,如果 A 方法在事务中,则抛出异常
PROPAGATION.NESTED nested 嵌套事务,底层将使用 Savepoint 形成嵌套事务

在事务管理过程中,传播行为可以控制是否需要创建事务以及如何创建事务,通常情况下,数据的查询不会改变事务的传播行为,所以不需要进行事务管理,而对于数据的增加、修改和删除等操作,必须进行事务管理,如果没有指定事务的传播行为,Spring默认的传播行为是required。

  • TransactionStatus,描述事务的状态,它描述了某一时间点上事务的状态信息,包含六个操作
名称 说明
flush 刷新事务
hasSavepoint 获取事务是否在保存点
isCompleted 获取事务是否完成
isNewTransaction 获取是否是新事务
isRollbackOnly 获取是否回滚
setRollbackonly 设置事务回滚
Spring的事务管理由两种方式
  • 传统的编程式事务管理,通过编写代码实现的事务管理
  • 基于AOP技术实现声明式的事务
基于AOP的声明式事务的实现方式
  • 基于XML方式的声明式事务
  • 通过Annotation注解方式的事务管理

基于XML方式的声明式事务

以银行转帐为例展示Spring的声明式事务

//新建一个数据库
CREATE DATABASE spring;
USE spring;
CREATE TABLE account (
    id INT (11) PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(20) NOT NULL,
    money INT DEFAULT NULL
);
INSERT INTO account VALUES (1,'zhangsan',1000);
INSERT INTO account VALUES (2,'lisi',1000);

//创建 c3p0-db.properties
jdbc.driverClass = com.mysql.jdbc.Driver
jdbc.jdbcUrl = jdbc:mysql://localhost:3306/spring
jdbc.user = root
jdbc.password = root
//实现 DAO
//创建 AccountDao 接口
//创建一个名为 com.mengma.dao 的包,在该包下创建一个接口 AccountDao
public interface AccountDao {
    // 汇款
    public void out(String outUser, int money);
    // 收款
    public void in(String inUser, int money);
}
//创建DAO层接口实现类
//创建一个名为 com.mengma.dao.impl 的包,在该包下创建实现类 AccountDaoImpl
import org.springframework.jdbc.core.JdbcTemplate;
import com.mengma.dao.AccountDao;
public class AccountDaoImpl implements AccountDao {
    private JdbcTemplate jdbcTemplate;
    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }
    // 汇款的实现方法
    public void out(String outUser, int money) {
        this.jdbcTemplate.update("update account set money =money-?   where username =?", money, outUser);
    }
    // 收款的实现方法
    public void in(String inUser, int money) {
        this.jdbcTemplate.update("update account set money =money+?     where username =?", money, inUser);
    }
}
//实现 Service
//创建 Service 层接口
//创建一个名为 com.mengma.service 的包,在该包下创建接口 AccountService
public interface AccountService {
    // 转账
    public void transfer(String outUser, String inUser, int money);
}
//创建 Service 层接口实现类
//创建一个名为 com.mengma.service.impl 的包,在该包下创建实现类AccountServiceImpl

import com.mengma.dao.AccountDao;
public class AccountServiceImpl {
    private AccountDao accountDao;
    public void setAccountDao(AccountDao accountDao) {
        this.accountDao = accountDao;
    }
    public void transfer(String outUser, String inUser, int money) {
        this.accountDao.out(outUser, money);
        this.accountDao.in(inUser, money);
    }
}
//创建 Spring 配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans-2.5.xsd  
            http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context.xsd
            http://www.springframework.org/schema/tx
            http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
            http://www.springframework.org/schema/aop
            http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">
    <!-- 加载properties文件 -->
    <context:property-placeholder location="classpath:c3p0-db.properties" />
    <!-- 配置数据源,读取properties文件信息 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="${jdbc.driverClass}" />
        <property name="jdbcUrl" value="${jdbc.jdbcUrl}" />
        <property name="user" value="${jdbc.user}" />
        <property name="password" value="${jdbc.password}" />
    </bean>
    <!-- 配置jdbc模板 -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource" />
    </bean>
    <!-- 配置dao -->
    <bean id="accountDao" class="com.mengma.dao.impl.AccountDaoImpl">
        <property name="jdbcTemplate" ref="jdbcTemplate" />
    </bean>
    <!-- 配置service -->
    <bean id="accountService" class="com.mengma.service.impl.AccountServiceImpl">
        <property name="accountDao" ref="accountDao" />
    </bean>
    <!-- 事务管理器,依赖于数据源 -->
    <bean id="txManager"
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    </bean>
    <!-- 编写通知:对事务进行增强(通知),需要编写切入点和具体执行事务的细节 -->
    <tx:advice id="txAdvice" transaction-manager="txManager">
        <tx:attributes>
            <!-- 给切入点方法添加事务详情,name表示方法名称,*表示任意方法名称,propagation用于设置传播行为,read-only表示隔离级别,是否只读 -->
            <tx:method name="find*" propagation="SUPPORTS"
                rollback-for="Exception" />
            <tx:method name="*" propagation="REQUIRED" isolation="DEFAULT"
                read-only="false" />
        </tx:attributes>
    </tx:advice>
    <!-- aop编写,让Spring自动对目标生成代理,需要使用AspectJ的表达式 -->
    <aop:config>
        <!-- 切入点 -->
        <aop:pointcut expression="execution(* com.mengma.service.*.*(..))"
            id="txPointCut" />
        <!-- 切面:将切入点与通知整合 -->
        <aop:advisor pointcut-ref="txPointCut" advice-ref="txAdvice" />
    </aop:config>
</beans>
// 创建测试类
public class AccountTest {
    @Test
    public void test() {
        // 获得Spring容器,并操作
        String xmlPath = "applicationContext.xml";
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext(
                xmlPath);
        AccountService accountService = (AccountService) applicationContext
                .getBean("accountService");
        accountService.transfer("zhangsan", "lisi", 100);
    }
}

执行结果

模拟转账失败

//将transfer方法修改为如下
public void transfer(String outUser, String inUser, int money) {
    this.accountDao.out(outUser, money);
    //模拟断电
    int i = 1/0;
    this.accountDao.in(inUser, money);
}

执行结果

由此可知,表中数据并没有发生改变,则表示事务不能正常提交,Spring事务管理生效

基于Annotation实现声明式事务

//修改applicationContext.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"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
            http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context.xsd
            http://www.springframework.org/schema/tx
            http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
            ">
    <!-- 加载properties文件 -->
    <context:property-placeholder location="classpath:c3p0-db.properties"/>
    <!--扫描含com.mengma包下的所有注解-->
    <context:component-scan base-package="com.mengma"/>
    <!-- 配置数据源,读取properties文件信息 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="${jdbc.driverClass}"/>
        <property name="jdbcUrl" value="${jdbc.jdbcUrl}"/>
        <property name="user" value="${jdbc.user}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
    <!-- 配置jdbc模板 -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- 事务管理器,依赖于数据源 -->
    <bean id="txManager"
          class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <!-- 注册事务管理驱动 -->
    <tx:annotation-driven transaction-manager="txManager"/>
</beans>
//加注解
import com.mengma.dao.AccountDao;
import com.mengma.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT, readOnly = false)
@Service("accountService")
public class AccountServiceImpl implements AccountService {
    @Autowired
    private AccountDao accountDao;
    public void setAccountDao(AccountDao accountDao) {
        this.accountDao = accountDao;
    }
    public void transfer(String outUser, String inUser, int money) {
        this.accountDao.out(outUser, money);
        this.accountDao.in(inUser, money);
    }
}

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import com.mengma.dao.AccountDao;
import org.springframework.stereotype.Repository;
import javax.annotation.Resource;
@Repository("accountDao")
public class AccountDaoImpl implements AccountDao {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }
    // 汇款的实现方法
    public void out(String outUser, int money) {
        this.jdbcTemplate.update("update account set money =money-? where username =?", money, outUser);
    }
    // 收款的实现方法
    public void in(String inUser, int money) {
        this.jdbcTemplate.update("update account set money =money+? where username =?", money, inUser);
    }
}

执行结果和使用XML实现结果相同