spring讲义
spring官网
下文中所有项目都是通过 maven 构建的quickstart项目
csdn比较好的博客
1.什么是Spring框架
-
它是一个容器,帮助解决企业开发的难度,减轻对项目模块之间的管理,类和类之间的管理,帮助开发人员创建对象,管理对象之间的关系。
-
它是整合其它框架的框架,
-
它的核心是IOC和AOP (控制反转,面向切面编程)能够实现模块之间,类之间的解耦合
-
它由20多个模块构成。
-
它在很多领域都提供优秀的解决方案.
依赖:classA中使用classB的属性或者方法,叫做classA依赖classB
我们课程里学Spring,SpringMVC,SpringBoot,SpringCloud
什么是spring:
- 是一个框架,核心技术是 ioc,aop,实现解耦合
- spring是一个容器,容器中存放的是java对象,需要做的是把对象放入到容器中
怎么使用spring:
- spring是一个容器,把项目中用的对象放入到容器中
- 让容器完成对象的创建,对象之间关系的管理(属性赋值:基础类型和引用类型)
- 我们在程序中从容器中获取 要使用的对象
什么样的对象放到容器中:
- dao类,service类,controller类,工具类
- spring中的对象默认都是单例的,在容器中叫这个名称的对象(id)只有一个
不放入到spring容器中的对象:
- 实体类对象,实体类数据来自数据库的
- servlet,listener,filter等。交给tomcat服务器创建对象
框架怎么学
框架是一个软件,其它人写好的软件。
- 知道框架能做什么。 mybatis ---- 访问数据库,对表中的数据执行增删改查
- 框架的语法:框架要完成一个功能,需要一定的步骤支持
- 框架的内部实现,框架内部怎么做,原理是什么
- 手写框架、
2.Spring的特点
-
轻量级
由20多个模块构成,每个jar包都很小,小于1M,核心包也就3M左右。
对代码无污染。
-
面向接口编程
使用接口,就是面向灵活,项目的可扩展性,可维护性都极高。
接口不关心实现类的类型。使用时接口指向实现类,切换实现类即可切换整个功能。
-
AOP:面向切面编程
就是将公共的,通用的,重复的代码单独开发,在需要的时候反织回去(设计成接口,接口指向实现类)。底层的原理是动态代理
-
整合其它框架
它整合后使其它框架更易用.
-
IOC(inversion of control) 控制反转,是一个理论,概念,思想
控制:创建对象,对象的属性赋值,对象间的关系管理
反转:把对象的创建,赋值,管理工作交给代码之外的容器实现,也就是对象的创建由其他外部资源完成
目的:减少代码的改动,实现不同的功能,实现解耦合
spring使用依赖注入(DI)实现了IOC的功能
java创建对象的方式
- 构造器
- 反射
- 序列化(对象保存到磁盘)
- 克隆
- ioc:容器创建对象
- 动态代理
ioc的体现:
servlet:
-
创建类继承 HttpServlet
-
在web.xml中注册servlet(见ervlet-jsp笔记:使用标签)
-
开发人员没有创建Servlet对象,
-
Servlet对象由Tomcat服务器创建的
tomcat作为容器,里面存放的有Servlet对象,Listener,Filter对象
IOC的技术实现
DI是IOC的技术实现
DI(依赖注入):只需要在程序中提供要使用对象的名称就可以。至于对象的创建和赋值,查找都由容器内部实现
spring是使用di实现ioc的功能,spring底层创建对象,使用的是反射机制。
ioc能够实现业务对象之间的解耦合,例如 service 和dao 对象之间的解构合
DI的实现:
- spring的配置文件中,使用标签和属性完成,叫做基于XML的di实现(第4节)
- 使用spring中的注解,完成属性赋值,叫做基于注解的di实现(第6节)
基于xml的DI给属性赋值(第4节)
- set注入(设值注入):spring调用类的set方法,在set方法中可以实现属性的赋值。(需提供无参构造器和set方法)
- 构造注入:spring调用类的含参构造器,创建对象,在构造方法中完成赋值
3.什么是IOC
概念,语法
控制反转IOC(Inversion of Control)是一个概念,是一种思想。DI是这种思想的具体实现
由Spring容器进行对象的创建和依赖注入(给属性赋值)。程序员在使用时直接取出使用.
当spring应用了IOC,一个对象依赖的其他对象会通过被动的方式传递进来(由容器创建并传值。依赖注入DI),而不是这个对象自己创建或查找依赖对象
控制:创建对象,对象的属性赋值,对象间的关系管理
-
正转(使用无参构造,程序员通过set方法为对象赋值):
由程序员进行对象的创建和依赖注入称为正转。程序员说了算.
Student stu = new Student(); ===>程序员创建对象 stu.setName("张三"); ===>程序员进行赋值 stu.setAge(22);
-
反转(提供无参构造和set方法,容器自动创建对象):
由Spring容器创建对象和依赖注入(属性赋值)称为反转,将控制权从程序员手中夺走,交给Spring容器,称为反转.。
容器说了算.
// applicationContext.xml
<!--告诉spring创建对象
声明bean,就是告诉spring要创建某个类的对象
id:对象的自定义名称,唯一值,spring通过这个名称找到对象
class:类的全限定名(不能是接口,与mybatis区分开,因为spring是反射创建对象,必须使用类
property:对象属性赋值。name为类中成员变量名,value为类中基本类型赋值,引用类型用 ref
spring就完成:Student stu = new Student(); // 找com.bjpowernode.pojo包下Student类的无参构造器
spring把创建好的对象放到map中,spring框架有一个map存放对象
springMap.put(id的值,对象) // String类型的对象名, Object类型的对象(Bean对象)
例如:springMap.put("stu", new Student());
一个bean标签声明一个对象
-->
<!--
spring使用xml解析器解析这些数据,利用 java.bean.PropertyEditor完成类型转换。
从String类型到所需要的参数值类型
-->
<beans>
<bean id="stu" class="com.bjpowernode.pojo.Student"> ===>Spring容器负责对象的创建
<property name="name" value="张三"> <!-- setName("张三");-->===>Spring容器依赖注入值
“ <property name="age" value="22">
</bean>
<!-- spring 不仅能创建一个非自定义类的对象,也能创建一个存在的某个类的对象 -->
<bean id="mydate" class="java.util.Date">
<propety name="time" value="83641235"></propety> <!-- 调用Date类的 setTime()方法-->
</bean>
</beans>
<!--
spring的配置文件:
1. beans:根标签,spring把java对象成为bean
2. spring-beans.xsd是约束文件,和mybatis指定的 dtd文件是一样的
-->
ioc容器的基础(补)
什么是bean?
- 在spring中,由spring IOC容器管理的对象叫做Bean。也就是在 applicationContext.xml中配置的对象
- Bean是由Spring IOC容器实例化,组装和以其他方式管理的对象
- Bean以及它们之间的依赖关系通过容器配置元数据(xml,java注解 or java代码)来反映
ban对象之间的依赖关系叫做依赖
-
依赖注入:一个对象依赖的其他对象会通过被动的方式传递进,由容器传
-
依赖注入两种方式
-
setter注入
-
p命名空间注入:本质还是setter注入,但是使xml配置更简洁
在头文件中加入:xmlns:p="http://www.springframework.org/schema/p" <bean id="user" class="com.zjs.User" p:userId="11" p:name="张三" p:age="20"></bean>
-
-
构造方法注入
-
提供了IOC容器的基本功能,是Spring框架的IOC容器的基础
-
org.springframework.beans
-
org.springframework.context
IOC容器中两个重要的接口
-
BeanFactory
- 提供了一种能够管理任何类型对象的高级配置机制
- 工厂模式的具体实现
- 使用IOC 将配置 和 代码分离
- BeanFactory容器实例化后并不会自动实例化Bean,只有当Bean被使用时, BeanFactory容器才会对该Bean进行实例化与依赖关系的配置(依赖注入)
-
ApplicationContext
-
是 BeanFactory的子接口
-
子类
- ClassPathXmlApplicationContext
- 从类路径中的xml文件载入上下文信息
- 常用,移植性强
- FileSystemXmlApplicationContext
- 从文件系统中的xml文件载入上下文定义信息
- ClassPathXmlApplicationContext
-
ApplicationContext容器实例化后会自动对所有单实例Bean进行实例化与依赖关系的配置,使之处于待用状态
-
spring中有过以下代码
ApplicationContext ac = new ClassPathXmlApplicationContext("s01/applicationContext.xml");
spring通过读取Bean的配置完成创建Bean工厂,常用 xml,注解 配置元数据。其中 bean标签用于配置javaBean对象。
-
项目1
项目1
-
项目构建
-
新建空项目
- 在 project structure 中配置jdk的版本。
- 在settings中搜索maven,配置maven本地的settings.xml文件和maven仓库
-
添加新模块(maven构建的quickstart项目)
-
修改该模块的目录
-
在pom.xml文件中添加配置(添加spring的依赖,添加资源文件指定(见mybatis笔记)) 上依赖
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.5.RELEASE</version> </dependency>
-
刷新maven,在 src/main/resources 下新建 XML Configuration File(Mybatis中新建的是普通的 File)。命名为applicationContext.xml(spring的规范)上配置
- 在其中配置javaBean对象,供容器创建对象使用(底层用的是反射)
-
-
-
在pojo包下创建实体类(使用set注入:必须包含无参构造器,get/set方法),在applicationContext.xml中进行如下 第4节 的配置(目的是让容器为我们创建对象)
-
进行测试(test)
//由spring容器进行容器的创建(容器一启动,就创建xml配置文件下bean标签中的所有对象) // 如果想从spring容器中取出对象,则需要先创建容器对象,并启动才可以取对象 // 配置文件可以放在某个目录下,根据 目录名/xml 页可以取到该对象。这里便放到了 src/main/resources/s01包下 // 创建表示spring容器的对象,ApplicationContext接口 // ClassPathXmlApplicationContext:表示从类路径中加载spring的配置文件,进行对象的创建(在这个配置文件中有 bean对象的定义) // 也可以写成 "classpath:s01/applicationContext.xml" // classpath表示类的根路径;classpath*表示除了类路径之外,同时也会查找依赖库(.jar)下的目录 // 类路径 指的是 编译后的class文件的目录,在web项目中是target/WEB-INF/classes目录 ApplicationContext ac = new ClassPathXmlApplicationContext("s01/applicationContext.xml"); // 从容器中获取某个对象,通过 bean标签中的id属性值 // getBean中内容为 配置文件<bean>标签中的id属性值 Student stu = (Student)ac.getBean("stu"); Sysetm.out.println(stu); // 使用spring提供的方法,获取容器中定义的对象的数量,即bean标签的个数 ac.getBeanDefinitionCount(); // 容器中每个定义的对象的名称,即 xml中id属性的值 ac.getBeanDefinitionNames();
切记:
- Spring容器在启动时,就创建配置文件applicationContext.xml中所有的对象(一个bean就是一个对象)
- spring创建对象:默认调用的是无参数构造方法
4. 基于xml的IOC (适用于改动比较多)
项目1
以下代码都是在 上文中的applicationContext.xml中写的
-
创建对象(容器启动时创建)
<!-- 声明bean,就是告诉spring要创建某个类的对象 id 为对象名。对象的自定义名称,唯一值。spring通过这个名称找到对象 name 为类中属性的名称; class:类的全限定名称(不能是接口,因为spring是反射机制创建对象,必须使用类),也就是spring创建的对象的类型。后续会用到别名 --> <bean id="stu" class="com.bjpowernode.pojo.Student"></bean>
-
给创建的对象赋值
-
使用setter注入。(无参构造器,提供set方法)
Student stu = new Student(); stu.setName("李四"); stu.setAge("22");
spring创建对象,默认调用的是无参数构造方法,然后调用类的set方法,在set方法中完成属性赋值(在set方法中进行任何操作均可,由开发人员控制)
注入分为简单类型注入和引用类型注入(注入:即赋值)
-
简单类型注入值使用value属性(Spring中规定:基本数据类型 + String类型 称为简单类型)
-
引用类型注入值使用ref属性
- 引用类型自动注入
-
必须要注意:使用setter注入必须提供无参的构造方法,必须提供setXXX()方法.
<!--创建学生对象:com.zjs.pojo2包--> <bean id="stu" class="com.bjpowernode.pojo2.Student"> ===>简单类型注入:用 value <property name="name" value="李四"></property> <!-- setName("李四") --> <property name="age" value="22"></property> <!-- setAge("22") --> <!-- 引用类型的自动注入:分为 按类型注入 和 按名称注入(采用自动注入,下面这行依赖注入的语句可以不要): 在 bean标签中有一个 autowire属性,值为 byType相当于注解@Autowired, 值为 byName 相当于注解@Autowired @Qualifier("指定名称") 也相当于@Resource(name="指定名称") byType(按类型注入): java类中引用类型的数据类型和spring配置文件中bean的class属性是同 源关系(见第6节),这样的bean能够赋值给引用类型 注:在xml中使用按类型注入,若碰到父子类等有多个可注入的对象时,会报 expected single matching bean but found 2 错误(项目4 s03是同类型问题注解版本 的,可由其重写问题的测试代码) 与第6节的注解方式不同,使用注解遇到该问题,会默认注入父类,而不会报错 byName(按名称注入): java类中引用类型的属性名和spring配置文件中bean中id名称一样且数据 类型一致(引用类型和bean的class属性一致),这样的容器中的bean (java对象),spring能够赋值给引用类型 --> ===>引用类型注入:用ref <property name="school" ref="school"></property> <!-- setSchool(school),这里传的值是对象,即bean标签中id属性值,也就是 school对象。这个对象是spring容器为我们创建好了的,是我们在xml中配置好了,所以spring为我们创建 --> </bean> <!--创建学校对象--> <bean id="school" class="com.bjpowernode.pojo2.School"> <property name="name" value="清华大学"></property><!-- setName("清华大学") --> <property name="address" value="海淀区"></property><!-- setAddress("海淀区") --> </bean> <bean id="mydate" class="java.util.Date"> </bean>
测试文件
//注意:为了避免冲突,将该段示例的 xml文件放至 src/main/resources/s02包下 @Test public void testSchoolSpring() { ApplicationContext ac = new ClassPathXmlApplicationContext("s02/applicationContext.xml"); //School school = (School) ac.getBean("school"); //System.out.println(school); Student stu = (Student) ac.getBean("stu"); System.out.println(stu); } @Test public void testMyDate(){ ApplicationContext ac = new ClassPathXmlApplicationContext("s02/applicationContext.xml"); Date date = (Date)ac.getBean("mydate"); System.out.println("date:" date); } /** 单元测试:一个工具类库,做测试方法使用 单元:指定的是方法,一个类中有很多方法,一个方法称为单元 使用单元测试必须在pom.xml中加入 junit依赖。 创建测试类:src/test/java目录中创建的类 创建测试方法:public方法;无返回值void;方法名称自定义,建议名:test+你要测试方法的名称;无形参;方法上加@Test,这样的方法可以单独执行,不用使用main方法 */
-
使用构造方法注入(含参构造器,无set方法)
Student stu = new Student(“张三”,22);
spring调用类的含参构造器,在创建对象的同时,在构造方法中给属性赋值,构造注入使用<constructor-arg>标签
标签:一个 表示构造方法中的一个形参
标签属性:
name:表示构造方法的形参名
index:表示构造方法的参数的位置,参数从左往右位置是 0,1,2……
value:构造方法的形参是简单类型的,使用 value
ref:构造方法的形参是引用类型的,使用 ref
// id 为 对象名 name 为构造函数形参名 a.使用构造方法的形参名进行注入值(标签中name属性就是构造器中的形参名):只有含参构造器,无set方法 <bean id="school" class="com.bjpowernode.pojo3.School"> <!-- School类含参构造器形参分别为 name1, address1。这里要和第一种方法区分开--> <constructor-arg name="name1" value="清华大学"></constructor-arg> <constructor-arg name="address1" value="海淀区"></constructor-arg> </bean> b.使用构造方法参数的下标注入值:只有含参构造器,无set方法 <bean id="stu" class="com.bjpowernode.pojo3.Student"> <constructor-arg index="0" value="钱七"></constructor-arg> <constructor-arg index="1" value="22"></constructor-arg> <constructor-arg index="2" ref="school"></constructor-arg> </bean> c.使用默认的构造方法的参数的顺序注入值:只有含参构造器,无set方法 <bean id="stuSequence" class="com.bjpowernode.pojo3.Student"> <constructor-arg value="陈十"></constructor-arg> <constructor-arg value="22"></constructor-arg> <constructor-arg ref="school"></constructor-arg> </bean>
-
-
如果javaBean的属性值类型是Conllection中的List,Set,Map和Properties集合时,则spring配置文件中可以使用集合元素。
<!-- 通过<list>配置多个属性文件 --> <property name="locations"> // locations 指定使用文件方式配置属性 <list> <value>classpath:cf1.properties</value> //文件的全路径 <value>classpath:cf2.properties</value> </list> </property>
在sm整合中,一般使用以下标签即可
<context:property-placeholder location="jdbc.properties"></context:property-placeholder>
-
前置依赖(补)
Bean(A,B),A不是B的属性,但B的某些值的初始化又是依赖于A,这种依赖关系称为 前置依赖
因为彼此属性之间没有强连接,无法使用ref进行关联配置,使用 spring提供了 depends-on属性。
depends-on 的值设置为 前置依赖Bean的id(如上例中的A的id)
<bean id="beanB" class="com.zjs.pojo.BeanAclass" depends-on="beanA"></bean>
前置依赖Bean会在本Bean实例化之前被创建,在其之后被销毁(栈)。前置依赖Bean可以有多个,通过分号,空格进行分割
循环依赖
Bean(C,D),C使用构造注入D,D使用构造注入C,称为循环依赖
此时 spring报如下错误
Error creating bean with name ‘beanC’: Request bean is currently in creating:Is there an unresoulvable circular reference?
解决办法:
- 将其中的一个或两个依赖注入修改成 set注入
命名空间
- c命名空间:对 构造器注入进行简写
- p命名空间:对 set注入进行简写
使用该命名空间,需要在 根元素加入相应的命名空间。如下
xmlns:c=“http://www.springframework.org/schema/c”
xmlns:p=“http://www.springframework.org/schema/p”
idea有提示,无需手动导入
<bean id="stu" class="com.zjs.pojo.Student" c:name="张三" c:age="19"></bean>
格式:(p命名空间用法相同)
- 普通类型:c:构造参数名=“值”
- 引用类型:c:构造参数名-ref=“值”
5.项目案例(三层架构)
项目2
项目2(无需在pom.xml中进行任何配置,无需添加spring的配置文件)
-
使用三层架构进行用户的插入操作.(只是一个模拟,了解三层架构)
-
界面层,业务逻辑层,数据访问层(模拟,目前还没用到mybatis).
-
Spring会接管三层架构中哪些对象的创建?
-
界面层的对象,业务逻辑层的对象,数据访问层的对象。
-
非Spring接管下的三层项目构建:
-
实体类:
com.bjpowernode.pojo: Users[uid,uname,uage,get,set,toString]
-
数据访问层(mybatis干活的地方):处理业务逻辑层提交的数据,进行数据库的增删改查
com.bjpowernode.dao:
UsersMapper.java(接口)
UsersMapperImpl.java(实现类) ==》这里并没有使用Mybatis中的xml映射文件,只是模拟public interface UsersMapper { // 增加用户 int insert(Users user); } public class UsersMapperImpl implements UsersMapper{ public int insert(Users user) { System.out.println(user.getUname() + "用户添加成功"); return 1; } }
-
业务逻辑层:
业务逻辑层中必定有数据访问层的对象:
private UsersMapper usersMapper= new UsersMapperImpl(); // 接口指向实现类
com.bjpowernode.service:
UsersService.java(接口)
com.bjpowernode.service.Impl:
UsersServiceImpl.java(实现类 )
public interface UsersService { // 调用 dao层,增加用户 int insert(Users user); } public class UsersServiceImpl implements UsersService{ private UsersMapper usersMapper = new UsersMapperImpl(); public int insert(Users user) { // 这里可以添加更复杂的业务 return usersMapper.insert(user); } }
-
界面层(springmvc干活的地方):接收前端提交的数据,交给业务逻辑层进行处理
界面层接收前端数据,进行请求转发或重定向(servlet),springmvc框架优化了该功能(MVC三层架构:视图层,模型层,控制层)
界面层必定有业务逻辑层的对象:
private UsersService usersService = new UsersServiceImpl();
com.bjpowernode.controller:
UsersController.java
public class UsersController { private UsersService usersService = new UsersServiceImpl(); // 界面层的功能实现:对外提供访问的功能 // user 为 前端传过来的数据 public int insert(Users user){ return usersService.insert(user); } }
//测试类 public class MyTest { @Test public void testInsertUsers() { UsersController usersController = new UsersController(); int num = usersController.insert(new Users(1, "张三", 18)); System.out.println(num); } }
项目三:spring接管三层对象的创建(基于xml)
基于注解的spring接管三层对象的创建见第6节最后
在xml文件中编写bean代码,让spring容器自动创建对象,具体实现见项目3(基于项目2)
需要添加spring的依赖 和 spring的配置文件applicationContext.xml
// 数据访问层
public interface UsersMapper {
// 增加用户
int insert(Users user);
}
public class UsersMapperImpl implements UsersMapper{
public int insert(Users user) {
System.out.println(user.getUname() + "用户添加成功");
return 1;
}
}
// 业务逻辑层
public interface UsersService {
// 调用 dao层,增加用户
int insert(Users user);
}
public class UsersServiceImpl implements UsersService{
// 对象交由容器创建,能够实现代码的解耦合
private UsersMapper usersMapper; // 接口指向实现类(实现类由spring创建)
// 交给spring去 依赖注入值,必须提供无参构造器,set方法。无含参构造器会默认提供无参构造器
public void setUsersMapper(UsersMapper usersMapper) {
this.usersMapper = usersMapper;
}
public int insert(Users user) {
return usersMapper.insert(user);
}
}
// 界面层
public class UsersController {
private UsersService usersService; // 接口指向实现类(实现类由spring创建)
public void setUsersService(UsersService usersService) {
this.usersService = usersService;
}
// 界面层的功能实现:对外提供访问的功能
// user 为 前端传过来的数据
public int insert(Users user){
return usersService.insert(user);
}
}
<!-- src/main/resources/applicationContext.xml:交由spring容器创建对象 -->
<!-- 创建数据访问层的对象:接口的实现类对象 -->
<bean id="uMapper" class="com.zjs.dao.UsersMapperImpl"></bean>
<!-- 创建业务逻辑层的对象 -->
<bean id="uService" class="com.zjs.service.UsersServiceImpl">
<property name="usersMapper" ref="uMapper"></property> <!--setUsersMapper(UsersMapper usersMapper); 接口指向 id为uMapper 的实现类对象-->
</bean>
<!-- 创建界面层的对象 -->
<bean id="uController" class="com.zjs.controller.UsersController">
<property name="usersService" ref="uService"></property>
</bean>
@Test
public void testInsertUsers() {
// 创建对象并启动容器
ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
// 取出对象
UsersController uController = (UsersController) ac.getBean("uController");
// 测试功能
uController.insert(new Users(1, "王五", 19));
}
6.基于注解的IOC(适用于改动比较少)
也称为DI(Dependency Injection),它是IOC的具体实现的技术.
使用注解
上依赖(pom.xml):在加入spring-context同时,已经将spring-aop依赖间接加入了。使用注解必须使用 spring-aop依赖
上配置: 基于注解的IOC,必须要在Spring的核心配置文件(applicationContext.xml)中添加包扫描.
<!-- 组件扫描器,组件就是java对象(作用是:说明注解在你的项目中的位置,从而创建对象)
base-package:指定注解在你项目中的包名
component-scan工作方式:容器启动后,spring会扫描遍历 base-package指定的包,
把包中和子包中的所有类,找到类中的注解,按照注解的功能创建对象,或给属性赋值
-->
// 之前都是在此处使用bean标签,提供信息给spring创建对象、赋值
如今只需要一个 组件扫描器,由它扫描指定的包,找到注解并创建对象、赋值
<context:component-scan base-package="com.bjpowernode.s01"></context:component-scan>
药: 创建对象并依赖注入
汤: xml ==》注解annotation
-
创建对象的注解(放在类名上面)
多注解利于项目分层,使用语法和@Component一样,但有额外的功能:@Repository @Service @Controller
-
@Component:
- 可以创建任意对象,等同于标签的功能。创建的对象的默认名称是类名的驼峰命名法。
- 也可以指定对象的名称: @Component(value=“指定名称”)。指定名称 等同于就 xml文件中bean标签的id属性值,其中value可以省略,名称是唯一的(创建的对象在整个spring容器中就一个)
-
@Controller:专门用来创建控制器的对象(Servlet),这种对象可以接收用户的请求,可以返回处理结果 给客户端.
-
@Service:专门用来创建业务逻辑层的对象,负责向下访问数据访问层,处理完毕后的结果返回给界面 层.(业务处理,可以有事务等功能)
-
@Repository:专门用来创建数据访问层的对象(放在DAO的实现类上面),负责数据库中的增删改查所有操作.
案例: 以上4个注解都符合 以下分析 // @Component("stu")等同于 <bean id="stu" class="com.zjs.s01.Student" /> // @Component 等同于 <bean id="student" class="com.zjs.s01.Student" /> id默认为类名的驼峰命名法。也就是 spring创建了如下对象:Student student = new Stundent(); @Component("stu") //交给Spring去创建对象,就是在容器启动时创建。spring将创建的对象放到Map集合中(FactoryBeanRegistrySupport类),这个同 第3节概念,语法小节 public class Student { @Value("张三") ===>简单类型的值注入 private String name; @Value("22") private int age; ...// 可提供也可不提供。提供调用子类的,不提供调用容器的 }
-
-
依赖注入的注解(依赖注入就是赋值,不过这个注入值的操作由容器完成)
-
简单类型(8种基本类型+String)的注入,放在属性定义的上面:
@Value:用来给简单类型注入值(无需提供set方法;有一个value属性,是String类型的,用于注入值。可以省略)
-
引用类型的注入:
spring中通过注解给引用类型赋值,使用的是自动注入原理(见 笔记第4节),支持byName,byType
byName(按名称注入): java类中引用类型的属性名和spring配置文件中bean中id名称一样且数据 类型一致,这样容器中的bean,spring能够赋值给引用类型
byType(按类型注入): java类中引用类型的数据类型和spring配置文件中bean的class属性是同 源关系,这样的bean能够赋值给引用类型-
@Autowired:使用类型注入值(使用的是byType自动注入),从整个Bean工厂中搜索同源类型的对象进行注入。同源类型也可注入.
什么是同源类型:
被注入的类型:简单理解为 类中的类成员变量的类型
spring容器注入的类型:容器为类中类成员注入值,该值的类型
-
被注入的类型与spring容器注入的类型是完全相同的类型(见项目中s02相关代码)
java类中引用类型属性的数据类型 和 bean的class的值 是一样的
-
被注入的类型(父)与spring容器注入的类型(子)是父子类(见项目中s03相关代码)
java类中引用类型属性的数据类型(父) 和 bean的class的值(子) 是父子关系的
注意:
在有父子类的情况下,若使用按类型注入,就意味着有多个可注入的对象(同源类型均可注入),默认选择父类进行注入。
此时按照名称进行二次筛选,选中与被注入对象相同名称的对象进行注入
- 项目s03中的Student类中School类成员变量就是按类型注入
- 多个可注入的对象:指的是School类和它的子类
- 按名称进行筛选:若给父子类对象指定了名称,则会从父子类中选择名称与Student类中School类成员变量名相同 的 父类/子类 进行注入
-
被注入的类型(接口)与spring容器注入的类型(实现类)是接口和实现类的类型
java类中引用类型属性的数据类型(接口) 和 bean的class的值(实现类) 是接口和实现类关系的
使用按类型注入(byType),若有多个实现类对象可被注入,则会报错;需要采用按名称注入
见 项目5 (项目5是在项目2上改造的,见第5节项目3也可)
-
-
@Autowired
@Qualifier(“名称”):使用名称注入值,从整个Bean工厂中搜索相同名称的对象进行注入。此处名称即bean标签中的id(byName)当然,用 JDK中的注解@Resource也可,可以减少spring的耦合(@Resource 的功能 = 使用名称注入值)
@Resource是来自JDK1.8中的注解,可以为引用类型赋值,使用的是自动注入原理,支持byName,byType。默认是byName(先使用byName自动注入,如果赋值失败,再使用byType)
设置@Resource为只使用byName的方式,需要添加一个属性 name,name的值是bean的id(名称)
-
-
注意:如果有父子类的情况下,直接按名称进行注入值.
项目4
项目4
-
项目构建
-
添加目录
-
添加依赖(pom.xml)
- spring的依赖
- 添加资源文件指定(build标签,详见mybatis)
-
相关代码
s01 // com.zjs.s01.Student 实体类 /** * @Component * 交给spring去创建对象,在容器启动时创建,必须在配置文件中添加包扫描 * 创建的对象的默认名称是类名的驼峰命名法,也可指定名称:@Component("指定名称"), 用 ac取值的时候用默认名称或指定名称 */ @Component public class Student { @Value("张三") ===>简单类型的值注入(无需提供set方法) private String name; @Value("18") private int age; @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } public Student() { // 可省略,这里只是为了说明spring调用了无参构造器为我们创建了对象 System.out.println("学生对象的无参构造方法......"); } } // s01/applicationContext.xml <!-- 使用注解一定要:添加包扫描 --> <context:component-scan base-package="com.zjs.s01"></context:component-scan> // test @Test public void testStudent() { // 启动容器,此时对象便已创建完成 ApplicationContext ac = new ClassPathXmlApplicationContext("s01/applicationContext.xml"); // 取出对象,使用 @Component创建的对象的默认名称是类名的驼峰命名法 Student stu = (Student) ac.getBean("student"); // 若 使用 @Component("stu"),则需 Student stu = (Student) ac.getBean("stu");才能取出对象,注解里面的值 相当于 bean标签中的id值 System.out.println(stu); }
s02 // 实体类 @Component public class School { @Value("清华大学") private String name; @Value("海定区") private String address; @Override public String toString() { return "School{" + "name='" + name + '\'' + ", address='" + address + '\'' + '}'; } public School() { // 可省略,这里只是为了说明spring调用了无参构造器为我们创建了对象 System.out.println("School的无参构造方法。。。。。。"); } } @Component public class Student { @Value("王五") private String name; @Value("16") private int age; /** 第4节 中有较为简洁的讲解 1. 引用类型按类型注入。从整个Bean工厂(注解 or xml)中搜索School类型的同源类型的对象进行注 入,注入的对象默认使用类名的驼峰命名法,该例中是 school。也可以指定对象名(无需提供set方 法)===> byType @Autowired required属性:boolean类型,默认为true -》表示引用类型赋值失败,程序报错,并终止执行 required = false:引用类型如果赋值失败,程序正常执行,引用类型赋值是null 推荐使用 true */ // 2. 引用类型按名称注入:将名称为 school的对象(通过注解创建的) 赋给Student类的 school属性[类中引用类型属性名 和 bean中id值 相同] ===> byName // 等同于 @Resource(name="school") 这里的name不能省略 // School类中使用@Component注解,School对象由容器创建,默认使用类名的驼峰命名法,即school。如果School类中用 @Component("schoolNew")为容器所创建的对象指定了名称, @Qualifier里的 school 也要换成 schoolNew @Autowired @Qualifier("school") private School school; @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + ", school=" + school + '}'; } } // s02/applicationContext.xml <!-- 添加包扫描 --> <context:component-scan base-package="com.zjs.s02"></context:component-scan> // 测试类 @Test public void testStudent() { ApplicationContext ac = new ClassPathXmlApplicationContext("s02/applicationContext.xml"); // 取出student对象 Student student = (Student) ac.getBean("student"); System.out.println(student); }
s03 @Component public class SubSchool extends School { @Value("清华附小") private String name; @Value("海淀小区") private String address; public SubSchool() { // 可不提供 System.out.println("这是SubSchool的构造方法。。。。。。"); } @Override public String toString() { return "SubSchool{" + "name='" + name + '\'' + ", address='" + address + '\'' + '}'; } } // s03/applicationContext.xml <!-- 添加包扫描 --> <context:component-scan base-package="com.zjs.s03"></context:component-scan> // 测试类: /** 1.Student类中的 School变量 按 类型注入:@Autowired: 1. 有父子类,使用默认名称 的情况下,注入给student的对象会有多个,但会默认注入父类 2. 若不使用默认名称: 给父类 @Component("schoolNew"), 子类 @Component("school"),则会注入子类 这是由于 Student类中 School类成员变量名为 school,若Student类中 School类成员 变量名改为schoolNew,则又会注入父类【底层采用了按名称注入】 2.Student类中的 School变量 按 名称注入: 若给父类 @Component("schoolFu"), 子类 @Component("schoolZi"), Student类中School变量按 名称注入 @Autowired @Qualifier("指定名称") 若指定名称为schoolFu,则注入父类对象;若指定名称为schoolZi,则注入子类对象。这时候又跟Student类中School变量的变量名没有关系(前提是保证给父子类指定的对象名 与Student类中School变量的变量名不同) 创建子类对象的同时,会调用父类无参的构造方法 */ @Test public void testStudent() { ApplicationContext ac = new ClassPathXmlApplicationContext("s03/applicationContext.xml"); Student student = (Student) ac.getBean("student"); System.out.println(student); }
补充内容:
采用上述方法,有一个弊端就是,这些由容器生成的对象的赋值更改起来很麻烦,要一个个的去改。所以有以下改进:
-
在resources目录下创建属性配置文件(普通文件)
// resources\test.properties myname=张三 myage=18
中文乱码:改为utf-8 并进行第2步
若依然乱码,则需要在设置编码之后,重新编写 属性配置文件
-
在 applicationContext.xml中加载属性配置文件
<context:property-placeholder location="classpath:test.properties" file-encoding="utf-8"/>
-
通过${} 的方式使用配置文件中的值,例如:
@Component public class Student { // @Value("张三") @Value("${myname}") ==》这个myname中的值是从test.properties文件中读取到的,下面同理 private String name; @Value("${myage}") private int age; }
通过这种方式,只需要集中修改配置文件中的信息,而不需要在代码中查找
-
项目5(三层)
项目5 :(项目结构见第5节项目2)
spring接管三层对象的创建(基于注解):===》基于xml见第5节项目3
需要添加spring的依赖 和 spring的配置文件applicationContext.xml)
// 数据访问层
public interface UsersMapper {
// 增加用户
int insert(Users user);
}
@Repository // 交给spring框架去创建数据访问层的对象,该节前面有记
public class UsersMapperImpl implements UsersMapper{
public int insert(Users user) {
System.out.println(user.getUname() + "用户添加成功");
return 1;
}
}
// 业务逻辑层
public interface UsersService {
// 调用 dao层,增加用户
int insert(Users user);
}
@Service // 交给spring框架去创建业务逻辑层的对象,该节前面有记
public class UsersServiceImpl implements UsersService{
@Autowired
private UsersMapper usersMapper; // 接口指向实现类(实现类由spring创建,通过 @Repository)
// 交给spring去依赖注入值,必须提供无参构造器,set方法。会默认提供无参构造器
public void setUsersMapper(UsersMapper usersMapper) {
this.usersMapper = usersMapper;
}
public int insert(Users user) {
return usersMapper.insert(user);
}
}
// 界面层
@Controller // 交给spring框架去界面层的对象,该节前面有记
public class UsersController {
@Autowired
private UsersService usersService; // 接口指向实现类(实现类由spring创建)
public void setUsersService(UsersService usersService) {
this.usersService = usersService;
}
// 界面层的功能实现:对外提供访问的功能
// user 为 前端传过来的数据
public int insert(Users user){
return usersService.insert(user);
}
}
<!-- src/main/resources/applicationContext.xml:交由spring容器创建对象 -->
<!-- 基于注解的开发,必须包扫描:推荐使用第 7 节第1点 -->
<context:component-scan base-package="com.zjs"></context:component-scan>
// 测试类
@Test
public void testInserUsers() {
ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
UsersController usersController = (UsersController) ac.getBean("usersController");
int num = usersController.insert(new Users(19, "张节", 20));
System.out.println(num);
}
Bean详解(补)
什么是bean?
- 在spring中,由spring IOC容器管理的对象叫做Bean。
- Bean是由Spring IOC容器实例化,组装和以其他方式管理的对象
- Bean以及它们之间的依赖关系通过容器配置元数据(xml,java注解 or java代码)来反映
Bean的作用域
在spring配置文件中可以使用 scope 来声明对象的作用域或存活时间。
- 即 spring 容器 在对象进入其相应的 scope 之前,创建并配置这些对象
- 在该对象不在处于这些 scope 的限定之后,spring容器通常会销毁这些对象
比如:
<bean id="stu" class="com.zjs.pojo.Student" scope="prototype"></bean>
spring5中的 六种作用域
-
singleton 单例作用域(默认)
-
从容器启动或第一次实例化开始,只要容器没有退出或销毁,该类型的单一实例就会一直存活
-
spring可以同一个类进行多个单例Bean的配置,也就是一个类可以对应到多个不同id的对象
-
-
prototype 原型作用域
- 原型作用域的Bean在使用容器的 getBean()方法获取的时候,每次得到的都是一个新的对象。
- 作为依赖对象注入到其他Bean的时候,也会产生一个新的类对象
- 始终以懒加载的机制的Bean进行实例化。懒加载的配置对其无效
-
request 请求作用域
- 针对每次HTTP请求,spring都会创建一个Bean实例
-
session 会话作用域
- 使用于Http Session,同一个 session 共享同一个 Bean实例
-
application 应用作用域
- 整个web应用,也就是在 ServletContext 生命周期中使用一个bean实例
-
websocke
- html5新特性。在一个websocket连接的生命周期中共用一个bean实例
Bean的自动装配
- 自动注入,见项目1
Bean的基于 Annotation 的装配
- 自动注入的注解版本,见项目4
懒加载Bean
默认情况下,spring容器在启动时会将所有的单例Bean实例化。
- 好处:如果配置有问题,实例化出错在容器启动的时候就可以提前发现
- 劣势:一次需要实例化的Bean太多,会延缓服务启动的速度,影响系统性能
懒加载Bean
-
对于一些不需要再容器启动的时候就创建的对象,可以推迟到需要用的时候才去加载和创建
-
可以全局设置和个别设置
-
全局设置(少用)
-
设置根标签的属性 default-lazy-init 为 true
<beans default-lazy-init="true"></bean>
-
-
个别设置
-
设置bean标签的属性 lazy-init 为 true
<bean id="stu" class="com.bjpowernode.pojo.Student" lazy-init="true"></bean>
-
Bean的生命周期(老杜)
- BeanFactoryPostProcessor(Bean的定义处理)
- 构造函数实例化Bean
- setter注入
- Aware相关接口的实现
- BeanPostProcessor 前置处理
- 判断 是否继承 InitiallizingBean接口 并 覆写方法
- 判断 是否配置自定义的 init-method
- BeanPostProcessor 后置处理
- Bean存活并使用
- 容器销毁
- 是否继承DisposableBean接口并覆写方法
- 判断 是否配置自定义的 destory-method
7.三种添加包扫描的方式
基于注解的开发,必须包扫描
-
单个包扫描(推荐使用)
<!-- 使用多次组件扫描器,指定不同的包 --> <context:component-scan base-package="com.bjpowernode.controller"></context:component-scan> <context:component-scan base-package="com.bjpowernode.service.impl"></context:component-scan> <context:component-scan base-package="com.bjpowernode.dao"></context:component-scan>
-
多个包扫描,多个包之间以逗号或空格或分号分隔
<!-- 使用分隔符(;或,)分隔多个包名 --> <context:component-scan base-package="com.bjpowernode.controller, com.bjpowernode.service, com.bjpowernode.dao"></context:component-scan>
-
扫描根包(不推荐,会降低容器启动的速度,导致多做无用功.)
<!-- 指定父包 --> <context:component-scan base-package="com.bjpowernode"></context:component-scan>
8.为应用指定多个 Spring 配置文件
当项目越来越大,需要多人合作开发,一个配置就存在很大隐患.
多个配置优势:
- 每个文件的大小比一个文件要小很多,效率高
- 避免多人竞争带来的冲突
如果项目中有多个模块(相关的功能在一起),一个模块一个配置文件。
如学生成绩管理系统中,学生考勤模块一个配置文件,学生成绩模块一个配置文件
多文件的分配方式:
- 按功能模块,一个模块一个配置文件
- 按类的功能,数据库相关的配置一个配置文件,做事务的功能一个配置文件,做service功能的一个配置文件等
拆分配置文件的策略
-
按层拆
applicationContext_controller.xml <bean id="uController" class="com.bjpowernode.controller.UsersController"> <bean id="bController" class="com.bjpowernode.controller.BookController"> applicationContext_service.xml <bean id="uService" class="com.bjpowernode.controller.UsersService"> <bean id="bService" class="com.bjpowernode.controller.BookService"> applicationContext_mapper.xml <bean id="uMapper" class="com.bjpowernode.controller.UsersMapper"> <bean id="bMapper" class="com.bjpowernode.controller.BookMapper">
-
按功能拆
applicationContext_users.xml <bean id="uController" class="com.bjpowernode.controller.UsersController"> <bean id="uService" class="com.bjpowernode.controller.UsersService"> <bean id="uMapper" class="com.bjpowernode.controller.UsersMapper"> applicationContext_book.xml <bean id="bController" class="com.bjpowernode.controller.BookController"> <bean id="bService" class="com.bjpowernode.controller.BookService"> <bean id="bMapper" class="com.bjpowernode.controller.BookMapper">
注意:当按以上两种方法进行拆分后,通常需要配合第9节的内容一同使用(可拿项目3以及项目5练手)
项目3的分层:
// 以下4个xml文件都是在resource目录下创建的
applicationContext_mapper.xml
<!-- 创建数据访问层的对象 -->
<bean id="uMapper" class="com.zjs.dao.UsersMapperImpl"></bean>
applicationContext_service.xml
<!-- 创建业务逻辑层的对象 -->
<bean id="uService" class="com.zjs.service.Impl.UsersServiceImpl">
<property name="usersMapper" ref="uMapper"></property>
</bean>
applicationContext_controller.xml
<!-- 创建界面层的对象 -->
<bean id="uController" class="com.zjs.controller.UsersController">
<property name="usersService" ref="uService"></property>
</bean>
total.xml(主配置文件:用来包含其他的配置文件的。主配置文件一般是不定义对象的)
<!-- 导入文件:见第9节 -->
<import resource="applicationContext_mapper.xml"></import>
<import resource="applicationContext_service.xml"></import>
<import resource="applicationContext_controller.xml"></import>
经过以上更改后,创建容器对象并启动时,ApplicationContext ac = new ClassPathXmlApplicationContext(“total.xml”);
@Test
public void testInsertUsers() {
// 创建对象并启动,加载的是主配置文件
ApplicationContext ac = new ClassPathXmlApplicationContext("total.xml");
// 取出对象
UsersController uController = (UsersController) ac.getBean("uController");
// 测试功能
uController.insert(new Users(1, "王五", 19));
}
项目5的分层跟整合与以上描述大相径庭,不在赘述
9.spring配置文件的整合
语法:
关键字:"classpath:"表示类路径(class文件所在的目录:target/classes)
在spring的配置文件中要指定其他文件的位置,需要使用classpath,告诉spring到哪去加载读取文件
规范写法: resource=“classpath: “加” resource目录下xml文件的路径(不含resource目录)”
当然,如果主配置文件和其他配置文件在同一包下,“classpath:包名” 可以省略(本教程采用的就是这种)
1)单个文件导入
<import resource="applicationContext_mapper.xml"></import>
<import resource="applicationContext_service.xml"></import>
<import resource="applicationContext_controller.xml"></import>
2)批量导入:使用通配符(切入点表达式中有介绍)
注:主配置文件名称不能包含在通配符的范围内(不能叫做applicationContext_total.xml)
使用这种方案,通配符指定的所有配置文件需要放在一个目录中。通配符对一个目录下的所有文件生效
<import resource="applicationContext_*.xml"></import>
至此,spring有关IOC的内容全部结束
10.面向切面编程AOP
动态代理:
-
可以在程序的执行过程中,创建代理对象。通过代理对象执行方法,给目标类的方法增加额外的功能(功能增强)。实现业务方法 和 非业务方法(如:日志,事务)的解耦合
-
实现方式
- jdk动态代理,使用jdk中的Proxy,Method,InvocationHanderl创建代理对象。jdk动态代理要求目标类必须实现接口 ===》第11节第5小节
- cglib动态代理,第三方的工具库,创建代理对象,原理是继承。通过继承目标类,创建子类。子类就是代理对象。要求目标类不能是final的,方法也不能是final的
-
动态代理的作用(aop的作用)
-
在目标类源代码不改变的情况下,增加功能(非业务逻辑的代码【切面类】 的织入 通过动态代理的方式实现,在不改变目标类的源码的前提下实现了功能增强)
-
减少代码的重复(理由同上)
-
专注业务逻辑代码
-
解耦合,让你的业务功能和日志,事务非事务功能分离
-
AOP(Aspect Orient Programming):面向切面编程,基于动态代理的,可以使用jdk,cglib两种代理方式
AOP就是动态代理的规范化,把动态代理的实现步骤,方式都定义好了,让开发人员用一种统一的方式,使用动态代理。在目标类对象不修改源代码的情况下,增加其功能
OOP:面向对象编程
- 切面:公共的,通用的,重复的功能称为切面(切面一般都是非业务方法,独立使用的,给目标类增强功能的)。比如事务,日志,权限验证
面向切面编程就是将切面提取出来,单独开发,在需要调用的方法中通过动态代理的方式进行织入.
切面的三要素:
- 切面的功能:干什么 ===》切面类(@Aspect)实现切面功能
- 切面的执行位置: Pointcut表示切面执行的位置 ==> 切入点表达式
- 切面的执行时间:使用Advice表示时间,在目标方法之前,还是目标方法之后 ==》 通知
11.手写AOP框架(项目6)
为了方便理解所写的简易版AOP框架。spring实现了AOP,具体使用见下节
项目6
业务:图书购买业务
切面:事务
-
第一个版本:业务和切面紧耦合在一起,没有拆分.
/** * 图书购买业务和事务切面耦合在一起 */ public class BookServiceImpl { public void buy(){ try { System.out.println("事务开启......."); System.out.println("图书购买业务功能实现..........."); System.out.println("事务提交......."); } catch (Exception e) { System.out.println("事务回滚......."); } } }
-
第二个版本:使用子类代理的方式拆分业务和切面.
/** * 使用子类代理的方式进行图书业务和事务切面的拆分(动态代理笔记在mybatis文件夹) */ 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) { System.out.println("事务回滚........."); } } } // test public class MyTest02 { @Test public void test02(){ BookServiceImpl service = new SubBookServiceImpl(); service.buy(); } }
-
第三个版本:使用静态代理(见mybatis笔记)拆分业务和切面。业务和业务接口已拆分。此时切面紧耦合在业务中.
public interface Service { //规定业务功能 void buy(); } /** * 目标对象:业务功能的具体实现 */ public class BookServiceImpl implements Service { @Override public void buy() { System.out.println("图书购买业务功能实现............"); } } public class ProductServiceImpl implements Service { @Override public void buy() { System.out.println("商品购买业务实现........."); } } /** * 静态代理已经实现了目标对象的灵活切换 * 图书购买业务,商品购买业务 */ public class Agent implements Service { //设计成员变量的类型为接口,为了灵活切换目标对象 public 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("事务回滚......."); } } } public class MyTest03 { @Test public void test02(){ Service agent = new Agent(new ProductServiceImpl()); agent.buy(); } }
-
第四个版本:使用静态代理拆分业务和业务接口,切面和切面接口.
// Service BookServiceImpl ProductServiceImpl类 同第3个,此处不再给出 //切面接口(非业务方法的实现类,类似于jdbc中为简化开发创建的工具类) // 通过工具类和动态代理以及反射机制,将 工具类中的 非业务方法 织入到 目标类的业务方法 中,实现了业务方法 和 非业务方法 的解耦合 public interface AOP { // default关键字:使得实现类没必要实现接口中的所有方法,例如下面的 LogAop default void before(){} default void after(){} default void exception(){} } // 不同的切面 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 exception() { 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(); //事务 日志 //业务 target.buy(); //图书 商品 //切面 aop.after(); //事务 } catch (Exception e) { //切面 aop.exception(); } } } public class MyTest04 { @Test public void test02(){ Service agent = new Agent(new ProductServiceImpl(),new TransAop()); Service agent1 = new Agent(agent,new LogAop()); agent1.buy(); } }
-
第五个版本:使用动态代理完成第四个版本的优化.
// 在4的基础上,修改Agent类为动态代理即可。通过 ProxyFactory得到动态代理对象Agent /* 扩展功能更易实现,例如: 1.Service 接口添加方法: default String show(int age){return null;} 2.BookServiceImpl 类 实现该方法。 这就是动态代理的好处,不管怎么扩充功能,代理类中的代码都不用改变【实现解耦合】。而使用静态代理,Agent类中代码要一直维护。 补充: 下面的代码有一个问题,当我们执行 buy方法 或 show方法,都会加入切面的功能 如何实现在执行 buy方法时 加入切面功能,而执行 show方法时不加入切面功能?==> 通过 method 对象 String methodName = method.getName(); // 获取正在执行的方法的名称 if("buy".equals(methodName)){ aop.before(); obj = method.invoke(target, args); // BookServiceImpl.buy(); aop.after(); }else{ obj = method.invoke(target, args); // BookServiceImpl.show(); } */ public class ProxyFactory { // target 为 Service接口的实现类对象 public static Object getAgent(Service target, AOP aop){ //通过 类Proxy 返回生成的动态代理对象 return Proxy.newProxyInstance( //类加载器 target.getClass().getClassLoader(), //目标对象实现的所有的接口 target.getClass().getInterfaces(), //代理功能实现类对象 new InvocationHandler() { // 通过代理对象Proxy执行方法时,会调用InvocationHandler类中的invoke() @Override public Object invoke( //生成的代理对象 Object proxy, //类Method 为正在被调用的目标方法如 buy(),show() Method method, //目标方法的参数 Object[] args) throws Throwable { // 目标方法的执行结果 Object obj = null; try { // System.out.println(method.getName()); //获取正在被调用的方法名 //切面:非业务功能的实现(如事务,日志)==》执行切面类方法(非业务方法) aop.before(); //业务功能的实现:target为目标类对象 ==》执行目标类的方法(业务方法) // 无论后续在目标类中增加多少业务功能,这里总能够通过反射机制获取到目标类 中所有的业务方法(相比于静态代理,改动的代码少了很多)。在开头也有讲 obj = method.invoke(target, args); //切面 aop.after(); } catch (Exception e) { //切面 aop.exception(); } return obj; //目标方法的返回值 } } ); } } // test public class MyTest05 { @Test public void test02(){ //得到动态代理对象 Service agent = (Service) ProxyFactory.getAgent(new BookServiceImpl(),new TransAop()); agent.buy(); } @Test public void test03(){ //得到动态代理对象 Service agent = (Service) ProxyFactory.getAgent(new BookServiceImpl(),new LogAop()); System.out.println(agent.getClass()); String s = agent.show(22); System.out.println(s); } }
通过以上代码不难看出,通过 工具类(切面类)和 动态代理,能够实现业务功能(目标类) 和 非业务功能(切面类)的解耦合
为了简化开发,动态代理对象 的实现已经被框架写好了。例如下面讲的AspectJ框架(第17节)。
aop是一个规范,是实现动态代理的一个规范化,一个标准。底层是框架为我们实现了的
aop技术的实现框架:
1. spring,主要在事务处理时使用spring内部实现的aop。很少用,比较笨重 1. aspectJ,一个开源的,专门做aop的一个框架
我们所需要做的,就是
-
编写业务接口,业务实现类,和切面的实现类,
-
xml文件中进行业务实现类和切面实现类的绑定(底层的实现就是动态代理)==》参见17节项目7
通过动态代理,将切面类中的 非业务逻辑代码 织入到 实现类中。实现 非业务逻辑代码 和 业务逻辑代码的解耦合。这个功能有框架为我们实现好了,例如下面介绍的AspectJ框架。使得程序员专注于业务逻辑的实现
12.Spring支持的AOP的实现
相关项目
Spring支持AOP的编程(没怎么用,用的是AspectJ),常用的有以下几种:
-
Before通知:在目标方法被调用前调用,涉及接口org.springframework.aop.MethodBeforeAdvice;
-
After通知:在目标方法被调用后调用,涉及接口为org.springframework.aop.AfterReturningAdvice;
-
Throws通知:目标方法抛出异常时调用,涉及接口org.springframework.aop.ThrowsAdvice;
-
Around通知:拦截对目标对象方法调用,涉及接口为org.aopalliance.intercept.MethodInterceptor。
(事务就是一种Around通知)
13.AOP常用的术语
-
切面(Aspect):就是那些重复的,公共的,通用的功能称为切面。表示增强的功能,一般是非业务功能。例 如:日志,事务,权限验证,参数检查,统计信息
-
连接点(JoinPoint):连接业务方法和切面的位置,连接点其实就是目标类中的某个业务方法。因为在目标方法中要实现(连接)目标方法的功能和切面功能.
-
切入点(Pointcut):指定切入的位置,多个连接点构成切入点。切入点可以是一个目标方法,可以是一个类中 的所有方法,可以是某个包下的所有类中的方法.(为多个业务方法 增强同一种功能,这些业务方法的集合称为切入点)
-
目标对象:操作谁(给哪个类的方法增加功能),谁就是目标对象.
-
通知(Advice):来指定切面方法执行的时机。是在目标方法执行前还是执行后还是出错时,还是环绕目标方法切入切面功能.
切面的三要素:
- 切面的功能代码:干什么 ===》切面类(@Aspect注解标识的类都是切面类)
- 切面的执行位置: Pointcut表示切面执行的位置 ==> 切入点表达式
- 切面的执行时机:使用Advice表示时间,在目标方法之前,还是目标方法之后 ==》 通知
14.什么是AspectJ框架
AspectJ 是一个优秀的面向切面的框架。它扩展了 Java 语言,提供了强大的切面实现(实现了AOP)。它因为是基于java语言开发的,可以无缝扩展。easy to learn and use(易学易用).
使用AOP:目的是给已经存在的一些类和方法,增加额外的功能。前提是不改变目标类中的代码,进行功能增强
使用aspectJ实现aop的步骤:(见18节前项目7)
-
新建maven项目
-
加入依赖
- spring 的依赖
- aspectJ 的依赖
-
创建目标类:接口和它的实现类(在其中编写 业务方法)
-
创建切面类:
-
类上面需添加 @Aspect 注解(表明当前类是切面,用于给业务方法增加功能)
-
类中定义方法,这些方法就是切面要执行的 非业务逻辑代码。用于给目标类增强功能
在方法的上面加入 aspectj中的通知注解,例如 @Before;还需要指定切入点表达式 execution()
-
-
创建spring的配置文件:声明对象,把对象交给容器统一管理
声明对象可以使用注解 或者 xml配置文件
-
声明目标类对象
-
声明切面类对象
-
声明aspectJ框架中的自动代理生成器标签
自动代理生成器:用来完成代理对象的自动创建功能的【项目6中第5个是自己创建动态代理对象,现在这里让容器自动创建代理对象】用来绑定目标类和切面类,实现功能增强
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
-
-
创建测试类,从spring容器中获取目标对象(实际就是代理对象)
通过代理对象执行方法,实现aop的功能增强
15.AspectJ常见通知类型
回顾第13节:
切面的执行时机:在aop规范中叫做 Advice(通知)
AspectJ 中常用的通知(增强)有四种类型:前面三个用的比较多
-
前置通知@Before
-
后置通知@AfterReturning
-
环绕通知@Around
-
最终通知@After
-
定义切入点@Pointcut(了解)
-
@AfterThrowing
16.AspectJ 的切入点表达式(掌握)
表示切面执行的位置
规范的公式:
execution(访问权限 方法返回值 方法声明(参数) 异常类型)
简化后的公式:
execution( 方法返回值 方法声明(参数) )
用到的符号:
* :代表 任意个任意的字符(通配符)
… : 如果出现在方法的参数中,则代表任意参数
如果出现在路径中,则代表本路径及其所有的子路径
+:用在类名后,表示当前类及其子类
用在接口后,表示当前接口及其实现类
示例:(见讲义:红色的是重点)
- execution(public * *(…)) :任意的公共方法
- execution(* set*(…)):任何一个以“set”开始的方法
- execution(* com.xyz.service.impl.*.*(…)):
任意的返回值类型,在com.xyz.service.impl包下的任意类的任意方法的任意参数
execution(* com.xyz.service…*.*(…)):
任意的返回值类型 ,在com.xyz.service包及其子包下的任意类的任意方法的 任意参数
com.xyz.service.a.b..(…) com.xyz.service..(…)
execution(* *…service.*.*(…)):指定所有包下的 service子包下的任意类的任意方法的任意参数
execution(* *.service.*.*(…)):指定只有 一级包下的service子包下任意类的任意方法的任意参数 为切入点
17.AspectJ的前置通知@Before
在目标方法执行前切入切面功能。在切面方法中不可以获得目标方法的返回值,只能得到目标方法的签名(在切面方法形参中 使用JoinPoint类,项目7中有具体解释).
项目7(切面类的创建)
项目7
实现的步骤:
-
pom.xml 添加依赖(spring的依赖 和 aspectJ的依赖)
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.5.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>5.2.5.RELEASE</version> </dependency> // 资源文件绑定 <build> // 见mybatis笔记第6节 </build>
-
代码实现
三个类都是在com.zjs.s01包下创建的
-
创建业务接口(若没有目标类的接口,框架自动使用cglib动态代理)
public interface SomeService { String doSome(String name, int age); void show(); }
-
创建业务实现类
public class SomeServiceImpl implements SomeService { public String doSome(String name, int age) { System.out.println("doSome的业务功能实现。。。。"); return "abcd"; } public void show(){ System.out.println("show的业务功能实现。。。。"); } }
-
创建切面类,实现切面方法,为目标类增强功能
//表明当前类是切面类:给业务方法增加功能的类,这个类中有切面的功能代码 // 必须配置自动代理生成器,该注解才能够被识别,否则会报错 //交给AspectJ的框架去识别切面类(MyAspect) @Aspect public class MyAspect { /** * 所有切面的功能都是由切面方法来实现的 * 可以将各种切面都在此类中进行开发 * * 前置通知的切面方法的规范: * 1)访问权限是public * 2)方法的返回值是void * 3)方法名称自定义 * 4)方法没有参数,如果有也只能是JoinPoint类型(获取目标方法【需进行功能增强的方法】的 * 签名)。 JoinPoint对象:连接点,即业务方法,表示 要加入切面功能的业务方法。 目标方法执行时,会将目标方法的信息封装到 JoinPoint对象中 作用是:可以在通知方法(切面方法)中获取目标方法执行时的信息,例如方法名称, 方法的实参。 如果你的切面功能中需要用到目标方法的信息,就在 切面方法中加入形参 JoinPoint 这个JoinPoint参数的值是由框架赋予的,必须是切面方法中第一个位置的参数 * 5)必须使用@Before注解来声明切入的时机是前切功能和切入点 * @Before注解的参数:value 指定切入点表达式,表示切面的功能执行的位置 * 业务方法: * public String doSome(String name, int age) */ //这种切入点表达式 直接写死了,执行show方法时无法织入切面方法 @Before(value = "execution(public String com.zjs.s01.SomeServiceImpl.*(String,int))") public void myBefore(){ System.out.println("切面方法中的前置通知功能实现............"); } @Before(value = "execution(public * com.zjs.s01.SomeServiceImpl.*(..))") public void myBefore(){ System.out.println("切面方法中的前置通知功能实现............"); } // s01包下任意类的任意方法(推荐使用) @Before(value = "execution( * com.zjs.s01.*.*(..))") public void myBefore(JoinPoint jp){ // 参数只能是 JoinPoint类型 System.out.println("切面方法中的前置通知功能实现............"); System.out.println("目标方法的签名:"+jp.getSignature()); System.out.println("目标方法的参数值:"+ Arrays.toString(jp.getArgs())); } //s01包及其子包下任意类的任意方法 @Before(value = "execution( * com.zjs.s01..*(..))") public void myBefore(){ System.out.println("切面方法中的前置通知功能实现............"); } // 太开放了,不推荐 @Before(value = "execution( * *(..))") public void myBefore(){ System.out.println("切面方法中的前置通知功能实现............"); } }
-
在applicationContext.xml文件中进行切面绑定(切面类和业务逻辑层实现类的绑定)(src\main\resources\s01)
<!-- 创建业务对象 --> <bean id="someService" class="com.zjs.s01.SomeServiceImpl"></bean> <!-- 创建切面对象--> <bean id="myaspect" class="com.zjs.s01.MyAspect"></bean> <!-- 若使用注解,上述两行也可省略,但必须添加包扫描 1. SomeServiceImpl类添加 @Service 注解 2. MyAspect 类添加 @Component 注解 3. 在xml文件中添加包扫描 <context:component-scan base-package="com.zjs.s01"></context:component-scan> 进行如此修改后,test中访问业务对象,默认创建的对象名是 类名的驼峰命名法。创建的业务对象是代理对象 SomeService someService = (SomeService) ac.getBean("someServiceImpl"); --> <!-- 绑定:默认采用jdk动态代理绑定,见18节(再也不用像第11节第5点那样自己创建动态代理对象了) 自动代理生成器:创建目标对象的代理对象,是在内存中实现的。修改目标对象在内存中的结构,创建为代理 对象,所以目标对象就是被修改后的代理对象 aspectj-autoproxy会把spring容器中所有的目标对象(业务对象),一次性都生成代理对象 自动代理生成器通过@Aspect注解扫描切面类,通过 通知和切入点表达式 明确切入的位置和时机,找到目标对 象,并生成代理对象 --> <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
-
测试类
@Test public void test01() { // 启动容器,此时创建对象 ApplicationContext ac = new ClassPathXmlApplicationContext("s01/applicationContext.xml"); SomeService someService = (SomeService) ac.getBean("someService"); System.out.println(someService.getClass()); // Proxy类型,jdk动态代理类型 String s = someService.doSome("张三", 22); System.out.println(s); }
18.AspectJ框架切换JDK动态代理和CGLib动态代理
动态代理的知识见mybatis里面的笔记
- jdk动态代理
// applicationContext.xml
<aop:aspectj-autoproxy></aop:aspectj-autoproxy> ===>默认是JDK动态代理,取时必须使用接口类型接动态代理对象
// test
ApplicationContext ac = new ClassPathXmlApplicationContext("s01/applicationContext.xml");
SomeService someService = (SomeService) ac.getBean("someService");
// SomeServiceImpl someService = (SomeServiceImpl) ac.getBean("someService"); 类型转换异常,左边是 SomeServiceImpl类型,右边是 Proxy动态代理类型
- CGLib动态代理(又称为子类代理,目标类没有接口只能用这种方式实现,spring自动应用cglib)
applicationContext.xml
<aop:aspectj-autoproxy proxy-target-class="true"></aop:aspectj-autoproxy> ==>设置为CGLib子类代理,可以使用接口或实现类接动态代理对象
// test
ApplicationContext ac = new ClassPathXmlApplicationContext("s01/applicationContext.xml");
// 没有SomeService接口
SomeServiceImpl someService = (SomeServiceImpl) ac.getBean("someService");
System.out.println(someService.getClass());
记住:使用接口来接动态代理对象,永远不出错.
19.后置通知@AfterReturning
后置通知是在目标方法执行后切入切面功能。可以得到目标方法的返回值,可以根据这个返回值做不同的处理功能
如果目标方法的返回值是简单类型(8种基本类型+String)则不可改变,如果目标方法的返回值是引用类型则可以改变.(值传递和引用传递)
代码实现:四个类都是在com.zjs.s02包下创建的
public class Student{
private String name;
private int age;
// set get 含参、无参构造器,toString
}
public interface SomeService {
String doSome(String name, int age);
Student change();
}
@Service // 交由容器创建对象
public class SomeServiceImpl implements SomeService{
public String doSome(String name, int age) {
System.out.println("doSome业务方法被执行。。。。");
// System.out.println(1/0); // 后置通知在目标方法出现异常时不会执行
return "abcd";
}
public Student change(){
System.out.println("change方法被执行。。。。");
return new Student("张三", 22);
}
}
@Aspect
@Component
public class MyAspect {
/**
* 后置通知的方法的规范:
* 1)访问权限是public
* 2)方法没有返回值void
* 3)方法名称自定义
* 4)方法有参数(也可以没有参数,如果目标方法没有返回值,则可以写无参的方法。但一般会写有参,这样 * 可以处理无参也可以处理有参)。这个切面方法的参数就是目标方法的返回值:Object
* 5)使用@AfterReturning注解 表明是后置通知
* 属性:
* value:指定切入点表达式
* returning:指定目标方法的返回值的名称。此名称必须与 切面方法的形参名称一致.
*业务方法:
* public String doSome(String name, int age)
* JoinPoint对象一定要放在形参的第一个位置
后置通知的执行:
Object obj = doSome(name, age); // 得到目标方法的返回值
myAfterReturning(JoinPoint jp, obj); // 执行切面方法
*/
@AfterReturning(value = "execution(* com.zjs.s02.*.*(..))",returning = "obj")
public void myAfterReturning(JoinPoint jp, Object obj){
// obj 代表目标方法的返回值。 Object obj = doSome(name, age);
System.out.println("后置通知功能实现.............." + jp.getSignature());
if(obj != null){
if(obj instanceof String){
obj = obj.toString().toUpperCase();
System.out.println("在切面方法中目标方法的返回值:"+obj);
}
if(obj instanceof Student){
Student stu = (Student) obj;
stu.setName("李四");
System.out.println("在切面方法中目标方法的返回值:"+stu);
}
}
}
}
// src\main\resources\s02\applicationContext.xml
<context:component-scan base-package="com.zjs.s02"></context:component-scan>
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
@Test
public void test01() { // 基本类型无法通过 后置通知 改变其值
ApplicationContext ac = new ClassPathXmlApplicationContext("s02/applicationContext.xml");
SomeService someService = (SomeService) ac.getBean("someServiceImpl");
String s = someService.doSome("张三", 22);
System.out.println("测试目标方法中的返回值" + s);
}
@Test
public void test02() { // 引用类型能通过 后置通知 改变其值
ApplicationContext ac = new ClassPathXmlApplicationContext("s02/applicationContext.xml");
SomeService someService = (SomeService) ac.getBean("someServiceImpl");
Student stu = someService.change();
System.out.println("测试目标方法中的返回值" + stu);
}
20.环绕通知@Around
它是通过拦截目标方法的方式 ,在目标方法前后都能增强功能的通知。
它是功能最强大的通知,一般在事务中使用此通知。(目标方法之前开启事务,执行目标方法,在目标方法之后提交事务)
它可以轻易的改变目标方法的返回值(比后置通知强,它能改变基本数据类型的值);也能控制目标方法是否被调用执行
环绕通知就等同于jdk动态代理的 InvocationHandler接口
代码实现:3个类都是在com.zjs.s03包下创建的
public interface SomeService {
String doSome(String name, int age);
}
@Service // 交由容器创建对象
public class SomeServiceImpl implements SomeService{
public String doSome(String name, int age) {
System.out.println("doSome业务方法被执行。。。。" + name);
return "abcd";
}
}
@Aspect
@Component
public class MyAspect {
/**
* 环绕通知方法的规范:
* 1)访问权限是public
* 2)切面方法有返回值,切面方法的返回值就是目标方法的返回值,可以被修改
* 3)方法名称自定义
* 4)方法有参数,是固定的ProceedingJoinPoint,作用是执行目标方法。类似于动态代理中 * 的 Method类
* 5)回避异常Throwable
* 6)使用@Around注解声明是环绕通知
* 属性:
* value:指定切入点表达式
*/
// ProceedingJoinPoint 继承了 JoinPoint,故他也可以拿到目标方法的函数签名
@Around(value = "execution(* com.bjpowernode.s03.*.*(..))")
public Object myAround(ProceedingJoinPoint pjp) throws Throwable {
// 获取目标方法的函数签名(控制目标方法是否执行,通过参数)
Object args[] = pjp.getArgs();
System.out.println(Arrays.toString(args));
//前切功能实现:在目标方法前增强功能
System.out.println("环绕通知中的前置功能实现............");
//目标方法调用:obj即目标方法的返回值 Object obj = doSome();
Object obj = pjp.proceed(pjp.getArgs()); //等价于 method.invoke();
//后切功能实现:在目标方法后增强功能
System.out.println("环绕通知中的后置功能实现............");
return obj.toString().toUpperCase(); //改变了目标方法的返回值
}
}
// src\main\resources\s03\applicationContext.xml
<context:component-scan base-package="com.zjs.s03"></context:component-scan>
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
@Test
public void test01() {
ApplicationContext ac = new ClassPathXmlApplicationContext("s03/applicationContext.xml");
SomeService someService = (SomeService) ac.getBean("someServiceImpl");
String s = someService.doSome("张三", 22); //实际执行的是 myAround();
System.out.println("测试目标方法中的返回值" + s);
}
21.最终通知@After
若目标方法不能正常执行(出现异常),则对于环绕通知而言,后切功能代码无法执行;后置通知无法执行。
而对于最终通知,无论目标方法是否正常执行,最终通知的代码都会被执行.
一般在其中做资源清理工作
代码:
3个类都是在com.zjs.s04包下创建的
// SomeService 类见20节,此处不再给出
@Service // 交由容器创建对象
public class SomeServiceImpl implements SomeService{
public String doSome(String name, int age) {
System.out.println("doSome业务方法被执行。。。。" + name);
//System.out.println(1/0); //就算出现异常,最终通知依然可以执行
return "abcd";
}
}
@Aspect
@Component
public class MyAspect {
/**
* 最终通知方法的规范
* 1)访问权限是public
* 2)方法没有返回值
* 3)方法名称自定义
* 4)方法没有参数,如果有也只能是JoinPoint
* 5)使用@After注解表明是最终通知
* 参数:
* value:指定切入点表达式
*/
@After(value = "execution(* com.zjs.s04.*.*(..))")
public void myAfter(){
System.out.println("最终通知的功能........");
}
}
// src\main\resources\s04\applicationContext.xml
<context:component-scan base-package="com.zjs.s04"></context:component-scan>
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
@Test
public void test01() {
ApplicationContext ac = new ClassPathXmlApplicationContext("s04/applicationContext.xml");
SomeService someService = (SomeService) ac.getBean("someServiceImpl");
String s = someService.doSome("张三", 22);
System.out.println("测试目标方法中的返回值" + s);
}
异常通知@AfterThrowing
// 切面方法
/*
执行:
try{
目标方法;
}catch(Exception e){
myAfterThrowing(e);
}
*/
@AfterThrowing(value="execution(* com.zjs.s05.*.*(..))", throwing="ex")
public void myAfterThrowing(Exception ex){
System.out.println("异常通知,目标方法发生异常时执行:" + ex.getMessage());
}
22.给切入点表达式起别名@Pointcut
如果多个切面切入到同一个切入点,可以使用别名简化开发。该注解不是前面所讲的通知注解
在一个空方法上使用@Pointcut注解,此方法就是切入点表达式的别名,其他通知中的value属性就能用该别名代替切入点表达式。
@Aspect
@Component
public class MyAspect {
@After(value = "mycut()")
public void myAfter(){
System.out.println("最终通知的功能........");
}
@Before(value = "mycut()")
public void myBefore(){
System.out.println("前置通知的功能........");
}
@AfterReturning(value = "mycut()",returning = "obj")
public void myAfterReturning(Object obj){
System.out.println("后置通知的功能........");
}
@Around(value = "mycut()")
public Object myAround(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("环绕通知中的前置通知的功能........");
Object obj = pjp.proceed(pjp.getArgs());
System.out.println("环绕通知中的后置通知的功能........");
return obj;
}
// value 指定切入点表达式
@Pointcut(value = "execution(* com.bjpowernode.s04.*.*(..))")
private void mycut(){} // 这个方法一般不会供外界调用
}
执行顺序:环绕通知的前置通知 ==》前置通知 =》环绕通知的后置通知 ==》最终通知 ==》后置通知
23.SM整合的步骤;spring处理事务
用的技术是ioc。使用spring的ioc技术,将mybatis和spring集成在一起,像一个框架
ioc能让spring为我们创建对象,故可以把mybatis中的对象交给spring统一创建,开发人员从spring容器中获取对象
回顾mybatis使用步骤,对象
-
定义 dao接口:StudentMapper
-
定义mapper文件:StudetnMapper.xml
-
定义mybatis的主配置文件:SqlMapConfig.xml
必须实现: 1. 读取 数据库的配置文件 2. 配置 数据库 3. 指定mapper文件的位置 可以不用实现: 4. 配置别名 5,打印日志
-
创建 dao的代理对象:StudentMapper sMapper = SqlSession.getMapper(StudentMaper.class);
要创建的对象有:SqlSessionFactory ===> SqlSession ===> sMapper。这些交给spring创建
我们会使用独立的连接池类替换 mybatis默认自己带的,把连接池类也交给spring创建
SqlSessionFactory的创建要读取主配置文件(所以项目8第7步要配置数据源)
通过以上说明,我们需要让spring创建的mybatis对象有:(项目8 第 7 步)
-
独立的连接池类对象,使用阿里的 druid连接池
-
SqlSessionFactory对象,得到SqlSession对象。
使用SqlSessionFactoryBean对象在内部创建SqlSessionFactory
-
dao代理对象。
使用MapperScannConfigure,在这个类的内部调用 getMapper方法,创建接口的dao对象
需要学习的就是上面三个对象的创建语法:使用xml的 bean标签
项目8:SM整合
项目8
-
建表 springuser.sql,添加idea中数据库可视化(Mybatis中有讲)
-
新建项目,选择quickstart模板
-
修改目录,并添加三层目录(pojo,dao(mapper),service,到了springmvc还要添加 controller)
-
修改pom.xml文件,添加相关的依赖(使用老师提供 Spring+MyBatis依赖(mysql驱动使用mysql8,要修改)
spring 的依赖; mybatis的依赖; mysql驱动;spring的事务的依赖;
mybatis和spring集成的依赖:mybatis官方提供的,用来在spring项目中创建mybatis的 SqlSessionFactory,dao对象的
-
添加MyBatis相应的模板(SqlMapConfig.xml和XXXMapper.xml文件)
添加模板的目的是不在像mybatis笔记中那样 去 粘贴头文件了
添加模板的步骤:Settings ==> file and code templates ==> 添加模板,将路径中的文件模板添加到idea
-
拷贝jdbc.propertiest属性文件到resources目录下jdbc.properties(老师给的是连接mysql5的,这里我用的是mysql8的,记得改driver和密码。注意两者细微的差别(jdbc.propertiest 和 maven导入依赖),这个在 MyBatis笔记中有讲)
-
添加SqlMapConfig.xml文件(MyBatis主配置文件)【使用第5步的模板进行修改】
// resources 目录下(由于spring接管了部分MyBatis的功能,所以此处给出优化后的配置文件) <settings> <setting name="logImpl" value="STDOUT_LOGGING"/> </settings>
mybatis的配置
-
添加applicationContext_mapper.xml(重点:这里接管了 mybatis主配置文件中的绝大部分功能,如读取配置文件,配置数据源,指定mapper文件位置,设置别名)
添加第7,8步的xml文件需选择 XML Configuration File。如果没有该选项,需要刷新一下pom.xml文件
// resources 目录下 <!-- 读取属性文件:配置数据源时${}里面要用到该文件中的内容 property-placeholder:属性占位符 --> <context:property-placeholder location="jdbc.properties"></context:property-placeholder> <!-- 创建数据源:druid连接池 --> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${jdbc.driverClassName}"></property> <property name="url" value="${jdbc.url}"></property> <!-- serUrl() --> <property name="username" value="${jdbc.username}"></property> <property name="password" value="${jdbc.password}"></property> </bean> <!-- 配置mybatis中提供的 SqlSessionFactoryBean类,这个类内部创建SqlSessionFactory。 在mybatis中创建 SqlSessionFactory 需要读取 主配置文件。所以这里要配置数据源和主配置文件 --> <bean class="org.mybatis.spring.SqlSessionFactoryBean"> <!-- 配置数据源:set注入,将druid连接池赋给了dataSource --> <property name="dataSource" ref="dataSource"></property> <!-- 配置Mybatis的主配置文件, configLocation是Resource类型,读取配置文件 它的赋值要使用value,指定文件路径,最好加上 classpath: --> <property name="configLocation" value="SqlMapperConfig.xml"></property> <!-- 注册实体类别名:一定有com.zjs.pojo包 --> <property name="typeAliasesPackage" value="com.zjs.pojo"></property> </bean> <!-- 注册mapper.xml文件:一定有com.zjs.mapper包 MapperScannerConfigurer,这个类内部会调用getMapper(),生成每个dao接口的代理对象 --> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <!-- 若在上面指定了 SqlSessionFactoryBean对象的id,这里还需要添加如下配置: <property name="SqlSessionFactoryBeanName" value="SqlSessionFactoryBean对象的id"></property> `--> <!-- 指定包名,包名是dao接口所在的包 MapperScannerConfigurer会扫描这个包(可以扫描多个包)中的所有接口,把每个接口都执行一次 getMapper()方法,得到每个接口的dao对象。 创建好的dao对象放入到spring的容器中,默认创建的dao对象的名称是接口名称的驼峰命名法 --> <property name="basePackage" value="com.zjs.mapper"></property> </bean>
SM整合就是:利用spring的ioc技术,让spring创建 mybatis框架中需要的对象,主要有 数据源对象(druid连接池),SqlSessionFactoryBean对象,MapperScannerConfigure对象
-
添加applicationContext_service.xml
<!-- 导入applicationContext_mapper.xml文件 --> <import resource="applicationContext_mapper.xml"></import> <!-- SM是基于注解的开发,所以一定要添加包扫描:一定有com.zjs.service.impl包--> <context:component-scan base-package="com.zjs.service.impl"></context:component-scan> <!-- 事务处理:第31节声明式事务 or 第24节注解式事务 -->
spring的配置
至此配置结束,因为不是web项目,所以不用在web.xml中注册框架(都没那个目录)
-
在pojo下添加Users实体类,Accounts实体类
public class Users { private Integer userId; private String userName; private String userPass; // 无参,含参构造器,get,set方法,toString }
-
添加mapper包,添加UsersMapper接口和UsersMapper.xml文件并开发(DAO)
UsersMapper.xml文件的创建使用第5步的模板进行修改
public interface UsersMapper { int insert(Users user); } <mapper namespace="com.zjs.mapper.UsersMapper"> <insert id="insert" parameterType="users"> insert into users values (#{userId}, #{userName}, #{userPass}) </insert> </mapper>
-
添加service包,添加UsersService接口和UsersServiceImpl实现类
// com.zjs.service public interface UsersService { // 增加用户 int insert(Users user); } // com.zjs.service.impl @Service // 交给容器创建对象 public class UsersServiceImpl implements UsersService { // 所有业务逻辑层一定有逻辑访问层对象:以前是要得到sqlSession对象,调用getMapper方法,现在只要一个注解 @Autowired // 在Bean工厂找同源类型进行注入 UsersMapper usersMapper; public int insert(Users user) { return usersMapper.insert(user); } }
从下到上进行开发:pojo层 > dao层(mapper)> service层 ==> controller层
-
添加测试类进行功能测试
@Test public void testInsert() { ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext_service.xml"); // ac.getBeanDefinitionNames(); // 获取spring所创建的所有对象的名称 // 取出 UsersServiceImpl:接口指向实现类。这里的 usersService 是 Proxy类型(代理) UsersService usersService = (UsersService) ac.getBean("usersServiceImpl"); // SM整合后事务是自动提交的,所以增删改操作不需要手动commit int num = usersService.insert(new Users(100, "张三", "123")); System.out.println(num); }
补充:spring的事务处理(理论)
-
什么是事务?
事务是指一组sql语句的集合。集合中有多条sql语句, 我们希望这些语句能够都成功,否则的话就都失败。这些sql语句的执行是一致的,作为一个整体执行
-
什么时候使用事务?
当我的操作,涉及到多个表,或者是多个sql语句的insert,update,delete。需要保证这些语句都是成功才能完成我的功能,或者都失败。保证操作是符合要求的
-
在java代码中写程序,控制事务,此时事务应该放在哪里呢?
service类的业务方法上,因为业务方法会调用多个dao方法,执行多个sql语句
-
通常使用jdbc访问数据库,还是mybatis访问数据库,怎么处理事务?
jdbc访问数据库,处理事务: Connection conn; conn.commit(); conn.rollback();
mybatis访问数据库,处理事务:SqlSession.commit(); SqlSession.rollback();
hibernate访问数据库,处理事务:Session.commit(); Session.rollback();
-
问题4中事务的处理方式,有什么不足?
- 不同的数据库访问技术,处理事务的对象,方法不同。
- 程序员需要了解不同数据库访问技术使用事务的原理,掌握多种数据库中事务的处理逻辑。什么时候提交事务,什么时候回滚事务,以及处理事务的多种方法
总结:多种数据库访问技术,有不同的事务处理机制,对象,方法。
-
怎么解决不足?
spring提供了一种处理事务的统一模型,能使用统一的步骤,方式完成多种不同数据库访问技术的事务处理
使用spring的事务处理机制,可以完成 mybatis 访问数据库的事务处理,也能完成 hibernate访问数据库的事务处理
-
处理事务,需要怎么做,做什么?
spring处理事务的模型,使用的步骤都是固定的。把事务使用的信息提供给spring就可以了
-
事务内部提交,回滚事务,使用的是事务管理器对象,代替你手动 commit, rollback
事务管理器是一个接口和它的众多实现类
接口:PlatformTransactionManager,定义了事务重要方法,commit,rollback
实现类:spring把每一种数据库访问技术对应的事务处理类都创建好了
mybatis访问数据库 ----- spring创建好的实现类是 DataSourceTransactionManager
hibernate访问数据库 ----- spring创建好的实现类是 HibernateTransactionManager
怎么使用:
你需要告诉spring你用的是那种数据库的访问技术,怎么告诉spring?
==》声明数据库访问技术对应的事务管理器实现类,在spring的配置文件中使用 bean标签 声明即可。
例如要使用mybatis访问数据库,应该在 xml配置文件中配置
-
你的业务方法需要什么样的事务,说明需要的事务的类型
说明方法需要的业务:
-
事务的隔离级别(TransactionDefinition接口中定义):
- READ_UNCOMMITTED:读未提交。未解决任何并发问题
- READ_COMMITTED:读已提交。解决脏读,存在不可重复读 与 幻读
- REPEATABLE_READ:可重复读。解决脏读,不可重复读,存在幻读
- SERIALIZABLE:可串行化。不存在并发问题(加锁)
- DEFAULT:采用 DB 默认的事务隔离级别。mysql默认为 可重复读,oracle默认为 读已提交
-
事务的超时时间:表示一个方法的最长执行时间,如果方法执行时超过了时间,事务就回滚。单位是秒,整数值,默认是 -1
-
事务的传播行为:控制业务方法是不是有事务的,是什么样的事务的
7个传播行为:表示你的业务方法调用的时候,事务在方法之间是如何使用的(前三个必须掌握)
-
PROPAGATION_REQUIRED
若是该传播行为,指定方法必须在事务中执行。
若当前存在事务,就加入到当前事务中;若当前没有事务则创建一个新事务。 spring的默认事务传播行为
-
PROPAGATION_REQUIRES_NEW
总是新建一个事务
若当前存在事务,就将当前事务挂起,直到新事务执行完毕
-
PROPAGATION_SUPPORTS
指定方法支持当前事务,若当前没有事务,也可以以非事务方式执行
-
PROPAGATION_MANDATORY
-
PROPAGATION_NESTED
-
PROPAGATION_NEVER
-
PROPAGATION_NOT_SUPPORTED
-
-
-
提交事务,回滚事务的时机
-
当你的业务方法执行成功,没有异常抛出,当方法执行完毕,spring在方法执行后提交事务。事务管理器commit
-
当你的业务方法抛出运行时异常或ERROR,spring执行回滚,调用事务管理器的rollback
运行时异常的定义:RuntimeException 和它的子类都是运行时异常,例如NullPointException
-
当你的业务方法抛出非运行时异常,主要是受查异常时,提交事务
受查异常:在你写代码中,必须处理的异常,例如 IOException,SQLException
-
总结spring的事务
- 管理事务的是 事务管理器和它的实现类
- spring的事务是一个统一模型
- 指定要使用的事务管理器实现类,使用 bean标签。不同的数据库有不同的事务管理器实现类
- 指定那些类,哪些方法需要加入事务的功能(一般是 service层要是实现事务)
- 指定方法需要的隔离级别,传播行为,超时时间
-
你需要告诉spring,你的项目中类信息,方法的名称,方法的事务的传播行为
spring框架中提供的事务处理方案
-
适合中小项目使用的注解方案(24节)
spring框架自己用 aop是给业务方法增加事务的功能,使用 @Transactional注解增加事务(第24节)。
@Transactional注解是spring框架自己的注解,放在public方法的上面,表示当前方法具有事务,可以给注解的属性赋值,表示具体的隔离级别,传播行为,异常信息等等。也可以放到类上面(用的比较少,表示该类的所有方法都使用事务)
-
使用 @Transactional的步骤
-
在applicationContext_service.xml中声明事务管理器对象
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean>
-
在applicationContext_service.xml中开启事务注解驱动,告诉spring框架,我要使用注解的方式管理事务(对应标题:基于注解的事务)
<tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
spring使用aop机制,创建 @Transactional所在的类代理对象,给方法加入事务的功能。
-
spring给业务方法加入事务:
使用 aop的环绕通知,在你的业务方法执行之前开启事务,在业务方法执行之后提交或回滚事务。
@Around(“你要增加的事务功能的业务方法名称”)
Object myAround(){
开启事务,spring给你开启
try{
buy(1001, 10);
spring的事务管理.commit();
}catch(Exception e){
spring的事务管理.rollback();
}
}
-
-
在你的方法的上面加入 @Transactional
-
-
-
适合大型项目(31节)
有很多的类,方法,需要大量的配置事务,使用 aspectJ框架功能,在spring的配置文件中声明类,方法需要的事务。这种方式业务方法和事务配置完全分离。【**声明式事务:**第31节有详细过程】
-
实现步骤:都是在xml配置文件中实现
-
使用的是 aspectJ框架,需要加入依赖
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>5.2.5.RELEASE</version> </dependency>
-
在applicationContext_service.xml中声明事务管理器对象
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean>
-
声明方法需要的事务类型(配置方法的事务属性【隔离级别,传播行为,超时时长】)
-
配置aop:指定哪些类要创建代理
-
-
-
项目中的所有事务,必须添加到业务逻辑层上
24.基于注解的事务添加步骤
在项目8基础之上补充:
-
在applicationContext_service.xml文件中添加事务管理器,以及事务的注解驱动
<!--声明事务管理器:使用spring的事务处理,声明mybatis的事务管理器--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!--因为事务必须关联数据库处理,所以要配置数据源,项目8中的数据源 --> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 一定要导入以tx结尾的包 开启事务注解驱动,告诉spring使用注解管理事务,创建代理对象 ransaction-manage:事务管理器对象的id --> <tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
-
在业务逻辑的实现类(AccountsServiceImpl)上添加注解(也可以在使用事务的方法上添加注解)
@Transactional(propagation = Propagation.REQUIRED)
REQUIRED表示 增删改操作时必须添加的事务传播特性 propagation:传播
代码实现
在项目8基础上测试 事务
准备工作
//1. pojo下添加,Accounts实体类
public class Accounts {
private Integer aid;
private String aname;
private String acontent;
// 无参,含参构造器,get,set方法,toString
}
// 2.mapper包下:
// AccountsMapper
public interface AccountsMapper {
// 添加账户的功能
int save(Accounts account);
}
// AccountsMapper.xml
<mapper namespace="com.zjs.mapper.AccountsMapper">
<insert id="save" parameterType="accounts">
insert into accounts values(#{aid}, #{aname}, #{acontent})
</insert>
</mapper>
// 3.service包下:
public interface AccountsService {
// 增加账号
int save(Accounts account);
}
// service.impl包下:
@Service
public class AccountsServiceImpl implements AccountsService {
@Autowired
AccountsMapper accountsMapper;
@Transactional(propagation = Propagation.REQUIRED,
//noRollbackForClassName = {"ArithmeticException"}//添加此句后,尽管发生算术异常,也不会撤销数据库中的数据)
public int save(Accounts account) {
int num = 0;
num = accountsMapper.save(account);
// 手动抛出运行时异常,则插入到数据库中的数据必须撤销,即底层会自动回滚
// 如何实现上述功能?==》添加事务,见笔记(添加事务管理器,添加事务的注解驱动,添加注解)
System.out.println(1/0); // 也用于测试第30节内容
System.out.println("增加账户成功!num = " + num);
return num;
}
}
//测试类
@Test
public void testAccounts() {
ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext_service.xml");
AccountsService accountsService = (AccountsService) ac.getBean("accountsServiceImpl");
System.out.println(accountsService.getClass());
accountsService.save(new Accounts(202, "李四2", "账户安全2"));
}
25.@Transactional注解参数详解
@Transactional(propagation = Propagation.REQUIRED, // 事务的传播特性(第29节)
noRollbackForClassName = {"ArithmeticException"}, // 指定发生什么异常不回滚,使用的是异常的名称
noRollbackFor = ArithmeticException.class, // 指定发生什么异常不回滚,使用的是异常的类型
rollbackForClassName = "", // 指定发生什么异常必须回滚(如自定义异常)
rollbackFor = ArithmeticException.class, // 指定发生什么异常必须回滚
timeout = -1, // 连接超时设置,默认值是-1,表示永不超时
readOnly = false, // 默认是false,如果是查询操作,必须设置为true.
isolation = Isolation.DEFAULT // 使用数据库自已的隔离级别
)
rollbackFor:表示发生指定的异常一定回滚
处理逻辑是:
1. spring框架会首先检测方法抛出的异常是不是在rollbackFor的属性值中。如果异常在rollbackFor列表中,不管是什么类型的异常,一定回滚。如上述的ArithmeticException
2. 如果抛出的异常不在rollbackfor列表中,spring会判断异常是不是RuntimeException,如果是一定回滚
26.Spring的两种事务处理方式
在项目8后的补充内容中也有所讲解
-
注解式的事务(24节)
- 使用@Transactional注解完成事务控制
- 此注解可添加到类上,则对类中所有方法执行事务的设定。
- 此注解可添加到方法上,只是对此方法执行事务的处理.
-
声明式事务(必须掌握:31节)
- 在配置文件中添加一次,整个项目遵循事务的设定.
27.Spring中事务的五大隔离级别
在项目8后补充的一节也有讲
-
未提交读(Read Uncommitted):允许脏读,也就是可能读取到其他会话中未提交事务修改的数据
-
提交读(Read Committed):只能读取到已经提交的数据。Oracle等多数数据库默认都是该级别 (不重复读)
-
可重复读(Repeated Read):可重复读。在同一个事务内的查询都是事务开始时刻一致的,InnoDB默认级别。在SQL标准中,该隔离级别消除了不可重复读,但是还存在幻象读,但是innoDB解决了幻读
-
串行读(Serializable):完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞
-
使用数据库默认的隔离级别 isolation = Isolation.DEFAULT
-
MySQL:mysql默认的事务处理级别是’REPEATABLE-READ’,也就是可重复读
-
Oracle:oracle数据库支持READ COMMITTED 和 SERIALIZABLE 这两种事务隔离级别。默认系统事务
隔离级别是READ COMMITTED,也就是读已提交
-
28.为什么添加事务管理器
在项目8后补充的一节也有讲
- JDBC: Connection con.commit(); con.rollback();
- MyBatis: SqlSession sqlSession.commit(); sqlSession.rollback();
- Hibernate: Session session.commit(); session.rollback();
事务管理器用来生成相应数据库连接技术的 连接+执行语句的 对象.
如果使用MyBatis框架,必须使用DataSourceTransactionManager类完成处理
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--因为事务必须关联数据库处理,所以要配置数据源-->
<property name="dataSource" ref="dataSource"></property>
</bean>
项目中的所有事务,必须添加到业务逻辑层上,这点在项目8后的补充内容中有所提及
29.Spring事务的传播特性(见讲义)
项目8后的补充内容也有较为详细的讲解
多个事务之间的合并,互斥等都可以通过设置事务的传播特性来解决.
常用
- PROPAGATION_REQUIRED:必被包含事务(增删改必用)
- PROPAGATION_REQUIRES_NEW:自己新开事务,不管之前是否有事务
- PROPAGATION_SUPPORTS:支持事务,如果加入的方法有事务,则支持事务,如果没有,自己不新开事务
- PROPAGATION_NEVER:不能运行在事务中,如果包在事务中,抛异常
- PROPAGATION_NOT_SUPPORTED:不支持事务,运行在非事务的环境
不常用
- PROPAGATION_MANDATORY:必须包在事务中,没有事务则抛异常
- PROPAGATION_NESTED:嵌套事务
30.测试传播特性改造24节项目8
UserServiceImpl包裹了AccountsServiceImpl
@Service //交给Spring去创建对象
public class UsersServiceImpl implements UsersService {
@Autowired
UsersMapper usersMapper;
@Autowired
AccountsService accountsService;
@Override
@Transactional(propagation = Propagation.REQUIRED)
public int insert(Users users) {
int num = usersMapper.insert(users);
System.out.println("用户增加成功!num="+num);
//调用帐户的增加操作,调用的帐户的业务逻辑层的功能。而这里面有运行时异常,会发生回滚,因为两者的事务传播行为都是 required
num = accountsService.save(new Accounts(300,"王五","帐户好的呢!"));
return num;
}
}
//同下图规律,因为 UsersServiceImpl的事务传播行为是 required, AccountsServiceImpl的事务传播行为也是 requird,所以两个类共享事务。当 AccountsServiceImpl中抛出异常后,表users 和 表accounts 都不能够插入数据
31.声明式事务(spring)
Spring非常有名的事务处理方式:声明式事务.
要求项目中的方法命名有规范
-
完成增加操作包含 add save insert set
-
更新操作包含 update change modify
-
删除操作包含 delete drop remove clear
-
查询操作包含 select find search get
配置事务切面时可以使用通配符*来匹配所有方法
// src\main\resources\applicationContext_trans.xml,test类中容器启动导入的文件也要是该文件
<!-- 此配置文件与 applicationContext_service.xml的功能一样,只是事务配置不同 -->
<!-- 导入applicationContext_mapper.xml文件 -->
<import resource="applicationContext_mapper.xml"></import>
<!-- 包扫描 -->
<context:component-scan base-package="com.zjs.service.impl"></context:component-scan>
<!-- 添加事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--
// 设置优先级:order越大优先级越高,即 基于注解的事务优先级 高于 声明式事务。这样就屏蔽了下面所进行的全局配置,而使用@Transactional注解中所配置的信息
<tx:annotation-driven order=”100“ transaction-manager="transactionManager"></tx:annotation-driven>
-->
<!--
配置事务切面:即声明(需要事务的)业务方法它的事务属性(隔离级别,传播行为,超时时机)。不需要事务的方 法不在这里面进行配置即可。实现了事务配置和源码的剥离
id:自定义名称,表示<tx:advice />标签中中配置的内容
transaction-manager:事务管理器对象的id。事务管理器对象是上面引入的
-->
<tx:advice id="myadvice" transaction-manager="transactionManager">
<!-- tx:attributes:配置事务属性 -->
<tx:attributes>
<!-- tx:method:给不同的方法配置事务属性,没指定的用默认值
name:方法名称,可使用通配符。 *select*表示方法名中带有select的所有方法
propagation:传播行为,枚举值
isoation:隔离级别
rollback-for:指定的异常类名,全限定类名。发生异常一定回滚
-->
<tx:method name="*select*" read-only="true"/> //查询操作readonly必须设置为true,见25节。查询操作不需要事务所以不用配置 propagation
<tx:method name="*find*" read-only="true"/>
<tx:method name="*search*" read-only="true"/>
<tx:method name="*get*" read-only="true"/>
<tx:method name="*insert*" propagation="REQUIRED" no-rollback-for="ArithmeticException"/>
<tx:method name="*add*" propagation="REQUIRED"/>
<tx:method name="*save*" propagation="REQUIRED" no-rollback-for="ArithmeticException"/>
<tx:method name="*set*" propagation="REQUIRED"/>
<tx:method name="*update*" propagation="REQUIRED"/>
<tx:method name="*change*" propagation="REQUIRED"/>
<tx:method name="*modify*" propagation="REQUIRED"/>
<tx:method name="*delete*" propagation="REQUIRED"/>
<tx:method name="*remove*" propagation="REQUIRED"/>
<tx:method name="*drop*" propagation="REQUIRED"/>
<tx:method name="*clear*" propagation="REQUIRED"/>
<!-- 除以上方法之外的所有方法的事务属性 -->
<tx:method name="*" propagation="SUPPORTS"/>
</tx:attributes>
</tx:advice>
<!-- 绑定切面和切入点:-->
<aop:config>
<!-- 配置切入点表达式(起别名):指定哪些包中的类要使用事务(一般都在service层中使用事务)
id:切入点表达式的名称,唯一值
expression:切入点表达式,指定那些类要使用事务,aspectj会创建代理对象
表明 所有包的service.impl子包下所有类中所有方法都满足如上事务的配置。
这里也可以写成 com.zjs.service.impl.*.*(..)
-->
<aop:pointcut id="mycut" expression="execution(* *..service.impl.*.*(..))"></aop:pointcut>
<!-- 配置增强器:关联advice和pointcut
advice-ref:通知,<tx:advice /> 中配置的内容,即其id值
pointcut-ref:切入点表达式的id
-->
<aop:advisor order="1" advice-ref="myadvice" pointcut-ref="mycut"></aop:advisor>
</aop:config>
<tx:advice />标签显得冗余是因为荣姐把所有的全配置了,在目前做的玩具项目中,不需要进行如此细致的配置。name使用具体的方法名即可,也不用配置那么多
补充:SM + servlet
按照学习顺序:servlet + jsp ===》 mybatis ==》 spring,我们现在就可以通过以上技术创建web项目,到了后期学了springmvc框架,就可以将servlet+jsp替换掉,SSM框架集合完毕
web项目中如何使用容器对象?
-
做的是javase项目有main方法的,执行代码是执行main方法的,在main里面创建的容器对象
ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
-
web项目是在tomcat服务器上运行的,tomcat一启动,项目一直运行的。(web项目的main方法在tomcat中)
需求:
web项目中 容器对象ac 只需要创建一次,把容器对象ac 放入到全局作用域 ServletContext中
如何实现:
使用监听器,当全局作用域对象被创建时,创建容器对象,并存入ServletContext
监听器作用:
-
创建容器对象:
ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
-
把容器对象放入到ServletContext域中:
ServletContext.setAttribute(key, ac);
监听器对象可以自己创建,也可以使用框架中提供好的 ContextLoaderListener
ContextLoaderListener底层实现:
-
创建容器对象:
-
它有一个变量:private WebApplicationContext context;
context = this.createWebApplicationContext(servletContext); public interface WebApplicationContext extends ApplicationContext
ApplicationContext:javase项目中使用的容器对象
WebApplicationContext:web项目中使用的容器对象
-
-
将容器对象放到 ServletContext域中
-
// key: WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE // value: this.context servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context); String ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE = WebApplicationContext.class.getName() + ".ROOT";
-
项目代码
需求:使用 项目8中的 users表 springuser.sql,完成用户从jsp中提交数据,然后将数据保存到 mysql数据库中 的功能,并且将数据显示到页面(该项目并没有使用到事务)
步骤:
-
通过maven创建web项目
-
修改目录,将web.xml和index.jsp删除并新建。添加三层目录结构(pojo,mapper,service,controller)
-
在pom.xml文件中加入依赖, Spring+MyBatis依赖(mysql驱动使用mysql8,要修改)
因为使用到 servlet 和 jsp,所以pom.xml文件中还需要加入 servlet 和 jsp的 依赖
<dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> </dependency> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>javax.servlet.jsp-api</artifactId> <version>2.3.3</version> </dependency>
-
参见第23节项目8,进行如下的配置(代码一点没变,赋值粘贴即可)
- 拷贝jdbc.properties属性文件(jdbc.properties 改 driver 和 密码)
- 添加mybatis 的主配置文件 SqlMapConfig.xml
- 添加 applicationContext_mapper.xml,让spring创建 mybtis需要的对象
- 添加applicationContext_service.xml,导入mapper.xml,以及包扫描,和事务相关的配置
- 创建实体类 Users
- 添加mapper包,添加UsersMapper接口和UsersMapper.xml文件并开发(DAO)
- 添加service包,添加UsersService接口和UsersServiceImpl实现类
-
在index.jsp中进行数据的提交,result.jsp中显示添加成功
index.jsp: <p>注册用户</p> <form action="${pageContext.request.contextPath}/reg" method="post"> <table> <tr> <td>id</td> <td><input type="text" name="id"></td> </tr> <tr> <td>name</td> <td><input type="text" name="name"></td> </tr> <tr> <td>password</td> <td><input type="password" name="password"></td> </tr> <tr> <td></td> <td><input type="submit" value="注册用户"></td> </tr> </table> </form> result.jsp: <h2>result.jsp注册成功</h2>
-
在controller包中编写servlet,并在web.xml文件中注册该servlet
public class RegisterServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String id = request.getParameter("id"); String name = request.getParameter("name"); String password = request.getParameter("password"); // 创建spring的容器对象 ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext_service.xml"); System.out.println("容器的信息:" + ac); UsersService usersServiceImpl = (UsersService) ac.getBean("usersServiceImpl"); Users user = new Users(); user.setUserId(Integer.parseInt(id)); user.setUserName(name); user.setUserPass(password); usersServiceImpl.insert(user); request.getRequestDispatcher("/result.jsp").forward(request, response); } }
web.xml <servlet> <servlet-name>RegisterServlet</servlet-name> <servlet-class>com.zjs.controller.RegisterServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>RegisterServlet</servlet-name> <url-pattern>/reg</url-pattern> </servlet-mapping>
-
配置tomcat,运行程序
整个的流程到这基本上已经通了,但是有一个问题:在RegisterServlet中,spring的容器对象ClassPathXmlApplicationContext是我们手动创建的,我们每提交一次表单,就会新建一个 spring的容器对象,这样太浪费资源。spring的容器对象只需要创建一次,把其放到全局作用域 ServletContext中即可
为此,我们需要使用 监听器,让其为我们创建spring 的容器对象【还需要配置过滤器,不然会有中文乱码,可以看springmvc的相关部分】
为了添加监听器,步骤如下:
-
添加依赖
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>5.2.5.RELEASE</version> </dependency>
-
在 web.xml中注册监听器:ContextLoaderListener
<!-- 注册监听器: 配置监听器:目的是创建容器对象,创建了容器对象,就能把 applicationContext.xml配置文件中的 所有对象都创建好,用户发起请求就可以直接使用对象l 监听器被创建对象后,会读取 /WEB-INF/applicationContext.xml配置文件 为什么要读取文件:因为在监听器中要创建 ApplicationContext对象,需要加载配置文件。 /WEB-INF/applicationContext.xml就是监听器默认读取的sping配置文 件路径 可以修改默认的文件位置:使用 context-param重新指定文件的位置 一个小问题: 1. 报错: Could not open ServletContext resource [/jdbc.properties] 出错原因: 在applicationContext_mapper.xml中加载配置文件时: <context:property-placeholder location="jdbc.properties"/> 要将其改为: <context:property-placeholder location="classpath:jdbc.properties"/> 2. 报错:Could not open ServletContext resource [/SqlMapConfig.xml] 出错原因: 在applicationContext_mapper.xml中配置 SqlSessionFactoryBean时: <property name="configLocation" value="SqlMapConfig.xml"/> 要将其改为: <property name="configLocation" value="classpath:SqlMapConfig.xml"/> 原因:ClassPath指定的是java加载类的路径。只有类在ClassPath中,java命令才能找到它,并解释它。 --> <context-param> <!-- contextConfigLocation:表示配置文件的路径 --> <param-name>contextConfigLocation</param-name> <!-- 自定义配置文件的路径:service.xml中导入了 mapper.xml --> <param-value>classpath:applicationContext_service.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener>
修改servlet中的代码,获取ServletContext域中的容器对象:
WebApplicationContext ac = null;
//1. 获取ServletContext中的容器对象,创建好的容器对象,拿来就用
String key = " WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE";
Object attr = getServletContext().getAttribute(key);
if (attr != null) {
ac = (WebApplicationContext) attr;
}
//2. 使用框架中的方法,获取容器对象
ServletContext sc = getServletContext();
ac = WebApplicationContextUtils.getRequiredWebApplicationContext(sc);
补充:在web.xml中配置中文乱码过滤器(使用框架中提供好的:CharacterEncodingFilter)
<filter>
<filter-name>encode</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceRequestEncoding</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>forceResponseEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encode</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
user.setUserId(Integer.parseInt(id));
user.setUserName(name);
user.setUserPass(password);
usersServiceImpl.insert(user);
request.getRequestDispatcher("/result.jsp").forward(request, response);
}
}
```xml
web.xml
<servlet>
<servlet-name>RegisterServlet</servlet-name>
<servlet-class>com.zjs.controller.RegisterServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>RegisterServlet</servlet-name>
<url-pattern>/reg</url-pattern>
</servlet-mapping>
- 配置tomcat,运行程序
整个的流程到这基本上已经通了,但是有一个问题:在RegisterServlet中,spring的容器对象ClassPathXmlApplicationContext是我们手动创建的,我们每提交一次表单,就会新建一个 spring的容器对象,这样太浪费资源。spring的容器对象只需要创建一次,把其放到全局作用域 ServletContext中即可
为此,我们需要使用 监听器,让其为我们创建spring 的容器对象【还需要配置过滤器,不然会有中文乱码,可以看springmvc的相关部分】
为了添加监听器,步骤如下:
-
添加依赖
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>5.2.5.RELEASE</version> </dependency>
-
在 web.xml中注册监听器:ContextLoaderListener
<!-- 注册监听器: 配置监听器:目的是创建容器对象,创建了容器对象,就能把 applicationContext.xml配置文件中的 所有对象都创建好,用户发起请求就可以直接使用对象l 监听器被创建对象后,会读取 /WEB-INF/applicationContext.xml配置文件 为什么要读取文件:因为在监听器中要创建 ApplicationContext对象,需要加载配置文件。 /WEB-INF/applicationContext.xml就是监听器默认读取的sping配置文 件路径 可以修改默认的文件位置:使用 context-param重新指定文件的位置 一个小问题: 1. 报错: Could not open ServletContext resource [/jdbc.properties] 出错原因: 在applicationContext_mapper.xml中加载配置文件时: <context:property-placeholder location="jdbc.properties"/> 要将其改为: <context:property-placeholder location="classpath:jdbc.properties"/> 2. 报错:Could not open ServletContext resource [/SqlMapConfig.xml] 出错原因: 在applicationContext_mapper.xml中配置 SqlSessionFactoryBean时: <property name="configLocation" value="SqlMapConfig.xml"/> 要将其改为: <property name="configLocation" value="classpath:SqlMapConfig.xml"/> 原因:ClassPath指定的是java加载类的路径。只有类在ClassPath中,java命令才能找到它,并解释它。 --> <context-param> <!-- contextConfigLocation:表示配置文件的路径 --> <param-name>contextConfigLocation</param-name> <!-- 自定义配置文件的路径:service.xml中导入了 mapper.xml --> <param-value>classpath:applicationContext_service.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener>
修改servlet中的代码,获取ServletContext域中的容器对象:
WebApplicationContext ac = null;
//1. 获取ServletContext中的容器对象,创建好的容器对象,拿来就用
String key = " WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE";
Object attr = getServletContext().getAttribute(key);
if (attr != null) {
ac = (WebApplicationContext) attr;
}
//2. 使用框架中的方法,获取容器对象
ServletContext sc = getServletContext();
ac = WebApplicationContextUtils.getRequiredWebApplicationContext(sc);
补充:在web.xml中配置中文乱码过滤器(使用框架中提供好的:CharacterEncodingFilter)
<filter>
<filter-name>encode</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceRequestEncoding</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>forceResponseEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encode</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>