Spring Cache

Spring 自身并没有实现缓存解决方案, 但是它对缓存功能提供了声明式的支持, 能够与多种流行的缓存实现进行集成。

Spring 的缓存抽象在很大程度上是围绕切面构建的。 在 Spring 中启用缓存时,会创建一个切面, 它触发一个或更多的 Spring 的缓存注解。

1 启用对缓存的支持

配置类上添加@EnableCaching启用 Spring 对注解驱动缓存的支持。

在方法上添加@Cacheable@CacheEvict注解使用 Spring 的缓存抽象。

@Configuration
@EnableCaching
public class CachingConfig {
@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager();
}
}

1.1 配置缓存管理器

缓存管理器是 Spring 缓存抽象的核心, 它能够与多个流行的缓存实现进行集成。

Spring 3.1 内置五个缓存管理器实现

  • SimpleCacheManager
  • NoOpCacheManager
  • ConcurrentMapCacheManager
  • CompositeCacheManager
  • EhCacheCacheManager

Spring Data 又提供了两个缓存管理器:

  • RedisCacheManager(来自于 Spring Data Redis 项目)
  • GemfireCacheManager(来自于 Spring Data GemFire 项目)
1)使用单个

Redis 可以用来为 Spring 缓存抽象机制存储缓存条目, Spring Data Redis 提供了 RedisCacheManager, 这是 CacheManager 的一个实现。

@Configuration
@EnableCaching
public class CachingConfig {
@Bean
public CacheManager cacheManager(RedisTemplate redisTemplate) {
return new RedisCacheManager(redisTemplate);
}
@Bean
public JedisConnectionFactory redisConnectionFactory() {
JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory();
jedisConnectionFactory.afterPropertiesSet();
return jedisConnectionFactory;
}
@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory redisCF) {
RedisTemplate<String, String> redisTemplate = new RedisTemplate<String, String>();
redisTemplate.setConnectionFactory(redisCF);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
2)使用多个

CompositeCacheManager 要通过一个或更多的缓存管理器来进行配置, 它会迭代这些缓存管理器, 以查找之前所缓存的值。

@Bean
public CacheManager cacheManager(
net.sf.ehcache.CacheManager cm,
javax.cache.CacheManager jcm) {
CompositeCacheManager cacheManager = new CompositeCacheManager();
List<CacheManager> managers = new ArrayList<CacheManager>();
managers.add(new JCacheCacheManager(jcm));
managers.add(new EhCacheCacheManager(cm))
managers.add(new RedisCacheManager(redisTemplate()));
cacheManager.setCacheManagers(managers);
return cacheManager;
}

2 为方法添加注解以支持缓存

所有注解都能运用在方法或类上。 放在类上时, 缓存行为会应用到这个类的所有方法上。

注 解 描 述
@Cacheable (读)调用前,先在缓存中查找方法的返回值。如有则返回,否则调用,返回值会放到缓存之中
@CachePut (写)方法始终被调用,返回值放到缓存中
@CacheEvict (删)在缓存中清除一个或多个条目
@Caching 这是一个分组的注解, 能够同时应用多个其他的缓存注解

2.1 填充缓存

@Cacheable 和 @CachePut 都有一个名 value 属性,缓存名称

@Cacheable("spittleCache")
public Spittle findOne(long id) {
try {
return jdbcTemplate.queryForObject(
SELECT_SPITTLE_BY_ID,
new SpittleRowMapper(),
id);
} catch (EmptyResultDataAccessException e) {
return null;
}
}
1)将值放到缓存之中

有 @CachePut 注解的方法始终都会被调用, 而且它的返回值也会放到缓存中。

适合用在保存、修改操作上,即刻缓存。

2)自定义缓存 key

@Cacheable 和 @CachePut 都有一个名为 key 属性, 这个属性能够替换默认的 key, 它是通过一个 SpEL 表达式计算得到的。

在为缓存编写 SpEL 表达式的时候, Spring 暴露了一些很有用的元数据。

表 达 式 描 述
#root.args 传递给缓存方法的参数, 形式为数组
#root.caches 该方法执行时所对应的缓存, 形式为数组
#root.target 目标对象#root.targetClass 目标对象的类, 是#root.target.class的简写形式
#root.method 缓存方法
#root.methodName 缓存方法的名字, 是#root.method.name的简写形式
#result 方法调用的返回值(不能用在@Cacheable注解上)
#Argument 任意的方法参数名(如#argName) 或参数索引(如#a0或#p0)
@CachePut(value="spittleCache", key="#result.id")
Spittle save(Spittle spittle);
3)条件化缓存

@Cacheable 和 @CachePut 提供了两个属性用以实现条件化缓存: unless 和 condition。

unless 为 true 阻止添加到缓存中,condition 为 false 缓存被禁用。

@Cacheable(value="spittleCache" unless="#result.message.contains('NoCache')")
Spittle findOne(long id);

