Spring


2019/2/1 Java Spring

# Spring

# Spring概述

Spring 是分层的 Java SE/EE 应用 全栈式(full-stack)轻量级开源框架,以 IoC(Inverse Of Control: 控制反转)和 AOP(Aspect Oriented Programming:面向切面编程)为内核,提供了表现层 Spring MVC 和持久层 Spring JDBC 以及业务层事务管理等众多的企业级应用技术,还能整合开源世界众多 著名的第三方框架和类库,逐渐成为使用最多的 Java EE 企业应用开源框架。

# Spring的发展历程

1997 年 IBM 提出了 EJB 的思想
Spring 诞生于2002,成型与2003,作者是Rod Johnson,他写了两本书

  • 《Expert One-to-One J2EE Desion and Development》
  • 《Expert One-to-One J2EE Development without EJB》(2004)

阐述了 J2EE 开发不使用 EJB 的解决方式(Spring 雏形)
2017年9月份发布了Spring的最新版本Spring 5.0通用版(GA) JDK版本要求1.8及以上

# Spring的优势

# 方便解耦,简化开发

通过 Spring 提供的 IoC 容器,可以将对象间的依赖关系交由 Spring 进行控制,避免硬编码所造成的过度程序耦合。用户也不必再为单例模式类、属性文件解析等这些很底层的需求编写代码,可以更专注于上层的应用。

# AOP 编程的支持

通过 Spring 的 AOP 功能,方便进行面向切面的编程,许多不容易用传统OOP实现的功能可以通过 AOP 轻松应付。

# 声明式事务的支持

可以将我们从单调烦闷的事务管理代码中解脱出来,通过声明式方式灵活的进行事务的管理,提高开发效率和质量。

# 方便程序的测试

可以用非容器依赖的编程方式进行几乎所有的测试工作,测试不再是昂贵的操作,而是随手可做的事情。

# 方便集成各种优秀框架

Spring 可以降低各种框架的使用难度,提供了对各种优秀框架(Struts、Hibernate、Hessian、Quartz等)的直接支持。

# 降低 JavaEE API 的使用难度

Spring 对 JavaEE API(如 JDBC、JavaMail、远程调用等)进行了薄薄的封装层,使这些API的使用难度大为降低。

# Spring 源码是经典学习范例

Spring 的源代码设计精妙、结构清晰、匠心独用,处处体现着大师对 Java 设计模式灵活运用以及对 Java 技术的高深造诣。它的源代码无意是 Java 技术的最佳实践的范例。

# Spring的体系结构

# IoC的概念及作用

# 什么是程序的耦合

耦合性(Coupling),也叫耦合度,是对模块间关联程度的度量。

简单来说,耦合就是:程序间的依赖关系。它包括:

  • 类之间的依赖
  • 方法间的依赖

程序间的耦合不可能完全消除,只能降低程序间的依赖关系,这叫做解耦

# 解决程序耦合的思路

开发中要做到:编译期不依赖,运行时才依赖

比如:

  • 数据库驱动加载,我们一般都这么写:

    Class.forName("com.mysql.jdbc.Driver");
    //参数是字符串,利用反射加载的数据库驱动
    
    1
    2

    其实注册驱动可以这么写:

    DriverManager.registerDriver(new com.mysql.jdbc.Driver());
    
    1

    为什么我们平常都按照上面的写呢?

    因为下面这句带了new关键字,如果数据库驱动文件不在了,程序在编译的时候就通过不了;而上面那句是按照字符串来的,就算数据库驱动文件不在,也可以通过编译,这就降低了程序间的耦合度。

# 工厂模式解耦

在我们普通的Servlet、Service、Dao三层架构中,表现层Servlet必须要new一个业务层Service的实现类,而业务层Service也必须要new一个持久层Dao的实现类;这就出现了耦合!解决办法是用工厂模式解耦!

# 控制反转(IOC)

控制反转是一种设计思想。

就是将你设计好的对象交给Spring控制new出来,而不是由你自己直接new,从而达到降低耦合的目的。所以叫做控制反转。

# IOC的使用

# 准备spring的开发包

Spring官网:https://spring.io/

jar包下载地址:https://repo.spring.io/release/org/springframework/spring/

