Spring框架

官方文档

1. Spring介绍

Spring框架是一个开放源代码的J2EE应用程序框架,由Rod Johnson发起,是针对bean生命周期进行管理的轻量级容器(lightweight container)。 Spring解决了开发者在J2EE开发中遇到的许多常见的问题,提供了功能强大I0CAOPWeb MVC等功能。Spring可以单独应用于构筑应用程序,也可以和Struts、Webwork、Tapestry等众多Web框架组合使用,并且可以与 Swing等桌面应用程序AP组合。因此,Spring不仅仅能应用于J2EE应用程序之中,也可以应用于桌面应用程序以及小应用程序之中。Spring框架主要由七部分组成,分别是SpringCore、SpringAOP、SpringORM、Spring DAO、SpringContext、 Spring Web和 SpringWebMVC.

总结:

  • bean生命周期管理:提供了java对象的创建、销毁、使用等生命周期管理功能。
  • 轻量级: 使用简单,配置容易
  • 容器: Spring可以管理对象,创建好对象,放入Spring容器中,容器负责管理这些对象生命周期
    Spring框架.png

自底向上的内部结构:

  • Spring的集成测试
  • Spring的核心容器
  • Spring的AOP技术
  • Spring的数据访问技术(和mybatis整合后,不再学习,但是事务管理必须由Spring提供)
  • Spring的Web开发技术(也就是SpringMVC)

2.搭建环境

只是为了测试Spring的简单功能,并没有实际的业务逻辑代码。等后续全部整合以后再进行业务逻辑的编写。

2.1 创建普通的Maven项目

1.创建Maven项目.jpg

2.2 引入依赖,Spring组件有很多,这里只为了演示核心容器,所以只导入核心容器相关的依赖

由上面的结构图可以看到,核心容器主要有core,beans,context,expression四个模块。所以我们只需要导入这四个依赖即可。但是通过图片可以看到,spring-context依赖中还关联了spring-aopspring-beans等依赖,所以我们不再需要单独导入其他依赖。
2.引入依赖.jpg

    <dependencies>
<!--        只需要导入spring-context依赖即可,其他依赖maven会自动导入-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.3.33</version>
        </dependency>
    </dependencies>

2.3 spring配置文件

spring的功能大多数都依赖配置文件来完成。文件名称无限制,通常命名为:spring.xml,spring-context.xml等。最常用的是applicationContext.xml

在resources目录下创建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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 后续再配置bean -->

</beans>

tip:主要关注头文件,一般创建xml方式会有Spring配置的选项,如果没有可以直接ctrl+c/v的,只需要有个印象就可以,一定不能写错。

2.4 测试使用

这里只是测试Spring的核心容器,所以只需要测试最简单的bean的创建和注入即可。

2.4.1 首先,创建一个简单的测试类

package org.demo;

public class MyClass {
    public void test(){
        System.out.println("测试spring运行环境....");
    }
}

2.4.2 使用spring容器管理bean(即使用spring来创建对象,通过编写applicationContext.xml来配置bean)

<?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标签,就是spring管理的一个类-->
<!--    id bean的唯一标识,也就是创建该类后的对象名,后续通过id就可以从容器中获得该对象-->
<!--    class  类的全限定名称-->
    <bean id="myClass" class="org.demo.MyClass"/>

</beans>

2.4.3 测试类中创建工厂Bean,由工厂加载配置文件,通过配置文件创建容器,从容器中获得对象

package org.demo;

import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class MyClassTest {
   @Test
   public void testClass(){
       //配置文件路径
       String path= "applicationContext.xml";
       //通过配置文件,创建容器
       ApplicationContext context = new ClassPathXmlApplicationContext(path);
       //通过容器获得Bean对象(通过配置文件中bean标签的id)
//        MyClass myClass = (MyClass) context.getBean("myClass");
       //这种方法也可以,不需要强转
       MyClass myClass = context.getBean("myClass", MyClass.class);
       //调用对象中的测试方法
       myClass.test();
   }
}

3. IOC(inversion of control,控制反转) 和 DI (Dependency injection,依赖注入)

Spring的核心功能之一就是IOC和DI。IOC是一种设计思想,DI是IOC的一种具体实现。

  • IOC(控制反转): 创建对象控制权从程序员转移到Spring容器。也就是说,由Spring容器来创建对象,而不是程序员自己创建对象。(就像上面,是通过容器获取Bean对象)

简单来说,就是由我们自己去new对象,变为由Spring容器去创建对象。

  • DI(依赖注入): 依赖关系由Spring容器注入到对象中。也就是说,Spring容器在创建对象时,会自动将依赖的对象注入到对象中

接下来,通过简单编写代码来理解IOC和DI。

3.1 IOC

3.1.1 编写dao接口和实现类

因为仅作演示,所以不操作数据库,用输出代表方法的执行。

package org.demo.dao;



public interface BookDao {
    void getAllBook();
}

实现类

package org.demo.dao.impl;

import org.demo.dao.BookDao;


public class BookDaoImpl implements BookDao {

    @Override
    public void getAllBook() {
        System.out.println("访问数据访问层");
    }
}

3.1.2 编写service接口和实现类

package org.demo.service;


public interface BookService {

    void getAllBook();

}

实现类

package org.demo.service.impl;

import org.demo.dao.BookDao;
import org.demo.dao.impl.BookDaoImpl;
import org.demo.service.BookService;


public class BookServiceImpl implements BookService {

//    以前我们需要在这里创建dao对象
    private BookDao bookDao = new BookDaoImpl();

    @Override
    public void getAllBook() {
        System.out.println("访问业务层");
        bookDao.getAllBook();
    }
}

3.1.3 编写配置文件,注册业务对象和dao对象

    <bean id="bookService" class="org.demo.service.impl.BookServiceImpl"/>

3.1.4 测试类

    @Test
    public void testServiceClass(){
        //配置文件路径
        String path= "applicationContext.xml";
        //通过配置文件,创建容器
        ApplicationContext context = new ClassPathXmlApplicationContext(path);
        //通过容器获得Bean对象(通过配置文件中bean标签的id)
        BookService service = context.getBean("bookService", BookService.class);
        //调用对象中的测试方法
        service.getAllBook();
    }

输出结果:

访问业务层
访问数据访问层

可以看到,业务层调用了dao层的对象,并且业务层对象并不是我们自己创建的,而是由Spring容器创建的,这就是IOC的作用。

3.1.5 修改业务层实现类和注册dao对象,让dao由spring容器创建

那现在还有个新的问题,我们使用的dao对象,是我们自己手动new创建的,并不是从容器中拿到的,那我们对业务层实现类就行修改,我想让dao也能由spring容器去创建。

package org.demo.service.impl;

import org.demo.dao.BookDao;
import org.demo.dao.impl.BookDaoImpl;
import org.demo.service.BookService;


public class BookServiceImpl implements BookService {

//    以前我们需要在这里创建dao对象
//    private BookDao bookDao = new BookDaoImpl();
//      现在我们不需要去手动创建对象,只需要声明属性,spring会自动创建对象并赋值
    private BookDao bookDao;

    @Override
    public void getAllBook() {
        System.out.println("访问业务层");
        bookDao.getAllBook();
    }
}
<?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标签,就是spring管理的一个类-->
<!--    id bean的唯一标识,也就是创建该类后的对象名,后续通过id就可以从容器中获得该对象-->
<!--    class  类的全限定名称-->
    <bean id="myClass" class="org.demo.MyClass"/>

<!--    注册业务层对象-->
    <bean id="bookService" class="org.demo.service.impl.BookServiceImpl"/>

<!--    注册dao层对象-->
    <bean id="bookDao" class="org.demo.dao.impl.BookDaoImpl"/>
</beans>

接下来我们再测试执行一下,看看dao对象有没有由容器管理

测试输出

java.lang.NullPointerException: Cannot invoke "org.demo.dao.BookDao.getAllBook()" because "this.bookDao" is null

3.1.6 注入dao对象

看到报错了,说我们的dao对象是空的,这是为什么,不是说spring会帮助我们创建对象,并进行赋值吗?难道说我们的dao对象并没有注册到容器中吗?

那我们接下来修改一下测试类,测试一下dao对象是否注册到容器中了。

    @Test
    public void testServiceClass(){
        //配置文件路径
        String path= "applicationContext.xml";
        //通过配置文件,创建容器
        ApplicationContext context = new ClassPathXmlApplicationContext(path);
//        //通过容器获得Bean对象(通过配置文件中bean标签的id)
//        BookService service = context.getBean("bookService", BookService.class);
//        //调用对象中的测试方法
//        service.getAllBook();
        BookDao bookDao = context.getBean("bookDao", BookDao.class);
        bookDao.getAllBook();
    }

输出

访问数据访问层

正常输出了呀,那为什么使用业务层再去调用,会出现dao对象为空的情况呢?那我们接下来去官方文档查看一下,看看有没有什么其他的配置。

其实,到这个时候,我们就已经到了下一步,DI(依赖注入)的配置了。我们的对象已经由容器创建了,就差DI对dao属性自动赋值

要实现依赖注入很简单,只需要给出属性的setter方法,就可以实现赋值,现在再修改一次业务层,为dao属性提供setter方法。

package org.demo.service.impl;