2.2 移除缓存条目

带有 @CacheEvict 注解的方法被调用,在缓存中移除。

适用于删除操作。

@CacheEvict("spittleCache")
void remove(long spittleId);

Spring Data MongoDB

文档数据库适合处理没什么关联关系的独立实体

Spring Data MongoDB 提供了三种方式在 Spring 应用中使用 MongoDB:

  • 通过注解实现对象-文档映射;
  • 使用 MongoTemplate 实现基于模板的数据库访问;
  • 自动化的运行时 Repository 生成功能。

1 启用 MongoDB

@EnableMongoRepositories 注解启用 MongoDB

1.1 方法一

MongoFactoryBean、MongoTemplate

@Configuration
@EnableMongoRepositories(basePackages="orders.db")//Enable MongoDB repositories
public class MongoConfig {
//MongoClient bean
@Bean
public MongoFactoryBean mongo() {
MongoFactoryBean mongo = new MongoFactoryBean();
mongo.setHost("localhost");
return mongo;
}
//MongoTemplate bean
@Bean
public MongoOperations mongoTemplate(Mongo mongo) {
return new MongoTemplate(mongo, "OrdersDB");
}
}

1.2 方法二

配置类扩展 AbstractMongoConfiguration 并重载 getDatabaseName() 和 mongo() 方法。

@Configuration
@EnableMongoRepositories("orders.db")
public class MongoConfig extends AbstractMongoConfiguration {
@Override
protected String getDatabaseName() {
return "OrdersDB";
}
@Override
public Mongo mongo() throws Exception {
//认证
MongoCredential credential =
MongoCredential.createMongoCRCredential(
env.getProperty("mongo.username"),
"OrdersDB",
env.getProperty("mongo.password").toCharArray());
return new MongoClient(
new ServerAddress("localhost", 37017),
Arrays.asList(credential));
//无需认证
return new MongoClient("mongodbserver", 27017);
}
}

2 注解 MongoDB 持久化

Spring Data MongoDB 提供了一些将 Java 类型映射为 MongoDB 文档的注解。

注解 描述
@Document 标示映射到 MongoDB 文档上的领域对象
@Id 标示某个域为ID域
@DbRef 标示某个域要引用其他的文档, 这个文档有可能位于另外一个数据库中
@Field 为文档域指定自定义的元数据
@Version 标示某个属性用作版本域
@Document
public class Order {
@Id
private String id;

@Field("client")
private String customer;

private Collection<Item> items = new LinkedHashSet<Item>();
...
}

3 使用 MongoTemplate 访问 MongoDB

MongoOperations 是 MongoTemplate 所实现的接口, 不使用具体实现是一个好的做法, 尤其是在注入的时候

@Autowired
MongoOperations mongo;

long orderCount = mongo.getCollection("order").count();

Order order = new Order();
... // set properties and add line items
mongo.save(order, "order");

String orderId = ...;
Order order = mongo.findById(orderId, Order.class);

List<Order> chucksOrders = mongo.find(Query.query(
Criteria.where("client").is("Chuck Wagon")), Order.class);

List<Order> chucksWebOrders = mongo.find(Query.query(
Criteria.where("customer").is("Chuck Wagon")
.and("type").is("WEB")), Order.class);

mongo.remove(order);

4 编写 MongoDB Repository

扩展 MongoRepository。任何扩展 Repository 的接口将会在运行时自动生成实现。

MongoRepository 接口有两个参数, 第一个是带有 @Document 注解的对象类型, 也就是该 Repository 要处理的类型。 第二个参数是带有 @Id 注解的属性类型。

public interface OrderRepository extends MongoRepository<Order, String> {
}

Spring Data Redis

Spring Data Redis 包含了多个模板实现, 用来完成 Redis 数据库的数据存取功能。

1 连接到Redis

Spring Data Redis 为四种 Redis 客户端实现提供了连接工厂:

  • JedisConnectionFactory
  • JredisConnectionFactory
  • LettuceConnectionFactory
  • SrpConnectionFactory

配置为 bean

@Bean
public RedisConnectionFactory redisCF() {
JedisConnectionFactory cf = new JedisConnectionFactory();
cf.setHostName("redis-server");
cf.setPort(7379);
cf.setPassword("foobared");
return cf;
}

2 使用 RedisTemplate

Spring Data Redis 提供了两个模板:

  • RedisTemplate
  • StringRedisTemplate

RedisTemplate 简化 Redis 数据访问, 持久化各种类型的 key 和 value。StringRedisTemplate 扩展了 RedisTemplate,只关注 String 类型。

@Bean
public RedisTemplate<String, Product> redisTemplate(RedisConnectionFactory cf) {
RedisTemplate<String, Product> redis = new RedisTemplate<String, Product>();
redis.setConnectionFactory(cf);
return redis;
}

3 使用 key 和 value 的序列化器