# Spring项目入门

  1. 首先创建简单的Servlet、Service、Dao三层架构的接口以及其实现类。

  2. 创建Spring的配置文件(叫什么都行,一般叫applicationContext.xml),记得导入约束。(idea可以直接new一个自带约束的Spring Config.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">
    
        <!-- id应该是随便写的,当然要能认识,class为bean的全类名 -->
        <bean id="userService" class="xyz.sky03.service.impl.UserServiceImpl"></bean>
        <bean id="UserDao" class="xyz.sky03.dao.impl.UserDaoImpl"></bean>
    </beans>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
  3. 获取Spring new的bean对象

    package xyz.sky03.servlet;
    
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    import xyz.sky03.dao.IUserDao;
    import xyz.sky03.service.IUserService;
    
    public class UserServlet {
        public static void main(String[] args){
    
            //1.获取核心容器对象
            ApplicationContext ac= new ClassPathXmlApplicationContext("bean.xml");
    
            //2.根据id获取Bean对象  以下两种方式都可以
            //这里没有返回指定类型,所以需要强转类型
            IUserService service = (IUserService) ac.getBean("userService"); 
            //这里第一个参数填配置文件中<bean>的id,因为参数里指定了class,所以可以直接得到对应的类型
            IUserDao dao = ac.getBean("UserDao", IUserDao.class);
    
            System.out.println(ac);
            System.out.println(service);
            System.out.println(dao);
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24

其中:

  • ApplicationContext的三个常用实现类

    • ClassPathXmlApplicationContext:它可以加载类路径下的配置文件,不在加载不了(常用)
    • FileSystemXmlApplicationContext:它可以加载磁盘任意路径下的配置文件(必须有访问权限)
    • AnnotationConfigApplicationContext:它是用于读取注解创建容器的
  • Spring的两种工厂类

    • BeanFactory:Spring 容器中的顶层接口 延迟加载,调用getBean的时候,才会生成类的实例;单例对象适用。
    • ApplicationContext:BeanFactory的子接口。(一般用这个) 立即加载,加载配置文件的时候,就会将Spring管理的类全部实例化;多例对象适用;它还会根据创建的对象是多例还是单例,自动选择立即加载还是延迟加载。
  • 关于配置文件中 <bean>标签用id和name的区别

    • id :使用了 唯一约束,里面 不能有特殊字符
    • name :没有唯一约束(理论上可以出现重复,但实际开发中不能出现)
      • 命名上 可以出现特殊字符
    • 另外Spring和Struts1整合的时候:
      • Struts1的id或name前面必须加个"/",但是Spring的id前面是不能加"/"的
      • 所以要用name! like this:<bean name="/a1" class="...">

# IOC中bean标签和管理对象细节

# 创建bean的三种方式

  1. 使用默认构造函数创建。

    在spring的配置文件中使用bean标签,配以id和class属性之后,且没有其他属性和标签时。采用的就是默认构造函数创建bean对象,此时如果类中没有默认构造函数,则对象无法创建。

    <bean id="userService" class="xyz.sky03.service.impl.UserServiceImpl"></bean>
    
    1
  2. 使用普通工厂中的方法创建对象(使用某个类中的方法创建对象,并存入spring容器)

    <!-- 此bean有创建UserService的方法 -->
    <bean id="instanceFactory" class="xyz.sky03.factory.InstanceFactory"></bean>
    <!-- 此bean用来调用上一个bean的创建UserService的方法-->
    <!-- factory-bean指定从哪个bean来创建UserService,factory-method:从bean的哪个方法得到UserService对象-->
    <bean id="userService" factory-bean="instanceFactory" factory-method="getUserService"></bean>
    
    1
    2
    3
    4
    5
  3. 使用工厂中的静态方法创建对象(使用某个类中的静态方法创建对象,并存入spring容器)

    <bean id="userService" class="xyz.sky03.factory.StaticFactory" factory-method="getUserService"></bean>
    
    1

# bean对象的作用范围调整

  • 方法:使用bean标签的scope属性
  • 作用:用于指定bean的作用范围
  • 取值:常用的就是单例和多例
    • singleton:单例的(默认值)
    • prototype:多例的
    • request:作用于web应用的请求范围
    • session:作用于web应用的会话范围
    • global-session:作用于集群环境的会话(全局会划范围),当不是集群环境时,它就相当于session

# bean对象的生命周期

  • 单例对象
    • 出生:当容器创建时对象出生
    • 活着:只要容器还在,对象就一直活着
    • 死亡:容器销毁,对象死亡
    • 总结:单例对象的生命周期和容器相同
  • 多例对象
    • 出生:当我们使用对象时,Spring为我们创建
    • 活着:对象只要是在使用过程中,就一直活着
    • 死亡:当对象长时间不用,且没有别的对象引用时,由Java的垃圾回收器回收
  • 手动配置bean的生命周期
    • 在bean标签里有两个属性
      • init-method:用于指定Bean被初始化的时候执行的方法
      • destroy-method:用于指定Bean被销毁的时候执行的方法
        • Bean一般是单例创建,工厂关闭,所以会跳过destroy-method指定的方法

# 依赖注入DI

# 概念

依赖注入(Dependency Injection)就是bean实体类构造函数为有参构造函数,这时候为了能创建bean对象,就需要对构造函数注入参数!

# 依赖注入的方式

# 使用构造函数注入

  • 使用标签:constructor-arg

  • 标签使用的位置:bean标签的内部

  • 标签的属性:

    • type:根据要注入的数据的数据类型,来注入构造函数中对应的某个或某些参数;

      • 也就是说如果构造函数中有多个相同的数据类型,此属性就不适用了
    • index:索引,指定要注入的属性给构造函数中对应索引的位置 (从0开始)

    • name:给构造函数中的指定名称的参数赋值 (常用)

      ==========以上三个用于指定给构造函数中哪个参数赋值=========

    • value:用于提供基本类型和String类型的数据

      • 如:日期类型,就不能用value来注入
    • ref:用于指定其他的bean类型数据。它指的就是在Spring的IOC核心容器中出现过的bean对象

    示例:

    <bean id="userService" class="xyz.sky03.service.impl.UserServiceImpl">
        <constructor-arg name="name" value="泰斯特"></constructor-arg>
        <constructor-arg name="age" value="18"></constructor-arg>
        <constructor-arg name="birthday" ref="now"></constructor-arg>
    </bean>
    
    <!-- 配置一个日期对象 -->
    <bean id="now" class="java.util.Date"></bean>
    
    1
    2
    3
    4
    5
    6
    7
    8
  • 优势:

  • 保证一些必要的属性在Bean实例化时就得到设置,并且确保了Bean实例在实例化后就可以使用。

  • 弊端:

  • 改变了bean对象的实例化方式,使我们在创建对象时,即使用不到这些数据,也必须提供。

# 使用属性的Setter方法注入 (常用)

  • 标签:property
  • 位置:bean标签的子标签
  • 要求:
    • 要创建的bean要有其各个属性的Setter方法(不需要Getter方法)
      • Spring只会检查Bean中是否有对应的Setter方法,至于Bean中是否有对应的属性变量则不做要求。
    • 提供一个无参的构造方法(必须!)
      • 假设bean类中显示定义了一个带参的构造函数,如public Bean(String test),则需要同时提供一个默认构造函数public Bean(),否则使用属性注入时将抛出异常。
  • 标签的属性
    • name:用于指定注入时所调用的Setter方法名称,或其他bean的id
    • value:用于提供基本类型和String类型的数据
    • ref:用于指定其他的bean类型数据。它指的就是在Spring的IOC核心容器中出现过的bean对象
  • 原理:
    • Spring先调用Bean的默认构造函数实例化Bean对象,然后通过反射的方式调用Setter方法注入属性值。

# 复杂类型的注入(集合类型)

  • 集合类型的参数 无论是value 还是ref 都不能注入!
  • 正确的姿势是:
  • 集合分两个结构 ListMap
  • List 结构的集合在Setter属性注入的方式基础上,在property标签的内部使用以下标签
    • listarrayset
  • Map 结构的集合,同理,在property标签的内部使用以下标签
    • mapprops

# 使用注解注入

# 常用注解

# 创建bean对象

  1. 先在主配置文件上加入 context 的约束,再配置一个<context:component-scan>的标签,还有base-package的属性,用于告知Spring创建容器时要扫描的包(去找带@Component的注解)

    <?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"
            xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context.xsd">
    
        <context:component-scan base-package="xyz.sky03"></context:component-scan>
    </beans>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
  2. 在bean的实体类上面加 @Component 注解,bean的id默认为bean的首字母小写的类名,也可以加参数指定id

    @Component(value="userService")@Component("userService") //只有value属性时,可以省略不写
    
    1
    2
    3
  3. 其他注解:

    以下三个注解的作用和 @Component 一样,它们是Spring为我们提供的三层架构使用的注解,使我们的三层对象更加清晰

    • @Controller :一般用于表现层
    • @Service :一般用于业务层
    • @Repository :一般用于持久层

# 注入数据

  • @Autowired

    • 作用:自动按照类型注入。只要Spring容器中有唯一的一个bean对象类型和要注入的变量类型匹配(包括其实现类),就可以注入成功!
    • 但是当IOC容器有多个相同类型的bean时,Spring会自动根据要创建的对象名和IOC容器中的id匹配。
    • 如果IOC容器中没有任何bean的类型和要注入的变量类型匹配,则会报错。
    • 注解的位置:很多地方都能写。一般都是类的上面,或方法的上面。
    • 不需要set方法。
  • @Qualifier

    • 作用:在@Autowired的基础上再按照名称注入。

    • 属性:value,用来指定注入的名称。

    • 位置:在给类成员注入时无法单独使用,必须在前面加上@Autowired ,否则报错!

    • 在给方法参数注入时可以单独使用

      @Autowired
      @Qualifier("userDao")
      private IUserDao userDao;
      
      1
      2
      3
  • @Resource

    • 作用:直接按照bean的id注入。它可以独立使用
    • 属性:name,用来指定bean的id
  • 以上三个注入都只能注入bean类型的数据,而基本类型和String类型无法使用上述注解实现。另外,集合类型的注入只能通过XML来实现。

  • @Value

    • 作用:注入基本类型和String类型的数据
    • 属性:value,用于指定数据的值。它可以使用Spring中SpEL(也就是spring的el表达式)
    • SpEL的写法:${表达式}

# 改变作用范围

  • @Scope
    • 作用:用于指定bean的作用范围
    • 属性:value,指定范围的取值。常用取值:singleton prototype (单例和多例)
    • 位置:bean类的上面

# 生命周期相关(了解)

  • @PreDestroy
    • 作用:用于指定销毁方法
    • 位置 :在销毁方法的上面
    • 另外,获取容器对象的时候,如果使用的是ApplicationContext,会跳过销毁方法的执行,必须使用ClassPathXmlApplicationContext
    • 还有,Spring是不负责多例的销毁!
  • @PostConstruct
    • 作用:用于指定初始化方法
    • 位置:在初始化方法的上面

# 新注解

由于以上注解只能用于注解我们自己写的类,脱离不了配置文件,所以采用新注解

新建一个名为config.SpringConfiguration的类(叫什么都行),在其中配置一下注解:

  • @Configuration
    • 作用:指定当前类为一个配置类

    • 细节:当配置类作为AnnotationConfigApplicationContext对象创建的参数时,该注解可以不写。可以用class文件直接指定配置类。

      ApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfiguration.class);
      
      1
    • 位置:目标类的上方

  • @ComponentScan
    • 作用:用于通过注解指定Spring在创建容器时要扫描的包
    • 属性:value和basePackages,二者作用一样,都是用于指定创建容器时要扫描的包。
    • 相当于:XML中的 <context:component-scan base-package="xyz.sky03"></context:component-scan>
    • 位置:配置类的上方
  • @Bean
    • 作用:用于把当前方法的返回值作为bean对象存入spring的ioc容器中
    • 属性:name,用于指定bean的id。当不写时,默认值是当前方法的名称
    • 细节:当我们使用注解配置方法时,如果方法有参数,spring框架会去容器中查找有没有可用的bean对象。查找的方式和Autowired注解的作用是一样的
  • @Import
    • 作用:用于导入其他的配置类
    • 属性:value,用于指定其他配置类的字节码。
    • 细节:带@Import注解的类就是父配置类(主配置类),而被导入的类都是子配置类(副配置类)
  • @PropertySource
    • 作用:用于指定properties文件的位置

    • 属性:value,指定文件的名称和路径。

      • 其中关键字:classpath:表示类路径下
      @PropertySource("classpath:jdbcConfig.properties")@PropertySource("classpath:xyz/sky03/jdbcConfig.properties")
      
      1
      2
      3

# Spring整合Junit测试

  1. 首先要明白:

    1. 应用程序的入口:main方法
    2. junit单元测试中,没有main方法也能执行
      • junit集成了一个main方法
      • 该方法就会判断当前测试类中哪些方法有 @Test注解
      • junit就让有Test注解的方法执行
    3. junit不会管我们是否采用spring框架
      • 在执行测试方法时,junit根本不知道我们是不是使用了spring框架
      • 所以也就不会为我们读取配置文件/配置类创建spring核心容器
    4. 由以上三点可知
      • 当测试方法执行时,没有Ioc容器,就算写了@Autowired注解,也无法实现注入
  2. 导入Spring整合Junit的jar包或maven坐标 spring-test

  3. Junit提供了一个注解 @Runwith,可以把原有的main方法替换成spring提供的main方法

    • @RunWith(SpringJUnit4ClassRunner.class)
    • 参数为字节码文件
  4. @ContextConfiguration ,用于告知spring的运行器,spring和ioc创建是基于xml还是注解的,并且说明位置。

    • 属性:
      • locations:指定xml文件的位置,加上classpath关键字,表示在类路径下

        @ContextConfiguration(locations = "classpath:bean.xml")
        
        1
      • classes:指定注解类所在地位置

        @ContextConfiguration(classes = SpringConfiguration.class)
        
        1
  5. 注意:使用Spring 5.x版本的时候,要求Junit的版本必须是4.12及以上,否则执行以上内容会报错

# 动态代理

  • 特点:字节码随用随创建,随用随加载
  • 作用:不修改源码的基础上对方法增强
  • 分类:
    • 基于接口的动态代理
    • 基于子类的动态代理

# 基于接口的动态代理

  • 提供者:JDK官方的Proxy类

  • 要求:被代理类最少实现一个接口,如果没有则不能使用

  • 如何创建:使用Proxy类中的newProxyInstance方法

  • newProxyInstance方法参数:

    • ClassLoader:类加载器
      • 它是用于加载代理对象字节码的。和被代理对象使用相同的类加载器。
      • 固定写法:代理谁就写谁的 getClassLoader()
    • Class[]:字节码数组
      • 它是用于让代理对象和被代理对象有相同方法。实现相同的接口
      • 固定写法:代理谁就写谁的 getInterfaces()
    • InvocationHandler:用于提供增强的代码
      • 用来写如何代理的代码。我们一般都是些一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。
      • 此接口的实现类都是谁用谁写。
  • 示例:

    IProducer proxyProducer = (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(),
            producer.getClass().getInterfaces(),
            new InvocationHandler() {
                /**
                 * 作用:执行被代理对象的任何接口方法都会经过该方法
                 * 方法参数的含义
                 * @param proxy   代理对象的引用
                 * @param method  当前执行的方法
                 * @param args    当前执行方法所需的参数
                 * @return        和被代理对象方法有相同的返回值
                 * @throws Throwable
                 */
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    //提供增强的代码
                    Object returnValue = null;
    
                    //1.获取方法执行的参数
                    Float money = (Float)args[0];
                    //2.判断当前方法是不是销售
                    if("saleProduct".equals(method.getName())) {
                        returnValue = method.invoke(producer, money*0.8f);
                    }
                    return returnValue;
                }
            });
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26

# 基于子类的动态代理

基于子类的动态代理是需要jar包的支持 cglib.jarasm.jar

  • 提供者:第三方 cglib库的 Enhancer类

  • 如何创建:使用Enhancer类中的create方法

  • 要求:被代理类不能是 最终类(不能创建子类)

  • create方法参数:

    • Class:字节码
      • 用于指定被代理对象的字节码
    • Callback:用于提供增强代码
      • 我们一般写的都是该接口的子接口实现类:MethodInterceptor
  • 示例:

    Producer cglibProducer = (Producer)Enhancer.create(producer.getClass(), new MethodInterceptor() {
        /**
         * 执行北地阿里对象的任何方法都会经过该方法
         * @param proxy
         * @param method
         * @param args
         *    以上三个参数和基于接口的动态代理中invoke方法的参数是一样的
         * @param methodProxy :当前执行方法的代理对象
         * @return
         * @throws Throwable
         */
        @Override
        public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
            //提供增强的代码
            Object returnValue = null;
    
            //1.获取方法执行的参数
            Float money = (Float)args[0];
            //2.判断当前方法是不是销售
            if("saleProduct".equals(method.getName())) {
                returnValue = method.invoke(producer, money*0.8f);
            }
            return returnValue;
        }
    });
    cglibProducer.saleProduct(12000f);
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26

