AI 摘要

文章系统梳理 SSM 框架中 IoC 与 AOP 的扩展用法:先以 XML 方式演示异常抛出、最终、环绕等五种增强,并对比前置、后置场景;随后展开构造注入、p 命名空间、集合等依赖注入技巧,比较 setter 与构造优劣;继而用 @Component、@Service、@Autowired、@Resource 等注解简化 IoC,结合 @Aspect、@Around 等注解实现零配置 AOP,给出完整示例与最佳实践,帮助开发者在企业轻量级项目中灵活选用配置方式。

增强类型扩展

异常抛出增强

异常抛出增强是指当目标方法抛出异常时进行织入操作的一种增强方式,常用于为项目提供统一的异常处理功能,具有可灵活插拔的特点。


示例:异常抛出增强

用户模块业务逻辑接口实现类(cn.duozai.dao.UserServiceImpl):

public class UserServiceImpl implements UserService {

    // ...

    /**
     * 保存用户信息
     *
     * @return 保存结果
     */
    @Override
    public boolean save() {
        // 调用userDao的方法保存用户信息
        int result = userDao.save();

        // 模拟产生异常
        System.out.println(1/0);
      
        return result > 0;
    }

}

日志增强处理类(cn.duozai.aop.UserServiceLogger):

public class UserServiceLogger {

    // ...
  
    /**
     * 异常抛出增强:方法抛出异常时做增强
     *
     * @param jp 连接点对象
     * @param ex 具体异常对象
     * @return void
     */
    public void afterThrowing(JoinPoint jp, Exception ex) {
        logger.debug(jp.getSignature().getName() + "方法抛出异常: " + ex.getMessage());
    }

}

Spring 核心配置文件(classpath:applicationContext.xml):

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns: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">

    <!-- ... -->

    <aop:config>
        <!-- ... -->

        <aop:aspect ref="userServiceLogger">
            <!-- ... -->

            <!--
                <aop:after-throwing>标签:定义异常抛出增强,在方法抛出异常时做额外操作
                throwing属性:指定为获取异常对象并传递给增强方法中的ex参数
            -->
            <aop:after-throwing method="afterThrowing" pointcut-ref="pointcut" throwing="ex"/>
        </aop:aspect>
    </aop:config>

</beans>

示例效果:


最终增强

最终增强是指无论目标对象的方法正常运行还是抛出异常,该增强处理都会被执行的一种增强方式,与 Java 中的 finally 代码块的作用相似,常用于释放资源等功能,具有可灵活插拔的特点。


示例:最终增强

日志增强处理类(cn.duozai.aop.UserServiceLogger):

public class UserServiceLogger {

    // ...

    /**
     * 最终增强:方法执行完毕,无论是否抛出异常,都会执行增强操作
     *
     * @param jp 连接点对象
     * @return void
     */
    public void after(JoinPoint jp) {
        logger.debug(jp.getSignature().getName() + "方法执行结束");
    }

}

Spring 核心配置文件(classpath:applicationContext.xml):

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns: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">

    <!-- ... -->

    <aop:config>
        <!-- ... -->

        <aop:aspect ref="userServiceLogger">
            <!-- ... -->

            <!--
                <aop:after>标签:定义最终增强,无论方法是否抛出异常,都会执行增强方法
            -->
            <aop:after method="after" pointcut-ref="pointcut"/>
        </aop:aspect>
    </aop:config>

</beans>

示例效果:


环绕增强

环绕增强是指在目标对象方法前后都可以进行织入的一种增强处理方式,在环绕增强中,可以获取或修改目标方法的参数、返回值,可以对它进行异常处理,甚至可以决定目标方法是否执行。


示例:环绕增强

日志增强处理类(cn.duozai.aop.UserServiceLogger):

public class UserServiceLogger {

    // ...