Spring Data Redis 提供了多个这样的序列化器, 包括:

  • GenericToStringSerializer: 使用 Spring 转换服务进行序列化;
  • JacksonJsonRedisSerializer: 使用 Jackson 1, 将对象序列化为 JSON;
  • Jackson2JsonRedisSerializer: 使用 Jackson 2, 将对象序列化为 JSON;
  • JdkSerializationRedisSerializer: 使用 Java 序列化;
  • OxmSerializer: 使用 Spring O/X 映射的编排器和解排器(marshaler 和 unmarshaler) 实现序列化, 用于 XML 序列化;
  • StringRedisSerializer: 序列化 String 类型的 key 和 value。

RedisTemplate 会使用 JdkSerializationRedisSerializer,StringRedisTemplate 默认会使用 StringRedisSerializer。setKeySerializer() 和 setValueSerializer() 方法可以分别设置序列化器。

@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 使用 Jackson2JsonRedisSerialize 替换默认序列化
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// 设置 value 的序列化规则和 key 的序列化规则
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}

JdbcTemplate

为了避免业务层模块强依赖于某种类型的数据库,Spring 数据库访问层以接口形式对外提供服务。

使用 Spring JDBC 的异常体系,描述能力强,且平台无关。

1 数据访问模板化

数据访问过程中的固定步骤和变量分为两类:

  • 模板(templates):处理数据访问的固定部分——事务控制、 管理资源以及处理异 常。
  • 回调(callbacks):处理应用程序相关的数据访问——语句、 绑定参数以及整理结果集

针对不同的持久化平台, Spring提供了多个可选的模板。

ORM 持久化技术 模板类(org.springframework.*)
JDBC jdbc.core.JdbcTemplate
JCA CCI jca.cci.core.CciTemplate
Hibernate orm.hibernate5.HibernateTemplate
iBATIS orm.ibatis.SqlMapClientTemplate
JDO orm.jdo.JdoTemplate
JPA orm.jpa.JpaTemplate

2 配置 DataSource

使用 JdbcTemplate 需设置 DataSource,提供了多种配置方式:

  1. JDBC 驱动;
  2. JNDI 查询;
  3. 数据库连接池;

生产环境建议使用从连接池获取连接的数据源;如果有可能,倾向于通过应用服务器的 JNDI 来获取数据源。

2.1 JNDI 数据源

在 Spring 应用部署的服务器中配置数据源,通过 JNDI 获取。数据源配置在应用外部,允许在访问数据库的时再查找数据源,且支持热切换。

public JndiObjectFactoryBean dataSource() {
JndiObjectFactoryBean jndiObjectFB = new JndiObjectFactoryBean();
jndiObjectFB.setJndiName("/jdbc/xxxx");
jndiObjectFB.setResourceRef(true);
jndiObjectFB.setProxyInterface(javax.sql.DataSource.class);
return jndiObjectFB;
}

2.2 数据库连接池

有多项开源数据源连接池实现:Apache Commons DBCP、c3p0、BoneCP

DBCP 为例,BasicDataSource 配置连接池

public BasicDataSource dataSource() {
BasicDataSource ds = new BasicDataSource();
ds.setDriverClassName("org.h2.Driver");
ds.setUrl("jdbc:h2:tcp://localhost/~/xxxx");
ds.setUsername("sa");
ds.setPassword("");
ds.setInitialSize(5);
return ds;
}

2.3、JDBC 驱动

通过 JDBC 驱动定义数据源是最简单的配置方式。 org.springframework.jdbc.datasource 包中提供了三个数据源类:

  1. DriverManagerDataSource:每次请求连接时都返回新的连接,用过的连接会马上关闭并释放资源;
  2. SimpleDriverDataSource:与 DriverManagerDataSource 类似,但直接使用JDBC驱动,免去了类在特定环境(如 OSGi 容器)中可能遇到的类加载问题。
  3. SingleConnectionDataSource:每次都返回同一个连接对象,可以理解为只有 1 个连接的数据源连接池。

跟之前配置 DBCP 的 BasicDataSource 类似,例如配置 DriverManagerDataSource

@Bean
public DataSource dataSource() {
DriverManagerDataSource ds = new DriverManagerDataSource();
ds.setDriverClassName("org.h2.Driver");
ds.setUrl("jdbc:h2:tcp://localhost/~/xxxx");
ds.setUsername("sa");
ds.setPassword("");
return ds;
}

已上三数据源对多线程支持都不好,强烈建议使用数据库连接池。

2.4 选择数据源

借助 Spring 的 bean-profiles 特性,在不同的环境中配置不同的数据源。利用 @Profile 注解,在运行时根据激活的 profile 选择指定的数据源。

