spring学习笔记
视频地址:https://www.bilibili.com/video/BV1nz4y1d7uy/?vd_source=c9d6648c0583f404d322e2130b32ea96
0.Spring介绍
- Spring是一个框架,核心技术为IOC和AOP,主要功能在于实现解耦合,同时spring也可以理解为是一个容器(Spring底层使用concurrentHashMap容器存储创建的对象),容器中存放的是Java对象(bean),可以根据需要从容器中取出相应的对象。如何把对象放入spring容器中呢?主要有以下两种方法:
- 使用xml配置文件,具体来说就是使用
<bean>
标签。 - 通过注解将对象放入spring容器中,
- 使用xml配置文件,具体来说就是使用
- Spring 容器是一个超级大工厂,负责创建、管理所有的 Java 对象,这些 Java 对象被称 为 Bean。Spring 容器管理着容器中 Bean 之间的依赖关系,Spring 使用“依赖注入”的方式来管理 Bean 之间的依赖关系。使用 IoC 实现对象之间的解耦和。
- 存在两种方法将对象放入容器中:(1)通过创建spring容器的配置文件,并在配置文件中利用
<bean>
声明对象。(2)通过一系列注解(注解的底层原理也是基于配置文件)。而后根据需求通过BeanFactory
或者ApplicationContext
中的getBean("id")
方法获取容器中的对象。
1.Spring IOC
1.1 概念
Spring IOC也称为控制反转,把对象的创建,赋值,管理工作都交给Spring容器实现,也就是对象的创建是由其它外部资源完成。
-
控制指的就是创建对象,对对象的属性赋值,对象之间的关系管理的权力。
-
反转指的就是把原来的开发人员管理,创建对象的权限转移给代码之外的容器实现。 由容器代替开发人员管理对象,创建对象。
-
正转:开发人员主动管理对象。在代码中,使用new构造方法创建对象。
public static void main(String args[]){ Student student = new Student(); // 在代码中, 创建对象。--正转。 }
IOC的作用:实现解耦合,让对象的管理更加方便(可以在减少代码的改动的基础上,实现不同的功能。)
1.2 技术实现
- IoC 是一个概念,是一种思想,其实现方式多种多样。当前比较流行的实现方式是依赖注入(DI)。
- IOC的技术实现:Spring框架使用依赖注入(DI)实现 IoC。而Ioc拥有创建对象的权力,究其根本,Spring底层创利用反射机制实现对象的创建。
所谓的依赖注入,其实是当一个bean实例引用到了另外一个bean实例时,spring容器帮助我们创建依赖bean实例并注入(传递)到另一个bean中。分析这个过程,可以发现存在着循环依赖等问题。
依赖注入的理解:在调用无参构造函数创建对象后,就要对对象的属性进行初始化。初始化是由容器自动完成的,整个赋值过程称为依赖注入。 【可参考:https://aliyuque.antfin.com/lizixuan.lzx/kczhzp/mpytnlknvxt3cxo6?singleDoc#】
1.3 spring IOC的简单使用
- 创建maven项目
- maven项目引入依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
- 定义接口和接口的实现类(创建类)
//接口
package com.sysu.service;
public interface SomeService {
public void doSome();
}
//接口实现类
package com.sysu.service.impl;
import com.sysu.service.SomeService;
public class SomeServiceImpl implements SomeService {
@Override
public void doSome() {
System.out.println("执行dosome方法");
}
}
- 创建spring配置文件,使用
<bean>
标签声明对象。
在main/resources
目录下创建一个beans.xml文件,该文件即为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">
<!--bean标签的作用:告诉spring创建对象
id:对象的名称,唯一值。spring通过这个名称找到对象
class:类的全限定名称(不能是接口,因为spring是反射机制创建对象,必须使用类.
一个bean标签的作用等同于让spring完成Someservice someService(id值) = new SomeserviceImpl();
在bean标签内使用<property name="" value=""/>标签可以实现给对象内的属性赋值。
然后spring会把创建好的对象放入到map中,springMap.put(id的值,对象);
例如springMap.put("someservice", new SomeserviceImpl())
一个bean标签声明一个对象。根据id值就能获取唯一对应的对象
-->
<bean id="someService" class="com.sysu.service.impl.SomeServiceImpl"></bean>
</beans>
<!--
spring的配置文件
1.beans: 根标签,spring把java对象成为bean。
2.spring-beans.xsd是约束文件,和mybatis指定dtd是一样的。
-->
- 定义测试类,通过
ApplicationContext
接口和它的实现类ClassPathXmlApplicationContext
中的方法getBean完成容器中对象的获取。
@Test
public void test02(){
String config = "beans.xml";
//创建spring容器对象,同时会创建配置文件中所有bean标签声明的对象(默认采用无参构造函数)
ApplicationContext ac = new ClassPathXmlApplicationContext(config);
//根据id从spring容器中获取目标对象
SomeService someService = (SomeService) ac.getBean("someService");
someService.doSome();
}
1.4 基于XML的DI(了解)
- 在调用无参构造函数创建对象后,就要对对象的属性进行初始化。初始化是由容器自动完成的,整个赋值过程称为依赖注入。 依赖注入(DI)主要分为两种:基于XML的DI(了解) + 基于注解的DI(重点)
- 基于XML的DI分为三种:set 注入、构造注入以及自动注入,其中自动注入只负责给引用类型属性赋值,分为byName以及byType(见@Autowired注解)两种。
- byName:按名称注入,类中引用类型的【属性名】和spring容器中bean的【id】一样、数据类型一样,则把这样的bean赋值给引用类型。
- set注入以及构造注入既可以给简单类型(8种基本数据类型以及string)赋值也可以给引用类型赋值。
- set注入底层原理:spring会调用类中属性对应的set方法,在set方法中完成属性赋值。
- 构造注入底层原理:spring调用类的有参构造函数完成属性赋值。
- 有关基于XML的DI可以参考视频。
1.5 基于注解的DI(重点)
使用注解的简单步骤(@Component)
-
使用maven构建项目
-
在maven配置文件中加入依赖
- 在加入spring-context依赖的同时,会自动间接加入spring-aop的依赖。使用注解必须使用spring-aop依赖
-
创建类,在类中加入spring的注解
package com.sysu.entity
//@Component注解的作用
//(1)告诉spring创建一个名为myStudent的对象(默认调用无参构造函数),并放置spring容器中。起作用等同于spring配置文件中的bean标签,该注解有一个value属性,该属性对应的值即为创建的对象的名称,等同于bean Id。
//(2)@Component("自定义创建对象名")【最常用的方式】
//(3)value属性可以省略,直接使用@Component即可。则创建类的名称由spring默认提供:类名首字母小写
//(4)当某个类不属于业务层、持久层、控制层的时候,即使用@Component注解进行对象的创建。
@Component(value = "myStudent")
public class Student(){
private String name;
private Integer age;
//省略get、set方法
}
spring中和@Component注解功能一致、使用语法相同的注解有:
1. @Repository(用在持久层类的上面): 放在dao的实现类上面,表示创建dao对象, dao对象是能访问数据库的。
2. @Service(用在业务层类的上面): 放在service的实现类上面,创建service对象, service对象是做业务处理,可以有事务等功能的。
3. @Controller(用在控制器的上面):放在控制器(处理器)类的上面,创建控制器对象的,控制器对象能够接收用户提交的参数,显示请求的处理结果。
- 创建spring的配置文件(applicationContext.xml),加入一个组件扫描器的标签,说明注解在你的项目中的位置
<?xml version="1.0" encoding= "UTF-8" ?>
<beans xmIns="http://www.springframework.org/schema/beans”
xmlns:xsi="http://www.W3.org/2001/XMschema-instance"
xmLns:context="http://www.springframework.org/schema/context"
xsi:schemalocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!--声明组件扫描器(Component-scan),组件就是java对象
basepackage:指定注解在你的项目中的包名。
component-scan工作方式: spring会扫描遍历base-package指定的包
把包中和子包中的所有类中的注解,按照注解的功能创建对象,或给属性赋值。
-->
<context:component-scan base-package="com.sysu.entity"/>
</beans>
- 测试方法
@Test
public void test01(){
String config = "applicationContext.xml";
ApplicationContext ctx = new ClassPathXmlApplicationContext(config);
Student stu = (Student)ctx.getBean("myStudent");
System.out.println(stu);
}
@Value
- @Value:给类中的简单类型(8种基本数据类型+string)属性进行赋值
- 使用位置: 在属性定义的上面,无需set方法。
package com.sysu.entity
@Component("myStudent")
public class Student(){
@Value("张飞")
private String name;
@Value("29")
private Integer age;
}
@Test
public void test01(){
String config = "applicationContext.xml";
ApplicationContext ctx = new ClassPathXmlApplicationContext(config);
Student stu = (Student)ctx.getBean("myStudent");
System.out.println(stu);
}
@Autowired
- @Autowired: 给类中的引用类型属性进行赋值。
- @Autowired使用的是自动注入原理(byName,byType),默认使用的是byType自动注入。
- 使用的位置:在属性定义的上面,无需set方法。
- 在本实例中,当扫描到@Autowired注解时,需要提供一个和School类同源的对象,因此还需要先实例化一个School对象mySchool.
- 同源对象:
- 由相同类生成的两个对象
- 类和类之间是继承关系,由这两个类生成的对象也为同源对象
- 类和类之间是接口与实现类关系,由这两个类生成的对象也为同源对象
package com.sysu.entity
@Component("myStudent")
public class Student(){
@Value("张飞")
private String name;
@Value("29")
private Integer age;
@Autowired
private School school;
}
-------------------------------------------------
package com.sysu.entity
@Component("mySchool")
public class School{
@Value("中山大学")
private String name;
@Value("广州")
private String address;
}
@Test
public void test01(){
String config = "applicationContext.xml";
ApplicationContext ctx = new ClassPathXmlApplicationContext(config);
Student stu = (Student)ctx.getBean("myStudent");
System.out.println(stu);
}
- @Autowired注解 +@Qualifier注解即可实现采用byName的方式对引用类型属性进行赋值
- @Qualifier注解使用方法:@Qualifier("给属性赋值的对象的名称")注解
package com.sysu.entity
@Component("myStudent")
public class Student(){
@Value("张飞")
private String name;
@Value("29")
private Integer age;
@Autowired
@Qualifier("mySchool")
private School school;
}
-------------------------------------------------
package com.sysu.entity
@Component("mySchool")
public class School{
@Value("中山大学")
private String name;
@Value("广州")
private String address;
}
@Test
public void test01(){
String config = "applicationContext.xml";
ApplicationContext ctx = new ClassPathXmlApplicationContext(config);
Student stu = (Student)ctx.getBean("myStudent");
System.out.println(stu);
}
@Resource注解
- 该注解来自JDK,spring框架提供了对这个注解的功能支持,可以使用该注解给引用类型属性进行赋值,该注解的功能和@Autowired相同,底层原理也是使用自动注入,支持byName(默认)、byType。
- 使用的位置:在引用类型属性定义的上面,无需set方法。
- 先采用byName注入,如果失败则采用byType方式注入。
package com.sysu.entity
@Component("myStudent")
public class Student(){
@Value("张飞")
private String name;
@Value("29")
private Integer age;
@Resource("mySchool")
private School school;
}
-------------------------------------------------
package com.sysu.entity
@Component("mySchool")
public class School{
@Value("中山大学")
private String name;
@Value("广州")
private String address;
}
@Test
public void test01(){
String config = "applicationContext.xml";
ApplicationContext ctx = new ClassPathXmlApplicationContext(config);
Student stu = (Student)ctx.getBean("myStudent");
System.out.println(stu);
}
1.6 使用多个Spring配置文件
- 在实际应用里,随着应用规模的增加,系统中Bean数量也大量增加,导致配置文件变得非常庞大、臃肿。为了避免这种情况的产生,提高配置文件的可读性与可维护性,可以将Spring配置文件分解成多个配置文件。
- 多个配置文件中有一个总配置文件,总配置文件会引入其余配置文件。在 Java 代码中只需要使用总配置文件对容器进行初始化即可。
例如一个项目中存在三个spring配置文件
spring-school.xml
spring-student.xml
total.xml
total.xml配置文件的内容如下:(classpath为类路径,即配置文件相对于target/classes目录的路径)
<import resource="classpath:spring-school.xml"/>
<import resource="classpath:spring-student.xml"/>
1.7 注解与xml的对比
注解优点:
- 方便、直观、高效(代码少,没有配置文件的书写那么复杂)。
注解缺点:
- 以硬编码的方式写入到 Java 代码中,修改是需要重新编译代码的。
XML优点:
- 配置和代码是分离的
- 在 xml 中做修改,无需编译代码,只需重启服务器即可将新的配置加载。
XML缺点
- 编写麻烦,效率低,大型项目过于复杂。
2.AOP
2.1 AOP基本概念
-
理解角度一:AOP(Aspect Orient Programming),面向切面编程。它是一种思想,常见的实现该思想的框架有Spring以及AspectJ。这两个框架底层都是通过动态代理实现AOP,即AOP是从动态角度考虑程序的运行,所以AOP这种思想的作用可以等价于动态代理的作用。
-
AOP(面向切面编程)的含义:合理的安排切面的执行时间(在目标方法前, 还是目标方法后)、合理的安排切面执行的位置(例如在哪个类或者哪个方法需要进行功能增强)
-
AOP作用
- 可以在你类源码不变的情况下,给类增加一些额外的功能,减少代码的冗余。同时能够实现业务功能和非业务功能【日志、事务等】的解耦合。
- 具体来说就是在程序执行过程中,通过创建代理对象,然后让代理对象执行方法,从而实现给目标类的方法增加额外的功能。
-
aop的使用场景:
- 当你需要修改系统中某个类的功能,原有类的功能不完善,而你又没有源代码的情况。
- 当你需要给项目中多个类增加相同的功能时。
- 给业务方法增加事务、日志输出等功能时。
2.2 AOP的实现
对于AOP这种编程思想,很多框架都进行了实现。最常见的是Spring框架以及AspectJ框架。
- spring:spring在内部实现了aop规范,能做aop的工作。spring主要在事务处理时使用aop。然而项目开发中很少使用spring的aop实现。 因为spring的aop比较笨重。
- aspectJ: 一个开源的专门做aop的框架。spring框架中集成了aspectj框架,通过spring就能使用aspectj的功能。在Spring中使用AOP开发时,一般都使用AspectJ的实现方式。aspectJ框架实现aop有两种方式:使用xml的配置文件以及使用注解。
2.3 AOP相关术语以及aspectJ实现原理
Aspect(切面)
给你的目标类增加的功能就可以认为是一个切面,切面一般都是非业务方法且独立使用的。常见的切面有:日志记录、输出执行时间、参数检查、事务等等。
JoinPoint(连接点)
表示切面的执行位置。例如给业务方法A增加切面,则业务方法A就是JoinPoint。JoinPoint一般表示一个业务方法。
Pointcut(切入点)
和JoinPoint的唯一区别在于:JoinPoint表示一个方法,Pointcut表示多个方法(多个连接点的集合)。
目标对象
给类A的方法增加功能,则类A就是目标对象。
Advice(通知)
表示切面执行的时间,比如切面是在业务方法之前执行,还是在业务方法之后执行。AspectJ有5个常用的注解用于表示切面的执行时间,@Before(切入点表达式)
、@AfterReturning(切入点表达式)
、@Around(切入点表达式)
、@AfterThrowing(切入点表达式)
、@After(切入点表达式)
。
切面三要素
一个切面有三个关键的要素:
- (切面)切面的功能代码:切面是干什么的
- (切入点)切面的执行位置,使用Pointcut表示切面执行的位置
- (切面执行时间)切面的执行时间,使用Advice表示时间,在目标方法之前,还是目标方法之后。
切面三要素在aspectJ框架中的实现
上文提到切面三要素:切面的功能代码(自己书写的)、切面的执行位置、切面的执行时间
-
在aspectJ框架中,使用5个常见的注解(也称为5个常用的通知[Advice]注解)表示切面的执行时间,切面的执行时间又叫做Advice(通知):
- @Before(切入点表达式)
- @AfterReturning(切入点表达式)
- @Around(切入点表达式)
- @AfterThrowing(切入点表达式)
- @After(切入点表达式)
- 例如:@Before(value = "execution(* com.sysu.serviceImpl..(..))")
-
在aspectJ框架中,使用切入点表达式表示切面执行的位置
execution(方法访问权限、方法返回值、方法声明(参数类型)、异常类型)不加粗部分表示可以省略。
execution(public * *(..))
指定切入点为:任意公共方法。
execution(* set*(..))
指定切入点为:任何一个以“set”开始的方法。
execution(* com.xyz.service.*.*(..))
指定切入点为:定义在 service 包里的任意类的任意方法。
execution(* com.xyz.service..*.*(..))
指定切入点为:定义在 service 包或者子包里的任意类的任意方法。“..”出现在类名中时,后面必须跟“*”,表示包、子包下的所有类。
execution(* *..service.*.*(..))
指定所有包下的service包里的任意类的任意方法为切入点
2.4 AspectJ框架在项目中的使用
具体可以见动力节点视频(P52开始)
使用aspectj实现aop的基本步骤:
1.新建maven项目
2.加入依赖
spring依赖
aspectj依锁
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.20</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.1</version>
</dependency>
<dependency>
<groupId> org.aspectj</groupId >
<artifactId>aspectjweaver</artifactId >
<version> 1.6.11</version >
</dependency>
3.创建目标类:接口和他的实现类。要做的是给类中的方法增加功能
4.创建切面类:普通类
1)在类的上方加入@Aspect注解,表明该类为切面类
2)在类中定义方法,方法就是切面要执行的功能代码,然后在方法的上面加入aspectj中的通知注解(5个中的任意一个),例如@Before(切入点表达式)
5.创建spring的配置文件:声明对象,把对象交给容器统一管理.声明对象你可以使用注解或者xml配置文件<bean>标签
1)声明目标对象
2)声明切面类对象
3)声明aspectj框架中的自动代理生成器标签<aop:aspectj-autoproxy/>。自动代理生成器:用来完成代理对象的自动创建功能(将spring容器中所有的目标对象一次性都生成代理对象)。
6.创建测试类,从spring容器中获取目标对象(实际就是代理对象).通过代理执行方法,实现aop的功能增强。
java.lang.ClassCastException: com.sun.proxy.$Proxy* cannot be cast to***问题解决方案:https://www.cnblogs.com/MrSaver/p/6104130.html
2.5 AspectJ5个通知注解和常用注解
@Before
- 被
@Before
注解修饰的方法称为前置通知方法,即在目标方法执行之前执行。 @Before(value="切入点表达式")
- 前置通知方法的定义要求:
- 公共方法public
- 方法没有返回值
- 方法可以没有参数,也可以有参数[JoinPoint]。JoinPoint参数的作用:可以在切面方法中获取业务方法执行时的信息,例如方法名,方法的形参等等。这个参数的值由框架赋予,且必须放在第一个位置。
@Aspect
public class MyAspect {
@Before("execution(* *..SomeServiceImpl.do*(..))")
public void myBefore(JoinPoint jp){
//JoinPoint能够获取到方法的定义,方法的参数等信息
System.out.println("连接点的方法定义: "+ jp.getsignature());
System.out.println("连接点方法的参数个数:"+jp.getArgs().length);方法参数的信息
Object args []= jp.getArgs();
for(Object arg: args){
System.out.println(arg);
}
//切面代码的功能,例如日志的输出,事务的处理
System.out.println("前置通知:在目标方法之前先执行,例如输出日志");
}
}
@AfterReturning
- 被
@AfterReturning
注解修饰的方法称为后置通知方法,即在目标方法执行之后执行。能够获取到目标方法的返回值,可以修改这个返回值或者根据这个返回值进行不同的处理。 @AfterReturning(value="切入点表达式",returning=“自定义变量名,表示目标方法的返回值”)
。注意自定义变量名必须和 @AfterReturning注解修饰的方法的形参名一样。- 后置通知方法的定义
- 公共方法public
- 方法没有返回值
- 该方法除了可以包含 JoinPoint 参数外,还可以包含用于接收目标方法返回值的变量。该变量最好为 Object 类型,因为目标方法的返回值可能是任何类型。
@Aspect
public class MyAspect {
@AfterReturning(value="execution(* *..SomeServiceImpl.doOther(..))",returnint="result")
public void myAfterReturning(Object result){
if(result != null){
String s = (String)result;
System.out.println("接收到目标方法的返回值");
}
System.out.println("后置通知:在目标方法之后执行的功能增强,例如执行事务处理〈切面〉");
}
}
@Around
- 被
@AfterReturning
注解修饰的方法称为环绕通知方法,即在目标方法执行之前之后执行。 @Around(value="切入点表达式")
- 环绕通知方法的定义
- 公共方法public
- 方法有返回值, 推荐使用Object,该返回值就是目标方法的返回值,是可以被修改的
- 该方法除了可以包含 JoinPoint 参数外,还有固定的参数
ProceedingJoinPoint
,该参数用于执行目标方法。
@Aspect
public class MyAspect {
@Around("execution(* *..SomeServiceImpl.doFirst(..))")
public Object myAround(ProceedingJoinPoint pjp) throws Throwable{
Object obj = null;
//增强功能
System.out.print1n("环绕通知:在目标方法之前执行的,例如输出日志");
//执行目标方法的调用,等同于JDK动态代理中的method.invoke(target,angs)
obj = pjp.proceed();
//增强功能
System.out.print1n("环绕通知:在目标方法之后执行的,例如处理事务");//返回目标方法的执行结果
return obj;
}
}
@AfterThrowing
- 被
@AfterThrowing
注解修饰的方法称为异常通知方法,即在目标方法抛出异常之后执行。类似于try/catch
- @AfterThrowing(value="切入点表达式",throwing="自定义变量名")。注意自定义变量名必须和 @AfterThrowing注解修饰的方法的形参名一样。
- 异常通知方法的定义
- 公共方法public
- 方法没有返回值
- 该方法除了可以包含 JoinPoint 参数外,还可以包含Throwable对象,用于表示目标方法发生的异常对象。
@AfterThrowing(value="execution(* *..SomeServiceImpl.doSecond(..))",throwing="ex")
public void myAfterThrowing(Throwable ex){
//把异常发生的时间,位置,原因记录到数据库,日志文件等等,
//可以在异常发生时,把异常信息通过短信,邮件发送给开发人员。
System.out.print1n("异常通知:在目标方法抛出异常时执行的,异常原因:" + ex.gethessage());
}
@After
- 被
@After
注解修饰的方法称为最终通知方法,无论目标方法是否抛出异常都会执行。类似于finally
@Around(value="切入点表达式")
- 最终通知方法的定义要求:
- 公共方法public
- 方法没有返回值
- 方法可以没有参数,也可以有参数[JoinPoint]
@After("execution(* *..SomeServiceImpl.doThird(..))")
public void myAfter(){
System.out.println("最终通知:总是会被执行的方法");
}
@Pointcut
- 用于定义和管理切入点,如果项目中有多个切入点表达式是重复的,可以复用的。那么可以使用@Pointcut对切入点表达式进行定义。
- @Pointcut(value="切入点表达式"),使用位置在自定义的方法上面
- 当在一个通知方法的上面使用@Pointcut,此时这个方法的名称就是切入点表达式的别名。在其它的通知方法中,value属性就可以直接使用这个方法名称代替切入点表达式。
@Aspect
public class MyAspect {
@Pointcut(value="execution(* *..SomeServiceImpl.doThird(..))")
private void mypt(){
//无需代码
}
}
可以使用value="mypt()"代替value="execution(* *..SomeServiceImpl.doThird(..))"。
2.6 动态代理
- 动态代理的功能:可以在你类源码不变的情况下,给类增加一些额外的功能,减少代码的冗余。
- 具体来说就是在程序执行过程中,通过创建代理对象,然后让代理对象执行方法,从而实现给目标类的方法增加额外的功能。动态代理有两种实现方式:JDK动态代理与CGLIB动态代理。
(1) JDK动态代理
JDK动态代理要求需要进行功能扩展的类必须实现接口,Java语言中通过Java.lang.reflect包提供三个类支持代理模式:Proxy、Method和InovcationHandler。
JDK动态代理实现步骤:
1.创建目标类
2.创建InovacationHandler接口的实现类,在这个类中给目标方法增加功能。
3.使用JDK中的类Proxy,创建代理对象,使用代理对象实现功能增加。
具体看动力节点的动态代理视频。
(2) CGLIB动态代理
- 如果目标类实现了接口,使用JDK动态代理来生成代理类及代理类实例对象(底层通过反射实现)。
- 对于没有实现接口的对象,利用cglib为其提供代理,在运行时动态的生成某个类的子类,注意,CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为
final
,那么它是无法使用CGLIB做动态代理的。 - CGLIB原理:动态生成一个要代理类的子类,子类继承并重写要代理的类的所有不是final的方法。在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。它比使用java反射的JDK动态代理要快。
3.Spring与MyBatis的整合
使用Spring的IOC核心技术,把MyBatis框架中使用的对象交给Spring统一创建和管理。
参考:https://cloud.tencent.com/developer/article/1935568
1.新建maven项目
2.加入maven的依赖
1) spring依赖
2) mybatis依赖
3) mysql驱动
4) spring的事务的依赖
5) mybatis和spring集成的依赖:mybatis官方提供的,用来在spring项目中创建mybatis的sqlsesissonFactory, dao对象的
3.创建实体类
4.创建dao接口和mapper文件
5.创建mybatis主配置文件
6.创建service接口和实现类,属性是dao。
7.创建spring的配置文件:利用spring中的bean标签创建与mybatis相关的对象
1)创建数据源对象
2)创建sqlsessionFactory对象
3)创建Dao对象
4)声明自定义的service
8.创建测试类,获取Service对象,通过service调用dao完成数据库的访问。
源码见GitHub仓库ch06-Spring-MyBatis
4.Spring事务
4.1 使用事务的时机
- 当我的操作,涉及得到多个表或者多个sql语句(例如insert,update,delete)时,需要保证这些语句要么全部都执行成功,要么全部都执行失败(事务的原子性),这时候就需要使用事务对多个sql语句进行管理。
- 在Java代码中,一般都在service类的业务方法上进行事务的操作,因为通常业务方法会调用多个dao方法,执行多个sql语句
4.2 JDBC和Mybatis如何处理事务
JDBC创建连接Connection conn
; 利用conn.commit()
、 conn.rollback()
进行事务的处理。
mybatis访问数据库,利用SqlSession.commit()
、SqlSession.rollback()
进行事务的处理。
缺点:多种数据库的访问技术,有不同的事务处理的机制,对象,方法。
4.3 Spring事务
Spring提供一种处理事务的统一模型, 能使用统一步骤和方式完成多种不同数据库的事务处理功能。例如使用spring的事务处理机制,既可以完成mybatis访问数据库的事务处理,同时也可以完成hibernate访问数据库的事务处理。
Spring处理事务的模型以及使用的步骤都是固定的。开发人员只需要把事务相关信息(例如使用哪种数据库访问技术对事务进行管理,JDBC、Mybatis还是hibernate)提供给spring就可以了。
(1) 事务管理器对象
事务管理器是一个接口和他的众多实现类。它的作用在于完成commit()、rollback()等操作。同时spring把每一种数据库访问技术对应的事务处理类都创建好了。
-
如果使用mybatis访问数据库,则spring在内部会创建mybatis对应的事务管理器对象:
DataSourceTransactionManager
-
如果使用hibernate访问数据库,则spring在内部会创建hibernate对应的事务管理器对象:
HibernateTransactionManager
-如何把事务相关信息提供给Spring?
-在Spring的配置文件中使用标签声明数据库访问技术对应的事务管理器对象,例如,你要使用mybatis访问数据库,你应该在xml配置文件中进行如下的配置:<bean id=“xxx" class="...DataSourceTransactionManager">
(2) 事务定义接口
事务定义接口TransactionDefinition
中定义了事务描述相关的三类常量:事务隔离级别、 事务传播行为、事务默认超时时限,及对它们的操作。
(3) 事务提交、回滚的时机
-
当你的业务方法,执行成功并且没有抛出异常,在该方法执行完毕之后,spring会自动调用事务管理对象中的
commit()
方法进行事务的提交。 -
当你的业务方法抛出运行时异常或ERROR时候,spring会自动调用事务管理器对象中的
rollback()
方法进行事务的回滚。 -
当你的业务方法抛出非运行时异常, 主要是受查异常时,spring会自动调用事务管理对象中的
commit()
方法进行事务的提交。
4.4 事务描述的三要素
(1) 事务隔离级别
事务的隔离级别
DEFAULT:采用 DB 默认的事务隔离级别。MySql的默认为REPEATABLE_READ; Oracle默认为READ_COMMITTED。
➢ READ_UNCOMMITTED:读未提交。未解决任何并发问题。
➢ READ_COMMITTED:读已提交。解决脏读,存在不可重复读与幻读。
➢ REPEATABLE_READ:可重复读。解决脏读、不可重复读,存在幻读
➢ SERIALIZABLE:串行化。不存在并发问题。
(2) 事务传播行为
事务传播行为是指,处于不同事务中的方法在相互调用时,执行期间事务的维护情况。如A事务中的方法doSome()调用B事务中的方法oOther(),在调用执行期间,事务的维护情况就称为事务传播行为。事务传播行为是加在方法上的。事务传播行为常量都是以 PROPAGATION_ 开头,形如 PROPAGATION_XXX。
7个传播行为,表示你的业务方法调用时,事务在方法之间是如何使用的。
PROPAGATION_REQUIRED(掌握)
PROPAGATION_REQUIRES_NEW(掌握)
PROPAGATION_SUPPORTS(掌握)
PROPAGATION_MANDATORY
PROPAGATION_NESTED
PROPAGATION_NEVER
PROPAGATION_NOT_SUPPORTED
(3) 事务默认超时时限
表示一个方法最长的执行时间,如果方法执行时间超过了默认超时时限,事务就会回滚。单位是秒, 整数值, 默认是-1(不配置). 该值一般就使用默认值即可。
4.5 使用Spring的事务注解管理事务
- 事务在Spring框架中的实现是依赖AOP思想的,上文说到AOP的实现方式有两种:spring框架自己实现的aop以及借助Aspectj框架实现的AOP,因此事务的管理方式也存在两种:使用Spring的事务注解管理事务、使用AspectJ的AOP配置管理事务。
- Spring的事务注解适合在中小型项目中使用,spring框架底层采用自己实现的AOP给业务方法增加事务的功能。具体而言采用@Around环绕通知实现。
- 使用
@Transactional
注解增加事务。必须将该注解放在public方法的上面,表示当前方法具有事务。并且可以给注解的属性赋值,表示具体的隔离级别,传播行为,异常信息等等,不同通常都采用默认值即可。
具体的使用步骤
1.在Spring配置文件中声明事务管理器对象:<bean id="xx" class="DataSourceTransactionManager">
2.开启事务注解驱动,告诉spring框架,我要使用注解的方式进行事务的管理。
<!--声明事务注解驱动-->
<tx: annotation-driven transaction-manager="transactionManager"/>
3.在业务层public方法的上面加入@Trancational注解。
- Spring的事务注解管理事务的底层原理
spring使用aop机制,创建@Transactional所在的类代理对象,给方法加入事务的功能。具体而言就是:在你的业务方法执行之前,先开启事务,在业务方法之后提交或回滚事务,使用aop的环绕通知。
@Around("你要增加的事务功能的业务方法名称")
Object myAround(){
//开启事务,spring给你开启
try{
buy(1001,10);
//spring的事务管理器.commit();
}catch(Exception e){
//spring的事务管理器.rollback();
}
}
代码见github
4.6 使用AspectJ的AOP配置管理事务
AspectJ的AOP配置管理事务适合大型项目,当有大量的类、方法需要进行事务管理,则使用aspectj框架功能进行事务的配置与管理。
使用该方法进行事务管理的优势在于:只需要在spring配置文件中声明类,方法需要的事务,可以让业务方法和事务配置解耦合。
实现步骤: 都是在xml配置文件中实现。
1.加入aspectJ依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
2.声明事务管理器对象
<bean id="xx" class="DataSourceTransactionManager">
3.声明方法需要的事务类型(配置方法的事务属性【隔离级别,传播行为,超时】)
4.配置aop:指定哪些哪类要创建代理。
具体代码过程见github
Spring常见面试题
1.BeanFactory 和 ApplicationContext的区别
共同点:BeanFactory
和ApplicationContext
都是Spring IOC的核心接口,都是用于从容器中获取Spring beans
的,但是,他们二者有很大不同。
(1) Spring beans就是被Spring容器所管理的Java对象。
(2) Spring容器本质是一个concurrentHashMap,负责实例化,配置和装配Spring beans,然后对象的获取则由接口BeanFactory或ApplicationContext进行获取。
-
(懒加载)BeanFactroy采用的是延迟加载形式来注入Bean的,即只有在使用到某个Bean时(调用getBean()),才对该Bean进行加载实例化,
-
(即时加载)ApplicationContext则相反,它是在容器启动时,一次性创建了所有的Bean。
BeanFacotry
优缺点:应用启动的时候占用资源很少,对资源要求较高的应用,比较有优势;但是这样就不能发现一些存在的Spring的配置问题,例如Bean的某一个属性没有注入,BeanFacotry加载后,直至第一次使用调用getBean方法才会抛出异常;
ApplicationContext
优缺点:在容器启动时,就可以发现Spring中是否存在配置错误,但是内存空间占用更大,且当应用程序配置Bean较多时,程序启动较慢。
2.Bean的作用域
所谓Bean的作用域是指spring容器创建Bean后的生存周期即由创建到销毁的整个过程。之前我们所创建的所有Bean其作用域都是Singleton,这是Spring默认的,在这样的作用域下,每一个Bean的实例只会被创建一次,而且Spring容器在整个应用程序生存期中都可以使用该实例。因此之前的代码中spring容器创建Bean后,通过代码获取的bean,无论多少次,都是同一个Bean的实例
<!-- 默认情况下无需声明Singleton -->
<bean name="accountDao" scope="singleton"
class="com.zejian.spring.springIoc.dao.impl.AccountDaoImpl"/>
- singleton : 唯一bean实例,Spring 中的 bean 默认都是单例的(单例模式)。
- prototype : 每次请求都会创建一个新的 bean 实例。
- request : 每一次HTTP请求都会产生一个新的bean,该bean仅在同一个HTTP request内相同。不同http请求则bean也不相同
- session :在一个HTTP Session中,一个Bean对应一个实例。同一个浏览器中访问属于同一次会话,因此SessionBean实例都是同一个实例对象。当换一个浏览器则不同。
3.解释一下AOP
参考:https://blog.csdn.net/javazejian/article/details/56267036
AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务⽆关,却为业务模块所共同调⽤的逻辑(例如事务处理、⽇志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。
aop的作用:
- 在目标类不增加代码的情况下,给目标类增加功能。
- 减少重复的代码
- 让开发人员更加专注于业务逻辑的实现
- 解耦合:将业务功能和日志、事务等非业务功能解耦
aop的使用场景:
- 当你需要修改系统中某个类的功能,原有类的功能不完善,而你又没有源代码的情况。
- 当你需要给项目中多个类增加相同的功能时。
- 给业务方法增加事务、日志输出等功能时。
AOP可以简单理解为aspect(切面)应用到目标函数(类)的过程。对于这个过程,一般分为动态织入(两种方式)和静态织入,因此AOP有两种实现方式:
- Spring aop(动态织入,又分为两种方式)
- aspectj aop(静态织入)
(1)Spring aop
Spring aop底层通过代理实现
- 如果目标类实现了接口,使用JDK动态代理来生成代理类及代理类实例对象(底层通过反射实现)。
- 对于没有实现接口的对象,利用cglib为其提供代理,在运行时动态的生成某个类的子类,注意,CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为
final
,那么它是无法使用CGLIB做动态代理的。 - CGLIB原理:动态生成一个要代理类的子类,子类继承并重写要代理的类的所有不是final的方法。在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。它比使用java反射的JDK动态代理要快。
代理对象可以看作是目标对象的加强版,它是对目标对象中方法功能的一个扩充。
Joinpoint(连接点) | 指那些被拦截到的点,可以理解为是目标类的方法 |
---|---|
Pointcut(切入点) | 指要对哪些Joinpoint(目标类的方法)进行拦截,即被拦截的连接点 |
Advice(通知) | 指拦截到Joinpoint(目标类的方法)之后要做的事情,对方法增强的内容 |
Aspect(切面) | 切入点和通知的结合 |
(2)aspectj aop
aspectj aop底层是基于字节码操作实现的
- ApectJ采用的就是静态织入的方式。 ApectJ主要采用的是编译期织入,在这个期间使用AspectJ的acj编译器(类似javac)把aspect类编译成class字节码后,在java目标类编译时织入,即先编译aspect类再编译目标类。
- Spring 2.0版本之后,spring aop采用了aspectj语法来定义切面,但仍然没有使用 AspectJ 的编译器,底层依是动态代理技术的实现,因此并不依赖于 AspectJ 的编译器。