# Spring的AOP

# AOP概念

AOP:全称是 Aspect Oriented Programming 即:面向切面编程

简单的说它就是把我们程序重复的代码抽取出来,在需要执行的时候,使用动态代理的技术,在不修改源码的基础上,对我们的已有方法进行增强。

# AOP作用

  1. AOP主要作用就是横切关注点的分离和织入,横切关注点可能包含很多,比如非业务的:日志、事务处理、缓存、性能统计、权限控制等等这些非业务的基础功能;也可以关注业务
  2. 完善oop
  3. 降低组件和模块之间的耦合性
  4. 使系统容易扩展
  5. 而且由于关注点分离从而可以获得组件的更好复用

# AOP发展历程

AOP思想最早由AOP联盟组织提出。Spring是使用这种思想的最好架构。

  • Spring的AOP有自己的实现方式(非常繁琐),后来有个很AOP框架叫AspectJ,Spring就将其引入到自己的框架中了,弃用了自己的实现方式。

# AOP中的细节

# AOP的底层实现方式

使用动态代理技术

# 关于代理方式的选择

在 spring 中,框架会根据目标类是否实现了接口来决定采用哪种动态代理的方式。(基于接口,还是基于子类)

# AOP相关术语

  • 连接点(Joinpoint):
    • 所谓连接点就是可以被增强的方法。
  • 切入点(Pointcut):
    • 被增强的方法就叫做切入点。
  • 总结
    • 所有的切入点都是连接点,所有的连接点被增强后就是切入点!
  • 通知/增强(Advice):
    • 所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知。
    • 通知的类型:前置通知,后置通知,异常通知,最终通知,环绕通知。
      • 前置通知:位于invoke方法之前
      • 后置通知:位于invoke方法之后
      • 异常通知:在catch里面
      • 最终通知:在finally里面
      • 环绕通知:整个的invoke方法就是环绕通知。
  • 引介(Introduction):
    • 引介是一种特殊的通知在不修改类代码的前提下, Introduction 可以在运行期为类动态地添加一些方法或 Field。(类层面上的增强)
  • 目标对象(Target):
    • 被代理的对象。
  • 织入(Weaving):
    • 是指把增强应用到目标对象来创建新的代理对象的过程。
    • spring 采用动态代理织入,而 AspectJ 采用编译期织入和类装载期织入。
  • 代理(Proxy):
    • 一个类被 AOP 织入增强后,就产生一个结果代理类。
  • 切面(Aspect):
    • 是多个切入点和多个通知(引介)的结合。

