一篇文章带你掌握主流基础框架——Spring
这篇文章中我们将会介绍Spring的框架以及本体内容,包括核心容器,注解开发,AOP以及事务等内容
那么简单说明一下Spring的必要性:
- Spring技术是JavaEE开发的必备技能,企业开发技术选型率高达90%!
- Spring可以帮助简化开发,降低企业级开发的复杂度
- Spring可以进行框架整合,高效整合其他技术,提高企业级应用开发与运行效率
Spring的核心内容:
- Ioc技术
- DI技术
- AOP
- 事务处理
Spring可进行的框架整合:
- MaBatis
- MyBatis-plus
- Struts
- Struts2
- Hibernate
在接下来的文章中,我们会学习Spring的框架思想,学习Spring的基本操作,结合案例熟练掌握
温馨提醒:在学习本篇文章前请先学习JavaWeb相关内容
(HTTP,Tomcat,Servlet,Request,Response,MVC,Cookie,Session,Ajax,Vue等内容)
初识Spring
官网:Spring | Home
Spring发展至今已经形成了一套开发的生态圈,Spring提供了相当多的项目,每个项目用于完成特定功能
我们常用的主流技术包括有:
- Spring Framework:Spring框架
- Spring Boot:Spring简化代码开发
- Spring Cloud:Spring分布设计
Spring FrameWork系统架构
在系统学习Spring之前,我们需要先来了解FrameWork系统结构
- Spring FrameWork是Spring生态圈中最基本的项目,是其他项目的根基
我们现在所使用的Spring FrameWork是4.0版本,已经趋于稳定
下面我们对架构图进行解释:
- Core Container:核心容器
- AOP:面向切面编程
- Aspects:AOP思想实现
- Data Access:数据访问
- Data Intergration:数据集成
- Web:Web开发
- Test:单元测试与集成测试
我们可以在官方中获得如此评价:
- 强大的基于 JavaBeans 的采用控制反转(Inversion of Control,IoC)原则的配置管理,使得应用程序的组建更加快捷简易。
- 数据库事务的一般化抽象层,允许插件式事务管理器,简化事务的划分使之与底层无关。
- 一个可用于从 applet 到 Java EE 等不同运行环境的核心 Bean 工厂。
核心概念介绍
首先我们思索一下我们之前的业务层与数据层:
// 数据层接口public interface BookDao { public void save();}
// 数据层实现public class BookDaoImpl implements BookDao { public void save() { System.out.println("book dao save ..."); }}
// 业务层接口public interface BookService { public void save();}
// 业务层实现public class BookServiceImpl implements BookService { private BookDao bookDao; public void save() { bookDao.save(); }}
如果我们修改BookDaoImpl内容,那么相对应的业务层实现中的bookDao的new实现也要进行修改,甚至下方方法的对象也要进行修改
Spring使用前问题
代码书写现状:
- 耦合度偏高
解放方案:
- 使用对象时,在程序中不要主动使用new产生对象,转换为由外部提供对象
Spring思想以及实现
IoC(Inversion of Control)控制反转思想:
- 使用对象时,由主动new创建对象转换为由外部提供对象
- 此过程中对象创建控制权由程序转移到外部,被称为控制反转
DI(Dependency Injection)依赖注入:
- 在容器中建立Bean与Bean之间的依赖关系和整个过程,被称为依赖注入
Spring技术对Ioc思想进行了实现:
- Spring提供了一个容器,被称为Ioc容器,用来充当IoC思想的外部
- IoC容器负责对象的创建,初始化等一系列工作,被创建和管理的对象在IoC容器中被称为Bean
// 数据层实现public class BookDaoImpl implements BookDao { public void save() { System.out.println("book dao save ..."); }}
// IoC容器/*包含daoservice两者可以建立连接*/
// 业务层实现public class BookServiceImpl implements BookService { private BookDao bookDao; public void save() { bookDao.save(); }}
目的:充分解耦
- IoC:使用IoC容器管理bean
- DI:在IoC容器内将有依赖关系的bean进行关系绑定
最终效果:
- 使用对象不仅可以直接从IoC容器中获取,还可以将已获得的Bean之间绑定依赖关系
IoC入门
首先我们需要明白IoC的使用规则:
- IoC负责管理什么:Service和Dao
- 如何被管理的对象告知IoC容器:(配置)
- 被管理的对象交给IoC容器,如何获得IoC容器:(接口)
- IoC容器得到之后,如何获得Bean:(接口方法)
- 使用Spring所需要导入的坐标:(pom.xml)
下面我们给出IoC入门的详细步骤:
- 创建Maven项目,在pom.xml中导入坐标
org.springframework spring-context 5.2.10.RELEASE
- 创建Spring.xml的配置包(applicationContext.xml,导入坐标后xml中更新该XML)
- 主函数
package com.itheima;import com.itheima.dao.BookDao;import com.itheima.service.BookService;import org.springframework.context.ApplicationContext;import org.springframework.context.support.ClassPathXmlApplicationContext;public class App2 { public static void main(String[] args) { //3.获取IoC容器 ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); //4.获取bean(根据bean配置id获取)//BookDao bookDao = (BookDao) ctx.getBean("bookDao");//bookDao.save(); // 注意:需要类型转化 BookService bookService = (BookService) ctx.getBean("bookService"); bookService.save(); }}
DI入门
首先我们需要明白DI的使用规则:
- 基于IoC管理bean
- Service中使用new形式创建Dao对象是否保留:(否)
- Service中需要Dao对象如何进入到Service中:(提供方法)
- Service与Dao之间的关系如何描述:(配置)
下面我们给出DI入门的详细步骤(基于IoC入门):
- 删除new方法
public class BookServiceImpl implements BookService { //5.删除业务层中使用new的方式创建的dao对象 private BookDao bookDao; public void save() { System.out.println("book service save ..."); bookDao.save(); }}
- 创建对象的set方法
public class BookServiceImpl implements BookService { //5.删除业务层中使用new的方式创建的dao对象 private BookDao bookDao; public void save() { System.out.println("book service save ..."); bookDao.save(); } //6.提供对应的set方法 public void setBookDao(BookDao bookDao) { this.bookDao = bookDao; }}
- 创建Dao和Service的连接
Bean整体介绍
Bean是保存在IoC中的对象,我们通过配置的方式获得Bean
下面我们从三个方面分别讲解Bean:
bean基本配置
首先我们先介绍bean本身性质:
类别 | 描述 |
---|---|
名称 | bean |
类型 | 标签 |
所属 | beans标签 |
功能 | 定义Spring核心容器管理对象 |
格式 | |
属性列表 | id:bean的id,使用容器可以通过id值获得对应的bean,在一个容器中id值唯一 class:bean的类型,即配置的bean的全路径类名 |
范例 |
然后我们介绍一下bean的别名:
类别 | 描述 |
---|---|
名称 | name |
类型 | 标签 |
所属 | bean标签 |
功能 | 定义bean的别名,可定义多个,使用逗号,分号,空格分隔 |
范例 |
正常情况下,使用id和name都可以获得bean,但推荐还是使用唯一id
获得bean无论通过id还是name获取,如果无法找到则抛出异常NosuchBeanDefinitionException
最后我们介绍一下bean的作用范围scope:
类别 | 描述 |
---|---|
名称 | scope |
类型 | 标签 |
所属 | bean标签 |
功能 | 定义bean的作用范围,可选范围如下: singleton:单列(默认) prototype:非单列 |
范例 |
这里的scope指产生对象的数量
我们的scope在默认情况下是singleton,因为很多对象只需要创建一次,多次创建会导致内存膨胀
合适交给容器进行管理的bean(singleton):
- 表现层对象
- 业务层对象
- 数据层对象
- 工具对象
不合适交给容器进行管理的bean(prototype):
- 封装实体的域对象(带有状态的bean)
bean实例化
bean的实例化通常分为四种方法,我们在下面一一介绍:
- 构造方法(常用)
我们需要在数据类中提供构造方法,配置条件中不需要改变
// 数据类public class BookDaoImpl implements BookDao { public BookDaoImpl() { System.out.println("book dao constructor is running ...."); } public void save() { System.out.println("book dao save ..."); }}
若无参构造方法不存在,则抛出异常BeanCreationException
- 静态工厂(了解)
我们在之前的案例中存在有对象工厂的说法,我们可以设置工厂并调用其方法得到bean
// 静态工厂package com.itheima.factory;import com.itheima.dao.OrderDao;import com.itheima.dao.impl.OrderDaoImpl;//静态工厂创建对象public class OrderDaoFactory { public static OrderDao getOrderDao(){ System.out.println("factory setup...."); return new OrderDaoImpl(); }}
- 实例工厂(了解)
和静态工厂相同,但不同点是方法不是静态,我们需要提前创建一个bean
// 实例工厂package com.itheima.factory;import com.itheima.dao.UserDao;import com.itheima.dao.impl.UserDaoImpl;//实例工厂创建对象public class UserDaoFactory { public UserDao getUserDao(){ return new UserDaoImpl(); }}
- FactoryBean(重要实用)
除了我们之前自己定义的工厂外,Spring提供了一种官方版本的FactoryBean
// FactoryBean工厂(需接口,中填写数据类接口)package com.itheima.factory;import com.itheima.dao.UserDao;import com.itheima.dao.impl.UserDaoImpl;import org.springframework.beans.factory.FactoryBean;//FactoryBean创建对象public class UserDaoFactoryBean implements FactoryBean { //代替原始实例工厂中创建对象的方法 // 返回创建对象类型为UserDaoImpl() public UserDao getObject() throws Exception { return new UserDaoImpl(); } // 这里填写接口类型 public Class getObjectType() { return UserDao.class; } // 可以修改来修改其scope属性 public boolean isSingleton() { return false; }}
bean生命周期
我们先来接单介绍生命周期相关概念:
- 生命周期:从创建到消亡的完整过程
- bean生命周期:bean从创建到销毁的整体过程
- bean生命周期控制:在bean创建后到销毁前做一些事情
接下来我们介绍生命周期控制方法:
- 数据层提供控制方法
由数据层提供方法,在xml配置文件中设置该方法
// 数据层package com.itheima.dao.impl;import com.itheima.dao.BookDao;public class BookDaoImpl implements BookDao { public void save() { System.out.println("book dao save ..."); } //表示bean初始化对应的操作 public void init(){ System.out.println("init..."); } //表示bean销毁前对应的操作 public void destory(){ System.out.println("destory..."); }}
- 接口控制方法(了解)
Spring为创建提供了两个接口,我们只需要继承并实现该方法即可
package com.itheima.service.impl;import com.itheima.dao.BookDao;import com.itheima.service.BookService;import org.springframework.beans.factory.DisposableBean;import org.springframework.beans.factory.InitializingBean;// InitializingBean,DisposableBean 分别对应afterPropertiesSet,destroy方法,代表创建和销毁public class BookServiceImpl implements BookService, InitializingBean, DisposableBean { private BookDao bookDao; public void setBookDao(BookDao bookDao) { System.out.println("set ....."); this.bookDao = bookDao; } public void save() { System.out.println("book service save ..."); bookDao.save(); } public void destroy() throws Exception { System.out.println("service destroy"); } public void afterPropertiesSet() throws Exception { System.out.println("service init"); }}
我们需要提及一下bean的销毁时机:(了解即可)
- 因为默认情况下,我们的bean不会被销毁,因为虚拟机会直接退出,ClassPathXmlApplicationContext会被忽略销毁过程
所以如果我们希望销毁bean观察到destroy的实现,需要手动关闭:
- 手动关闭容器方法:
package com.itheima;import com.itheima.dao.BookDao;import org.springframework.context.ApplicationContext;import org.springframework.context.support.ClassPathXmlApplicationContext;public class AppForLifeCycle { public static void main( String[] args ) { // 注意:这里需要采用ClassPathXmlApplicationContext作为对象,因为只有这个类才具有close方法 ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); BookDao bookDao = (BookDao) ctx.getBean("bookDao"); bookDao.save(); //关闭容器 ctx.close(); }}
- 注册关闭钩子,在虚拟机退出前先关闭容器再推出虚拟机
package com.itheima;import com.itheima.dao.BookDao;import org.springframework.context.ApplicationContext;import org.springframework.context.support.ClassPathXmlApplicationContext;public class AppForLifeCycle { public static void main( String[] args ) { ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); BookDao bookDao = (BookDao) ctx.getBean("bookDao"); bookDao.save(); //注册关闭钩子函数,在虚拟机退出之前回调此函数,关闭容器 ctx.registerShutdownHook(); }}
最后我们统计一下整体生命周期:
- 初始化容器:创建对象(分配内存)->执行构造方法->执行属性注入(set操作)->执行bean初始化方法
- 使用bean:执行业务操作
- 关闭/销毁容器:执行bean销毁方法
依赖注入方式
首先我们要知道类中传递数据的方法有两种:
- 普通方法(Set方法)
- 构造方法
然后我们要知道数据的类型大体分为两种:
- 引入类型(数据层)
- 简单类型(基本数据类型和String)
所以我们把依赖注入方式分为四种:
- setter注入
- 简单类型
- 引用类型
- 构造器注入
- 简单类型
- 引入类型
setter注入简单类型
首先我们需要在bean种定义简单类型属性并提供可以访问的set方法
package com.itheima.dao.impl;import com.itheima.dao.BookDao;public class BookDaoImpl implements BookDao { private String databaseName; private int connectionNum; //setter注入需要提供要注入对象的set方法 public void setConnectionNum(int connectionNum) { this.connectionNum = connectionNum; } //setter注入需要提供要注入对象的set方法 public void setDatabaseName(String databaseName) { this.databaseName = databaseName; } public void save() { System.out.println("book dao save ..."+databaseName+","+connectionNum); }}
然后在配置中使用property标签value属性注入简单类型数据
setter注入引用类型
首先我们需要在bean种定义引用类型属性并提供可以访问的set方法
package com.itheima.service.impl;import com.itheima.dao.BookDao;import com.itheima.dao.UserDao;import com.itheima.service.BookService;public class BookServiceImpl implements BookService{ private BookDao bookDao; private UserDao userDao; //setter注入需要提供要注入对象的set方法 public void setUserDao(UserDao userDao) { this.userDao = userDao; } //setter注入需要提供要注入对象的set方法 public void setBookDao(BookDao bookDao) { this.bookDao = bookDao; } public void save() { System.out.println("book service save ..."); bookDao.save(); userDao.save(); }}
然后在配置中使用property标签ref属性注入引用类型数据
构造器注入简单类型(了解)
在bean中定义简单类型属性并提供可访问的set方法
public class BookDaoImpl implements BookDao{private int connectionNumber; pubilc void setConnectionNumber(int connectionNumber){this.connectionNumber = connectionNumber; }}
配置中使用constructor-arg标签value属性注入简单类型数据
根据构造方法参数名称注入
构造器注入引用类型(了解)
在bean中定义引用类型属性并提供可访问的构造方法
public class BookDaoImpl implements BookDao{private BookBao bookBao; pubilc void setConnectionNumber(int connectionNumber){this.bookBao = bookBao; }}
配置中使用constructor-arg标签ref属性注入简单类型数据
构造器注入参数配置问题(了解)
在前面我们已经介绍了构造器的注入方法
但如果我们在bean中的数据名称发生改变,配置就不再适配,所以提供了一些方法来解决参数配置问题:
- 配置中使用constructor-arg标签type属性设置按形参类型注入
根据构造方法参数类型注入
- 配置中使用constructor-arg标签index属性设置按形参类型注入
依赖注入方式选择
依赖注入方式有以下选择标准:
- 强制依赖使用构造器进行,使用setter注入有概率不进行注入导致null对象出现
- 可选依赖使用setter注入进行,灵活性高
- Spring框架倡导使用构造器,第三方框架内部大多数采用构造器注入的形式进行数据初始化,相对严谨
- 如果有必要可以两者并用,使用构造器注入完成强制依赖的注入,使用setter注入完成可选依赖的注入
- 实际开发中根据情况分析,如果受控对象没有提供setter方法则只能采用构造器注入
- 自己开发的模块尽量推荐setter注入
依赖自动装配
在前面我们学习了手动注入的方法,但Spring其实为我们提供了一种依赖自动装配的语法:
- IoC容器根据bean所依赖的资源在容器中自动查找并注入bean中的过程称为自动装配
自动装配方式:
- 按类型(常用)
- 按名称
- 按构造方法
- 不启用
自动装配语法:
依赖自动装配特征:
- 自动装配用于引用类型注入,不能对简单类型进行操作
- 使用按类型装配时(byType)必须保障容器中相同类型的bean唯一,推荐使用
- 使用按名称装配时(byName)必须保障容器中具有指定名称的bean,因变量名与配置耦合,不推荐使用
- 自动装配优先级低于setter注入和构造器注入,同时出现时,自动装配配置失效
依赖集合注入
除了基本类型和引入类型外,我们有时也需要注入集合
下面我们简单介绍一下结合的注入:
// 数据类 package com.itheima.dao.impl;import com.itheima.dao.BookDao;import java.util.*;public class BookDaoImpl implements BookDao { private int[] array; private List list; private Set set; private Map map; private Properties properties; public void setArray(int[] array) { this.array = array; } public void setList(List list) { this.list = list; } public void setSet(Set set) { this.set = set; } public void setMap(Map map) { this.map = map; } public void setProperties(Properties properties) { this.properties = properties; } public void save() { System.out.println("book dao save ..."); System.out.println("遍历数组:" + Arrays.toString(array)); System.out.println("遍历List" + list); System.out.println("遍历Set" + set); System.out.println("遍历Map" + map); System.out.println("遍历Properties" + properties); }}
<!--注意:name:对应实现类中的内部成员名称里的array等为固定词汇--> 100 200 300 itcast itheima boxuegu chuanzhihui itcast itheima boxuegu boxuegu china henan kaifeng
案例:数据源对象管理
针对一个新的数据源对象,我们采用两步来创建bean(我们以druid为案例):
- 导入druid坐标
4.0.0 com.itheima spring_09_datasource 1.0-SNAPSHOT org.springframework spring-context 5.2.10.RELEASE com.alibaba druid 1.1.16 mysql mysql-connector-java 5.1.47
- 配置数据源对象作为Spring管理的bean
案例:加载properties文件
这个案例我们将会介绍如何加载properties文件,并将文件带入到property基本信息中
我们大致将步骤分为以下三步:
- 开辟context命名空间:
- 使用context命名空间,加载指定properties文件
- 使用${}读取加载的属性值
除了上述的基本操作,我们在context命名空间的使用中有很多需要注意的点:
- 不加载系统属性
- 加载多个properties文件
- 加载所有properties文件
- 加载properties文件标准格式
- 从类路径或jar包中搜索并加载properties文件
核心容器
前面已经完成bean与依赖注入的相关知识学习,接下来我们主要学习的是IOC容器中的核心容器。
这里所说的核心容器,大家可以把它简单的理解为ApplicationContext,接下来我们从以下几个问题入手来学习下容器的相关知识:
- 如何创建容器?
- 创建好容器后,如何从容器中获取bean对象?
- 容器类的层次结构是什么?
- BeanFactory是什么?
容器的创建方式
案例中创建ApplicationContext的方式为(类路径下的XML配置文件):
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
除了上面这种方式,Spring还提供了另外一种创建方式为(文件的绝对路径):
ApplicationContext ctx = new FileSystemXmlApplicationContext("D:\\workspace\\spring\\spring_10_container\\src\\main\\resources\\applicationContext.xml");
Bean的三种获取方式
方式一,就是目前案例中获取的方式:
BookDao bookDao = (BookDao) ctx.getBean("bookDao");
这种方式存在的问题是每次获取的时候都需要进行类型转换
方式二:
BookDao bookDao = ctx.getBean("bookDao",BookDao.class);
这种方式可以解决类型强转问题,但是参数又多加了一个,相对来说没有简化多少。
方式三:
BookDao bookDao = ctx.getBean(BookDao.class);
这种方式就类似我们之前所学习依赖注入中的按类型注入。必须要确保IOC容器中该类型对应的bean对象只能有一个。
容器类层次结构
下面我们给出容器的层次图
BeanFactory的使用
使用BeanFactory来创建IOC容器的具体实现方式为:
public class AppForBeanFactory { public static void main(String[] args) { Resource resources = new ClassPathResource("applicationContext.xml"); BeanFactory bf = new XmlBeanFactory(resources); BookDao bookDao = bf.getBean(BookDao.class); bookDao.save(); }}
为了更好的看出BeanFactory和ApplicationContext之间的区别,在BookDaoImpl添加如下构造函数:
public class BookDaoImpl implements BookDao { public BookDaoImpl() { System.out.println("constructor"); } public void save() { System.out.println("book dao save ..." ); }}
如果不去获取bean对象,打印会发现:
BeanFactory是延迟加载,只有在获取bean对象的时候才会去创建
ApplicationContext是立即加载,容器加载的时候就会创建bean对象
ApplicationContext要想成为延迟加载,只需要按照如下方式进行配置
核心概念总结
接下来我们对前面知识的一个总结,共包含如下内容:
容器相关
- BeanFactory是IoC容器的顶层接口,初始化BeanFactory对象时,加载的bean延迟加载
- ApplicationContext接口是Spring容器的核心接口,初始化时bean立即加载
- ApplicationContext接口提供基础的bean操作相关方法,通过其他接口扩展其功能
- ApplicationContext接口常用初始化类
- ClassPathXmlApplicationContext(常用)
- FileSystemXmlApplicationContext
bean相关
依赖注入相关
注解开发
在上述的开发中,我们采用xml配置文件的形式来说依旧显得有些复杂
这时我们就需要发挥Spring的优点:简化开发,通过注解来简化开发过程
下面我们会通过多个方面将Bean逐步转化为注解
注解开发Bean
在前面的内容中,我们的bean在xml配置文件中装配
在后期,我们的bean可以采用注解的形式,直接在实现类中注解表示为bean
我们采用@Component定义bean,可以添加参数表示id,也可以不添加参数,后期我们采用class类的类型来进行匹配
package com.itheima.dao.impl;import com.itheima.dao.BookDao;import org.springframework.stereotype.Component;import org.springframework.stereotype.Controller;import org.springframework.stereotype.Repository;//@Component定义bean@Component("bookDao")public class BookDaoImpl implements BookDao { public void save() { System.out.println("book dao save ..."); }}
package com.itheima.service.impl;import com.itheima.dao.BookDao;import com.itheima.service.BookService;import org.springframework.stereotype.Component;import org.springframework.stereotype.Service;//@Component定义bean@Componentpublic class BookServiceImpl implements BookService { private BookDao bookDao; public void setBookDao(BookDao bookDao) { this.bookDao = bookDao; } public void save() { System.out.println("book service save ..."); bookDao.save(); }}
@Componenty延伸出了三种类型,在实现手法上是一致,但可以具体使用于各种类中(仅用于自我识别)
- @Controller:用于表现层bean定义
- @Service:用于业务层bean定义
- @Repository:用于数据层定义
package com.itheima.dao.impl;import com.itheima.dao.BookDao;import org.springframework.stereotype.Component;import org.springframework.stereotype.Controller;import org.springframework.stereotype.Repository;//@Component定义bean//@Component("bookDao")//@Repository:@Component衍生注解@Repository("bookDao")public class BookDaoImpl implements BookDao { public void save() { System.out.println("book dao save ..."); }}
package com.itheima.service.impl;import com.itheima.dao.BookDao;import com.itheima.service.BookService;import org.springframework.stereotype.Component;import org.springframework.stereotype.Service;//@Component定义bean//@Component//@Service:@Component衍生注解@Servicepublic class BookServiceImpl implements BookService { private BookDao bookDao; public void setBookDao(BookDao bookDao) { this.bookDao = bookDao; } public void save() { System.out.println("book service save ..."); bookDao.save(); }}
但是,在上述情况下,即使我们将@Component的类定义为bean
我们的xml文件是无法探测到的,所以我们需要配置相关扫描组件来扫描bean
<!--:表示扫描文件base-package:表示扫描路径-->
纯注解开发
我们前面所提到的注解开发属于2.5的附属版本
在Spring3.0版本,Spring就提供了纯注解开发模式,利用java类代替配置文件,开启了Spring快速开发时代
在之前我们的xml配置文件是很繁琐的:
但是我们可以通过创建单独的类来表示配置文件:
- @Configuration:用于声明当前类为Spring配置类
- @ComponentScan:用于扫描类文件(类似于)
package com.itheima.config;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;//声明当前类为Spring配置类@Configuration//设置bean扫描路径,多个路径书写为字符串数组格式@ComponentScan({"com.itheima.service","com.itheima.dao"})public class SpringConfig {}
注意:因为该类属于配置类,我们通常单列一个文件夹来表示
常用文件夹:config
命名规范:SpringConfig,UserConfig…
因为我们的开发不再依靠于xml配置文件,所以在主函数中的Spring容器获得方式也发生了改变:
package com.itheima;import com.itheima.dao.BookDao;import com.itheima.service.BookService;import org.springframework.context.ApplicationContext;import org.springframework.context.support.ClassPathXmlApplicationContext;public class App { public static void main(String[] args) { // 这是我们之前的获取方式,采用路径获取xml文件 // ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); // 这是新的获取方式,直接提供配置类的类型 // AnnotationConfigApplicationContext加载Spring配置类初始化Spring容器 ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class); // 后面操作无需变化 BookDao bookDao = (BookDao) ctx.getBean("bookDao"); System.out.println(bookDao); //按类型获取bean BookService bookService = ctx.getBean(BookService.class); System.out.println(bookService); }}
注解开发Bean作用范围与管理
既然我们的Bean开发从xml转移到注解开发,那么一些配置设置同样发生改变
首先我们介绍Scope范围的设置方式:
- @Scope:定义bean的作用范围
package com.itheima.dao.impl;import com.itheima.dao.BookDao;import org.springframework.context.annotation.Scope;import org.springframework.stereotype.Repository;import javax.annotation.PostConstruct;import javax.annotation.PreDestroy;@Repository//@Scope设置bean的作用范围(singleton或prototype),可以不添加默认singleton@Scope("singleton")public class BookDaoImpl implements BookDao { public void save() { System.out.println("book dao save ..."); }}
然后我们介绍一下bean生命周期的init和destroy操作:
- @PostConstruct:定义init操作,表示构造后操作
- @PreDestroy:定义destroy操作,表示销毁前操作
依赖注入(自动装配)
在Spring3.0中,省略掉了前面繁琐的依赖注入,我们的bean依赖注入只留下了自动装配这一操作:
- 使用@Autowired注解开启自动装配模式(按类型)
- 当存在相同类型时,我们采用@Qualifier开启按名自动装配
package com.itheima.service.impl;import com.itheima.dao.BookDao;import com.itheima.service.BookService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Service;@Servicepublic class BookServiceImpl implements BookService { //@Autowired:注入引用类型,自动装配模式,默认按类型装配 @Autowired //@Qualifier:自动装配bean时按bean名称装配 @Qualifier("bookDao") private BookDao bookDao; public void save() { System.out.println("book service save ..."); bookDao.save(); }}
注意:自动装配基于反射设计创建对象并暴力反射对应属性为私有属性初始化数据,因此无需提供setter方法
注意:自动转配建议使用无参构造方法创建对象(默认),如果不提供对应构造方法,请提供唯一的构造方法
注意:@Qualifier是基于@Autowired实现的,必须保证先有Autowired才能存在Qualifier
除了上述的bean类型装配,我们的简单类型装配依旧存在:
- 我们采用@Value的形式来配置简单类型的值
package com.itheima.dao.impl;import com.itheima.dao.BookDao;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Repository;@Repository("bookDao")public class BookDaoImpl implements BookDao { //@Value:注入简单类型(无需提供set方法) @Value("123") private String name; public void save() { System.out.println("book dao save ..." + name); }}
之所以使用@Value的形式配置,是因为我们的类型值不一定是由手动输入的,有可能来自于Properties资源:
- 首先我们需要在Springconfig中配置相关资源
package com.itheima.config;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.PropertySource;@Configuration@ComponentScan("com.itheima")//@PropertySource加载properties配置文件@PropertySource({"jdbc.properties"})public class SpringConfig {}
- 然后我们在数据层调用时,采用${}来匹配数据
package com.itheima.dao.impl;import com.itheima.dao.BookDao;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Repository;@Repository("bookDao")public class BookDaoImpl implements BookDao { //@Value:注入简单类型(无需提供set方法) @Value("${name}") private String name; public void save() { System.out.println("book dao save ..." + name); }}
注解开发第三方bean
我们在实际开发中不仅仅需要对自己的bean进行管理,有时候可能需要引进其他的bean
下面我们以Druid为例进行讲解:
- 首先在pom.xml中导入Druid坐标
4.0.0 com.itheima spring_14_annotation_third_bean_manager 1.0-SNAPSHOT org.springframework spring-context 5.2.10.RELEASE com.alibaba druid 1.1.16
- 使用@Bean配置第三方Bean
// 该bean同样属于config文件,我们同样放置在config文件夹下// 在后续我们将会讲解如何进行连接package com.itheima.config;import com.alibaba.druid.pool.DruidDataSource;import com.itheima.dao.BookDao;import org.springframework.beans.factory.annotation.Value;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import javax.sql.DataSource;public class JdbcConfig { // 1.定义一个方法获得要管理的对象 // 2.添加@Bean,表示当前方法的返回值是一个bean // @Bean修饰的方法,形参根据类型自动装配 @Bean public DataSource dataSource(){ DruidDataSource ds = new DruidDataSource(); ds.setDriverClassName("com.mysql.jdbc.Driver"); ds.setUrl("jdbc:mysql://localhost:3306/spring_db"); ds.setUsername("root"); ds.setPassword("123456"); return ds; }}
- 将独立的配置类加入核心配置(导入法)
// SpringConfigpackage com.itheima.config;import com.alibaba.druid.pool.DruidDataSource;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Import;import javax.sql.DataSource;@Configuration@ComponentScan("com.itheima")//@Import:导入配置信息(如果需要多个,同样采用{}数组形式)@Import({JdbcConfig.class})public class SpringConfig {}
// JdbcConfigpackage com.itheima.config;import com.alibaba.druid.pool.DruidDataSource;import com.itheima.dao.BookDao;import org.springframework.beans.factory.annotation.Value;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import javax.sql.DataSource;//@Configurationpublic class JdbcConfig { //@Bean修饰的方法,形参根据类型自动装配 @Bean public DataSource dataSource(){ DruidDataSource ds = new DruidDataSource();// 配置信息 return ds; }}
注意:除了上述的导入法外还存在有其他方法,但导入法属于主流,因此我们不介绍其他流派,感兴趣的同学可以去查阅一下
注解开发为第三方导入资源
我们的第三方bean也可能需要导入部分资源,下面我们进行简单介绍:
- 简单类型依赖注入
package com.itheima.config;import com.alibaba.druid.pool.DruidDataSource;import com.itheima.dao.BookDao;import org.springframework.beans.factory.annotation.Value;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import javax.sql.DataSource;//@Configurationpublic class JdbcConfig { //1.定义一个方法获得要管理的对象 @Value("com.mysql.jdbc.Driver") private String driver; @Value("jdbc:mysql://localhost:3306/spring_db") private String url; @Value("root") private String userName; @Value("root") private String password; //2.添加@Bean,表示当前方法的返回值是一个bean //@Bean修饰的方法,形参根据类型自动装配 @Bean public DataSource dataSource(){ DruidDataSource ds = new DruidDataSource(); ds.setDriverClassName(driver); ds.setUrl(url); ds.setUsername(userName); ds.setPassword(password); return ds; }}
- 依赖类型依赖注入
package com.itheima.config;import com.alibaba.druid.pool.DruidDataSource;import com.itheima.dao.BookDao;import org.springframework.beans.factory.annotation.Value;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import javax.sql.DataSource;public class JdbcConfig { @Bean public DataSource dataSource(BookDao bookDao){ // 我们只需要调用即可,系统会为我们自动装配 System.out.println(bookDao); } }
引入类型注入只需要为bean定义方法设置形参即可,容器会根据类型自动装配对象
注解开发对比
最后我们通过和前述非注解开发的对比来结束这一章节:
功能 | XML配置 | 注解 |
---|---|---|
定义bean | bean标签: – id标签 – class标签 | @Component – @controller – @Service – @Repository @ComponentScan |
设置依赖注入 | Setter注入 构造器注入 自动装配 | @Autowired @Qualifier @Value |
配置第三方bean | bean标签 静态工厂 实例工厂 FactoryBean | @Bean |
作用范围 | scope属性 | @Scope |
生命周期 | 标准接口 init-method destroy-method | @PostConstructor @preDestroy |
Spring整合MyBatis和Junit
在前面的内容中我们已经学习了Spring的Framework的大部分内容
接下来让我们来整合我们之前所学习的内容,整体的运用Spring来简化操作
Spring整合MyBatis
首先我们来详细讲解MyBatis的整合
Spring整合MyBatis思维导论
在整合之前,我们回忆一下MyBatis的单体操作:
- 首先我们需要准备数据库内容(这里不做展示)
- 连接数据库配置文件
- 与数据库相关的实体类
package com.itheima.domain;import java.io.Serializable;public class Account implements Serializable { private Integer id; private String name; private Double money; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Double getMoney() { return money; } public void setMoney(Double money) { this.money = money; } @Override public String toString() { return "Account{" + "id=" + id + ", name='" + name + '\'' + ", money=" + money + '}'; }}
- 数据层(这里全做注解,采取Mapper全权管理的形式)
package com.itheima.dao;import com.itheima.domain.Account;import org.apache.ibatis.annotations.Delete;import org.apache.ibatis.annotations.Insert;import org.apache.ibatis.annotations.Select;import org.apache.ibatis.annotations.Update;import java.util.List;public interface AccountDao { @Insert("insert into tbl_account(name,money)values(#{name},#{money})") void save(Account account); @Delete("delete from tbl_account where id = #{id} ") void delete(Integer id); @Update("update tbl_account set name = #{name} , money = #{money} where id = #{id} ") void update(Account account); @Select("select * from tbl_account") List findAll(); @Select("select * from tbl_account where id = #{id} ") Account findById(Integer id);}
- 服务层
package com.itheima.service.impl;import com.itheima.dao.AccountDao;import com.itheima.domain.Account;import com.itheima.service.AccountService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import java.util.List;@Servicepublic class AccountServiceImpl implements AccountService { @Autowired private AccountDao accountDao; public void save(Account account) { accountDao.save(account); } public void update(Account account){ accountDao.update(account); } public void delete(Integer id) { accountDao.delete(id); } public Account findById(Integer id) { return accountDao.findById(id); } public List findAll() { return accountDao.findAll(); }}
- 主函数
import com.itheima.dao.AccountDao;import com.itheima.domain.Account;import org.apache.ibatis.io.Resources;import org.apache.ibatis.session.SqlSession;import org.apache.ibatis.session.SqlSessionFactory;import org.apache.ibatis.session.SqlSessionFactoryBuilder;import java.io.IOException;import java.io.InputStream;public class App { public static void main(String[] args) throws IOException { // 1. 创建SqlSessionFactoryBuilder对象 SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder(); // 2. 加载SqlMapConfig.xml配置文件 InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig.xml.bak"); // 3. 创建SqlSessionFactory对象 SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream); // 4. 获取SqlSession SqlSession sqlSession = sqlSessionFactory.openSession(); // 5. 执行SqlSession对象执行查询,获取结果User AccountDao accountDao = sqlSession.getMapper(AccountDao.class); Account ac = accountDao.findById(2); System.out.println(ac); // 6. 释放资源 sqlSession.close(); }}
在上述内容中,我们重点分析配置文件和主函数的内容,因为我们的Spring的主要目的是为了管理Bean
所以我们需要在MyBatis中找到符合要求的Bean:
import com.itheima.dao.AccountDao;import com.itheima.domain.Account;import org.apache.ibatis.io.Resources;import org.apache.ibatis.session.SqlSession;import org.apache.ibatis.session.SqlSessionFactory;import org.apache.ibatis.session.SqlSessionFactoryBuilder;import java.io.IOException;import java.io.InputStream;public class App { public static void main(String[] args) throws IOException { // SqlSessionFactory属于主体Bean SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder(); InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig.xml.bak"); SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream); // SqlSession由SqlSessionFactory创建 SqlSession sqlSession = sqlSessionFactory.openSession(); AccountDao accountDao = sqlSession.getMapper(AccountDao.class); Account ac = accountDao.findById(2); System.out.println(ac); sqlSession.close(); }}
所以我们的整体操作其实就是为了整合MyBatis的Bean
Spring整合MyBatis具体操作
接下来我们给出具体操作:
- 导入相关包
4.0.0 com.itheima spring_15_spring_mybatis 1.0-SNAPSHOT org.springframework spring-context 5.2.10.RELEASE com.alibaba druid 1.1.16 org.mybatis mybatis 3.5.6 mysql mysql-connector-java 5.1.47 org.springframework spring-jdbc 5.2.10.RELEASE org.mybatis mybatis-spring 1.3.0
- 创建配置环境Config
// SpringConfig(前面已讲解)package com.itheima.config;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Import;import org.springframework.context.annotation.PropertySource;@Configuration@ComponentScan("com.itheima")//@PropertySource:加载类路径jdbc.properties文件@PropertySource("classpath:jdbc.properties")@Import({JdbcConfig.class,MybatisConfig.class})public class SpringConfig {}
// JdbcConfig(前面已讲解)package com.itheima.config;import com.alibaba.druid.pool.DruidDataSource;import org.springframework.beans.factory.annotation.Value;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import javax.sql.DataSource;public class JdbcConfig { @Value("${jdbc.driver}") private String driver; @Value("${jdbc.url}") private String url; @Value("${jdbc.username}") private String userName; @Value("${jdbc.password}") private String password; @Bean public DataSource dataSource(){ DruidDataSource ds = new DruidDataSource(); ds.setDriverClassName(driver); ds.setUrl(url); ds.setUsername(userName); ds.setPassword(password); return ds; }}
// MyBatisConfig(MyBatis重点内容)package com.itheima.config;import org.mybatis.spring.SqlSessionFactoryBean;import org.mybatis.spring.mapper.MapperScannerConfigurer;import org.springframework.context.annotation.Bean;import javax.sql.DataSource;public class MybatisConfig { //定义bean,SqlSessionFactoryBean,用于产生SqlSessionFactory对象 @Bean public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){ SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean(); ssfb.setTypeAliasesPackage("com.itheima.domain"); ssfb.setDataSource(dataSource); return ssfb; } /* SqlSessionFactoryBean属于mybatis-spring提供的新的对象,用于快速产生SqlSessionFactory对象 ssfb.setTypeAliasesPackage("com.itheima.domain"); 对应于 ssfb.setDataSource(dataSource); 对应于 DataSource的配置信息 上述语句基本均为固定语句 只有Package的别名包需要修改内容 */ //定义bean,返回MapperScannerConfigurer对象 @Bean public MapperScannerConfigurer mapperScannerConfigurer(){ MapperScannerConfigurer msc = new MapperScannerConfigurer(); msc.setBasePackage("com.itheima.dao"); return msc; } /*MapperScannerConfigurer属于mybatis-spring提供的新的对象,返回MapperScannerConfigurer对象我们同样只需要设置映射包setBasePackage上述语句基本均为固定语句 只有Package的映射名包需要修改内容 */ }
- 主函数(其他内容基本不做修改)
import com.itheima.config.SpringConfig;import com.itheima.domain.Account;import com.itheima.service.AccountService;import org.springframework.context.ApplicationContext;import org.springframework.context.annotation.AnnotationConfigApplicationContext;public class App { public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class); AccountService accountService = ctx.getBean(AccountService.class); Account ac = accountService.findById(1); System.out.println(ac); }}
Spring整合Junit
我们对于Junit的整合建立于Spring与MyBatis已经整合的基础上,所以上述内容请务必明白!
Spring整合Junit具有一定固定格式,我们直接写出步骤:
- 导入包
4.0.0 com.itheima spring_16_spring_junit 1.0-SNAPSHOT org.springframework spring-context 5.2.10.RELEASE com.alibaba druid 1.1.16 org.mybatis mybatis 3.5.6 mysql mysql-connector-java 5.1.47 org.springframework spring-jdbc 5.2.10.RELEASE org.mybatis mybatis-spring 1.3.0 junit junit 4.12 test org.springframework spring-test 5.2.10.RELEASE
- 书写Junit的Test代码内容
// 下述内容均在test文件夹下进行package com.itheima.service;import com.itheima.config.SpringConfig;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.test.context.ContextConfiguration;import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;//设置类运行器(固定形式)@RunWith(SpringJUnit4ClassRunner.class)//设置Spring环境对应的配置类(匹配你所使用的Spring,注意需要写classes的形式)@ContextConfiguration(classes = SpringConfig.class)public class AccountServiceTest { //支持自动装配注入bean @Autowired private AccountService accountService; @Test public void testFindById(){ System.out.println(accountService.findById(1)); } @Test public void testFindAll(){ System.out.println(accountService.findAll()); }}
SpringAOP
我们在开篇有提及到AOP,现在让我们来详细介绍一下AOP~
SpringAOP简介
首先我们来介绍一下AOP:
- AOP(Aspect Oriented Programming)面向切面编程,一种编程范式,指导开发者如何组织程序结构
- OOP(Object Oriented Programming)面向对象编程,也是一种编程范式,我们列举出来是为了表示和AOP概念相似
AOP作用:
- 在不惊动原始设计的基础上为其进行功能增强
Spring理念:
- 无入侵式/无侵入式
AOP核心概念:
- 连接点:程序执行过程中的任意位置,粒度为执行方法,抛出异常,设置变量等
- 切入点:匹配连接点的式子
- 通知:在切入点处执行的操作,也就是共性功能
- 通知类:存放通知的类
- 切面:描述通知与切入点的关系
通俗解释:
实现类中的各个方法被称为连接点
如果我们希望在这些连接点中设置相同的部分,可以采用通知进行设置
我们利用通知和连接点进行连接,连接点就可以执行通知中的方法并且同时执行连接点的方法
被连接的连接点被称为切入点,存放通知的类被称为通知类
SpringAOP入门
我们同样采用一个案例进行SpringAOP入门介绍
案例设定:测试接口执行效率
简化设定:在接口执行前输出当前系统时间
开发模式:XML or 注解(我们现在大部分使用注解)
具体操作:
- 导入坐标(pom.xml)
4.0.0 com.itheima spring_18_aop_quickstart 1.0-SNAPSHOT org.springframework spring-context 5.2.10.RELEASE org.aspectj aspectjweaver 1.9.4
- 制作连接点方法(原始方法,不发生改变)
// 接口BookDaocom.itheima.dao;public interface BookDao { public void save(); public void update();}
// 实现类BookDaoImplpackage com.itheima.dao.impl;import com.itheima.dao.BookDao;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Repository;@Repositorypublic class BookDaoImpl implements BookDao { public void save() { System.out.println(System.currentTimeMillis()); System.out.println("book dao save ..."); } public void update(){ System.out.println("book dao update ..."); }}
- 制作共性功能(通知类与通知)
// 我们推荐单独列出一个AOP文件夹,写下所有通知相关代码// MyAdvice通知类package com.itheima.aop;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.aspectj.lang.annotation.Pointcut;import org.springframework.stereotype.Component;/*具体流程:1.创建该类2.将该类设置为Spring中的Bean集中管理:@Component3.设置为切面类注明该类作用:@Aspect4.首先写出具体共性方法method,正常书写即可5.写出切入点pt:切入点定义依托一个不具有实际意义的方法进行,即无参数,无返回值,无方法体,最好私有6.对切入点进行设置:@Pointcut;我们后续讲解7.对切入点和通知进行连接:@Before;我们后续进行讲解这里简单介绍一下@Pointcut("execution(void com.itheima.dao.BookDao.update())")@Pointcut:注释execution:表示运行void:返回类型com.itheima.dao.BookDao.update():地址+类/接口+方法+方法参数*///通知类必须配置成Spring管理的bean@Component//设置当前类为切面类类@Aspectpublic class MyAdvice { //设置切入点,要求配置在方法上方 @Pointcut("execution(void com.itheima.dao.BookDao.update())") private void pt(){} //设置在切入点pt()的前面运行当前操作(前置通知) @Before("pt()") public void method(){ System.out.println(System.currentTimeMillis()); }}/*在运行后,我们会发现,每次调用方法后,在执行前给出当前系统时间*/
- 为SpringConfig设置相关需求
// SpringConfigpackage com.itheima.config;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.EnableAspectJAutoProxy;@Configuration@ComponentScan("com.itheima")//开启注解开发AOP功能(我们的通知类采用注解开发)@EnableAspectJAutoProxypublic class SpringConfig {}
SpringAOP工作流程
我们先简单介绍AOP的大概工作流程便于讲解底层知识:
- Spring容器启动
- 读取所有切面配置中的切入点
- 初始化bean,判定对应的类中的方法是否能匹配到任意切入点
- 获得bean执行方法
首先我们要注意切入点的读取问题:
- 我们在读取切入点时只读取匹配成功的切入点,其他未使用的切入点不进行读取,节省内存
然后我们会根据bean是否能匹配切入点来分别处理:
- 当我们匹配失败时,创建对象,获得bean,调用方法并执行,完成操作
- 当我们匹配成功时,创建原始对象(目标对象)的代理对象,获得代理对象的bean,根据代理对象的运行模式运行原始方法与增强内容,完成操作
这里我们进行几个名词解释:
目标对象:我们的初始对象,被一个或者多个切面所通知的对象
代理对象:我们根据目标对象所衍生出来的对象,不再是原对象;我们希望通过对代理对象的修改来完成AOP操作
SpringAOP切入点表达式
我们已经简单了解了SpringAOP的具体使用,接下来让我们来仔细分析AOP的各部分
首先我们先来介绍AOP的切入点和切入点表达式定义:
- 切入点:要进行加强的方法
- 切入点表达式:要进行增强的方法的描述方式
AOP切入点表达式大致分为两种:
- 接口下的方法:execution(void com.itheima.dao.BookDao.update())
- 实现类的方法:execution(void com.itheima.dao.impl.BookDaoImpl.update())
切入点表达式的具体格式:
- 动作关键字(访问修饰符 返回值 包名.类/接口.方法名(参数)异常名)
- execution(public User com.itheima.service.UserService.findById(int))
具体名词解释:
- 动作关键字:描述切入点的行为动作,例如execution表示执行到指定切入点
- 访问修饰符:public,private,可省略(省略为public)
- 异常名:方法定义中抛出指定异常,可省略
AOP切入点表达式通配符:
- *:单个的独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现
- ..:多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写
- +:专用于匹配子类类型
我们给出相关例子:
package com.itheima.aop;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.aspectj.lang.annotation.Pointcut;import org.springframework.stereotype.Component;@Component@Aspectpublic class MyAdvice { //切入点表达式: //表示接口下的方法// @Pointcut("execution(void com.itheima.dao.BookDao.update())") //表示实现类的方法// @Pointcut("execution(void com.itheima.dao.impl.BookDaoImpl.update())") //表示任意返回类型的单个参数的BookDaoImpl实现类的update方法// @Pointcut("execution(* com.itheima.dao.impl.BookDaoImpl.update(*))") //表示com开头两层文件(第三层为类或接口)的update方法// @Pointcut("execution(void com.*.*.*.update())") //表示所有以e结尾方法// @Pointcut("execution(* *..*e(..))") //表示以com开头所有无参方法// @Pointcut("execution(void com..*())") //表示com.itheima下的任意文件夹下的以Service结尾的实现类的以find开头的方法// @Pointcut("execution(* com.itheima.*.*Service.find*(..))") //执行com.itheima包下的任意包下的名称以Service结尾的类或接口中的save方法,参数任意,返回值任意 @Pointcut("execution(* com.itheima.*.*Service.save(..))") private void pt(){} @Before("pt()") public void method(){ System.out.println(System.currentTimeMillis()); }}
AOP切入点书写技巧:
- 所有代码按照规范开发,否则下述技巧失效
- 描述切入点通常描述接口,而不是描述实现类
- 访问控制修饰符针对接口开发均采用public描述(可省略访问控制修饰符描述)
- 返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用*通配快速描述
- 包名书写尽量不使用..匹配,效率过低,常用*做单个包描述匹配,或精准匹配
- 接口名/类名书写名称与模块相关的采用*匹配,例如UserService采用*Service,绑定业务层接口名
- 方法名书写以动词进行精准匹配,名词采用*匹配,例如getById采用getBy*
- 参数规则较为复杂,根据业务方法灵活调整
- 通常不使用异常作为匹配规则
SpringAOP通知类型
AOP通知描述了抽取的共性功能,根据共性功能抽取位置的不同,最终运行代码时要加入到合理的位置
AOP通知一共分为五种:
- 前置通知
package com.itheima.aop;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.*;import org.springframework.stereotype.Component;@Component@Aspectpublic class MyAdvice { @Pointcut("execution(void com.itheima.dao.BookDao.update())") private void pt(){} @Pointcut("execution(int com.itheima.dao.BookDao.select())") private void pt2(){} //@Before:前置通知,在原始方法运行之前执行 @Before("pt()") public void before() { System.out.println("before advice ..."); }}
- 后置通知
package com.itheima.aop;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.*;import org.springframework.stereotype.Component;@Component@Aspectpublic class MyAdvice { @Pointcut("execution(void com.itheima.dao.BookDao.update())") private void pt(){} @Pointcut("execution(int com.itheima.dao.BookDao.select())") private void pt2(){} //@After:后置通知,在原始方法运行之后执行 @After("pt2()") public void after() { System.out.println("after advice ..."); }}
- 环绕通知(重点)
package com.itheima.aop;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.*;import org.springframework.stereotype.Component;@Component@Aspectpublic class MyAdvice { @Pointcut("execution(void com.itheima.dao.BookDao.update())") private void pt(){} @Pointcut("execution(int com.itheima.dao.BookDao.select())") private void pt2(){} //@Around:环绕通知,在原始方法运行的前后执行 @Around("pt()") public Object around(ProceedingJoinPoint pjp) throws Throwable { System.out.println("around before advice ..."); //表示对原始操作的调用 Object ret = pjp.proceed(); System.out.println("around after advice ..."); return ret; }}/*@Around注意事项:1.环绕通知必须依赖形参ProceedingJoinPoint才能实现对原始方法的调用,进而实现原始方法调用前后同时添加通知2.通知中如果未使用ProceedingJoinPoint对原始方法进行调用将跳过原始方法的执行3.对原始方法的调用可以不接收返回值,通知方法设置为void即可,如果接收返回值,必须设置为Object类型4.原始方法的返回类型如果是void类型,通知方法的返回类型可以设置成void,也可以设置为Object5.由于无法预知原始方法运行后是否出现问题,因此需要抛出异常,抛出Throwable对象*/
- 返回后通知(了解)
package com.itheima.aop;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.*;import org.springframework.stereotype.Component;@Component@Aspectpublic class MyAdvice { @Pointcut("execution(void com.itheima.dao.BookDao.update())") private void pt(){} @Pointcut("execution(int com.itheima.dao.BookDao.select())") private void pt2(){} //@AfterReturning:返回后通知,在原始方法执行完毕后运行,且原始方法执行过程中未出现异常现象 @AfterReturning("pt2()") public void afterReturning() { System.out.println("afterReturning advice ..."); }}
- 抛出异常后通知(了解)
package com.itheima.aop;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.*;import org.springframework.stereotype.Component;@Component@Aspectpublic class MyAdvice { @Pointcut("execution(void com.itheima.dao.BookDao.update())") private void pt(){} @Pointcut("execution(int com.itheima.dao.BookDao.select())") private void pt2(){} //@AfterThrowing:抛出异常后通知,在原始方法执行过程中出现异常后运行 @AfterThrowing("pt2()") public void afterThrowing() { System.out.println("afterThrowing advice ..."); }}
下面我们针对环绕通知给出一个案例讲解:
需求:任意业务层接口执行均显示其执行效率(执行时长)
// 我们这里只给出SpringAOP的代码解释package com.itheima.aop;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.Signature;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Pointcut;import org.springframework.stereotype.Component;@Component@Aspectpublic class ProjectAdvice { //匹配业务层的所有方法 @Pointcut("execution(* com.itheima.service.*Service.*(..))") private void servicePt(){} //设置环绕通知,在原始操作的运行前后记录执行时间 @Around("servicePt()") public void runSpeed(ProceedingJoinPoint pjp) throws Throwable { //获取执行的签名对象 Signature signature = pjp.getSignature(); String className = signature.getDeclaringTypeName(); String methodName = signature.getName(); long start = System.currentTimeMillis(); for (int i = 0; i " +(end-start) + "ms"); }}
SpringAOP通知获得数据
我们可以注意到在上述通知中我们是存在有参数的,接下来我们针对这些参数做出相关解释~
通知可选参数:
- 环绕通知:ProceedingJoinPoint对象
- 其他通知:JoinPoint对象
注意:JoinPoint是ProceedingJoinPoint的父类
接下来我们分别从参数数据,返回值数据,异常数据三个方面进行讲解:
- 参数数据:JoinPoint对象描述了连接点方法的运行状态,可以获得到原始方法的调用参数
package com.itheima.aop;import org.aspectj.lang.JoinPoint;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.*;import org.springframework.stereotype.Component;import java.util.Arrays;@Component@Aspectpublic class MyAdvice { @Pointcut("execution(* com.itheima.dao.BookDao.findName(..))") private void pt(){} //JoinPoint:用于描述切入点的对象,必须配置成通知方法中的第一个参数,可用于获取原始方法调用的参数 @Before("pt()") public void before(JoinPoint jp) { Object[] args = jp.getArgs(); System.out.println(Arrays.toString(args)); System.out.println("before advice ..." ); } @After("pt()") public void after(JoinPoint jp) { Object[] args = jp.getArgs(); System.out.println(Arrays.toString(args)); System.out.println("after advice ..."); } //ProceedingJoinPoint:专用于环绕通知,是JoinPoint子类,可以实现对原始方法的调用 @Around("pt()") public Object around(ProceedingJoinPoint pjp) { Object[] args = pjp.getArgs(); System.out.println(Arrays.toString(args)); }}
- 返回值数据
package com.itheima.aop;import org.aspectj.lang.JoinPoint;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.*;import org.springframework.stereotype.Component;import java.util.Arrays;@Component@Aspectpublic class MyAdvice { @Pointcut("execution(* com.itheima.dao.BookDao.findName(..))") private void pt(){} @Around("pt()") public Object around(ProceedingJoinPoint pjp) { Object ret = pjp.proceed(args); return ret; } //设置返回后通知获取原始方法的返回值,要求returning属性值必须与方法形参名相同 @AfterReturning(value = "pt()",returning = "ret") public void afterReturning(JoinPoint jp,String ret) { System.out.println("afterReturning advice ..." + ret); }}
- 返回异常数据
package com.itheima.aop;import org.aspectj.lang.JoinPoint;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.*;import org.springframework.stereotype.Component;import java.util.Arrays;@Component@Aspectpublic class MyAdvice { @Pointcut("execution(* com.itheima.dao.BookDao.findName(..))") private void pt(){} @Around("pt()") public Object around(ProceedingJoinPoint pjp) { Object[] args = pjp.getArgs(); System.out.println(Arrays.toString(args)); args[0] = 666; Object ret = null; try { ret = pjp.proceed(args); } catch (Throwable t) { t.printStackTrace(); } return ret; } //设置抛出异常后通知获取原始方法运行时抛出的异常对象,要求throwing属性值必须与方法形参名相同 @AfterThrowing(value = "pt()",throwing = "t") public void afterThrowing(Throwable t) { System.out.println("afterThrowing advice ..."+t); }}
下面我们针对数据处理给出一个案例讲解:
需求:对密码的尾部空格作出兼容性处理
package com.itheima.aop;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Pointcut;import org.springframework.stereotype.Component;@Component@Aspectpublic class DataAdvice { @Pointcut("execution(boolean com.itheima.service.*Service.*(*,*))") private void servicePt(){} @Around("DataAdvice.servicePt()") public Object trimStr(ProceedingJoinPoint pjp) throws Throwable { Object[] args = pjp.getArgs(); for (int i = 0; i < args.length; i++) { //判断参数是不是字符串 if(args[i].getClass().equals(String.class)){ args[i] = args[i].toString().trim(); } } Object ret = pjp.proceed(args); return ret; }}
Spring事务
我们在之前的文章中已经多次提及过事务,这里再重新声明一下事务:
- 事务作用:在数据层保障一系列的数据库操作同成功与失败
- Spring事务作用:在数据层或业务层保障一系列的数据库操作成功与失败
Spring事务入门
我们通过一个案例来进行事务的讲解:
需求:实现任意两个账户间转账操作
需求微缩:A账户减钱,B账户加钱
分析:
- 数据层提供基础操作,指定用户减钱,指定用户加钱
- 业务层提供转账操作,调用减钱和加钱操作
- 提供两个账号和操作金额执行转账操作
- 基于Spring整合MyBatis环境搭配上述操作
结果分析:
- 程序正常执行,账户A减钱账户B加钱
- 程序失败执行,转账失败,但异常前操作成功,异常后操作失败,整体业务失败
具体修改实施步骤:
- 业务层接口上添加Spring事务管理
package com.itheima.service;import org.springframework.transaction.annotation.Transactional;import java.io.FileNotFoundException;import java.io.IOException;public interface AccountService { /** * 转账操作 * @param out 传出方 * @param in 转入方 * @param money 金额 */ //配置当前接口方法具有事务 @Transactional public void transfer(String out,String in ,Double money) ;}/*Spring注解式事务通常添加在业务层接口而不会添加到业务层实现类,降低耦合注解式事务可以添加到业务方法上表示当前方法开始事务,也可以添加到接口上表示当前接口所有方法开启事务*/
- 设置事务管理器
package com.itheima.config;import com.alibaba.druid.pool.DruidDataSource;import org.springframework.beans.factory.annotation.Value;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.jdbc.datasource.DataSourceTransactionManager;import org.springframework.transaction.PlatformTransactionManager;import javax.sql.DataSource;public class JdbcConfig { @Value("${jdbc.driver}") private String driver; @Value("${jdbc.url}") private String url; @Value("${jdbc.username}") private String userName; @Value("${jdbc.password}") private String password; @Bean public DataSource dataSource(){ DruidDataSource ds = new DruidDataSource(); ds.setDriverClassName(driver); ds.setUrl(url); ds.setUsername(userName); ds.setPassword(password); return ds; } //配置事务管理器,mybatis使用的是jdbc事务 @Bean public PlatformTransactionManager transactionManager(DataSource dataSource){ DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(); transactionManager.setDataSource(dataSource); return transactionManager; }}/*同MyBatis的配置java文件一样上述语句基本属于固定语句事务管理器根据实现技术进行选择MyBatis框架使用的是Jdbc事务*/
- 开启注解式事务驱动
package com.itheima.config;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Import;import org.springframework.context.annotation.PropertySource;import org.springframework.transaction.annotation.EnableTransactionManagement;@Configuration@ComponentScan("com.itheima")@PropertySource("classpath:jdbc.properties")@Import({JdbcConfig.class,MybatisConfig.class})//开启注解式事务驱动@EnableTransactionManagementpublic class SpringConfig {}
这里我们介绍两个新概念:
- 事务管理员:发起事务方,在Spring中通常指业务层开启事务的方法(上述表示transfer方法)
- 事务协调员:加入事务方,在Spring中通常代表数据层方法,也可以是业务层方法(上述表示out和in方法)
Spring事务属性
Spring的事务通常用@Transactional注解来表示
我们同样可以为@Transactional注解携带一些信息来管理事务的属性
属性 | 作用 | 示例 |
---|---|---|
readOnly | 设置是否为只读事务 | readOnly=true 只读事务 |
timeout | 设置事务超时时间 | timeout=-1永不超时 |
rollbackFor | 设置事务回滚异常(class) | rollbackFor={NullPointException.class} |
rollbackForClassName | 设置事务回滚异常(String) | 同上格式为字符串 |
noRollbackFor | 设置事务不回滚异常(class) | noRollbackFor={NullPointException.class} |
noRollbackForClassName | 设置事务不回滚异常(String) | 同上格式为字符串 |
propagation | 设置事务传播行为 | …….. |
除了上述属性外,我们还需要仔细介绍propagation属性:
- 事务传播行为:事务协调员对事务管理员所携带事务的处理态度
在实际开发中我们会利用propagation属性完成一些特殊操作
我们采用一个案例来进行说明:
需求:在上述转账的基础上,无论失败成功均保存一条日志记录转账信息
需求微缩:A账户减钱,B账户加钱,数据库记录日志
分析:
- 基于转账操作案例添加日志模块,实现数据库中记录日志
- 业务层转账操作,调用减钱,加钱与记录日志功能
实现效果预期:
- 无论转账操作是否成功,均进行日志记录
存在问题:
- 日志记录与转账操作隶属于一个事务,两者只能同时成功同时失败
新增代码:
- 给出日志相关信息
// 日志数据层package com.itheima.dao;import org.apache.ibatis.annotations.Insert;public interface LogDao { @Insert("insert into tbl_log (info,createDate) values(#{info},now())") void log(String info);}
// 日志业务层package com.itheima.service.impl;import com.itheima.dao.LogDao;import com.itheima.service.LogService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Propagation;import org.springframework.transaction.annotation.Transactional;@Servicepublic class LogServiceImpl implements LogService { @Autowired private LogDao logDao; public void log(String out,String in,Double money ) { logDao.log("转账操作由"+out+"到"+in+",金额:"+money); }}
- 主函数
package com.itheima.service.impl;import com.itheima.dao.AccountDao;import com.itheima.service.AccountService;import com.itheima.service.LogService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;import java.io.*;@Servicepublic class AccountServiceImpl implements AccountService { @Autowired private AccountDao accountDao; @Autowired private LogService logService; public void transfer(String out,String in ,Double money) { try{ accountDao.outMoney(out,money); int i = 1/0; accountDao.inMoney(in,money); }finally { logService.log(out,in,money); } }}
修改后代码:
- 在日志业务层设置日志操作为单独事务
// 日志业务层package com.itheima.service.impl;import com.itheima.dao.LogDao;import com.itheima.service.LogService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Propagation;import org.springframework.transaction.annotation.Transactional;@Servicepublic class LogServiceImpl implements LogService { @Autowired private LogDao logDao; @Transactional(propagation = Propagation.REQUIRES_NEW) public void log(String out,String in,Double money ) { logDao.log("转账操作由"+out+"到"+in+",金额:"+money); }}
最后我们给出事务传播行为表:
传播属性 | 事务管理员 | 事务协调员 |
---|---|---|
REQUIRED(默认) | 开启T | 加入T |
REQUIRED(默认) | 无 | 新建T |
REQUIRES_NEW | 开启T | 新建T |
REQUIRES_NEW | 无 | 新建T |
SUPPORTS | 开启T | 加入T |
SUPPORTS | 无 | 无 |
NOT_SUPPORTED | 开启T | 无 |
NOT_SUPPORTED | 无 | 无 |
MANDATORY | 开启T | 加入T |
MANDATORY | 无 | ERROR |
NEVER | 开启T | ERROR |
NEVER | 无 | 无 |
NESTED |
NESTED:设置savePoint,一旦事务回滚,事务将回滚到savePoint处,交由客户响应提交/回滚
结束语
好的,关于Spring的内容就介绍到这里,希望能为你带来帮助!
附录
该文章属于学习内容,具体参考B站黑马程序员李老师的SMM框架课程
这里附上链接:Spring-00-Spring课程介绍_哔哩哔哩_bilibili