一、Spring框架概述
1.1、Spring是什么
Spring 是分层的 Java SE/EE 应用 full-stack【全栈式】 轻量级开源框架,以 IoC(Inverse **Of Control:**
反转控制)和 **AOP(Aspect Oriented Programming:面向切面编程)**为核心,提供了展现层 Spring MVC 和持久层 Spring JDBC 以及业务层事务管理等众多的企业级应用技术,还能整合开源世界众多著名的第三方框架和类库,逐渐成为使用最多的 Java EE 企业应用开源框架。
Spring倡导以==“最少侵入”的方式来管理应用中的代码,这意味着我们可以随时安装或者卸载 Spring。这里的"最少侵入"可以理解为:我们的项目中所使用的类无需继承框架提供的任何类,这样我们在更换框架时,之前写过的代码几乎可以继续使用。==
轻量级:轻量级是相对于重量级而言的,轻量级一般就是非入侵性的、所依赖的东西非常少、资源占用非常少、部署简单等等,其实就是比较容易使用,而重量级正好相反 。
1.2、Spring的优势
-
方便解耦,简化开发
通过 Spring 提供的 IoC 容器,可以将==对象间的依赖关系交由 Spring 进行控制==,避免硬编码所造成的过度程序耦合。用户也不必再为单例模式类、属性文件解析等这些很底层的需求编写代码,可以更专注于上层的应用。
-
AOP 编程的支持
通过 Spring 的 AOP 功能,方便进行面向切面的编程,许多不容易用传统 OOP 实现的功能可以通过 AOP 轻松应付。
-
声明式事务的支持
可以将我们从单调烦闷的事务管理代码中解脱出来,通过声明式方式灵活的进行事务的管理,提高开发效率和质量。
-
方便程序的测试
可以用非容器依赖的编程方式进行几乎所有的测试工作,测试不再是麻烦的操作,而是随手可做的事情。
-
方便集成各种优秀框架
Spring 可以降低各种框架的使用难度,提供了对各种优秀框架(Struts、Hibernate、SpringMVC、MyBatis等)的直接支持。
-
降低 JavaEE API 的使用难度
Spring 对 JavaEE API(如 JDBC、JavaMail、远程调用等)进行了薄薄的封装层,使这些 API 的使用难度大为降低。
-
Spring 框架中包括了 J2EE 三层的每一层的解决方案(一站式)
-
Java 源码是经典学习范例
Spring 的源代码设计精妙、结构清晰、匠心独用,处处体现着大师对 Java 设计模式灵活运用以及对 Java 技术的高深造诣。它的源代码无意是 Java 技术的最佳实践的范例。
1.3、Spring能帮助我们做什么
- Spring 能帮我们根据配置文件创建及组装对象之间的依赖关系
- Spring 面向切面编程能帮助我们无耦合的实现日志记录,性能统计,安全控制。
- Spring 能非常简单的帮我们管理数据库事务。
- Spring 还提供了与第三方数据访问框架(如Hibernate、JPA)无缝集成,而且自己也提供了一套JDBC访问模板来方便数据库访问。
- Spring 还提供与第三方Web(如Struts1/2、JSF)框架无缝集成,而且自己也提供了一套Spring MVC框架,来方便web层搭建。
- Spring 能方便的与Java EE(如Java Mail、任务调度)整合,与更多技术整合(比如缓存框架)。
- 等等…
1.4、Spring体系结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ohDYwI1g-1634112348618)(images/001_Spring体系结构.png)]
1.5、Spring的官网
1.6、Spring的使命/理念
总结:简化Java应用程序开发
二、传统方式创建对象的弊端
以经典的三层架构MVC作为案例,以前我们都是这么干的,看如下代码:
2.1、案例如下
2.1.1、控制器层(Controller)
// 用户控制器
public class UserServlet{
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response){
// ...
userService.login();
// ...
}
}
2.1.2、业务层(Service)
// 用户Service接口
public interface UserService{
public User login(String uname,String pwd);
}
// 用户Service实现类
public class UserServiceImpl implements UserService{
private UserDao userDao = new UserDaoImpl();
public User login(String uname,String pwd){
userDao.selectByUnameAndPwd(uname,pwd);
}
}
2.1.3、数据库访问层(Dao)
// 用户Dao接口
public interface UserDao{
public User selectByUnameAndPwd(String uname,String pwd);
}
// 用户Dao实现
public class UserDaoImpl implements UserDao{
public User selectByUnameAndPwd(String uname,String pwd){
// 编写sql语句
}
}
2.2、问题分析
按照以前的做法,可以发现,我们的UserServlet要依赖UserServiceImpl实现,UserServiceImpl要依赖UserDaoImpl实现,一个很明显的问题是:对于这些具体的实现类是由我们程序员自己去主动手动创建(new)出来的,那么意味着假如某个实现类发生了变化,将可能导致整个应用的不可用【编译不通过等】,那么这个问题的本质原因就在于这个具体的类是我们自己硬编码到Java类中的。试想:能否有一种方式可以实现在不改变代码的前提下实现自如的切换具体的实现类呢?答案是可以的,那么我们可以将创建这个实现类的工作交给别人来做,而我们就不再去自己主动创建类的对象了。那么解决方案就是:Spring的Ioc。
三、IoC(控制反转)/DI(依赖注入)
以四章的HelloWorld为例,这个创建对象的工作不是由程序员主动去创建,而是交给了框架,比如说Spring,那么这个时候我们称**创建对象的控制权交给了其他人,那么就称之为控制反转。**
3.1、IoC:Inverse of Control(控制反转)
- 不是什么技术,而是一种设计思想,就是将原本在程序中手动创建对象的控制权,交由Spring框架来管理。
- 若要使用某个对象,只需要从 Spring 容器中获取需要使用的对象,不关心对象的创建过程,也就是把创建对象的控制权反转给了Spring框架
3.2、生活案例
在现实生活中,人们要用到一样东西的时候,第一反应就是去找到这件东西,比如想喝新鲜橙汁,在没有饮品店的日子里,最直观的做法就是:买果汁机、买橙子,然后准备开水。值得注意的是:这些都是你自己**“主动”创造**的过程,也就是说一杯橙汁需要你自己创造。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nGEUrEgj-1634112348622)(images/002_主动创建.png)]
然而到了今时今日,由于饮品店的盛行,当我们想喝橙汁时,第一想法就转换成了找到饮品店的联系方式,通过电话等渠道描述你的需要、地址、联系方式等,下订单等待,过一会儿就会有人送来橙汁了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z1d2JF5V-1634112348623)(images/003_被动创建.png)]
注意:你并没有“主动”去创造橙汁,橙汁是由饮品店创造的,而不是你,然而也完全达到了你的要求,甚至比你创造的要好上那么一些。
3.3、DI:Dependency Injection(依赖注入)
含义:
- 指 Spring 创建对象的过程中,将对象依赖属性(简单值,集合,对象)通过配置设值给该对象
总结:
- IoC 和 DI 其实是同一个概念的不同角度
- 描述,DI 相对 IoC 而言,明确描述了“被注入对象依赖 IoC 容器配置依赖对象”
3.4、开发方式
- 基于xml的配置文件方式
- 基于注解的方式
3.5、总结
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cSyE6WDp-1634112348626)(images/014.png)]
四、HelloWorld案例
以HelloWorld为例,编写一个程序,让创建对象的工作由Spring帮助我们创建。
4.1、编写Java类
public class HelloWorld {
private String name;
public void setName(String name) {
this.name = name;
}
public void sayHi(){
System.out.println(name + ",HelloWorld!");
}
}
4.2、传统方式测试
@Test
public void testSayHi() throws Exception{
HelloWorld helloWorld = new HelloWorld();
helloWorld.setName("段康家");
helloWorld.sayHi();
}
这种做法是以前最常用的做法,HelloWorld这个类的对象是我们程序员自己去创建并为属性赋值,但是要使用Spring,该如何实现同样的功能呢?看4.3以后的章节。
4.3、导入Spring依赖的包
<dependencies>
<!-- 导入Spring的jar包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.1.RELEASE</version>
</dependency>
<!-- 单元测试框架 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
4.4、编写Spring主配置文件
说明:主配置文件的名称一般叫beans.xml或在applicationContext.xml
在resources目录下新建beans.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 https://www.springframework.org/schema/beans/spring-beans.xsd"> <!-- 把HelloWorld对象的创建工作交给Spring--> <bean id="helloWorld" class="cn.bdqn.HelloWorld"> <property name="name" value="彭依凝"/> </bean></beans>
4.5、测试Spring
@Testpublic void testSayHi() throws Exception{ // 1、读取主配置文件信息,获取核心容器对象 ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml"); // 2、从容器中根据id获取对象(bean) HelloWorld helloWorld = (HelloWorld) ac.getBean("helloWorld"); // 3、调用bean的方法 helloWorld.sayHi();}
4.6、案例简单细节
4.6.1、ApplicationContext的三个常用实现类
-
ClassPathXmlApplicationContext
该类可以加载类路径下的配置文件,要求配置文件必须在类路径下。不在的话,加载不了。
-
FileSystemXmlApplicationContext
它可以加载磁盘任意路径下的配置文件。
-
AnnotationConfigApplicationContext
它是用于读取注解创建容器的
4.6.2、BeanFactory和ApplicationContext的区别
4.6.2.1、BeanFactory
是Spring里面最底层的接口,提供了最简单的容器的功能,只提供了实例化对象和拿对象的功能。
BeanFactory在启动的时候不会去实例化Bean,只有从容器中拿Bean的时候才会去实例化。延迟加载
public class UserServiceImpl { public UserServiceImpl(){ System.out.println("对象的构造方法执行了"); }}
<bean id="userService" class="cn.bdqn.UserServiceImpl"/>
@Testpublic void testUserServiceImpl() throws Exception{ // 加载配置文件创建容器并不会导致bean的立即初始化 Resource resource = new ClassPathResource("beans.xml"); BeanFactory bf = new XmlBeanFactory(resource); // 只有再去真正要使用的某个bean的时候才会初始化 UserServiceImpl userService = (UserServiceImpl) bf.getBean("userService"); System.out.println(userService);}
4.6.2.2、ApplicationContext
应用上下文,该接口其实是BeanFactory的子接口,提供了更多的有用的功能:
- 国际化
-轻松的能够与Spring的AOP集成
-消息发布
-…
ApplicationContext在启动的时候就把所有的Bean全部实例化了。立即加载
案例在此就不再举例了。
五、Spring的Bean管理(基于xml的配置)
5.1、概念
在Spring里面通过配置文件==创建对象==。
5.2、创建Bean对象的三种方式
5.2.1、使用默认构造函数创建方式
5.2.1.1、定义Bean
public class UserServiceImpl { }
5.2.1.2、主配置文件中配置bean
<!-- 方式一:使用默认构造函数方式创建Bean --><beans> <bean id="userService" class="cn.bdqn.UserServiceImpl"></bean></beans>
5.2.1.3、测试Bean
@Testpublic void testUserService() throws Exception{ // 1、读取主配置文件信息,获取核心容器对象 ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml"); // 2、从容器中根据id获取对象(bean) UserServiceImpl userService = (UserServiceImpl) ac.getBean("userService"); // 3、打印bean System.out.println(userService);}
5.2.1.4、注意点
此种方式采用的就是通过默认构造函数的方式创建Bean,假设我们给UserServiceImpl添加了一个带参的构造方法,则运行会报错,原因在于当我们为某个类自定义构造方法的时候,Java编译器便不会为该类提供默认的不带参数的构造方法了。
5.2.2、使用工厂中的实例方法创建方式
5.2.2.1、定义工厂
// UserService的工厂,作用是创建UserServiceBean对象public class UserServiceImplFactory { public UserServiceImpl createUserService(){ return new UserServiceImpl(); }}
5.2.2.2、定义Bean
public class UserServiceImpl { }
5.2.2.3、主配置文件中配置Bean
<beans> <!-- 方式二:使用工厂中提供的实例方法创建Bean --> <!-- 第一步:把该工厂定义出来 --> <bean id="userServiceFactory" class="cn.bdqn.UserServiceImplFactory"/> <!-- 第二步:定义Bean(通过userServiceFactory中提供的实例方法)--> <bean id="userService" factory-bean="userServiceFactory" factory-method="createUserService"/></beans>
5.2.2.4、测试
@Testpublic void testUserService() throws Exception{ // 1、读取主配置文件信息,获取核心容器对象 ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml"); // 2、从容器中根据id获取对象(bean) UserServiceImpl userService = (UserServiceImpl) ac.getBean("userService"); // 3、打印bean System.out.println(userService);}
5.2.3、使用工厂中的静态方法创建方式
5.2.3.1、定义工厂
// UserService的工厂,作用是创建UserServiceBean对象public class UserServiceImplFactory { public static UserServiceImpl createUserService(){ return new UserServiceImpl(); }}
5.2.3.2、定义Bean
public class UserServiceImpl {}
5.2.3.3、主配置文件中配置Bean
<beans> <!-- 方式三:使用工厂中提供的静态方法创建Bean --> <!-- 定义Bean(通过工厂类的静态方法创建) --> <bean id="userService" class="cn.bdqn.UserServiceImplFactory" factory-method="createUserService"> </bean></beans>
5.2.3.4、测试
@Testpublic void testUserService() throws Exception{ // 1、读取主配置文件信息,获取核心容器对象 ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml"); // 2、从容器中根据id获取对象(bean) UserServiceImpl userService = (UserServiceImpl) ac.getBean("userService"); // 3、打印bean System.out.println(userService);}
5.3、Bean对象的作用域
5.3.1、说明
Spring对Bean的默认的作用域(作用范围)是singleton【单例】
5.3.2、作用域类型
-
singleton:单例的(默认值),只会new一次。
-
prototype:多例的,用到一次就会new一次。
-
request:作用于web应用的请求范围,Spring创建这个类之后,将这个类存到request范围内。
-
session:应用于web项目的会话范围,Spring创建这个类之后,将这个类存到session范围内。
-
global-session:作用于集群环境的会话范围(全局会话范围),当不是集群环境时,它就是session。
5.3.3、注意细节
实际开发中用得最多的就是singleton和prototype,在整合struts2的时候使用prototype,在整合SpringMVC的时候使用singleton。
5.3.4、如何修改Bean的作用域
bean标签的scope属性,作用:指定bean的作用范围。
5.3.5、测试
5.3.5.1、测试singleton单例
public class UserServiceImpl { }
<beans> <bean id="userService" class="cn.bdqn.UserServiceImpl" /></beans>
@Testpublic void testUserService() throws Exception{ // 1、读取主配置文件信息,获取核心容器对象 ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml"); // 2、从容器中根据id获取对象(bean) UserServiceImpl userService1 = (UserServiceImpl) ac.getBean("userService"); UserServiceImpl userService2 = (UserServiceImpl) ac.getBean("userService"); // 3、打印bean System.out.println(userService1 == userService2); // true}
5.3.5.2、测试prototype多例
public class UserServiceImpl { }
<bean id="userService" class="cn.bdqn.UserServiceImpl" scope="prototype"/>
@Testpublic void testUserService() throws Exception{ // 1、读取主配置文件信息,获取核心容器对象 ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml"); // 2、从容器中根据id获取对象(bean) UserServiceImpl userService1 = (UserServiceImpl) ac.getBean("userService"); UserServiceImpl userService2 = (UserServiceImpl) ac.getBean("userService"); // 3、打印bean System.out.println(userService1 == userService2); // false}
5.4、Bean对象的生命周期
5.4.1、单例对象
5.4.1.1、说明
出生:当容器创建时对象出生活着:只要容器还在,对象一直活着死亡:容器销毁,对象消亡
5.4.1.2、测试
5.4.1.2.1、定义Bean
public class UserServiceImpl { public UserServiceImpl(){ System.out.println("对象的构造方法执行了"); } public void init(){ System.out.println("对象初始化了"); } public void destroy(){ System.out.println("对象销毁了"); }}
5.4.1.2.2、主配置文件中配置Bean
<beans> <bean id="userService" class="cn.bdqn.UserServiceImpl" scope="singleton" init-method="init" destroy-method="destroy"/></beans>
5.4.1.2.3、测试
@Testpublic void testUserService() throws Exception{ // 1、读取主配置文件信息,获取核心容器对象 ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml"); ac.close();}// 结果:对于单例对象来说,只要容器创建了,那么对象就创建了。类似于立即加载。
5.4.1.2.4、测试结果
对象的构造方法执行了对象初始化了对象销毁了
总结:单例对象的生命周期和容器相同
5.4.2、多例对象
5.4.2.1、说明
出生:当我们使用对象时spring框架为我们创建活着:对象只要是在使用过程中就一直活着。死亡:当对象长时间不用,且没有别的对象引用时,由Java的垃圾回收器回收
5.4.2.2、测试
5.4.2.2.1、定义Bean
public class UserServiceImpl { public UserServiceImpl(){ System.out.println("对象的构造方法执行了"); } public void init(){ System.out.println("对象初始化了"); } public void destroy(){ System.out.println("对象销毁了"); }}
5.4.2.2.2、主配置文件中配置Bean
<beans> <bean id="userService" class="cn.bdqn.UserServiceImpl" scope="prototype" init-method="init" destroy-method="destroy"/></beans>
5.4.2.2.3、测试1
@Testpublic void testUserService() throws Exception{ // 1、读取主配置文件信息,获取核心容器对象 ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml"); ac.close();}// 结果:什么都不输出,说明容器启动的时候,对于多例对象来说并不会创建
5.4.2.2.4、测试2
@Testpublic void testUserService() throws Exception{ // 1、读取主配置文件信息,获取核心容器对象 ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml"); UserServiceImpl userService = (UserServiceImpl) ac.getBean("userService"); System.out.println(userService); ac.close();}/** 结果: 对象的构造方法执行了 对象初始化了 说明: 对于多例对象来说,只有等到真正使用到该对象的时候才会创建。类似于懒加载。**/
对于多例的Bean,Spring框架是不负责管理的
5.5、总结
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nXCwTeOH-1634112348627)(images/012_Spring对Bean的管理.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5LuL5N3E-1634112348628)(images/015.png)]
六、Spring的依赖注入
6.1、说明
-
全称
Dependency Injection(DI)
-
与IoC的关系
曾在第一章提到过,IoC和DI其实说的是一个意思,可以这么说:IoC是一种思想,DI是对这种思想的一种具体实现
-
依赖关系的管理
以后都交给spring来维护,在当前类需要用到其他类的对象,由spring为我们提供,我们只需要在配置文件中说明。
-
依赖关系的维护
就称之为依赖注入。
-
能注入的数据:有三类
基本类型和String。
其他bean类型(在配置文件中或者注解配置过的bean)
复杂类型/集合类型
-
注入的方式:有三种
第一种:使用构造函数提供
第二种:使用set方法提供
第三种:使用注解提供(参考第七章节)
6.2、构造函数注入
6.2.1、方式一【index索引方式】
6.2.1.1、定义Bean
public class Person { private Integer id; private String name; // 姓名 private Integer age; // 年龄 private Double weight; // 体重 public Person(Integer id, String name, Integer age) { this.id = id; this.name = name; this.age = age; }}
6.2.1.2、主配置文件中配置Bean
<beans> <bean id="person" class="cn.bdqn.Person"> <constructor-arg index="0" value="1" /> <constructor-arg index="1" value="王浩"/> <constructor-arg index="2" value="20"/> </bean></beans>
6.2.1.3、测试
@Testpublic void testPerson() throws Exception{ // 1、读取主配置文件信息,获取核心容器对象 ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml"); Person person = (Person) ac.getBean("person"); System.out.println(person); // Person{id=1, name='王浩', age=20}}
6.2.2、方式二【index+type组合方式】
说明:案例1采用的index索引的方式实现注入,就以目前案例来说是完全没问题的,但是如果Bean中存在下面情况,可能就不怎么适用了。
需求:我现在要创建Person类的对象,调用Person(Integer id, String name, Double weight)构造方法
6.2.2.1、定义Bean
说明:为Person类中的weight属性初始化,并专门为其添加构造方法
public class Person { private Integer id; private String name; // 姓名 private Integer age; // 年龄 private Double weight; // 体重 public Person(Integer id, String name, Integer age) { this.id = id; this.name = name; this.age = age; } // 专门为weight属性定义的构造方法 public Person(Integer id, String name, Double weight) { this.id = id; this.name = name; this.weight = weight; }}
6.2.2.2、主配置文件配置Bean
<beans> <bean id="person" class="cn.bdqn.Person"> <constructor-arg index="0" value="1" /> <constructor-arg index="1" value="王浩"/> <constructor-arg index="2" value="180"/> </bean></beans>
6.2.2.3、测试
@Testpublic void testPerson() throws Exception{ // 1、读取主配置文件信息,获取核心容器对象 ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml"); Person person = (Person) ac.getBean("person"); System.out.println(person); // Person{id=1, name='王浩', age=180}}
经过测试发现,并不满足我的需求,还是找的是public Person(Integer id, String name, Integer age)构造方法
6.2.2.4、解决方案
通过type明确指定类型。
<bean> <bean id="person" class="cn.bdqn.Person"> <constructor-arg index="0" type="java.lang.Integer" value="1" /> <constructor-arg index="1" type="java.lang.String" value="王浩"/> <constructor-arg index="2" type="java.lang.Double" value="180"/> </bean></bean>
6.2.3、方式三【name方式】
前两种方式通过index+type的确可以解决问题,但是总是觉得还是有些麻烦,能否有更加简单的方式呢?就是直接采用参数名的方式更易于阅读和使用。
6.2.3.1、定义Bean
Bean的定义同6.2.2.1章节。
6.2.3.2、主配置文件配置Bean
<beans> <bean id="person" class="cn.bdqn.Person"> <constructor-arg name="id" value="2"/> <constructor-arg name="name" value="史周冲"/> <constructor-arg name="age" value="3"/> </bean></beans>
6.2.3.3、测试
@Testpublic void testPerson() throws Exception{ // 1、读取主配置文件信息,获取核心容器对象 ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml"); Person person = (Person) ac.getBean("person"); System.out.println(person); // Person{id=1, name='王浩', age=3}}
6.2.4、补充细节
6.2.4.1、定义Bean
public class Person { private Integer id; private String name; // 姓名 private Date birthday; // 出生日期 public Person(Integer id, String name, Date birthday) { this.id = id; this.name = name; this.birthday = birthday; }}
6.2.4.2、主配置文件配置Bean
<beans> <bean id="person" class="cn.bdqn.Person"> <constructor-arg name="id" value="2"/> <constructor-arg name="name" value="史周冲"/> <constructor-arg name="birthday" value="2019-09-09" /> </bean></beans>
6.2.4.3、测试
// 测试后发现程序报错,原因在于:期望需要一个Date类型,而你现在传了一个字符串,数据类型不匹配。Unsatisfied dependency expressed through constructor parameter 2: Could not convert argument value of type [java.lang.String] to required type [java.util.Date]
6.2.4.4、解决方案
<beans> <bean id="person" class="cn.bdqn.Person"> <constructor-arg name="id" value="2"/> <constructor-arg name="name" value="史周冲"/> <!-- 注意:用了ref属性--> <constructor-arg name="birthday" ref="currentDate"/> </bean> <!-- 定义日期Bean,Spring就会帮助我们new一个Date对象--> <bean id="currentDate" class="java.util.Date"/></beans>
6.2.5、总结
-
使用的标签:
constructor-arg
-
标签出现的位置:
bean标签的内部
-
标签中的属性:
type:用于指定要注入的数据的数据类型。
index:用于指定要注入的数据给构造函数中指定索引位置的参数赋值,索引的位置是从0开始。
name:用于指定给构造函数中指定名称的参数赋值。
value:用于提供基本类型和String类型的数据。
ref:引用,用于指定其他的bean类型数据。它指的就是在spring的Ioc核心容器中出现过的bean对象
-
优势:
假设我们需要创建一个对象时,需要明确初始化一些数据,那么这种方式显然是很好的。因为通过构造函数创建对象时候,如果不指定具体的参数是无法把对象创建成功的。可以起到一个约束的作用。
-
劣势:
改变了bean对象的实例化方式,使我们在创建对象时,如果用不到这些数据,也必须提供。
6.3、set方法注入
6.3.1、定义Bean
public class User { private String name; private Date born; public void setName(String name) { this.name = name; } public void setBorn(Date born) { this.born = born; }}
说明:set注入方式不必生成get方法
6.3.2、主配置文件配置Bean
<bean id="currentDate" class="java.util.Date"/> <bean id="user" class="cn.bdqn.User"> <property name="name" value="宋炜烨"/> <property name="born" ref="currentDate"/></bean>
6.3.3、测试
@Testpublic void testUser() throws Exception{ // 1、读取主配置文件信息,获取核心容器对象 ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml"); User user = (User) ac.getBean("user"); System.out.println(user); // User{name='宋炜烨', born=Thu Nov 14 22:29:11 CST 2019}}
6.3.4、总结
-
涉及的标签
property
-
出现的位置
bean标签的内部
-
标签的属性
name:用于指定注入时所调用的set方法名称。
value:用于提供基本类型和String类型的数据。
ref:用于指定其他的bean类型数据。它指的就是在spring的Ioc核心容器中出现过的bean对象。
-
优势
创建对象时没有明确的限制,可以直接使用默认构造函数。
-
劣势
如果有某个成员属性必须有值,则有可能再使用该对象的时候并没有通过set方法注入值,可能拿到为空的值。
6.4、复杂类型的注入
6.4.1、注入数组类型【array】
6.4.1.1、定义Bean
public class Cat { private String[] arrs; public void setArrs(String[] arrs) { this.arrs = arrs; }}
6.4.1.2、主配置文件配置Bean
<beans> <bean id="cat" class="cn.bdqn.Cat"> <property name="arrs"> <array> <value>崔灿</value> <value>时贝妮</value> </array> </property> </bean></beans>
6.4.2、注入List类型【list】
6.4.2.1、定义Bean
public class Cat { private List<String> arrList; public void setArrList(List<String> arrList) { this.arrList = arrList; }}
6.4.2.2、主配置文件配置Bean
<beans> <bean id="cat" class="cn.bdqn.Cat"> <property name="arrList"> <list> <value>乔峰</value> <value>马夫人</value> </list> </property> </bean></beans>
6.4.3、注入Set类型【set】
6.4.3.1、定义Bean
public class Cat { private Set<String> arrSet; public void setArrSet(Set<String> arrSet) { this.arrSet = arrSet; }}
6.4.3.2、主配置文件配置Bean
<beans> <bean id="cat" class="cn.bdqn.Cat"> <property name="arrSet"> <set> <value>段誉</value> <value>鸠摩智</value> </set> </property> </bean></beans>
6.4.4、注入Map类型【Map】
6.4.4.1、定义Bean
public class Cat { private Map<String,Object> arrMap; public void setArrMap(Map<String, Object> arrMap) { this.arrMap = arrMap; }}
6.4.4.2、主配置文件配置Bean
<bean> <property name="arrMap"> <map> <entry key="S001" value="彭依凝"/> <entry key="S002" value="段康家"/> <entry key="S003" value="王浩"/> </map> </property></bean>
6.4.5、注入Properties类型
6.4.5.1、定义Bean
public class Cat { private Properties props; public void setProps(Properties props) { this.props = props; }}
6.4.5.2、主配置文件配置Bean
<bean id="cat" class="cn.bdqn.Cat"> <property name="props"> <props> <prop key="A001">虚竹</prop> <prop key="A002">扫地僧</prop> </props> </property></bean>
6.4.6、总结
-
用于给List结构集合注入的标签:
list、array、set
-
用于个Map结构集合注入的标签:
map 、props
-
总结
结构相同,标签可以互换
七、Spring的Bean管理【基于注解】
7.1、说明
回顾一下基于xml配置的Spring对Bean的管理 ,对Bean的完整管理如下所示:
<bean id="" class="" init-method="" destroy-method="" scope=""> <property name="" value=""/> <property name="" ref=""/></bean>
分析可以发现:我们对Bean的管理就四个方面,分别是:
- 用于创建对象的
- 用于注入数据的
- 用于改变Bean的作用域的
- 和Bean的生命周期相关的
其实对于注解来说,也是包括了这四个方面,换句话说,使用注解的方式管理Bean和使用xml的方式管理Bean作用是完全一样的,区别仅仅在于配置的形式不同而已。
7.2、用于创建对象的
7.2.1、Component注解
7.2.1.1、定义Bean
@Componentpublic class DogService { }
7.2.1.2、主配置文件配置扫描注解
<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 https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <!-- 告诉spring在创建容器时要扫描的包 --> <context:component-scan base-package="cn.bdqn"/></beans>
7.2.1.3、测试
@Testpublic void testDogServiceImpl() throws Exception{ // 1、读取主配置文件信息,获取核心容器对象 ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml"); DogServiceImpl dogService = (DogServiceImpl) ac.getBean("dogServiceImpl"); System.out.println(dogService);}
7.2.1.4、Component注解总结
-
作用
用于把对当前修饰的类创建出来,并存放到Spring容器中。
-
属性
a. 用该注解所创建的对象默认的id名称是当前类名,且首字母改小写
b. 可以通过value属性手动的指定bean的id。
7.2.2、Controller注解
一般用在表现层,例如SpringMVC、Struts2
7.2.3、Service注解
一般用在业务层,例如Service层
7.2.4、Repository注解
一般用在持久层7.2.5、总结
- Controller、Service、Repository这三个注解他们的作用和属性与Component是一模一样。
- 既然一模一样,之所以Spring框架还要提供,主要是Spring框架为我们提供明确的三层使用的注解,使我们的三层对象更加清晰。
- 他们的作用就和在XML配置文件中编写一个标签实现的功能是一样的
7.3、用于注入数据的
7.3.1、Autowired注解
作用:
自动按照==类型==注入。
7.3.1.1、定义Bean
// 用户service接口public interface UserService { public void printUserDao();}
// 用户service接口实现,bean的名称改为:userService@Service("userService")public class UserServiceImpl implements UserService { @Autowired private UserDao userDao; // 打印UserDao,看是否可以将值打印出来,如果打印出来说明值真的注入成功了 public void printUserDao(){ System.out.println(userDao); }}
// 用户UserDao接口public interface UserDao {}
// 用户UserDao接口实现,bean的名称改为:userDao01@Repository("userDao01")public class UserDaoImpl01 implements UserDao {}
7.3.1.2、主配置文件配置扫描注解
<beans> <!-- 告诉spring在创建容器时要扫描的包 --> <context:component-scan base-package="cn.bdqn"/></beans>
7.3.1.3、测试
@Testpublic void testUserServiceImpl() throws Exception{ // 1、读取主配置文件信息,获取核心容器对象 ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml"); UserService userService = (UserService) ac.getBean("userService"); userService.printUserDao(); // cn.bdqn.dao.impl.UserDaoImpl@7a52f2a2}
7.3.1.4、改造
假设系统中存在两个UserDao的实现,现在再添加一个。现在再添加测试:
@Repository("userDao02")public class UserDaoImpl02 implements UserDao {}
再次运行程序,会发现,程序报错:
No qualifying bean of type 'cn.bdqn.dao.UserDao' available: expected single matching bean but found 2: userDao01,userDao02翻译:没有找到一个可用的UserDao,期望能够匹配一个,但是发现了2个。换句话说,由于是根据类型匹配的,而userDao01,userDao02都是符合注入的类型的,不知道要用哪个了.
该如何解决呢?既然Spring不知道具体要用哪个了,那我们由开发者来去指定其中的一个告诉Spring你就用这个注入就可以了,那么实现方式就是:通过名称告诉即可
解决方案:
@Service("userService")public class UserServiceImpl implements UserService { // 将变量的名称修改为userDao01,那么依赖注入的时候就使用UserDaoImpl01 @Autowired private UserDao userDao01;}
再次测试,程序正常执行。
7.3.1.5、总结
- 该注解是根据类型自动注入,假如只要容器中有唯一的一个bean对象类型和要注入的变量类型匹配,就可以注入成功。
- 如果ioc容器中没有任何bean的类型和要注入的变量类型匹配,则报错。
- 如果容器中有多个bean对象类型和要注入的变量类型匹配,则可能会发生错误,解决办法就是修改变量的名称为其中的某一个bean的名称
7.3.2、Qualifier注解
在7.3.1章节使用Autowired注解的时候,会存在一个问题,就是如果系统中存在多个类型的Bean都匹配的时候,就会找不到到底要使用哪个Bean对象了,会报错,我们采取的解决办法是:修改变量名即可解决 , 但是这种做法实际上是挺菜的,我现在就想使用userDao这个变量名,那么能否有一种更好的解决办法呢?答案是肯定的,即使用Qualifier注解。
7.3.2.1、定义Bean
Bean的定义,仍然采用7.3.1章节所定义好的Bean,唯一的区别是UserServiceImpl这个Bean,修改如下:
public class UserServiceImpl implements UserService { @Autowired @Qualifier(value = "userDao01") // 通过此注解中的value属性明确指定要用哪个name的bean private UserDao userDao;}
7.3.2.2、总结
- 在按照类型注入的基础之上再按照名称注入,它在给类的成员变量注入时不能单独使用,需要搭配Autowired注解。
7.3.3、Resource注解
7.3.3.1、定义Bean
Bean的定义,仍然采用7.3.1章节所定义好的Bean,唯一的区别是UserServiceImpl这个Bean,修改如下:
@Service("userService")public class UserServiceImpl implements UserService { @Resource(name = "userDao01") private UserDao userDao;}
7.3.3.2、总结
- 该注解采用的是直接按照bean的id注入,它可以独立使用,name用于指定bean的id。
注意:
使用Autowired、Qualifier以及Resource这三个注解都只能注入其他bean类型的数据,对于基本数据类型和String类型是无法通过使用该3个注解实现。同时,对于集合数据类型的注入只能通过XML来实现。
7.3.4、Value注解
作用:
用于注入基本类型和String类型的数据。
7.3.4.1、案例1
7.3.4.1.1、定义Bean
@Component("jdbcUtils")public class JdbcUtils { @Value("com.mysql.jdbc.Driver") private String driverClass;}
7.3.4.1.2、测试
@Testpublic void testJdbcUtils() throws Exception{ // 1、读取主配置文件信息,获取核心容器对象 ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml"); JdbcUtils utils = (JdbcUtils) ac.getBean("jdbcUtils"); System.out.println(utils); // com.mysql.jdbc.Driver}
7.3.4.2、案例2
如果Value注解用于案例1,那这样太菜了,我们要为driverClass这个变量赋值,那岂不是直接赋值得了,还需要搞一个Value直接赋值吗?显然是没有必要的,所以一般来说,Value注解常用于对配置文件内容的读取。
7.3.4.2.1、创建db.properties文件
driverClass=com.mysql.jdbc.Driverport=3306
7.3.4.2.2、主配置文件需要将properties文件引入进来
<beans> <!-- 告诉spring在创建容器时要扫描的包 --> <context:component-scan base-package="cn.bdqn"/> <!-- 将properties文件引入到Spring框架中--> <context:property-placeholder location="classpath:db.properties"/></beans>
7.3.4.2.3、定义Bean
@Component("jdbcUtils")public class JdbcUtils { @Value("${driverClass}") private String driverClass; @Value("${port}") private Integer port;}
7.3.4.2.4、测试
@Testpublic void testJdbcUtils() throws Exception{ // 1、读取主配置文件信息,获取核心容器对象 ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml"); JdbcUtils utils = (JdbcUtils) ac.getBean("jdbcUtils"); System.out.println(utils); // com.mysql.jdbc.Driver,3306}
7.3.4.3、总结
该注解通过可以实现对基本数据类型和String类型的注入,并且是支持使用Spring的EL表达式。那么对于Spring的EL表达式的语法就是:${表达式}。
7.3.5、大总结
以上注解的作用就和在xml配置文件中的bean标签中写一个标签的作用是一样的
7.4、用于改变Bean的作用域的
方式:
- 使用Scope注解,作用是用于指定bean的作用范围。该注解有一个value属性,可以指定范围的取值,常用取值:singleton、 prototype
- 该Scope注解默认的value值就是单例的【singleton】
7.4.1、定义Bean
Bean的定义,仍然采用7.3.1章节所定义好的Bean,唯一的区别是UserServiceImpl这个Bean修改作用域,修改如下:
@Service("userService")@Scope("singleton")public class UserServiceImpl implements UserService{ }
7.4.2、测试
@Testpublic void testUserServiceImpl() throws Exception{ // 1、读取主配置文件信息,获取核心容器对象 ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml"); UserService userService = (UserService) ac.getBean("userService"); UserService userService2 = (UserService) ac.getBean("userService"); System.out.println(userService == userService2); // true}
改造一下:如果将上例的UserServiceImpl中的Scope改为prototype,则再次测试的时候会返回false。
7.4.3、总结
Scope该注解的作用就和在bean标签中使用scope属性实现的功能是一样的
7.5、和Bean的生命周期相关的
7.5.1、定义Bean
Bean的定义,仍然采用7.3.1章节所定义好的Bean,唯一的区别是UserServiceImpl这个Bean添加了一个初始化方法和销毁方法。
@Service("userService")public class UserServiceImpl implements UserService { @PostConstruct public void init(){ System.out.println("对象初始化了"); } @PreDestroy public void destroy(){ System.out.println("对象销毁了"); }}
7.5.2、测试
@Testpublic void testUserServiceImpl() throws Exception{ // 1、读取主配置文件信息,获取核心容器对象 ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml"); ac.close();}// 对象初始化了// 对象销毁了
7.5.3、总结
这个两个注解作用就和在bean标签中使用init-method和destroy-methode的作用是一样的。
八、Spring新注解
8.1、Configuration注解
8.1.1、定义一个类
public class SpringMainConfig {}
8.1.2、使用Configuration注解修饰类
@Configurationpublic class SpringMainConfig { }
8.1.3、作用
使用Configuration注解修饰的类表示的是:当前类是一个配置类。该类的作用和beans.xml是一样的,换句话说,该注解所修饰的类就是用来代替beans.xml文件的。
8.2、Bean注解
8.2.1、定义bean
public class User { private Integer id; private String name; public User(Integer id, String name) { this.id = id; this.name = name; }}
8.2.2、在主配置类中注册bean
在以前,使用xml去注册bean的时候,使用的是bean标签,形如:
<bean id="user" class="cn.bdqn.User"/>
现在改为使用注解,使用的是@Bean注解。
@Configurationpublic class SpringMainConfig { // 给容器中注册一个bean;类型为方法返回值的类型,默认方法名作为id,即bean的名字默认是方法名 @Bean public User user(){ return new User(1,"段康家"); }}
8.2.3、测试容器中是否有该bean
@Testpublic void testUser() throws Exception{ ApplicationContext ac = new AnnotationConfigApplicationContext(SpringMainConfig.class); User user = (User) ac.getBean("user"); System.out.println(user); // User{id=1, name='段康家'}}
8.2.4、注册bean的同时可以指定bean名称
@Configurationpublic class SpringMainConfig { // 给容器中注册一个Bean;类型为返回值的类型,id默认是用方法名作为id // 可以再注册Bean的同时,不用方法名作为id,可以指定bean的名称。 @Bean("user01") public User user(){ return new User(1,"段康家"); }}
8.2.5、补充内容
8.2.5.1、案例1【方法有参数】
// 定义UserDao接口public interface UserDao {}
// 定义UserDao接口的实现类public class UserDaoImpl01 implements UserDao{}
// 定义UserServicepublic class UserServiceImpl { private UserDao userDao; // 构造方法接收UserDao类型的对象 public UserServiceImpl(UserDao userDao){ this.userDao = userDao; }}
重点看主配置类:
@Configurationpublic class SpringMainConfig { // 给容器中注册一个Bean;类型为返回值的类型,id默认是用方法名作为id @Bean public UserServiceImpl userService(UserDao userDao){ return new UserServiceImpl(userDao); } @Bean public UserDao userDao01(){ return new UserDaoImpl01(); }}
@Testpublic void testUser() throws Exception{ ApplicationContext ac = new AnnotationConfigApplicationContext(SpringMainConfig.class); UserServiceImpl userService = (UserServiceImpl) ac.getBean("userService"); System.out.println(userService);}
说明:测试后发现程序能够正常运行,说明UserDao能够正常的以构造函数参数的形式注入到UserServiceImpl类中。
总结:当我们使用Bean注解配置方法时,如果方法有参数,spring框架会去容器中查找有没有可用的bean对象。查找的方式和Autowired注解的作用是一样的。即:Spring会从容器中根据类型注入
8.2.5.2、案例2
当然还有一种情况是:容器中注册了多个相同类型的Bean,那么Spring又是如何注入的呢?在8.2.5.1案例基础之上再添加一个UserDaoImpl02。
public class UserDaoImpl02 implements UserDao{}
在再主配置类中注册该UserDaoImpl02的Bean.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-92kPof0f-1634112348629)(images/004_Bean注册.png)]
通过看图可以知道,有问题,不能够实现自动注入,该怎么办呢?两种方式:
a、修改方法的参数
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lwcnpYSh-1634112348630)(images/005_Bean多个类型解决方案1.png)]
b、指定Bean的名称
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2rJM9Cl6-1634112348631)(images/006_Bean多个类型解决方案2.png)]
总结:本质上来说,解决方案1和解决方案2是一样的。
c、使用Qualifier注解。
实际上在7.3.2章节中,已经讲到了关于Qualifier注解的使用,通过该注解实现的功能是:当系统中存在多个相同类型的bean的时候,Spring就会注入失败,这个时候就需要再根据名称实现注入,而使用Qualifier注解可以达到根据给定名称的bean实现注入 。在以前讲解的过程中,我们是把该注解应用到了对类的成员变量上 ,但是需要注意的细节是:该注解应用到成员变量上时,注入时不能单独使用,需要搭配Autowired注解 。 但是,这里有一个例外,那么就是该注解也可以应用在方法参数上。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Lg3mgnNS-1634112348632)(images/011_Qualifier的使用.png)]
特别注意:当把Qualifier注解应用在方法参数上时,可以单独使用
8.3、ComponentScan注解
==作用:==指定spring在创建容器时要扫描的包。作用就如同在配置文件中这样的定义:
<beans> <context:component-scan base-package="cn.bdqn"/></beans>
8.3.1、案例1
第一步:定义三个Bean,分别是CatController、CatService、CatMapper,并分别用@Controller、@Service、@Repository修饰。
package cn.bdqn.controller;@Controllerpublic class CatController { }
package cn.bdqn.service;@Servicepublic class CatServiceImpl {}
package cn.bdqn.mapper;@Repositorypublic class CatMapper { }
第二步:要想真正的注册到容器中,就必须要让容器扫描到此注解所修饰的类所在的包。
@Configuration@ComponentScan(value = {"cn.bdqn"})public class SpringMainConfig {}
第三步:测试容器中是否注册了该bean。
@Testpublic void testComponentScan() throws Exception{ ApplicationContext ac = new AnnotationConfigApplicationContext(SpringMainConfig.class); // 获取Spring容器中已经注册好的bean,把所有bean的名字获取出来 String[] names = ac.getBeanDefinitionNames(); for (String name:names) { System.out.println(name); }}/** 结果: catController catMapper catServiceImpl*/
总结:默认情况下,Spring会扫描cn.bdqn包以及子包下面所有的使用@Controller、@Service、@Repository、@Component所修饰的bean注册到容器中
8.3.2、案例2
**需求:**我现在不想扫描被@Controller所修饰的类注册到容器中,即让Spring扫描不到即可。言外之意就是:把所有被@Controller所修饰的类给排除掉。
说明:定义的CatController、CatServiceImpl、CatMapper不变,使用的是案例1。唯一要修改的配置是主配置文件类。为@ComponentScan注解添加excludeFilters属性即可。
**含义:**excludeFilters的作用指定扫描的时候按照什么规则排除那些组件。
**做法:**修改主配置类,即
@Configuration@ComponentScan( value = {"cn.bdqn"}, excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = {Controller.class}) })public class SpringMainConfig { }
**测试:**测试代码同案例1
/** catMapper catServiceImpl**/
8.3.3、案例3
**需求:**我现在只想扫描被@Controller所修饰的类注册到容器中,被其他修饰的注解不扫描。
说明:定义的CatController、CatServiceImpl、CatMapper不变,使用的是案例1。唯一要修改的配置是主配置文件类。为@ComponentScan注解添加includeFilters属性即可。
**含义:**includeFilters的作用指定扫描的时候只需要包含哪些组件。
**做法:**修改主配置类,即
@Configuration@ComponentScan(value = {"cn.bdqn"}, includeFilters = { @ComponentScan.Filter(type = FilterType.ANNOTATION, value = {Controller.class, Service.class})})public class SpringMainConfig {}
**测试:**测试代码同案例1
/** catController catMapper catServiceImpl 测试,发现,不仅仅Controller被扫描进来了,包括Service,Mapper也都扫描进来了,好像不起作用,没有生效,原因是在于在使用includeFilters做只包含的时候,还需要关闭默认的过滤规则【在默认情况下,Spring是扫描指定包及子包下的被@Component、@Controller、@Service等bean的。】*/
主配置文件类修改如下:
@Configuration@ComponentScan(value = {"cn.bdqn"}, includeFilters = { @ComponentScan.Filter(type = FilterType.ANNOTATION, value = {Controller.class, Service.class})}, useDefaultFilters = false)public class SpringMainConfig {}
测试:
/** catController 测试发现:结果正常,符合预期结果,只扫描到了Controller注解。*/
8.4、Scope注解
作用:可以改变bean的作用域。在Spring中,Bean的默认作用域是单例的。
8.4.1、定义Bean
public class User { private Integer id; private String name; public User(Integer id, String name) { this.id = id; this.name = name; }}
8.4.2、注册Bean并定义作用域
@Configurationpublic class SpringMainConfig { @Bean @Scope public User user(){ System.out.println("对象创建啦"); return new User(1,"HelloWorld"); }}
8.4.3、测试
@Testpublic void testComponentScan() throws Exception{ ApplicationContext ac = new AnnotationConfigApplicationContext(SpringMainConfig.class);}/** 对象创建啦*/
结果:注册的Bean默认是单实例的,Spring容器将会在启动的时候调用方法创建对象后放入容器中,以后每次需要拿对象了就直接从容器中拿,而且每次拿的还是同一个。
8.4.4、将Bean的作用域改为多例
@Configurationpublic class SpringMainConfig { @Bean @Scope("prototype") public User user(){ System.out.println("对象创建啦"); return new User(1,"HelloWorld"); }}
8.4.5、测试1
@Testpublic void testPrototype() throws Exception{ ApplicationContext ac = new AnnotationConfigApplicationContext(SpringMainConfig.class);}/** 测试后发现容器启动的时候,多例对象并不会创建。*/
8.4.6、测试2
@Testpublic void testPrototype() throws Exception{ ApplicationContext ac = new AnnotationConfigApplicationContext(SpringMainConfig.class); User user01 = (User) ac.getBean("user"); User user02 = (User) ac.getBean("user");}/** 对象创建啦 对象创建啦*/
总结:对于多实例来说,Spring容器在启动的时候并不会去调用方法创建对象并放在容器中,而是每次获取的时候才会调用方法创建对象,并且每次获取到的对象还不一样。
8.5、Lazy注解
作用:用此注解修饰的Bean表示的该Bean需要被懒加载,即不会随着Spring容器的启动而去创建。
8.5.1、注册Bean
@Configurationpublic class SpringMainConfig { @Bean @Lazy(value = true) // 开启懒加载 public User user(){ System.out.println("对象创建啦"); return new User(1,"HelloWorld"); }}
8.5.2、测试
@Testpublic void testSingleton() throws Exception{ ApplicationContext ac = new AnnotationConfigApplicationContext(SpringMainConfig.class);}/** 控制台什么都没有输出,因为我们的Bean被Layz注解修饰了,即使Spring容器启动了,我们的Bean也不会去创建,只有等到了真正要用到该bean了才去创建。*/
8.5.3、总结
- 这个Lazy注解是针对单实例Bean才有效,因为也只有单实例Bean才会默认在容器启动的时候创建对象。
- 懒加载:容器启动不创建对象,只有在第一次使用(获取)Bean的时候采取创建对象,并初始化。
- 该注解的作用等价于。
8.6、Conditional注解【了解】
作用: 根据一定的条件进行判断,当满足这个条件时给容器注入Bean。
==位置:==既可以修饰类也可以修饰方法。
==需求:==当前操作系统如果是window,则注入WindoBean,如果是linux系统,则注入LinuxBean。
首先查看下@Conditional注解的源代码,即:
public @interface Conditional { // 该注解的属性值是一个Class类型的数组,数组的Class元素类型必须继承Condition接口,即元素的类型必 // 须是Condition类型。 Class<? extends Condition>[] value(); }
继续Condition接口的源代码:
public interface Condition { // 提供一个返回boolean值的方法,返回true则注入bean,false则不注入 boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);}
8.6.1、定义两个Bean
// Window的beanpublic class WindowBean { private String name; public WindowBean(String name) { this.name = name; }}
// Linux的Beanpublic class LinuxBean { private String name; public LinuxBean(String name) { this.name = name; }}
8.6.2、定义两个类实现Condition接口
// Window环境的条件public class WindowCondition implements Condition { // context:上下文环境 public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { String osName = context.getEnvironment().getProperty("os.name"); if (osName.contains("Windows")){ return true; } return false; }}
// Linux环境条件public class LinuxCondition implements Condition { @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { String osName = context.getEnvironment().getProperty("os.name"); if (osName.contains("linux")){ return true; } return false; }}
8.6.3、注册Bean
@Configurationpublic class SpringMainConfig { // 如果当前环境是Winows环境则注入WindowBean @Bean @Conditional(value = {WindowCondition.class}) public WindowBean windowBean(){ return new WindowBean("这个是window操作系统"); } // 如果当前环境是Linux环境则注入LinuxBean @Bean @Conditional(value = {LinuxCondition.class}) public LinuxBean linuxBean(){ return new LinuxBean("这个是Linux操作系统"); }}
8.6.4、测试1
@Testpublic void testBean() throws Exception{ ApplicationContext ac = new AnnotationConfigApplicationContext(SpringMainConfig.class); // 查看Spring容器中所有注册的Bean String[] names = ac.getBeanDefinitionNames(); for (String name:names) { System.out.println(name); }}/** windowBean 由于现在是在window操作系统上测试,故仅仅注册了WindowBean。*/
8.6.5、测试2
测试下如果程序运行在linux下,是否能够注入LinuxBean?测试过程,即:运行程序的时候手动的指定运行时参数信息,步骤如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xk4gh7np-1634112348633)(images/007_位置.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VH999F3h-1634112348633)(images/008_配置运行时参数.png)]
测试后,发现,打印的结果是:LinuxBean。
8.6.6、总结
- 使用Conditional注解修饰在方法上,只有当条件满足的时候,即返回true,才会将所修饰的方法调用并且会创建Bean对象添加到容器中。
- 使用Conditional也可以修饰类,比如修饰主配置类,那么假如这个主配置类中定义了很多的Bean,如果该注解修饰了一个类,则只有条件满足的时候,即返回true,那么主配置类中所定义的Bean才会注册到容器中。即:@Conditional标注在类上就决定了该类中的一批bean是否能够注入。
8.7、Import注解
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XBx6vPqK-1634112348634)(images/009_Import注解.png)]
8.7.1、用法一
简单来说,Import注解可以去修饰常规组件类,意思是:所谓的常规组件类就是一个普通的Java类, 而如果这个普通的Java类被@Import注解修饰,那么也可以将这个Java类注册到Spring容器中,成为Bean。
8.7.1.1、定义Bean
public class Dog { }
8.7.1.2、注册Bean
@Configuration@Import(value = {Dog.class})public class SpringMainConfig {}
8.7.1.3、测试容器中是否有该Bean
@Testpublic void testDogBean() throws Exception{ ApplicationContext ac = new AnnotationConfigApplicationContext(SpringMainConfig.class); // 查看Spring容器中所有注册的Bean String[] names = ac.getBeanDefinitionNames(); for (String name:names) { System.out.println(name); }}/** cn.bdqn.Dog*/
8.7.1.4、注意
使用Import注解导入一个常规组件的时候,这个Bean的名称是类的全路径
8.7.2、用法二
即Import可以导入被@Configuration注解所修饰的类,换句话说,Import注解可以去导入其他的配置类,存在的一个细节是:假如这个配置类里面也注册了多个Bean,那么同样也会将这些个Bean一并注册到Spring容器中。
8.7.2.1、定义Bean
// 奥迪public class AuDi { }
// 宝马public class BMW {}
8.7.2.2、新建配置类并注册Bean
// 针对系统中存在的车的一个配置类@Configurationpublic class Car { @Bean public AuDi auDi(){ return new AuDi(); } @Bean public BMW bmw(){ return new BMW(); }}
8.7.2.3、Import导入配置类
@Configuration@Import(value = {Car.class})public class SpringMainConfig {}
8.7.2.4、测试容器有哪些Bean
测试代码如同8.7.1.3,结果如下:
/** cn.bdqn.Car ---> 配置类 auDi ---> 配置类中的奥迪Bean bmw ---> 配置类中的宝马Bean*/
8.7.2.5、总结
- 测试后发现,如果Import导入的是一个配置类,那么Spring会将这个配置类、以及配置类中定义的Bean都会注册到容器中。
- 同时,有Import注解修饰的那个配置类是主配置类,被Import注解导入的配置类也叫子配置类。例如用法二,SpringMainConfig这个配置类就是主配置类,Car这个配置类称之为子配置类。
- 这样做的好处:可以将不同功能、不同业务的Bean单独分开配置,而不是统一的都配置到一个配置类中,单独配置,结构清晰,功能划分的也很清楚。
8.8、PropertySource注解
==作用:可以设置properties文件的位置,这样的话,就可以通过Value注解读取配置文件的内容。==该注解的作用等同于:
<context:property-placeholder location="classpath:db.properties"/>
位置:该注解通常和@Configuration注解一起使用。
8.8.1、定义Pig.properties文件
id=1name=丁春秋
8.8.2、主配置类
@Configuration // 主配置类@ComponentScan("cn.bdqn") // 扫描包,目的是能够扫描到Pig类@PropertySource(value = {"pig.properties"}) //加载配置文件public class SpringMainConfig { }
8.8.3、测试
@Testpublic void testPig() throws Exception{ ApplicationContext ac = new AnnotationConfigApplicationContext(SpringMainConfig.class); Pig pig = (Pig) ac.getBean("pig"); System.out.println(pig); // 1,name}
九、Spring整合junit
9.1、导入spring整合junit的jar
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.2.1.RELEASE</version> <scope>test</scope></dependency>
9.2、在测试类上添加注解
@ContextConfiguration(classes = {SpringMainConfig.class})@RunWith(SpringJUnit4ClassRunner.class)public class ConditionalTest { @Autowired private Pig pig; @Test public void testPig() throws Exception{ System.out.println(pig); }}
9.3、说明
@ContextConfiguration注解需要注意的细节是:
- classes:指定的是主配置类的字节码
- locations:指定xml文件的位置
十、面向切面编程(AOP)
10.1、问题引出
首先来看一个问题,假如我现在有一个UserServiceImpl用户业务类,其中呢,有一个保存用户的方法,即:
public class UserServiceImpl { // 保存用户 public void save(User user){ // 调用Dao新增用户 }}
现在的需求是:我要在保存用户之前新增事务的功能,你可能会这么做:
public void save(User user){ // 1、新增事务功能代码 // 调用Dao新增用户}
现在的需求又变了,我要在保存用户之前不仅新增事务功能了,还要添加日志功能了,你可能又会这么做:
public void save(User user){ // 2、新增记录日志功能代码 // 1、新增事务功能代码 // 调用Dao新增用户}
现在的需求又变了,我要在保存用户之前不仅新增事务功能了,日志功能了,还要增加开始执行保存用户方法的开始时间的功能了,你可能又会这么做:
public void save(User user){ // 3、新增统计开始执行保存用户方法的开始时间功能 // 2、新增记录日志功能代码 // 1、新增事务功能代码 // 调用Dao新增用户}
现在的需求又变了,我要在保存用户之前不仅新增事务、日志功能、统计方法开始执行时间,还要新增用户是否有权限访问这个方法功能,你可能又会这么做:
public void save(User user){ // 4、新增判断用户是否有权限功能 // 3、新增统计开始执行保存用户方法的开始时间功能 // 2、新增记录日志功能代码 // 1、新增事务功能代码 // 调用Dao新增用户}
需求又在一直改变改变着,可能忽然领导突然哪一天大腿一拍,说这些新增的功能都不要了,好,我们把我们辛苦写的代码全部注释了,也可能一气之下全删了;又过了几天,领导又抽风了,说我们需要新增事务和权限,好,我们又吭哧吭哧的去改代码了;…领导反复抽风,我们反复改反复改…最后我们顶不住压力,自杀了。
出现这个问题的原因在于:反复改需求,而这些需求与我们真正要保存用户的业务逻辑关系不大,同时,我们还应该需要考虑到的是:我们的系统中可能不仅仅只有用户模块,还有商品模块,订单模块,分类模块等等。而这些模块在保存相应的数据的时候,都有可能需要新增事务、日志、时间统计、权限等功能。那么如果我们对保存的数据方法都全部写一遍新增事务功能、新增日志功能、新增时间统计功能、新增权限功能,这样的代码无疑是很臃肿的,而且是很难维护的,想到这里,你可能会这么做,把公共的代码抽取出来,可能这么干:
public class BaseService { // 新增事务 public void transaction(){ System.out.println("事务功能"); } // 新增日志 public void log(){ System.out.println("日志功能"); }}
public class UserServiceImpl extends BaseService{ public void save(User user){ super.log(); super.transaction(); // 调用Dao新增用户 }}
解决方案:把公共代码抽取到一个父类的BaseService,让具体的模块Service继承BaseService,然后调用父类的功能,这样做即:通过继承的方式实现对业务方法的功能的增强。
但是这样做还有一个问题,虽然我们其他模块也需要此功能时,可以采用继承的方式来做,但是一个很严重的问题是:我们的业务功能在去调用的时候,对业务功能的增强实际上还是硬编码了。还是没有解决方便维护的问题,那我们期望能够解决的问题是:能否“神不知鬼不觉”的在不改变源代码的情况下去扩展功能呢? 答案肯定是可以的,那这就是AOP,同时,这个也是我们学习AOP的原因所在,也是AOP的作用所在。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9aXYvLf6-1634112348635)(images/010_AOP切面.png)]
从上图可以看出,如果从系统的横向角度来看,我们的权限功能,日志功能,事务功能是把系统的各个模块中的各个业务方法在执行之前从前面拦截了,好像拿了一把刀把一个完整的苹果切成一半,形成了一个切面。这个也就是=="面向切面编程==中切面的含义。
10.2、AOP
10.2.1、概念
AOP:全称是 Aspect Oriented Programming 即:面向切面编程/面向方面编程。AOP不是一项新的技术,是OOP(面向对象编程)技术的补充,是OOP的延续,同时也是Spring最重要的内容之一。
简单来说,AOP就是把我们程序重复的代码抽取出来,在需要执行的时候,使用代理技术,在不修改源码的基础上,对我们的已有方法进行增强。
Spring框架的AOP机制可以让开发者把业务流程中的通用功能抽取出来,单独编写功能代码。在业务流程执行过程中,Spring框架会根据业务流程要求,自动把独立编写的功能代码切入到流程的合适位置。
10.2.2、作用
在程序运行期间,不修改源码对已有方法进行增强。
10.2.3、优势
- 减少重复代码
- 提高开发效率
- 维护方便
10.2.4、实现方式
动态代理
10.2.5、专业术语
10.2.5.1、连接点
全英叫:Joinpoint。所谓连接点是指那些被拦截到的点。在 spring 中,这些点指的是方法,因为 spring 只支持方法类型的连接点。
10.2.5.2、切入点
全英叫:Pointcut。所谓切入点是指我们要对哪些 Joinpoint 进行拦截。
10.2.5.3、通知/增强
全英叫:Advice。所谓通知/增强是指拦截到 Joinpoint 之后所要做的事情就是通知。说白了,就是一段功能性的代码。
通知的类型
-
前置通知
-
后置通知
-
异常通知
-
最终通知
-
环绕通知
try{ // 前置通知【开启事务】 // 业务代码.... // 后置通知【提交事务】}catch (Exception e){ // 异常通知 【回滚事务】}finally { // 最终通知 【释放资源】}
10.2.5.4、织入
全英叫:Weaving。是指把增强应用到目标对象来创建新的代理对象的过程,是一个动作。
10.2.5.5、切面
全英叫:Aspect。是切入点和通知/增强的结合。
10.2.5.6、引介【了解】
全英叫:Introduction。引介是一种特殊的通知在不修改类代码的前提下, Introduction可以在运行期为类动态地添加一些方法或Field。
十一、基于XML的AOP
11.1、打印日志案例
11.1.1、beans.xml中添加aop的约束
<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 https://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/aop https://www.springframework.org/schema/aop/spring-aop.xsd"></beans>
11.1.2、定义Bean
package cn.bdqn.domain;public class User {}
package cn.bdqn.service;public interface UserService { // 保存用户 public void save(User user); // 根据id查询用户 public User queryById(Integer id); // 查询全部用户 public List<User> queryAll();}
package cn.bdqn.service;public class UserServiceImpl implements UserService{ // 保存用户 public void save(User user){ } // 根据id查询用户 public User queryById(Integer id){ return new User(); } // 查询全部用户 public List<User> queryAll(){ return new ArrayList<User>(); }}
11.2、定义记录日志的类【切面】
package cn.bdqn.advice;// 定义记录日志的类,这个类就封装了我们所有的公共的代码public class Logger { // 该方法的作用是在切入点方法执行之前执行 public void beforePrintLog(){ System.out.println("开始打印日志啦"); }}
11.3、导入AOP的依赖
<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.4</version></dependency>
11.4、主配置文件中配置AOP
<beans> <!-- 1、注册UserServiceImpl这个Bean --> <bean id="userService" class="cn.bdqn.service.UserServiceImpl"/> <!-- 2、以下操作都是Spring基于XML的AOP配置步骤 2.1 把通知/增强Bean也需要注册到Spring容器中 2.2 使用<aop:config/>标签来去声明开始AOP的配置了 2.3 使用<aop:aspect/>标签来去表示开始配置切面了 可以想一下:既然要配置切面,那切面就是切入点和通知的结合,所以肯定需要配置切入点和通知这两部分 id属性:是给切面提供一个唯一标识 ref属性:是指定通知类bean的Id。 2.4 在<aop:aspect/>标签的内部使用对应标签来配置通知的类型 前置通知/后置通知/异常通知/最终通知 需求:beforePrintLog方法在切入点方法执行之前之前:所以是前置通知 前置通知:<aop:before/> method属性:用于指定Logger类中哪个方法是前置通知 pointcut属性:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强 3、切入点表达式的写法: 关键字:execution(表达式) 表达式: 访问修饰符 方法返回值 包名1.包名2...类名.方法名(参数列表) 需求: 我现在就想对UserServiceImpl类中的queryAll方法进行拦截 public java.util.List cn.bdqn.service.UserServiceImpl.queryAll() --> <!-- 2.1 把通知/增强Bean也需要注册到Spring容器中 --> <bean id="logger" class="cn.bdqn.advice.Logger"/> <!-- 2.2 使用此标签来去声明开始AOP的配置了--> <aop:config> <!--配置切面 --> <aop:aspect id="loggerAdvice" ref="logger"> <!-- 配置通知的类型,并且建立增强方法和切入点方法的关联--> <aop:before method="beforePrintLog" pointcut="execution(public java.util.List cn.bdqn.service.UserServiceImpl.queryAll())"/> </aop:aspect> </aop:config></beans>
11.5、测试
@Testpublic void testUserServiceImpl() throws Exception{ ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml"); UserService userService = (UserService) ac.getBean("userService"); userService.queryAll();}
11.6、切入点表达式
问题:我们上面的案例经过测试发现确实在调用业务方法之前增加了日志功能,但是问题是仅仅能针对某一个业务方法进行增强,而我们的业务方法又有可能有很多,所以显然一个一个的去配置很麻烦,如何更加灵活的去配置呢?这个就需要使用到切入点表达式
语法:execution(表达式)
访问修饰符 方法返回值 包名1.包名2...类名.方法名(参数列表)
11.6.1、访问修饰符可以省略
// 完整写法public java.util.List cn.bdqn.service.UserServiceImpl.queryAll())// 标准写法java.util.List cn.bdqn.service.UserServiceImpl.queryAll())
11.6.2、返回值可以使用通配符,表示任意返回值
* cn.bdqn.service.UserServiceImpl.queryAll())
11.6.3、包名可以使用通配符表示任意包。有几级包,就几个*
* *.*.*.UserServiceImpl.queryAll())
但是对于包来说,连续的写3个*,显然也是麻烦的,那么可以使用“…”表示当前包及其子包。
// 表示的是任意包下的只要有UserServiceImpl类都会对queryAll方法进行增强* *..UserServiceImpl.queryAll())
11.6.4、类名也可以用*
* *..*.queryAll()
11.6.5、方法也可以用*
* *..*.*()
11.6.6、参数列表
写法1、可以直接写数据类型: 基本类型直接写名称 int、double 引用类型写包名.类名的方式 java.lang.String、java.util.List写法2、可以使用通配符表示任意类型 前提是必须要有参数。写法3、使用.. 可以使用..表示有无参数均可,如果有参数则表示的可以是任意类型
11.6.7、全通配符写法
* *..*.*(..)
11.6.8、使用最多的写法
实际中的写法:切到业务层实现类下的所有方法。即:
* com.bdqn.service.impl.*.*(..)
11.7、通知类型的使用
11.7.1、在日志类中新增通知方法
// 定义记录日志的类,这个类就封装了我们所有的公共的代码public class Logger { // 该方法的作用是在切入点方法执行之前执行 public void beforePrintLog(){ System.out.println("前置通知(beforePrintLog):开始打印日志啦"); } // 该方法的作用是在切入点方法执行之后执行 public void afterReturningPrintLog(){ System.out.println("后置通知(afterReturningPrintLog):业务方法执行完了,日志打印"); } // 该方法的作用是在切入点方法执行出错后执行 public void afterThrowingPrintLog(){ System.out.println("异常通知(afterThrowingPrintLog):业务方法出现异常了,日志打印"); } // 该方法的作用是在切入点方法执行之后不管有没有错误,都最终要执行 public void afterPrintLog(){ System.out.println("最终通知(afterPrintLog):业务方法不管有没有异常了,日志打印"); }}
11.7.2、配置AOP
<beans> <!-- 2.1 把通知/增强Bean也需要注册到Spring容器中 --> <bean id="logger" class="cn.bdqn.advice.Logger"/> <!-- 2.2 使用此标签来去声明开始AOP的配置了--> <aop:config> <!--配置切面 --> <aop:aspect id="loggerAdvice" ref="logger"> <!-- 配置前置通知:在切入点方法执行之前执行--> <aop:before method="beforePrintLog" pointcut="execution(* cn.bdqn.service.UserServiceImpl.queryAll())"/> <!-- 后置通知:在切入点方法正常执行之后值。它和异常通知永远只能执行一个--> <aop:after-returning method="afterReturningPrintLog" pointcut="execution(* cn.bdqn.service.UserServiceImpl.queryAll())"/> <!--配置异常通知:在切入点方法执行产生异常之后执行。它和后置通知永远只能执行一个--> <aop:after-throwing method="afterThrowingPrintLog" pointcut="execution(* cn.bdqn.service.UserServiceImpl.queryAll())"/> <!--配置最终通知:无论切入点方法是否正常执行它都会在其后面执行--> <aop:after method="afterPrintLog" pointcut="execution(* cn.bdqn.service.UserServiceImpl.queryAll())"/> </aop:aspect> </aop:config></beans>
11.7.3、测试
@Testpublic void testUserServiceImpl() throws Exception{ ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml"); UserService userService = (UserService) ac.getBean("userService"); userService.queryAll();}/*** 前置通知(beforePrintLog):开始打印日志啦 查询全部用户执行啦 后置通知(afterReturningPrintLog):业务方法执行完了,日志打印 最终通知(afterPrintLog):业务方法不管有没有异常了,日志打印**/
11.8、切入点表达式改进
通过11.7可以发现,我们在配置文件中配置了四种通知类型,其中的pointcut配置的是切入点表达式,发现是一模一样的,那么有没有一种改进写法呢?可以将表达式抽取出来,将来可以引用。
11.8.1、方式一
<beans> <!-- 1、注册UserServiceImpl这个Bean --> <bean id="userService" class="cn.bdqn.service.UserServiceImpl"/> <!-- 2、以下操作都是Spring基于XML的AOP配置步骤--> <!-- 2.1 把通知/增强Bean也需要注册到Spring容器中 --> <bean id="logger" class="cn.bdqn.advice.Logger"/> <!-- 2.2 使用此标签来去声明开始AOP的配置了--> <aop:config> <!--配置切面 --> <aop:aspect id="loggerAdvice" ref="logger"> <!-- 配置切入点表达式 id属性用于指定切入点表达式的唯一标识。 expression属性用于指定表达式内容 此标签写在aop:aspect标签内部只能当前切面使用。 --> <aop:pointcut id="loggerPt" expression="execution(* cn.bdqn.service.UserServiceImpl.queryAll())"/> <!-- 配置前置通知:在切入点方法执行之前执行--> <aop:before method="beforePrintLog" pointcut-ref="loggerPt"/> <!-- 后置通知:在切入点方法正常执行之后值。它和异常通知永远只能执行一个--> <aop:after-returning method="afterReturningPrintLog" pointcut-ref="loggerPt"/> <!--配置异常通知:在切入点方法执行产生异常之后执行。它和后置通知永远只能执行一个--> <aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="loggerPt"/> <!--配置最终通知:无论切入点方法是否正常执行它都会在其后面执行--> <aop:after method="afterPrintLog" pointcut-ref="loggerPt"/> </aop:aspect> </aop:config></beans>
11.8.2、方式二
对于方式一,我们将aop:pointcut标签写在了aop:aspect里面,这样的话这切入点表达式只能被当前的切面使用,而如果其他切面想使用就使用不到了,所以我们可以把这个切入点表示再定义到外面。
<beans> <bean id="userService" class="cn.bdqn.service.UserServiceImpl"/> <!-- 2、以下操作都是Spring基于XML的AOP配置步骤--> <!-- 2.1 把通知/增强Bean也需要注册到Spring容器中 --> <bean id="logger" class="cn.bdqn.advice.Logger"/> <!-- 2.2 使用此标签来去声明开始AOP的配置了--> <aop:config> <!-- 配置切入点表达式 id属性用于指定切入点表达式的唯一标识。 expression属性用于指定表达式内容 此标签写在aop:aspect标签外面,那么所有的切面都可以使用。 --> <aop:pointcut id="loggerPt" expression="execution(* cn.bdqn.service.UserServiceImpl.queryAll())"/> <!--配置切面 --> <aop:aspect id="loggerAdvice" ref="logger"> <!-- 配置前置通知:在切入点方法执行之前执行--> <aop:before method="beforePrintLog" pointcut-ref="loggerPt"/> <!-- 后置通知:在切入点方法正常执行之后值。它和异常通知永远只能执行一个--> <aop:after-returning method="afterReturningPrintLog" pointcut-ref="loggerPt"/> <!--配置异常通知:在切入点方法执行产生异常之后执行。它和后置通知永远只能执行一个--> <aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="loggerPt"/> <!--配置最终通知:无论切入点方法是否正常执行它都会在其后面执行--> <aop:after method="afterPrintLog" pointcut-ref="loggerPt"/> </aop:aspect> </aop:config></beans>
11.9、环绕通知
11.9.1、在日志记录类中新增环绕通知
public class Logger { // 环绕通知 public void aroundPrintLog(){ System.out.println("环绕通知....aroundPrintLog....."); }}
11.9.2、AOP配置环绕通知
<beans> <!-- 1、注册UserServiceImpl这个Bean --> <bean id="userService" class="cn.bdqn.service.UserServiceImpl"/> <!-- 2、以下操作都是Spring基于XML的AOP配置步骤--> <!-- 2.1 把通知/增强Bean也需要注册到Spring容器中 --> <bean id="logger" class="cn.bdqn.advice.Logger"/> <!-- 2.2 使用此标签来去声明开始AOP的配置了--> <aop:config> <!-- 配置切入点表达式 --> <aop:pointcut id="loggerPt" expression="execution(* cn.bdqn.service.UserServiceImpl.queryAll())"/> <!--配置切面 --> <aop:aspect id="loggerAdvice" ref="logger"> <!-- 环绕通知--> <aop:around method="aroundPrintLog" pointcut-ref="loggerPt"/> </aop:aspect> </aop:config></beans>
11.9.3、测试1
@Testpublic void testUserServiceImpl() throws Exception{ ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml"); UserService userService = (UserService) ac.getBean("userService"); userService.queryAll();}/** 环绕通知....aroundPrintLog..... 发现:仅仅打印了环绕通知的代码。当我们配置了环绕通知之后,切入点方法没有执行,而通知方法执行了*/
11.9.4、解决
Spring框架为我们提供了一个接口:ProceedingJoinPoint。该接口有一个方法proceed(),此方法就相当于明确调用切入点方法。该接口可以作为环绕通知的方法参数,在程序执行时,spring框架会为我们提供该接口的实现类供我们使用。
public class Logger { // 环绕通知 public Object aroundPrintLog(ProceedingJoinPoint pjp){ Object result = null; try{ Object[] args = pjp.getArgs(); System.out.println(pjp.getSignature().getName()); System.out.println("前置"); result = pjp.proceed(args); System.out.println("后置"); return result; }catch (Throwable t){ System.out.println("异常"); throw new RuntimeException(t); }finally { System.out.println("最终"); } }}/** 环绕通知:它是spring框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式。*/
十二、基于注解的AOP
12.1、配置Spring环境
<dependencies> <!-- 导入Spring的jar包--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.1.RELEASE</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.4</version> </dependency></dependencies>
12.2、在beans.xml文件中定义AOP约束
<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 https://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/aop https://www.springframework.org/schema/aop/spring-aop.xsd"></beans>
12.3、定义记录日志的类【切面】
@Component("logger")@Aspect // 表示的是一个切面public class Logger { // 目的:在调用业务方法之前进行增强【前置通知】 @Before("execution(* cn.bdqn.service.impl.*.*(..))") public void beforePrintLog(){ System.out.println("前置通知----beforePrintLog---开始打印日志啦"); } // 后置通知 @AfterReturning("execution(* cn.bdqn.service.impl.*.*(..))") public void afterReturningPrintLog(){ System.out.println("后置通知----afterReturningPrintLog"); }}
注意,该类的两个细节:
a、@Component注解向容器中注册一个Bean。
b、@Aspect注解表示这个是一个切面类。
c、@Before注解表示的是这个是前置增强/前置通知。
12.4、定义Bean
package cn.bdqn.domain;public class User {}
package cn.bdqn.service;public interface UserService { // 保存用户 public void save(User user);}
package cn.bdqn.service.impl;@Service("userService") // 向容器中注册Beanpublic class UserServiceImpl implements UserService { @Override public void save(User user) { System.out.println("保存用户啦"); }}
注意:对于业务Bean,我们也需要通过@Service注解来向容器中注册。
12.5、在主配置文件中配置扫描的包
<beans> <context:component-scan base-package="cn.bdqn"/></beans>
12.6、在主配置文件中去开启AOP的注解支持
<beans> <aop:aspectj-autoproxy/></beans>
12.7、测试
public class UserServiceTest { @Test public void testUserService() throws Exception{ ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml"); UserService userService = (UserService) ac.getBean("userService"); userService.queryAll(); }}
12.8、优化改进
问题:我们看到对于切面类中定义的通知,有一个共性问题是,切入点表达式是相同的 , 那我们在想能否也像xml配置的那样,把切入点表达式给抽取出来呢?答案是可以的,改造如下:
@Component("logger")@Aspect // 表示的是一个切面public class Logger { @Pointcut("execution(* cn.bdqn.service.impl.*.*(..))") private void pt(){} // 目的:在调用业务方法之前进行增强【前置通知】 @Before("pt()") public void beforePrintLog(){ System.out.println("前置通知----beforePrintLog---开始打印日志啦"); } // 演示的后置通知 @AfterReturning("pt()") public void afterReturningPrintLog(){ System.out.println("后置通知----afterReturningPrintLog"); }}
12.9、总结
-
配置业务Bean
@Service("userService")public class UserServiceImpl implements UserService{ }
-
配置切面Bean
-
需要在切面类上定义@Aspect // 表示的是一个切面
@Component("logger")@Aspect // 表示的是一个切面public class Logger { }
-
在切面类中的通知方法上定义相应的通知
@Before: 前置通知@AfterReturning:后置通知@AfterThrowing: 异常通知@After:最终通知@Around: 环绕通知
-
定义切入点表达式
@Before("execution(* cn.bdqn.service.impl.*.*(..))")public void beforePrintLog(){ System.out.println("前置通知----beforePrintLog---开始打印日志啦");}
-
在主配置文件中去开启AOP的注解
aop:aspectj-autoproxy/
十三、Spring事务控制
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jrIvdmk1-1634112348636)(images/013_AOP的场景事务控制功能.png)]
13.0、说明
- 事务需要放在业务层(service)
- Spring的事务是基于AOP的
- Spring的事务控制有两种:编程式事务【了解】和声明式事务【重点】
- 声明式事务分为:基于xml配置和基于注解配置
13.1、事务的环境准备
13.1.1、导入依赖
<dependencies> <!-- 导入Spring的jar包--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.1.RELEASE</version> </dependency> <!-- 添加对AOP的支持 --> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.4</version> </dependency> <!-- 事务的jar包 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>5.2.1.RELEASE</version> </dependency> <!-- 数据库驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.48</version> </dependency> <!-- 操作数据库的支持 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.2.1.RELEASE</version> </dependency> <!-- 单元测试框架 --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency></dependencies>
13.1.2、添加AOP的约束
<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" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans https://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 https://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"></beans>
13.1.3、数据库准备
create database bank;use bank;create table t_account( id int primary key auto_increment, name varchar(30), balance int);insert into t_account(name,balance) values('段康家',2000);insert into t_account(name,balance) values('彭依凝',2000);
13.1.4、定义Bean
// 定义实体public class Account { private Integer id; private String name; private Integer balance;}
// 业务接口public interface AccountService { public void transfer(Integer srcId,Integer destId,Integer money);}public class AccountServiceImpl implements AccountService { @Override public void transfer(Integer srcId, Integer destId, Integer money) { // 源账号 Account srcAccount = accountDao.selectById(srcId); Integer srcBalance = srcAccount.getBalance(); srcAccount.setBalance(srcBalance - money); accountDao.updateById(srcAccount); // 目标账号 Account destAccount = accountDao.selectById(destId); Integer destBalance = destAccount.getBalance(); destAccount.setBalance(destBalance + money); accountDao.updateById(destAccount); }}
// dao接口public interface AccountDao { public Account selectById(Integer id); public void updateById(Account account); }// dao实现类public class AccountDaoImpl implements AccountDao { @Override public Account selectById(Integer id) { String sql = "select id,name,balance from t_account where id = ?"; List<Account> accounts = getJdbcTemplate().query(sql,new BeanPropertyRowMapper<Account>(Account.class),id); return accounts.get(0); } @Override public void updateById(Account account) { String sql = "update t_account set balance = ? where id = ?"; getJdbcTemplate().update(sql,account.getBalance(),account.getId()); }}
13.1.5、主配置文件定义
<beans> <!-- 配置Dao层--> <bean id="accountDao" class="cn.bdqn.dao.impl.AccountDaoImpl"> <property name="dataSource" ref=""/> </bean> <!-- 配置业务层--> <bean id="accountService" class="cn.bdqn.service.impl.AccountServiceImpl"> <property name="accountDao" ref="accountDao"></property> </bean> <!-- 配置数据源 --> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver"></property> <property name="url" value="jdbc:mysql:///bank"></property> <property name="username" value="root"></property> <property name="password" value="1234567"></property> </bean></beans>
13.1.6、测试
@Testpublic void testTransfer() throws Exception{ ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml"); AccountService accountService = (AccountService) ac.getBean("accountService"); accountService.transfer(1,2,100);}
13.2、基于xml的声明式事务
13.2.1、配置事务管理器
<!-- 1、配置事务管理器 --><bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/></bean>
13.2.2、配置事务的通知
<!-- 2、配置事务的通知 --><tx:advice id="txAdvice" transaction-manager="transactionManager"/>
13.2.3、配置AOP
重点是配置切入点表达式及事务通知和切入点表达式的关系
<!-- 3、开启配置AOP 配置切入点表达式及事务通知和切入点表达式的关系--><aop:config> <aop:pointcut id="pt" expression="execution(* cn.bdqn.service.impl.*.*(..))"/> <aop:advisor advice-ref="txAdvice" pointcut-ref="pt"/></aop:config>
13.2.4、配置事务的属性
注意:事务的属性在tx:advice标签的内部。
/*isolation:用于指定事务的隔离级别。默认值是DEFAULT,表示使用数据库的默认隔离级别。propagation:用于指定事务的传播行为。默认值是REQUIRED,表示一定会有事务,增删改的选择。查询方法可以选择SUPPORTS。read-only:用于指定事务是否只读。只有查询方法才能设置为true。默认值是false,表示读写。timeout:用于指定事务的超时时间,默认值是-1,表示永不超时。如果指定了数值,以秒为单位。rollback-for:用于指定一个异常,当产生该异常时,事务回滚,产生其他异常时,事务不回滚。没有默认值。表示任何异常都回滚。no-rollback-for:用于指定一个异常,当产生该异常时,事务不回滚,产生其他异常时事务回滚。没有默认值。表示任何异常都回滚。*/
<!-- 2、配置事务的通知 --><tx:advice id="txAdvice" transaction-manager="transactionManager"> <!-- 配置事务的属性--> <tx:attributes> <tx:method name="transfer" isolation="DEFAULT" propagation="REQUIRED" read-only="false"/> </tx:attributes></tx:advice>
13.3、基于注解的声明式事务
13.3.1、配置事务管理器
<!-- 1、配置事务管理器 --><bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/></bean>
13.3.2、在业务类添加注解
@Service("accountService")public class AccountServiceImpl implements AccountService { @Autowired private AccountDao accountDao;
13.3.3、配置JdbcTemplate模板
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"/></bean>
13.3.4、在Dao类添加注解
@Repository("accountDao")public class AccountDaoImpl implements AccountDao { @Autowired private JdbcTemplate jdbcTemplate;}
13.3.5、配置类中扫描包
<context:component-scan base-package="cn.bdqn"/>
13.3.6、开启spring对注解事务的支持
<tx:annotation-driven transaction-manager="transactionManager"/>
13.3.7、在业务层@Transactional注解
@Service("accountService")@Transactionalpublic class AccountServiceImpl implements AccountService { @Autowired private AccountDao accountDao;}
13.3.8、测试
@Testpublic void testTransfer() throws Exception{ ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml"); AccountService accountService = (AccountService) ac.getBean("accountService"); accountService.transfer(1,2,100);}