import org.demo.dao.BookDao;
import org.demo.dao.impl.BookDaoImpl;
import org.demo.service.BookService;


public class BookServiceImpl implements BookService {

//    以前我们需要在这里创建dao对象
//    private BookDao bookDao = new BookDaoImpl();
//      现在我们不需要去手动创建对象,只需要声明属性,spring会自动创建对象并赋值
    private BookDao bookDao;

    public void setBookDao(BookDao bookDao) {
        this.bookDao = bookDao;
    }

    @Override
    public void getAllBook() {
        System.out.println("访问业务层");
        bookDao.getAllBook();
    }
}

设置完成setter方法后,注意,还没有结束。
bean创建dao对象.PNG

我们查看官网文档后发现,给出的例子中,业务层对象,在bean中声明了property标签,并设置了ref属性,来指定(引用)dao对象。

那我们直接照猫画虎,也为我们的业务层对象,添加property标签,并使用ref来引用注入我们的dao对象。

<?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标签,就是spring管理的一个类-->
<!--    id bean的唯一标识,也就是创建该类后的对象名,后续通过id就可以从容器中获得该对象-->
<!--    class  类的全限定名称-->
    <bean id="myClass" class="org.demo.MyClass"/>

<!--    注册业务层对象-->
    <bean id="bookService" class="org.demo.service.impl.BookServiceImpl">
<!--        注入数据层对象-->
<!--        name 是注入的属性名-->
<!--        ref 给引用类型值,也就是注入的bean的id-->
<!--        value 给基本类型值  如 <property name="age" value="20"/>-->
        <property name="bookDao" ref="bookDao"/>
    </bean>

<!--    注册dao层对象-->
    <bean id="bookDao" class="org.demo.dao.impl.BookDaoImpl"/>
</beans>

接下来,我们再去测试类执行代码

   @Test
    public void testServiceClass(){
        //配置文件路径
        String path= "applicationContext.xml";
        //通过配置文件,创建容器
        ApplicationContext context = new ClassPathXmlApplicationContext(path);
        //通过容器获得Bean对象(通过配置文件中bean标签的id)
        BookService service = context.getBean("bookService", BookService.class);
        //调用对象中的测试方法
        service.getAllBook();
    }

输出

访问业务层
访问数据访问层

可以看到,业务层调用了dao层的对象,并且业务层对象并不是我们自己创建的,而是由Spring容器创建的,这就是IOC的作用。并且,我们还实现了DI(依赖注入)的配置,dao对象也由Spring容器创建并注入到业务层对象中。

3.2 DI

DI(依赖注入)是IOC的一种具体实现。依赖注入是指当一个对象需要另一个对象来协助完成其工作时,通过容器(Spring容器)来提供这个依赖对象。也就是属性赋值。

上面,我们简单演示了一下DI,通过setter方法来注入dao对象。现在我们详细说一下DI的方法。

  • setters方法注入
  • 构造方法注入
  • 自动赋值注入

3.2.1 setters方法注入

最常用的注入方式,通过setter方法注入。接下来我们使用setter方法演示一下注入不同类型。

  1. 创建测试实体类,并设置getter/setter方法,方便注入。
package org.demo.entity;

import java.util.*;

public class TestDI {
    private Integer age;
    private String name;
    private Date birthday;
    private String[] hobby;
    private List<String> bookList;
    private Set<String> phones;
    private Map<String, String> map;

    public Integer getAge() {
        return age;
    }

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

    public String getName() {
        return name;
    }

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

    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    public String[] getHobby() {
        return hobby;
    }

    public void setHobby(String[] hobby) {
        this.hobby = hobby;
    }

    public List<String> getBookList() {
        return bookList;
    }

    public void setBookList(List<String> bookList) {
        this.bookList = bookList;
    }

    public Set<String> getPhones() {
        return phones;
    }

    public void setPhones(Set<String> phones) {
        this.phones = phones;
    }

    public Map<String, String> getMap() {
        return map;
    }

    public void setMap(Map<String, String> map) {
        this.map = map;
    }

    @Override
    public String toString() {
        return "TestDI{" +
                "age=" + age +
                ", name='" + name + '\'' +
                ", birthday=" + birthday +
                ", hobby=" + Arrays.toString(hobby) +
                ", bookList=" + bookList +
                ", phones=" + phones +
                ", map=" + map +
                '}';
    }
}
  1. 修改配置文件,注册对象并添加属性
    <bean id="test" class="org.demo.entity.TestDI">
<!--        基本数据类型-->
        <property name="age" value="20"/>
        <property name="name" value="张三"/>
        <property name="birthday" value="2004/5/6"/>

<!--        引用数据类型-->
<!--        数组类型-->
        <property name="hobby">
            <array>
                <value>篮球</value>
                <value>读书</value>
                <value>听音乐</value>
            </array>
        </property>
<!--        List集合类型-->
        <property name="bookList">
            <list>
                <value>理想国</value>
                <value>墨菲定律</value>
            </list>
        </property>
        <!--        Map集合类型-->
        <property name="map">
            <map>
                <entry key="山西省" value="太原市"/>
            </map>
        </property>
        <property name="phones">
            <set>
                <value>13288888888</value>
                <value>13288866666</value>
            </set>
        </property>
    </bean>
  1. 测试类测试输出
    @Test
    public void testDI(){
        //配置文件路径
        String path= "applicationContext.xml";
        //通过配置文件,创建容器
        ApplicationContext context = new ClassPathXmlApplicationContext(path);
        //通过容器获得Bean对象(通过配置文件中bean标签的id)
        TestDI testDI = context.getBean("test", TestDI.class);
        //调用对象中的测试方法
        System.out.println(testDI.toString());
    }

输出

TestDI{age=20, name='张三', birthday=Thu May 06 00:00:00 HKT 2004, hobby=[篮球, 读书, 听音乐], bookList=[理想国, 墨菲定律], phones=[13288888888, 13288866666], map={山西省=太原市}}

可以看到,我们创建了不同类型的属性,设置了setter方法,在配置文件中进行了赋值,成功的注入到了对象中。

总结

  • 属性必须要有setter方法
  • 注入时配置文件写标签
  • name写属性名
  • 基本数据类型/包装类/String使用value赋值,其他用ref赋值

3.2.2 构造方法注入

  1. 创建测试类,并添加构造方法
package org.demo.entity;

public class TestDI2 {
    private String name;
    private Integer age;

    public TestDI2() {
    }