@Configuration
public class DataSourceConfiguration {
@Profile("development")
@Bean
public DataSource embeddedDataSource() {
return new EmbeddedDatabaseBuilder()
...
}

@Profile("qa")
@Bean
public BasicDataSource basicDataSource() {
BasicDataSource ds = new BasicDataSource();
...
return ds;
}

@Profile("production")
@Bean
public DataSource dataSource() {
JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean();
...
return (DataSource)jndiObjectFactoryBean.getObject();
}
}

3 使用 JdbcTemplate

JDBC 直接操作数据库, 处理与数据库访问相关的所有事情, 样板代码繁琐,但很重要。Spring 的 JDBC 框架承担了资源管理和异常处理的样板代码,开发者只需编写读写数据的必需代码。

JdbcTemplate 是最主要的 JDBC 模板,支持简单的 JDBC 数据库访问功能以及基于索引参数的查询;还提供了 NameParameterJdbcTemplate 支持命名参数。

3.1 CRUD

使用 JdbcTemplate 前需设置 DataSource

JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
  • 增删改
    • update(final String sql)
String sql = "insert into user (username, password) values (?, ?)";
jdbcTemplate.update(sql, username, password);

当调用 update() 方法时,JdbcTemplate 调用 jdbc 获取一个连接、创建一个 statement,并执行插入语句。内部捕获了可能抛出的 SQLException 异常,然后转为更具体的数据库访问异常,并重新抛出。

    • queryForObject(String sql, RowMapper rowMapper, Object… args)
User user = jdbcOperations.queryForObject(sql,new UserMapper (),param);

public class UserMapper implements RowMapper<User>{
@Override
public User mapRow(ResultSet resultSet, int rows) throws SQLException {
User user = new User();
user.setUsername(resultSet.getString(1));
user.setPassword(resultSet.getString(2));
return user;
}
}

需要实现一个 RowMapper 对象,JdbcTemplate 会调用 mapRow() 方法,从结果集中取出对应属性的值,并构造对象。


Spring Data JPA

Java 持久化 API(Java Persistence API, JPA)基于 POJO 的持久化机制,制定了规范,具体由厂商实现。

只需按照约定好的【方法命名规则】编写 Repository 接口,不需要实现类,就可实现对数据库的访问和操作。

hibernate 实现了 JPA 规范,Spring Data JPA 进一步封装了 Hibernate。

1 配置

maven

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

application.properties

spring.jpa.properties.hibernate.hbm2ddl.auto = update
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.show-sql = true

spring.jpa.properties.hibernate.hbm2ddl.auto 有几种配置:

  • create:每次加载生成新表。
  • create-drop:每次加载生成新表,但当 SessionFactory 关闭时,所生成的表将自动删除。
  • update:第一次加载时创建数据表,以后加载根据实体更新,只增不删。
  • validate:每次加载验证数据表结构,只会和已经存在的数据表进行比较,根据model修改表结构,但不会创建新表。

不配置此项,表示禁用自动建表功能

2 简单使用

2.1 建立 entity

@Entity
@Data
public class User {
@Id
@GeneratedValue
private long id;
@Column(nullable = false, unique = true)
private String userName;
@Column(nullable = false)
private int age;
}

2.2 扩展 Repository 接口

扩展 Repository 接口或其子接口 JpaRepository、 PagingAndSortingRepository 和 CrudRepository

应用启动后自动生成接口的实现,提供默认通用方法。

public interface UserRepository extends JpaRepository<User, Long> {
}

2.3 启用 Spring Data JPA

@EnableJpaRepositories 注解,扫描查找扩展自 Spring Data JPA Repository 接口的所有接口。

@Configuration
@EnableJpaRepositories{basePackages="com.xxx.xxx"}
public class JpaConfiguration{
...
}

3 定义查询方法

只需使用属性名和关键字构建 Repository 方法签名, 就能让 Spring Data JPA 生成方法实现

当创建 Repository 实现的时候, Spring Data 会检查 Repository 接口的所有方法, 解析方法的名称, 并基于被持久化的对象来试图推测方法的目的。

3.1 方法组成

Repository 方法是由一个动词、 一个可选的主题、 关键词 By 以及一个断言所组成。

User findByUsername(String userName);

findByUsername() 这个样例中, 动词是 find, 断言是 Username, 主题并没有指 定, 暗含的主题是 User。

3.2 比较操作

断言中的多个限制条件可以进行比较操作

  • IsAfter、 After、 IsGreaterThan、 GreaterThan
  • IsGreaterThanEqual、 GreaterThanEqual

4 声明自定义查询

如果 Spring Data 的这个小型的 DSL 无法满足需求,还可以通过 @Query 注解来解决问题。

Query("select * from User where username like '%abc'")
List<User> findByAllAbcUser();

5 混合自定义的功能

当 Spring Data JPA 为 Repository 接口生成实现的时候, 它还会查找名字与接口相同, 并且添加了 Impl 后缀的一个类。 如果这个类存在的话, Spring Data JPA 将会把它的方法与 Spring DataJPA 所生成的方法合并在一起。


切面

1 创建切面

