Spring

Spring

1 Spring概述

1.1 Spring框架是什么

​ Spring 是于2003 年兴起的一个轻量级的Java 开发框架,它是为了解决企业应用开发的复杂性而创建的。Spring的核心是控制反转(IoC)和面向切面编程(AOP)。Spring是可以在Java SE/EE中使用的轻量级开源框架。

​ Spring的主要作用就是为代码“解耦”,降低代码间的耦合度。就是让对象和对象(模块和模块)之间关系不是使用代码关联,而是通过配置来说明。即在Spring中说明对象(模块)的关系。

​ Spring根据代码的功能特点,使用Ioc降低业务对象之间耦合度。IoC使得主业务在相互调用过程中,不用再自己维护关系了,即不用再自己创建要使用的对象了。而是由Spring容器统一管理,自动“注入”,注入即赋值。 而AOP使得系统级服务得到了最大复用,且不用再由程序员手工将系统级服务“混杂”到主业务逻辑中了,而是由Spring容器统一完成“织入”。

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

1.2 Spring优点

​ Spring是一个框架,是一个半成品的软件。有20个模块组成。它是一个容器管理对象,容器是装东西的,Spring容器不装文本,数字。装的是对象。Spring是存储对象的容器。

轻量

​ Spring框架使用的jar都比较小,一般在1M以下或者几百kb。Spring核心功能的所需的jar总共在3M左右。 Spring框架运行占用的资源少,运行效率高。不依赖其他jar 。

针对接口编程,解耦合

​ Spring提供了Ioc控制反转,由容器管理对象,对象的依赖关系。原来在程序代码中的对象创建方式,现在由容器完成。对象之间的依赖解耦合。

AOP编程的支持

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

​ 在Spring中,开发人员可以从繁杂的事务管理代码中解脱出来,通过声明式方式灵活地进行事务的管理,提高开发效率和质量。

方便集成各种优秀框架

​ Spring不排斥各种优秀的开源框架,相反Spring可以降低各种框架的使用难度,Spring提供了对各种优秀框架(如Struts,Hibernate、MyBatis)等的直接支持。简化框架的使用。 Spring像插线板一样,其他框架是插头,可以容易的组合到一起。需要使用哪个框架,就把这个插头放入插线板。不需要可以轻易的移除。

1.3 Spring体系结构

在这里插入图片描述

​ Spring由20多个模块组成,它们可以分为数据访问/集成(Data Access/Integration)、Web、面向切面编程(AOP, Aspects)、提供JVM的代理(Instrumentation)、消息发送(Messaging)、核心容器(Core Container)和测试(Test)。

2 IoC 控制反转

​ 控制反转(IoC,Inversion of Control),是一个概念,是一种思想。指将传统上由程序代码直接操控的对象调用权交给容器,通过容器来实现对象的装配和管理。控制反转就是对对象控制权的转移,从程序代码本身反转到了外部容器。通过容器实现对象的创建,属性赋值,依赖的管理。

​ IoC是一个概念,是一种思想,其实现方式多种多样。当前比较流行的实现方式是依赖注入。应用广泛。

​ 依赖:classA类中含有classB的实例,在classA中调用classB的方法完成功能,即classA对classB有依赖。

​ Ioc的实现:

​ ➢ 依赖注入:DI(Dependency Injection),程序代码不做定位查询,这些工作由容器自行完成。

​ 依赖注入DI是指程序运行过程中,若需要调用另一个对象协助时,无须在代码中创建被调用者,而是依赖于外部容器,由外部容器创建后传递给程序。

​ Spring的依赖注入对调用者与被调用者几乎没有任何要求,完全支持对象之间依赖关系的管理。

​ Spring框架使用依赖注入(DI)实现IoC。

​ Spring容器是一个超级大工厂,负责创建、管理所有的Java对象,这些Java对象被称为Bean。Spring容器管理着容器中Bean之间的依赖关系,Spring使用“依赖注入”的方式来管理Bean之间的依赖关系。使用IoC实现对象之间的解耦和。

2.1 实现步骤

1.创建maven项目

2.加入maven的依赖

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.5.RELEASE</version>
</dependency>

spring的依赖,版本5.2.5版本
junit依赖

<!--单元测试-->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.11</version>
    <scope>test</scope>
</dependency>

3.创建类(接口和他的实现类)
和没有使用框架一样, 就是普通的类。
4.创建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">

    <!--告诉spring创建对象
        声明bean , 就是告诉spring要创建某个类的对象
        id:对象的自定义名称,唯一值。 spring通过这个名称找到对象
        class:类的全限定名称(不能是接口,因为spring是反射机制创建对象,必须使用类)

        spring就完成 SomeService someService = new SomeServiceImpl();
        spring是把创建好的对象放入到map中, spring框架有一个map存放对象的。
           springMap.put(id的值, 对象);
           例如 springMap.put("someService", new SomeServiceImpl());

        一个bean标签声明一个对象。
    -->
    <bean id="someService" class="com.bjpowernode.service.impl.SomeServiceImpl" />

    <bean id="someService1" class="com.bjpowernode.service.impl.SomeServiceImpl" scope="prototype"/>

    <!--
       spring能创建一个非自定义类的对象吗, 创建一个存在的某个类的对象。
    -->
    <bean id="mydate" class="java.util.Date" />

</beans>

5.测试spring创建的。

@Test
public void test01(){
    SomeService service  = new SomeServiceImpl();
    service.doSome();
}

/**
     * spring默认创建对象的时间:在创建spring的容器时,会创建配置文件中的所有的对象。
     * spring创建对象:默认调用的是无参数构造方法
     */
@Test
public void test02(){
    //使用spring容器创建的对象
    //1.指定spring配置文件的名称
    String config="beans.xml";
    //2.创建表示spring容器的对象, ApplicationContext
    // ApplicationContext就是表示Spring容器,通过容器获取对象了
    // ClassPathXmlApplicationContext:表示从类路径中加载spring的配置文件
    ApplicationContext ac = new ClassPathXmlApplicationContext(config);

    //从容器中获取某个对象, 你要调用对象的方法
    //getBean("配置文件中的bean的id值")
    SomeService service = (SomeService) ac.getBean("someService");

    //使用spring创建好的对象
    service.doSome();

}

/**
     * 获取spring容器中 java 对象的信息
     */
@Test
public void test03(){
    String config="beans.xml";
    ApplicationContext ac = new ClassPathXmlApplicationContext(config);
    //使用spring提供的方法, 获取容器中定义的对象的数量
    int nums  = ac.getBeanDefinitionCount();
    System.out.println("容器中定义的对象数量:"+nums);
    //容器中每个定义的对象的名称
    String names [] = ac.getBeanDefinitionNames();
    for(String name:names){
        System.out.println(name);
    }
}

//获取一个非自定义的类对象
@Test
public void test04(){
    String config="beans.xml";
    ApplicationContext ac = new ClassPathXmlApplicationContext(config);
    //使用getBean();
    Date my = (Date) ac.getBean("mydate");
    System.out.println("Date:"+my);
}

2.2 基于XML的DI

2.2.1 注入分类

​ bean实例在调用无参构造器创建对象后,就要对bean对象的属性进行初始化。初始化是由容器自动完成的,称为注入。 根据注入方式的不同,常用的有两类:set注入、构造注入。

2.2.1.1 set注入

​ set注入也叫设值注入是指,通过setter方法传入被调用者的实例。这种注入方式简单、直观,因而在Spring的依赖注入中大量使用。

简单类型

private String name;
private int age;

public Student() {
    System.out.println("spring会调用类的无参数构造方法创建对象");
}

public void setEmail(String email){
    System.out.println("setEmail="+email);
}

/*
  需要有set方法,没有set方法是报错的。
  Bean property 'name' is not writable or has an invalid setter method
 */
public void setName(String name) {
    System.out.println("setName:"+name);
    this.name = name.toUpperCase();
}

public void setAge(int age) {
    System.out.println("setAge:"+age);
    this.age = age;
}

public String getName() {
    return name;
}

public int getAge() {
    return age;
}

@Override
public String toString() {
    return "Student{" +
        "name='" + name + '\'' +
        ", age=" + age +
        '}';
}
<!--声明student对象
        注入:就是赋值的意思
        简单类型: spring中规定java的基本数据类型和String都是简单类型。
        di:给属性赋值
        1. set注入(设值注入) :spring调用类的set方法, 你可以在set方法中完成属性赋值
         1)简单类型的set注入
            <bean id="xx" class="yyy">
               <property name="属性名字" value="此属性的值"/>
               一个property只能给一个属性赋值
               <property....>
            </bean>
    -->
<bean id="myStudent" class="com.bjpowernode.ba01.Student" >
    <property name="name" value="李四lisi" /><!--setName("李四")-->
    <property name="age" value="22" /><!--setAge(21)-->
    <property name="email" value="lisi@qq.com" /><!--setEmail("lisi@qq.com")-->
</bean>

<bean id="mydate" class="java.util.Date">
    <property name="time" value="8364297429" /><!--setTime(8364297429)-->
</bean>

引用类型

当指定bean的某属性值为另一bean的实例时,通过ref指定它们间的引用关系。ref的值必须为某bean的id值。

public class School {

    private String name;
    private String address;

    public void setName(String name) {
        this.name = name;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    @Override
    public String toString() {
        return "School{" +
                "name='" + name + '\'' +
                ", address='" + address + '\'' +
                '}';
    }
}

public class Student {

    private String name;
    private int age;

    //声明一个引用类型
    private School school;


    public Student() {
        System.out.println("spring会调用类的无参数构造方法创建对象");
    }

    // 包名.类名.方法名称
    // com.bjpowernode.ba02.Student.setName()
    public void setName(String name) {
        System.out.println("setName:"+name);
        this.name = name;
    }

    public void setAge(int age) {
        System.out.println("setAge:"+age);
        this.age = age;
    }

