目录
3.4、在配置文件中配置的类必须是自定义的吗,可以使用JDK中的类吗,例如:java.util.Date?
3.4、ClassPathXmlApplicationContext是从类路径中加载配置文件,如果没有在类路径当中,又应该如何加载配置文件呢?
3.5、从spring容器中获取的bean对象默认是Object类型 ,如何准确获取对应的对象类型?
7.6、BeanFactory和FactoryBean的区别(经典面试题)
9.2 单例singleton + set注入模式产生循环依赖
十五、AOP面向切面编程(Aspect Oriented Programming)
mybatis实现原理是:xml解析+代理模式+反射机制
spring实现原理是:xml解析+工厂模式+反射机制
代理模式的定义:为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。
工厂模式:是我们最常用的实例化对象模式了,是用工厂方法代替new操作的一种模式。以后new时就要多个心眼,是否可以考虑使用工厂模式,虽然这样做,可能多做一些工作,但会给你系统带来更大的可扩展性和尽量少的修改量。
Spring在什么时候会解析spring.xml?
在你调用
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml"); 这一行代码的时候
一、启示录
在三层架构中,持久层操作数据库,业务层逻辑层用来调用持久层处理业务逻辑,显示层调用业务层负责响应展示的。
/**
* 基于mysql数据库的一个持久层实现类
*/
public class UserDaoImplForMySQL implements UserDao {
@Override
public void deleteById() {
System.out.println("mysql正在删除用户信息......");
}
}
//业务层
public class UserServiceImpl implements UserService {
//业务层是调用持久层
private UserDao userDao = new UserDaoImplForMySQL();
@Override
public void deleteUser() {
userDao.deleteById();
//处理业务的方法
}
}
/**
* 表示层,相当于mvc中的控制器
*/
public class UserAction{
//表示层调业务层,业务层调持久层
private UserService userService = new UserServiceImpl();
//删除用户请求
public void deleteUserRequest(){
userService.deleteUser();
}
}
public class Test {
public static void main(String[] args) {
UserAction userAction = new UserAction();
userAction.deleteUserRequest();
}
}
可以看出,UserDaoImplForMySQL中主要是连接MySQL数据库进行操作。
如果更换到Oracle数据库上,则需要再提供一个UserDaoImplForOracle,如下:
很明显,以上的操作正在进行功能的扩展,添加了一个新的类UserDaoImplForOracle来应付数据库的变化,这里的变化会引起连锁反应吗?当然会,如果想要切换到Oracle数据库上,UserServiceImpl类代码就需要修改,如下:
//业务层
public class UserServiceImpl implements UserService {
//业务层是调用持久层
//private UserDao userDao = new UserDaoImplForMySQL();
private UserDao userDao = new UserDaoImplForOracle();
@Override
public void deleteUser() {
userDao.deleteById();
//处理业务的方法
}
}
以上代码明显耦合度太高,这很明显不符合ocp原则以及DIP原则。
你可能会说,上面的代码已经面向接口编程了呀:
确实已经面向接口编程了,但对象的创建是:new UserDaoImplForOracle()显然并没有完全面向接口编程,还是使用到了具体的接口实现类。什么叫做完全面向接口编程?什么叫做完全符合依赖倒置原则呢?请看以下代码:
//业务层
public class UserServiceImpl implements UserService {
//业务层是调用持久层
//private UserDao userDao = new UserDaoImplForMySQL();
private UserDao userDao;
@Override
public void deleteUser() {
userDao.deleteById();
//处理业务的方法
}
}
如果代码是这样编写的,才算是完全面向接口编程,才符合依赖倒置原则。那你可能会问,这样userDao是null,在执行的时候就会出现空指针异常呀。你说的有道理,确实是这样的,所以我们要解决这个问题。解决空指针异常的问题,其实就是解决两个核心的问题:
- 第一个问题:谁来负责对象的创建。【也就是说谁来:new UserDaoImplForOracle()/new UserDaoImplForMySQL()】
- 第二个问题:谁来负责把创建的对象赋到这个属性上。【也就是说谁来把上面创建的对象赋给userDao属性】
如果我们把以上两个核心问题解决了,就可以做到既符合OCP开闭原则,又符合依赖倒置原则。
很荣幸的通知你:Spring框架可以做到。
在Spring框架中,它可以帮助我们new对象,并且它还可以将new出来的对象赋到属性上。换句话说,Spring框架可以帮助我们创建对象,并且可以帮助我们维护对象和对象之间的关系。比如:
1. OCP开闭原则 * 什么是OCP? OCP是软件七大开发原则当中最基本的一个原则:开闭原则 对什么开?对扩展开放。 对什么闭?对修改关闭。 * OCP原则是最核心的,最基本的,其他的六个原则都是为这个原则服务的。 * OCP开闭原则的核心是什么? 只要你在扩展系统功能的时候,没有修改以前写好的代码,那么你就是符合OCP原则的。 反之,如果在扩展系统功能的时候,你修改了之前的代码,那么这个设计是失败的,违背OCP原则。 * 当进行系统功能扩展的时候,如果动了之前稳定的程序,修改了之前的程序,之前所有程序都需要进行重新测试。这是不想看到的,因为非常麻烦。 可以很明显的看出,上层是依赖下层的。UserController依赖UserServiceImpl,而UserServiceImpl依赖UserDaoImplForMySQL,这样就会导致下面只要改动,上面必然会受牵连(跟着也会改),所谓牵一发而动全身。这样也就同时违背了另一个开发原则:依赖倒置原则。 2. 依赖倒置原则(DIP原则) * 什么是依赖倒置原则? 面向接口编程,面向抽象编程,不要面向具体编程。 * 依赖倒置原则的目的? 降低程序的耦合度,提高扩展力。 * 什么叫做符合依赖倒置? 上 不依赖 下,就是符合。 * 什么叫做违背依赖倒置? 上 依赖 下,就是违背。 只要“下”一改动,“上”就受到牵连。 3. 当前程序的设计,显然既违背OCP,又违背DIP,怎么办? 可以采用“控制反转”这种编程思想来解决这个问题。 4. 什么是控制反转? 控制反转:IoC(Inversion of Control) 反转是什么呢? 反转的是两件事: 第一件事:我不在程序中采用硬编码的方式来new对象了。(new对象我不管了,new对象的权利交出去了。) 第二件事:我不在程序中采用硬编码的方式来维护对象的关系了。(对象之间关系的维护权,我也不管了,交出去了。) 控制反转:是一种编程思想。或者叫做一种新型的设计模式。由于出现的比较新,没有被纳入GoF23种设计模式范围内。 5. Spring框架 * Spring框架实现了控制反转IoC这种思想 Spring框架可以帮你new对象。 Spring框架可以帮你维护对象和对象之间的关系。 * Spring是一个实现了IoC思想的容器。 * 控制反转的实现方式有多种,其中比较重要的叫做:依赖注入(Dependency Injection,简称DI)。 * 控制反转是思想。依赖注入是这种思想的具体实现。 * 依赖注入DI,又包括常见的两种方式: 第一种:set注入(执行set方法给属性赋值) 第二种:构造方法注入(执行构造方法给属性赋值) * 依赖注入 中 “依赖”是什么意思? “注入”是什么意思? 依赖:A对象和B对象的关系。 注入:是一种手段,通过这种手段,可以让A对象和B对象产生关系。 依赖注入:对象A和对象B之间的关系,靠注入的手段来维护。而注入包括:set注入和构造注入。 6. 注意术语: OCP:开闭原则(开发原则) DIP:依赖倒置原则(开发原则) IoC:控制反转(一种思想,一种新型的设计模式) DI:依赖注入(控制反转思想的具体实现方式)
面试点:
什么是开闭原则(OCP)?
对扩张开放,对修改关闭
什么是控制反转(IoC)?
是一种编程思想。或者叫做一种新型的设计模式,控制反转的实现方式有多种,其中比较重要的叫做依赖注入
什么是依赖注入(DI)?
依赖:A对象和B对象的关系。 注入:是一种手段,通过这种手段,可以让A对象和B对象产生关系。 依赖注入:对象A和对象B之间的关系,靠注入的手段来维护。而注入包括:set注入和构造注
二、Spring概述
2.1 Spring简介
Spring是一个开源框架,它由Rod Johnson创建。它是为了解决企业应用开发的复杂性而创建的。
从简单性、可测试性和松耦合的角度而言,任何Java应用都可以从Spring中受益。
Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架。
Spring最初的出现是为了解决EJB臃肿的设计,以及难以测试等问题。
Spring为简化开发而生,让程序员只需关注核心业务的实现,尽可能的不再关注非业务逻辑代码(事务控制,安全日志等)。
2.2 Spring8大模块
注意:Spring5版本之后是8个模块。在Spring5中新增了WebFlux模块。
1.Spring Core模块
这是Spring框架最基础的部分,它提供了依赖注入(DependencyInjection)特征来实现容器对Bean的管理。核心容器的主要组件是 BeanFactory,BeanFactory是工厂模式的一个实现,是任何Spring应用的核心。它使用IoC将应用配置和依赖从实际的应用代码中分离出来。
2.Spring Context模块
如果说核心模块中的BeanFactory使Spring成为容器的话,那么上下文模块就是Spring成为框架的原因。这个模块扩展了BeanFactory,增加了对国际化(I18N)消息、事件传播、验证的支持。另外提供了许多企业服务,例如电子邮件、JNDI访问、EJB集成、远程以及时序调度(scheduling)服务。也包括了对模版框架例如Velocity和FreeMarker集成的支持
3.Spring AOP模块
Spring在它的AOP模块中提供了对面向切面编程的丰富支持,Spring AOP 模块为基于 Spring 的应用程序中的对象提供了事务管理服务。通过使用 Spring AOP,不用依赖组件,就可以将声明性事务管理集成到应用程序中,可以自定义拦截器、切点、日志等操作。
4.Spring DAO模块
提供了一个JDBC的抽象层和异常层次结构,消除了烦琐的JDBC编码和数据库厂商特有的错误代码解析,用于简化JDBC。
5.Spring ORM模块
Spring提供了ORM模块。Spring并不试图实现它自己的ORM解决方案,而是为几种流行的ORM框架提供了集成方案,包括Hibernate、JDO和iBATIS SQL映射,这些都遵从 Spring 的通用事务和 DAO 异常层次结构。
6.Spring Web MVC模块
Spring为构建Web应用提供了一个功能全面的MVC框架。虽然Spring可以很容易地与其它MVC框架集成,例如Struts,但Spring的MVC框架使用IoC对控制逻辑和业务对象提供了完全的分离。
7.Spring WebFlux模块
Spring Framework 中包含的原始 Web 框架 Spring Web MVC 是专门为 Servlet API 和 Servlet 容器构建的。反应式堆栈 Web 框架 Spring WebFlux 是在 5.0 版的后期添加的。它是完全非阻塞的,支持反应式流(Reactive Stream)背压,并在Netty,Undertow和Servlet 3.1+容器等服务器上运行。
8.Spring Web模块
Web 上下文模块建立在应用程序上下文模块之上,为基于 Web 的应用程序提供了上下文,提供了Spring和其它Web框架的集成,比如Struts、WebWork。还提供了一些面向服务支持,例如:实现文件上传的multipart请求。
2.3spring的特点
1.轻量
a.从大小与开销两方面而言Spring都是轻量的。完整的Spring框架可以在一个大小只有1MB多的JAR文件里发布。并且Spring所需的处理开销也是微不足道的。
b.Spring是非侵入式的:Spring应用中的对象不依赖于Spring的特定类。
(非入侵式指的是说spring框架中的所有api都不需要依赖于其他服务和框架)
2.控制反转(spring的核心、基石)
Spring通过一种称作控制反转(IoC)的技术促进了松耦合。当应用了IoC,一个对象依赖的其它对象会通过被动的方式传递进来,而不是这个对象自己创建或者查找依赖对象。你可以认为IoC与JNDI相反——不是对象从容器中查找依赖,而是容器在对象初始化时不等对象请求就主动将依赖传递给它。
3.面向切面
Spring提供了面向切面编程的丰富支持,允许通过分离应用的业务逻辑与系统级服务(例如审计(auditing)和事务(transaction)管理)进行内聚性的开发。应用对象只实现它们应该做的——完成业务逻辑——仅此而已。它们并不负责(甚至是意识)其它的系统级关注点,例如日志或事务支持。(学完你就理解了)
4.容器
Spring包含并管理应用对象的配置和生命周期,在这个意义上它是一种容器,你可以配置你的每个bean如何被创建——基于一个可配置原型(prototype),你的bean可以创建一个单独的实例或者每次需要时都生成一个新的实例——以及它们是如何相互关联的。然而,Spring不应该被混同于传统的重量级的EJB容器,它们经常是庞大与笨重的,难以使用。
(之所以称spring是一个容器是因为他可以动态的管理你的java对象,并且他把里面管理的每一个java对象称为bean,那放置bean翻译为豆子的框架就像是一个容器,之所以需要动态的管理这个javabean对象是因为如果你在底层采用硬编码的方式把javabean对象写死,那么你后面需要扩展或者切换某些服务则需要修改源代码,这样很明显不符合开闭原则ocp和依赖导致原则dip,就比如前面那个启示录中的代码)
5.框架
Spring可以将简单的组件配置、组合成为复杂的应用。在Spring中,应用对象被声明式地组合,典型地是在一个XML文件里。Spring也提供了很多基础功能(事务管理、持久化框架集成等等),将应用逻辑的开发留给了你。
三、spring入门
3.1案例
//创建maven项目,引入依赖
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.2</version>
</dependency>
</dependencies>
//创建java类,这个java类实例化后就是一个bean对象
public class User {
public User() {
System.out.println("hhh");
}
}
<!--创建bead的配置文件,注意建议放在resources下-->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--这个名字不一定要叫spring.xml,随意-->
<!--ioc一共就两件事,1.创建对象 2.管理对象与对象之间的依赖关系-->
<!--
bean标签的两个重要属性:
id:这个bean的身份证号,不能重复,唯一标识符
class:必须是类的全路径(带包名的类名字)
-->
<bean id="userBean" class="com.yzh.spring6.bean.User"/>
</beans>
//测试
public class firstSpringTest {
@Test
public void testUser(){
//1.获取spring容器对象
//ApplicationContext翻译为应用上下文。 他是一个接口,其实就是spring容器
//ClassPathXmlApplicationContext是ApplicationContext接口中的一个实现类
//专门从类的根路径下开始找bean对象的配置文件,并加载
//注意:这行代码只要一执行就相当于启动Spring容器,解析spring.xml,并且实例
//化spring.xml下关联的所有bean对象
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
//经过上面一行代码我获取到了一个Spring容器,并且把spring.xml中关联的所有
//bean对象实例化放到这个容器里面了
//2.根据bean对象的id从Spring容器中获取这个对象
Object userBean = applicationContext.getBean("userBean");
System.out.println(userBean);
}
}
架构图:
测试结果
3.2、spring是如何创建对象的?
底层通过反射机制得到类的字节码文件,然后调用类的无参构造方法。
所以所以要想让spring给你创建对象,必须保证无参数构造方法是存在的。
大致如下:
Class clazz = Class.forName("com.powernode.spring6.bean.User");
Object obj = clazz.newInstance();
3.3、spring创建好的bean对象是如何储存的?
3.4、在配置文件中配置的类必须是自定义的吗,可以使用JDK中的类吗,例如:java.util.Date?
在spring配置文件中配置的bean可以任意类,只要这个类不是抽象的,并且提供了无参数构造方法。
3.4、ClassPathXmlApplicationContext是从类路径中加载配置文件,如果没有在类路径当中,又应该如何加载配置文件呢?
没有在类路径中的话,需要使用FileSystemXmlApplicationContext类进行加载配置文件。
这种方式较少用。一般都是将配置文件放到类路径当中,这样可移植性更强。
ApplicationContext applicationContext2 = new FileSystemXmlApplicationContext("d:/spring6.xml");
Vip vip = applicationContext2.getBean("vipBean2", Vip.class);
3.5、从spring容器中获取的bean对象默认是Object类型 ,如何准确获取对应的对象类型?
只需要在获取对象时多加一个参数即可,如下:
User userBean = applicationContext.getBean("userBean", User.class);
3.6、你还需要知道的细节
1.在一个模块下像spring.xml这样的"bean.xml"配置文档可以有多个
2."bean.xml"配置文档的名字随意
3.ApplicationContext的超级父接口BeanFactory
BeanFactory是Spring容器的超级接口。ApplicationContext是BeanFactory的子接口。
3.7、Spring6启用Log4j2日志框架
从Spring5之后,Spring框架支持集成的日志框架是Log4j2,但是我们得手动配置、启用。
第一步:引入Log4j2的依赖
<!--log4j2的依赖--> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.19.0</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-slf4j2-impl</artifactId> <version>2.19.0</version> </dependency>
第二步:在类的根路径下提供log4j2.xml配置文件(文件名固定为:log4j2.xml,文件必须放到类根路径下。)
<?xml version="1.0" encoding="UTF-8"?> <configuration> <loggers> <!-- level指定日志级别,从低到高的优先级: ALL < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFF --> <root level="DEBUG"> <appender-ref ref="spring6log"/> </root> </loggers> <appenders> <!--输出日志信息到控制台--> <console name="spring6log" target="SYSTEM_OUT"> <!--控制日志输出的格式--> <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss SSS} [%t] %-3level %logger{1024} - %msg%n"/> </console> </appenders> </configuration>
四、Spring对IoC的实现 (最核心)
4.1 IoC 控制反转
控制反转是一种思想
控制反转是为了降低程序耦合度,提高程序扩展力,达到OCP原则,达到DIP原则.
控制反转,反转的是什么?
将对象的创建权利交出去,交给第三方容器负责
将对象和对象之间关系的维护权交出去,交给第三方容器负责控制反转这种思想如何实现呢?采用的是 依赖注入 (Dependency Injection) 简称 DI
4.2 依赖注入
依赖注入实现了控制反转的思想。
Spring通过依赖注入的方式来完成Bean管理的。
Bean管理说的是:Bean对象的创建,以及Bean对象中属性的赋值(或者叫做Bean对象之间关系的维护)。
4.2.1 set注入
set注入,基于set方法实现的,底层会通过反射机制调用属性对应的set方法然后给属性赋值。这种方式要求属性必须对外提供set方法。
案例:
架构:
持久层:UserDao
public class UserDao {
public void insert(){
System.out.println("正在保存用户数据。");
}
}
业务层:UserService
public class UserService {
private UserDao userDao;
// 使用set方式注入,必须提供set方法。
// 反射机制要调用这个方法给属性赋值的。
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public void save(){
userDao.insert();
}
}
bean配置文档:spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--
这里是关键
思路:想清楚你现在要解决什么?
==========================================
业务层代码:
private UserDao userDao;
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public void save(){
userDao.insert();
}
===========================================
你要做的是要调用业务层的save方法时解决userDao参数的空指针异常,
那么我创建持业务层对象时就得把这个持久层对象UserDao给传进去。
但是,我们知道使用spring容器来创建bean对象他只会调用类的无参构造方法
那么他是怎么把这个持久层的对象传进去呢?
他是先创建好业务层的对象后在调用业务层的set方法来进行传参的
步骤:
先创建一个持久层的对象UserDao(这个对象提供给业务层中的set方法),
然后在创建一个业务层对象,注意在业务层对象的bean标签中需要内置一个
property标签用于传参,参数就是前面创建好的那个持久层对象。
在property标签属性解读:
name:业务层UserService中的setXxx方法的xxx(去掉get,Xxx首字母小写)
ref:翻译为引用,value为spring容器中的bead对象id
比如下面的:
我再spring容器中创建了两个bean对象,
userDaoBean对应com.yzh.spring6.dao.UserDao类
userServiceBean对应com.yzh.spring6.service.UserService
在创建完com.yzh.spring6.service.UserService对象后
调用里面的setUserDao(com.yzh.spring6.dao.UserDao userDao)
方法,把userDaoBean对象传递进去
-->
<bean id="userDaoBean" class="com.yzh.spring6.dao.UserDao"/>
<bean id="userServiceBean" class="com.yzh.spring6.service.UserService">
<property name="userDao" ref="userDaoBean"/>
</bean>
</beans>
测试:
public class test {
@Test
public void test1(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
UserService userServiceBean = applicationContext.getBean("userServiceBean", UserService.class);
userServiceBean.save();
}
}
另外,对于property标签来说,ref属性也可以采用标签的方式,但使用ref属性是多数的:
总结:set注入的核心实现原理:通过反射机制创建bean对象,通过创建好的bean对象调用set方法来给属性赋值,让两个对象之间产生关系。
4.2.2 构造注入
核心原理:通过调用构造方法来给属性赋值。
持久层
public class OrderDao {
public void deleteById(){
System.out.println("正在删除订单。。。");
}
}
业务层
public class StructureService {
private UserDao userDao;
public StructureService(UserDao userDao) {
this.userDao = userDao;
}
public void save(){
userDao.insert();
}
}
bean对象配置文档
<!--通过构造方法中的参数下标进行传递-->
<bean id="structureServiceBean" class="com.yzh.spring6.service.StructureService">
<constructor-arg index="0" ref="userDaoBean"/>
</bean>
<!--通过构造方法中的参数名字进行传递-->
<bean id="structureServiceBean" class="com.yzh.spring6.service.StructureService">
<constructor-arg name="userDao" ref="userDaoBean"/>
</bean>
测试
@Test
public void test2(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
StructureService structureServiceBean = applicationContext.getBean("structureServiceBean", StructureService.class);
structureServiceBean.save();
}
如果构造方法有两个参数:
业务层
public class OrderService {
private OrderDao orderDao;
private UserDao userDao;
// 通过反射机制调用构造方法给属性赋值
public OrderService(OrderDao orderDao, UserDao userDao) {
this.orderDao = orderDao;
this.userDao = userDao;
}
public void delete(){
orderDao.deleteById();
userDao.insert();
}
}
bean对象配置文档
<bean id="orderDaoBean" class="com.powernode.spring6.dao.OrderDao"/>
<bean id="orderServiceBean" class="com.powernode.spring6.service.OrderService">
<!--第一个参数下标是0-->
<constructor-arg index="0" ref="orderDaoBean"/>
<!--第二个参数下标是1-->
<constructor-arg index="1" ref="userDaoBean"/>
</bean>
<bean id="userDaoBean" class="com.powernode.spring6.dao.UserDao"/>
可以不使用下标也不使用名字
<bean id="orderDaoBean" class="com.powernode.spring6.dao.OrderDao"/>
<bean id="orderServiceBean" class="com.powernode.spring6.service.OrderService">
<!--没有指定下标,也没有指定参数名字-->
<constructor-arg ref="orderDaoBean"/>
<constructor-arg ref="userDaoBean"/>
</bean>
<bean id="userDaoBean" class="com.powernode.spring6.dao.UserDao"/>
通过测试得知,通过构造方法注入的时候:
- 可以通过下标
- 可以通过参数名
- 也可以不指定下标和参数名,可以类型自动推断(特是根据参数类型自动匹配的,但是若出现两个参数类型一样那么就会出错)不建议使用这种方式可读性太差。
4.3 set注入专题
依赖注入常用方式有两种,set和构造,set注入一般使用的最多的。
4.3.1 注入外部Bean
<?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="userDaoBean" class="com.powernode.spring6.dao.UserDao"/>
<bean id="userServiceBean" class="com.powernode.spring6.service.UserService">
<property name="userDao" ref="userDaoBean"/>
</bean>
</beans>
外部Bean的特点:bean定义到外面,在property标签中使用ref属性进行注入。通常这种方式是常用。
4.3.2 注入内部Bean
内部Bean的方式:在bean标签中嵌套bean标签。
<?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="userServiceBean" class="com.powernode.spring6.service.UserService">
<property name="userDao">
<bean class="com.powernode.spring6.dao.UserDao"/>
</property>
</bean>
</beans>
4.3.3 注入简单类型
我们之前在注入对象的时候是自己定义的类对象,如果像这种int类型的属性,我们称为简单类型,这种简单类型在注入的时候要使用value属性,不能使用ref
对象的属性是int类型:
public class User{
private int age;
public void setAge(int age){
this.age = age;
}
}
编写spring配置文件:spring-simple-type.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="userBean" class="com.powernode.spring6.beans.User">
<!--如果像这种int类型的属性,我们称为简单类型,这种简单类型在注入的时候要使用value属性,不能使用ref-->
<!--这样也可以<property name="age" value="20"/>-->
<property name="age">
<value>20</value>
</property>
</bean>
</beans>
需要特别注意:如果给简单类型赋值,使用value属性或value标签。而不是ref。
简单类型包括哪些呢?
通过源码分析得知,简单类型包括:
- 基本数据类型
- 基本数据类型对应的包装类
- String或其他的CharSequence子类
- Number子类
- Date子类
- Enum子类
- URI
- URL
- Temporal子类
- Locale
- Class
- 另外还包括以上简单值类型对应的数组类型。
这里有一个点需要注意:虽然spring把Date定义为简单类型,但是你在为他传值的时候必须按照Date的toString格式来赋值,这个很鸡肋,如下:
就是说你在spring.xml文档中把Date看做是简单类型,使用value对其进行注入那么就必须得使用Sun Mar 19 13:32:53 GMT+08:00 2023这样的格式进行传值,这个很鸡肋。我们一把不会把Date看做是一个简单数据类型来去注入,具体怎么操作后面会说到。
这里也算是一个面试点:spring里面简单数据类型有哪些?
八种简单数据类型+对应的包装类+字符串
+枚举+日期+时区(Temporal)
+URI+URL
+Local+Class
idea按两下shift搜索BeanUtils进去找到isSimpleValueType也可以看到他规定的有哪些简单的数据类型。
简单类型的经典应用是我们定义了一个数据源后让spring容器去管理这个数据源对象,给driver、url、user、password赋值(数据源都得连接数据库,并且数据源必须实现javax.sql.DataSource接口)
4.3.4 级联属性赋值(了解)
public class Clazz {
private String name;
public Clazz() {
}
public Clazz(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Clazz{" +
"name='" + name + '\'' +
'}';
}
}
public class Student {
private String name;
private Clazz clazz;
public Student() {
}
public Student(String name, Clazz clazz) {
this.name = name;
this.clazz = clazz;
}
public void setName(String name) {
this.name = name;
}
public void setClazz(Clazz clazz) {
this.clazz = clazz;
}
public Clazz getClazz() {
return clazz;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", clazz=" + clazz +
'}';
}
}
<?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="clazzBean" class="com.powernode.spring6.beans.Clazz"/>
<bean id="student" class="com.powernode.spring6.beans.Student">
<property name="name" value="张三"/>
<!--要点1:以下两行配置的顺序不能颠倒-->
<property name="clazz" ref="clazzBean"/>
<!--要点2:clazz属性必须有getter方法,就是说下面name的值
clazz.name spring底层是先调用getClazz()方法获取到Clazz对象
然后在调用Clazz对象中的setName()方法进行赋值的,这也是为什么
说上面这个要点1的必要性,若顺序颠倒你这个getClazz拿到的是一个空指针 -->
<property name="clazz.name" value="高三一班"/>
</bean>
</beans>
要点:
- 在spring配置文件中,如上,注意顺序。
- 在Student类中,clazz属性必须提供getter方法。
4.3.5 注入数组
当数组中的元素是简单类型:
Person
package com.powernode.spring6.beans;
import java.util.Arrays;
public class Person {
private String[] favariteFoods;
public void setFavariteFoods(String[] favariteFoods) {
this.favariteFoods = favariteFoods;
}
@Override
public String toString() {
return "Person{" +
"favariteFoods=" + Arrays.toString(favariteFoods) +
'}';
}
}
spring-array-simple.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="person" class="com.powernode.spring6.beans.Person">
<property name="favariteFoods">
<array>
<value>鸡排</value>
<value>汉堡</value>
<value>鹅肝</value>
</array>
</property>
</bean>
</beans>
DITest.testArraySimple()
@Test
public void testArraySimple(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-array-simple.xml");
Person person = applicationContext.getBean("person", Person.class);
System.out.println(person);
}
当数组中的元素是非简单类型:一个订单中包含多个商品。
Goods (翻译为商品)
package com.powernode.spring6.beans;
public class Goods {
private String name;
public Goods() {
}
public Goods(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Goods{" +
"name='" + name + '\'' +
'}';
}
}
Order (翻译为订单)
package com.powernode.spring6.beans;
import java.util.Arrays;
public class Order {
// 一个订单中有多个商品
private Goods[] goods;
public Order() {
}
public Order(Goods[] goods) {
this.goods = goods;
}
public void setGoods(Goods[] goods) {
this.goods = goods;
}
@Override
public String toString() {
return "Order{" +
"goods=" + Arrays.toString(goods) +
'}';
}
}
spring-array.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="goods1" class="com.powernode.spring6.beans.Goods">
<property name="name" value="西瓜"/>
</bean>
<bean id="goods2" class="com.powernode.spring6.beans.Goods">
<property name="name" value="苹果"/>
</bean>
<bean id="order" class="com.powernode.spring6.beans.Order">
<property name="goods">
<array>
<!--在这里,因为数组里面放的是对象,所以得用引用类标签
ref 全称reference翻译为引文、参考的意思,它就是用来
引用bean对象的
-->
<ref bean="goods1"/>
<ref bean="goods2"/>
</array>
</property>
</bean>
</beans>
要点:
- 如果数组中是简单类型,使用value标签。
- 如果数组中是非简单类型,使用ref标签。
4.3.6 注入List集合
List集合:有序可重复
People
package com.powernode.spring6.beans;
import java.util.List;
public class People {
// 集合中存人名,名字可重复
private List<String> names;
public void setNames(List<String> names) {
this.names = names;
}
@Override
public String toString() {
return "People{" +
"names=" + names +
'}';
}
}
spring-collection.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="peopleBean" class="com.powernode.spring6.beans.People">
<property name="names">
<list>
<value>铁锤</value>
<value>张三</value>
<value>张三</value>
<value>张三</value>
<value>狼</value>
</list>
</property>
</bean>
</beans>
注意:注入List集合的时候使用list标签,如果List集合中是简单类型使用value标签,反之使用ref标签。
4.3.7 注入Set集合
Set集合:无序不可重复
People
package com.powernode.spring6.beans;
import java.util.List;
import java.util.Set;
public class People {
// 电话不能重复
private Set<String> phones;
public void setPhones(Set<String> phones) {
this.phones = phones;
}
//......
@Override
public String toString() {
return "People{" +
"phones=" + phones +
", names=" + names +
'}';
}
}
spring-collection.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="peopleBean" class="com.powernode.spring6.beans.People">
<property name="phones">
<set>
<!--非简单类型可以使用ref,简单类型使用value-->
<value>110</value>
<value>110</value>
<value>120</value>
<value>120</value>
<value>119</value>
<value>119</value>
</set>
</property>
</bean>
</beans>
4.3.8 注入Map集合
People
package com.powernode.spring6.beans;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class People {
// 一个人有多个住址
private Map<Integer, String> addrs;
public void setAddrs(Map<Integer, String> addrs) {
this.addrs = addrs;
}
//......
@Override
public String toString() {
return "People{" +
"addrs=" + addrs +
", phones=" + phones +
", names=" + names +
'}';
}
}
spring-collection.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="peopleBean" class="com.powernode.spring6.beans.People">
<property name="addrs">
<map>
<!--如果key不是简单类型,使用 key-ref 属性-->
<!--如果value不是简单类型,使用 value-ref 属性-->
<!--一对键值对就是一个entry-->
<entry key="1" value="北京大兴区"/>
<entry key="2" value="上海浦东区"/>
<entry key="3" value="深圳宝安区"/>
</map>
</property>
</bean>
</beans>
4.3.9 注入Properties
java.util.Properties继承java.util.Hashtable,所以Properties也是一个Map集合。
People
package com.powernode.spring6.beans;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
public class People {
private Properties MySqlConncetion_Properties;
public void setMySqlConncetion_Properties(Properties properties) {
this.properties = properties;
}
//......
@Override
public String toString() {
return "People{" +
"MySqlConncetion_Properties=" + MySqlConncetion_Properties+
", addrs=" + addrs +
", phones=" + phones +
", names=" + names +
'}';
}
}
spring-collection.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="peopleBean" class="com.powernode.spring6.beans.People">
<property name="mySqlConncetion_Properties">
<props>
<prop key="driver">com.mysql.cj.jdbc.Driver</prop>
<prop key="url">jdbc:mysql://localhost:3306/spring</prop>
<prop key="username">root</prop>
<prop key="password">123456</prop>
</props>
</property>
</bean>
</beans>
4.3.10 注入null和空字符串
注入空字符串使用:<value/> 或者 value=""
注入null使用:<null/> 或者 不为该属性赋值
- 我们先来看一下,怎么注入空字符串。
Vip
package com.powernode.spring6.beans;
public class Vip {
private String email;
public void setEmail(String email) {
this.email = email;
}
@Override
public String toString() {
return "Vip{" +
"email='" + email + '\'' +
'}';
}
}
spring-null.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="vipBean" class="com.powernode.spring6.beans.Vip">
<!--空串的第一种方式-->
<!--<property name="email" value=""/>-->
<!--空串的第二种方式-->
<property name="email">
<value/>
</property>
</bean>
</beans>
- 怎么注入null呢?
-
第一种方式:不给属性赋值
spring-null.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="vipBean" class="com.powernode.spring6.beans.Vip" />
</beans>
第二种方式:使用<null/>
<?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="vipBean" class="com.powernode.spring6.beans.Vip">
<property name="email">
<null/>
</property>
</bean>
</beans>
4.3.11 注入的值中含有特殊符号
XML中有5个特殊字符,分别是:<、>、'、"、&
以上5个特殊符号在XML中会被特殊对待,会被当做XML语法的一部分进行解析,如果这些特殊符号直接出现在注入的字符串当中,会报错。
解决方案包括两种:
- 第一种:特殊符号使用转义字符代替。
- 第二种:将含有特殊符号的字符串放到:<![CDATA[]]> 当中。因为放在CDATA区中的数据不会被XML文件解析器解析。
5个特殊字符对应的转义字符分别是:
特殊字符 | 转义字符 |
> | > |
< | < |
' | ' |
" | " |
& | & |
先使用转义字符来代替:
Math
package com.powernode.spring6.beans;
public class Math {
private String result;
public void setResult(String result) {
this.result = result;
}
@Override
public String toString() {
return "Math{" +
"result='" + result + '\'' +
'}';
}
}
spring-special.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="mathBean" class="com.powernode.spring6.beans.Math">
<property name="result" value="2 < 3"/>
</bean>
</beans>
我们再来使用CDATA方式:
spring-special.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="mathBean" class="com.powernode.spring6.beans.Math">
<property name="result">
<!--只能使用value标签-->
<value><![CDATA[2 < 3]]></value>
</property>
</bean>
</beans>
4.4 p命名空间注入
什么是命名空间?
命名空间是用来组织和重用代码的。如同名字一样的意思,NameSpace(名字空间),之所以出来这样一个东西,是因为人类可用的单词数太少,并且不同的人写的程序不可能所有的变量都没有重名现象,对于库来说,这个问题尤其严重,如果两个人写的库文件中出现同名的变量或函数(不可避免),使用起来就有问题了。为了解决这个问题,引入了名字空间这个概念,通过使用 namespace xxx;你所使用的库函数或变量就是在该名字空间中定义的,这样一来就不会引起不必要的冲突了。在XML里,任何元素类别或者属性因此分为两部分名字,一个是命名空间里的名字另一个是它的本地名。在XML里,命名空间通常是一个统一资源识别符(URI)的名字。而URI只当名字用。主要目的是为了避免名字的冲突
目的:简化配置。
使用p命名空间注入的前提条件包括两个:
- 第一:在XML头部信息中添加p命名空间的配置信息:xmlns:p="http://www.springframework.org/schema/p"
- 第二:p命名空间注入是基于setter方法的,所以需要对应的属性提供setter方法。
面试点:p命名空间怎么注入的?
底层还是set方法去操作的注入的。
Customer
package com.powernode.spring6.beans;
public class Customer {
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 "Customer{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
spring-p.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:p="http://www.springframework.org/schema/p" ====》得添加这一行规范
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="customerBean" class="com.powernode.spring6.beans.Customer">
<property name="name" value="zhangsan" />
<property name="age" value="20" />
</bean>
<!--命名空间写法-->
<bean id="customerBean" class="com.powernode.spring6.beans.Customer"
p:name="zhangsan" p:age="20"/>
</beans>
注意:这个p命名空间写法底层还是通过set方法注入的,必须得有set方法。所以p命名空间实际上是对set注入的简化。
4.5 c命名空间注入
c命名空间是简化构造方法注入的。
使用c命名空间的两个前提条件:
第一:需要在xml配置文件头部添加信息:xmlns:c="http://www.springframework.org/schema/c"
第二:需要提供构造方法。
MyTime
package com.powernode.spring6.beans;
public class MyTime {
private int year;
private int month;
private int day;
public MyTime(int year, int month, int day) {
this.year = year;
this.month = month;
this.day = day;
}
@Override
public String toString() {
return "MyTime{" +
"year=" + year +
", month=" + month +
", day=" + day +
'}';
}
}
spring-c.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:c="http://www.springframework.org/schema/c" =====>得添加这一行规范
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="myTimeBean" class="com.powernode.spring6.beans.MyTime">
<constructor-arg index="" value=""/>
<constructor-arg name="" ref=""/>
</bean>
<!-- ===================================c命名空间写法============================================= -->
<!--<bean id="myTimeBean" class="com.powernode.spring6.beans.MyTime" c:year="1970" c:month="1" c:day="1"/>-->
<bean id="myTimeBean" class="com.powernode.spring6.beans.MyTime" c:_0="2008" c:_1="8" c:_2="8"/>
</beans>
所以,c命名空间是依靠构造方法的。
注意:不管是p命名空间还是c命名空间,注入的时候都可以注入简单类型以及非简单类型。
4.6 util命名空间
使用util命名空间可以让配置复用。(比如说我用util命名空间配置了一个链接数据库的信息)
然后我有两个数据源对象都需要连接同一个数据库,那么我这两个数据源都可以引用你配置的信息。
使用util命名空间的前提是:在spring配置文件头部添加配置信息。如下:
MyDataSource1
package com.powernode.spring6.beans;
import java.util.Properties;
public class MyDataSource1 {
private Properties properties;
public void setProperties(Properties properties) {
this.properties = properties;
}
@Override
public String toString() {
return "MyDataSource1{" +
"properties=" + properties +
'}';
}
}
MyDataSource2
package com.powernode.spring6.beans;
import java.util.Properties;
public class MyDataSource2 {
private Properties properties;
public void setProperties(Properties properties) {
this.properties = properties;
}
@Override
public String toString() {
return "MyDataSource2{" +
"properties=" + properties +
'}';
}
}
spring-util.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">
<util:properties id="prop">
<prop key="driver">com.mysql.cj.jdbc.Driver</prop>
<prop key="url">jdbc:mysql://localhost:3306/spring</prop>
<prop key="username">root</prop>
<prop key="password">123456</prop>
</util:properties>
<bean id="dataSource1" class="com.powernode.spring6.beans.MyDataSource1">
<property name="properties" ref="prop"/>
</bean>
<bean id="dataSource2" class="com.powernode.spring6.beans.MyDataSource2">
<property name="properties" ref="prop"/>
</bean>
</beans>
4.7自动装配
不想做笔记了.........
自动装配是基于set来去注入的,也是就是去匹配你的setXxx中的参数,有两种自动装配方式,一种是根据参数的名字,另外一种是根据参数的类型。
<bean id="userService" class="com.powernode.spring6.service.UserService" autowire="byName"/>
<bean id="aaa" class="com.powernode.spring6.dao.UserDao"/>
//关键:autowire="byName" 表示基于setXxx(Object aaa),根据根据参数名称自动去spring容器中
找到对应的bean对象
而对应的bean对象的id值必须为setXxx中的xxx
那根据上面的代码可以知道这个setXxx长这样:
public void setAaa(UserDao userDao) {
this.userDao= userDao;
}
切勿以为你这个被注入的bead的id为参数的名字,基于set注入那么不管你怎么自动装配
他都是基于setXxx中的xxx
根据类型自动装配:
前提条件:被注入的bean对象的的java类型在spring容器中是唯一的。也就是说在spring容器中不可以再出现另外一个相同类型的bean。
public class AccountService {
private AccountDao accountDao;
public void setAccountDao(AccountDao accountDao) {
this.accountDao = accountDao;
}
public void save(){
accountDao.insert();
}
}
===============================================================
<?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">
<!--byType表示根据类型自动装配-->
<bean id="accountService" class="com.powernode.spring6.service.AccountService" autowire="byType"/>
<bean class="com.powernode.spring6.dao.AccountDao"/>
</beans>
关键词: autowire="byType"
解析:他相当于在spring容器中创建了一个匿名的bean对象,类型为com.powernode.spring6.dao.AccountDao
调用com.powernode.spring6.service.AccountService的无参构造创建好对象后,因为你用了autowire属性,那这个属性他和property一样,会调用setXxx方法,拿到xxx后去匹配spring容器中的xxx类型的bean对象。那么重点就在于你的setXxx这个Xxx名字是一个类名,他需要和spring容器中的bean对象的类型匹配,你这个bean对象是否匿名无所谓。因为他是按照xxx类型去spring容器中匹配bean对象的,而这个xxx类型的bean对象在spring容器中只能有一个这样类型的bean对象。
4.8 Spring引入外部属性配置文件
我们都知道编写数据源的时候是需要连接数据库的信息的,例如:driver url username password等信息。这些信息可以单独写到一个属性配置文件中吗,这样用户修改起来会更加的方便。当然可以。
第一步:写一个数据源类,提供相关属性。
MyDataSource
package com.powernode.spring6.beans;
public class MyDataSource implements DataSource {
@Override
public String toString() {
return "MyDataSource{" +
"driver='" + driver + '\'' +
", url='" + url + '\'' +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
private String driver;
private String url;
private String username;
private String password;
public void setDriver(String driver) {
this.driver = driver;
}
public void setUrl(String url) {
this.url = url;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
//......
}
第二步:在类路径下新建jdbc.properties文件,并配置信息。
jdbc.properties
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/spring
username=root
password=root123
第三步:在spring配置文件中引入context命名空间。
<?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 http://www.springframework.org/schema/context/spring-context.xsd">
</beans>
第四步:在spring中配置使用jdbc.properties文件。
<?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 http://www.springframework.org/schema/context/spring-context.xsd">
<context:property-placeholder location="jdbc.properties"/>
<bean id="dataSource" class="com.powernode.spring6.beans.MyDataSource">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</bean>
</beans>
你会发现这里有一个瑕疵,就是你的username并不是root,而是系统用户名,因为你在properties文件中使用的是username这个key,而系统用户名的key也叫username,spring默认会先去找系统的用户名。所以一般这样写properties文档
然后再把bean.xml那里的value也改一下
这样子就可以了
五、Bean的作用域(重要)
代码解释:
singleton:单例
prototype:多例
scope:范围、作用域
5.1 单例singleton
我们在前面学习的时候,在bean.xml中的bean对象时其实省略了一个属性作用域scope,他的值有8个选项,其中重要的有两个,一个是单例singleton、一个是多例prototype多例,默认情况下scope的值为singleton。
看一个案例:
SpringBean
package com.powernode.spring6.beans;
public class SpringBean {
}
spring-scope.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="sb" class="com.powernode.spring6.beans.SpringBean" scope="singleton"/>
</beans>
<!--当前scope为single,可以省略不写,因为默认就是singleton-->
测试程序
@Test
public void testScope(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-scope.xml");
SpringBean sb1 = applicationContext.getBean("sb", SpringBean.class);
System.out.println(sb1);
SpringBean sb2 = applicationContext.getBean("sb", SpringBean.class);
System.out.println(sb2);
}
从测试结果可以看得出来每一次从spring容器中获取bean对象都是同一个。
问:如果配置的bean为单例模式,那么这个bean在什么时候会被创建?
答:在初始化Spring上下文的时候被创建。也就是在下面这一行代码执行的时候。
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("spring-scope.xml");
这行代码执行完后spring-scope.xml中的所有单例bean对象会被实例化。
注:是单例的bean才会被实例化哦,如果spring-scope.xml里面包含
多例的bean是不会在初始化Spring上下文的时候创建的
5.2 多例prototype
SpringBean
package com.powernode.spring6.beans;
public class SpringBean {
}
spring-scope.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="sb" class="com.powernode.spring6.beans.SpringBean" scope="prototype"/>
</beans>
测试程序
@Test
public void testScope(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-scope.xml");
SpringBean sb1 = applicationContext.getBean("sb", SpringBean.class);
System.out.println(sb1);
SpringBean sb2 = applicationContext.getBean("sb", SpringBean.class);
System.out.println(sb2);
}
可以看到每一次从spring容器中获取sb这个bean对象都不是同一个。
问:如果配置的bean为多例模式,那么这个bean在什么时候会被创建?
答:每从spring容器中获取一次这个bean对象就会创建一次。比如:
SpringBean sb1 = applicationContext.getBean("sb", SpringBean.class);
SpringBean sb2 = applicationContext.getBean("sb", SpringBean.class);
会创建两次
5.3scope的其他值
虽然他有八个值,但是这8个值都是用来限定当前这个bean对象的作用域的。
scope属性的值不止两个,它一共包括8个选项:
- singleton:默认的,单例,初始化spring上下文时创建。
- prototype:原型。每调用一次getBean()方法则获取一个新的Bean对象。或每次注入的时候都是新对象。
- request:一个请求对应一个Bean。仅限于在WEB应用中使用,需要引入web框架依赖。
- session:一个会话对应一个Bean。仅限于在WEB应用中使用需要引入web框架依赖。。
- global session:portlet应用中专用的。如果在Servlet的WEB应用中使用global session的话,和session一个效果。(portlet和servlet都是规范。servlet运行在servlet容器中,例如Tomcat。portlet运行在portlet容器中。)
- application:一个应用对应一个Bean。仅限于在WEB应用中使用。
- websocket:一个websocket生命周期对应一个Bean。仅限于在WEB应用中使用。
- 自定义scope:很少使用。
六、GoF之工厂模式
- 设计模式:一种可以被重复利用的解决方案。
- GoF(Gang of Four),中文名——四人组。
- 《Design Patterns: Elements of Reusable Object-Oriented Software》(即《设计模式》一书),1995年由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 合著。这几位作者常被称为"四人组(Gang of Four)"。
- 该书中描述了23种设计模式。我们平常所说的设计模式就是指这23种设计模式。
- 不过除了GoF23种设计模式之外,还有其它的设计模式,比如:JavaEE的设计模式(DAO模式、MVC模式等)。
- GoF23种设计模式可分为三大类:
- 创建型(5个):解决对象创建问题。
- 单例模式
- 原型模式
- 建造者模式
- 抽象工厂模式
- 工厂方法模式
- 结构型(7个):一些类或对象组合在一起的经典结构。
- 代理模式
- 桥接模式
- 外观模式
- 享元模式
- 组合模式
- 适配器模式
- 装饰模式
- 行为型(11个):解决类或对象之间的交互问题。
- 策略模式
- 解释器模式
- 中介者模式
- 访问者模式
- 状态模式
- 备忘录模式
- 命令模式
- 迭代子模式
- 观察者模式
- 责任链模式
- 模板方法模式
- 工厂模式是解决对象创建问题的,所以工厂模式属于创建型设计模式。这里为什么学习工厂模式呢?这是因为Spring框架底层使用了大量的工厂模式。
6.1 工厂模式的三种形态
工厂模式通常有三种形态:
- 第一种:简单工厂模式(Simple Factory):不属于23种设计模式之一。简单工厂模式又叫做:静态 工厂方法模式。简单工厂模式是工厂方法模式的一种特殊实现。
- 第二种:工厂方法模式(Factory Method):是23种设计模式之一。
- 第三种:抽象工厂模式(Abstract Factory):是23种设计模式之一。
6.2 简单工厂模式
简单工厂模式的角色包括三个:
- 抽象产品 角色
- 具体产品 角色
- 工厂类 角色
简单工厂模式的代码如下:
抽象产品角色:Weapon-武器
package com.powernode.factory;
/**
* 武器类(抽象产品角色)
**/
public abstract class Weapon {
/**
* 所有的武器都有攻击行为,attack翻译为攻击
*/
public abstract void attack();
}
具体产品角色:Tank、Fighter、Dagger (坦克、战斗机、匕首)
package com.powernode.factory;
/**
* 坦克(具体产品角色)
**/
public class Tank extends Weapon{
@Override
public void attack() {
System.out.println("坦克开炮!");
}
}
package com.powernode.factory;
/**
* 战斗机(具体产品角色)
**/
public class Fighter extends Weapon{
@Override
public void attack() {
System.out.println("战斗机投下原子弹!");
}
}
package com.powernode.factory;
/**
* 匕首(具体产品角色)
**/
public class Dagger extends Weapon{
@Override
public void attack() {
System.out.println("砍他丫的!");
}
}
工厂类角色:WeaponFactory 翻译为武器工厂
package com.powernode.factory;
/**
* 工厂类角色
* @author 动力节点
**/
public class WeaponFactory {
/**
* 根据不同的武器类型生产武器
* @param weaponType 武器类型
* @return 武器对象
*/
public static Weapon get(String weaponType){
if (weaponType == null || weaponType.trim().length() == 0) {
return null;
}
Weapon weapon = null;
if ("TANK".equals(weaponType)) {
weapon = new Tank();
} else if ("FIGHTER".equals(weaponType)) {
weapon = new Fighter();
} else if ("DAGGER".equals(weaponType)) {
weapon = new Dagger();
} else {
throw new RuntimeException("不支持该武器!");
}
return weapon;
}
}
测试程序(客户端程序):Client翻译为客户
package com.powernode.factory;
/**
* 客户端
**/
public class Client {
public static void main(String[] args) {
Weapon weapon1 = WeaponFactory.get("TANK");
weapon1.attack();
Weapon weapon2 = WeaponFactory.get("FIGHTER");
weapon2.attack();
Weapon weapon3 = WeaponFactory.get("DAGGER");
weapon3.attack();
}
}
简单工厂模式的优点:
- 客户端程序不需要关心对象的创建细节,需要哪个对象时,只需要向工厂索要即可,初步实现了责任的分离。客户端只负责“消费”,工厂负责“生产”。生产和消费分离。
简单工厂模式的缺点:
- 缺点1:工厂类集中了所有产品的创造逻辑,形成一个无所不知的全能类,有人把它叫做上帝类。显然工厂类非常关键,不能出问题,一旦出问题,整个系统瘫痪。
- 缺点2:不符合OCP开闭原则,在进行系统扩展时,需要修改工厂类。
Spring中的BeanFactory就使用了简单工厂模式。
6.3 工厂方法模式
工厂方法模式既保留了简单工厂模式的优点,同时又解决了简单工厂模式的缺点。
工厂方法模式的角色包括:
- 抽象工厂角色
- 具体工厂角色
- 抽象产品角色
- 具体产品角色
可以对比一下简单工厂模式
代码如下:
抽象产品角色:Weapon(武器类)
package com.powernode.factory;
/**
* 武器类(抽象产品角色)
**/
public abstract class Weapon {
/**
* 所有武器都有攻击行为
*/
public abstract void attack();
}
具体产品角色:Gun、Fighter (坦克、战斗机...这些都属于武器继承了武器类)
package com.powernode.factory;
/**
* 具体产品角色:枪
**/
public class Gun extends Weapon{
@Override
public void attack() {
System.out.println("开枪射击!");
}
}
package com.powernode.factory;
/**
* 具体产品角色:战斗机
**/
public class Fighter extends Weapon{
@Override
public void attack() {
System.out.println("战斗机发射核弹!");
}
}
抽象工厂角色:WeaponFactory(抽象武器工厂,所有的具体武器工厂都得实现它)
package com.powernode.factory;
/**
* 武器工厂接口(抽象工厂角色)
**/
//所有具体武器工厂都有获取武器的方法
public interface WeaponFactory {
Weapon get();
}
具体工厂角色(GunFactory、FighterFactory...不同的武器工厂制造不同的武器)
package com.powernode.factory;
/**
* 专门生产武器枪的工厂(具体工厂角色)
**/
public class GunFactory implements WeaponFactory{
@Override
public Weapon get() {
return new Gun();
}
}
package com.powernode.factory;
/**
* 专门生产武器战斗机的工厂(具体工厂角色)
**/
public class FighterFactory implements WeaponFactory{
@Override
public Weapon get() {
return new Fighter();
}
}
客户端程序:
package com.powernode.factory;
public class Client {
public static void main(String[] args) {
WeaponFactory factory = new GunFactory();
Weapon weapon = factory.get();
weapon.attack();
WeaponFactory factory1 = new FighterFactory();
Weapon weapon1 = factory1.get();
weapon1.attack();
}
}
如果想扩展一个新的产品,只要新增一个产品类,再新增一个该产品对应的工厂即可。
七、四种Bean的获取方式
7.1 通过构造方法实例化
我们之前一直使用的就是这种方式。默认情况下,会调用Bean的无参数构造方法。在spring配置文件中直接配置类全路径,Spring会自动调用该类的无参数构造方法来实例化Bear
User
public class User {
public User() {
System.out.println("User类的无参数构造方法执行");
}
}
spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="userBean" class="com.yzh.spring6.bean.User"/>
</beans>
@Test
public void test_Instantiation(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
User user = applicationContext.getBean("userBean", User.class);
System.out.println(user);
}
7.2通过简单工厂模式实例化
public class User {
public User() {
System.out.println("User类的无参数构造方法执行");
}
}
//根据静态方法获取Bean
public class UserFactoryByStatic {
public static User getUser(){
return new User();
}
}
<bean id="userFactoryByStaticBean" class="com.yzh.spring6.bean.UserFactory"
factory-method="getUser"/>
通过简单工厂模式。你需要在Spring配置文件中告诉Spring框架,调用哪个类的哪个方法获取Bean
factory-method 属性指定的是工厂类当中的哪一个方法来获取bean。也就是告诉Spring框架,调用这个方法可以获取Bean。
7.3通过工厂类获取Bean
public class User {
public User() {
System.out.println("User类的无参数构造方法执行");
}
}
//注意看下面这个方法不是静态的了,
// 也就是说你必须先把UserFactory这个工厂对象先在spring容器中创造出来
//你才可以让spring去调用getUser方法来获取bean
public class UserFactory {
public User getUser(){
return new User();
}
}
<!--先获取工厂类的bean对象,因为我的目的是获取User这个bean对象
但是在工厂类里面提供的获取方法是实例方法,所以我得先得到工厂实例-->
<bean id="userFactory" class="com.yzh.spring6.bean.UserFactory"/>
<!--根据工厂实例对象提供的方法获取目标bean对象
factory-bean="userFactory"属性是用来指定工厂bean的-->
<bean id="userFactory_getUser" factory-bean="userFactory" factory-method="getUser"/>
@Test
public void test_userFactoryBean(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
User user = applicationContext.getBean("userFactory_getUser", User.class);
System.out.println(user);
}
7.4通过FactoryBean接口来实现
这种方式实际上就是第三种方式的简化。由于你编写的类实现了FactoryBean接口,所以这个类是一个特殊的类,不需要你再手动指定: factory-bean、factory-method通过一个特殊的Bean—— 工厂Bean。来返回一个普通的Bean Person对象。通过FactoryBean这个工厂Bean 主要是想对普通Bean进行加工处理。
这个工厂接口FactoryBean就相当于第三种方式自定义的UserFactory。只要你实现了他并重写了他的方法就可以省略factory-bean、factory-method。
Perso类
public class Person {
public Person() {
System.out.println("Person的实例方法被调用了");
}
}
PersonFactory
public class PersonFactory implements FactoryBean<Person> {
@Override
public Person getObject() throws Exception {
return new Person();
}
@Override
public Class<?> getObjectType() {
return null;
}
@Override
public boolean isSingleton() {
//isSingleton()默认返回true,代表是否单例。
//可以改为false变为多例
return FactoryBean.super.isSingleton();
}
}
spring.xml
<bean id="person" class="com.yzh.spring6.bean.PersonFactory"/>
@Test
public void test_PersonFactory(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
Person person = applicationContext.getBean("person", Person.class);
System.out.println(person);
}
7.5总结四种方式
7.6、BeanFactory和FactoryBean的区别(经典面试题)
7.6.1、BeanFactory
Spring IoC容器的顶级接口,BeanFactory被翻译为“Bean工厂”,在Spring的IoC容器中,“Bean工厂”负责创建Bean对象。不管你用什么方式创建Bean对象都是源于这个接口。
7.6.2 FactoryBean
FactoryBean:它是一个Bean,是一个能够辅助Spring实例化其它Bean对象的一个Bean。
在Spring中,Bean可以分为两类:
- 第一类:普通Bean
- 第二类:工厂Bean(记住:工厂Bean也是一种Bean,只不过这种Bean比较特殊,它可以辅助Spring实例化其它Bean对象。)
7.7 注入自定义Date
我们前面说过,java.util.Date在Spring中被当做简单类型,简单类型在注入的时候可以直接使用value属性或value标签来完成。但我们之前已经测试过了,对于Date类型来说,采用value属性或value标签赋值的时候,对日期字符串的格式要求非常严格,必须是这种格式的:Mon Oct 10 14:30:26 CST 2022。其他格式是不会被识别的。
如以下代码:省略(自己去看语雀)
这种情况下,我们就可以使用FactoryBean来完成这个骚操作。
public class Student {
//生日
Date birthday;
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
@Override
public String toString() {
return "Student{" +
"birthday=" + birthday +
'}';
}
}
编写DateFactory实现FactoryBean接口:
package com.yzh.spring6.bean;
import org.springframework.beans.factory.FactoryBean;
import java.text.SimpleDateFormat;
import java.util.Date;
public class DateFactory implements FactoryBean<Date> {
//通过DateFactory这个工厂Bean来辅助创建Date这个Bean.
//首先我这个DateFactory是来辅助创建日期对象的,那你肯定会传
//一个日期给我并且是以字符串的类型传过来的,日期格式我暂时解析为yyyy-MM-DD
//创建一个属性date用来接收
String date;
//创建构造方法为date赋值
public DateFactory(String date) {
this.date = date;
}
@Override
public Date getObject() throws Exception {
//通过构造方法赋值完毕后,我在这里对这个字符串解析
//解析成yyyy-MM-DD这样的一个类型的Date对象
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-DD");
Date date1 = sdf.parse(date);
return date1;
}
@Override
public Class<?> getObjectType() {
return null;
}
}
<bean id="dateBean" class="com.yzh.spring6.bean.DateFactory">
<constructor-arg index="0" value="2000-01-02"/>
</bean>
<bean id="studentBean" class="com.yzh.spring6.bean.Student">
<property name="birthday" ref="dateBean"/>
</bean>
执行结果:
八、Bean的生命周期(非常重要)
8.1什么是Bean对象的生命周期?
Spring其实就是一个管理Bean对象的工厂。它负责对象的创建,对象的销毁等。
所谓的生命周期就是:对象从创建开始到最终销毁的整个过程。
什么时候创建Bean对象?
创建Bean对象的前后会调用什么方法?
Bean对象什么时候销毁?
Bean对象的销毁前后调用什么方法?
8.2 为什么要知道Bean的生命周期
其实生命周期的本质是:在哪个时间节点上调用了哪个类的哪个方法。
我们需要充分的了解在这个生命线上,都有哪些特殊的时间节点。
只有我们知道了特殊的时间节点都在哪,到时我们才可以确定代码写到哪。
我们可能需要在某个特殊的时间点上执行一段特定的代码,这段代码就可以放到这个节点上。当生命线走到这里的时候,自然会被调用。
8.3Bean的生命周期五步
1.实例化后的Bean(调用Bean的无参构造方法)
2.Bea的属性赋值(调用set方法)
3.初始化Bean(调用Bean的init方法,这个init方法需要自己写)
4.使用Bean
5.销毁Bean(调用Bean的destroy方法,这个destroy方法需要自己写)
代码如下:
User类
public class User {
private String name;
public User() {
System.out.println("第一步:实例化Bean(调用无参构造方法)");
}
public void setName(String name) {
System.out.println("第二步:为属性赋值(调用set方法)");
this.name = name;
}
public void initBean(){
System.out.println("第三步:初始化Bean(调用自己写的init方法)");
}
public void destroyBean(){
System.out.println("第五步:销毁Bean(调用自己写的destroy方法)");
}
}
spring.xml
<bean id="userBean" class="com.yzh.spring6.bean.User"
init-method="initBean" destroy-method="destroyBean">
<property name="name" value="zhangsan"/>
</bean>
//注意:这个初始化方法init和销毁方法destroy需要在这里手动指定
测试:
@Test
public void test1(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
User userBean = applicationContext.getBean("userBean", User.class);
System.out.println("第四步:使用Bean"+userBean);
//注意:第五步销毁bean需要自己手动调用ClassPathXmlApplicationContext
//的close方法才可以。得先把applicationContext转成ClassPathXmlApplicationContext
ClassPathXmlApplicationContext context = (ClassPathXmlApplicationContext) applicationContext;
context.close();
}
8.4、Bean生命周期七步(必须记住)
相比于前面的五步,多出来的那两步在哪里?在Bean初始化前后 Bean生命周期七步: 1.实例化Bean(调用无参构造器) 2.为属性赋值(调用set方法) 3.执行"Bean后处理器"的before方法 4.初始化Bean(调用init方法,这个init方法需要自己写并且需要在spring.xml配置文件中指定) 5.执行"Bean后处理器"的after方法 6.使用Bean 7.销毁Bean(调用destroy方法,这个destroy方法需要自己写并且需要在spring.xml配置文件中指定) 代码如下:
User类
public class User {
private String name;
public User() {
System.out.println("第一步:实例化Bean(调用无参构造方法)");
}
public void setName(String name) {
System.out.println("第二步:为属性赋值(调用set方法)");
this.name = name;
}
public void initBean(){
System.out.println("第四步:初始化Bean(调用自己写的init方法)");
}
public void destroyBean(){
System.out.println("第七步:销毁Bean(调用自己写的destroy方法)");
}
}
Bean后处理器类
public class LogBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("第三步:调用'Bean后处理器'的before方法");
return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName);
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("第五步:调用'Bean后处理器'的after方法");
return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);
}
}
注:虽然这是一个接口,但是这两个方法需要手动重写,
这两个方法中都含有两个参数:
第一个参数:刚创建好的Bean对象
第二个参数:bean的名字
sping.xml
<!--注意这个Bean后处理器是针对于当前整个配置文档的bean对象的-->
<bean class="com.yzh.spring6.bean.LogBeanPostProcessor"/>
<bean id="userBean" class="com.yzh.spring6.bean.User"
init-method="initBean" destroy-method="destroyBean">
<property name="name" value="zhangsan"/>
</bean>
测试:
public class UserTest {
@Test
public void test1(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
User userBean = applicationContext.getBean("userBean", User.class);
System.out.println("第六步:使用Bean"+userBean);
//注意:第五步销毁bean需要自己手动调用ClassPathXmlApplicationContext
//的close方法才可以。得先把applicationContext转成ClassPathXmlApplicationContext
ClassPathXmlApplicationContext context = (ClassPathXmlApplicationContext) applicationContext;
context.close();
}
}
执行效果:
注:这个Bean后处理器需要自定义一个类并且实现BeanPostProcessor这个接口,并且需要重写里面的两个默认方法,before和after方法。
关于Bean对象的生命周期这七步必须要记住
8.5 Bean生命周期之10步
Bean生命周期十步: 比七步添加的那三步在哪里?
点位1: 在“Bean后处理器"before方法之前
点位2: 在“Bean后处理器”before方法之后
点位3: 使用Bean之后,或者说销毁Bean之前
添加的这三个点位的特点:都是在检查你这个Bean是否实现了某些特定的接口,如果实现了这些接口,则Spring容器会调用这个接口中的方法
如果根据源码跟踪,可以划分更细粒度的步骤,10步:
代码如下:
package com.powernode.spring6.bean;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.*;
/**
* @className User
*/
public class User implements BeanNameAware, BeanClassLoaderAware, BeanFactoryAware, InitializingBean, DisposableBean {
private String name;
public User() {
System.out.println("1.实例化Bean");
}
public void setName(String name) {
this.name = name;
System.out.println("2.Bean属性赋值");
}
public void initBean(){
System.out.println("6.初始化Bean");
}
public void destroyBean(){
System.out.println("10.销毁Bean");
}
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
System.out.println("3.类加载器:" + classLoader);
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
System.out.println("3.Bean工厂:" + beanFactory);
}
@Override
public void setBeanName(String name) {
System.out.println("3.bean名字:" + name);
}
@Override
public void destroy() throws Exception {
System.out.println("9.DisposableBean destroy");
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("5.afterPropertiesSet执行");
}
}
LogBeanPostProcessor
package com.powernode.spring6.bean;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
/**
* @className LogBeanPostProcessor
**/
public class LogBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("4.Bean后处理器的before方法执行,即将开始初始化");
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("7.Bean后处理器的after方法执行,已完成初始化");
return bean;
}
}
通过测试可以看出来:
- InitializingBean的方法早于init-method的执行。
- DisposableBean的方法早于destroy-method的执行。
对于SpringBean的生命周期,掌握之前的7步即可。够用。
总结:
先把五步记住了,然后想从五步到起步增加的那两步的点位,包括从七步到十步时增加的那三步的点位。
五步:
1.实例化Bean(调用无参构造)
2.对Bean的属性赋值(调用set方法)
3.初始化Bean(调用自定义的init方法,同时需要在配置文件中指定这个方法)
4.使用Bean
5.销毁Bean
七步:相较于五步,增加的两部点位在初始化Bean
1.实例化Bean(调用无参构造)
2.对Bean的属性赋值(调用set方法)
3.调用"Bean后处理器"的before方法
4.初始化Bean(调用自定义的init方法,同时需要在配置文件中指定这个方法)
5.调用"Bean后处理器"的after方法
6.使用Bean
7.销毁Bean
十步:相较于七步,增加的三步,点位在"Bean后处理器"的before方法,以及销毁Bean
1.实例化Bean(调用无参构造)
2.对Bean的属性赋值(调用set方法)
3.检查Bean对象是否实现了Aware的相关接口,并设置相关依赖
4.调用"Bean后处理器"的before方法
5.检查Bean对象是否实现了Aware的相关接口,并设置相关依赖
6.初始化Bean(调用自定义的init方法,同时需要在配置文件中指定这个方法)
7.调用"Bean后处理器"的after方法
8.使用Bean
9.检查Bean对象是否实现了Aware的相关接口,并设置相关依赖
10.销毁Bean
图中检查Bean是否实现了Aware的相关接口是什么意思?
Aware相关的接口包括:BeanNameAware、BeanClassLoaderAware、BeanFactoryAware
- 当Bean实现了BeanNameAware,Spring会将Bean的名字传递给Bean。
- 当Bean实现了BeanClassLoaderAware,Spring会将加载该Bean的类加载器传递给Bean。
- 当Bean实现了BeanFactoryAware,Spring会将Bean工厂对象传递给Bean。
测试以上10步,可以让User类实现5个接口,并实现所有方法:
- BeanNameAware
- BeanClassLoaderAware
- BeanFactoryAware
- InitializingBean
- DisposableBean
8.6 Bean的作用域不同,管理方式不同
Bean的作用域scope的值:singleton单例、prototype(不是单例)
singleton为默认值,在spring.xml解析的时候创建scope为singleton的bean,若scope为prototype则每一次获取bean的时候都会创建一个新的bean
Spring 根据Bean的作用域来选择管理方式。
- 对于singleton(单例)作用域的Bean,Spring 能够精确地知道该Bean何时被创建,何时初始化完成,以及何时被销毁;
- 而对于 prototype(不是单例)作用域的 Bean,Spring 只负责创建,当容器创建了 Bean 的实例后,Bean 的实例就交给客户端代码管理,Spring 容器将不再跟踪其生命周期。
我们把之前User类的spring.xml文件中的配置scope设置为prototype:
<?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">
<!--
init-method属性指定初始化方法。
destroy-method属性指定销毁方法。
-->
<bean id="userBean" class="com.powernode.spring6.bean.User" init-method="initBean" destroy-method="destroyBean" scope="prototype">
<property name="name" value="zhangsan"/>
</bean>
<!--配置Bean后处理器。这个后处理器将作用于当前配置文件中所有的bean。-->
<bean class="com.powernode.spring6.bean.LogBeanPostProcessor"/>
</beans>
注:那三个3执行的都是Aware相关的接口方法,所以看做是同一步骤3.
也就是说如果你的作用域是不是单例的话,少了一下两个步骤:
8.7 自己new的对象如何让Spring管理
有些时候可能会遇到这样的需求,某个java对象是我们自己new的,然后我们希望这个对象被Spring容器管理,怎么实现?
User
package com.powernode.spring6.bean;
/**
* @className User
* @since 1.0
**/
public class User {
}
package com.powernode.spring6.test;
import com.powernode.spring6.bean.User;
import org.junit.Test;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
/**
* @className RegisterBeanTest
* @since 1.0
**/
public class RegisterBeanTest {
@Test
public void testBeanRegister(){
// 自己new的对象
User user = new User();
System.out.println(user);
// 创建 默认可列表BeanFactory 对象
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
// 注册Bean
factory.registerSingleton("userBean", user);
// 从spring容器中获取bean
User userBean = factory.getBean("userBean", User.class);
System.out.println(userBean);
}
}
九、Bean的循环依赖问题
9.1 什么是Bean的循环依赖
A对象中有B属性。B对象中有A属性。这就是循环依赖。我依赖你,你也依赖我。
比如:丈夫类Husband,妻子类Wife。Husband中有Wife的引用。Wife中有Husband的引用。
Husband
package com.powernode.spring6.bean;
/**
* @className Husband
* @since 1.0
**/
public class Husband {
private String name;
private Wife wife;
}
Wife
package com.powernode.spring6.bean;
/**
* @className Wife
* @since 1.0
**/
public class Wife {
private String name;
private Husband husband;
}
9.2 单例singleton + set注入模式产生循环依赖
我们来编写程序,测试一下在singleton+setter的模式下产生的循环依赖,Spring是否能够解决?
Husband
package com.powernode.spring6.bean;
public class Husband {
private String name;
private Wife wife;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setWife(Wife wife) {
this.wife = wife;
}
// toString()方法重写时需要注意:不能直接输出wife,输出wife.getName()。要不然会出现递归导致的栈内存溢出错误。
@Override
public String toString() {
return "Husband{" +
"name='" + name + '\'' +
", wife=" + wife.getName() +
'}';
}
}
Wife
package com.powernode.spring6.bean;
public class Wife {
private String name;
private Husband husband;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setHusband(Husband husband) {
this.husband = husband;
}
// toString()方法重写时需要注意:不能直接输出husband,输出husband.getName()。要不然会出现递归导致的栈内存溢出错误。
@Override
public String toString() {
return "Wife{" +
"name='" + name + '\'' +
", husband=" + husband.getName() +
'}';
}
}
<?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="husbandBean" class="com.powernode.spring6.bean.Husband" scope="singleton">
<property name="name" value="张三"/>
<property name="wife" ref="wifeBean"/>
</bean>
<bean id="wifeBean" class="com.powernode.spring6.bean.Wife" scope="singleton">
<property name="name" value="小花"/>
<property name="husband" ref="husbandBean"/>
</bean>
</beans>
package com.powernode.spring6.test;
import com.powernode.spring6.bean.Husband;
import com.powernode.spring6.bean.Wife;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class CircularDependencyTest {
@Test
public void testSingletonAndSet(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
Husband husbandBean = applicationContext.getBean("husbandBean", Husband.class);
Wife wifeBean = applicationContext.getBean("wifeBean", Wife.class);
System.out.println(husbandBean);
System.out.println(wifeBean);
}
}
什么意思?
就是说你的两个类现在明显是你中有我我中有你,如果你直接去new的话,不管你new哪一个他都会报错递归产生栈溢出,但是你现在是交给spring容器去创建,并不是你在new。
在singleton + setter模式下,为什么循环依赖不会出现问题,Spring是如何应对的?
主要的原因是,在这种模式下SpringjBean的管理主要分为清晰的两个阶段:第一个阶段:在Spring容器加载的时候,实例化Bean,只要其中任意一个Bean实例化之后,马上进行“曝光”[不等属性赋值就曝光]
第二个阶段: Bean“曝光"之后,再进行属性的赋值( 调用set方法。)。
核心解决方案是:实例化对象和对象的属性赋值分为两个阶段来完成的。注意:只有在scope为singleton的情况下Bean才会采取"曝光"的措施。
执行过程:
1.调用ClassPathXmlApplicationContext("spring.xml"),
2.解析spring.xml,实例化IoC容器中的所有单例Bean(调用无参构造实例化,实例化后里面的属性值都是默认值)放到一个Map中,也就是会先解析以下两句,然后实例化Bean放入Map
在实例化后是先在IoC容器曝光在放入Map集合。
3.对Map中的单例Bean进行注入(也就是属性赋值),我在注入的时候是可以从IoC容器中找到对应的bean引用的(我前面已经曝光了)。
注:这是一个面试点,spring是如何解决循环依赖的问题的?曝光一词必须记住.
曝光的前提是已经实例化bean
9.3多例prototype+set注入模式下产生循环依赖
一句话:如果你的两个bean产生循环依赖,那么你这两个bean的作用域scope不可以都为prototype,否则报BeanCurrentlyInCreationException异常。
解析一下:
首先当scope为prototype时,你这个bean对象在spring.xml解析时不会被实例化,他在你调用getBean的时候实例化,并且他实例化的时候不会提前曝光,因为这个不是唯一的,你每调用一次getBean就实例化一个,人家为什么还要曝光?没啥用,只有单例唯一的bean曝光才有意义。
所以:一句话若两个bean之间存在循环依赖,那么其中一个bean的作用域scope的值必须为单例singleton。
9.4 singleton + 构造注入 产生循环依赖
还是一句话:
基于构造方式去创建的bean无法解决循环依赖的问题。会报:BeanCurrentlyInCreationException
主要原因是因为通过构造方法注入导致的:因为构造方法注入会导致实例化对象的过程和对象属性赋值的过程没有分离开,必须在一起完成导致的
解析:曝光的前提是你得先实例化出来你这个bean,你回想一下bean的生命周期,
实例化bean->属性赋值->初始化bean->使用bean->销毁
这是bean生命周期五步的,那构造注入实例化bean就必须得对他的属性赋值了,在赋值的时候没办法没办法从IoC容器中找到对应的wifeBean和husbandBean,因为你这个实例化都没完成就没法曝光那自然就找不到。那为什么set注入能找到?
因为set注入完成实例化调用的是无参的都构造方法,实例化完后属性的值是默认值(比如String = null),他在实例化的时候不需要先对属性值先初始化,后面才调用set方法对属性初始化。而你这个构造注入在实例化的时候就得对属性进行初始化赋值。
总结
1.只有在(singleton+set注入)模式下才可以解决两个bean之间产生的循环依赖。
2.(singleton+set注入)模式解决的主要原因是清晰的分为两步:
a.先实例化spring.xml中的所有单例Bean,然后存入一个Map集合曝光
b.后执行注入
3.曝光的前提是你这个bean可以实例化。其实在往Map集合里面存实例化Bean对象的时候这一步往里面存的这一步骤就是曝光的动作。
4.举一个例子:
Class User{
String name;
int age;
public void setName(String name){
this.name = name;
}
public void setAge(String name){
this.age= age;
}
}
对应
<bean id="UserBean" class="com.yzh.User">
首先这个UserBean是符合单例+set模式。
在解析spring.xml时会实例化一个:
User{
String name = null;
int age = 0;
public void setName(String name){
this.name = name;
}
public void setAge(String name){
this.age= age;
}
}
这样的Bean对象放入Map集合。然后存完所有的实例化bean后在去执行里面对应的set注入
9.5 Spring解决循环依赖的机理
建议去看视频
你需要知道的是解决Spring循环依赖中存在三级缓存,每一级缓存对应一个Map集合。
在解析完spring.xml后会有一个Map集合(也称缓存),那么往这个Map集合中存的动作其实就是曝光的动作。里面存着所有单例Bean的实例化对象。然后在对里面的属性进行set注入,在进行set注入的时候先从第一级缓存(里面存着完整的bean,bean的属性是已经完成赋值的了)中找,若找不到则对第二级缓存(里面存着实例化都的bean)进行查找,若二级缓存中找不到则对第三季缓存进行查找。若还找不到则从原来曝光后的集合中获取,获取完后这个对象现在就在第三级缓存集合中,此时你已经获取到了这个bean对象了即可以完成set注入。但是他底层还会做一件事就是把这个对象存入二级缓存中,然后清空三级缓存。
十、回顾反射机制
调用方法的四个要素: 1.调用哪个对象? 2.调用哪个方法? 3.调用方法时传什么参数? 4.方法执行结果返回什么值? 总结:调用哪个对象的那个方法,传什么参数,返回什么值 即使是使用反射机制也需要这四个要素
public class Test2 {
/**
* 需求:
* 假设你现在已知以下信息:
* 1。有这样一个类,类名叫做: com.powernode.reflect.User
* 2。这个类符合javabean规范。属性私有化,对外提供公开的setter和getter方法3。 你还知道这个类当中有一个属性,属性的名字叫做 age
* 4。并且你还知道age 属性的类型是int类型请使用反射机制调用相关方法,给User对象的age属性赋值。
*/
public static void main(String[] args) throws Exception {
//全类名
String className = "com.yzh.User";
//属性名
String propertyName = "age";
//获取类
Class<?> clazz = Class.forName(className);
//获取属性对象
Field field = clazz.getDeclaredField("age");
//拼接方法名
String methodName = "set"+propertyName.toUpperCase().charAt(0) + propertyName.substring(1);
//获取方法对象(第一个参数为方法名,第二个参数为返回类型)
Method declaredMethod = clazz.getDeclaredMethod(methodName,field.getType());
//准备对象
Object o = clazz.getDeclaredConstructor().newInstance();
//调用方法
declaredMethod.invoke(o, 13);
System.out.println(o);
}
}
十一、手写框架
不写
十二、Spring IoC注解式开发
12.1、回顾注解
注解的存在主要是为了简化XML的配置。Spring6倡导全注解开发。
我们来回顾一下:
- 第一:注解怎么定义,注解中的属性怎么定义?
- 第二:注解怎么使用?
- 第三:通过反射机制怎么读取注解?
注解怎么定义,注解中的属性怎么定义?
package com.powernode.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(value = {ElementType.TYPE})
@Retention(value = RetentionPolicy.RUNTIME)
public @interface Component {
String value();
}
以上是自定义了一个注解:Component
该注解上面修饰的注解包括:Target注解和Retention注解,这两个注解被称为元注解。
Target注解用来设置Component注解可以出现的位置,以上代表表示Component注解只能用在类和接口上。
Retention注解用来设置Component注解的保持性策略,以上代表Component注解可以被反射机制读取。
String value(); 是Component注解中的一个属性。该属性类型String,属性名是value。
注解怎么使用?
@Target(value = {ElementType.TYPE,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Component {
String value();
}
//注意看:我现在是@Retention(RetentionPolicy.RUNTIME)
代表若某个类使用了@Componen注解,则代表他的反射Clazz类
可以获取到Component注解的信息,若为@Retention(RetentionPolicy.SOURCE)
则代表Component这个注解只能出现在源码,在反射机制上的Clazz类不可用
//使用Component注解
@Component(value = "value值")
public class User {
}
public class Test1 {
public static void main(String[] args) throws Exception{
//反射机制获取类,并且我可以获取到这个类的注解
Class<?> clazz = Class.forName("com.yzh.test.User");
//判断类上是否有Component注解
if(clazz.isAnnotationPresent(Component.class)){
//获取注解对象
Component annotation = clazz.getAnnotation(Component.class);
//获取注解中的信息
System.out.println(annotation.value());
}
}
}
12.2、一个案例需求
需求:
假设我们现在只知道包名:com.yzh.bean。至于这个包下有多少个类我不知道,我现在要写一个程序,扫描这个包下所有的类,如果类中使用了Component注解则这个类实例化,并放入一个Map中,若没有则不实例化。
自定义注解:Component
@Target(value = {ElementType.TYPE,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Component {
String value();
}
在com.yzh.bean下创建三个类:
public class Student {}
@Component(value = "UserBean")
public class User {}
@Component("VipBean")
public class Vip {}
注意:此时这有User和Vip这两个类使用了Component注解
实现需求:sacnBean
public class scanBean {
public static void main(String[] args) {
Map maps = new HashMap();
String packageName = "com.yzh.bean";
//把“.”斜杠转成"/"
String classPackage = packageName.replaceAll("\\.", "/");
// System.out.println(className);
//获取加载器(这个加载器会从类的根目录下开始加载也就是从java文件下开始加载)
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
//根据包名获取URL对象,里面包含了绝对路径
// file:/D:/idea_java_project/Spring6/review-annotation/target/classes/com/yzh/bean
URL url = systemClassLoader.getResource(classPackage);
//根据url对象获取绝对路径
// /D:/idea_java_project/Spring6/review-annotation/target/classes/com/yzh/bean
String path = url.getPath();
//创建文件对象读取/D:/idea_java_project/Spring6/review-annotation/target/classes/com/yzh/bean
File file = new File(path);
//读取里面的所有类名
File[] files = file.listFiles();
Arrays.stream(files).forEach(f -> {
try {
/**
* System.out.println(f.getName());
* 输出:Student.class、User.class、Vip.class
*/
String fName1 = f.getName();
//分割Xxx.class 取第一部分
String fName2 = fName1.split("\\.")[0];
/**
* System.out.println(fName2);
* 输出:Student、User、Vip
*/
//拼接全类名
String className = packageName + "." +fName2;
/**
* System.out.println(className);
* 打印:
* com.yzh.bean.Student
* com.yzh.bean.User
* com.yzh.bean.Vip
*/
//通过反射获取Class类对象
Class<?> clazz = Class.forName(className);
//根据反射类对象查看当前当前类是否有Component注解
if (clazz.isAnnotationPresent(Component.class)) {
//若有Component注解,则做一下操作
//获取注解信息,作为maps的key
Component component = clazz.getAnnotation(Component.class);
String id = component.value();
//实例化对象作为maps的value
Object o = clazz.getDeclaredConstructor().newInstance();
maps.put(id,o);
}
}catch (Exception e){
e.printStackTrace();
}
});
System.out.println(maps);
}
}
执行结果:
12.3 声明Bean的注解
负责声明Bean的注解,常见的包括四个:
- @Component
- @Controller
- @Service
- @Repository
源码如下:
通过源码可以看到,@Controller、@Service、@Repository这三个注解都是@Component注解的别名。
也就是说:这四个注解的功能都一样。用哪个都可以。
只是为了增强程序的可读性,建议:
- 控制器类上使用:Controller
- service类上使用:Service
- dao类上使用:Repository
他们都是只有一个value属性。value属性用来指定bean的id,也就是bean的名字。写完注解后直接可以把bean的配置删掉,只需要加上扫包这个动作即可
12.4 Spring注解的使用
注意点:bean的id要和注释上面的value值一致
如何使用以上的注解呢?
- 第一步:加入aop的依赖
- 第二步:在配置文件中添加context命名空间
- 第三步:在配置文件中指定扫描的包
- 第四步:在Bean类上使用注解
第一步:加入aop的依赖
我们可以看到当加入spring-context依赖之后,会关联加入aop的依赖。所以这一步不用做。
第二步:在配置文件中添加context命名空间
xmlns:context="http://www.springframework.org/schema/context"
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
这个不是记的,复制第二行和第五行把beans改为context即可
第三步:在配置文件中指定要扫描的包
<context:component-scan base-package="com.yzh.bean"/>
第四步:在Bean类上使用注解
Order
package com.yzh.bean;
import org.springframework.stereotype.Component;
//@Component(value = "orderBean")
//@Component("orderBean")
//以上方式都可以
//@Component 这种方式的话,有别名机制,默认为类名首字母该小写,则bean对应的id为order
@Component
public class Order {
}
User
@Repository(value = "userBean")
public class User {
}
Person
@Service(value = "personBean")
public class Person {
}
Student
@Controller(value="studentBean")
public class Student {
}
测试:
@Test
public void test1(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
Object orderBean = applicationContext.getBean("order");
System.out.println(orderBean);
Object personBean = applicationContext.getBean("personBean");
System.out.println(personBean);
Object studentBean = applicationContext.getBean("studentBean");
System.out.println(studentBean);
Object userBean = applicationContext.getBean("userBean");
System.out.println(userBean);
}
perfect !!!
12.5 多个包怎么扫描?
如果是多个包怎么办?有两种解决方案:
- 第一种:在配置文件中指定多个包,用逗号隔开。
- 第二种:指定多个包的共同父包。
<context:component-scan base-package=
"com.powernode.spring6.bean,
com.powernode.spring6.bean2"/>
或者
<context:component-scan base-package="com.powernode.spring6"/>
12.6 选择性实例化Bean
假设在某个包下有很多Bean,有的Bean上标注了Component,有的标注了Controller,有的标注了Service,有的标注了Repository,现在由于某种特殊业务的需要,只允许其中所有的Controller参与Bean管理,其他的都不实例化。这应该怎么办呢?
package com.powernode.spring6.bean3;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
@Component
public class A {
public A() {
System.out.println("A的无参数构造方法执行");
}
}
@Controller
class B {
public B() {
System.out.println("B的无参数构造方法执行");
}
}
@Service
class C {
public C() {
System.out.println("C的无参数构造方法执行");
}
}
@Repository
class D {
public D() {
System.out.println("D的无参数构造方法执行");
}
}
@Controller
class E {
public E() {
System.out.println("E的无参数构造方法执行");
}
}
@Controller
class F {
public F() {
System.out.println("F的无参数构造方法执行");
}
}
我只想实例化bean3包下的所用使用Controller注解的类。配置文件这样写:
<?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 http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.powernode.spring6.bean3" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
</beans>
-------------------------------------------------------
注:
在扫包的标签context:component-scan中添加属性use-default-filters="false"
则代表这个包下的所有bean不被实例化,use-default-filters这个的默认值为
true。
然后里面使用context:include-filter标签选择你要过滤的bean,
type="annotation" 代表采用注解类型去过滤
expression翻译为表达式:用于指定注解类型,填全注解名
外层失效内层过滤的就是生效的bean
也可以将use-default-filters设置为true(不写就是true),并且采用exclude-filter方式排出哪些注解标注的Bean不参与实例化:
这样的话那就是外层生效,那内层过滤的就是生效的bean
<context:component-scan base-package="com.powernode.spring6.bean3">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Repository"/>
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Service"/>
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
12.7 负责注入的注解
@Component @Controller @Service @Repository 这四个注解是用来声明Bean的,声明后这些Bean将被实例化。接下来我们看一下,如何给Bean的属性赋值。给Bean属性赋值需要用到这些注解:
- @Value
- @Autowired
- @Qualifier
- @Resource
12.7.1 @Value注解
当属性的类型是简单类型时,可以使用@Value注解进行注入。
package com.powernode.spring6.bean4;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class User {
@Value(value = "zhangsan")
private String name;
@Value("20")
private int age;
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
开启包扫描:
<?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 http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.powernode.spring6.bean4"/>
</beans>
@Test
public void testValue(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-injection.xml");
Object user = applicationContext.getBean("user");
System.out.println(user);
}
执行结果:
通过以上代码可以发现,我们并没有给属性提供setter方法,但仍然可以完成属性赋值。
如果提供setter方法,并且在setter方法上添加@Value注解,可以完成注入吗?尝试一下:
package com.powernode.spring6.bean4;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class User {
private String name;
private int age;
@Value("李四")
public void setName(String name) {
this.name = name;
}
@Value("30")
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
执行结果:
通过测试可以得知,@Value注解可以直接使用在属性上,也可以使用在setter方法上。都是可以的。都可以完成属性的赋值。
为了简化代码,以后我们一般不提供setter方法,直接在属性上使用@Value注解完成属性赋值。
出于好奇,我们再来测试一下,是否能够通过构造方法完成注入:
package com.powernode.spring6.bean4;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class User {
private String name;
private int age;
public User(@Value("隔壁老王") String name, @Value("33") int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
执行结果:
通过测试得知:@Value注解可以出现在属性上、setter方法上、以及构造方法的形参上。可见Spring给我们提供了多样化的注入。太灵活了。
12.7.2 @Autowired与@Qualifier
@Autowired注解可以用来注入非简单类型。被翻译为:自动连线的,或者自动装配。
单独使用@Autowired注解,默认根据类型装配。【默认是byType】
看一下它的源码:
package org.springframework.beans.factory.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
boolean required() default true;
}
源码中有两处需要注意:
- 第一处:该注解可以标注在哪里?
- 构造方法上
- 方法上
- 形参上
- 属性上
- 注解上
- 第二处:该注解有一个required属性,默认值是true,表示在注入的时候要求被注入的Bean必须是存在的,如果不存在则报错。如果required属性设置为false,表示注入的Bean存在或者不存在都没关系,存在的话就注入,不存在的话,也不报错。
我们先在属性上使用@Autowired注解:
UserDao接口
package com.powernode.spring6.dao;
public interface UserDao {
void insert();
}
UserDao实现类
package com.powernode.spring6.dao;
import org.springframework.stereotype.Repository;
@Repository //纳入bean管理
public class UserDaoForMySQL implements UserDao{
@Override
public void insert() {
System.out.println("正在向mysql数据库插入User数据");
}
}
UserService
package com.powernode.spring6.service;
import com.powernode.spring6.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service // 纳入bean管理
public class UserService {
@Autowired // 在属性上注入
private UserDao userDao;
// 没有提供构造方法和setter方法。
public void save(){
userDao.insert();
}
}
配置包扫描
<?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 http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.powernode.spring6.dao,com.powernode.spring6.service"/>
</beans>
测试程序
@Test
public void testAutowired(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-injection.xml");
UserService userService = applicationContext.getBean("userService", UserService.class);
userService.save();
}
接下来,再来测试一下@Autowired注解出现在setter方法上:
UserService
package com.powernode.spring6.service;
import com.powernode.spring6.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
private UserDao userDao;
@Autowired
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public void save(){
userDao.insert();
}
}
我们再来看看能不能出现在构造方法上:
UserService
package com.powernode.spring6.service;
import com.powernode.spring6.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
private UserDao userDao;
@Autowired
public UserService(UserDao userDao) {
this.userDao = userDao;
}
public void save(){
userDao.insert();
}
}
再来看看,这个注解能不能只标注在构造方法的形参上:
UserService
package com.powernode.spring6.service;
import com.powernode.spring6.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
private UserDao userDao;
public UserService(@Autowired UserDao userDao) {
this.userDao = userDao;
}
public void save(){
userDao.insert();
}
}
执行结果:
还有更劲爆的,当有参数的构造方法只有一个时,@Autowired注解可以省略。
到此为止,我们已经清楚@Autowired注解可以出现在哪些位置了。
@Autowired注解默认是byType进行注入的,也就是说根据类型注入的,如果以上程序中,UserDao接口还有另外一个实现类,会出现问题吗?
当你写完这个新的实现类之后,此时IDEA工具已经提示错误信息了:
错误信息中说:不能装配,UserDao这个Bean的数量大于1.
怎么解决这个问题呢?当然要byName,根据名称进行装配了。
@Autowired注解和@Qualifier注解联合起来才可以根据名称进行装配,在@Qualifier注解中指定Bean名称。
总结:
- @Autowired注解可以出现在:属性上、构造方法上、构造方法的参数上、setter方法上。
- 当带参数的构造方法只有一个,@Autowired注解可以省略。
- @Autowired注解默认根据类型注入。如果要根据名称注入的话,需要配合@Qualifier注解一起使用。
12.7.3@Resource
@Resource注解也可以完成非简单类型注入。那它和@Autowired注解有什么区别?
- @Resource注解是JDK扩展包中的,也就是说属于JDK的一部分。所以该注解是标准注解,更加具有通用性。(JSR-250标准中制定的注解类型。JSR是Java规范提案。)
- @Autowired注解是Spring框架自己的。
- @Resource注解默认根据名称装配byName,未指定name时,使用属性名作为name。通过name找不到的话会自动启动通过类型byType装配。
- @Autowired注解默认根据类型装配byType,如果想根据名称装配,需要配合@Qualifier注解一起用。
- @Resource注解用在属性上、setter方法上。
- @Autowired注解用在属性上、setter方法上、构造方法上、构造方法参数上。
@Resource注解属于JDK扩展包,所以不在JDK当中,需要额外引入以下依赖:【如果是JDK8的话不需要额外引入依赖。高于JDK11或低于JDK8需要引入以下依赖。】
如果你是Spring6+版本请使用这个依赖
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>2.1.1</version>
</dependency>
一定要注意:如果你用Spring6,要知道Spring6不再支持JavaEE,它支持的是JakartaEE9。(Oracle把JavaEE贡献给Apache了,Apache把JavaEE的名字改成JakartaEE了,大家之前所接触的所有的 javax.* 包名统一修改为 jakarta.*包名了。)
如果你是spring5-版本请使用这个依赖
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
@Resource注解的源码如下:
测试一下:
给这个UserDaoForOracle起名xyz
-----------------------------------------------
package com.powernode.spring6.dao;
import org.springframework.stereotype.Repository;
@Repository("xyz")
public class UserDaoForOracle implements UserDao{
@Override
public void insert() {
System.out.println("正在向Oracle数据库插入User数据");
}
}
在UserService中使用Resource注解根据name注入
----------------------------------------------------
package com.powernode.spring6.service;
import com.powernode.spring6.dao.UserDao;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Resource(name = "xyz")
private UserDao userDao;
public void save(){
userDao.insert();
}
}
执行测试程序:
我们把UserDaoForOracle的名字xyz修改为userDao,让这个Bean的名字和UserService类中的UserDao属性名一致:
UserDaoForOracle
------------------------------------------
package com.powernode.spring6.dao;
import org.springframework.stereotype.Repository;
@Repository("userDao")
public class UserDaoForOracle implements UserDao{
@Override
public void insert() {
System.out.println("正在向Oracle数据库插入User数据");
}
}
UserService类中Resource注解并没有指定name
------------------------------------------------
package com.powernode.spring6.service;
import com.powernode.spring6.dao.UserDao;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Resource
private UserDao userDao;
public void save(){
userDao.insert();
}
}
通过测试得知,当@Resource注解使用时没有指定name的时候,还是根据name进行查找,这个name是属性名。
接下来把UserService类中的属性名修改一下:
UserService的属性名修改为userDao2
--------------------------------------
package com.powernode.spring6.service;
import com.powernode.spring6.dao.UserDao;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Resource
private UserDao userDao2;
public void save(){
userDao2.insert();
}
}
根据异常信息得知:显然当通过name找不到的时候,自然会启动byType进行注入。以上的错误是因为UserDao接口下有两个实现类导致的。所以根据类型注入就会报错。
我们再来看@Resource注解使用在setter方法上可以吗?
UserService添加setter方法并使用注解标注
--------------------------------------------
package com.powernode.spring6.service;
import com.powernode.spring6.dao.UserDao;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
@Service
public class UserService {
private UserDao userDao;
@Resource
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public void save(){
userDao.insert();
}
}
注意这个setter方法的方法名,setUserDao去掉set之后,将首字母变小写userDao,userDao就是name
执行结果:
当然,也可以指定name:
UserService
--------------------------------
package com.powernode.spring6.service;
import com.powernode.spring6.dao.UserDao;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
@Service
public class UserService {
private UserDao userDao;
@Resource(name = "userDaoForMySQL")
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public void save(){
userDao.insert();
}
}
总结:
@Resource注解:默认byName注入,没有指定name时把属性名当做name,根据name找不到时,才会byType注入。byType注入时,某种类型的Bean只能有一个。
12.8 全注解式开发
所谓的全注解开发就是不再使用spring配置文件了。写一个配置类来代替配置文件。
配置类代替spring配置文件
package com.powernode.spring6.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScans;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan({"com.powernode.spring6.dao", "com.powernode.spring6.service"})
public class Spring6Configuration {
}
编写测试程序:不再new ClassPathXmlApplicationContext()对象了。
@Test
public void testNoXml(){
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Spring6Configuration.class);
UserService userService = applicationContext.getBean("userService", UserService.class);
userService.save();
}
十三、JdbcTemplate
这章是经典白雪.....
JdbcTemplate是Spring提供的一个JDBC模板类,是对JDBC的封装,简化JDBC代码。
当然,你也可以不用,可以让Spring集成其它的ORM框架,例如:MyBatis、Hibernate等。
接下来我们简单来学习一下,使用JdbcTemplate完成增删改查。
十四、GoF代理模式
什么是代理模式?
演员:目标对象 替身:代理类 观众:客户端 察觉不到替身的存在
生活场景1:牛村的牛二看上了隔壁村小花,牛二不好意思直接找小花,于是牛二找来了媒婆王妈妈。这里面就有一个非常典型的代理模式。牛二不能和小花直接对接,只能找一个中间人。其中王妈妈是代理类,牛二是目标类。王妈妈代替牛二和小花先见个面。(现实生活中的婚介所)【在程序中,对象A和对象B无法直接交互时。】
生活场景2:你刚到北京,要租房子,可以自己找,也可以找链家帮你找。其中链家是代理类,你是目标类。你们两个都有共同的行为:找房子。不过链家除了满足你找房子,另外会收取一些费用的。(现实生活中的房产中介)【在程序中,功能需要增强时。】
西游记场景:八戒和高小姐的故事。八戒要强抢民女高翠兰。悟空得知此事之后怎么做的?悟空幻化成高小姐的模样。代替高小姐与八戒会面。其中八戒是客户端程序。悟空是代理类。高小姐是目标类。那天夜里,在八戒眼里,眼前的就是高小姐,对于八戒来说,他是不知道眼前的高小姐是悟空幻化的,在他内心里这就是高小姐。所以悟空代替高小姐和八戒亲了嘴儿。这是非常典型的代理模式实现的保护机制。代理模式中有一个非常重要的特点:对于客户端程序来说,使用代理对象时就像在使用目标对象一样。【在程序中,目标需要被保护时】
业务场景:系统中有A、B、C三个模块,使用这些模块的前提是需要用户登录,也就是说在A模块中要编写判断登录的代码,B模块中也要编写,C模块中还要编写,这些判断登录的代码反复出现,显然代码没有得到复用,可以为A、B、C三个模块提供一个代理,在代理当中写一次登录判断即可。代理的逻辑是:请求来了之后,判断用户是否登录了,如果已经登录了,则执行对应的目标,如果没有登录则跳转到登录页面。【在程序中,目标不但受到保护,并且代码也得到了复用。】
代理模式是GoF23种设计模式之一。属于结构型设计模式。
代理模式的作用是:为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个客户不想或者不能直接引用一个对象,此时可以通过一个称之为“代理”的第三者来实现间接引用。代理对象可以在客户端和目标对象之间起到中介的作用,并且可以通过代理对象去掉客户不应该看到的内容和服务或者添加客户需要的额外服务。 通过引入一个新的对象来实现对真实对象的操作或者将新的对象作为真实对象的一个替身,这种实现机制即为代理模式,通过引入代理对象来间接访问一个对象,这就是代理模式的模式动机。
代理模式中的角色:
- 代理类(代理主题)
- 目标类(真实主题)
- 代理类和目标类的公共接口(抽象主题):客户端在使用代理类时就像在使用目标类,不被客户端所察觉,所以代理类和目标类要有共同的行为,也就是实现共同的接口。
代理模式的类图:
代理模式在代码实现上,包括两种形式:
- 静态代理
- 动态代理
注:出现以下场景需要考虑代理模式:
1.在程序中,对象A和对象B无法直接交互时。
2.在程序中,功能需要增强时。
3.在程序中,目标不但受到保护,并且代码也得到了复用。
14.1 静态代理
场景:计算订单服务中的各个方法耗时
结构图:
package com.yzh;
//订单服务接口
public interface OrderService {
//生成订单
void generate();
//订单详情
void detail();
//修改订单
void modify();
}
package com.yzh.target;
import com.yzh.OrderService;
//目标对象类
//静态代理中目标对象和代理对象都实现同一个接口或者同一些接口
public class OrderServiceTarget implements OrderService {
@Override
public void generate() {
try {
System.out.println("正在生成订单......");
Thread.sleep(1234);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void detail() {
try {
Thread.sleep(456);
System.out.println("订单详情......");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void modify() {
try {
Thread.sleep(789);
System.out.println("修改订单......");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
package com.yzh.proxy;
import com.yzh.OrderService;
//代理对象类
//静态代理中目标对象和代理对象都实现同一个接口或者同一些接口
public class OrderServiceProxy implements OrderService {
//需要把目标对象传过来,用接口来接收,因为目标对象实现了这个接口
private OrderService orderService;
public OrderServiceProxy(OrderService orderService) {
this.orderService = orderService;
}
@Override
public void generate() {
long start = System.currentTimeMillis();
orderService.generate();//调用目标对象的方法
long end = System.currentTimeMillis();
System.out.println("耗时:" + (end-start) + "毫秒");
}
@Override
public void detail() {
long start = System.currentTimeMillis();
orderService.detail();//调用目标对象的方法
long end = System.currentTimeMillis();
System.out.println("耗时:" + (end-start) + "毫秒");
}
@Override
public void modify() {
long start = System.currentTimeMillis();
orderService.modify();//调用目标对象的方法
long end = System.currentTimeMillis();
System.out.println("耗时:" + (end-start) + "毫秒");
}
}
package com.yzh;
import com.yzh.proxy.OrderServiceProxy;
import com.yzh.target.OrderServiceTarget;
public class client {
public static void main(String[] args) {
//创建目标对象
OrderService target = new OrderServiceTarget();
//创建代理对象
OrderService proxy = new OrderServiceProxy(target);
//调用代理方法
proxy.generate();
proxy.detail();
proxy.modify();
}
}
这种方式的优点:符合OCP开闭原则,同时采用的是关联关系,所以程序的耦合度较低。所以这种方案是被推荐的。
以上就是代理模式中的静态代理,其中OrderService接口是代理类和目标类的共同接口。OrderServiceImpl是目标类。OrderServiceProxy是代理类。
大家思考一下:如果系统中业务接口很多,一个接口对应一个代理类,显然也是不合理的,会导致类爆炸。怎么解决这个问题?动态代理可以解决。因为在动态代理中可以在内存中动态的为我们生成代理类的字节码。代理类不需要我们写了。类爆炸解决了,而且代码只需要写一次,代码也会得到复用。
14.2 JDK内置的动态代理(非常重要)
首先你要明确知道的:
1.JDK内置的动态代理方式,目标类和代理类都实现了同一个接口或者同一些接口
2.动态代理对象会在内存中生成字节码文件,并且在内存中生成对象。
3.要想使用JDK内置的动态代理,目标类必须实现一个接口
接口类:(目标类和代理类都会实现这个接口)
package org.yzh;
public interface OrderService {
//生成订单
void generate();
//订单详情
void detail();
//修改订单
void modify();
//返回订单号
String oddNumbers();
}
目标类
package org.yzh;
//目标对象类
public class OrderServiceImpl implements OrderService{
@Override
public void generate() {
try {
Thread.sleep(1234);
System.out.println("订单生成中.......");
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void detail() {
try {
Thread.sleep(567);
System.out.println("订单详情.......");
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void modify() {
try {
Thread.sleep(899);
System.out.println("订单修改中.......");
} catch (Exception e) {
e.printStackTrace();
}
}
//返回订单号
@Override
public String oddNumbers() {
return "这是一个订单号";
}
}
InvocationHandler实现类
package org.yzh;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class TimeInvocationHandler implements InvocationHandler {
private OrderService target;
public TimeInvocationHandler(OrderService target) {
this.target = target;
}
/**
* invoke方法的三个参数:
* invoke方法是JDK负责调用的,所以JDK 调用这个方法的时候会自动给我们
* 传过来这三个参数我们可以在invoke方法的大括号中直接使用。
* 第一个参数: 0bject proxy 代理对象的引用。这个参数使用较少。
* 第二个参数: Method method 目标对象上的目标方法。 (要执行的目标方法就是它。)
* 第三个参数: 0bject[] args 月标方法上的实参。
*
* invoke什么时候被调用?
* 当代理对象调用代理方法的时候就会调用这个invoke方法
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//这里可以写代理对象的增强代码也可以调用目标对象的目标方法
long start = System.currentTimeMillis();//增强代码
Object returnValue = method.invoke(target, args);//目标对象的目标方法
long end = System.currentTimeMillis();//增强代码
System.out.println("耗时:" +(end - start)+ "毫秒");//增强代码
return returnValue;//目标方法的返回值
/**我举个例子
* 你调用了
* proxy.generate();这个方法
* 那么在invoke里面
* method.invoke(target, args);
* 上面这一行代码就会去调用target里面的generate()方法
* 相当于说你用代理对象去调用generate()方法,本质上调用的还是
* 目标对象的generate()方法,因为无论你调用啥代理方法都进去到invoke
* 方法,只不过他可以在invoke内部写增强代码
*/
}
}
不需要写代理类,直接写测试
package org.yzh;
import java.lang.reflect.Proxy;
public class Client {
public static void main(String[] args) {
//创建目标对象
OrderService target = new OrderServiceImpl();
//创建代理对象(动态的代理对象会直接生成字节码文件,然后通过你这个字节码文件在内存中实例化一个代理对象
// 而不是生成到你的硬盘中)
//Proxy.newProxyInstance()这一步操作做了两件事:
// 1.在内存中生成代理对象的字节码文件,
// 2.然后创建代理对象并返回
//ClassLoader loader :类加载器
// 目标对象的类加载器
//Class<?>[] interfaces :接口数组
// 目标对象的实现接口,注意这是一个数组代表如果目标对象实现多个接口,这里可以传多个
//InvocationHandler h: InvocationHandler接口的实现类 对象
// 翻译为“调用处理程序” 这是一个接口,要求你传递一个该接口的实现类的对象给他。
// 作用:
// 实现该接口后必须需要从写一个叫invoke的方法,这个方法里面可以写代理对象的
// 增强代码,也可以调用目标对象的方法(因为这个方法中要求传递一个目标对象)
OrderService proxy = (OrderService)Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new TimeInvocationHandler(target));
//调用代理方法
proxy.generate();
proxy.detail();
proxy.modify();
String s = proxy.oddNumbers();
System.out.println(s);
}
}
14.3 CGLIB动态代理
没学,自己看语雀+视频
CGLIB既可以代理接口,又可以代理类。底层采用继承的方式实现。所以被代理的目标类不能使用final修饰。
面试题:CGLIB动态代理和JDK动态代理的区别
十五、AOP面向切面编程(Aspect Oriented Programming)
这一块内容一定要先去看前面的JDK动态代理!!!对帮助理解有非常大的作用
IoC使软件组件松耦合。AOP让你能够捕捉系统中经常使用的功能,把它转化成组件。
AOP(Aspect Oriented Programming):面向切面编程,面向方面编程。(AOP是一种编程技术)
AOP是对OOP的补充延伸。
AOP底层使用的就是动态代理来实现的。
Spring的AOP使用的动态代理是:JDK动态代理 + CGLIB动态代理技术。Spring在这两种动态代理中灵活切换,如果是代理接口,会默认使用JDK动态代理,如果要代理某个类,这个类没有实现接口,就会切换使用CGLIB。当然,你也可以强制通过一些配置让Spring只使用CGLIB。
15.1 AOP介绍
一般一个系统当中都会有一些系统服务,例如:日志、事务管理、安全等。这些系统服务被称为:交叉业务
这些交叉业务几乎是通用的,不管你是做银行账户转账,还是删除用户数据。日志、事务管理、安全,这些都是需要做的。
如果在每一个业务处理过程当中,都掺杂这些交叉业务代码进去的话,存在两方面问题:
- 第一:交叉业务代码在多个业务流程中反复出现,显然这个交叉业务代码没有得到复用。并且修改这些交叉业务代码的话,需要修改多处。
- 第二:程序员无法专注核心业务代码的编写,在编写核心业务代码的同时还需要处理这些交叉业务。
使用AOP可以很轻松的解决以上问题。
请看下图,可以帮助你快速理解AOP的思想:
用一句话总结AOP:将与核心业务无关的代码独立的抽取出来,形成一个独立的组件,然后以横向交叉的方式应用到业务流程当中的过程被称为AOP。
AOP的优点:
- 第一:代码复用性增强。
- 第二:代码易维护。
- 第三:使开发者更关注业务逻辑。
15.2 AOP的七大术语
连接点Joinpoint:描述位置,在程序的整个执行流程中,可以织入切面的位置。方法的执行前后,异常抛出之后等位置。
切点 Pointcut:目标方法(核心业务逻辑),在程序执行流程中,真正织入切面的方法。(一个切点对应多个连接点)
通知 Advice:也叫增强,交叉业务代码
- 通知包括:
- 前置通知(位于切点前面)
- 后置通知(位于切点后面)
- 环绕通知(位于切点前面和后面)
- 异常通知(位于catch块)
- 最终通知(位于final块)
切面 Aspect:切点+通知就是切面(细想:通知/增强包围着切点,形成的整一块就是切面)
织入 Weaving:是一个过程,把通知/增强的代码应用到目标对象从而形成一个代理对象的过程叫织入。
代理对象 Proxy:一个目标对象被织入通知后产生的新对象。
目标对象 Target:被织入通知的对象。
15.3 切点表达式
切点表达式用来定义通知(Advice)往哪些方法上切入。
切入点表达式语法格式:
execution([访问控制权限修饰符] 返回值类型 [全限定类名]方法名(形式参数列表) [异常])
访问控制权限修饰符:
- 可选项。
- 没写,就是4个权限都包括。
- 写public就表示只包括公开的方法。
返回值类型:
- 必填项。
- * 表示返回值类型任意。
全限定类名:
- 可选项。
- 两个点“..”代表当前包以及子包下的所有类。
- 省略时表示所有的类。
方法名:
- 必填项。
- *表示所有方法。
- set*表示所有的set方法。
形式参数列表:
- 必填项
- () 表示没有参数的方法
- (..) 参数类型和个数随意的方法
- (*) 只有一个参数的方法
- (*, String) 第一个参数类型随意,第二个参数是String的。
异常:
- 可选项。
- 省略时表示任意异常类型。
理解以下的切点表达式:
15.4 使用Spring的AOP
Spring对AOP的实现包括以下3种方式:
- 第一种方式:Spring框架结合AspectJ框架实现的AOP,基于注解方式。
- 第二种方式:Spring框架结合AspectJ框架实现的AOP,基于XML方式。
- 第三种方式:Spring框架自己实现的AOP,基于XML配置方式。
实际开发中,都是Spring+AspectJ来实现AOP。所以我们重点学习第一种和第二种方式。
什么是AspectJ?(Eclipse组织的一个支持AOP的框架。AspectJ框架是独立于Spring框架之外的一个框架,Spring框架用了AspectJ)
AspectJ项目起源于帕洛阿尔托(Palo Alto)研究中心(缩写为PARC)。该中心由Xerox集团资助,Gregor Kiczales领导,从1997年开始致力于AspectJ的开发,1998年第一次发布给外部用户,2001年发布1.0 release。为了推动AspectJ技术和社团的发展,PARC在2003年3月正式将AspectJ项目移交给了Eclipse组织,因为AspectJ的发展和受关注程度大大超出了PARC的预期,他们已经无力继续维持它的发展。
15.4.1 环境准备
1.引入依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.2</version>
</dependency>
<!--aspects依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>6.0.2</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
2.在spring.xml中为context和aop引用命名空间
<?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"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
</beans>
15.4.2 基于AspectJ的AOP注解式开发
定义目标类以及目标方法
/**
* 目标类
*/
@Service
public class UserService {
public void login(){
System.out.println("目标对象正在执行......");
}
}
定义切面类(切面类中的方法确实就是通知,在每一个通知\方法上标注通知注解,注解后面跟着括号,括号里面内置切点表达式,代表你要往那个目标类进行增强)
/**
* 这是一个切面,需要用@Aspect注解标注
* 标注完后就是切面类,当你配置了开启动态代理后
* 在组件扫包的时候把会检查包下的每一个类是否
* 存在这个@Aspect注解,如果有的话就会为这个类
* 创建一个动态代理对象
*/
@Aspect
@Component
public class logAspect {
//@Before("切点表达式")-->前置通知注解
@Before("execution(* com.yzh.spring6.service.UserService.*(..))")
public void advice(){
System.out.println("这是一点增强代码");
}
}
配置spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--组件扫描-->
<context:component-scan base-package="com.yzh.spring6.service"/>
<!--开启自动代理(切面扫描),下面这行代码表示在组价扫描的时候检查类是否有@Aspect注解
如果有的话会自动代理对象-->
<aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>
----------------------------------------------------------------
<!--
<aop:aspectj-autoproxy proxy-target-class="true"/> 开启自动代理之后,凡事带有
@Aspect注解的bean都会生成代理对象。proxy-target-class="true" 表示采用cglib动态代理。
proxy-target-class="false" 表示采用jdk动态代理。默认值是false。即使写成false,
当没有接口的时候,也会自动选择cglib生成代理类。
-->
public class test {
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
UserService userService = applicationContext.getBean("userService", UserService.class);
userService.login();
}
}
15.5 AOP基于注解细节
15.5.1 五大通知类型
通知类型包括:
- 前置通知:@Before 目标方法执行之前的通知
- 后置通知:@AfterReturning 目标方法执行之后的通知
- 环绕通知:@Around 目标方法之前添加通知,同时目标方法执行之后添加通知。
- 异常通知:@AfterThrowing 发生异常之后执行的通知
- 最终通知:@After 放在finally语句块中的通知
接下来,编写程序来测试这几个通知的执行顺序:
切面类
package com.powernode.spring6.service;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
// 切面类
@Component
@Aspect
public class MyAspect {
@Around("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("环绕通知开始");
// 执行目标方法。
proceedingJoinPoint.proceed();
System.out.println("环绕通知结束");
}
@Before("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void beforeAdvice(){
System.out.println("前置通知");
}
@AfterReturning("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void afterReturningAdvice(){
System.out.println("后置通知");
}
@AfterThrowing("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void afterThrowingAdvice(){
System.out.println("异常通知");
}
@After("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void afterAdvice(){
System.out.println("最终通知");
}
}
目标类和目标方法
package com.powernode.spring6.service;
import org.springframework.stereotype.Component;
// 目标类
@Component
public class OrderService {
// 目标方法
public void generate(){
System.out.println("订单已生成!");
}
}
测试程序
package com.powernode.spring6.test;
import com.powernode.spring6.service.OrderService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class AOPTest {
@Test
public void testAOP(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-aspectj-aop-annotation.xml");
OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
orderService.generate();
}
}
通过上面的执行结果就可以判断他们的执行顺序了,这里不再赘述。
结果中没有异常通知,这是因为目标程序执行过程中没有发生异常。我们尝试让目标方法发生异常:
在目标类中发生异常
package com.powernode.spring6.service;
import org.springframework.stereotype.Component;
// 目标类
@Component
public class OrderService {
// 目标方法
public void generate(){
System.out.println("订单已生成!");
if (1 == 1) {
throw new RuntimeException("模拟异常发生");
}
}
}
通过测试得知,当发生异常之后,最终通知也会执行,因为最终通知@After会出现在finally语句块中。
出现异常之后,后置通知和环绕通知的结束部分不会执行。
15.5.2 切面优先级@Order
我们知道,业务流程当中不一定只有一个切面,可能有的切面控制事务,有的记录日志,有的进行安全控制,如果多个切面的话,顺序如何控制:可以使用@Order注解来标识切面类,为@Order注解的value指定一个整数型的数字,数字越小,优先级越高。
再定义一个切面类,如下:
另一个切面类,并设置优先级
package com.powernode.spring6.service;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Aspect
@Component
@Order(1) //设置优先级
public class YourAspect {
@Around("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("YourAspect环绕通知开始");
// 执行目标方法。
proceedingJoinPoint.proceed();
System.out.println("YourAspect环绕通知结束");
}
@Before("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void beforeAdvice(){
System.out.println("YourAspect前置通知");
}
@AfterReturning("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void afterReturningAdvice(){
System.out.println("YourAspect后置通知");
}
@AfterThrowing("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void afterThrowingAdvice(){
System.out.println("YourAspect异常通知");
}
@After("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void afterAdvice(){
System.out.println("YourAspect最终通知");
}
}
设置切面类MyAspect的优先级
package com.powernode.spring6.service;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
// 切面类
@Component
@Aspect
@Order(2) //设置优先级
public class MyAspect {
@Around("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("环绕通知开始");
// 执行目标方法。
proceedingJoinPoint.proceed();
System.out.println("环绕通知结束");
}
@Before("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void beforeAdvice(){
System.out.println("前置通知");
}
@AfterReturning("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void afterReturningAdvice(){
System.out.println("后置通知");
}
@AfterThrowing("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void afterThrowingAdvice(){
System.out.println("异常通知");
}
@After("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void afterAdvice(){
System.out.println("最终通知");
}
}
注意看:现在YourAspect这个切面类中的所有通知都是以YourAspect开头的,并且这两个切面类都是服务于同一个切点
执行测试程序:
通过修改@Order注解的整数值来切换顺序,执行测试程序:
15.5.3 通用切点表达式@Pointcut
观看以下代码中的切点表达式:
package com.powernode.spring6.service;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
// 切面类
@Component
@Aspect
@Order(2)
public class MyAspect {
@Around("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("环绕通知开始");
// 执行目标方法。
proceedingJoinPoint.proceed();
System.out.println("环绕通知结束");
}
@Before("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void beforeAdvice(){
System.out.println("前置通知");
}
@AfterReturning("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void afterReturningAdvice(){
System.out.println("后置通知");
}
@AfterThrowing("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void afterThrowingAdvice(){
System.out.println("异常通知");
}
@After("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void afterAdvice(){
System.out.println("最终通知");
}
}
缺点是:
- 第一:切点表达式重复写了多次,没有得到复用。
- 第二:如果要修改切点表达式,需要修改多处,难维护。
对于这种情况我们可以这样做:将切点表达式单独的定义出来,在需要的位置引入即可。如下:
package com.powernode.spring6.service;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
// 切面类
@Component
@Aspect
@Order(2)
public class MyAspect {
//单独定义一个方法来设置通用表达式
@Pointcut("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void pointcut(){}
@Around("pointcut()")
public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("环绕通知开始");
// 执行目标方法。
proceedingJoinPoint.proceed();
System.out.println("环绕通知结束");
}
@Before("pointcut()")
public void beforeAdvice(){
System.out.println("前置通知");
}
@AfterReturning("pointcut()")
public void afterReturningAdvice(){
System.out.println("后置通知");
}
@AfterThrowing("pointcut()")
public void afterThrowingAdvice(){
System.out.println("异常通知");
}
@After("pointcut()")
public void afterAdvice(){
System.out.println("最终通知");
}
}
总结:当一个切点表达式在多个通知上重复利用,那么你就可以把这个切点表达式独立出来作为一个通用的表达式
使用@Pointcut注解来定义独立的切点表达式。
注意这个@Pointcut注解标注的方法随意,只是起到一个能够让@Pointcut注解编写的位置。并且可以跨类使用,比如说我再当前切点类中定义的通用表达式在另外一个切点类也是可以使用的只不过需要加全限定类名如下:
本类当中直接调用通用切点对应的方法
跨类使用:全限定类名.方法名
15.5.4 连接点
连接点是一个对象,其实我们在前面已经用过一个连接点了,我们在前面环绕通知中用到一个叫ProceedingJoinPoint 翻译为进程连接点
这就是一个连接点,这个连接点是专门用在循环通知中的,接下来介绍另外一个连接点:Pointcut
翻译为切入点,这个对象里面有很多方法,目前我们只需要知道一个叫getSignature()——获取目标方法的签名(签名长这样:修饰符 返回值 方法名,比如public void fangfa名 这一段就是方法的签名)。
通过joinPoint.getSignature获取方法的签名对象Signature,这个签名对象保存了目标方法的具体信息(访问修饰符 返回类型 方法名 )然后通过这个签名对象可以获取到对应的目标方法的具体信息。
15.5.5 全注解式开发AOP
就是编写一个类,在这个类上面使用大量注解来代替spring的配置文件,spring配置文件消失了,如下:
//这个类的角色:替代spring.xml,是一个可以创建Bean对象的Bean
//想象一下如果是在spring.xml配置文档中是怎么做的?
//1.首先你得有一个spring.xml配置文档吧,
@Configuration //翻译为配置,作用是代替spring.xml配置文档
//2.是不是得组件扫描进行扫包?不管是切面类还是目标类或者工厂类都得需要纳入IoC容器进行管理
@ComponentScan({"com.yzh.spring6.service"})//组件扫描
//开启AspectJ动态代理机制(表示在组价扫描的时候检查类是否有@Aspect注解如果有的话会自动代理对象,并且使用cglib方式生成)
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class springConfig {
}
测试程序中获取应用上下文的方式也得改变
@Test
public void testSpringConfig(){
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(springConfig.class);
UserService userService = applicationContext.getBean("userService", UserService.class);
userService.login();
}
执行结果如下:
15.5.6 AOP基于XML方式实现
没做笔记,这个不怎么用,用到再说吧
15.6 AOP的实际案例:事务处理
项目中的事务控制是在所难免的。在一个业务流程当中,可能需要多条DML语句共同完成,为了保证数据的安全,这多条DML语句要么同时成功,要么同时失败。这就需要添加事务控制的代码。例如以下伪代码:
class 业务类1{
public void 业务方法1(){
try{
// 开启事务
startTransaction();
// 执行核心业务逻辑
step1();
step2();
step3();
....
// 提交事务
commitTransaction();
}catch(Exception e){
// 回滚事务
rollbackTransaction();
}
}
public void 业务方法2(){
try{
// 开启事务
startTransaction();
// 执行核心业务逻辑
step1();
step2();
step3();
....
// 提交事务
commitTransaction();
}catch(Exception e){
// 回滚事务
rollbackTransaction();
}
}
public void 业务方法3(){
try{
// 开启事务
startTransaction();
// 执行核心业务逻辑
step1();
step2();
step3();
....
// 提交事务
commitTransaction();
}catch(Exception e){
// 回滚事务
rollbackTransaction();
}
}
}
class 业务类2{
public void 业务方法1(){
try{
// 开启事务
startTransaction();
// 执行核心业务逻辑
step1();
step2();
step3();
....
// 提交事务
commitTransaction();
}catch(Exception e){
// 回滚事务
rollbackTransaction();
}
}
public void 业务方法2(){
try{
// 开启事务
startTransaction();
// 执行核心业务逻辑
step1();
step2();
step3();
....
// 提交事务
commitTransaction();
}catch(Exception e){
// 回滚事务
rollbackTransaction();
}
}
public void 业务方法3(){
try{
// 开启事务
startTransaction();
// 执行核心业务逻辑
step1();
step2();
step3();
....
// 提交事务
commitTransaction();
}catch(Exception e){
// 回滚事务
rollbackTransaction();
}
}
}
//......
可以看到,这些业务类中的每一个业务方法都是需要控制事务的,而控制事务的代码又是固定的格式,都是:
try{
// 开启事务
startTransaction();
// 执行核心业务逻辑
//......
// 提交事务
commitTransaction();
}catch(Exception e){
// 回滚事务
rollbackTransaction();
}
这个控制事务的代码就是和业务逻辑没有关系的“交叉业务”。以上伪代码当中可以看到这些交叉业务的代码没有得到复用,并且如果这些交叉业务代码需要修改,那必然需要修改多处,难维护,怎么解决?可以采用AOP思想解决。可以把以上控制事务的代码作为环绕通知,切入到目标类的方法当中。接下来我们做一下这件事,有两个业务类,如下:
银行账户的业务类
package com.yzh.service;
import org.springframework.stereotype.Service;
/**
* 目标类
* 生成订单类
*/
@Service(value = "generateOrderService")
public class GenerateOrderService {
//生成订单(目标方法)
public void orderGenerate(){
System.out.println("订单生成中......");
}
//取消订单(目标方法)
public void orderCancel(){
System.out.println("订单取消......");
}
}
订单业务类
package com.yzh.service;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
//目标类
//转账服务
@Service(value = "transferService")
public class TransferService {
public void transfer(){
System.out.println("正在转账......");
String str = null;
str.toString();//模拟异常
}
//取款业务
public void withdraw(){
System.out.println("正在取款......");
}
}
切面类
package com.yzh.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
/**
* 切面类
*/
@Aspect
@Component
public class TransferAspect {
//回滚事务
@Around("execution(* com.yzh.service..*(..))")
public void TransactionAspect(ProceedingJoinPoint proceedingJoinPoint){
try {
System.out.println("开启事务");//前环绕
proceedingJoinPoint.proceed();//目标方法
System.out.println("提交事务");
} catch (Throwable e) {//后环绕
System.out.println("出现异常回滚事务");
}
}
}
配置文件
<?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"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<context:component-scan base-package="com.yzh"/>
<aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>
public class testAspect {
@Test
public void test1(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
GenerateOrderService generateOrderService = applicationContext.getBean("generateOrderService", GenerateOrderService.class);
TransferService transferService = applicationContext.getBean("transferService", TransferService.class);
transferService.transfer();
generateOrderService.orderGenerate();
transferService.withdraw();
generateOrderService.orderCancel();
}
}
十六、Spring对事务的支持
16.1 事务概述
- 什么是事务
- 在一个业务流程当中,通常需要多条DML(insert delete update)语句共同联合才能完成,这多条DML语句必须同时成功,或者同时失败,这样才能保证数据的安全。
- 多条DML要么同时成功,要么同时失败,这叫做事务。
- 事务:Transaction(tx)
- 事务的四个处理过程:
- 第一步:开启事务 (start transaction)
- 第二步:执行核心业务代码
- 第三步:提交事务(如果核心业务处理过程中没有出现异常)(commit transaction)
- 第四步:回滚事务(如果核心业务处理过程中出现异常)(rollback transaction)
- 事务的四个特性:
- A 原子性:事务是最小的工作单元,不可再分。
- C 一致性:事务要求要么同时成功,要么同时失败。事务前和事务后的总量不变。
- I 隔离性:事务和事务之间因为有隔离性,才可以保证互不干扰。
- D 持久性:持久性是事务结束的标志。
16.2 引入事务场景
以银行账户转账为例学习事务。两个账户act-001和act-002。act-001账户向act-002账户转账10000,必须同时成功,或者同时失败。(一个减成功,一个加成功, 这两条update语句必须同时成功,或同时失败。)
连接数据库的技术采用Spring框架的JdbcTemplate。
采用三层架构搭建:
此处省略
我们直接看一段代码
public class AccountServiceImpl implements AccountService {
//业务层需要调用持久层所以需要一个持久层对象,并且采用这种@Resource注入的方式进行注入
@Resource(name = "accountDaoImpl")
private AccountDao accountDao;
//转账服务,需提供转出账户、转入账户、转账金额
@Override
public void transfer(String fromActno, String toActno, double money) {
//先获取转出账户对象
Account fromAccount = accountDao.selectByActno(fromActno);
//判断转出账户余额是否大于转账金额
if (fromAccount.getBalance() < money){
throw new RuntimeException("余额不足,转账失败!!!");
}
//若程序能走到这里代表转出账户余额充足
//先获取转入账户对象
Account toAccount = accountDao.selectByActno(toActno);
//现在我内存中已经存在两个账户对象,分别是转出和转入的账户对象
//先更新内存中的两个对象余额,完成内存对象之间的转账
fromAccount.setBalance(fromAccount.getBalance() - money);
toAccount.setBalance(toAccount.getBalance() + money);
//再把更新余额后的两个对象更新到数据库
int count = accountDao.update(fromAccount);
// String str = null;
// str.toString();
count += accountDao.update(toAccount);
if (count != 2){
throw new RuntimeException("转账失败,请联系Bank");
}
}
}
这是一个很经典的转账,很显然在上面这个方法是需要一个事务来控制的,那么传统方式就是采用硬编码的方式把整个方法包裹住。那我们来看看在spring.xml给我们提供的简便事务机制使用方式。
16.3 Spring对事务的支持
16.3.1 Spring实现事务的两种方式
- 编程式事务
- 通过编写代码的方式来实现事务的管理。
- 声明式事务
- 基于注解方式
- 基于XML配置方式
16.3.2 Spring事务管理API
Spring对事务的管理底层实现方式是基于AOP实现的。采用AOP的方式进行了封装。所以Spring专门针对事务开发了一套API,API的核心接口如下:
PlatformTransactionManager接口:spring事务管理器的核心接口。在Spring6中它有两个实现类:
- DataSourceTransactionManager:支持JdbcTemplate、MyBatis、Hibernate等事务管理。
- JtaTransactionManager:支持分布式事务管理。
这两个实现类spring都给我们实现了,我们只需要把它放入IoC容器即可使用
16.3.3 声明式事务之注解实现方式(最常用)
第一步:在spring配置文件中配置事务管理器。
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--直接打DataSourceTransactionManager他会自动补全包名
他还需要一个数据源,为什么?因为你是对数据库控制事务对不对?
那他是不是这样控制的
--------------------------------------------------------------------
开启事务(将自动提交机制关闭就是开启事务):conn.setAutoComnmit(false);
核心业务执行
无异常提交事务:conn.commit();
有异常回滚事务:conn.rollback();
-----------------------------------------------------------------------
那这个连接对象conn是不是需要给他?
那数据源是不是有很多连接对象?
所以给他数据源
-->
第二步:在spring配置文件中引入tx命名空间。
<?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"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
//注意:tx是transaction的缩写,transaction翻译为事务
第三步:在spring配置文件中配置“事务注解驱动器”,开始注解的方式控制事务。
<!--开启事务注解驱动器,也就是开启事务注解。告诉spring框架,采用注解的方式去控制事务.
这里需要为tx配置命名空间,tx是transaction(事务)的缩写-->
<tx:annotation-driven transaction-manager="txManager"/>
第四步:在service类上或方法上添加@Transactional注解
@Service(value = "accountServiceImpl")
//@Transactional 或者这里 (这个类中所有方法都用事务控制)
public class AccountServiceImpl implements AccountService {
@Resource(name = "accountDaoImpl")
private AccountDao accountDao;
@Override
@Transactional //放在这里(仅这个方法用事务控制)
public void transfer(String fromActno, String toActno, double money) {
Account fromAccount = accountDao.selectByActno(fromActno);
if (fromAccount.getBalance() < money){
throw new RuntimeException("余额不足,转账失败!!!");
}
Account toAccount = accountDao.selectByActno(toActno);
fromAccount.setBalance(fromAccount.getBalance() - money);
toAccount.setBalance(toAccount.getBalance() + money);
int count = accountDao.update(fromAccount);
count += accountDao.update(toAccount);
if (count != 2){
throw new RuntimeException("转账失败,请联系Bank");
}
}
}
16.4 事务的属性
事务属性包括哪些
以上都是重要的属性:
- 事务传播行为
- 事务隔离级别
- 事务超时
- 只读事务
- 设置出现哪些异常回滚事务
- 设置出现哪些异常不回滚事务
16.4.1 事务传播行为
什么是事务的传播行为?
在service类中有a()方法和b()方法,a()方法上有事务,b()方法上也有事务,当a()方法执行过程中调用了b()方法,事物就发生了碰撞了,那事务是如何传递的?合并到一个事务里?还是开启一个新的事务?这就是事务传播行为。
事务传播行为在spring框架中被定义为枚举类型:
一共有七种传播行为:
- REQUIRED:支持当前事务,如果不存在就新建一个(默认)【没有就新建,有就加入】
- SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行【有就加入,没有就不管了】
- MANDATORY:必须运行在一个事务中,如果当前没有事务正在发生,将抛出一个异常【有就加入,没有就抛异常】
- REQUIRES_NEW:开启一个新的事务,如果一个事务已经存在,则将这个存在的事务挂起【不管有没有,直接开启一个新事务,开启的新事务和之前的事务不存在嵌套关系,之前事务被挂起】
- NOT_SUPPORTED:以非事务方式运行,如果有事务存在,挂起当前事务【不支持事务,存在就挂起】
- NEVER:以非事务方式运行,如果有事务存在,抛出异常【不支持事务,存在就抛异常】
- NESTED:如果当前正有一个事务在进行中,则该方法应当运行在一个嵌套式事务中。被嵌套的事务可以独立于外层事务进行提交或回滚。如果外层事务不存在,行为就像REQUIRED一样。【有事务的话,就在这个事务里再嵌套一个完全独立的事务,嵌套的事务可以独立的提交和回滚。没有事务就和REQUIRED一样。】
挂起可以认为是暂停的意思
在代码中设置事务的传播行为:
@Transactional(propagation = Propagation.REQUIRED)
一定要集成Log4j2日志框架,在日志信息中可以看到更加详细的信息。
16.4.2 事务隔离级别
事务隔离级别类似于教室A和教室B之间的那道墙,隔离级别越高表示墙体越厚。隔音效果越好。
数据库中读取数据存在的三大问题:(三大读问题)
脏读:读取到没有提交到数据库的数据,叫做脏读。也就是说读取到内存中的数据叫脏读。(关键词:读取到内存中的数据)
幻读:比如说两个事务并发执行操作同一张表,第一个事务对表中的所有记录进行了修改,在他修改的同时,第二个事务向表中插入“一条新纪录”。那么当地一个事务执行完毕后发现还有一条记录没有被修改,这条数据就是第二个事务插入的那条新纪录,对于事务一来说这就造成了幻读。(关键词:多事务并发)
不可重复读:在同一个事务当中,第一次和第二次读取的数据不一样。
事务隔离级别包括四个级别:
- 读未提交:READ_UNCOMMITTED
- 这种隔离级别,存在脏读问题,所谓的脏读(dirty read)表示能够读取到其它事务未提交的数据。
- 读提交:READ_COMMITTED
- 解决了脏读问题,其它事务提交之后才能读到,但存在不可重复读问题。
- 可重复读:REPEATABLE_READ
- 解决了不可重复读,可以达到可重复读效果,只要当前事务不结束,读取到的数据一直都是一样的。但存在幻读问题。
- 序列化:SERIALIZABLE
- 解决了幻读问题,事务排队执行。不支持并发。
大家可以通过一个表格来记忆:
隔离级别
脏读
不可重复读
幻读
读未提交
有
有
有
读提交
无
有
有
可重复读
无
无
有
序列化
无
无
无
注意:多事务并发是交替执行,多事务并发那么你这些事务肯定是在某一事件段同时待在内存中的,操作系统调度这些事务去轮流切换访问同一个硬件资源(比如数据库表).那么在这种情况下你的肯定会存在脏读、幻读、不可重复读的情况。(联想一下操作系统中学的多道批处理和分时)
在Spring代码中如何设置隔离级别?
隔离级别在spring中以枚举类型存在:
@Transactional(isolation = Isolation.READ_COMMITTED)
这里有一个演示案例,可以去看看视频P129
16.4.2 事务超时
首先搞清楚事务超时中的超时指的是哪个时间段,先看下面这张图:
这个超时时间可以手动设置:
@Transactional(timeout = 10)
//设置超时时间为10秒
案例:
@Transactional(timeout = 10) // 设置事务超时时间为10秒。
public void save(Account act) {
accountDao.insert(act);
// 模拟超时
try {
Thread.sleep(1000 * 15);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//注意:现在这个事务肯定是超过十秒的,但是因为从开启事务到执行
//accountDao.insert(act);这最后一条DML语句没有超过10秒则这个事务
//不会回滚,但是如果把模拟超时放在DML前面则会回滚,如下:
@Transactional(timeout = 10) // 设置事务超时时间为10秒。
public void save(Account act) {
// 模拟超时
try {
Thread.sleep(1000 * 15);
} catch (InterruptedException e) {
e.printStackTrace();
}
accountDao.insert(act);
}
16.4.3 只读事务
代码如下:
@Transactional(readOnly = true)
将当前事务设置为只读事务,在该事务执行过程中只允许select语句执行,delete insert update均不可执行。
该特性的作用是:启动spring的优化策略。提高select语句执行效率。
主要作用就是为了提高效率,如果该事务中确实没有增删改操作,建议设置为只读事务这也是一个面试点。
问个问题:只读事务怎么传播?
16.4.4 设置哪些异常回滚事务
代码如下:
@Transactional(rollbackFor = RuntimeException.class)
表示只有发生RuntimeException异常或该异常的子类异常才回滚。
16.4.5 设置哪些异常回滚事务
代码如下:
@Transactional(rollbackFor = RuntimeException.class)
表示发生NullPointerException或该异常的子类异常不回滚,其他异常则回滚。
16.5 全注解式开发
全注解式开发就是写一个类来代替spring.xml,然后在获取App上下文对象的时候采用
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Spring6Config.class);
这种方式去获取即可。
<?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"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<!--配置扫包-->
<context:component-scan base-package="com.yzh.bank"/>
<!--配置德鲁伊数据源-->
<bean id="DruidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/spring6"/>
<property name="username" value="root"/>
<property name="password" value=""/>
</bean>
<!--配置JdbcTemplate,这个bean会被注入到Dao层的jdbcTemplate属性中-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="DruidDataSource"/>
</bean>
<!--配置事务管理器,下面的tx是transaction(事务)的缩写-->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="DruidDataSource"/>
</bean>
<!--开启事务注解驱动器,也就是开启事务注解。告诉spring框架,采用注解的方式去控制事务.
这里需要为tx配置命名空间,tx是transaction(事务)的缩写-->
<tx:annotation-driven transaction-manager="txManager"/>
</beans>
创建一个类:SpringConfig来替代上面这个xml配置文档
package com.yzh.bank.utils;
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.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
@Configuration //翻译为配置,作用是代替spring.xml配置文档
@ComponentScan("com.yzh.bank")//扫包,相当于<context:component-scan base-package="com.yzh.bank"/>
@EnableTransactionManagement //开启事务管理器,相当于<tx:annotation-driven transaction-manager="txManager"/>
public class Spring6Config {
/**
* Spring框架,看到这个@Bean注解后,会调用这个被标注的方法,
* 这个方法的返回值是一java对象,这java对象会自动纳入IOC容器营理.
* 返回的对象就是Spring 容器当中的一个Bean 了。
* 这里取bean的名字是: dataSource(取名随意)
*/
@Bean(name = "dataSource")
public DataSource getDataSource(){
DruidDataSource dataSource1 = new DruidDataSource();
dataSource1.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource1.setUrl("jdbc:mysql://localhost:3306/spring6");
dataSource1.setUsername("root");
dataSource1.setPassword("");
return dataSource1;
}
//DataSource dataSource这个参数会类型or名字去IoC容器里面找,完成自动装配
//显然前面那个@Bean(name = "dataSource")会自动装配到这里
@Bean(name = "jdbcTemplate")
public JdbcTemplate getJdbcTemplate(DataSource dataSource){
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(dataSource);
return jdbcTemplate;
}
//DataSource dataSource这个参数会类型or名字去IoC容器里面找,完成自动装配
//显然前面那个@Bean(name = "dataSource")会自动装配到这里
@Bean(name = "txManager")
public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource){
DataSourceTransactionManager txManager = new DataSourceTransactionManager();
txManager.setDataSource(dataSource);
return txManager;
}
}
这里有一个细节不知道你注意到没有:xml中的bean标签对应的是类内部的方法,其他标签会采用注解的方式在类上面声明。
16.6 声明式事务之XML实现方式
没学,用到你在回去看吧,P135
十七、Spring6整合JUnit5
17.1 Spring对JUnit4的支持
准备工作:引入spring对JUnit4支持的依赖(需要两个依赖)
<!--spring对junit的支持相关依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>6.0.2</version>
</dependency>
<!--junit4依赖-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
单元测试:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring.xml")
public class testTransfer {
@Autowired//根据类型自动注入(AccountService这个类或者他的子类都可以,但只能存在一个这样的类型)
private AccountService accountServiceImpl;
@Test
public void test1(){
//这里不需要再去获取应用上下文对象了,也不需要再去getBean获取bean了
//getBean的步骤直接在上面使用自动装配为属性赋值拿到了
try {
accountServiceImpl.transfer("act-001","act-002",10000);
System.out.println("转账成功");
} catch (Exception e) {
e.printStackTrace();
}
}
}
17.1 Spring对JUnit5的支持
准备工作:引入spring对JUnit4支持的依赖(需要两个依赖)
<!--spring对junit的支持相关依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>6.0.2</version>
</dependency>
<!--junit5依赖-->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.0</version>
<scope>test</scope>
</dependency>
单元测试:
package com.powernode.spring6.test;
import com.powernode.spring6.bean.User;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@ExtendWith(SpringExtension.class)//注意这里和junit4不一样了
@ContextConfiguration("classpath:spring.xml")
public class SpringJUnit5Test {
@Autowired
private User user;
@Test
public void testUser(){
System.out.println(user.getName());
}
}
在JUnit5当中,可以使用Spring提供的以下两个注解,标注到单元测试类上,这样在类当中就可以使用为单元测试的属性注入使用注解了。
@ExtendWith(SpringExtension.class)
@ContextConfiguration("classpath:spring.xml")
总结:从上面可以看出其实无论是junit4还是5 其实主要目的就是为了在单元测试中可以对属性注入,这样我们就是省略创建app上下文然后通过上下文去getBean了。也就是:
省略这一步,然后直接在单元测试类中定义一个要获取的Bean,然后注解为他注入。
十八、Spring6集成MyBatis3.5
18.1 实现步骤
- 第一步:准备数据库表
- 使用t_act表(账户表)
- 第二步:IDEA中创建一个模块,并引入依赖
- spring-context
- spring-jdbc
- mysql驱动
- mybatis
- mybatis-spring:mybatis提供的与spring框架集成的依赖
- 德鲁伊连接池
- junit
- 第三步:基于三层架构实现,所以提前创建好所有的包
- com.powernode.bank.mapper
- com.powernode.bank.service
- com.powernode.bank.service.impl
- com.powernode.bank.pojo
- 第四步:编写pojo
- Account,属性私有化,提供公开的setter getter和toString。
- 第五步:编写mapper接口
- AccountMapper接口,定义方法
- 第六步:编写mapper配置文件
- 在配置文件中配置命名空间,以及每一个方法对应的sql。
- 第七步:编写service接口和service接口实现类
- AccountService
- AccountServiceImpl
- 第八步:编写jdbc.properties配置文件
- 数据库连接池相关信息
- 第九步:编写mybatis-config.xml配置文件
- 该文件可以没有,大部分的配置可以转移到spring配置文件中。
- 如果遇到mybatis相关的系统级配置,还是需要这个文件。
- 第十步:编写spring.xml配置文件
- 组件扫描
- 引入外部的属性文件
- 数据源
- SqlSessionFactoryBean配置
- 注入mybatis核心配置文件路径
- 注入数据源
- 指定别名包
- Mapper扫描配置器
- 指定扫描的包
- 事务管理器DataSourceTransactionManager
- 注入数据源
- 启用事务注解
- 注入事务管理器
- 第十一步:编写测试程序,并添加事务,进行测试
18.2 具体实现
具体实现就不写了,一下是需要用到的依赖
<!--
○ spring-context
○ spring-jdbc
○ mysql驱动
○ mybatis
○ mybatis-spring:mybatis提供的与spring框架集成的依赖
○ 德鲁伊连接池
○ junit
-->
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.2</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>6.0.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.11</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.13</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<!--@Resource注入依赖,如果使用@Autowire可以不用引用这个-->
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>2.1.1</version>
</dependency>
</dependencies>
spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"
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 http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<!--
○ 组件扫描
○ 引入外部的属性文件
○ 数据源
○ SqlSessionFactoryBean配置
■ 注入mybatis核心配置文件路径
■ 指定别名包
■ 注入数据源
○ Mapper扫描配置器
■ 指定扫描的包
○ 事务管理器DataSourceTransactionManager
■ 注入数据源
○ 启用事务注解
■ 注入事务管理器
-->
<!--组件扫描-->
<context:component-scan base-package="com.yzh.bank"/>
<!--引入外部的属性文件-->
<context:property-placeholder location="jdbc.properties"/>
<!--数据源-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<!--
○ SqlSessionFactoryBean配置
■ 注入mybatis核心配置文件路径
■ 指定别名包
■ 注入数据源
这个类只有一个所以不需要id
-->
<bean class="org.mybatis.spring.SqlSessionFactoryBean">
<!--mybatis核心配置文件-->
<property name="configLocation" value="mybatis-config.xml"/>
<!--注入数据源-->
<property name="dataSource" ref="dataSource"/>
<!--起别名-->
<property name="typeAliasesPackage" value="com.yzh.bank.pojo"/>
</bean>
<!--Mapper扫描器-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.yzh.bank.mapper"/>
</bean>
<!--事务管理器-->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--开启事务注解-->
<tx:annotation-driven transaction-manager="txManager"/>
</beans>
mybatis-config.xml
<?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">
<!--
○ 该文件可以没有,大部分的配置可以转移到spring配置文件中。
○ 如果遇到mybatis相关的系统级配置,还是需要这个文件。
-->
<configuration>
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
</configuration>
18.3 Spring配置文件的import
spring配置文件有多个,并且可以在spring的核心配置文件中使用import进行引入,我们可以将组件扫描单独定义到一个配置文件中,如下:
注意:在实际开发中,service单独配置到一个文件中,dao单独配置到一个文件中,然后在核心配置文件中引入,养成好习惯。
十九、Spring中的八大模式
19.1 简单工厂模式
BeanFactory的getBean()方法,通过唯一标识来获取Bean对象。是典型的简单工厂模式(静态工厂模式);
19.2 工厂方法模式
FactoryBean是典型的工厂方法模式。在配置文件中通过factory-method属性来指定工厂方法,该方法是一个实例方法。
19.3 单例模式
Spring用的是双重判断加锁的单例模式。请看下面代码,我们之前讲解Bean的循环依赖的时候见过:
19.4 代理模式
Spring的AOP就是使用了动态代理实现的。
19.5 装饰器模式
JavaSE中的IO流是非常典型的装饰器模式。
Spring 中配置 DataSource 的时候,这些dataSource可能是各种不同类型的,比如不同的数据库:Oracle、SQL Server、MySQL等,也可能是不同的数据源:比如apache 提供的org.apache.commons.dbcp.BasicDataSource、spring提供的org.springframework.jndi.JndiObjectFactoryBean等。
这时,能否在尽可能少修改原有类代码下的情况下,做到动态切换不同的数据源?此时就可以用到装饰者模式。
Spring根据每次请求的不同,将dataSource属性设置成不同的数据源,以到达切换数据源的目的。
Spring中类名中带有:Decorator和Wrapper单词的类,都是装饰器模式。
19.6 观察者模式
定义对象间的一对多的关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。Spring中观察者模式一般用在listener的实现。
Spring中的事件编程模型就是观察者模式的实现。在Spring中定义了一个ApplicationListener接口,用来监听Application的事件,Application其实就是ApplicationContext,ApplicationContext内置了几个事件,其中比较容易理解的是:ContextRefreshedEvent、ContextStartedEvent、ContextStoppedEvent、ContextClosedEvent
19.7 策略模式
策略模式是行为性模式,调用不同的方法,适应行为的变化 ,强调父类的调用子类的特性 。
getHandler是HandlerMapping接口中的唯一方法,用于根据请求找到匹配的处理器。
比如我们自己写了AccountDao接口,然后这个接口下有不同的实现类:AccountDaoForMySQL,AccountDaoForOracle。对于service来说不需要关心底层具体的实现,只需要面向AccountDao接口调用,底层可以灵活切换实现,这就是策略模式。
19.8 模板方法模式
Spring中的JdbcTemplate类就是一个模板类。它就是一个模板方法设计模式的体现。在模板类的模板方法execute中编写核心算法,具体的实现步骤在子类中完成。