    public TestDI2(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

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

  1. 修改配置文件,注册对象并添加属性
    <bean id="test2" class="org.demo.entity.TestDI2">
        <!-- 构造注入,需要使用constructor-arg标签 -->
<!--        type 参数类型-->
<!--        index 参数下标-->
<!--        name 参数名  -->
<!--        上面三个属性只能做参数辅助,不能同时使用-->
        <constructor-arg name="age" value="24"/>
        <constructor-arg name="name" value="李四"/>
    </bean>
  1. 测试类测试输出
    @Test
    public void testDI2(){
        //配置文件路径
        String path= "applicationContext.xml";
        //通过配置文件,创建容器
        ApplicationContext context = new ClassPathXmlApplicationContext(path);
        //通过容器获得Bean对象(通过配置文件中bean标签的id)
        TestDI2 testDI2 = context.getBean("test2", TestDI2.class);
        //调用对象中的测试方法
        System.out.println(testDI2.toString());
    }

输出

TestDI2{name='李四', age='24'}

可以看到我们通过构造方法,在配置文件中使用标签,注入了属性。

tip:如果我们想用构造方法注入一个属性,类那边需要有对应的构造方法,否则会报错。

3.2.3 自动注入

自动注入是指Spring容器在创建对象时,会自动判断对象所需的依赖对象,并自动注入。主要是给引用类型赋值。

上面我们注入引用类型的时候,都是手动使用ref的方式去引用,现在我们可以修改一下配置文件,让其实现自动注入。

<?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标签,就是spring管理的一个类-->
<!--    id bean的唯一标识,也就是创建该类后的对象名,后续通过id就可以从容器中获得该对象-->


<!--    注册业务层对象-->
<!--    autowire 自动装配,两种方案-->
<!--    1. byType:通过类型赋值-->
<!--    解释:  spring容器创建BookServiceImpl时,会解析到该对象需要BookDao这些的属性spring容器中刚好有一个bookDao对象,-->
<!--    类型刚好也是BookDao,所以可以自动注入-->
<!--    2. byName:通过名字注入-->
<!--    解释:  当需要注入属性时,如果容器中如果同类型有多个,那就会不知道注入此时就可以通过对象名来注入-->

    <bean id="bookService" class="org.demo.service.impl.BookServiceImpl" autowire="byName"/>

<!--    注册dao层对象-->
    <bean id="bookDao" class="org.demo.dao.impl.BookDaoImpl"/>
</beans>

测试类直接测试

    @Test
    public void testServiceClass(){
        //配置文件路径
        String path= "applicationContext.xml";
        //通过配置文件,创建容器
        ApplicationContext context = new ClassPathXmlApplicationContext(path);
        //通过容器获得Bean对象(通过配置文件中bean标签的id)
        BookService service = context.getBean("bookService", BookService.class);
        //调用对象中的测试方法
        service.getAllBook();
    }

输出

访问业务层
访问数据访问层

可以看到,我们在bean标签中添加了属性autowire自动装配,让其按照byName名称来匹配实现自动注入(不再演示byType的了,只要改一下就行了,大差不差)。

tip:使用自动装配,必须有setter方法,只是简化了ref手动引用。还有需要注意的是,name是指需要注入的属性名和bean的id要一致,所以写代码尽量规范命名。

3.3 Bean的细节

IOC创建变量的时候,可以控制对象的创建方式,比如单例模式、多例模式等。

  • 单例模式:单实例,即为单个对象,也就是说这个类有且仅有一个对象
  • 多例模式:多示例,即为多个对象,也就是说这个类可以有多个对象

接下来演示一下如何查看对象的创建模式,调用对象的hasCode()方法即可

    @Test
    public void testClass(){
        //配置文件路径
        String path= "applicationContext.xml";
        //通过配置文件,创建容器
        ApplicationContext context = new ClassPathXmlApplicationContext(path);
        //通过容器获得Bean对象(通过配置文件中bean标签的id)
//        MyClass myClass = (MyClass) context.getBean("myClass");
        MyClass myClass = context.getBean("myClass", MyClass.class);
        System.out.println(myClass.hashCode());
        MyClass myClass1 = context.getBean("myClass", MyClass.class);
        System.out.println(myClass1.hashCode());
    }

输出

249034932
249034932

我们从容器中获取了两次对象,并输出了对应的哈希值,发现哈希值相同,说明我们取到的是同一个对象,这就说明IOC默认是单例的。

也就是说,在配置文件中,bean默认是

<!-- singleton  默认 单例 -->
<bean id="myClass" class="org.demo.MyClass" scope="singleton"/>

这个时候我们可以改成多例

    <bean id="myClass" class="org.demo.MyClass" scope="prototype"/>

这个时候再测试输出一下查看对象哈希值

输出

1278254413
940584193

这个时候就可以看到,哈希值不同,说明取到的并不是同一个对象。

tip:默认是单例模式就可以,因为要实现对象的共享。

3.4 注解开发IOC(最常使用)

注解开发,省略大部分配置,提高开发效率。

3.4.1 IOC 相关注解

注解声明作用解释
@Component通用注解,可以注解类、方法、属性
@Repository注解数据访问层(DAO层)类,作用同@Component
@Service注解业务层类,作用同@Component
@Controller注解控制层类,作用同@Component
@Scope注解作用域,可以指定Bean的作用域,如单例、多例等

tip:以上注解可以取代配置文件中的<bean>标签

3.4.2 DI 相关注解

注解声明作用解释
@Autowired注解自动装配,主要给引用类型注入(默认按照类型注入)
@Resource注解自动装配,作用同@Autowired,适用于同类型有多个的情况(默认按照名字注入)
@Value注解注入基本类型数据

tip:以上注解可以取代配置文件中的<property>标签

3.4.3 开始注解开发

  1. 创建dao层接口及其实现类并添加注解

UserDao.java

package org.demo.dao;

public interface UserDao {
    void getUserInfo();
}

UserDaoImpl.java

package org.demo.dao.impl;

import org.demo.dao.UserDao;
import org.springframework.stereotype.Repository;


/**
 * @Repository 注解标识当前类为持久层组件 相当于<bean id="userDaoImpl" class="org.demo.dao.impl.UserDaoImpl"/>
 */
@Repository
public class UserDaoImpl implements UserDao {
    @Override
    public void getUserInfo() {
        System.out.println("数据访问层UserDaoImpl=====>获得用户信息");
    }
}
  1. 创建业务类及实现类

UserService.java

package org.demo.service;

public interface UserService {
    void getUserInfo();
}

UserServiceImpl.java

package org.demo.service.impl;

import org.demo.dao.UserDao;
import org.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;


/**
 * @Service 注解 将UserServiceImpl类交给Spring容器管理,相当于配置文件中的<bean id="userServiceImpl" class="org.demo.service.impl.UserServiceImpl"/>
 */
@Service
public class UserServiceImpl implements UserService {

    /**
     * @Autowired 注解, 自动装配,autowird="byType" 根据类型查找
     * */
    @Autowired
    private UserDao userDao;

    @Override
    public void getUserInfo() {
        System.out.println("业务层UserServiceImpl======>获取用户信息");
        userDao.getUserInfo();
    }
}
  1. 编写配置文件
    applicationContextAnno.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"
       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容器管理 -->
    <context:component-scan base-package="org.demo"/>

</beans>
  1. 测试类
    @Test
    public void testAnno(){
        //配置文件路径
        String path= "applicationContextAnno.xml";
        //通过配置文件,创建容器
        ApplicationContext context = new ClassPathXmlApplicationContext(path);
        //通过容器获得Bean对象(通过配置文件中bean标签的id)
        UserService userService = context.getBean("userService", UserService.class);
        //调用对象中的测试方法
        userService.getUserInfo();
    }
  1. 输出
org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'userService' available
...
...

看到输出,报错了,说No bean named 'userService' available,没有找到userService,这是因为我们使用了@Service这种注解,它会默认命名为对象名的首字母小写,我们是在UserServiceImpl类上加的注解,所以这里我们应该写userServiceImpl

  1. 修改代码,在进行测试输出
    @Test
    public void testAnno(){
        //配置文件路径
        String path= "applicationContextAnno.xml";
        //通过配置文件,创建容器
        ApplicationContext context = new ClassPathXmlApplicationContext(path);
        //通过容器获得Bean对象(通过配置文件中bean标签的id)
        //name 是 对象名的首字母小写注解在UserServiceImpl类。所以名称为userServiceImpl 
        UserService userService = context.getBean("userServiceImpl", UserService.class);
        //调用对象中的测试方法
        userService.getUserInfo();
    }

此时再进行测试输出

业务层UserServiceImpl======>获取用户信息
数据访问层UserDaoImpl=====>获得用户信息

可以看到,此时我们就使用注解,自动配置注入了相应的对象,成功访问了业务层和数据访问层,大大省略了配置文件的编写,所写的也仅仅只有一句<context:component-scan base-package="org.demo"/>用来指明哪里有注解需要扫描配置而已。

  1. 扩展基本类型注解赋值

创建User实体类,使用@Component注解将User类交给Spring容器管理,并使用@Value注解给基本类型属性赋值。
User.java

package org.demo.entity;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.sql.Date;

@Component
public class User {
    @Value("1")
    private Integer id;



    @Value("大海")
    private String name;
    @Value("666666@163.com")
    private String email;
    @Value("山西省太原市迎泽区")
    private String address;
    @Value("13666666666")
    private String phone;
    @Value("2024/10/30")
    private Date createTime;
    @Value("2024/10/30")
    private Date updateTime;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getAddress() {
        return address;
    }

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

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public Date getCreateTime() {
        return createTime;
    }

    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }

    public Date getUpdateTime() {
        return updateTime;
    }

    public void setUpdateTime(Date updateTime) {
        this.updateTime = updateTime;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", email='" + email + '\'' +
                ", address='" + address + '\'' +
                ", phone='" + phone + '\'' +
                ", createTime=" + createTime +
                ", updateTime=" + updateTime +
                '}';
    }
}

编写测试类

    @Test
    public void testUserAnno(){
        //配置文件路径
        String path= "applicationContextAnno.xml";
        //通过配置文件,创建容器
        ApplicationContext context = new ClassPathXmlApplicationContext(path);
        //通过容器获得Bean对象(通过配置文件中bean标签的id)
        //因为没有指定名称,所以默认为类名首字母小写
        User user = context.getBean("user", User.class);
        //调用对象中的测试方法
        System.out.println(user.toString());
    }

测试输出

User{id=1, name='大海', email='666666@163.com', address='山西省太原市迎泽区', phone='13666666666', createTime=Wed Oct 30 00:00:00 HKT 2024, updateTime=Wed Oct 30 00:00:00 HKT 2024}

可以看到,输出的User对象已经自动注入了相应的属性值,而且基本类型属性也被赋值成功。

tip:Date对象引入的包为java.util.Date,不要引入成java.sql.Date,会报错。

3.4.4 注解使用细节

  1. IOC创建对象的注解,四个功能一样,都可以创建对象,但是尽量还是按照名称在规定的位置使用,符合规范
  2. 加上创建对象的注解后,默认对象是类名首字母小写,即需要通过类名的首字母小写来从容器中获得对象
  3. 如果我们不想使用默认的对象名,可以手动指定对象名,例如:@Service("userService"),这样就不会按照默认的规则来创建对象了。
  4. 自动注入@AutoWired,默认是按照类型注入的,但是如果有多个相同类型的不同实现类对象,就会注入报错,这时候就需要我们使用@Resource注解,它可以根据名字注入。例如:@Resource(name="userDaoImpl2"),这样就不会报错了。

4. 代理设计模式

代理的设计理念是限制对象的直接访问,即不能通过 new 的方式得到想要的对象,而是访问该对象的代理类。这样的话,我们就保护了内部对象,如果有一天内部对象因为 某个原因换了个名或者换了个方法字段等等,那对访问者来说 一点不影响,因为他拿到的只是代理类而已,从而使该访问对 象具有高扩展性。

代理类可以实现拦截方法修改原方法的参数和返回值,满足了代理自身需求和目的也就是代理的方法增强性。

按照代理的创建时期,代理可分为:静态代理动态代理

静态代理由开发者手动创建,在程序运行前,已经存在;

动态代理不需要手动创建,它是在程序运行时动态的创建代理类。

总结:代理模式--给某个对象提供一个代理,以改变对该对象的访 问方式

4.1 静态代理

我们可以创建FangDong房东类,创建chuzu()出租方法,使用FangDongProxy中介类来实现静态代理的演示。