使用 @Aspect 注解创建切面

@Aspect
@Component
public class LogAspect {
@Pointcut("execution(* cn.xxx..*ServiceImpl.*(..))")
public void printLog() {
}
@Before("printLog()")
public void before(JoinPoint jp) {
}
@After("printLog()")
public void after(JoinPoint jp) {
}
@AfterReturning(pointcut = "execution(* cn.xxx..*ServiceImpl.*(..))", returning = "returnVal")
public void afterReturn(JoinPoint jp, String returnVal) {
}
@AfterThrowing(value = "printLog()", throwing = "e")
public void afterThrow(JoinPoint jp, Throwable e) {
}
}

2 通知注解

  • @After(最终通知):通知方法会在目标方法返回或抛出异常后调用
  • @AfterReturning(后置通知):通知方法会在目标方法返回后调用
  • @AfterThrowing(异常通知):通知方法会在目标方法抛出异常后调用
  • @Around(环绕通知):通知方法会将目标方法封装起来
  • @Before(前置通知):通知方法会在目标方法调用之前执行

3 JoinPoint

常用的方法

  • Object[] getArgs:返回目标方法的参数
  • Signature getSignature:返回目标方法的签名
  • Object getTarget:返回被织入增强处理的目标对象
  • Object getThis:返回AOP框架为目标对象生成的代理对象

当使用 @Around 处理时,需要将第一个参数定义为 ProceedingJoinPoint 类型,该类是 JoinPoint 的子类。

4 切面执行顺序

4.1 被一个 Aspect 类拦截

正常: img

异常: img

4.2 被多个 Aspect 类拦截

1)执行顺序

优先级高的切面先执行,同一切面增强类型优先的先执行,增强类型相同的随机执行

2)指定优先级
  1. 让切面类实现 org.springframework.core.Ordered 接口:实现该接口的 int getOrder() 方法,该方法返回值越小,优先级越高
  2. 直接使用 @Order 注解来修饰一个切面类:使用这个注解时可以配置一个 int 类型的 value 属性,该属性值越小,优先级越高

AOP简介

AOP 即 Aspect Oriented Programming,面向切面编程。

AOP 允许程序员模块化横向业务逻辑,或定义核心部分的功能,例如日志管理和事务管理。

1 AOP 术语

img

  • 通知(Advice)
    • 通知表示在方法执行前后需要执行的动作,是切面真正要做的工作。通知负责定义切面whatwhen
  • 连接点(JointPoint)
    • 连接点表示在应用中可以插入切面,执行通知的地点。
  • 切点(Pointcut)
    • 切点定义了 where切点指定一个或者多个连接点,而通知通过切点接入。
  • 切面(Aspect)
    • 切面 = 通知 + 切点。定义 whatwhenwhere。是 AOP 的核心,将多个类的通用行为封装为可重用的模块。
  • 引入(Introduction)
    • 引入允许向现有类添加新方法或属性。类似于设计模式中的装饰者模式——在不改变现有类的基础上为之增加属性和方法。
  • 织入(Weaving)
    • 织入是把切面应用到目标对象并创建新的代理对象的过程。
    • 切面在指定的连接点被织入到目标对象中。
    • 在目标对象的生命周期里有多个点可以进行织入:编译期(AspectJ)、类加载期(LTW)、运行期(Spring AOP)
  • 目标对象
    • 被一个或者多个切面通知的对象。它通常是一个代理对象。也被称做被通知(advised)对象。
  • 代理
    • 代理是将通知应用到目标对象后创建的对象。从客户端的角度看,代理对象和目标对象是一样的。
  • 自动代理
    • BeanNameAutoProxyCreator:bean名称自动代理创建器
    • DefaultAdvisorAutoProxyCreator:默认通知者自动代理创建器
    • Metadata autoproxying:元数据自动代理
  • Concern(核心逻辑)
    • 表示在应用程序中一个模块的行为。Concern 可以定义为我们想要实现的功能。
  • Cross-cutting concern(横向的通用逻辑)
    • 指的是整个应用程序都会用到的功能,它影响整个应用程序。例如,日志管理(Logging)、安全管理(Security)以及数据交互

2 Spring 的 AOP 支持

Spring 对 AOP 有以下四种支持:

  • 基于代理的 Spring AOP
  • Pure-POJO aspects
  • 基于 @AspectJ 注解的 aspects
  • 注入 AspectJ aspects(所有版本的 Spring 都支持)

前三种属于 Spring AOP 框架,基于动态代理机制构建,仅支持函数级别的拦截。如果需要实现更复杂的 AOP 功能,则应使用 AspectJ 框架

在 Spring AOP 框架中,通过在 beans 的外围包含一个代理类来将切面织入到这些 beans。调用者跟代理类直接联系,代理类拦截函数调用,然后执行切面逻辑之后再调用真正的目标对象的方法。

img