    /**
     * 环绕增强
     *
     * @param jp 连接点对象,是JoinPoint的子接口
     * @return 方法返回值对象
     */
    public Object around(ProceedingJoinPoint jp) {
        logger.debug(jp.getSignature().getName() + "方法开始执行");

        Object result = null;
        try {
            // 调用真正的目标方法
            result = jp.proceed();

            logger.debug("方法执行后,返回:" + result);
        } catch (Throwable ex) {
            logger.debug(jp.getSignature().getName() + "方法发生异常: " + ex.getMessage());
        } finally {
            logger.debug(jp.getSignature().getName() + "方法执行结束");
        }

        return result;
    }

}

Spring 核心配置文件(classpath:applicationContext.xml):

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns: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">

    <!-- ... -->

    <aop:config>
        <!-- ... -->

        <aop:aspect ref="userServiceLogger">
            <!--
                <aop:around>标签:定义环绕增强
            -->
            <aop:around method="around" pointcut-ref="pointcut"/>
        </aop:aspect>
    </aop:config>

</beans>

示例效果:


增强总结

前置增强(before):

  • 作用时机:在目标方法执行之前执行增强逻辑。
  • 使用场景:记录方法开始执行的相关信息,如记录开始时间、传入的参数情况等。

后置增强(afterReturning):

  • 作用时机:在目标方法正常执行完成后(没有抛出异常的情况下)执行增强逻辑。
  • 使用场景:对方法返回结果进行加工、做一些补充的日志记录用于记录方法成功执行后的状态,便于后续的分析排查问题等。

异常抛出增强(afterThrowing):

  • 作用时机:当目标方法执行过程中抛出异常时被触发。
  • 使用场景:统一的异常处理机制,避免异常直接抛给上层调用者导致系统不稳定,也方便对异常进行归类、记录和按特定规则反馈异常相关提示等。

最终增强(after):

  • 作用时机:不管目标方法是正常执行完毕还是抛出异常,最终增强里的逻辑都会执行。
  • 使用场景:资源清理工作,如关闭文件流、释放数据库连接、清理一些临时占用的内存缓存等。

环绕增强(around):

  • 作用时机:在目标方法执行前、执行时、执行后都进行相关逻辑控制,相当于把前置、后置等增强的功能融合在了一起,并且可以灵活决定目标方法是否真的要执行。
  • 使用场景:对方法执行的整个流程进行全面控制,常用于需要精细控制目标方法执行过程、进行复杂的事务管理(比如决定事务的提交、回滚时机等)以及整合多个不同阶段的处理逻辑的场景。

依赖注入方式扩展

构造注入

构造注入是指 Spring 通过构造方法为属性赋值的一种注入方式,可以在对象初始化时为属性赋值,具有良好的实效性。

构造注入的基本用法:

<bean id="..." class="...">
    <constructor-arg name="构造方法中的参数名称" ref="要注入的依赖对象" />
    <constructor-arg name="构造方法中的参数名称" value="要注入的值" />
    <!-- ... -->
</bean>

构造注入能保证依赖不可变、方便依赖关系初始化及利于单元测试,但是构造函数参数多易使代码臃肿,处理可选依赖不太便捷。


示例:构造注入

用户模块业务逻辑接口实现类(cn.duozai.dao.UserServiceImpl):

public class UserServiceImpl implements UserService {

    /**
     * 用户模块数据访问层对象,通过Spring进行注入
     */
    UserDao userDao;

    /**
     * 设置用户模块数据访问层对象
     * 借助set方法来设置依赖对象:setter注入/构造注入
     *
     * @param userDao 用户模块数据访问层对象
     * @return void
     */
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

    /**
     * 构造函数
     * 借助构造方法来设置依赖对象:构造注入
     *
     * @param userDao 用户模块数据访问层对象
     * @return null
     */
    public UserServiceImpl(UserDao userDao) {
        this.userDao = userDao;
    }

    /**
     * 保存用户信息
     *
     * @return 保存结果
     */
    @Override
    public boolean save() {
        // 调用userDao的方法保存用户信息
        int result = userDao.save();
        return result > 0;
    }

}

