该技术博客是关于动力节点Spring教程的笔记总结,方便自己学习的同时希望能为大家带来帮助!
相关文章推荐 :
- 【3万字详解】一篇文章搞定Mybatis框架
- 【步骤详细】手把手整合Spring + Mybatis
Spring 是于 2003 年兴起的一个轻量级的 Java 开发框架,它是为了解决企业应用开发的复杂性而创建的。Spring 的核心是控制反转(IoC)和面向切面编程(AOP)。Spring 是可以在 Java SE/EE 中使用的轻量级开源框架。
Spring 的主要作用就是为代码“解耦”,降低代码间的耦合度。就是让对象和对象(模块和模块)之间关系不是使用代码关联,而是通过配置来说明。即在 Spring 中说明对象(模块)的关系。
Spring 根据代码的功能特点,使用 Ioc 降低业务对象之间耦合度。IoC 使得主业务在相互调用过程中,不用再自己维护关系了,即不用再自己创建要使用的对象了。而是由 Spring 容器统一管理,自动“注入”,注入即赋值。 而 AOP 使得系统级服务得到了最大复用,且不用再由程序员手工将系统级服务“混杂”到主业务逻辑中了,而是由 Spring 容器统一完成“织入”。
Spring官方网址
Spring 是一个框架,是一个半成品的软件。有 20 个模块组成。它是一个容器管理对象,容器是装东西的,Spring 容器不装文本,数字。装的是对象。Spring 是存储对象的容器。
Spring 框架使用的 jar 都比较小,一般在 1M 以下或者几百 kb。Spring 核心功能的所需的 jar 总共在 3M 左右。
Spring 框架运行占用的资源少,运行效率高。不依赖其他 jar。
Spring 提供了 Ioc 控制反转,由容器管理对象,对象的依赖关系。原来在程序代码中的对象创建方式,现在由容器完成。对象之间的依赖解耦合。
通过 Spring 提供的 AOP 功能,方便进行面向切面的编程,许多不容易用传统 OOP 实现的功能可以通过 AOP 轻松应付
在 Spring 中,开发人员可以从繁杂的事务管理代码中解脱出来,通过声明式方式灵活地进行事务的管理,提高开发效率和质量。
Spring 不排斥各种优秀的开源框架,相反 Spring 可以降低各种框架的使用难度,Spring提供了对各种优秀框架(如 Struts,Hibernate、MyBatis)等的直接支持。简化框架的使用。Spring 像插线板一样,其他框架是插头,可以容易的组合到一起。需要使用哪个框架,就把这个插头放入插线板。不需要可以轻易的移除。
Spring 由 20 多个模块组成,它们可以分为数据访问/集成(Data Access/Integration)、Web、面向切面编程(AOP, Aspects)、提供JVM的代理(Instrumentation)、消息发送(Messaging)、核心容器(Core Container)和测试(Test)
创建一个普通的Maven项目,导入Maven依赖:
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-contextartifactId>
<version>5.2.5.RELEASEversion>
dependency>
//接口
public interface SomeService {
void doSome();
}
//实体类
public class SomeServiceImpl implements SomeService {
public void doSome() {
System.out.println("执行了实现类的doSome()方法");
}
}
在 src/main/resources/目录现创建一个 xml 文件,文件名可以随意,但 Spring 建议的名称为 applicationContext.xml。
spring 配置中需要加入约束文件才能正常使用,约束文件是 xsd 扩展名。
<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.xu.service.impl.SomeServiceImpl"/>
beans>
/**
* Spring默认创建对象的时间:在创建Spring容器时,会创建配置文件中所有的对象
* Spring创建对象:默认调用的是无参构造
*/
@Test
public void test01() {
//使用Spring容器创建对象
//1.指定Spring配置文件名称
String config = "applicationContext.xml";
//2.创建Spring容器对象:ApplicationContext
//ApplicationContext:表示Spring容器,我们可以通过Spring容器获取对象
//ClassPathXmlApplicationContext:表示从类路径中加载Spring的配置文件
ApplicationContext context = new ClassPathXmlApplicationContext(config);
//3.从Spring容器中获取对象,调用对象的方法
//getBean("配置文件中bean的id值");
SomeService someService = (SomeService) context.getBean("someService");
someService.doSome();
}
/**
* 获取Spring容器中java对象的信息
*/
@Test
public void test2() {
//通过Spring配置文件,获取Spring容器
String config = "applicationContext.xml";
ApplicationContext context = new ClassPathXmlApplicationContext(config);
//使用Spring提供的方法获取,获取容器中对象的个数
int counts = context.getBeanDefinitionCount();
System.out.println("Spring容器中对象的数量为:" + counts);
//获取容器中每个对象定义的id名称
String[] names = context.getBeanDefinitionNames();
for (String name : names) {
System.out.println(name);
}
}
在 applicationContext.xml 配置文件注册 java.util.Date:
<bean id="mydate" class="java.util.Date"/>
进行测试:
@Test
public void test3() {
String config = "applicationContext.xml";
ApplicationContext context = new ClassPathXmlApplicationContext(config);
Date date = (Date) context.getBean("mydate");
System.out.println(date);
}
ApplicationContext 用于加载 Spring 的配置文件,在程序中充当“容器”的角色。其实现类有两个。
若 Spring 配置文件存放在项目的类路径下,则使用 ClassPathXmlApplicationContext 实现
类进行加载。
//使用Spring容器创建对象
//1.指定Spring配置文件名称
String config = "applicationContext.xml";
//2.创建Spring容器对象:ApplicationContext
//ApplicationContext:表示Spring容器,我们可以通过Spring容器获取对象
//ClassPathXmlApplicationContext:表示从类路径中加载Spring的配置文件
ApplicationContext context = new ClassPathXmlApplicationContext(config);
ApplicationContext 容器,会在容器对象初始化时,将其中的所有对象一次性全部装配好。
以后代码中若要使用到这些对象,只需从内存中直接获取即可。执行效率较高,但占用内存。
//创建Spring容器:此时容器中所有对象均已创建完毕
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
bean 实例在调用无参构造器创建对象后,就要对 bean 对象的属性进行初始化。初始化是由容器自动完成的,称为注入(DI)。
根据注入方式的不同,常用的有两类:
set 注入也叫设值注入,是指通过 setter 方法传入被调用者的实例。这种注入方式简单、直观,因而在 Spring 的依赖注入中大量使用。
简单类型:
public class Student {
private String name;
private int age;
//set、toString方法省略
}
<bean id="myStudent" class="com.xu.pojo.Student">
<property name="name" value="李四"/>
<property name="age" value="20"/>
bean>
@Test
public void test4() {
//创建Spring容器
String config = "applicationContext1.xml";
ApplicationContext context = new ClassPathXmlApplicationContext(config);
//从容器中获取Student对象
Student student = (Student) context.getBean("myStudent");
System.out.println(student);
}
引用类型:
当指定 bean 的某属性值为另一 bean 的实例时,通过 ref 指定它们间的引用关系。ref 的值必须为某 bean 的 id 值。
public class School {
private String name;
private String address;
//set、toString方法省略
}
public class Student {
private String name;
private int age;
private School school;
//set、toString方法省略
}
<bean id="myStudent" class="com.xu.pojo.Student">
<property name="name" value="李四"/>
<property name="age" value="20"/>
<property name="school" ref="mySchool"/>
bean>
<bean id="mySchool" class="com.xu.pojo.School">
<property name="name" value="北京大学"/>
<property name="address" value="北京的海淀区"/>
bean>
@Test
public void test4() {
//创建Spring容器
String config = "applicationContext1.xml";
ApplicationContext context = new ClassPathXmlApplicationContext(config);
//从容器中获取Student对象
Student student = (Student) context.getBean("myStudent");
System.out.println(student);
}
构造注入是指,在构造调用者实例的同时,完成被调用者的实例化。即,使用构造器设置依赖关系。
举例:
//创建有参构造方法
public Student(String name, int age, School school) {
this.name = name;
this.age = age;
this.school = school;
}
<bean id="myStudent" class="com.xu.pojo.Student">
<constructor-arg name="name" value="张三"/>
<constructor-arg name="age" value="20"/>
<constructor-arg name="school" ref="mySchool"/>
bean>
<bean id="myStudent2" class="com.xu.pojo.Student">
<constructor-arg index="0" value="李四"/>
<constructor-arg index="1" value="21"/>
<constructor-arg index="2" ref="mySchool"/>
bean>
<bean id="mySchool" class="com.xu.pojo.School">
<property name="name" value="北京大学"/>
<property name="address" value="北京的海淀区"/>
bean>
@Test
public void test4() {
//创建Spring容器
String config = "applicationContext1.xml";
ApplicationContext context = new ClassPathXmlApplicationContext(config);
//从容器中获取Student对象
Student student = (Student) context.getBean("myStudent");
System.out.println(student);
}
@Test
public void test5() {
//创建Spring容器
String config = "applicationContext1.xml";
ApplicationContext context = new ClassPathXmlApplicationContext(config);
//从容器中获取Student对象
Student student = (Student) context.getBean("myStudent2");
System.out.println(student);
}
对于引用类型属性的注入,也可不在配置文件中显示的注入。可以通过为标签设置 autowire 属性值,为引用类型属性进行隐式自动注入(默认不自动注入引用类型属性)。根据自动注入判断标准的不同,可以分为两种:
当配置文件中被调用者 bean 的 id 值与代码中调用者 bean 类的属性名相同时,可使用byName 方式,让容器自动将被调用者 bean 注入给调用者 bean。容器是通过调用者的 bean 类的属性名与配置文件的被调用者 bean 的 id 进行比较而实现自动注入的。
举例:
<bean id="myStudent" class="com.xu.pojo.Student" autowire="byName">
<property name="name" value="李四"/>
<property name="age" value="21"/>
bean>
<bean id="school" class="com.xu.pojo.School">
<property name="name" value="北京大学"/>
<property name="address" value="北京的海淀区"/>
bean>
使用 byType 方式自动注入,要求:配置文件中被调用者 bean 的 class 属性指定的类,要与代码中调用者 bean 类的某引用类型属性类型同源。即要么相同,要么有 is-a 关系(子类,或是实现类)。但这样的同源的被调用 bean 只能有一个。多于一个,容器就不知该匹配哪一个了。
举例:
<bean id="myStudent" class="com.xu.pojo.Student" autowire="byName">
<property name="name" value="李四"/>
<property name="age" value="21"/>
bean>
<bean id="school" class="com.xu.pojo.School">
<property name="name" value="北京大学"/>
<property name="address" value="北京的海淀区"/>
bean>
在实际应用里,随着应用规模的增加,系统中 Bean 数量也大量增加,导致配置文件变得非常庞大、臃肿。为了避免这种情况的产生,提高配置文件的可读性与可维护性,可以将 Spring 配置文件分解成多个配置文件。
包含关系的配置文件:
多个配置文件中有一个总文件,总配置文件将各其它子文件通过 import 引入。在 Java 代码中只需要使用总配置文件对容器进行初始化即可。
举例:
主配置文件 total.xml 代码如下,分为两种方式:
<import resource="classpath:spring-student.xml"/>
<import resource="classpath:spring-school.xml"/>
=====================================================================================
<import resource="classpath:spring04/spring-*.xml"/>
对于 DI 使用注解,将不再需要在 Spring 配置文件中声明 bean 实例。Spring 中使用注解,需要在原有 Spring 运行环境基础上再做一些改变。
需要在 Spring 配置文件中配置组件扫描器,用于在指定的基本包中扫描注解。
applicationContext.xml 配置文件:
<context:component-scan base-package="com.xu.pojo"/>
扫描多个包的三种方式:
<context:component-scan base-package="com.xu.bao1"/>
<context:component-scan base-package="com.xu.bao2"/>
<context:component-scan base-package="com.xu.bao1;com.xu.bao2"/>
<context:component-scan base-package="com.xu"/>
但不建议使用顶级的父包(com包),扫描的路径比较多,导致容器启动时间变慢。指定到目标包才合适。也就是注解所在包全路径。
需要在类上使用注解@Component,该注解的 value 属性用于指定该 bean 的 id 值。
/**
* @Component:该注解用于创建对象,等同于的功能
* 属性(value):就是对象的名称,也就是bean的id值
* value的值是唯一的,创建的对象在Spring容器中只有一个
* 位置在类的上面
*
* @Component(value = "myStudent")等同于
*
*/
//注解中的value可以省略
@Component(value = "myStudent")
public class Student {
private String name;
private Integer age;
//set、toString方法已省略
}
另外,Spring 还提供了 3 个创建对象的注解:
这三个注解与@Component 都可以创建对象,但这三个注解还有其他的含义,@Service创建业务层对象,业务层对象可以加入事务功能,@Controller 注解创建的对象可以作为处理器接收用户的请求。
@Repository,@Service,@Controller 是对@Component 注解的细化,标注不同层的对象。即持久层对象,业务层对象,控制层对象。
如果 @Component 不指定 value 属性,bean 的 id 是类名的首字母小写。
需要在属性上使用注解@Value,该注解的 value 属性用于指定要注入的值。
使用该注解完成属性注入时,类中无需 setter。当然,若属性有 setter,则也可将其加到 setter 上。
举例:
@Component("myStudent")
public class Student {
/**
* @Value:简单类型的属性赋值
* 属性:value是String类型的,表示简单类型的属性值
* 位置:1.在成员变量的上面,无需set方法(推荐使用)
* 2.在set方法的上面
*/
@Value(value = "张飞")
private String name;
@Value("29")
private Integer age;
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
需要在引用属性上使用注解@Autowired,该注解默认使用按类型自动装配 Bean 的方式。
使用该注解完成属性注入时,类中无需 setter。当然,若属性有 setter,则也可将其加到 setter 上。
举例:
@Component("mySchool")
public class School {
@Value("北京大学")
private String name;
@Value("北京的海淀区")
private String address;
}
======================================================================
@Component("myStudent")
public class Student {
@Value(value = "张飞")
private String name;
@Value("29")
private Integer age;
/**
* 引用类型
* @Autowired:Spring框架提供的注解,实现引用类型的赋值
* Spring中通过注解给引用类型赋值,使用的是自动注入原理,支持byName,byType
* @Autowired:默认使用的是byType自动注入
*
* 位置:1)在成员变量的上面,无需set方法(推荐使用)
* 2)在set方法
*/
@Autowired
private School school;
}
需要在引用属性上联合使用注解@Autowired 与@Qualifier。@Qualifier 的 value 属性用于指定要匹配的 Bean 的 id 值。类中无需 set 方法,也可加到 set 方法上。
举例:
@Component("myStudent")
public class Student {
/**
* 引用类型
* @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;
}
@Autowired 还有一个属性 required,默认值为 true,表示当匹配失败后,会终止程序运行。若将其值设置为 false,则匹配失败,将被忽略,未匹配的属性值为 null。
@Component("myStudent")
public class Student {
/**
* 引用类型
* @Autowired:Spring框架提供的注解,实现引用类型的赋值
* Spring中通过注解给引用类型赋值,使用的是自动注入原理,支持byName,byType
* @Autowired:默认使用的是byType自动注入
*
* 属性:required是boolean类型,默认值为true
* required=true表示引用类型如果赋值失败,程序报错,并终止执行
* required=false表示引用类型如果赋值失败,程序正常执行,引用类型为null
*/
@Autowired(required = false)
@Qualifier("mySchool1")
private School school;
}
Spring提供了对 jdk中@Resource注解的支持。@Resource 注解既可以按名称匹配Bean,也可以按类型匹配 Bean。默认是按名称注入。使用该注解,要求 JDK 必须是 6 及以上版本。@Resource 可在属性上,也可在 set 方法上。
@Resource 注解若不带任何参数,采用默认按名称的方式注入,按名称不能注入 bean,则会按照类型进行 Bean 的匹配注入。
举例:
@Component("myStudent")
public class Student {
/**
* 引用类型
* @Resource:来自jdk中的注解,Spring框架提供了对这个注解的功能支持
* 可以使用该注解给引用类型的属性赋值
* 也是自动注入原理,支持byName,byType(默认是byName)
*
* 位置:1.在成员变量的上面,无需set方法(推荐使用)
* 2.在set方法上面
*/
//默认是byName:先使用byName自动注入,如果byName赋值失败,再使用byType
@Resource
private School school;
}
@Resource 注解指定其 name 属性,则 name 的值即为按照名称进行匹配的 Bean 的 id。
举例:
@Component("myStudent")
public class Student {
/**
* 引用类型
* @Resource:来自jdk中的注解,Spring框架提供了对这个注解的功能支持
* 可以使用该注解给引用类型的属性赋值
* 也是自动注入原理,支持byName,byType(默认是byName)
*
* 位置:1.在成员变量的上面,无需set方法(推荐使用)
* 2.在set方法上面
*
* @Resource如果只使用byName方式,需要增加一个属性:name
* name的值是bean的id名称
*/
//只使用byName方式
@Resource(name = "mySchool")
private School school;
}
注解优点:
注解缺点:以硬编码的方式写入到 Java 代码中,修改是需要重新编译代码的。
XML优点:
XML缺点:编写麻烦,效率低,大型项目过于复杂。
方式一
先定义好接口与一个实现类,该实现类中除了要实现接口中的方法外,还要再写两个非业务方法。非业务方法也称为交叉业务逻辑:
然后,再使接口方法调用它们。接口方法也称为主业务逻辑。
SomeService接口:
public interface SomeService {
void doSome();
void doOther();
}
SomeService接口实现类:
public class SomeServiceImpl implements SomeService {
//重写接口中doSome方法
public void doSome() {
doLog();
System.out.println("执行业务方法doSome");
doTrans();
}
//重写接口中doOther方法
public void doOther() {
doLog();
System.out.println("执行业务方法doOther");
doTrans();
}
//日志功能方法
public void doLog(){
System.out.println("非业务功能,日志功能,在方法开始时输出日志");
}
//事务功能方法
public void doTrans(){
System.out.println("非业务功能,在业务方法执行之后,加入事务");
}
}
方式二
当然,也可以有另一种解决方案:将这些交叉业务逻辑代码放到专门的工具类或处理类中,由主业务逻辑调用。
创建一个工具类:
public class ServiceTools {
public static void doLog(){
System.out.println("非业务功能,日志功能,在方法开始时输出日志");
}
public static void doTrans(){
System.out.println("非业务功能,在业务方法执行之后,加入事务");
}
}
修改SomeServiceImpl类:
public class SomeServiceImpl implements SomeService {
//重写接口中doSome方法
public void doSome() {
ServiceTools.doLog();
System.out.println("执行业务方法doSome");
ServiceTools.doTrans();
}
//重写接口中doOther方法
public void doOther() {
ServiceTools.doLog();
System.out.println("执行业务方法doOther");
ServiceTools.doTrans();
}
}
方式三
以上的解决方案,还是存在弊端:交叉业务与主业务深度耦合在一起。当交叉业务逻辑较多时,在主业务代码中会出现大量的交叉业务逻辑代码调用语句,大大影响了主业务逻辑的可读性,降低了代码的可维护性,同时也增加了开发难度。
所以,可以采用动态代理方式。在不修改主业务逻辑的前提下,扩展和增强其功能。
功能增强:
public class MyInvocationHandler implements InvocationHandler {
//目标对象:SomeServiceImpl类
private Object target;
public MyInvocationHandler(Object target) {
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//通过代理对象执行方法时,会调用这个invoke()
Object res = null;
ServiceTools.doLog();
//执行目标类的方法(doSome,doOther),通过Method类实现
res = method.invoke(target,args);
ServiceTools.doTrans();
//返回目标方法执行结果
return res;
}
}
进行测试:
public class MyTest {
public static void main(String[] args) {
//使用jdk的Proxy创建代理对象
//创建目标对象
SomeService target = new SomeServiceImpl();
//创建InvocationHandler对象
InvocationHandler handler = new MyInvocationHandler(target);
//使用Proxy创建代理
SomeService proxy = (SomeService) Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(),handler);
//通过代理执行方法,会调用handler中的invoke()
proxy.doSome();
}
}
AOP(Aspect Orient Programming),面向切面编程。面向切面编程是从动态角度考虑程序运行过程。
AOP 底层,就是采用动态代理模式实现的。采用了两种代理:
AOP 为 Aspect Oriented Programming 的缩写,意为:面向切面编程,可通过运行期动态代理实现程序功能的统一维护的一种技术。AOP 是 Spring 框架中的一个重要内容。利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
面向切面编程,就是将交叉业务逻辑封装成切面,利用 AOP 容器的功能将切面织入到主业务逻辑中。所谓交叉业务逻辑是指,通用的、与主业务逻辑无关的代码,如安全检查、事务、日志、缓存等。
若不使用 AOP,则会出现代码纠缠,即交叉业务逻辑与主业务逻辑混合在一起。这样,会使主业务逻辑变的混杂不清。
例如,转账,在真正转账业务逻辑前后,需要权限控制、日志记录、加载事务、结束事务等交叉业务逻辑,而这些业务逻辑与主业务逻辑间并无直接关系。但,它们的代码量所占比重能达到总代码量的一半甚至还多。它们的存在,不仅产生了大量的“冗余”代码,还大大干扰了主业务逻辑—转账。
注意:面向切面编程只是面向对象编程的一种补充。
使用 AOP 减少重复代码,专注业务实现:
切面表示增强的功能, 就是一堆代码,完成某个一个非业务功能。
常见的切面功能:日志, 事务, 统计信息, 参数检查, 权限验证。
连接业务方法和切面的位置。说白了就是某个类中的业务方法。
切入点指声明的一个或多个连接点的集合。通过切入点指定一组方法。
被标记为 final 的方法是不能作为连接点与切入点的。因为最终的是不能被修改的,不能被增强的。
目 标 对 象 指 将 要 被 增 强 的 对 象 。 即 包 含 主 业 务 逻 辑 的 类 的 对 象 。 上 例 中 的 StudentServiceImpl 的对象若被增强,则该类称为目标类,该类对象称为目标对象。当然,不被增强,也就无所谓目标不目标了。
通知表示切面的执行时间,Advice 也叫增强。上例中的 MyInvocationHandler 就可以理解为是一种通知。换个角度来说,通知定义了增强代码切入到目标代码的时间点,是目标方法执行之前执行,还是之后执行等。通知类型不同,切入时间不同。
切入点定义切入的位置,通知定义切入的时间。
对于 AOP 这种编程思想,很多框架都进行了实现。Spring 就是其中之一,可以完成面向切面编程。然而,AspectJ 也实现了 AOP 的功能,且其实现方式更为简捷,使用更为方便,而且还支持注解式开发。所以,Spring 又将 AspectJ 的对于 AOP 的实现也引入到了自己的框架中。
在 Spring 中使用 AOP 开发时,一般使用 AspectJ 的实现方式。
AspectJ 是一个开源的专门用于做aop的框架。
是一个优秀面向切面的框架,它扩展了 Java 语言,提供了强大的切面实现。
AspectJ 中常用的通知有五种类型:
AspectJ 定义了专门的表达式用于指定切入点。表达式的原型是:
/**
* modifiers-pattern] 访问权限类型
* ret-type-pattern 返回值类型
* declaring-type-pattern 包名类名
* name-pattern(param-pattern) 方法名(参数类型和参数个数)
* throws-pattern 抛出异常类型
* ?表示可选的部分
*/
execution(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 子包下所有类(接口)中所有方法为切入点
AspectJ 提供了以注解方式对于 AOP 的实现。
Step1:导入Maven依赖
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.13version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-contextartifactId>
<version>5.2.0.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-aspectsartifactId>
<version>5.3.6version>
dependency>
Step2:定义业务接口与实现类
public interface SomeService {
void doSome(String name,Integer age);
}
======================================================================
//目标类
public class SomeServiceImpl implements SomeService{
//给doSome方法增加一个功能,在doSome()执行之前,输出方法的执行时间
public void doSome(String name, Integer age) {
System.out.println("=====目标方法doSome()=====");
}
}
Step3:定义切面类
类中定义了若干普通方法,将作为不同的通知方法,用来增强功能。
/**
* @Aspect:是aspectj框架中的注解
* 作用:表示当前类是切面类
* 切面类:用来给业务方法增加功能的类,在这个类中有切面的功能代码
*/
@Aspect
public class MyAspect {
/**
* 定义方法用于实现切面功能
*
* 方法定义的要求:
* 1.公共方法 public
* 2.方法没有返回值
* 3.方法名称自定义
* 4.方法可以有参数,也可以没有
* 如果有参数,参数不可以自定义,有几种参数类型可以使用
*/
/**
* @before:前置通知注解
* 属性:value,是切入点表达式,表示切面功能执行的位置
* 特点:1.在目标方法之前先执行
* 2.不会影响目标方法的执行
*/
@Before(value = "execution(public void com.xu.service.SomeServiceImpl.doSome(String,Integer))")
public void myBefore(){
//就是切面要执行的功能代码
System.out.println("前置通知:在目标方法执行之前输出执行时间:" + new Date());
}
}
Step4:Spring容器中注册目标对象和切面类对象
<bean id="someService" class="com.xu.service.SomeServiceImpl"/>
<bean id="myAspect" class="com.xu.service.MyAspect"/>
Step5:注册 AspectJ 的自动代理
在定义好切面 Aspect 后,需要通知 Spring 容器,让容器生成“目标类+ 切面”的代理对象。这个代理是由容器自动生成的。只需要在 Spring 配置文件中注册一个基于 aspectj 的自动代理生成器,其就会自动扫描到@Aspect 注解,并按通知类型与切入点,将其织入,并生成代理。
aop:aspectj-autoproxy 的底层是由 AnnotationAwareAspectJAutoProxyCreator 实现的。从其类名就可看出,是基于 AspectJ 的注解适配自动代理生成器。
其工作原理是,aop:aspectj-autoproxy 通过扫描找到@Aspect 定义的切面类,再由切面类根据切入点找到目标类的目标方法,再由通知类型找到切入的时间点。
<aop:aspectj-autoproxy/>
Step6:进行测试
@Test
public void test01(){
String config = "applicationContext.xml";
ApplicationContext context = new ClassPathXmlApplicationContext(config);
//从容器中获取目标对象(代理对象)
SomeService proxy = (SomeService) context.getBean("someService");
//通过代理对象执行方法,实现目标方法执行时,功能的增强
proxy.doSome("lisi",20);
}
在目标方法执行之前执行。被注解为前置通知的方法,可以包含一个 JoinPoint 类型参数。该类型的对象本身就是切入点表达式。通过该参数,可获取切入点表达式、方法签名、目标对象等。
不光前置通知的方法,可以包含一个 JoinPoint 类型参数,所有的通知方法均可包含该参数。
@Aspect
public class MyAspect {
/**
* 指定通知方法中的参数: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("前置通知:在目标方法执行之前输出执行时间:" + new Date());
}
}
在目标方法执行之后执行。由于是目标方法之后执行,所以可以获取到目标方法的返回值。该注解的 returning 属性就是用于指定接收方法返回值的变量名的。所以,被注解为后置通知的方法,除了可以包含 JoinPoint 参数外,还可以包含用于接收返回值的变量。该变量最好为 Object 类型,因为目标方法的返回值可能是任何类型。
接口方法:
public interface SomeService {
String doOther(String name,Integer age);
}
实现方法:
public class SomeServiceImpl implements SomeService{
public String doOther(String name, Integer age) {
System.out.println("=====目标方法doOther()=====");
return "abcd";
}
}
定义切面:
@Aspect
public class MyAspect {
/**
* 后置通知定义方法:方法有参数,推荐是Object
* @AfterReturning:后置通知
* 属性:1.value 切入点表达式
* 2.returning 自定义的变量,表示目标方法的返回值
* 自定义的变量名必须和通知方法的形参名一样
*
* 特点:1.在目标方法之后执行
* 2.能够获取目标方法的返回值,可以根据这个返回值做不同的处理功能
* Object res = doOther();
* 3.可以修改返回值
*
* 后置通知的执行:
* Object res = doOther();
* myAfterReturning(res);
*/
@AfterReturning(value = "execution(* *..SomeServiceImpl.doOther(..))",
returning = "res")
public void myAfterReturning(Object res){
//Object res:是目标方法执行之后的返回值,根据返回值做切面的功能处理
System.out.println("后置通知:在目标方法执行之后执行,获取到返回值是:" + res);
//修改目标方法的执行结果
if (res != null){
res = "Hello AspectJ";
}
}
}
在目标方法执行之前之后执行。被注解为环绕增强的方法要有返回值,Object 类型。并且方法可以包含一个 ProceedingJoinPoint 类型的参数。接口 ProceedingJoinPoint 其有一个proceed()方法,用于执行目标方法。若目标方法有返回值,则该方法的返回值就是目标方法的返回值。最后,环绕增强方法将其返回值返回。该增强方法实际是拦截了目标方法的执行。
接口方法:
public interface SomeService {
String doFirst(String name,Integer age);
}
实现方法:
public class SomeServiceImpl implements SomeService{
public String doFirst(String name, Integer age) {
System.out.println("=====目标方法doFirst()=====");
return "doFirst";
}
}
定义切面:
@Aspect
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 {
//实现环绕通知
Object result = null;
System.out.println("环绕通知:在目标方法之前,输出时间:" + new Date());
//目标方法调用,在目标方法前或者后加入功能
result = pjp.proceed(); //等同于 method.invoke()
System.out.println("环绕通知:在目标方法之后,提交事务");
//返回目标方法的执行结果
return result;
}
}
当较多的通知增强方法使用相同的 execution 切入点表达式时,编写、维护均较为麻烦。AspectJ 提供了@Pointcut 注解,用于定义 execution 切入点表达式。
其用法是,将@Pointcut 注解在一个方法之上,以后所有的 execution 的 value 属性值均可使用该方法名作为切入点。代表的就是@Pointcut 定义的切入点。这个使用@Pointcut 注解的方法一般使用 private 的标识方法,即没有实际作用的方法。
@Aspect
public class MyAspect {
@After(value = "mypt()")
public void myAfter() {
System.out.println("最终通知:总是会被执行的代码");
}
@Before(value = "mypt()")
public void myBefore() {
System.out.println("前置通知:在目标方法之前先执行");
}
/**
* @Pointcut:定义和管理切入点,如果项目中有多个切入点表达式是重复的、k可以复用的
* 可以使用该注解
*
* 属性:value 切入点表达式
* 特点:当使用该注解定义在方法上,此时这个方法的名称就是切入点表达式的别名
* 其他的通知中,value属性就可以使用这个方法的名称代替切入点表达式了
*/
@Pointcut(value = "execution(* *..SomeServiceImpl.doOther(..))")
private void mypt(){
//无需代码
}
}
事务原本是数据库中的概念,在 Dao 层。但一般情况下,需要将事务提升到业务层,即 Service 层。这样做是为了能够使用事务的特性来管理具体的业务。
在 Spring 中通常可以通过以下两种方式来实现对事务的管理:
事务管理器是 PlatformTransactionManager 接口对象。其主要用于完成事务的提交、回滚,及获取事务的状态信息。
PlatformTransactionManager 接口有两个常用的实现类:
Spring 事务的默认回滚方式是:发生运行时异常和 error 时回滚,发生受查(编译)异常时提交。不过,对于受查异常,程序员也可以手工设置其回滚方式。
事务定义接口 TransactionDefinition 中定义了事务描述相关的三类常量:事务隔离级别、事务传播行为、事务默认超时时限,及对它们的操作。
这些常量均是以 ISOLATION_开头。即形如 ISOLATION_XXX。
所谓事务传播行为是指,处于不同事务中的方法在相互调用时,执行期间事务的维护情况。如,A 事务中的方法 doSome()调用 B 事务中的方法 doOther(),在调用执行期间事务的维护情况,就称为事务传播行为。事务传播行为是加在方法上的。
事务传播行为常量都是以 PROPAGATION_ 开头,形如 PROPAGATION_XXX。
PROPAGATION_REQUIRED
PROPAGATION_REQUIRES_NEW
PROPAGATION_SUPPORTS
PROPAGATION_MANDATORY
PROPAGATION_NESTED
PROPAGATION_NEVER
PROPAGATION_NOT_SUPPORTED
PROPAGATION_REQUIRED
指定的方法必须在事务内执行。若当前存在事务,就加入到当前事务中;若当前没有事务,则创建一个新事务。这种传播行为是最常见的选择,也是 Spring 默认的事务传播行为。
如该传播行为加在 doOther()方法上。若 doSome()方法在调用 doOther()方法时就是在事务内运行的,则 doOther()方法的执行也加入到该事务内执行。若 doSome()方法在调用doOther()方法时没有在事务内执行,则 doOther()方法会创建一个事务,并在其中执行。
PROPAGATION_SUPPORTS
指定的方法支持当前事务,但若当前没有事务,也可以以非事务方式执行。
PROPAGATION_REQUIRES_NEW
总是新建一个事务,若当前存在事务,就将当前事务挂起,直到新事务执行完毕。
常量 TIMEOUT_DEFAULT 定义了事务底层默认的超时时限,sql 语句的执行时长。
注意,事务的超时时限起作用的条件比较多,且超时的时间计算点较复杂。所以,该值一般就使用默认值即可。
该案例要实现:购买商品,模拟用户下订单,向订单表添加销售记录,从商品表减少库存。
创建两个数据库表 sale , goods
<dependencies>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.13version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-contextartifactId>
<version>5.2.5.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-txartifactId>
<version>5.2.5.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-jdbcartifactId>
<version>5.2.5.RELEASEversion>
dependency>
<dependency>
<groupId>org.mybatisgroupId>
<artifactId>mybatisartifactId>
<version>3.5.1version>
dependency>
<dependency>
<groupId>org.mybatisgroupId>
<artifactId>mybatis-springartifactId>
<version>1.3.1version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.9version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.1.12version>
dependency>
dependencies>
<build>
<resources>
<resource>
<directory>src/main/javadirectory>
<includes>
<include>**/*.propertiesinclude>
<include>**/*.xmlinclude>
includes>
<filtering>falsefiltering>
resource>
<resource>
<directory>src/main/resourcesdirectory>
<includes>
<include>**/*.propertiesinclude>
<include>**/*.xmlinclude>
includes>
<filtering>falsefiltering>
resource>
resources>
build>
根据两张表,分别创建Goods类 与 Sale类
public class Goods {
private Integer id;
private String name;
private Integer amount;
private Float price;
//get、set方法已省略
}
public class Sale {
private Integer id;
private Integer gid;
private Integer nums;
//get、set方法已省略
}
GoodsMapper接口:
public interface GoodsMapper {
//更新库存,goods表示本次购买的商品信息
int updateGoods(Goods goods);
//查询商品的信息
Goods selectGoods(Integer id);
}
SaleMapper接口:
public interface SaleMapper {
//插入销售记录
int insertSale(Sale sale);
}
SaleMapper.xml:
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xu.mapper.SaleMapper">
<insert id="insertSale">
insert into sale(gid,nums) values (#{gid},#{nums});
insert>
mapper>
GoodsMapper.xml:
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xu.mapper.GoodsMapper">
<update id="updateGoods">
update set amount = amount - #{amount} where id = #{id};
update>
<select id="selectGoods" resultType="com.xu.pojo.Goods">
select id,name,amount,price from goods where id = #{id};
select>
mapper>
DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<typeAliases>
<package name="com.xu.pojo"/>
typeAliases>
<mappers>
<package name="com.xu.mapper"/>
mappers>
configuration>
定义 service 层可能会抛出的异常类 NotEnoughException
//自定义的运行时异常
public class NotEnoughException extends RuntimeException{
public NotEnoughException() {
super();
}
public NotEnoughException(String message) {
super(message);
}
}
public interface BugGoodsService {
//购买商品,参数分别是:购买商品的编号、数量
void buy(Integer goodsId,Integer nums);
}
@Component("buyService")
public class BuyGoodsServiceImpl implements BugGoodsService {
@Autowired
private GoodsMapper goodsMapper;
@Autowired
private SaleMapper saleMapper;
public void buy(Integer goodsId, Integer nums) {
//记录销售的信息,向sale表添加记录
Sale sale = new Sale();
sale.setGid(goodsId);
sale.setNums(nums);
saleMapper.insertSale(sale);
//更新库存
Goods goods = goodsMapper.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);
goodsMapper.updateGoods(buyGoods);
}
}
<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.xu.service"/>
<bean id="myDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<property name="url" value="jdbc:mysql://localhost:3306/ssm"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
<property name="maxActive" value="20"/>
bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="myDataSource"/>
<property name="configLocation" value="classpath:mybatis-config.xml"/>
bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
<property name="basePackage" value="com.xu.mapper"/>
bean>
beans>
现在就可以在无事务代理的情况下运行了
public class MyTest {
@Test
public void test01(){
String config = "applicationContext.xml";
ApplicationContext context = new ClassPathXmlApplicationContext(config);
BuyGoodsServiceImpl service = (BuyGoodsServiceImpl) context.getBean("buySerivce");
//调用方法
service.buy(1001,10);
}
}
通过@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 注解在类上,则表示该类上所有的方法均将在执行时织入事务。
实现步骤如下:
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="myDataSource"/>
bean>
<tx:annotation-driven transaction-manager="transactionManager"/>
/**
* 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
使用 XML 配置事务代理的方式的不足是,每个目标类都需要配置事务代理。当目标类较多,配置文件会变得非常臃肿。
使用 XML 配置顾问方式可以自动为每个符合切入点表达式的类生成事务代理。其用法很简单,只需将前面代码中关于事务代理的配置删除,再替换为如下内容即可。
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-aspectsartifactId>
<version>5.2.5.RELEASEversion>
dependency>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="myDataSource"/>
bean>
为事务通知设置相关属性。用于指定要将事务以什么方式织入给哪些方法。
例如,应用到 buy 方法上的事务要求是必须的,且当 buy 方法发生异常后要回滚业务。
<tx:advice id="myAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="buy" propagation="REQUIRED" isolation="DEFAULT"
rollback-for="java.lang.NullPointerException,com.xu.excep.NotEnoughException"/>
<tx:method name="add*" propagation="REQUIRES_NEW"/>
tx:attributes>
tx:advice>
指定将配置好的事务通知,织入给谁。
<aop:config>
<aop:pointcut id="servicePt" expression="execution(* *..service..*.*(..))"/>
<aop:advisor advice-ref="myAdvice" pointcut-ref="servicePt"/>
aop:config>
测试类中要从容器中获取的是目标对象。
public class MyTest {
@Test
public void test01(){
String config = "applicationContext.xml";
ApplicationContext context = new ClassPathXmlApplicationContext(config);
BuyGoodsServiceImpl service = (BuyGoodsServiceImpl) context.getBean("buySerivce");
//调用方法
service.buy(1001,10);
}
}