    public void setSchool(School school) {
        System.out.println("setSchool:"+school);
        this.school = school;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", school=" + school +
                '}';
    }
}
<!--
         2) 引用类型的set注入 : spring调用类的set方法
           <bean id="xxx" class="yyy">
              <property name="属性名称" ref="bean的id(对象的名称)" />
           </bean>
    -->
    <bean id="myStudent" class="com.bjpowernode.ba02.Student" >
        <property name="name" value="李四" />
        <property name="age" value="26" />
        <!--引用类型-->
        <property name="school" ref="mySchool" /><!--setSchool(mySchool)-->
    </bean>

    <!--声明School对象-->
    <bean id="mySchool" class="com.bjpowernode.ba02.School">
        <property name="name" value="北京大学"/>
        <property name="address" value="北京的海淀区" />
    </bean>
2.2.1.2 构造注入

构造注入是指,在构造调用者实例的同时,完成被调用者的实例化。即,使用构造器设置依赖关系。

public class Student {

    private String name;
    private int age;

    //声明一个引用类型
    private School school;


    public Student() {
        System.out.println("spring会调用类的无参数构造方法创建对象");
    }

    /**
     * 创建有参数构造方法
     */
    public Student(String myname,int myage, School mySchool){
        System.out.println("=====Student有参数构造方法======");
        //属性赋值
        this.name  = myname;
        this.age  = myage;
        this.school = mySchool;

    }

    public void setName(String name) {
        this.name = name;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public void setSchool(School school) {
        this.school = school;
    }


    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", school=" + school +
                '}';
    }
}
<!--
        2.构造注入:spring调用类有参数构造方法,在创建对象的同时,在构造方法中给属性赋值。
          构造注入使用 <constructor-arg> 标签
          <constructor-arg> 标签:一个<constructor-arg>表示构造方法一个参数。
          <constructor-arg> 标签属性:
             name:表示构造方法的形参名
             index:表示构造方法的参数的位置,参数从左往右位置是 0 , 1 ,2的顺序
             value:构造方法的形参类型是简单类型的,使用value
             ref:构造方法的形参类型是引用类型的,使用ref
    -->


    <!--使用name属性实现构造注入-->
    <bean id="myStudent" class="com.bjpowernode.ba03.Student" >
        <constructor-arg name="myage" value="20" />
        <constructor-arg name="mySchool" ref="myXueXiao" />
        <constructor-arg name="myname" value="周良"/>
    </bean>

    <!--使用index属性-->
    <bean id="myStudent2" class="com.bjpowernode.ba03.Student">
        <constructor-arg index="1" value="22" />
        <constructor-arg index="0" value="李四" />
        <constructor-arg index="2" ref="myXueXiao" />
    </bean>

    <!--省略index-->
    <bean id="myStudent3" class="com.bjpowernode.ba03.Student">
        <constructor-arg  value="张强强" />
        <constructor-arg  value="22" />
        <constructor-arg  ref="myXueXiao" />
    </bean>
    <!--声明School对象-->
    <bean id="myXueXiao" class="com.bjpowernode.ba03.School">
        <property name="name" value="清华大学"/>
        <property name="address" value="北京的海淀区" />
    </bean>

    <!--创建File,使用构造注入-->
    <bean id="myfile" class="java.io.File">
        <constructor-arg name="parent" value="D:\course\JavaProjects\spring-course\ch01-hello-spring" />
        <constructor-arg name="child" value="readme.txt" />
    </bean>
2.2.2 引用类型属性自动注入

​ 对于引用类型属性的注入,也可不在配置文件中显示的注入。可以通过为<bean/>标签设置autowire属性值,为引用类型属性进行隐式自动注入(默认是不自动注入引用类型属性)。根据自动注入判断标准的不同,可以分为两种:

​ byName:根据名称自动注入

​ byType: 根据类型自动注入

2.2.2.1 byName方式自动注入

​ 当配置文件中被调用者bean的id值与代码中调用者bean类的属性名相同时,可使用byName方式,让容器自动将被调用者bean注入给调用者bean。容器是通过调用者的bean类的属性名与配置文件的被调用者bean的id进行比较而实现自动注入的。

<!--
            引用类型的自动注入: spring框架根据某些规则可以给引用类型赋值。·不用你在给引用类型赋值了
       使用的规则常用的是byName, byType.
       1.byName(按名称注入) : java类中引用类型的属性名和spring容器中(配置文件)<bean>的id名称一样,
                              且数据类型是一致的,这样的容器中的bean,spring能够赋值给引用类型。
         语法:
         <bean id="xx" class="yyy" autowire="byName">
            简单类型属性赋值
         </bean>
    -->
    <!--byName-->
    <bean id="myStudent" class="com.bjpowernode.ba04.Student"  autowire="byName">
        <property name="name" value="李四" />
        <property name="age" value="26" />
        <!--引用类型-->
        <!--<property name="school" ref="mySchool" />-->
    </bean>

    <!--声明School对象-->
    <bean id="school" class="com.bjpowernode.ba04.School">
        <property name="name" value="清华大学"/>
        <property name="address" value="北京的海淀区" />
    </bean>
2.2.2.2 byType方式自动注入

​ 使用byType方式自动注入,要求:配置文件中被调用者bean的class属性指定的类,要与代码中调用者bean类的某引用类型属性类型同源。即要么相同,要么有is-a关系(子类,或是实现类)。但这样的同源的被调用bean只能有一个。多于一个,容器就不知该匹配哪一个了。

<!--
       2.byType(按类型注入) : java类中引用类型的数据类型和spring容器中(配置文件)<bean>的class属性
                              是同源关系的,这样的bean能够赋值给引用类型
         同源就是一类的意思:
          1.java类中引用类型的数据类型和bean的class的值是一样的。
          2.java类中引用类型的数据类型和bean的class的值父子类关系的。
          3.java类中引用类型的数据类型和bean的class的值接口和实现类关系的
         语法:
         <bean id="xx" class="yyy" autowire="byType">
            简单类型属性赋值
         </bean>
		 注意:在byType中, 在xml配置文件中声明bean只能有一个符合条件的,
              多余一个是错误的
    -->
	<!--byType-->
    <bean id="myStudent" class="com.bjpowernode.ba05.Student"  autowire="byType">
        <property name="name" value="张飒" />
        <property name="age" value="26" />
        <!--引用类型-->
        <!--<property name="school" ref="mySchool" />-->
    </bean>

    <!--声明School对象-->
    <bean id="mySchool" class="com.bjpowernode.ba05.School">
        <property name="name" value="人民大学"/>
        <property name="address" value="北京的海淀区" />
    </bean>
2.2.3 为应用指定多个Spring配置文件

​ 在实际应用里,随着应用规模的增加,系统中Bean数量也大量增加,导致配置文件变得非常庞大、臃肿。为了避免这种情况的产生,提高配置文件的可读性与可维护性,可以将Spring配置文件分解成多个配置文件。

spring-school.xml

<!--School模块所有bean的声明, School模块的配置文件-->
<!--声明School对象-->
<bean id="mySchool" class="com.bjpowernode.ba06.School">
    <property name="name" value="航空大学"/>
    <property name="address" value="北京的海淀区" />
</bean>

spring-student.xml

<!--
student模块所有bean的声明
-->
<!--byType-->
<bean id="myStudent" class="com.bjpowernode.ba06.Student"  autowire="byType">
<property name="name" value="张飒" />
<property name="age" value="30" />
<!--引用类型-->
<!--<property name="school" ref="mySchool" />-->
</bean>

total.xml

<!--
         包含关系的配置文件:
         spring-total表示主配置文件 : 包含其他的配置文件的,主配置文件一般是不定义对象的。
         语法:<import resource="其他配置文件的路径" />
         关键字:"classpath:" 表示类路径(class文件所在的目录),
               在spring的配置文件中要指定其他文件的位置, 需要使用classpath,告诉spring到哪去加载读取文件。
    -->

<!--加载的是文件列表-->
<!--
    <import resource="classpath:ba06/spring-school.xml" />
    <import resource="classpath:ba06/spring-student.xml" />
    -->

<!--
       在包含关系的配置文件中,可以通配符(*:表示任意字符)
       注意: 主的配置文件名称不能包含在通配符的范围内(不能叫做spring-total.xml)
    -->
<import resource="classpath:ba06/spring-*.xml" />

2.3 基于注解的DI

​ 对于DI使用注解,将不再需要在Spring配置文件中声明bean实例。Spring中使用注解,需要在原有Spring运行环境基础上再做一些改变。 需要在Spring配置文件中配置组件扫描器,用于在指定的基本包中扫描注解。

<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
       https://www.springframework.org/schema/context/spring-context.xsd">

    <!--声明组件扫描器(component-scan),组件就是java对象
        base-package:指定注解在你的项目中的包名。
        component-scan工作方式: spring会扫描遍历base-package指定的包,
           把包中和子包中的所有类,找到类中的注解,按照注解的功能创建对象,或给属性赋值。

       加入了component-scan标签,配置文件的变化:
        1.加入一个新的约束文件spring-context.xsd
        2.给这个新的约束文件起个命名空间的名称
    -->
    <context:component-scan base-package="com.bjpowernode.ba02" />
</beans>

指定多个包的三种方式:

1)使用多个context:component-scan指定不同的包路径

<!--第一种方式:使用多次组件扫描器,指定不同的包-->
<context:component-scan base-package="com.bjpowernode.ba01"/>
<context:component-scan base-package="com.bjpowernode.ba02"/>

2)指定base-package的值使用分隔符

分隔符可以使用逗号(,)分号(;)还可以使用空格,不建议使用空格。

<!--第二种方式:使用分隔符(;或,)分隔多个包名-->
<context:component-scan base-package="com.bjpowernode.ba01;com.bjpowernode.ba02" />

3)base-package是指定到父包名

​ 但不建议使用顶级的父包,扫描的路径比较多,导致容器启动时间变慢。指定到目标包和合适的。也就是注解所在包全路径。

