SSM Chapter 06 IoC 和 AOP 使用扩展 笔记
本章目标:
- 理解构造注入
- 理解不同数据类型的注入方法
- 掌握p命名空间注入
- 理解更多增强类型的使用方法
- 掌握使用注解实现IoC的方法
- 掌握使用注解实现AOP的方法
1 . 多种方式实现依赖注入
在前一章中, 我们使用Spring通过setter访问器实现了对属性的赋值,这种做法称为设值注入.除此之外,Spring还提供了通过构造方法赋值的能力,称为构造注入.
1.1 构造注入
问题 : 如何通过构造注入为业务类注入所依赖的数据访问层对象,实现保存用户数据的功能?
分析 : 解决问题的步骤如下 :
- (1) 获取Spring 开发包 并为 工程添加Spring支持
- (2) 为业务层 和 数据访问层 设计接口,声明所需方法
- (3) 编写数据访问层UserDao的实现类,完成具体的持久化操作
- (4) 在业务实现类中声明 UserDao 接口类型的属性 , 并添加适当的构造方法为属性赋值
- (5) 在Spring的配置文件中,将DAO对象以构造注入的方式,赋值给业务实例中的UserDao类型的属性
- (6) 在代码中获取Spring配置文件中装配好的业务类对象,实现程序功能
1. 使用maven创建项目并添加Spring支持
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!--普通java工程 导入spring的核心包 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<!--加入Spring aop依赖的aspectj-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.2</version>
</dependency>
<!--log4j -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<!-- spring4 运行需要依赖于 commons-logging 组件 -->
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
<!--导入 Spring 测试包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
2. 为业务层 和 数据访问层 设计接口,声明所需方法
UserDao.java代码如下:
public interface UserDao {
/**
* 保存用户信息
* @param user
* @return
*/
int save(User user);
}
UserService.java代码如下:
/**
* 业务层
*/
public interface UserService {
void addNewUser(User user);
}
3. 编写数据访问层UserDao的实现类,完成具体的持久化操作
public class UserDaoImpl implements UserDao {
@Override
public int save(User user) {
System.out.println("保存用户信息到数据库");
return 1;
}
}
4. 在业务实现类中声明 UserDao 接口类型的属性 , 并添加适当的构造方法为属性赋值
/**
* 业务实现类
*/
public class UserServiceImpl implements UserService {
private UserDao dao;
//使用设置注入时 Spring 通过JavaBean的无参构造方法实例化对象.为了保证程序使用的灵活性,
//建议添加无参构造
public UserServiceImpl() {
super();
}
//用于为dao属性赋值的构造方法
public UserServiceImpl(UserDao dao) {
this.dao = dao;
}
@Override
public void addNewUser(User user) {
dao.save(user);
}
}
5. 在Spring的配置文件中,将DAO对象以构造注入的方式,赋值给业务实例中的UserDao类型的属性
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 ">
<!-- 定义UserDao对象,并指定id为dao -->
<bean id="dao" class="dao.impl.UserDaoImpl"/>
<!-- 定义UserService的实现类对象,并指定id为service -->
<bean id="service" class="service.impl.UserServiceImpl">
<!-- 通过定义单参数的构造方法 为service的dao属性赋值 -->
<!-- 引用id为dao的对象 为service中的dao属性赋值 -->
<constructor-arg ref="dao"/>
</bean>
</beans>
6. 在代码中获取Spring配置文件中装配好的业务类对象,实现程序功能
测试代码如下:
public class TestUserService {
@Test
public void testAddNewUser(){
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = applicationContext.getBean(UserService.class);
User user = new User();
user.setId(1);
user.setUsername("tom");
user.setPassword("123");
userService.addNewUser(user);
}
}
经验 :
(1) 一个 <constructor-arg >
元素表示构造方法的一个参数,且使用时不区分顺序.当构造方法的参数出现混淆,无法区分时, 可以通过<constructor-arg>
元素的index属性指定该参数位置的索引,位置从0开始.<constructor-arg>
元素还提供了type属性用来指定参数的类型,编码字符串和基本数据类型的混淆.
(2) 构造注入时效性好,在对象实例化时就得到所依赖的对象,便于在对象初始化方法中,使用依赖对象;但受限于方法重载的形式,使用灵活性不足.设值注入使用灵活,但时效性不足,并且大量的setter访问器增加了类的复杂性.Spring并不倾向于某种注入方式,用户可根据实际情况进行合理选择.
当然 , Spring 提供的注入方法,并不止只这两种,只是这两种方式用的最普遍,有兴趣的可以通过Spring的开发手册了解其他注入方式.
Spring的一个最大的目的就是使JAVA EE开发更加容易。同时,Spring之所以与Struts、Hibernate等单层框架不同,是因为Spring致力于提供一个以统一的、高效的方式构造整个应用,并且可以将单层框架以最佳的组合揉和在一起建立一个连贯的体系。
可以说Spring是一个提供了更完善开发环境的一个框架,可以为POJO(Plain Old Java Object)对象提供企业级的服务
1.2 使用p命名空间实现属性注入:
在之前使用的依赖注入的操作模式是最为常见的操作模式,也是在实际开发之中使用最多的形式,但是从Spring 2.x版本之后开始增加了另外一种操作形式,称为p命名空间操作
p命名空间的特点:使用属性而不是子元素的形式配置Bean的属性,从而简化了配置代码.
语法:
- 对于直接量(基本数据类型、字符串)属性:p:属性名=”属性值”
- 对于引用Bean的属性:p:属性名-ref=”Bean的id”
使用传统的<property>
子元素配置的代码如下:
<?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="user" class="entity.User">
<property name="id" value="1"/>
<property name="username" value="jack"/>
<property name="password" value="123"/>
</bean>
<bean id="dao" class="dao.impl.UserDaoImpl"/>
<bean id="service" class="service.impl.UserServiceImpl">
<property name="dao" ref="dao" />
<!--<constructor-arg type="dao.UserDao" ref="dao" />
<constructor-arg type="java.lang.String" value="hello,world"/>-->
</bean>
</beans>
使用p命名空间改进配置,注意使用前先添加p命名空间的声明.关键代码如下:
<?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:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd ">
<bean id="user" class="entity.User" p:id="1" p:username="jack"
p:password="123" />
<bean id="dao" class="dao.impl.UserDaoImpl"/>
<bean id="service" class="service.impl.UserServiceImpl" p:dao-ref="dao" />
</beans>
通过对比可以看出,使用p命名空间简化配置效果很明显.其使用方式可以总结如下:
对于直接量(基本数据类型,字符串)属性,使用方式如下:
- 对于直接量(基本数据类型、字符串)属性:p:属性名=”属性值”
- 对于引用Bean的属性:p:属性名-ref=”Bean的id”
1.3 使用c命名空间实现属性注入:
Spring3.1中引入c命名空间 . c命名空间的用法和p命名空间类似,其对应于constructor-arg,即可以将constructor-arg元素替换为bean的一个以c命名空间前缀开始的属性。使用c命名空间之前也需要通过xmlns:c=”http://www.springframework.org/schema/c”
进行声明。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:c="http://www.springframework.org/schema/c"
xmlns:aop="http://www.springframework.org/schema/aop"
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/aop
https://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 传统的使用constructor-arg通过构造方法注入的bean定义 -->
<bean id="user1" class="entity.User">
<constructor-arg index="0" value="1"/>
<constructor-arg index="1" value="张三"/>
<constructor-arg index="2" value="1234"/>
</bean>
<!-- 使用c命名空间通过构造方法注入的bean定义 -->
<bean id="user2" class="entity.User" c:_0="2" c:username="李四" c:_2="lisi"/>
</beans>
如上所示,c命名空间的用法和p命名空间的用法类似。
对于通过构造方法注入原始类型的对象,可以把对应的构造参数名称加上c命名空间的前缀作为bean的一个属性进行定义,对应的值即是构造参数的值;
如果通过构造参数注入的是其它bean的一个引用,则可将该构造参数名称加上“-ref”,再加上c命名空间的前缀作为该bean的一个属性进行定义,如:**c:_2-ref=""**
或者 **c:username-ref=""**
。
1.4 注入不同数据类型:
Spring 提供了不同的标签来实现各种不同类型参数的注入,这些标签对于设值注入和构造注入都适用.对于构造注入,只需将所介绍的标签添加到<constructor-arg>
与</constructor-arg>
中间即可.
1. 注入直接量(基本数据类型 字符串):
对于基本数据类型 及其 包装类 字符串,除了可以使用value属性,还可以通过<value>
子元素注入,关键代码如下:
<bean id="user" class="entity.User">
<property name="id">
<value>1</value>
</property>
<property name="username">
<value>张三</value>
</property>
<property name="password">
<value>zhangsan</value>
</property>
</bean>
如果属性值中包含了 XML 中的特殊字符 (**&**
,**<**
,**>**
,**"**
,**'**
),则注入时需要进行处理,通常可以采用两种办法:使用**<![CDATA[]]>**
标记或把特殊字符替换为实体引用.关键代码如下:
<bean id="user" class="entity.User">
<property name="id">
<value>1</value>
</property>
<property name="username">
<value><![CDATA[P&G]]></value>
</property>
<property name="password">
<value>zhangsan</value>
</property>
</bean>
<!-- 把XML特殊字符替换为实体引用 -->
<bean id="user1" class="entity.User">
<property name="id">
<value>1</value>
</property>
<property name="username">
<value>P&G</value>
</property>
<property name="password">
<value>zhangsan</value>
</property>
</bean>
在XML中有5个预定义的实体引用,如表所示
符号 | 实体引用 |
---|---|
< |
< |
> |
> |
& |
& |
' |
' |
" |
" |
注意:
严格的来讲,在XML中仅有字符
"<"
和"&"
是非法的,其他3个符号是合法的,但是把他们替换为实体引用是个好习惯.
2. 引用其他Bean组件
Spring 中定义的Bean可以互相引用,从来建立依赖关系,除了使用ref属性,还可以通过<ref>
子元素实现,关键代码如下:
<bean id="dao" class="dao.impl.UserDaoImpl"/>
<bean id="service" class="service.impl.UserServiceImpl">
<!-- 为service的dao属性赋值,需要注意的是这里需要调用 setDao()方法 -->
<property name="dao">
<!-- 引用id为dao的对象 为service的dao属性赋值 -->
<ref bean="dao"/>
</property>
</bean>
<ref>
标签中的bean属性用来指定要引用的Bean的id.除了bean属性,这里再为大家介绍local属性.关键代码如下:
<bean id="dao" class="dao.impl.UserDaoImpl"/>
<bean id="service" class="service.impl.UserServiceImpl">
<!-- 为service的dao属性赋值,需要注意的是这里需要调用 setDao()方法 -->
<property name="dao">
<!-- 引用id为dao的对象 为service的dao属性赋值 -->
<ref local="dao"/>
</property>
</bean>
使用ref中的local属性时,程序报错,报错信息是:Attribute local is not allowed here.
通过查询Spring的官方文档,分析错误原因是Spring4.X的以上版本不支持该属性了。 下面是官方说明:
The local attribute on the ref element is no longer supported in the 4.0 beans xsd since it does not provide value over a regular bean reference anymore. Simply change your existing ref local references to ref bean when upgrading to the 4.0 schema.
官方建议使用bean在Spring4.0以上的版本。
至于在Spring 4.0 以下,local属性与bean属性的用法似乎是一致的,都是用来指定要引用的Bean的id.
它们的区别在于:Spring 的配置文件可以拆分多个,使用local属性只能在同一个配置文件中检索Bean的id,而使用bean属性可以在其他配置文件中检索id.
综上,ref中的local属性有很大的局限性,所以在Spring 4.X 之后的版本,就不再支持这个属性了
3. 使用内部Bean
如果一个Bean组件仅在一处需要使用 , 可以把定义为内部Bean.关键代码如下:
<bean id="service" class="service.impl.UserServiceImpl">
<!-- 为service的dao属性赋值,需要注意的是这里需要调用 setDao()方法 -->
<property name="dao">
<!-- 定义UserDao 对象 -->
<bean class="dao.impl.UserDaoImpl"/>
</property>
</bean>
这样,这个 UserDaoImpl 类型的Bean就只能被service使用,无法被其他Bean引用
4. 注入集合类型属性
对于List 或 数组类型 的属性, 可以使用<list>
标签注入.User类中增加List<String>
hobbies 属性, 关键代码如下:
<bean id="user" class="entity.User">
<property name="hobbies">
<list>
<!-- 定义list 或 数组中的元素 -->
<value>足球</value>
<value>篮球</value>
<value>排球</value>
</list>
</property>
</bean>
<list>
标签中间可以使用 <value>
,<ref>
等标签注入集合元素,甚至是另一个<list>
标签;
对于Set类型的属性,可以使用 <set>
标签注入.将User类中List<String>
hobbies 属性的数据结构改为Set<String>
关键代码如下:
<bean id="user" class="entity.User">
<property name="hobbies">
<set>
<!-- 定义list 或 数组中的元素 -->
<value>足球</value>
<value>篮球</value>
<value>排球</value>
</set>
</property>
</bean>
<set>
标签中间也可以使用<value>
,<ref>
等标签注入集合元素
对于Map类型的属性, 可以<map>
标签方式注入,修改User类中的Set<String>
hobbies 属性的数据结构为Map<String,String>
, 关键代码如下:
<bean id="user" class="entity.User">
<property name="hobbies">
<map>
<!-- 定义Map中的键值对 -->
<entry key="football" value="足球"/>
<entry>
<key><value>basketball</value></key>
<value>篮球</value>
</entry>
</map>
</property>
</bean>
如果Map中的键或值是Bean对象,可以上面代码中的<value>
元素换成<ref>
或者将value属性换成ref属性;
对于Properties类型的属性,可以使用<props>
元素注入.将User中hobbies属性的类型 修改为 Properties类型,关键代码如下:
<bean id="user" class="entity.User">
<property name="hobbies">
<props>
<!-- 定义Properties中的键值对 -->
<prop key="footlball">足球</prop>
<prop key="basketball">篮球</prop>
</props>
</property>
</bean>
Properties中的键和值 通常都是字符串类型.
5. 注入null 和 空字符串 值
可以使用<value></value>
注入空字符串值, 使用 <null/>
注入null值.关键代码如下:
<bean id="user2" class="entity.User">
<!--使用 <value></value> 注入空字符串,相当于 username="" -->
<property name="username">
<value></value>
</property>
<!--使用 <null/> 注入null值,相当于 password=null -->
<property name="password">
<null/>
</property>
</bean>
6. 扩展Springutil
标签的使用:
首先在Spring的配置文件中添加util
命名空间 , 代码如下:
<?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:p="http://www.springframework.org/schema/p"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util.xsd">
</beans>
作用如下:
分别使用
<util:list>
、<util:map>
、<util:set>
、<util:properties>
等标签。用它来取代ListFactoryBean、MapFactoryBean、SetFactoryBean、PropertiesFactoryBean。
其中的
<util:properties>
标签可以通过location属性指定其properties属性文件的位置。
示例如下:
<util-list>
演示示例如下:<bean id="user" class="entity.User"> <property name="hobbies" ref="list"/> </bean> <util:list id="list" value-type="java.lang.String" list-class="java.util.ArrayList"> <!-- 定义list 或 数组中的元素 --> <value>篮球</value> <value>排球</value> <value>冰球</value> </util:list>
<util-set>
演示示例如下: ```xml
- `<util-map>` 演示如下:
```xml
<bean id="user" class="entity.User">
<property name="hobbies" ref="map"/>
</bean>
<util:map id="map" key-type="java.lang.String" value-type="java.lang.Object"
map-class="java.util.HashMap">
<!-- 定义Map中的键值对 -->
<entry key="CN" value="中国"/>
<entry key="USA" value="美国"/>
<entry key="AUS" value="澳大利亚"/>
</util:map>
<util-properties>
演示如下:<bean id="user" class="entity.User"> <property name="hobbies" ref="properties"/> </bean> <util:properties id="properties"> <!-- 定义Properties中的键值对 --> <prop key="footlball">足球</prop> <prop key="basketball">篮球</prop> </util:properties>
<util:properties/>
元素的另一种用法 , 可以加载属性文件,示例如下:<util:properties id="xxx" location="classpath:xxxxx.properties">
"classpath":
表明,将从类路径上查找并装载xxx属性文件.总结 : spring util-命名空间中的元素 | 元素 | 描述 | | —- | —- | |
<util:constant>
| 引用某个类型public static 域,并将其暴露为bean | |<util:list>
| 创建一个java.util.list类型的bean,其中包含值或者引用 | |<util:map>
| 创建一个java.util.map类型的bean,其中包含值或者引用 | |<util:properties>
| 创建一个java.util.properties类型的bean | |<util:property-path>
| 引用一个属性(或内嵌属性),并将其暴露为bean | |<util:set>
| 创建一个java.util.set类型的bean,其中包含值或者引用 |
2 . 其他增强类型
Spring 支持多种增强类型,除了之前介绍的前置增强和后置增强,这里在补充介绍几种常用的增强类型
2.1 异常抛出增强
异常抛出增强的特点是 在目标方法抛出异常时 织入增强处理. 使用异常抛出增强,可以为各功能模块提供统一的,可以拔插的异常处理方案. 实现异常抛出增强的代码如下:
/**
* 定义包含增强的JavaBean
*/
public class ErrorLogger {
private final Logger logger = Logger.getLogger(this.getClass());
public void afterThrowing(JoinPoint joinPoint,RuntimeException e){
logger.error("方法名是:"+joinPoint.getSignature().getName() +"," +
"方法抛出的异常是:"+e);
}
}
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"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd ">
<bean id="dao" class="dao.impl.UserDaoImpl"/>
<bean id="service" class="service.impl.UserServiceImpl">
<property name="dao" ref="dao" />
</bean>
<!-- 声明增强方法所在的Bean -->
<bean id="errorLogger" class="aop.ErrorLogger"/>
<!-- 配置切面 -->
<aop:config>
<!-- 定义切入点 -->
<aop:pointcut id="pointcut"
expression="execution(* service.UserService.*(..))"/>
<!-- 引用包含增强方法的Bean -->
<aop:aspect ref="errorLogger">
<!-- 将afterThrowing() 方法 定义为异常抛出增强 并引用pointcut 切入点 -->
<!-- 通过 throwing属性 指定 名为 e 的参数注入异常实例 -->
<aop:after-throwing method="afterThrowing"
pointcut-ref="pointcut" throwing="e"/>
</aop:aspect>
</aop:config>
</beans>
修改UserDaoImpl.java 中save()方法, 方法抛出异常,测试异常抛出增强是否执行
public class UserDaoImpl implements UserDao {
@Override
public int save(User user) {
// 这里并未实现完整的数据库操作,仅为说明问题
System.out.println("保存用户信息到数据库");
throw new RuntimeException("为测试程序运行效果抛出的异常");
}
}
运行测试代码,控制台抛出异常,并且执行了自定义的异常抛出增强处理的方法
总结 :
使用<aop:after-throwing>
元素可以定义异常抛出增强.如果需要获取抛出的异常,可以为增强方法声明相关类型的参数,并通过<aop:after-throwing>
元素的throwing 属性 指定该类型参数名称,Spring 会自动为其注入从目标方法抛出的异常实例.
2.2 最终增强
最终增强的特点是:无论该方法抛出异常还是正常退出,该增强都会得到执行,其作用类似与异常处理机制中finally块的作用,一般用于释放资源.使用最终增强,就可以为各功能模块提供统一的,可拔插的处理方案.实现最终增强的代码如下:
定义包含增强方法的JavaBean:
/**
* 定义包含增强方法的JavaBean
*/
public class AfterLogger {
private final Logger logger = Logger.getLogger(this.getClass());
public void afterLogger(JoinPoint joinPoint){
logger.info(joinPoint.getSignature().getName() + " 方法结束执行.");
}
}
修改Spring 的配置文件,关键代码如下:
<!-- 声明增强方法所在的Bean -->
<bean id="afterLogger" class="aop.AfterLogger"/>
<!-- 配置切面 -->
<aop:config>
<!-- 定义切入点 -->
<aop:pointcut id="pointcut" expression="execution(* service.UserService.*(..))"/>
<!-- 引用包含增强方法的Bean -->
<aop:aspect ref="afterLogger">
<!-- 将afterLogger() 方法 定义为最终增强 并引用pointcut 切入点 -->
<aop:after method="afterLogger" pointcut-ref="pointcut" />
</aop:aspect>
</aop:config>
使用<aop:after>
元素即可定义最终增强
2.3 环绕增强
环绕增强 在 目标方法的前后都可以织入增强处理 . 环绕增强是功能最大的增强处理,Spring把目标方法的控制权全部交给了它. 在环绕增强处理中, 可以获取或修改目标方法的参数,返回值, 可以对它进行异常处理,甚至可以决定目标方法是否被执行,实现环绕增强的代码如下:
定义包含环绕增强方法的JavaBean:
/**
* 定义包含环绕增强的JavaBean
*/
public class AroundLogger {
private final Logger logger = Logger.getLogger(this.getClass());
public Object aroundLogger(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
logger.info("调用:"+proceedingJoinPoint.getSignature().getName()+
"方法之前," +
"方法入参是:"+Arrays.toString(proceedingJoinPoint.getArgs()));
try {
//int i = 3/0;
//执行目标方法 并获取其返回值
Object result = proceedingJoinPoint.proceed();
logger.info("调用 "+proceedingJoinPoint.getTarget()+"的"+proceedingJoinPoint.getSignature().getName()+"方法,方法的返回值是:"+result);
return result;
}catch (Throwable e){
logger.error(proceedingJoinPoint.getSignature().getName()+
"方法抛出异常,异常信息是:"+e);
throw e;
}finally {
logger.info(proceedingJoinPoint.getSignature().getName()+"方法结束执行");
}
}
}
Spring 配置文件中的关键代码如下:
<!-- 声明增强方法所在的Bean -->
<bean id="aroundLogger" class="aop.AroundLogger"/>
<!-- 配置切面 -->
<aop:config>
<!-- 定义切入点 -->
<aop:pointcut id="pointcut" expression="execution(* service.UserService.*(..))"/>
<!-- 引用包含增强方法的Bean -->
<aop:aspect ref="aroundLogger">
<!-- 将aroundLogger() 方法 定义为最终增强 并引用pointcut 切入点 -->
<aop:around method="aroundLogger" pointcut-ref="pointcut" />
</aop:aspect>
</aop:config>
使用**<aop:around>**
元素可以定义环绕增强,通过为增强方法声明 ProceedingJoinPoint类型的参数,可以获得连接点信息,所用方法与JoinPoint相同.ProceedingJoinPoint 是JoinPoint 的子接口.其不但封装目标方法与入参数组,还封装了被代理的目标对象,通过它的proceed() 方法 可以调用 真正的目标方法,从而达到对连接点的完全控制.
3 . 使用注解实现 IoC的配置
之前学习了多种和Spring IoC有关的配置技巧,这些技巧都是基于XML形式的配置文件进行的.除了XML形式的配置文件,Spring 从 2.0 版本开始引入注解的配置方式,将Bean 的配置信息 和Bean实现类结合在一起,进一步减少配置文件的代码量.
3.1 使用注解定义Bean
我们可以在JavaBean 中通过注解实现Bean组件的定义.其配置方式如下:
/**
* 用户 DAO 类, 实现UserDao 接口,负责User类的持久化操作
*/
//通过注解定义了一个DAO
@Component("userDao")
public class UserDaoImpl implements UserDao {
@Override
public int save(User user) {
// 这里并未实现完整的数据库操作,仅为说明问题
System.out.println("保存用户信息到数据库");
return 1;
}
}
以上代码通过注解定义了一个名为userDao 的 Bean. @Component(“userDao”) 的作用与在XML配置文件中编写<bean id="userDao" class="dao.impl.UserDaoImpl"/>
等效.
除了@Compont,Spring还提供了3个特殊的注解:
- @Repository : 用于标注 DAO 类.
- @Service : 用于标注业务类
- @Controller : 用于标注控制器类
使用特定的注解 使组件的用途更加清晰,并且Spring在以后的版本中可能会为它们添加特殊的功能,所以推荐使用特定的注解来标注特定的实现类
3.2 使用注解实现 Bean 组件装配:
Spring提供了@Autowired 注解 实现Bean的装配,关键代码如下:
/**
* 用户业务类,实现对User功能的业务管理
*/
@Service("userService")
public class UserServiceImpl implements UserService {
//声明接口类型的引用 和 具体实现类解耦合
@Autowired
private UserDao dao;
@Override
public int addNewUser(User user) {
dao.save(user);
return 1;
}
}
以上代码通过@Service 标注了一个业务Bean,并使用@Autowired 为 dao属性注入依赖的对象,Spring将直接对dao属性进行赋值,此时类中可以省略相关的setter方法;
@Autowired 采用按类型匹配的方式 为 属性自动装配合适的依赖对象,即容器会查找和属性类型相匹配的Bean组件,并自动为属性注入.有关Spring 自动装配的详细内容将在后续章节中介绍.
若容器中有一个以上类型相匹配的Bean时,则可以使用@Qualifier指定所需的Bean的名称,关键代码如下:
/**
* 用户业务类,实现对User功能的业务管理
*/
@Service("userService")
public class UserServiceImpl implements UserService {
//声明接口类型的引用 和 具体实现类解耦合
@Autowired
@Qualifier("userDao")
private UserDao dao;
@Override
public int addNewUser(User user) {
dao.save(user);
return 1;
}
}
3.3 加载注解定义的Bean
使用注解定义完Bean组件,接下来就可以使用注解的配置信息启动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"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd ">
<!--扫描包中注解标注的类 -->
<context:component-scan base-package="dao,service,entity" />
</beans>
在以上代码中,首先在Spring 配置文件中 添加 对 context 命名空间的声明,然后使用context命名空间下的component-scan 标签扫描注解标注的类. base-package 属性指定了需要扫描的基准包(多个包名之间可用逗号隔开).Spring 会 扫描这些包中所有的类,获取Bean的定义信息.
测试代码如下:
@Test
public void testAddNewUser(){
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = applicationContext.getBean(UserService.class);
User user = applicationContext.getBean(User.class);
logger.info(user);
userService.addNewUser(user);
}
可以使用注入进行测试,代码如下:
/**
* 测试类
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class TestUserService {
private Logger logger = Logger.getLogger(this.getClass());
@Autowired
private User user;
@Autowired
private UserService userService;
@Test
public void testAddNewUser(){
logger.info(user);
userService.addNewUser(user);
}
}
注意:使用@Autowired 注解进行装配时,如果找不到向匹配的Bean组件,Spring容器会抛出异常.因此如果依赖不是必需的,为避免抛出异常,可以将required属性设置为false,required属性默认为true,即必须找到匹配的Bean完成装配,否则抛出异常;
3.4 使用 Java 标准注解完成装配
除了提供@Autowired 注解,Spring 还支持使用JSR-250中定义的@Resource注解实现组件装配.该标准注解也能对类的成员变量 或者 方法入参提供注入功能
说明:JSR 全称是 Java Specification Requests, 即 Java 规范提案.Java的版本和功能在不断的更新和扩展,JSR就是用来规范这些功能和接口的标准,已经称为Java业界的一个重要的标准.
@Resource 有一个name属性,默认情况下,Spring 将这个属性的值解释为要注入的Bean的名称,用法如下:
@Service
public class UserServiceImpl implements UserService {
//为dao属性 注入 名为 userDao的Bean
@Resource(name = "userDao")
private UserDao dao;
@Override
public int addNewUser(User user) {
dao.save(user);
return 1;
}
}
如果没有显式指定Bean的名称,@Resource注解将会根据字段名 或者 setter 方法名中产生默认的名称:如果注解应用于字段,将使用字段名作为Bean的名称;如果注解应用于setter方法,Bean名称就是通过setter方法得到属性名.
查找名为 dao 的Bean,并注入给dao属性 代码如下:
@Service
public class UserServiceImpl implements UserService {
//查找名为 dao 的Bean,并注入给dao属性
@Resource
private UserDao dao;
@Override
public int addNewUser(User user) {
dao.save(user);
return 1;
}
}
查找名为 userDao 的Bean,并注入给setter方法. 代码如下:
@Service
public class UserServiceImpl implements UserService {
private UserDao dao;
@Override
public int addNewUser(User user) {
dao.save(user);
return 1;
}
@Resource//查找名为userDao的Bean,并注入给setter方法
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
}
如果没有显式指定Bean名称,且无法找到与默认Bean名称匹配的Bean组件,@Resource注解会由按名称查找的方式自动 变为按类型匹配的方式进行装配. 例如上一个示例中没有显式指定要查找的Bean名称,且如果不存在名为dao的Bean组件,@Resource 注解就会转而查找和属性类型相匹配的Bean组件并注入
注意
使用@Resource
注解是跟jdk版本有关系的,对于jdk9及其以上的版本,若使用@Resource
注解时,会抛出空指针异常.解决方式在pom.xml文件加入下面的依赖:
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
4 . 使用注解定义切面
4.1 AspectJ 简介:
AspectJ 是一个面向切面的框架,它扩展了Java语言,定义了AOP语法,能够在编码的织入,所以它有一个专门的编译器用来生成遵守字节码编码规范的Class文件.
@AspectJ 是AspectJ 5 新增的功能,使用JDK 5.0 注解技术 和 正规的 AspectJ 切点表达式语言描述切面. 因此在使用@AspectJ之前,需要保证JDK的版本是5.0及其以上,否则无法使用注解技术.
Spring 通过集成 AspectJ实现了以注解的方式定义切面,大大减少了配置文件的工作类.此外 因为 Java的反射机制无法获取方法参数名,Spring还需要利用轻量级的字节码处理框架asm(已集成在Spring Core 模块中) 处理@AspectJ 中所描述的方法参数名.
解释: ASM : 是一个 Java 字节码操控框架。它能够以二进制形式修改已有类或者动态生成类。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类.
了解了AspectJ,接下来就可以开始编写基于@AspectJ 注解的切面了.
4.2 使用注解标注切面
问题: 在Spring中如何使用注解来实现日志切面?
分析:解决问题步骤如下:
(1) 使用注解定义前置增强 和 后置增强 实现日志功能
(2) 编写Spring 配置文件,完成切面织入
1. 使用注解定义切面以实现日志功能,代码如下:
/**
* 使用注解定义切面
*/
@Component//此注解相当于配置文件中的<bean id="userServiceLogger" class="aop.UserServiceLogger"/>
@Aspect//此注解相当于Spring配置文件中的<aop:aspect ref="userServiceLogger">
public class UserServiceLogger {
private final Logger logger = Logger.getLogger(this.getClass());
@Before("execution(* service.UserService.*(..))")//定义前置增强的方法与配置文件中的<before/>一致
public void before(JoinPoint joinPoint){
logger.info("调用:"+joinPoint.getTarget()+"对象的"+joinPoint.getSignature().getName()+"方法.方法入参是:"+joinPoint.getArgs());
}
@AfterReturning(pointcut = "execution(* service.UserService.*(..))",
returning = "result")//定义后置增强的方法 与配置文件中的<after-returning/>一致
public void afterReturning(JoinPoint joinPoint,Object result){
logger.info("调用:"+joinPoint.getTarget()+"的"+joinPoint.getSignature().getName()+"方法,方法的返回值是:"+result);
}
}
在上述代码中 :
- 使用@AspectJ 注解 将 UserServiceLogger定义为切面,
- 使用@Compont注解注入到Spring的Bean容器中,
- 并使用@Before注解将before()方法 定义为前置增强,
- 使用@AfterReturning 注解将 afterReturning()方法定义为后置增强.
- 为了能够获得当前连接点的信息,在增强方法中添加了 JoinPoint 类型的参数,Spring会自动注入该实例,对于后置增强,还可以定义一个参数用于接收目标方法的返回值.
- 需要注意的是:必须在@AfterReturning注解中 通过returning属性指定该参数的名称,Spring会将目标方法的返回值赋值给指定名称的参数.
分析上述代码,@Before 注解 和@AfterReturning注解分别指定了各自的切入点为 UserService接口中的所有方法.而对于相同的切入点,可以统一定义,以便于重用和维护.代码如下:
/**
* 使用注解定义切面
*/
@Component//此注解相当于配置文件中的<bean id="userServiceLogger" class="aop.UserServiceLogger"/>
@Aspect//此注解相当于Spring配置文件中的<aop:aspect ref="userServiceLogger">
public class UserServiceLogger {
private final Logger logger = Logger.getLogger(this.getClass());
//通过自定义方法简化切入点表达式
@Pointcut("execution(* service.UserService.*(..))")
public void pointcut(){}
@Before("pointcut()")//定义前置增强的方法与配置文件中的<before/>一致
public void before(JoinPoint joinPoint){
logger.info("调用:"+joinPoint.getTarget()+"对象的"+joinPoint.getSignature().getName()+"方法.方法入参是:"+ Arrays.toString(joinPoint.getArgs()));
}
@AfterReturning(pointcut = "pointcut()",returning = "result")
//定义后置增强的方法 与配置文件中的<after-returning/>一致
public void afterReturning(JoinPoint joinPoint,Object result){
logger.info("调用:"+joinPoint.getTarget()+"的"+joinPoint.getSignature().getName()+"方法,方法的返回值是:"+result);
}
}
切入点表达式 使用 @Pointcut 注解来表示,而切入点的签名则需要通过一个普通的方法 定义来提供,如上述代码中的 pointcut() 方法, 作为切入点签名的方法必须返回void 类型.切入点定义好之后,就可以使用 "pointcut()"
签名进行引用
2. 切面定义完后,还需要在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"
xmlns:aop="http://www.springframework.org/schema/aop"
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/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd ">
<!--扫描包中注解标注的类 -->
<context:component-scan base-package="dao,service,entity,aop" />
<!-- 启用 Spring对于@AspectJ注解的支持 -->
<aop:aspectj-autoproxy />
</beans>
配置文件中首先需要导入 aop 命名空间. 只需在配置文件中添加 **<aop:aspectj-autoproxy />**
元素 就可以启用对@AspectJ 注解的支持,Spring 将为自动匹配的Bean创建代理.
编写测试代码,控制台正确输出结果.
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class TestConfigUser {
@Autowired
private User user;
@Autowired
private UserService userService;
private final Logger logger = Logger.getLogger(this.getClass());
@Test
public void testAddNewUser(){
logger.info(user);
userService.addNewUser(user);
}
}
> 扩展(使用配置类):
将Spring 配置文件中的 配置信息 定义成一个普通的Java类,代码如下:
//此注解相当于beans标签
@Configuration
//此注解的作用相当于:配置文件中的<context:component-scan>元素
@ComponentScan(basePackages={"entity","dao","service","aop"})
@EnableAspectJAutoProxy//此注解的作用是 启用 Spring对@Aspect注解的支持
public class AppConfig{
}
增加测试类,代码如下:
@RunWith(SpringJUnit4ClassRunner.class)
//使用classes属性 加载配置类信息
@ContextConfiguration(classes = AppConfig.class)
public class TestUserService {
private Logger logger = Logger.getLogger(this.getClass());
@Autowired
private User user;
@Autowired
private UserService userService;
@Test
public void testAddNewUser(){
logger.info(user);
userService.addNewUser(user);
}
}
运行测试代码,控制台正确输出结果.
4.3 使用注解定义其他类型的增强
> 定义异常抛出增强,代码如下:
/**
* 定义包含增强的JavaBean
*/
@Component
@Aspect
public class ErrorLogger {
private final Logger logger = Logger.getLogger(this.getClass());
@AfterThrowing(pointcut = "execution(* service.UserService.*(..))",
throwing = "e")
public void afterThrowing(JoinPoint joinPoint,RuntimeException e){
logger.error("方法名是:"+joinPoint.getSignature().getName() +",方法抛出的异常是:"+e);
}
}
使用@AfterThrowing 注解可以定义异常抛出增强.如果需要获取抛出的异常,可以为增强方法声明相关类型的参数,并通过@AfterThrowing 注解中的 throwing 属性指定该参数名称,Spring 会为其注入从目标方法抛出的异常实例.
> 定义最终增强 , 代码如下:
/**
* 定义包含最终增强方法的JavaBean
*/
@Component
@Aspect
public class AfterLogger {
private final Logger logger = Logger.getLogger(this.getClass());
@After("execution(* service.UserService.*(..))")
public void afterLogger(JoinPoint joinPoint){
logger.info(joinPoint.getSignature().getName() + " 方法结束执行.");
}
}
使用@After 注解可以定义最终增强
> 定义环绕增强 , 代码如下:
/**
* 定义包含环绕增强的JavaBean
*/
@Component
@Aspect
public class AroundLogger {
private final Logger logger = Logger.getLogger(this.getClass());
@Around("execution(* service.UserService.*(..))")
public Object aroundLogger(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
logger.info("环绕增强 调用:"+proceedingJoinPoint.getSignature().getName()+
"方法之前," +
"方法入参是:"+ Arrays.toString(proceedingJoinPoint.getArgs()));
try {
//执行目标方法 并获取其返回值
Object result = proceedingJoinPoint.proceed();
logger.info("环绕增强 调用 "+proceedingJoinPoint.getTarget()+"的"+proceedingJoinPoint.getSignature().getName()+"方法,方法的返回值是:"+result);
return result;
}catch (Throwable e){
logger.error("环绕增强中的 "+proceedingJoinPoint.getSignature().getName()+
"方法抛出异常,异常信息是:"+e);
throw e;
}finally {
logger.info("环绕增强中的 "+proceedingJoinPoint.getSignature().getName()+
"方法结束执行");
}
}
}
使用@Around 注解可以定义环绕增强.通过为增强方法声明 ProceedingJoinPoint 类型的参数,可以获得连接点信息. 通过它的proceed() 方法可以调用真正的目标方法,从而实现对连接点的完全控制.
4.4 Spring的切面配置小结
Spring 在同一个问题上提供了多种灵活选择,反倒容易令初学者感到迷惑.我们应该根据项目的具体情况做出选择:如果项目采用 JDK 5.0 以上的版本,可以考虑使用@AspectJ 注解方式,减少配置工作量 ;
如果不愿意使用注解或项目采用的JDK版本较低而无法使用注解,则可以选择使用<aop:aspect>
配合JavaBean的形式.