Spring 是分层的 Java SE/EE 应用 full-stack【全栈式】 轻量级开源框架,以 IoC(Inverse **Of Control:**
反转控制)和 **AOP(Aspect Oriented Programming:面向切面编程)**为核心,提供了展现层 Spring MVC 和持久层 Spring JDBC 以及业务层事务管理等众多的企业级应用技术,还能整合开源世界众多著名的第三方框架和类库,逐渐成为使用最多的 Java EE 企业应用开源框架。
Spring倡导以==“最少侵入”的方式来管理应用中的代码,这意味着我们可以随时安装或者卸载 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 技术的最佳实践的范例。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ohDYwI1g-1634112348618)(images/001_Spring体系结构.png)]
官网
总结:简化Java应用程序开发
以经典的三层架构MVC作为案例,以前我们都是这么干的,看如下代码:
// 用户控制器
public class UserServlet{
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response){
// ...
userService.login();
// ...
}
}
// 用户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);
}
}
// 用户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语句
}
}
按照以前的做法,可以发现,我们的UserServlet要依赖UserServiceImpl实现,UserServiceImpl要依赖UserDaoImpl实现,一个很明显的问题是:对于这些具体的实现类是由我们程序员自己去主动手动创建(new)出来的,那么意味着假如某个实现类发生了变化,将可能导致整个应用的不可用【编译不通过等】,那么这个问题的本质原因就在于这个具体的类是我们自己硬编码到Java类中的。试想:能否有一种方式可以实现在不改变代码的前提下实现自如的切换具体的实现类呢?答案是可以的,那么我们可以将创建这个实现类的工作交给别人来做,而我们就不再去自己主动创建类的对象了。那么解决方案就是:Spring的Ioc。
以四章的HelloWorld为例,这个创建对象的工作不是由程序员主动去创建,而是交给了框架,比如说Spring,那么这个时候我们称**创建对象的控制权交给了其他人,那么就称之为控制反转。**
在现实生活中,人们要用到一样东西的时候,第一反应就是去找到这件东西,比如想喝新鲜橙汁,在没有饮品店的日子里,最直观的做法就是:买果汁机、买橙子,然后准备开水。值得注意的是:这些都是你自己**“主动”创造**的过程,也就是说一杯橙汁需要你自己创造。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nGEUrEgj-1634112348622)(images/002_主动创建.png)]
然而到了今时今日,由于饮品店的盛行,当我们想喝橙汁时,第一想法就转换成了找到饮品店的联系方式,通过电话等渠道描述你的需要、地址、联系方式等,下订单等待,过一会儿就会有人送来橙汁了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z1d2JF5V-1634112348623)(images/003_被动创建.png)]
注意:你并没有“主动”去创造橙汁,橙汁是由饮品店创造的,而不是你,然而也完全达到了你的要求,甚至比你创造的要好上那么一些。
含义:
总结:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cSyE6WDp-1634112348626)(images/014.png)]
以HelloWorld为例,编写一个程序,让创建对象的工作由Spring帮助我们创建。
public class HelloWorld {
private String name;
public void setName(String name) {
this.name = name;
}
public void sayHi(){
System.out.println(name + ",HelloWorld!");
}
}
@Test
public void testSayHi() throws Exception{
HelloWorld helloWorld = new HelloWorld();
helloWorld.setName("段康家");
helloWorld.sayHi();
}
这种做法是以前最常用的做法,HelloWorld这个类的对象是我们程序员自己去创建并为属性赋值,但是要使用Spring,该如何实现同样的功能呢?看4.3以后的章节。
<dependencies>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-contextartifactId>
<version>5.2.1.RELEASEversion>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.12version>
<scope>testscope>
dependency>
dependencies>
说明:主配置文件的名称一般叫beans.xml或在applicationContext.xml
在resources目录下新建beans.xml文件。
<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"> <bean id="helloWorld" class="cn.bdqn.HelloWorld"> <property name="name" value="彭依凝"/> bean>beans>
@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();}
ClassPathXmlApplicationContext
该类可以加载类路径下的配置文件,要求配置文件必须在类路径下。不在的话,加载不了。
FileSystemXmlApplicationContext
它可以加载磁盘任意路径下的配置文件。
AnnotationConfigApplicationContext
它是用于读取注解创建容器的
是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);}
应用上下文,该接口其实是BeanFactory的子接口,提供了更多的有用的功能:
ApplicationContext在启动的时候就把所有的Bean全部实例化了。立即加载
案例在此就不再举例了。
在Spring里面通过配置文件==创建对象==。
public class UserServiceImpl { }
<beans> <bean id="userService" class="cn.bdqn.UserServiceImpl">bean>beans>
@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);}
此种方式采用的就是通过默认构造函数的方式创建Bean,假设我们给UserServiceImpl添加了一个带参的构造方法,则运行会报错,原因在于当我们为某个类自定义构造方法的时候,Java编译器便不会为该类提供默认的不带参数的构造方法了。
// UserService的工厂,作用是创建UserServiceBean对象public class UserServiceImplFactory { public UserServiceImpl createUserService(){ return new UserServiceImpl(); }}
public class UserServiceImpl { }
<beans> <bean id="userServiceFactory" class="cn.bdqn.UserServiceImplFactory"/> <bean id="userService" factory-bean="userServiceFactory" factory-method="createUserService"/>beans>
@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);}
// UserService的工厂,作用是创建UserServiceBean对象public class UserServiceImplFactory { public static UserServiceImpl createUserService(){ return new UserServiceImpl(); }}
public class UserServiceImpl {}
<beans> <bean id="userService" class="cn.bdqn.UserServiceImplFactory" factory-method="createUserService"> bean>beans>
@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);}
Spring对Bean的默认的作用域(作用范围)是singleton【单例】
singleton:单例的(默认值),只会new一次。
prototype:多例的,用到一次就会new一次。
request:作用于web应用的请求范围,Spring创建这个类之后,将这个类存到request范围内。
session:应用于web项目的会话范围,Spring创建这个类之后,将这个类存到session范围内。
global-session:作用于集群环境的会话范围(全局会话范围),当不是集群环境时,它就是session。
实际开发中用得最多的就是singleton和prototype,在整合struts2的时候使用prototype,在整合SpringMVC的时候使用singleton。
bean标签的scope属性,作用:指定bean的作用范围。
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}
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}
出生:当容器创建时对象出生活着:只要容器还在,对象一直活着死亡:容器销毁,对象消亡
public class UserServiceImpl { public UserServiceImpl(){ System.out.println("对象的构造方法执行了"); } public void init(){ System.out.println("对象初始化了"); } public void destroy(){ System.out.println("对象销毁了"); }}
<beans> <bean id="userService" class="cn.bdqn.UserServiceImpl" scope="singleton" init-method="init" destroy-method="destroy"/>beans>
@Testpublic void testUserService() throws Exception{ // 1、读取主配置文件信息,获取核心容器对象 ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml"); ac.close();}// 结果:对于单例对象来说,只要容器创建了,那么对象就创建了。类似于立即加载。
对象的构造方法执行了对象初始化了对象销毁了
总结:单例对象的生命周期和容器相同
出生:当我们使用对象时spring框架为我们创建活着:对象只要是在使用过程中就一直活着。死亡:当对象长时间不用,且没有别的对象引用时,由Java的垃圾回收器回收
public class UserServiceImpl { public UserServiceImpl(){ System.out.println("对象的构造方法执行了"); } public void init(){ System.out.println("对象初始化了"); } public void destroy(){ System.out.println("对象销毁了"); }}
<beans> <bean id="userService" class="cn.bdqn.UserServiceImpl" scope="prototype" init-method="init" destroy-method="destroy"/>beans>
@Testpublic void testUserService() throws Exception{ // 1、读取主配置文件信息,获取核心容器对象 ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml"); ac.close();}// 结果:什么都不输出,说明容器启动的时候,对于多例对象来说并不会创建
@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框架是不负责管理的
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nXCwTeOH-1634112348627)(images/012_Spring对Bean的管理.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5LuL5N3E-1634112348628)(images/015.png)]
全称
Dependency Injection(DI)
与IoC的关系
曾在第一章提到过,IoC和DI其实说的是一个意思,可以这么说:IoC是一种思想,DI是对这种思想的一种具体实现
依赖关系的管理
以后都交给spring来维护,在当前类需要用到其他类的对象,由spring为我们提供,我们只需要在配置文件中说明。
依赖关系的维护
就称之为依赖注入。
能注入的数据:有三类
基本类型和String。
其他bean类型(在配置文件中或者注解配置过的bean)
复杂类型/集合类型
注入的方式:有三种
第一种:使用构造函数提供
第二种:使用set方法提供
第三种:使用注解提供(参考第七章节)
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; }}
<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>
@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}}
说明:案例1采用的index索引的方式实现注入,就以目前案例来说是完全没问题的,但是如果Bean中存在下面情况,可能就不怎么适用了。
需求:我现在要创建Person类的对象,调用Person(Integer id, String name, Double weight)构造方法
说明:为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; }}
<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>
@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)构造方法
通过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>
前两种方式通过index+type的确可以解决问题,但是总是觉得还是有些麻烦,能否有更加简单的方式呢?就是直接采用参数名的方式更易于阅读和使用。
Bean的定义同6.2.2.1章节。
<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>
@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}}
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; }}
<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>
// 测试后发现程序报错,原因在于:期望需要一个Date类型,而你现在传了一个字符串,数据类型不匹配。Unsatisfied dependency expressed through constructor parameter 2: Could not convert argument value of type [java.lang.String] to required type [java.util.Date]
<beans> <bean id="person" class="cn.bdqn.Person"> <constructor-arg name="id" value="2"/> <constructor-arg name="name" value="史周冲"/> <constructor-arg name="birthday" ref="currentDate"/> bean> <bean id="currentDate" class="java.util.Date"/>beans>
使用的标签:
constructor-arg
标签出现的位置:
bean标签的内部
标签中的属性:
type:用于指定要注入的数据的数据类型。
index:用于指定要注入的数据给构造函数中指定索引位置的参数赋值,索引的位置是从0开始。
name:用于指定给构造函数中指定名称的参数赋值。
value:用于提供基本类型和String类型的数据。
ref:引用,用于指定其他的bean类型数据。它指的就是在spring的Ioc核心容器中出现过的bean对象
优势:
假设我们需要创建一个对象时,需要明确初始化一些数据,那么这种方式显然是很好的。因为通过构造函数创建对象时候,如果不指定具体的参数是无法把对象创建成功的。可以起到一个约束的作用。
劣势:
改变了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方法
<bean id="currentDate" class="java.util.Date"/> <bean id="user" class="cn.bdqn.User"> <property name="name" value="宋炜烨"/> <property name="born" ref="currentDate"/>bean>
@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}}
涉及的标签
property
出现的位置
bean标签的内部
标签的属性
name:用于指定注入时所调用的set方法名称。
value:用于提供基本类型和String类型的数据。
ref:用于指定其他的bean类型数据。它指的就是在spring的Ioc核心容器中出现过的bean对象。
优势
创建对象时没有明确的限制,可以直接使用默认构造函数。
劣势
如果有某个成员属性必须有值,则有可能再使用该对象的时候并没有通过set方法注入值,可能拿到为空的值。
public class Cat { private String[] arrs; public void setArrs(String[] arrs) { this.arrs = arrs; }}
<beans> <bean id="cat" class="cn.bdqn.Cat"> <property name="arrs"> <array> <value>崔灿value> <value>时贝妮value> array> property> bean>beans>
public class Cat { private List<String> arrList; public void setArrList(List<String> arrList) { this.arrList = arrList; }}
<beans> <bean id="cat" class="cn.bdqn.Cat"> <property name="arrList"> <list> <value>乔峰value> <value>马夫人value> list> property> bean>beans>
public class Cat { private Set<String> arrSet; public void setArrSet(Set<String> arrSet) { this.arrSet = arrSet; }}
<beans> <bean id="cat" class="cn.bdqn.Cat"> <property name="arrSet"> <set> <value>段誉value> <value>鸠摩智value> set> property> bean>beans>
public class Cat { private Map<String,Object> arrMap; public void setArrMap(Map<String, Object> arrMap) { this.arrMap = arrMap; }}
<bean> <property name="arrMap"> <map> <entry key="S001" value="彭依凝"/> <entry key="S002" value="段康家"/> <entry key="S003" value="王浩"/> map> property>bean>
public class Cat { private Properties props; public void setProps(Properties props) { this.props = props; }}
<bean id="cat" class="cn.bdqn.Cat"> <property name="props"> <props> <prop key="A001">虚竹prop> <prop key="A002">扫地僧prop> props> property>bean>
用于给List结构集合注入的标签:
list、array、set
用于个Map结构集合注入的标签:
map 、props
总结
结构相同,标签可以互换
回顾一下基于xml配置的Spring对Bean的管理 ,对Bean的完整管理如下所示:
<bean id="" class="" init-method="" destroy-method="" scope=""> <property name="" value=""/> <property name="" ref=""/>bean>
分析可以发现:我们对Bean的管理就四个方面,分别是:
其实对于注解来说,也是包括了这四个方面,换句话说,使用注解的方式管理Bean和使用xml的方式管理Bean作用是完全一样的,区别仅仅在于配置的形式不同而已。
@Componentpublic class DogService { }
<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"> <context:component-scan base-package="cn.bdqn"/>beans>
@Testpublic void testDogServiceImpl() throws Exception{ // 1、读取主配置文件信息,获取核心容器对象 ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml"); DogServiceImpl dogService = (DogServiceImpl) ac.getBean("dogServiceImpl"); System.out.println(dogService);}
作用
用于把对当前修饰的类创建出来,并存放到Spring容器中。
属性
a. 用该注解所创建的对象默认的id名称是当前类名,且首字母改小写
b. 可以通过value属性手动的指定bean的id。
一般用在表现层,例如SpringMVC、Struts2
一般用在业务层,例如Service层
一般用在持久层7.2.5、总结
作用:
自动按照==类型==注入。
// 用户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 {}
<beans> <context:component-scan base-package="cn.bdqn"/>beans>
@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}
假设系统中存在两个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章节使用Autowired注解的时候,会存在一个问题,就是如果系统中存在多个类型的Bean都匹配的时候,就会找不到到底要使用哪个Bean对象了,会报错,我们采取的解决办法是:修改变量名即可解决 , 但是这种做法实际上是挺菜的,我现在就想使用userDao这个变量名,那么能否有一种更好的解决办法呢?答案是肯定的,即使用Qualifier注解。
Bean的定义,仍然采用7.3.1章节所定义好的Bean,唯一的区别是UserServiceImpl这个Bean,修改如下:
public class UserServiceImpl implements UserService { @Autowired @Qualifier(value = "userDao01") // 通过此注解中的value属性明确指定要用哪个name的bean private UserDao userDao;}
Bean的定义,仍然采用7.3.1章节所定义好的Bean,唯一的区别是UserServiceImpl这个Bean,修改如下:
@Service("userService")public class UserServiceImpl implements UserService { @Resource(name = "userDao01") private UserDao userDao;}
注意:
使用Autowired、Qualifier以及Resource这三个注解都只能注入其他bean类型的数据,对于基本数据类型和String类型是无法通过使用该3个注解实现。同时,对于集合数据类型的注入只能通过XML来实现。
作用:
用于注入基本类型和String类型的数据。
@Component("jdbcUtils")public class JdbcUtils { @Value("com.mysql.jdbc.Driver") private String driverClass;}
@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}
如果Value注解用于案例1,那这样太菜了,我们要为driverClass这个变量赋值,那岂不是直接赋值得了,还需要搞一个Value直接赋值吗?显然是没有必要的,所以一般来说,Value注解常用于对配置文件内容的读取。
driverClass=com.mysql.jdbc.Driverport=3306
<beans> <context:component-scan base-package="cn.bdqn"/> <context:property-placeholder location="classpath:db.properties"/>beans>
@Component("jdbcUtils")public class JdbcUtils { @Value("${driverClass}") private String driverClass; @Value("${port}") private Integer port;}
@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}
该注解通过可以实现对基本数据类型和String类型的注入,并且是支持使用Spring的EL表达式。那么对于Spring的EL表达式的语法就是:${表达式}。
以上注解的作用就和在xml配置文件中的bean标签中写一个标签的作用是一样的
方式:
Bean的定义,仍然采用7.3.1章节所定义好的Bean,唯一的区别是UserServiceImpl这个Bean修改作用域,修改如下:
@Service("userService")@Scope("singleton")public class UserServiceImpl implements UserService{ }
@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。
Scope该注解的作用就和在bean标签中使用scope属性实现的功能是一样的
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("对象销毁了"); }}
@Testpublic void testUserServiceImpl() throws Exception{ // 1、读取主配置文件信息,获取核心容器对象 ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml"); ac.close();}// 对象初始化了// 对象销毁了
这个两个注解作用就和在bean标签中使用init-method和destroy-methode的作用是一样的。
public class SpringMainConfig {}
@Configurationpublic class SpringMainConfig { }
使用Configuration注解修饰的类表示的是:当前类是一个配置类。该类的作用和beans.xml是一样的,换句话说,该注解所修饰的类就是用来代替beans.xml文件的。
public class User { private Integer id; private String name; public User(Integer id, String name) { this.id = id; this.name = name; }}
在以前,使用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,"段康家"); }}
@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='段康家'}}
@Configurationpublic class SpringMainConfig { // 给容器中注册一个Bean;类型为返回值的类型,id默认是用方法名作为id // 可以再注册Bean的同时,不用方法名作为id,可以指定bean的名称。 @Bean("user01") public User user(){ return new User(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会从容器中根据类型注入
当然还有一种情况是:容器中注册了多个相同类型的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注解应用在方法参数上时,可以单独使用
==作用:==指定spring在创建容器时要扫描的包。作用就如同在配置文件中这样的定义:
<beans> <context:component-scan base-package="cn.bdqn"/>beans>
第一步:定义三个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注册到容器中
**需求:**我现在不想扫描被@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**/
**需求:**我现在只想扫描被@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注解。*/
作用:可以改变bean的作用域。在Spring中,Bean的默认作用域是单例的。
public class User { private Integer id; private String name; public User(Integer id, String name) { this.id = id; this.name = name; }}
@Configurationpublic class SpringMainConfig { @Bean @Scope public User user(){ System.out.println("对象创建啦"); return new User(1,"HelloWorld"); }}
@Testpublic void testComponentScan() throws Exception{ ApplicationContext ac = new AnnotationConfigApplicationContext(SpringMainConfig.class);}/** 对象创建啦*/
结果:注册的Bean默认是单实例的,Spring容器将会在启动的时候调用方法创建对象后放入容器中,以后每次需要拿对象了就直接从容器中拿,而且每次拿的还是同一个。
@Configurationpublic class SpringMainConfig { @Bean @Scope("prototype") public User user(){ System.out.println("对象创建啦"); return new User(1,"HelloWorld"); }}
@Testpublic void testPrototype() throws Exception{ ApplicationContext ac = new AnnotationConfigApplicationContext(SpringMainConfig.class);}/** 测试后发现容器启动的时候,多例对象并不会创建。*/
@Testpublic void testPrototype() throws Exception{ ApplicationContext ac = new AnnotationConfigApplicationContext(SpringMainConfig.class); User user01 = (User) ac.getBean("user"); User user02 = (User) ac.getBean("user");}/** 对象创建啦 对象创建啦*/
总结:对于多实例来说,Spring容器在启动的时候并不会去调用方法创建对象并放在容器中,而是每次获取的时候才会调用方法创建对象,并且每次获取到的对象还不一样。
作用:用此注解修饰的Bean表示的该Bean需要被懒加载,即不会随着Spring容器的启动而去创建。
@Configurationpublic class SpringMainConfig { @Bean @Lazy(value = true) // 开启懒加载 public User user(){ System.out.println("对象创建啦"); return new User(1,"HelloWorld"); }}
@Testpublic void testSingleton() throws Exception{ ApplicationContext ac = new AnnotationConfigApplicationContext(SpringMainConfig.class);}/** 控制台什么都没有输出,因为我们的Bean被Layz注解修饰了,即使Spring容器启动了,我们的Bean也不会去创建,只有等到了真正要用到该bean了才去创建。*/
作用: 根据一定的条件进行判断,当满足这个条件时给容器注入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);}
// 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; }}
// 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; }}
@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操作系统"); }}
@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。*/
测试下如果程序运行在linux下,是否能够注入LinuxBean?测试过程,即:运行程序的时候手动的指定运行时参数信息,步骤如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xk4gh7np-1634112348633)(images/007_位置.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VH999F3h-1634112348633)(images/008_配置运行时参数.png)]
测试后,发现,打印的结果是:LinuxBean。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XBx6vPqK-1634112348634)(images/009_Import注解.png)]
简单来说,Import注解可以去修饰常规组件类,意思是:所谓的常规组件类就是一个普通的Java类, 而如果这个普通的Java类被@Import注解修饰,那么也可以将这个Java类注册到Spring容器中,成为Bean。
public class Dog { }
@Configuration@Import(value = {Dog.class})public class SpringMainConfig {}
@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*/
使用Import注解导入一个常规组件的时候,这个Bean的名称是类的全路径
即Import可以导入被@Configuration注解所修饰的类,换句话说,Import注解可以去导入其他的配置类,存在的一个细节是:假如这个配置类里面也注册了多个Bean,那么同样也会将这些个Bean一并注册到Spring容器中。
// 奥迪public class AuDi { }
// 宝马public class BMW {}
// 针对系统中存在的车的一个配置类@Configurationpublic class Car { @Bean public AuDi auDi(){ return new AuDi(); } @Bean public BMW bmw(){ return new BMW(); }}
@Configuration@Import(value = {Car.class})public class SpringMainConfig {}
测试代码如同8.7.1.3,结果如下:
/** cn.bdqn.Car ---> 配置类 auDi ---> 配置类中的奥迪Bean bmw ---> 配置类中的宝马Bean*/
==作用:可以设置properties文件的位置,这样的话,就可以通过Value注解读取配置文件的内容。==该注解的作用等同于:
<context:property-placeholder location="classpath:db.properties"/>
位置:该注解通常和@Configuration注解一起使用。
id=1name=丁春秋
@Configuration // 主配置类@ComponentScan("cn.bdqn") // 扫描包,目的是能够扫描到Pig类@PropertySource(value = {"pig.properties"}) //加载配置文件public class SpringMainConfig { }
@Testpublic void testPig() throws Exception{ ApplicationContext ac = new AnnotationConfigApplicationContext(SpringMainConfig.class); Pig pig = (Pig) ac.getBean("pig"); System.out.println(pig); // 1,name}
<dependency> <groupId>org.springframeworkgroupId> <artifactId>spring-testartifactId> <version>5.2.1.RELEASEversion> <scope>testscope>dependency>
@ContextConfiguration(classes = {SpringMainConfig.class})@RunWith(SpringJUnit4ClassRunner.class)public class ConditionalTest { @Autowired private Pig pig; @Test public void testPig() throws Exception{ System.out.println(pig); }}
@ContextConfiguration注解需要注意的细节是:
首先来看一个问题,假如我现在有一个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)]
从上图可以看出,如果从系统的横向角度来看,我们的权限功能,日志功能,事务功能是把系统的各个模块中的各个业务方法在执行之前从前面拦截了,好像拿了一把刀把一个完整的苹果切成一半,形成了一个切面。这个也就是=="面向切面编程==中切面的含义。
AOP:全称是 Aspect Oriented Programming 即:面向切面编程/面向方面编程。AOP不是一项新的技术,是OOP(面向对象编程)技术的补充,是OOP的延续,同时也是Spring最重要的内容之一。
简单来说,AOP就是把我们程序重复的代码抽取出来,在需要执行的时候,使用代理技术,在不修改源码的基础上,对我们的已有方法进行增强。
Spring框架的AOP机制可以让开发者把业务流程中的通用功能抽取出来,单独编写功能代码。在业务流程执行过程中,Spring框架会根据业务流程要求,自动把独立编写的功能代码切入到流程的合适位置。
在程序运行期间,不修改源码对已有方法进行增强。
动态代理
全英叫:Joinpoint。所谓连接点是指那些被拦截到的点。在 spring 中,这些点指的是方法,因为 spring 只支持方法类型的连接点。
全英叫:Pointcut。所谓切入点是指我们要对哪些 Joinpoint 进行拦截。
全英叫:Advice。所谓通知/增强是指拦截到 Joinpoint 之后所要做的事情就是通知。说白了,就是一段功能性的代码。
通知的类型
前置通知
后置通知
异常通知
最终通知
环绕通知
try{ // 前置通知【开启事务】 // 业务代码.... // 后置通知【提交事务】}catch (Exception e){ // 异常通知 【回滚事务】}finally { // 最终通知 【释放资源】}
全英叫:Weaving。是指把增强应用到目标对象来创建新的代理对象的过程,是一个动作。
全英叫:Aspect。是切入点和通知/增强的结合。
全英叫:Introduction。引介是一种特殊的通知在不修改类代码的前提下, Introduction可以在运行期为类动态地添加一些方法或Field。
<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>
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 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 queryAll(){ return new ArrayList(); }}
package cn.bdqn.advice;// 定义记录日志的类,这个类就封装了我们所有的公共的代码public class Logger { // 该方法的作用是在切入点方法执行之前执行 public void beforePrintLog(){ System.out.println("开始打印日志啦"); }}
<dependency> <groupId>org.aspectjgroupId> <artifactId>aspectjweaverartifactId> <version>1.9.4version>dependency>
<beans> <bean id="userService" class="cn.bdqn.service.UserServiceImpl"/> <bean id="logger" class="cn.bdqn.advice.Logger"/> <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>
@Testpublic void testUserServiceImpl() throws Exception{ ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml"); UserService userService = (UserService) ac.getBean("userService"); userService.queryAll();}
问题:我们上面的案例经过测试发现确实在调用业务方法之前增加了日志功能,但是问题是仅仅能针对某一个业务方法进行增强,而我们的业务方法又有可能有很多,所以显然一个一个的去配置很麻烦,如何更加灵活的去配置呢?这个就需要使用到切入点表达式
语法:execution(表达式)
访问修饰符 方法返回值 包名1.包名2...类名.方法名(参数列表)
// 完整写法public java.util.List cn.bdqn.service.UserServiceImpl.queryAll())// 标准写法java.util.List cn.bdqn.service.UserServiceImpl.queryAll())
* cn.bdqn.service.UserServiceImpl.queryAll())
* *.*.*.UserServiceImpl.queryAll())
但是对于包来说,连续的写3个*,显然也是麻烦的,那么可以使用“…”表示当前包及其子包。
// 表示的是任意包下的只要有UserServiceImpl类都会对queryAll方法进行增强* *..UserServiceImpl.queryAll())
* *..*.queryAll()
* *..*.*()
写法1、可以直接写数据类型: 基本类型直接写名称 int、double 引用类型写包名.类名的方式 java.lang.String、java.util.List写法2、可以使用通配符表示任意类型 前提是必须要有参数。写法3、使用.. 可以使用..表示有无参数均可,如果有参数则表示的可以是任意类型
* *..*.*(..)
实际中的写法:切到业务层实现类下的所有方法。即:
* com.bdqn.service.impl.*.*(..)
// 定义记录日志的类,这个类就封装了我们所有的公共的代码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):业务方法不管有没有异常了,日志打印"); }}
<beans> <bean id="logger" class="cn.bdqn.advice.Logger"/> <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>
@Testpublic void testUserServiceImpl() throws Exception{ ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml"); UserService userService = (UserService) ac.getBean("userService"); userService.queryAll();}/*** 前置通知(beforePrintLog):开始打印日志啦 查询全部用户执行啦 后置通知(afterReturningPrintLog):业务方法执行完了,日志打印 最终通知(afterPrintLog):业务方法不管有没有异常了,日志打印**/
通过11.7可以发现,我们在配置文件中配置了四种通知类型,其中的pointcut配置的是切入点表达式,发现是一模一样的,那么有没有一种改进写法呢?可以将表达式抽取出来,将来可以引用。
<beans> <bean id="userService" class="cn.bdqn.service.UserServiceImpl"/> <bean id="logger" class="cn.bdqn.advice.Logger"/> <aop:config> <aop:aspect id="loggerAdvice" ref="logger"> <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>
对于方式一,我们将aop:pointcut标签写在了aop:aspect里面,这样的话这切入点表达式只能被当前的切面使用,而如果其他切面想使用就使用不到了,所以我们可以把这个切入点表示再定义到外面。
<beans> <bean id="userService" class="cn.bdqn.service.UserServiceImpl"/> <bean id="logger" class="cn.bdqn.advice.Logger"/> <aop:config> <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>
public class Logger { // 环绕通知 public void aroundPrintLog(){ System.out.println("环绕通知....aroundPrintLog....."); }}
<beans> <bean id="userService" class="cn.bdqn.service.UserServiceImpl"/> <bean id="logger" class="cn.bdqn.advice.Logger"/> <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>
@Testpublic void testUserServiceImpl() throws Exception{ ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml"); UserService userService = (UserService) ac.getBean("userService"); userService.queryAll();}/** 环绕通知....aroundPrintLog..... 发现:仅仅打印了环绕通知的代码。当我们配置了环绕通知之后,切入点方法没有执行,而通知方法执行了*/
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框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式。*/
<dependencies> <dependency> <groupId>org.springframeworkgroupId> <artifactId>spring-contextartifactId> <version>5.2.1.RELEASEversion> dependency> <dependency> <groupId>org.aspectjgroupId> <artifactId>aspectjweaverartifactId> <version>1.9.4version> dependency>dependencies>
<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>
@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注解表示的是这个是前置增强/前置通知。
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注解来向容器中注册。
<beans> <context:component-scan base-package="cn.bdqn"/>beans>
<beans> <aop:aspectj-autoproxy/>beans>
public class UserServiceTest { @Test public void testUserService() throws Exception{ ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml"); UserService userService = (UserService) ac.getBean("userService"); userService.queryAll(); }}
问题:我们看到对于切面类中定义的通知,有一个共性问题是,切入点表达式是相同的 , 那我们在想能否也像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"); }}
配置业务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/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jrIvdmk1-1634112348636)(images/013_AOP的场景事务控制功能.png)]
<dependencies> <dependency> <groupId>org.springframeworkgroupId> <artifactId>spring-contextartifactId> <version>5.2.1.RELEASEversion> dependency> <dependency> <groupId>org.aspectjgroupId> <artifactId>aspectjweaverartifactId> <version>1.9.4version> dependency> <dependency> <groupId>org.springframeworkgroupId> <artifactId>spring-txartifactId> <version>5.2.1.RELEASEversion> dependency> <dependency> <groupId>mysqlgroupId> <artifactId>mysql-connector-javaartifactId> <version>5.1.48version> dependency> <dependency> <groupId>org.springframeworkgroupId> <artifactId>spring-jdbcartifactId> <version>5.2.1.RELEASEversion> dependency> <dependency> <groupId>junitgroupId> <artifactId>junitartifactId> <version>4.12version> <scope>testscope> dependency>dependencies>
<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>
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);
// 定义实体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 accounts = getJdbcTemplate().query(sql,new BeanPropertyRowMapper(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()); }}
<beans> <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>
@Testpublic void testTransfer() throws Exception{ ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml"); AccountService accountService = (AccountService) ac.getBean("accountService"); accountService.transfer(1,2,100);}
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/>bean>
<tx:advice id="txAdvice" transaction-manager="transactionManager"/>
重点是配置切入点表达式及事务通知和切入点表达式的关系
<aop:config> <aop:pointcut id="pt" expression="execution(* cn.bdqn.service.impl.*.*(..))"/> <aop:advisor advice-ref="txAdvice" pointcut-ref="pt"/>aop:config>
注意:事务的属性在tx:advice标签的内部。
/*isolation:用于指定事务的隔离级别。默认值是DEFAULT,表示使用数据库的默认隔离级别。propagation:用于指定事务的传播行为。默认值是REQUIRED,表示一定会有事务,增删改的选择。查询方法可以选择SUPPORTS。read-only:用于指定事务是否只读。只有查询方法才能设置为true。默认值是false,表示读写。timeout:用于指定事务的超时时间,默认值是-1,表示永不超时。如果指定了数值,以秒为单位。rollback-for:用于指定一个异常,当产生该异常时,事务回滚,产生其他异常时,事务不回滚。没有默认值。表示任何异常都回滚。no-rollback-for:用于指定一个异常,当产生该异常时,事务不回滚,产生其他异常时事务回滚。没有默认值。表示任何异常都回滚。*/
<tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <tx:method name="transfer" isolation="DEFAULT" propagation="REQUIRED" read-only="false"/> tx:attributes>tx:advice>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/>bean>
@Service("accountService")public class AccountServiceImpl implements AccountService { @Autowired private AccountDao accountDao;
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"/>bean>
@Repository("accountDao")public class AccountDaoImpl implements AccountDao { @Autowired private JdbcTemplate jdbcTemplate;}
<context:component-scan base-package="cn.bdqn"/>
<tx:annotation-driven transaction-manager="transactionManager"/>
@Service("accountService")@Transactionalpublic class AccountServiceImpl implements AccountService { @Autowired private AccountDao accountDao;}
@Testpublic void testTransfer() throws Exception{ ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml"); AccountService accountService = (AccountService) ac.getBean("accountService"); accountService.transfer(1,2,100);}