<!--第三种方式:指定父包-->
<context:component-scan base-package="com.bjpowernode" />
2.3.1 定义Bean的注解@Component

需要在类上使用注解@Component,该注解的value属性用于指定该bean的id值。

/**
 * @Component: 创建对象的, 等同于<bean>的功能
 *     属性:value 就是对象的名称,也就是bean的id值,
 *          value的值是唯一的,创建的对象在整个spring容器中就一个
 *     位置:在类的上面
 *
 *  @Component(value = "myStudent")等同于
 *   <bean id="myStudent" class="com.bjpowernode.ba01.Student" />
 *
 *  spring中和@Component功能一致,创建对象的注解还有:
 *  1.@Repository(用在持久层类的上面) : 放在dao的实现类上面,
 *               表示创建dao对象,dao对象是能访问数据库的。
 *  2.@Service(用在业务层类的上面):放在service的实现类上面,
 *              创建service对象,service对象是做业务处理,可以有事务等功能的。
 *  3.@Controller(用在控制器的上面):放在控制器(处理器)类的上面,创建控制器对象的,
 *              控制器对象,能够接受用户提交的参数,显示请求的处理结果。
 *  以上三个注解的使用语法和@Component一样的。 都能创建对象,但是这三个注解还有额外的功能。
 *  @Repository,@Service,@Controller是给项目的对象分层的。
 *
 *
 */
//使用value属性,指定对象名称
//@Component(value = "myStudent")

//省略value
@Component("myStudent")

//不指定对象名称,由spring提供默认名称: 类名的首字母小写
//@Component
public class Student {
}
2.3.2 简单类型属性注入@Value

​ 需要在属性上使用注解@Value,该注解的value属性用于指定要注入的值。 使用该注解完成属性注入时,类中无需setter。当然,若属性有setter,则也可将其加到setter上。

/**
     * @Value: 简单类型的属性赋值
     *   属性: value 是String类型的,表示简单类型的属性值
     *   位置: 1.在属性定义的上面,无需set方法,推荐使用。
     *         2.在set方法的上面
     */
    //@Value("李四" )
    @Value("${myname}") //使用属性配置文件中的数据
    private String name;

    @Value("${myage}")  //使用属性配置文件中的数据
    private Integer age;

    public Student() {
        System.out.println("==student无参数构造方法===");
    }

    public void setName(String name) {
        this.name = name;
    }
    //@Value("30")
    public void setAge(Integer age) {
        System.out.println("setAge:"+age);
        this.age = age;
    }

application.xml

<!--加载属性配置文件-->
<context:property-placeholder location="classpath:test.properties" />

test.properties

myname=nnnn
myage=20
2.3.3 byType自动注入@Autowired

​ 需要在引用属性上使用注解@Autowired,该注解默认使用按类型自动装配Bean的方式。 使用该注解完成属性注入时,类中无需setter。当然,若属性有setter,则也可将其加到setter上。

 /**
     * 引用类型
     * @Autowired: spring框架提供的注解,实现引用类型的赋值。
     * spring中通过注解给引用类型赋值,使用的是自动注入原理 ,支持byName, byType
     * @Autowired:默认使用的是byType自动注入。
     *
     *  位置:1)在属性定义的上面,无需set方法, 推荐使用
     *       2)在set方法的上面
     */
@Autowired
private School school;
2.3.4 byName自动注入@Autowired与@Qualifier

​ 需要在引用属性上联合使用注解@Autowired与@Qualifier。@Qualifier的value属性用于指定要匹配的Bean的id值。类中无需set方法,也可加到set方法上。

/**
 * 引用类型
 *  如果要使用byName方式,需要做的是:
 *  1.在属性上面加入@Autowired
 *  2.在属性上面加入@Qualifier(value="bean的id") :表示使用指定名称的bean完成赋值。
 */

//byName自动注入
@Autowired
@Qualifier("mySchool")
private School school;

​ @Autowired还有一个属性required,默认值为true,表示当匹配失败后,会终止程序运行。若将其值设置为false,则匹配失败,将被忽略,未匹配的属性值为null。

/**
 * 引用类型
 *   属性:required ,是一个boolean类型的,默认true
 *       required=true:表示引用类型赋值失败,程序报错,并终止执行。
 *       required=false:引用类型如果赋值失败, 程序正常执行,引用类型是null
 */
//byName自动注入
@Autowired(required = false)
@Qualifier("mySchool-1")
private School school;
2.3.5 JDK注解@Resource自动注入

​ Spring提供了对jdk中@Resource注解的支持。@Resource注解既可以按名称匹配Bean,也可以按类型匹配Bean。默认是按名称注入。使用该注解,要求JDK必须是6及以上版本。@Resource可在属性上,也可在set方法上。

byType注入引用类型属性

@Resource注解若不带任何参数,采用默认按名称的方式注入,按名称不能注入bean,则会按照类型进行Bean的匹配注入。

/**
 * 引用类型
 * @Resource: 来自jdk中的注解,spring框架提供了对这个注解的功能支持,可以使用它给引用类型赋值
 *            使用的也是自动注入原理,支持byName, byType .默认是byName
 *  位置: 1.在属性定义的上面,无需set方法,推荐使用。
 *        2.在set方法的上面
 */
//默认是byName: 先使用byName自动注入,如果byName赋值失败,再使用byType
@Resource
private School school;

byName注入引用类型属性

@Resource注解指定其name属性,则name的值即为按照名称进行匹配的Bean的id。

/**
 * 引用类型
 * @Resource只使用byName方式,需要增加一个属性 name
 * name的值是bean的id(名称)
 */
//只使用byName
@Resource(name = "mySchool")
private School school;

3 AOP面向切面编程

3.1 动态代理

​ 动态代理是指,程序在整个运行过程中根本就不存在目标类的代理类,目标对象的代理对象只是由的代理生成工具(不是真实定义的类)在程序运行时由JVM根据反射等机制动态生成的。代理对象与目标对象的代理关系在程序运行时才确立。

3.1.1 JDK动态代理

​ 动态代理的实现方式常用有两种:使用JDK的Proxy,与通过CGLIB生成代理。JDK的动态代理要求目标对象必须实现接口,这是java设计上的要求。

​ 从jdk1.3以来,java语言通过java.lang.reflect包提供三个类支持代理模式,Proxy,Method和InvocationHandler。

3.1.2 CGLIB动态代理

​ CGLIB(Code Generation Library)是一个开源项目。是一个强大的,高效能,高质量的Code生成类库,它可以在运行期扩展java类与实现java接口。它广泛的被许多AOP的框架使用,例如Spring AOP。

​ 使用JDK的Proxy实现代理,要求目标类与代理类实现相同的接口。若目标类不存在接口,则无法使用该方式实现。但对于无接口的类,要为其创建动态代理,就要使用CGLIB来实现。

​ CGLIB代理的生成原则是生成目标类的子类,而子类是增强过的,这个子类对象就是代理对象。所以,使用CGLIB生成动态代理,要求目标类必须能够被继承,既不能是final的类。

​ CGLIB经常被应用在框架中,例如Spring,Hibernate等。CGLIB的代理效率高于JDK。项目中直接使用动态代理的地方不多。一般都使用框架提供的功能。

3.2 不使用AOP的开发方式

MyIncationHandler.java

public class MyIncationHandler implements InvocationHandler {

    //目标对象
    private Object target; //SomeServiceImpl类

    public MyIncationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //通过代理对象执行方法时,会调用执行这个invoke()
        System.out.println("执行MyIncationHandler中的invoke()");
        System.out.println("method名称:"+method.getName());
        String methodName = method.getName();
        Object res = null;

        if("doSome".equals(methodName)){ //JoinPoint  Pointcut
            ServiceTools.doLog(); //在目标方法之前,输出时间
            //执行目标类的方法,通过Method类实现
            res  = method.invoke(target,args); //SomeServiceImpl.doSome()
            ServiceTools.doTrans(); //在目标方法执行之后,提交事务
        } else {
            res  = method.invoke(target,args); //SomeServiceImpl.doOther()
        }

        //目标方法的执行结果
        return res;
    }
}

SomeService.java

public interface SomeService {

    void doSome();
    void doOther();
}

SomeServiceImpl.java

public class SomeServiceImpl implements SomeService {
    @Override
    public void doSome() {
        System.out.println("执行业务方法doSome");
        int res  = 10 + 20;
        //更新购买商品的库存, 生成订单, 结算整个购买商品的总价。
    }

    @Override
    public void doOther() {
        System.out.println("执行业务方法doOther");
    }
}

ServiceTools.java

public class ServiceTools {

    public static void doLog(){
        System.out.println("非业务方法,方法的执行时间:"+ new Date());
    }

    public static void doTrans(){
        //方法的最后,提交事务
        System.out.println("非业务方法,方法执行完毕后,提交事务");
    }
}

main

public static void main(String[] args) {
        //调用doSome, doOther
//        SomeService service = new SomeServiceImpl();
//        service.doSome();
//        System.out.println("============================================");
//        service.doOther();

        //使用jdk的Proxy创建代理对象
        //创建目标对象
        SomeService target = new SomeServiceImpl();

        //创建InvocationHandler对象
        InvocationHandler handler = new MyIncationHandler(target);

        //使用Proxy创建代理
        SomeService proxy = (SomeService) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),handler);
        //com.sun.proxy.$Proxy0
        System.out.println("proxy======"+proxy.getClass().getName());
        //通过代理执行方法,会调用handler中的invoke()
        proxy.doSome();
        System.out.println("==================================================");
        proxy.doOther();
    }

3.3 AOP

​ AOP(Aspect Orient Programming),面向切面编程。面向切面编程是从动态角度考虑程序运行过程。 AOP底层,就是采用动态代理模式实现的。采用了两种代理:JDK的动态代理,与CGLIB的动态代理。