  1. 创建FangDong()类 及其实现类
package org.demo.proxy;

public interface FangDong {
    void chuzu();
}


package org.demo.proxy;


public class FangDongImpl implements FangDong{
    @Override
    public void chuzu() {
        System.out.println("房东出租房子");
    }
}

  1. 创建代理类
package org.demo.proxy;

public class FangDongProxy {

    private FangDong fangDong;

    public FangDongProxy(FangDong fangDong){
        this.fangDong = fangDong;
    }

    public void chuzu() {
        // 前置增强
        System.out.println("中介发布出租信息");
        fangDong.chuzu();
        //后置增强
        System.out.println("签订合同,中介负责后期维护");
    }
}
  1. 编写测试类
package org.demo.proxy;

public class TestProxy {
    public static void main(String[] args) {
        FangDongProxy fangDong = new FangDongProxy(new FangDongImpl());
        fangDong.chuzu();
    }
}
  1. 测试输出
中介发布出租信息
房东出租房子
签订合同,中介负责后期维护

这就是静态代理,FangdongProxy在程序运行之前,是提前创建好的,从而对我们的FangDong类实现了前置和后置增强。

4.2 动态代理

其实我们发现,实现代理,需要新写代理类,这是租房需要一个代理类,那我们如果需要买车, 买酒等等,那就得再去写代理买车,买酒等的代理类去实现一些前后增强,随着我们要做的事情越多,代理类也就越多,多个代理类的功能代码也就显得更加冗余。这时候,有没有办法出现一个代理类,能够根据我们的目的,动态产生代理,从而减少代码冗余呢?这就到了我们的这一小节,动态代理。

动态代理:也就是在程序运行过程中,动态的为目标类产生代理类。

实现动态代理,有两种方案:

  1. JDK动态代理(JDK自带)
    • JDK动态代理,只能代理接口,即目标类必须有接口
  2. cglib动态代理(第三方技术,需要导入jar包)
    • 目标类可以是接口也可以是实现类

4.2.1 JDK动态代理

  1. 创建代理类MyInvocationHandler
package org.demo.proxy.dynamic;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class MyInvocationHandle implements InvocationHandler {
    private Object target;
    public MyInvocationHandle(Object target)
    {
        this.target = target;
    }

    /**
     *
     * @param proxy 代理对象
     * @param method 目标方法
     * @param args 目标方法执行所需要的参数
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//        method.invoke(代理对象,参数);
        System.out.println("中介前期宣传(方法前日志)");
        Object ret = method.invoke(target, args);
        System.out.println("中介后期维护(方法后日志)");
        return ret;
    }
}
  1. 创建目标类及其实现类

将上面静态代理的类及其实现类复制过来即可,不再给出

  1. 编写测试类
package org.demo.proxy.dynamic;

import java.lang.reflect.Proxy;

public class TestDynamic {
    public static void main(String[] args) {
        FangDong fangDong = new FangDongImpl();

        /**
         * ClassLoader loader,     类加载器
         * Class<?>[] interfaces,  目标对象实现的接口
         * InvocationHandler h     类拦截器
         *
         */
        FangDong ret = (FangDong) Proxy.newProxyInstance(fangDong.getClass().getClassLoader(), fangDong.getClass().getInterfaces(),new MyInvocationHandle(fangDong));
        ret.chuzu();
    }
}
  1. 测试输出
中介前期宣传(方法前日志)
房东出租房子
中介后期维护(方法后日志)

可以看到,我们使用了JDK提供的动态代理,编写了代理接口,这样无论有多少个目标类需要去代理,我们都不用再去重复的编写新的代理类,仅需要这一个代理类即可完成所有代理操作。

tip:注意,使用JKD的代理,目标类必须实现接口!

4.2.2 cglib 动态代理

cglib 动态代理,虽然是第三方技术,需要引入第三方jar包,但是spring框架已经整合了cglib技术,所以在导入了spring依赖后,就已经导入了cglib包(在spring-core下就可以看到),所以不需要另外导入。cglib虽然写法同jdk代理不同,但是思想是极其相似的。

  1. 创建代理类MyProxyInterceptor.java
package org.demo.proxy.cglib;

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 MyProxyInterceptor implements MethodInterceptor {


//    cglib增强器
   private Enhancer enhancer = new Enhancer();

   public MyProxyInterceptor(Class tragetClass){
       enhancer.setSuperclass(tragetClass);
       enhancer.setCallback(this);
   }

//    获得代理对象
   public Object getProxy() {
       return enhancer.create();
   }

   /**
    *
    * @param o         目标对象
    * @param method    目标方法
    * @param objects   方法参数
    * @param methodProxy 代理方法
    */
   @Override
   public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
       System.out.println("cglib在方法执行前,添加日志");
       Object ret = methodProxy.invokeSuper(o, objects);
       System.out.println("cglib在方法执行后,添加日志");
       return ret;
   }
}

  1. 创建目标类及其实现类

同上,不再给出

  1. 创建测试类
package org.demo.proxy.dynamic;

import org.demo.proxy.cglib.MyProxyInterceptor;

public class TestDynamic {
    public static void main(String[] args) {
        FangDong fangDong = (FangDong) new MyProxyInterceptor(FangDongImpl.class).getProxy();
        fangDong.chuzu();
    }
}
  1. 测试输出
cglib在方法执行前,添加日志
房东出租房子
cglib在方法执行后,添加日志

可以看到,使用cglib 动态代理,也可以很方便的动态创建代理,减少我们代码的编写量和可维护性。

tip:cglib动态代理于jdk动态代理的最大区别就是,cglib要求目标实现类,可以没有接口,即为FangDongImpl 可以不实现自FangDong,但是JDK代理不行,必须要求有接口有实现类。

5. AOP(面向切面编程)

5.1 AOP概述

spring中的另外一个重要功能,AOP(Aspect-Oriented Programming,面向切面编程),它可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可维护性。

AOP面向切面编程,利用 一种称为横切的技术,剖开封装的对象内部,并将那些影响了多个类的公共行为抽取出封装到一个可重用模块,并将其命名为Aspect,即切面。所谓切面,简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。

就比如下图中,业务层的一些方法,都需要在方法前开启事务或者进行权限校验,方法调用后又需要打印日志记录,成功提交事务,失败了回滚事务,这些都是与业务无关的操作,如果每个方法都要写这些代码,代码量会很大,而且如果有多个方法都需要这样的操作,那么代码的重复率会很高,这时候就可以使用AOP来进行封装。

AOP切入前.png

这个时候,我们将重复的代码进行抽取出来,封装成一个切面,然后在业务层的每个方法调用之前/后,都要先调用这个切面,然后再调用业务层的方法,这样就可以将这些与业务无关的操作封装起来,从而提高代码的复用率。如下图

AOP切入后.png

面向切面编程的作用:就是将项目中与核心逻辑无关的代码横向抽取成切面类,通过织入作用到目标方法,以使目标方法执行前后达到增强的效果

原理: AOP底层使用的就是动态代理,给AOP指定哪些类型(目标类)需要增强,就会产生对应的代理对象,代理对象执行方法前后会先执行增强的方法

好处:减少系统的重复代码,降低模块之间的耦合度,便于维护,可以只关注核心业务

5.2 AOP术语

  • 连接点(Joinpoint):连接点是程序类中客观存在的方法,可被Spring拦截并切入内容即每个方法在切入之前,都是连接点
  • 切入点(Pointcut):被Spring切入连接点。即真正会增强的目标方法
  • 通知、增强(Advice):可以为切入点添加额外功能,分为:前置通知、后置通知、异常通知、环绕通知等。
  • 目标对象(Target):代理的目标对象
  • 引介(introduction):一种特殊的增强,可在运行期为类动态添加Field和Method。
  • 织入(Weaving):把通知应用到具体的类,进而创建新的代理类的过程。
  • 代理(Proxy):被AOP织入通知后,产生的结果类。
  • 切面(Aspect):由切点和通知组成,将横切逻辑织入切面所指定的连接点中。

5.3 应用场景

  • 事务管理
  • 权限校验
  • 日志记录
  • 性能检测
  • 等等

5.4 AOP入门编程实现

需求UserService及其实现类,在其方法执行前/后执行增强的方法。

编程的过程:

  • 创建项目
  • 导入依赖
    • 核心依赖
    • 切面依赖
  • 目标类
  • 切面类
  • 织入(配置文件设置)
  • 测试
  1. 创建项目,创建普通maven项目即可

  2. 导入依赖,导入spring核心依赖和aop依赖即可

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.3.33</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>5.3.33</version>
        </dependency>
  1. 编写业务层及其实现类(目标类)
package org.demo.service;

public interface UserService {
    void findUserById();
    void deleteUserById();
}


package org.demo.service.impl;

import org.demo.service.UserService;

