大家好,又见面了,我是你们的朋友全栈君。
-
Spring Bean
-
Spring AOP
-
Spring 事务管理
1 Spring入门
1.1 Spring 简介
1.1.1 Spring的由来
Spring是一个轻量级Java开发框架,最早有Rod Johnson创建,目的是为了解决企业级应用开发的业务逻辑层和其他各层的耦合问题。它是一个分层的JavaSE/JavaEE full-stack(一站式)轻量级开源框架,为开发Java应用程序提供全面的基础架构支持。Spring负责基础架构,因此Java开发者可以专注于应用程序的开发。
1.1.2 Spring的优点
(1)方便解耦,简化开发
Spring就是一个大工厂,可以将所有对象创建和依赖的关系维护,交给Spring管理。
(2)AOP编程的支持
Spring提供面向切面编程,可以方便的实现对程序进行权限拦截、运行监控等功能。
(3)声明式事务的支持
只需要通过配置就可以完成对事务的管理,而无需手动编程。
(4)方便程序的测试
Spring对Junit4支持,可以通过注解方便的测试Spring程序。
(5)方便集成各种优秀框架
Spring不排斥各种优秀的开源框架,其内部提供了对各种优秀框架的直接支持(如:Struts、Hibernate、MyBatis等)。
(6)降低JavaEE API的使用难度
Spring对JavaEE开发中非常难用的一些API(JDBC、JavaMail、远程调用等),都提供了封装,使这些API应用难度大大降低。
1.1.3 Spring的体系结构
Spring框架至今已集成了20多个模块,这些模块分布在核心容器(Core Container)、数据访问/集成(Data Access/Integration)层、Web层、AOP(Aspect Oriented Programming)模块、植入(Instrumentation)模块、消息传输(Messaging)、测试(Test)模块。
Spring体系结构如下图:
- 核心容器
Spring的核心容器是其他模块建立的基础,有Spring-core、Spring-beans、Spring-context、Spring-context-support和Spring-expression(String表达式语言)等模块组成。
Spring-core模块:提供了框架的基本组成部分,包括控制反转(Inversion of Control,IOC)和依赖注入(Dependency Injection,DI)功能。
Spring-beans模块:提供了BeanFactory,是工厂模式的一个经典实现,Spring将管理对象称为Bean。
Spring-context模块:建立在Core和Beans模块的基础之上,提供一个框架式的对象访问方式,是访问定义和配置的任何对象的媒介。ApplicationContext接口是Context模块的焦点。
Spring-context-support模块:支持整合第三方库到Spring应用程序上下文,特别是用于高速缓存(EhCache、JCache)和任务调度(CommonJ、Quartz)的支持。
Spring-expression模块:提供了强大的表达式语言去支持运行时查询和操作对象图。这是对JSP2.1规范中规定的统一表达式语言(Unified EL)的扩展。该语言支持设置和获取属性值、属性分配、方法调用、访问数组、集合和索引器的内容、逻辑和算术运算、变量命名以及从Spring的IOC容器中以名称检索对象。它还支持列表投影、选择以及常用的列表聚合。
-
AOP和Instrumentation
Spring-aop模块:提供了一个符合AOP要求的面向切面的编程实现,允许定义方法拦截器和切入点,将代码按照功能进行分离,以便干净地解耦。
Spring-aspects模块:提供了与AspectJ的集成功能,AspectJ是一个功能强大且成熟的AOP框架。
Spring-instrument模块:提供了类植入(Instrumentation)支持和类加载器的实现,可以在特定的应用服务器中使用。 -
消息
Spring4.0以后新增了消息(Spring-messaging)模块,该模块提供了对消息传递体系结构和协议的支持。
- 数据访问/集成
数据访问/集成层由JDBC、ORM、OXM、JMS和事务模块组成。
Spring-jdbc模块:提供了一个JDBC的抽象层,消除了烦琐的JDBC编码和数据库厂商特有的错误代码解析。
Spring-orm模块:为流行的对象关系映射(Object-Relational Mapping)API提供集成层,包括JPA和Hibernate。使用Spring-orm模块可以将这些O/R映射框架与Spring提供的所有其他功能结合使用,例如声明式事务管理功能。
Spring-oxm模块:提供了一个支持对象/XML映射的抽象层实现,例如JAXB、Castor、JiBX和XStream。
Spring-jms模块(Java Messaging Service):指Java消息传递服务,包含用于生产和使用消息的功能。自Spring4.1以后,提供了与Spring-messaging模块的集成。
Spring-tx模块(事务模块):支持用于实现特殊接口和所有POJO(普通Java对象)类的编程和声明式事务管理。
- Web
Web层由Spring-web、Spring-webmvc、Spring-websocket和Portlet模块组成。
Spring-web模块:提供了基本的Web开发集成功能,例如多文件上传功能、使用Servlet监听器初始化一个IOC容器以及Web应用上下文。
Spring-webmvc模块:也称为Web-Servlet模块,包含用于web应用程序的Spring MVC和REST Web Services实现。Spring MVC框架提供了领域模型代码和Web表单之间的清晰分离,并与Spring Framework的所有其他功能集成。
Spring-websocket模块:Spring4.0以后新增的模块,它提供了WebSocket和SocketJS的实现。
Portlet模块:类似于Servlet模块的功能,提供了Portlet环境下的MVC实现。
- 测试
Spring-test模块支持使用JUnit或TestNG对Spring组件进行单元测试和集成测试。
1.2 Spring 开发环境
1.2.1 环境准备
- JDK
- Tomcat
这里自行下载安装
1.2.2 创建 Spring 工程
-
可以创建maven项目,然后在线导入 Spring 的 jar 包。
前提是已安装配置好maven。 -
可以使用Idea直接创建Spring项目,这里的jar包自动下载好了。
1.3 使用IDEA开发 Spring 入门程序
- 使用IDEA创建 Spring 工厂
- 创建接口TestDao
Spring 解决的是业务逻辑层和其他层的耦合问题,因此它将面向接口的编程思想贯穿整个系统应用。
在 src 目录下创建demo1包,并创建接口 TestDao,在定义一个方法 sayHello方法,代码如下:
package demo1;
public interface TestDao {
public void sayHello();
}
- 创建接口的实现类 TestDaoImpl
在 demo1 包下创建 实现类 TestDaoImpl,代码如下:
package demo1;
public class TestDaoImpl implements TestDao {
@Override
public void sayHello() {
System.out.println("Hello,Spring");
}
}
- 创建配置文件 applicationContext.xml
在 src 目录下创建 Spring 的配置文件 applicationContext.xml,并在该文件使用实现类 TestDaoImpl 创建一个 id 为 testDaoImpl 的Bean,代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="testDaoImpl" class="demo1.TestDaoImpl"/>
</beans>
配置文件的名称可以自定义,配置文件信息不需要我们手写,可直接在 Spring 帮助文档复制。
- 创建测试类
在 src 目录下创建一个 Test 的包代码如下:
package demo1;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Test {
public static void main(String[] args) {
ApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
TestDaoImpl testDaoImpl = (TestDaoImpl) classPathXmlApplicationContext.getBean("testDaoImpl");
testDaoImpl.sayHello();
}
}
输出结果:
在上述main方法并没有使用new运算符创建TestDaoImpl实例,而是通过 Spring 容器获取实现类的对象,这就是 SpringIoC 的工作机制。
2 Spring IoC
2.1 Spring IoC 的基本概念
控制反转(Inversion of Control,IoC)是一个比较抽象的概念,是Spring框架的核心,用来消除计算机程序的耦合问题。依赖注入(Dependency Injection,DI)是 IoC 的另外一个说法,只是从不同的角度描述相同的概念。下面通过实际生活中的一个例子来解释 IoC 和 DI 。
当人们需要一件东西时,第一反应就是找东西,例如吃面包。在没有面包店和有面包店两种情况下,您会怎么做?在没有面包店时,最直观的做法可能是您按照自己的口味制作面包,也就是一个面包需要主动制作。然而时至今日,各种网点、实体店盛行,已经没有必要自己制作面包。想吃面包了,去网店或实体店把自己的口味告诉店家,一会就可以吃到面包了。注意,您并没有制作面包,而是由店家制作,但是完全符合您的胃口。
上面只是列举一个非常简单的例子,但包含了控制反转的思想,即把制作面包的主动权交给店家。下面通过面向对象编程思想继续探讨这两个概念。
当某个Java对象(调用者,例如您)需要调用另一个Java对象(被调用者,即被依赖对象,例如面包)时,在传统编程模式下,调用者通常会采用“new被调用者”的代码方式来创建对象(例如您自己制作面包)。这种方式会增加调用者与被调用者之间的耦合性,不利于后期代码的升级与维护。
当Spring框架出现后,对象的实例不再由调用者来创建,而是由Spring容器(例如面包店)来创建。Spring容器会负责控制程序之间的关系(例如面包店负责控制您与面包的关系),而不是由调用者的程序代码直接控制。这样,控制权由调用者转移到Spring 容器,控制权发生了反转,这就是Spring的控制反转。
从Spring容器角度来看,Spring 容器负责将被依赖对象赋值给调用者的成员变量,相当于为调用者注入它所依赖的实例,这就是Spring的依赖注入。
综上所述,控制反转是一种通过描述(在Spring中可以是XML或注解)并通过第三方去产生或获取特定对象的方式。在Spring 中实现控制反转的是IoC容器,其实现方法是依赖注入。
2.2 Spring IoC 容器
实现控制反转的时 Spring IoC 容器。Spring IoC 容器的设计主要基于BeanFactory 和 ApplicationContext 两个接口。
2.2.1 BeanFactory
BeanFactory由org.springframework. beans.factory.BeanFactory接口定义,它提供了完整的IoC服务支持,是一个管理Bean的工厂,主要负责初始化各种Bean。BeanFactory 接口有多个实现类,其中比较常用的是org.springframework .beans factory.xml.XmlBeanFactory,该类会根据XML配置文件中的定义来装配Bean(有关Bean的知识将在第3章讲解)。
在创建BeanFactory实例时需要提供XML文件的绝对路径。例如可以将第1章chl应用中main 方法的代码修改如下:
package demo1;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.core.io.FileSystemResource;
public class Test {
public static void main(String[] args) {
// ApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
// TestDaoImpl testDaoImpl = (TestDaoImpl) classPathXmlApplicationContext.getBean("testDaoImpl");
// testDaoImpl.sayHello();
BeanFactory beanFactory = new XmlBeanFactory(new FileSystemResource("applicationContext.xml的绝对路径"));
TestDaoImpl testDaoImpl = (TestDaoImpl) beanFactory.getBean("testDaoImpl");
testDaoImpl.sayHello();
}
}
使用BeanFactory实例加载配置文件在实际开发并不多见,我们只需要了解即可。
2.2.2 ApplicationContext
ApplicationContext是 BeanFactory 的子接口,也称应用上下文,由org.springframework.context.ApplicationContext接口定义。ApplicationContext接口包含 BeanFactory 的所有功能外,还添加了对国际化、资源访问、事件传播等内容的支持。
创建ApplicationContext接口实例通常有以下三种方法:
- 通过ClassPathXmlApplicationContext 创建
ClassPathXmlApplicationContext 将从类路径目录(src根目录)中寻找指定的XML配置文件。
测试代码:
package demo1;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Test {
public static void main(String[] args) {
ApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
TestDaoImpl testDaoImpl = (TestDaoImpl) classPathXmlApplicationContext.getBean("testDaoImpl");
testDaoImpl.sayHello();
}
}
这里用的是第一章的配置文件和TestDao,TestDaoImpl。
- 通过FileSystemXmlApplicationContext创建
FileSystemXmlApplicationContext将从配置文件的绝对路径中寻找XML配置文件,找到并装载完成 ApplicationContext 的实例化工作。
package demo1;
import org.springframework.context.support.FileSystemXmlApplicationContext;
public class Test {
public static void main(String[] args) {
FileSystemXmlApplicationContext fileSystemXmlApplicationContext = new FileSystemXmlApplicationContext("ApplicationContext.xml文件的绝对路径");
TestDaoImpl testDaoImpl = (TestDaoImpl) fileSystemXmlApplicationContext.getBean("testDaoImpl");
testDaoImpl.sayHello();
}
}
采用绝对路径的加载方式将导致程序的灵活性变差,一般不推荐使用。因此,通常在 Spring 的 java 应用中采取通过 ClassPathXmlApplicationContext 类来实例化 ApplicationContext 容器的实例化工作将交给Web服务器完成。
- 通过Web服务器实例化ApplicationContext 容器
在Web服务器实例化ApplicationContext 容器时,一般使用基于org.springframework.web.context.ContextLoaderListener 的实现方式(需要将spring-web-5.0.2.RELEASE.jar复制到WEB-INF/lib目录中),此方法只需要在web.xml中添加以下代码:
<context-param>
<!-- 加载src目录下的 applicationContext.xml文件-->
<param-name>contextConfigLocation</param-name>
<param-value>
classpath:applicationContext.xml
</param-value>
</context-param>
<!-- 指定以 ContextLoaderListener 方式启动 Spring 容器 -->
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
2.3 依赖注入的类型
在 Spring 中实现 IoC 容器的方法是依赖注入,依赖注入的作用是在使用 Spring 框架创建对象时动态地将其所依赖的对象(例如属性值)注入 Bean 组件中。Spring 框架的依赖注入通常有两种实现方法,一种是使用构造方法注入,另一种是使用属性的 setter 方法注入。
2.3.1 使用构造方法注入
Spring 框架可以采用java的反射机制,通过构造方法完成依赖注入。下面开始代码演示:
我们在demo2中演示。
目的:在service 中使用构造方法依赖注入 TestDIDao 接口对象。
- 创建 dao 包
TestDIDao 接口代码如下:
package demo2.dao;
public interface TestDIDao {
public void sayHello();
}
TestDIDaoImpl 实现类的代码如下:
package demo2.dao;
public class TestDIDaoImpl implements TestDIDao{
@Override
public void sayHello() {
System.out.println("TestDIDao say: hello,Spring!!");
}
}
- 创建 service 包
TestDIService 接口代码如下:
package demo2.service;
public interface TestDIService {
public void sayHello();
}
TestDIServiceImpl 实现类的代码如下:
package demo2.service;
import demo2.dao.TestDIDao;
public class TestDIServiceImpl implements TestDIService {
private TestDIDao testDIDao;
// 构造方法,用于实现依赖注入接口testDIDao
public TestDIServiceImpl(TestDIDao testDIDao){
super();
this.testDIDao = testDIDao;
}
@Override
public void sayHello() {
testDIDao.sayHello();
System.out.println("TestDIService 构造方法注入say:,hello Spring");
}
}
- 编写配置文件
在demo2包下创建applicationContext.xml文件。在配置文件中首先将TestDIDaoImpl 类托管给 Spring,让Spring 创建其对象,然后service.TestDIServiceImpl 类托管给 Spring,让 Spring 创建其对象,同时给构造方法传递实参。配置文件具体代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="testDIDaoImpl" class="demo2.dao.TestDIDaoImpl"/>
<bean id="testServiceImpl" class="demo2.service.TestDIServiceImpl">
// index = 0 表示 第一个参数为 引用的 testDIDaoImpl
<constructor-arg index="0" ref="testDIDaoImpl"/>
</bean>
</beans>
在配置文件中,constructor-arg 元素用于定义构造方法的参数,index 用于定义参数的位置,ref 指定某个实例的引用,如果参数时常量值,ref由value代替。
- 创建 Test 类
package demo2;
import demo2.service.TestDIService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Test {
public static void main(String[] args) {
ApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("demo2/applicationContext.xml");
TestDIService testServiceImpl = (TestDIService) classPathXmlApplicationContext.getBean("testServiceImpl");
testServiceImpl.sayHello();
}
}
输出结果:
2.3.2 使用属性的 setter 方法注入
使用 setter 方法注入是 Spring 框架中最主流的注入方式,它利用 Java Bean 规范所定义的 setter 方法完成注入,灵活且可读性高。对于 setter 方法注入,Spring 框架也是使用了 Java 的反射机制实现的。下面代码讲解如何使用 setter 方法注入:
- 创建接口实现类 TestDIServiceImpl2
在 TestDIServiceImpl2 中使用属性的 setter 方法依赖注入 TestDIDao 接口对象,具体代码如下:
package demo2.service;
import demo2.dao.TestDIDao;
public class TestDIServiceImpl2 implements TestDIService {
private TestDIDao testDIDao;
// 添加 testDIDao 属性的 setter 方法,用于实现依赖注入
public void setTestDIDao(TestDIDao testDIDao) {
this.testDIDao = testDIDao;
}
@Override
public void sayHello() {
testDIDao.sayHello();
System.out.println("TestDIServiceImpl2 : setter 依赖注入");
}
}
-
将 TestDIServiceImpl2 类托管给 Spring
将 TestDIServiceImpl2 类托管给 Spring,让 Spring 创建其对象,同时调用 TestDIServiceImpl2 类的 setter 方法完成依赖注入。ApplicationContext.xml配置文件代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="testDaoImpl" class="demo2.dao.TestDIDaoImpl"/>
<bean id="testServiceImpl" class="demo2.service.TestDIServiceImpl">
<constructor-arg index="0" ref="testDaoImpl"/>
</bean>
<bean id="testServiceImpl2" class="demo2.service.TestDIServiceImpl2">
<property name="testDIDao" ref="testDaoImpl"/>
</bean>
</beans>
- 在测试类 Test 中测试:
package demo2;
import demo2.service.TestDIService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Test {
public static void main(String[] args) {
ApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("demo2/applicationContext.xml");
TestDIService testServiceImpl2 = (TestDIService) classPathXmlApplicationContext.getBean("testServiceImpl2");
testServiceImpl2.sayHello();
}
}
输出结果:
3 Spring Bean
3.1 Bean 的配置
Spring 可以看作一个大型工厂,用于生产和管理 Spring 容器中的 Bean。如果要使用这个工厂生产和管理 Bean,需要开发者将 Bean 配置在 Spring 的配置文件中。Spring 框架支持 XML 和Properties 两种格式的配置文件,在实际开发中常用 XML 格式的配置文件。
从前面的学习得知 XML 配置文件的根元素是< beans>,
< beans>中包含多个< bean>子元素,每个< bean>元素定义一个 Bean,并描述 Bean 如何被装配到 Spring 中。< bean>元素的常用属性及其子元素如下图:
属性或子元素名称 | 描 述 |
---|---|
id | Bean 在 BeanFactory中唯一的标识,在代码中通过 BeanFactory 获取 Bean 实例时需要以此作为索引名称 |
class | Bean 的具体实现类,使用类的名(例如:demo1.TestDIDaoImpl) |
scope | 指定 Bean 实例的作用域,具体在后面讲解 |
constructor-arg | < bean>元素的子元素,使用构造方法注入,指定构造方法的参数。该元素的 index属性指定参数的序号,ref 属性指定对 BeanFactory 中其他 Bean 的引用关系,type 属性指定参数的类型,value 属性指定参数的常量值 |
property | < bean>元素的子元素,用于设置一个属性。该元素的name属性指定Bean 实例中相应的属性名称,value 属性指定 Bean 的属性值,ref 属性指定属性对BeanFactory 中其他 Bean 的引用关系 |
list | property元素的子元素,用于封装 List 或数组类型的依赖注入,具体后面介绍(3.5) |
map | property元素的子元素,用于封装 Map 类型的依赖注入,具体后面介绍(3.5) |
set | property元素的子元素,用于封装 Set 类型的依赖注入,具体后面介绍(3.5) |
entry | map元素的子元素,用于设置一个键值对,具体后面介绍(3.5) |
Bean 的配置示例代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="testDaoImpl" class="demo2.dao.TestDIDaoImpl"/>
<bean id="testServiceImpl" class="demo2.service.TestDIServiceImpl">
<constructor-arg index="0" ref="testDaoImpl"/>
</bean>
<bean id="testServiceImpl2" class="demo2.service.TestDIServiceImpl2">
<property name="testDIDao" ref="testDaoImpl"/>
</bean>
</beans>
3.2 Bean 的实例化
在面向对象编程中,如果想使用某个对象,需要实现实例化该对象。同样,在 Spring 框架中,如果像使用 Spring 容器中的 Bean,也需要实例化 Bean。Spring 框架实例化 Bean 有3种方式,即构造方法实例化、静态工厂实例化和实例工厂实例化(其中,最常用的是构造方法实例化)。
3.2.1 构造方法实例化
在 Spring 框架中,Spring 容器可以调用 Bean 对应类中的无参构造方法来实例化 Bean,这种方法称为无参构造方法实例化。下面代码演示过程:
- 创建demo3包
- 创建 BeanClass 类
在demo3包中创建 BeanClass 类,代码如下:
package demo3;
public class BeanClass {
public String messages;
// 无参构造
public BeanClass(){
messages = "构造方法实例化Bean";
}
// 有参构造
public BeanClass(String s){
messages = s;
}
}
- 创建配置文件
在demo3包下创建 applicationContext.xml 文件,在配置文件中定义一个 id为 constructorInstance 的 Bean,代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="constructorInstance" class="demo3.BeanClass"/>
</beans>
- 创建测试类
在demo3包下创建Test类并进行测试,代码如下:
package demo3;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Test {
public static void main(String[] args) {
ApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("demo3/applicationContext.xml");
BeanClass constructorInstance = (BeanClass) classPathXmlApplicationContext.getBean("constructorInstance");
System.out.println(constructorInstance + constructorInstance.messages);
}
}
输出结果:
我们可以发现它走的是无参构造方法进行实例化。
3.2.2 静态工厂实例化
在使用静态工厂实例化 Bean 时要求开发者在工厂类中创建一个静态方法来创建 Bean 的实例。在配置 Bean 时,class 属性指定静态工厂类,同时还需要使用 factory-method 属性指定工厂类中的静态方法。下面通过代码演示:
- 创建工厂类 BeanStaticFactory
在 demo3包中创建 BeanStaticFactory,该类中有一个静态方法来实例化对象,具体代码如下:
package demo3;
public class BeanStaticFactory {
private static BeanClass beanInstance = new BeanClass("调用静态工厂方法");
public static BeanClass createInstance(){
return beanInstance;
}
}
- 编辑demo3包下的配置文件 applicationContext.xml 代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="constructorInstance" class="demo3.BeanClass"/>
<!-- 静态工厂方法实例化 Bean,createInstance 为静态工厂类 BeanStaticFactory 中的静态方法 -->
<bean id="staticFactoryInstance" class="demo3.BeanStaticFactory" factory-method="createInstance"/>
</beans>
- 编写测试代码:
package demo3;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Test {
public static void main(String[] args) {
ApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("demo3/applicationContext.xml");
BeanClass staticFactoryInstance = (BeanClass) classPathXmlApplicationContext.getBean("staticFactoryInstance");
System.out.println(staticFactoryInstance + staticFactoryInstance.messages);
}
}
输出结果如下:
3.2.3 实例工厂实例化
在使用实例工厂实例化 Bean 时要求开发者在工厂类中创建一个实例方法来创建 Bean 的实例。在配置 Bean 时需要使用 factory-bean 属性指定配置的实例工厂,同时还需要使用 factory-method 属性指定实例工厂中的实例方法。下面通过代码讲解:
- 创建工厂类 BeanInstanceFactory
在demo3包中创建工厂类 BeanInstanceFactory,该类中有一个实例方法来实例对象,代码如下:
package demo3;
public class BeanInstanceFactory {
public BeanClass createInstanceFactory(){
return new BeanClass("调用实例工厂方法实例化 Bean");
}
}
- 编辑demo3包下的配置文件 applicationContext.xml 代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 配置工厂 -->
<bean id="myFactory" class="demo3.BeanInstanceFactory"/>
<!-- 使用 factory-bean 属性指定配置工厂,使用 factory-method 属性指定使用工厂中的哪一个方法实例化 Bean -->
<bean id="instanceFactoryInstance" factory-bean="myFactory" factory-method="createInstanceFactory"/>
</beans>
- 编写测试代码:
package demo3;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Test {
public static void main(String[] args) {
ApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("demo3/applicationContext.xml");
BeanClass instanceFactoryInstance = (BeanClass) classPathXmlApplicationContext.getBean("instanceFactoryInstance");
System.out.println(instanceFactoryInstance+instanceFactoryInstance.messages);
}
}
输出结果如下:
3.3 Bean 的作用域
在 Spring 中不仅可以完成 Bean 的实例化,还可以为 Bean 指定作用域。在 Spring 5.0 中为 Bean 的实例定义了如下所示的作用域
作用域名称 | 描述 |
---|---|
singleton | 默认的作用域,使用 singleton 定义的 Bean 在 Spring 容器中只有一个 Bean实例 |
prototype | Spring 容器每次获取prototype 定义的 Bean,容器都将创建一个新的 Bean 实例 |
request | 在一个 HTTP 请求中容器将返回一个 Bean实例,不同的 HTTP 请求返回不同的 Bean实例。仅在 Web Spring 应用程序上下文中使用 |
session | 在一个 HTTP Session中,容器将返回同一个 Bean 实例。仅在 Web Spring 应用程序上下文中使用 |
application | 为每个 ServletContext 对象创建一个实例,即同一个应用共享一个 Bean 实例。仅在 Web Spring 应用程序上下文中使用 |
websocket | 为每个 WebSocket 对象创建一个 Bean 实例。仅在 Web Spring 应用程序上下文中使用 |
3.3.1 singleton作用域
当将 bean 的 scope 设置为 singleton 时,Spring IoC 容器仅生成和管理一个 Bean 实例。在使用 id 或 name 获取 Bean 实例时,IoC 容器将返回共享的 Bean 实例。
由于 singleon 时 scope 的默认方式,因此:
<bean id="constructorInstance" class="demo3.BeanClass"/>
和
<bean id="constructorInstance" class="demo3.BeanClass" scope="singleton"/>
是等价的。
测试 singleton 的作用域,代码如下:
package demo3;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Test {
public static void main(String[] args) {
ApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("demo3/applicationContext.xml");
BeanClass constructorInstance = (BeanClass) classPathXmlApplicationContext.getBean("constructorInstance");
BeanClass constructorInstance2 = (BeanClass) classPathXmlApplicationContext.getBean("constructorInstance");
System.out.println(constructorInstance);
System.out.println(constructorInstance2);
}
}
输出结果如下:
由上图我们可以得知,在使用 id 和 name 获得 Bean 实例时,IoC 容器仅返回一个 Bean 实例。
3.3.2 prototype作用域
当将 Bean 的 scope 设置为 prototype 时,Spring IoC 容器将为每次请求创建一个新的实例。将3.3.1中applicationContext.xml配置文件修改如下:
<bean id="constructorInstance" class="demo3.BeanClass" scope="prototype"/>
测试代码不变,输出结果如下:
从上图运行结果得知,在使用 id 或 name 两次获取 Bean 实例时,IoC 容器将返回两个不同的 Bean 实例。
3.4 Bean 的生命周期
一个对象的生命周期包括创建(实例化与初始化)、使用以及销毁等阶段,在 Spring 中,Bean 对象周期也遵循这一过程, 但是 Spring 提供了许多对外接口,允许开发者对3个过程(实例化、初始化、销毁)的前后做一些操作。 在 Spring Bean 中,实例化是为 Bean 对象开辟空间,初始化则是对属性的初始化。
Spring 容器可以管理 singleton 作用域 Bean 的生命周期,在此作用域下,Spring 能够精确地知道 Bean 何时被创建,何时初始化完成,以及何时被销毁。而对于 prototype 作用域的 Bean , Spring 只负责创建,当容器创建了 Bean 的实例后,Bean 实例就交给了客户端的代码管理,Spring 容器将不再跟踪其生命周期,并且不会管理那些被配置成 prototype 作用域的 Bean 。Spring 中 Bean 的生命周期的执行是一个很复杂的过程,可借鉴 Servlet 的生命周期“实例化初始化 (init)–>接收请求 (service)–>销毁 (destroy)”来理解 Bean 的生命周期。
Bean 的生命周期的整个过程如下:
(1)根据 Bean 的配置情况实例化一个 Bean。
(2)根据 Spring 上下文对实例化的 Bean 进行依赖注入,即对 Bean 的属性进行初始化。
(3)如果 Bean 实现了 BeanNameAware 接口,将调用它实现的setBeanName(Stringbeanld) 方法,此处参数传递的是 Spring 配置文件中 Bean 的 id。
(4)如果 Bean 实现了 BeanFactoryAware 接口,将调用它实现的setBeanFactory 方法,此处参数传递的是当前 Spring 工厂实例的引用。
(5)如果 Bean 实现了 ApplicationContextAware 接口,将调用它实现的 setApplicationContext(applicationContext)方法,此处参数传递的是 Spring 上下文实例的引用。
(6)如果 Bean 关联了 BeanPostProcessor 接口,将调用初始化方法postProcessBeforeInitialization(Object obj, String s)对 Bean 进行操作。
(7)如果 Bean实现了 InitializingBean 接口,将调用 afterPropertiesSet方法。
(8)如果 Bean在Spring 配置文件中配置了 init-method 属性,将自动调用其配置的初始化方法。
(9)如果 Bean 关联了 BeanPostProcessor 接口,将调用postProcessAfterlnitializatior(Object obj, String s) 方法,由于是在 Bean初始化结束时调用After方法,也可用于内存或缓存技术。
注意:以上工作完成后就可以使用该 Bean ,由于该Bean的作用域是singleton,所以调用的是同一个 Bean 实例。
(10)当 Bean 不再需要时将进入销毁阶段,如果 Bean 实现了DiposableBaen 接口,则调用其实现的destroy方法将 Spring 中的 Bean销毁。
(11)如果配置文件中通过 destroy-method 属性指定了 Bean 的销毁方法,将调用其配置的销毁方法进行销毁。
在 Spring 中,通过实现特定的接口或通过< bean> 元素设置可以对 Bean 的生命周期过程产生影响。开发者可以随意地配置< bean> 元素的属性,但不建议过多地使用 Bean 实现接口,因为这样将使代码和 Spring 聚合比较紧密。下面通过实例演示 Bean 的生命周期。
- 创建 Bean 的实现类
在 src 下创建一个 demo4 的包,在此包下创建 BeanLife 类。在 BeanLife类中有两个方法,一个演示初始化过程,一个演示销毁过程。具体代码如下:
package demo4;
public class BeanLife {
public void initMyself(){
System.out.println(this.getClass().getName()+" 执行自定义的初始化方法");
}
public void destroyMyself(){
System.out.println(this.getClass().getName()+" 执行自定义的销毁方法");
}
}
- 配置 Bean
在demo4 中创建 ApplicationContext.xml 配置文件,代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 配置 bean,使用 init-method 属性指定初始化方法,使用 destroy-method 属性指定销毁方法 -->
<bean id="BeanLife" class="demo4.BeanLife" init-method="initMyself" destroy-method="destroyMyself"/>
</beans>
- 测试生命周期
在 demo4 中创建 Test 测试类,代码如下:
package demo4;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Test {
public static void main(String[] args) {
// 为了方便演示销毁方法的执行,这里使用 ClassPathXmlApplicationContext
ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("demo4/applicationContext.xml");
System.out.println("获取对象前");
BeanLife beanLife = (BeanLife) classPathXmlApplicationContext.getBean("BeanLife");
System.out.println("获取对象后"+beanLife);
classPathXmlApplicationContext.close();
}
}
输出结果:
从图中我们可以看出,在加载配置文件时执行了 Bean 的初始化方法 initMyself;在获得对象后,关闭容器时,执行了 Bean 的销毁方法 destroyMyself。
3.5 Bean 的装配方式
Bean 的装配可以理解为将 Bean 依赖注入到 Spring 容器中,Bean 的装配方式即 Bean 依赖注入的方式。Spring 容器支持基于 XML 配置的装配、基于注解的装配以及自动装配等多种装配方式,其中最受亲睐的装配方式是基于注解的装配。本章主要讲解基于 XML 配置的装配和基于注解的装配。
3.5.1 基于 XML 配置的装配
基于 XML 配置的装配方式已经有很久的历史了,曾经是主要的装配方式。通过 2.3 节的学习,我们知道 Spring 提供了两种基于XML 配置的装配方式,即使用构造方法注入和使用属性的setter方法注入。
在使用构造方法注入方式装配 Bean时,Bean 的实现类需要提供带参数的构造方法,并在配置文件中使用< bean>元素的子元素< constructor-arg>来定义构造方法的参数;在使用属性的setter方法注入方式装配 Bean时,Bean 的实现类需要提供一个默认无参数的构造方法,并为需要注入的属性提供对应的 setter 方法,另外还需要使用< bean>元素的子元素< property>为每个属性注入值。
下面通过一个实例来演示基于XML配置的装配方式。
- 创建 Bean 的实现类
在 src 目录下创建 assemble 包,在此包下创建 ComplexUser 类。在ComplexUser 类中分别使用构造方法和使用属性的 setter 方法注入。具体代码如下:
package assemble;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class ComplexUser {
private String name;
private List<String> hobbyList;
private Map<String,String> residenceMap;
private Set<String> aliasSet;
private String[] array;
// 无参构造,setter 方法注入时需要用到。
public ComplexUser() {
super();
}
// 有参构造,构造器方法注入时需要用到。
public ComplexUser(String name, List<String> hobbyList, Map<String, String> residenceMap, Set<String> aliasSet, String[] array) {
this.name = name;
this.hobbyList = hobbyList;
this.residenceMap = residenceMap;
this.aliasSet = aliasSet;
this.array = array;
}
/** * 属性的 set 方法 */
public void setName(String name) {
this.name = name;
}
public void setHobbyList(List<String> hobbyList) {
this.hobbyList = hobbyList;
}
public void setResidenceMap(Map<String, String> residenceMap) {
this.residenceMap = residenceMap;
}
public void setAliasSet(Set<String> aliasSet) {
this.aliasSet = aliasSet;
}
public void setArray(String[] array) {
this.array = array;
}
@Override
public String toString() {
return "ComplexUser{" +
"name='" + name + '\'' +
", hobbyList=" + hobbyList +
", residenceMap=" + residenceMap +
", aliasSet=" + aliasSet +
", array=" + Arrays.toString(array) +
'}';
}
}
- 配置 Bean
在 Spring 配置文件中使用 ComplexUser 配置 Bean 的两个实例,具体代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 使用构造方法注入 -->
<bean id="user1" class="assemble.ComplexUser">
<constructor-arg index="0" value="wdf"/>
<constructor-arg index="1">
<list>
<value>唱歌</value>
<value>爬山</value>
<value>玩游戏</value>
</list>
</constructor-arg>
<constructor-arg index="2">
<map>
<entry key="dalian" value="大连"/>
<entry key="beijing" value="北京"/>
<entry key="shanghai" value="上海"/>
</map>
</constructor-arg>
<constructor-arg index="3">
<set>
<value>wdf100</value>
<value>wdf101</value>
<value>wdf102</value>
</set>
</constructor-arg>
<constructor-arg index="4">
<array>
<value>aaa</value>
<value>bbb</value>
</array>
</constructor-arg>
</bean>
<!-- 使用 setter 方法注入 -->
<bean id="user2" class="assemble.ComplexUser">
<property name="name" value="wdf2"/>
<property name="hobbyList">
<list>
<value>看书</value>
<value>学习</value>
</list>
</property>
<property name="residenceMap">
<map>
<entry key="shenzhen" value="深圳"/>
<entry key="tianjin" value="天津"/>
</map>
</property>
<property name="aliasSet">
<set>
<value>wdf104</value>
<value>wdf105</value>
</set>
</property>
<property name="array">
<array>
<value>cccc</value>
<value>dddd</value>
</array>
</property>
</bean>
</beans>
- 测试基于 XML 配置的装配方式
具体代码如下:
package assemble;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Test {
public static void main(String[] args) {
ApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("assemble/applicationContext.xml");
// 使用构造方法装配
ComplexUser user1 = (ComplexUser) classPathXmlApplicationContext.getBean("user1");
// 使用 setter 方法装配
ComplexUser user2 = (ComplexUser) classPathXmlApplicationContext.getBean("user2");
System.out.println(user1);
System.out.println(user2);
}
}
输出结果如下:
3.5.2 基于注解的装配
在 Spring 框架中,尽管使用 XML 配置文件可以简单地装配 Bean,但如果应用中有大量的 Bean 需要装配,会导致 XML 配置文件过于庞大,不方便以后的升级与维护,因此更多的时候推荐开发者使用注解(annotation)的方式去装配 Bean。
在 Spring 框架中定义了一系列的注解,下面介绍几种常用的注解。
一 、@Component
该注解是一个泛化的概念,仅仅表示一个组件对象(Bean),可以作用在任何层次上。下面通过一个实例讲解@Component。
- 创建 Bean的实现类
在 src 目录下创建 annotation 包,在该包下创建 Bean 的实现类 AnnotationUser,代码如下:
package annotation;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component()
/** * 相当于把 id 为 annotationUser 的 Bean 注册到 Spring 容器中。 */
public class AnnotationUser {
@Value("wdf") // 只能注入简单的值,对于复杂的值目前使用该方法还解决不了
private String name;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
- 配置注解
现在有了 Bean 的实现类,但不能进行测试,因为 Spring 容器并不知道去哪里扫描 Bean 对象,需要在配置文件中配置注解,在annotation包下创建applicationContext.xml 具体代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<!-- 扫描annotation包下的注解 -->
<context:component-scan base-package="annotation"/>
</beans>
- 测试 Bean 实例
测试代码如下:
package annotation;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Test {
public static void main(String[] args) {
ApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("annotation/applicationContext.xml");
AnnotationUser annotationUser = (AnnotationUser) classPathXmlApplicationContext.getBean("annotationUser");
System.out.println(annotationUser.getName());
}
}
输出结果:
注:在 Spring 4.0以上的版本,配置注解指定包中的注解进行扫描前需要实现导入 Spring AOP 的 JAR 包 spring-aop-5.0.2.RELEASE.jar
二、@Repository
该注解用于将数据访问层(DAO)的类表示为 Bean,即注解数据访问层 Bean,其功能与@Component 相同。
三、@Service
该注解用于标注一个业务逻辑组件类(Service层),其功能与@Component 相同。
四、@Controller
该注解标注一个控制器组件类(Spring MVC 的 Controller),其功能与@Component 相同。
五、@Autowired
该注解可以对类成员变量、方法及构造方法进行标注,完成自动装配的工作。通过使用@Autowired 来消除 setter 和 getter 方法。默认按照 Bean 的类型进行装配。
六、@Resource
该注解与@Autowired 的功能一样,区别在于该注解默认是按照名称来装配注入的,只有当找不到名称匹配的 Bean 时才会按照类型来装配注入;而@Autowired 默认按照 Bean 的类型进行装配,如果想按照名称来装配注入,则需要和@Qualifier 注解一起使用
@Resource 注解有两个属性,name和type。name 属性指定 Bean 实例名称,即按照名称来装配注入;type 属性指定 Bean 的类型,即按照 Bean 的类型进行装配。
七、@Qualifier
该注解与 @Autowired 注解配合使用。当@Autowired 注解需要按照名称来装配注入时需要和该注解一起使用,Bean 的实例名称由@Qualifier 注解的参数指定。
在上面几个注解中,虽然@Repository、@Service和@Controller 等注解的功能与@Component 注解相同,但为了使类的标注更加清晰(层次化),在实际开发中推荐使用@Repository 标注数据访问层(DAO层)、使用@Service 标注业务逻辑层(Service 层)、使用@Controller 标注控制器层(控制层)。
下面通过一个实例讲解如何使用这些注解:
- 创建DAO层
在 src 中创建 annotation.dao 包,在该包下创建 TestDao 接口和 TestDaoImpl实现类,并将实现类 TestDaoImpl 使用 @Repository 注解标注为数据访问层。
TestDao 的代码如下:
package annotation.dao;
public interface TestDao {
public void save();
}
TestDaoImpl 的代码如下:
package annotation.dao;
import org.springframework.stereotype.Repository;
@Repository("testDaoImpl")
// 相当于@Repository,但如果在 service 层使用@Resource(name=”testDaoImpl“), testDaoImpl 不能省略
public class TestDaoImpl implements TestDao {
@Override
public void save() {
System.out.println("testDao save");
}
}
- 创建 Service 层
在 src 中创建 annotation.service 包,在该包下创建 TestService 接口和 TestServiceImpl 实现类,并将实现类 TestServiceImpl 使用@Service 注解标注为业务逻辑层。
TestService 的代码如下:
package annotation.service;
public interface TestService {
public void save();
}
TestServiceImpl 的代码如下:
package annotation.service;
import annotation.dao.TestDao;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service("testServiceImpl")
// 相当于@Service
public class TestServiceImpl implements TestService {
@Resource(name = "testDaoImpl")
private TestDao testDao;
@Override
public void save() {
testDao.save();
System.out.println("testService save");
}
}
- 创建 Controller 层:
在 src 中创建 annotation.controller 包,在该包下创建 TestController 类,并将 TestController 类使用@Controller 注解标注为控制层。
TestController 的代码如下:
package annotation.controller;
import annotation.service.TestService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
@Controller
public class TestController {
@Autowired
private TestService testService;
public void save(){
testService.save();
System.out.println("testController save");
}
}
- 配置注解
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<!-- 扫描annotation包下的注解 -->
<context:component-scan base-package="annotation"/>
</beans>
- 测试
package annotation.service;
import annotation.controller.TestController;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class test {
public static void main(String[] args) {
ApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("annotation/applicationContext.xml");
TestController testController = (TestController) classPathXmlApplicationContext.getBean("testController");
testController.save();
}
}
输出结果:
4 Spring AOP
Spring AOP 是 Spring 框架体系结构中非常重要的功能模块之一,该模块提供了面向切面编程实现。面向切面编程在事物处理、日志记录、安全控制等操作中被广泛使用。本章将对 Spring AOP 的相关概念及实现进行详细讲解。
4.1 Spring AOP 的基本概念
4.1.1 AOP 的概念
AOP (Aspect-Oriented Programming)即面向切面编程,它与OOP( Object-OrientedProgramming,面向对象编程)相辅相成,提供了与 OOP 不同的抽象软件结构的视角。在 OOP 中,以类作为程序的基本单元,而 AOP 中的基本单元是Aspect(切面)。Struts2 的拦截器设计就是基于 AOP 的思想,是个比较经典的应用。
在业务处理代码中通常有日志记录、性能统计、安全控制、事务处理、异常处理等操作。尽管使用 OOP 可以通过封装或继承的方式达到代码的重用,但仍然有同样的代码分散在各个方法中。因此,采用 OOP 处理日志记录等操作不仅增加了开发者的工作量,而且提高了升级维护的困难。为了解决此类问题,AOP 思想应运而生。AOP 采取横向抽取机制,即将分散在各个方法中的重复代码提取出来,然后在程序编译或运行阶段将这些抽取出来的代码应用到需要执行的地方。这种横向抽取机制采用传统的 OOP 是无法办到的,因为 OOP 实现的是父子关系的纵向重用。但是 AOP 不是 OOP 的替代品,而是 OOP 的补充,它们相辅相成。
在 AOP 中,横向抽取机制的类与切面的关系:
4.1.2 AOP 的术语
在 Spring AOP 框架中涉及以下常用术语。
-
切面
切面(Aspect)是指封装横切到系统功能(例如事物处理)的类。 -
连接点
连接点(Joinpoint)是指程序运行中的一些时间点,例如方法的调用或异常的抛出。 -
切入点
切入点(Pointcut)是指需要处理的连接点。在 Spring AOP 中,所有的方法执行都是连接点,而切入点是一个描述信息,他修饰的是连接点,通过切入点确定哪些连接点需要被处理。切面、连接点和切入点的关系如下:
-
通知
通知(Advice)是由切面添加到特定的连接点(满足切入点规则)的一段代码,即在定义好的切入点处所要执行的程序代码,可以将其理解为切面开启后切面的方法,因此通知是切面的具体实现。 -
引入
引入(Introduction)允许在现有的实现类中添加自定义的方法和属性。 -
目标对象
目标对象(Target Object)是指所有被通知的对象。如果 AOP 框架使用运行时代理的方式(动态的 AOP)来实现切面,那么通知对象总是一个代理对象。 -
代理
代理(Proxy)是通知应用到目标对象之后被动态创建的对象。 -
织入
织入(Weaving)是将切面代码插入到目标对象上,从而生成代理对象的过程。根据不同的实现技术,AOP 织入有 3 中方式;编译器织入,需要由特殊的 Java 编译器;类装载期织入,需要有特殊的类装载器;动态代理织入,在运行期为目标类添加通过生成子类的方式。Spring AOP 框架默认采用动态代理织入,而 AspectJ(基于 Java 语言的 AOP框架)采用编译期织入和类装载期织入。
4.2 动态代理
在 Java 中有很多动态代理技术,例如 JDK、CGLIB、Javassist、ASM,其中最常用的动态代理技术是 JDK 和 CGLIB。目前,在 Spring AOP 中常用 JDK 和 CGLIB 两种动态代理技术。
4.2.1 JDK 动态代理
JDK 动态代理是 java.lang.reflect.*包提供的方式,它必须借助一个接口才能产生代理对象,因此,对于使用业务接口的类,Spring 默认使用 JDK 动态代理实现 AOP。下面通过一个实例演示如何使用 JDK 动态代理实现 Spring AOP,具体步骤如下:
注:这里我们使用maven工程,其好处是导入jar包时十分方便。
- 安装 maven
- 配置 maven
- 使用 Idea 创建 maven工程
一直点下一步就可以。
在pom 文件添加相关依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.feng</groupId>
<artifactId>Spring2</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.3.RELEASE</version>
</dependency>
</dependencies>
</project>
把 src 目录删掉,我们在其中创建 Module。
创建 JDK module,子目录中的 pom 会继承父目录的 pom。
- 我们在 JDK module 的 src 目录下创建 dynamic.jdk 包,在该包创建接口 TestDao 和接口实现类 TestDaoImpl。该实现类作为目标类,在代理类中对其方法进行增强处理。
TestDao 的代码如下:
package dynamic.jdk;
public interface TestDao {
public void save();
public void modify();
public void delete();
}
TestDaoImpl 的代码如下:
package dynamic.jdk;
public class TestDaoImpl implements TestDao {
public void save() {
System.out.println("保存");
}
public void modify() {
System.out.println("修改");
}
public void delete() {
System.out.println("删除");
}
}
- 创建切面类
在 JDK module 的 dynamic.jdk 目录下创建切面类 MyAspect,注意在该类中可以定义多个通知(增强处理的功能方法)
MyAspect 的代码如下:
package dynamic.jdk;
public class MyAspect {
public void check(){
System.out.println("模拟权限控制");
}
public void except(){
System.out.println("模拟异常处理");
}
public void log(){
System.out.println("模拟日志");
}
}
- 创建代理类
在 dynamic.jdk 包中创建代理类 JDKDynamicProxy。在 JDK 动态代理类必须实现 java.lang.reflect.InvocationHandler 接口,并编写代理方法,在代理方法中需要通过 Proxy 实现动态代理。
JDKDynamicProxy 的代码如下:
package dynamic.jdk;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class JDKDynamicProxy implements InvocationHandler {
// 声明目标类接口对象(真实对象)
private TestDao testDao;
// 创建代理的方法,建立代理对象和真实对象的代理关系,并返回代理对象
public Object createProxy(TestDao testDao){
this.testDao = testDao;
// 类加载器
ClassLoader cld = JDKDynamicProxy.class.getClassLoader();
// 被代理对象实现的所有接口
Class[] classes = testDao.getClass().getInterfaces();
// 使用代理类进行增强,返回代理后的对象
return Proxy.newProxyInstance(cld,classes,this);
}
/** * 代理的逻辑对象 * @param proxy 被代理的对象 * @param method 将要执行的方法 * @param args 执行方法时需要的参数 * @return 返回代理结果 * @throws Throwable */
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 创建一个切面
MyAspect myAspect = new MyAspect();
// 前增强
myAspect.check();
myAspect.except();
// 在目标类上调用方法并传入参数,相当于调用 testDao中的方法
Object obj = method.invoke(testDao,args);
// 后增强
myAspect.log();
return obj;
}
}
- 创建测试类
在test.java 包下创建Test类进行测试:
import dynamic.jdk.JDKDynamicProxy;
import dynamic.jdk.TestDao;
import dynamic.jdk.TestDaoImpl;
public class Test {
public static void main(String[] args) {
// 创建代理对象
JDKDynamicProxy jdkDynamicProxy = new JDKDynamicProxy();
// 创建目标对象
TestDao testDao = new TestDaoImpl();
// 从代理对象中获取增强后的目标对象,该对象是一个被代理的对象,它会进入代理的逻辑方法 invoke中
TestDao testDaoAdvice = (TestDao) jdkDynamicProxy.createProxy(testDao);
// 执行方法
testDaoAdvice.save();
System.out.println("-----------");
testDaoAdvice.modify();
System.out.println("-----------");
testDaoAdvice.delete();
System.out.println("-----------");
}
}
运行结果如下:
4.2.2 CGLIB
从 4.2.1 节可知,JDK 动态代理必须提供接口才能使用,对于没有提供接口的类,只能采用 CGLIB 动态代理。
CGLIB(Code Generation Library)是一个高性能开源的代码生成包,采用非常底层的字节码技术,对指定的目标生成一个子类,并对子类进行增强。在 Spring Core 包中已经集成了 CGLIB 所需要的 jar 包。下面通过一个实例演示:
-
创建 CGLIB module
-
创建目标类
在 CGLIB module 下创建 dynamic.cglib包,在该包下创建 TestDao。
TestDao 的代码如下:
package dynamic.cglib;
public class TestDao {
public void save() {
System.out.println("保存");
}
public void modify() {
System.out.println("修改");
}
public void delete() {
System.out.println("删除");
}
}
- 创建代理类
在 dynamic.cglib 包下创建代理类 CglibDynamicProxy,该类实现 MethodInterceptor 接口。
CglibDynamicProxy 的代码如下:
package dynamic.cglib;
import dynamic.jdk.MyAspect;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class CglibDynamicProxy implements MethodInterceptor {
public Object createProxy(Object target){
// 创建一个动态类对象,即增强类对象
Enhancer enhancer = new Enhancer();
// 确定需要增强的类,设置其父类
enhancer.setSuperclass(target.getClass());
// 确定代理逻辑对象为当前对象,要求当前对象实现 MethodInterceptor 的方法
enhancer.setCallback(this);
// 返回创建的代理对象
return enhancer.create();
}
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
// 创建一个切面
MyAspect myAspect = new MyAspect();
// 前增强
myAspect.check();
myAspect.except();
// 目标方法执行,返回代理结果
Object obj = methodProxy.invokeSuper(o,objects);
// 后增强
myAspect.log();
return obj;
}
}
注:这里的切面我们还是用的 JDK 代理时用到的切面
- 创建测试类
具体代码如下:
package dynamic.cglib;
public class Test {
public static void main(String[] args) {
// 创建代理对象
CglibDynamicProxy cdp = new CglibDynamicProxy();
// 创建目标对象
TestDao testDao = new TestDao();
// 获取增强后的目标对象
TestDao proxy = (TestDao) cdp.createProxy(testDao);
// 执行方法
proxy.delete();
System.out.println("---------");
proxy.save();
System.out.println("---------");
proxy.modify();
System.out.println("---------");
}
}
输出如下:
4.3 基于代理类的 AOP 实现
从 4.2 节可知,在 Spring 中默认使用 JDK 动态代理实现 AOP 编程。使用 org.springframework.aop.framework.ProxyFactoryBean 创建代理是 Spring AOP 实现的基本方式。
一、通知类型
在讲解 ProxyFactoryBean 之前先了解以下 Spring 的通知类型。根据 Spring 中通知在目标类方法的连接点位置,通知可以分为 6 中类型。
-
环绕通知
环绕通知实在目标方法执行前和执行后实施增强,可应用于日志记录、事物处理等功能。 -
前置通知
前置通知是在目标方法执行前实施增强,可应用于权限管理等功能。 -
后置返回通知
后置返回通知是在目标方法成功执行后实施增强,可应用于关闭流、删除临时文件等功能。
5 Spring 事务管理
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/156324.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...