# AOP入门

# 基于XML的AOP配置

# 四个通知的配置及切入点表达式配置
  • <aop:config> :表示AOP的配置,所有AOP相关配置都在其内部

  • <aop:aspect> :表示配置一个切面

    • 位置:在 <aop:config> 的内部
    • 属性: id :是给切面提供的一个唯一标识
    • 属性: ref :是指定切面类或通知类bean的id
  • <aop:before> :表示配置一个前置通知

    • 位置:在 <aop:aspect> 的内部

    • 属性: method :用于指定通知中的哪个方法为前置通知!

    • 属性: pointcut :用于指定 切入点表达式,该表达式的含义指的是对业务层中哪些方法增强

    • 切入点表达式 的写法:

      • 关键字:execution(表达式)

      • 表达式:

        • 访问修饰符 返回值 包名.包名.包名...类名.方法名(参数列表)
      • 标准的表达式写法:

        public void xyz.sky03.service.impl.UserServiceImpl.saveUser()
        
        1
        • 其中:
          • 访问修饰符可以省略

            void xyz.sky03.service.impl.UserServiceImpl.saveUser()
            
            1
          • 返回值可以使用通配符,表示任意返回值

            * xyz.sky03.service.impl.UserServiceImpl.saveUser()
            
            1
          • 包名可以使用通配符,表示任意包。但是有几级包,就需要写几个 *.

            * *.*.*.*.UserServiceImpl.saveUser()
            
            1
          • 包名可以使用 .. 表示当前包及其子包

            * *..UserServiceImpl.saveUser()
            
            1
          • 类名和方法名都可以使用 * 来实现通配

            * *..*.*()
            
            1
    • 表达式的参数列表

      • 可以直接写的数据类型:
        • 基本类型直接写名称 int
        • 引用类型写包名.类名的方式 java.lang.String
        • 可以使用通配符表示任意类型,但是必须有参数
        • 可以使用..表示有无参数均可,有参数可以是任意类型
    • 全通配写法:

      * *..*.*(..)
      
      1
    • 实际业务开发中切入点表达式的通常写法:

      • 切到业务层实现类下的所有方法
      * xyz.sky03.service.impl.*.*(..)
      
      1
  • <aop:after-returning> :后置通知,它和异常通知永远只能执行一个。

    • 它也有 methodpointcut 属性,用法和 <aop:before> 的属性一样
  • <aop:after-throwing> :异常通知,它和后置通知只能执行一个。(要么try,要么catch)

  • 它也有 methodpointcut 属性,用法和 <aop:before> 的属性一样

  • <aop:after> :最终通知

  • 它也有 methodpointcut 属性,用法和 <aop:before> 的属性一样