public class UserServiceImpl implements UserService {
    @Override
    public void findUserById() {
        System.out.println("业务层=====》查询用户");
    }

    @Override
    public void deleteUserById() {
        System.out.println("业务层=====》删除用户");
    }
}

  1. 切面类(做环绕演示,环绕包括前置和后置)
package org.demo.aspect;

import org.aspectj.lang.ProceedingJoinPoint;

public class MyAspect {
    /**
     * 定义通知的方法,前置通知 和 后置通知
     */


    /**
     * 环绕通知,包括前置通知和后置通知
     * @param point 连接点/切入点,即目标方法
     */
    public Object around(ProceedingJoinPoint point) throws Throwable {
        System.out.println("环绕通知====>方法前  开启事务/日志记录/权限校验");
        Object proceed = point.proceed();
        System.out.println("环绕通知====>方法后  提交事务/日志记录");
        return proceed;
    }
}

  1. 织入(配置文件设置)
<?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">
    <bean id="userService" class="org.demo.service.impl.UserServiceImpl"/>
    
    <bean id="myAspect" class="org.demo.aspect.MyAspect"/>
    
    <aop:config>
<!--        确定切面-->
        <aop:aspect ref="myAspect">
<!--            1.确定通知/增强类型-->
<!--            2.确定切面中增强的方法-->
<!--            3.确定目标类以及目标方法
            execution(返回值类型 包路径.类名.方法名(参数列表))
            返回值类型任意 就写 *
            类名任意 就写 *
            方法名就写具体方法名,如果该类所有方法都需要通知/增强,也写 *
            参数列表任意 就写 (..)
-->
            <aop:around method="around" pointcut="execution(* org.demo.service.impl.UserServiceImpl.*(..))"/>
        </aop:aspect>
    </aop:config>
</beans>
  1. 测试
package org.demo;

import org.demo.service.UserService;
import org.demo.service.impl.UserServiceImpl;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class TestAspect {

    @Test
    public void testAround() {
        ApplicationContext context = new ClassPathXmlApplicationContext("application-context.xml");
        UserService userService = context.getBean("userService",UserService.class);
        userService.findUserById();
        System.out.println("===============================");
        userService.deleteUserById();
    }
}

输出

环绕通知====>方法前  开启事务/日志记录/权限校验
业务层=====》查询用户
环绕通知====>方法后  提交事务/日志记录
===============================
环绕通知====>方法前  开启事务/日志记录/权限校验
业务层=====》删除用户
环绕通知====>方法后  提交事务/日志记录

可以看到正常的业务逻辑执行了,并且在执行前后都执行了增强的方法。因为我们在织入的时候,选择的是UserServiceImpl中的所有方法,所以findUserById和deleteUserById都被织入了。

5.5 其他增强

  • 环绕通知(目标方法执行前后,都有增强的方法,上面已经演示过)
    • 常用于事务管理(事务管理有开启和关闭)
  • 前置通知
    • 常用于权限校验
  • 后置通知
    • 常用于日志记录或者释放资源
  • 后置返回通知
    • 常用于获得目标方法的返回值,处理返回值
  • 异常通知
    • 常用于代码有异常后去执行一些功能

5.5.1 前置通知

  1. 编写前置方法
    /**
     * 前置通知 JoinPoint 目标对象
     */
    public void before(JoinPoint point) {
        Object target = point.getTarget();
        System.out.println("目标对象:" + target);
        Signature signature = point.getSignature();
        System.out.println("调用方法签名:" + signature);
        System.out.println("实现前置增强====》权限校验功能");
    }
  1. 编写配置文件
    <aop:before method="before" pointcut="execution(* org.demo.service.impl.UserServiceImpl.*(..))"/>
  1. 测试
    @Test
    public void testBefore() {
        ApplicationContext context = new ClassPathXmlApplicationContext("application-context.xml");
        UserService userService = context.getBean("userService",UserService.class);
        userService.findUserById();
    }

4.输出

目标对象:org.demo.service.impl.UserServiceImpl@2254127a
调用方法签名:void org.demo.service.UserService.findUserById()
实现前置增强====》权限校验功能
业务层=====》查询用

前置通知和环绕通知的配置相似,前置通知传入的是目标对象,可以看到目标对象是UserServiceImpl,并且调用了findUserById和deleteUserById方法,通常在前置通知中,进行权限校验,如果校验不通过,则不执行目标方法,我们可以手动抛出异常,或者返回false,让目标方法不执行。
5. 修改通知方法,模拟权限校验不通过不执行目标方法

    public void before(JoinPoint point) {
        Object target = point.getTarget();
        System.out.println("目标对象:" + target);
        Signature signature = point.getSignature();
        System.out.println("调用方法签名:" + signature);
        System.out.println("实现前置增强====》权限校验功能");
        throw new RuntimeException("校验未通过");
    }
  1. 再次输出测试
目标对象:org.demo.service.impl.UserServiceImpl@2254127a
调用方法签名:void org.demo.service.UserService.findUserById()
实现前置增强====》权限校验功能

java.lang.RuntimeException: 校验未通过`
at org.demo.aspect.MyAspect.before(MyAspect.java:21)
...
...

tip:这里的权限校验仅作参考,实际并不是这样粗糙的编写异常让目标方法不执行,而是应该根据实际情况编写具体的权限校验逻辑。

5.5.2 后置通知

后置通知同前置通知基本相似,只不过是在目标方法执行后执行,配置文件也由<aop:before>修改为<aop:after>,在修改方法名即可。

  1. 编写后置方法`
    /**
     * 后置通知
     */
    public void after() {
        System.out.println("后置增强=====>记录日志,释放资源");
    }
  1. 编写配置文件
            <aop:after method="after" pointcut="execution(* org.demo.service.impl.UserServiceImpl.*(..))"/>
  1. 测试
    public void testafter() {
        ApplicationContext context = new ClassPathXmlApplicationContext("application-context.xml");
        UserService userService = context.getBean("userService",UserService.class);
        userService.findUserById();
       System.out.println("===============================");
       userService.deleteUserById();
    }
  1. 输出
业务层=====》查询用户
后置增强=====>记录日志,释放资源
===============================
业务层=====》删除用户
后置增强=====>记录日志,释放资源

tip:后置通知方法可以写JoinPoint point也可以不写,这个主要是为了获取对象,但是后置通知一般都是用于记录日志,释放资源,获取对象也不会进行什么操作。

5.5.3 后置返回通知

后置返回通知是在目标方法执行后,获取目标方法的返回值,并进行一些处理。

  1. 编写后置返回方法
    /**
     * 后置返回通知
     * @param result 用于接收目标方法的返回值
     * @return 处理后返回
     */

    public Object afterReturn(Object result) {
        System.out.println("后置返回通知=====>获取到的返回结果:" + result);
//        模拟业务处理
        if(result instanceof Integer value){
            return value * 5;
        }
        return result;
    }
  1. 编写配置文件
<!-- 多了一个returning'属性,里面填写后置返回通知方法的返回值参数名,这里是result -->
    <aop:after-returning method="afterReturn" returning="result" pointcut="execution(* org.demo.service.impl.UserServiceImpl.*(..))"/>
  1. 测试
    @Test
    public void testAfterReturn() {
        ApplicationContext context = new ClassPathXmlApplicationContext("application-context.xml");
        UserService userService = context.getBean("userService",UserService.class);
        userService.findUserById();
        System.out.println("===============================");
        int ret = userService.deleteUserById();
        System.out.println("处理后的返回值:" + ret);
    }
  1. 输出
业务层=====》查询用户
后置返回通知=====>获取到的返回结果:null
===============================
业务层=====》删除用户
后置返回通知=====>获取到的返回结果:100
处理后的返回值:100

这时候 我们发现了一个问题,为什么,我们在后置返回方法中判断如果返回值是Integer类型,就强制转换后,乘5,然后返回,但是为什么返回值还是100呢?
这是因为后置返回通知无法改变目标方法的返回值,如果我们需要改变返回值,可以考虑使用环绕通知

5.5.4 异常通知

异常通知主要是为了捕获目标方法的异常,并进行一些处理。

  1. 编写异常通知方法
    /**
     * 异常通知
     * @param ex 目标方法抛出的异常
     */
    public void afterThrowing(Throwable ex) {
        System.out.println("异常通知=====>" + ex.getMessage());
    }
  1. 编写配置文件
<!-- 注意这里参数使用的是throwing属性,里面填写异常通知方法的异常参数名,这里是ex -->
    <aop:after-throwing method="afterThrowing"  throwing="ex"  pointcut="execution(* org.demo.service.impl.UserServiceImpl.*(..))"/>
  1. 为了测试,我们在业务层中抛出一个异常
    @Override
    public Integer deleteUserById() {
        System.out.println("业务层=====》删除用户");
        return 100/0;
    }
  1. 测试
    @Test
    public void testAfterThrowing() {
        ApplicationContext context = new ClassPathXmlApplicationContext("application-context.xml");
        UserService userService = context.getBean("userService",UserService.class);
        userService.findUserById();
        System.out.println("===============================");
        int ret = userService.deleteUserById();
    }

输出

业务层=====》查询用户
===============================
业务层=====》删除用户
异常通知=====>/ by zero

java.lang.ArithmeticException: / by zero
at org.demo.service.impl.UserServiceImpl.deleteUserById(UserServiceImpl.java:14)
...
...