​ jdk动态代理,使用jdk中的Proxy,Method,InvocaitonHanderl创建代理对象。jdk动态代理要求目标类必须实现接口。

	cglib动态代理:第三方的工具库,创建代理对象,原理是继承。 通过继承目标类,创建子类。子类就是代理对象。 要求目标类不能是final的, 方法也不能是final的。

​ AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,可通过运行期动态代理实现程序功能的统一维护的一种技术。AOP是Spring框架中的一个重要内容。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

​ 面向切面编程,就是将交叉业务逻辑封装成切面,利用AOP容器的功能将切面织入到主业务逻辑中。所谓交叉业务逻辑是指,通用的、与主业务逻辑无关的代码,如安全检查、事务、日志、缓存等。 若不使用AOP,则会出现代码纠缠,即交叉业务逻辑与主业务逻辑混合在一起。这样,会使主业务逻辑变的混杂不清。 例如,转账,在真正转账业务逻辑前后,需要权限控制、日志记录、加载事务、结束事务等交叉业务逻辑,而这些业务逻辑与主业务逻辑间并无直接关系。但,它们的代码量所占比重能达到总代码量的一半甚至还多。它们的存在,不仅产生了大量的“冗余”代码,还大大干扰了主业务逻辑—转账。

动态代理的作用:

​ 1)在目标类源代码不改变的情况下,增加功能。
​ 2)减少代码的重复。
​ 3)专注业务逻辑代码。
​ 4)解耦合,让你的业务功能和日志,事务非业务功能分离。

3.4 AOP编程术语(掌握)

(1) 切面(Aspect)

	切面泛指交叉业务逻辑。上例中的事务处理、日志处理就可以理解为切面。常用的切面是通知(Advice)。实际就是对主业务逻辑的一种增强。 

(2) 连接点(JoinPoint)

​ 连接点指可以被切面织入的具体方法。通常业务接口中的方法均为连接点。

(3) 切入点(Pointcut)

​ 切入点指声明的一个或多个连接点的集合。通过切入点指定一组方法。 被标记为final的方法是不能作为连接点与切入点的。因为最终的是不能被修改的,不能被增强的。

(4) 目标对象(Target)

​ 目标对象指将要被增强的对象。即包含主业务逻辑的类的对象。上例中的StudentServiceImpl的对象若被增强,则该类称为目标类,该类对象称为目标对象。当然,不被增强,也就无所谓目标不目标了。

(5) 通知(Advice)

​ 通知表示切面的执行时间,Advice也叫增强。上例中的MyInvocationHandler就可以理解为是一种通知。换个角度来说,通知定义了增强代码切入到目标代码的时间点,是目标方法执行之前执行,还是之后执行等。通知类型不同,切入时间不同。 切入点定义切入的位置,通知定义切入的时间。

3.5 AspectJ对AOP的实现

对于AOP这种编程思想,很多框架都进行了实现。Spring就是其中之一,可以完成面向切面编程。然而,AspectJ也实现了AOP的功能,且其实现方式更为简捷,使用更为方便,而且还支持注解式开发。所以,Spring又将AspectJ的对于AOP的实现也引入到了自己的框架中。 在Spring中使用AOP开发时,一般使用AspectJ的实现方式。

AspectJ简介

AspectJ是一个优秀面向切面的框架,它扩展了Java语言,提供了强大的切面实现。

官网地址:http://www.eclipse.org/aspectj/

3.5.1 AspectJ的通知类型

​ AspectJ中常用的通知有五种类型:

​ (1)前置通知

​ (2)后置通知

​ (3)环绕通知

​ (4)异常通知

​ (5)最终通知

3.5.2 AspectJ的切入点表达式

AspectJ定义了专门的表达式用于指定切入点。表达式的原型是:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)  throws-pattern?) 
解释: modifiers-pattern 访问权限类型 
	  ret-type-pattern 返回值类型 
      declaring-type-pattern 包名类名 
      name-pattern(param-pattern) 方法名(参数类型和参数个数) 
      throws-pattern 抛出异常类型  
      ?表示可选的部分 
以上表达式共4个部分。 execution(访问权限 方法返回值 方法声明(参数) 异常类型) 

​ 切入点表达式要匹配的对象就是目标方法的方法名。所以,execution表达式中明显就是方法的签名。注意,表达式访问权限、异常类型可省略部分,各部分间用空格分开。在其中可以使用以下符号:

符号意义
*0至过个任意字符
用在方法参数中,表示任意多个参数;
用在包名后,表示当前包及其子包路径
+用在类名后,表示当前类及其子类;
用在接口后,表示当前接口及其实现类
举例: 
execution(public * *(..)) 指定切入点为:任意公共方法。 
execution(* set*(..)) 指定切入点为:任何一个以“set”开始的方法。 
execution(* com.xyz.service.*.*(..)) 指定切入点为:定义在service包里的任意类的任意方法。 execution(* com.xyz.service..*.*(..)) 指定切入点为:定义在service包或者子包里的任意类的任意方法。“..”出现在类名中时,后面必须跟“*”,表示包、子包下的所有类。 
execution(* *..service.*.*(..)) 指定所有包下的serivce子包下所有类(接口)中所有方法为切入点 execution(* *.service.*.*(..)) 指定只有一级包下的serivce子包下所有类(接口)中所有方法为切入点 execution(* *.ISomeService.*(..)) 指定只有一级包下的ISomeSerivce接口中所有方法为切入点 execution(* *..ISomeService.*(..)) 指定所有包下的ISomeSerivce接口中所有方法为切入点 
execution(* com.xyz.service.IAccountService.*(..)) 指定切入点为:IAccountService 接口中的任意方法。 
execution(* com.xyz.service.IAccountService+.*(..)) 指定切入点为:IAccountService 若为接口,则为接口中的任意方法及其所有实现类中的任意方法;若为类,则为该类及其子类中的任意方法。 
execution(* joke(String,int))) 指定切入点为:所有的joke(String,int)方法,且joke()方法的第一个参数是String,第二个参数是int。如果方法中的参数类型是java.lang包下的类,可以直接使用类名,否则必须使用全限定类名,如joke( java.util.List, int)。 
execution(* joke(String,*))) 指定切入点为:所有的joke()方法,该方法第一个参数为String,第二个参数可以是任意类型,如joke(String s1,String s2)和joke(String s1,double d2)都是,但joke(String s1,double d2,String s3)不是。 
execution(* joke(String,..))) 指定切入点为:所有的joke()方法,该方法第一个参数为String,后面可以有任意个参数且参数类型不限,如joke(String s1)、joke(String s1,String s2)和joke(String s1,double d2,String s3)都是。 
execution(* joke(Object)) 指定切入点为:所有的joke()方法,方法拥有一个参数,且参数是Object类型。joke(Object ob)是,但joke(String s)与joke(User u)均不是。 
execution(* joke(Object+))) 指定切入点为:所有的joke()方法,方法拥有一个参数,且参数是Object类型或该类的子类。不仅joke(Object ob)是,joke(String s)和joke(User u)也是。 
3.5.3 AspectJ的开发环境
<dependency>  
    <groupId>org.springframework</groupId>  
    <artifactId>spring-context</artifactId>  
    <version>5.2.5.RELEASE</version> 
</dependency>  
<dependency>  
    <groupId>org.springframework</groupId>  
    <artifactId>spring-aspects</artifactId>  
    <version>5.2.5.RELEASE</version> 
</dependency> 

引入AOP约束

在AspectJ实现AOP时,要引入AOP的约束。配置文件中使用的AOP约束中的标签,均是AspectJ框架使用的,而非Spring框架本身在实现AOP时使用的。 AspectJ对于AOP的实现有注解和配置文件两种方式,常用是注解方式。

3.5.4 AspectJ基于注解的AOP实现

实现步骤

public interface SomeService {
    void doSome(String name,Integer age);
}
//目标类
public class SomeServiceImpl implements SomeService {
    @Override
    public void doSome(String name,Integer age) {
        //给doSome方法增加一个功能,在doSome()执行之前, 输出方法的执行时间
        System.out.println("====目标方法doSome()====");
    }

    public void doOther(String name,Integer age) {
        //给doSome方法增加一个功能,在doSome()执行之前, 输出方法的执行时间
        System.out.println("====目标方法doSome()====");
    }
}
/**
 *  @Aspect : 是aspectj框架中的注解。
 *     作用:表示当前类是切面类。
 *     切面类:是用来给业务方法增加功能的类,在这个类中有切面的功能代码
 *     位置:在类定义的上面
 */
@Aspect
public class MyAspect {
    /**
     * 定义方法,方法是实现切面功能的。
     * 方法的定义要求:
     * 1.公共方法 public
     * 2.方法没有返回值
     * 3.方法名称自定义
     * 4.方法可以有参数,也可以没有参数。
     *   如果有参数,参数不是自定义的,有几个参数类型可以使用。
     */


    /**
     * @Before: 前置通知注解
     *   属性:value ,是切入点表达式,表示切面的功能执行的位置。
     *   位置:在方法的上面
     * 特点:
     *  1.在目标方法之前先执行的
     *  2.不会改变目标方法的执行结果
     *  3.不会影响目标方法的执行。
     */
   /* @Before(value = "execution(public void com.bjpowernode.ba01.SomeServiceImpl.doSome(String,Integer))")
    public void myBefore(){
        //就是你切面要执行的功能代码
        System.out.println("前置通知, 切面功能:在目标方法之前输出执行时间:"+ new Date());
    }*/


    /*@Before(value = "execution(void com.bjpowernode.ba01.SomeServiceImpl.doSome(String,Integer))")
    public void myBefore(){
        //就是你切面要执行的功能代码
        System.out.println("1=====前置通知, 切面功能:在目标方法之前输出执行时间:"+ new Date());
    }*/

  /*  @Before(value = "execution(void *..SomeServiceImpl.doSome(String,Integer))")
    public void myBefore(){
        //就是你切面要执行的功能代码
        System.out.println("2=====前置通知, 切面功能:在目标方法之前输出执行时间:"+ new Date());
    }*/

