Loading... # 1 Spring 简介 > 基于【尚硅谷】SSM框架全套教程av258534438整理的笔记 ## 1.1 Spring概述 Spring 是最受欢迎的企业级 Java 应用程序开发框架,数以百万的来自世界各地的开发人员使用 Spring 框架来创建性能好、易于测试、可重用的代码。 官网地址:[https://spring.io](https://spring.io/) Spring家族:https://spring.io/projects ## 1.2 Spring Framework Spring 基础框架,可以视为 Spring 基础设施,基本上任何其他 Spring 项目都是以 Spring Framework 为基础的。 ### 1.2.1 Spring Framework特性 - 非侵入式:使用 Spring Framework 开发应用程序时,Spring 对应用程序本身的结构影响非常小。对领域模型可以做到零污染;对功能性组件也只需要使用几个简单的注解进行标记,完全不会 破坏原有结构,反而能将组件结构进一步简化。这就使得基于 Spring Framework 开发应用程序时结构清晰、简洁优雅。 - 控制反转:IOC——Inversion of Control,**翻转资源获取方向**。把自己创建资源、向环境索取资源 变成环境将资源准备好,我们享受资源注入。 - 面向切面编程:AOP——Aspect Oriented Programming,**在不修改源代码的基础上增强代码功能**。 - 容器:Spring IOC 是一个容器,因为它包含并且管理组件对象的生命周期。组件享受到了容器化 的管理,替程序员屏蔽了组件创建过程中的大量细节,极大的降低了使用门槛,大幅度提高了开发效率。 - 组件化:Spring 实现了使用简单的组件配置组合成一个复杂的应用。在 Spring 中可以使用 XML 和 Java 注解组合这些对象。这使得我们可以基于一个个功能明确、边界清晰的组件有条不紊的搭 建超大型复杂应用系统。 - 声明式:很多以前需要编写代码才能实现的功能,现在只需要声明需求即可由框架代为实现。 - 一站式:在 IOC 和 AOP 的基础上可以整合各种企业应用的开源框架和优秀的第三方类库。而且 Spring 旗下的项目已经覆盖了广泛领域,很多方面的功能性需求可以在 Spring Framework 的基 础上全部使用 Spring 来实现。 ### 1.2.2 Spring Framework五大功能模块 | 功能模块 | 功能介绍 | | ------------------------- | ------------------------------------------------------------- | | Core Container | 核心容器,在 Spring 环境下使用任何功能都必须基于 IOC 容器。 | | AOP&Aspects | 面向切面编程 | | Testing | 提供了对 junit 或 TestNG 测试框架的整合。 | | Data Access/Integration | 提供了对数据访问/集成的功能。 | | Spring MVC | 提供了面向Web应用程序的集成功能。 | # 2 IOC ## 2.1 IOC容器 ### 2.1.1 IOC思想 IOC:Inversion of Control,翻译过来是**反转控制**。 **反转控制**的思想完全颠覆了应用程序组件获取资源的传统方式:反转了资源的获取方向——改由容器主 动的将资源推送给需要的组件,开发人员不需要知道容器是如何创建资源对象的,只需要提供接收资源 的方式即可,极大的降低了学习成本,提高了开发的效率。这种行为也称为查找的被动形式。 DI:Dependency Injection,翻译过来是**依赖注入**。 DI 是 IOC 的另一种表述方式:即组件以一些预先定义好的方式(例如:setter 方法)接受来自于容器 的资源注入。相对于IOC而言,这种表述更直接。 结论:IOC 就是一种反转控制的思想, 而 **DI 是对 IOC 的一种具体实现**。 ### 2.1.2 IOC容器在Spring中的实现 Spring 的 IOC 容器就是 IOC 思想的一个落地的产品实现。IOC 容器中管理的组件也叫做 bean。在创建 bean 之前,首先需要创建 IOC 容器。Spring 提供了 IOC 容器的两种实现方式: 1. BeanFactory 这是 IOC 容器的基本实现,是 Spring 内部使用的接口。面向 Spring 本身,不提供给开发人员使用。 2. ApplicationContext BeanFactory 的子接口,提供了更多高级特性。面向 Spring 的使用者,几乎所有场合都使用 ApplicationContext 而不是底层的 BeanFactory。 3. ApplicationContext的主要实现类 | 类型名 | 简介 | | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | ClassPathXmlApplicationContext | 通过读取**类路径**下的 XML 格式的配置文件创建 IOC 容器 对象 | | FileSystemXmlApplicationContext | 通过**文件系统路径**读取 XML 格式的配置文件创建 IOC 容 器对象 | | ConfigurableApplicationContext | ApplicationContext 的**子接口**,包含一些**扩展方法** refresh() 和 close() ,让 ApplicationContext 具有启动、 关闭和刷新上下文的能力 | | WebApplicationContext | 专门为**Web 应用**准备,基于 Web 环境创建 IOC 容器对 象,并将对象引入存入 ServletContext 域中。 | ## 2.2 基于XML管理bean ### 2.2.1 一个入门案例 1. 创建一个空的Maven项目 2. 所需依赖 ```xml <dependency> <!-- 基于Maven依赖传递性,导入spring-context依赖即可导入当前所需所有jar包 --> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.22</version> </dependency> <!-- https://mvnrepository.com/artifact/junit/junit --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency> ``` 3. 项目的结构 <img src="https://snz04pap001files.storage.live.com/y4myqyVNF1693mLsQotPzu0MQvL5qB3rW8pwyJiINrRooFS4El6S5pXmPlIjPTExFWggbAjdkaUNlczed4WvNCAi8SFm8l_Xj0fGnoxHmPAvLk98XRv_dZRQQLdfGCzOkwhPucgbtVu4NjmUEpKA5Gf94wxx0a2iSoMTHihTJdWQ7ZtGT3hPU58G3gI2sXiRxmt?width=635&height=423&cropmode=none" alt="image-20220919112032450" style="zoom:80%;" style=""> 4. 创建HelloWorld类 ```java public class HelloWorld { public void say() { System.out.println("Hello Spring !"); } } ``` 5. Spring配置文件中配置bean ```xml <?xml version="1.0" encoding="UTF-8"?> <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/beans" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!-- bean:配置一个bean对象,将对象交给IOC容器管理 属性: id:bean的唯一标识,不能重复 class:设置bean对象所对应的类型 --> <bean id="helloWorld" class="pojo.HelloWorld"></bean> </beans> ``` 6. 测试类 ```java public class HelloWorldTest { @Test public void test() { // 获取IOC容器 // 使用文件系统路径的路径 // ApplicationContext ioc = new FileSystemXmlApplicationContext("E:\\Work\\StudySpring\\spring_helloworld\\src\\main\\resources\\applicationContext.xml") ApplicationContext ioc = new ClassPathXmlApplicationContext("applicationContext.xml"); // 获取IOC容器中的bean HelloWorld helloworld = (HelloWorld) ioc.getBean("helloWorld"); helloworld.say(); } } ``` 7. 思路 ![image-20220919112359994](https://snz04pap001files.storage.live.com/y4mvLu31qjLKJ65uUhOQhBGx4_dn_aNlMtGDn_xI80EwAV8DNjhteWDIs7cvhOEG37_IxowY2ZftB5ZDhXpJ8GuWdEsy5Jr__33fBw_ab72qVyuKFG2irc5Gt3bb09upl-LKam1UHAVWLSsQZ4PuglNe5DWGeoos_vmfJSdAL3NqMn3U8sjZq7VZV06FcGGvlvc?width=751&height=319&cropmode=none) ### 2.2.2 获取bean的三种方式 xml文件中配置了如下的bean ```java <bean id="studentOne" class="pojo.Student"></bean> ``` 1. 根据bean的id获取 由于 id 属性指定了 bean 的唯一标识,所以根据 bean 标签的 id 属性可以精确获取到一个组件对象。 ```java public class IocByXMLTest { @Test public void testIOC() { // 获取IOC容器 ApplicationContext ioc = new ClassPathXmlApplicationContext("applicationContext.xml"); // 获取bean // 1.根据id获取 Student student1 = (Student) ioc.getBean("studentOne"); System.out.println(student1); } ``` 2. 根据bean的类型获取 注意:根据类型bean时,要求IOC容器中有且**只有一个类型匹配**的bean 若**没有任何一个类型匹配**的bean,此时抛出异常:NoSuchBeanDefinitionException 若有**多个类型匹配**的bean,此时抛出异常:NoUniqueBeanDefinitionException ```java public class IocByXMLTest { @Test public void testIOC() { // 获取IOC容器 ApplicationContext ioc = new ClassPathXmlApplicationContext("applicationContext.xml"); // 2.根据类型获取 pojo.Student student2 = ioc.getBean(pojo.Student.class); System.out.println(student2); } ``` 3. 根据bean的id和类型获取 ```java public class IocByXMLTest { @Test public void testIOC() { // 获取IOC容器 ApplicationContext ioc = new ClassPathXmlApplicationContext("applicationContext.xml"); // 3.根据类型和id获取 Student student3 = ioc.getBean("studentOne", Student.class); System.out.println(student3); } ``` 4. 结论: * 根据类型来获取bean时,在**满足bean唯一性的前提**下其实只是看:【对象 **instanceof** 指定的类型】的返回结果只要返回的时true就可以认定为和类型匹配,能够获取到 * 即通过bean的**类型**、bean所**继承的类的类型**、bean所**实现的接口**的类型都可以获取bean 5. 扩展 - 如果组件类实现了接口,根据接口类型可以获取 bean 吗? 可以,前提是bean唯一 - 如果一个接口有多个实现类,这些实现类都配置了 bean,根据接口类型可以获取 bean 吗? 不行,因为bean不唯一 ## 2.3 依赖注入 学生类Student有以下属性,并且有无参,有参构造,Setter和Getter函数 ```java public class Student { private Integer sid; private String sname; private Integer age; private String gender; private Double score; private Clazz clazz; private String[] hobby; private Map<String, Teacher> teacherMap; } ``` ### 2.3.1 Setter注入 setter方式注入 - property:通过成员变量的**set方法**进行赋值 - name:设置需要赋值的属性名(和set方法有关) - value:设置为属性所赋的值 ```xml <bean id="studentTwo" class="pojo.Student"> <property name="sid" value="1001"></property> <property name="sname" value="张三"></property> <property name="age" value="18"></property> <property name="gender" value="男"></property> </bean> ``` ### 2.3.2 构造器注入 Student类有以下有参构造 ```java public Student(Integer sid, String sname, String gender, Integer age) { this.sid = sid; this.sname = sname; this.age = age; this.gender = gender; } public Student(Integer sid, String sname, Integer age, String[] hobby) { this.sid = sid; this.sname = sname; this.age = age; this.hobby = hobby; } public Student(Integer sid, String sname, String gender, Double score) { this.sid = sid; this.sname = sname; this.score = score; this.gender = gender; } ``` 构造器通过调用有参构造进行赋值,constructor-arg标签还有两个属性可以进一步描述构造器参数: - index属性:指定参数所在位置的索引(从0开始) - name属性:指定参数名 可以发现赋值的19有两个有参构造符合,此时需要使用**name属性**指定参数名 ```xml <!-- 构造器注入 --> <bean id="studentThree" class="pojo.Student"> <constructor-arg value="1002"></constructor-arg> <constructor-arg value="李四"></constructor-arg> <constructor-arg value="女"></constructor-arg> <constructor-arg value="19" name="age"></constructor-arg> </bean> ``` ### 2.3.3 特殊值的处理 1. 字面量赋值 > 什么是字面量? > > int a = 10; 声明一个变量a,初始化为10,此时a就不代表字母a了,而是作为一个变量的名字。当我们引用a 的时候,我们实际上拿到的值是10。 而如果a是带引号的:'a',那么它现在不是一个变量,它就是代表a这个字母本身,这就是字面 量。所以字面量没有引申含义,就是我们看到的这个数据本身。 > ```xml <property name="sid" value="1004"></property> ``` 2. null值 ```xml <property name="age"> <null /> </property> ``` 3. xml实体 小于号在XML文档中用来定义标签的开始,不能随便使用 ```xml <!-- 特殊值的处理 <:< >:> 或使用CDATA节,其中的内容会原样解析 idea快捷方式CD CDATA节是xml中一个特殊的标签,不能写成属性 --> <property name="sname" value="<王五>"></property> ``` 4. 使用CDATA节 CDATA中的C代表Character,是文本、字符的含义,CDATA就表示纯文本数据,所以CDATA节中写什么符号都随意 ```xml <property name="sname"> <value><![CDATA[<王五>]]></value> </property> ``` ### 2.3.4 为类类型的属性赋值 1. 引用外部已经声明的bean ```xml <!--为类类型属性赋值--> <bean id="studentFive" class="pojo.Student"> <!-- 引用外部的bean ref:引用IOC容器中某个bean的id --> <property name="clazzOne" ref="clazz"></property> </bean> <!--外部的bean--> <bean id="clazzOne" class="pojo.Clazz"></bean> ``` 2. 内部bean的方式 ```xml <bean id="studentFive" class="pojo.Student"> <property name="clazz"> <!--内部bean,只能在当前bean的内部使用,不能直接通过IOC容器获取--> <bean id="clazzInner" class="pojo.Clazz"> <property name="cid" value="2002"></property> <property name="cname" value="2班"></property> </bean> </property> </bean> ``` 3. 级联属性赋值 ```xml <bean id="studentFive" class="pojo.Student"> <!-- 一定先引用某个bean为属性赋值,才可以使用级联方式更新属性 --> <property name="clazz" ref="clazzOne"></property> <property name="clazz.clazzId" value="3333"></property> <property name="clazz.clazzName" value="最强王者班"></property> </bean> <!--外部的bean--> <bean id="clazzOne" class="pojo.Clazz"></bean> ``` ### 2.3.5 为数组类型属性赋值 ```xml <bean id="studentFive" class="pojo.Student"> <!--注入数组--> <property name="hobby"> <array> <value>吃饭</value> <value>喝酒</value> </array> </property> </bean> ``` ### 2.3.6 为集合类型属性赋值 可以写在内部或者为外部 注意:使用util:list、util:map标签必须**引入相应的命名空间**,可以通过idea的提示功能选择 ```xml <!--配置一个集合类型的bean,需要使用util的约束--> <util:list id="studentList"> <ref bean="studentOne"></ref> <ref bean="studentTwo"></ref> <ref bean="studentThree"></ref> </util:list> <!--为集合类型属性赋值--> <bean id="clazzOne" class="pojo.Clazz"> <property name="cid" value="2001"></property> <property name="cname" value="1班"></property> <property name="students" ref="studentList"></property> <!--<property name="students">--> <!-- <list>--> <!-- <ref bean="studentOne"></ref>--> <!-- <ref bean="studentTwo"></ref>--> <!-- <ref bean="studentThree"></ref>--> <!-- </list>--> <!--</property>--> </bean> ``` ### 2.3.7 为Map属性赋值 ```xml <bean id="teacherOne" class="pojo.Teacher"> <property name="tid" value="10001"></property> <property name="tname" value="宝1"></property> </bean> <bean id="teacherTwo" class="pojo.Teacher"> <property name="tid" value="10001"></property> <property name="tname" value="宝2"></property> </bean> <util:map id="teacherMap"> <entry key="10010" value-ref="teacherOne"></entry> <entry key="10020" value-ref="teacherTwo"></entry> </util:map> ``` ### 2.3.8 p命名空间(了解即可) 引入p命名空间后,可以通过以下方式为bean的各个属性赋值 ```xml <bean id="studentSix" class="pojo.Student" p:sid="1006" p:sname="小明" p:teacherMap-ref="teacherMap"> </bean> ``` ### 2.3.9 引入外部的属性文件 这里演示引入外部的jdbc配置文件 ![image-20220919132354550](https://snz04pap001files.storage.live.com/y4mj_Qzs_KAmii8t6u3Iih7VLc1q44Y2hS1KxRsQv6ExYHf5g6fUtk2y8se8fYSNcNPw68TgWb3WNNpBGnxeF8lD81tanLIfIPGwUXuLi3YfuDZhmvH0S3oQn_FKvXgTdJnsQUMmyYG9MciRf6N6afhgmhjhCTukbUCtrZrThswLVSS2MM8woGDDThCu1sy24x7?width=578&height=165&cropmode=none) `jdbc.properties` ```properties jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/ssm?serverTimezone=UTC jdbc.username=root jdbc.password=11111 ``` `spring.datasource.xml` ```xml <!--引入jdbc.properties,之后可以通过${key}的方式访问value--> <context:property-placeholder location="jdbc.properties"></context:property-placeholder> <!--配置数据源bean--> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${jdbc.driver}"></property> <property name="url" value="${jdbc.url}"></property> <property name="username" value="${jdbc.username}"></property> <property name="password" value="${jdbc.password}"></property> </bean> ``` ## 2.4 bean的作用域 1. 概念 在Spring中可以通过配置bean标签的scope属性来指定bean的作用域范围,各取值含义参加下表: | 取值 | 含义 | 创建对象的时机 | | ------------------- | ----------------------------------------- | ----------------- | | singleton(默认) | 在IOC容器中,这个bean的对象始终为单实例 | IOC容器初始化时 | | prototype | 这个bean在IOC容器中有多个实例 | 获取bean时 | 在WebApplicationContext环境下还会有另外两个作用域(但不常用): | 取值 | 含义 | | --------- | ---------------------- | | request | 在一个请求范围内有效 | | session | 在一个会话范围内有效 | 2. 在User类中有如下字段,有参/无参构造,Getter和Setter方法 ```java public class User { private Integer id; private String username; private String password; private Integer age; } ``` 3. 配置bean ```xml <!-- scope属性:取值singleton(默认值),bean在IOC容器中只有一个实例,IOC容器初始化时创建对象 --> <!-- scope属性:取值prototype,bean在IOC容器中可以有多个实例,getBean()时创建对象 --> <bean class="com.atguigu.bean.User" scope="prototype"></bean> ``` 4. 测试 ```java @Test public void testBeanScope(){ ApplicationContext ac = new ClassPathXmlApplicationContext("spring-scope.xml"); User user1 = ac.getBean(User.class); User user2 = ac.getBean(User.class); System.out.println(user1==user2); } ``` 使用singleton单例模式时,返回结果为true 使用prototype多例模式时,返回结果为false ## 2.5 bean的生命周期 1. 具体的生命周期过程 Spring bean的生命周期只有四个主要阶段,其他都是在这四个主要阶段前后的扩展点,这四个阶段是: 1.实例化 Instantiation 2.属性赋值 Populate 3.初始化 Initialization 4.销毁 Destruction 2. 扩展后大致如下 - bean对象创建(调用无参构造器) - 给bean对象设置属性 - bean对象初始化之前操作(由bean的后置处理器负责) - bean对象初始化(需在配置bean时指定初始化方法) - bean对象初始化之后操作(由bean的后置处理器负责) - bean对象就绪可以使用 - bean对象销毁(需在配置bean时指定销毁方法) - IOC容器关闭 3. 修改User类 ```java public class User { private Integer id; private String username; private String password; private Integer age; public User() { System.out.println("生命周期:1、创建对象"); } public User(Integer id, String username, String password, Integer age) { this.id = id; this.username = username; this.password = password; this.age = age; } public Integer getId() { return id; } public void setId(Integer id) { System.out.println("生命周期:2、依赖注入"); this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public void initMethod(){ System.out.println("生命周期:3、初始化"); } public void destroyMethod(){ System.out.println("生命周期:5、销毁"); } @Override public String toString() { return "User{" + "id=" + id + ", username='" + username + '\'' + ", password='" + password + '\'' + ", age=" + age + '}'; } } ``` 4. 配置bean ```xml <!-- 使用init-method属性指定初始化方法 --> <!-- 使用destroy-method属性指定销毁方法 --> <bean class="com.atguigu.bean.User" scope="prototype" init-method="initMethod" destroy-method="destroyMethod"> <property name="id" value="1001"></property> <property name="username" value="admin"></property> <property name="password" value="123456"></property> <property name="age" value="23"></property> </bean> ``` 5. 测试 ```java @Test public void testLife(){ ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-lifecycle.xml"); User bean = ac.getBean(User.class); System.out.println("生命周期:4、通过IOC容器获取bean并使用"); ac.close(); } ``` 6. bean的后置处理器 bean的后置处理器会在生命周期的初始化前后添加额外的操作,需要实现BeanPostProcessor接口,且配置到IOC容器中,需要注意的是,bean后置处理器不是单独针对某一个bean生效,而是针对IOC容器中**所有bean**都会执行 创建bean的后置处理器: ```java public class MyBeanProcessor implements BeanPostProcessor { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { System.out.println("初始化前:" + beanName + " = " + bean); return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { System.out.println("初始化后:" + beanName + " = " + bean); return bean; } } ``` 配置后置处理器 ```xml <!-- bean的后置处理器要放入IOC容器才能生效 --> <bean id="myBeanProcessor" class="com.atguigu.spring.process.MyBeanProcessor"/> ``` ## 2.6 FactoryBean 1. FactoryBean是Spring提供的一种整合第三方框架的常用机制。和普通的bean不同,配置一个 FactoryBean类型的bean,在获取bean的时候得到的并不是class属性中配置的这个类的对象,而是 getObject()方法的返回值。通过这种机制,Spring可以帮我们把复杂组件创建的详细过程和繁琐细节都 屏蔽起来,只把最简洁的使用界面展示给我们。 2. 创建类UserFactoryBean ```java public class UserFactoryBean implements FactoryBean<User> { @Override public User getObject() throws Exception { return new User(); } @Override public Class<?> getObjectType() { return User.class; } } ``` 3. 配置bean ```xml <bean id="user" class="com.atguigu.bean.UserFactoryBean"></bean> ``` 4. 测试 ```java @Test public void testUserFactoryBean(){ //获取IOC容器 ApplicationContext ac = new ClassPathXmlApplicationContext("spring-factorybean.xml"); User user = (User) ac.getBean("user"); System.out.println(user); } ``` ## 2.7 基于XML的自动装配 > 自动装配: 根据指定的策略,在IOC容器中匹配某一个bean,自动为指定的bean中所依赖的类类型或接口类型属性赋值 使用bean标签的**autowire属性**设置自动装配效果 以下演示将Dao自动装配注入到Service层中,Service自动装配到Controller层中 ```xml <bean id="userController" class="controller.UserController" autowire="byType"></bean> <bean id="userService" class="service.impl.UserServiceImpl" autowire="byType"></bean> <bean id="userDao" class="dao.impl.UserDaoImpl"></bean> ``` 相当于手动的 ```xml <bean id="userController" class="controller.UserController"> <property name="userService" ref="userService"></property> </bean> <bean id="userService" class="service.impl.UserServiceImpl"> <property name="userDao" ref="userDao"></property> </bean> <bean id="userDao" class="dao.impl.UserDaoImpl"></bean> ``` 自动装配有两种方式 1. byType byType:根据类型匹配IOC容器中的某个兼容类型的bean,为属性自动赋值。若在IOC中,没有任何一个兼容类型的bean能够为属性赋值,则该属性不装配,即值为默认值 `null`。若在IOC中,有多个兼容类型的bean能够为属性赋值,则抛出异常 `NoUniqueBeanDefinitionException` 2. byName byName:将自动装配的属性的属性名,作为bean的id在IOC容器中匹配相对应的bean进行赋值, 当容器中有多个相同类型的bean可以被匹配到,可以使用byName。 一般使用byType,因为在正常开发过程中ioc容器中只会放一次一个类型的bean ## 2.8 基于注解管理bean ### 2.8.1 注解 标识组件常用注解: - @Component:将类标识为普通组件 - @Controller:将类标识为控制层组件 - @Service:将类标识为业务层组件 - @Repository:将类标识为持久层组件 @service的源码如下,Service就是Component扩展的新的名字 ```java @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Component public @interface Service { @AliasFor( annotation = Component.class ) String value() default ""; } ``` 这四个注解是相同的,通过源码可以得知,@Controller、@Service、@Repository这三个注解只是在@Component注解的基础上起了三个新的名字。 ### 2.8.2 扫描 1. 创建控制层组件UserController ```java @Controller public class UserController { } ``` 2. 创建接口UserService ```java public interface UserService { } ``` 3. 创建业务层组件UserServiceImpl ```java @Service public class UserServiceImpl implements UserService { } ``` 4. 创建接口UserDao ```java public interface UserDao { } ``` 5. 创建持久层组件UserDaoImpl ```java @Repository public class UserDaoImpl implements UserDao { } ``` 6. 配置扫描 ```xml <!--扫描组件--> <context:component-scan base-package="spring"></context:component-scan> ``` 7. 排除/包含扫描 ```xml <!-- context:exclude-filter:排除扫描 type:设置排除扫描的方式 type="annotation|assignable" annotation:根据注解的类型进行排除,expression需要设置排除的注解的全类名 assignable:根据类的类型进行排除,expression需要设置排除的类的全类名 context:include-filter:包含扫描 注意:需要在context:component-scan标签中设置use-default-filters="false" use-default-filters="true",所设置的包下所有的类都需要扫描,此时可以使用排除扫描 use-default-filters="false",所设置的包下所有的类都不需要扫描,此时可以使用包含扫描 --> <!--扫描组件--> <context:component-scan base-package="spring" use-default-filters="true"> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/> <context:exclude-filter type="assignable" expression="spring.service.impl.UserServiceImpl"/> </context:component-scan> ``` 使用包含扫描`use-default-filters="false"` ```xml <context:component-scan base-package="spring" use-default-filters="true"> <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/> </context:component-scan> ``` ### 2.8.3 基于注解的自动装配 @Autowired注解、@Qualifier注解 1. 在成员变量上直接标记@Autowired注解即可完成自动装配,以后在项目中正式用法就是如此 ```java @Controller public class UserController { @Autowired @Qualifier("userServiceImpl") private UserService userService; public void saveUser() { userService.saveUser(); } } ``` 2. @Autowired注解也可以标记在setter方法和有参构造上 3. @Autowired注解的原理 ![image-20220919164353064](https://snz04pap001files.storage.live.com/y4m_hXn2edlUvgXEhBbLzh_1zeJzbVSYC7W3-i6EWg4IwRJqfxYSHZjn_-w_cQTRCJPRYY54LIaDZe0iVFM2DhkXS2elq9Bf04aMfTVx8XG-pSauhqZcRCk1oDjxQhjp1yPcRn_cDO3qjfUesAozIs2RGLuJCTp0jycJK3VWLX12f_lvaRPWWrxCud6DDmiP9l7?width=539&height=475&cropmode=none) - 首先根据所需要的组件类型到IOC容器中查找 - 能够找到唯一的bean:直接执行装配 - 如果完全找不到匹配这个类型的bean:装配失败 - 和所需类型匹配的bean不止一个 - 没有@Qualifier注解:根据@Autowired标记位置成员变量的变量名作为bean的id进行 匹配 - 能够找到:执行装配 - 找不到:装配失败 - 使用@Qualifier注解:根据@Qualifier注解中指定的名称作为bean的id进行匹配 - 能够找到:执行装配 - 找不到:装配失败 # 3 AOP ## 3.1 代理 ### 3.1.1 代理模式 计算器的接口类Calculator,实现加、减、乘、除 ```java public interface Calculator { int add(int i, int j); int sub(int i, int j); int mul(int i, int j); int div(int i, int j); } ``` 一个计算器的实现类CalculatorImpl ```java public class CalculatorImpl implements Calculator { @Override public int add(int i, int j) { // System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j); int result = i + j; System.out.println("方法内部 result = " + result); // System.out.println("[日志] add 方法结束了,结果是:" + result); return result; } @Override public int sub(int i, int j) { // System.out.println("[日志] sub 方法开始了,参数是:" + i + "," + j); int result = i - j; System.out.println("方法内部 result = " + result); // System.out.println("[日志] sub 方法结束了,结果是:" + result); return result; } @Override public int mul(int i, int j) { // System.out.println("[日志] mul 方法开始了,参数是:" + i + "," + j); int result = i * j; System.out.println("方法内部 result = " + result); // System.out.println("[日志] mul 方法结束了,结果是:" + result); return result; } @Override public int div(int i, int j) { // System.out.println("[日志] div 方法开始了,参数是:" + i + "," + j); int result = i / j; System.out.println("方法内部 result = " + result); // System.out.println("[日志] div 方法结束了,结果是:" + result); return result; } } ``` > 针对带日志功能的实现类,我们发现有如下缺陷: > > 1. 对核心业务功能有干扰,导致程序员在开发核心业务功能时分散了精力 > 2. 附加功能分散在各个业务功能方法中,不利于统一维护 简介: 二十三种设计模式中的一种,属于结构型模式。它的作用就是通过提供一个代理类,让我们在调用目标 方法的时候,不再是直接对目标方法进行调用,而是通过代理类**间接调用**。让不属于目标方法核心逻辑 的代码从目标方法中剥离出来——**解耦**。调用目标方法时先调用代理对象的方法,减少对目标方法的调 用和打扰,同时让附加功能能够集中在一起也有利于统一维护。 <img src="https://snz04pap001files.storage.live.com/y4miXcwKJ6ffHdMN-JqWEJB27jHa7EWYn4hQQZ39VP5G9L8UWRkPah3XwB9BxLmm3Gu1LLW1CyLKRPhSOPRBOYjNTT6BZwZljzQWNjw9IKF_V86ZFzLV3z_SGEhvqdR1vPizWm2j-nr1LM4V-PgoI5niCcsGR-PPB9bNmYMmQerX8Fiq4s0KK2xnjGwlwDsyKcr?width=1175&height=770&cropmode=none" alt="image-20220919170916712" style="zoom:50%;" style=""> 相关术语: - 代理:将非核心逻辑剥离出来以后,封装这些非核心逻辑的类、对象、方法。 - 目标:被代理“套用”了非核心逻辑代码的类、对象、方法。 ### 3.1.2 静态代理 创建一个静态代理类`CalculatorStaticProxy`,这里只演示加方法 ```java public class CalculatorStaticProxy implements Calculator { // 将被代理的目标对象声明为成员变量 private CalculatorImpl target; public CalculatorStaticProxy(CalculatorImpl target) { this.target = target; } @Override public int add(int i, int j) { // 附加功能由代理类中的代理方法来实现 System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j); // 通过目标对象来实现核心业务逻辑 int result = target.add(i, j); System.out.println("[日志] add 方法结束了,结果是:" + result); return result; } } ``` ### 3.1.3 动态代理 动态代理有两种: * 1.jdk动态代理,要求必须有接口,最终生成的代理类在com.sun.proxy包下,类名为$proxy+数字 * 2.cglib动态代理,最终生成的代理类会继承目标类,并且和目标类在相同的包下 * 当被代理类没有接口的时候使用 cglib, 有接口则使用JDK ```java public class ProxyFactory { private Object target; public ProxyFactory(Object target) { this.target = target; } public Object getProxy() { /* Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h) ClassLoader loader:指定加载动态生成的代理类的类加载器 Class<?>[] interfaces:获取目标对象实现的所有接口的class对象的数组 InvocationHandler h:设置代理类中的抽象方法如何重写 */ ClassLoader classLoader = this.getClass().getClassLoader(); Class<?>[] interfaces = target.getClass().getInterfaces(); InvocationHandler h = new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object result = null; try { System.out.println("日志,方法:" + method.getName() + ",参数:" + Arrays.toString(args)); // proxy表示代理对象,method表示要执行的方法,args表示要执行的方法的参数 result = method.invoke(target, args); System.out.println("日志,方法:" + method.getName() + ",结果:" + result); } catch (Exception e) { e.printStackTrace(); System.out.println("日志,方法:" + method.getName() + ",异常:" + e); } finally { System.out.println("日志,方法执行完毕"); } return result; } }; return Proxy.newProxyInstance(classLoader, interfaces, h); } } ``` 测试 ```java 当被代理类没有接口的时候使用 cglib, 有接口则使用JDKpublic class ProxyTest { @Test public void testProxy() { // 静态代理测试 CalculatorStaticProxy calculatorStaticProxy = new CalculatorStaticProxy(new CalculatorImpl()); calculatorStaticProxy.add(1, 2); System.out.println("--------------------------------"); // 动态代理测试 ProxyFactory proxyFactory = new ProxyFactory(new CalculatorImpl()); Calculator proxy = (Calculator) proxyFactory.getProxy(); proxy.add(2, 3); } } ``` ## 3.2 AOP ### 3.2.1 AOP概述 AOP(Aspect Oriented Programming)是一种设计思想,是软件设计领域中的面向切面编程,它是面 向对象编程的一种补充和完善,它以通过预编译方式和运行期动态代理方式实现在不修改源代码的情况 下给程序动态统一添加额外功能的一种技术。 ### 3.2.2 相关术语 1. 横切关注点 从每个方法中抽取出来的同一类非核心业务。在同一个项目中,我们可以使用多个横切关注点对相关方 法进行多个不同方面的增强。 2. 通知 - 前置通知:在被代理的目标方法**前**执行 - 返回通知:在被代理的目标方法**成功结束**后执行(寿终正寝) - 异常通知:在被代理的目标方法**异常结束**后执行(死于非命) - 后置通知:在被代理的目标方法**最终结束**后执行(盖棺定论) - 环绕通知:使用try...catch...finally结构围绕**整个**被代理的目标方法,包括上面四种通知对应的所有位置 <img src="https://snz04pap001files.storage.live.com/y4m35n0oNmenNmJhI4ixmTA7H2h3_-S0olBNelBS9DZ-j9ASKCWypnMDnw1aQRD-rg4GBoR_qrvQlnkbKDvxKs4PzVQrZte25Pt1iLzDWgUfhpwi5FuQyY9kB4hwpZf0bKov6213FfrD_oe7MynLfanzEhXAWqBP0w3s8tq6dbR7P_I-aeylqrRCrTu-INeMtSd?width=1208&height=811&cropmode=none" alt="image-20220919184513724" style="zoom:50%;" style=""> 3. 切面 封装通知方法的类。 <img src="https://snz04pap001files.storage.live.com/y4mmw11bdGk7BRou5CWIUkyBr6vCMladrHCQOPy0heJHyzkQIuV6HXUPY0PxGfVa0sqUjLFYBasLgxQDf_qldcwKZegQZVp1Ix01HVHsTnob1A3V5nYMDVLqan5KAXq9m-bcKe8QcwWUrY9wYUaRdJu-Ns8Esi8ToqhtQRMcdOSJVrpgbCgCSHapi9fimeMhyji?width=1189&height=778&cropmode=none" alt="image-20220919184541691" style="zoom:50%;" style=""> 4. 目标 被代理的目标对象 5. 代理 向目标对象应用通知之后创建的代理对象。 6. 连接点 这也是一个纯逻辑概念,不是语法定义的。 把方法排成一排,每一个横切位置看成x轴方向,把方法从上到下执行的顺序看成y轴,x轴和y轴的交叉 点就是连接点。 <img src="https://snz04pap001files.storage.live.com/y4mf6Ad5sXt7Y5UF-XD_rvkW10Q9HiFh1X3wE-F7diubn6rQxIqIYNb5KU38qbTPoRtkqxNqYA6HZjATYGNiSJC3AZ2PNTg-3rLkHTubFaw6MjlxVjs5_N4tlGTKDEJUdBbpI0cQZjhtNI_aTjmUK3YJ_1uE8WRhE-oAF3I3g0rRPPtt1iWYGq8tyYuEiiQ4yzv?width=1143&height=709&cropmode=none" alt="image-20220919184608471" style="zoom:50%;" style=""> 7. 切入点 定位连接点的方式。每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物(从逻辑上来说)。如果把连接点看作数据库中的记录,那么切入点就是查询记录的 SQL 语句。 Spring 的 AOP 技术可以通过切入点定位到特定的连接点。切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。 ### 3.2.3 作用 - **简化代码**:把方法中固定位置的重复的代码抽取出来,让被抽取的方法更专注于自己的核心功能,提高内聚性。 - **代码增强**:把特定的功能封装到切面类中,看哪里有需要,就往上套,被套用了切面逻辑的方法就 被切面给增强了。 ## 3.3 基于注解的AOP ### 3.3.1 技术说明 ![image-20220919185044914](https://snz04pap001files.storage.live.com/y4m6V_4LyGUSTukTuK9tn8351c6zhyI-1kFeD_YiAoezwTMA8H_0v_wUCKjFFJcc7KM-69vnkqKf2unKaaPLfbNQ7Ttqr5tzMcH3daJqvW0C5ETJWTzoxDkoZjNuxzkA4SqHq-f6zWKrnE7obrxBElP06lqiQxsh5hUcnTx2IrQae4jkpiQE_LJ-R9qCQi_3b1d?width=745&height=599&cropmode=none) - 动态代理(InvocationHandler):JDK原生的实现方式,需要被代理的目标类必须实现接口。因为这个技术要求代理对象和目标对象实现同样的接口(兄弟两个拜把子模式)。 - cglib:通过继承被代理的目标类(认干爹模式)实现代理,所以不需要目标类实现接口。 - AspectJ:本质上是静态代理,将代理逻辑“织入”被代理的目标类编译得到的字节码文件,所以最终效果是动态的。weaver就是织入器。Spring只是借用了AspectJ中的注解。 ### 3.3.2 AOP的依赖 ```xml <!-- spring-aspects会帮我们传递过来aspectjweaver --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>5.3.1</version> </dependency> ``` 目标类依然是上面的计算器 ### 3.3.3 创建切面类 切面类和目标类都需要注册到IOC容器中,使用@Aspect标识将当前组件标识为切面组件,之后要去Spring的配置文件中开启基于注解的AOP功能 ```java @Component @Aspect // 将当前组件标识为切面组件 public class LoggerAspect { } ``` ### 3.3.4 开启基于注解的AOP功能 配置xml ```xml <!-- AOP的注意事项: 切面类和目标类都需要交给IOC容器管理 切面类必须通过@Aspect注解标识为一个切面 在Spring的配置文件中设置<aop:aspectj-autoproxy />开启基于注解的AOP功能 --> <context:component-scan base-package="com.zerolouis.spring.annotation"></context:component-scan> <!--开启基于注解的AOP功能--> <aop:aspectj-autoproxy/> ``` ### 3.3.5 各种通知 ```java @Component @Aspect // 将当前组件标识为切面组件 public class LoggerAspect { @Pointcut("execution(* com.zerolouis.spring.annotation.CalculatorImpl.*(..))") public void pointCut() { } // @Before("execution(public int com.zerolouis.spring.annotation.CalculatorImpl.add(int,int))") // @Before("execution(* com.zerolouis.spring.annotation.CalculatorImpl.*(..))") @Before("pointCut()") public void beforeAdviceMethod(JoinPoint joinPoint) { // 获取连接点所对应方法的方法名 Signature signature = joinPoint.getSignature(); // 获取连接点所对应方法的参数 Object[] args = joinPoint.getArgs(); System.out.println("LoggerAspect,前置通知,方法:" + signature.getName() + ",参数" + Arrays.toString(args)); } @After("pointCut()") public void afterAdviceMethod(JoinPoint joinPoint) { Signature signature = joinPoint.getSignature(); System.out.println("LoggerAspect,后置通知,方法:" + signature.getName() + ",执行完毕"); } /* * 在返回通知中若要获取目标对象方法的返回值 * 只需要通过@AfterReturning注解的returning属性 * 就可以将通知方法的某个参数指定为接受目标对象方法的返回值的参数 * */ @AfterReturning(value = "pointCut()", returning = "result") public void afterReturningAdviceMethod(JoinPoint joinPoint, Object result) { Signature signature = joinPoint.getSignature(); System.out.println("LoggerAspect,返回通知,方法:" + signature.getName() + ",结果:" + result); } @AfterThrowing(value = "pointCut()", throwing = "ex") public void afterThrowingAdviceMethod(JoinPoint joinPoint, Throwable ex) { Signature signature = joinPoint.getSignature(); System.out.println("LoggerAspect,异常通知,方法:" + signature.getName() + ",异常通知:" + ex); } @Around("pointCut()") public Object aroundAdviceMethod(ProceedingJoinPoint joinPoint) { Object result = null; try { System.out.println("环绕通知-->前置通知"); // 表示目标对象方法的执行 result = joinPoint.proceed(); System.out.println("环绕通知-->返回通知"); } catch (Throwable e) { e.printStackTrace(); System.out.println("环绕通知-->异常通知"); } finally { System.out.println("环绕通知-->后置通知"); } return result; } } ``` 在切面中,需要通过指定的注解将方法标识为通知方法 - @Before: 前置通知,在目标对象方法执行之前执行 - @After: 后置通知,在目标对象方法的finally字句中执行 * @AfterReturning: 返回通知,在目标对象方法返回值之后执行 * @AfterThrowing: 异常通知,在目标对象方法的catch字句中执行 * @Around:环绕通知,使用try...catch...finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置 环绕通知和其他通知**可以共存**,但是一般两者使用其一 ### 3.3.6 切入点表达式 切入点表达式:设置在标识通知的注解的value属性中 ```java @Before("execution(public int com.zerolouis.spring.annotation.CalculatorImpl.add(int,int))") @Before("execution(* com.zerolouis.spring.annotation.CalculatorImpl.*(..))") ``` * 第一个*表示任意的访问修饰符和返回值类型 * 第二个*表示类中任意的方法 * ..表示任意的参数列表 * 类的地方也可以使用*,表示包下所有的类 <img src="https://snz04pap001files.storage.live.com/y4mQcnUsWBFEtM__TlCZtR0DZzKKIdZ-s1WxnC323Z9FaDtIoBNBXz5uW8NncoKCAO9eKeTvKt9fJpUzngZg7F4UuQ76NCvoMeaHhgKZYbJ5-x7kL6OMMbskKSaik25IvI0cvzhTLFpMmErOcQ6NvYnHB39bITPg38MebZvJRlU3PH6KcYop2K0cdL1oDi7JB54?width=1356&height=411&cropmode=none" alt="image-20220919195050164" style="zoom:80%;" style=""> ### 3.3.7 重用切入点表达式 1. @Pointcut声明一个公共的切入点表达式 ```java @Pointcut("execution(* com.zerolouis.spring.annotation.CalculatorImpl.*(..))") public void pointCut(){ } ``` 2. 使用方式: - 在同一个切面中使用:`@Before("pointcut()")` - 在不同的切面中使用:`@Before("com.zerolouis.spring.CommonPointCut.pointCut()")` ### 3.3.8 获取通知的相关信息 1. 获取连接点信息 在通知方法的参数位置,设置**JoinPoint类型**的参数,就可以获取连接点所对应方法的信息 ```java @Before("pointCut()") public void beforeAdviceMethod(JoinPoint joinPoint) { // 获取连接点所对应方法的方法名 Signature signature = joinPoint.getSignature(); // 获取连接点所对应方法的参数 Object[] args = joinPoint.getArgs(); System.out.println("LoggerAspect,前置通知,方法:" + signature.getName() + ",参数" + Arrays.toString(args)); } ``` 2. 获取目标方法的返回值 @AfterReturning中的**属性returning**,用来将通知方法的某个形参,接收目标方法的返回值 ```java /* * 在返回通知中若要获取目标对象方法的返回值 * 只需要通过@AfterReturning注解的returning属性 * 就可以将通知方法的某个参数指定为接受目标对象方法的返回值的参数 * */ @AfterReturning(value = "pointCut()", returning = "result") public void afterReturningAdviceMethod(JoinPoint joinPoint, Object result) { Signature signature = joinPoint.getSignature(); System.out.println("LoggerAspect,返回通知,方法:" + signature.getName() + ",结果:" + result); } ``` 3. 获取目标方法的异常 @AfterThrowing中的**属性throwing**,用来将通知方法的某个形参,接收目标方法的异常 ```java @AfterThrowing(value = "pointCut()", throwing = "ex") public void afterThrowingAdviceMethod(JoinPoint joinPoint, Throwable ex) { Signature signature = joinPoint.getSignature(); System.out.println("LoggerAspect,异常通知,方法:" + signature.getName() + ",异常通知:" + ex); } ``` ### 3.3.9 切面的优先级 相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序。 - 优先级高的切面:外面 - 优先级低的切面:里面 可以通过@Order注解的value属性设置优先级,默认值**Integer的最大值** @Order注解的value**属性值越小**,优先级越高 ```java @Component @Aspect @Order(1) public class ValidateAspect { @Before("com.zerolouis.spring.annotation.LoggerAspect.pointCut()") public void beforeMehtod() { System.out.println("ValidateAspect-->前置通知"); } } ``` ![image-20220919200546919](https://snz04pap001files.storage.live.com/y4mzq60FfgSED0ZwzUCtuelOw2fCnGt-1eca24kNaDZb692XVvkqfePJzXwOhlkvCPCUxo0K8IGWMdnJCmSOVeg1-5JJtEN1T7eqMDX3qrdmPub2wUYTVMDTixnDF951JIVGkvLBlIHIsRKuUzDoUOIb06TEGlQchftxflE80m6cLl-L92ciW4JRKZP_kYa-8lk?width=741&height=315&cropmode=none) ## 3.4 基于XML的AOP(了解) ```xml <context:component-scan base-package="com.atguigu.aop.xml"></context:component-scan> <aop:config> <!--配置切面类--> <aop:aspect ref="loggerAspect"> <aop:pointcut id="pointCut" expression="execution(* com.atguigu.aop.xml.CalculatorImpl.*(..))"/> <aop:before method="beforeMethod" pointcut-ref="pointCut"></aop:before> <aop:after method="afterMethod" pointcut-ref="pointCut"></aop:after> <aop:after-returning method="afterReturningMethod" returning="result" pointcut-ref="pointCut"></aop:after-returning> <aop:after-throwing method="afterThrowingMethod" throwing="ex" pointcut-ref="pointCut"></aop:after-throwing> <aop:around method="aroundMethod" pointcut-ref="pointCut"></aop:around> </aop:aspect> <aop:aspect ref="validateAspect" order="1"> <aop:before method="validateBeforeMethod" pointcut-ref="pointCut"></aop:before> </aop:aspect> </aop:config> ``` # 4 声明式事务 ## 4.1 JdbcTemplate ### 4.1.1 概念 Spring 框架对 JDBC 进行封装,使用 JdbcTemplate 方便实现对数据库操作 ### 4.1.2 依赖 ```xml <!-- https://mvnrepository.com/artifact/com.alibaba/druid --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.11</version> </dependency> <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.30</version> </dependency> <!-- https://mvnrepository.com/artifact/junit/junit --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency> !-- 基于Maven依赖传递性,导入spring-context依赖即可导入当前所需所有jar包 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.22</version> </dependency> <!-- Spring 持久化层支持jar包 --> <!-- Spring 在执行持久化层操作、与持久化层技术进行整合过程中,需要使用orm、jdbc、tx三个 jar包 --> <!-- 导入 orm 包就可以通过 Maven 的依赖传递性把其他两个也导入 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-orm</artifactId> <version>5.3.22</version> </dependency> <!-- Spring 测试相关 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.3.1</version> </dependency> ``` ### 4.1.3 配置数据源 ```xml <context:property-placeholder location="jdbc.properties"></context:property-placeholder> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${jdbc.driver}"></property> <property name="url" value="${jdbc.url}"></property> <property name="username" value="${jdbc.username}"></property> <property name="password" value="${jdbc.password}"></property> </bean> <bean class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"></property> </bean> ``` ### 4.1.4 测试类 ```java // 指定当前测试类在Spring的测试环境中执行,此时就可以通过注入的方式直接获取IOC容器中的bean @RunWith(SpringJUnit4ClassRunner.class) // 设置Spring测试环境的配置文件 @ContextConfiguration("classpath:spring-jdbctemplate.xml") public class JdbcTemplateTest { @Autowired private JdbcTemplate jdbcTemplate; @Test public void testJdbc() { String sql = "insert into sheet1 values(null,?,?,?,?)"; jdbcTemplate.update(sql, "测试", "男", "123123", "asdfas"); } @Test public void testGetUserId() { String sql = "select * from sheet1 where Sno = ?"; User user = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(User.class), 2030110778); System.out.println(user); } @Test public void testGetAllUser() { String sql = "select * from sheet1"; List<User> list = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class)); list.forEach(System.out::println); } @Test public void testGetCount() { String sql = "select count(*) from sheet1"; Integer count = jdbcTemplate.queryForObject(sql, Integer.class); System.out.println(count); } } ``` ## 4.2 声明式事务的概念 ### 4.2.1 编程式事务 ```java Connection conn = ...; try { // 开启事务:关闭事务的自动提交 conn.setAutoCommit(false); // 核心操作 // 提交事务 conn.commit(); }catch(Exception e){ // 回滚事务 conn.rollBack(); }finally{ // 释放数据库连接 conn.close(); } ``` - 细节没有被屏蔽:具体操作过程中,所有细节都需要程序员自己来完成,比较繁琐。 - 代码复用性不高:如果没有有效抽取出来,每次实现功能都需要自己编写代码,代码就没有得到复用。 ### 4.2.2 声明式事务 既然事务控制的代码有规律可循,代码的结构基本是确定的,所以框架就可以将固定模式的代码抽取出来,进行相关的封装。 封装起来后,我们只需要在配置文件中进行简单的配置即可完成操作。 - 好处1:提高开发效率 - 好处2:消除了冗余的代码 - 好处3:框架会综合考虑相关领域中在实际开发环境下有可能遇到的各种问题,进行了健壮性、性能等各个方面的优化 所以,我们可以总结下面两个概念: - 编程式:**自己写**代码实现功能 - 声明式:通过**配置**让**框架**实现功能 ## 4.3 声明式事务的使用 ### 4.3.1 引入依赖 同4.1.2中的依赖 ### 4.3.2 配置数据库和组件 ![image-20220919204131122](https://snz04pap001files.storage.live.com/y4mGMrhwl_9K03O8Ka6Pt_k7rQUGFbu0bDoytPDvHAad_hXjYRXcuEJvKRREMGUhvxhF7GcyGgl-MszsCEbN6SffldmyUHyKJ304izXu33qzpCWdvaBI_bfJnde3bgV3Osmv4LB4RDS23IriKWIS1lbbnNPeLpYoF5bHR7JQ0H34PSRxh_Fx-Fh4y7iFoc8m0g0?width=436&height=394&cropmode=none) 这里模拟用户买书操作,有两个数据库 <img src="https://snz04pap001files.storage.live.com/y4mUmirJrIBYJqjAk_LfFbjI-tjGl_7d2Z_iI18jqsybGmRx0Lz5k3IqN_VhUxo3LMfPAwGwJ9AkZutgZZCqkbppIs2k8ncLhwAJ5MOShTGWuNOTWSj2OFgaOW9cRGtDq-4hk1oLF8TprO9Pxq724Qre-YODz7Yg9T2lA2VqqGjJxRMkp8iaqviaS_PMaYqxWGi?width=1601&height=287&cropmode=none" style="zoom:80%;" /> 其中stock和balance是无符号int数据,最小值为0 ### 4.3.3 配置Spring ```xml <!--扫描组件--> <context:component-scan base-package="spring"></context:component-scan> <!--引入jdbc.properties--> <context:property-placeholder location="jdbc.properties"></context:property-placeholder> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${jdbc.driver}"></property> <property name="url" value="${jdbc.url}"></property> <property name="username" value="${jdbc.username}"></property> <property name="password" value="${jdbc.password}"></property> </bean> <bean class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"></property> </bean> ``` ### 4.3.4 无事务的情况 业务层BookServiceImpl ```java @Service public class BookServiceImpl implements BookService { @Autowired private BookDao bookDao; @Override public void buyBook(Integer userId, Integer bookId) { // 查询图书的价格 Integer price = bookDao.getPriceByBookId(bookId); // 更新图书的库存 bookDao.updateStock(bookId); // 更新用户的余额 bookDao.updateBalance(userId, price); } } ``` 测试类 ```java @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:tx-annotation.xml") public class TxByAnnotationTest { @Autowired private BookController bookController; @Test public void testBuyBook() { bookController.buyBook(1, 1); } } ``` userId=1用户买bookId=1书的操作: 1. 查询图书价格80 2. 更新图书的库存100-1=99 3. 更新用户余额,20-80=-50,由于是无符号数,已经超过了最小值,sql抛出异常SQLException 但由于没有添加事务,图书的库存更新了,但是用户的余额没有更新 显然这样的结果是错误的,购买图书是一个完整的功能,更新库存和更新余额要么都成功要么都失败 ### 4.3.5 添加事务 1. 在Spring配置文件中配置事务管理器,并开启事务的注解驱动 注意:导入的名称空间需要 **tx 结尾**的那个。 ```xml <!--配置事务管理器--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 开启事务的注解驱动 将使用@Transactional注解所表示的方法或类中所有的方法使用事务进行管理 transaction-manager属性设置事务管理器的id --> <tx:annotation-driven transaction-manager="transactionManager"/> ``` 2. 添加事务的注解 因为service层表示业务逻辑层,一个方法表示一个完成的功能,因此处理事务一般在**service层**处理 在BookServiceImpl的buybook()添加注解@Transactional ```java @Service public class BookServiceImpl implements BookService { @Autowired private BookDao bookDao; @Override @Transactional public void buyBook(Integer userId, Integer bookId) { // 查询图书的价格 Integer price = bookDao.getPriceByBookId(bookId); // 更新图书的库存 bookDao.updateStock(bookId); // 更新用户的余额 bookDao.updateBalance(userId, price); } } ``` ## 4.4 事务属性 ```java @Service public class BookServiceImpl implements BookService { @Autowired private BookDao bookDao; @Override @Transactional( // 只读属性 readOnly = false, // 超时,抛出异常并强制回滚 timeout = 3, // 不造成回滚的异常 // noRollbackFor = {ArithmeticException.class}, noRollbackForClassName = "java.lang.ArithmeticException", // 设置事务的隔离级别 isolation = Isolation.DEFAULT ) public void buyBook(Integer userId, Integer bookId) { // 查询图书的价格 Integer price = bookDao.getPriceByBookId(bookId); // 更新图书的库存 bookDao.updateStock(bookId); // 更新用户的余额 bookDao.updateBalance(userId, price); // System.out.println(1/0); } } ``` ### 4.4.1 只读属性 只读属性 readOnly = true / false 对一个查询操作来说,如果我们把它设置成只读,就能够明确告诉数据库,这个操作**不涉及写操作**。这样数据库就能够针对查询操作来进行优化。 当`readOnly = true`时,对增删改操作设置只读会抛出下面异常: `Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed` ### 4.4.2 超时属性 超时属性 timeout = 数字 超时回滚,释放资源。 超时后抛出异常:`org.springframework.transaction.TransactionTimedOutException: Transaction timed out: deadline was Fri Jun 04 16:25:39 CST 2022` ### 4.4.3 回滚策略 声明式事务默认只针对运行时异常回滚,编译时异常不回滚。 可以通过@Transactional中相关属性设置回滚策略 - rollbackFor属性:需要设置一个Class类型的对象 - rollbackForClassName属性:需要设置一个字符串类型的全类名 - noRollbackFor属性:需要设置一个Class类型的对象 - rollbackFor属性:需要设置一个字符串类型的全类名 ```java @Service public class BookServiceImpl implements BookService { @Autowired private BookDao bookDao; @Override @Transactional( // 不造成回滚的异常 // noRollbackFor = {ArithmeticException.class}, noRollbackForClassName = "java.lang.ArithmeticException" ) public void buyBook(Integer userId, Integer bookId) { // 查询图书的价格 Integer price = bookDao.getPriceByBookId(bookId); // 更新图书的库存 bookDao.updateStock(bookId); // 更新用户的余额 bookDao.updateBalance(userId, price); System.out.println(1/0); } } ``` 虽然购买图书功能中出现了数学运算异常(ArithmeticException),但是我们设置的回滚策略是,当 出现ArithmeticException不发生回滚,因此购买图书的操作正常执行 ### 4.4.4 事务隔离级别 数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。一个事务与其他事务隔离的程度称为隔离级别。SQL标准中规定了多种事务隔离级别,不同隔离级别对应不同 的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。 隔离级别一共有四种: - **读未提交**:READ UNCOMMITTED 允许Transaction01读取Transaction02未提交的修改。 - **读已提交**:READ COMMITTED、 要求Transaction01只能读取Transaction02已提交的修改。 - **可重复读**:REPEATABLE READ 确保Transaction01可以多次从一个字段中读取到相同的值,即Transaction01执行期间禁止其它 事务对这个字段进行更新。 - **串行化**:SERIALIZABLE 确保Transaction01可以多次从一个表中读取到相同的行,在Transaction01执行期间,禁止其它 事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但性能十分低下。 各个隔离级别解决并发问题的能力见下表: | 隔离级别 | 脏读 | 不可重复读 | 幻读 | | ------------------ | ------ | ------------ | ------ | | READ UNCOMMITTED | 有 | 有 | 有 | | READ COMMITTED | 无 | 有 | 有 | | REPEATABLE READ | 无 | 无 | 有 | | SERIALIZABLE | 无 | 无 | 无 | 使用方式: ```java @Transactional(isolation = Isolation.DEFAULT) //使用数据库默认的隔离级别 @Transactional(isolation = Isolation.READ_UNCOMMITTED) //读未提交 @Transactional(isolation = Isolation.READ_COMMITTED) //读已提交 @Transactional(isolation = Isolation.REPEATABLE_READ) //可重复读 @Transactional(isolation = Isolation.SERIALIZABLE) //串行化 ``` ### 4.4.5 事务传播行为 1. 简介 当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中 运行,也可能开启一个新事务,并在自己的事务中运行。 2. 现有A,B两个方法,A方法调用B方法,且两者都声明了事务,我们从 B 的角度来思考: - B使用自己的事务:如果B回滚了,不会影响A的回滚。 `propagation = Propagation.REQUIRED_NEW` - B使用A的事务:如果B发生回滚了,则A也发生回滚。 `propagation = Propagation.REQUIRED` 3. 测试 ```java @Controller public class BookController { @Autowired private BookService bookService; @Autowired private CheckoutService checkoutService; public void buyBook(Integer userId, Integer bookId) { bookService.buyBook(userId, bookId); } public void checkout(Integer userId, Integer[] bookIds) { checkoutService.checkout(userId, bookIds); } } ``` ```java @Service public class CheckoutServiceImpl implements CheckoutService { @Autowired private BookService bookService; @Override @Transactional( // 事务的传播行为 propagation = Propagation.REQUIRES_NEW ) public void checkout(Integer userId, Integer[] bookIds) { for (Integer bookId : bookIds) { bookService.buyBook(userId, bookId); } } } ``` 4. 可以通过@Transactional中的propagation属性设置事务传播行为, - 默认情况为`@Transactional(propagation = Propagation.REQUIRED)`,表示如果当前线程上有已经开启的事务可用,那么就在这个事务中运行。 经过观察,购买图书的方法buyBook()在checkout()中被调 用,checkout()上有事务注解,因此在此事务中执行。所购买的两本图书的价格为80和50,而用户的余额为100,因此在购买第二本图书时余额不足失败,导致整个checkout()回滚,即只要有一本书买不了,就都买不了 - `@Transactional(propagation = Propagation.REQUIRES_NEW)`,表示不管当前线程上是否有已经开启的事务,都要开启新事务。 同样的场景,每次购买图书都是在buyBook()的事务中执行,因此第一本图书购买成功,事务结束,第二本图书购买失败,只在第二次的buyBook()中回滚,购买第一本图书不受影响,即能买几本就买几本。 ## 4.5 基于XML的声明式事务(了解) ### 4.5.1 Sring的配置文件 注意:基于xml实现的声明式事务,必须引入aspectJ的依赖 将Spring配置文件中去掉tx:annotation-driven 标签,并添加配置: ```xml <aop:config> <!-- 配置事务通知和切入点表达式 --> <aop:advisor advice-ref="txAdvice" pointcut="execution(* com.atguigu.spring.tx.xml.service.impl.*.*(..))"></aop:advisor> </aop:config> <!-- tx:advice标签:配置事务通知 --> <!-- id属性:给事务通知标签设置唯一标识,便于引用 --> <!-- transaction-manager属性:关联事务管理器 --> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <!-- tx:method标签:配置具体的事务方法 --> <!-- name属性:指定方法名,可以使用星号代表多个字符 --> <tx:method name="get*" read-only="true"/> <tx:method name="query*" read-only="true"/> <tx:method name="find*" read-only="true"/> <!-- read-only属性:设置只读属性 --> <!-- rollback-for属性:设置回滚的异常 --> <!-- no-rollback-for属性:设置不回滚的异常 --> <!-- isolation属性:设置事务的隔离级别 --> <!-- timeout属性:设置事务的超时属性 --> <!-- propagation属性:设置事务的传播行为 --> <tx:method name="save*" read-only="false" rollbackfor="java.lang.Exception" propagation="REQUIRES_NEW"/> <tx:method name="update*" read-only="false" rollbackfor="java.lang.Exception" propagation="REQUIRES_NEW"/> <tx:method name="delete*" read-only="false" rollbackfor="java.lang.Exception" propagation="REQUIRES_NEW"/> </tx:attributes> </tx:advice> ``` 最后修改:2022 年 09 月 19 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 如果觉得我的文章对你有用,请随意赞赏