可以看到,我们在业务层删除方法返回值里故意抛出了一个异常,然后在配置文件中配置了异常通知,可以看到后置异常报告打印了异常信息,并没有影响到业务逻辑的执行。

tip:通知可以一起使用,比如前置通知和后置通知,前置通知可以进行权限校验,后置通知可以记录日志,异常通知可以进行异常处理等。

5.6 AOP底层动态代理

AOP的底层原理实际上就是使用的动态代理,使用的技术是JDKcglib混合。

为什么我会知道用的是jdk和cglib动态代理呢?很简单,我们可以模拟一下试一下。

首先,为了便于演示,我们使用后置通知,并且手动抛出异常。直接在测试类中运行

业务层=====》查询用户
后置增强=====>记录日志,释放资源
===============================
业务层=====》删除用户
后置增强=====>记录日志,释放资源

java.lang.ArithmeticException: / by zero

	at org.demo.service.impl.UserServiceImpl.deleteUserById(UserServiceImpl.java:14)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:569)
	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:241)
	at jdk.proxy2/jdk.proxy2.$Proxy10.deleteUserById(Unknown Source)

可以看到,报错信息中,出现了JdkDynamicAopProxy这个关键词,这个关键词的意思就是JDK动态代理

接下来改动一下业务层,不再实现接口,改为普通方法

package org.demo.service.impl;


public class UserServiceImpl {
   // @Override
    public void findUserById() {
        System.out.println("业务层=====》查询用户");
    }

   // @Override
    public Integer deleteUserById() {
        System.out.println("业务层=====》删除用户");
        return 100/0;
    }
}

再改动一下测试类,因为我们改动了业务层不再实现接口,所以测试类从容器中获取对象转换那里需要改动

    @Test
    public void testAround() {
        ApplicationContext context = new ClassPathXmlApplicationContext("application-context.xml");
        UserServiceImpl userService = context.getBean("userService",UserServiceImpl.class);
        userService.findUserById();
        System.out.println("===============================");
        int ret = userService.deleteUserById();
    }

再次运行,可以看到

业务层=====》查询用户
后置增强=====>记录日志,释放资源
===============================
业务层=====》删除用户
后置增强=====>记录日志,释放资源

java.lang.ArithmeticException: / by zero

	at org.demo.service.impl.UserServiceImpl.deleteUserById(UserServiceImpl.java:14)
	at org.demo.service.impl.UserServiceImpl$$FastClassBySpringCGLIB$$fed48ef6.invoke(<generated>)
	at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:762)
	at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:762)
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:707)
	at org.demo.service.impl.UserServiceImpl$$EnhancerBySpringCGLIB$$16cf8a7e.deleteUserById(<generated>)

报错信息中,频繁出现了CglibAopProxy,也就是cglib动态代理。

由此我们可以得知:

  • 当目标类有接口的时候,使用的就是JDK动态代理。
  • 当目标类没有接口的时候,使用的就是cglib动态代理。

tip:当你要启动多个通知方法配置时,且作用范围相同,可以将方法提取出来放置在<aop:pointcut>中,然后在<aop:before><aop:after><aop:after-returning><aop:after-throwing>等中引用。
例如:

    <aop:config>
        <aop:aspect ref="myAspect">
            <aop:pointcut id="methodScope" expression="execution(* org.demo.service.impl.UserServiceImpl.*(..))"/>
            <aop:before method="before" pointcut-ref="methodScope"/>
            <aop:after method="after" pointcut-ref="methodScope"/>
        </aop:aspect>
    </aop:config>

6. 注解开发

使用注解开发可以简化配置,提高开发效率。本节,则使用注解开发IOC、DI、AOP。
AOP相关的注解有:
| 注解声明 | 作用解释|
| --- | --- |
| @Aspect | 定义切面类,标注切面类,可以被Spring AOP框架识别|
| @Pointcut | 定义切点,标注切点,可以被切面类引用(也就是AOP生效的范围)|
| @Before | 前置通知,在目标方法执行前执行|
| @After | 后置通知,在目标方法执行后执行|
| @AfterReturning | 后置返回通知,在目标方法执行后,获取目标方法的返回值|
| @AfterThrowing | 异常通知,在目标方法抛出异常后执行|
| @Around | 环绕通知,在目标方法执行前后,执行自定义逻辑|

接下来通过改写上面的案例,来详细理解一下每个注解的使用。

6.1 IOC注解,只需要在UserServiceImpl实现类添加注解@Service即可

因为仅作演示,没用到注入,所以不需要使用注入注解。

package org.demo.service.impl;

import org.demo.service.UserService;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {
    @Override
    public void findUserById() {
        System.out.println("业务层=====》查询用户");
    }

    @Override
    public Integer deleteUserById() {
        System.out.println("业务层=====》删除用户");
        return 100;
    }
}

6.2 编写切面类,使用注解完成配置

@Component注解用于将切面类注册到Spring容器中,@Aspect注解用于定义切面类,@Pointcut注解用于定义切点,@Before、@After、@AfterReturning、@AfterThrowing、@Around注解用于定义通知方法。

package org.demo.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class MyAnnoAspect {

    @Pointcut("execution(* org.demo.service.impl.UserServiceImpl.*(..))")
    void myPoint(){}

    @Before("myPoint()")
    public void before(JoinPoint point) {
        Object target = point.getTarget();
        System.out.println("目标对象:" + target);
        Signature signature = point.getSignature();
        System.out.println("调用方法签名:" + signature);
        System.out.println("实现前置增强====》权限校验功能");
//        throw new RuntimeException("校验未通过");
    }


    @After("myPoint()")
    public void after() {
        System.out.println("后置增强=====>记录日志,释放资源");
    }


    @AfterReturning(value = "myPoint()",returning = "result")
    public Object afterReturn(Object result) {
        System.out.println("后置返回通知=====>获取到的返回结果:" + result);
//        模拟业务处理
        if(result instanceof Integer value){
            return value * 5;
        }
        return result;
    }


    @AfterThrowing(value = "myPoint()",throwing = "ex")
    public void afterThrowing(Throwable ex) {
        System.out.println("异常通知=====>" + ex.getMessage());
    }


    @Around("myPoint()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        System.out.println("环绕增强====>方法前  开启事务/日志记录/权限校验");
        Object proceed = point.proceed();
        System.out.println("方法返回值:" + proceed);
        System.out.println("环绕增强====>方法后  提交事务/日志记录");
        if(proceed instanceof Integer){
            return (Integer) proceed * 5;
        }
        return proceed;
    }
}

6.3 编写配置文件,配置扫描注解位置,启用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: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/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

<!--    扫描IOC、DI注解-->
    <context:component-scan base-package="org.demo"/>

<!--    AOP开启自动代理,注解生效-->
    <aop:aspectj-autoproxy/>
</beans>

6.4 测试

    @Test
    public void testAnno() {
        ApplicationContext context = new ClassPathXmlApplicationContext("application-contextAnno.xml");
        UserService userService = context.getBean("userServiceImpl",UserService.class);
        userService.findUserById();
        System.out.println("===============================");
        int ret = userService.deleteUserById();
    }

输出

环绕增强====>方法前  开启事务/日志记录/权限校验
目标对象:org.demo.service.impl.UserServiceImpl@d771cc9
调用方法签名:void org.demo.service.UserService.findUserById()
实现前置增强====》权限校验功能
业务层=====》查询用户
后置返回通知=====>获取到的返回结果:null
后置增强=====>记录日志,释放资源
方法返回值:null
环绕增强====>方法后  提交事务/日志记录
===============================
环绕增强====>方法前  开启事务/日志记录/权限校验
目标对象:org.demo.service.impl.UserServiceImpl@d771cc9
调用方法签名:Integer org.demo.service.UserService.deleteUserById()
实现前置增强====》权限校验功能
业务层=====》删除用户
后置返回通知=====>获取到的返回结果:100
后置增强=====>记录日志,释放资源
方法返回值:100
环绕增强====>方法后  提交事务/日志记录

因为所有通知方法都被调用,所以输出了很多信息。接下来将注解详细对应一下配置文件中的标签。

  • @Service注解对应<bean>标签
    <bean id="userService" class="org.demo.service.impl.UserServiceImpl"/>
  • @Component注解对应<bean>标签,