    /*@Before(value = "execution(* *..SomeServiceImpl.*(..))")
    public void myBefore(){
        //就是你切面要执行的功能代码
        System.out.println("3=====前置通知, 切面功能:在目标方法之前输出执行时间:"+ new Date());
    }*/

    /*@Before(value = "execution(* do*(..))")
    public void myBefore2(){
        //就是你切面要执行的功能代码
        System.out.println("4=====前置通知, 切面功能:在目标方法之前输出执行时间:"+ new Date());
    }*/


    /*@Before(value = "execution(* com.bjpowernode.ba01.*ServiceImpl.*(..))")
    public void myBefore2(){
        //就是你切面要执行的功能代码
        System.out.println("2=====前置通知, 切面功能:在目标方法之前输出执行时间:"+ new Date());
    }*/
}
<?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.xsd
       http://www.springframework.org/schema/aop
       https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--把对象交给spring容器,由spring容器统一创建,管理对象-->
    <!--声明目标对象-->
    <bean id="someService" class="com.bjpowernode.ba08.SomeServiceImpl" />

    <!--声明切面类对象-->
    <bean id="myAspect" class="com.bjpowernode.ba08.MyAspect" />

    <!--声明自动代理生成器:使用aspectj框架内部的功能,创建目标对象的代理对象。
        创建代理对象是在内存中实现的, 修改目标对象的内存中的结构。 创建为代理对象
        所以目标对象就是被修改后的代理对象.

        aspectj-autoproxy:会把spring容器中的所有的目标对象,一次性都生成代理对象。
    -->
    <aop:aspectj-autoproxy />
</beans>

​ <aop:aspectj-autoproxy/>的底层是由AnnotationAwareAspectJAutoProxyCreator实现的。从其类名就可看出,是基于AspectJ的注解适配自动代理生成器。 其工作原理是,<aop:aspectj-autoproxy/>通过扫描找到@Aspect定义的切面类,再由切面类根据切入点找到目标类的目标方法,再由通知类型找到切入的时间点。

@Test
public void test01(){
    String config="applicationContext.xml";
    ApplicationContext ctx = new ClassPathXmlApplicationContext(config);
    //从容器中获取目标对象
    SomeService proxy = (SomeService) ctx.getBean("someService");

    //com.sun.proxy.$Proxy8 :jdk动态代理
    //com.sun.proxy.$Proxy0
    System.out.println("proxy:"+proxy.getClass().getName());
    //通过代理的对象执行方法,实现目标方法执行时,增强了功能
    proxy.doSome("lisi",20);
}

JoinPoint

​ 该类型的对象本身就是切入点表达式。通过该参数,可获取切入点表达式、方法签名、目标对象等。 不光前置通知的方法,可以包含一个JoinPoint类型参数,所有的通知方法均可包含该参数。

/**
     * 指定通知方法中的参数 : JoinPoint
     * JoinPoint:业务方法,要加入切面功能的业务方法
     *    作用是:可以在通知方法中获取方法执行时的信息, 例如方法名称,方法的实参。
     *    如果你的切面功能中需要用到方法的信息,就加入JoinPoint.
     *    这个JoinPoint参数的值是由框架赋予, 必须是第一个位置的参数
     */
    @Before(value = "execution(void *..SomeServiceImpl.doSome(String,Integer))")
    public void myBefore(JoinPoint jp){
        //获取方法的完整定义
        System.out.println("方法的签名(定义)="+jp.getSignature());
        System.out.println("方法的名称="+jp.getSignature().getName());
        //获取方法的实参
        Object args [] = jp.getArgs();
        for (Object arg:args){
            System.out.println("参数="+arg);
        }
        //就是你切面要执行的功能代码
        System.out.println("2=====前置通知, 切面功能:在目标方法之前输出执行时间:"+ new Date());
    }

@AfterReturning后置通知-注解有returning属性

​ 在目标方法执行之后执行。由于是目标方法之后执行,所以可以获取到目标方法的返回值。该注解的returning属性就是用于指定接收方法返回值的变量名的。所以,被注解为后置通知的方法,除了可以包含JoinPoint参数外,还可以包含用于接收返回值的变量。该变量最好为Object类型,因为目标方法的返回值可能是任何类型。

public interface SomeService {
    void doSome(String name, Integer age);
    String doOther(String name,Integer age);
    Student doOther2(String name,Integer age);
}
//目标类
public class SomeServiceImpl implements SomeService {
    @Override
    public void doSome(String name,Integer age) {
        //给doSome方法增加一个功能,在doSome()执行之前, 输出方法的执行时间
        System.out.println("====目标方法doSome()====");
    }

    @Override
    public String doOther(String name, Integer age) {
        System.out.println("====目标方法doOther()====");
        return "abcd";
    }

    @Override
    public Student doOther2(String name, Integer age) {
        Student student = new Student();
        student.setName("lisi");
        student.setAge(20);
        return student;
    }
}
/**
 *  @Aspect : 是aspectj框架中的注解。
 *     作用:表示当前类是切面类。
 *     切面类:是用来给业务方法增加功能的类,在这个类中有切面的功能代码
 *     位置:在类定义的上面
 */
@Aspect
public class MyAspect {
    /**
     * 后置通知定义方法,方法是实现切面功能的。
     * 方法的定义要求:
     * 1.公共方法 public
     * 2.方法没有返回值
     * 3.方法名称自定义
     * 4.方法有参数的,推荐是Object ,参数名自定义
     */

    /**
     * @AfterReturning:后置通知
     *    属性:1.value 切入点表达式
     *         2.returning 自定义的变量,表示目标方法的返回值的。
     *          自定义变量名必须和通知方法的形参名一样。
     *    位置:在方法定义的上面
     * 特点:
     *  1。在目标方法之后执行的。
     *  2. 能够获取到目标方法的返回值,可以根据这个返回值做不同的处理功能
     *      Object res = doOther();
     *  3. 可以修改这个返回值
     *
     *  后置通知的执行
     *    Object res = doOther();
     *    参数传递: 传值, 传引用
     *    myAfterReturing(res);
     *    System.out.println("res="+res)
     *
     */
    @AfterReturning(value = "execution(* *..SomeServiceImpl.doOther(..))",
                    returning = "res")
    public void myAfterReturing(  JoinPoint jp  ,Object res ){
        // Object res:是目标方法执行后的返回值,根据返回值做你的切面的功能处理
        System.out.println("后置通知:方法的定义"+ jp.getSignature());
        System.out.println("后置通知:在目标方法之后执行的,获取的返回值是:"+res);
        if(res.equals("abcd")){
            //做一些功能
        } else{
            //做其它功能
        }

        //修改目标方法的返回值, 看一下是否会影响 最后的方法调用结果
        if( res != null){
            res = "Hello Aspectj";
        }
    }

    //作业
    @AfterReturning(value = "execution(* *..SomeServiceImpl.doOther2(..))",
            returning = "res")
    public void myAfterReturing2(Object res){
        // Object res:是目标方法执行后的返回值,根据返回值做你的切面的功能处理
        System.out.println("后置通知:在目标方法之后执行的,获取的返回值是:"+res);

        //修改目标方法的返回值, 看一下是否会影响 最后的方法调用结果
        //如果修改了res的内容,属性值等,是不是会影响最后的调用结果呢

    }
}

@Around环绕通知-增强方法有ProceedingJoinPoint参数

​ 在目标方法执行之前之后执行。被注解为环绕增强的方法要有返回值,Object类型。并且方法可以包含一个ProceedingJoinPoint类型的参数。接口ProceedingJoinPoint其有一个proceed()方法,用于执行目标方法。若目标方法有返回值,则该方法的返回值就是目标方法的返回值。最后,环绕增强方法将其返回值返回。该增强方法实际是拦截了目标方法的执行。

@Aspect
public class MyAspect {
    /**
     * 环绕通知方法的定义格式
     *  1.public
     *  2.必须有一个返回值,推荐使用Object
     *  3.方法名称自定义
     *  4.方法有参数,固定的参数 ProceedingJoinPoint
     */

    /**
     * @Around: 环绕通知
     *    属性:value 切入点表达式
     *    位置:在方法的定义什么
     * 特点:
     *   1.它是功能最强的通知
     *   2.在目标方法的前和后都能增强功能。
     *   3.控制目标方法是否被调用执行
     *   4.修改原来的目标方法的执行结果。 影响最后的调用结果
     *
     *  环绕通知,等同于jdk动态代理的,InvocationHandler接口
     *
     *  参数:  ProceedingJoinPoint 就等同于 Method
     *         作用:执行目标方法的
     *  返回值: 就是目标方法的执行结果,可以被修改。
     *
     *  环绕通知: 经常做事务, 在目标方法之前开启事务,执行目标方法, 在目标方法之后提交事务
     */
    @Around(value = "execution(* *..SomeServiceImpl.doFirst(..))")
    public Object myAround(ProceedingJoinPoint pjp) throws Throwable {

        String name = "";
        //获取第一个参数值
        Object args [] = pjp.getArgs();
        if( args!= null && args.length > 1){
              Object arg=  args[0];
              name =(String)arg;
        }

        //实现环绕通知
        Object result = null;
        System.out.println("环绕通知:在目标方法之前,输出时间:"+ new Date());
        //1.目标方法调用
        if( "zhangsan".equals(name)){
            //符合条件,调用目标方法
            result = pjp.proceed(); //method.invoke(); Object result = doFirst();

        }

        System.out.println("环绕通知:在目标方法之后,提交事务");
        //2.在目标方法的前或者后加入功能

        //修改目标方法的执行结果, 影响方法最后的调用结果
        if( result != null){
              result = "Hello AspectJ AOP";
        }

        //返回目标方法的执行结果
        return result;
    }
}

@AfterThrowing异常通知-注解中有throwing属性

​ 在目标方法抛出异常后执行。该注解的throwing属性用于指定所发生的异常类对象。当然,被注解为异常通知的方法可以包含一个参数Throwable,参数名称为throwing指定的名称,表示发生的异常对象。