Spring 核心配置文件(classpath:applicationContext.xml):

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns: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="userDao" class="cn.duozai.dao.UserDaoImpl"/>
    <bean id="userService" class="cn.duozai.service.UserServiceImpl">
        <!-- <property>标签:依赖set方法进行注入,即set注入/设值注入 -->
        <!-- <property name="userDao" ref="userDao"/> -->

        <!--
            <constructor-arg>:构造函数注入
            name属性:构造函数的参数名
            ref属性:引用spring容器中bean对象
            value属性:构造函数的参数值
        -->
        <constructor-arg name="userDao" ref="userDao"/>
    </bean>

</beans>

示例效果:


p 命名空间注入

使用 Spring 做属性注入时,需要使用 property 标签,使用 p 命名空间,可以简化 property 标签,但其本质上还是 setter 注入。

p 命名空间的基本用法:

<bean id="..." class="..." p:属性="要注入的值"/>
<bean id="..." class="..." p:属性-ref="要注入的依赖对象"/>

在 Spring 配置文件中使用 p 命名空间,需要先添加 p 命名空间的声明。

p 命名空间的声明:

xmlns:p="http://www.springframework.org/schema/p"

p 命名空间的特点是使用属性而不是子元素的形式配置 Bean 的属性。


示例:p 命名空间注入

Spring 核心配置文件(classpath:applicationContext.xml):

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       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
        http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd">

    <bean id="userDao" class="cn.duozai.dao.UserDaoImpl"/>
    <!--
        p命名空间注入和property本质上都是set注入,都需要提供set方法
        property以标签的形式存在,p命名空间以属性的形式存在
        p:xxx="值"
        p:xxx-ref="引用对象"
    -->
    <bean id="userService" class="cn.duozai.service.UserServiceImpl"
          p:userDao-ref="userDao"/>

</beans>

不同数据类型的注入

Spring 注入基本数据类型、字符串的基本用法:

<bean id="..." class="...">
    <property name="...">
        <value></value>
    </property>

    <property name="..." value=""/>
</bean>

Spring 注入对象的基本用法:

<bean id="..." class="...">
    <property name="..." ref="..."/>
</bean>

Spring 注入空字符串的基本用法:

<bean id="..." class="...">
    <property name="...">
        <value></value>
    </property>
</bean>

Spring 注入 null 值的基本用法:

<bean id="..." class="...">
    <property name="...">
        <null/>
    </property>
</bean>

Spring 注入 List 的基本用法:

<bean id="..." class="...">
    <property name="...">
        <list>
            <value>...</value>
            <value>...</value>
        </list>
    </property>
</bean>

Spring 注入 Set 的基本用法:

<bean id="..." class="...">
    <property name="...">
        <set>
            <value>...</value>
            <value>...</value>
        </set>
    </property>
</bean>

Spring 注入 Map 的基本用法:

<bean id="..." class="...">
    <property name="...">
        <map>
            <entry key="..." value="..."/>
            <entry key="..." value="..."/>
        </map>
    </property>
</bean>

Spring 注入 Properties 的基本用法:

<bean id="..." class="...">
    <props name="...">
        <prop key="..." value="..."/>
        <prop key="..." value="..."/>
    </property>
</bean>

注入总结

setter 设值注入:

  • 原理:依赖对象通过类的 Setter 方法注入到目标对象中。
  • 优点:可选依赖配置灵活,代码可读性较好。
  • 缺点:可能导致对象处于不一致状态,不利于对象的不可变特性维护。

构造注入:

  • 原理:通过类的构造方法来注入依赖。
  • 优点:依赖不可变,在对象构造完成后所有必需的依赖都已经被完全初始化。
  • 缺点:构造方法参数过多时变得复杂,不够灵活,不适合可选依赖场景。

p 命名空间注入:

  • 原理:本质上还是 setter 设值注入,使用属性而不是子元素的形式配置 Bean 的属性。
  • 在 Spring 配置文件中使用 p 命名空间,需要先添加 p 命名空间的声明。

IoC 注解实现

使用 Spring 注解实现 IoC

当项目越来越庞大时,使用 XML 配置 IoC,需要编写大量代码,不利于项目维护,从 Spring 2.0 开始引入注解的配置方式来优化 XML 配置。