# 切入点表达式的通用写法
  • <aop:pointcut> 用于配置切入点表达式
    • 属性 id :用于指定表达式的唯一标识。
    • 属性 expression :用于指定表达式内容
    • 注意:当此标签在 <aop:aspect> 内部时,只能在当前切面使用(当前<aop:aspect>标签内); 如果此标签配置在 <aop:aspect> 外部时(必须是在<aop:aspect> 前面),所有切面都可以使用
# 以上四个通知还有一个属性叫 pointcut-ref
  • 用于指定切入点表达式的id
# 环绕通知
  • <aop:around>
    • 属性 method :指定方法
    • 属性 pointcut-ref :指定切入点表达式的id
  • Spring框架提供一个接口:ProceedingJoinPoint。该接口有一个方法proceed(),此方法就相当于调用切入点方法。而如果不配置该方法,就会只执行通知方法,不执行切入点方法。
  • 它是spring框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式。
public Object aroundPringLog(ProceedingJoinPoint pjp){
    Object rtValue = null;
    try{
        Object[] args = pjp.getArgs();//得到方法执行所需的参数

        System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。前置");

        rtValue = pjp.proceed(args);//明确调用业务层方法(切入点方法)

        System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。后置");

        return rtValue;
    }catch (Throwable t){
        System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。异常");
        throw new RuntimeException(t);
    }finally {
        System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。最终");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 基于注解的AOP配置

  1. 首先还是在配置文件中加入注解的约束context

    <!-- 配置spring创建容器时要扫描的包-->
    <context:component-scan base-package="xyz.sky03"></context:component-scan>
    
    1
    2
  2. 然后在配置文件中开启注解AOP的支持

    <!-- 配置spring开启注解AOP的支持 -->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
    
    1
    2
  3. 配置注解

    • @Aspect :表示当前类是一个切面类

      • 位置:在类名的上方
    • @Pointcut :来指定切入点表达式

      • 位置:在一个方法上
      • 其id为:该方法的方法名(),一定要带括号,否则报错!
      @Pointcut("execution(* xyz.sky03.service.impl.*.*(..))")
      private void pt1(){}
      
      1
      2
    • @Before :前置通知

      • 位置:前置通知方法上方
      • 参数:为切入点id,一般为方法名,记得带上方法后的括号
      • Junit也有@Before,具体适用的哪个,得看导包
    • @AfterReturning :后置通知

      • 位置:后置通知方法的上方
      • 参数:为切入点id,记得带上括号
    • @AfterThrowing :异常通知

      • 位置:异常通知方法的上方
      • 参数:为切入点id,记得带上括号
    • @After :最终通知

      • 位置:最终通知方法的上方
      • 参数:为切入点id,记得带上括号
    • @Around :环绕通知

      • 位置:环绕通知方法上方
      • 参数:为切入点id,记得带上括号
    • @EnableAspectJAutoProxy :注解方式开启AOP支持

      • 相当于

        <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
        
        1

# JdbcDaoSupport类

此类是解决SpringTemplate在多个Dao中创建多个SpringTemplate对象的类

具体方法:

  • Dao实现类继承JdbcDaoSupport
  • 向Dao实现类中注入 DataSource对象
    • Dao实现类的父类,也就是JdbcDaoSupportDataSource对象参数
  • 不用new SpringTemplate对象,直接使用父类的方法super.getJdbcTemplate(),得到SpringTemplate对象
  • 执行增删改查业务
  • 使用注解的时候不能使用此类

# AOP的声明式事务

# 基于XML

持久层使用的是JdbcTemplate

  1. 导入AOP和事务的约束

    <?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:tx="http://www.springframework.org/schema/tx"
          xsi:schemaLocation="
            http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/tx
            http://www.springframework.org/schema/tx/spring-tx.xsd
            http://www.springframework.org/schema/aop
            http://www.springframework.org/schema/aop/spring-aop.xsd">
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
  2. 配置service和dao以及DataSource的bean

    <!-- 配置Service -->
    <bean id="accountService" class="xyz.sky03.service.impl.AccountServiceImpl">
        <!-- 注入dao -->
        <property name="accountDao" ref="accountDao"></property>
    </bean>
    
    <!--配置Dao对象-->
    <bean id="accountDao" class="xyz.sky03.dao.impl.AccountDaoImpl">
        <!--注入数据源-->
        <property name="dataSource" ref="datasource"></property>
    </bean>
    
    <!--配置数据源的bean-->
    <bean id="datasource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="url" value="jdbc:mysql:///eesy"></property>
        <property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
        <property name="username" value="root"></property>
        <property name="password" value="admin"></property>
    
    </bean>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
  3. 配置声明式事务

    • 配置事务管理器

      • 注意:事务管理器需要注入 DataSource 对象
      <!-- 配置事务管理器 -->
      <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
          <property name="dataSource" ref="dataSource"></property>
      </bean>
      
      1
      2
      3
      4
    • 配置事物的通知

      • 使用<tx:advice> 标签来配置事务通知
      • 属性 id:事务通知的唯一标识
      • 属性 transaction-manager:给事务通知提供一个事务管理器引用
      <!-- 配置事务的通知-->
      <tx:advice id="txAdvice" transaction-manager="transactionManager"></tx:advice>
      
      1
      2
    • 配置AOP中的通用切入点表达式

    • 建立事务通知和切入点表达式的对应关系

      • 属性 advice-ref:指定哪个事务通知
      • 属性 pointcut-ref:指定切入点表达式
      <!-- 配置aop-->
      <aop:config>
          <!-- 配置切入点表达式-->
          <aop:pointcut id="pt1" expression="execution(* xyz.sky03.service.impl.*.*(..))"></aop:pointcut>
          <!--建立切入点表达式和事务通知的对应关系 -->
          <aop:advisor advice-ref="txAdvice" pointcut-ref="pt1"></aop:advisor>
      </aop:config>
      
      1
      2
      3
      4
      5
      6
      7
    • 配置事务的属性

      • 标签:<tx:attributes><tx:method> (单标签,貌似单双无所谓)
      • 位置:<tx:attributes><tx:advice> 标签的内部、<tx:method><tx:attributes>的内部
      • <tx:attributes>的内部可以配置多个事务
      • <tx:method>标签属性:
        • name:指定对哪个方法配置事务,一般会有很多方法都需要事务,这时候可以用通配符 * 或者 find*
        • isolation:用于指定事务的隔离级别。默认值是DEFAULT,表示使用数据库的默认隔离级别。
        • propagation:用于指定事务的传播行为。默认值是REQUIRED,表示一定会有事务,增删改的选择。查询方法可以选择SUPPORTS。
        • read-only:用于指定事务是否只读。只有查询方法才能设置为true。默认值是false,表示读写。
        • timeout:用于指定事务的超时时间,默认值是-1,表示永不超时。如果指定了数值,以秒为单位。
        • rollback-for:用于指定一个异常,当产生该异常时,事务回滚,产生其他异常时,事务不回滚。没有默认值。不配置此属性,表示任何异常都回滚。
        • no-rollback-for:用于指定一个异常,当产生该异常时,事务不回滚,产生其他异常时事务回滚。没有默认值。不配置此属性,表示任何异常都回滚。
      <!-- 配置事务的通知-->
      <tx:advice id="txAdvice" transaction-manager="transactionManager">
          <tx:attributes>
              <!-- 二者优先级 find* 范围小,优先级高 find开头的一般是查询方法,所以配置read-only为true-->
      
              <!-- * 表示都能匹配上-->
              <tx:method name="*" propagation="REQUIRED" read-only="false"/>
              <!-- find* 表示只能find开头的方法能匹配上-->
              <tx:method name="find*" propagation="SUPPORTS" read-only="true"></tx:method>
          </tx:attributes>
      </tx:advice>
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
  4. 项目示例:AOP的事务配置基于XML

# 基于注解

  1. 用注解首先要配置要扫描的包

    <!-- 配置spring创建容器时要扫描的包-->
    <context:component-scan base-package="xyz.sky03"></context:component-scan>
    
    1
    2
  2. 再用注解配置Service和Dao、还有JdbcTemplate和DataSource

    因为 JdbcDaoSupport使用注解会很不方便,所以自己配置JdbcTemplate的bean,再注入DataSource

    在Dao实现类上用 @Autowired 对JdbcTemplate注入

    <!-- 配置JdbcTemplate-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"></property>
    </bean>
    
    <!--配置数据源的bean-->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="url" value="jdbc:mysql:///eesy"></property>
        <property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
        <property name="username" value="root"></property>
        <property name="password" value="admin"></property>
    
    </bean>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
  3. 配置事务管理器

    <!-- 配置事务管理器 -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"></property>
    </bean>
    
    1
    2
    3
    4
  4. 开启Spring对注解事务的支持

    <!-- 开启spring对注解事务的支持-->
    <tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
    
    1
    2
  5. 在需要事务支持的地方使用@Transactional注解

    • 该注解使用位置:类名上方
    • 一般用在Service实现类
    • 属性:跟<tx:method> 标签的属性一样
    • 细节:在类的上方配置 @Transactional(propagation= Propagation.SUPPORTS,readOnly=true) ,表示该类中的所有方法都是只读型事务,那增删改的方法就需要单独配置读写型事务
      • 在方法的上方加上 @Transactional(propagation= Propagation.REQUIRED,readOnly=false) 该优先级大于类上方的配置
  6. 所以注解的方法的缺点很明显,需要在每个业务类或者增删改的方法上加注解,XML文件方式则可以一劳永逸。

  7. 因为因为已经在事务类和方法上加了事务注解,所以不需要切入点了

  8. 项目示例:AOP的事务配置基于注解

# 纯注解

  1. 创建配置类。在上一个基于注解的基础上,在config包中创建一个配置类SpringConfiguration,在该类上加 @Configuration注解,表示该类为配置类;当配置类作为AnnotationConfigApplicationContext对象创建的参数时,该注解可以不写。

  2. 配置要扫描的包。在SpringConfiguration 类的上方加@ComponentScan("xyz.sky03")

  3. 创建数据库配置类。在config包中创建JdbcConfig类,用于配置JdbcTemplate和DataSource。

    示例:

    package config;
    
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.Bean;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.jdbc.datasource.DriverManagerDataSource;
    
    import javax.sql.DataSource;
    
    /**
    * 和连接数据库相关的配置类
    */
    public class JdbcConfig {
    
        @Value("${jdbc.driver}")
        private String driver;
    
        @Value("${jdbc.url}")
        private String url;
    
        @Value("${jdbc.username}")
        private String username;
    
        @Value("${jdbc.password}")
        private String password;
    
        /**
        * 创建JdbcTemplate
        * @param dataSource
        * @return
        */
        @Bean(name="jdbcTemplate")
        public JdbcTemplate createJdbcTemplate(DataSource dataSource){
            return new JdbcTemplate(dataSource);
        }
    
        /**
        * 创建数据源对象
        * @return
        */
        @Bean(name="dataSource")
        public DataSource createDataSource(){
            DriverManagerDataSource ds = new DriverManagerDataSource();
            ds.setDriverClassName(driver);
            ds.setUrl(url);
            ds.setUsername(username);
            ds.setPassword(password);
            return ds;
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
  4. 在配置类上导入数据库配置类。加上@Import({JdbcConfig.class}) 注解,导入数据库配置类。

  5. 配置数据库连接信息文件。在resource中创建jdbcConfig.properties 文件,用于配置数据库连接信息。

    示例:

    jdbc.driver=com.mysql.jdbc.Driver
    jdbc.url=jdbc:mysql:///eesy
    jdbc.username=root
    jdbc.password=admin
    
    1
    2
    3
    4
  6. 加载数据库连接文件。在配置类上加@PropertySource("jdbcConfig.properties") 注解,加载数据库连接的信息。

  7. 开启Spring对注解事务的支持。在配置类上加 @EnableTransactionManagement

  8. 配置事务管理器。在config包中创建 TransactionConfig

    示例:

    package config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.jdbc.datasource.DataSourceTransactionManager;
    import org.springframework.transaction.PlatformTransactionManager;
    
    import javax.sql.DataSource;
    
    /**
    * 和事务相关的配置类
    */
    public class TransactionConfig {
    
        /**
        * 用于创建事务管理器对象
        * @param dataSource
        * @return
        */
        @Bean(name="transactionManager")
        //PlatformTransactionManager是一个接口
    
        public PlatformTransactionManager createTransactionManager(DataSource dataSource){
    
            return new DataSourceTransactionManager(dataSource);
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
  9. 导入 TransactionConfig类。在配置类上加入 @Import({JdbcConfig.class,TransactionConfig.class}) 注解。

  10. 在测试类上加入 @ContextConfiguration(classes= SpringConfiguration.class) 注解。注意:属性为classes。

  11. 项目示例:Spring纯注解的事务配置

# AOP的编程式事务

编程式事务太麻烦,有过多重复代码,一般不推介使用。

Initializing...

山楂树之恋
程佳佳