虽然 Spring AOP 能够满足许多应用的切面需求, 但是与 AspectJ 相比, Spring AOP 是一个功能比较弱的 AOP 解决方案。 AspectJ 提供了 Spring AOP 所不能支持的许多类型的切点。

AOP 的源码中用到了两种动态代理来实现拦截切入功能:

  • jdk 动态代理
    • jdk 动态代理是由 java 内部的反射机制来实现的,反射机制在生成类的过程中比较高效
    • 如果加入容器的目标对象有实现接口,用 JDK 代理;
  • Cglib 动态代理
    • cglib 动态代理底层则是借助 asm 来实现的,asm 在生成类之后的相关执行过程中比较高效
    • 如果目标对象没有实现接口,用 Cglib 代理

3 AspectJ

AspectJ 注解生效

通过类级别的 @EnableAspectJAutoProxy 注解开启自动代理机制

@Configuration
@EnableAspectJAutoProxy //开启AspectJ的自动代理机制
@ComponentScan
public class ConcertConfig {
}

切点

1 编写切点

@Pointcut 定义可重复使用的切点

@Pointcut("execution(public * com.xxx.mapper.*.*(..))")
public void pointCut() {
}

1.1 表示式

由下列方式来定义或者通过 &&、 ||、 !、 的方式进行组合:

1)方法
  • execution:匹配方法
  • within:限制匹配范围
  • this:匹配 AOP 代理类的执行方法
  • target:匹配目标类的执行方法
  • args:将匹配方法的参数传入通知,名称必须相同
2)注解
  • @within:匹配类注解
  • @target:匹配类注解
  • @args:匹配参数注解
  • @annotation:匹配方法注解
3)bean
  • bean():匹配制定 bean

1.2 格式

“?”代表可选项

execution(修饰符? 返回值 类路径? 方法名(参数)异常类型?) 
  • 返回值
    *匹配任意
  • 方法名
    *匹配任意
  • 参数
    指定参数类型,,隔开
    *任意类型
    ..任意个数

1.3 例子

  • 任意公共方法:execution(public * *(..))
  • 任何“set”开头方法:execution(* set*(..))
  • 某类任意方法:execution(* com.xyz.service.AccountService.*(..))
  • 某包里任意方法: execution(* com.xyz.service.*.*(..))
  • 某包及子包任意方法:execution(* com.xyz.service..*.*(..))
  • 某包任意类: within(com.test.spring.aop.pointcutexp.*)
  • 实现了 Intf 接口的所有类:this(com.abc.Intf)
  • 带有 @xxx 注解的所有类的任意方法:
    • @within(org.abc.xxx)
    • @target(org.abc.xxx))
  • 带有 @xxx 注解的任意方法:@annotation(org.abc.xxx))
  • 参数带有有 @xxx 注解的方法:@args(org.abc.xxx)
  • 参数类型: args(String)

动态注册 Bean

1 注册接口

有两种接口可动态注册 bean

  1. BeanDefinitionRegistry
    注册 Bean 定义,使用BeanDefinitionRegistry 接口 registerBeanDefinition() 方法,根据BeanDefinition实例化 bean 实例
  2. SingletonBeanRegistry
    注册 bean 实例,使用SingletonBeanRegistry 接口registerSingleton() 方法,注册单例 bean。

DefaultListableBeanFactory 接口同时实现了这两个接口

2 普通注册

如果是在普通 Bean 中注册,那么该 bean 则无法被BeanPostProcessor处理,没有 AOP 的支持。

public class PersonManagerRegisterController {

@Autowired
GenericApplicationContext applicationContext;

@Autowired
ConfigurableBeanFactory beanFactory;

public void registerPersonManager() {
PersonDao personDao = applicationContext.getBean(PersonDao.class);
PersonManager personManager = new PersonManager();
personManager.setPersonDao(personDao);
beanFactory.registerSingleton("personManager", personManager);
}
}

3 BeanFactoryPostProcessor 注册

在 Spring 容器的启动过程中,BeanFactory 载入 bean 的定义后会立刻执行 BeanFactoryPostProcessor,此时动态注册 bean,则可以保证动态注册的 bean 被 BeanPostProcessor 处理,并且可以保证其的实例化和初始化总是先于依赖它的 bean。

public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
DefaultListableBeanFactory defaultListableBeanFactory
= (DefaultListableBeanFactory) beanFactory;

//注册 Bean 定义
BeanDefinitionBuilder beanDefinitionBuilder =
BeanDefinitionBuilder.genericBeanDefinition(PersonManager.class);
beanDefinitionBuilder.addPropertyReference("personDao", "personDao");
BeanDefinition personManagerBeanDefinition = beanDefinitionBuilder.getRawBeanDefinition();
defaultListableBeanFactory.registerBeanDefinition("personManager1", personManagerBeanDefinition);

