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支持

  1. <dependency>
  2. <groupId>junit</groupId>
  3. <artifactId>junit</artifactId>
  4. <version>4.12</version>
  5. <scope>test</scope>
  6. </dependency>
  7. <!--普通java工程 导入spring的核心包 -->
  8. <dependency>
  9. <groupId>org.springframework</groupId>
  10. <artifactId>spring-context</artifactId>
  11. <version>5.1.5.RELEASE</version>
  12. </dependency>
  13. <!--加入Spring aop依赖的aspectj-->
  14. <dependency>
  15. <groupId>org.aspectj</groupId>
  16. <artifactId>aspectjweaver</artifactId>
  17. <version>1.9.2</version>
  18. </dependency>
  19. <!--log4j -->
  20. <dependency>
  21. <groupId>log4j</groupId>
  22. <artifactId>log4j</artifactId>
  23. <version>1.2.17</version>
  24. </dependency>
  25. <!-- spring4 运行需要依赖于 commons-logging 组件 -->
  26. <dependency>
  27. <groupId>commons-logging</groupId>
  28. <artifactId>commons-logging</artifactId>
  29. <version>1.2</version>
  30. </dependency>
  31. <!--导入 Spring 测试包-->
  32. <dependency>
  33. <groupId>org.springframework</groupId>
  34. <artifactId>spring-test</artifactId>
  35. <version>5.1.5.RELEASE</version>
  36. </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&amp;G</value>
  </property>
  <property name="password">
    <value>zhangsan</value>
  </property>
</bean>

在XML中有5个预定义的实体引用,如表所示

符号 实体引用
< &lt;
> &gt;
& &amp;
' &apos;
" &quot;

注意:

严格的来讲,在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个特殊的注解:

使用特定的注解 使组件的用途更加清晰,并且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的形式.