从一个简单的 JPA 示例开始本文主要讲述 Spring Data JPA。为了不至于给 JPA 和 Spring 的初学者造成较大的学习曲线，我们首先从 JPA 开始，简单介绍一个 JPA 示例；接着重构该示例，并引入 Spring 框架。这两部分不会涉及过多的篇幅，如果希望能够深入学习 Spring 和 JPA，可以根据本文最后提供的参考资料进一步学习。自 JPA 伴随 Java EE 5 发布以来，受到了各大厂商及开源社区的追捧，各种商用的和开源的 JPA 框架如雨后春笋般出现，为开发者提供了丰富的选择。它一改之前 EJB 2.x 中实体 Bean 笨重且难以使用的形象，充分吸收了在开源社区已经相对成熟的 ORM 思想。另外，它并不依赖于 EJB 容器，可以作为一个独立的持久层技术而存在。目前比较成熟的 JPA 框架主要包括 Jboss 的 Hibernate EntityManager、Oracle 捐献给 Eclipse 社区的 EclipseLink、Apache 的 OpenJPA 等。本文的示例代码基于 Hibernate EntityManager 开发，但是读者几乎不用修改任何代码，便可以非常容易地切换到其他 JPA 框架，因为代码中使用到的都是 JPA 规范提供的接口 / 类，并没有使用到框架本身的私有特性。示例主要涉及七个文件，但是很清晰：业务层包含一个接口和一个实现；持久层包含一个接口、一个实现、一个实体类；另外加上一个 JPA 配置文件和一个测试类。相关类 / 接口代码如下：清单 1. 实体类 AccountInfo.java@Entity @Table(name = "t_accountinfo") public class AccountInfo implements Serializable { private Long accountId; private Integer balance; // 此处省略 getter 和 setter 方法。 }清单 2. 业务层接口 UserService.javapublic interface UserService { public AccountInfo createNewAccount(String user, String pwd, Integer init); }清单 3. 业务层的实现类 UserServiceImpl.javaimport com.xcar.nvwa.grpc.bidding.UserInfo; public class UserServiceImpl implements UserService { private UserDao userDao = new UserDaoImpl(); public AccountInfo createNewAccount(String user, String pwd, Integer init){ // 封装域对象 AccountInfo accountInfo = new AccountInfo(); UserInfo userInfo = new UserInfo(); userInfo.setUsername(user); userInfo.setPassword(pwd); accountInfo.setBalance(init); accountInfo.setUserInfo(userInfo); // 调用持久层，完成数据的保存 return userDao.save(accountInfo); } }清单 4. 持久层接口public interface UserDao { public AccountInfo save(AccountInfo accountInfo); }清单 5. 持久层的实现类public class UserDaoImpl implements UserDao { public AccountInfo save(AccountInfo accountInfo) { EntityManagerFactory emf = Persistence.createEntityManagerFactory("SimplePU"); EntityManager em = emf.createEntityManager(); em.getTransaction().begin(); em.persist(accountInfo); em.getTransaction().commit(); emf.close(); return accountInfo; } }清单 6. JPA 标准配置文件 persistence.xml<?xml version="1.0" encoding="UTF-8"?> <persistence xmlns="http://java.sun.com/xml/ns/persistence" version="2.0"> <persistence-unit name="SimplePU" transaction-type="RESOURCE_LOCAL"> <provider>org.hibernate.ejb.HibernatePersistence</provider> <class>footmark.springdata.jpa.domain.UserInfo</class> <class>footmark.springdata.jpa.domain.AccountInfo</class> <properties> <property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver"/> <property name="hibernate.connection.url" value="jdbc:mysql://10.40.74.197:3306/zhangjp"/> <property name="hibernate.connection.username" value="root"/> <property name="hibernate.connection.password" value="root"/> <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5Dialect"/> <property name="hibernate.show_sql" value="true"/> <property name="hibernate.format_sql" value="true"/> <property name="hibernate.use_sql_comments" value="false"/> <property name="hibernate.hbm2ddl.auto" value="update"/> </properties> </persistence-unit> </persistence>清单 7. 本文使用如下的 main 方法进行开发者测试public class SimpleSpringJpaDemo { public static void main(String[] args) { new UserServiceImpl().createNewAccount("ZhangJianPing", "123456", 1); } }简述 Spring 框架对 JPA 的支持接下来我们引入 Spring，以展示 Spring 框架对 JPA 的支持。业务层接口 UserService 保持不变，UserServiceImpl 中增加了三个注解，以让 Spring 完成依赖注入，因此不再需要使用 new 操作符创建 UserDaoImpl 对象了。同时我们还使用了 Spring 的声明式事务：清单 8. 配置为 Spring Bean 的业务层实现@Service("userService") public class UserServiceImpl implements UserService { @Autowired private UserDao userDao; @Transactional public AccountInfo createNewAccount( String name, String pwd, Integer init) { …… } }对于持久层，UserDao 接口也不需要修改，只需修改 UserDaoImpl 实现，修改后的代码如下：清单 9. 配置为 Spring Bean 的持久层实现@Repository("userDao") public class UserDaoImpl implements UserDao { @PersistenceContext private EntityManager em; @Transactional public Long save(AccountInfo accountInfo) { em.persist(accountInfo); return accountInfo.getAccountId(); } }清单 10. Spring 配置文件<?xml version="1.0" encoding="UTF-8"?> <beans...> <context:component-scan base-package="footmark.springdata.jpa"/> <tx:annotation-driven transaction-manager="transactionManager"/> <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"> <property name="entityManagerFactory" ref="entityManagerFactory"/> </bean> <bean id="entityManagerFactory" class= "org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> </bean> </beans>清单 11. 改造后的基于 Spring 的开发者测试代码public class SimpleSpringJpaDemo{ public static void main(String[] args){ ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("spring-demo-cfg.xml"); UserService userService = ctx.getBean("userService", UserService.class); userService.createNewAccount("ZhangJianPing", "123456", 1); } }通过对比重构前后的代码，可以发现 Spring 对 JPA 的简化已经非常出色了，我们可以大致总结一下 Spring 框架对 JPA 提供的支持主要体现在如下几个方面：首先，它使得 JPA 配置变得更加灵活。JPA 规范要求，配置文件必须命名为 persistence.xml，并存在于类路径下的 META-INF 目录中。该文件通常包含了初始化 JPA 引擎所需的全部信息。Spring 提供的 LocalContainerEntityManagerFactoryBean 提供了非常灵活的配置，persistence.xml 中的信息都可以在此以属性注入的方式提供。其次，Spring 实现了部分在 EJB 容器环境下才具有的功能，比如对 @PersistenceContext、@PersistenceUnit 的容器注入支持。第三，也是最具意义的，Spring 将 EntityManager 的创建与销毁、事务管理等代码抽取出来，并由其统一管理，开发者不需要关心这些。如前面的代码所示，业务方法中只剩下操作领域对象的代码，事务管理和 EntityManager 创建、销毁的代码都不再需要开发者关心了。更进一步：Spring Data JPA 让一切近乎完美通过前面的分析可以看出，Spring 对 JPA 的支持已经非常强大，开发者只需关心核心业务逻辑的实现代码，无需过多关注 EntityManager 的创建、事务处理等 JPA 相关的处理，这基本上也是作为一个开发框架而言所能做到的极限了。然而，Spring 开发小组并没有止步，他们再接再厉，于最近推出了 Spring Data JPA 框架，主要针对的就是 Spring 唯一没有简化到的业务逻辑代码。至此，开发者连仅剩的实现持久层业务逻辑的工作都省了，唯一要做的，就只是声明持久层的接口，其他都交给 Spring Data JPA 来帮你完成！至此，读者可能会存在一个疑问：框架怎么可能代替开发者实现业务逻辑呢？毕竟，每一个应用的持久层业务甚至领域对象都不尽相同，框架是怎么做到的呢？其实这背后的思想并不复杂，比如，当你看到 UserDao.findUserById() 这样一个方法声明，大致应该能判断出这是根据给定条件的 ID 查询出满足条件的 User 对象。Spring Data JPA 做的便是规范方法的名字，根据符合规范的名字来确定方法需要实现什么样的逻辑。接下来我们针对前面的例子进行改造，让 Spring Data JPA 来帮助我们完成业务逻辑。在着手写代码之前，开发者需要先 下载 Spring Data JPA 的发布包（需要同时下载 Spring Data Commons 和 Spring Data JPA 两个发布包，Commons 是 Spring Data 的公共基础包），并把相关的依赖 JAR 文件加入到 CLASSPATH 中。首先，让持久层接口 UserDao 继承 Repository 接口。该接口使用了泛型，需要为其提供两个类型：第一个为该接口处理的域对象类型，第二个为该域对象的主键类型。修改后的 UserDao 如下：清单 12. Spring Data JPA 风格的持久层接口public interface UserDao extends Repository<AccountInfo, Long> { public AccountInfo save(AccountInfo accountInfo); }然后删除 UserDaoImpl 类，因为我们前面说过，框架会为我们完成业务逻辑。最后，我们需要在 Spring 配置文件中增加如下配置，以使 Spring 识别出需要为其实现的持久层接口：清单 13. 在 Spring 配置文件中启用扫描并自动创建代理的功能<!-- 需要在 <beans> 标签中增加对 jpa 命名空间的引用 --> <jpa:repositories base-package="footmark.springdata.jpa.dao" entity-manager-factory-ref="entityManagerFactory" transaction-manager-ref="transactionManager"/>至此便大功告成了！执行一下测试代码，然后看一下数据库，新的数据已经如我们预期的添加到表中了。如果要再增加新的持久层业务，比如希望查询出给 ID 的 AccountInfo 对象，该怎么办呢？很简单，在 UserDao 接口中增加一行代码即可：清单 14. 修改后的持久层接口，增加一个方法声明public interface UserDao extends Repository<AccountInfo, Long> { public AccountInfo save(AccountInfo accountInfo); // 你需要做的，仅仅是新增如下一行方法声明 public AccountInfo findByAccountId(Long accountId); }下面总结一下使用 Spring Data JPA 进行持久层开发大致需要的三个步骤：声明持久层的接口，该接口继承 Repository。Repository 是一个标记型接口，它不包含任何方法，当然如果有需要，Spring Data 也提供了若干 Repository 子接口，其中定义了一些常用的增删改查，以及分页相关的方法。在接口中声明需要的业务方法。Spring Data 将根据给定的策略（具体策略稍后讲解）来为其生成实现代码。在 Spring 配置文件中增加一行声明，让 Spring 为声明的接口创建代理对象。配置了 <jpa:repositories> 后，Spring 初始化容器时将会扫描 base-package 指定的包目录及其子目录，为继承 Repository 或其子接口的接口创建代理对象，并将代理对象注册为 Spring Bean，业务层便可以通过 Spring 自动封装的特性来直接使用该对象。此外，<jpa:repositories> 还提供了一些属性和子标签，便于做更细粒度的控制。可以在 <jpa:repositories> 内部使用 <context:include-filter>、<context:exclude-filter> 来过滤掉一些不希望被扫描到的接口。具体的使用方法见 Spring 参考文档。应该继承哪个接口？前面提到，持久层接口继承 Repository 并不是唯一选择。Repository 接口是 Spring Data 的一个核心接口，它不提供任何方法，开发者需要在自己定义的接口中声明需要的方法。与继承 Repository 等价的一种方式，就是在持久层接口上使用 @RepositoryDefinition 注解，并为其指定 domainClass 和 idClass 属性。如下两种方式是完全等价的：清单 15. 两种等价的继承接口方式示例public interface UserDao extends Repository<AccountInfo, Long> { …… } @RepositoryDefinition(domainClass = AccountInfo.class, idClass = Long.class) public interface UserDao { …… }如果持久层接口较多，且每一个接口都需要声明相似的增删改查方法，直接继承 Repository 就显得有些啰嗦，这时可以继承 CrudRepository，它会自动为域对象创建增删改查方法，供业务层直接使用。开发者只是多写了 "Crud" 四个字母，即刻便为域对象提供了开箱即用的十个增删改查方法。但是，使用 CrudRepository 也有副作用，它可能暴露了你不希望暴露给业务层的方法。比如某些接口你只希望提供增加的操作而不希望提供删除的方法。针对这种情况，开发者只能退回到 Repository 接口，然后到 CrudRepository 中把希望保留的方法声明复制到自定义的接口中即可。分页查询和排序是持久层常用的功能，Spring Data 为此提供了 PagingAndSortingRepository 接口，它继承自 CrudRepository 接口，在 CrudRepository 基础上新增了两个与分页有关的方法。但是，我们很少会将自定义的持久层接口直接继承自 PagingAndSortingRepository，而是在继承 Repository 或 CrudRepository 的基础上，在自己声明的方法参数列表最后增加一个 Pageable 或 Sort 类型的参数，用于指定分页或排序信息即可，这比直接使用 PagingAndSortingRepository 提供了更大的灵活性。JpaRepository 是继承自 PagingAndSortingRepository 的针对 JPA 技术提供的接口，它在父接口的基础上，提供了其他一些方法，比如 flush()，saveAndFlush()，deleteInBatch() 等。如果有这样的需求，则可以继承该接口。上述四个接口，开发者到底该如何选择？其实依据很简单，根据具体的业务需求，选择其中之一。笔者建议在通常情况下优先选择 Repository 接口。因为 Repository 接口已经能满足日常需求，其他接口能做到的在 Repository 中也能做到，彼此之间并不存在功能强弱的问题。只是 Repository 需要显示声明需要的方法，而其他则可能已经提供了相关的方法，不需要再显式声明，但如果对 Spring Data JPA 不熟悉，别人在检视代码或者接手相关代码时会有疑惑，他们不明白为什么明明在持久层接口中声明了三个方法，而在业务层使用该接口时，却发现有七八个方法可用，从这个角度而言，应该优先考虑使用 Repository 接口。前面提到，Spring Data JPA 在后台为持久层接口创建代理对象时，会解析方法名字，并实现相应的功能。除了通过方法名字以外，它还可以通过如下两种方式指定查询语句：Spring Data JPA 可以访问 JPA 命名查询语句。开发者只需要在定义命名查询语句时，为其指定一个符合给定格式的名字，Spring Data JPA 便会在创建代理对象时，使用该命名查询语句来实现其功能。开发者还可以直接在声明的方法上面使用 @Query 注解，并提供一个查询语句作为参数，Spring Data JPA 在创建代理对象时，便以提供的查询语句来实现其功能。下面我们分别讲述三种创建查询的方式。通过解析方法名创建查询通过前面的例子，读者基本上对解析方法名创建查询的方式有了一个大致的了解，这也是 Spring Data JPA 吸引开发者的一个很重要的因素。该功能其实并非 Spring Data JPA 首创，而是源自一个开源的 JPA 框架 Hades，该框架的作者 Oliver Gierke 本身又是 Spring Data JPA 项目的 Leader，所以把 Hades 的优势引入到 Spring Data JPA 也就是顺理成章的了。框架在进行方法名解析时，会先把方法名多余的前缀截取掉，比如 find、findBy、read、readBy、get、getBy，然后对剩下部分进行解析。并且如果方法的最后一个参数是 Sort 或者 Pageable 类型，也会提取相关的信息，以便按规则进行排序或者分页查询。在创建查询时，我们通过在方法名中使用属性名称来表达，比如 findByUserAddressZip()。框架在解析该方法时，首先剔除 findBy，然后对剩下的属性进行解析，详细规则如下（此处假设该方法针对的域对象为 AccountInfo 类型）：先判断 userAddressZip（根据 POJO 规范，首字母变为小写，下同）是否为 AccountInfo 的一个属性，如果是，则表示根据该属性进行查询；如果没有该属性，继续第二步；从右往左截取第一个大写字母开头的字符串（此处为 Zip），然后检查剩下的字符串是否为 AccountInfo 的一个属性，如果是，则表示根据该属性进行查询；如果没有该属性，则重复第二步，继续从右往左截取；最后假设 user 为 AccountInfo 的一个属性；接着处理剩下部分（AddressZip），先判断 user 所对应的类型是否有 addressZip 属性，如果有，则表示该方法最终是根据 AccountInfo.user.addressZip 的取值进行查询；否则继续按照步骤 2 的规则从右往左截取，最终表示根据 AccountInfo.user.address.zip 的值进行查询。可能会存在一种特殊情况，比如 AccountInfo 包含一个 user 的属性，也有一个 userAddress 属性，此时会存在混淆。读者可以明确在属性之间加上 _ 以显式表达意图，比如 findByUser_AddressZip() 或者 findByUserAddress_Zip()。在查询时，通常需要同时根据多个属性进行查询，且查询的条件也格式各样（大于某个值、在某个范围等等），Spring Data JPA 为此提供了一些表达条件查询的关键字，大致如下：And --- 等价于 SQL 中的 and 关键字，比如 findByUsernameAndPassword(String user, String pwd)；Or --- 等价于 SQL 中的 or 关键字，比如 findByUsernameOrAddress(String user, String addr)；Between --- 等价于 SQL 中的 between 关键字，比如 findBySalaryBetween(int max, int min)；LessThan --- 等价于 SQL 中的 <，比如 findBySalaryLessThan(int max)；GreaterThan --- 等价于 SQL 中的 >，比如 findBySalaryGreaterThan(int min)；IsNull --- 等价于 SQL 中的 is null，比如 findByUsernameIsNull()；IsNotNull --- 等价于 SQL 中的 is not null，比如 findByUsernameIsNotNull()；NotNull --- 与 IsNotNull 等价；Like --- 等价于 SQL 中的 like，比如 findByUsernameLike(String user)；NotLike --- 等价于 SQL 中的 not like，比如 findByUsernameNotLike(String user)；OrderBy --- 等价于 SQL 中的 order by，比如 findByUsernameOrderBySalaryAsc(String user)；Not --- 等价于 SQL 中的 !=，比如 findByUsernameNot(String user)；In --- 等价于 SQL 中的 in，比如 findByUsernameIn(Collection<String> userList)，方法的参数可以是 Collection 类型，也可以是数组或者不定长参数；NotIn --- 等价于 SQL 中的 not in，比如 findByUsernameNotIn(Collection<String> userList)，方法的参数可以是 Collection 类型，也可以是数组或者不定长参数。使用 @Query 创建查询@Query 注解的使用非常简单，只需在声明的方法上面标注该注解，同时提供一个 JPQL 查询语句即可，如下所示：清单 16. 使用 @Query 提供自定义查询语句示例public interface UserDao extends Repository<AccountInfo, Long> { @Query("select a from AccountInfo a where a.accountId = ?1") public AccountInfo findByAccountId(Long accountId); @Query("select a from AccountInfo a where a.balance > ?1") public Page<AccountInfo> findByBalanceGreaterThan( Integer balance, Pageable pageable); }很多开发者在创建 JPQL 时喜欢使用命名参数来代替位置编号，@Query 也对此提供了支持。JPQL 语句中通过 : 变量 的格式来指定参数，同时在方法的参数前面使用 @Param 将方法参数与 JPQL 中的命名参数对应，示例如下：清单 17. @Query 支持命名参数示例public interface UserDao extends Repository<AccountInfo, Long> { public AccountInfo save(AccountInfo accountInfo); @Query("from AccountInfo a where a.accountId = :id") public AccountInfo findByAccountId(@Param("id") Long accountId); @Query("from AccountInfo a where a.balance > :balance") public Page<AccountInfo> findByBalanceGreaterThan( @Param("balance") Integer balance, Pageable pageable); }此外，开发者也可以通过使用 @Query 来执行一个更新操作，为此，我们需要在使用 @Query 的同时，用 @Modifying 来将该操作标识为修改查询，这样框架最终会生成一个更新的操作，而非查询。如下所示：清单 18. 使用 @Modifying 将查询标识为修改查询@Modifying @Query("update AccountInfo a set a.salary = ?1 where a.salary < ?2") public int increaseSalary(int after, int before);通过调用 JPA 命名查询语句创建查询命名查询是 JPA 提供的一种将查询语句从方法体中独立出来，以供多个方法共用的功能。Spring Data JPA 对命名查询也提供了很好的支持。用户只需要按照 JPA 规范在 orm.xml 文件或者在代码中使用 @NamedQuery（或 @NamedNativeQuery）定义好查询语句，唯一要做的就是为该语句命名时，需要满足 DomainClass.methodName() 的命名规则。假设定义了如下接口：清单 19. 使用 JPA 命名查询时，声明接口及方法时不需要什么特殊处理public interface UserDao extends Repository<AccountInfo, Long> { ...... public List<AccountInfo> findTop5(); }如果希望为 findTop5() 创建命名查询，并与之关联，我们只需要在适当的位置定义命名查询语句，并将其命名为 AccountInfo.findTop5，框架在创建代理类的过程中，解析到该方法时，优先查找名为 AccountInfo.findTop5 的命名查询定义，如果没有找到，则尝试解析方法名，根据方法名字创建查询。创建查询的顺序Spring Data JPA 在为接口创建代理对象时，如果发现同时存在多种上述情况可用，它该优先采用哪种策略呢？为此，<jpa:repositories> 提供了 query-lookup-strategy 属性，用以指定查找的顺序。它有如下三个取值：create --- 通过解析方法名字来创建查询。即使有符合的命名查询，或者方法通过 @Query 指定的查询语句，都将会被忽略。create-if-not-found --- 如果方法通过 @Query 指定了查询语句，则使用该语句实现查询；如果没有，则查找是否定义了符合条件的命名查询，如果找到，则使用该命名查询；如果两者都没有找到，则通过解析方法名字来创建查询。这是 query-lookup-strategy 属性的默认值。use-declared-query --- 如果方法通过 @Query 指定了查询语句，则使用该语句实现查询；如果没有，则查找是否定义了符合条件的命名查询，如果找到，则使用该命名查询；如果两者都没有找到，则抛出异常。Spring Data JPA 对事务的支持默认情况下，Spring Data JPA 实现的方法都是使用事务的。针对查询类型的方法，其等价于 @Transactional(readOnly=true)；增删改类型的方法，等价于 @Transactional。可以看出，除了将查询的方法设为只读事务外，其他事务属性均采用默认值。如果用户觉得有必要，可以在接口方法上使用 @Transactional 显式指定事务属性，该值覆盖 Spring Data JPA 提供的默认值。同时，开发者也可以在业务层方法上使用 @Transactional 指定事务属性，这主要针对一个业务层方法多次调用持久层方法的情况。持久层的事务会根据设置的事务传播行为来决定是挂起业务层事务还是加入业务层的事务。具体 @Transactional 的使用，请参考 Spring 的参考文档。为接口中的部分方法提供自定义实现有些时候，开发者可能需要在某些方法中做一些特殊的处理，此时自动生成的代理对象不能完全满足要求。为了享受 Spring Data JPA 带给我们的便利，同时又能够为部分方法提供自定义实现，我们可以采用如下的方法：将需要开发者手动实现的方法从持久层接口（假设为 AccountDao）中抽取出来，独立成一个新的接口（假设为 AccountDaoPlus），并让 AccountDao 继承 AccountDaoPlus；为 AccountDaoPlus 提供自定义实现（假设为 AccountDaoPlusImpl）；将 AccountDaoPlusImpl 配置为 Spring Bean；在 <jpa:repositories> 中按清单 19 的方式进行配置。清单 20. 指定自定义实现类<jpa:repositories base-package="footmark.springdata.jpa.dao"> <jpa:repository id="accountDao" repository-impl-ref="accountDaoPlus" /> </jpa:repositories> <bean id="accountDaoPlus" class="......."/>此外，<jpa:repositories> 提供了一个 repository-impl-postfix 属性，用以指定实现类的后缀。假设做了如下配置：清单 21. 设置自动查找时默认的自定义实现类命名规则<jpa:repositories base-package="footmark.springdata.jpa.dao" repository-impl-postfix="Impl"/>则在框架扫描到 AccountDao 接口时，它将尝试在相同的包目录下查找 AccountDaoImpl.java，如果找到，便将其中的实现方法作为最终生成的代理类中相应方法的实现。结束语本文主要介绍了 Spring Data JPA 的使用，以及它与 Spring 框架的无缝集成。Spring Data JPA 其实并不依赖于 Spring 框架，有兴趣的读者可以参考本文最后的"参考资源"进一步学习。参考资源访问 Spring Data 主页，了解 Spring Data 框架及其针对不同的持久化技术提供的支持，这里也提供了丰富的学习文档，并且可以下载最新的 Spring Data JPA 发布版。参与 Spring Data 论坛，在这里，你可以与世界各地的开发者交流，你提出的问题也会在第一时间得到解答。Spring Data JPA 的项目负责人写的 一篇指导开发者入门的文章。可以把它作为学习 Spring Data JPA 框架的一个起点。下载 SpringSource Tool Suite，这是 SpringSource 提供的一款基于 Eclipse 的免费 IDE，为开发 Spring 应用提供了强大的支持。下载资源样例代码 (sample-code.rar | 5KB)说明：本文内容基于 Spring 3.0.x 及 Spring Data JPA 1.0.1 版本编写，部分链接或配置方式可能随版本迭代有所变化，请参考官方最新文档适配当前开发环境。
什么是事务处理事务是计算机应用中不可或缺的组件模型，它保证了用户操作的原子性（Atomicity）、一致性（Consistency）、隔离性（Isolation）和持久性（Durability）。关于事务最经典的示例莫过于信用卡转账：将用户 A 账户中的 500 元人民币转移到用户 B 的账户中，其操作流程如下：将 A 账户中的金额减少 500；将 B 账户中的金额增加 500。这两个操作必须保证 ACID 的事务属性：即要么全部成功，要么全部失败。假若没有事务保障，用户的账号金额将可能发生问题：假如第一步操作成功而第二步失败，那么用户 A 账户中的金额就将减少 500 元，而用户 B 的账号却没有任何增加（不翼而飞）；同样如果第一步出错而第二步成功，那么用户 A 的账户金额不变，而用户 B 的账号将增加 500 元（凭空而生）。上述任何一种错误都会产生严重的数据不一致问题，事务的缺失对于一个稳定的生产系统是不可接受的。J2EE 事务处理方式1. 本地事务本地事务紧密依赖于底层资源管理器（例如数据库连接），事务处理局限在当前事务资源内。此种事务处理方式不存在对应用服务器的依赖，因而部署灵活，但无法支持多数据源的分布式事务。在数据库连接中使用本地事务示例如下：清单 1. 本地事务处理实例public void transferAccount() { Connection conn = null; Statement stmt = null; try { conn = getDataSource().getConnection(); // 将自动提交设置为 false， // 若设置为 true 则数据库将会把每一次数据更新认定为一个事务并自动提交 conn.setAutoCommit(false); stmt = conn.createStatement(); // 将 A 账户中的金额减少 500 stmt.execute("update t_account set amount = amount - 500 where account_id = 'A'"); // 将 B 账户中的金额增加 500 stmt.execute("update t_account set amount = amount + 500 where account_id = 'B'"); // 提交事务 conn.commit(); // 事务提交：转账的两步操作同时成功 } catch (SQLException sqle) { try { // 发生异常，回滚在本事务中的操作 conn.rollback(); // 事务回滚：转账的两步操作完全撤销 stmt.close(); conn.close(); } catch (Exception ignore) { // 忽略关闭异常 } sqle.printStackTrace(); } }2. 分布式事务处理Java 事务编程接口（JTA：Java Transaction API）和 Java 事务服务（JTS：Java Transaction Service）为 J2EE 平台提供了分布式事务服务。分布式事务（Distributed Transaction）包括事务管理器（Transaction Manager）和一个或多个支持 XA 协议的资源管理器（Resource Manager）。我们可以将资源管理器看做任意类型的持久化数据存储；事务管理器承担着所有事务参与单元的协调与控制。JTA 事务有效地屏蔽了底层事务资源，使应用可以以透明的方式参与到事务处理中。但是与本地事务相比，XA 协议的系统开销大，在系统开发过程中应慎重考虑是否确实需要分布式事务。若确实需要分布式事务以协调多个事务资源，则应实现和配置所支持 XA 协议的事务资源，如 JMS、JDBC 数据库连接池等。使用 JTA 处理事务的示例如下（注意：connA 和 connB 是来自不同数据库的连接）：清单 2. JTA 事务处理public void transferAccount() { UserTransaction userTx = null; Connection connA = null; Statement stmtA = null; Connection connB = null; Statement stmtB = null; try { // 获得 Transaction 管理对象 userTx = (UserTransaction) getContext().lookup("java:comp/UserTransaction"); // 从数据库 A 中取得数据库连接 connA = getDataSourceA().getConnection(); // 从数据库 B 中取得数据库连接 connB = getDataSourceB().getConnection(); // 启动事务 userTx.begin(); // 将 A 账户中的金额减少 500 stmtA = connA.createStatement(); stmtA.execute("update t_account set amount = amount - 500 where account_id = 'A'"); // 将 B 账户中的金额增加 500 stmtB = connB.createStatement(); stmtB.execute("update t_account set amount = amount + 500 where account_id = 'B'"); // 提交事务 userTx.commit(); // 事务提交：转账的两步操作同时成功（数据库 A 和数据库 B 中的数据被同时更新） } catch (SQLException sqle) { try { // 发生异常，回滚在本事务中的操作 userTx.rollback(); // 事务回滚：转账的两步操作完全撤销 // (数据库 A 和数据库 B 中的数据更新被同时撤销） stmtA.close(); connA.close(); stmtB.close(); connB.close(); } catch (Exception ignore) { // 忽略关闭异常 } sqle.printStackTrace(); } catch (Exception ne) { ne.printStackTrace(); } }JTA 实现原理很多开发人员都会对 JTA 的内部工作机制感兴趣：我编写的代码没有任何与事务资源（如数据库连接）互动的代码，但是我的操作（数据库更新）却实实在在的被包含在了事务中，那 JTA 究竟是通过何种方式来实现这种透明性的呢？要理解 JTA 的实现原理，首先需要了解其架构：它包括事务管理器（Transaction Manager）和一个或多个支持 XA 协议的资源管理器（Resource Manager）两部分。我们可以将资源管理器看做任意类型的持久化数据存储；事务管理器则承担着所有事务参与单元的协调与控制。根据所面向对象的不同，我们可以将 JTA 的事务管理器和资源管理器理解为两个方面：面向开发人员的使用接口（事务管理器）和面向服务提供商的实现接口（资源管理器）。其中开发接口的主要部分即为上述示例中引用的 UserTransaction 对象，开发人员通过此接口在信息系统中实现分布式事务；而实现接口则用来规范提供商（如数据库连接提供商）所提供的事务服务，它约定了事务的资源管理功能，使得 JTA 可以在异构事务资源之间执行协同沟通。以数据库为例，IBM 公司提供了实现分布式事务的数据库驱动程序，Oracle 也提供了实现分布式事务的数据库驱动程序。在同时使用 DB2 和 Oracle 两种数据库连接时，JTA 即可以根据约定的接口协调这两种事务资源从而实现分布式事务。正是基于统一规范的不同实现使得 JTA 可以协调与控制不同数据库或者 JMS 厂商的事务资源，其架构如下图所示：图 1. JTA 体系架构点击查看大图开发人员使用开发人员接口，实现应用程序对全局事务的支持；各提供商（数据库，JMS 等）依据提供商接口的规范提供事务资源管理功能；事务管理器（TransactionManager）将应用对分布式事务的使用映射到实际的事务资源并在事务资源间进行协调与控制。下面，本文将对包括 UserTransaction、Transaction 和 TransactionManager 在内的三个主要接口以及其定义的方法进行介绍。面向开发人员的接口：UserTransaction面向开发人员的接口为 UserTransaction（使用方法如上例所示），开发人员通常只使用此接口实现 JTA 事务管理，其定义了如下的方法：begin() - 开始一个分布式事务（在后台 TransactionManager 会创建一个 Transaction 事务对象并把此对象通过 ThreadLocal 关联到当前线程上）commit() - 提交事务（在后台 TransactionManager 会从当前线程下取出事务对象并把此对象所代表的事务提交）rollback() - 回滚事务（在后台 TransactionManager 会从当前线程下取出事务对象并把此对象所代表的事务回滚）getStatus() - 返回关联到当前线程的分布式事务的状态（Status 对象里边定义了所有的事务状态，感兴趣的读者可以参考 API 文档）setRollbackOnly() - 标识关联到当前线程的分布式事务将被回滚面向提供商的实现接口：Transaction 与 TransactionManager面向提供商的实现接口主要涉及到 TransactionManager 和 Transaction 两个对象。Transaction 代表了一个物理意义上的事务，在开发人员调用 UserTransaction.begin() 方法时 TransactionManager 会创建一个 Transaction 事务对象（标志着事务的开始）并把此对象通过 ThreadLocal 关联到当前线程。UserTransaction 接口中的 commit()、rollback()，getStatus() 等方法都将最终委托给 Transaction 类的对应方法执行。Transaction 接口定义了如下的方法：commit() - 协调不同的事务资源共同完成事务的提交rollback() - 协调不同的事务资源共同完成事务的回滚setRollbackOnly() - 标识关联到当前线程的分布式事务将被回滚getStatus() - 返回关联到当前线程的分布式事务的状态enListResource(XAResource xaRes, int flag) - 将事务资源加入到当前的事务中（在上述示例中，在对数据库 A 操作时其所代表的事务资源将被关联到当前事务中，同样，在对数据库 B 操作时其所代表的事务资源也将被关联到当前事务中）delistResource(XAResource xaRes, int flag) - 将事务资源从当前事务中删除registerSynchronization(Synchronization sync) - 回调接口。Hibernate 等 ORM 工具都有自己的事务控制机制来保证事务，但同时它们还需要一种回调机制以便在事务完成时得到通知从而触发一些处理工作，如清除缓存等。这就涉及到了 Transaction 的回调接口 registerSynchronization。工具可以通过此接口将回调程序注入到事务中，当事务成功提交后，回调程序将被激活。TransactionManager 本身并不承担实际的事务处理功能，它更多的是充当用户接口和实现接口之间的桥梁。下面列出了 TransactionManager 中定义的方法，可以看到此接口中的大部分事务方法与 UserTransaction 和 Transaction 相同。在开发人员调用 UserTransaction.begin() 方法时 TransactionManager 会创建一个 Transaction 事务对象（标志着事务的开始）并把此对象通过 ThreadLocal 关联到当前线程上；同样 UserTransaction.commit() 会调用 TransactionManager.commit()，方法将从当前线程下取出事务对象 Transaction 并把此对象所代表的事务提交，即调用 Transaction.commit()。begin() - 开始事务commit() - 提交事务rollback() - 回滚事务getStatus() - 返回当前事务状态setRollbackOnly()getTransaction() - 返回关联到当前线程的事务setTransactionTimeout(int seconds) - 设置事务超时时间resume(Transaction tobj) - 继续当前线程关联的事务suspend() - 挂起当前线程关联的事务在系统开发过程中会遇到需要将事务资源暂时排除的操作，此时就需要调用 suspend() 方法将当前的事务挂起：在此方法后面所做的任何操作将不会被包括在事务中，在非事务性操作完成后调用 resume() 以继续事务（注：要进行此操作需要获得 TransactionManager 对象，其获得方式在不同的 J2EE 应用服务器上是不一样的）。下面将通过具体的代码向读者介绍 JTA 实现原理。下图列出了示例实现中涉及到的 Java 类，其中 UserTransactionImpl 实现了 UserTransaction 接口，TransactionManagerImpl 实现了 TransactionManager 接口，TransactionImpl 实现了 Transaction 接口。图 2. JTA 实现类图点击查看大图清单 3. 开始事务 - UserTransactionImpl implements UserTransactionpublic void begin() throws NotSupportedException, SystemException { // 将开始事务的操作委托给 TransactionManagerImpl TransactionManagerImpl.singleton().begin(); }清单 4. 开始事务 - TransactionManagerImpl implements TransactionManager// 此处 transactionHolder 用于将 Transaction 所代表的事务对象关联到线程上 private static ThreadLocal<TransactionImpl> transactionHolder = new ThreadLocal<TransactionImpl>(); // TransactionManager 必须维护一个全局对象，因此使用单实例模式实现 private static TransactionManagerImpl singleton = new TransactionManagerImpl(); private TransactionManagerImpl() { } public static TransactionManagerImpl singleton() { return singleton; } public void begin() throws NotSupportedException, SystemException { // XidImpl 实现了 Xid 接口，其作用是唯一标识一个事务 XidImpl xid = new XidImpl(); // 创建事务对象，并将对象关联到线程 TransactionImpl tx = new TransactionImpl(xid); transactionHolder.set(tx); }现在我们就可以理解 Transaction 接口上没有定义 begin 方法的原因了：Transaction 对象本身就代表了一个事务，在它被创建的时候就表明事务已经开始，因此也就不需要额外定义 begin() 方法了。清单 5. 提交事务 - UserTransactionImpl implements UserTransactionpublic void commit() throws RollbackException, HeuristicMixedException, HeuristicRollbackException, SecurityException, IllegalStateException, SystemException { // 检查是否是 Roll back only 事务，如果是回滚事务 if (rollBackOnly) { rollback(); return; } else { // 将提交事务的操作委托给 TransactionManagerImpl TransactionManagerImpl.singleton().commit(); } }清单 6. 提交事务 - TransactionManagerImpl implements TransactionManagerpublic void commit() throws RollbackException, HeuristicMixedException, HeuristicRollbackException, SecurityException, IllegalStateException, SystemException { // 取得当前事务所关联的事务并通过其 commit 方法提交 TransactionImpl tx = transactionHolder.get(); tx.commit(); }同理，rollback、getStatus、setRollbackOnly 等方法也采用了与 commit() 相同的方式实现。UserTransaction 对象不会对事务进行任何控制，所有的事务方法都是通过 TransactionManager 传递到实际的事务资源即 Transaction 对象上。上述示例演示了 JTA 事务的处理过程，下面将为您展示事务资源（数据库连接，JMS）是如何以透明的方式加入到 JTA 事务中的。首先需要明确的一点是，在 JTA 事务代码中获得的数据库源（DataSource）必须是支持分布式事务的。在如下的代码示例中，尽管所有的数据库操作都被包含在了 JTA 事务中，但是因为 MySql 的数据库连接是通过本地方式获得的，对 MySql 的任何更新将不会被自动包含在全局事务中。清单 7. JTA 事务处理（混合资源示例）public void transferAccount() { UserTransaction userTx = null; Connection mySqlConnection = null; Statement mySqlStat = null; Connection connB = null; Statement stmtB = null; try { // 获得 Transaction 管理对象 userTx = (UserTransaction) getContext().lookup("java:comp/UserTransaction"); // 以本地方式获得 mySql 数据库连接 mySqlConnection = DriverManager.getConnection("localhost:1111"); // 从数据库 B 中取得数据库连接，getDataSourceB 返回应用服务器的数据源 connB = getDataSourceB().getConnection(); // 启动事务 userTx.begin(); // 将 A 账户中的金额减少 500 // mySqlConnection 是从本地获得的数据库连接，不会被包含在全局事务中 mySqlStat = mySqlConnection.createStatement(); mySqlStat.execute("update t_account set amount = amount - 500 where account_id = 'A'"); // connB 是从应用服务器得的数据库连接，会被包含在全局事务中 stmtB = connB.createStatement(); stmtB.execute("update t_account set amount = amount + 500 where account_id = 'B'"); // 事务提交：connB 的操作被提交，mySqlConnection 的操作不会被提交 userTx.commit(); } catch (SQLException sqle) { // 处理异常代码 } catch (Exception ne) { ne.printStackTrace(); } }为什么必须从支持事务的数据源中获得的数据库连接才支持分布式事务呢？其实支持事务的数据源与普通的数据源是不同的，它实现了额外的 XADataSource 接口。我们可以简单的将 XADataSource 理解为普通的数据源（继承了 java.sql.PooledConnection），只是它为支持分布式事务而增加了 getXAResource 方法。另外，由 XADataSource 返回的数据库连接与普通连接也是不同的，此连接除了实现 java.sql.Connection 定义的所有功能之外还实现了 XAConnection 接口。我们可以把 XAConnection 理解为普通的数据库连接，它支持所有 JDBC 规范的数据库操作，不同之处在于 XAConnection 增加了对分布式事务的支持。通过下面的类图读者可以对这几个接口的关系有所了解：图 3. 事务资源类图点击查看大图应用程序从支持分布式事务的数据源获得的数据库连接是 XAConnection 接口的实现，而由此数据库连接创建的会话（Statement）也为了支持分布式事务而增加了功能，如下代码所示：清单 8. JTA 事务资源处理public void transferAccount() { UserTransaction userTx = null; Connection conn = null; Statement stmt = null; try { // 获得 Transaction 管理对象 userTx = (UserTransaction) getContext().lookup("java:comp/UserTransaction"); // 从数据库中取得数据库连接，getDataSourceB 返回支持分布式事务的数据源 conn = getDataSourceB().getConnection(); // 会话 stmt 已经为支持分布式事务进行了功能增强 stmt = conn.createStatement(); // 启动事务 userTx.begin(); stmt.execute("update t_account ... where account_id = 'A'"); userTx.commit(); } catch (SQLException sqle) { // 处理异常代码 } catch (Exception ne) { ne.printStackTrace(); } }我们来看一下由 XAConnection 数据库连接创建的会话（Statement）部分的代码实现（不同的 JTA 提供商会有不同的实现方式，此处代码示例只是向您演示事务资源是如何被自动加入到事务中）。我们以会话对象的 execute 方法为例，通过在方法开始部分增加对 associateWithTransactionIfNecessary 方法的调用，即可以保证在 JTA 事务期间，对任何数据库连接的操作都会被透明的加入到事务中。清单 9. 将事务资源自动关联到事务对象 - XAStatement implements Statementpublic void execute(String sql) { // 对于每次数据库操作都检查此会话所在的数据库连接是否已经被加入到事务中 associateWithTransactionIfNecessary(); try { // 处理数据库操作的代码 .... } catch (SQLException sqle) { // 处理异常代码 } catch (Exception ne) { ne.printStackTrace(); } } public void associateWithTransactionIfNecessary() { // 获得 TransactionManager TransactionManager tm = getTransactionManager(); Transaction tx = tm.getTransaction(); // 检查当前线程是否有分布式事务 if (tx != null) { // 在分布式事务内，通过 tx 对象判断当前数据连接是否已经被包含在事务中， // 如果不是那么将此连接加入到事务中 Connection conn = this.getConnection(); // tx.hasCurrentResource, xaConn.getDataSource() 不是标准的 JTA // 接口方法，是为了实现分布式事务而增加的自定义方法 if (!tx.hasCurrentResource(conn)) { XAConnection xaConn = (XAConnection) conn; XADataSource xaSource = xaConn.getDataSource(); // 调用 Transaction 的接口方法，将数据库事务资源加入到当前事务中 tx.enListResource(xaSource.getXAResource(), 1); } } }XAResource 与 XidXAResource 是 Distributed Transaction Processing: The XA Specification 标准的 Java 实现，它是对底层事务资源的抽象，定义了分布式事务处理过程中事务管理器和资源管理器之间的协议，各事务资源提供商（如 JDBC 驱动，JMS）将提供此接口的实现。使用此接口，开发人员可以通过自己的编程实现分布式事务处理，但这些通常都是由应用服务器实现的（服务器自带实现更加高效，稳定）。为了说明，我们将举例说明它的使用方式。在使用分布式事务之前，为了区分事务使之不发生混淆，必须实现一个 Xid 类用来标识事务，可以把 Xid 想象成事务的一个标志符，每次在新事务创建时都会为事务分配一个 Xid，Xid 包含三个元素：formatID、gtrid（全局事务标识符）和 bqual（分支修饰词标识符）。formatID 通常是零，这意味着你将使用 OSI CCR（Open Systems Interconnection Commitment, Concurrency 和 Recovery 标准）来命名；如果你要使用另外一种格式，那么 formatID 应该大于零，-1 值意味着 Xid 为无效。gtrid 和 bqual 分别包含 64 个字节二进制码来分别标识全局事务和分支事务，唯一的要求是 gtrid 和 bqual 必须是全局唯一的。XAResource 接口中主要定义了如下方法：commit() - 提交事务isSameRM(XAResource xares) - 检查当前的 XAResource 与参数是否同一事务资源prepare() - 通知资源管理器准备事务的提交工作rollback() - 通知资源管理器回滚事务在事务被提交时，Transaction 对象会收集所有被当前事务包含的 XAResource 资源，然后调用资源的提交方法，如下代码所示：清单 10. 提交事务 - TransactionImpl implements Transactionpublic void commit() throws RollbackException, HeuristicMixedException, HeuristicRollbackException, SecurityException, IllegalStateException, SystemException { // 得到当前事务中的所有事务资源 List<XAResource> list = getAllEnlistedResources(); // 通知所有的事务资源管理器，准备提交事务 // 对于生产级别的实现，此处需要进行额外处理以处理某些资源准备过程中出现的异常 for (XAResource xa : list) { xa.prepare(); } // 所有事务性资源，提交事务 for (XAResource xa : list) { xa.commit(); } }结束语通过如上介绍相信读者对 JTA 的原理已经有所了解，本文中的示例代码都是理想情况下的假设实现。一款完善成熟的 JTA 事务实现需要考虑与处理的细节非常多，如性能（提交事务的时候使用多线程方式并发提交事务）、容错（网络，系统异常）等，其成熟也需要经过较长时间的积累。感兴趣的读者可以阅读一些开源 JTA 实现以进一步深入学习。说明：本文基于经典的 J2EE 与 JTA 规范编写。随着 Java EE 移交至 Eclipse 基金会并更名为 Jakarta EE，相关包名（如 javax.transaction 变更为 jakarta.transaction）及实现细节可能有所调整。文中部分外部链接（如 IBM developerWorks）可能已归档或跳转，请参考官方最新文档获取时效信息。相关主题Java 官方网站"Java Transaction API Specification"提供了详细的 JTA 信息、包括版本，规范等，感兴趣的读者可以参考这篇文章"Java Transaction API 概述"( 编程中国 ) 详细描述了 XA 资源的使用原理，感兴趣的读者可以参照学习"JOTM"是一款开源的 JTA 实现，读者可以参照其源代码深入学习developerWorks Java 技术专区：这里有数百篇关于 Java 编程各个方面的文章。
@RequestParam @RequestBody @PathVariable 等参数绑定注解详解
引言承接上一篇文章对 @RequestMapping 地址映射的讲解，本文主要讲解请求数据（Request）到处理器方法（Handler Method）参数数据绑定时所用到的注解，以及它们各自适用的情形。注解分类简介Handler Method 参数绑定常用的注解，根据它们处理的 Request 不同内容部分，主要分为以下四类（本文主要讲解常用类型）：处理 Request URI 部分（指 URI Template 中的 variable，不含 queryString 部分）：@PathVariable处理 Request Header 部分：@RequestHeader, @CookieValue处理 Request Body 部分：@RequestParam, @RequestBody处理 Attribute 类型：@SessionAttributes, @ModelAttribute1. @PathVariable当使用 @RequestMapping 的 URI Template 样式映射时（例如 someUrl/{paramId}），可通过 @PathVariable 注解将 URI 中的变量值绑定到方法的参数上。示例代码：@Controller @RequestMapping("/owners/{ownerId}") public class RelativePathUriTemplateController { @RequestMapping("/pets/{petId}") public void findPet(@PathVariable String ownerId, @PathVariable String petId, Model model) { // implementation omitted } }上述代码将 URI Template 中的变量 ownerId 和 petId 的值，分别绑定到方法的参数上。若方法参数名称与 URI Template 中的变量名称不一致，需要在 @PathVariable("name") 中指定 URI Template 中的名称。2. @RequestHeader、@CookieValue@RequestHeader@RequestHeader 注解可以把 Request 请求 Header 部分的值绑定到方法的参数上。示例场景：这是一个 Request 的 Header 部分：Host localhost:8080 Accept text/html,application/xhtml+xml,application/xml;q=0.9 Accept-Language fr,en-gb;q=0.7,en;q=0.3 Accept-Encoding gzip,deflate Accept-Charset ISO-8859-1,utf-8;q=0.7,*;q=0.7 Keep-Alive 300绑定代码：@RequestMapping("/displayHeaderInfo.do") public void displayHeaderInfo(@RequestHeader("Accept-Encoding") String encoding, @RequestHeader("Keep-Alive") long keepAlive) { //... }上面的代码将 Request Header 部分的 Accept-Encoding 值绑定到参数 encoding 上，Keep-Alive Header 的值绑定到参数 keepAlive 上。@CookieValue@CookieValue 可以把 Request Header 中关于 Cookie 的值绑定到方法的参数上。示例场景：例如有如下 Cookie 值：JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84参数绑定代码：@RequestMapping("/displayHeaderInfo.do") public void displayHeaderInfo(@CookieValue("JSESSIONID") String cookie) { //... }即把 JSESSIONID 的值绑定到参数 cookie 上。3. @RequestParam, @RequestBody@RequestParam常用来处理简单类型的绑定。通过 Request.getParameter() 获取的 String 可直接转换为简单类型（String 到简单类型的转换操作由 ConversionService 配置的转换器来完成）。因为使用 request.getParameter() 方式获取参数，所以既可以处理 GET 方式中 queryString 的值，也可以处理 POST 方式中 Body Data 的值。用来处理 Content-Type 为 application/x-www-form-urlencoded 编码的内容，提交方式支持 GET、POST。该注解有两个主要属性：value：用来指定要传入值的参数名称。required：用来指示参数是否必须绑定。示例代码：@Controller @RequestMapping("/pets") @SessionAttributes("pet") public class EditPetForm { // ... @RequestMapping(method = RequestMethod.GET) public String setupForm(@RequestParam("petId") int petId, ModelMap model) { Pet pet = this.clinic.loadPet(petId); model.addAttribute("pet", pet); return "petForm"; } // ... }@RequestBody该注解常用来处理 Content-Type 不是 application/x-www-form-urlencoded 编码的内容，例如 application/json, application/xml 等。它是通过使用 HandlerAdapter 配置的 HttpMessageConverters 来解析 POST Data Body，然后绑定到相应的 Bean 上的。因为配置有 FormHttpMessageConverter，所以也可以用来处理 application/x-www-form-urlencoded 的内容，处理完的结果放在一个 MultiValueMap<String, String> 里，这种情况在某些特殊需求下使用，详情查看 FormHttpMessageConverter API。示例代码：@RequestMapping(value = "/something", method = RequestMethod.PUT) public void handle(@RequestBody String body, Writer writer) throws IOException { writer.write(body); }4. @SessionAttributes, @ModelAttribute@SessionAttributes该注解用来绑定 HttpSession 中的 Attribute 对象的值，便于在方法中的参数里使用。该注解有 value、types 两个属性，可以通过名字和类型指定要使用的 Attribute 对象。示例代码：@Controller @RequestMapping("/editPet.do") @SessionAttributes("pet") public class EditPetForm { // ... }@ModelAttribute该注解有两个用法：一个是用于方法上，一个是用于参数上。用于方法上时：通常用来在处理 @RequestMapping 之前，为请求绑定需要从后台查询的 Model。用于参数上时：用来通过名称对应，把相应名称的值绑定到注解的参数 Bean 上。要绑定的值来源于：@SessionAttributes 启用的 Attribute 对象上；@ModelAttribute 用于方法上时指定的 Model 对象；上述两种情况都没有时，new 一个需要绑定的 Bean 对象，然后把 Request 中按名称对应的方式把值绑定到 Bean 中。用到方法上 @ModelAttribute 的示例代码：// Add one attribute // The return value of the method is added to the model under the name "account" // You can customize the name via @ModelAttribute("myAccount") @ModelAttribute public Account addAccount(@RequestParam String number) { return accountManager.findAccount(number); }这种方式实际的效果就是在调用 @RequestMapping 的方法之前，为 Request 对象的 Model 里 put("account", Account)。用在参数上的 @ModelAttribute 示例代码：@RequestMapping(value="/owners/{ownerId}/pets/{petId}/edit", method = RequestMethod.POST) public String processSubmit(@ModelAttribute Pet pet) { }首先查询 @SessionAttributes 有无绑定的 Pet 对象，若没有则查询 @ModelAttribute 方法层面上是否绑定了 Pet 对象，若没有则将 URI Template 中的值按对应的名称绑定到 Pet 对象的各属性上。补充讲解：在不给定注解的情况下，参数是怎样绑定的？通过分析 AnnotationMethodHandlerAdapter 和 RequestMappingHandlerAdapter 的源代码发现，方法的参数在不给定注解的情况下：若要绑定的对象是简单类型：调用 @RequestParam 来处理。若要绑定的对象是复杂类型：调用 @ModelAttribute 来处理。这里的简单类型指 Java 的原始类型（boolean, int 等）、原始类型对象（Boolean, Integer 等）、String、Date 等 ConversionService 里可以直接 String 转换成目标对象的类型。下面贴出 AnnotationMethodHandlerAdapter 中绑定参数的部分源代码：private Object[] resolveHandlerArguments(Method handlerMethod, Object handler, NativeWebRequest webRequest, ExtendedModelMap implicitModel) throws Exception { Class[] paramTypes = handlerMethod.getParameterTypes(); Object[] args = new Object[paramTypes.length]; for (int i = 0; i < args.length; i++) { MethodParameter methodParam = new MethodParameter(handlerMethod, i); methodParam.initParameterNameDiscovery(this.parameterNameDiscoverer); GenericTypeResolver.resolveParameterType(methodParam, handler.getClass()); String paramName = null; String headerName = null; boolean requestBodyFound = false; String cookieName = null; String pathVarName = null; String attrName = null; boolean required = false; String defaultValue = null; boolean validate = false; Object[] validationHints = null; int annotationsFound = 0; Annotation[] paramAnns = methodParam.getParameterAnnotations(); for (Annotation paramAnn : paramAnns) { if (RequestParam.class.isInstance(paramAnn)) { RequestParam requestParam = (RequestParam) paramAnn; paramName = requestParam.value(); required = requestParam.required(); defaultValue = parseDefaultValueAttribute(requestParam.defaultValue()); annotationsFound++; } else if (RequestHeader.class.isInstance(paramAnn)) { RequestHeader requestHeader = (RequestHeader) paramAnn; headerName = requestHeader.value(); required = requestHeader.required(); defaultValue = parseDefaultValueAttribute(requestHeader.defaultValue()); annotationsFound++; } else if (RequestBody.class.isInstance(paramAnn)) { requestBodyFound = true; annotationsFound++; } else if (CookieValue.class.isInstance(paramAnn)) { CookieValue cookieValue = (CookieValue) paramAnn; cookieName = cookieValue.value(); required = cookieValue.required(); defaultValue = parseDefaultValueAttribute(cookieValue.defaultValue()); annotationsFound++; } else if (PathVariable.class.isInstance(paramAnn)) { PathVariable pathVar = (PathVariable) paramAnn; pathVarName = pathVar.value(); annotationsFound++; } else if (ModelAttribute.class.isInstance(paramAnn)) { ModelAttribute attr = (ModelAttribute) paramAnn; attrName = attr.value(); annotationsFound++; } else if (Value.class.isInstance(paramAnn)) { defaultValue = ((Value) paramAnn).value(); } else if (paramAnn.annotationType().getSimpleName().startsWith("Valid")) { validate = true; Object value = AnnotationUtils.getValue(paramAnn); validationHints = (value instanceof Object[] ? (Object[]) value : new Object[] {value}); } } if (annotationsFound > 1) { throw new IllegalStateException("Handler parameter annotations are exclusive choices - " + "do not specify more than one such annotation on the same parameter: " + handlerMethod); } if (annotationsFound == 0) {// 若没有发现注解 Object argValue = resolveCommonArgument(methodParam, webRequest); //判断 WebRquest 是否可赋值给参数 if (argValue != WebArgumentResolver.UNRESOLVED) { args[i] = argValue; } else if (defaultValue != null) { args[i] = resolveDefaultValue(defaultValue); } else { Class<?> paramType = methodParam.getParameterType(); if (Model.class.isAssignableFrom(paramType) || Map.class.isAssignableFrom(paramType)) { if (!paramType.isAssignableFrom(implicitModel.getClass())) { throw new IllegalStateException("Argument [" + paramType.getSimpleName() + "] is of type " + "Model or Map but is not assignable from the actual model. You may need to switch " + "newer MVC infrastructure classes to use this argument."); } args[i] = implicitModel; } else if (SessionStatus.class.isAssignableFrom(paramType)) { args[i] = this.sessionStatus; } else if (HttpEntity.class.isAssignableFrom(paramType)) { args[i] = resolveHttpEntityRequest(methodParam, webRequest); } else if (Errors.class.isAssignableFrom(paramType)) { throw new IllegalStateException("Errors/BindingResult argument declared " + "without preceding model attribute. Check your handler method signature!"); } else if (BeanUtils.isSimpleProperty(paramType)) {// 判断是否参数类型是否是简单类型，若是在使用@RequestParam 方式来处理，否则使用@ModelAttribute 方式处理 paramName = ""; } else { attrName = ""; } } } if (paramName != null) { args[i] = resolveRequestParam(paramName, required, defaultValue, methodParam, webRequest, handler); } else if (headerName != null) { args[i] = resolveRequestHeader(headerName, required, defaultValue, methodParam, webRequest, handler); } else if (requestBodyFound) { args[i] = resolveRequestBody(methodParam, webRequest, handler); } else if (cookieName != null) { args[i] = resolveCookieValue(cookieName, required, defaultValue, methodParam, webRequest, handler); } else if (pathVarName != null) { args[i] = resolvePathVariable(pathVarName, methodParam, webRequest, handler); } else if (attrName != null) { WebDataBinder binder = resolveModelAttribute(attrName, methodParam, implicitModel, webRequest, handler); boolean assignBindingResult = (args.length > i + 1 && Errors.class.isAssignableFrom(paramTypes[i + 1])); if (binder.getTarget() != null) { doBind(binder, webRequest, validate, validationHints, !assignBindingResult); } args[i] = binder.getTarget(); if (assignBindingResult) { args[i + 1] = binder.getBindingResult(); i++; } implicitModel.putAll(binder.getBindingResult().getModel()); } } return args; }RequestMappingHandlerAdapter 中使用的参数绑定，代码稍微有些不同，有兴趣的同仁可以分析下，最后处理的结果都是一样的。示例 1：@RequestMapping({"/", "/home"}) public String showHomePage(String key){ logger.debug("key="+key); return "home"; }这种情况下，就调用默认的 @RequestParam 来处理。示例 2：@RequestMapping(method = RequestMethod.POST) public String doRegister(User user){ if(logger.isDebugEnabled()){ logger.debug("process url[/user], method[post] in "+getClass()); logger.debug(user); } return "user"; }这种情况下，就调用 @ModelAttribute 来处理。参考文档Spring Web Doc: spring-3.1.0/docs/spring-framework-reference/html/mvc.html说明：本文基于 Spring 3.1.0 版本编写，文中涉及的 AnnotationMethodHandlerAdapter 类在 Spring 3.2 中已被标记为 deprecated，并在 Spring 5.0 中移除（由 RequestMappingHandlerAdapter 完全取代）。虽然参数绑定的核心逻辑与结论在现代版本中依然适用，但源码分析部分仅适用于旧版本参考。
访问控制与 RBAC 模型访问控制通常的多用户系统都会涉及到访问控制（Access Control）。所谓访问控制，是指通过某种方式允许或限制用户访问能力及范围的一种方法。这主要是由于系统需要对关键资源进行保护，防止由于非法入侵或者误操作对业务系统造成破坏。简而言之，访问控制即定义哪些用户可以访问哪些资源。一般而言，访问控制系统包括三个组成部分：主体（Subject）：发出访问请求的实体，通常指用户或者用户进程。客体（Object）：被保护的资源，通常是程序、数据、文件或者设备。访问策略（Access Policy）：主体和客体的映射规则，确定一个主体是否对客体具有访问能力。RBAC 模型基于角色的访问控制（Role-Based Access Control, RBAC）的概念在七十年代就已经提出，但是直到九十年代由于安全需求的发展才又引起了广泛关注。RBAC 的核心思想是将系统资源的访问权限进行分类或者建立层次关系，抽象为角色的概念，然后根据安全策略将用户和角色关联，从而实现了用户和权限之间的对照。RBAC 通过引入角色并将其作为权限管理的中介，将访问控制系统分为两个部分，即权限与角色的关联和角色与用户的关联，具有灵活易控制的优点。RBAC 模型的建立和实现是基于角色实现访问控制研究中的两大热点。RBAC96 模型因全面系统地描述了多层次的含义而得到广泛认可。美国国家标准技术协会（NIST）基于 RBAC96 制定了 RBAC 标准，将 RBAC 主要分为：Core RBAC、Hierarchical RBAC、Constraint RBAC。RBAC96 模型包含 4 个层次，如图 1 所示：图 1. RBAC96 模型其中：RBAC0：包含 RBAC 的核心部分即 Core RBAC，是最基本的模型，仅包含基本的 RBAC 元素，即用户、角色、权限、会话。角色之间是平级关系，对象之间没有附加的约束。RBAC1：在 RBAC0 的基础上定义了角色继承关系。RBAC2：同样包含了 RBAC0，并且定义了约束，实际中需要考虑的常见的约束包括角色互斥和角色基数两种。RBAC3：包含了 RBAC1 和 RBAC2，即既有角色的继承关系，又包含约束关系，是最复杂的一种模型。图 2 清晰描述了 RBAC 模型中的用户、角色、权限、继承关系及约束关系。图 2. RBAC 模型关系图约束是 RBAC 的一个重要组成部分，比较常见的约束包括：互斥角色约束：指的是同一用户只可以分配到互斥角色集合中最多一个角色，以支持“职责分离”的原则。必备角色约束：也称为先决条件约束，是定义在用户角色分配关系（UA）和权限角色分配关系（PA）上的重要约束，是指所分配的角色或者权限依赖于另外一种角色或者权限。基数约束：同样也是定义在 UA 和 PA 上的约束，规定了一个角色可以被分配的最大用户数或者一项权限可允许分配的角色数目，从而来控制高级权限在系统中的分配。流行的 Web 安全框架随着网络应用的不断发展，越来越多的应用系统开始移植到 Web 环境，并且具有不同程度的访问控制的需求。随着这种需求的积累，出现了很多基于 RBAC 的 Web 访问控制框架。这些框架的核心理论就是 RBAC，并且为开发人员提供了方便的接口来完成定制化的需求。Membership in ASP.NETASP.NET 中提供了非常好的用户身份认证的管理框架——Membership，提供的功能包括创建新用户、身份认证、密码管理等。默认情况下，ASP.NET Membership 可以支持所有的 ASP.NET 应用程序，并提供了非常友好的使用方式，甚至提供了向导来创建所需要的数据结构——aspnet_regsql.exe。图 3. Membership 数据模型.NET 平台的易用性毋庸置疑，开发者只需要配置 web.config 文件，引入 authentication 配置，增加 membership 节点，再配置其角色管理 roleManager 基本上就完成了对 Membership 的引用。并且 .NET 甚至提供了用户控件 login 和 createUserWizard 等，通过属性的简单配置即可激活 Membership 的认证，完成创建用户的功能等等。本文中的企业应用没有基于 .NET 平台的 Membership 来实现，因此 Membership 并不是本文的重点，不再赘述。Spring Security in JavaJava Web 开发领域中的用于进行认证授权的框架为数众多，单就从 RBAC 的角度而言，Spring Security 已经占据重要的地位，并且被越来越广泛地应用在各种 Web 系统中，为基于 J2EE 企业应用软件提供了全面安全服务。Spring Security 来源于早期的 Spring 开发者邮件列表中的一个问题——提供一个基于 Spring 的安全实现，最初被称之为 Acegi，在 2003 年初作为 Sourceforge 的项目出现，并得到不断的应用和完善。2007 年底，它正式成为 Spring 的组合项目，更名为 Spring Security。接下来，我们将基于其架构重点研究 Spring Security，并基于实际应用需求来讨论一些定制化的实现方法。Spring Security 概述项目概览Spring Security 的上手非常简单，当然，这需要您对基本的认证授权有一些应用背景，并能了解一些其中的核心概念，如上文提到的用户、角色、权限等等。通过 Spring Security 项目的首页，您可以方便地得到它的介绍、文档、样例等等，并且，您还可以基于 Spring Security 的源码构建您的工程。这样，您可以深入地了解它的底层实现，快速地定位问题，方便地进行进一步扩展。特性Spring Security 为企业级应用提供了方便、灵活的安全解决方案，在众多的项目应用中得到锤炼和修正，已经非常稳定和强大。Spring Security 具有如下的特性：易于部署：通过 Spring 的依赖注入，Spring Security 提供了自定义的 XML 命名空间，可以通过不超过 10 行的配置就可以让您的应用程序受到保护。当然，这并不要求您的应用程序一定是基于 Spring 的，Spring Security 和 Struts 的结合使用非常普遍。非侵入式引用：通过 Spring Security 的 Filter Chain，应用程序基本上不需要做额外的太多修改，通过可插拔的方式，保证了应用程序本身和 Security 相关的代码的各自独立。明晰的授权服务：通过基于表达式的配置，安全访问控制规则可以方便灵活地基于当前请求上下文进行定义，不需要额外进行编码。同时 Spring Security 支持多种认证方式，并提供了定义良好的 API、多样化的认证后端等等，并且允许客户应用程序使用多种授权库，比如 XML 文件、数据库等，方便和既有的数据库系统进行集成。而且，它还提供了很多实用的功能，比如密码加密、Remember-me 持久化登录信息等等。RBAC 中的主要概念在 Spring Security 中的对照RBAC 中的几个主要构成在 Spring Security 中得到了很好的对应：主体：Spring Security 中的 Authentication 对象就是 RBAC 中的主体，多数情况下，可以被强制转换为 UserDetails 对象。这是一个 Spring Security 的核心接口，它代表一个主体，是扩展的，而且是为特定程序服务的。获得用户信息的最常用方法就是 loadUserByUsername(String username)，当成功通过验证时，UserDetails 会被用来建立 Authentication 对象，保存在 SecurityContextHolder 里。客体：Spring Security 中的客体可以是 URL，也可以是 Method。访问策略接口：Spring Security 通过 getAuthorities() 方法提供了 GrantedAuthority 对象数组，代表赋予到主体的权限。这些权限通常使用角色表示，比如本文中计费系统中声明的角色 ROLE_ADMINISTRATOR 或 ROLE_PROVIDER。核心服务Security Filter ChainSpring Security 框架提供了一个过滤器链，来提供各种服务。通过下面的过滤器链表格，我们可以看到 Spring Security 为我们提供了哪些实用的服务。表 1. Spring Security 过滤器清单别名过滤器类对应的命名空间元素CHANNEL_FILTERChannelProcessingFilterhttp/intercept-url@requires-channelCONCURRENT_SESSION_FILTERConcurrentSessionFiltersession-management/concurrency-controlSECURITY_CONTEXT_FILTERSecurityContextPersistenceFilterhttpLOGOUT_FILTERLogoutFilterhttp/logoutX509_FILTERX509AuthenticationFilterhttp/x509PRE_AUTH_FILTERAbstractPreAuthenticatedProcessingFilterN/ACAS_FILTERCasAuthenticationFilterN/AFORM_LOGIN_FILTERUsernamePasswordAuthenticationFilterhttp/form-loginBASIC_AUTH_FILTERBasicAuthenticationFilterhttp/http-basicSERVLET_API_SUPPORT_FILTERSecurityContextHolderAwareFilterhttp/@servlet-api-provisionREMEMBER_ME_FILTERRememberMeAuthenticationFilterhttp/remember-meANONYMOUS_FILTERAnonymousAuthenticationFilterhttp/anonymousSESSION_MANAGEMENT_FILTERSessionManagementFiltersession-managementEXCEPTION_TRANSLATION_FILTERExceptionTranslationFilterhttpFILTER_SECURITY_INTERCEPTORFilterSecurityInterceptorhttpSWITCH_USER_FILTERSwitchUserAuthenticationFilterN/A开发人员可以使用 before 或者 after 属性来将自定义的过滤器添加到过滤器链中。本文中的计费系统中采用了自定义的 loginFilter 和 logoutFilter，并使用 before 属性将其分别置于 FORM_LOGIN_FILTER 和 LOGOUT_FILTER 之前，如清单 1 所示：清单 1. 过滤器配置示例<custom-filter before="FORM_LOGIN_FILTER" ref="loginFilter" /> <custom-filter before="LOGOUT_FILTER" ref="logoutFilter" />AuthenticationManagerAuthenticationManager 只是一个接口，其实现可以让开发人员灵活选择。在 Spring Security 中的默认实现是 ProviderManager，Spring Security 通过委派一系列配置好的 AuthenticationProvider，遍历 Providers 的过程中调用 authenticate() 方法，以保证获取不同来源的身份认证。若某个 Provider 能成功确认当前用户的身份，authenticate() 方法会返回一个完整的包含用户授权信息的 Authentication 对象，否则会抛出一个 AuthenticationException。AccessDecisionManager当我们使用命名空间配置时，默认的 AccessDecisionManager 实例会自动注册，基于我们设置的 intercept-url 和 protect-pointcut 权限属性内容，来为方法级别调用和 Web 请求级别访问提供认证服务。基于 Spring Security 实现认证授权数据模型多数基于 RBAC 的框架都提供了基础的数据模型，通常，我们需要对其进行扩展来满足企业级应用系统的需求。扩展的方式一般有两种，即在原有数据结构上进行修改，扩展自定义字段；或者，通过表之间的关联将应用系统的数据结构和 RBAC 的数据结构分离。两种方式各有优劣，通常需要看应用的复杂程度及对框架的开放程度进行不同的选择。本文中计费系统因为用户角色权限关系清晰，故采用在原有数据结构基础上进行自定义数据字段的方式，比如，在 Users 表中增加了系统用户的一些属性，如联系方式、电话等等。快速实现计费系统认证授权需求Spring Security 可以被应用在多种不同的认证环境下，因为它的目标是自管理，即不依赖于容器，所以备受推崇，因为这样可以方便地进行部署和移植。计费系统中主要包含三类用户，即超级管理员、运营商管理员和普通用户。每种用户可以使用的系统功能模块集合和数据集合是不同的。比如，运营商管理员不能使用网关设备管理模块，并且只能管理属于自己的用户，而超级管理员可以管理网关设备，并且可以管理全部用户。因为 Spring Security 基于 URL 实现对资源的授权，所以在开发初期，我们需要规划好资源的路径，以便于进行授权配置，即建立 admin、provider、customer 目录，网关设备的管理目录放在 admin 目录下，而超级管理员和运营商管理员都具有的功能模块放在 provider 目录下。引入 Spring Security通过上面的需求分析，可以看出，使用 Spring Security 完全可以实现计费系统的认证授权功能，并且可以非常快速地验证。首先，我们要将需要的 jar 文件引入到工程中，包括：spring-security-acl-3.0.3.RELEASE.jarspring-security-config-3.0.3.RELEASE.jarspring-security-core-3.0.3.RELEASE.jarspring-security-taglibs-3.0.3.RELEASE.jarspring-security-web-3.0.3.RELEASE.jar其次，我们需要完成以下几个部分的修改：修改 web.xml 设置过滤器，代码如清单 2 所示。清单 2. Web.xml 配置示例<filter> <filter-name>springSecurityFilterChain</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>springSecurityFilterChain</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>修改 applicationContext-Security.xml，这个文件可以根据需要设定，在容器启动时加载，其中设置了实现 HTTP 安全和认证授权相关的服务 Bean 来应用框架的认证授权功能，如清单 3 所示。清单 3. applicationContext-Security.xml 配置示例<http entry-point-ref="loginUrlEntryPoint" access-denied-page="/common/403.jsp"> <intercept-url pattern="/image/**" filters="none" /> <intercept-url pattern="/common/**" filters="none" /> <intercept-url pattern="/admin/**" access="ROLE_ADMIN" /> <intercept-url pattern="/admin_provider/**" access="ROLE_ADMIN" /> <intercept-url pattern="/admin_provider/**" access="ROLE_PROVIDER" /> <custom-filter before="FORM_LOGIN_FILTER" ref="loginFilter" /> <custom-filter position="FORM_LOGIN_FILTER" ref="adminLoginFilter" /> <custom-filter before="LOGOUT_FILTER" ref="logoutFilter" /> <custom-filter position="LOGOUT_FILTER" ref="adminLogoutFilter" /> </http> <authentication-manager alias="authenticationManager"> <authentication-provider user-service-ref="usersService"> </authentication-provider> </authentication-manager>实现自定义的 userDetailsService。通常我们需要对认证授权相关的数据结构进行其他的操作，这些操作并没有集成在 Spring Security 中，因此多数我们需要实现自己的 userDetailsService，如清单 4 所示。清单 4. userDetailsService Bean 配置示例<beans:bean id="usersService" class="com.fengfan.service.impl.UsersServiceImpl"> <beans:property name="dataSource" ref="dataSource"/> <beans:property name="authenticationManager" ref="authenticationManager"/> <beans:property name="usersDAO" ref="usersDAO"/> </beans:bean>通过以上几部分的修改，我们就可以方便地集成 Spring Security，进入企业应用系统的认证授权开发了。接下来，我们将讨论更多的定制化需求的实现方式，以此来说明 Spring Security 在企业级应用系统开发中的可扩展性和灵活性及易用性。定制化需求计费系统中有很多定制化的需求实现，下面，我们将就各个需求点进行说明，Spring Security 的成熟度和灵活性在这里得到了充分的体现。基于 URL 的授权实现通过上面的系统需求，我们实现了清单 5 所示的基于 URL 的权限约束：清单 5. 基于 URL 的权限约束<intercept-url pattern="/admin/**" access="ROLE_ADMIN" /> <intercept-url pattern="/admin_provider/**" access="ROLE_ADMIN" /> <intercept-url pattern="/admin_provider/**" access="ROLE_PROVIDER" />这种资源的授权方式要求在开发初期对目录结构进行较清晰的规划，从而避免相同功能页面的重复拷贝的问题。当然，我们可以将资源及授权操作通过数据结构的进一步扩展都保存在数据库中，这样具有更大的灵活性。自定义的 UserDetailsService除了认证授权涉及到系统用户外，系统本身需要提供对系统用户的信息维护功能，因此，我们需要自定义 UserDetailsService，这也非常方便，我们只需要继承 JdbcUserDetailsManager，并实现 IUsersService 接口即可。这样，我们就可以在 UsersServiceImpl 中实现定制的和系统用户相关的方法，同时通过继承也得到了和安全认证相关的方法，如清单 6 所示。清单 6. UsersServiceImpl 方法声明public class UsersServiceImpl extends JdbcUserDetailsManager implements IUsersService如何使用 Remember-Me通常我们在应用系统中如果要实现这个功能，需要增加一定的编码工作才能实现，需要操作 Cookies 对象。但是 Spring Security 为 Remember-Me 实现提供了必要的调用钩子，并提供了两个 Remember-Me 的具体实现。其中一个使用散列来保护基于 Cookie 标记的安全性，另一个使用了数据库或其他持久化存储机制来保存生成的标记。我们只需要在 <http> 段下面添加 <remember-me> 元素就可以使用 Remember-Me 认证。会话同步控制首先，您需要把下面的监听器添加到您的 web.xml 文件里，让 Spring Security 获得 Session 生存周期事件：<listener> <listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class> </listener>然后可以在 HTTP 安全配置中增加一个 session-management 段的配置，设置其中的 concurrency-control 来控制一个帐号最多允许登录多少次，比如 <concurrency-control max-sessions="1" error-if-maximum-exceeded="true"/>，这样，当重复登录时就会出现错误提示。不同的登录页面在上面的 HTTP 安全的设置部分，我们已经看到为不同的用户的登录和退出设置了不同的自定义过滤器，下面我们通过超级管理的 loginFilter 的配置来说明如何实现不同的用户登录之后到不同的主页，如清单 7 所示。清单 7. 超级管理员的登录配置 <beans:bean id="adminLoginFilter" class="org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter"> <beans:property name="authenticationManager" ref="authenticationManager" /> <beans:property name="authenticationFailureHandler" ref="adminFailureHandler" /> <beans:property name="authenticationSuccessHandler" ref="adminSuccessHandler" /> <beans:property name="filterProcessesUrl" value="/login_admin" /> </beans:bean>从上面的配置我们看到当登录成功后，使用 authenticationSuccessHandler 来处理后续动作，我们来看一下如下的这个 Bean 的配置，就可以明白如何通过 defaultTargetUrl 属性来设置其登录成功之后跳转的页面了。我们只需要再配置另外一个 Bean，设置不同的属性值，就可以实现不同用户登录之后到达不同的主页面的需求，如清单 8 所示。清单 8. 不同主页面跳转配置<beans:bean id="adminSuccessHandler" class="com.fengfan.security.AppAuthenticationSuccessHandler"> <beans:property name="alwaysUseDefaultTargetUrl" value="true" /> <beans:property name="defaultTargetUrl" value="/admin/index.jsp" /> <beans:property name="usersDAO" ref="usersDAO"/> </beans:bean>同样的，我们可以设置不同的 logoutFilter，这样不同角色的用户选择退出系统之后可以拥有不同的退出成功的页面或者跳转的相应的重新登录的页面。修改认证信息后不需要重新登录很多时候，用户在登录之后可能对自身的信息进行修改，但是，我们不需要用户来重新登录，而是希望修改的信息在当前的会话中立即生效。Spring Security 提供了相应的接口方便我们实现这种需求，比如我们在用户修改密码之后，将主体中的信息也同时更新，如清单 9 所示。清单 9. 修改密码代码示范userCache.removeUserFromCache(username); SecurityContextHolder.getContext().setAuthentication( createNewAuthentication(currentUser, newPassword));总结本文从计费系统的实际应用出发，通过分析系统的认证授权需求，研究 RBAC 的基础模型和 Spring Security 框架的核心组件及服务，充分发挥 Spring Security 的成熟度和灵活性的优势，快速完善地实现了计费管理中的认证授权系统，取得了很好的效果。相关主题查看文章"Role Based Access Control（RBAC) and Role Based Security"，了解 RBAC 模型的基本原理。查看文章"An Introduction to Role-based Access Control"，了解常见 RBAC 模型及应用。查看文章"Spring Security"，了解 Spring Security 的设计和使用。查看文章"Introduction to Membership"，了解微软身份认证管理框架。developerWorks Java 技术专区：这里有数百篇关于 Java 编程各个方面的文章。说明：本文基于 Spring Security 3.x 版本编写（文中提及最新版本为 3.1.0），当前 Spring Security 主流版本已更新至 6.x，部分配置方式、类名及命名空间可能存在差异，实际开发中请参考官方最新文档。
Spring MVC 3.2 技术预览（三）：动手写一个异步Controller方法
引言在前面的文章中，我介绍了 Servlet 3 与 Spring MVC 3.2 中支持异步的新特性，并探讨了一些实时更新的技术背景。在这篇文章中，我将展示 Spring MVC 3.2 新特性的技术细节，以及它们对 Spring MVC 请求生命周期的多方面影响。异步 Controller 方法基础如果需要将 Controller 层的方法转变为异步方法，只需将方法的返回值类型改为 Callable 即可。例如：原本返回视图名 String 的方法，可以改为返回 Callable<String>。原本返回 ResponseEntity 的方法，可以改为返回 Callable<ResponseEntity>。其他返回值类型依此类推。在这种处理方式中，除了 Controller 层方法会在另一个线程中处理完成外，其他工作方式没有发生任何变化。当方法改为异步处理后，保持处理逻辑的简单非常重要。因为即便只是将方法改为异步方式，仍有诸多相关问题需要考虑。示例代码GitHub 上 spring-mvc-showcase 项目的 spring-mvc-async 分支里，提供了许多 Controller 层异步方法的示例。返回 @ResponseBody例如下面的 @ResponseBody 方法，返回了视图名 String：@RequestMapping(value="/response/annotation", method=RequestMethod.GET) public @ResponseBody Callable<String> responseBody() { return new Callable<String>() { public String call() throws Exception { // Do some work.. Thread.sleep(3000L); return "The String ResponseBody"; } }; }返回 ResponseEntity以及下面的 ResponseEntity 方法：@RequestMapping(value="/response/entity/headers", method=RequestMethod.GET) public Callable<ResponseEntity<String>> responseEntityCustomHeaders() { return new Callable<ResponseEntity<String>>() { public ResponseEntity<String> call() throws Exception { // Do some work.. Thread.sleep(3000L); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.TEXT_PLAIN); return new ResponseEntity<String>( "The String ResponseBody with custom header Content-Type=text/plain", headers, HttpStatus.OK); } }; }重定向视图还有 redirect 类型的视图名方法：@RequestMapping(value="/uriTemplate", method=RequestMethod.GET) public Callable<String> uriTemplate(final RedirectAttributes redirectAttrs) { return new Callable<String>() { public String call() throws Exception { // Do some work.. Thread.sleep(3000L); redirectAttrs.addAttribute("account", "a123"); // Used as URI template variable redirectAttrs.addAttribute("date", new LocalDate(2011, 12, 31)); // Appended as a query parameter return "redirect:/redirect/{account}"; } }; }异常处理添加了 @RequestMapping 注解和 @ResponseBody 注解的方法中，这些注解同样会应用到返回值 Callable 中。添加了 @ExceptionHandler 注解的方法也一样，它可以处理 Controller 层方法返回的 Callable 中抛出的异常。package org.springframework.samples.mvc.exceptions; import java.util.concurrent.Callable; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; @Controller public class ExceptionController { @RequestMapping("/exception") public @ResponseBody Callable<String> exception() { return new Callable<String>() { public String call() throws Exception { // Do some work.. Thread.sleep(2000L); throw new IllegalStateException("Sorry!"); } }; } @ExceptionHandler public @ResponseBody String handle(IllegalStateException e) { return "IllegalStateException handled!"; } }在 GitHub 中提交的 这个版本，记录了其中全部更新的情况。运行日志分析如果你运行了上面的任意一个方法，将会在控制台看到如下信息：16:19:23 [http-bio-8080-exec-3] DispatcherServlet - DispatcherServlet with name 'appServlet' processing ... 16:19:23 [http-bio-8080-exec-3] RequestMappingHandlerMapping - Looking up handler method for path /views/html 16:19:23 [http-bio-8080-exec-3] RequestMappingHandlerMapping - Returning handler method ... 16:19:23 [http-bio-8080-exec-3] DispatcherServlet - Exiting request thread and leaving the response open 16:19:23 [SimpleAsyncTaskExecutor-1] DispatcherServlet - Resuming asynchronous processing of ... 16:19:26 [SimpleAsyncTaskExecutor-1] DispatcherServlet - Rendering view ... 16:19:26 [SimpleAsyncTaskExecutor-1] JstlView - Added model object 'fruit' 16:19:26 [SimpleAsyncTaskExecutor-1] JstlView - Added model object 'foo' 16:19:26 [SimpleAsyncTaskExecutor-1] JstlView - Forwarding to resource [/WEB-INF/views/views/html.jsp] 16:19:26 [SimpleAsyncTaskExecutor-1] DispatcherServlet - Successfully completed request 16:19:26 [SimpleAsyncTaskExecutor-1] AsyncExecutionChainRunnable - Completing async request processing 从上面的日志信息中可以看出，Servlet 容器调用的线程马上就执行完了方法，而余下的处理内容则在 3 秒钟后由另外一个线程完成。除了线程切换之外，上面的日志信息与普通的请求处理信息是一样的。线程池配置正如上面的日志信息所示，返回值 Callable 会默认调用 SimpleAsyncTaskExecutor 类来处理。这个类非常简单，而且不会重用线程。在实际的产品环境中，你可能需要使用 AsyncTaskExecutor 类来针对所处环境进行适当的配置，甚至有可能你已经有了一个配置好的 AsyncTaskExecutor 类。可以通过 RequestMappingHandlerAdapter 类中的 asyncTaskExecutor 属性来引用它。超时设定超时设定是我们需要考虑的一个非常重要的方面。因为 Servlet 容器可能会强制将一个超时的未完成异步请求关闭。你可以通过 RequestMappingHandlerAdapter 类中的 asyncRequestTimeout 属性指定超时时间。如果不指定超时时间，超时的时长将取决于 Servlet 容器所设定的时间。在 Tomcat 中，这个超时时间被默认设定为 10 秒钟（从 Servlet 容器调用的线程执行时就开始计时）。在超时后仍然使用一个 request 或 response 的影响是不确定的。在实际使用中，Servlet 容器将尝试重用 request 和 response 对象。这样一来，避免在超时后仍然使用 request 和 response 将变得非常重要。事实上，没有方法可以检测 request 是否已经超时。但是 Servlet API 中，当请求超时或网络出现问题时，将提供一个声明式的回调函数。Spring MVC 中自动注册了这个声明，因此可以得知一对请求响应是否不应该被使用。想要完全理解上面的过程，可以参考其中涉及到的三个线程：请求处理开始的线程（Servlet 容器调用的线程）。执行 Callable 方法的线程（异步线程）。Servlet 容器向 Spring MVC 声明超时的线程。异常处理异步处理中 HandlerExceptionResolver 类和异常处理的机制没有太多不同。当返回 Callable 之前发生了异常，处理方式与普通异常一样。当执行 Callable 方法的过程中产生异常，处理方式也与普通异常一样，只不过将在当前线程（异步线程）中处理，并将仍然返回未处理的 500 状态的 response。ThreadLocal 属性Spring MVC 的一些部分和 Spring MVC 应用程序可能会依赖 ThreadLocal 存储来获取 request、locale 及其他信息。当以异步的方式执行 Callable 方法时，异步线程将不会自动拥有相同的 ThreadLocal 属性。OpenSessionInViewFilter 和 OpenSessionInViewInterceptor 也被更新为以透明的方式工作。而当 Controller 层的方法使用了 @Transactional 注解时，方法返回时就将完成事务，而不会扩展到执行 Callable 方法的内部。如果 Callable 需要处理事务，则需要委托（delegate）一个事务组件。拦截器处理已注册的 HandlerInterceptor 实例将与异步请求协作工作。主要的区别是：preHandle 在 Servlet 容器线程开始的时候调用。postHandle 和 afterCompletion 方法则在异步线程中调用。在大多数情况下不会出现问题，除非 HandlerInterceptor 设置并清除了 ThreadLocal 属性。需要如此做的拦截器可能实现了 AsyncHandlerInterceptor 接口，这个接口为异步请求的处理添加了生命周期。Servlet 过滤器一些过滤器将正常工作。而其他的过滤器将尝试在 Servlet 容器线程退出后执行后置处理（post-processing）。这样的过滤器需要进行一定的修改，用来在异步线程中完成后置处理。所有的 Spring 过滤器都已经按照要求（按照异步请求处理的要求）进行修改，来与异步请求协同工作。但是第三方的过滤器是否能够在 Spring MVC 下正常处理异步请求，取决于这些过滤器的实现细节。总结在我的下一篇文章中，我将使用一个基于接收外部事件（AMQP 消息）的示例，将其使用传统轮询的方式修改为使用长轮询的方式，用来在浏览器中显示实时信息。原文地址：http://blog.springsource.org/2012/05/10/spring-mvc-3-2-preview-making-a-controller-method-asynchronous/说明：本文基于 Spring MVC 3.2 技术预览版编写，属于较早期的异步支持方案（基于 Callable）。现代 Spring 版本（如 Spring 5+）推荐使用 CompletableFuture、DeferredResult 或响应式编程模型（WebFlux）来处理异步请求，具体实现细节可能有所不同。
什么是 RestClient？RestClient 是 Spring Framework 6.1 引入的一个同步 HTTP 客户端，旨在替代传统的 RestTemplate。它提供了更简洁、现代的 API 设计，专注于同步请求场景，支持链式调用（Fluent API），并能够与 Spring 生态（如错误处理、拦截器等）无缝集成。适用版本：本文内容适用于 Spring Framework 6.1+ 及 Spring Boot 3.2+。核心特性同步请求：专为传统阻塞式调用场景设计，线程模型简单直观。链式调用：采用 Fluent API 风格，代码结构更清晰，可读性更强。灵活配置：支持自定义请求头、拦截器、错误处理策略等。生态集成：可直接作为 Bean 注入，或复用 RestTemplate 的相关组件（如 HttpMessageConverter）。依赖配置确保项目使用 Spring Boot 3.2+，或手动引入 spring-web 6.1+。在 Spring Boot 项目中，通常无需指定版本号，由父工程管理依赖。 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>快速使用示例1. 创建 RestClient 实例通过 Builder 模式进行基础配置，包括基础 URL 和默认请求头。import org.springframework.web.client.RestClient; // 基础配置 RestClient client = RestClient.builder() .baseUrl("https://api.example.com") // 设置基础 URL .defaultHeader("Accept", "application/json") // 设置默认请求头 .build();2. 发起 GET 请求获取 JSON 响应并自动反序列化为 Java 对象。// 获取 JSON 响应并反序列化为对象 User user = client.get() .uri("/users/{id}", 1) // 设置 URI 及路径变量 .retrieve() // 发起请求并准备提取响应 .body(User.class); // 将响应体反序列化为 User 对象 System.out.println(user.getName());3. 发起 POST 请求提交 JSON 数据到服务器，并接收创建后的资源信息。// 提交 JSON 数据 User newUser = new User("Alice", 30); User createdUser = client.post() .uri("/users") .contentType(MediaType.APPLICATION_JSON) .body(newUser) // 设置请求体 .retrieve() .body(User.class);4. 处理错误自定义状态码处理逻辑，针对特定 HTTP 状态抛出业务异常。// 自定义错误处理 client.get() .uri("/users/{id}", 999) .retrieve() .onStatus(status -> status.value() == 404, (req, res) -> { throw new UserNotFoundException("User not found"); }) .body(User.class);高级功能1. 添加拦截器通过拦截器统一处理认证头或其他跨切面逻辑。RestClient client = RestClient.builder() .baseUrl("https://api.example.com") .requestInterceptor(headers -> { // 添加认证头 headers.set("Authorization", "Bearer token"); }) .build();2. 自定义消息转换器注册或自定义 HTTP 消息转换器，以支持特殊的数据格式序列化与反序列化。RestClient client = RestClient.builder() .messageConverters(converters -> { converters.add(new MappingJackson2HttpMessageConverter()); converters.add(new StringHttpMessageConverter()); }) .build();3. 文件下载处理二进制流响应，例如下载 PDF 或图片文件。client.get() .uri("/files/{name}", "report.pdf") .accept(MediaType.APPLICATION_PDF) .retrieve() .body(InputStreamResource.class); // 获取文件输入流资源与旧组件的对比特性RestClient (6.1+)RestTemplate (旧)WebClient (响应式)请求类型同步同步异步/非阻塞API 设计链式调用 (Fluent)传统方法调用链式调用 + Reactor适用场景简单同步 HTTP 调用旧项目兼容维护响应式或复杂异步场景主要依赖spring-webspring-webspring-webflux总结推荐场景：在 Spring 6.1+ 及 Spring Boot 3.2+ 的新项目中，优先使用 RestClient 替代 RestTemplate。核心优势：语法简洁直观、易于扩展、与 Spring 生态深度集成。注意事项：RestClient 专注于同步阻塞场景；如果需要异步或响应式编程模型，请继续使用 WebClient。说明：本文示例基于 Spring Framework 6.1 及 Spring Boot 3.2 版本编写。若使用更低版本，部分 API 可能不可用，请升级框架版本或沿用 RestTemplate。
AOP 概述AOP（Aspect Oriented Programming，面向切面编程）作为面向对象编程（OOP）的一种补充，广泛应用于处理具有横切性质的系统级服务，如事务管理、安全检查、缓存、对象池管理等。AOP 实现的关键在于 AOP 框架自动创建的 AOP 代理。AOP 代理可分为 静态代理 和 动态代理 两大类：静态代理：指使用 AOP 框架提供的命令进行编译，在编译阶段生成 AOP 代理类，因此也称为 编译时增强。动态代理：指在运行时借助 JDK 动态代理、CGLIB 等在内存中“临时”生成 AOP 动态代理类，因此也称为 运行时增强。AOP 的存在价值在传统 OOP 编程中，软件系统由一系列相互依赖的对象组成，这些对象被抽象成类，并通过继承管理类与类之间一般到特殊的关系。我们可以通过分析、抽象出具有特定属性与行为的对象，并通过对象间的协作形成完整的软件功能。然而，随着软件规模增大和应用升级，OOP 逐渐暴露出一些难以解决的问题。假设系统中有 3 段完全相似的代码，传统开发通常采用“复制”、“粘贴”的方式来完成。通过这种方式开发出来的软件结构如图 1 所示。图 1. 多个地方包含相同代码的软件点击查看大图如图 1 所示，这种做法存在明显不足：如果深色代码段需要修改，必须打开 3 个地方进行修改。若这段代码分布在 100 个甚至 1000 个地方，维护后果将不堪设想。为了解决这个问题，通常会将深色代码部分定义成一个方法，然后在 3 个代码段中分别调用该方法。在这种方式下，软件系统的结构如图 2 所示。图 2. 通过方法调用实现系统功能点击查看大图对于如图 2 所示的系统，如果需要修改深色部分的代码，只需修改被调用的方法即可，大大降低了软件后期维护的复杂度。然而，对于方法 1、方法 2、方法 3 依然需要显式调用深色方法。在某些特殊情况下，应用需要业务方法与横切逻辑彻底分离——即业务方法无须直接调用深色方法。例如，系统前期设计时只实现了核心业务功能，随后需求变更频繁：需要为方法增加事务控制；需要进行用户合法性验证；需要增加日志记录；......面对这样的情况，通常有两种做法：根据需求说明书，直接拒绝客户要求。拥抱需求，满足客户的需求。第一种做法显然不可取。若采用第二种做法，是否每次都要定义一个新方法，然后修改所有业务方法增加调用？工作量巨大。我们希望有一种机制：只要定义该方法，无须在业务方法中显式调用，系统会“自动”执行该特殊方法。实现这个需求的技术就是 AOP。AOP 专门用于处理系统中分布于各个模块（不同方法）中的交叉关注点问题。在 Java EE 应用中，常常通过 AOP 来处理具有横切性质的系统级服务，AOP 已经成为一种非常常用的解决方案。使用 AspectJ 的编译时增强进行 AOPAspectJ 是一个基于 Java 语言的 AOP 框架，提供了强大的 AOP 功能，其他很多 AOP 框架都借鉴或采纳了其中的一些思想。AspectJ 主要包括两个部分：语言规范：定义了如何表达、定义 AOP 编程中的语法规范，方便地用 AOP 来解决 Java 语言中存在的交叉关注点问题。工具部分：包括编译器、调试工具等。AspectJ 是最早、功能比较强大的 AOP 实现之一。在 Java 领域，AspectJ 中的很多语法结构基本上已成为 AOP 领域的标准。下载、安装 AspectJ 比较简单，读者登录 AspectJ 官网（http://www.eclipse.org/aspectj），即可下载到一个可执行的 JAR 包，使用 java -jar aspectj-1.x.x.jar 命令、多次单击"Next"按钮即可成功安装。成功安装 AspectJ 之后，将会在安装路径（例如 E:\Java\AOP\aspectj1.6）下看到如下文件结构：bin：存放了 aj、aj5、ajc、ajdoc、ajbrowser 等命令。其中 ajc 命令最常用，作用类似于 javac，用于对普通 Java 类进行编译时增强。docs：存放了 AspectJ 的使用说明、参考手册、API 文档等。lib：存放了 4 个 JAR 文件，是 AspectJ 的核心类库。相关授权文件。虽然 AspectJ 是 Eclipse 基金组织的开源项目，且提供了 Eclipse 的 AJDT 插件（AspectJ Development Tools），但 AspectJ 绝对无须依赖于 Eclipse 工具。实际上，AspectJ 的用法非常简单，就像使用 JDK 编译、运行 Java 程序一样。下面通过一个简单的程序来示范 AspectJ 的用法，并分析 AspectJ 如何在编译时进行增强。首先编写一个简单的 Java 类，模拟一个业务组件。清单 1. Hello.javapublic class Hello { // 定义一个简单方法，模拟应用中的业务逻辑方法 public void sayHello() { System.out.println("Hello AspectJ!"); } // 主方法，程序的入口 public static void main(String[] args) { Hello h = new Hello(); h.sayHello(); } }上面 Hello 类模拟了一个业务逻辑组件。编译、运行该 Java 程序，结果没有任何悬念，程序将在控制台打印"Hello AspectJ!"字符串。假设现在客户需要在执行 sayHello() 方法之前启动事务，当该方法执行结束时关闭事务。在传统编程模式下，必须手动修改 sayHello() 方法；如果改为使用 AspectJ，则无须修改上面的 sayHello() 方法。下面我们定义一个特殊的 Java 类。清单 2. TxAspect.javapublic aspect TxAspect { // 指定执行 Hello.sayHello() 方法时执行下面代码块 void around(): call(void Hello.sayHello()) { System.out.println("开始事务 ..."); proceed(); System.out.println("事务结束 ..."); } }可能读者已经发现，上面类文件中不是使用 class、interface、enum 定义 Java 类，而是使用了 aspect。难道 Java 语言又新增了关键字？没有！上面的 TxAspect 根本不是一个 Java 类，所以 aspect 也不是 Java 支持的关键字，它只是 AspectJ 才能识别的关键字。上面粗体字代码也不是方法，它只是指定当程序执行 Hello 对象的 sayHello() 方法时，系统将改为执行粗体字代码的花括号代码块，其中 proceed() 代表回调原来的 sayHello() 方法。正如前面提到的，Java 无法识别 TxAspect.java 文件的内容，所以我们要使用 ajc.exe 命令来编译上面的 Java 程序。为了能在命令行使用 ajc.exe 命令，需要把 AspectJ 安装目录下的 bin 路径（比如 E:\Java\AOP\aspectj1.6\bin 目录）添加到系统的 PATH 环境变量中。接下来执行如下命令进行编译：ajc -d . Hello.java TxAspect.java我们可以把 ajc.exe 理解成 javac.exe 命令，都用于编译 Java 程序，区别是 ajc.exe 命令可识别 AspectJ 的语法；从这个意义上看，我们可以将 ajc.exe 当成一个增强版的 javac.exe 命令。运行该 Hello 类依然无须任何改变，因为 Hello 类位于 lee 包下。程序使用如下命令运行 Hello 类：java lee.Hello运行该程序，将看到一个令人惊喜的结果：开始事务 ... Hello AspectJ! 事务结束 ...从上面运行结果来看，我们完全可以不对 Hello.java 类进行任何修改，同时又可以满足客户的需求。上面程序只是在控制台打印“开始事务 ..."、“结束事务 ..."来模拟了事务操作，实际上我们可用实际的事务操作代码来代替这两行简单的语句。如果客户再次提出新需求，需要在 sayHello() 方法后增加记录日志的功能，那也很简单，我们再定义一个 LogAspect，程序如下：清单 3. LogAspect.javapublic aspect LogAspect { // 定义一个 PointCut，其名为 logPointcut // 该 PointCut 对应于指定 Hello 对象的 sayHello 方法 pointcut logPointcut(): execution(void Hello.sayHello()); // 在 logPointcut 之后执行下面代码块 after(): logPointcut() { System.out.println("记录日志 ..."); } }上面程序的粗体字代码定义了一个 Pointcut：logPointcut 等同于执行 Hello 对象的 sayHello() 方法，并指定在 logPointcut 之后执行简单的代码块，也就是说，在 sayHello() 方法之后执行指定代码块。使用如下命令来编译上面的 Java 程序：ajc -d . *.java再次运行 Hello 类，将看到如下运行结果：开始事务 ... Hello AspectJ! 记录日志 ... 事务结束 ...从上面运行结果来看，通过使用 AspectJ 提供的 AOP 支持，我们可以为 sayHello() 方法不断增加新功能。为什么在对 Hello 类没有任何修改的前提下，而 Hello 类能不断地、动态增加新功能呢？这看上去并不符合 Java 基本语法规则。实际上我们可以使用 Java 的反编译工具来反编译前面程序生成的 Hello.class 文件，发现 Hello.class 文件的代码如下：清单 4. Hello.classpackage lee; import java.io.PrintStream; import org.aspectj.runtime.internal.AroundClosure; public class Hello { public void sayHello() { try { System.out.println("Hello AspectJ!"); } catch (Throwable localThrowable) { LogAspect.aspectOf().ajc$after$lee_LogAspect$1$9fd5dd97(); throw localThrowable; } LogAspect.aspectOf().ajc$after$lee_LogAspect$1$9fd5dd97(); } // ... private static final void sayHello_aroundBody1$advice(Hello target, TxAspect ajc$aspectInstance, AroundClosure ajc$aroundClosure) { System.out.println("开始事务 ..."); AroundClosure localAroundClosure = ajc$aroundClosure; sayHello_aroundBody0(target); System.out.println("事务结束 ..."); } }不难发现这个 Hello.class 文件不是由原来的 Hello.java 文件编译得到的，该 Hello.class 里新增了很多内容。这表明 AspectJ 在编译时“自动”编译得到了一个新类，这个新类增强了原有的 Hello.java 类的功能，因此 AspectJ 通常被称为 编译时增强 的 AOP 框架。提示：与 AspectJ 相对的还有另外一种 AOP 框架，它们不需要在编译时对目标类进行增强，而是运行时生成目标类的代理类。该代理类要么与目标类实现相同的接口，要么是目标类的子类。总之，代理类的实例可作为目标类的实例来使用。一般来说，编译时增强的 AOP 框架在性能上更有优势，因为运行时动态增强的 AOP 框架需要每次运行时都进行动态增强。实际上，AspectJ 允许同时为多个方法添加新功能，只要我们定义 Pointcut 时指定匹配更多的方法即可。如下片段：pointcut xxxPointcut(): execution(void H*.say*());上面程序中的 xxxPointcut 将可以匹配所有以 H 开头的类中、所有以 say 开头的方法，但该方法返回的必须是 void；如果不想匹配任意的返回值类型，则可将代码改为如下形式：pointcut xxxPointcut(): execution(* H*.say*());关于如何定义 AspectJ 中的 Aspect、Pointcut 等，读者可以参考 AspectJ 安装路径下的 doc 目录里的 quick5.pdf 文件。使用 Spring AOP与 AspectJ 相同的是，Spring AOP 同样需要对目标类进行增强，也就是生成新的 AOP 代理类；与 AspectJ 不同的是，Spring AOP 无需使用任何特殊命令对 Java 源代码进行编译，它采用运行时动态地、在内存中临时生成“代理类”的方式来生成 AOP 代理。Spring 允许使用 AspectJ Annotation 用于定义方面（Aspect）、切入点（Pointcut）和增强处理（Advice），Spring 框架则可识别并根据这些 Annotation 来生成 AOP 代理。Spring 只是使用了和 AspectJ 5 一样的注解，但并没有使用 AspectJ 的编译器或者织入器（_Weaver_），底层依然使用的是 Spring AOP，依然是在运行时动态生成 AOP 代理，并不依赖于 AspectJ 的编译器或者织入器。简单地说，Spring 依然采用运行时生成动态代理的方式来增强目标对象，所以它不需要增加额外的编译，也不需要 AspectJ 的织入器支持；而 AspectJ 采用编译时增强，所以 AspectJ 需要使用自己的编译器来编译 Java 文件，还需要织入器。为了启用 Spring 对 @AspectJ 方面配置的支持，并保证 Spring 容器中的目标 Bean 被一个或多个方面自动增强，必须在 Spring 配置文件中配置如下片段：清单 8. bean.xml (部分)<?xml version="1.0" encoding="GBK"?> <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/> </beans>当然，如果我们希望完全启动 Spring 的“零配置”功能，则还需要启用 Spring 的“零配置”支持，让 Spring 自动搜索指定路径下 Bean 类。所谓 自动增强，指的是 Spring 会判断一个或多个方面是否需要对指定 Bean 进行增强，并据此自动生成相应的代理，从而使得增强处理在合适的时候被调用。如果不打算使用 Spring 的 XML Schema 配置方式，则应该在 Spring 配置文件中增加如下片段来启用 @AspectJ 支持： <bean class="org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator"/>上面配置文件中的 AnnotationAwareAspectJAutoProxyCreator 是一个 Bean 后处理器（BeanPostProcessor），该 Bean 后处理器将会为容器中 Bean 生成 AOP 代理。当启动了 @AspectJ 支持后，只要我们在 Spring 容器中配置一个带 @Aspect 注释的 Bean，Spring 将会自动识别该 Bean，并将该 Bean 作为方面 Bean 处理。在 Spring 容器中配置方面 Bean（即带 @Aspect 注释的 Bean），与配置普通 Bean 没有任何区别，一样使用 <bean.../> 元素进行配置，一样支持使用依赖注入来配置属性值；如果我们启动了 Spring 的“零配置”特性，一样可以让 Spring 自动搜索，并装载指定路径下的方面 Bean。使用 @Aspect 标注一个 Java 类，该 Java 类将会作为方面 Bean，如下面代码片段所示：// 使用 @Aspect 定义一个方面类 @Aspect public class LogAspect { // 定义该类的其他内容 ... }方面类（用 @Aspect 修饰的类）和其他类一样可以有方法、属性定义，还可能包括切入点、增强处理定义。当我们使用 @Aspect 来修饰一个 Java 类之后，Spring 将不会把该 Bean 当成组件 Bean 处理，因此负责自动增强的后处理 Bean 将会略过该 Bean，不会对该 Bean 进行任何增强处理。开发时无须担心使用 @Aspect 定义的方面类被增强处理，当 Spring 容器检测到某个 Bean 类使用了 @Aspect 标注之后，Spring 容器不会对该 Bean 类进行增强。下面将会考虑采用 Spring AOP 来改写前面介绍的例子。使用一个简单的 Chinese 类来模拟业务逻辑组件：清单 5. Chinese.java@Component public class Chinese { // 实现 Person 接口的 sayHello() 方法 public String sayHello(String name) { System.out.println("-- 正在执行 sayHello 方法 --"); // 返回简单的字符串 return name + " Hello , Spring AOP"; } // 定义一个 eat() 方法 public void eat(String food) { System.out.println("我正在吃 :" + food); } }提供了上面 Chinese 类之后，接下来假设同样需要为上面 Chinese 类的每个方法增加事务控制、日志记录，此时可以考虑使用 Around、AfterReturning 两种增强处理。先看 AfterReturning 增强处理代码。清单 6. AfterReturningAdviceTest.java// 定义一个方面 @Aspect public class AfterReturningAdviceTest { // 匹配 org.crazyit.app.service.impl 包下所有类的、 // 所有方法的执行作为切入点 @AfterReturning(returning = "rvt", pointcut = "execution(* org.crazyit.app.service.impl.*.*(..))") public void log(Object rvt) { System.out.println("获取目标方法返回值 :" + rvt); System.out.println("模拟记录日志功能 ..."); } }上面 Aspect 类使用了 @Aspect 修饰，这样 Spring 会将它当成一个方面 Bean 进行处理。其中程序中粗体字代码指定将会在调用 org.crazyit.app.service.impl 包下的所有类的所有方法之后织入 log(Object rvt) 方法。再看 Around 增强处理代码：清单 7. AroundAdviceTest.java// 定义一个方面 @Aspect public class AroundAdviceTest { // 匹配 org.crazyit.app.service.impl 包下所有类的、 // 所有方法的执行作为切入点 @Around("execution(* org.crazyit.app.service.impl.*.*(..))") public Object processTx(ProceedingJoinPoint jp) throws java.lang.Throwable { System.out.println("执行目标方法之前，模拟开始事务 ..."); // 执行目标方法，并保存目标方法执行后的返回值 Object rvt = jp.proceed(new String[]{"被改变的参数"}); System.out.println("执行目标方法之后，模拟结束事务 ..."); return rvt + " 新增的内容"; } }与前面的 AfterReturning 增强处理类似的，此处同样使用了 @Aspect 来修饰前面 Bean，其中粗体字代码指定在调用 org.crazyit.app.service.impl 包下的所有类的所有方法的“前后（Around）”织入 processTx(ProceedingJoinPoint jp) 方法。需要指出的是，虽然此处只介绍了 Spring AOP 的 AfterReturning、Around 两种增强处理，但实际上 Spring 还支持 Before、After、AfterThrowing 等增强处理。关于 Spring AOP 编程更多、更细致的编程细节，可以参考《轻量级 Java EE 企业应用实战》一书。本示例采用了 Spring 的零配置来开启 Spring AOP，因此上面 Chinese 类使用了 @Component 修饰，而方面 Bean 则使用了 @Aspect 修饰，方面 Bean 中的 Advice 则分别使用了 @AfterReturning、@Around 修饰。接下来只要为 Spring 提供如下配置文件即可：清单 8. bean.xml<?xml version="1.0" encoding="GBK"?> <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-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd"> <context:component-scan base-package="org.crazyit.app.service,org.crazyit.app.advice"> <context:include-filter type="annotation" expression="org.aspectj.lang.annotation.Aspect"/> </context:component-scan> <aop:aspectj-autoproxy/> </beans>接下来按传统方式来获取 Spring 容器中 chinese Bean、并调用该 Bean 的两个方法，程序代码如下：清单 9. BeanTest.javapublic class BeanTest { public static void main(String[] args) { // 创建 Spring 容器 ApplicationContext ctx = new ClassPathXmlApplicationContext("bean.xml"); Chinese p = ctx.getBean("chinese", Chinese.class); System.out.println(p.sayHello("张三")); p.eat("西瓜"); } }从上面开发过程可以看出，对于 Spring AOP 而言，开发者提供的业务组件、方面 Bean 并没有任何特别的地方，只是方面 Bean 需要使用 @Aspect 修饰即可。程序不需要使用特别的编译器、织入器进行处理。运行上面程序，将可以看到如下执行结果：执行目标方法之前，模拟开始事务 ... -- 正在执行 sayHello 方法 -- 执行目标方法之后，模拟结束事务 ... 获取目标方法返回值 : 被改变的参数 Hello , Spring AOP 新增的内容 模拟记录日志功能 ... 被改变的参数 Hello , Spring AOP 新增的内容 执行目标方法之前，模拟开始事务 ... 我正在吃 : 被改变的参数 执行目标方法之后，模拟结束事务 ... 获取目标方法返回值 :null 新增的内容 模拟记录日志功能 ...虽然程序是在调用 Chinese 对象的 sayHello、eat 两个方法，但从上面运行结果不难看出：实际执行的绝对不是 Chinese 对象的方法，而是 AOP 代理的方法。也就是说，Spring AOP 同样为 Chinese 类生成了 AOP 代理类。这一点可通过在程序中增加如下代码看出：System.out.println(p.getClass());上面代码可以输出 p 变量所引用对象的实现类，再次执行程序将可以看到上面代码产生 class org.crazyit.app.service.impl.Chinese$$EnhancerByCGLIB$$290441d2 的输出，这才是 p 变量所引用的对象的实现类，这个类也就是 Spring AOP 动态生成的 AOP 代理类。从 AOP 代理类的类名可以看出，AOP 代理类是由 CGLIB 来生成的。如果将上面程序稍作修改：只要让上面业务逻辑类 Chinese 类实现一个任意接口——这种做法更符合 Spring 所倡导的“面向接口编程”的原则。假设程序为 Chinese 类提供如下 Person 接口，并让 Chinese 类实现该接口：清单 10. Person.javapublic interface Person { String sayHello(String name); void eat(String food); }接下来让 BeanTest 类面向 Person 接口、而不是 Chinese 类编程。即将 BeanTest 类改为如下形式：清单 11. BeanTest.javapublic class BeanTest { public static void main(String[] args) { // 创建 Spring 容器 ApplicationContext ctx = new ClassPathXmlApplicationContext("bean.xml"); Person p = ctx.getBean("chinese", Person.class); System.out.println(p.sayHello("张三")); p.eat("西瓜"); System.out.println(p.getClass()); } }原来的程序是将面向 Chinese 类编程，现在将该程序改为面向 Person 接口编程，再次运行该程序，程序运行结果没有发生改变。只是 System.out.println(p.getClass()); 将会输出 class $Proxy7，这说明此时的 AOP 代理并不是由 CGLIB 生成的，而是由 JDK 动态代理 生成的。Spring AOP 框架对 AOP 代理类的处理原则是：如果目标对象的实现类实现了接口，Spring AOP 将会采用 JDK 动态代理 来生成 AOP 代理类；如果目标对象的实现类没有实现接口，Spring AOP 将会采用 CGLIB 来生成 AOP 代理类。不过这个选择过程对开发者完全透明、开发者也无需关心。Spring AOP 会动态选择使用 JDK 动态代理、CGLIB 来生成 AOP 代理。如果目标类实现了接口，Spring AOP 则无需 CGLIB 的支持，直接使用 JDK 提供的 Proxy 和 InvocationHandler 来生成 AOP 代理即可。关于如何使用 Proxy 和 InvocationHandler 来生成动态代理不在本文介绍范围之内，如果读者感兴趣则可自行参考 Java API 文档或《疯狂 Java 讲义》。Spring AOP 原理剖析通过前面介绍可以知道：AOP 代理其实是由 AOP 框架动态生成的一个对象，该对象可作为目标对象使用。AOP 代理包含了目标对象的全部方法，但 AOP 代理中的方法与目标对象的方法存在差异：AOP 方法在特定切入点添加了增强处理，并回调了目标对象的方法。AOP 代理所包含的方法与目标对象的方法示意图如图 3 所示。图 3. AOP 代理的方法与目标对象的方法点击查看大图Spring 的 AOP 代理由 Spring 的 IoC 容器负责生成、管理，其依赖关系也由 IoC 容器负责管理。因此，AOP 代理可以直接使用容器中的其他 Bean 实例作为目标，这种关系可由 IoC 容器的依赖注入提供。纵观 AOP 编程，其中需要程序员参与的只有 3 个部分：定义普通业务组件。定义切入点，一个切入点可能横切多个业务组件。定义增强处理，增强处理就是在 AOP 框架为普通业务组件织入的处理动作。上面 3 个部分的第一个部分是最平常不过的事情，无须额外说明。那么进行 AOP 编程的关键就是定义切入点和定义增强处理。一旦定义了合适的切入点和增强处理，AOP 框架将会自动生成 AOP 代理，而 AOP 代理的方法大致有如下公式：代理对象的方法 = 增强处理 + 被代理对象的方法在上面这个业务定义中，不难发现 Spring AOP 的实现原理其实很简单：AOP 框架负责动态地生成 AOP 代理类，这个代理类的方法则由 Advice 和回调目标对象的方法所组成。对于前面提到的图 2 所示的软件调用结构：当方法 1、方法 2、方法 3 ……都需要去调用某个具有“横切”性质的方法时，传统的做法是程序员去手动修改方法 1、方法 2、方法 3 ……、通过代码来调用这个具有“横切”性质的方法，但这种做法的可扩展性不好，因为每次都要改代码。于是 AOP 框架出现了，AOP 框架则可以“动态的”生成一个新的代理类，而这个代理类所包含的方法 1、方法 2、方法 3 ……也增加了调用这个具有“横切”性质的方法。但这种调用由 AOP 框架自动生成的代理类来负责，因此具有了极好的扩展性。程序员无需手动修改方法 1、方法 2、方法 3 的代码，程序员只要定义切入点即可。AOP 框架所生成的 AOP 代理类中包含了新的方法 1、方法 2、方法 3，而 AOP 框架会根据切入点来决定是否要在方法 1、方法 2、方法 3 中回调具有“横切”性质的方法。简而言之：AOP 原理的奥妙就在于动态地生成了代理类，这个代理类实现了图 2 的调用，这种调用无需程序员修改代码。接下来介绍的 CGLIB 就是一个代理生成库，下面介绍如何使用 CGLIB 来生成代理类。使用 CGLIB 生成代理类CGLIB（Code Generation Library），简单来说，就是一个代码生成类库。它可以在运行时候动态生成某个类的子类。此处使用前面定义的 Chinese 类，现在改为直接使用 CGLIB 来生成代理，这个代理类同样可以实现 Spring AOP 代理所达到的效果。下面先为 CGLIB 提供一个拦截器实现类：清单 12. AroundAdvice.javapublic class AroundAdvice implements MethodInterceptor { public Object intercept(Object target, Method method, Object[] args, MethodProxy proxy) throws java.lang.Throwable { System.out.println("执行目标方法之前，模拟开始事务 ..."); // 执行目标方法，并保存目标方法执行后的返回值 Object rvt = proxy.invokeSuper(target, new String[]{"被改变的参数"}); System.out.println("执行目标方法之后，模拟结束事务 ..."); return rvt + " 新增的内容"; } }上面这个 AroundAdvice.java 的作用就像前面介绍的 Around Advice，它可以在调用目标方法之前、调用目标方法之后织入增强处理。接下来程序提供一个 ChineseProxyFactory 类，这个 ChineseProxyFactory 类会通过 CGLIB 来为 Chinese 生成代理类：清单 13. ChineseProxyFactory.javapublic class ChineseProxyFactory { public static Chinese getAuthInstance() { Enhancer en = new Enhancer(); // 设置要代理的目标类 en.setSuperclass(Chinese.class); // 设置要代理的拦截器 en.setCallback(new AroundAdvice()); // 生成代理类的实例 return (Chinese) en.create(); } }上面粗体字代码就是使用 CGLIB 的 Enhancer 生成代理对象的关键代码。此时的 Enhancer 将以 Chinese 类作为目标类，以 AroundAdvice 对象作为"Advice"，程序将会生成一个 Chinese 的子类。这个子类就是 CGLIB 生成代理类，它可作为 Chinese 对象使用，但它增强了 Chinese 类的方法。测试 Chinese 代理类的主程序如下：清单 14. Main.javapublic class Main { public static void main(String[] args) { Chinese chin = ChineseProxyFactory.getAuthInstance(); System.out.println(chin.sayHello("孙悟空")); chin.eat("西瓜"); System.out.println(chin.getClass()); } }运行上面主程序，看到如下输出结果：执行目标方法之前，模拟开始事务 ... -- 正在执行 sayHello 方法 -- 执行目标方法之后，模拟结束事务 ... 被改变的参数 Hello , CGLIB 新增的内容 执行目标方法之前，模拟开始事务 ... 我正在吃 : 被改变的参数 执行目标方法之后，模拟结束事务 ... class lee.Chinese$$EnhancerByCGLIB$$4bd097d9从上面输出结果来看，CGLIB 生成的代理完全可以作为 Chinese 对象来使用，而且 CGLIB 代理对象的 sayHello()、eat() 两个方法已经增加了事务控制（只是模拟）。这个 CGLIB 代理其实就是 Spring AOP 所生成的 AOP 代理。通过程序最后的输出，不难发现这个代理对象的实现类是 lee.Chinese$$EnhancerByCGLIB$$4bd097d9，这就是 CGLIB 所生成的代理类，这个代理类的格式与前面 Spring AOP 所生成的代理类的格式完全相同。这就是 Spring AOP 的根本所在：Spring AOP 就是通过 CGLIB 来动态地生成代理对象，这个代理对象就是所谓的 AOP 代理，而 AOP 代理的方法则通过在目标对象的切入点动态地织入增强处理，从而完成了对目标方法的增强。小结AOP 广泛应用于处理一些具有横切性质的系统级服务，AOP 的出现是对 OOP 的良好补充，它使得开发者能用更优雅的方式处理具有横切性质的服务。不管是那种 AOP 实现，不论是 AspectJ、还是 Spring AOP，它们都需要动态地生成一个 AOP 代理类，区别只是生成 AOP 代理类的时机不同：AspectJ：采用编译时生成 AOP 代理类，因此具有更好的性能，但需要使用特定的编译器进行处理。Spring AOP：采用运行时生成 AOP 代理类，因此无需使用特定编译器进行处理。由于 Spring AOP 需要在每次运行时生成 AOP 代理，因此性能略差一些。相关主题李刚编著 《疯狂 Java 讲义》，本书全面介绍了 Java 语言本身的相关知识。李刚编著 《轻量级 Java EE 企业应用实战 ( 第三版 ) 》，本书全面介绍了 Struts 2.2、Spring 3.0、Hibernate 3.6 整合开发的知识，并通过实际案例介绍了 3 大框架在实际项目中应用。Spring Framework Reference Documentation Java，全面介绍 Spring 框架功能和用法的参考手册。Spring JavaDoc，介绍 Spring 3.0 所有 API 功能和用法的参考文档。developerWorks Java 技术专区：这里有数百篇关于 Java 编程各个方面的文章。说明：本文内容基于 Spring 3.0 与 AspectJ 1.6 版本编写，部分配置方式（如 XML Schema）及 API 细节在更新版本中可能有所变化，但 AOP 核心原理与 CGLIB 应用机制依然适用。
编者注：本文为历史博文归档；涉及 JDK、框架与工具链版本请以当前官方文档为准。引用外链图片可能失效，阅读时请注意时效性。前言一直想抽空研读 Spring 源码，但真正着手时发现并非易事。Spring 发展多年，其规模已非一般开源框架可比，主要架构和流程错综复杂，难以轻易抓住要害。但可以肯定的是，IoC（控制反转）和 AOP（面向切面编程）是其根基，所有的功能扩展及对其它开源框架的支持均基于这两点构建。因此，掌握 Spring 源码的核心在于搞定 IoC 和 AOP 这两块内容。IoC 容器核心从原理上来说，IoC 非常简单，无非是从配置文件解析开始，到最后在内置容器中管理各个对象。但从 Spring IoC 源码来看，这是一个非常庞大的体系。因为 Spring 支持的特性太多，我们不太可能仔细阅读每一个细节，也不太可能在有限篇幅中表达所有细节，只能抓住关键生命周期中的关键步骤。在我看来，IoC 最核心的是两个过程：IoC 容器初始化和IoC 依赖注入。下面通过简单的图示来表述其中的关键过程。相关延伸阅读：Spring 源码学习（二）------ AOPAOP 实现机制AOP 有一些特有的概念，如 advisor、advice 和 pointcut 等，使用或配置起来略显复杂，让人感觉有些距离感。其实，它的实现就是一组标准设计模式的组合使用：Factory、Proxy、Chain of Responsibility。只要搞清楚这几个设计模式，阅读 AOP 源码是比较容易的。设计模式视角首先看看 ProxyFactoryBean 这个类，这是 AOP 使用的入口。从 AOP 拿到的 bean object 就是通过 ProxyFactoryBean.getObject() 得到的。顺着这条线下去，会发现 AOP 就是通过 Proxy 模式 从实际要执行的 target 做了包装。而 Proxy 还不止一套方案，通过 Factory 封装了两套 Proxy 实现方案：JDK 动态 Proxy 和 Cglib Proxy。之所以有两套实现，主要是因为 JDK 动态 Proxy 必须要 target 实现某个接口；如果不满足这个条件，就会用 Cglib 增强字节码的方式来实现 proxy。代理模式应用就拿 JDK Proxy 为例，Spring AOP 使用了标准的 JDK 提供的动态 Proxy 方案。我们先看看标准的动态 Proxy 是什么样子，看下面类图：Client 通过 Proxy.newProxyInstance(classLoader, proxiedInterfaces, invocationHandler) 就能拿到 target 的 proxy object。在执行 target 的方法时，就会先执行到 DynamicProxy 中的 invoke 方法，从而实现代理包装。基于这个道理来看 Spring AOP 的实现，实际上就是标准地基于这个方式来做的。Spring AOP 的所有“花招”都体现在 JdkDynamicAopProxy.invoke 中（当然在 Cglib 中是通过 callback 来做的，道理类似）。责任链模式应用通过看 JdkDynamicAopProxy.invoke 的源码会发现，Spring AOP 的各种逻辑是通过 Chain of Responsibility（责任链）模式 串起来的。先看看一个标准的 Chain of Responsibility 是什么样子，看下面的类图：Chain of Responsibility 的关键在于 Invocation 与 Interceptor 的配合，主要原则就两条：Invocation 需要维护 Interceptor 集合和游标，每次调用 invoke 时需要先调用游标所在的 Interceptor.invoke；如果游标已超过最后一个 Interceptor，则调用实际 target 的方法。Interceptor 的 invoke 中除了要执行自己的拦截逻辑，还要通过 Invocation.invoke 把调用传递下去。拦截的灵活性就体现在 Invocation.invoke 执行与否和执行的顺序。以上逻辑通过时序图来看，如下图所示：理解 Chain of Responsibility 后再来看 Spring AOP，JdkDynamicAopProxy.invoke 做的事情就是以上 Client 做的事情：首先通过 this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass) 组装 interceptor chain。然后 new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain) 得到 invocation。最后通过 invocation.proceed() 启动责任链。术语说明advice 英 [əd'vaɪs] 美 [əd'vaɪs]n. 建议；忠告；劝告；通知advise 英 [əd'vaɪz] 美 [əd'vaɪz]vt. 建议；劝告，忠告；通知；警告vi. 建议；与…商量在 Spring AOP 中，我们主要关注的是 advice（通知/建议），即切面在特定连接点执行的动作。而 Spring AOP 的那些概念都体现在组装 interceptor chain 中，advisor、advice 和 pointcut 无非就是帮助你描述如何对 Target 进行拦截。对这一块感兴趣的朋友可以好好读读里面的代码。另外，Spring AOP 提供了各种各样的 Interceptor 来实现各种形式的横切，具体做法可以详细看看各 Interceptor 的实现。总结综上所述，整个流程如下图所示：总之，把握了 Factory、Proxy、Chain of Responsibility 的运用，也就把握了 Spring AOP 的实现原理。道理虽然简单，但其中的精髓和原由还是值得我们继续深思的。版本说明：本文内容基于早期 Spring 版本源码分析（参考链接时间约为 2011 年），部分类名或实现细节在新版本中可能有所调整，请以当前官方文档及最新源码为准。
在现代 Java 开发领域，Spring 框架占据着核心地位。Spring AOP（Aspect-Oriented Programming，面向切面编程）作为其关键特性之一，为开发者提供了一种强大的编程范式，用于实现横切关注点（Cross-Cutting Concerns）的模块化。无论是日志记录、事务管理、性能监控还是安全控制，Spring AOP 都能让代码更加简洁、可维护且易于扩展。本文将深入探索 Spring AOP 的奥秘，从基础概念到高级应用，助你在开发之旅中掌握这一利器。1. Spring AOP 简介1.1 什么是 AOPAOP 是一种编程思想，旨在将横切关注点（如日志记录、安全检查、事务管理等）从核心业务逻辑中分离出来，以提高代码的模块化程度和可维护性。与传统的面向对象编程（OOP）关注于类和对象的封装、继承和多态不同，AOP 关注的是在不修改源代码的情况下，对程序的运行时行为进行增强。1.2 Spring AOP 的作用Spring AOP 通过在运行时动态地将横切关注点织入（Weaving）到目标对象的方法执行过程中，实现了对目标对象行为的增强。例如，在电商系统中，我们可以使用 Spring AOP 记录用户操作日志，而无需在每个业务方法中手动添加日志代码。这样，当业务逻辑发生变化时，日志逻辑可以独立维护，不会干扰核心业务代码。1.3 核心术语理解 AOP 的核心术语是掌握 Spring AOP 的基础：术语英文说明切面Aspect模块化的横切关注点，包含了切点和通知。例如，一个日志切面定义了在哪里记录日志以及如何记录。连接点Join Point程序执行过程中的特定点，如方法调用、方法执行结束、异常抛出等。Spring AOP 中主要指方法执行。切点Pointcut一组连接点的集合，用于指定在哪些连接点上应用切面的通知。例如，匹配所有以 "get" 开头的方法。通知Advice切面在特定连接点上执行的代码。包括前置、后置、环绕、返回和异常通知等。目标对象Target Object被切面增强的对象，即实际执行业务逻辑的对象。代理ProxySpring AOP 通过代理模式实现切面功能。代理对象包裹目标对象，在方法执行前后执行通知逻辑。2. Spring AOP 的实现方式2.1 基于代理的实现Spring AOP 默认使用基于代理的方式来实现切面功能。它为目标对象创建一个代理对象，当调用目标对象的方法时，实际上是调用代理对象的方法。代理对象会在方法执行前后或异常抛出时，执行切面的通知逻辑。2.2 AspectJ 框架集成除了基于代理的方式，Spring AOP 还支持与 AspectJ 框架集成。AspectJ 是一个功能强大的 AOP 框架，提供了更丰富的 AOP 功能，如编译时织入（Compile-time Weaving）和加载时织入（Load-time Weaving）。使用 AspectJ，我们可以在编译时或类加载时将切面织入到目标类中，而不局限于运行时动态代理。2.3 如何选择在实际应用中，需根据具体需求选择合适的实现方式：基于代理：如果对性能要求不是特别高，且主要关注运行时动态增强，默认方式通常足够满足需求。AspectJ 集成：如果需要更强大的 AOP 功能（如对构造函数、字段等进行增强），或者希望在编译时或加载时进行织入，可以考虑使用 AspectJ 与 Spring AOP 集成。3. Spring AOP 的配置方式3.1 XML 配置在早期的 Spring 项目中，XML 配置是常见方式。我们可以在 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.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <bean id="userService" class="com.example.service.UserServiceImpl"/> <bean id="loggingAspect" class="com.example.aspect.LoggingAspect"/> <aop:config> <aop:pointcut id="userServiceMethodPointcut" expression="execution(* com.example.service.UserServiceImpl.*(..))"/> <aop:aspect ref="loggingAspect"> <aop:before pointcut-ref="userServiceMethodPointcut" method="logBefore"/> </aop:aspect> </aop:config> </beans>3.2 注解配置随着 Java 注解的广泛应用，Spring AOP 支持使用注解来配置切面。这种方式更加简洁直观，减少了 XML 配置的繁琐性，是目前推荐的方式。import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; @Aspect @Component public class LoggingAspect { @Before("execution(* com.example.service.UserServiceImpl.*(..))") public void logBefore() { System.out.println("Before method execution: Logging..."); } }3.3 Java 配置类除了 XML 和注解，我们还可以使用 Java 配置类来配置 Spring AOP。这种方式将配置逻辑集中在 Java 代码中，便于类型安全管理和维护。import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; @Configuration @EnableAspectJAutoProxy public class AppConfig { // 定义目标对象 @Bean public UserService userService() { return new UserServiceImpl(); } // 定义切面 @Bean public LoggingAspect loggingAspect() { return new LoggingAspect(); } }4. Spring AOP 的通知类型Spring AOP 提供了多种通知类型，以适应不同的增强需求：4.1 前置通知（Before Advice）在目标方法执行之前执行。可用于参数验证、权限检查等准备工作。例如，在用户登录前检查用户名和密码是否为空。4.2 后置通知（After Advice）在目标方法执行之后执行，无论方法是否抛出异常。可用于资源清理、记录方法执行时间等。例如，在数据库操作后关闭连接。4.3 环绕通知（Around Advice）功能最强大的通知类型。可以在目标方法执行前后进行自定义逻辑处理，完全控制目标方法的执行过程（包括决定是否执行）。例如，实现缓存功能：执行前检查缓存，若不存在则执行目标方法并将结果存入缓存。4.4 返回通知（After Returning Advice）在目标方法成功返回后执行。可以获取目标方法的返回值并进行后续处理，如结果格式化或转换。4.5 异常通知（After Throwing Advice）在目标方法抛出异常时执行。可用于处理异常情况，如记录异常日志、事务回滚等。综合示例代码以下示例展示了各种通知类型的实际用法：import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.AfterThrowing; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; @Aspect @Component public class MyAspect { // 前置通知 @Before("execution(* com.example.service.UserServiceImpl.*(..))") public void beforeMethod(JoinPoint joinPoint) { System.out.println("Before method: " + joinPoint.getSignature().getName()); } // 后置通知 @After("execution(* com.example.service.UserServiceImpl.*(..))") public void afterMethod(JoinPoint joinPoint) { System.out.println("After method: " + joinPoint.getSignature().getName()); } // 环绕通知 @Around("execution(* com.example.service.UserServiceImpl.*(..))") public Object aroundMethod(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { System.out.println("Around before method: " + proceedingJoinPoint.getSignature().getName()); Object result = proceedingJoinPoint.proceed(); System.out.println("Around after method: " + proceedingJoinPoint.getSignature().getName()); return result; } // 返回通知 @AfterReturning(pointcut = "execution(* com.example.service.UserServiceImpl.*(..))", returning = "result") public void afterReturningMethod(JoinPoint joinPoint, Object result) { System.out.println("After returning method: " + joinPoint.getSignature().getName() + ", result: " + result); } // 异常通知 @AfterThrowing(pointcut = "execution(* com.example.service.UserServiceImpl.*(..))", throwing = "ex") public void afterThrowingMethod(JoinPoint joinPoint, Exception ex) { System.out.println("After throwing method: " + joinPoint.getSignature().getName() + ", exception: " + ex.getMessage()); } }5. Spring AOP 的切点表达式5.1 切点表达式语法切点表达式用于指定在哪些连接点上应用通知。Spring AOP 使用 AspectJ 的切点表达式语法，具有强大的表达能力。基本语法如下：execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)各部分含义：modifiers-pattern：方法修饰符模式（如 public），可用 * 表示任意。ret-type-pattern：返回值类型模式（* 表示任意，void 表示无返回值）。declaring-type-pattern：声明类型模式（类或接口的全限定名）。name-pattern：方法名模式。param-pattern：参数模式（(..) 表示任意参数，(int, String) 表示特定参数）。throws-pattern：异常类型模式。5.2 常见示例匹配所有 public 方法：execution(public * *(..))匹配指定包下所有类的所有方法：execution(* com.example.service..*.*(..))匹配特定类中以 "get" 开头的方法：execution(* com.example.service.UserServiceImpl.get*(..))匹配特定包下返回 String 且有一个参数的方法：execution(String com.example.service..*.*(String))5.3 组合与复用可以使用逻辑运算符（&&、||、!）组合多个表达式。例如：execution(* com.example.service..*.*(..)) && !execution(* com.example.service.UserServiceImpl.get*(..))表示匹配包下所有方法，但排除 UserServiceImpl 类中以 "get" 开头的方法。此外，使用 @Pointcut 注解可定义可复用的切点：import org.aspectj.lang.annotation.Pointcut; @Aspect @Component public class MyAspect { @Pointcut("execution(* com.example.service.UserServiceImpl.*(..))") public void userServiceMethodPointcut() {} @Before("userServiceMethodPointcut()") public void beforeMethod() { System.out.println("Before method execution in UserServiceImpl"); } }6. 应用场景Spring AOP 在企业级开发中有着广泛的应用场景：日志记录：将日志逻辑从业务代码中分离，记录方法执行时间、参数和返回值，便于性能分析和故障排查。事务管理：结合 Spring 事务机制，实现事务的自动开启、提交和回滚，确保数据一致性。权限控制：在方法执行前判断当前用户权限，实现安全控制（如仅管理员可执行特定操作）。性能监控：记录关键方法的执行时间和调用次数，帮助发现性能瓶颈。缓存管理：在执行前检查缓存，若命中则直接返回，否则执行目标方法并更新缓存，提升响应速度。7. 性能考虑与优化7.1 代理对象的创建开销Spring AOP 基于代理模式，创建代理对象会带来一定的性能开销。在 Spring 容器中，Bean 通常为单例，代理对象在容器启动时创建，因此运行时开销主要在于方法调用的转发，而非频繁创建代理。7.2 方法调用的额外开销通过代理对象调用目标方法会增加少量的方法调用开销。特别是在使用环绕通知时，若通知逻辑复杂，可能对性能产生较大影响。建议保持通知逻辑简洁高效，避免在通知中进行复杂的计算或数据库操作。7.3 优化建议合理选型：根据需求权衡性能和功能，选择基于代理或 AspectJ。优化表达式：避免使用过于复杂或低效的切点表达式，提高匹配效率。针对性优化：对性能关键的方法，减少不必要的通知应用或采用更高效的通知逻辑。8. 总结Spring AOP 作为 Spring 框架的强大特性之一，为 Java 开发者提供了一种优雅的方式来处理横切关注点。通过将横切逻辑从核心业务代码中分离，我们实现了代码的模块化、可维护性和可扩展性。从基础概念到配置应用，再到性能优化，合理运用 Spring AOP 可以大大提高开发效率，降低代码复杂度，提升应用质量。提示：为了更直观地展示相关概念，建议在实际阅读或写作时配合 Spring AOP 工作原理示意图 或 不同通知类型执行流程示意图，以增强对代理模式和织入过程的理解。说明：本文基于 Spring Framework 传统配置方式编写。在现代 Spring Boot 项目中，AOP 通常通过引入 spring-boot-starter-aop 依赖并配合注解自动配置，无需手动编写 XML 或复杂的 Java Config 配置类，使用体验更加便捷。
Spring AOP 机制：JDK Dynamic Proxy vs CGLIB简而言之，Spring AOP 是基于代理（Proxy-based）的。也就是说，AOP 是通过代理实现的。Spring 使用以下两种方式之一为给定的目标 Bean 创建代理：JDK 动态代理（JDK Dynamic Proxies）：当被代理的目标实现了至少一个接口时，首选此方式。CGLIB：如果目标对象没有实现任何接口，可以使用 CGLIB。也可以通过以下方式强制使用：将 <aop:config> 元素的 proxy-target-class 属性设置为 true。在使用 @AspectJ 自动代理支持时，将 proxy-target-class 属性设置为 true。注意事项与局限性Spring AOP 与 AspectJ 的关系：Spring AOP 其实只是兼容了 AspectJ 的注解，但是底层跟 AspectJ 一点关系都没有。Spring AOP 的局限性：因为 Spring AOP 是基于代理（proxy-based）和方法（method-based）的，所以它有如下局限性：不能增强 final 或者静态方法。内部方法调用（self-call）不会被 AOP 拦截。因为目标对象（target-object）没有被增强，this 引用的是目标对象本身。参考：Spring AOP top problem #1 - aspects are not applied使用 AspectJ 可以无限制地使用 AOP，但是使用起来相对复杂很多，需要仔细权衡：10.4 Choosing which AOP declaration style to use10.8 Using AspectJ with Spring applicationsSpring AOP vs AspectJ AOPSpring AOP：运行时织入（Runtime weaving）。如果存在接口则使用动态代理概念，如果提供了直接实现则使用 CGLIB 库。AspectJ：编译时织入（Compile time weaving）。如果源码可用，通过 AspectJ Java 工具（ajc 编译器）进行；或者进行后编译织入（post compilation weaving，使用编译后的文件）。此外，还可以启用与 Spring 结合的加载时织入（load time weaving）——这需要 aspectj 定义文件并提供灵活性。编译时织入在某些情况下可以提供性能优势，而且 Spring AOP 中的连接点（joinpoint）定义仅限于方法定义，而 AspectJ 则不受此限制。织入时机（Weaving Process Time）The AspectJ weaver takes class files as input and produces class files as output. The weaving process itself can take place at one of three different times: compile-time, post-compile time, and load-time. The class files produced by the weaving process (and hence the run-time behaviour of an application) are the same regardless of the approach chosen.Compile-time weaving is the simplest approach. When you have the source code for an application, ajc will compile from source and produce woven class files as output. The invocation of the weaver is integral to the ajc compilation process. The aspects themselves may be in source or binary form. If the aspects are required for the affected classes to compile, then you must weave at compile-time. Aspects are required, e.g., when they add members to a class and other classes being compiled reference the added members.Post-compile weaving (also sometimes called binary weaving) is used to weave existing class files and JAR files. As with compile-time weaving, the aspects used for weaving may be in source or binary form, and may themselves be woven by aspects.Load-time weaving (LTW) is simply binary weaving defered until the point that a class loader loads a class file and defines the class to the JVM. To support this, one or more "weaving class loaders", either provided explicitly by the run-time environment or enabled through a "weaving agent" are required.You may also hear the term "run-time weaving". We define this as the weaving of classes that have already been defined to the JVM (without reloading those classes). AspectJ 5 does not provide explicit support for run-time weaving although simple coding patterns can support dynamically enabling and disabling advice in aspects.Spring AOP 底层实现定义了一个 AopProxy 接口，参见 AopProxy：package org.springframework.aop.framework; /** * Delegate interface for a configured AOP proxy, allowing for the creation * of actual proxy objects. * * Out-of-the-box implementations are available for JDK dynamic proxies * and for CGLIB proxies, as applied by {@link DefaultAopProxyFactory}. * * @author Rod Johnson * @author Juergen Hoeller * @see DefaultAopProxyFactory */ public interface AopProxy { Object getProxy(); Object getProxy(ClassLoader classLoader); }AopProxy 主要有两个实现类：JdkDynamicAopProxyCglibAopProxyJdkDynamicAopProxyJdkDynamicAopProxy 基于 JDK 的 java.lang.reflect.Proxy 实现。基于目标对象的接口创建代理非常简单：final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializable { public Object getProxy(ClassLoader classLoader) { if (logger.isDebugEnabled()) { logger.debug("Creating JDK dynamic proxy: target source is " + this.advised.getTargetSource()); } // Determine the complete set of interfaces to proxy for the given AOP configuration. Class[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised); findDefinedEqualsAndHashCodeMethods(proxiedInterfaces); // Returns an instance of a proxy class for the specified interfaces that dispatches method invocations to the specified invocation handler. return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this); } }使用 Proxy 类的 newProxyInstance 方法：public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)注意上面的 InvocationHandler 参数本身就是 JdkDynamicAopProxy 实例。这是真的，因为 JdkDynamicAopProxy 也实现了 InvocationHandler 接口：public interface InvocationHandler { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable; }invoke 方法的实现代码太长，此处不再粘贴。如果你感兴趣可以直接查看源码。只需记住它不过是代理模式（Proxy Pattern）。代理实现了目标对象的接口，并持有目标的引用。CglibAopProxyCglibAopProxy 使用 CGLIB 库来生成代理对象。final class CglibAopProxy implements AopProxy, Serializable { public Object getProxy() { return getProxy(null); } public Object getProxy(ClassLoader classLoader) { try { Class<?> rootClass = this.advised.getTargetClass(); Assert.state(rootClass != null, "Target class must be available for creating a CGLIB proxy"); Class<?> proxySuperClass = rootClass; if (ClassUtils.isCglibProxyClass(rootClass)) { proxySuperClass = rootClass.getSuperclass(); Class<?>[] additionalInterfaces = rootClass.getInterfaces(); for (Class<?> additionalInterface : additionalInterfaces) { this.advised.addInterface(additionalInterface); } } // Validate the class, writing log messages as necessary. validateClassIfNecessary(proxySuperClass); // Configure CGLIB Enhancer... Enhancer enhancer = createEnhancer(); if (classLoader != null) { enhancer.setClassLoader(classLoader); if (classLoader instanceof SmartClassLoader && ((SmartClassLoader) classLoader).isClassReloadable(proxySuperClass)) { enhancer.setUseCache(false); } } enhancer.setSuperclass(proxySuperClass); enhancer.setStrategy(new UndeclaredThrowableStrategy(UndeclaredThrowableException.class)); enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised)); enhancer.setInterceptDuringConstruction(false); Callback[] callbacks = getCallbacks(rootClass); enhancer.setCallbacks(callbacks); enhancer.setCallbackFilter(new ProxyCallbackFilter( this.advised.getConfigurationOnlyCopy(), this.fixedInterceptorMap, this.fixedInterceptorOffset)); Class<?>[] types = new Class[callbacks.length]; for (int x = 0; x < types.length; x++) { types[x] = callbacks[x].getClass(); } enhancer.setCallbackTypes(types); // Generate the proxy class and create a proxy instance. Object proxy; if (this.constructorArgs != null) { proxy = enhancer.create(this.constructorArgTypes, this.constructorArgs); } else { proxy = enhancer.create(); } return proxy; } catch (Throwable ex) { // ..Exception handle.. throw new AopConfigException("Unexpected AOP exception", ex); } } } 注意 CglibAopProxy 与 JdkDynamicAopProxy 的不同之处。CglibAopProxy 将构造一个继承目标类的类（Enhancer），而不是实现目标类的接口。enhancer.setSuperclass(proxySuperClass);这就是为什么它可以代理没有接口的目标对象。另请注意，JdkDynamicAopProxy 和 CglibAopProxy 都是在运行时（on the fly）构造代理对象，而不是编译时。因为它在实例化代理对象之前必须先构造代理类，这可能是一个性能瓶颈。但在 Spring 中，这通常只在启动 Spring 应用上下文时发生一次。DefaultAopProxyFactorySpring 使用 DefaultAopProxyFactory 来创建 AOP 代理对象。如果对于给定的 AdvisedSupport 实例，以下任一条件为真，DefaultAopProxyFactory 将自动创建基于 CGLIB 的代理：设置了 optimize 标志设置了 proxyTargetClass 标志未指定代理接口public class DefaultAopProxyFactory implements AopProxyFactory, Serializable { public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException { if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) { Class targetClass = config.getTargetClass(); if (targetClass == null) { throw new AopConfigException("TargetSource cannot determine target class: " + "Either an interface or a target is required for proxy creation."); } if (targetClass.isInterface()) { return new JdkDynamicAopProxy(config); } return CglibProxyFactory.createCglibProxy(config); } else { return new JdkDynamicAopProxy(config); } } /** * Inner factory class used to just introduce a CGLIB dependency * when actually creating a CGLIB proxy. */ private static class CglibProxyFactory { public static AopProxy createCglibProxy(AdvisedSupport advisedSupport) { return new CglibAopProxy(advisedSupport); } } }参考资料9. Aspect Oriented Programming with Spring8.6 Proxying mechanismsJava Reflection: Dynamic ProxiesWeaving with AspectJ AspectJ 非常好的介绍文章Spring AOP APIs Spring AOP API 介绍。介绍了 Spring AOP 底层使用到的核心类和接口。比如 ProxyFactoryBean。强烈推荐阅读。Spring Auto proxy creator example 介绍怎样使用 ProxyFactoryBean 创建 AOP 代理。说明：文中部分链接指向 Spring 3.x 版本文档，核心原理在后续版本中依然适用，但具体类实现细节可能随版本演进有所调整。