//注册 bean 实例
PersonDao personDao = beanFactory.getBean(PersonDao.class);
PersonManager personManager = new PersonManager();
personManager.setPersonDao(personDao);
beanFactory.registerSingleton("personManager2", personManager);
}
}

4 Spring 官方实现

4.1 ImportBeanDefinitionRegistrar

Spring 官方在动态注册 bean 时,大多使用 ImportBeanDefinitionRegistrar 接口。

所有实现了该接口的类的都会被 ConfigurationClassPostProcessor 处理,ConfigurationClassPostProcessor 实现了 BeanFactoryPostProcessor 接口,所以 ImportBeanDefinitionRegistrar 中动态注册的 bean 是优先与依赖其的 bean 初始化的,也能被 aop、validator 等机制处理。

4.2 BeanDefinitionRegistryPostProcessor

Spring 官方另一个大量使用的动态注册接口是 BeanDefinitionRegistryPostProcessor,这个接口扩展自 BeanFactoryPostProcessor,专门用于动态注册 Bean。

@Component
public class PersonBeanDefinitionRegistryPostProcessor
implements BeanDefinitionRegistryPostProcessor {

// 注册 Bean 定义
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry)
throws BeansException {
// 构造 bean 定义
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder
.genericBeanDefinition(PersonManager.class);
//设置依赖
beanDefinitionBuilder.addPropertyReference("personDao", "personDao");
BeanDefinition personManagerBeanDefinition = beanDefinitionBuilder
.getRawBeanDefinition();
//注册bean定义
registry.registerBeanDefinition("personManager1", personManagerBeanDefinition);
}

// 注册 Bean 实例
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
throws BeansException {

BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder
.genericBeanDefinition(PersonManager.class, () -> {
PersonDao personDao = beanFactory.getBean(PersonDao.class);
PersonManager personManager = new PersonManager();
personManager.setPersonDao(personDao);
return personManager;
});
BeanDefinition personManagerBeanDefinition = beanDefinitionBuilder
.getRawBeanDefinition();
((DefaultListableBeanFactory) beanFactory)
.registerBeanDefinition("personManager2", personManagerBeanDefinition);
}
}

依赖注入

概念

IoC(Inversion of Control,控制反转 ):控制权由应用代码中转到了外部容器,控制权的转移,所谓控制反转。

依赖注入:依赖注入是控制反转(IOC)的一个层面,用创建对象而只需要描述如何创建它们。

IoC 负责装配 Bean,即创建应用对象之间协作关系,是依赖注入(DI,Dependency injection)的本质。

装配方式优先级:自动装配 > Java 配置文件 > XML 配置文件

1 自动装配

Spring 容器可以自动配置相互协作 beans 之间的关联关系。这意味着 Spring 可以自动配置一个 bean 和其他协作 bean 之间的关系。

Spring 从两个角度来实现自动装配

1.1 组件扫描

Spring 自动发现 ApplicationContext 中所创建的 bean

1)@Component

让 Spring 为类自动创建 bean,默认 ID 是类名称的首字母小写。也可给注解传入指定的参数指定名字。

@Component  
@Component("lonelyHeartsClub")

还有 @Repository、@Service、@Controller 等

2)@ComponentScan

开启 Component 扫描,默认设置该目录以及子目录下所有被 @Component 注解修饰的类。也可指定扫描的包。

@ComponentScan("soundsystem")//扫描指定包  
@ComponentScan(basePackages = "soundsystem")
@ComponentScan(basePackages = {"soundsystem", "video"})
@ComponentScan(basePackageClasses = {CDPlayer.class, DVDPlayer.class})//扫描指定类所属的包

1.2 自动装配

Spring 自动满足 bean 之间的依赖

@Autowired:声明自动装配,可用在构造器、方法、成员变量上。

只要对应类型的 bean 有且只有一个,则会自动装配。推荐通过构造器注入以避免 NullPointerException 问题。

1)注入到 Java 集合类

@Autowired 可直接将接口的实现 Bean 注入到如下几种类型的集合中

  • list:用来注入一系列的值,允许有相同的值。
  • set:用来注入一些列的值,不允许有相同的值。
  • map:用来注入一组”键-值”对,key 必须为 String。
  • props:也可以用来注入一组”键-值”对,这里的键、值都字符串类型。
@Autowired
private List<BeanInterface> list;
@Autowired
private Map<String,Interface> map;

application.properties

table-info.list[0]=list_value1
table-info.list[1]=list_value2
table-info.map.map_key1=map_value1
table-info.map.map_key2=map_value2
table-info.maplist.map_key1[0]=maplist_value1
table-info.maplist.map_key1[1]=maplist_value2
table-info.maplist.map_key2[0]=maplist_value3

TableConfig

@ConfigurationProperties(prefix = "table-info")
public class TableConfig {
private List<String> list;
private Map<String, String> map;
private Map<String, List<String>> maplist;
}
2)允许 null 值

默认要求依赖对象必须存在,如允许 null 值,可设置 @Autowired(required=false)