/**
 *  @Aspect : 是aspectj框架中的注解。
 *     作用:表示当前类是切面类。
 *     切面类:是用来给业务方法增加功能的类,在这个类中有切面的功能代码
 *     位置:在类定义的上面
 */
@Aspect
public class MyAspect {
    /**
     * 异常通知方法的定义格式
     *  1.public
     *  2.没有返回值
     *  3.方法名称自定义
     *  4.方法有个一个Exception, 如果还有是JoinPoint,
     */

    /**
     * @AfterThrowing:异常通知
     *     属性:1. value 切入点表达式
     *          2. throwinng 自定义的变量,表示目标方法抛出的异常对象。
     *             变量名必须和方法的参数名一样
     * 特点:
     *   1. 在目标方法抛出异常时执行的
     *   2. 可以做异常的监控程序, 监控目标方法执行时是不是有异常。
     *      如果有异常,可以发送邮件,短信进行通知
     *
     *  执行就是:
     *   try{
     *       SomeServiceImpl.doSecond(..)
     *   }catch(Exception e){
     *       myAfterThrowing(e);
     *   }
     */
    @AfterThrowing(value = "execution(* *..SomeServiceImpl.doSecond(..))",
            throwing = "ex")
    public void myAfterThrowing(Exception ex) {
        System.out.println("异常通知:方法发生异常时,执行:"+ex.getMessage());
        //发送邮件,短信,通知开发人员
    }

}

@After最终通知

​ 无论目标方法是否抛出异常,该增强均会被执行。

/**
 *  @Aspect : 是aspectj框架中的注解。
 *     作用:表示当前类是切面类。
 *     切面类:是用来给业务方法增加功能的类,在这个类中有切面的功能代码
 *     位置:在类定义的上面
 */
@Aspect
public class MyAspect {
    /**
     * 最终通知方法的定义格式
     *  1.public
     *  2.没有返回值
     *  3.方法名称自定义
     *  4.方法没有参数,  如果还有是JoinPoint,
     */

    /**
     * @After :最终通知
     *    属性: value 切入点表达式
     *    位置: 在方法的上面
     * 特点:
     *  1.总是会执行
     *  2.在目标方法之后执行的
     *
     *  try{
     *      SomeServiceImpl.doThird(..)
     *  }catch(Exception e){
     *
     *  }finally{
     *      myAfter()
     *  }
     *
     */
    @After(value = "execution(* *..SomeServiceImpl.doThird(..))")
    public  void  myAfter(){
        System.out.println("执行最终通知,总是会被执行的代码");
        //一般做资源清除工作的。
     }

}

@Pointcut定义切入点

当较多的通知增强方法使用相同的execution切入点表达式时,编写、维护均较为麻烦。AspectJ提供了@Pointcut注解,用于定义execution切入点表达式。 其用法是,将@Pointcut注解在一个方法之上,以后所有的execution的value属性值均可使用该方法名作为切入点。代表的就是@Pointcut定义的切入点。这个使用@Pointcut注解的方法一般使用private的标识方法,即没有实际作用的方法。

/**
 *  @Aspect : 是aspectj框架中的注解。
 *     作用:表示当前类是切面类。
 *     切面类:是用来给业务方法增加功能的类,在这个类中有切面的功能代码
 *     位置:在类定义的上面
 */
@Aspect
public class MyAspect {


    @After(value = "mypt()")
    public  void  myAfter(){
        System.out.println("执行最终通知,总是会被执行的代码");
        //一般做资源清除工作的。
     }

    @Before(value = "mypt()")
    public  void  myBefore(){
        System.out.println("前置通知,在目标方法之前先执行的");
    }

    /**
     * @Pointcut: 定义和管理切入点, 如果你的项目中有多个切入点表达式是重复的,可以复用的。
     *            可以使用@Pointcut
     *    属性:value 切入点表达式
     *    位置:在自定义的方法上面
     * 特点:
     *   当使用@Pointcut定义在一个方法的上面 ,此时这个方法的名称就是切入点表达式的别名。
     *   其它的通知中,value属性就可以使用这个方法名称,代替切入点表达式了
     */
    @Pointcut(value = "execution(* *..SomeServiceImpl.doThird(..))" )
    private void mypt(){
        //无需代码,
    }

}

没有接口是cglib代理

//目标类
public class SomeServiceImpl {
    public void doThird() {
        System.out.println("执行业务方法doThird()");
    }
}
@Test
public void test01(){
    String config="applicationContext.xml";
    ApplicationContext ctx = new ClassPathXmlApplicationContext(config);
    //从容器中获取目标对象
    SomeServiceImpl proxy = (SomeServiceImpl) ctx.getBean("someService");
    /**
     * 目标类没有接口,使用cglib动态代理, spring框架会自动应用cglib
     * com.bjpowernode.ba07.SomeServiceImpl$$EnhancerBySpringCGLIB$$575c8b90
     */
    System.out.println("proxy:"+proxy.getClass().getName());
    //通过代理的对象执行方法,实现目标方法执行时,增强了功能
    proxy.doThird();
}

有接口也可使用cglib代理

<!--
       如果你期望目标类有接口,使用cglib代理
       proxy-target-class="true":告诉框架,要使用cglib动态代理
    -->
    <aop:aspectj-autoproxy proxy-target-class="true"/>

4 Spring集成MyBatis

​ 将MyBatis与Spring进行整合,主要解决的问题就是将SqlSessionFactory对象交由Spring来管理。所以,该整合,只需要将SqlSessionFactory的对象生成器SqlSessionFactoryBean注册在Spring容器中,再将其注入给Dao的实现类即可完成整合。

​ 实现Spring与MyBatis的整合常用的方式:扫描的Mapper动态代理 。

4.1 步骤

1.新建maven项目
2.加入maven的依赖
  1)spring依赖
  2)mybatis依赖
  3)mysql驱动
  4)spring的事务的依赖
  5)mybatis和spring集成的依赖: mybatis官方体用的,用来在spring项目中创建mybatis
     的SqlSesissonFactory,dao对象的
3.创建实体类
4.创建dao接口和mapper文件
5.创建mybatis主配置文件
6.创建Service接口和实现类,属性是dao。
7.创建spring的配置文件:声明mybatis的对象交给spring创建
 1)数据源DataSource
 2)SqlSessionFactory
 3) Dao对象
 4)声明自定义的service

8.创建测试类,获取Service对象,通过service调用dao完成数据库的访问

4.2 maven依赖pom.xml

<dependencies>
    <!--单元测试-->
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
    <!--spring核心ioc-->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>5.2.5.RELEASE</version>
    </dependency>
    <!--做spring事务用到的-->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-tx</artifactId>
      <version>5.2.5.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-jdbc</artifactId>
      <version>5.2.5.RELEASE</version>
    </dependency>
    <!--mybatis依赖-->
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>3.5.1</version>
    </dependency>
    <!--mybatis和spring集成的依赖-->
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis-spring</artifactId>
      <version>1.3.1</version>
    </dependency>
    <!--mysql驱动-->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.9</version>
    </dependency>
    <!--阿里公司的数据库连接池-->
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid</artifactId>
      <version>1.1.12</version>
    </dependency>
  </dependencies>

  <build>
    <!--目的是把src/main/java目录中的xml文件包含到输出结果中。输出到classes目录中-->
    <resources>
      <resource>
        <directory>src/main/java</directory><!--所在的目录-->
        <includes><!--包括目录下的.properties,.xml 文件都会扫描到-->
          <include>**/*.properties</include>
          <include>**/*.xml</include>
        </includes>
        <filtering>false</filtering>
      </resource>
    </resources>
    <!--指定jdk的版本-->
    <plugins>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.1</version>
        <configuration>
          <source>1.8</source>
          <target>1.8</target>
        </configuration>
      </plugin>
    </plugins>
  </build>

4.3 定义MyBatis主配置文件

​ 在src下定义MyBatis的主配置文件,命名为mybatis.xml。 这里有两点需要注意:

(1)主配置文件中不再需要数据源的配置了。因为数据源要交给Spring容器来管理了。

(2)这里对mapper映射文件的注册,使用<package/>标签,即只需给出mapper映射文件所在的包即可。因为mapper的名称与Dao接口名相同,可以使用这种简单注册方式。这种方式的好处是,若有多个映射文件,这里的配置也是不用改变的。当然,也可使用原来的<resource/>标签方式。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

    <!--settings:控制mybatis全局行为-->
    <settings>
        <!--设置mybatis输出日志-->
        <setting name="logImpl" value="STDOUT_LOGGING"/>
    </settings>

    <!--设置别名-->
    <typeAliases>
        <!--name:实体类所在的包名
            表示com.bjpowernode.domain包中的列名就是别名
            你可以使用Student表示com.bjpowenrode.domain.Student
        -->
        <package name="com.bjpowernode.domain"/>
    </typeAliases>


    <!-- sql mapper(sql映射文件)的位置-->
    <mappers>
        <!--
          name:是包名, 这个包中的所有mapper.xml一次都能加载
        -->
        <package name="com.bjpowernode.dao"/>
    </mappers>
</configuration>

4.4 Spring配置文件

4.4.1 数据源的配置

Druid数据源DruidDataSource

​ Druid是阿里的开源数据库连接池。是Java语言中最好的数据库连接池。Druid能够提供强大的监控和扩展功能。Druid与其他数据库连接池的最大区别是提供数据库的 。

​ 官网:https://github.com/alibaba/druid

​ 使用地址:https://github.com/alibaba/druid/wiki/常见问题

4.4.2 从属性文件读取数据库连接信息

jdbc.properties:

jdbc.url=jdbc:mysql://localhost:3306/springdb
jdbc.username=root
jdbc.passwd=123456
jdbc.max=30

​ Spring配置文件从属性文件中读取数据时,需要在<property/>的value属性中使用${ },将在属性文件中定义的key括起来,以引用指定属性的值。

