一、基本概述
1.1、spring是什么?
Spring 是于 2003 年兴起的一个轻量级的 Java 开发框架,它是为了解决企业应用开发的复杂性而创建的。Spring 的核心是控制反转(IoC)和面向切面编程(AOP)。Spring 是可以在 Java SE/EE 中使用的轻量级开源框架。它的官方网址是:https://spring.io/
以上是比较官方的定义,实际上,spring就是一个容器,类比如下图所示:
说白了,Spring容器就是一个“工厂”,可以为类创建对象、管理对象的一个"容器工厂",也就是说,我们不再需要人为的去new对象了,只需要编写配置文件即可,创建对象以及对象的增删改查等交给Spring容器来解决。它的出现极大的简化了创建对象以及管理对象的难度,能实现模块与模块之间、类与类之间的解耦合。
你可能会问,类创建对象?我们直接通过new的方式创建对象不香吗?为啥还要花费这么大的力气去引入一个这么庞大又复杂的容器去创建对象呢?解耦合是什么鬼?为什么要解耦合?
其实你说得十分有道理,先别急,且看我慢慢分析。
1.2、spring框架的优势?
Spring 是一个框架,是一个半成品的软件。有 20 个模块组成。它是一个容器管理对象,容器是装东西的,Spring 容器不装文本,数字。装的是对象。Spring 是存储对象的容器。
(1) 轻量
Spring 框架使用的 jar 都比较小,一般在 1M 以下或者几百 kb。Spring 核心功能的所需的 jar 总共在 3M 左右。Spring 框架运行占用的资源少,运行效率高。不依赖其他 jar。
(2) 针对接口编程,解耦合
Spring 提供了 Ioc 控制反转,由容器管理对象,对象的依赖关系。原来在程序代码中的对象创建方式,现在由容器完成。对象之间的依赖解耦合。
(3) AOP 编程的支持
通过 Spring 提供的 AOP 功能,方便进行面向切面的编程,许多不容易用传统 OOP 实现的功能可以通过 AOP 轻松应付,在 Spring 中,开发人员可以从繁杂的事务管理代码中解脱出来,通过声明式方式灵活地
进行事务的管理,提高开发效率和质量。
(4) 方便集成各种优秀框架
Spring 不排斥各种优秀的开源框架,相反 Spring 可以降低各种框架的使用难度,Spring提供了对各种优秀框架(如 Struts,Hibernate、MyBatis)等的直接支持。简化框架的使用。Spring 像插线板一样,其他框架是插头,可以容易的组合到一起。需要使用哪个框架,就把这个插头放入插线板。不需要可以轻易的移除。
二、IOC
我们得先明确两个spring的核心技术概念,IOC(控制反转)和AOP(面向切面编程)。
2.1、什么是控制反转?
IOC(Inversion of Control):控制反转,它表示一种理论,一种思想方法:**把对象的创建,赋值,管理等工作都交给代码之外的容器来实现。而容器则由开发人员创建。**这可能有点难以理解,举个简单的例子:
Student student = new Student();
这是我们之前创建对象的方式,这个对象是由你直接new出来的,这叫做正转,反转的意思就是这个对象不再交由你来创建,而是交给代码之外的spring容器来创建,你只需要往spring中输入一些配置参数,对象即可自动创建出来。而控制的意思就是完成反转操作的操控者,在这里也就是Spring容器。Spring的身份其实跟我们日常生活中的遇到的中介有点类似。
2.2、为什么要使用IOC?
回答这个问题,直接上一个直观一点的例子,看以下代码:
import lombok.Data;
@Data
public class Student {
private Integer id;
private String name;
private String age;
}
这是我们定义好的一个实体类,上面的@Data注解其实是利用了一个第三方的lombok的jar包,帮助你省略了set,get方法的书写,但是使用的时候还是可以直接使用的。OK,实体类有了,那么接下来就是创建实体类的对象了
public class Test{
public static void main(String[] args) {
Student student = new Student();
student.setName("张三");
student.setAge("25");
}
}
这时,我们在主方法里面创建了Student类的实体对象,咋一看,似乎并没有什么毛病,确实很完美。但是,你想过没有,你这种创建对象的方式,实际上就相当于把创建对象给写死了,换句话说,当你需要修改对象的一些属性值,或者增加对象一些设置时,你就不得不去修改源代码,也就是说,你需要修改源代码文件。当然,修改一个源码文件当然是没有很大的问题的,但是对于企业级开发那种动辄上百个类的情况,你修改源码,一来**不符合OCP开发原则,二来,维护十分的繁琐,极其耗时间,而且还容易出错。**那怎么解决这个问题呢?
没错,IOC就是解决这个问题的。它将属性的值以及一些相关的设置单独的抽取成XML配置文件,作为开发人员的你,只需要完成XML文件的配置即可,其它的全部交给Spring框架去实现。Spring框架的底层运用了大量的反射机制,实现了将对象的创建以及赋值,这里就不展开叙述底层是如何实现的,有兴趣的同学可以去啃一下源码。
2.3、如何实现控制反转?
控制反转目前比较流行的实现方式是依赖注入(Dependency Injection),也叫做DI。
依赖:classA类中含有classB的实例,在classA中调用classB中的方法完成功能,即说classA对ClassB有依赖
依赖注入就是指:程序运行过程中,若需要调用另一个对象协助时,无须在代码中创建被调用者,而是依赖于外部容器,由外部容器创建后传递给程序。
Spring的框架正是使用了DI的方式实现了IOC:Spring 容器是一个超级大工厂,负责创建、管理所有的 Java 对象,这些 Java 对象被称为 Bean。Spring 容器管理着容器中 Bean 之间的依赖关系,Spring 使用“依赖注入”的方式来管理 Bean 之间的依赖关系。使用 IoC 实现对象之间的解耦和。
2.3.1、利用Spring容器实现创建对象
【1】:新建一个Maven工程:
【2】:输入工程名称,点击下一步
【3】:点击完成
【4】:创建Maven项目成功
【5】:添加Spring依赖
【6】:定义接口与实体类
public interface SomeService {
// 定义测试方法
void doSome();
}
========================================================
public class SomeServiceImpl implements SomeService {
public SomeServiceImpl(){
System.out.println("接口实现类中的构造方法执行了");
}
@Override
public void doSome() {
System.out.println("接口中的方法执行了");
}
}
【7】:创建Spring配置文件,以xml为后缀,名称自定义,建议使用applicationContext.xml,这里顺便说一下Spring配置文件的作用,目的是为了绑定类与Spring框架。让Spring扫描到你要创建的对象是哪一个类。并且,一个bean标签代表的就是一个对象
【8】:测试
嗯…,执行到这一步,相信还是有许多小伙伴会有疑问,要是没有接口咋办,也可以实现创建对象吗?如果是抽象类呢?怎么实现?这个代码咋就把对象创建出来了??**事实上,只要是一个实体类,并且在Spring的配置文件中定义好了bean标签、id以及class属性值,其实是都可以完成对象的创建的。**各位可以自己去测试一下。
好,接下来说一下Spring的执行流程吧,回到测试类test(),我们一行一行代码往下读,第一个是定义的一个字符串类型的变量config用来保存配置文件的路径的,这里需要注意的是,这个路径是从你的resources包开始的,也就是说,如果你在resource包下的子包,那么路径就需要进行相应的修改。例如:
那么这时,你的config的值应该修改为:
接下来的代码的执行过程可以简单的理解成:Spring通过加载config配置文件创建出来的一个容器对象,同时创建配置文件中的所有对象。也就是说这个大容器里面已经存放着在applicationContext.xml文件中的标签class属性值所对应的实体类的对象了。具体的底层实现十分复杂,有兴趣的同学可以研究一下源码。(看到没,这就是框架的一个优势所在,都帮你封装好了,不需要去理解底层是如何实现的,你只需要学会去使用这些框架即可)
OK,对象创建好了,那么我怎么从Spring容器中去取出这个对象呢?通过以下这个方法
/*
其中传入的参数就是在配置文件中定义的ID属性的值,事实上,配置文件中ID属性值的作用其实就类似于我们的身份证号,方便让Spring容器可以快速的找到并定位出你要取出的是哪一个实体类的对象
*/
SomeServiceImpl myService = (SomeServiceImpl) ac.getBean("myService");
类对象我们有了,那么接下来就是对对象的属性进行赋值,我们不可能创建一个里面啥数据都没有的类对象是不?肯定是需要在你创建类对象的同时,顺便把数据封装进对象中。这其实就是我们面向对象的程序设计的一大优势之一。对于Spring来说,对象的属性进行赋值这一过程也是由Spring自动完成的,这一过程我们称之为注入,也叫做依赖注入(Dependency Injection)简称DI,根据注入的方式不同,注入可以分为两大类,基于XML配置文件的DI以及基于注解的DI
2.3.2、 基于XML文件的DI
XML文件的注入也可以分为两类,一类是set注入,另一类是构造注入
(1)set注入
这种注入的方式,顾名思义,就是通过set方法来实现注入的,这种方式简单、直观,因此在Spring的依赖注入中被大量使用。例如:
// 实体类
public class Student {
private Integer id;
private String name;
private Integer age;
public void setId(Integer id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
}
}
<!--XML配置文件-->
<bean id="myStudent" class="com.beim.entity.Student">
<property name="id" value="1"/> <!--setId(1)-->
<property name="name" value="张三"/> <!--setName("张三")-->
<property name="age" value="20"/> <!--setAge(20)-->
</bean>
// 测试类
@Test
public void test() {
// 定义配置文件路径
String config = "test/applicationContext.xml";
// 测试创建对象,在这个对象创建的时候其实就已经在Spring容器中执行了无参构造了
ApplicationContext ac = new ClassPathXmlApplicationContext(config);
// 从容器中拿出对象,传入的参数是XML文件bean对象的ID属性值
Student myStudent = (Student) ac.getBean("myStudent");
System.out.println(myStudent);
}
输出的结果:Student{id=1, name=‘张三’, age=20},再比如说如果是JDK定义的类:
<!--JDK定义的类-->
<bean id="myDate" class="java.util.Date">
<property name="time" value="1640736000000"/>
</bean>
@Test
public void test() {
// 定义配置文件路径
String config = "test/applicationContext.xml";
// 测试创建对象,在这个对象创建的时候其实就已经在Spring容器中执行了无参构造了
ApplicationContext ac = new ClassPathXmlApplicationContext(config);
// 从容器中拿出对象,传入的参数是XML文件bean对象的ID属性值
Date date = (Date) ac.getBean("myDate");
// 格式化日期
String strDateFormat = "yyyy-MM-dd HH:mm:ss";
SimpleDateFormat sdf = new SimpleDateFormat(strDateFormat);
System.out.println(sdf.format(date));
}
输出的结果为:2021-12-29 08:00:00
再比如说如果是引用类型的注入:
// 定义学校类
public class School {
private String name;
private String adress;
public void setName(String name) {
this.name = name;
}
public void setAdress(String adress) {
this.adress = adress;
}
@Override
public String toString() {
return "Shcool{" +
"name='" + name + '\'' +
", adress='" + adress + '\'' +
'}';
}
}
// 定义学生类
public class Student {
private Integer id;
private String name;
private Integer age;
private School school;
public void setId(Integer id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
public void setAge(Integer age) {
this.age = age;
}
public void setSchool(School school) {
this.school = school;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
", school=" + school +
'}';
}
}
<!--XML文件的写法有两种-->
<!--Student对象-->
<bean id="myStudent" class="com.beim.entity.Student">
<property name="id" value="1"/> <!--setId(1)-->
<property name="name" value="张三"/> <!--setName("张三")-->
<property name="age" value="20"/> <!--setAge(20)-->
<property name="school" ref="mySchool"/> <!--这个rel的值其实就是School的ID值-->
</bean>
<!--定义School对象-->
<bean id="mySchool" class="com.beim.entity.School">
<property name="name" value="清华大学"/>
<property name="adress" value="北京市"/>
</bean>
// 测试方法
@Test
public void test() {
// 定义配置文件路径
String config = "test/applicationContext.xml";
// 测试创建对象,在这个对象创建的时候其实就已经在Spring容器中执行了无参构造了
ApplicationContext ac = new ClassPathXmlApplicationContext(config);
// 从容器中拿出对象,传入的参数是XML文件bean对象的ID属性值
Student myStudent = (Student) ac.getBean("myStudent");
// 输出
System.out.println(myStudent);
}
需要注意的是:
- Spring在完成属性的注入之前,必须得先创建完毕对象,也就是说是先执行实体类的构造方法,然后再进行属性值的注入
- 无论是自定义类还是非自定义类,只要存在set方法,不管类中是否存在对应的属性,程序都可以正常执行,spring内部是直接调用set方法将value值传递进去作为参数
(2)构造注入
相信看完上面的set注入,小伙伴们也应该可以猜出构造注入大致的语法格式是怎么样的了,顾名思义构造注入也就是利用有参的构造方法在创建对象的同时完成属性值的注入,与set注入不同的地方就是在于XML文件的配置有所不一样以及在实体类上需要添加一个有参数的构造方法:
// 有参数构造方法
public Student(Integer id, String name, Integer age) {
this.id = id;
this.name = name;
this.age = age;
}
<!--XML文件的配置-->
<bean id="myStudent" class="com.beim.entity.Student">
<constructor-arg name="id" value="1"/>
<constructor-arg name="name" value="李四"/>
<constructor-arg name="age" value="22"/>
</bean>
事实上,标签中用于指定参数的属性如下表:
属性名称 | 作用 |
---|---|
name | 实体类的属性名称 |
value | 实体类属性值 |
index | 索引值,用于标记实体类中属性的顺序,从上往下依次为0,1,2,3… |
2.3.3、基于注解的DI
使用注解的方式进行依赖注入在开发中是使用比较多的,因为其更加简洁,直观。它不再需要在XML配置文件中创建实例对象,只需要声明一个组件扫描器即可。
(1)使用注解的步骤
1、加入Maven依赖,spring-context,在你加入spring-context依赖时,会间接加入到spring-aop依赖,要使用注解进行依赖注入,必须得将spring-aop添加到pom文件中
2、在类中配置spring的注解
3、在spring的配置文件中,加入一个组件扫描器的标签,说明注解在你的项目中的位置
(2)常用注解介绍
1.@Component:创建对象的,等同于的功能,其实就是将它声明为一个可以被Spring配置文件中的组件扫描器扫描到的类。
/*
括号内传递的参数是该实体类的ID值,若不传,则默认为大写的实体类类名
@Component("myStudent")其实就等同于<bean id = "myStudent" class = com.beim.Student/>
*/
@Component("myStudent")
public class Student {
// @Value:这个注解其实就是用来给属性赋值的。十分容易理解
@Value("1")
/*
@Autowired:这个注解其实跟@Value是一样的,只不过它针对的是引用类型的属性赋值。它有一个
required属性,是一个boolean类型的,其值默认为true,
值为true:如果引用类型赋值失败,则程序报错,并终止执行,
值为false:如果引用类型赋值失败,程序正常执行,但引用类型的值变为null
这个注解的使用需要注意的是:
如果是针对引用类型的自动注入,它默认是使用byType进行自动注入的,如果想要修改为byName的注入方式,可以在其后加一个@Qualifier注解,括号内传入的参数为ID值
*/
@Autowired
@Qualifier(value = "mySchool") // value属性的值用来指定School对象的ID
private School school;
}
跟@Component功能一致的创建对象的注解还有:
**@Repository(用在持久层上):**放在DAO实现类上面,表示创建DAO对象,DAO对象是能够访问数据库的。
**@Service(用在业务层上面):**放在Service的实现类上面,创建Service对象,Service对象是做业务处理的,可以有事务处理等功能。
**@Controller(用在控制器上面):**放在控制器(处理器)类的上面,创建控制器对象的,控制器对象,能够接收用户提交的参数,显示请求的处理结果。
以上三个注解的使用语法其实与@Component一样,都能创建对象,但是这三个注解还有额外的功能,它们之间的关系有点类似于,@Component是父亲,其他三个是儿子一样,儿子能干的事,父亲也都能干(别较真,这里只是做一个形象的比喻),虽然父亲也能干,但是儿子毕竟还是年轻,有些事让儿子做起来效率会更高一些,更加的有针对性。也不知道这个比喻恰不恰当,官方的说法就是说:这三个注解针对MVC架构开发的某些类更加的有针对性,其本质也是从@Component中衍生过来的。再说得直白一点就是,这三个注解是@Component的非空真子集。
(3)@Resource
**Spring提供了对 jdk中@Resource注解的支持。@Resource 注解既可以按名称匹配Bean,也可以按类型匹配 Bean。默认是按名称注入。**使用该注解,要求 JDK 必须是 6 及以上版本。@Resource 可在属性上,也可在 set 方法上。
@Resource 注解若不带任何参数,采用默认按名称的方式注入,按名称不能注入 bean,则会按照类型进行 Bean 的匹配注入。例如
@Resource 注解指定其 name 属性,则 name 的值即为按照名称进行匹配的 Bean 的 id。例如:
(4)完整使用案例如下
<?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">
<!--声明一个组件扫描器,只需要保证包下存在带有注解的实体类即可-->
<context:component-scan base-package="com.beim.entity"/>
</beans>
指定多个包的三种声明方式:
<!--第一种方式:声明多个组件扫描器-->
<context:component-scan base-package="com.beim.entity"/>
<context:component-scan base-package="com.beim.domain"/>
<!--第二种方式:使用分隔符逗号或者分号都可以,但是注意必须得是英文的-->
<context:component-scan base-package="com.beim.entity,com.beim.domain"/>
<!--第三种方式:指定到父包名-->
<context:component-scan base-package="com.beim"/>
XMl配置文件声明完成之后,接着就可以直接在实体类中使用注解了:
@Component("myStudent")
public class Student {
@Value("1")
private Integer id;
@Value("张三")
private String name;
@Value("25")
private Integer age;
@Autowired
@Qualifier(value = "mySchool") // value属性的值用来指定School对象的ID
private School school;
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
", school=" + school +
'}';
}
}
// 括号内传递的参数是该实体类的ID值,若不传,则默认为大写的实体类类名
@Component("mySchool")
public class School {
@Value("北京大学")
private String name;
@Value("北京市")
private String adress;
@Override
public String toString() {
return "School{" +
"name='" + name + '\'' +
", adress='" + adress + '\'' +
'}';
}
}
// 测试方法
@Test
public void test() {
// 定义配置文件路径
String config = "test/applicationContext.xml";
// 测试创建对象,在这个对象创建的时候其实就已经在Spring容器中执行了无参构造了
ApplicationContext ac = new ClassPathXmlApplicationContext(config);
// 从容器中拿出对象,传入的参数是XML文件bean对象的ID属性值
Student myStudent = (Student) ac.getBean("myStudent");
// 输出
System.out.println(myStudent);
}
输出:Student{id=1, name=‘张三’, age=25, school=School{name=‘北京大学’, adress=‘北京市’}}
2.3.4、自动注入
自动注入是什么鬼?事实上,在XML配置文件总对于引用类型的属性注入,我们可以隐式的注入,那么怎么个隐式注入呢?标签有一个属性,这哥们叫做autowire,通过设置它的值来做到隐式注入**(默认是不自动注入的)**。根据自动注入判断标准的不同,它的值有两种:
- byName:根据名称进行注入
- byType:根据类型进行注入
需要注意的是:
- 自动注入针对的只是引用类型的属性
- 自动注入既可以是XML文件的注入,也可以是注解的方式进行注入
(1)byName方式注入
在使用这种方式进行注入的时候,注意:必须要保证标签的ID属性值必须与类的属性名保持一致
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cyp278M9-1646825366504)(E:\Pictures\Typora-pictures\1640790886829.png)]
(2)使用byType的方式注入
使用 byType 方式自动注入,要求:配置文件中被调用者 bean 的 class 属性指定的类,要与代码中调用者 bean 类的某引用类型属性类型同源。即要么相同,要么有 is-a 关系(子类,或是实现类)。但这样的同源的被调用 bean 只能有一个。多于一个,Spring容器就不知该匹配哪一个了。例如:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0gmGXhs2-1646825366505)(E:\Pictures\Typora-pictures\1640791752628.png)]
2.3.5、指定多个Spring配置文件
有的时候,当项目变得异常庞大的时候,如果我们将所有的配置文件的信息全都配置到一个文件中,这时配置文件就会显得十分的臃肿。因此,这就需要分模块设置多个配置文件,那么如何将多个配置文件绑定到一起呢?这就要使用到标签了,例如:
<!--applicationContext01.xml-->
<bean id="myStudent01" class="com.beim.entity.Student" >
<property name="id" value="1"/>
<property name="name" value="张三"/>
<property name="age" value="22"/>
<property name="school" ref="myschool01"/>
</bean>
<bean id="myschool01" class="com.beim.entity.School">
<property name="name" value="清华大学"/>
<property name="adress" value="北京市"/>
</bean>
<!--注:只是文件的核心代码,并非全部代码-->
<!--applicationContext02.xml-->
<bean id="myStudent01" class="com.beim.entity.Student" >
<property name="id" value="2"/>
<property name="name" value="李四"/>
<property name="age" value="20"/>
<property name="school" ref="myschool02"/>
</bean>
<bean id="myschool02" class="com.beim.entity.School">
<property name="name" value="北京大学"/>
<property name="adress" value="北京市"/>
</bean>
<!--注:只是文件的核心代码,并非全部代码-->
<!--total.xml-->
<import resource="classpath:applicationContext01.xml"/>
<import resource="classpath:applicationContext02.xml"/>
测试程序的代码为:
@Test
public void test() {
// 定义配置文件路径
String config = "total.xml";
ApplicationContext ac = new ClassPathXmlApplicationContext(config);
Student myStudent01 = (Student) ac.getBean("myStudent01");
Student myStudent02 = (Student) ac.getBean("myStudent02");
// 输出
System.out.println(myStudent01);
System.out.println(myStudent02);
}
输出的结果为:
Student{id=1, name=‘张三’, age=22, school=School{name=‘清华大学’, adress=‘北京市’}}
Student{id=2, name=‘李四’, age=20, school=School{name=‘北京大学’, adress=‘北京市’}}
另外,在total中填入其他子文件的地址的时候还可以使用通配符,这种方式用的比较少,这里就不一一演示了,语法与之前接触到的通配符的语法是一致的。感兴趣的盆友可以自行查阅官方文档。
2.3.6、注解与XML文件的对比
注解的优点是:
- 方便
- 直观
- 高效(代码较少,不需要繁杂的书写配置文件)
它的缺点也是显而易见的,以硬编码的方式将注解嵌入到源码中,每一次修改都需要重新进行编译,速度就大大降低了。
XML文件的优点是:
- 与源码耦合度低,XML文件与源代码是分离开的,耦合度较低
- 在XML文件中修改代码,无需编译,只需要重启服务器即可将新的配置文件加载进去。速度更快
它的缺点是:编写麻烦,效率低,大型项目过于复杂。
总的来说,两种方式各有优劣,看具体的应用场景来使用,一般用注解的情况会多一点,但如果你项目中需要经常修改源代码的话,建议使用XMl文件的方式。
三、AOP
3.1、什么是AOP?
AOP(Aspect Oriented Programming),意为面向切面编程,那么什么是切面?我们为啥要使用切面?等一系列的问题会接种而来,先别急,再此之前,先来复习一下动态代理的概念。
3.1.1、动态代理
底层的动态代理的实现是十分复杂的,这里我们也不需要去深究底层是如何实现的,我们只需要明白一点,动态代理有什么作用,换句话说,我们使用动态代理跟不使用动态代理的有哪些不为人知的优势?为了理解上述问题,我们先来看一个例子:
// 先定义一个接口
public interface SomeService {
void doSome();
}
// 接口实现类
public class SomeServiceImpl implements SomeService {
@Override
public void doSome() {
System.out.println("业务方法实现了!!!");
}
}
// 测试类
@Test
public void test() {
SomeService someService = new SomeServiceImpl();
someService.doSome();
}
很显然,都不需要我上输出结果的截图,输出的结果肯定是业务方法实现了。好,现在,我们需要往doSome()方法中增加功能,比如说,我需要在输出业务方法实现了之前,先输出一个时间,如果按照我们以往的做法,是不是就直接在代码上加了一个时间输出了,但是你这样做你有没有想过:
现在只是一个简单为了便于理解的例子,如果以后是那种已经上线的大型的项目的话,你可以直接修改源代码吗?(显然不能直接先让项目下线,然后将功能增加上去之后再次上线,显然是不能这样做的)别忘了,我们编程是有一个OCP原则的,而且,如果需要增加功能的目标方法有几百个,那你就需要写几百次,这就产生了大量冗余的代码。因此,我们如何能在不修改源代码的基础上增加功能吗?
没错,动态代理干的就是这件事,它的底层实现较为复杂,这里只需要知道动态代理能干啥就行了,先说结论:
- 在目标类源代码不改变的情况下,增加功能。
- 减少代码的重复
- 专注业务逻辑代码
- 解耦合,让你的业务功能和日志,事务等非业务功能分离。
目前,动态代理有两种比较主流的实现方式,一种是JDK自带的动态代理,另外一种是使用CGLIB的方式实现动态代理:
jdk动态代理:使用jdk中的Proxy,Method,InvocaitonHanderl创建代理对象,jdk动态代理要求目标类必须实现了接口。
cglib动态代理:第三方的工具库,创建代理对象,原理是继承。 通过继承目标类,创建子类。子类就是代理对象。 要求目标类不能是final的, 方法也不能是final的。
3.1.2、怎样理解AOP?
OK,理解了以上概念,我们回过头去看之前的几个疑问,其实就已经很好理解了。
切面:其实也就是给你的目标类增加的额外的功能,就是切面。注意是在不改变原有的代码的基础之上。
切面的特点:一般都是与业务逻辑代码分离开来的功能。
事实上,原生的JDK动态代理以及CGLIB的动态代理实现也是比较复杂的,而面向切面编程,就是解决了这个问题,它将实现动态代理的过程流程化,框架化了,并且引入了切面的概念,让我们实现动态代理更加的简单化。说白了它就是一个高级封装体,能让程序员自身以更加优雅的方式实现动态代理。
对于程序员本身来说,我们只需要做到,
-
需要在分析项目的时候,找出切面
-
合理的安排切面的执行时间(在目标方法之前,还是在目标方法之后)
-
合理的安全的切面执行的位置,在哪个类,哪个方法后增加增强功能
当需要在业务代码中加入非业务功能如日志,事务处理等代码的时候,我们只需要将非业务代码封装成切面,然后织入到目标方法中即可,这极大的简化了开发,提高了效率。
注意:面向切面编程只是对面向对象编程的一种补充,它只是一种编程的思想,并不是Spring中独有的,只是Spring中集成了面向切面编程的方法。
3.2、AOP编程术语
在正式开始学习如何使用这种编程思想的同时,我们还是需要先理解一些AOP的编程术语。
(1) 切面(Aspect)
切面泛指交叉业务逻辑。也就是你需要在增加在主业务逻辑中的功能模块。在上例中,你要增强的方法模块封装成一个类之后,这个类就是切面类。
(2) 连接点(JoinPoint)
连接点指可以被切面织入的具体方法。也就是你的目标方法,通常业务接口中的方法均为连接点。在上例中就是指doSome()方法
(3) 切入点(Pointcut)
切入点指声明的一个或多个连接点的集合。通过切入点指定一组方法。被标记为 final 的方法是不能作为连接点与切入点的。因为最终的是不能被修改的,不能被增强的。
(4) 目标对象(Target)
目 标 对 象 指 将 要 被 增 强 的 对 象 。 即 包 含 主 业 务 逻 辑 的 类 的 对 象 。 上 例 中 SomeServiceImpl的对象若被增强,则该类称为目标类,该类对象称为目标对象。当然,不被增强,也就无所谓目标不目标了。
(5) 通知(Advice)
通知表示切面的执行时间,Advice 也叫增强。上例中的 MyInvocationHandler 就可以理解为是一种通知。换个角度来说,通知定义了增强代码切入到目标代码的时间点,是目标方法执行之前执行,还是之后执行等。通知类型不同,切入时间不同。切入点定义切入的位置,通知定义切入的时间。
一个切面的定义有三个要素:
第一:切面的功能代码,即切面要干什么?
第二:切面的执行位置,即切面在哪执行,也就是切入点在哪?
第三:切面的执行时间,即切面什么时候执行,在目标方法之前、之后还是之间执行
3.3、AspectJ框架
对于 AOP 这种编程思想,很多框架都进行了实现。Spring 就是其中之一,可以完成面向切面编程,但Spring本身对AOP的实现比较笨重。而AspectJ框架不仅实现了 AOP 的功能,且其实现方式更为简捷,使用更为方便,而且还支持注解式开发。所以,Spring 又将 AspectJ 的对于 AOP 的实现也引入到了自己的框架中(所以说,Spring的扩展性是很强的)。在 Spring 中使用 AOP 开发时,一般使用 AspectJ 的实现方式。
这里简单的对AspectJ框架做一个介绍,这是一个优秀的开源的框架,其官网地址为:http://www.eclipse.org/aspectj/,是eclipse上的一个开源项目。在正式开始使用这各框架之前,我们先来理解几个AspectJ框架中的几个术语:
3.3.1、通知类型
AspectJ 中常用的通知有五种类型:
(1)前置通知
(2)后置通知
(3)环绕通知
(4)异常通知
(5)最终通知
3.3.2、切入点表达式
AspectJ 定义了专门的表达式用于指定切入点。表达式的原型是:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
解释:
modifiers-pattern] 访问权限类型
ret-type-pattern 返回值类型
declaring-type-pattern 包名类名
name-pattern(param-pattern) 方法名(参数类型和参数个数)
throws-pattern 抛出异常类型
?表示可选的部分
以上表达式共 4 个部分。
execution(访问权限 方法返回值 方法声明(参数) 异常类型)
看到这里,相信已经有小伙伴可以知道这个切入点表达式的意义是什么了吧?切入点表达式要匹配的对象就是目标方法的方法名。所以,execution 表达式中就是方法的签名,就是帮助你定位目标方法的。指定切入点的。注意,表达式中黑色文字表示可省略部分,各部分间用空格分开。在其中可以使用以下符号:
举例:
// 指定切入点为:任意公共方法
execution(public **(..))
// 指定切入点为:任何一个以“set”开始的方法。
execution(* set*(..))
// 指定切入点为:定义在 service 包里的任意类的任意方法。参数也是任意的
execution(* com.xyz.service.*.*(..))
/*
指定切入点为:定义在 service 包或者子包里的任意类的任意方法。“..”出现在类名中时,后
面必须跟“*”,表示包、子包下的所有类。
*/
execution(* com.xyz.service..*.*(..))
// 指定所有包下的 serivce 子包下所有类(接口)中所有方法为切入点
execution(* *..service.*.*(..))
// 指定只有一级包下的 serivce 子包下所有类(接口)中所有方法为切入点
execution(* *.service.*.*(..))
// 指定只有一级包下的 ISomeSerivce 接口中所有方法为切入点
execution(* *.ISomeService.*(..))
// 指定所有包下的 ISomeSerivce 接口中所有方法为切入点
execution(* *..ISomeService.*(..))
// 指定切入点为:IAccountService 接口中的任意方法。
execution(* com.xyz.service.IAccountService.*(..))
/*
指定切入点为:IAccountService 若为接口,则为接口中的任意方法及其所有实现类中的任意
方法;若为类,则为该类及其子类中的任意方法。
*/
execution(* com.xyz.service.IAccountService+.*(..))
/*
指定切入点为:所有的 joke(String,int)方法,且 joke()方法的第一个参数是 String,第二个参
数是 int。如果方法中的参数类型是 java.lang 包下的类,可以直接使用类名,否则必须使用全限定类名, 如 joke( java.util.List, int)。
*/
execution(* joke(String,int)))
/*
指定切入点为:所有的 joke()方法,该方法第一个参数为 String,第二个参数可以是任意类
型,如joke(String s1,String s2)和joke(String s1,double d2)都是,但joke(String s1,double d2,String s3)不是。
*/
execution(* joke(String,*)))
/*
指定切入点为:所有的 joke()方法,该方法第一个参数为 String,后面可以有任意个参数且
参数类型不限,如 joke(String s1)、joke(String s1,String s2)和 joke(String s1,double d2,String s3)
都是。
*/
execution(* joke(String,..)))
/*
指定切入点为:所有的 joke()方法,方法拥有一个参数,且参数是 Object 类型。joke(Object ob)
是,但,joke(String s)与 joke(User u)均不是。
*/
execution(* joke(Object))
/*
指定切入点为:所有的 joke()方法,方法拥有一个参数,且参数是 Object 类型或该类的子类。
不仅 joke(Object ob)是,joke(String s)和 joke(User u)也是。
*/
execution(* joke(Object+)))
3.3.3、实现AspectJ框架
【1】先加入Maven依赖
<!--添加Spring依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
<!--添加aspectJ的依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
【2】定义一个切面类
@Aspect
public class MyAspect {
// 前置通知,表示在目标方法之前加入增强
@Before(value = "execution(public void doSome(..))")
public void doLog(){
System.out.println("后置增强的方法执行了!!!输出的值为:");
}
}
【3】声明配置文件
<!--声明目标JavaBean对象-->
<bean id="myService" class="com.beim.service.SomeServiceImpl"/>
<!--声明切面类对象-->
<bean id="myAspect" class="com.beim.service.MyAspect"/>
<!--
声明一个自动代理生成器,它可以用来生成代理对象
其工作原理是:根据@Aspect注解定义的类找到切面类,再由切面类通过切入点找到目标类中的目标方法,再由通知类型找到切入的时间点。(是前置还是后置,还是中间。)
-->
<aop:aspectj-autoproxy/>
【4】输出测试
@Test
public void test() {
String config = "applicationContext.xml";
ApplicationContext ac = new ClassPathXmlApplicationContext(config);
// 得到代理对象
SomeService myService = (SomeService) ac.getBean("myService");
myService.doSome();
}
输出:
增强的方法执行了!!!
业务方法实现了!!!
3.4、各种通知注解的演示
(1)@Before(前置通知)
在目标方法执行之前执行。被注解为前置通知的方法,可以包含一个 JoinPoint 类型参数。该类型的对象本身就是切入点表达式。通过该参数,可获取切入点表达式、方法签名、目标对象等。不光前置通知的方法,可以包含一个 JoinPoint 类型参数,所有的通知方法均可包含该参数。
【1】目标方法
public interface SomeService {
void doSome(String name,Integer age);
}
public class SomeServiceImpl implements SomeService {
@Override
public void doSome(String name,Integer args) {
System.out.println("业务方法实现了!!!");
}
}
【2】切面类的定义:
@Aspect
public class MyAspect {
/*
JoinPoint参数,代表的是目标方法对象,可以获取目标方法的签名、参数等信息,其必须作为
第一个参数传入搭配切面方法中。
*/
@Before(value = "execution(public void doSome(..))")
public void doLog(JoinPoint jp){
// 获取连接点的方法定义
System.out.println("连接点的方法定义为:" + jp.getSignature());
// 获取方法返回的参数的个数
System.out.println("连接点方法的返回参数的个数为:" + jp.getArgs().length);
// 输出方法参数的信息
Object[] args = jp.getArgs();
for (Object obj :
args) {
System.out.println(obj);
}
System.out.println("增强的方法执行了!!!");
}
}
【3】测试类
@Test
public void test() {
String config = "applicationContext.xml";
ApplicationContext ac = new ClassPathXmlApplicationContext(config);
// 获取目标类代理对象
SomeService myService = (SomeService) ac.getBean("myService");
myService.doSome("张三",22);
}
【4】输出:
连接点的方法定义为:void com.beim.service.SomeService.doSome(String,Integer)
连接点方法的返回参数的个数为:2
张三
22
增强的方法执行了!!!
业务方法实现了!!!
(2)@AfterReturning(后置通知)
在目标方法执行之后执行。由于是目标方法之后执行,所以可以获取到目标方法的返回值。该注解returning 属性就是用于指定接收方法返回值的变量名的。所以,被注解为后置通知的方法,除了可以包含 JoinPoint 参外,还可以包含用于接收返回值的变量。该变量最好为 Object 类型,因为目标方法的返回值可能是任何类型。
public interface SomeService {
String doSome(String name,Integer age);
}
public class SomeServiceImpl implements SomeService {
@Override
public String doSome(String name,Integer args) {
System.out.println("业务方法实现了!!!");
return name;
}
}
@Aspect
public class MyAspect {
// 后置通知
@AfterReturning(value = "execution(String doSome(..))",returning = "ret")
public void doLog(Object ret){
System.out.println(ret);
System.out.println("增强的方法执行了!!!");
}
}
@Test
public void test() {
String config = "applicationContext.xml";
ApplicationContext ac = new ClassPathXmlApplicationContext(config);
SomeService myService = (SomeService) ac.getBean("myService");
myService.doSome("张三",22);
}
输出:
业务方法实现了!!!
张三
增强的方法执行了!!!
它其实就相当于:
Object ret = doSome();
doLog(Object ret);
(3)@Around(环绕通知)
在目标方法执行之前之后执行。被注解为环绕增强的方法要有返回值,Object 类型。并且方法可以包含一个 ProceedingJoinPoint 类型的参数(继承了JointPoint类)。接口 ProceedingJoinPoint 其有一个proceed()方法,用于执行目标方法。若目标方法有返回值,则该方法的返回值就是目标方法的返回值。最后,环绕增强方法将其返回值返回。**该增强方法实际是拦截了目标方法的执行。**例如(接口与接口实现类与3.4.2的一致):
// 环绕通知
@Around(value = "execution(String doSome(..))")
public void doLog(ProceedingJoinPoint pjp) throws Throwable {
// 先执行前置增强功能
System.out.println("前置增强功能执行了!!!");
// 执行目标方法
pjp.proceed();
// 获得目标方法的返回值并输出
String proceed = (String) pjp.proceed();
System.out.println(proceed);
// 执行后置增强功能
System.out.println("后置增强功能执行了!!!");
}
@Test
public void test() {
String config = "applicationContext.xml";
ApplicationContext ac = new ClassPathXmlApplicationContext(config);
SomeService myService = (SomeService) ac.getBean("myService");
myService.doSome("张三",22);
}
输出:
前置增强功能执行了!!!
业务方法实现了!!!
业务方法实现了!!!
张三
后置增强功能执行了!!!
(4)@AfterThrowing(异常通知)
在目标方法抛出异常后执行。该注解的 throwing 属性用于指定所发生的异常类对象。当然,被注解为异常通知的方法可以包含一个参数 Throwable,参数名称为 throwing 指定的名称,表示发生的异常对象。这种通知用得比较少,一般都是当目标方法可能会抛出异常时进行使用。有点类似于try…catch语句中的catch语句中的输出,例如:
public interface SomeService {
void doSome(Integer num);
}
public class SomeServiceImpl implements SomeService {
@Override
public void doSome(Integer num) {
System.out.println("发生异常" + num/0);
}
}
@AfterThrowing(value = "execution(void doSome(..))",throwing = "th")
public void doLog(Throwable th) throws Throwable {
// 打印异常的输出
System.out.println("抛出算术异常" + th.getMessage());
}
@Test
public void test() {
String config = "applicationContext.xml";
ApplicationContext ac = new ClassPathXmlApplicationContext(config);
SomeService myService = (SomeService) ac.getBean("myService");
myService.doSome(10);
}
输出:
(5)@After(最终通知)
**这个异常其实就类似于try…catch中的finally子句,不管怎样都是在最后执行。**使用也十分的简单,例如:
public interface SomeService {
void doSome();
}
public class SomeServiceImpl implements SomeService {
@Override
public void doSome() {
System.out.println("业务方法执行了!!!");
}
}
@After(value = "execution(void doSome(..))")
public void doLog(){
System.out.println("最终方法输出了!!!");
}
@Test
public void test() {
String config = "applicationContext.xml";
ApplicationContext ac = new ClassPathXmlApplicationContext(config);
SomeService myService = (SomeService) ac.getBean("myService");
myService.doSome();
}
输出:
业务方法执行了!!!
最终方法输出了!!!
(6)Pointcut(定义切入点)
有时候,如果我们的目标方法是一致的,当需要多目标方法进行进行多个注解,那么就可以使用定义切入点的方法,这个用得就十分的少了,它的主要是用来给切入点表达式定义一个别名,例如:
@After(value = "myPointcut()")
public void doLog(){
System.out.println("最终方法输出了!!!");
}
@Pointcut(value = "execution(void doSome(..))")
public void myPointcut(){
// 里面不需要写代码
}
其最后的输出结果与(5)中的一致。
1、目标类中有接口的话就是JDK的动态代理的方式。
2、目标类没有接口使用的是CGLIB的动态代理的方式,Spring框架会自动应用CGLIB
3、CGLIB只需要要求目标类可以继承即可。并且如果目标类没有接口,也是可以使用CGLIB动态代理的,但需要在Spring的配置文件中加入一个:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j24oswDY-1646825366507)(E:\Pictures\Typora-pictures\1641191996813.png)]
4、CGLIB的动态代理方式在程序中执行的效率会更高一些
四、Spring中集成Mybatis
要实现Spring与mybatis的整合,需要引入一个新的依赖Spring与Mybatis集成的依赖:
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.1</version>
</dependency>
有了 这个依赖之后,就可以将Spring的配置文件与Mybatis的配置文件进行绑定。绑定之后,Mybatis配置文件中的数据源就不需要配置了,交给Spring来配置。因为,数据源的配置其实本质上就是创SqlSessionFactory 对象,因此,这个对象可以交给Spring来创建以及管理。**并且,这个数据源我们使用阿里的druid数据库连接池来进行配置。**这样的话,我们只需要在Spring的配置文件中注册druid的bean即可。
我们可以用一个形象的比喻来类比整合的过程:Spring 像插线板一样,mybatis 框架是插头,可以容易的组合到一起。插线板 spring 插上 mybatis,两个框架就是一个整体。
4.1、具体整合的实现
【1】新建一个maven项目:
【2】添加maven依赖:
<dependencies>
<!--测试包依赖-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<!--Spring依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
<!--Mybatis的依赖-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.1</version>
</dependency>
<!--Mybatis与Spring集成的依赖-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.1</version>
</dependency>
<!--JDBC的依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
<!--MySQL数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.9</version>
</dependency>
<!--druid数据库连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.12</version>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/java</directory><!--所在的目录-->
<includes><!--包括目录下的.properties,.xml 文件都会扫描到-->
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
<plugins>
<!--编译器插件-->
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
【3】创建实体类
public class Student {
private String name;
private Integer age;
public Student(String name, Integer age) {
this.name = name;
this.age = age;
}
public void setName(String name) {
this.name = name;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
【4】定义接口
public interface StudentDao {
// 添加学生
int insertStudent(Student student);
// 删除学生
int deleteStudent(Integer studentId);
// 修改学生信息
int updateStudent(Student student);
// 查询所有学生信息
List<Student> findAllStudent();
// 根据Id进行查询学生信息
Student findStudentById(Integer id);
}
【5】定义接口映射文件
<mapper namespace="com.beim.dao.StudentDao">
<!--添加学生信息-->
<insert id="insertStudent">
insert into student(name,age) values (#{name},#{age})
</insert>
<!--删除学生信息-->
<delete id="deleteStudent">
delete from student where id = #{studentId}
</delete>
<!--更新学生信息-->
<update id="updateStudent">
update student set name = #{name},age = #{age}
</update>
<!--查询所有学生信息-->
<select id="findAllStudent" resultType="com.beim.entity.Student">
select name,age from student
</select>
<!--根据ID值查询学生信息-->
<select id="findStudentById" resultType="com.beim.entity.Student">
select name,age from student where id = #{studentId}
</select>
</mapper>
【6】定义Service接口和实现类
public interface StudentService {
// 添加学生
int insertStudent(Student student);
// 删除学生
int deleteStudent(Integer studentId);
// 修改学生信息
int updateStudent(Student student);
// 查询所有学生信息
List<Student> findAllStudent();
// 根据Id进行查询学生信息
Student findStudentById(Integer studentId);
}
public class StudentServiceImpl implements StudentService {
private StudentDao studentDao;
// 为了实现对StudentDao的set注入
public StudentServiceImpl(StudentDao studentDao) {
this.studentDao = studentDao;
}
@Override
public int insertStudent(Student student) {
return studentDao.insertStudent(student);
}
@Override
public int deleteStudent(Integer studentId) {
return studentDao.deleteStudent(studentId);
}
@Override
public int updateStudent(Student student) {
return studentDao.updateStudent(student);
}
@Override
public List<Student> findAllStudent() {
return studentDao.findAllStudent();
}
@Override
public Student findStudentById(Integer studentId) {
return studentDao.findStudentById(studentId);
}
}
【7】定义Mybatis配置文件
<configuration>
<settings>
<!--设置Mybatis的输出日志-->
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
<mappers>
<mapper resource="com/beim/dao/StudentDao.xml"/>
</mappers>
</configuration>
【8】创建Spring的配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--注册Druid的实例对象,利用阿里的Druid数据库连接池来连接数据库-->
<bean id="myDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<!--URL地址-->
<property name="url" value="jdbc:mysql://localhost:8080/springdb"/>
<!--用户名-->
<property name="username" value="root"/>
<!--密码-->
<property name="password" value="333" />
</bean>
<!--创建SqlSessionFactory对象来创建DAO对象-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!--set注入,把数据库连接池付给了dataSource属性-->
<property name="dataSource" ref="myDataSource" />
<!--mybatis主配置文件的位置
configLocation属性是Resource类型,读取配置文件
它的赋值,使用value,指定文件的路径,使用classpath:表示配置文件的位置
-->
<property name="configLocation" value="classpath:mybatis-config.xml" />
</bean>
<!--创建DAO对象用来操作数据库-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!--指定SqlSessionFactory对象的id-->
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
<!--指定包名, 包名是dao接口所在的包名。
MapperScannerConfigurer会扫描这个包中的所有接口,把每个接口都执行
一次getMapper()方法,得到每个接口的dao对象。
创建好的dao对象放入到spring的容器中的。 dao对象的默认名称是 接口名首字母小写
-->
<property name="basePackage" value="com.beim.dao"/>
</bean>
<!--创建service对象-->
<bean id="studentService" class="com.beim.service.impl.StudentServiceImpl">
<property name="studentDao" ref="studentDao" />
</bean>
</beans>
这里有一点我需要说明一下,就是我们在写完创建DAO对象的bean标签之后,后面在写service对象的标签时,对StudentServiceImpl实现类中的StudentDao进行依赖注入时,ref的值为何不使用ID进行赋值,而是写成了ref=“studentDao”,也就是说:
<!--创建Dao对象-->
<bean id = "myStudentDao"class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
<property name="basePackage" value="com.beim.dao"/>
</bean>
<!--创建service对象-->
<bean id="studentService" class="com.beim.service.impl.StudentServiceImpl">
<property name="studentDao" ref="myStudentDao" />
</bean>
这种赋值的写法就跟我们之前提到过的,对于引用类型的依赖注入方法就一致了,这样ref的值就可以自定义了,其值随着DAO对象的ID的改变而改变,这样不就更加的方便一些吗?但实际的结果却是报了一个异常:
Bean must be of ‘com.beim.dao.StudentDao’ type ,这是为何呢?要弄明白这个问题,就先要去看一下Spring是如何创建DAO对象的。实际上,Spring创建DAO对象借助的是MapperScannerConfigurer类,翻译成中文是叫做映射扫描配置器。这个类有一个basePackage属性,它的值就是mapper映射文件所在的包。
这个属性的执行原理是:扫描该包下的所有接口,把每个接口都执行一次getMapper()方法,得到每个接口的dao对象。然后将每个接口的DAO对象放入到Spring容器中,并且DAO对象的名称为首字母小写的接口名。
所以,在你设置好basePackage标签的时候,其实DAO对象就已经创建好了,因此我们只能用其默认的名称,也就是ref的值只能为首字母小写的接口名。
【9】测试
public class MyTest {
@Test
public void MyTest(){
String config = "spring-config.xml";
ApplicationContext ac = new ClassPathXmlApplicationContext(config);
StudentService studentService = (StudentService) ac.getBean("studentService");
List<Student> allStudent = studentService.findAllStudent();
System.out.println(allStudent);
}
}
输出结果这里就不放上去了,大家可以自行去测试
【10】升级
实际上,对于数据源的URL等配置信息,我们可以写到一个单独的配置文件中去,如下所示:
<!--
把数据库的配置信息,写在一个独立的文件,编译修改数据库的配置内容
spring知道jdbc.properties文件的位置
-->
<context:property-placeholder location="classpath:jdbc.properties" />
<!--声明数据源DataSource, 作用是连接数据库的-->
<bean id="myDataSource" class="com.alibaba.druid.pool.DruidDataSource"
init-method="init" destroy-method="close">
<!--set注入给DruidDataSource提供连接数据库信息 -->
<!-- 使用属性配置文件中的数据,语法 ${key} -->
<property name="url" value="${jdbc.url}" /><!--setUrl()-->
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}" />
<property name="maxActive" value="${jdbc.max}" />
</bean>
jdbc.properties配置文件放入到resources包下:
这样配置的好处就是将数据库的配置信息与XML文件解耦合,这样修改维护成本较低。需要注意的是,Spring和mybatis整合在一起使用时,事务是自动提交的。
五、Spring的事务提交机制
要了解Spring的事务提交机制,我们先简单的回顾一下事务机制,在MySQL的学习中,相信大家一个都知道事务的作用机理是什么,具体的原生事务怎么实现的,底层原理是什么,在这里就不做过多分析了,我们只简单的回顾一下事务能干啥?
说白了,学到这个份上,事务用得上的作用就是它能保证你处理一组事情成功与否的一致性,举个例子:**我向你转账100块钱,那么实现这个功能就应该包括两个模块,第一个模块是是保证我的账户余额减少100,第二个模块就是保证你的账户余额增加100元。**好,现在如果你那边的银行系统出现了故障,而我这边的系统是正常的,我这边的余额已经减少了100元,但你的账户余额由于系统故障没有增加100元,那这不就出现问题了吗?当然,我们这里还只是涉及到一个金额不高的转账?如果是金额很高的呢?那将会造成十分严重的安全后果,于是我们引入了事务机制,**它可以保证我的转账操作中的两个模块要么都成功,要么都失败!**如果任何一方出现问题,都会实现回滚,从而保证安全性。
明白了事务的大致作用,还存在一个问题?那就是事务机制只是一个思想,不同技术框架,不同数据库实现事务机制的方式不一致,比如说,如果你习惯用MySQL,那你就得按照MySQL的方式来实现事务,如果你用的是Oracle,那你就得使用Oracle的方式来实现事务,如果你是…这对于程序员来说就十分的麻烦了,因为我们不可能记住这么多种实现方式,那么,有没有一个统一的实现方式呢?
为此,Spring框架的出现,就解决了这个问题,**它提供了一个统一的模型,规范了事务的实现机理,不同的数据库的实现机理都可以归纳为这个统一的模型。**如下图所示
作为开发人员的我们,只需要面向Spring就行了。Spring主要提供了两种实现事务的方法。
5.1、Spring事务管理的API
事务管理器是 PlatformTransactionManager 接口对象。其主要用于完成事务的提交、回滚,及获取事务的状态信息。
该接口有两个常用的实现类:
- DataSourceTransactionManager:使用 JDBC 或 MyBatis 进行数据库操作时使用。
- HibernateTransactionManager:使用 Hibernate 进行持久化数据时使用。
Spring的回滚方式:Spring 事务的默认回滚方式是:发生运行时异常和 error 时回滚,发生受查(编译)异常时提交。不过,对于受查异常,程序员也可以手动设置其回滚方式。
这里需要注意一下就是,Spring的回滚方式:我们都知道异常分为运行时异常和编译时异常两种,当程序出现了运行时异常和Error时,Spring是默认直接回滚的,如果是编译时异常,Spring是默认直接提交事务的。
5.2、Spring使用注解实现事务
【1】我们新建一个maven项目,取名为:ch-spring-transaction
【2】添加pom文件的依赖
<!--Spring中支持事务机制的两个依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
2、声明式事务:把事务相关的资源和内容都提交给Spring,Spring就能处理事务的提交和回滚了,几乎不需要写代码。
3、使用transactionak注解的步骤:
- 第一步:需要声明事务管理器对象
- 第二步:开启事务注解驱动
- 第三步:在目标方法上加入注解
4、事务的注解必须放入到public方法上,如果是其他类型的方法没有意义,因为它Spring会将方法前面的权限修饰符去掉,所以是没有意义的。
5、rollbackfor处理逻辑:
(1)spring框架会首先检查方法抛出的异常是不是在rollbackfor的属性值值中,如果异常在rollbackfor列表中,不管是什么类型的异常,一定回滚
(2)如果抛出的异常不再rollbackFor的列表中,Spring会判断异常是不是RuntimeException,如果是,一定回滚