还有 @Resource 等

3)推荐对构造函数进行注释

@Autowired
private EnterpriseDbService service;

改为

private final EnterpriseDbService service;

@Autowired
public EnterpriseDbController(EnterpriseDbService service) {
this.service = service;
}

可以明确成员变量的加载顺序

2 Java 配置文件

想要装配第三方库中的组件时,因为没办法在它的类上添加 @Component 和 @Autowired 注解,不能自动化配置,必须要采用显式装配。

2.1 创建配置类

@Configuration:表明是配置类

2.2 声明 bean

@Bean:声明 bean,需编写方法创建所需类型实例, 注解 @Bean。默认 ID 是方法名,也可通过 name 属性指定名字。

@Bean(name = "lonelyHeartsClub")
public CompactDisc sgtPeppers() {
return new SgtPeppers();
}

2.3 装配 bean

在 JavaConfig 中装配 bean 的最简单方式就是引用创建 bean 的方法。

默认情况下,Spring 中所有的 bean 都是单例模式,Spring 会拦截 @Bean 注解的函数调用,并返回之前创建好的 bean。

@Bean
public CDPlayer cdPlayer() {
return new CDPlayer(sgtPeppers());
}

3 XML 配置文件

维护已有的 XML 配置, 在完成新的 Spring 工作时, 使用自动化配置和 JavaConfig。

创建一个 XML 文件, 并且以 元素为根。使用 元素声明 bean 并指定 class 属性。 元素注入构造器

可以同时使用两种方式的依赖注入,最好的选择是使用构造器参数实现强制依赖注入,使用 setter 方法实现可选的依赖关系。

3.1 set 注入

首先容器会触发一个无参构造函数或无参静态工厂方法实例化对象,之后容器调用 bean 中的setter 方法完成 Setter 方法依赖注入。

private OrderServiceImp orderService;
public void setOrderService(OrderServiceImp orderService) {
this.orderService = orderService;
}

Spring 配置 XML 文件:其中配置声明 OrderAction 类存在属性 orderService。程式运行时候,会将已经实例化的 orderService 对象调用 setOrderService 方式注入。

<bean name="orderAction" class="com.pec.action.OrderAction">
<property name="orderService" ref="orderService"></property>
</bean>
<bean name="orderService" class="com.pec.service.imp.OrderServiceImp"></bean>

3.2 构造器注入

构造器依赖注入在容器触发构造器的时候完成,该构造器有一系列的参数,每个参数代表注入的对象。

private OrderServiceImp orderService;
public OrderAction(OrderServiceImp orderService) {
this.orderService = orderService;
}

Spring 配置 XML 文件

<bean name="orderAction" class="com.pec.action.OrderAction">
<constructor-arg ref="orderService"></constructor-arg>
</bean>
<bean name="orderService" class="com.pec.service.imp.OrderServiceImp"></bean>

4 多种配置方法混用

通常的做法是:无论使用 JavaConfig 或者 XML 装配,都要创建一个 root configuration;并且在这个配置文件中开启自动扫描机制

4.1 JavaConfig 中引用

  • @Import:导入其他的 JavaConfig
  • @ImportResource:导入 XML 配置文件
@Import({CDPlayerConfig.class, CDConfig.class})
@ImportResource("classpath: cd-config.xml")

4.2 XML 配置中引用

标签引入其他的 XML 配置文件

<import resource="classpath:/spring/calculate-web.xml"/>

标签导入 Java 配置文件

<bean class="soundsystem.CDConfig" />

5 BeanFactory 和 FactoryBean的区别

BeanFactory

BeanFactory,以Factory结尾,表示它是一个工厂类(接口), 它负责生产和管理bean的一个工厂。在Spring中,BeanFactory是IOC容器的核心接口,它的职责包括:实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。

FactoryBean

一般情况下,Spring通过反射机制利用的class属性指定实现类实例化Bean,在某些情况下,实例化Bean过程比较复杂,如果按照传统的方式,则需要在中提供大量的配置信息。配置方式的灵活性是受限的,这时采用编码的方式可能会得到一个简单的方案。Spring为此提供了一个org.springframework.bean.factory.FactoryBean的工厂类接口,用户可以通过实现该接口定制实例化Bean的逻辑。FactoryBean接口对于Spring框架来说占用重要的地位,Spring自身就提供了70多个FactoryBean的实现。它们隐藏了实例化一些复杂Bean的细节,给上层应用带来了便利。从Spring3.0开始,FactoryBean开始支持泛型,即接口声明改为FactoryBean的形式

以Bean结尾,表示它是一个Bean,不同于普通Bean的是:它是实现了FactoryBean接口的Bean,根据该Bean的ID从BeanFactory中获取的实际上是FactoryBean的getObject()返回的对象,而不是FactoryBean本身,如果要获取FactoryBean对象,请在id前面加一个&符号来获取。