<bean id="MyAspectAnno" class="org.demo.aspect.MyAspectAnno"/>
  • @Aspect注解对应
    <aop:config>
        <aop:aspect ref="MyAspectAnno">
        </aop:aspect>
    </aop:config>`
  • @Pointcut注解对应
    <aop:pointcut id="methodScope" expression="execution(* org.demo.service.impl.UserServiceImpl.*(..))"/>
  • @Before("myPoint()")注解对应
    <aop:before method="before" pointcut-ref="methodScope"/>
  • @After("myPoint()")注解对应
    <aop:after method="after" pointcut-ref="methodScope"/>
  • @AfterReturning(value = "myPoint()",returning = "result")注解对应
    <aop:after-returning method="afterReturn" pointcut-ref="methodScope" returning="result"/>
  • @AfterThrowing(value = "myPoint()",throwing = "ex")注解对应
    <aop:after-throwing method="afterThrowing" pointcut-ref="methodScope" throwing="ex"/>
  • @Around("myPoint()")注解对应
    <aop:around method="around" pointcut-ref="methodScope"/>

最后,只需要在配置文件中配置好扫描注解的位置,让spring可以扫描到IOC、DI的相关注解,再配置好AOP的自动代理,就可以生效AOP相关的注解,至此,我们就可以通过注解开发,我们可以很方便的完成IOC\DI\AOP开发配置,提高开发效率。

tip:如果按照配置完成,启动测试报错,相关错误是org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'userService' available这样子的,那你一定是忘记了在IOC小节中,使用注解后,默认名字为类名首字母小写。

7. Spring + Mybatis整合

在Spring小节中,主要学习了IOC和AOP的使用,本节将学习如何利用IOC和AOP来完成Spring与Mybatis的整合。

  • IOC:控制反转,用来创建对象
    • XxxService
    • 通过数据源获取数据库连接
    • 创建SqlSessionFactory
    • 创建SqlSession
    • 获取XxxMapper代理对象
  • AOP:面向切面编程
    • 事务管理

具体的整合思路:

  1. 创建项目
  2. 导入依赖
    • spring核心依赖
    • aop依赖
    • mybatis依赖
    • 数据库驱动依赖
    • 单元测试依赖
    • 日志依赖
    • 连接池依赖
    • 专业用与spring和mybatis整合的依赖
    • spring-jdbc依赖,用于DAO层支持
  3. 配置文件
    • spring配置文件
    • mybatis配置文件
    • db配置文件
    • log4配置文件
  4. 具体业务代码
    • 实体类 pojo/entity
    • XxxMapper、XxxMapper.xml dao/mapper
    • XxxService、 XxxServiceImpl service
  5. 测试

7.1 创建项目

直接创建Maven项目即可,此处不再赘述

7.2 导入依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>springAndMybatis</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
<!--        spring核心依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.3.33</version>
        </dependency>
<!--        mysql数据库驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.49</version>
        </dependency>
<!--        aop依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>5.3.33</version>
        </dependency>
<!--        mybatis依赖-->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.14</version>
        </dependency>
<!--        单元测试依赖-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>
<!--        日志依赖-->
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.12</version>
        </dependency>
<!--        mybatis分页插件依赖-->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper</artifactId>
            <version>5.3.3</version>
        </dependency>
<!--        alibaba数据池依赖-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.8</version>
        </dependency>
<!--        整合依赖-->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>1.3.1</version>
        </dependency>
<!--        spring-jdbc依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.3.33</version>
        </dependency>
    </dependencies>

    <build>
        <!--    在IDEA的MAVEN项目中,默认源代码(java)目录下的xml等资源文件并不在编译的时候y一块打包进classes文件夹,-->
        <!--    而是直接舍弃掉,为了解决这个问题,有两种方法-->
        <!--    方法1 将xml或者properties等配置文件放在resources下,并修改配置文件的代码,比如说注册xml文件的位置-->
        <!--    方法2 在maven中添加过滤,将java下的xml进行过 滤-->
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <!--          默认  新添加自定义则失效-->
                    <include>*.xml</include>
                    <!--          新添加 */代表一级目录 **/代表多级目录-->
                    <include>**/*.xml</include>
                </includes>
                <filtering>true</filtering>
            </resource>
        </resources>
    </build>
</project>

7.3 配置文件

因为之前写过mybatis、db、log4的配置,所以这里直接给出,主要是spring的配置。

log4.properties

log4j.rootLogger=ERROR, stdout

log4j.logger.org.demo.mapper=DEBUG


log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n

jdbc.properties

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/book?useSSL=false
jdbc.username=root
jdbc.password=
jdbc.initialSize=5
jdbc.maxActive=20
jdbc.minIdle=3
jdbc.maxWait=0
jdbc.timeBetweenEvictionRunsMillis=0
jdbc.minEvictableIdleTimeMillis=30000

mybatis-config.xml,由于和spring整合,所以大部分配置都在spring中配置。(我觉得在spring中配置插件太麻烦,所以没挪过去,会给出对应配置)

<?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>
        <setting name="logImpl" value="LOG4J"/>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <setting name="cacheEnabled" value="true"/>
    </settings>


    <plugins>
<!--        插件拦截器的路径-->
        <plugin interceptor="com.github.pagehelper.PageInterceptor"/>
    </plugins>

</configuration>

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: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/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--    扫描注解-->
    <context:component-scan base-package="org.demo"/>
<!--        启用aop自动代理-->
<!--    <aop:aspectj-autoproxy/>-->

<!--    1. 加载数据源配置文件-->
    <context:property-placeholder location="classpath:jdbc.properties"/>

<!--    2. 创建数据源-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.driver}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
        <property name="initialSize" value="${jdbc.initialSize}"/>
        <property name="maxActive" value="${jdbc.maxActive}"/>
        <property name="minIdle" value="${jdbc.minIdle}"/>
        <property name="maxWait" value="${jdbc.maxWait}"/>
        <property name="timeBetweenEvictionRunsMillis" value="${jdbc.timeBetweenEvictionRunsMillis}"/>
        <property name="minEvictableIdleTimeMillis" value="${jdbc.minEvictableIdleTimeMillis}"/>
    </bean>

<!--    3. 创建SqlSessionFactory-->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
<!--        这些配置,如果mybaitis-config.xml写了,就可以不用再写了-->
        <property name="typeAliasesPackage" value="org.demo.entity"/>
<!--        可以直接引用-->
        <property name="configLocation" value="mybatis-config.xml"/>

<!--        配置mybatis的插件-->
<!--        <property name="plugins">-->
<!--            <set>-->
<!--                <bean class="com.github.pagehelper.PageInterceptor">-->
<!--                    <property name="properties">-->
<!--                        <props>-->
<!--                            <prop key="helperDialect">mysql</prop>-->
<!--                        </props>-->
<!--                    </property>-->
<!--                </bean>-->
<!--            </set>-->
<!--        </property>-->
    </bean>

<!--    4. 扫描mapper,使mapper插入容器,获得mapper代理-->
    <bean id="mapperScannerConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!--        此处是上面的工厂id-->
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
<!--            扫描mapper,产生代理对象,加入spring容器-->
        <property name="basePackage" value="org.demo.mapper"/>
    </bean>

</beans>

7.4 实现具体业务代码

  1. 实体类 org.demo.entity下创建 Book.java
package org.demo.entity;

import java.io.Serializable;

public class Book implements Serializable {
    private Integer id;
    private String name;
    private String author;
    private String desc;
    private Double price;



    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

    public Double getPrice() {
        return price;
    }

    public void setPrice(Double price) {
        this.price = price;
    }

    @Override
    public String toString() {
        return "Book{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", author='" + author + '\'' +
                ", desc='" + desc + '\'' +
                ", price=" + price +
                '}';
    }
}
  1. 创建mapper接口及其对应的xml映射文件 org.demo.mapper下创建 BookMapper.java、BookMapper.xml

BookMapper.java

package org.demo.mapper;

import org.demo.entity.Book;

public interface BookMapper {
    Book selectById(Integer id);
}

BookMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.demo.mapper.BookMapper">
    <select id="selectById" resultType="Book">
        select * from bookinfo where id = #{id}
    </select>
</mapper>

  1. 创建service接口及其实现类 org.demo.service下创建 BookService.java、BookServiceImpl.java

BookService.java

package org.demo.service;

import org.demo.entity.Book;

public interface BookService {
    Book selectById(Integer id);
}

BookServiceImpl.java

package org.demo.service.impl;

import org.demo.entity.Book;
import org.demo.mapper.BookMapper;
import org.demo.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class BookServiceImpl implements BookService {
    @Autowired
    private BookMapper bookMapper;

    public Book selectById(Integer id) {
        return bookMapper.selectById(id);
    }
}

7.5 测试

直接创建测试类,测试service的selectById方法

    @Test
    public void testSelectById(){
        String url ="applicationContext.xml";
        ApplicationContext context = new ClassPathXmlApplicationContext(url);
        BookService bookServiceImpl = context.getBean("bookServiceImpl", BookService.class);
        Book book = bookServiceImpl.selectById(1);
        System.out.println(book);
    }

输出

DEBUG [main] - ==>  Preparing: select * from bookinfo where id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <==      Total: 1
Book{id=1, name='道听途说', author='何金银', desc='清空二级缓存测试', price=29.7}

8. spring实现事务管理

本来在之前学习mybatis时,提供了jdbc事务管理的功能,但是在spring和mybatis整合之后,规定事务管理必须由springtx来管理。

Spring管理事务,有两种方案:

  • 编程式事务:也就是在具体业务的执行区域,手动编写代码控制事务
    • 需要手动给每个方法编写事务代码,
    • 代码大量冗余,业务代码不灵活
  • 声明式事务:通过AOP的方式,在配置文件中声明事务的属性
    • 在applicationContext.xml中配置AOP,声明哪些方法需要事务管理
    • 业务代码不需要任何改动,就可以实现事务管理

在学习声明式事务之前,先来回忆一下,AOP是如何配置的。

AOP配置过程:
- 目标方法
- 定义切面类,定义增强的方法
- 配置文件中织入

那声明式事务也是通过AOP方式实现的,应该如何配置呢?

事务配置过程:
- 目标方法
- 使用spring提供的事务管理器,即增强的方法(不需要我们自己去定义了)
- 配置文件中织入

8.1 声明式事务配置使用

接下来,我们使用一个例子,来学习spring事务管理。
需求:数据库中有account表,我们对其进行操作,实现转账功能。

  1. 编写实体类Account.java
package org.demo.entity;

import java.io.Serializable;

public class Account implements Serializable {
   private Integer id;
   private String name;
   private Double balance;

   @Override
   public String toString() {
       return "Account{" +
               "id=" + id +
               ", name='" + name + '\'' +
               ", balance=" + balance +
               '}';
   }

   public Integer getId() {
       return id;
   }

   public void setId(Integer id) {
       this.id = id;
   }

   public String getName() {
       return name;
   }

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

   public Double getBalance() {
       return balance;
   }

   public void setBalance(Double balance) {
       this.balance = balance;
   }
}

  1. 编写mapper接口及其对应的xml映射文件AccountMapper.java、AccountMapper.xml

AccountMapper.java

package org.demo.mapper;

import org.apache.ibatis.annotations.Param;

public interface AccountMapper {
   int addMoney(@Param("toId") int toId, @Param("balance")int balance);
   int delMoney(@Param("fromId") int fromId,@Param("balance") int balance);
}

AccountMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
       "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.demo.mapper.AccountMapper">
   <update id="addMoney">
       update account set balance = balance + #{balance} where id = #{toId}
   </update>

   <update id="delMoney">
       update account set balance = balance - #{balance} where id = #{fromId}
   </update>
</mapper>
  1. 编写service接口及其实现类AccountService.java、AccountServiceImpl.java

AccountService.java

package org.demo.service;

public interface AccountService {
    boolean transferMoney(Integer fromId,Integer toId,Integer balance);
}

AccountServiceImpl.java

package org.demo.service.impl;

import org.demo.mapper.AccountMapper;
import org.demo.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class AccountServiceImpl implements AccountService {
    @Autowired
    private AccountMapper accountMapper;

    @Override
    public boolean transferMoney(Integer fromId, Integer toId, Integer balance) {
        System.out.println("转账开始");
        //减少
        int i = accountMapper.delMoney(fromId, balance);

        i = 100/0;
        //增加
        int j = accountMapper.addMoney(toId, balance);

        return i == 1 && j == 1;
    }
}
  1. 配置spring事务管理器
<!--    5. 配置事务管理 相当于切面-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--            需要注入数据源-->
    <property name="dataSource" ref="dataSource"/>
</bean>
<!--    6.配置事务管理的增强方法(配置事务特性)-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<!--        配置目标方法-->
    <tx:attributes>
<!--            目标方法名-->
        <tx:method name="transferMoney"/>
<!--            除此之外还可以模糊匹配写 匹配transferXxx()之类的方法-->
<!--            <tx:method name="transfer*"/>-->
<!--            所有方法都需要事务-->
        <tx:method name="*"/>
    </tx:attributes>
</tx:advice>

<!--    7. 织入AOP(将增强的方法作用到目标方法上)-->
<aop:config>
    <aop:pointcut id="myAspect" expression="execution(* org.demo.service.impl.*.*(..))"/>
    <aop:advisor advice-ref="txAdvice" pointcut-ref="myAspect"/>
</aop:config>
  1. 测试

为了验证我们的事务管理是否配置成功,我已经在业务层故意写了一个异常,接下来我们可以编写一个测试类,测试转账功能,查看如果出现异常,是否会进行回滚。

    @Test
    public void testTransfer(){
        String url ="applicationContext.xml";
        ApplicationContext context = new ClassPathXmlApplicationContext(url);
        AccountService accountServiceImpl = context.getBean("accountServiceImpl", AccountService.class);
        System.out.println(accountServiceImpl.transferMoney(1, 2, 300));
    }

首先先将刚才配置的事务管理注释掉,运行测试类,输出结果为:

转账开始
DEBUG [main] - ==>  Preparing: update account set balance = balance - ? where id = ?
DEBUG [main] - ==> Parameters: 300(Integer), 1(Integer)
DEBUG [main] - <==    Updates: 1

java.lang.ArithmeticException: / by zero
...

数据库查看到信息,余额变为700,说明在我们没有配置事务管理器前,是没有事务的,出现了异常,余额仍然出现了变动,这显然是不对的。

接下来取消注释掉我们的事务配置,再次运行测试类,输出结果为:

转账开始
DEBUG [main] - ==>  Preparing: update account set balance = balance - ? where id = ?
DEBUG [main] - ==> Parameters: 300(Integer), 1(Integer)
DEBUG [main] - <==    Updates: 1

java.lang.ArithmeticException: / by zero

数据库查看信息,可以发现,数据库中数据并没有发生变化,说明业务发生了异常以后,事务自动进行了回滚,确保了数据的一致性。

8.2 事务属性配置

  1. propagation:事务的传播行为,有以下几种取值:

    • REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是默认值(适合增删改高频率方法)。
    • SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行(适合查询等方法)。
    • MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
    • REQUIRES_NEW:创建一个新的事务,无论当前是否存在事务。
    • NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
    • NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
  2. isolation:事务的隔离级别,有以下几种取值:

    • DEFAULT:使用数据库默认的隔离级别。
    • READ_UNCOMMITTED:最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
    • READ_COMMITTED:允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生(ORACLE数据库默认级别)。
    • REPEATABLE_READ:对同一字段的多次读取结果都是一致的,除非数据被本身事务外的事务改变,可以阻止脏读和不可重复读,但幻读仍有可能发生(MYSQL数据库默认级别)。
    • SERIALIZABLE:最高的隔离级别,完全串行化的读写,并发控制,防止脏读、幻读和不可重复读。
  3. timeout:事务的超时时间,单位为秒,默认为-1,表示没有超时时间限制。

  4. read-only:是否为只读事务,true表示只读(只能查询),false表示可读写。

  5. rollback-for:出现哪些异常,事务回滚。

  6. no-rollback-for:出现哪些异常,不回滚事务。

8.3 注解开发事务

使用注解开发事务,可以大大减少配置的复杂度,提高开发效率。例如配置文件中的<tx:advice><aop:config>标签,都可以用注解来替代。

注解开发事务很简单,只需要在需要事务的方法上添加注解即可,注解有以下几种:

  • @Transactional:最常用的注解,可以应用到方法、类、接口上,表示该方法的事务属性。
  • @EnableTransactionManagement:开启事务管理,相当于配置文件中的<tx:annotation-driven>标签。
  • @Transactional(propagation=Propagation.REQUIRED, isolation=Isolation.DEFAULT, timeout=30, rollbackFor=Exception.class):自定义事务属性。

接下来对上面进行改动,使用注解开发:

  1. 修改业务层实现类,使用注解
package org.demo.service.impl;

import org.demo.mapper.AccountMapper;
import org.demo.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class AccountServiceImpl implements AccountService {
    @Autowired
    private AccountMapper accountMapper;

    @Transactional
    @Override
    public boolean transferMoney(Integer fromId, Integer toId, Integer balance) {
        System.out.println("转账开始");
        //减少
        int i = accountMapper.delMoney(fromId, balance);

        i = 100/0;
        //增加
        int j = accountMapper.addMoney(toId, balance);

        return i == 1 && j == 1;
    }
}

  1. 修改配置文件,启用注解

<!--    5. 配置事务管理 相当于切面-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--            需要注入数据源-->
        <property name="dataSource" ref="dataSource"/>
    </bean>
<!--    6.配置事务管理的增强方法(配置事务特性)-->
<!--    <tx:advice id="txAdvice" transaction-manager="transactionManager">-->
<!--&lt;!&ndash;        配置目标方法&ndash;&gt;-->
<!--        <tx:attributes>-->
<!--&lt;!&ndash;            目标方法名&ndash;&gt;-->
<!--            <tx:method name="transferMoney"/>-->
<!--&lt;!&ndash;            除此之外还可以模糊匹配写 匹配transferXxx()之类的方法&ndash;&gt;-->
<!--&lt;!&ndash;            <tx:method name="transfer*"/>&ndash;&gt;-->
<!--&lt;!&ndash;            所有方法都需要事务&ndash;&gt;-->
<!--            <tx:method name="*"/>-->
<!--        </tx:attributes>-->
<!--    </tx:advice>-->

<!--&lt;!&ndash;    7. 织入AOP(将增强的方法作用到目标方法上)&ndash;&gt;-->
<!--    <aop:config>-->
<!--        <aop:pointcut id="myAspect" expression="execution(* org.demo.service.impl.*.*(..))"/>-->
<!--        <aop:advisor advice-ref="txAdvice" pointcut-ref="myAspect"/>-->
<!--    </aop:config>-->

<!--    使用注解开发事务,将6/7替换为8-->
    <tx:annotation-driven transaction-manager="transactionManager"/>
  1. 测试输出
转账开始
DEBUG [main] - ==>  Preparing: update account set balance = balance - ? where id = ?
DEBUG [main] - ==> Parameters: 300(Integer), 1(Integer)
DEBUG [main] - <==    Updates: 1

java.lang.ArithmeticException: / by zero

可以看到,使用注解大大减少了配置文件的编写,并且也成功实现了业务异常后事务的回滚。