<!--声明数据源DataSource, 作用是连接数据库的-->
<bean id="myDataSource" class="com.alibaba.druid.pool.DruidDataSource"
      init-method="init" destroy-method="close">
    <!--set注入给DruidDataSource提供连接数据库信息 -->
    <!--    使用属性配置文件中的数据,语法 ${key} -->
    <property name="url" value="${jdbc.url}" /><!--setUrl()-->
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.passwd}" />
    <property name="maxActive" value="${jdbc.max}" />
</bean>

​ 该属性文件若要被Spring配置文件读取,其必须在配置文件中进行注册。使用<context>标签。 <context:property-placeholder/>方式

​ 该方式要求在Spring配置文件头部加入spring-context.xsd约束文件 <context:property-placeholder/>标签中有一个属性location,用于指定属性文件的位置。

<context:property-placeholder location="classpath:jdbc.properties" />
4.4.3 注册SqlSessionFactoryBean
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <!--set注入,把数据库连接池付给了dataSource属性-->
        <property name="dataSource" ref="myDataSource" />
        <!--mybatis主配置文件的位置
           configLocation属性是Resource类型,读取配置文件
           它的赋值,使用value,指定文件的路径,使用classpath:表示文件的位置
        -->
        <property name="configLocation" value="classpath:mybatis.xml" />
    </bean>
4.4.4 定义Mapper扫描配置器MapperScannerConfigurer

​ Mapper扫描配置器MapperScannerConfigurer会自动生成指定的基本包中mapper的代理对象。该Bean无需设置id属性。basePackage使用分号或逗号设置多个包。

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <!--指定SqlSessionFactory对象的id-->
    <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
    <!--指定包名, 包名是dao接口所在的包名。
    MapperScannerConfigurer会扫描这个包中的所有接口,把每个接口都执行
    一次getMapper()方法,得到每个接口的dao对象。
    创建好的dao对象放入到spring的容器中的。 dao对象的默认名称是 接口名首字母小写
    -->
    <property name="basePackage" value="com.bjpowernode.dao"/>
</bean>
4.4.5 完整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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

    <!--
       把数据库的配置信息,写在一个独立的文件,编译修改数据库的配置内容
       spring知道jdbc.properties文件的位置
    -->
    <context:property-placeholder location="classpath:jdbc.properties" />

    <!--声明数据源DataSource, 作用是连接数据库的-->
    <bean id="myDataSource" class="com.alibaba.druid.pool.DruidDataSource"
          init-method="init" destroy-method="close">
        <!--set注入给DruidDataSource提供连接数据库信息 -->
        <!--    使用属性配置文件中的数据,语法 ${key} -->
        <property name="url" value="${jdbc.url}" /><!--setUrl()-->
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.passwd}" />
        <property name="maxActive" value="${jdbc.max}" />
    </bean>

    <!--声明的是mybatis中提供的SqlSessionFactoryBean类,这个类内部创建SqlSessionFactory的
        SqlSessionFactory  sqlSessionFactory = new ..
    -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <!--set注入,把数据库连接池付给了dataSource属性-->
        <property name="dataSource" ref="myDataSource" />
        <!--mybatis主配置文件的位置
           configLocation属性是Resource类型,读取配置文件
           它的赋值,使用value,指定文件的路径,使用classpath:表示文件的位置
        -->
        <property name="configLocation" value="classpath:mybatis.xml" />
    </bean>

    <!--创建dao对象,使用SqlSession的getMapper(StudentDao.class)
        MapperScannerConfigurer:在内部调用getMapper()生成每个dao接口的代理对象。

    -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <!--指定SqlSessionFactory对象的id-->
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
        <!--指定包名, 包名是dao接口所在的包名。
            MapperScannerConfigurer会扫描这个包中的所有接口,把每个接口都执行
            一次getMapper()方法,得到每个接口的dao对象。
            创建好的dao对象放入到spring的容器中的。 dao对象的默认名称是 接口名首字母小写
        -->
        <property name="basePackage" value="com.bjpowernode.dao"/>
    </bean>

    <!--声明service-->
    <bean id="studentService" class="com.bjpowernode.service.impl.StudentServiceImpl">
        <property name="studentDao" ref="studentDao" />
    </bean>

</beans>

5 Spring事务

5.1 Spring的事务管理

​ 事务原本是数据库中的概念,在Dao层。但一般情况下,需要将事务提升到业务层,即Service层。这样做是为了能够使用事务的特性来管理具体的业务。

​ 在Spring中通常可以通过以下两种方式来实现对事务的管理:

​ (1)使用Spring的事务注解管理事务

​ (2)使用AspectJ的AOP配置管理事务

5.2 Spring事务管理API

​ Spring的事务管理,主要用到两个事务相关的接口。

5.2.1 事务管理器接口

​ 事务管理器是PlatformTransactionManager接口对象。其主要用于完成事务的提交、回滚,及获取事务的状态信息。

常用的两个实现类

​ PlatformTransactionManager接口有两个常用的实现类:

​ ➢ DataSourceTransactionManager:使用JDBC或MyBatis进行数据库操作时使用。

​ ➢ HibernateTransactionManager:使用Hibernate进行持久化数据时使用。

Spring的回滚方式

​ Spring事务的默认回滚方式是:发生运行时异常和error时回滚,发生受查(编译)异常时提交。不过,对于受查异常,程序员也可以手工设置其回滚方式。

5.2.2 事务定义接口

​ 事务定义接口TransactionDefinition中定义了事务描述相关的三类常量:事务隔离级别、事务传播行为、事务默认超时时限,及对它们的操作。

定义了五个事务隔离级别常量

这些常量均是以ISOLATION_开头。即形如ISOLATION_XXX。

➢ DEFAULT:采用DB默认的事务隔离级别。MySql的默认为REPEATABLE_READ; Oracle默认为 READ_COMMITTED。

➢ READ_UNCOMMITTED:读未提交。未解决任何并发问题。

➢ READ_COMMITTED:读已提交。解决脏读,存在不可重复读与幻读。

➢ REPEATABLE_READ:可重复读。解决脏读、不可重复读,存在幻读

➢ SERIALIZABLE:串行化。不存在并发问题。

定义了七个事务传播行为

​ 所谓事务传播行为是指,处于不同事务中的方法在相互调用时,执行期间事务的维护情况。如,A事务中的方法doSome()调用B事务中的方法doOther(),在调用执行期间事务的维护情况,就称为事务传播行为。事务传播行为是加在方法上的。

​ 事务传播行为常量都是以PROPAGATION_ 开头,形如PROPAGATION_XXX。

PROPAGATION_REQUIRED 
PROPAGATION_REQUIRES_NEW 
PROPAGATION_SUPPORTS  

PROPAGATION_MANDATORY 
PROPAGATION_NESTED 
PROPAGATION_NEVER 
PROPAGATION_NOT_SUPPORTED 
  1. PROPAGATION_REQUIRED

​ 指定的方法必须在事务内执行。若当前存在事务,就加入到当前事务中;若当前没有事务,则创建一个新事务。这种传播行为是最常见的选择,也是Spring默认的事务传播行为。

  1. PROPAGATION_SUPPORTS

    ​ 指定的方法支持当前事务,但若当前没有事务,也可以以非事务方式执行。

  2. PROPAGATION_REQUIRES_NEW

    ​ 总是新建一个事务,若当前存在事务,就将当前事务挂起,直到新事务执行完毕。

定义了默认事务超时时限

​ 常量TIMEOUT_DEFAULT定义了事务底层默认的超时时限,sql语句的执行时长。

​ 注意,事务的超时时限起作用的条件比较多,且超时的时间计算点较复杂。所以,该值一般就使用默认值即可。

5.3 使用Spring的事务注解管理事务

​ 通过@Transactional注解方式,可将事务织入到相应public方法中,实现事务管理。

​ @Transactional的所有可选属性如下所示:

➢ propagation:用于设置事务传播属性。该属性类型为Propagation枚举,默认值为Propagation.REQUIRED。 

➢ isolation:用于设置事务的隔离级别。该属性类型为Isolation枚举,默认值为Isolation.DEFAULT。 

➢ readOnly:用于设置该方法对数据库的操作是否是只读的。该属性为boolean,默认值为false。 

➢ timeout:用于设置本操作与数据库连接的超时时限。单位为秒,类型为int,默认值为-1,即没有时限。 

➢ rollbackFor:指定需要回滚的异常类。类型为Class[],默认值为空数组。当然,若只有一个异常类时,可以不使用数组。 

➢ rollbackForClassName:指定需要回滚的异常类类名。类型为String[],默认值为空数组。当然,若只有一个异常类时,可以不使用数组。 

➢ noRollbackFor:指定不需要回滚的异常类。类型为Class[],默认值为空数组。当然,若只有一个异常类时,可以不使用数组。 

➢ noRollbackForClassName:指定不需要回滚的异常类类名。类型为String[],默认值为空数组。当然,若只有一个异常类时,可以不使用数组。  

需要注意的是,@Transactional若用在方法上,只能用于public方法上。对于其他非public方法,如果加上了注解@Transactional,虽然Spring不会报错,但不会将指定事务织入到该方法中。因为Spring会忽略掉所有非public方法上的@Transaction注解。 

若@Transaction注解在类上,则表示该类上所有的方法均将在执行时织入事务。 

spring配置:

<!--使用spring的事务处理-->
<!--1. 声明事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <!--连接的数据库, 指定数据源-->
    <property name="dataSource" ref="myDataSource" />
</bean>

<!--2. 开启事务注解驱动,告诉spring使用注解管理事务,创建代理对象
       transaction-manager:事务管理器对象的id
-->
<tx:annotation-driven transaction-manager="transactionManager" />

service层:

public class BuyGoodsServiceImpl implements BuyGoodsService {

    private SaleDao saleDao;
    private GoodsDao goodsDao;