使用 Spring 注解实现 IoC,需要依赖 Spring AOP,故使用 Spring 注解时,需要同时导入 IoC 和 AOP 相关依赖和命名空间,并开启 Spring 注解扫包,才能使用注解实现 IoC。

Spring IoC 的基本注解:

名称描述
@Component将一个类定义为 Spring 的 Bean 组件,与 XML 配置中的 bean 标签作用相同
@Repository作用同 @Component,用于标注 DAO 类
@Service作用同 @Component,用于标注 Service 类
@Controller作用同 @Component,用于标注 Controller 类
@Autowired根据对象类型注入 Bean 对象
@Qualifier与 @Autowired 组合使用,根据 Bean 对象名称注入 Bean 对象

示例:使用 Spring 注解实现 IoC

用户模块数据访问接口实现类(cn.duozai.dao.UserDaoImpl):

/**
 * @Repository注解:标记为Bean对象,即将该类交给Spring管理
 * value属性:Bean对象的名称,默认为类名首字母小写
 * 相当于 <bean id="userDao" class="cn.duozai.dao.UserDaoImpl">
 */
@Repository
public class UserDaoImpl implements UserDao {

    private static final Logger logger = Logger.getLogger(UserDaoImpl.class);

    /**
     * 保存用户信息
     *
     * @return SQL语句影响行数
     */
    @Override
    public int save() {
        logger.debug("保存用户信息到数据库");
        return 1;
    }

}

用户模块业务逻辑接口实现类(cn.duozai.dao.UserServiceImpl):

/**
 * @Service注解:标记为Bean对象,即将该类交给Spring管理
 */
public class UserServiceImpl implements UserService {

    /**
     * Service定义对于DAO层的引用,从Spring中注入DAO
     * @Autowired注解:注入类型为UserDao的Bean对象
     */
    @Autowired
    UserDao userDao;

    /**
     * 保存用户信息
     *
     * @return 保存结果
     */
    @Override
    public boolean save() {
        // 调用userDao的方法保存用户信息
        int result = userDao.save();
        return result > 0;
    }

}

Spring 核心配置文件(classpath:applicationContext.xml):

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:p="http://www.springframework.org/schema/p"
       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
        http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 扫描包中注解标注的类,使注解生效 -->
    <context:component-scan base-package="cn.duozai"/>

</beans>

测试类(test.SpringTest):

public class SpringTest {

    /**
     * Spring测试
     *
     * @return void
     */
    @Test
    public void test() {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        // 使用@Service注解标记UserServiceImpl时,没有为其取名,其默认为类名首字母小写
        UserService userService = (UserService) applicationContext.getBean("userServiceImpl");
        userService.save();
    }

}

示例效果:


@Autowired 注解默认会根据对象的类型来注入对应的 Bean 对象,当出现相同类型的 Bean 对象时,就会报错,可以结合 @Qualifier 注解根据 Bean 的名称进行注入。


示例:使用 Autowired + Qualifier 实现 IoC

用户模块业务逻辑接口实现类(cn.duozai.dao.UserServiceImpl):

@Service
public class UserServiceImpl implements UserService {

    /**
     * Service定义对于DAO层的引用,从Spring中注入DAO
     * @Autowired注解:注入类型为UserDao的Bean对象
     * @Qualifier注解:注入名称为userDao的Bean对象
     */
    @Autowired
    @Qualifier("userDao")
    UserDao userDao;

    // ...

}

使用 Java 标准注解完成装配

Spring 支持使用 Java 标准注解 @Resource 进行Bean 对象的注入。

@Resource 注解默认根据对象的名称进行 Bean 对象注入,当通过对象名称无法找到 Bean 对象时,会自动按照对象的类型进行 Bean 对象注入。


示例:使用 Java 标准注解完成装配

用户模块业务逻辑接口实现类(cn.duozai.dao.UserServiceImpl):

@Service
public class UserServiceImpl implements UserService {

    /**
     * Service定义对于DAO层的引用,从Spring中注入DAO
     * @Resource注解:先根据名称注入,后根据类型注入
     */
    @Resource
    UserDao userDao;

