➢Spring全家桶:Spring、SpringMvc、SpringBoot、SpringCloud ➢Spring主要作用:为代码“解耦”,降低代码间的耦合度。 ➢Spring核心技术:IoC、AOP。 IoC:使用IoC降低业务对象之间耦合度。IoC使业务在相互调用过程中,不用在自己维护关系,即不用再自己创建所需要的对象,而是由Spring容器统一管理,自动“注入”。 AOP:使系统级服务得到了最大服用,且不用在由程序员手动将系统及服务“混杂”到业务逻辑中,由Spring容器统一完成。 Spring的主要作用就是为代码“解耦”,降低代码间的耦合度。就是让对象和对象(模块和模块)之间关系不是使用代码关联,而是通过配置来说明。即在Spring中说明对象(模块)的关系。 Spring根据代码的功能特点,使用Ioc降低业务对象之间耦合度。IoC使得主业务在相互调用过程中,不用再自己维护关系了,即不用再自己创建要使用的对象了。而是由Spring容器统一管理,自动“注入”,注入即赋值。而AOP使得系统级服务得到了最大复用,且不用再由程序员手工将系统级服务“混杂”到主业务逻辑中了,而是由Spring容器统一完成“织入”。 ➢Spring官网:https://spring.io
➢轻量:Spring的所需要的jar包都非常小,一般1M以下,几百kb。核心功能所需要的jar包总共3M左右。 Spring框架运行占有资源少,运行效率高,不依赖其他jar。 ➢针对接口编程,解耦合 :Spring 提供了 Ioc 控制反转,由容器管理对象,对象的依赖关系。原来在程序代码中的 对象创建方式,现在由容器完成。对象之间的依赖解耦合。 ➢AOP 编程的支持 :通过 Spring 提供的 AOP 功能,方便进行面向切面的编程,许多不容易用传统 OOP 实现 的功能可以通过 AOP 轻松应付 在 Spring 中,开发人员可以从繁杂的事务管理代码中解脱出来,通过声明式方式灵活地 进行事务的管理,提高开发效率和质量。 ➢方便集成各种优秀框架 :Spring 不排斥各种优秀的开源框架,相反 Spring 可以降低各种框架的使用难度,Spring 提供了对各种优秀框架(如 Struts,Hibernate、MyBatis)等的直接支持。简化框架的使用。 Spring 像插线板一样,其他框架是插头,可以容易的组合到一起。需要使用哪个框架,就把 这个插头放入插线板。不需要可以轻易的移除。
控制反转(IoC,Inversion of Control),是一个概念,是一种思想。指将传统上由程序代 码直接操控的对象调用权交给容器,通过容器来实现对象的装配和管理。控制反转就是对对 象控制权的转移,从程序代码本身反转到了外部容器。通过容器实现对象的创建,属性赋值, 依赖的管理。
➢依赖注入:DI(Dependency Injection),程序的代码不做定位查询,这些工作由容器自行完成。 依赖注入: DI 是指程序运行过程中,若需要调用另一个对象协助时,无须在代码中创建 被调用者,而是依赖于外部容器,由外部容器创建后传递给程序。 Spring 的依赖注入对调用者与被调用者几乎没有任何要求,完全支持对象之间依赖关系 的管理。 Spring 框架使用依赖注入(DI)实现 IoC。 Spring 容器是一个超级大工厂,负责创建、管理所有的 Java 对象,这些 Java 对象被称 为 Bean。Spring 容器管理着容器中 Bean 之间的依赖关系,Spring 使用“依赖注入”的方式 来管理 Bean 之间的依赖关系。使用 IoC 实现对象之间的解耦和。
在 src/main/resources/目录现创建一个 xml 文件,文件名可以随意,但 Spring 建议的名称为 applicationContext.xml。 spring 配置中需要加入约束文件才能正常使用,约束文件是 xsd 扩展名。
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="someService" class="com.bjpowernode.service.impl.SomeServiceImpl"/> <bean id="someService1" class="com.bjpowernode.service.impl.SomeServiceImpl"/> </beans>< beans/ >: 用于定义一个实例对象。一个实例对应一个 bean 元素。 id:该属性是 Bean 实例的唯一标识,程序通过 id 属性访问 Bean,Bean 与 Bean 间的依 赖关系也是通过 id 属性关联的。 class:指定该 Bean 所属的类,注意这里只能是类,不能是接口。
spring 配置文件加入 java.util.Date 定义:
<bean id="myDate" class="java.util.Date" />MyTest 测试类中: 调用 getBean(“myDate”); 获取日期类对象。
@Test public void test04(){ String config = "beans.xml"; ApplicationContext applicationContext = new ClassPathXmlApplicationContext(config); //使用getBean() Date date = (Date) applicationContext.getBean("mydate"); System.out.println("时间:" + date); }输出结果:
时间:Fri Sep 04 14:07:01 CST 2020ApplicationContext接口(容器) ApplicationContext用于加载Spring的配置文件,在程序中充当“容器”的角色。其实现类由两个: ➢ClassPathXmlApplicationContext ➢FileSystemXmlApplicationContext
ClassPathXmlApplicationContext:配置文件放在项目的类路径下,使用该类进行加载 FileSystemXmlApplicationContext:配置文件在系统文件夹下,使用该类进行加载 ApplicationContext容器中对象的装配时机 ApplicationContext容器,会在容器对象初始化时,将其中的所有对象一次性全部装配好。以后代码中若要使用到这些对象,只需要从内存中直接获取即可,执行效率较高,但占用内存较大。 spring创建对象:默认调用的是无参数的构造方法 使用spring容器创建的java对象
di的实现有两种: 1.在Spring的配置文件中,使用标签和属性完成,叫做基于XML的di实现 2.使用Spring中的注解,完成属性赋值,叫做基于注解的di实现。
bean示例在调用无参构造器创建对象后,就要对bean对象的属性进行初始化。初始化是由容器自动完成的,称为注入。 根据注入的方式不同,常用的由两类:set注入、构造注入
set注入也叫设值注入,是指:通过setter方法传入被调用者的实例。这种注入方式简单、直观。因而在Spring的依赖注入中大量使用。
1.创建相关类
public class Student { private String name; private int age; public void setName(String name) { this.name = name; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } }2.设置配置文件
<bean id="myStudent" class="com.bjpowernode.ba01.Student"> <property name="name" value="李四"/> <property name="age" value="20"/> </bean>3.测试
@Test public void test01(){ String config = "ba01/applicationContext.xml"; ApplicationContext context = new ClassPathXmlApplicationContext(config); Student myStudent = (Student) context.getBean("myStudent"); System.out.println(myStudent); } }结果
Student{name='李四', age=20}当指定某属性值为另以bean的实例时,通过ref指定他们之间的引用关系。ref的值必须为某bean的id值。
<bean id="myStudent" class="com.bjpowernode.ba02.Student"> <property name="name" value="李四"/> <property name="age" value="26"/> <property name="school" ref="mySchool"/> </bean> <bean id="mySchool" class="com.bjpowernode.ba02.School"> <property name="name" value="北京大兴"/> <property name="address" value="动力节点"/> </bean> @Test public void test02(){ String config = "ba02/applicationContext.xml"; ApplicationContext context = new ClassPathXmlApplicationContext(config); Student student = (Student) context.getBean("myStudent"); System.out.println(student); } Student{school=School{name='北京大兴', address='动力节点'}, name='李四', age=26}构造注入是指:在构造调用者实例的同时,完成被调用者的实例化。即,使用构造器设置依赖关系
//有参构造方法 public Student(String name, int age, School school) { this.name = name; this.age = age; this.school = school; } <bean id="myStudent" class="com.bjpowernode.ba03.Student"> <constructor-arg name="name" value="李四"/> <constructor-arg name="age" value="25"/> <constructor-arg name="school" value="mySchool"/> </bean> <bean id="mySchool" class="com.bjpowernode.ba03.School"> <property name="name" value="动力节点"/> <property name="address" value="北京大兴"/> </bean><constructor-arg />标签中用于指定参数的属性有: ➢name:指定参数名称。 ➢index:指明该参数对应着构造器的第几个参数,从 0 开始。不过,该属性不要也行, 但要注意,若参数类型相同,或之间有包含关系,则需要保证赋值顺序要与构造器中的参数 顺序一致。 ➢➢value:是指构造方法的形参类型是简单类型的,使用value ➢➢ref:构造方法的形参类型是引用类型的,使用ref
@Test public void test02(){ String config = "ba03/applicationContext.xml"; ApplicationContext context = new ClassPathXmlApplicationContext(config); Student student = (Student) context.getBean("myStudent"); System.out.println(student); } Student{school=School{name='动力节点', address='北京大兴'}, name='李四', age=25}对于引用类型属性的注入,也可不在配置文件中显示的注入。可以通过为标签 设置 autowire 属性值,为引用类型属性进行隐式自动注入(默认是不自动注入引用类型属 性)。根据自动注入判断标准的不同,可以分为两种: byName:根据名称自动注入 byType: 根据类型自动注入
当配置文件中被调用者 bean 的 id 值与代码中调用者 bean 类的属性名相同时,可使用 byName 方式,让容器自动将被调用者 bean 注入给调用者 bean。容器是通过调用者的 bean 类的属性名与配置文件的被调用者 bean 的 id 进行比较而实现自动注入的。 java类中引用类型的属性名和spring容器中(配置文件)的id名称一样,且数据类型是一致的,这样的容器中的bean,spring能够赋值给引用类型。 语法:<bean id=“xx” class=“yyy” autowire=“byName”>简单类型属性赋值</bean>
<bean id="myStudent" class="com.bjpowernode.ba04.Student" autowire="byName"> <property name="name" value="李四" /> <property name="age" value="26" /> <!--引用类型--> <!--<property name="school" ref="mySchool" />--> </bean> <!--声明School对象--> <bean id="school" class="com.bjpowernode.ba04.School"> <property name="name" value="清华大学"/> <property name="address" value="北京的海淀区" /> </bean> @Test public void test01(){ String config="ba04/applicationContext.xml"; ApplicationContext ac = new ClassPathXmlApplicationContext(config); //从容器中获取Student对象 Student myStudent = (Student) ac.getBean("myStudent"); System.out.println("student对象="+myStudent); } student对象=Student{name='李四', age=26, school=School{name='清华大学', address='北京的海淀区'}}使用 byType 方式自动注入,要求:配置文件中被调用者 bean 的 class 属性指定的类, 要与代码中调用者 bean 类的某引用类型属性类型同源。即要么相同,要么有 is-a 关系(子 类,或是实现类)。但这样的同源的被调用 bean 只能有一个。多于一个,容器就不知该匹配 哪一个了。 java类中引用类型的数据类型和spring容器中(配置文件)的class属性 是同源关系的,这样的bean能够赋值给引用类型 同源就是一类的意思: 1.java类中引用类型的数据类型和bean的class的值是一样的。 2.java类中引用类型的数据类型和bean的class的值父子类关系的。 3.java类中引用类型的数据类型和bean的class的值接口和实现类关系的 语法:<bean id=“xx” class=“yyy” autowire=“byType”>简单类型属性赋值</bean> 注意:在byType中,在xml配置文件中声明bean只能有一个符合条件的。
<!--byType--> <bean id="myStudent" class="com.bjpowernode.ba05.Student" autowire="byType"> <property name="name" value="张飒" /> <property name="age" value="26" /> <!--引用类型--> <!--<property name="school" ref="mySchool" />--> </bean> <!--声明School对象--> <bean id="mySchool" class="com.bjpowernode.ba05.School"> <property name="name" value="人民大学"/> <property name="address" value="北京的海淀区" /> </bean> @Test public void test01(){ String config="ba05/applicationContext.xml"; ApplicationContext ac = new ClassPathXmlApplicationContext(config); //从容器中获取Student对象 Student myStudent = (Student) ac.getBean("myStudent"); System.out.println("student对象="+myStudent); } student对象=Student{name='张飒', age=26, school=School{name='人民大学', address='北京的海淀区'}&emslp;在实际应用里,随着应用规模的增加,系统中 Bean 数量也大量增加,导致配置文件变得非常庞大、臃肿。为了避免这种情况的产生,提高配置文件的可读性与可维护性,可以将 Spring 配置文件分解成多个配置文件。 包含关系的配置文件:
多个配置文件中有一个总文件,总配置文件将各其它子文件通过<import/>引入。在 Java 代码中只需要使用总配置文件对容器进行初始化即可。 语法: <import resource="其它配置文件的路径"/>关键字:"classpath:"表示类路径(class文件所在的目录)
在spring的配置文件中需要指定其他文件的位置,不要使用classpath,告诉spring到哪里去加载读取文件 <!--加载的是文件列表--> <import resource="classpath:ba06/spring-school.xml" /> <import resource="classpath:ba06/spring-student.xml" /> @Test public void test01(){ //加载的是总的文件 String config= "ba06/total.xml"; ApplicationContext ac = new ClassPathXmlApplicationContext(config); //从容器中获取Student对象 Student myStudent = (Student) ac.getBean("myStudent"); System.out.println("student对象="+myStudent); } student对象=Student{name='张飒', age=30, school=School{name='航空大学', address='北京的海淀区'}}在包含关系的配置文件中,可以使用通配符(∗:表示任意字符)。但,此时要求父配置文件名不能满足∗所能匹配的格式,否则将出现 循环递归包含。就本例而言,父配置文件不能匹配 spring-*.xml 的格式,即不能起名为 spring-total.xml。
<import resource="classpath:ba06/spring-*.xml" />对于 DI 使用注解,将不再需要在 Spring 配置文件中声明 bean 实例。Spring 中使用注解, 需要在原有 Spring 运行环境基础上再做一些改变。 需要在 Spring 配置文件中配置组件扫描器,用于在指定的基本包中扫描注解。 通过注解完成java对象的创建,属性赋值。 使用注解的步骤: 1.加入maven的依赖:spring-context,在加入spring-context的同时,也会加入spring-aop的依赖,使用注解必须使用spring-aop依赖
<!--spring依赖--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.5.RELEASE</version> </dependency>2.在类中加入spring的注解(多个不同功能的注解) 3.在spring的配置文件中,加入一个组件扫描器的标签,说明注解在你的项目中的位置。
声明组件扫描器(component-scan),组件就是java对象 base-package:指定注解在你的项目中的包名。 component-scan工作方式: spring会扫描遍历base-package指定的包,把包中和子包中的所有类,找到类中的注解,按照注解的功能创建对象,或给属性赋值。 加入了component-scan标签,配置文件的变化: 1.加入一个新的约束文件spring-context.xsd 2.给这个新的约束文件起个命名空间的名称
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <context:component-scan base-package="com.bjpowernode.ba02" />注解分类: 1.@Component @Component: 创建对象的, 等同于<bean>的功能 属性:value 就是对象的名称,也就是bean的id值,value的值是唯一的,创建的对象在整个spring容器中就一个 位置:在类的上面 @Component(value = “myStudent”)等同于<bean id=“myStudent” class=“com.bjpowernode.ba01.Student” /> spring中和@Component功能一致,创建对象的注解还有: 1.@Repository(用在持久层类的上面) : 放在dao的实现类上面,表示创建dao对象,dao对象是能访问数据库的。 2.@Service(用在业务层类的上面):放在service的实现类上面,创建service对象,service对象是做业务处理,可以有事务等功能的。 3.@Controller(用在控制器的上面):放在控制器(处理器)类的上面,创建控制器对象的,控制器对象,能够接受用户提交的参数,显示请求的处理结果。 以上三个注解的使用语法和@Component一样的。 都能创建对象,但是这三个注解还有额外的功能.@Repository,@Service,@Controller是给项目的对象分层的。 2.@Respotory 3.@Service 4.@Controller 5.@Value 6.@Autowired 7.@Resource
需要在类上使用注解@Component,该注解的 value 属性用于指定该 bean 的 id 值。
@Component: 创建对象的, 等同于<bean>的功能
属性:value 就是对象的名称,也就是bean的id值,value的值是唯一的,创建的对象在整个spring容器中就一个
@Component(value = “myStudent”)等同于<bean id=“myStudent” class=“com.bjpowernode.ba01.Student” /> @Component 不指定 value 属性,bean 的 id 是类名的首字母小写。
@Component("myStudent") //不指定对象名称,由spring提供默认名称: 类名的首字母小写 //@Component public class Student { private String name; private Integer age; public Student() { System.out.println("==student无参数构造方法==="); } public void setName(String name) { this.name = name; } public void setAge(Integer age) { this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } }另外,Spring 还提供了 3 个创建对象的注解: ➢ @Repository 用于对 DAO 实现类进行注解 ➢ @Service 用于对 Service 实现类进行注解 ➢ @Controller 用于对 Controller 实现类进行注解 这三个注解与@Component 都可以创建对象,但这三个注解还有其他的含义,@Service 创建业务层对象,业务层对象可以加入事务功能,@Controller 注解创建的对象可以作为处 理器接收用户的请求。 @Repository,@Service,@Controller 是对@Component 注解的细化,标注不同层的对 象。即持久层对象,业务层对象,控制层对象。 指定多个包的三种方式: 1)使用多个context:component-scan指定不同的包路径
<!--第一种方式:使用多次组件扫描器,指定不同的包--> <context:component-scan base-package="com.bjpowernode.ba01"/> <context:component-scan base-package="com.bjpowernode.ba02"/>2)指定base-package的值使用分隔符 分隔符可以使用逗号(,)分号(;)还可以使用空格,不建议使用空格。
<!--第二种方式:使用分隔符(;或,)分隔多个包名--> <context:component-scan base-package="com.bjpowernode.ba01;com.bjpowernode.ba02" />3)base-package是指定到父包名 base-package的值表是基本包,容器启动会扫描包及其子包中的注解,当然也会扫描到子包下级的子包。所以base-package可以指定一个父包就可以。
<!--第三种方式:指定父包--> <context:component-scan base-package="com.bjpowernode" />或者最顶级的父包
<!--第三种方式:指定父包--> <context:component-scan base-package="com" />但不建议使用顶级的父包,扫描的路径比较多,导致容器启动时间变慢。指定到目标包和合适的。也就是注解所在包全路径。例如注解的类在com.bjpowernode.beans包中
<!--第三种方式:指定父包--> <context:component-scan base-package="com.bjpowernode.beans" />需要在属性上使用注解@Value,该注解的value属性用于指定要注入的值。使用该注解完成属性注入时,类中无需setter。当然,若属性有setter,则也可将其加到setter上。
@Component("myStudent") public class Student { /** * @Value: 简单类型的属性赋值 * 属性: value 是String类型的,表示简单类型的属性值 * 位置: 1.在属性定义的上面,无需set方法,推荐使用。 * 2.在set方法的上面 */ //@Value("李四" ) @Value("${myname}") //使用属性配置文件中的数据 private String name; @Value("${myage}") //使用属性配置文件中的数据 private Integer age;需要在引用属性上使用注解@Autowired,该注解默认使用按类型自动装配Bean的方式。使用该注解完成属性注入时,类中无需setter。当然,若属性有setter,则也可将其加到setter上。 spring中通过注解给引用类型赋值,使用的是自动注入原理 ,支持byName, byType。 @Autowired:默认使用的是byType自动注入。
@Component("myStudent") public class Student { @Value("李四" ) private String name; private Integer age; /** * 引用类型 * @Autowired: spring框架提供的注解,实现引用类型的赋值。 * spring中通过注解给引用类型赋值,使用的是自动注入原理 ,支持byName, byType * @Autowired:默认使用的是byType自动注入。 * * 位置:1)在属性定义的上面,无需set方法, 推荐使用 * 2)在set方法的上面 */ @Autowired private School school; public Student() { System.out.println("==student无参数构造方法==="); } public void setName(String name) { this.name = name; } @Value("30") public void setAge(Integer age) { System.out.println("setAge:"+age); this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + ", school=" + school + '}'; } } @Component("mySchool") public class School { @Value("北京大学") private String name; @Value("北京的海淀区") private String address; 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 + '\'' + '}'; } } public class MyTest03 { @Test public void test01(){ String config="applicationContext.xml"; ApplicationContext ctx = new ClassPathXmlApplicationContext(config); //从容器中获取对象 Student student = (Student) ctx.getBean("myStudent"); System.out.println("student="+student); } }结果
student=Student{name='李四', age=30, school=School{name='北京大学', address='北京的海淀区'}}需要在引用属性上联合使用注解@Autowired与@Qualifier。@Qualifier的value属性用于指定要匹配的Bean的id值。类中无需set方法,也可加到set方法上。
@Component("myStudent") public class Student { @Value("李四" ) private String name; private Integer age; /** * 引用类型 * @Autowired: spring框架提供的注解,实现引用类型的赋值。 * spring中通过注解给引用类型赋值,使用的是自动注入原理 ,支持byName, byType * @Autowired:默认使用的是byType自动注入。 * 位置:1)在属性定义的上面,无需set方法, 推荐使用 * 2)在set方法的上面 * 如果要使用byName方式,需要做的是: * 1.在属性上面加入@Autowired * 2.在属性上面加入@Qualifier(value="bean的id") :表示使用指定名称的bean完成赋值。 */ //byName自动注入 @Autowired @Qualifier("mySchool") private School school; public Student() { System.out.println("==student无参数构造方法==="); } public void setName(String name) { this.name = name; } @Value("30") public void setAge(Integer age) { System.out.println("setAge:"+age); this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + ", school=" + school + '}'; } }@Autowired还有一个属性required,默认值为true,表示当匹配失败后,会终止程序运行。若将其值设置为false,则匹配失败,将被忽略,未匹配的属性值为null。
//byName自动注入 @Autowired(required = false) @Qualifier("mySchool") private School school;Spring提供了对jdk中@Resource注解的支持。@Resource注解既可以按名称匹配Bean,也可以按类型匹配Bean。默认是按名称注入。使用该注解,要求JDK必须是6及以上版本。@Resource可在属性上,也可在set方法上。
@Resource注解若不带任何参数,采用默认按名称的方式注入,按名称不能注入bean,则会按照类型进行Bean的匹配注入。
@Resource注解指定其name属性,则name的值即为按照名称进行匹配的Bean的id。
@Component("myStudent") public class Student { @Value("李四" ) private String name; private Integer age; /** * 引用类型 * @Resource: 来自jdk中的注解,spring框架提供了对这个注解的功能支持,可以使用它给引用类型赋值 * 使用的也是自动注入原理,支持byName, byType .默认是byName * 位置: 1.在属性定义的上面,无需set方法,推荐使用。 * 2.在set方法的上面 * @Resource只使用byName方式,需要增加一个属性 name * name的值是bean的id(名称) */ //只使用byName @Resource(name = "mySchool") private School school; public Student() { System.out.println("==student无参数构造方法==="); } public void setName(String name) { this.name = name; } @Value("30") public void setAge(Integer age) { System.out.println("setAge:"+age); this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + ", school=" + school + '}'; } }注解优点是: ⚫方便 ⚫直观 ⚫高效(代码少,没有配置文件的书写那么复杂)。 其弊端也显而易见:以硬编码的方式写入到Java代码中,修改是需要重新编译代码的。 XML方式优点是: ⚫配置和代码是分离的 ⚫在xml中做修改,无需编译代码,只需重启服务器即可将新的配置加载。 xml的缺点是:编写麻烦,效率低,大型项目过于复杂。
动态代理是指,程序在整个运行过程中根本就不存在目标类的代理类,目标对象的代理对象只是由代理生成工具(不是真实定义的类)在程序运行时由JVM根据反射等机制动态生成的。代理对象与目标对象的的代理关系在程序运行时才确立
动态代理的实现方式常用的有两种:使用JDK的Proxy,与通过CGLIB生成代理。 jdk的动态代理要求目标对象必须实现接口,这是java设计上的要求。 从jdk1.3以来,java语言通过Java.lang.reflect包提供三个类支持代理模式Proxy,Method和InovationHanler。
CGLIB(Code Generation Library)是一个开源项目。是一个强大的,高性能的,高质量的Code生成类库,它可以在运行期扩展Java类与实现Java接口。它广泛的被许多AOP的框架使用,例如:Spring AOP. 使用JDK的Proxy实现代理,要求目标与代理类实现相同的接口。若目标类不存在接口,则无法使用该方式实现。但对于无接口的类,要为其创建动态代理,就要使用CGLIB来实现。 CGLIB代理的生成原理是生成目标类的子类,而子类是增强过的,这个子类对象就是代理对象。所以,使用CGLIB生成动态代理,要求目标必须能够被继承,既不能是final的类。 CGLIB经常被应用在框架中,例如:
Step1:项目aop_leadin1 先定义好接口与一个实现类,该实现类中除了要实现接口中的方法外,还要再写两个非业务方法。非业务方法也称为交叉业务逻辑: ➢doTransaction():用于事务处理 ➢doLog():用于日志处理然后,再使接口方法调用它们。接口方法也称为主业务逻辑。
public interface SomeService { void doSome(); void doOther(); } public class SomeServiceImpl3 implements SomeService { @Override public void doSome() { doLog(); System.out.println("执行业务方法doSome"); doTrans(); } @Override public void doOther() { doLog(); System.out.println("执行业务方法doOther"); doTrans(); } public void doLog(){ System.out.println("非业务功能,日志功能,在方法开始时输出日志"); } public void doTrans(){ System.out.println("非业务功能,在业务方法执行之后,加入事务"); } }Step2:项目aop_leadin2 当然,也可以有另一种解决方案:将这些交叉业务逻辑代码放到专门的工具类或处理类中,由主业务逻辑调用。
public class ServiceTools { public static void doLog(){ System.out.println("非业务方法,方法的执行时间:"+ new Date()); } public static void doTrans(){ System.out.println("非业务方法,方法执行完毕后,提交事务"); } } public class SomeServiceImpl2 implements SomeService { @Override public void doSome() { ServiceTools.doLog(); System.out.println("执行业务方法doSome"); ServiceTools.doTrans(); } @Override public void doOther() { ServiceTools.doLog(); System.out.println("执行业务方法doOther"); ServiceTools.doTrans(); } }Step3:项目aop_leadin3以上的解决方案,还是存在弊端:交叉业务与主业务深度耦合在一起。当交叉业务逻辑较多时,在主业务代码中会出现大量的交叉业务逻辑代码调用语句,大大影响了主业务逻辑的可读性,降低了代码的可维护性,同时也增加了开发难度。 所以,可以采用动态代理方式。在不修改主业务逻辑的前提下,扩展和增强其功能。 功能增强:
public class MyIncationHandler implements InvocationHandler { //目标对象 private Object target; //SomeServiceImpl类 public MyIncationHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //通过代理对象执行方法时,会调用执行这个invoke() System.out.println("执行MyIncationHandler中的invoke()"); System.out.println("method名称:"+method.getName()); String methodName = method.getName(); Object res = null; if("doSome".equals(methodName)){ //JoinPoint Pointcut ServiceTools.doLog(); //在目标方法之前,输出时间 //执行目标类的方法,通过Method类实现 res = method.invoke(target,args); //SomeServiceImpl.doSome() ServiceTools.doTrans(); //在目标方法执行之后,提交事务 } else { res = method.invoke(target,args); //SomeServiceImpl.doOther() } //目标方法的执行结果 return res; } } public class MyApp { public static void main(String[] args) { //使用jdk的Proxy创建代理对象 //创建目标对象 SomeService target = new SomeServiceImpl(); //创建InvocationHandler对象 InvocationHandler handler = new MyIncationHandler(target); //使用Proxy创建代理 SomeService proxy = (SomeService) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(),handler); //com.sun.proxy.$Proxy0 System.out.println("proxy======"+proxy.getClass().getName()); //通过代理执行方法,会调用handler中的invoke() proxy.doSome(); System.out.println("=================================================="); proxy.doOther(); } }AOP面向切面编程,基于动态代理的,可以使用jdk,cglib两种代理方式。 AOP就是动态代理的规范化,把动态代理的实现步骤,方式都定义好了,让开发人员用一种统一的方式,使用动态代理。 怎么理解面向切面编程? 1.需要在分析项目功能时,找出切面。 2.合理的安排切面的执行时间(是在目标方法前,还是目标方法后) 3.合理的安排切面执行的位置,在哪个类,哪个方法增加增强功能
AOP(Aspect Orient Programming),面向切面编程。面向切面编程是从动态角度考虑程序运行过程。 AOP底层,就是采用动态代理模式实现的。采用了两种代理:JDK的动态代理,与CGLIB的动态代理。
AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,可通过运行期动态代理实现程序功能的统一维护的一种技术。AOP是Spring框架中的一个重要内容。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。面向切面编程,就是将交叉业务逻辑封装成切面,利用AOP容器的功能将切面织入到主业务逻辑中。所谓交叉业务逻辑是指,通用的、与主业务逻辑无关的代码,如安全检查、事务、日志、缓存等。 若不使用AOP,则会出现代码纠缠,即交叉业务逻辑与主业务逻辑混合在一起。这样,会使主业务逻辑变的混杂不清。 例如,转账,在真正转账业务逻辑前后,需要权限控制、日志记录、加载事务、结束事务等交叉业务逻辑,而这些业务逻辑与主业务逻辑间并无直接关系。但,它们的代码量所占比重能达到总代码量的一半甚至还多。它们的存在,不仅产生了大量的“冗余”代码,还大大干扰了主业务逻辑—转账。
1.减少重复; 2.专注业务;注意:面向切面编程只是面向对象编程的一种补充。 使用AOP减少重复代码,专注业务实现:
(1)切面(Aspect)
切面泛指交叉业务逻辑。上例中的事务处理、日志处理就可以理解为切面。常用的切面是通知(Advice)。实际就是对主业务逻辑的一种增强。 常见的切面功能有日志、事务、统计信息、参数检查、权限验证等。
(2)连接点(JoinPoint)
连接点指可以被切面织入的具体方法。通常业务接口中的方法均为连接点。
(3)切入点(Pointcut)
切入点指声明的一个或多个连接点的集合。通过切入点指定一组方法。被标记为final的方法是不能作为连接点与切入点的。因为最终的是不能被修改的,不能被增强的。
(4)目标对象(Target)
目标对象指将要被增强的对象。即包含主业务逻辑的类的对象。上例中的StudentServiceImpl的对象若被增强,则该类称为目标类,该类对象称为目标对象。当然,不被增强,也就无所谓目标不目标了。
(5)通知(Advice)
通知表示切面的执行时间,Advice也叫增强。上例中MyInvocationHandler就可以理解为是一种通知。换个角度来说,通知定义了增强代码切入到目标代码的时间点,是目标方法执行之前执行,还是之后执行等。通知类型不同,切入时间不同。切入点定义切入的位置,通知定义切入的时间。 切面有三个关键要素: 1.切面的功能代码,切面要干什么 2.切面的执行位置,使用Pointcut表示切面执行的位置。 3.切面的执行时间,使用Advice表示时间,在目标方法之前,还是目标方法之后。
对于AOP这种编程思想,很多框架都进行了实现。Spring就是其中之一,可以完成面向切面编程。然而,AspectJ也实现了AOP的功能,且其实现方式更为简捷,使用更为方便,而且还支持注解式开发。所以,Spring又将AspectJ的对于AOP的实现也引入到了自己的框架中。 在Spring中使用AOP开发时,一般使用AspectJ的实现方式。 AspectJ简介 AspectJ是一个优秀面向切面的框架,它扩展了Java语言,提供了强大的切面实现。 官网地址:http://www.eclipse.org/aspectj/ AspetJ是Eclipse的开源项目,官网介绍如下: a seamless aspect-oriented extension to the Javatm programming language(一种基于Java平台的面向切面编程的语言) Java platform compatible(兼容Java平台,可以无缝扩展)easy to learn and use(易学易用) aspectJ:一个开源的专门做aop的框架。spring框架中集成了aspectj框架,通过spring就能使用aspectj的功能 aspectJ框架实现aop有两种方式: 1.使用xml的配置文件 2.使用注解,我们在项目中要做aop功能,一般都使用注解,aspectj有5个注解。
AspectJ中常用的通知有五种类型: (1)前置通知 @Before (2)后置通知 @AfterReturning (3)环绕通知 @Around (4)异常通知 @AfterThrowing (5)最终通知 @After
AspectJ定义了专门的表达式用于指定切入点。表达式的原型是:
execution(modifiers-pattern? ret-type-patterndeclaring-type-pattern?name-pattern(param-pattern)throws-pattern? )解释: modifiers-pattern] 访问权限类型 ret-type-pattern 返回值类型 declaring-type-pattern 包名类名 name-pattern(param-pattern) 方法名(参数类型和参数个数) throws-pattern 抛出异常类型 ? 表示可选的部分 以上表达式共4个部分。execution(访问权限 方法返回值 方法声明(参数) 异常类型) 切入点表达式要匹配的对象就是目标方法的方法名。所以,execution表达式中明显就是方法的签名。注意,表达式中黑色文字表示可省略部分,各部分间用空格分开。在其中可以使用以下符号: 举例: execution(public * * (…)) 指定切入点为:任意公共方法。 execution(* set* (…)) 指定切入点为:任何一个以“set”开始的方法。 execution(* com.xyz.service.*.*(…)) 指定切入点为:定义在service包里的任意类的任意方法。 execution(* com.xyz.service…*.*(…)) 指定切入点为:定义在service包或者子包里的任意类的任意方法。“…”出现在类名中时,后面必须跟“”,表示包、子包下的所有类。 execution(* *…service.*.*(…)) 指定所有包下的serivce子包下所有类(接口)中所有方法为切入点 execution(* *.service.*.*(…)) 指定只有一级包下的serivce子包下所有类(接口)中所有方法为切入点 execution(* *.ISomeService.*(…)) 指定只有一级包下的ISomeSerivce接口中所有方法为切入点e xecution(* *…ISomeService.*(…)) 指定所有包下的ISomeSerivce接口中所有方法为切入点 execution(* com.xyz.service.IAccountService.*(…)) 指定切入点为:IAccountService 接口中的任意方法。 execution(* com.xyz.service.IAccountService+.*(…)) 指定切入点为:IAccountService 若为接口,则为接口中的任意方法及其所有实现类中的任意方法;若为类,则为该类及其子类中的任意方法。 execution(* joke(String,int))) 指定切入点为:所有的joke(String,int)方法,且joke()方法的第一个参数是String,第二个参数是int。如果方法中的参数类型是java.lang包下的类,可以直接使用类名,否则必须使用全限定类名,如joke( java.util.List, int)。 execution(* joke(String,*))) 指定切入点为:所有的joke()方法,该方法第一个参数为String,第二个参数可以是任意类型,如joke(String s1,String s2)和joke(String s1,double d2)都是,但joke(String s1,doubled2,String s3)不是。 execution(* joke(String,…))) 指定切入点为:所有的joke()方法,该方法第一个参数为String,后面可以有任意个参数且参数类型不限,如joke(String s1)、joke(String s1,String s2)和joke(String s1,double d2,String s3)都是。 execution(* joke(Object)) 指定切入点为:所有的joke()方法,方法拥有一个参数,且参数是Object类型。joke(Object ob)是,但,joke(String s)与joke(User u)均不是。 execution(* joke(Object+))) 指定切入点为:所有的joke()方法,方法拥有一个参数,且参数是Object类型或该类的子类。不仅joke(Object ob)是,joke(String s)和joke(User u)也是。
(1)maven依赖
<!--spring依赖--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.5.RELEASE</version> </dependency> <!--aspectj依赖--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>5.2.5.RELEASE</version> </dependency> <!--插件--> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.1</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins>(2)引入AOP约束 在AspectJ实现AOP时,要引入AOP的约束。配置文件中使用的AOP约束中的标签,均是AspectJ框架使用的,而非Spring框架本身在实现AOP时使用的。 AspectJ对于AOP的实现有注解和配置文件两种方式,常用是注解方式。
AspectJ提供了以注解方式对于AOP的实现。 (1)实现步骤 A、Step1:定义业务接口与实现类
public interface SomeService { void doSome(String name,Integer age); } public class SomeServiceImpl implements SomeService { @Override public void doSome(String name,Integer age) { //给doSome方法增加一个功能,在doSome()执行之前, 输出方法的执行时间 System.out.println("====目标方法doSome()===="); }B、Step2:定义切面类 类中定义了若干普通方法,将作为不同的通知方法,用来增强功能。
/** * @Aspect : 是aspectj框架中的注解。 * 作用:表示当前类是切面类。 * 切面类:是用来给业务方法增加功能的类,在这个类中有切面的功能代码 * 位置:在类定义的上面 */ @Aspect public class MyAspect { /** * 定义方法,方法是实现切面功能的。 * 方法的定义要求: * 1.公共方法 public * 2.方法没有返回值 * 3.方法名称自定义 * 4.方法可以有参数,也可以没有参数。 * 如果有参数,参数不是自定义的,有几个参数类型可以使用。 */ /** * @Before: 前置通知注解 * 属性:value ,是切入点表达式,表示切面的功能执行的位置。 * 位置:在方法的上面 * 特点: * 1.在目标方法之前先执行的 * 2.不会改变目标方法的执行结果 * 3.不会影响目标方法的执行。 */ /** * @Before: 前置通知注解 * 属性:value ,是切入点表达式,表示切面的功能执行的位置。 * 位置:在方法的上面 * 特点: * 1.在目标方法之前先执行的 * 2.不会改变目标方法的执行结果 * 3.不会影响目标方法的执行。 */ @Before(value = "execution(public void com.bjpowernode.ba01.SomeServiceImpl.doSome(String,Integer))") public void myBefore(){ //就是你切面要执行的功能代码 System.out.println("前置通知, 切面功能:在目标方法之前输出执行时间:"+ new Date()); } }C、Step3:声明目标对象切面类对象
<!--把对象交给spring容器,由spring容器统一创建,管理对象--> <!--声明目标对象--> <bean id="someService" class="com.bjpowernode.ba08.SomeServiceImpl" /> <!--声明切面类对象--> <bean id="myAspect" class="com.bjpowernode.ba08.MyAspect" />D、Step4:注册AspectJ的自动代理
<!--声明自动代理生成器:使用aspectj框架内部的功能,创建目标对象的代理对象。 创建代理对象是在内存中实现的, 修改目标对象的内存中的结构。 创建为代理对象 所以目标对象就是被修改后的代理对象. aspectj-autoproxy:会把spring容器中的所有的目标对象,一次性都生成代理对象。--> <aop:aspectj-autoproxy /><aop:aspectj-autoproxy/>的底层是由AnnotationAwareAspectJAutoProxyCreator实现的。从其类名就可看出,是基于AspectJ的注解适配自动代理生成器。 其工作原理是,<aop:aspectj-autoproxy/>通过扫描找到@Aspect定义的切面类,再由切面类根据切入点找到目标类的目标方法,再由通知类型找到切入的时间点。 E、Step5:测试类中使用目标对象的id
public class MyTest01 { @Test public void test01(){ String config="applicationContext.xml"; ApplicationContext ctx = new ClassPathXmlApplicationContext(config); //从容器中获取目标对象 SomeService proxy = (SomeService) ctx.getBean("someService"); //com.sun.proxy.$Proxy8 :jdk动态代理 System.out.println("proxy:"+proxy.getClass().getName()); //通过代理的对象执行方法,实现目标方法执行时,增强了功能 proxy.doSome("lisi",20); } }(2)[掌握]@Before前置通知-方法有JoinPoint参数 在目标方法执行之前执行。被注解为前置通知的方法,可以包含一个JoinPoint类型参数。该类型的对象本身就是切入点表达式。通过该参数,可获取切入点表达式、方法签名、目标对象等。 不光前置通知的方法,可以包含一个JoinPoint类型参数,所有的通知方法均可包含该参数。
/** * 指定通知方法中的参数 : JoinPoint * JoinPoint:业务方法,要加入切面功能的业务方法 * 作用是:可以在通知方法中获取方法执行时的信息, 例如方法名称,方法的实参。 * 如果你的切面功能中需要用到方法的信息,就加入JoinPoint. * 这个JoinPoint参数的值是由框架赋予, 必须是第一个位置的参数 */ @Before(value = "execution(void *..SomeServiceImpl.doSome(String,Integer))") public void myBefore(JoinPoint jp){ //获取方法的完整定义 System.out.println("方法的签名(定义)="+jp.getSignature()); System.out.println("方法的名称="+jp.getSignature().getName()); //获取方法的实参 Object args [] = jp.getArgs(); for (Object arg:args){ System.out.println("参数="+arg); } //就是你切面要执行的功能代码 System.out.println("2=====前置通知, 切面功能:在目标方法之前输出执行时间:"+ new Date()); }(3)[掌握]@AfterReturning后置通知-注解有returning属性 在目标方法执行之后执行。由于是目标方法之后执行,所以可以获取到目标方法的返回值。该注解的returning属性就是用于指定接收方法返回值的变量名的。所以,被注解为后置通知的方法,除了可以包含JoinPoint参数外,还可以包含用于接收返回值的变量。该变量最好为Object类型,因为目标方法的返回值可能是任何类型。 接口增加方法:
public interface SomeService { void doSome(String name, Integer age); String doOther(String name,Integer age); Student doOther2(String name,Integer age); }实现方法:
@Override public String doOther(String name, Integer age) { System.out.println("====目标方法doOther()===="); return "abcd"; }定义切面:
/** * @Aspect : 是aspectj框架中的注解。 * 作用:表示当前类是切面类。 * 切面类:是用来给业务方法增加功能的类,在这个类中有切面的功能代码 * 位置:在类定义的上面 */ @Aspect public class MyAspect { /** * 后置通知定义方法,方法是实现切面功能的。 * 方法的定义要求: * 1.公共方法 public * 2.方法没有返回值 * 3.方法名称自定义 * 4.方法有参数的,推荐是Object ,参数名自定义 */ /** * @AfterReturning:后置通知 * 属性:1.value 切入点表达式 * 2.returning 自定义的变量,表示目标方法的返回值的。 * 自定义变量名必须和通知方法的形参名一样。 * 位置:在方法定义的上面 * 特点: * 1。在目标方法之后执行的。 * 2. 能够获取到目标方法的返回值,可以根据这个返回值做不同的处理功能 * Object res = doOther(); * 3. 可以修改这个返回值 * * 后置通知的执行 * Object res = doOther(); * 参数传递: 传值, 传引用 * myAfterReturing(res); * System.out.println("res="+res) * */ @AfterReturning(value = "execution(* *..SomeServiceImpl.doOther(..))", returning = "res") public void myAfterReturing( JoinPoint jp ,Object res ){ // Object res:是目标方法执行后的返回值,根据返回值做你的切面的功能处理 System.out.println("后置通知:方法的定义"+ jp.getSignature()); System.out.println("后置通知:在目标方法之后执行的,获取的返回值是:"+res); if(res.equals("abcd")){ //做一些功能 } else{ //做其它功能 } //修改目标方法的返回值, 看一下是否会影响 最后的方法调用结果 if( res != null){ res = "Hello Aspectj"; } } }(4)[掌握]@Around环绕通知-增强方法有ProceedingJoinPoint参数 在目标方法执行之前之后执行。被注解为环绕增强的方法要有返回值,Object类型。并且方法可以包含一个ProceedingJoinPoint类型的参数。接口ProceedingJoinPoint其有一个proceed()方法,用于执行目标方法。若目标方法有返回值,则该方法的返回值就是目标方法的返回值。最后,环绕增强方法将其返回值返回。该增强方法实际是拦截了目标方法的执行。 接口增加方法:
public interface SomeService { void doSome(String name, Integer age); String doOther(String name, Integer age); Student doOther2(String name, Integer age); String doFirst(String name,Integer age); }接口方法的实现:
@Override public String doFirst(String name, Integer age) { System.out.println("====业务方法doFirst()===="); return "doFirst"; }定义切面:
public class MyAspect { /** * 环绕通知方法的定义格式 * 1.public * 2.必须有一个返回值,推荐使用Object * 3.方法名称自定义 * 4.方法有参数,固定的参数 ProceedingJoinPoint */ /** * @Around: 环绕通知 * 属性:value 切入点表达式 * 位置:在方法的定义什么 * 特点: * 1.它是功能最强的通知 * 2.在目标方法的前和后都能增强功能。 * 3.控制目标方法是否被调用执行 * 4.修改原来的目标方法的执行结果。 影响最后的调用结果 * * 环绕通知,等同于jdk动态代理的,InvocationHandler接口 * * 参数: ProceedingJoinPoint 就等同于 Method * 作用:执行目标方法的 * 返回值: 就是目标方法的执行结果,可以被修改。 * * 环绕通知: 经常做事务, 在目标方法之前开启事务,执行目标方法, 在目标方法之后提交事务 */ @Around(value = "execution(* *..SomeServiceImpl.doFirst(..))") public Object myAround(ProceedingJoinPoint pjp) throws Throwable { String name = ""; //获取第一个参数值 Object args [] = pjp.getArgs(); if( args!= null && args.length > 1){ Object arg= args[0]; name =(String)arg; } //实现环绕通知 Object result = null; System.out.println("环绕通知:在目标方法之前,输出时间:"+ new Date()); //1.目标方法调用 if( "zhangsan".equals(name)){ //符合条件,调用目标方法 result = pjp.proceed(); //method.invoke(); Object result = doFirst(); } System.out.println("环绕通知:在目标方法之后,提交事务"); //2.在目标方法的前或者后加入功能 //修改目标方法的执行结果, 影响方法最后的调用结果 if( result != null){ result = "Hello AspectJ AOP"; } //返回目标方法的执行结果 return result; } }(5) [了解]@AfterThrowing 异常通知-注解中有 throwing 属性 在目标方法抛出异常后执行。该注解的 throwing 属性用于指定所发生的异常类对象。 当然,被注解为异常通知的方法可以包含一个参数 Throwable,参数名称为 throwing 指定的 名称,表示发生的异常对象。 增加业务方法:
public interface SomeService { void doSome(String name, Integer age); String doOther(String name, Integer age); Student doOther2(String name, Integer age); String doFirst(String name, Integer age); void doSecond(); void doThird(); }方法实现:
@Override public void doSecond() { System.out.println("执行业务方法doSecond()" + (10/0)); }定义切面
@AfterThrowing(value = "execution(* *..SomeServiceImpl.doSecond(..))", throwing = "ex") public void myAfterThrowing(Exception ex) { System.out.println("异常通知:方法发生异常时,执行:"+ex.getMessage()); //发送邮件,短信,通知开发人员 }(6) [了解]@After 最终通知 无论目标方法是否抛出异常,该增强均会被执行。 增加方法:
public interface SomeService { void doSome(String name, Integer age); String doOther(String name, Integer age); Student doOther2(String name, Integer age); String doFirst(String name, Integer age); void doSecond(); void doThird(); }方法实现:
@Override public void doThird() { System.out.println("执行业务方法doThird()"+ (10/0)); }定义切面:
/** * @Aspect : 是aspectj框架中的注解。 * 作用:表示当前类是切面类。 * 切面类:是用来给业务方法增加功能的类,在这个类中有切面的功能代码 * 位置:在类定义的上面 */ @Aspect public class MyAspect { /** * 最终通知方法的定义格式 * 1.public * 2.没有返回值 * 3.方法名称自定义 * 4.方法没有参数, 如果还有是JoinPoint, */ /** * @After :最终通知 * 属性: value 切入点表达式 * 位置: 在方法的上面 * 特点: * 1.总是会执行 * 2.在目标方法之后执行的 * try{ * SomeServiceImpl.doThird(..) * }catch(Exception e){ * * }finally{ * myAfter() * } */ @After(value = "execution(* *..SomeServiceImpl.doThird(..))") public void myAfter(){ System.out.println("执行最终通知,总是会被执行的代码"); //一般做资源清除工作的。 } }(7) @Pointcut 定义切入点 当较多的通知增强方法使用相同的 execution 切入点表达式时,编写、维护均较为麻烦。 AspectJ 提供了@Pointcut 注解,用于定义 execution 切入点表达式。 其用法是,将@Pointcut 注解在一个方法之上,以后所有的 execution 的 value 属性值均 可使用该方法名作为切入点。代表的就是@Pointcut 定义的切入点。这个使用@Pointcut 注解 的方法一般使用 private 的标识方法,即没有实际作用的方法。
@After(value = "execution(* *..SomeServiceImpl.doThird(..))") public void myAfter(){ System.out.println("执行最终通知,总是会被执行的代码"); //一般做资源清除工作的。 } /** * @Pointcut: 定义和管理切入点, 如果你的项目中有多个切入点表达式是重复的,可以复用的。 * 可以使用@Pointcut * 属性:value 切入点表达式 * 位置:在自定义的方法上面 * 特点: * 当使用@Pointcut定义在一个方法的上面 ,此时这个方法的名称就是切入点表达式的别名。 * 其它的通知中,value属性就可以使用这个方法名称,代替切入点表达式了 */ @Pointcut(value = "execution(* *..SomeServiceImpl.doThird(..))" ) private void mypt(){ //无需代码, }将 MyBatis与 Spring 进行整合,主要解决的问题就是将 SqlSessionFactory 对象交由 Spring 来管理。所以,该整合,只需要将 SqlSessionFactory 的对象生成器 SqlSessionFactoryBean 注 册在 Spring 容器中,再将其注入给 Dao 的实现类即可完成整合。 实现 Spring 与 MyBatis 的整合常用的方式:扫描的 Mapper 动态代理 Spring 像插线板一样,mybatis 框架是插头,可以容易的组合到一起。插线板 spring 插 上 mybatis,两个框架就是一个整体。
插件:
<build> <resources> <resource> <directory>src/main/java</directory><!--所在的目录--> <includes><!--包括目录下的.properties,.xml 文件都会扫描到--> <include>**/*.properties</include> <include>**/*.xml</include> </includes> <filtering>false</filtering> </resource> </resources> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.1</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </build>在 Dao 接口的包中创建 MyBatis 的映射文件 mapper,命名与接口名相同,本例为StudentDao.xml。mapper 中的 namespace 取值也为 Dao 接口的全限定性名。
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.bjpowernode.dao.StudentDao"> <insert id="insertStudent"> insert into student values(#{id},#{name},#{email},#{age}) </insert> <select id="selectStudents" resultType="Student"> select id,name,email,age from student order by id desc </select> </mapper>接口定义:
public interface StudentService { int addStudent(Student student); List<Student> queryStudents(); }定义实现类:
public class StudentServiceImpl implements StudentService { //引用类型 private StudentDao studentDao; //使用set注入,赋值 public void setStudentDao(StudentDao studentDao) { this.studentDao = studentDao; } @Override public int addStudent(Student student) { int nums = studentDao.insertStudent(student); return nums; } @Override public List<Student> queryStudents() { List<Student> students = studentDao.selectStudents(); return students; } }在 src 下定义 MyBatis 的主配置文件,命名为 mybatis.xml。 这里有两点需要注意: (1)主配置文件中不再需要数据源的配置了。因为数据源要交给 Spring 容器来管理了。 (2)这里对 mapper 映射文件的注册,使用标签,即只需给出 mapper 映射文件 所在的包即可。因为 mapper 的名称与 Dao 接口名相同,可以使用这种简单注册方式。这种 方式的好处是,若有多个映射文件,这里的配置也是不用改变的。当然,也可使用原来的 标签方式。
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <!--settings:控制mybatis全局行为--> <settings> <!--设置mybatis输出日志--> <setting name="logImpl" value="STDOUT_LOGGING"/> </settings> <!--设置别名--> <typeAliases> <!--name:实体类所在的包名 表示com.bjpowernode.domain包中的列名就是别名 你可以使用Student表示com.bjpowenrode.domain.Student --> <package name="com.bjpowernode.domain"/> </typeAliases> <!-- sql mapper(sql映射文件)的位置--> <mappers> <!-- name:是包名, 这个包中的所有mapper.xml一次都能加载 --> <package name="com.bjpowernode.dao"/> </mappers> </configuration>(1) 数据源的配置(掌握) 使用 JDBC 模板,首先需要配置好数据源,数据源直接以 Bean 的形式配置在 Spring 配 置文件中。根据数据源的不同,其配置方式不同: Druid 数据源 DruidDataSource Druid 是阿里的开源数据库连接池。是 Java 语言中最好的数据库连接池。Druid 能 够提供强大的监控和扩展功能。Druid 与其他数据库连接池的最大区别是提供数据库的 官网:https://github.com/alibaba/druid 使用地址:https://github.com/alibaba/druid/wiki/常见问题 配置连接池: Spring 配置文件:
<!-- 把数据库的配置信息,写在一个独立的文件,编译修改数据库的配置内容 spring知道jdbc.properties文件的位置 配置案例的Druid数据库连接池,无需配置驱动,可以根据url检测出驱动 --> <context:property-placeholder location="classpath:jdbc.properties" /> <!--声明数据源DataSource, 作用是连接数据库的--> <bean id="myDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <!--set注入给DruidDataSource提供连接数据库信息 --> <!-- 使用属性配置文件中的数据,语法 ${key} --> <property name="url" value="${jdbc.url}" /><!--setUrl()--> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.passwd}" /> <property name="maxActive" value="${jdbc.max}" /> </bean>(2) 从属性文件读取数据库连接信息 为了便于维护,可以将数据库连接信息写入到属性文件中,使 Spring 配置文件从中读取 数据。 属性文件名称自定义,但一般都是放在 src 下。 Spring 配置文件从属性文件中读取数据时,需要在的 value 属性中使用${ }, 将在属性文件中定义的 key 括起来,以引用指定属性的值。
<property name="url" value="${jdbc.url}" /><!--setUrl()-->该属性文件若要被 Spring 配置文件读取,其必须在配置文件中进行注册。使用 标签。 context:property-placeholder/方式(掌握) 该方式要求在 Spring 配置文件头部加入 spring-context.xsd 约束文件 context:property-placeholder/标签中有一个属性 location,用于指定属性文件的位置。
<!--引入属性配置文件 spring-context.xsd location:指定属性配置文件的位置,使用classpath表示类路径--> <context:property-placeholder location="classpath:jdbc.properties" />(3) 注册 SqlSessionFactoryBean
<!--声明的是mybatis中提供的SqlSessionFactoryBean类,这个类内部创建SqlSessionFactory的 SqlSessionFactory sqlSessionFactory = new .. --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <!--set注入,把数据库连接池付给了dataSource属性--> <property name="dataSource" ref="myDataSource" /> <!--mybatis主配置文件的位置 configLocation属性是Resource类型,读取配置文件 它的赋值,使用value,指定文件的路径,使用classpath:表示文件的位置 --> <property name="configLocation" value="classpath:mybatis.xml" /> </bean>(4) 定义 Mapper 扫描配置器 MapperScannerConfigurer Mapper 扫描配置器 MapperScannerConfigurer 会自动生成指定的基本包中 mapper 的代 理对象。该 Bean 无需设置 id 属性。basePackage 使用分号或逗号设置多个包。
<!--创建dao对象,使用SqlSession的getMapper(StudentDao.class) MapperScannerConfigurer:在内部调用getMapper()生成每个dao接口的代理对象。 --> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <!--指定SqlSessionFactory对象的id--> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" /> <!--指定包名, 包名是dao接口所在的包名。 MapperScannerConfigurer会扫描这个包中的所有接口,把每个接口都执行 一次getMapper()方法,得到每个接口的dao对象。 创建好的dao对象放入到spring的容器中的。 dao对象的默认名称是 接口名首字母小写 --> <property name="basePackage" value="com.bjpowernode.dao"/> </bean>向 Service 注入 Mapper 代理对象时需要注意,由于通过 Mapper 扫描配置器 MapperScannerConfigurer 生成的 Mapper 代理对象没有名称,所以在向 Service 注入 Mapper 代理时,无法通过名称注入。但可通过接口的简单类名注入,因为生成的是这个 Dao 接口 的对象。
<!--注册StudentService对象,给属性StudentDao注入值--> <bean id="studentService" class="com.bjpowernode.service.impl.StudentServiceImpl"> <property name="studentDao" ref="studentDao" /> </bean>事务原本是数据库中的概念,在 Dao 层。但一般情况下,需要将事务提升到业务层, 即 Service 层。这样做是为了能够使用事务的特性来管理具体的业务。 在 Spring 中通常可以通过以下两种方式来实现对事务的管理: (1)使用 Spring 的事务注解管理事务 (2)使用 AspectJ 的 AOP 配置管理事务
Spring 的事务管理,主要用到两个事务相关的接口。 (1) 事务管理器接口(重点) 事务管理器是 PlatformTransactionManager 接口对象。其主要用于完成事务的提交、回 滚,及获取事务的状态信息。 A、 常用的两个实现类 PlatformTransactionManager 接口有两个常用的实现类: ➢ DataSourceTransactionManager:使用 JDBC 或 MyBatis 进行数据库操作时使用。 ➢ HibernateTransactionManager:使用 Hibernate 进行持久化数据时使用。 B、 Spring 的回滚方式(理解) Spring 事务的默认回滚方式是:发生运行时异常和 error 时回滚,发生受查(编译)异常时 提交。不过,对于受查异常,程序员也可以手工设置其回滚方式。 C、 回顾错误与异常(理解) Throwable 类是 Java 语言中所有错误或异常的超类。只有当对象是此类(或其子类之一) 的实例时,才能通过 Java 虚拟机或者 Java 的 throw 语句抛出。 Error 是程序在运行过程中出现的无法处理的错误,比如 OutOfMemoryError、 ThreadDeath、NoSuchMethodError 等。当这些错误发生时,程序是无法处理(捕获或抛出) 的,JVM 一般会终止线程。 程序在编译和运行时出现的另一类错误称之为异常,它是JVM通知程序员的一种方式。 通过这种方式,让程序员知道已经或可能出现错误,要求程序员对其进行处理。 异常分为运行时异常与受查异常。 运行时异常,是 RuntimeException 类或其子类,即只有在运行时才出现的异常。如, NullPointerException、ArrayIndexOutOfBoundsException、IllegalArgumentException 等均属于 运行时异常。这些异常由 JVM 抛出,在编译时不要求必须处理(捕获或抛出)。但,只要代 码编写足够仔细,程序足够健壮,运行时异常是可以避免的。 受查异常,也叫编译时异常,即在代码编写时要求必须捕获或抛出的异常,若不处理, 则无法通过编译。如 SQLException,ClassNotFoundException,IOException 等都属于受查异常。 RuntimeException 及其子类以外的异常,均属于受查异常。当然,用户自定义的 Exception 的子类,即用户自定义的异常也属受查异常。程序员在定义异常时,只要未明确声明定义的 为 RuntimeException 的子类,那么定义的就是受查异常。
(2) 事务定义接口 事务定义接口TransactionDefinition中定义了事务描述相关的三类常量:事务隔离级别、 事务传播行为、事务默认超时时限,及对它们的操作。 A、 定义了五个事务隔离级别常量(掌握) 这些常量均是以 ISOLATION_开头。即形如 ISOLATION_XXX。 ➢ DEFAULT:采用 DB 默认的事务隔离级别。MySql 的默认为 REPEATABLE_READ; Oracle 默认为 READ_COMMITTED。 ➢ READ_UNCOMMITTED:读未提交。未解决任何并发问题。 ➢ READ_COMMITTED:读已提交。解决脏读,存在不可重复读与幻读。 ➢ REPEATABLE_READ:可重复读。解决脏读、不可重复读,存在幻读 ➢ SERIALIZABLE:串行化。不存在并发问题。 B、 定义了七个事务传播行为常量(掌握) 所谓事务传播行为是指,处于不同事务中的方法在相互调用时,执行期间事务的维护情 况。如,A 事务中的方法 doSome()调用 B 事务中的方法 doOther(),在调用执行期间事务的 维护情况,就称为事务传播行为。 事务传播行为是加在方法上的。 事务传播行为常量都是以 PROPAGATION_ 开头,形如 PROPAGATION_XXX。
PROPAGATION_REQUIRED PROPAGATION_REQUIRES_NEW PROPAGATION_SUPPORTS
PROPAGATION_MANDATORY PROPAGATION_NESTED PROPAGATION_NEVER PROPAGATION_NOT_SUPPORTED a、 PROPAGATION_REQUIRED: 指定的方法必须在事务内执行。若当前存在事务,就加入到当前事务中;若当前没有事 务,则创建一个新事务。这种传播行为是最常见的选择,也是 Spring 默认的事务传播行为。 如该传播行为加在 doOther()方法上。若 doSome()方法在调用 doOther()方法时就是在事 务内运行的,则 doOther()方法的执行也加入到该事务内执行。若 doSome()方法在调用 doOther()方法时没有在事务内执行,则 doOther()方法会创建一个事务,并在其中执行。 b、 PROPAGATION_SUPPORTS 指定的方法支持当前事务,但若当前没有事务,也可以以非事务方式执行。
c、 PROPAGATION_REQUIRES_NEW 总是新建一个事务,若当前存在事务,就将当前事务挂起,直到新事务执行完毕。 d、 定义了默认事务超时时限 常量 TIMEOUT_DEFAULT 定义了事务底层默认的超时时限,sql 语句的执行时长。 注意,事务的超时时限起作用的条件比较多,且超时的时间计算点较复杂。所以,该 值一般就使用默认值即可。
举例:购买商品 trans_sale 项目 本例要实现购买商品,模拟用户下订单,向订单表添加销售记录,从商品表减少库存。 实现步骤: Step0:创建数据库表 创建两个数据库表 sale , goods sale 销售表 goods 商品表 goods 表数据 Step1: maven 依赖 pom.xml
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.5.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>5.2.5.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.2.5.RELEASE</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.1</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.9</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.12</version> </dependency> 插件 <build> <resources> <resource> <directory>src/main/java</directory><!--所在的目录--> <includes><!--包括目录下的.properties,.xml 文件都会扫描到--> <include>**/*.properties</include> <include>**/*.xml</include> </includes> <filtering>false</filtering> </resource> </resources> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.1</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </build>Step2:创建实体类
public class Goods { private Integer id; private String name; private Integer amount; private Float price; public class Sale { private Integer id; private Integer gid; private Integer nums;Step3:定义 dao 接口
public interface GoodsDao { //更新库存 //goods表示本次用户购买的商品信息, id, 购买数量 int updateGoods(Goods goods); //查询商品的信息 Goods selectGoods(Integer id); } public interface SaleDao { //增加销售记录 int insertSale(Sale sale); }Step4:定义 dao 接口对应的 sql 映射文件
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.bjpowernode.dao.GoodsDao"> <select id="selectGoods" resultType="com.bjpowernode.domain.Goods"> select id,name,amount,price from goods where id=#{gid} </select> <update id="updateGoods"> update goods set amount = amount - #{amount} where id=#{id} </update> </mapper> <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.bjpowernode.dao.SaleDao"> <insert id="insertSale"> insert into sale(gid,nums) values(#{gid},#{nums}) </insert> </mapper>Step5:定义异常类 定义 service 层可能会抛出的异常类 NotEnoughException
//自定义的运行时异常 public class NotEnoughException extends RuntimeException { public NotEnoughException() { super(); } public NotEnoughException(String message) { super(message); } }Step6:定义 Service 接口 定义 Service 接口 BuyGoodsService
public interface BuyGoodsService { //购买商品的方法, goodsId:购买商品的编号, nums:购买的数量 void buy(Integer goodsId,Integer nums); }Step7:定义 service 的实现类
public class BuyGoodsServiceImpl implements BuyGoodsService { private SaleDao saleDao; private GoodsDao goodsDao; @Override public void buy(Integer goodsId, Integer nums) { System.out.println("=====buy方法的开始===="); //记录销售信息,向sale表添加记录 Sale sale = new Sale(); sale.setGid(goodsId); sale.setNums(nums); saleDao.insertSale(sale); //更新库存 Goods goods = goodsDao.selectGoods(goodsId); if( goods == null){ //商品不存在 throw new NullPointerException("编号是:"+goodsId+",商品不存在"); } else if( goods.getAmount() < nums){ //商品库存不足 throw new NotEnoughException("编号是:"+goodsId+",商品库存不足"); } //修改库存了 Goods buyGoods = new Goods(); buyGoods.setId( goodsId); buyGoods.setAmount(nums); goodsDao.updateGoods(buyGoods); System.out.println("=====buy方法的完成===="); } public void setSaleDao(SaleDao saleDao) { this.saleDao = saleDao; } public void setGoodsDao(GoodsDao goodsDao) { this.goodsDao = goodsDao; } }Step8:修改 Spring 配置文件内容 声明 Mybatis 对象 声明业务层对象
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <!-- 把数据库的配置信息,写在一个独立的文件,编译修改数据库的配置内容 spring知道jdbc.properties文件的位置 --> <context:property-placeholder location="classpath:jdbc.properties" /> <!--声明数据源DataSource, 作用是连接数据库的--> <bean id="myDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <!--set注入给DruidDataSource提供连接数据库信息 --> <!-- 使用属性配置文件中的数据,语法 ${key} --> <property name="url" value="${jdbc.url}" /><!--setUrl()--> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.passwd}" /> <property name="maxActive" value="${jdbc.max}" /> </bean> <!--声明的是mybatis中提供的SqlSessionFactoryBean类,这个类内部创建SqlSessionFactory的 SqlSessionFactory sqlSessionFactory = new .. --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <!--set注入,把数据库连接池付给了dataSource属性--> <property name="dataSource" ref="myDataSource" /> <!--mybatis主配置文件的位置 configLocation属性是Resource类型,读取配置文件 它的赋值,使用value,指定文件的路径,使用classpath:表示文件的位置 --> <property name="configLocation" value="classpath:mybatis.xml" /> </bean> <!--创建dao对象,使用SqlSession的getMapper(StudentDao.class) MapperScannerConfigurer:在内部调用getMapper()生成每个dao接口的代理对象。 --> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <!--指定SqlSessionFactory对象的id--> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" /> <!--指定包名, 包名是dao接口所在的包名。 MapperScannerConfigurer会扫描这个包中的所有接口,把每个接口都执行 一次getMapper()方法,得到每个接口的dao对象。 创建好的dao对象放入到spring的容器中的。 dao对象的默认名称是 接口名首字母小写 --> <property name="basePackage" value="com.bjpowernode.dao"/> </bean> <!--声明service--> <bean id="buyService" class="com.bjpowernode.service.impl.BuyGoodsServiceImpl"> <property name="goodsDao" ref="goodsDao" /> <property name="saleDao" ref="saleDao" /> </bean> </beans>Step9:定义测试类 定义测试类 MyTest。现在就可以在无事务代理的情况下运行了
public class MyTest { @Test public void test01(){ String config="applicationContext.xml"; ApplicationContext ctx = new ClassPathXmlApplicationContext(config); //从容器获取service BuyGoodsService service = (BuyGoodsService) ctx.getBean("buyService"); //调用方法 service.buy(1001,200); } }通过@Transactional 注解方式,可将事务织入到相应 public 方法中,实现事务管理。 @Transactional 的所有可选属性如下所示: ➢ propagation:用于设置事务传播属性。该属性类型为 Propagation 枚举,默认值为Propagation.REQUIRED。 ➢ isolation:用于设置事务的隔离级别。该属性类型为 Isolation 枚举,默认值为 Isolation.DEFAULT。 ➢ readOnly:用于设置该方法对数据库的操作是否是只读的。该属性为 boolean,默认值 为 false。 ➢ timeout:用于设置本操作与数据库连接的超时时限。单位为秒,类型为 int,默认值为 -1,即没有时限。 ➢ rollbackFor:指定需要回滚的异常类。类型为 Class[],默认值为空数组。当然,若只有 一个异常类时,可以不使用数组。 ➢ rollbackForClassName:指定需要回滚的异常类类名。类型为 String[],默认值为空数组。 当然,若只有一个异常类时,可以不使用数组。 ➢ noRollbackFor:指定不需要回滚的异常类。类型为 Class[],默认值为空数组。当然,若 只有一个异常类时,可以不使用数组。 ➢ noRollbackForClassName:指定不需要回滚的异常类类名。类型为 String[],默认值为空 数组。当然,若只有一个异常类时,可以不使用数组。 需要注意的是,@Transactional 若用在方法上,只能用于 public 方法上。对于其他非 public 方法,如果加上了注解@Transactional,虽然 Spring 不会报错,但不会将指定事务织入到该 方法中。因为 Spring 会忽略掉所有非 public 方法上的@Transaction 注解。 若@Transaction 注解在类上,则表示该类上所有的方法均将在执行时织入事务。 实现注解的事务步骤: 复制 trans_sale 项目,新项目 trans_sale_annotation
声明事务管理器 <!--1. 声明事务管理器--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!--连接的数据库, 指定数据源--> <property name="dataSource" ref="myDataSource" /> </bean> 开启注解驱动 <!--2. 开启事务注解驱动,告诉spring使用注解管理事务,创建代理对象 transaction-manager:事务管理器对象的id --> <tx:annotation-driven transaction-manager="transactionManager" /> 业务层 public 方法加入事务属性 public class BuyGoodsServiceImpl implements BuyGoodsService { private SaleDao saleDao; private GoodsDao goodsDao; /** * * rollbackFor:表示发生指定的异常一定回滚. * 处理逻辑是: * 1) spring框架会首先检查方法抛出的异常是不是在rollbackFor的属性值中 * 如果异常在rollbackFor列表中,不管是什么类型的异常,一定回滚。 * 2) 如果你的抛出的异常不在rollbackFor列表中,spring会判断异常是不是RuntimeException, * 如果是一定回滚。 * */ /* @Transactional( propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT, readOnly = false, rollbackFor = { NullPointerException.class, NotEnoughException.class } )*/ //使用的是事务控制的默认值, 默认的传播行为是REQUIRED,默认的隔离级别DEFAULT //默认抛出运行时异常,回滚事务。 @Transactional @Override public void buy(Integer goodsId, Integer nums) { System.out.println("=====buy方法的开始===="); //记录销售信息,向sale表添加记录 Sale sale = new Sale(); sale.setGid(goodsId); sale.setNums(nums); saleDao.insertSale(sale); //更新库存 Goods goods = goodsDao.selectGoods(goodsId); if( goods == null){ //商品不存在 throw new NullPointerException("编号是:"+goodsId+",商品不存在"); } else if( goods.getAmount() < nums){ //商品库存不足 throw new NotEnoughException("编号是:"+goodsId+",商品库存不足"); } //修改库存了 Goods buyGoods = new Goods(); buyGoods.setId( goodsId); buyGoods.setAmount(nums); goodsDao.updateGoods(buyGoods); System.out.println("=====buy方法的完成===="); } public void setSaleDao(SaleDao saleDao) { this.saleDao = saleDao; } public void setGoodsDao(GoodsDao goodsDao) { this.goodsDao = goodsDao; } }使用 XML 配置事务代理的方式的不足是,每个目标类都需要配置事务代理。当目标类 较多,配置文件会变得非常臃肿。 使用 XML 配置顾问方式可以自动为每个符合切入点表达式的类生成事务代理。其用法 很简单,只需将前面代码中关于事务代理的配置删除,再替换为如下内容即可。 Step1:复制项目 复制 trans_sale 项目,并重命名为 trans_sal_aspectj。在此基础上修改。 Step2:maven 依赖 pom.xml 新加入 aspectj 的依赖坐标
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>5.2.5.RELEASE</version> </dependency>Step3:在容器中添加事务管理器
<!--声明式事务处理:和源代码完全分离的--> <!--1.声明事务管理器对象--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="myDataSource" /> </bean>Step4:配置事务通知
<!--2.声明业务方法它的事务属性(隔离级别,传播行为,超时时间) id:自定义名称,表示 <tx:advice> 和 </tx:advice>之间的配置内容的 transaction-manager:事务管理器对象的id --> <tx:advice id="myAdvice" transaction-manager="transactionManager"> <!--tx:attributes:配置事务属性--> <tx:attributes> <!--tx:method:给具体的方法配置事务属性,method可以有多个,分别给不同的方法设置事务属性 name:方法名称,1)完整的方法名称,不带有包和类。 2)方法可以使用通配符,* 表示任意字符 propagation:传播行为,枚举值 isolation:隔离级别 rollback-for:你指定的异常类名,全限定类名。 发生异常一定回滚 --> <tx:method name="buy" propagation="REQUIRED" isolation="DEFAULT" rollback-for="java.lang.NullPointerException,com.bjpowernode.excep.NotEnoughException"/> <!--使用通配符,指定很多的方法--> <tx:method name="add*" propagation="REQUIRES_NEW" /> <!--指定修改方法--> <tx:method name="modify*" /> <!--删除方法--> <tx:method name="remove*" /> <!--查询方法,query,search,find--> <tx:method name="*" propagation="SUPPORTS" read-only="true" /> </tx:attributes> </tx:advice>Step5:配置增强器 指定将配置好的事务通知,织入给谁。
<!--配置aop--> <aop:config> <!--配置切入点表达式:指定哪些包中类,要使用事务 id:切入点表达式的名称,唯一值 expression:切入点表达式,指定哪些类要使用事务,aspectj会创建代理对象 com.bjpowernode.service com.crm.service com.service --> <aop:pointcut id="servicePt" expression="execution(* *..service..*.*(..))"/> <!--配置增强器:关联adivce和pointcut advice-ref:通知,上面tx:advice哪里的配置 pointcut-ref:切入点表达式的id --> <aop:advisor advice-ref="myAdvice" pointcut-ref="servicePt" /> </aop:config>Step6:修改测试类 测试类中要从容器中获取的是目标对象。
public class MyTest { @Test public void test01(){ String config="applicationContext.xml"; ApplicationContext ctx = new ClassPathXmlApplicationContext(config); //从容器获取service BuyGoodsService service = (BuyGoodsService) ctx.getBean("buyService"); //com.sun.proxy.$Proxy12 System.out.println("service是代理:"+service.getClass().getName()); //调用方法 service.buy(1001,10); } }在 Web 项目中使用 Spring 框架,首先要解决在 web 层(这里指 Servlet)中获取到 Spring 容器的问题。只要在 web 层获取到了 Spring 容器,便可从容器中获取到 Service 对象。
举例:springWeb 项目(在 spring-mybatis 基础上修改) Step1:新建一个 Maven Project 类型 maven-archetype-webapp Step2: 复制代码,配置文件,jar 将 spring-mybatis 项目中以下内容复制到当前项目中: (1)Service 层、Dao 层全部代码 (2)配置文件 applicationContext.xml 及 jdbc.properties,mybatis.xml (3)pom.xml (4)加入 servlet ,jsp 依赖 在之前原有的 pom.xml 文件中再加入以下的内容:
<!-- servlet依赖 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> <!-- jsp依赖 --> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>jsp-api</artifactId> <version>2.2.1-b03</version> <scope>provided</scope> </dependency>Step3:定义 index 页面
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> <p>注册学生</p> <form action="reg" method="post"> <table> <tr> <td>id</td> <td><input type="text" name="id"></td> </tr> <tr> <td>姓名:</td> <td><input type="text" name="name"></td> </tr> <tr> <td>email:</td> <td><input type="text" name="email"></td> </tr> <tr> <td>年龄:</td> <td><input type="text" name="age"></td> </tr> <tr> <td></td> <td><input type="submit" value="注册学生"></td> </tr> </table> </form> </body> </html>Step4:定义 RegisterServlet(重点代码)
public class RegisterServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String strId = request.getParameter("id"); String strName = request.getParameter("name"); String strEmail = request.getParameter("email"); String strAge = request.getParameter("age"); //创建spring的容器对象 //String config= "spring.xml"; //ApplicationContext ctx = new ClassPathXmlApplicationContext(config); WebApplicationContext ctx = null; //获取ServletContext中的容器对象,创建好的容器对象,拿来就用 /*String key = WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE; Object attr = getServletContext().getAttribute(key); if( attr != null){ ctx = (WebApplicationContext)attr; }*/ //使用框架中的方法,获取容器对象 ServletContext sc = getServletContext(); ctx = WebApplicationContextUtils.getRequiredWebApplicationContext(sc); System.out.println("容器对象的信息========"+ctx); //获取service StudentService service = (StudentService) ctx.getBean("studentService"); Student student = new Student(); student.setId(Integer.parseInt(strId)); student.setName(strName); student.setEmail(strEmail); student.setAge(Integer.valueOf(strAge)); service.addStudent(student); //给一个页面 request.getRequestDispatcher("/result.jsp").forward(request,response); } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { } }Step5:定义 success 页面
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> result.jsp 注册成功 </body> </html>Step6:web.xml 注册 Servlet
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <servlet> <servlet-name>RegisterServlet</servlet-name> <servlet-class>com.bjpowernode.controller.RegisterServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>RegisterServlet</servlet-name> <url-pattern>/reg</url-pattern> </servlet-mapping> </web-app>Step7:运行结果分析 当表单提交,跳转到 success.jsp 后,多刷新几次页面,查看后台输出,发现每刷新一次 页面,就 new 出一个新的 Spring 容器。即,每提交一次请求,就会创建一个新的 Spring 容 器。对于一个应用来说,只需要一个 Spring 容器即可。所以,将 Spring 容器的创建语句放 在 Servlet 的 doGet()或 doPost()方法中是有问题的。 此时,可以考虑,将 Spring 容器的创建放在 Servlet 进行初始化时进行,即执行 init()方 法时执行。并且,Servlet 还是单例多线程的,即一个业务只有一个 Servlet 实例,所有执行 该业务的用户执行的都是这一个 Servlet 实例。这样,Spring 容器就具有了唯一性了。 但是,Servlet 是一个业务一个 Servlet 实例,即 LoginServlet 只有一个,但还会有 StudentServlet、TeacherServlet 等。每个业务都会有一个 Servlet,都会执行自己的 init()方法, 也就都会创建一个 Spring 容器了。这样一来,Spring 容器就又不唯一了。
举例:springweb-2 项目(在 spring-web 项目基础上修改) 对于 Web 应用来说,ServletContext 对象是唯一的,一个 Web 应用,只有一个 ServletContext 对象,该对象是在 Web 应用装载时初始化的。若将 Spring 容器的创建时机, 放在 ServletContext 初始化时,就可以保证 Spring 容器的创建只会执行一次,也就保证了 Spring 容器在整个应用中的唯一性。 当 Spring 容器创建好后,在整个应用的生命周期过程中,Spring 容器应该是随时可以被访问的。即,Spring 容器应具有全局性。而放入 ServletContext 对象的属性,就具有应用的 全局性。所以,将创建好的 Spring 容器,以属性的形式放入到 ServletContext 的空间中,就 保证了 Spring 容器的全局性。 上述的这些工作,已经被封装在了如下的 Spring 的 Jar 包的相关 API 中: spring-web-5.2.5.RELEASE Step1:maven 依赖 pom.xml
<dependency> \ <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>5.2.5.RELEASE</version> </dependency>Step2:注册监听器 ContextLoaderListener 若要在 ServletContext 初 始 化 时 创 建 Spring 容 器 , 就 需 要 使 用 监 听 器 接 口 ServletContextListener 对 ServletContext 进行监听。在 web.xml 中注册该监听器。
<!--注册监听器ContextLoaderListener 监听器被创建对象后,会读取/WEB-INF/spring.xml 为什么要读取文件:因为在监听器中要创建ApplicationContext对象,需要加载配置文件。 /WEB-INF/applicationContext.xml就是监听器默认读取的spring配置文件路径 可以修改默认的文件位置,使用context-param重新指定文件的位置 配置监听器:目的是创建容器对象,创建了容器对象, 就能把spring.xml配置文件中的所有对象都创建好。 用户发起请求就可以直接使用对象了。 --> <context-param> <!-- contextConfigLocation:表示配置文件的路径 --> <param-name>contextConfigLocation</param-name> <!--自定义配置文件的路径--> <param-value>classpath:spring.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener>Spring 为该监听器接口定义了一个实现类 ContextLoaderListener,完成了两个很重要的 工作:创建容器对象,并将容器对象放入到了 ServletContext 的空间中。 打开 ContextLoaderListener 的源码。看到一共四个方法,两个是构造方法,一个初始化 方法,一个销毁方法。 所以,在这四个方法中较重要的方法应该就是 contextInitialized(),context 初始化方法。 跟踪 initWebApplicationContext()方法,可以看到,在其中创建了容器对象。 并且,将创建好的容器对象放入到了 ServletContext 的空间中,key 为一个常量: WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE。 Step3:指定 Spring 配置文件的位置 ContextLoaderListener 在对 Spring 容器进行创建时,需要加载 Spring 配置文件。其默认 的 Spring 配置文件位置与名称为:WEB-INF/applicationContext.xml。但,一般会将该配置文 件放置于项目的 classpath 下,即 src 下,所以需要在 web.xml 中对 Spring 配置文件的位置及 名称进行指定。
<context-param> <!-- contextConfigLocation:表示配置文件的路径 --> <param-name>contextConfigLocation</param-name> <!--自定义配置文件的路径--> <param-value>classpath:spring.xml</param-value> </context-param>Step4:获取 Spring 容器对象 在 Servlet 中获取容器对象的常用方式有两种: (1) 直接从 ServletContext 中获取 从对监听器 ContextLoaderListener 的源码分析可知,容器对象在 ServletContext 的中存 放的 key 为 WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE。所以,可 以直接通过 ServletContext 的 getAttribute()方法,按照指定的 key 将容器对象获取到。
//创建spring的容器对象 //String config= "spring.xml"; //ApplicationContext ctx = new ClassPathXmlApplicationContext(config); WebApplicationContext ctx = null; //获取ServletContext中的容器对象,创建好的容器对象,拿来就用 String key = WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE; Object attr = getServletContext().getAttribute(key); if( attr != null){ ctx = (WebApplicationContext)attr; }(2)通过WebApplicationContextUtils获取 工具类WebApplicationContextUtils有一个方法专门用于从ServletContext中获取Spring容器对象:getRequiredWebApplicationContext(ServletContext sc)
public class RegisterServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String strId = request.getParameter("id"); String strName = request.getParameter("name"); String strEmail = request.getParameter("email"); String strAge = request.getParameter("age"); //创建spring的容器对象 //String config= "spring.xml"; //ApplicationContext ctx = new ClassPathXmlApplicationContext(config); WebApplicationContext ctx = null; //获取ServletContext中的容器对象,创建好的容器对象,拿来就用 /*String key = WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE; Object attr = getServletContext().getAttribute(key); if( attr != null){ ctx = (WebApplicationContext)attr; } */ //使用框架中的方法,获取容器对象 ServletContext sc = getServletContext(); ctx = WebApplicationContextUtils.getRequiredWebApplicationContext(sc); System.out.println("容器对象的信息========"+ctx); //获取service StudentService service = (StudentService) ctx.getBean("studentService"); Student student = new Student(); student.setId(Integer.parseInt(strId)); student.setName(strName); student.setEmail(strEmail); student.setAge(Integer.valueOf(strAge)); service.addStudent(student); //给一个页面 request.getRequestDispatcher("/result.jsp").forward(request,response); } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { } }查其源码,看其调用关系,就可看到其是从ServletContext中读取的属性值,即Spring容器。 以上两种方式,无论使用哪种获取容器对象,刷新success页面后,可看到代码中使用的Spring容器均为同一个对象。