    /**
     *
     * rollbackFor:表示发生指定的异常一定回滚.
     *   处理逻辑是:
     *     1) spring框架会首先检查方法抛出的异常是不是在rollbackFor的属性值中
     *         如果异常在rollbackFor列表中,不管是什么类型的异常,一定回滚。
     *     2) 如果你的抛出的异常不在rollbackFor列表中,spring会判断异常是不是RuntimeException,
     *         如果是一定回滚。
     *
     */
   /* @Transactional(
            propagation = Propagation.REQUIRED,
            isolation = Isolation.DEFAULT,
            readOnly = false,
            rollbackFor = {
                    NullPointerException.class,  NotEnoughException.class
            }
    )*/

    //使用的是事务控制的默认值, 默认的传播行为是REQUIRED,默认的隔离级别DEFAULT
    //默认抛出运行时异常,回滚事务。
    @Transactional
    @Override
    public void buy(Integer goodsId, Integer nums) {
        System.out.println("=====buy方法的开始====");
        //记录销售信息,向sale表添加记录
        Sale sale  = new Sale();
        sale.setGid(goodsId);
        sale.setNums(nums);
        saleDao.insertSale(sale);

        //更新库存
        Goods goods  = goodsDao.selectGoods(goodsId);
        if( goods == null){
            //商品不存在
            throw  new  NullPointerException("编号是:"+goodsId+",商品不存在");
        } else if( goods.getAmount() < nums){
            //商品库存不足
            throw new NotEnoughException("编号是:"+goodsId+",商品库存不足");
        }
        //修改库存了
        Goods buyGoods = new Goods();
        buyGoods.setId( goodsId);
        buyGoods.setAmount(nums);
        goodsDao.updateGoods(buyGoods);
        System.out.println("=====buy方法的完成====");
    }


    public void setSaleDao(SaleDao saleDao) {
        this.saleDao = saleDao;
    }

    public void setGoodsDao(GoodsDao goodsDao) {
        this.goodsDao = goodsDao;
    }
}

5.4 使用AspectJ的AOP配置管理事务

<!--声明式事务处理:和源代码完全分离的-->
<!--1.声明事务管理器对象-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="myDataSource" />
</bean>

<!--2.声明业务方法它的事务属性(隔离级别,传播行为,超时时间)
      id:自定义名称,表示 <tx:advice> 和 </tx:advice>之间的配置内容的
      transaction-manager:事务管理器对象的id
-->
<tx:advice id="myAdvice" transaction-manager="transactionManager">
    <!--tx:attributes:配置事务属性-->
    <tx:attributes>
        <!--tx:method:给具体的方法配置事务属性,method可以有多个,分别给不同的方法设置事务属性
            name:方法名称,1)完整的方法名称,不带有包和类。
                          2)方法可以使用通配符,* 表示任意字符
            propagation:传播行为,枚举值
            isolation:隔离级别
            rollback-for:你指定的异常类名,全限定类名。 发生异常一定回滚
        -->
        <tx:method name="buy" propagation="REQUIRED" isolation="DEFAULT"
                   rollback-		        for="java.lang.NullPointerException,com.bjpowernode.excep.NotEnoughException"/>

        <!--使用通配符,指定很多的方法-->
        <tx:method name="add*" propagation="REQUIRES_NEW" />
        <!--指定修改方法-->
        <tx:method name="modify*" />
        <!--删除方法-->
        <tx:method name="remove*" />
        <!--查询方法,query,search,find-->
        <tx:method name="*" propagation="SUPPORTS" read-only="true" />
    </tx:attributes>
</tx:advice>

<!--配置aop-->
<aop:config>
    <!--配置切入点表达式:指定哪些包中类,要使用事务
        id:切入点表达式的名称,唯一值
        expression:切入点表达式,指定哪些类要使用事务,aspectj会创建代理对象

        com.bjpowernode.service
        com.crm.service
        com.service
    -->
    <aop:pointcut id="servicePt" expression="execution(* *..service..*.*(..))"/>

    <!--配置增强器:关联adivce和pointcut
       advice-ref:通知,上面tx:advice哪里的配置
       pointcut-ref:切入点表达式的id
    -->
    <aop:advisor advice-ref="myAdvice" pointcut-ref="servicePt" />
</aop:config>

6 Spring与Web

6.1 Web项目使用Spring的问题

​ 在Web项目中使用Spring框架,首先要解决在web层(这里指Servlet)中获取到Spring容器的问题。只要在web层获取到了Spring容器,便可从容器中获取到Service对象。

<!-- servlet依赖 --> 
<dependency>  
    <groupId>javax.servlet</groupId>  
    <artifactId>javax.servlet-api</artifactId>  
    <version>3.1.0</version>  
    <scope>provided</scope> 
</dependency>  
<!-- jsp依赖 --> 
<dependency>  
    <groupId>javax.servlet.jsp</groupId>  
    <artifactId>jsp-api</artifactId>  
    <version>2.2.1-b03</version>  
    <scope>provided</scope> 
</dependency> 
public class RegisterServlet extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        //创建spring的容器对象
        String config= "spring.xml";
        ApplicationContext ctx = new ClassPathXmlApplicationContext(config);

    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    }
}

​ 当表单提交,跳转到success.jsp后,多刷新几次页面,查看后台输出,发现每刷新一次页面,就new出一个新的Spring容器。即,每提交一次请求,就会创建一个新的Spring容器。对于一个应用来说,只需要一个Spring容器即可。所以,将Spring容器的创建语句放在Servlet的doGet()或doPost()方法中是有问题的。

​ 此时,可以考虑,将Spring容器的创建放在Servlet进行初始化时进行,即执行init()方法时执行。并且,Servlet还是单例多线程的,即一个业务只有一个Servlet实例,所有执行该业务的用户执行的都是这一个Servlet实例。这样,Spring容器就具有了唯一性了。 但是,Servlet是一个业务一个Servlet实例,即LoginServlet只有一个,但还会有StudentServlet、TeacherServlet等。每个业务都会有一个Servlet,都会执行自己的init()方法,也就都会创建一个Spring容器了。这样一来,Spring容器就又不唯一了。

6.2 使用Spring的监听器ContextLoaderListener

​ 对于Web应用来说,ServletContext对象是唯一的,一个Web应用,只有一个ServletContext对象,该对象是在Web应用装载时初始化的。若将Spring容器的创建时机,放在ServletContext初始化时,就可以保证Spring容器的创建只会执行一次,也就保证了Spring容器在整个应用中的唯一性。

​ 当Spring容器创建好后,在整个应用的生命周期过程中,Spring容器应该是随时可以被访问的。即,Spring容器应具有全局性。而放入ServletContext对象的属性,就具有应用的全局性。所以,将创建好的Spring容器,以属性的形式放入到ServletContext的空间中,就保证了Spring容器的全局性。

​ 上述的这些工作,已经被封装在了如下的Spring的Jar包的相关API中: spring-web-5.2.5.RELEASE

6.2.1 maven依赖pom.xml
<dependency>  
    <groupId>org.springframework</groupId>  
    <artifactId>spring-web</artifactId>  
    <version>5.2.5.RELEASE</version>  
</dependency> 
6.2.2 注册监听器ContextLoaderListener

​ 若要在ServletContext初始化时创建Spring容器,就需要使用监听器接口ServletContextListener对ServletContext进行监听。在web.xml中注册该监听器。

<listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

​ Spring为该监听器接口定义了一个实现类ContextLoaderListener,完成了两个很重要的工作:创建容器对象,并将容器对象放入到了ServletContext的空间中。 打开ContextLoaderListener的源码。看到一共四个方法,两个是构造方法,一个初始化方法,一个销毁方法。

​ 所以,在这四个方法中较重要的方法应该就是contextInitialized(),context初始化方法。

6.2.3 指定Spring配置文件的位置<context-param>

​ ContextLoaderListener在对Spring容器进行创建时,需要加载Spring配置文件。其默认的Spring配置文件位置与名称为:WEB-INF/applicationContext.xml。但,一般会将该配置文件放置于项目的classpath下,即src下,所以需要在web.xml中对Spring配置文件的位置及名称进行指定。

<!--注册监听器ContextLoaderListener
        监听器被创建对象后,会读取/WEB-INF/spring.xml
        为什么要读取文件:因为在监听器中要创建ApplicationContext对象,需要加载配置文件。
        /WEB-INF/applicationContext.xml就是监听器默认读取的spring配置文件路径

        可以修改默认的文件位置,使用context-param重新指定文件的位置

        配置监听器:目的是创建容器对象,创建了容器对象, 就能把spring.xml配置文件中的所有对象都创建好。
        用户发起请求就可以直接使用对象了。
    -->
    <context-param>
        <!-- contextConfigLocation:表示配置文件的路径  -->
        <param-name>contextConfigLocation</param-name>
        <!--自定义配置文件的路径-->
        <param-value>classpath:spring.xml</param-value>
    </context-param>
6.2.4 获取Spring容器对象

直接从ServletContext中获取

​ 从对监听器ContextLoaderListener的源码分析可知,容器对象在ServletContext的中存放的key为WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE。所以,可以直接通过ServletContext的getAttribute()方法,按照指定的key将容器对象获取到。

//获取ServletContext中的容器对象,创建好的容器对象,拿来就用
String key =  WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE;
Object attr  = getServletContext().getAttribute(key);
if( attr != null){
    ctx = (WebApplicationContext)attr;
}

通过WebApplicationContextUtils获取

​ 工具类WebApplicationContextUtils有一个方法专门用于从ServletContext中获取Spring容器对象:getRequiredWebApplicationContext(ServletContext sc)

//使用框架中的方法,获取容器对象
WebApplicationContext ctx = null;
ServletContext sc = getServletContext();
ctx = WebApplicationContextUtils.getRequiredWebApplicationContext(sc);

​ 以上两种方式,无论使用哪种获取容器对象,刷新success页面后,可看到代码中使用的Spring容器均为同一个对象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值