    // ...

}

装配注解总结

@Autowired:

  • 提供者:Spring 提供的注解。
  • 作用:根据类型自动寻找匹配的 Bean 进行注入,如果在容器中有多个同一类型的 Bean 时,可以结合 @Qualifier 注解来指定具体要注入的 Bean 名称。
  • IntelliJ IDEA 中不推荐使用 @Autowired 注解进行装配。

@Resource:

  • 提供者:Java EE 的标准注解(javax.annotation.Resource)。
  • 作用:既可以按照名称进行注入(默认先按名称查找,如果没找到再按类型查找),也可以按照类型进行注入。

@Inject:

  • 提供者:Java 依赖注入规范(JSR 330)中的注解。
  • 作用:与 @Autowired 类似,主要依据类型来注入 Bean。

AOP 注解实现

AspectJ 概述

AspectJ 是一个面向切面的框架,它扩展了 Java 语言,定义了 AOP 语法,能够在编译期提供代码的织入。

@AspectJ 是 AspectJ 5 新增的功能,使用 JDK 5.0 注解技术和正规的 AspectJ 切点表达式语言描述切面。

Spring 通过集成 AspectJ 框架实现了以注解的方式定义切面,使得配置文件的代码大大减少。

使用 Spring 注解标注切面

使用 Spring 注解实现 AOP,需要同时导入 IoC 和 AOP 相关依赖和命名空间,并开启 Spring 注解扫包,才能使用注解实现AOP。

Spring AOP 的基本注解:

名称描述
@Aspect将一个类标记为切面,里面包含了一些针对其他类的横切关注点的处理逻辑
@Before定义前置增强方法
@AfterReturning定义后置增强方法
@AfterThrowing定义异常抛出增强方法
@After定义最终增强方法
@Around定义环绕增强方法

示例:使用 Spring 注解标注切面

用户模块数据访问接口实现类(cn.duozai.dao.UserDaoImpl):

@Repository
public class UserDaoImpl implements UserDao {

    private static final Logger logger = Logger.getLogger(UserDaoImpl.class);

    /**
     * 保存用户信息
     *
     * @return SQL语句影响行数
     */
    @Override
    public int save() {
        logger.debug("保存用户信息到数据库");
        return 1;
    }

}

用户模块业务逻辑接口实现类(cn.duozai.dao.UserServiceImpl):

@Service
public class UserServiceImpl implements UserService {

    @Resource
    UserDao userDao;

    /**
     * 保存用户信息
     *
     * @return 保存结果
     */
    @Override
    public boolean save() {
        // 调用userDao的方法保存用户信息
        int result = userDao.save();
        return result > 0;
    }

}

日志增强处理类(cn.duozai.aop.UserServiceLogger):

/**
 * @Aspect注解:标记为切面类 = 切入点+增强处理
 * @Component注解:标记为Bean对象
 */
@Aspect
@Component
public class UserServiceLogger {

    private static final Logger logger = Logger.getLogger(UserServiceLogger.class);

    /**
     * 环绕增强
     *
     * @param jp 连接点对象,是JoinPoint的子接口
     * @return 方法返回值对象
     */
    @Around("execution(boolean save())")    // 标记环绕增强
    public Object around(ProceedingJoinPoint jp) {
        logger.debug(jp.getSignature().getName() + "方法开始执行");

        Object result = null;
        try {
            // 调用真正的目标方法
            result = jp.proceed();

            logger.debug("方法执行后,返回:" + result);
        } catch (Throwable ex) {
            logger.debug(jp.getSignature().getName() + "方法发生异常: " + ex.getMessage());
        } finally {
            logger.debug(jp.getSignature().getName() + "方法执行结束");
        }

        return result;
    }

}

Spring 核心配置文件(classpath:applicationContext.xml):

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:p="http://www.springframework.org/schema/p"
       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
        http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 扫描包中注解标注的类,使注解生效 -->
    <context:component-scan base-package="cn.duozai"/>
    <!-- 开启Spring AOP注解 -->
    <aop:aspectj-autoproxy/>

</beans>

示例效果: