Spring5
1. 什么是Spring框架
spring框架是一个容器,整合了其他框架的大框架,spring的核心是 IOC 和 AOP,它由20多个模块构成,在很多领域提供优秀的解决方案
1.1 spring的特点
- 轻量级
spring由20多个模块构成,每个jar包都很小,平均小于1M,核心包也就3M,对代码无污染 - 面向接口编程
使用接口,就是面向灵活,提高项目的可扩展性与可维护性,接口不关心实现类的类型,使用时接口指向实现类,切换实现类可切换功能 - AOP:面向切面编程
AOP就是面向切面编程,面向切面编程就是将公共的、通用的、重复的代码单独开发,在需要的时候反织回去给目标对象,面向对象编程时基于动态代理实现的 - 整合其他框架
spring整合后使其他框架更易用
2. IOC
2.1 什么是IOC
IOC就是控制反转(Inversion of Control)的概念,是一种编程思想,控制反转意思就是控制权的转换,要了解IOC就要了解什东西的控制在什么对象之间被转换:
具体就是对象的创建和注入的权利由程序员转换给了spring容器
2.1.1 正转:
由程序员进行对象的创建和依赖注入被称为正转,什么时候创建由程序员决定:
创建对象:
public class Student {
private String name;
private int age;
public Student() {
System.out.println("无参构造方法在创建对象的时候就会执行");
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
程序员端的创建对象和注入:
@Test
public void testStudent(){
//程序员创建对象
Student stu = new Student();
System.out.println(stu);
}
2.1.2 反转:
由spring容器创建对象和依赖注入成为反转,将控制权从程序员手中夺走,交给spring容器:
spring框架中bean的管理全都放在applicationContext.xml文件中:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 创建学生对象(是创建对象而不是创建对象的类:因为spring的ioc的特点是控制对象的创建和赋值,并不是类的创建)
等同于 Student student = new Student();
id: 创建的对象名
class: 要创建对象的类型,底层通过反射构建对象
-->
<bean id="stu" class="org.hyqwsq.spring.pojo.Student"></bean>
</beans>
spring容器获取对象:
@Test
public void testStudentSpring(){
//由Spring容器进行对象的创建
//在spring容器中获取创建的对象,要先创建容器对象,启动容器对象后才能获取容器中的对象
ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
Student student = (Student) ac.getBean("stu");
System.out.println(student);
}
对于交给spring容器管理端的对象,再spring容器启动的时候,就创建了所有对象
2.2. 基于xml的IOC
2.2.1 创建对象
<bean id="stu" class="org.hyqwsq.spring.pojo.Student"></bean>
2.2.2 给创建的对象赋值
2.2.2.1 使用setter方法注入
创建school对象:引用注入:
public class School {
private String name;
private String address;
为了给成员变量注入值,必须提供无参的构造方法和set方法:
public void setName(String name) {
this.name = name;
}
public void setAddress(String address) {
this.address = address;
}
@Override
public String toString() {
return "School{" +
"name='" + name + '\'' +
", address='" + address + '\'' +
'}';
}
}
创建学校对象:
<!-- 创建学校对象-->
<bean id="school" class="org.hyqwsq.spring.pojo.School">
<property name="name" value="北京大学"/>
<property name="address" value="北京"/>
</bean>
在School实体类中添加Student类型成员变量
public class Student {
private String name;
private int age;
//引用类型的成员变量,学生所在的学校
private School school;
public Student() {
System.out.println("无参构造方法在创建对象的时候就会执行");
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
public void setSchool(School school) {
this.school = school;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", school=" + school +
'}';
}
}
注入带引用类型的School对象
<!-- 创建学校对象-->
<bean id="sch" class="org.hyqwsq.spring.pojo.School">
<property name="name" value="北京大学"/>
<property name="address" value="北京"/>
</bean>
<!-- 创建学生对象-->
这里注入的school对象就是spring容器中创建的school对象
<bean id="stu" class="org.hyqwsq.spring.pojo.Student">
<property name="name" value="张三"/>
<property name="age" value="23"/>
<property name="school" value="school"/>
</bean>
set方法注入总结:
- ⭐使用set方法注入必须提供无参的构造方法
- 注入分为简单类型注入和引用类型注入
- 简单类型注入值使用value属性
- 引入类型注入值使用ref属性
- xml文件中成员变量赋值顺序不影响对象创建
2.2.2.2 使用构造方法注入
创建学校对象:
public class School {
private String name;
private String address;
//使用构造方法注入要提供有参构造函数
public School(String name, String address) {
this.name = name;
this.address = address;
}
- 通过构造方法的名称注入
<!-- 创建学校对象,通过构造方法注入值-->
<bean id="school" class="com.hyq.pojo.School">
<constructor-arg name="name" value="北京大学"/>
<constructor-arg name="address" value="北京"/>
</bean>
与set方法注入的区别是在bean对象中用来给成员变量赋值的标签是< construtor-arg >
要注意注入时的成员变量名称要对照着有参构造函数里的入参名称写
- 通过构造方法参数下标注入
<!-- 创建对象,通过构造方法参数下标注入-->
<bean id="school2" class="com.hyq.pojo.School">
<constructor-arg index="0" value="清华大学"/>
<constructor-arg index="1" value="北京"/>
</bean>
通过参数下标注入成员变量标签:
基础类型: < constructor-arg index=“” value=“” >
引用类型: < constructor-arg index=“” ref=“” >
使用构造方法注入总结:
- 用默认的构造方法的参数的顺序注入 就是等于用构造方法的参数下标注入,只不过不带参数名,但是顺序要严格按照有参构造方法中参数的顺序,错了不行
- ⭐构造方法注入的标签: constructor-arg
- 注入的成员变量名要与构造方法的参数一致
2.3 基于注解IOC
基于注解IOC也成为DI(Dependency Injection),是IOC的具体实现的技术,基于xml的IOC创建对象并依赖注入是在xml中进行的,但注解IOC是现在类中创建注解而后扫描注解两个步骤组成
2.3.1 创建对象的注解
作用在类方法前,类似xml的IOC中的创建bean对象
-
@Component :
可以创建任意对象 -
@Ccontroller :
专门用来撞见控制层控制器的对象(servlet),这种对象可以接收前端客户的请求,也可以返回处理结果给前端客户 -
@Service :
专门用来创建业务逻辑层的对象,负责向下访问数据访问层,将处理完毕后的结果返回给控制层 -
@Repository:
专门用来创建数据访问层的对象,负责数据库中的正伤害差所有操作
@Component //交给Spring容器去创建对象,就是在容器启动的时候就被创建
public class School {
private String name;
private String address;
2.3.2 依赖注入的注解
- 值类型的引用
1.1 @Value: 用来给简单类型注入值(8种基本类型+String) - 引用类型的注入
2.1 @Autowired: 使用类型注入值,从整个bean工厂中搜索 同源类型 的对象注入
2.2 @Autowired + @Qualifier: 使用名称注入值,从整个bean工厂中搜索相同名称的对象进行注入
同源类型是什么:
a. 被注入的类型(Studen中的school)与注入的类型是完全相同的类型
b. 被注入的类型(Student中的school父类)与注入类型(子类)是父子类关系
c. 被注入的类型(Student中的school接口)与注入类型(实现类)是接口和实现类的类型
区分注入与被注入关系:比如说在类中的一个成员变量:private Student stu; , 这里的Student就是被注入类型,而将来在spring容器中会有一个student对象注入进来给stu,那stu就是注入类型
@Value
@Component
public class School {
@Value("广州大学")
private String name;
@Value("广州大学城")
private String address;
@Autowired
创建student对象,通过引用类型的注入的@autowired()来到bean工厂中找有没有同样类型的对象,有就注入
@Component
public class Users {
private int uid;
private String uname;
private int uage;
@Autowired
private School school;
@Autowried + @Qualifier(“xxx”)
@Component("testQualifier")
public class School {
@Autowired
@Qualifier("testQualifier")
private School school;
这两个注解回到容器种找有咩有该名字的对象并注入,这两个是并用的,如果要按名称注入不能没有Autowired
2.3.3 添加包扫描
<!-- 添加包扫描-->
<context:component-scan base-package="com.hyq"/>
这上面会发现,在对象类中和xml文件配置包扫描中并没有给对象命名,不像之前基于xml的IOC一样在xml文件中注入会先命名,但是还是能从spring容器中找到值,这里涉及到IOC命名规则:
spring容器创建 注解IOC对象命名规则:
- 创建对象的名称默认是对象类型的驼峰命名法
- 也可以用@Component(“xxx”) 注解指定创建对象的名称
2.3.3.1 添加包扫描多种方式:
- 单个包扫描(推荐使用)
<context:component-scan base-package="com.hyq.controller"/>
<context:component-scan base-package="com.hyq.service"/>
- 多个包扫描,多个包之间用逗号或空格分开
<context:component-scan base-package="com.hyq.controller com.hyq.service com.hyq.dao"/>
- 扫描跟包(不推荐)
扫描跟包会导致容器启动速度降低,导致多做无用功
<context:component-scan base-package="com"/>
依赖注入的注解总结:
- 注解一定要添加包扫描
- 在有父子类的情况下,使用按类型注解,就以为者有多个可注入的对象,因为继承父类的子类可能不止一个,此时按照名称进行二次筛选,选中与被注入对象同名的对象注入
- 如果有父子类的情况下,直接按名称注入
3. 三层项目架构(使用引用依赖注入)
springIOC在架构中管理对象范围:除了实体类,其他三成都是spring接管
3.1创建三层架构
使用三层架构进行用户的插入操作——界面层、业务逻辑层、数据访问层
创建实体类:
public class Users {
private int uid;
private String uname;
private int uage;
public Users(int uid, String uname, int uage) {
this.uid = uid;
this.uname = uname;
this.uage = uage;
}
getset()
toString()
}
数据访问层:
public class UserMapperImpl implements UserMapper{
@Override
public int insert(Users users) {
System.out.println(users.getUname()+"用户模拟增加成功");
return 1;
}
}
业务逻辑层:
public class UserServiceImpl implements UserService {
//在所有的业务逻辑层中都必定存在数据访问层的对象,并且是接口指向实现类
private UserMapper userMapper = new UserMapperImpl();
@Override
public int insert(Users users) {
return userMapper.insert(users);
}
}
控制层:
public class UserController {
//如何访问业务逻辑层,就是创建对象
//所有的界面层都会有业务逻辑层的对象
public UserService userService = new UserServiceImpl();
//控制层的功能实现,对外提供访问的功能
public int insert(Users users){
return userService.insert(users);
}
}
测试类:
@Test
public void testInsertUsers() {
//创建UsersController对象
UserController userController = new UserController();
int num = userController.insert(new Users(100, "张三", 22));
System.out.println(num);
}
3.2 spring接管三层
导入spring依赖:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
接口不可以交给spring管理,只能把实现类交给spring
将类交给spring管理,用set()方法注入需要类提供set方法:
UserServiceImpl中:
//交给spring去依赖注入值,必须提供set方法
public void setUserMapper(UserMapper userMapper) {
this.userMapper = userMapper;
}
UserController中:
public void setUserService(UserService userService) {
this.userService = userService;
}
创建对象:
<!-- 创建数据访问层对象-->
<bean id="uMapper" class="com.hyq.dao.UserMapperImpl"/>
<!-- 创建业务层逻辑对象-->
<bean id="userService" class="com.hyq.service.Impl.UserServiceImpl">
<property name="userMapper" ref="uMapper"/>
</bean>
<!-- 创建控制层的对象-->
<bean id="uController" class="com.hyq.controller.UserController">
<property name="userService" ref="userService"/>
</bean>
4. spring配置文件拆分策略
当项目越来越大,需要多人合作开发,一个配置就存在很大的隐患,拆分配置文件可以为应用指定多个spring配置文件:
按层拆分
applicationContext_controller.xml(控制层文件)
<bean id="userController" class="com.hyq.controller.UserController"/>
----------------------
applicationContext_Service.xml(业务逻辑层问文件)
<bean id="userService" class="com.hyq.service.Impl.UserServiceImpl"/>
----------------------
applicationContext_mapper.xml(数据层文件)
<bean id="userMapper" class="com.hyq.dao.UserMapperImpl"/>
按功能拆分
applicationContext_user.xml(A功能的三层都放这个文件)
<bean id="userController" class="com.hyq.controller.UserController"/>
<bean id="userService" class="com.hyq.service.Impl.UserServiceImpl"/>
----------------------
applicationContext_admin.xml(B功能的三层都放个文件)
<bean id="adminController" class="com.hyq.controller.AdminController"/>
<bean id="adminService" class="com.hyq.service.Impl.AdminServiceImpl"/>
5. spring配置文件整合
5.1 单个导入
<!-- 单个导入-->
total.xml:
<import resource="applicationContext_controller.xml"/>
<import resource="applicationContext_Service.xml"/>
<import resource="applicationContext_mapper.xml"/>
5.2 批量导入
<!-- 单个导入-->
total.xml:
<import resource="applicationContext_*.xml"/>
⭐要使用批量注入就要求每个xml文件的命名都严格按照 applicationContext_xxx.xml的格式
6. 面向切面编程AOP
基本概念:
AOP(Aspect Orient Programming)就是面向切面编程,而面向切面编程就是将公共的,通用的,重复的功能提取出来单独开发,在需要调用的方法种通过动态代理的方式织入
这里将通过简单图书购买业务来对比和实现AOP,代码基于三层架构来实现:
6.1 第一个版本:
所有目标功能和切面功能都耦合在一起,完全没有拆分:
public class BookServiceImpl {
public void book(){
try {
System.out.println("事务开启。。。");
System.out.println("图书购买业务功能实现。。。。。。。。");
System.out.println("事务提交。。。。。。。。");
} catch (Exception e) {
e.printStackTrace();
}
}
}
这种实现方法把固定的事务和具体的功能实现都写在了一个类,比如要定场地要做演唱会前准备,到明星唱歌,再到场地回收都放在了一起,并且要具体到哪一个明星,耦合度最高,没有将业务和切面分开,如果要换一个明星、明星要转唱歌到跳舞、演唱工作准备要有变化都要变
6.2 第二个版本:
通过子类代理的方式拆分业务和切面
父类:
public class BookServiceImpl {
//在父类中只有干干净净的业务
public void buy(){
System.out.println("图书购买功能实现。。。。。。。");
}
}
子类:
public class SubBookServiceImpl extends BookServiceImpl {
@Override
public void buy(){
try {
//事务切面
System.out.println("事务开始。。。。。。。。。");
//主业务实现
super.buy();
//事务切面
System.out.println("事务回滚。。。。。。。。。。");
} catch (Exception e) {
e.printStackTrace();
}
}
}
优点:父子类的关系将主要业务和事务切面分开了,属于子类增强父类实现,一定程度上实现了解耦合,如果要变业务,就变父类,子类可以不怎么变,如果要变事务切面,就变子类
缺点:如果要换个业务目标对象类似换个歌手唱歌,就没办法做到,业务还是写死的
6.3 第三个版本:
使用静态代理拆分业务和切脉你,业务和业务接口已拆分,此时切面紧耦合在一起
静态代理实现流程:
目标业务方法:买东西—-void buy()
目标对象:图书购买—Book (目标对象实现业务接口,可以是图书购买、衣服购买、玩具购买)
代理对象:书店–store( 代理对象实现业务接口,方法种包含目标对象和各种事务)
客户端:人,访问系统的用户,客户端不能直接访问目标方法,必须通过代理对象实现业务
有接口有实现类先实现接口:
public interface Service {
//规定业务功能
public void buy();
}
目标对象(实现类):目标对象是业务功能的具体实现,例如:接口是唱歌,目标对象就是明星,接口是卖书,目标对象就是印书厂
public class BookServiceImpl implements Service {
@Override
public void buy() {
System.out.println("图书购买业务功能实现。。。。。");
}
}
代理对象:
// 静态代理对象实现了目标对象的灵活切换
//图书购买业务,商品购买业务,衣服购买业务,
public class Agent implements Service {
//设计成员变量的类型为接口,为了灵活切换目标对象,成员变量设计为接口是面向接口编程的特点之一
private Service target;
//使用构造方法传入目标对象,参数设计为接口是面向接口编程特点之二
public Agent(Service target) {
this.target = target;
}
@Override
public void buy() {
try {
//切面功能
System.out.println("事务开启。。。。。。。。。");
//业务功能
target.buy();
//切面功能
System.out.println("事务提交。。。。。。。。。。");
} catch (Exception e) {
System.out.println("事务回滚.........");
}
}
}
测试:
总结:
- 优点:是实现了目标对象的灵活切换,比如要换买书,买衣服,买玩具,只要创建不同的目标对象并实现业务接口Service的buy(),然后把不同的目标对象当作参数传入代理对象的有参构造方法中
- 缺点:切面功能还是写死在了代理对象中,如果要换事务处理过程或者多个事务处理过程,例如买书的目标对象可能要验书真假,买衣服不仅要验真假还要判断尺码,这样就没办法一个代理对象全部都适用,应该切掉代理对象中的事务处理过程,当调用的是买书,我就添加验真假的事务,当调用的是买衣服,就再加上判尺码的事务
6.4 第四个版本:
完善第三版本静态代理的设计,第三版本中成功在代理对象中切掉了目标对象,实现了目标对象灵活切换,但是事务切面方法还是固定在了代理对象中,第四版本解决这个问题,实现对切面对象的面对切面编程:
先做业务接口,再实现切面接口,最后再代理对象中做整合(上接口就是上灵活:)
public interface AOP {
//接口方法使用default,jdk8允许在实现类中空实现,不一样每一个实现类实现所有的接口方法,有需要哪个就实现哪个,简化了代码,提高效率
//事务开启
default void before(){};
//判断购买对象的真假
default void ifTrue(){};
//判断购买对象的尺码
default void ifCize(){};
//事务结束
default void after(){};
//事务异常
default void exvetion(){};
}
在接口中对方法设计成空实现,这样可以使接口方法灵活实现,不用每个目标方法都实现全部方法,需要实现哪个方法就重写哪个方法
切面方法:
切面事务的目标对象,此类实现事务开启的目标方法
public class LogAop implements AOP {
@Override
public void before() {
System.out.println("前置日志输入。。。。。。。");
}
}
public class TransAop implements AOP {
@Override
public void before() {
System.out.println("事务开启。。。。。。");
}
@Override
public void after() {
System.out.println("事务提交。。。。。。。");
}
@Override
public void exvetion() {
System.out.println("事务回滚。。。。。。。。");
}
}
public class IfTrueAop implements AOP {
@Override
public void ifTrue() {
System.out.println("判断购买对象真假.。。。。。。。。");
}
}
实现代理对象:(代理对象不能实现多个接口?)
public class Agent implements Service {
//传入主要业务的目标对对象、目标对象所需的切面对象
Service target;
AOP aop;
//使用有参构造初始化业务对象和切面对象,参数设计为接口
public Agent(Service target, AOP aop) {
this.target = target;
this.aop = aop;
}
@Override
public void buy() {
try {
//切面
aop.before();
//切面
aop.ifTrue();
//切面
aop.ifCize();
//业务
target.buy();
//切面
aop.after();
} catch (Exception e) {
//切面
aop.exvetion();
}
}
}
总结:
- 优点:实现切面对象灵活切换,并且可以根据不同目标对象设置不同的切面对象,灵活结合不同的切面对象,并且在代理对象中实现的目标对象接口方法中
- 缺点:还是像proxy3一样要把所有的切面方法都调用一遍,要是我有100个不同的切面对象,就要实现100遍,才能做到灵活切换,导致代码冗余
测试:test就等于客户,各个组件封装好后用户可以自行随意组装,不用全部堆在好几个不同的类里面,实现高度解耦合
切入多个切面:
public class testProxy4 {
@Test
public void testProxy4(){
Service agent = new Agent(new BookServiceImpl(),new IfTrueAop());
Service agent1 = new Agent(agent,new CizeAop());
agent1.buy();
}
}
静态代理总结:
- 实现目标对象的灵活切掉,和切面对象的切面灵活切
- 动态代理是实现业务的灵活组装拼接,第四个版本中只能实现一种业务buy,当客户端需要另外一种业务的时候,因为代理对象agent已经实现死了是Service的接口实现类,所以业务已经写死了,所以客户端要实现不同业务的时候就要换一个代理对象,又会增加代码的冗余,所以第五个版本就是要切开代理对象和Service的联系,用动态代理切掉业务实现
6.5 第五个版本:
目的:proxy4中,因为代理对象必须实现业务接口,比如Service.buy(); 并且在代理对象的重写方法中已经写死了目标对象实现的是buy(),所有当代理对象需要换一个业务方法的时候就没办法更换,只能换个代理类
所以第五版本中要通过 动态代理,在实现目标对对象灵活切换、切面对象灵活切换的情况加,实现接口业务方法的灵活切换
动态代理类:
public class ProxyFactory {
//当传入目标对象以及切面对象的时候,Proxy4是写死了直接调用两个对象的方法,
// Proxy5: 1. 先加载目标对象类,
// 2. 再获取目标对象实现了什么接口
// 3. 通过匿名内部类实现代理功能实现,相当于Proxy4的buy()内部实现的方法,通过目标对象传入的method和参数arg来动态实现
public static Object getAgent(Service target, AOP aop){
//返回生成的动态代理对象,这里返回的是动态代理对象
return Proxy.newProxyInstance(
//类加载器
target.getClass().getClassLoader(),
//目标对象实现的所有的接口
target.getClass().getInterfaces(),
//代理功能实现,匿名内部类实现
new InvocationHandler() {
@Override
public Object invoke(//生成的代理对象
Object proxy,
// 正在被调用的目标方法buy/show/sale
Method method,
//目标方法的参数
Object[] args) throws Throwable {
Object invoke = null;
try {
//切面
aop.before();
aop.ifCize();
aop.ifTrue();
//业务
invoke = method.invoke(target, args);
//切面
aop.after();
} catch (IllegalAccessException e) {
aop.exvetion();
}
//目标方法的返回值
return invoke;
}
}
);
}
}
接口中增加业务功能:最好用默认实现接口,谁想实现谁调用
public interface Service {
//业务功能,有接口有实现类先实现接口
public void buy();
//业务功能,增加有参数有返回值的方法
default public String show(int age){return null;}
default String sale(String name){return null;}
}
目标对象实现:
public class BookServiceImpl implements Service {
//目标对象实现
@Override
public void buy() {
System.out.println("图书购买功能实现。。。。。。。");
}
@Override
public String show(int age) {
System.out.println("这是show方法被调用实现.。。。。。。"+age);
return String.valueOf(age);
}
@Override
public String sale(String name) {
System.out.println("这是sale方法,要卖 "+name);
return "卖品:"+name;
}
}
测试:
7. Spring支持的AOP实现
常用有一下几种:
- Before通知
在目标方法被调用前调用,接口:org.springframework.aop.MethodBegoreAdvice; - After通知
在目标方法被调用后调用,接口:org.springframework.aop.AgerRetrurningAdvice; - Throws
在目标方法抛出异常时调用,接口:org.springframework.aop.ThrowsAdvice; - Around
拦截对目标方法调用,接口:org.aopalliance.intercept.MethodInterceptor;
8. AOP常用术语
- 切面:重复的,公共的,通用的功能成为切面,例如:日志、事务、权限
- 连接点:就是目标方法,因为在目标方法中要实现目标方法的功能和切面功能
- 切入点(Pointcut):指定给切入的位置,多个连接点构成切入点,切入点可以hi一个目标方法,可以是一个类中的所有方法,可以是某个包下的所有类中的方法
- 目标对象:操作谁,谁就是目标对象
- 通知(Advice):来指定切入的时机,实在目标方法执行前还是执行后还是出错时,还是环绕目标方法切入切面功能
9. AspectJ
9.1 什么是AspectJ
AspectJ是一个优秀的面向切面的框架,它扩展了 Java 语言,提供了强大的切面实现,它因为是基于Java语言开发,所以无缝衔接
9,2 AspectJ常见通知类型
AspectJ中常用的四大通知类型:
- 前置通知@Before⭐
- 后置通知@AfterReturning⭐
- 环绕通知@Around ⭐⭐⭐
- 最终通知@After
- 定义切入点@Pointcut
9.3 AspectJ的切入点表达式
规范:
execution( 访问权限 方法返回值 方法声明(参数) 异常类型 )
简化后:
execution( 方法返回值 方法声明(参数) )
字符:
” * ” : 代码任意个任意的字符(通配符)
“. .” : 如果出现在方法的参数中,代表任意参数,如果出现在路径中,则代表本路径及其所有的子路径
示例:
execution(public * *(..)) 权限为public的任意类中的任意方法的任意参数----任意的公共方法
execution(* set*(..)) 任意一个以set开头的方法
execution(* com.xxx.service.impl.*.*(..)) 任意的返回值类型,在com.xxx.service.impl
包下的任意类的任意方法的任意参数
execution(* com.xxx.service..*.*(..)) 任意的返回值类型,在com.xxx.service及其子
包下的任意类的任意方法的任意参数
execution(* *..service.*.*(..)) service之前可以有任意的子包
execution(* *.servuce.*.*(..)) service之前只有一个子包
9.4 @Before—前置通知
在目标方法执行前切入切面功能,在切面方法中不可以获得目标方法的返回值,只能得到目标方法的签名
创建业务接口:
public interface SomeService {
String doSome(String name,int age);
}
创建业务实现类:
@Service
public class SomeServiceImpl implements SomeService {
@Override
public String doSome(String name, int age) {
System.out.println("doSome的业务功能实现.........");
return "abcd";
}
}
创建切面类:
前置通知的切面方法的规范:
- 访问权限是public
- 方法的返回值是void
- 方法名称自定义
- 方法没有参数,如果有也只能是JoinPoint类型,并且多个参数的情况下JoinPoint必须在第一位,若有必首位是全部切面方法的规则
- 必须使用@Before注解来声明切入的时机是前切功能和切入点
参数 Value: 指定切入点表达式
@Aspect //交给AspectJ框架去识别切面类
@Component
public class MyAspect {
* 所有切面的功能都是由切面方法来实现的
* 可以将各种切面都在此类中进行开发
// @Before(value = "execution(public String com.hyqwsq.s01.SomeServiceImpl.doSome(String,int))")
// @Before(value = "execution(public * com.hyqwsq.s01.SomeServiceImpl.*(..))")
// @Before(value = "execution(* com.hyqwsq.s01.*.*(..))") //返回类型的com.hyqwsq.s01的任务类的任意方法其中任意参数,建议!
// @Before(value = "execution(* *(..))") //任务权限的任意方法,不建议!
@Before(value = "execution(* com.hyqwsq.s01.*.*(..))")
public void myBefore(){
System.out.println("切面方法中的前置功能实现。。。。。。。。");
}
//加参数的前置通知切面
@Before(value = "execution(* com.hyqwsq.s01.*.*(..))")
public void myBeforeWithCanShu(JoinPoint jp){
System.out.println("加参数的前置通知。。。。。。。。。。。");
System.out.println("目标方法的签名:"+jp.getSignature());
System.out.println("目标方法的参数:"+ Arrays.toString(jp.getArgs()));
}
}
在applicationContext.xml中绑定:
*绑定切面方法和业务方法
*类似动态代理的Proxy.newProxyInstance中的InvocationHandler匿名内部类
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
测试:
proxy-target-class=“true”: JDK动态代理转成CGLib子类代理
<aop:aspectj-autoproxy proxy-target-class="true"></aop:aspectj-autoproxy>
基于注解形式:
添加注解:
@Aspect //交给AspectJ框架去识别切面类
@Component
public class MyAspect {
@Service
public class SomeServiceImpl implements SomeService {
添加包扫描:
<!-- 添加包扫描-->
<context:component-scan base-package="com.hyqwsq.s01"> </context:component-scan>
9.5 @AfterReturning—后置通知
目标方法返回值是否可以被改:分情况:
业务接口:
public interface SomeService2 {
String doSome2(String name,int age);
Student changeStudnet(String name,int age);
}
目标对象实现类:
@Service(value = "someService")
public class SomeService2Impl implements SomeService2 {
@Override
public String doSome2(String name, int age) {
System.out.println("doSome2的业务方法被执行。。。。。。。。。");
return "abcde";
}
@Override
public Student changeStudnet(String name, int age) {
System.out.println("目标方法changeStudent执行。。。。。");
return new Student(name,age);
}
}
切面类:
后置通知的方法规则:
- 访问权限是public
- 方法没有返回值
- 方法名自定义
- 方法有参数(也可以没有参数,如果目标方法没有返回值,则可以写无参的方法,但一般都会写有参,这样可以处理无参可以处理有参),
这个切面方法的参数就是目标方法的返回值 - 使用@AfterReturning注解标明是后直通知
参数:
value:指定切入点表达式
returning:指定目标方法的返回值的名称,切面方法里的参数变量名必须于这里的名称一致
@Aspect
@Component
public class MyAspectAfter {
//后置通知方法中修改目标方法的返回值是引用类型
@AfterReturning(value = "execution(* com.hyqwsq.s02.*.*(..))",returning = "obj")
public void myAfterReturning2(Object obj){
System.out.println("第二个 后置通知功能实现 下面修改引用类型返回值。。。。。。。");
//在切面方法中修改目标方法的返回值,这里是引用类型
if(!Objects.isNull(obj)){
if(obj instanceof Student){
Student stu = (Student) obj;
stu.setName("王五");
stu.setAge(55);
System.out.println("在切面方法中修改了目标方法的返回值,这是在切面方法中打印 "+stu);
}
}
}
}
包扫描:
<!-- 添加包扫描-->
<context:component-scan base-package="com.hyqwsq.s02"> </context:component-scan>
<!--<!– 创建业务对象–>-->
<!-- <bean id="someService" class="com.hyqwsq.s01.SomeServiceImpl"></bean>-->
<!--<!– 创建切面对象–>-->
<!-- <bean id="myaspect" class="com.hyqwsq.s01.MyAspect"></bean>-->
<!-- 绑定切面方法和业务方法,类似动态代理的Proxy.newProxyInstance中的InvocationHandler匿名内部类-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
测试:
在切面方法中修改目标方法的返回值:
public class testAfterReturning {
@Test
public void testAfterReturning(){
ApplicationContext ac = new ClassPathXmlApplicationContext("s02/applicationContexts02.xml");
SomeService2 someService = (SomeService2) ac.getBean("someService");
System.out.println(someService.getClass());
String s = someService.doSome2("李四", 35);
System.out.println("在测试方法中打印的目标方法的返回值 "+s);
}
@Test
public void testAfterReturning2(){
ApplicationContext ac = new ClassPathXmlApplicationContext("s02/applicationContexts02.xml");
SomeService2 someService = (SomeService2) ac.getBean("someService");
System.out.println(someService.getClass());
Student s = someService.changeStudnet("李四", 35);
System.out.println("在测试方法中打印的目标方法的返回值 "+s);
}
}
所以基本类型的返回值改变在测试方法中没有变化,上面的测试结果中,后置功能里确实改变了大小写,但最后一条是测试类里的输出并没有被改变
引用类型可以被改变:
先创建一个实体类
public class Student {
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
目标犯法:
@Override
public Student changeStudnet(String name, int age) {
System.out.println("目标方法changeStudent执行。。。。。");
return new Student(name,age);
}
在切面方法中改变目标发方法的返回值:
//后置通知方法中修改目标方法的返回值是引用类型
@AfterReturning(value = "execution(* com.hyqwsq.s02.*.*(..))",returning = "obj")
public void myAfterReturning2(Object obj){
System.out.println("第二个 后置通知功能实现 下面修改引用类型返回值。。。。。。。");
//在切面方法中修改目标方法的返回值,这里是引用类型
if(!Objects.isNull(obj)){
if(obj instanceof Student){
Student stu = (Student) obj;
stu.setName("王五");
stu.setAge(55);
System.out.println("在切面方法中修改了目标方法的返回值,这是在切面方法中打印 "+stu);
}
}
}
总结:
⭐如果目标方法的返回值是8种基本类型或者是String类型,则不可被后置通知改变,如果目标方法的返回值是引用类型,则可以被后置通知改变
@Around—环绕通知:
环绕通知通过拦截目标方法的方式,在目标方法前后增强功能的通知,它是功能最强大的通知,一般事务使用此通知,它可以轻易地改变目标方法地返回值
目标方法接口和实现类:
public interface DoService3 {
String doSome(String name,int age);
}
@Service(value = "doService")
public class DoService3Impl implements DoService3 {
@Override
public String doSome(String name, int age) {
System.out.println("doSome的目标业务方法被执行。。。。。。"+name);
return "the three time target method";
}
}
环绕通知方法的规范:
- 访问权限是public
- 切面方法有返回值,返回值就是目标方法的返回值
- 方法名称自定义
- 方法有参数,参数就是目标方法本身
- 一定要回避异常
- 使用@Around注解生命软绕通知切入点表达式
参数:
value:指定切入点表达式
@Aspect
@Component
public class MyAround {
@Around(value = "execution(* com.hyqwsq.s03.*.*(..))")
public Object myAround(ProceedingJoinPoint pjp) throws Throwable {
//环绕的前切功能实现
System.out.println("环绕的前切功能实现。。。。。。。。。。。");
//目标方法调用
Object obj = pjp.proceed(pjp.getArgs());
//修改目标方法的返回值,可证明环绕方法可以修改后置方法改不了的基本类型和String类型
System.out.println(obj);
obj = "change the reslut";
//环绕的后切功能实现
System.out.println("环绕的后切功能实现。。。。。。。。");
return obj;
}
}
环绕切面功能的参数是目标方法,而目标方法是从ProceedingJoinPoint接口中传进来的,传进来pgp就是目标方法(记住就行)
pjp就是目标方法,.proceed()就是调用方法,如果有参数就调用 .getArgs()方法获取,这里就类似动态代理中的method,用method代替目标方法调用并在测试类中拿到的目标方法参数放到args中传入这里的process()和动态代理中的method方法中
测试类:
public class testAround {
@Test
public void testAround(){
ApplicationContext ava = new ClassPathXmlApplicationContext("s03/applicationContexts03.xml");
DoService3 doService = (DoService3) ava.getBean("doService");
System.out.println(doService.getClass());
String s = doService.doSome("陈六", 45);
System.out.println("在测试方法中目标方法的返回值: "+ s);
}
}
结果:
环绕通知总结:
环绕通知可以同时设置前置通知和后置通知,并且通过将目标方法本身设计为接口作为参数传入环绕通知方法,并通过接口来调用目标方法
与单独的前置通知和单独的后置通知的区别在于环绕通知可以修改目标方法中任意类型的返回值,而前置方法不可修改不可操作不可获取,而后置方法只能修改除8种基本类型外加String类型的其他引用类型
@After—最终通知:
后置通知意思无论情况都会被执行,当业务调用目标方法发生异常的时候,前置通知可能不受影响,而出异常后后置通知就会不被执行,环绕通知中也是前置执行后置不被执行,而最终通知无论有没有发生异常都会执行,功能和try catch中的finally一样,类似资源清理,释放对象,关闭资源
最终通知规则:
- 访问权限是public
- 方法没有返回值
- 方法名称自定义
- 方法没有参数,如果有也能是JoinPoint
- 使用@After注解表明是最终通知
参数:
value:指定切入点表达式
@Aspect
@Component
public class MyAspectFinally {
/**
*@Description
* 最终通知方法的规范:
* 1.访问权限是public
* 2。方法没有返回值
* 3.方法名称自定义
* 4.方法没有参数,如果有也能是JoinPoint
* 5.使用@After注解表明是最终通知
* 参数:
* value:指定切入点表达式
**/
@After(value = "execution(* com.hyqwsq.s04.*.*(..))")
public void myAfter(){
System.out.println("最终通知功能执行。。。。。。。。。。");
}
扫描包:
<!-- 添加包扫描-->
<context:component-scan base-package="com.hyqwsq.s04"> </context:component-scan>
<!-- 绑定切面方法和业务方法,类似动态代理的Proxy.newProxyInstance中的InvocationHandler匿名内部类-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
测试:
public class testAfter {
@Test
public void testAround(){
ApplicationContext ava = new ClassPathXmlApplicationContext("s04/applicationContexts04.xml");
SomeService4 someService = (SomeService4) ava.getBean("someService");
System.out.println(someService.getClass());
String s = someService.doSome("何七", 70);
System.out.println("在测试方法中目标方法的返回值: "+ s);
}
}
设置错误:
尽管目标方法中出现异常,最终通知一样可以被执行:
切面类中可以把全部类型的切面通知都放上去
@Aspect
@Component
public class MyAspectFinally {
@After(value = "execution(* com.hyqwsq.s04.*.*(..))")
public void myAfter(){
System.out.println("最终通知功能执行。。。。。。。。。。");
}
//一个切面方法类可以有多种不同的通知,也可以有多个相同的通知
@Before(value = "execution(* com.hyqwsq.s04.*.*(..))")
public void myBefore(){
System.out.println("切面方法中的前置功能实现。。。。。。。。");
}
@Around(value = "execution(* com.hyqwsq.s04.*.*(..))")
public Object myAround(ProceedingJoinPoint pjp) throws Throwable {
//环绕的前切功能实现
System.out.println("环绕的前切功能实现。。。。。。。。。。。");
//目标方法调用
Object obj = pjp.proceed(pjp.getArgs());
//修改目标方法的返回值,可证明环绕方法可以修改后置方法改不了的基本类型和String类型
System.out.println(obj);
obj = "change the reslut";
//环绕的后切功能实现
System.out.println("环绕的后切功能实现。。。。。。。。");
return obj;
}
//后置通知方法
@AfterReturning(value = "execution(* com.hyqwsq.s04.*.*(..))",returning = "obj")
public void myAfterReturning2(Object obj){
System.out.println("后置通知功能实现 下面修改引用类型返回值。。。。。。。");
//在切面方法中修改目标方法的返回值,这里是引用类型
if(!Objects.isNull(obj)){
if(obj instanceof Student){
Student stu = (Student) obj;
stu.setName("王五");
stu.setAge(55);
System.out.println("在切面方法中修改了目标方法的返回值,这是在切面方法中打印 "+stu);
}
}
}
}
同样类型的切面也能有多个:
@Before(value = "execution(* com.hyqwsq.s04.*.*(..))")
public void myBefore(){
System.out.println("切面方法中的前置功能实现。。。。。。。。");
}
@Before(value = "execution(* com.hyqwsq.s04.*.*(..))")
public void myBefore2(){
System.out.println("切面方法中的前置功能实现。。。。。。。。");
}
不但可以为一个方法绑定不同的通知,还可以绑定相同类型通知若干个
各种不同类型通知执行顺序,这就是spring排列过的最优通知
@Pintcut—切入点表达式别名
如果多个切入点切入到同一个切入点,可以使用别名简化开发,使用@Pointcut注解,创建一个空方法,此方法的名称就是别名
@After(value = "mycut()")
public void myAfter(){
System.out.println("最终通知功能执行。。。。。。。。。。");
}
@Pointcut(value = "execution(* com.hyqwsq.s04.*.*(..))")
public void mycut(){}
sm项目步骤:
spring添加事务管理器
事务注解:
加了事务注解后 实现类变成事务类型
noRollbackForClassName = {“”,“”}:按异常名称区分不回滚,这里表示发生了算数类型的异常不回滚,其他的异常全部回滚,制定发生什么异常不回滚,用的是异常的名称,也可以用异常的类型,.class
指定发生什么异常必须回滚,用的是异常的类型
timeout:连接超时设置
事务的特性:
如果已经有个事务在运行了,在调用新的业务方法的时候可以合并使用同一个事务,如果不需要还可以单开一个事务,还可以不适用事务,就是拒绝事务,事务传播特性有各种合并、互斥的使用方式
事务的传播特性一定体现在多个事务之间而不是单个事务,也可以说事务的传播特性依赖多个事务
用户的测试里包含了账户的方法:
场景2:
场景3:
B被合并到A的事务中,所以B报错,A会回滚就会把B执行的操作也一并回滚
场景4:
B方法在A中被调用,当A 中执行到B的时候,A的食物被挂起,B单独在一个没有事务的区域执行数据库操作,执行完后A再继续执行,A和B的区域是不交叉的
场景5:
场景6:
场景7:
场景8:
B的事务特性是NEVER代表该目标方法不能包在任意事务中调用
用户表操作还能执行,但是账户表操作的语句就没有了
声明式事务
指定异常不回滚:
添加注解式事务,并设置优先级高于声明式事务:
就可以用局部的注解屏蔽掉声明式事务,让有一些事务脱离于总体的声明式事务配置