Spring 框架采用分层的理念,根据功能的不同划分成了多个模块,这些模块大体可分为 Data Access/Integration(数据访问与集成)、Web、AOP、Aspects、Instrumentation(检测)、Messaging(消息处理)、Core Container(核心容器)和 Test。如下图所示(以下是 Spring Framework 4.x 版本后的系统架构图)
上图中包含了 Spring 框架的所有模块,这些模块可以满足一切企业级应用开发的需求,在开发过程中可以根据需求有选择性地使用所需要的模块。下面分别对这些模块的作用进行简单介绍。
数据访问/集成层包括 JDBC、ORM、OXM、JMS 和 Transactions 模块,具体介绍如下。
Spring 的 Web 层包括 Web、Servlet、WebSocket 和 Portlet 组件,具体介绍如下。
Spring 的核心容器是其他模块建立的基础,由 Beans 模块、Core 核心模块、Context 上下文模块和 SpEL 表达式语言模块组成,没有这些核心容器,也不可能有 AOP、Web 等上层的功能。具体介绍如下。
在 Core Container 之上是 AOP、Aspects 等模块,具体介绍如下:
Test 模块:Spring 支持 Junit 和 TestNG 测试框架,而且还额外提供了一些基于 Spring 的测试功能,比如在测试 Web 框架时,模拟 Http 请求的功能。
在 Eclipse 中创建一个简单的 Java 项目,依次单击 File -> New -> Java Project,这里将项目名称设置为 HelloSpring。
在项目中添加 Spring 和 logging 的 jar 包。鼠标右键项目 HelloSpring,单击 Build Path -> Add external archives,选择 jar 包,点击完成。目录结构如下。
运行该程序需要以下 3 个文件。您可以选择先导入以下文件,后面根据需求在添加相应的 jar 包,或直接导入所有 Spring 相关 jar 包。
创建 net.biancheng 包,在该包下创建 HelloWorld.java 和 MainApp.java 类。
HelloWorld.java 类的代码如下。
package net.biancheng;
public class HelloWorld {
private String message;
public void setMessage(String message) {
this.message = message;
}
public void getMessage() {
System.out.println("message : " + message);
}
}
MainApp.java 类的代码如下。
package net.biancheng;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class MainApp {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml");
HelloWorld obj = (HelloWorld) context.getBean("helloWorld");
obj.getMessage();
}
}
关于以上代码,需要注意以下两点:
在 src 目录下创建 Spring 配置文件 Beans.xml,内容如下。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<bean id="helloWorld" class="net.biancheng.HelloWorld">
<property name="message" value="Hello World!" />
</bean>
</beans>
您也可以将该配置文件命名为其它有效的名称。需要注意的是,该文件名必须与 MainApp.java 中读取的配置文件名称一致。
Beans.xml 用于给不同的 Bean 分配唯一的 ID,并给相应的 Bean 属性赋值。例如,在以上代码中,我们可以在不影响其它类的情况下,给 message 变量赋值。
运行 MainApp.java,Eclipse IDE 控制台中显示信息如下。
message : Hello World!
IoC 容器是 Spring 的核心,也可以称为 Spring 容器。Spring 通过 IoC 容器来管理对象的实例化和初始化,以及对象从创建到销毁的整个生命周期。
Spring 中使用的对象都由 IoC 容器管理,不需要我们手动使用 new 运算符创建对象。由 IoC 容器管理的对象称为 Spring Bean,Spring Bean 就是 Java 对象,和使用 new 运算符创建的对象没有区别。
Spring 通过读取 XML 或 Java 注解中的信息来获取哪些对象需要实例化。
Spring 提供 2 种不同类型的 IoC 容器,即 BeanFactory 和 ApplicationContext 容器。
BeanFactory 是最简单的容器,由 org.springframework.beans.factory.BeanFactory 接口定义,采用懒加载(lazy-load),所以容器启动比较快。BeanFactory 提供了容器最基本的功能。
为了能够兼容 Spring 集成的第三方框架(如 BeanFactoryAware、InitializingBean、DisposableBean),所以目前仍然保留了该接口。
简单来说,BeanFactory 就是一个管理 Bean 的工厂,它主要负责初始化各种 Bean,并调用它们的生命周期方法。
BeanFactory 接口有多个实现类,最常见的是 org.springframework.beans.factory.xml.XmlBeanFactory。使用 BeanFactory 需要创建 XmlBeanFactory 类的实例,通过 XmlBeanFactory 类的构造函数来传递 Resource 对象。如下所示。
Resource resource = new ClassPathResource("applicationContext.xml");
BeanFactory factory = new XmlBeanFactory(resource);
ApplicationContext 继承了 BeanFactory 接口,由 org.springframework.context.ApplicationContext 接口定义,对象在启动容器时加载。ApplicationContext 在 BeanFactory 的基础上增加了很多企业级功能,例如 AOP、国际化、事件支持等。
ApplicationContext 接口有两个常用的实现类,具体如下。
该类从类路径 ClassPath 中寻找指定的 XML 配置文件,并完成 ApplicationContext 的实例化工作,具体如下所示。
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(String configLocation);
在上述代码中,configLocation 参数用于指定 Spring 配置文件的名称和位置,如 Beans.xml。
该类从指定的文件系统路径中寻找指定的 XML 配置文件,并完成 ApplicationContext 的实例化工作,具体如下所示。
ApplicationContext applicationContext = new FileSystemXmlApplicationContext(String configLocation);
它与 ClassPathXmlApplicationContext 的区别是:在读取 Spring 的配置文件时,FileSystemXmlApplicationContext 不会从类路径中读取配置文件,而是通过参数指定配置文件的位置。即 FileSystemXmlApplicationContext 可以获取类路径之外的资源,如“F:/workspaces/Beans.xml”。
通常在 Java 项目中,会采用 ClassPathXmlApplicationContext 类实例化 ApplicationContext 容器的方式,而在 Web 项目中,ApplicationContext 容器的实例化工作会交由 Web 服务器完成。Web 服务器实例化 ApplicationContext 容器通常使用基于 ContextLoaderListener 实现的方式,它只需要在 web.xml 中添加如下代码:
<!--指定Spring配置文件的位置,有多个配置文件时,以逗号分隔-->
<context-param>
<param-name>contextConfigLocation</param-name>
<!--spring将加载spring目录下的applicationContext.xml文件-->
<param-value>
classpath:spring/applicationContext.xml
</param-value>
</context-param>
<!--指定以ContextLoaderListener方式启动Spring容器-->
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
需要注意的是,BeanFactory 和 ApplicationContext 都是通过 XML 配置文件加载 Bean 的。
二者的主要区别在于,如果 Bean 的某一个属性没有注入,使用 BeanFacotry 加载后,第一次调用 getBean() 方法时会抛出异常,而 ApplicationContext 则会在初始化时自检,这样有利于检查所依赖的属性是否注入。
因此,在实际开发中,通常都选择使用 ApplicationContext,只有在系统资源较少时,才考虑使用 BeanFactory。本教程中使用的是 ApplicationContext 容器。
由 Spring IoC 容器管理的对象称为 Bean,Bean 根据 Spring 配置文件中的信息创建。
可以把 Spring IoC 容器看作是一个大工厂,Bean 相当于工厂的产品,如果希望这个大工厂生产和管理 Bean,则需要告诉容器需要哪些 Bean,以及需要哪种方式装配 Bean。
Spring 配置文件支持两种格式,即 XML 文件格式和 Properties 文件格式。
通常情况下,Spring 的配置文件使用 XML 格式。XML 配置文件的根元素是 ,该元素包含了多个子元素 。每一个 元素都定义了一个 Bean,并描述了该 Bean 如何被装配到 Spring 容器中。
例如,《第一个Spring程序》一节中的 Beans.xml配置文件,代码如下所示:
上述代码中,使用 id 属性定义了 Bean,并使用 class 属性指定了 Bean 对应的类。
bean>元素的常用属性
属性名称 | 描述 |
---|---|
id | Bean 的唯一标识符,Spring 容器对 Bean 的配置和管理都通过该属性完成。id 的值必须以字母开始,可以使用字母、数字、下划线等符号。 |
name | name 属性中可以为 Bean 指定多个名称,每个名称之间用逗号或分号隔开。Spring 容器可以通过 name 属性配置和管理容器中的 Bean。 |
class | 该属性指定了 Bean 的具体实现类,它必须是一个完整的类名,即类的全限定名。 |
scope | 用于设定 Bean 实例的作用域,属性值可以为 singleton(单例)、prototype(原型)、request、session 和 global Session。其默认值是 singleton |
constructor-arg | 元素的子元素,可以使用此元素传入构造参数进行实例化。该元素的 index 属性指定构造参数的序号(从 0 开始),type 属性指定构造参数的类型 |
property | 元素的子元素,用于调用 Bean 实例中的 setter 方法来属性赋值,从而完成依赖注入。该元素的 name 属性用于指定 Bean 实例中相应的属性名 |
ref | 和 等元素的子元索,该元素中的 bean 属性用于指定对某个 Bean 实例的引用 |
value | 和 等元素的子元素,用于直接指定一个常量值 |
list | 用于封装 List 或数组类型的依赖注入 |
set | 用于封装 Set 类型的依赖注入 |
map | 用于封装 Map 类型的依赖注入 |
entry | 元素的子元素,用于设置一个键值对。其 key 属性指定字符串类型的键值,ref 或 value 子元素指定其值 |
init-method | 容器加载 Bean 时调用该方法,类似于 Servlet 中的 init() 方法 |
destroy-method | 容器删除 Bean 时调用该方法,类似于 Servlet 中的 destroy() 方法。该方法只在 scope=singleton 时有效 |
lazy-init | 懒加载,值为 true,容器在首次请求时才会创建 Bean 实例;值为 false,容器在启动时创建 Bean 实例。该方法只在 |
在配置文件中,除了可以定义 Bean 的属性值和相互之间的依赖关系,还可以声明 Bean 的作用域。例如,如果每次获取 Bean 时,都需要一个 Bean 实例,那么应该将 Bean 的 scope 属性定义为 prototype,如果 Spring 需要每次都返回一个相同的 Bean 实例,则应将 Bean 的 scope 属性定义为 singleton。
Spring 容器在初始化一个 Bean 实例时,同时会指定该实例的作用域。Spring 5 支持以下 6 种作用域。
默认值,单例模式,表示在 Spring 容器中只有一个 Bean 实例,Bean 以单例的方式存在。
原型模式,表示每次通过 Spring 容器获取 Bean 时,容器都会创建一个 Bean 实例。
每次 HTTP 请求,容器都会创建一个 Bean 实例。该作用域只在当前 HTTP Request 内有效。
同一个 HTTP Session 共享一个 Bean 实例,不同的 Session 使用不同的 Bean 实例。该作用域仅在当前 HTTP Session 内有效。
同一个 Web 应用共享一个 Bean 实例,该作用域在当前 ServletContext 内有效。
类似于 singleton,不同的是,singleton 表示每个 IoC 容器中仅有一个 Bean 实例,而同一个 Web 应用中可能会有多个 IoC 容器,但一个 Web 应用只会有一个 ServletContext,也可以说 application 才是 Web 应用中货真价实的单例模式。
websocket 的作用域是 WebSocket ,即在整个 WebSocket 中有效。
request、session、application、websocket 和 global Session 作用域只能在 Web 环境下使用,如果使用 ClassPathXmlApplicationContext 加载这些作用域中的任意一个的 Bean,就会抛出以下异常。
下面使用 Eclipse IDE 演示如何将 Bean 的作用域指定为 singleton,步骤如下:
package net.biancheng;
public class HelloWorld {
private String message;
public void setMessage(String message) {
this.message = message;
}
public void getMessage() {
System.out.println("message : " + message);
}
}
MainApp 类如下。
package net.biancheng;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class MainApp {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml");
HelloWorld objA = (HelloWorld) context.getBean("helloWorld");
objA.setMessage("对象A");
objA.getMessage();
HelloWorld objB = (HelloWorld) context.getBean("helloWorld");
objB.getMessage();
}
}
Beans.xml 文件内容如下。
在传统的 Java 应用中,Bean 的生命周期很简单,使用关键字 new 实例化 Bean,当不需要该 Bean 时,由 Java 自动进行垃圾回收。
Spring 中 Bean 的生命周期较复杂,可以表示为:Bean 的定义 -> Bean 的初始化 -> Bean 的使用 -> Bean 的销毁。
Spring 根据 Bean 的作用域来选择管理方式。对于 singleton 作用域的 Bean,Spring 能够精确地知道该 Bean 何时被创建,何时初始化完成,以及何时被销毁;而对于 prototype 作用域的 Bean,Spring 只负责创建,当容器创建了 Bean 的实例后,Bean 的实例就交给客户端代码管理,Spring 容器将不再跟踪其生命周期。
Spring 容器在确保一个 Bean 能够使用之前,会进行很多工作。Spring 容器中 Bean 的生命周期流程如下图所示。
Bean 生命周期的整个执行过程描述如下。
Spring 为 Bean 提供了细致全面的生命周期过程,实现特定的接口或设置 的属性都可以对 Bean 的生命周期过程产生影响。建议不要过多的使用 Bean 实现接口,因为这样会导致代码的耦合性过高。
了解 Spring 生命周期的意义就在于,可以利用 Bean 在其存活期间的指定时刻完成一些相关操作。一般情况下,会在 Bean 被初始化后和被销毁前执行一些相关操作。
Spring 官方提供了 3 种方法实现初始化回调和销毁回调:
在一个 Bean 中有多种生命周期回调方法时,优先级为:注解 > 接口 > XML。
org.springframework.beans.factory.InitializingBean 接口提供了以下方法:
void afterPropertiesSet() throws Exception;
您可以实现以上接口,在 afterPropertiesSet 方法内指定 Bean 初始化后需要执行的操作。
public class User implements InitializingBean {
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("调用接口:InitializingBean,方法:afterPropertiesSet,无参数");
}
}
可以通过 init-method 属性指定 Bean 初始化后执行的方法。
public class User {
public void init() {
System.out.println("调用init-method指定的初始化方法:init" );
}
}
使用 @PostConstruct 注解标明该方法为 Bean 初始化后的方法。
public class ExampleBean {
@PostConstruct
public void init() {
System.out.println("@PostConstruct注解指定的初始化方法:init" );
}
}
org.springframework.beans.factory.DisposableBean 接口提供了以下方法:
void destroy() throws Exception;
您可以实现以上接口,在 destroy 方法内指定 Bean 初始化后需要执行的操作。
纯文本复制
<bean id="..." class="..." />public class User implements InitializingBean { @Override public void afterPropertiesSet() throws Exception { System.out.println("调用接口:InitializingBean,方法:afterPropertiesSet,无参数"); }}
可以通过 destroy-method 属性指定 Bean 销毁后执行的方法。
public class User { public void destroy() { System.out.println("调用destroy-method指定的销毁方法:destroy" ); }}
使用 @PreDestory 注解标明该方法为 Bean 销毁前执行的方法。
public class ExampleBean { @PreDestory public void destroy() { System.out.println("@PreDestory注解指定的初始化方法:destroy" ); }}
下面使用 Eclipse IDE 演示如何通过配置 XML 的方式实现初始化回调和销毁回调,步骤如下:
HelloWorld 类代码如下。
package net.biancheng;public class HelloWorld { private String message; public void setMessage(String message) { this.message = message; } public void getMessage() { System.out.println("message : " + message); } public void init() { System.out.println("Bean正在进行初始化"); } public void destroy() { System.out.println("Bean将要被销毁"); }}
MainApp 类代码如下,该类中我们使用 AbstractApplicationContext 类的 registerShutdownHook() 方法,来确保正常关机并调用相关的 destroy() 方法。
package net.biancheng;import org.springframework.context.support.AbstractApplicationContext;import org.springframework.context.support.ClassPathXmlApplicationContext;public class MainApp { public static void main(String[] args) { AbstractApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml"); HelloWorld obj = (HelloWorld) context.getBean("helloWorld"); obj.getMessage(); context.registerShutdownHook(); }}
Beans.xml 配置文件代码如下。
<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"> <bean id="helloWorld" class="net.biancheng.HelloWorld" init-method="init" destroy-method="destroy"> <property name="message" value="Hello World!" /> </bean></beans>
运行结果如下。
Bean正在进行初始化
message : Hello World!
Bean将要被销毁
如果多个 Bean 需要使用相同的初始化或者销毁方法,不用为每个 bean 声明初始化和销毁方法,可以使用 default-init-method 和 default-destroy-method 属性,如下所示。
纯文本复制
...
BeanPostProcessor 接口也被称为后置处理器,通过该接口可以自定义调用初始化前后执行的操作方法。
BeanPostProcessor 接口源码如下:
public interface BeanPostProcessor { Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException; Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;}
postProcessBeforeInitialization 在 Bean 实例化、依赖注入后,初始化前调用。postProcessAfterInitialization 在 Bean 实例化、依赖注入、初始化都完成后调用。
当需要添加多个后置处理器实现类时,默认情况下 Spring 容器会根据后置处理器的定义顺序来依次调用。也可以通过实现 Ordered 接口的 getOrder 方法指定后置处理器的执行顺序。该方法返回值为整数,默认值为 0,值越大优先级越低。
下面使用 Eclipse IDE 演示 BeanPostProcessor 接口的用法,步骤如下:
HelloWorld 类代码如下。
package net.biancheng;public class HelloWorld {
private String message;
public void setMessage(String message) {
this.message = message; }
public void getMessage() {
System.out.println("Message : " + message); }
public void init() {
System.out.println("Bean正在初始化"); }
public void destroy() { System.out.println("Bean将要被销毁"); }}
InitHelloWorld 类代码如下。
package net.biancheng;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.core.Ordered;
public class InitHelloWorld implements BeanPostProcessor, Ordered {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { System.out.println("A Before : " + beanName);
return bean; }
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { System.out.println("A After : " + beanName);
return bean; }
@Override public int getOrder() {
return 5; }}
需要注意的是,postProcessBeforeInitialization 和 postProcessAfterInitialization 方法返回值不能为 null,否则会报空指针异常或者通过 getBean() 方法获取不到 Bean 实例对象,因为后置处理器从Spring IoC 容器中取出 Bean 实例对象后没有再次放回到 IoC 容器中。
InitHelloWorld2 的代码如下。
package net.biancheng;import org.springframework.beans.BeansException;import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.core.Ordered;public class InitHelloWorld2 implements BeanPostProcessor, Ordered {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { System.out.println("B Before : " + beanName);
return bean; }
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { System.out.println("B After : " + beanName);
return bean; }
@Override
public int getOrder() {
return 0; }}
Beans.xml 代码如下。
<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"> <bean id="helloWorld" class="net.biancheng.HelloWorld" init-method="init" destroy-method="destroy"> <property name="message" value="Hello World!" /> </bean> <!-- 注册处理器 --> <bean class="net.biancheng.InitHelloWorld" /> <bean class="net.biancheng.InitHelloWorld2" /></beans>
MainApp 类代码如下。
package net.biancheng;import org.springframework.context.support.AbstractApplicationContext;import org.springframework.context.support.ClassPathXmlApplicationContext;public class MainApp { public static void main(String[] args) { AbstractApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml"); HelloWorld obj = (HelloWorld) context.getBean("helloWorld"); obj.getMessage(); context.registerShutdownHook(); }}
运行结果如下。
B Before : helloWorld
A Before : helloWorld
Bean正在初始化
B After : helloWorld
A After : helloWorld
Message : Hello World!
Bean将要被销毁
由运行结果可以看出,postProcessBeforeInitialization 方法是在 Bean 实例化和依赖注入后,自定义初始化方法前执行的。而 postProcessAfterInitialization 方法是在自定义初始化方法后执行的。由于 getOrder 方法返回值越大,优先级越低,所以 InitHelloWorld2 先执行。
Bean 定义可以包含很多配置信息,包括构造函数参数、属性值和容器的一些具体信息,如初始化方法、销毁方法等。子 Bean 可以继承父 Bean 的配置数据,根据需要,子 Bean 可以重写值或添加其它值。
需要注意的是,Spring Bean 定义的继承与 Java 中的继承无关。您可以将父 Bean 的定义作为一个模板,其它子 Bean 从父 Bean 中继承所需的配置。
在配置文件中通过 parent 属性来指定继承的父 Bean。
下面使用 Eclipse IDE 演示 Bean 继承,步骤如下:
HelloWorld 类代码如下。
package net.biancheng;public class HelloWorld { private String message1; private String message2; public void setMessage1(String message) { this.message1 = message; } public void setMessage2(String message) { this.message2 = message; } public void getMessage1() { System.out.println("World Message1 : " + message1); } public void getMessage2() { System.out.println("World Message2 : " + message2); }}
HelloChina 类代码如下。
package net.biancheng;public class HelloChina { private String message1; private String message2; private String message3; public void setMessage1(String message) { this.message1 = message; } public void setMessage2(String message) { this.message2 = message; } public void setMessage3(String message) { this.message3 = message; } public void getMessage1() { System.out.println("China Message1 : " + message1); } public void getMessage2() { System.out.println("China Message2 : " + message2); } public void getMessage3() { System.out.println("China Message3 : " + message3); }}
在配置文件中,分别为 HelloWorld 中的 message1 和 message2 赋值。使用 parent 属性将 HelloChain 定义为 HelloWorld 的子类,并为 HelloChain 中的 message1 和 message3 赋值。Beans.xml 文件代码如下。
MainApp 类代码如下。
package net.biancheng;import org.springframework.context.ApplicationContext;import org.springframework.context.support.ClassPathXmlApplicationContext;public class MainApp { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml"); HelloWorld objA = (HelloWorld) context.getBean("helloWorld"); objA.getMessage1(); objA.getMessage2(); HelloChina objB = (HelloChina) context.getBean("helloChina"); objB.getMessage1(); objB.getMessage2(); objB.getMessage3(); }}
运行结果如下。
World Message1 : Hello World!
World Message2 : Hello World2!
China Message1 : Hello China!
China Message2 : Hello World2!
China Message3 : Hello China3!
由结果可以看出,我们在创建 helloChina 时并没有给 message2 赋值,但是由于 Bean 的继承,将值传递给了 message2。
Bean 定义可以包含很多配置信息,包括构造函数参数、属性值和容器的一些具体信息,如初始化方法、销毁方法等。子 Bean 可以继承父 Bean 的配置数据,根据需要,子 Bean 可以重写值或添加其它值。
需要注意的是,Spring Bean 定义的继承与 Java 中的继承无关。您可以将父 Bean 的定义作为一个模板,其它子 Bean 从父 Bean 中继承所需的配置。
在配置文件中通过 parent 属性来指定继承的父 Bean。
下面使用 Eclipse IDE 演示 Bean 继承,步骤如下:
HelloWorld 类代码如下。
package net.biancheng;
public class HelloWorld {
private String message1;
private String message2;
public void setMessage1(String message) {
this.message1 = message;
}
public void setMessage2(String message) {
this.message2 = message;
}
public void getMessage1() {
System.out.println("World Message1 : " + message1);
}
public void getMessage2() {
System.out.println("World Message2 : " + message2);
}
}
HelloChina 类代码如下。
package net.biancheng;
public class HelloChina {
private String message1;
private String message2;
private String message3;
public void setMessage1(String message) {
this.message1 = message;
}
public void setMessage2(String message) {
this.message2 = message;
}
public void setMessage3(String message) {
this.message3 = message;
}
public void getMessage1() {
System.out.println("China Message1 : " + message1);
}
public void getMessage2() {
System.out.println("China Message2 : " + message2);
}
public void getMessage3() {
System.out.println("China Message3 : " + message3);
}
}
在配置文件中,分别为 HelloWorld 中的 message1 和 message2 赋值。使用 parent 属性将 HelloChain 定义为 HelloWorld 的子类,并为 HelloChain 中的 message1 和 message3 赋值。Beans.xml 文件代码如下。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<bean id="helloWorld" class="net.biancheng.HelloWorld">
<property name="message1" value="Hello World!" />
<property name="message2" value="Hello World2!" />
</bean>
<bean id="helloChina" class="net.biancheng.HelloChina"
parent="helloWorld">
<property name="message1" value="Hello China!" />
<property name="message3" value="Hello China3!" />
</bean>
</beans>
MainApp 类代码如下。
package net.biancheng;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class MainApp {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml");
HelloWorld objA = (HelloWorld) context.getBean("helloWorld");
objA.getMessage1();
objA.getMessage2();
HelloChina objB = (HelloChina) context.getBean("helloChina");
objB.getMessage1();
objB.getMessage2();
objB.getMessage3();
}
}
运行结果如下。
World Message1 : Hello World!
World Message2 : Hello World2!
China Message1 : Hello China!
China Message2 : Hello World2!
China Message3 : Hello China3!
由结果可以看出,我们在创建 helloChina 时并没有给 message2 赋值,但是由于 Bean 的继承,将值传递给了 message2。
您可以创建一个 Bean 定义模板,该模板只能被继承,不能被实例化。创建 Bean 定义模板时,不用指定 class 属性,而是指定 abstarct=“true” 将该 Bean 定义为抽象 Bean,如下所示。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<bean id="beanTeamplate" abstract="true">
<property name="message1" value="Hello World!" />
<property name="message2" value="Hello World2!" />
<property name="message3" value="Hello World3!" />
</bean>
<bean id="helloChina" class="net.biancheng.HelloChina"
parent="beanTeamplate">
<property name="message1" value="Hello China!" />
<property name="message3" value="Hello China!" />
</bean>
</beans>
Spring 依赖注入(Dependency Injection,DI)和控制反转含义相同,它们是从两个角度描述的同一个概念。使用依赖注入可以更轻松的管理和测试应用程序。
当某个 Java 实例需要另一个 Java 实例时,传统的方法是由调用者创建被调用者的实例(例如,使用 new 关键字获得被调用者实例),而使用 Spring 框架后,被调用者的实例不再由调用者创建,而是由 Spring 容器创建,这称为控制反转。
Spring 容器在创建被调用者的实例时,会自动将调用者需要的对象实例注入给调用者,调用者通过 Spring 容器获得被调用者实例,这称为依赖注入。
依赖注入主要有两种实现方式,分别是 setter 注入(又称设值注入)和构造函数注入。具体介绍如下。
指 IoC 容器使用构造函数注入被依赖的实例。可以通过调用带参数的构造函数实现依赖注入,每个参数代表一个依赖。
指 IoC 容器使用 setter 方法注入被依赖的实例。通过调用无参构造器或无参 static 工厂方法实例化 Bean 后,调用该 Bean 的 setter 方法,即可实现基于 setter 的 DI。
在 Spring 实例化 Bean 的过程中,首先会调用默认的构造方法实例化 Bean 对象,然后通过 Java 的反射机制调用 setXxx() 方法进行属性的注入。因此,setter 注入要求 Bean 的对应类必须满足以下两点要求。
使用 setter 注入时,在 Spring 配置文件中,需要使用 元素的子元素 为每个属性注入值。而使用构造注入时,在配置文件中,主要使用 标签定义构造方法的参数,使用其 value 属性(或子元素)设置该参数的值。
下面使用 标签实现构造函数注入。
在 标签中,包含 ref、value、type、index 等属性。value 属性用于注入基本数据类型以及字符串类型的值;ref 属性用于注入已经定义好的 Bean;type 属性用来指定对应的构造函数,当构造函数有多个参数时,可以使用 index 属性指定参数的位置,index 属性值从 0 开始。
下面使用 Eclipse IDE 演示通过构造函数注入依赖项,步骤如下:
Person 类代码如下。
package net.biancheng;
public class Person {
private Man man;
public Person(Man man) {
System.out.println("在Person的有参构造函数内");
this.man = man;
}
public void man() {
man.show();
}
}
Man 类代码如下。
package net.biancheng;
public class Man {
private String name;
private int age;
public Man() {
System.out.println("在man的构造函数内");
}
public Man(String name, int age) {
System.out.println("在man的有参构造函数内");
this.name = name;
this.age = age;
}
public void show() {
System.out.println("名称:" + name + "\n年龄:" + age);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
Beans.xml 配置文件如下。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<bean id="man" class="net.biancheng.Man">
<constructor-arg value="bianchengbang" />
<constructor-arg value="12" type="int" />
</bean>
<bean id="person" class="net.biancheng.Person">
<constructor-arg ref="man" type="net.biancheng.Man"/>
</bean>
</beans>
MainApp 类代码如下。
package net.biancheng;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class MainApp {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml");
Person person = (Person) context.getBean("person");
person.man();
}
}
运行结果如下。
在man的有参构造函数内
在Person的有参构造函数内
名称:bianchengbang
年龄:12
下面使用 标签实现 setter 注入。
在 标签中,包含 name、ref、value 等属性。name 用于指定参数名称;value 属性用于注入基本数据类型以及字符串类型的值;ref 属性用于注入已经定义好的 Bean。
在例 1 的基础上修改 Person 类的内容,代码如下。
package net.biancheng;
public class Person {
private Man man;
public void man() {
man.show();
}
public Man getMan() {
return man;
}
public void setMan(Man man) {
System.out.println("在setMan方法内");
this.man = man;
}
}
Beans.xml 配置文件如下。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<bean id="person" class="net.biancheng.Person">
<property name="man" ref="man" />
</bean>
<bean id="man" class="net.biancheng.Man">
<property name="name" value="bianchengbang" />
<property name="age" value="12" />
</bean>
</beans>
运行结果如下。
在man的构造函数内
在setMan方法内
名称:bianchengbang
年龄:12
Java 中在类内部定义的类称为内部类,同理在 Bean 中定义的 Bean 称为内部 Bean。注入内部 Bean 使用 和 中的 标签。如下所示。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<bean id="outerBean" class="...">
<property name="target">
<!-- 定义内部Bean -->
<bean class="..." />
</property>
</bean>
</beans>
内部 Bean 的定义不需要指定 id 和 name 。如果指定了,容器也不会将其作为区分 Bean 的标识符,反而会无视内部 Bean 的 scope 属性。所以内部 Bean 总是匿名的,而且总是随着外部 Bean 创建。
下面使用 Eclipse IDE 演示如何注入内部 Bean,步骤如下:
Person 类代码如下。
package net.biancheng;
public class Person {
private Man man;
public Man getMan() {
return man;
}
public void setMan(Man man) {
System.out.println("在setMan方法内");
this.man = man;
}
public void man() {
man.show();
}
}
Man 类代码如下。
package net.biancheng;
public class Man {
private String name;
private int age;
public Man() {
System.out.println("在man的构造函数内");
}
public Man(String name, int age) {
System.out.println("在man的有参构造函数内");
this.name = name;
this.age = age;
}
public void show() {
System.out.println("名称:" + name + "\n年龄:" + age);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
Beans.xml 配置文件如下。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<bean id="person" class="net.biancheng.Person">
<property name="man">
<bean class="net.biancheng.Man">
<property name="name" value="bianchengbang" />
<property name="age" value="12" />
</bean>
</property>
</bean>
</beans>
MainApp 类代码如下。
package net.biancheng;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class MainApp {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml");
Person person = (Person) context.getBean("person");
person.man();
}
}
Bean 的装配可以理解为依赖关系注入,Bean 的装配方式也就是 Bean 的依赖注入方式。Spring 容器支持多种装配 Bean 的方式,如基于 XML 的 Bean 装配、基于 Annotation 的 Bean 装配和自动装配等。
Spring 基于 XML 的装配通常采用两种实现方式,即我们在《Spring依赖注入》一节介绍的 setter 注入和构造注入。本节介绍如何配置自动装配。
自动装配就是指 Spring 容器在不使用 和 标签的情况下,可以自动装配(autowire)相互协作的 Bean 之间的关联关系,将一个 Bean 注入其他 Bean 的 Property 中。
使用自动装配需要配置 元素的 autowire 属性。autowire 属性有五个值,具体说明如下表所示。
名称 | 说明 |
---|---|
no | 默认值,表示不使用自动装配,Bean 依赖必须通过 ref 元素定义。 |
byName | 根据 Property 的 name 自动装配,如果一个 Bean 的 name 和另一个 Bean 中的 Property 的 name 相同,则自动装配这个 Bean 到 Property 中。 |
byType | 根据 Property 的数据类型(Type)自动装配,如果一个 Bean 的数据类型兼容另一个 Bean 中 Property 的数据类型,则自动装配。 |
constructor | 类似于 byType,根据构造方法参数的数据类型,进行 byType 模式的自动装配。 |
autodetect(3.0版本不支持) | 如果 Bean 中有默认的构造方法,则用 constructor 模式,否则用 byType 模式。 |
下面使用 Eclipse IDE 演示 Spring Bean 的自动装配,步骤如下:
创建 SpringDemo 项目,并在 src 目录下创建 net.biancheng 包。
添加相应的 jar 包,可以参考《第一个Spring程序》一节。
在 net.biancheng 包下创建 Person、Man 和 MainApp 类。
在 src 目录下创建 Spring 配置文件 Beans.xml。
运行 SpringDemo 项目。
Person 类代码如下。
package net.biancheng;
public class Person {
private Man man;
public Person() {
System.out.println("在Person的构造函数内");
}
public Person(Man man) {
System.out.println("在Person的有参构造函数内");
this.man = man;
}
public void man() {
man.show();
}
public Man getMan() {
return man;
}
public void setMan(Man man) {
this.man = man;
}
}
Man 类代码如下。
package net.biancheng;
public class Man {
private String name;
private int age;
public Man() {
System.out.println("在man的构造函数内");
}
public Man(String name, int age) {
System.out.println("在man的有参构造函数内");
this.name = name;
this.age = age;
}
public void show() {
System.out.println("名称:" + name + "\n年龄:" + age);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
MainApp 类代码如下。
package net.biancheng;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class MainApp {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml");
Person person = (Person) context.getBean("person");
person.man();
}
}
autowire=“no” 表示不使用自动装配,需要手动注入,Bean 依赖通过 ref 元素定义,Beans.xml 配置文件如下。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<bean id="man" class="net.biancheng.Man">
<constructor-arg value="bianchengbang" />
<constructor-arg value="12" type="int" />
</bean>
<bean id="person" class="net.biancheng.Person" autowire="no">
<constructor-arg ref="man" type="net.biancheng.Man"/>
</bean>
</beans>
运行结果如下。
在man的有参构造函数内
在Person的有参构造函数内
名称:bianchengbang
年龄:12
autowire=“byName” 表示按属性名称自动装配,XML 文件中 Bean 的 id 必须与类中的属性名称相同。配置文件内容修改如下。
<bean id="man" class="net.biancheng.Man">
<constructor-arg value="bianchengbang" />
<constructor-arg value="12" type="int" />
</bean>
<bean id="person" class="net.biancheng.Person" autowire="byName"/>
运行结果如下。
在man的有参构造函数内
在Person的构造函数内
名称:bianchengbang
年龄:12
如果更改 Bean 的名称,很可能不会注入依赖项。
将 Bean 的名称更改为 man1,配置文件如下:
<bean id="man1" class="net.biancheng.Man">
<constructor-arg value="bianchengbang" />
<constructor-arg value="12" type="int" />
</bean>
<bean id="person" class="net.biancheng.Person" autowire="byName"/>
注入失败,异常信息为:
在man的有参构造函数内
Exception in thread “main” java.lang.NullPointerException
at net.biancheng.Person.man(Person.java:16)
at net.biancheng.MainApp.main(MainApp.java:10)
XML 文件中 Bean 的 id 与类中的属性名称可以不同,但必须只有一个类型的 Bean。配置文件内容修改如下。
<bean id="man1" class="net.biancheng.Man">
<constructor-arg value="bianchengbang" />
<constructor-arg value="12" type="int" />
</bean>
<bean id="person" class="net.biancheng.Person" autowire="byType"/>
运行结果如下。
在man的有参构造函数内
在Person的构造函数内
名称:bianchengbang
年龄:12
如果您有相同类型的多个 Bean,则注入失败,并且引发异常。
添加 id 为 man2 的 Bean,配置文件代码如下。
<bean id="man1" class="net.biancheng.Man">
<constructor-arg value="bianchengbang" />
<constructor-arg value="12" type="int" />
</bean>
<bean id="man2" class="net.biancheng.Man">
<constructor-arg value="bianchengbang" />
<constructor-arg value="12" type="int" />
</bean>
<bean id="person" class="net.biancheng.Person" autowire="byType"/>
异常信息为:
在man的有参构造函数内
在man的有参构造函数内
在Person的构造函数内
一月 26, 2021 1:34:14 下午 org.springframework.context.support.AbstractApplicationContext refresh
警告: Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name ‘person’ defined in class path resource [Beans.xml]: Unsatisfied dependency expressed through bean property ‘man’; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type ‘net.biancheng.Man’ available: expected single matching bean but found 2: man1,man2
…
类中构造函数的参数必须在配置文件中有相同的类型,配置文件内容修改如下。
<bean id="man" class="net.biancheng.Man">
<constructor-arg value="bianchengbang" />
<constructor-arg value="12" type="int" />
</bean>
<bean id="person" class="net.biancheng.Person" autowire="constructor"/>
运行结果如下。
在man的有参构造函数内
在Person的有参构造函数内
名称:bianchengbang
年龄:12
在 Spring 中,尽管可以使用 XML 配置文件实现 Bean 的装配工作,但如果应用中 Bean 的数量较多,会导致 XML 配置文件过于臃肿,从而给维护和升级带来一定的困难。
Java 从 JDK 5.0 以后,提供了 Annotation(注解)功能,Spring 2.5 版本开始也提供了对 Annotation 技术的全面支持,我们可以使用注解来配置依赖注入。
Spring 默认不使用注解装配 Bean,因此需要在配置文件中添加 context:annotation-config/,启用注解。
Spring 中常用的注解如下。
可以使用此注解描述 Spring 中的 Bean,但它是一个泛化的概念,仅仅表示一个组件(Bean),并且可以作用在任何层次。使用时只需将该注解标注在相应类上即可。
用于将数据访问层(DAO层)的类标识为 Spring 中的 Bean,其功能与 @Component 相同。
通常作用在业务层(Service 层),用于将业务层的类标识为 Spring 中的 Bean,其功能与 @Component 相同。
通常作用在控制层(如 Struts2 的 Action、SpringMVC 的 Controller),用于将控制层的类标识为 Spring 中的 Bean,其功能与 @Component 相同。
可以应用到 Bean 的属性变量、属性的 setter 方法、非 setter 方法及构造函数等,配合对应的注解处理器完成 Bean 的自动配置工作。默认按照 Bean 的类型进行装配。
作用与 Autowired 相同,区别在于 @Autowired 默认按照 Bean 类型装配,而 @Resource 默认按照 Bean 实例名称进行装配。
@Resource 中有两个重要属性:name 和 type。
Spring 将 name 属性解析为 Bean 的实例名称,type 属性解析为 Bean 的实例类型。如果指定 name 属性,则按实例名称进行装配;如果指定 type 属性,则按 Bean 类型进行装配。如果都不指定,则先按 Bean 实例名称装配,如果不能匹配,则再按照 Bean 类型进行装配;如果都无法匹配,则抛出 NoSuchBeanDefinitionException 异常。
与 @Autowired 注解配合使用,会将默认的按 Bean 类型装配修改为按 Bean 的实例名称装配,Bean 的实例名称由 @Qualifier 注解的参数指定。
AOP 的全称是“Aspect Oriented Programming”,即面向切面编程,和 OOP(面向对象编程)类似,也是一种编程思想。
AOP 采取横向抽取机制(动态代理),取代了传统纵向继承机制的重复性代码,其应用主要体现在事务处理、日志管理、权限控制、异常处理等方面。主要作用是分离功能性需求和非功能性需求,使开发人员可以集中处理某一个关注点或者横切逻辑,减少对业务代码的侵入,增强代码的可读性和可维护性。
简单的说,AOP 的作用就是保证开发者在不修改源代码的前提下,为系统中的业务组件添加某种通用功能。AOP 就是代理模式的典型应用。
目前最流行的 AOP 框架有两个,分别为 Spring AOP 和 AspectJ。
Spring AOP 是基于 AOP 编程模式的一个框架,它能够有效的减少系统间的重复代码,达到松耦合的目的。Spring AOP 使用纯 Java 实现,不需要专门的编译过程和类加载器,在运行期间通过代理方式向目标类植入增强的代码。有两种实现方式:基于接口的 JDK 动态代理和基于继承的 CGLIB 动态代理。
AspectJ 是一个基于 Java 语言的 AOP 框架,从 Spring 2.0 开始,Spring AOP 引入了对 AspectJ 的支持。AspectJ 扩展了 Java 语言,提供了一个专门的编译器,在编译时提供横向代码的植入。
AOP 提供了一种可插入的方式,可以在实际逻辑之前、之后或周围添加其它关注点。比如一个类中有以下 10 个方法。
class A{
public void m1(){...}
public void m2(){...}
public void m3(){...}
public void m4(){...}
public void m5(){...}
public void n1(){...}
public void n2(){...}
public void p1(){...}
public void p2(){...}
public void p3(){...}
}
以 m 开头的方法有 5 种,以 n 开头的方法有 2 种,以 p 开头的方法有 3 种。现在要求在以 m 开头的方法后添加发送通知功能。
在不使用 AOP 的情况下,我们必须修改以 m 开头的 5 种方法,在方法中调用发送通知的方法。
如果使用 AOP,我们不用在方法内调用发送通知的方法,只需要在类的方法中定义切入点,然后在 XML 文件中调用。如果需要删除或修改此功能,那么只需要在 XML 文件中进行更改。由此可以看出,使用 AOP 可以增强代码的可维护性。
为了更好地理解 AOP,我们需要了解一些它的相关术语。这些专业术语并不是 Spring 特有的,有些也同样适用于其它 AOP 框架,如 AspectJ。它们的含义如下表所示。
名称 | 说明 |
---|---|
Joinpoint(连接点) | 指那些被拦截到的点,在 Spring 中,指可以被动态代理拦截目标类的方法。 |
Pointcut(切入点) | 指要对哪些 Joinpoint 进行拦截,即被拦截的连接点。 |
Advice(通知) | 指拦截到 Joinpoint 之后要做的事情,即对切入点增强的内容。 |
Target(目标) | 指代理的目标对象。 |
Weaving(植入) | 指把增强代码应用到目标上,生成代理对象的过程。 |
Proxy(代理) | 指生成的代理对象。 |
Aspect(切面) | 切入点和通知的结合。 |
Advice 直译为通知,也有的资料翻译为“增强处理”,共有 5 种类型,如下表所示。
通知 | 说明 |
---|---|
before(前置通知) | 通知方法在目标方法调用之前执行 |
after(后置通知) | 通知方法在目标方法返回或异常后调用 |
after-returning(返回后通知) | 通知方法会在目标方法返回后调用 |
after-throwing(抛出异常通知) | 通知方法会在目标方法抛出异常后调用 |
around(环绕通知) | 通知方法会将目标方法封装起来 |
AOP 是 Spring 的核心之一,在 Spring 中经常会使用 AOP 来简化编程。在 Spring 框架中使用 AOP 主要有以下优势。
Spring JDK 动态代理需要实现 InvocationHandler 接口,重写 invoke 方法,客户端使用 Java.lang.reflect.Proxy 类产生动态代理类的对象。
下面使用 Eclipse IDE 演示 JDK 动态代理,步骤如下:
UserManager 类代码如下。
package net.biancheng;
public interface UserManager {
// 新增用户抽象方法
void addUser(String userName, String password);
// 删除用户抽象方法
void delUser(String userName);
}
UserManagerImpl 类代码如下。
package net.biancheng;
public class UserManagerImpl implements UserManager {
@Override
public void addUser(String userName, String password) {
System.out.println("正在执行添加用户方法");
System.out.println("用户名称: " + userName + " 密码: " + password);
}
@Override
public void delUser(String userName) {
System.out.println("正在执行删除用户方法");
System.out.println("用户名称: " + userName);
}
}
MyAspect 类代码如下。
package net.biancheng;
public class MyAspect {
public void myBefore() {
System.out.println("方法执行之前");
}
public void myAfter() {
System.out.println("方法执行之后");
}
}
MyAspect 类在切面中定义了两个增强的方法,分别为 myBefore 和 myAfter。
JdkProxy 类代码如下。
package net.biancheng;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* JDK动态代理实现InvocationHandler接口
*
* @author 编程帮
*
*/
public class JdkProxy implements InvocationHandler {
private Object target; // 需要代理的目标对象
final MyAspect myAspect = new MyAspect();
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
myAspect.myBefore();
Object result = method.invoke(target, args);
myAspect.myAfter();
return result;
}
// 定义获取代理对象方法
private Object getJDKProxy(Object targetObject) {
// 为目标对象target赋值
this.target = targetObject;
// JDK动态代理只能代理实现了接口的类,从 newProxyInstance 函数所需的参数就可以看出来
return Proxy.newProxyInstance(targetObject.getClass().getClassLoader(), targetObject.getClass().getInterfaces(),
this);
}
public static void main(String[] args) {
JdkProxy jdkProxy = new JdkProxy(); // 实例化JDKProxy对象
UserManager user = (UserManager) jdkProxy.getJDKProxy(new UserManagerImpl()); // 获取代理对象
user.addUser("bianchengbang", "www.biancheng.net"); // 执行新增方法
user.delUser("bianchengbang"); // 执行删除方法
}
}
运行结果如下。
方法执行之前
正在执行添加用户方法
用户名称: bianchengbang 密码: www.biancheng.net
方法执行之后
方法执行之前
正在执行删除用户方法
用户名称: bianchengbang
方法执行之后
通过学习《Spring JDK动态代理》一节可以了解到,JDK 动态代理使用起来非常简单,但是 JDK 动态代理的目标类必须要实现一个或多个接口,具有一定的局限性。如果不希望实现接口,可以使用 CGLIB代理。
CGLIB(Code Generation Library)是一个高性能开源的代码生成包,它被许多 AOP 框架所使用,其底层是通过使用一个小而快的字节码处理框架 ASM(Java 字节码操控框架)转换字节码并生成新的类。使用 CGLIB 需要导入 CGLIB 和 ASM 包,即 asm-x.x.jar 和 CGLIB-x.x.x.jar 。如果您已经导入了 Spring 的核心包 spring-core-x.x.x.RELEASE.jar,就不用再导入 asm-x.x.jar 和 cglib-x.x.x.jar 了。
Spring 核心包中包含 CGLIB 和 asm,也就是说 Spring 核心包已经集成了 CGLIB 所需要的包,所以在开发中不需要另外导入asm-x.x.jar 和 cglib-x.x.x.jar 包了。
下面使用 Eclipse IDE 演示 CGLIB 动态代理的使用,步骤如下:
创建 SpringDemo 项目,并在 src 目录下创建 net.biancheng 包。
导入相关 JAR 包。
在 net.biancheng 包下创建 UserManager(用户管理接口)、UserManagerImpl(用户管理接口实现类)、MyAspect(切面类)和 CGLIBProxy(动态代理类)。
运行 SpringDemo 项目。
JDK代理和CGLIB代理的区别
JDK 动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用 InvokeHandler 来处理。而 CGLIB 动态代理是利用 ASM 开源包,加载代理对象类的 class 文件,通过修改其字节码生成子类来处理。
JDK 动态代理只能对实现了接口的类生成代理,而不能针对类。
CGLIB 是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法。因为是继承,所以该类或方法不能声明成 final 类型。
生成代理实例性能:JDK > CGLIB
代理实例运行性能:JDK > CGLIB
在《Spring JDK动态代理》和《Spring CGLlB动态代理》一节我们学习了基于代理类的 AOP 实现,Spring 2.0 以后,Spring 新增了对 AspectJ 的支持。在新版本的 Spring 框架中,建议使用 AspectJ 方式开发 AOP。
AspectJ 是一个基于 Java 语言的 AOP 框架,它扩展了 Java 语言,提供了强大的 AOP 功能。
使用 AspectJ 需要导入以下 jar 包:
jar 包下载地址:https://www.eclipse.org/aspectj/downloads.php
使用 AspectJ 开发 AOP 通常有以下 2 种方式:
打开 AspectJ 包下载页面,选择相应的版本,这里我们下载的为 1.9.5 稳定版本。
点击 aspectj-1.9.5.jar 进入下载页面,选择 Select another mirror,如下图。
根据自己所处地区选择下载,这里我们选择的是中国科学技术大学的下载地址。
下载完成后,解压该文件,需要导入的 jar 包在 files 文件夹的 lib 目录下。
基于 XML 的声明式是指通过 Spring 配置文件的方式来定义切面、切入点及通知,而所有的切面和通知都必须定义在 aop:config 元素中。
在使用 aop:config 元素之前,我们需要先导入 Spring aop 命名空间,如下所示。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd ">
...
</beans>
在 Spring 配置文件中,使用 aop:aspect 元素定义切面,该元素可以将定义好的 Bean 转换为切面 Bean,所以使用 aop:aspect 之前需要先定义一个普通的 Spring Bean。
<aop:config> <aop:aspect id="myAspect" ref="aBean"> ... </aop:aspect></aop:config>
其中,id 用来定义该切面的唯一表示名称,ref 用于引用普通的 Spring Bean。
aop:pointcut 用来定义一个切入点,当 aop:pointcut元素作为 aop:config 元素的子元素定义时,表示该切入点是全局切入点,它可被多个切面所共享;当 aop:pointcut 元素作为 aop:aspect 元素的子元素时,表示该切入点只对当前切面有效。
<aop:config>
<aop:pointcut id="myPointCut"
expression="execution(* net.biancheng.service.*.*(..))"/>
</aop:config>
其中,id 用于指定切入点的唯一标识名称,execution 用于指定切入点关联的切入点表达式。
execution 格式为:execution(modifiers-pattern returning-type-pattern declaring-type-pattern name-pattern(param-pattern)throws-pattern)
*
表示可以为任何返回值。如果返回值为对象,则需指定全路径的类名。*
代表所有,set*
代表以 set 开头的所有方法。(..)
代表所有参数,(*)
代表一个参数,(*,String)
代表第一个参数可以为任何值,第二个为 String 类型的值。例如:execution(* net.biancheng.*.*(..))
表示匹配 net.biancheng 包中任意类的任意方法。
AspectJ 支持 5 种类型的 advice,如下。
<aop:aspect id="myAspect" ref="aBean">
<!-- 前置通知 -->
<aop:before pointcut-ref="myPointCut" method="..."/>
<!-- 后置通知 -->
<aop:after-returning pointcut-ref="myPointCut" method="..."/>
<!-- 环绕通知 -->
<aop:around pointcut-ref="myPointCut" method="..."/>
<!-- 异常通知 -->
<aop:after-throwing pointcut-ref="myPointCut" method="..."/>
<!-- 最终通知 -->
<aop:after pointcut-ref="myPointCut" method="..."/>
....
</aop:aspect>
在 Spring 中,尽管使用 XML 配置文件可以实现 AOP 开发,但是如果所有的相关配置都集中在配置文件中,势必会导致 XML 配置文件过于臃肿,从而给维护和升级带来一定的困难。
为此,AspectJ 框架为 AOP 开发提供了一套注解。AspectJ 允许使用注解定义切面、切入点和增强处理,Spring 框架可以根据这些注解生成 AOP 代理。
关于注解的介绍如表 1 所示。
名称 | 说明 |
---|---|
@Aspect | 用于定义一个切面。 |
@Pointcut | 用于定义一个切入点。 |
@Before | 用于定义前置通知,相当于 BeforeAdvice。 |
@AfterReturning | 用于定义后置通知,相当于 AfterReturningAdvice。 |
@Around | 用于定义环绕通知,相当于MethodInterceptor。 |
@AfterThrowing | 用于定义抛出通知,相当于ThrowAdvice。 |
@After | 用于定义最终final通知,不管是否异常,该通知都会执行。 |
@DeclareParents | 用于定义引介通知,相当于IntroductionInterceptor(不要求掌握)。 |
@Configuration
@EnableAspectJAutoProxy
public class Appconfig {
}
在 XML 文件中添加以下内容启用 @AspectJ。
aop:aspectj-autoproxy
AspectJ 类和其它普通的 Bean 一样,可以有方法和字段,不同的是 AspectJ 类需要使用 @Aspect 注解,如下所示。
package net.biancheng;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class AspectModule {
}
AspectJ 类也可以像其它 Bean 一样在 XML 中配置,如下。
<bean id = "myAspect" class = "net.biancheng.AspectModule">
...
</bean>
@Pointcut 注解用来定义一个切入点,如下。
// 要求:方法必须是private,返回值类型为void,名称自定义,没有参数
@Pointcut("execution(*net.biancheng..*.*(..))")
private void myPointCut() {
}
相当于以下代码
关于 execution 中表达式的使用说明,我们在《AspectJ基于XML开发AOP》一节介绍。
@AspectJ 支持 5 种类型的 advice,以下为使用 @Before 的示例。
@Before("myPointCut()")
public void beforeAdvice(){
...
}
下面使用 Eclipse IDE 演示 AspectJ 基于注解开发 AOP,步骤如下:
Logging 类代码如下。
package net.biancheng;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class Logging {
/**
* 定义切入点
*/
@Pointcut("execution(* net.biancheng.*.*(..))")
private void selectAll() {
}
/**
* 前置通知
*/
@Before("selectAll()")
public void beforeAdvice() {
System.out.println("前置通知");
}
/**
* 后置通知
*/
@After("selectAll()")
public void afterAdvice() {
System.out.println("后置通知");
}
/**
* 返回后通知
*/
@AfterReturning(pointcut = "selectAll()", returning = "retVal")
public void afterReturningAdvice(Object retVal) {
System.out.println("返回值为:" + retVal.toString());
}
/**
* 抛出异常通知
*/
@AfterThrowing(pointcut = "selectAll()", throwing = "ex")
public void afterThrowingAdvice(IllegalArgumentException ex) {
System.out.println("这里的异常为:" + ex.toString());
}
}
Man 类代码如下。
package net.biancheng;
public class Man {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public void throwException() {
System.out.println("抛出异常");
throw new IllegalArgumentException();
}
}
Beans.xml 代码如下。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd ">
<aop:aspectj-autoproxy />
<bean id="man" class="net.biancheng.Man">
<property name="name" value="bianchengbang" />
<property name="age" value="12" />
</bean>
<bean id="logging" class="net.biancheng.Logging" />
</beans>
MainApp 类代码如下。
package net.biancheng;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class MainApp {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml");
Man man = (Man) context.getBean("man");
man.getName();
man.getAge();
man.throwException();
}
}
Spring 针对数据库开发提供了 JdbcTemplate 类,该类封装了 JDBC,支持对数据库的所有操作。
JdbcTemplate 位于 spring-jdbc-x.x.x.jar 包中,其全限定命名为 org.springframework.jdbc.core.JdbcTemplate。此外使用 JdbcTemplate 还需要导入 spring-tx-x.x.x.jar 包,该包用来处理事务和异常。
在 Spring 中,JDBC 的相关信息在配置文件中完成,其配置模板如下所示。
<?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="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<!--数据库驱动-->
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<!--连接数据库的url-->
<property name= "url" value="jdbc:mysql://localhost/xx" />
<!--连接数据库的用户名-->
<property name="username" value="root" />
<!--连接数据库的密码-->
<property name="password" value="root" />
</bean>
<!--配置JDBC模板-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!--默认必须使用数据源-->
<property name="dataSource" ref="dataSource"/>
</bean>
<!--配置注入类-->
<bean id="xxx" class="xxx">
<property name="jdbcTemplate" ref="jdbcTemplate"/>
</bean>
...
</beans>
本节使用 MySQL 数据库,如果您使用的是其它数据库,需要对内容进行相应的修改。
上述代码中定义了 3 个 Bean,分别是 dataSource、jdbcTemplate 和需要注入类的 Bean。其中 dataSource 对应的是 DriverManagerDataSource 类,用于对数据源进行配置;jdbcTemplate 对应 JdbcTemplate 类,该类中定义了 JdbcTemplate 的相关配置。
在 dataSource 中,定义了 4 个连接数据库的属性,如下表所示。
属性名 | 说明 |
---|---|
driverClassName | 所使用的驱动名称,对应驱动 JAR 包中的 Driver 类 |
url | 数据源所在地址 |
username | 访问数据库的用户名 |
password | 访问数据库的密码 |
上表中的属性值需要根据数据库类型或者机器配置的不同进行相应设置。如果数据库类型不同,则需要更改驱动名称。如果数据库不在本地,则需要将 localhost 替换成相应的主机 IP。
在定义 JdbcTemplate 时,需要将 dataSource 注入到 JdbcTemplate 中。而在其他的类中要使用 JdbcTemplate,也需要将 JdbcTemplate 注入到使用类中(通常注入 dao 类中)。
在 JdbcTemplate 类中,提供了大量的查询和更新数据库的方法,如 query()、update() 等,如下表所示。
方法 | 说明 |
---|---|
public int update(String sql) | 用于执行新增、修改、删除等语句 args 表示需要传入到 query 中的参数 |
public int update(String sql,Object… args) | |
public void execute(String sql) | 可以执行任意 SQL,一般用于执行 DDL 语句 action 表示执行完 SQL 语句后,要调用的函数 |
public T execute(String sql, PreparedStatementCallback action) | |
public T query(String sql, ResultSetExtractor rse) | 用于执行查询语句 以 ResultSetExtractor 作为参数的 query 方法返回值为 Object,使用查询结果需要对其进行强制转型 以 RowMapper 作为参数的 query 方法返回值为 List |
下面通过实例演示使用 JdbcTemplate 类操作数据库,步骤如下:
User 类代码如下。
package net.biancheng;
public class User {
private int id;
private String name;
private int age;
public User() {
}
public User(String name, Integer age) {
this.name = name;
this.age = age;
}
// 省略set和get方法
}
UserDao 代码如下。
package net.biancheng;
import java.util.List;
public interface UserDao {
/**
* 初始化User表
*/
void createUserTable();
/**
* 保存用户
*/
void saveUser(User user);
/**
* 查询用户
*/
List<User> listUser();
}
UserDaoImpl 代码如下
package net.biancheng;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
public class UserDaoImpl implements UserDao {
private JdbcTemplate jdbcTemplate;
private UserDao userDao;
public JdbcTemplate getJdbcTemplate() {
return jdbcTemplate;
}
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public UserDao getUserDao() {
return userDao;
}
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public void setDataSource(DataSource datasource) {
this.jdbcTemplate = new JdbcTemplate(datasource);
}
@Override
public void createUserTable() {
this.jdbcTemplate.execute("CREATE TABLE `user` (\r\n" + " `id` int(11) NOT NULL AUTO_INCREMENT,\r\n"
+ " `name` varchar(50) DEFAULT NULL,\r\n" + " `age` int(11) DEFAULT NULL,\r\n"
+ " PRIMARY KEY (`id`)\r\n" + ") ENGINE=MyISAM DEFAULT CHARSET=utf8;");
}
@Override
public void saveUser(User user) {
this.jdbcTemplate.update("INSERT INTO USER(NAME,age) VALUES (?,?)", user.getName(), user.getAge());
}
@Override
public List<User> listUser() {
List<User> users = this.jdbcTemplate.query("SELECT NAME,age FROM USER", new RowMapper<User>() {
public User mapRow(ResultSet rs, int rowNum) throws SQLException {
User user = new User();
user.setName(rs.getString("name"));
user.setAge(rs.getInt("age"));
return user;
}
});
return users;
}
}
Beans.xml 代码如下。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
<!-- 配置数据源 -->
<bean id="dataSource"
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<!--数据库驱动 -->
<property name="driverClassName"
value="com.mysql.jdbc.Driver" />
<!--连接数据库的url -->
<property name="url" value="jdbc:mysql://localhost/test" />
<!--连接数据库的用户名 -->
<property name="username" value="root" />
<!--连接数据库的密码 -->
<property name="password" value="root" />
</bean>
<!--配置JDBC模板 -->
<bean id="jdbcTemplate"
class="org.springframework.jdbc.core.JdbcTemplate">
<!--默认必须使用数据源 -->
<property name="dataSource" ref="dataSource" />
</bean>
<bean id="userdao" class="net.biancheng.UserDaoImpl">
<property name="jdbcTemplate" ref="jdbcTemplate" />
</bean>
</beans>
MainApp 类代码如下。
package net.biancheng;
import java.util.List;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class MainApp {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("Beans.xml");
UserDao dao = (UserDao) ctx.getBean("userdao");
dao.createUserTable();
dao.saveUser(new User("bianchengbang", 12));
dao.saveUser(new User("baidu", 18));
List<User> users = dao.listUser();
for (User user : users) {
System.out.println("姓名:" + user.getName() + "\t年龄:" + user.getAge());
}
}
}