简

人生短暂,学海无边,而大道至简。


  • 首页

  • 归档

  • 分类

  • 标签

三、Spring后端之4-缓存数据

发表于 2017-09-25 | 分类于 Spring实战4版
启用声明式缓存
使用Ehcache、 Redis和GemFire实现缓存功能
注解驱动的缓存

Spring的缓存抽象:Spring自身并没有实现缓存解决方案, 但是它对缓存功能提供了声明式的支持, 能够与多种流行的缓存实现进行集成。  
1、启用对缓存的支持  
Spring对缓存的支持有两种方式:注解驱动的缓存;XML声明的缓存 。

使用Spring的缓存抽象时, 最为通用的方式就是在方法上添加@Cacheable和@CacheEvict注解。
在往bean上添加缓存注解之前, 必须要启用Spring对注解驱动缓存的支持。 如果我们使用Java配置的话, 那么可以在其中的一个配置类上添加@EnableCaching, 这样的话就能启用注解驱动的缓存。
如果以XML的方式配置应用的话, 那么可以使用Spring cache命名空间中的<cache:annotation-driven>元素来启用注解驱动的缓存。  
配置缓存管理器  
Spring 3.1内置了五个缓存管理器实现, 如下所示:
SimpleCacheManager
NoOpCacheManager
ConcurrentMapCacheManager
CompositeCacheManager
EhCacheCacheManager
  
Spring 3.2引入了另外一个缓存管理器, 这个管理器可以用在基于JCache(JSR-107) 的缓存提供商之中。 除了核心的Spring框架, SpringData又提供了两个缓存管理器:
RedisCacheManager(来自于Spring Data Redis项目)
GemfireCacheManager(来自于Spring Data GemFire项目)  

根据实际应用中,选择一个缓存管理器, 然后要在Spring应用上下文中, 以bean的形式对其进行配置。 前面配置了ConcurrentMapCacheManager, 并且知道它可能并不是实际应用的最佳选择。 现在, 看一下如何配置Spring其他的缓存管理器,
EhCacheCacheManager  
Ehcache是最为流行的缓存供应商之一。 Ehcache网站上说它是“Java领域应用最为广泛的缓存”。 鉴于它的广泛采用, Spring提供集成Ehcache的缓存管理器是很有意义的。 这个缓存管理器也就是EhCacheCacheManag。

cacheManager()方法创建了一个EhCacheCacheManager的实例, 这是通过传入Ehcache CacheManager实例实现的。 
因为Spring和EhCache都定义了CacheManager类型。 需要明确的是, EhCache的CacheManager要被注入到Spring的EhCacheCacheManager(Spring CacheManager的实现) 之中。
我们需要使用
EhCache的CacheManager来进行注入, 所以必须也要声明一个CacheManager bean。 为了对其进行简化, Spring提供了EhCacheManagerFactoryBean来生成EhCache的CacheManager。 方法ehcache()会创建并返回一个EhCacheManagerFactoryBean实例。 因为它是一个工厂bean(也就是说, 它实现了Spring的FactoryBean接口) , 所以注册在Spring应用上下文中的并不是EhCacheManagerFactoryBean的实例, 而是CacheManager的一个实例, 因此适合注入到EhCacheCacheManager之中。

除了在Spring中配置的bean, 还需要有针对EhCache的配置。 EhCache为XML定义了自己的配置模式, 我们需要在一个XML文件中配置缓存,该文件需要符合EhCache所定义的模式。 在创建EhCacheManagerFactoryBean的过程中, 需要告诉它EhCache配置文件在什么地方。 在这里通过调用setConfigLocation()方法, 传入ClassPath-Resource, 用来指明EhCache XML配置文件相对于根类路径(classpath) 的位置。至于ehcache.xml文件的内容, 不同的应用之间会有所差别, 但是至少需要声明一个最小的缓存。 例如, 如下的EhCache配置声明一个名为spittleCache的缓存, 它最大的堆存储为50MB, 存活时间为100秒。  
使用Redis缓存  
Redis可以用来为Spring缓存抽象机制存储缓存条目, Spring Data Redis提供了RedisCacheManager, 这是CacheManager的一个实现。RedisCacheManager会与一个Redis服务器协作, 并通过RedisTemplate将缓存条目存储到Redis中。
为了使用
RedisCacheManager, 我们需要RedisTemplate bean以及RedisConnectionFactory实现类(如JedisConnectionFactory) 的一个bean。在之前配置RedisTemplate、RedisConnectionFactory的基础上配置RedisCacheManager:
通过传递一个RedisTemplate实例作为其构造器的参数实现的。  

使用多个缓存管理器  
如果配置多个缓存管理器,可以使用Spring的CompositeCacheManager。
CompositeCacheManager要通过一个或更多的缓存管理器来进行配置, 它会迭代这些缓存管理器, 以查找之前所缓存的值。 以下的程序清
CompositeCacheManager会迭代一个缓存管理器的列表 ,它会迭代JCacheCacheManager、 EhCacheCacheManager和RedisCacheManager。

2、为方法添加注解以支持缓存  
Spring的缓存抽象是围绕切面构建的。 在Spring中启用缓存时, 会创建一个切面, 它触发一个或更多的Spring的缓存注解。所有注解都能运用在方法或类上。 当将其放在单个方法上时, 注解所描述的缓存行为只会运用到这个方法上。 如果注解放在类级别的话, 那么缓存行为就会应用到这个类的所有方法上。
注 解 描 述
@Cacheable 表明Spring在调用方法之前, 首先应该在缓存中查找方法的返回值。 如果这个值能够找到, 就会返回缓存的值。 否则的话, 这个方法就会被调用, 返回值会
放到缓存之中
@CachePut 表明Spring应该将方法的返回值放到缓存中。 在方法的调用前并不会检查缓存, 方法始终都会被调用
@CacheEvict 表明Spring应该在缓存中清除一个或多个条目
@Caching 这是一个分组的注解, 能够同时应用多个其他的缓存注解


填充缓存  
@Cacheable和@CachePut注解都可以填充缓存, 但是它们的工作方式略有差异。
@Cacheable首先在缓存中查找条目, 如果找到了匹配的条目, 那么就不会对方法进行调用了。 如果没有找到匹配的条目, 方法会被调用并且返回值要放到缓存之中。 而@CachePut并不会在缓存中检查匹配的值, 目标方法总是会被调用, 并将返回值添加到缓存之中。
@Cacheable和@CachePut有一些属性是共有的:
属 性 类 型 描 述
value String[] 要使用的缓存名称
condition String SpEL表达式, 如果得到的值是false的话, 不会将缓存应用到方法调用上
key String SpEL表达式, 用来计算自定义的缓存key
unless String SpEL表达式, 如果得到的值是true的话, 返回值不会放到缓存之中

在最简单的情况下, 在@Cacheable和@CachePut的这些属性中, 只需使用value属性指定一个或多个缓存即可。 例如, SpittleRepository的findOne()方法。 在初始保存之后, Spittle就不会再发生变化了。 通过在findOne()方法上添加@Cacheable注解,能够确保将Spittle保存在缓存中, 从而避免对数据库的不必要访问。 
当findOne()被调用时, 缓存切面会拦截调用并在缓存中查找之前以名spittleCache存储的返回值。 缓存的key是传递到findOne()方法中的id参数。 如果按照这个key能够找到值的话, 就会返回找到的值, 方法不会再被调用。 如果没有找到值的话, 那么就会调用这个方法,并将返回值放到缓存之中, 为下一次调用findOne()方法做好准备。

注意:通常@Cacheable 注解是放在接口定义中,而不是放在实现类中。
将值放到缓存之中
@Cacheable会条件性地触发对方法的调用, 这取决于缓存中是不是已经有了所需要的值, 对于所注解的方法, @CachePut采用了一种更为直接的流程。 带有@CachePut注解的方法始终都会被调用, 而且它的返回值也会放到缓存中。 这提供一种很便利的机制, 能够让我们在请求之前预先加载缓存。
例如, 当一个全新的
Spittle通过SpittleRepository的save()方法保存之后, 很可能马上就会请求这条记录。 所以, 当save()方法调用后, 立即将Spittle塞到缓存之中是很有意义的, 这样当其他人通过findOne()对其进行查找时, 它就已经准备就绪了。 为了实现这一点, 可以在save()方法上添加@CachePut注解, 如下所示:  
当save()方法被调用时, 首先保存Spittle, 然后返回的Spittle会被放到spittleCache缓存中。
在这里只有一个问题: 缓存的key。 默认的缓存key要基于方法的参数来确定。 因为save()方法的唯一参数就是Spittle, 所以它会用作缓存的key。 将Spittle放在缓存中, 而它的缓存key恰好是同一个。在这个场景中, 默认的缓存key并不是我们想要的。 在这里需要指定一个key。
自定义缓存key
@Cacheable和@CachePut都有一个名为key属性, 这个属性能够替换默认的key, 它是通过一个SpEL表达式计算得到的。 任意的SpEL表达式都是可行的, 但是更常见的场景是所定义的表达式与存储在缓存中的值有关, 据此计算得到key。
具体到我们这个场景, 我们需要将key设置为所保存Spittle的ID。 以参数形式传递给save()的Spittle还没有保存, 因此并没有ID。 我们只能通过save()返回的Spittle得到id属性。
在为缓存编写SpEL表达式的时候, Spring暴露了一些很有用的元数据。 SpEL中可用的缓存元数据。
表 达 式 描 述
#root.args 传递给缓存方法的参数, 形式为数组
#root.caches 该方法执行时所对应的缓存, 形式为数组
#root.target 目标对象
#root.targetClass 目标对象的类, 是#root.target.class的简写形式
#root.method 缓存方法
#root.methodName 缓存方法的名字, 是#root.method.name的简写形式
#result 方法调用的返回值(不能用在@Cacheable注解上)
#Argument 任意的方法参数名(如#argName) 或参数索引(如#a0或#p0)

表达式#result能够得到返回的Spittle。 借助这个对象, 我们可以通过将key属性设置为#result.id来引用id属性 :

条件化缓存  
@Cacheable和@CachePut提供了两个属性用以实现条件化缓存: unless和condition, 这两个属性都接受一个SpEL表达式。 如果unless属性的SpEL表达式计算结果为true, 那么缓存方法返回的数据就不会放到缓存中。 与之类似, 如果condition属性的SpEL表达式计算结果为false, 那么对于这个方法缓存就会被禁用掉。  

unless属性只能阻止将对象放进缓存, 但是在这个方法调用的时候, 依然会去缓存中进行查找, 如果找到了匹配的值, 就会返回找到的值。
condition的表达式计算结果为false, 那么在这个方法调用的过程中, 缓存是被禁用的。 就是说, 不会去缓存进行查找, 同时返回值也不会放进缓存中 。

移除缓存条目  
@CacheEvict注解的方法被调用的话, 那么会有一个或更多的条目会在缓存中移除。  
@CacheEvict能够应用在返回值为void的方法上, 而@Cacheable和@CachePut需要非void的返回值  
当remove()调用时, 会从缓存中删除一个条目。 被删除条目的key与传递进来的spittleId参数的值相等 ,属性:
属 性 类 型 描 述
value String [] 要使用的缓存名称
key String SpEL表达式, 用来计算自定义的缓存key
condition String SpEL表达式, 如果得到的值是false的话, 缓存不会应用到方法调用上
allEntries b oolean 如果为true的话, 特定缓存的所有条目都会被移除掉
beforeInvocation b oolean 如果为true的话, 在方法调用之前移除条目。 如果为false(默认值) 的话, 在方法成功调用之后再移除条目
Spring的缓存注解提供了一种优雅的方式在应用程序的代码中声明缓存规则。 但是, Spring还为缓存提供了XML命名空间。

3、使用XML声明缓存  
为什么要以XML的方式声明缓存:
在源码中添加Spring的注解有点不太舒服;你需要在没有源码的bean上应用缓存功能。
这样就将缓存配置与缓存数据的代码分隔开来。 Spring的cache命名空间提供了使用XML声明缓存规则的方法, 可以作为面向注解缓存的替代方案。 因为缓存是一种面向切面的行为, 所以cache命名空间会与Spring的aop命名空间结合起来使用, 用来声明缓存所应用的切点。
Spring的cache命名空间提供了以XML方式配置缓存规则的元素:
元素 描述
<cache:annotation-driven> 启用注解驱动的缓存。 等同于Java配置中的@EnableCaching
<cache:advice> 定义缓存通知(advice) 。 结合<aop:advisor>, 将通知应用到切点上
<cache:caching> 在缓存通知中, 定义一组特定的缓存规则
<cache:cacheable> 指明某个方法要进行缓存。 等同于@Cacheable注解
<cache:cache-put> 指明某个方法要填充缓存, 但不会考虑缓存中是否已有匹配的值。 等同于@CachePut注解
<cache:cache-evict> 指明某个方法要从缓存中移除一个或多个条目, 等同于@CacheEvict注解

三、Spring后端之3-使用NoSQL数据库

发表于 2017-09-24 | 分类于 Spring实战4版
  • 为MongoDB和Neo4j编写Repository
  • 为多种数据存储形式持久化数据
  • 组合使用Spring和Redis
上一章, 我们看到了Spring DataJPA, 它是Spring Data项目下的多个子项目之一。 通过在运行时自动生成Repository实现, Spring Data JPA能够让使用JPA的过程更加简单容易。
Spring Data还提供了对多种NoSQL数据库的支持, 包括MongoDB、 Neo4j和Redis。 它不仅支持自动化的Repository, 还支持基于模板的数据访问和映射注解。 在本章中, 将会看到如何为非关系型的NoSQL数据库编写Repository。 

1、使用MongoDB持久化文档数据  
  • 通过注解实现对象-文档映射;
  • 使用MongoTemplate实现基于模板的数据库访问;
  • 自动化的运行时Repository生成功能。
启用MongoDB  
需要配置MongoClient, 以便于访问MongoDB数据库。 同时, 我们还需要有一个MongoTemplate bean, 实现基于模板的数据库访问。
此外, 不是必须但强烈推荐启用Spring Data MongoDB的自动化Repository生成功能。
如下的程序清单展现了如何编写简单的Spring Data MongoDB配置类, 它包含了上述的几个bean:
除了直接声明这些bean, 我们还可以让配置类扩展AbstractMongo-Configuration并重载getDatabaseName()和mongo()方法。 
功能是相同的, 只不过在篇幅上更加简洁。 最为显著的区别在于这个配置中没有直接声明MongoTemplate bean, 但是会被隐式地创建。 我们在这里重载了getDatabaseName()方法来提供数据库的名称。 mongo()方法依然会创建一个MongoClient的实例, 因为它会抛出Exception, 所以我们可以直接使用MongoClient, 而不必再使用MongoFactoryBean了  

如果MongoDB服务器运行在生产配置上, 我认为你可能还启用了认证功能。 在这种情况下, 为了访问数据库, 我们还需要提供应用的凭证。
通过注解实现对象-文档映射,为模型添加注解, 实现MongoDB持久化  
当使用JPA的时候, 我们需要将Java实体类映射到关系型表和列上。 JPA规范提供了一些支持对象-关系映射的注解, 而有一些JPA实现, 如Hibernate, 也添加了自己的映射注解。
但是,
MongoDB并没有提供对象-文档映射的注解。 Spring Data MongoDB填补了这一空白, 提供了一些将Java类型映射为MongoDB文档的注解。 表描述了这些注解。  
@Document 标示映射到MongoDB文档上的领域对象
@Id 标示某个域为ID域
@DbRef 标示某个域要引用其他的文档, 这个文档有可能位于另外一个数据库中
@Field 为文档域指定自定义的元数据
@Version 标示某个属性用作版本域


使用
MongoTemplate实现基于模板的数据库访问
我们已经在配置类中配置了MongoTemplate bean, 不管是显式声明还是扩展AbstractMongoConfiguration都能实现相同的效果。 接下来, 需要做的就是将其注入到使用它的地方:  
之后就可以对数据库操作了。
MongoOperations暴露了多个使用MongoDB文档数据库的方法。 在这里, 我们不可能讨论所有的方法, 但是可以看一下最为常用的几个操作, 比如计算文档集合中有多少条文档。 使用注入的MongoOperations, 我们可以得到Order集合并调用count()来得到数量:  

自动化的运行时
Repository生成功能
我们已经通过@EnableMongoRepositories注解启用了Spring Data MongoDB的Repository功能, 接下来需要做的就是创建一个接口, Repository实现要基于这个接口来生成。 不过, 在这里, 我们不再扩展JpaRepository, 而是要扩展MongoRepository。 如下程序清单中的OrderRepository扩展了MongoRepository, 为Order文档提供了基本的CRUD操作。  
MongoRepository接口有两个参数, 第一个是带有@Document注解的对象类型, 也就是该Repository要处理的类型。 第二个参数是带有@Id注解的属性类型。
尽管
OrderRepository本身并没有定义任何方法, 但是它会继承多个方法, 包括对Order文档进行CRUD操作的方法。 表描述了OrderRepository继承的所有方法。  
通过扩展MongoRepository, Repository接口能够继承多个CRUD操作, 它们会由Spring Data MongoDB自动实现  
方 法 描 述
long count(); 返回指定Repository类型的文档数量
void delete(Iterable<? extends T); 删除与指定对象关联的所有文档
void delete(T); 删除与指定对象关联的文档
void delete(ID); 根据ID删除某一个文档
void deleteAll(); 删除指定Repository类型的所有文档
boolean exists(Object); 如果存在与指定对象相关联的文档, 则返回true
boolean exists(ID); 如果存在指定ID的文档, 则返回true
List<T> findAll(); 返回指定Repository类型的所有文档
List<T> findAll(Iterable<ID>); 返回指定文档ID对应的所有文档
List<T> findAll(Pageable); 为指定的Repository类型, 返回分页且排序的文档列表
List<T> findAll(Sort); 为指定的Repository类型, 返回排序后的所有文档列表
T findOne(ID); 为指定的ID返回单个文档
Save( terable<s>) ; 保存指定Iterable中的所有文档
save ( < S > ); 为给定的对象保存一条文档

指定查询  
@Query注解可以为Repository方法指定自定义的查询。 @Query能够像在JPA中那样用在MongoDB上。 唯一的区别在于针对MongoDB时, @Query会接受一个JSON查询, 而不是JPA查询。
例如, 假设我们想要查询给定类型的订单, 并且要求
customer的名称为“Chuck Wagon”。 OrderRepository中如下的方法声明能够完成所需的任务:

@Query中给定的JSON将会与所有的Order文档进行匹配, 并返回匹配的文档。 需要注意的是, type属性映射成了“?0”, 这表明type属性应该与查询方法的第零个参数相等。 如果有多个参数的话, 它们可以通过“?1”、 “?2”等方式进行引用。  

混合自定义的功能  
和对象-关系映射持久化数据小节中使用方式一样,不做介绍。

2、使用Neo4j操作图数据  
运用场景:
  • 用于联机事务图的持久化技术(通常直接实时地从应用程序中访问)。这类技术被称为图数据库,它们和“通常的”关系型数据库世界中的联机事务处理(Online Transactional Processing,OLTP)数据库是一样的。
  • 用于离线图分析的技术(通常都是按照一系列步骤执行)。
  • 场景介绍详见:https://www.jianshu.com/p/500448f810c5
    暂时不关注。

    3、使用Redis操作key-value数据  
    Redis是一种特殊类型的数据库, 它被称之为key-value存储。 可以简单理解为它们就是持久化的哈希Map。
    其实对于哈希
    Map或者key-value存储来说, 其实并没有太多的操作。 我们可以将某个value存储到特定的key上, 并且能够根据特定key, 获取value。 差不多也就是这样了。 因此, Spring Data的自动Repository生成功能并没有应用到Redis上。 不过, Spring Data的另外一个关键特性, 也就是面向模板的数据访问, 能够在使用Redis的时候, 为我们提供帮助。
    Spring Data Redis包含了多个模板实现, 用来完成Redis数据库的数据存取功能。 建SpringData Redis的模板, 我们首先需要有一个Redis连接工厂,Spring Data Redis提供了四个连接工厂供我们选择。  
    连接到Redis  
    Redis连接工厂会生成到Redis数据库服务器的连接。 Spring Data Redis为四种Redis客户端实现提供了连接工厂:
    JedisConnectionFactory
    JredisConnectionFactory
    LettuceConnectionFactory
    SrpConnectionFactory
    具体选择哪一个取决于你最适合你的需求。 从Spring Data Redis的角度来看, 这些连接工厂在适用性上都是相同的。之后就可以将连接工厂配置为Spring中的bean。 
    通过默认构造器创建的连接工厂会向localhost上的6379端口创建连接, 并且没有密码。 如果你的Redis服务器运行在其他的主机或端口上, 在创建连接工厂的时候, 可以设置这些属性:
    类似地, 如果你的Redis服务器配置为需要客户端认证的话, 那么可以通过调用setPassword()方法来设置密码: 
     
    使用RedisTemplate  
    Redis连接工厂会生成到Redis key-value存储的连接(以RedisConnection的形式) 。 借助RedisConnection, 可以存储和读取数据。 
    与之类似, 我们还可以使用RedisConnection来获取之前存储的问候信息: 

    与其他的Spring Data项目类似, Spring Data Redis以模板的形式提供了较高等级的数据访问方案。 实际上, Spring Data Redis提供了两个模板:
    RedisTemplate
    StringRedisTemplate
      
    RedisTemplate可以极大地简化Redis数据访问, 能够让我们持久化各种类型的key和value, 并不局限于字节数组。 在认识到key和value通常是String类型之后, StringRedisTemplate扩展了RedisTemplate, 只关注String类型。
     
    注意, RedisTemplate使用两个类型进行了参数化。 第一个是key的类型, 第二个是value的类型。 在这里所构建的RedisTemplate中, 将会保存Product对象作为value, 并将其赋予一个String类型的key。
    如果你所使用的
    value和key都是String类型, 那么可以考虑使用StringRedisTemplate来代替RedisTemplate  
    RedisTemplate的很多功能是以子API的形式提供的, 它们区分了单个值和集合值的场景  
    方 法 子API接口 描 述
    opsForValue() ValueOperations<K, V> 操作具有简单值的条目
    opsForList() ListOperations<K, V> 操作具有list值的条目
    opsForSet() SetOperations<K, V> 操作具有set值的条目
    opsForZSet() ZSetOperations<K, V> 操作具有ZSet值(排序的set) 的条目
    opsForHash() HashOperations<K, HK, HV> 操作具有hash值的条目
    boundValueOps(K) BoundValueOperations<K,V> 以绑定指定key的方式, 操作具有简单值的条目
    boundListOps(K) BoundListOperations<K,V> 以绑定指定key的方式, 操作具有list值的条目
    boundSetOps(K) BoundSetOperations<K,V> 以绑定指定key的方式, 操作具有set值的条目
    boundZSet(K) BoundZSetOperations<K,V> 以绑定指定key的方式, 操作具有ZSet值(排序的set) 的条目
    boundHashOps(K) BoundHashOperations<K,V> 以绑定指定key的方式, 操作具有hash值的条目

    假设我们想通过RedisTemplate<String, Product>保存Product, 其中key是sku属性的值。 

    使用key和value的序列化器  
    当某个条目保存到Redis key-value存储的时候, key和value都会使用Redis的序列化器(serializer) 进行序列化。 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。
    这些序列化器都实现了RedisSerializer接口, 如果其中没有符合需求的序列化器, 那么你还可以自行创建。
    RedisTemplate会使用JdkSerializationRedisSerializer, 这意味着key和value都会通过Java进行序列化。 StringRedisTemplate默认会使用StringRedis-Serializer, 这在我们的预料之中, 它实际上就是实现String与byte数组之间的相互转换。 这些默认的设置适用于很多的场景, 但有时候你可能会发现使用一个不同的序列化器也是很有用处的。
    例如, 假设当使用
    RedisTemplate的时候, 我们希望将Product类型的value序列化为JSON, 而key是String类型。 RedisTemplate的setKeySerializer()和setValueSerializer()方法就需要如下所示:  

    三、Spring后端之2-对象-关系映射持久化数据

    发表于 2017-09-23 | 分类于 Spring实战4版
    • 使用Spring和Hibernate
    • 借助上下文Session, 编写不依赖于Spring的Repository
    • 通过Spring使用JPA
    • 借助Spring Data实现自动化的JPA Repository
    持久化
    随着应用程序变得越来越复杂, 对持久化的需求也变得更复杂。 我们需要将对象的属性映射到数据库的列上, 并且需要自动生成语句和查询, 这样我们就能从无休止的问号字符串中解脱出来。 此外, 我们还需要一些更复杂的特性:
    延迟加载(Lazy loading) : 随着我们的对象关系变得越来越复杂, 有时候我们并不希望立即获取完整的对象间关系。 举一个典型的例子, 假设我们在查询一组PurchaseOrder对象, 而每个对象中都包含一个LineItem对象集合。 如果我们只关心PurchaseOrder的属性, 那查询出LineItem的数据就毫无意义。 而且这可能是开销很大的操作。 延迟加载允许我们只在需要的时候获取数据。
    预先抓取(Eager fetching) : 这与延迟加载是相对的。 借助于预先抓取, 我们可以使用一个查询获取完整的关联对象。 如果我们需要PurchaseOrder及其关联的LineItem对象, 预先抓取的功能可以在一个操作中将它们全部从数据库中取出来, 节省了多次查询的成本。
    级联(Cascading) : 有时, 更改数据库中的表会同时修改其他表。 回到我们订购单的例子中, 当删除Order对象时, 我们希望同时在数据库中删除关联的LineItem。

    有很多这样的框架实现,通用名称是对象/关系映射(object-relational mapping, ORM) 。 在持久层使用ORM工具, 可以节省数千行的代码和大量的开发时间。 ORM工具能够把你的注意力从容易出错的SQL代码转向如何实现应用程序的真正需求。
    Spring对多个持久化框架都提供了支持, 包括Hibernate、 iBATIS、 Java数据对象(Java Data Objects, JDO) 以及Java持久化API(JavaPersistence API, JPA) 。 与Spring对JDBC的支持那样, Spring对ORM框架的支持提供了与这些框架的集成点以及一些附加的服务:
    • 支持集成Spring声明式事务;
    • 透明的异常处理;
    • 线程安全的、 轻量级的模板类;
    • DAO支持类;
    • 资源管理。
    这里主要介绍两种ORM方案集成: Hibernate和JPA。 同时还会通过Spring Data JPA了解一下Spring Data项目。
    持久化集成
    Spring与Hibernate
    略

    Spring与Java持久化API
    Java持久化API(Java Persistence API, JPA) 诞生在EJB 2实体Bean的基础上, 并成为下一代Java持久化标准。 JPA是基于POJO的持久化机制, 它从Hibernate和Java数据对象(Java Data Object, JDO) 上借鉴了很多理念并加入了Java 5注解的特性。
    在Spring 2.0版本中, Spring首次集成了JPA的功能。 很多开发人员都推荐在基于Spring的应用程序中使用JPA实现持久化。 实际上, 有些人还将Spring-JPA的组合称为POJO开发的梦之队。

    在Spring中使用JPA的第一步是要在Spring应用上下文中将实体管理器工厂(entity manager factory) 按照bean的形式来进行配置。
    1、配置实体管理器工厂
    基于JPA的应用程序需要使用EntityManagerFactory的实现类来获取EntityManager实例。 JPA定义了两种类型的实体管理器:
    应用程序管理类型(Application-managed) : 当应用程序向实体管理器工厂直接请求实体管理器时, 工厂会创建一个实体管理器。 在这种模式下, 程序要负责打开或关闭实体管理器并在事务中对其进行控制。 这种方式的实体管理器适合于不运行在Java EE容器中的独立应用程序。
    容器管理类型(Container-managed) : 实体管理器由Java EE创建和管理。 应用程序根本不与实体管理器工厂打交道。 相反, 实体管理器直接通过注入或JNDI来获取。 容器负责配置实体管理器工厂。 这种类型的实体管理器最适用于Java EE容器, 在这种情况下会希望在persistence.xml指定的JPA配置之外保持一些自己对JPA的控制。

    以上的两种实体管理器实现了同一个EntityManager接口。 关键的区别不在于EntityManager本身, 而是在于EntityManager的创建和管理方式。 应用程序管理类型的EntityManager是由EntityManagerFactory创建的, 而后者是通过PersistenceProvider的createEntityManagerFactory()方法得到的。 与此相对, 容器管理类型的EntityManagerFactory是通过PersistenceProvider的createContainerEntityManager Factory()方法获得的。
    这对想使用JPA的Spring开发者来说又意味着什么呢? 其实这并没太大的关系。 不管你希望使用哪种EntityManagerFactory, Spring都会负责管理EntityManager。 如果你使用的是应用程序管理类型的实体管理器, Spring承担了应用程序的角色并以透明的方式处理EntityManager。 在容器管理的场景下, Spring会担当容器的角色。
    这两种实体管理器工厂分别由对应的Spring工厂Bean创建:
    LocalEntityManagerFactoryBean生成应用程序管理类型的EntityManager-Factory;
    LocalContainerEntityManagerFactoryBean生成容器管理类型的Entity-ManagerFactory。

    不管选择应用程序管理类型的还是容器管理类型的EntityManager Factory, 对于基于Spring的应用程序来讲是完全透明的。
    当组合使用Spring和JPA时, 处理EntityManagerFactory的复杂细节被隐藏了起来, 数据访问代码只需关注它们的真正目标即可, 也就是数据访问。

    应用程序管理类型和容器管理类型的实体管理器工厂之间唯一值得关注的区别是在Spring应用上下文中如何进行配置。 让我们先看看如何在Spring中配置应用程序管理类型的LocalEntityManagerFactoryBean, 然后再看看如何配置容器管理类型的LocalContainerEntityManagerFactoryBean。
    配置应用程序管理类型的JPA
    对于应用程序管理类型的实体管理器工厂来说, 它绝大部分配置信息来源于一个名为persistence.xml的配置文件。 这个文件必须位于类路径下的META-INF目录下。persistence.xml的作用在于定义一个或多个持久化单元。 持久化单元是同一个数据源下的一个或多个持久化类。 简单来讲, persistence.xml列出了一个或多个的持久化类以及一些其他的配置如数据源和基于XML的配置文件。 如下是一个典型的persistence.xml文件, 它是用于Spittr应用程序的:
    因为在persistence.xml文件中包含了大量的配置信息, 所以在Spring中需要配置的就很少了。 可以通过以下的@Bean注解方法在Spring中声明LocalEntityManagerFactoryBean:  
    赋给persistenceUnitName属性的值就是persistence.xml中持久化单元的名称。
    创建应用程序管理类型的
    EntityManagerFactory都是在persistence.xml中进行的,在应用程序管理的场景下(不考虑Spring时) , 完全由应用程序本身来负责获取EntityManagerFactory, 这是通过JPA实现的PersistenceProvider做到的。 如果每次请求EntityManagerFactory时都需要定义持久化单元, 那代码将会迅速膨胀。 通过将其配置在persistence.xml中, JPA就能够在这个特定的位置查找持久化单元定义了。
    但借助于
    Spring对JPA的支持, 我们不再需要直接处理PersistenceProvider了。 因此, 再将配置信息放在persistence.xml中就显得不那么明智了。 实际上, 这样做妨碍了我们在Spring中配置EntityManagerFactory(如果不是这样的话, 我们可以提供一个Spring配置的数据源) 。

    使用容器管理类型的JPA
    容器管理的JPA采取了一个不同的方式。 当运行在容器中时, 可以使用容器(在我们的场景下是Spring) 提供的信息来生成EntityManagerFactory。
    你可以将数据源信息配置在
    Spring应用上下文中, 而不是在persistence.xml中了。 例如, 如下的@Bean注解方法声明了在Spring中如何使用LocalContainerEntity-ManagerFactoryBean来配置容器管理类型的JPA:  
    这里, 我们使用了Spring配置的数据源来设置dataSource属性。 任何javax.sql.DataSource的实现都是可以的。 尽管数据源还可以在persistence.xml中进行配置, 但是这个属性指定的数据源具有更高的优先级。
    jpaVendorAdapter属性用于指明所使用的是哪一个厂商的JPA实现。 Spring提供了多个JPA厂商适配器:
    EclipseLinkJpaVendorAdapter
    HibernateJpaVendorAdapter
    OpenJpaVendorAdapter
    TopLinkJpaVendorAdapter
    (在Spring 3.1版本中, 已经将其废弃了)
    在本例中, 我们使用
    Hibernate作为JPA实现, 所以将其配置为Hibernate-JpaVendorAdapter:  
    有多个属性需要设置到厂商适配器上, 但是最重要的是database属性, 在上面我们设置了要使用的数据库是Hypersonic。
    数据库平台 属性database的值
    IBMDB2 DB2
    Apache Derby DERBY
    H2 H2
    Hypersonic HSQL
    Informix INFORMIX
    MySQL MYSQL
    Oracle ORACLE
    PostgresQL POSTGRESQL
    Microsoft SQL Server SQLSERVER
    Sybase SYBASE

    一些特定的动态持久化功能需要对持久化类按照指令(instrumentation) 进行修改才能支持。 在属性延迟加载(只在它们被实际访问时才从数据库中获取) 的对象中, 必须要包含知道如何查询未加载数据的代码。 一些框架使用动态代理实现延迟加载, 而有一些框架像JDO, 则是在编译时执行类指令。
    选择哪一种实体管理器工厂主要取决于如何使用它。 但是, 下面的小技巧可能会让你更加倾向于使用
    LocalContainerEntityManagerFactoryBean。
    persistence.xml文件的主要作用就在于识别持久化单元中的实体类。 但是从Spring 3.1开始, 我们能够在LocalContainerEntityManagerFactoryBean中直接设置packagesToScan属性:  
    在这个配置中, LocalContainerEntityManagerFactoryBean会扫描com.habuma.spittr.domain包, 查找带有@Entity注解的类。 因此, 没有必要在persistence.xml文件中进行声明了。 同时, 因为DataSource也是注入到LocalContainer-EntityManagerFactoryBean中的, 所以也没有必要在persistence.xml文件中配置数据库信息了。 那么结论就是, persistence.xml文件完全没有必要存在
    了! 你尽可以将其删除, 让
    LocalContainerEntityManagerFactoryBean来处理这些事情。
    从
    JNDI获取实体管理器工厂
    还有一件需要注意的事项, 如果将
    Spring应用程序部署在应用服务器中, EntityManagerFactory可能已经创建好了并且位于JNDI中等待查询使用。 在这种情况下, 可以使用Spring jee命名空间下的<jee:jndi-lookup>元素来获取对EntityManagerFactory的引用:
    我们也可以使用如下的Java配置来获取EntityManagerFactory  
    尽管这种方法没有返回EntityManagerFactory, 但是它的结果就是一个EntityManagerFactory bean。 这是因为它所返回的JndiObjectFactoryBean是FactoryBean接口的实现, 它能够创建EntityManagerFactory。
    不管你采用何种方式得到
    EntityManagerFactory, 一旦得到这样的对象, 接下来就可以编写Repository了。  
    2、编写基于JPA的Repository  
    Spring对JPA集成也提供了JpaTemplate模板以及对应的支持类JpaDaoSupport。 但是, 为了实现更纯粹的JPA方式, 基于模板的JPA已经被弃用了。 
    纯粹的JPA方式远胜于基于模板的JPA, 所以在本节中我们将会重点关注如何构建不依赖Spring的JPA Repository。 如下程序清单中的JpaSpitterRepository展现了如何开发不使用Spring JpaTemplate的JPA Repository。  
    需要注意的是EntityManagerFactory属性, 它使用了@PersistenceUnit注解, 因此, Spring会将EntityManagerFactory注入到Repository之中。 有了EntityManagerFactory之后, JpaSpitterRepository的方法就能使用它来创建EntityManager了, 然后EntityManager可以针对数据库执行操作。
    在
    JpaSpitterRepository中, 唯一的问题在于每个方法都会调用createEntityManager()。 除了引入易出错的重复代码以外, 这还意味着每次调用Repository的方法时, 都会创建一个新的EntityManager。 这种复杂性源于事务。 如果我们能够预先准备好EntityManager, 那会不会更加方便呢?
    这里的问题在于
    EntityManager并不是线程安全的, 一般来讲并不适合注入到像Repository这样共享的单例bean中。 但是, 这并不意味着我们没有办法要求注入EntityManager。 如下的程序清单展现了如何借助@PersistentContext注解为JpaSpitterRepository设置EntityManager。  
    在这个新版本的JpaSpitterRepository中, 直接设置了EntityManager, 在每个方法中就没有必要再通过EntityManagerFactory创建EntityManager了。 这种方式非常便利, 但是你可能会担心注入的EntityManager会有线程安全性的问题。
    @PersistenceContext并不会真正注入EntityManager,它没有将真正的EntityManager设置给Repository, 而是给了它一个EntityManager的代理。 真正的EntityManager是与当前事务相关联的那一个,如果不存在这样的EntityManager的话, 就会创建一个新的。 这样就以线程安全的方式使用实体管理器。
    @PersistenceUnit和@PersistenceContext是JPA规范提供的注解。 为了让Spring理解这些注解, 并注入EntityManager Factory或EntityManager, 我们必须要配置Spring的PersistenceAnnotationBeanPostProcessor。 
    如果你已经使用了<context:annotation-config>或<context:component-scan>, 就不用配置,元素会自动配置PersistenceAnnotationBeanPostProcessor bean。 

    JpaSpitterRepository使用了@Repository和@Transactional注解。 @Transactional表示Repository中的持久化方法是在事务上下文中执行的。对于@Repository注解, 它的作用与开发Hibernate上下文Session版本的Repository时是一致的。 由于没有使用模板类来处理异常, 所以我们需要为Repository添加@Repository注解, 这样PersistenceExceptionTranslationPostProcessor就会知道要将这个bean产生的异常转换成Spring的统一数据访问异常。
    既然提到了
    PersistenceExceptionTranslationPostProcessor, 要记住的是我们需要将其作为一个bean装配到Spring中, 不管对于JPA还是Hibernate, 异常转换都不是强制要求的。 如果你希望在Repository中抛出特定的JPA或Hibernate异常, 只需将PersistenceExceptionTranslationPostProcessor省略掉即可, 这样原来的异常就会正常地处理。 但是, 如果使用了Spring的
    异常转换, 你会将所有的数据访问异常置于
    Spring的体系之下, 这样以后切换持久化机制的话会更容易。  

    3、借助Spring Data实现自动化的JPA Repository  
    之前的示例中,它们依然还会直接与EntityManager交互来查询数据库。 还有模板的模式元素,比如addSpitter()方法:

    在任何具有一定规模的应用中, 几乎完全相同的方式多次编写这种方法。 实际除了所持久化的
    Spitter对象不同以外, 其他还是一致,JpaSpitterRepository中的其他方法也没有什么太大的创造性。 Repository中的方法都是很通用的。
    可以用一种方法来实现不用再写这些通用的方法,直接处理持久化对象,Spring Data JPA能够终结这种方式。 我们不再需要一遍遍地编写相同的Repository实现, Spring Data能够让我们只编写Repository接口就可以,根本就不再需要实现类了  

    /**

    * spring data jpa让我们解脱了DAO层的操作,基本上所有CRUD都可以依赖于它来实现,需要实现JpaRepository接口

    *

    * Repository: 最顶层的接口,是一个空接口,目的是为了统一所有的Repository的类型,且能让组件扫描时自动识别

    * CrudRepository: Repository的子接口,提供CRUD 的功能。

    * PagingAndSortingRepository:CrudRepository的子接口, 添加分页排序。

    * JpaRepository: PagingAndSortingRepository的子接口,增加批量操作等。

    * JpaSpecificationExecutor: 用来做复杂查询的接口。

    * 一般使用JpaRepository这个接口做查询即可.这个接口拥有如下方法:

    * delete删除或批量删除

    * findOne查找单个

    * findAll查找所有

    * save保存单个或批量保存

    * saveAndFlush保存并刷新到数据库

    */

    @Repository

    public interface SpitterRepository extends JpaRepository<Spitter, Long> {

    }

    编写Spring Data JPA Repository的关键在于要从一组接口中挑选一个进行扩展。 这里, SpitterRepository扩展了Spring Data JPA的JpaRepository 。 通过这种方式, JpaRepository进行了参数化, 所以它就能知道这是一个用来持久化Spitter对象的Repository, 并且Spitter的ID类型为Long。 另外, 它还会继承18个执行持久化操作的通用方法, 如保存Spitter、删除Spitter以及根据ID查询Spitter。
    其实, 我们根本不需要编写SpitterRepository的任何实现类, 相反, 我们让Spring Data来为我们做这件事请。 我们所需要做的就是对它提出要求。
    为了要求
    Spring Data创建SpitterRepository的实现, 我们需要在Spring配置中添加一个元素。 如下的程序清单展现了在XML配置中启用Spring Data JPA所需要添加的内容:  
    <jpa:repositories>元素掌握了Spring Data JPA的所有魔力。 就像<context:component-scan>元素一样, <jpa:repositories>元素也需要指定一个要进行扫描的base-package。 不过, <context:component-scan>会扫描包(及其子包) 来查找带有@Component注解的类, 而<jpa:repositories>会扫描它的基础包来查找扩展自Spring Data JPA Repository接口的所有接口。 如果发现了扩展自Repository的接口, 它会自动生成(在应用启动的时候) 这个接口的实现。
    如果要使用
    Java配置的话, 那就不需要使用<jpa:repositories>元素了, 而是要在Java配置类上添加@EnableJpaRepositories注解。 如下就是一个Java配置类, 它使用了@EnableJpaRepositories注解, 并且会扫描com.habuma.spittr.db包:  
    让我们回到SpitterRepository接口, 它扩展自JpaRepository, 而JpaRepository又扩展自Repository标记接口(虽然是间接的) 。 因此, SpitterRepository就传递性地扩展了Repository接口, 也就是Repository扫描时所要查找的接口。 当Spring Data找到它后, 就会创建SpitterRepository的实现类, 其中包含了继承自JpaRepository、 PagingAndSortingRepository和CrudRepository的18个方法。
    很重要的一点在于
    Repository的实现类是在应用启动的时候生成的, 也就是Spring的应用上下文创建的时候。 它并不是在构建时通过代码生成技术产生的, 也不是接口方法调用时才创建的。
    Spring Data JPA很棒的一点在于它能为Spitter对象提供18个便利的方法来进行通用的JPA操作, 而无需你编写任何持久化代码。 但是, 如果你的需求超过了它所提供的这18个方法的话, 该怎么办呢?  Spring Data JPA提供了几种方式来为Repository添加自定义的方法。 让我们看一下如何为Spring Data JPA编写自定义的查询方法。  
    定义查询方法  
    Spring Data定义了一组小型的领域特定语言(domain-specific language , DSL) , 在这里, 持久化的细节都是通过Repository方法的签名来描述的。  
    比如自定义一个findByName(String name)方法,我们只需要定义就行,Spring Data JPA在容器启动时候会自动去生成。
    Repository方法是由一个动词、 一个可选的主题(Subject) 、 关键词By以及一个断言所组成。 在findByUsername()这个样例中, 动词是find, 断言是Username, 主题并没有指定, 暗含
    的主题是
    Spitter。
    作为编写
    Repository方法名称的样例, 我们参照名为readSpitterByFirstname-OrLastname()的方法, 看一下方法中的各个部分是如何映射的。

    我们可以看到, 这里的动词是
    read, 与之前样例中的find有所差别。 Spring Data允许在方法名中使用四种动词: get、 read、 find和count。 其中, 动词get、 read和find是同义的, 这三个动词对应的Repository方法都会查询数据并返回对象。 而动词count则会返回匹配对象的数量, 而不是对象本身。  
    在断言中, 会有一个或多个限制结果的条件。 每个条件必须引用一个属性, 并且还可以指定一种比较操作。 如果省略比较操作符的话, 那么这暗指是一种相等比较操作。 不过, 我们也可以选择其他的比较操作, 包括如下的种类:
    IsAfter、 After、 IsGreaterThan、 GreaterThan
    IsGreaterThanEqual
    、 GreaterThanEqual
    IsBefore
    、 Before、 IsLessThan、 LessThan
    IsLessThanEqual
    、 LessThanEqual
    IsBetween
    、 Between
    IsNull
    、 Null
    IsNotNull
    、 NotNull
    IsIn
    、 In
    IsNotIn
    、 NotIn
    IsStartingWith
    、 StartingWith、 StartsWith
    IsEndingWith
    、 EndingWith、 EndsWith
    IsContaining
    、 Containing、 Contains
    IsLike
    、 Like
    IsNotLike
    、 NotLike
    IsTrue
    、 True
    IsFalse
    、 False
    Is
    、 Equals
    IsNot
    、 Not  

    @Repository

    public interface SpitterRepository extends JpaRepository<Spitter, Long> {

    /**

    * Spring-data-jpa的新特性,通过解析方法名创建查询。更多解析说明如下:

    * And => 等价于 SQL 中的 and 关键字 例如:findByUsernameAndPassword(String user, Striang pwd);

    * Or => 等价于 SQL 中的 or 关键字,例如:findByUsernameOrAddress(String user, String addr);

    * Between => 等价于 SQL 中的 between 关键字,例如:SalaryBetween(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 userList) ,方法的参数可以是 Collection 类型,也可以是数组或者不定长参数;

    * NotIn => 等价于 SQL 中的 "not in",例如: findByUsernameNotIn(Collection userList) ,方法的参数可以是 Collection 类型,也可以是数组或者不定长参数;

    * 创建一个按单字段排序的Sort对象: new Sort(Sort.Direction.DESC, "description").and(new Sort(Sort.Direction.ASC, "id"))

    */

    Spitter findByName(String name);

    Spitter findById(Long id);

    List<Spitter> findByNameLike(String name);

    //使用JPQL

    @Query("from User u where u.name=:name")

    Spitter findSpitter(@Param("name") String name);

    }

    声明自定义查询  
    如果一个方法并不符合Spring Data的方法命名约定。 当Spring Data试图生成这个方法的实现时, 无法将方法名的内容与Spitter元模型进行匹配, 因此会抛出异常。可以使用@Query注解, 为Spring Data提供要执行的查询。 

    @Query("from User u where u.name=:name")

    Spitter findSpitter(@Param("name") String name); 

    混合自定义的功能  
    有些时候, 我们需要Repository所提供的功能是无法用Spring Data的方法命名约定来描述的, 甚至无法用@Query注解设置查询来实现。 
    那就必须要在一个比Spring Data JPA更低的层级上使用JPA。而且没有必要完全放弃Spring Data JPA。 我们只需在必须使用较低层级JPA的方法上, 才使用这种传统的方式即可, 而对于Spring
    Data JPA
    知道该如何处理的功能, 我们依然可以通过它来实现。  
    当Spring Data JPA为Repository接口生成实现的时候, 它还会查找名字与接口相同, 并且添加了Impl后缀的一个类。 如果这个类存在的话, Spring Data JPA将会把它的方法与Spring Data JPA所生成的方法合并在一起。 对于SpitterRepository接口而言, 要查找的类名为SpitterRepositoryImpl。
    假如需要实现一个使用Spring Data JPA的方法命名约定或使用@Query均没有办法声明这样的方法。 最为可行的方案是使用如下的eliteSweep()方法。  
    注意, SpitterRepositoryImpl并没有实现SpitterRepository接口。 Spring Data JPA负责实现这个接口。 SpitterRepositoryImpl(将它与Spring Data的Repository关联起来的是它的名字) 实现了SpitterSweeper接口。
    我们还需要确保eliteSweep()方法会被声明在SpitterRepository接口中。 要实现这一点, 避免代码重复的简单方式就是修改SpitterRepository, 让它扩展SpitterSweeper:  
    如前所述, Spring Data JPA将实现类与接口关联起来是基于接口的名称。 但是, Impl后缀只是默认的做法, 如果你想使用其他后缀的话, 只需在配置@EnableJpa-Repositories的时候, 设置repositoryImplementationPostfix属性即可:  
    如果在XML中使用<jpa:repositories>元素来配置Spring Data JPA的话, 我们可以借助repository-impl-postfix属性指定后缀:  
    我们将后缀设置成了Helper, Spring Data JPA将会查找名为SpitterRepository-Helper的类, 用它来匹配SpitterRepository接口。  

      

    三、Spring后端之1-Spring JDBC

    发表于 2017-09-22 | 分类于 Spring实战4版
    Spring的数据访问哲学 
    Spring的目标之一就是允许我们在开发应用程序时, 能够遵循面向对象(OO) 原则中的“针对接口编程”。  
    在运用开发中,为了避免持久化的逻辑分散到应用的各个组件中, 最好将数据访问的功能放到一个或多个专注于此项任务的组件中。 这样的组件通常称为数据访问对象(data access object, DAO) 或Repository。为了避免应用与特定的数据访问策略耦合在一起, 编写良好的Repository应该以接口的方式暴露功能。
    这样做会有几个好处。 第一, 它使得服务对象易于测试, 因为它们不再与特定的数据访问实现绑定在一起。 实际上, 你可以为这些数据访问接口创建mock实现, 这样无需连接数据库就能测试服务对象, 而且会显著提升单元测试的效率并排除因数据不一致所造成的测试失败。
    此外, 数据访问层是以持久化技术无关的方式来进行访问的。 持久化方式的选择独立于
    Repository, 同时只有数据访问相关的方法才通过接口进行暴露。 这可以实现灵活的设计, 并且切换持久化框架对应用程序其他部分所带来的影响最小。 如果将数据访问层的实现细节渗透到应用程序的其他部分中, 那么整个应用程序将与数据访问层耦合在一起, 从而导致僵化的设计。
    接口是实现松耦合代码的关键, 并且应将其用于应用程序的各个层, 而不仅仅是持久化层。 还要说明一点, 尽管Spring鼓励使用接口, 但这并不是强制的——你可以使用Spring将bean(DAO或其他类型) 直接装配到另一个bean的某个属性中, 而不需要一定通过接口注入。为了将数据访问层与应用程序的其他部分隔离开来, Spring采用的方式之一就是提供统一的异常体系, 这个异常体系用在了它支持的所有持久化方案中。

    Spring的数据访问异常体系  

    JDBC中的SQLException,如果不强制捕获SQLException的话, JDBC就不能使用。 SQLException表示在尝试访问数据库的时出现了问题, 但是这个异常却没有告诉你哪里出错了以及如何进行处理。
    可能导致抛出SQLException的常见问题包括:
    应用程序无法连接数据库;应用程序无法连接数据库;
    要执行的查询存在语法错误;
    查询中所使用的表和/或列不存在;
    试图插入或更新的数据违反了数据库约束。
    SQLException的问题在于捕获到它的时候该如何处理。 事实上, 能够触发SQLException的问题通常是不能在catch代码块中解决的。 大多数抛出SQLException的情况表明发生了致命性错误。但是如果查询时出现了错误, 那在运行时基本上也是无能为力。如果无法从SQLException中恢复, 那为什么我们还要强制捕获它呢?
    即使对某些SQLException有处理方案, 我们还是要捕获SQLException并查看其属性才能获知问题根源的更多信息。 这是因为SQLException被视为处理数据访问所有问题的通用异常。 对于所有的数据访问问题都会抛出SQLException, 而不是对每种可能的问题都会有不同的异常类型。
    一些持久化框架提供了相对丰富的异常体系。 例如, Hibernate提供了二十个左右的异常, 分别对应于特定的数据访问问题。 这样就可以针对想处理的异常编写catch代码块。
    Hibernate的异常是其本身所特有的。 正如前面所言, 我们想将特定的持久化机制独立于数据访问层。 如果抛出了Hibernate所特有的异常, 那我们对Hibernate的使用将会渗透到应用程序的其他部分。 如果不这样做的话, 我们就得捕获持久化平台的异常, 然后将其作为平台无关的异常再次抛出。一方面, JDBC的异常体系过于简单了, 它算不上一个体系。 另一方面, Hibernate的异常体系是其本身所独有的。 我们需要的数据访问异常要具有描述性而且又与特定的持久化框架无关。
    Spring所提供的平台无关的持久化异常
    Spring JDBC提供的数据访问异常体系解决了以上的两个问题。 不同于JDBC, Spring提供了多个数据访问异常, 分别描述了它们抛出时所对应的问题。 如下描述了Spring的部分数据访问异常以及JDBC所提供的异常。
    JDBC的异常 Spring的数据访问异常
    BatchUpdateException
    DataTruncation
    SQLException
    SQLWarning
    BadSqlGrammarException
    CannotAcquireLockException
    CannotSerializeTransactionException
    CannotGetJdbcConnectionException
    CleanupFailureDataAccessException
    ConcurrencyFailureException
    DataAccessException
    DataAccessResourceFailureException
    DataIntegrityViolationException
    DataRetrievalFailureException
    DataSourceLookupApiUsageException
    DeadlockLoserDataAccessException
    DuplicateKeyException
    EmptyResultDataAccessException
    IncorrectResultSizeDataAccessException
    IncorrectUpdateSemanticsDataAccessException
    InvalidDataAccessApiUsageException
    InvalidDataAccessResourceUsageException
    InvalidResultSetAccessException
    JdbcUpdateAffectedIncorrectNumberOfRowsException
    LbRetrievalFailureException
    BatchUpdateException
    DataTruncation
    SQLException
    SQLWarning
    NonTransientDataAccessResourceException
    OptimisticLockingFailureException
    PermissionDeniedDataAccessException
    PessimisticLockingFailureException
    QueryTimeoutException
    RecoverableDataAccessException
    SQLWarningException
    SqlXmlFeatureNotImplementedException
    TransientDataAccessException
    TransientDataAccessResourceException
    TypeMismatchDataAccessException
    UncategorizedDataAccessException
    UncategorizedSQLException
    Spring的异常体系比JDBC简单的SQLException丰富得多, 但它并没有与特定的持久化方式相关联。 可以使用Spring抛出一致的异常, 而不用关心所选择的持久化方案。 这有助于我们将所选择持久化机制与数据访问层隔离开来。
    看! 不用写
    catch代码块
    表
    中没有体现出来的一点就是这些异常都继承自DataAccessException。 DataAccessException的特殊之处在于它是一个非检查型异常。 换句话说, 没有必要捕获Spring所抛出的数据访问异常(当然, 如果你想捕获的话也是完全可以的) 。
    DataAccessException只是Sping处理检查型异常和非检查型异常哲学的一个范例。 Spring认为触发异常的很多问题是不能在catch代码块中修复的。 Spring使用了非检查型异常, 而不是强制开发人员编写catch代码块(里面经常是空的) 。 

    数据访问模板化  
    Spring在数据访问中使用了模板方法模式。 不管我们使用什么样的技术, 都需要一些特定的数据访问步骤。 例如, 我们都需要获取一个到数据存储的连接并在处理完成后释放资源。 这都是在数据访问处理过程中的固定步骤, 但是每种数据访问方法又会有些不同, 我们会查询不同的对象或以不同的方式更新数据, 这都是数据访问过程中变化的部分。
    Spring将数据访问过程中固定的和可变的部分明确划分为两个不同的类: 模板(template) 和回调(callback) 。 模板管理过程中固定的部分,而回调处理自定义的数据访问代码。 下图展现了这两个类的职责。  
    Spring的模板类处理数据访问的固定部分——事务控制、 管理资源以及处理异常。 同时, 应用程序相关的数据访问——语句、 绑定参数以及整理结果集——在回调的实现中处理。 事实证明, 这是一个优雅的架构, 因为你只需关心自己的数据访问逻辑即可。
    针对不同的持久化平台,
    Spring提供了多个可选的模板。 如果直接使用JDBC, 那你可以选择JdbcTemplate。 如果你希望使用对象关系映射框架, 那HibernateTemplate或JpaTemplate可能会更适合你。 
    模板类(org.springframework.*) 用 途
    jca.cci.core.CciTemplate JCACCI连接
    jdbc.core.JdbcTemplate JDBC连接
    jdbc.core.namedparam.NamedParameterJdbcTemplate 支持命名参数的JDBC连接
    jdbc.core.simple.SimpleJdbcTemplate 通过Java 5简化后的JDBC连接(Spring 3.1中已经废弃)
    orm.hibernate3.HibernateTemplate Hibernate 3.x以上的Session
    orm.ibatis.SqlMapClientTemplate iBATIS SqlMap客户端
    orm.jdo.JdoTemplate Java数据对象(Java Data Object) 实现
    orm.jpa.JpaTemplate Java持久化API的实体管理器

    配置数据源

    无论选择Spring的哪种数据访问方式, 你都需要配置一个数据源的引用。 Spring提供了在Spring上下文中配置数据源bean的多种方式, 包括:
    通过
    JDBC驱动程序定义的数据源;
    通过
    JNDI查找的数据源;
    连接池的数据源。
    对于即将发布到生产环境中的应用程序, 我建议使用从连接池获取连接的数据源。 如果可能的话, 我倾向于通过应用服务器的
    JNDI来获取数据源。 首先看一下如何配置Spring从JNDI中获取数据源。  
    使用JNDI数据源  
    Spring应用程序经常部署在Java EE应用服务器中, 如WebSphere、 JBoss或甚至像Tomcat这样的Web容器中。 这些服务器允许你配置通过JNDI获取数据源。 这种配置的好处在于数据源完全可以在应用程序之外进行管理, 这样应用程序只需在访问数据库的时候查找数据源就可以了。 另外, 在应用服务器中管理的数据源通常以池的方式组织, 从而具备更好的性能, 并且还支持系统管理员对其进行热切换。
    利用
    Spring, 我们可以像使用Spring bean那样配置JNDI中数据源的引用并将其装配到需要的类中。 位于jee命名空间下的<jee:jndilookup>元素可以用于检索JNDI中的任何对象(包括数据源) 并将其作为Spring的bean。 例如, 如果应用程序的数据源配置在JNDI中, 我们
    可以使用
    <jee:jndi-lookup>元素将其装配到Spring中, 如下所示:  
    <jee:jndi-lookup id="datasource" jndi=name"/jdbc/SpitterDS" resource-ref="true"/>
    其中jndi-name属性用于指定JNDI中资源的名称。 如果只设置了jndi-name属性, 那么就会根据指定的名称查找数据源。 但是, 如果应用程序运行在Java应用服务器中, 你需要将resource-ref属性设置为true, 这样给定的jndi-name将会自动添加“java:comp/env/”前缀。
    如果想使用
    Java配置的话, 那我们可以借助JndiObjectFactoryBean从JNDI中查找DataSource:  
    使用数据源连接池  
    连接池中的大多数都能配置为Spring的数据源, 在一定程度上与Spring自带的DriverManagerDataSource或SingleConnectionDataSource很类似(我们稍后会对其进行介绍) 。 例如, 如下就是配置DBCPBasicDataSource的方式:  
    前四个属性是配置BasicDataSource所必需的。 属性driverClassName指定了JDBC驱动类的全限定类名。 在这里我们配置的是H2数据库的数据源。 属性url用于设置数据库的JDBC URL。 最后, username和password用于在连接数据库时进行认证。
    以上四个基本属性定义了
    BasicDataSource的连接信息。 除此以外, 还有多个配置数据源连接池的属性。 表列出了DBCPBasicDataSource最有用的一些池配置属性:  
    池配置属性 所指定的内容
    initialSize 池启动时创建的连接数量
    maxActive 同一时间可从池中分配的最多连接数。 如果设置为0, 表示无限制
    maxIdle 池里不会被释放的最多空闲连接数。 如果设置为0, 表示无限制
    maxOpenPreparedStatements 在同一时间能够从语句池中分配的预处理语句(prepared statement) 的最大数量。 如果设置为0, 表示无限制
    maxWait 在抛出异常之前, 池等待连接回收的最大时间(当没有可用连接时) 。 如果设置为-1, 表示无限等待
    minEvictableIdleTimeMillis 连接在池中保持空闲而不被回收的最大时间
    minIdle 在不创建新连接的情况下, 池中保持空闲的最小连接数
    poolPreparedStatements 是否对预处理语句(prepared statement) 进行池管理(布尔值

    基于JDBC驱动的数据源  
    在Spring中, 通过JDBC驱动定义数据源是最简单的配置方式。 Spring提供了三个这样的数据源类(均位于org.springframework.jdbc.datasource包中) 供选择:
    DriverManagerDataSource: 在每个连接请求时都会返回一个新建的连接。 与DBCP的BasicDataSource不同,由DriverManagerDataSource提供的连接并没有进行池化管理;
    SimpleDriverDataSource: 与DriverManagerDataSource的工作方式类似, 但是它直接使用JDBC驱动, 来解决在特定环境下的类加载问题, 这样的环境包括OSGi容器;
    SingleConnectionDataSource: 在每个连接请求时都会返回同一个的连接。 尽管SingleConnectionDataSource不是严格意义上的连接池数据源, 但是你可以将其视为只有一个连接的池。
    以上这些数据源的配置与
    DBCPBasicDataSource的配置类似。 例如, 如下就是配置DriverManagerDataSource的方法:  

    使用profile选择数据源  
    配置多种数据源,选择使用一种
    通过使用profile功能, 会在运行时选择数据源, 这取决于哪一个profile处于激活状态。当且仅当developmentprofile处于激活状态时, 会创建嵌入式数据库, 当且仅当qa profile处于激活状态时, 会创建DBCP BasicDataSource, 当且仅当productionprofile处于激活状态时, 会从JNDI获取数据源。
    为了内容的完整性, 如下的程序清单展现了如何使用
    Spring XML代替Java配置, 实现相同的profile配置。  
    在Spring中使用JDBC  

    持久化技术有很多种, 而Hibernate、 iBATIS和JPA只是其中的几种而已。 尽管如此, 还是有很多的应用程序使用最古老的方式将Java对象保存到数据库中:  这种久经考验并证明行之有效的持久化方法就是古老的JDBC。  
    使用JDBC模板  
    Spring为JDBC提供了三个模板类供选择:
    JdbcTemplate: 最基本的Spring JDBC模板, 这个模板支持简单的JDBC数据库访问功能以及基于索引参数的查询;
    NamedParameterJdbcTemplate: 使用该模板类执行查询时可以将值以命名参数的形式绑定到SQL中, 而不是使用简单的索引参数;
    SimpleJdbcTemplate: 该模板类利用Java 5的一些特性如自动装箱、 泛型以及可变参数列表来简化JDBC模板的使用。  
    以前, 在选择哪一个JDBC模板的时候, 我们需要仔细权衡。 但是从Spring 3.1开始, 做这个决定变得容易多了。 SimpleJdbcTemplate已经被废弃了, 其Java 5的特性被转移到了JdbcTemplate中, 并且只有在你需要使用命名参数的时候, 才需要使用NamedParameterJdbcTemplate。 这样的话, 对于大多数的JDBC任务来说, JdbcTemplate就是最好的可选方案。

    使用JdbcTemplate来插入数据
    为了让
    JdbcTemplate正常工作, 只需要为其设置DataSource就可以了, 这使得在Spring中配置JdbcTemplate非常容易, 如下面的@Bean方法所示:  
    DataSource是通过构造器参数注入进来的。 这里所引用的dataSourcebean可以是javax.sql.DataSource的任意实现 。 

    可以将jdbcTemplate装配到Repository中并使用它来访问数据库。  
    在这里, JdbcSpitterRepository类上使用了@Repository注解, 这表明它将会在组件扫描的时候自动创建。 它的构造器上使用了@Inject注解, 因此在创建的时候, 会自动获得一个JdbcOperations对象。 JdbcOperations是一个接口, 定义了JdbcTemplate所实现的操作。 通过注入JdbcOperations, 而不是具体的JdbcTemplate, 能够保证JdbcSpitterRepository通过JdbcOperations接口达到与JdbcTemplate保持松耦合。
    作为另外一种组件扫描和自动装配的方案, 我们可以将
    JdbcSpitterRepository显式声明为Spring中的bean, 如下所示:  
    在Repository中具备可用的JdbcTemplate后, 我们可以极大地简化程序清单10.4中的addSpitter()方法。 

    使用JdbcTemplate来读取数据  

    在JdbcTemplate中使用Java 8的Lambda表达式  

    使用命名参数  
    之前需要参数的顺序一致,通过命名参数方式,不需要一致,只要有对应的参数值就好了,把sql定义为如下:
    NamedParameterJdbcTemplate是一个特殊的JDBC模板类, 它支持使用命名参数。 在Spring中, NamedParameterJdbcTemplate的声明方式与常规的JdbcTemplate几乎完全相同,只需要在定义JdbcTemplate的时候替换为NamedParameterJdbcTemplate就可以。
    使用参数方式插入数据:








         

    二、Spring Web之9-Spring Security

    发表于 2017-09-21 | 分类于 Spring实战4版
    1、Spring Security简介
    Spring Security是为基于Spring的应用程序提供声明式安全保护的安全性框架。 Spring Security提供了完整的安全性解决方案, 它能够在Web请求级别和方法调用级别处理身份认证和授权。 因为基于Spring框架, 所以Spring Security充分利用了依赖注入(dependency injection, DI) 和面向切面的技术。
    Spring Security引入了一个全新的、 与安全性相关的XML命名空间。 这个新的命名空间连同注解和一些合理的默认设置, 将典型的安全性配置从几百行XML减少到十几行。 Spring Security 3.0融入了SpEL, 这进一步简化了安全性的配置。
    它的最新版本为3.2, Spring Security从两个角度来解决安全性问题。 它使用Servlet规范中的Filter保护Web请求并限制URL级别的访问。 Spring Security还能够使用Spring AOP保护方法调用——借助于对象代理和使用通知, 能够确保只有具备适当权限的用户才能访问安全保护的方法。
    理解Spring Security的模块  
    模 块 描 述
    ACL 支持通过访问控制列表(access control list, ACL) 为域对象提供安全性
    切面(Aspects) 一个很小的模块, 当使用Spring Security注解时, 会使用基于AspectJ的切面, 而不是使用标准的Spring AOP
    CAS客户端(CAS Client) 提供与Jasig的中心认证服务(Central Authentication Service, CAS) 进行集成的功能
    配置(Configuration) 包含通过XML和Java配置Spring Security的功能支持
    核心(Core) 提供Spring Security基本库
    加密(Cryptography) 提供了加密和密码编码的功能
    LDAP 支持基于LDAP进行认证
    OpenID 支持使用OpenID进行集中式认证
    Remoting 提供了对Spring Remoting的支持
    标签库(Tag Library) Spring Security的JSP标签库
    Web 提供了Spring Security基于Filter的Web安全性支持

    过滤Web请求
    Spring Security借助一系列Servlet Filter来提供各种安全性功能。 你可能会想, 这是否意味着我们需要在web.xml或WebApplicationInitializer中配置多个Filter呢? 实际上, 借助于Spring的小技巧, 我们只需配置一个Filter就可以了。
    DelegatingFilterProxy是一个特殊的Servlet Filter, 它本身所做的工作并不多。 只是将工作委托给一个javax.servlet.Filter实现类, 这个实现类作为一个<bean>注册在Spring应用的上下文中, 如图所示。
    DelegatingFilterProxy把Filter的处理逻辑委托给Spring应用

    上下文中所定义的一个代理Filter bean如果你喜欢在传统的web.xml中配置Servlet和Filter的话, 可以使用<filter>元素, 如下所示:
    在这里, 最重要的是<filter-name>设置成了springSecurityFilterChain。 这是因为我们马上就会将Spring Security配置在Web安全性之中, 这里会有一个名为springSecurityFilterChain的Filter bean, DelegatingFilterProxy会将过滤逻辑委托给它。如果你希望借助WebApplicationInitializer以Java的方式来配置Delegating-FilterProxy的话, 那么我们所需要做的就是创建一个扩展的新类:  
    AbstractSecurityWebApplicationInitializer实现了WebApplication-Initializer, 因此Spring会发现它, 并用它在Web容器中注册DelegatingFilterProxy。 可以重载它的appendFilters()或insertFilters()方法来注册自己选择的Filter,但是注册DelegatingFilterProxy的话, 并不需要重载任何方法。
    不管我们通过web.xml还是通过AbstractSecurityWebApplicationInitializer的子类来配置DelegatingFilterProxy, 它都会拦截发往应用中的请求, 并将请求委托给ID为springSecurityFilterChain bean。
    springSecurityFilterChain本身是另一个特殊的Filter, 它也被称为FilterChainProxy。 它可以链接任意一个或多个其他的Filter。Spring Security依赖一系列Servlet Filter来提供不同的安全特性。 几乎不需要知道这些细节, 不需要显式声明springSecurityFilterChain以及它所链接在一起的其他Filter。 当我们启用Web安全性的时候, 会自动创建这些Filter。

    启用Web安全性功能的最简单配置  
    @EnableWebSecurity注解将会启用Web安全功能。  Spring Security必须配置在一个实现了WebSecurityConfigurer的bean中, 或者扩展WebSecurityConfigurerAdapter。 在Spring应用上下文中, 任何实现了WebSecurityConfigurer的bean都可以用来配置Spring Security。
    @EnableWebSecurity可以启用任意Web应用的安全性功能, 不过, 如果你的应用碰巧是使用Spring MVC开发的, 那么就应该考虑使用@EnableWebMvcSecurity替代它  
    @EnableWebMvcSecurity注解还配置了一个Spring MVC参数解析解析器(argument resolver) , 这样的话处理器方法就能够通过带有@AuthenticationPrincipal注解的参数获得认证用户的principal(或username) 。 它同时还配置了一个bean, 在使用Spring表单绑定标签库来定义表单时, 这个bean会自动添加一个隐藏的跨站请求伪造(cross-site request forgery, CSRF) token输入域。
    可以通过重载WebSecurityConfigurerAdapter的三个configure()方法来配置Web安全性, 这个过程中会使用传递进来的参数设置行为。 表描述了这三个方法。  
    方 法 描 述
    configure(WebSecurity) 通过重载, 配置Spring Security的Filter链
    configure(HttpSecurity) 通过重载, 配置如何通过拦截器保护请求
    configure(AuthenticationManagerBuilder) 通过重载, 配置user-detail服务
    如果没有重写上述三个configure()方法中的任何一个, 这就说明了应用现在是被锁定的。默认的configure(HttpSecurity)实际上等同于如下所示:  
    这个简单的默认配置指定了该如何保护HTTP请求, 以及客户端认证用户的方案。 
    通过调用authorizeRequests()和anyRequest().authenticated()要求所有HTTP请求都要进行认证。 它也配置Spring Security支持基于表单的登录以及HTTP Basic方式的认证。
    同时, 因为我们没有重载
    configure(AuthenticationManagerBuilder)方法, 所以没有用户存储支撑认证过程。 没有用户存储, 实际上就等于没有用户。 在这里所有的请求都需要认证, 但是没有人能够登录成功。
    为了让
    Spring Security满足我们应用的需求, 还需要再添加一点配置。
    配置用户存储;
    指定哪些请求需要认证, 哪些请求不需要认证, 以及所需要的权限;
    提供一个自定义的登录页面, 替代原来简单的默认登录页。

    除了
    Spring Security的这些功能, 我们可能还希望基于安全限制, 有选择性地在Web视图上显示特定的内容。

    2、选择查询用户详细信息的服务  
    需要的是用户存储, 也就是用户名、 密码以及其他信息存储的地方, 在进行认证决策的时候, 会对其进行检索。Spring Security非常灵活, 能够基于各种数据存储来认证用户。 它内置了多种常见的用户存储场景, 如内存、 关系型数据库以及LDAP。 但我们也可以编写并插入自定义的用户存储实现。借助Spring Security的Java配置, 我们能够很容易地配置一个或多个数据存储方案。 
    使用基于内存的用户存储  
    安全配置类扩展了WebSecurityConfigurerAdapter, 配置用户存储的最简单方式是重载configure()方法, 并以AuthenticationManagerBuilder作为传入参数。 AuthenticationManagerBuilder有多个方法可以用来配置Spring Security对认证的支持。 通过inMemoryAuthentication()方法, 我们可以启用、 配置并任意填充基于内存的用户存储。
    通过调用inMemoryAuthentication()启用内存用户存储。 再调用withUser()方法为内存用户存储添加新的用户, 这个方法的参数是username。 withUser()方法返回的是UserDetailsManagerConfigurer.UserDetailsBuilder, 这个对象提供了多个进一步配置用户的方法, 包括设置用户密码的password()方法以及为给定用户授予一个或多个角色权限的roles()方法。
    在程序清单中, 添加了两个用户, “user”和“admin”, 密码均为“password”。 “user”用户具有USER角色, 而“admin”用户具有ADMIN和USER两个角色。 and()方法能够将多个用户的配置连接起来。
    roles()方法是authorities()方法的简写形式。 roles()方法所给定的值都会添加一个“ROLE_”前缀, 并将其作为权限授予给用户。 
    配置用户详细信息的方法  
    方 法 描 述
    accountExpired(boolean) 定义账号是否已经过期
    accountLocked(boolean) 定义账号是否已经锁定
    and() 用来连接配置
    authorities(GrantedAuthority...) 授予某个用户一项或多项权限
    authorities(List<? extends GrantedAuthority>) 授予某个用户一项或多项权限
    authorities(String...) 授予某个用户一项或多项权限
    credentialsExpired(boolean) 定义凭证是否已经过期
    disabled(boolean) 定义账号是否已被禁用
    password(String) 定义用户的密码
    roles(String...) 授予某个用户一项或多项角色
    对于调试和开发人员测试来讲, 基于内存的用户存储是很有用的, 但是对于生产级别的应用来讲,通常最好将用户数据保存在某种类型的数据库之中。

    基于数据库表进行认证  
    用户数据通常会存储在关系型数据库中, 并通过JDBC进行访问。 为了配置Spring Security使用以JDBC为支撑的用户存储, 我们可以使用jdbcAuthentication()方法, 所需的最少配置如下所示:  
    我们必须要配置的只是一个DataSource, DataSource是通过自动装配得到的。  

    重写默认的用户查询功能  
    尽管默认的最少配置能够让一切运转起来, 但是它对我们的数据库模式有一些要求。 它预期存在某些存储用户数据的表。 更具体来说, 下面的代码片段来源于Spring Security内部, 这块代码展现了当查找用户信息时所执行的SQL查询语句:  
    如果你能够在数据库中定义和填充满足这些查询的表, 那么基本上就不需要你再做什么额外的事情了。 但是, 也有可能你的数据库与上面所述并不一致, 那么你就会希望在查询上有更多的控制权。 如果是这样的话, 我们可以按照如下的方式配置自己的查询:  
    将默认的SQL查询替换为自定义的设计时,要遵循查询的基本协议。 所有查询都将用户名作为唯一的参数。 认证查询会选取用户名、 密码以及启用状态信息。 权限查询会选取零行或多行包含该用户名及其权限信息的数据。 群组权限查询会选取零行或多行数据, 每行数据中都会包含群组ID、 群组名称以及权限  。
    使用转码后的密码
    借助
    passwordEncoder()方法指定一个密码转码器(encoder) :  
    passwordEncoder()方法可以接受Spring Security中PasswordEncoder接口的任意实现。 Spring Security的加密模块包括了三个这样的实现: BCryptPasswordEncoder、 NoOpPasswordEncoder和StandardPasswordEncoder。
    但是如果内置的实现无法满足需求时, 你可以提供自定义的实现。
    基于LDAP进行认证  

    配置自定义的用户服务  
    假设我们需要认证的用户存储在非关系型数据库中,我们需要提供一个自定义的UserDetailsService接口实现。  
    实现loadUserByUsername()方法, 根据给定的用户名来查找用户。 
    为了使用SpitterUserService来认证用户, 我们可以通过userDetailsService()方法将其设置到安全配置中:  
    userDetailsService()方法(类似于jdbcAuthentication()、 ldapAuthentication以及inMemoryAuthentication()) 会配置一个用户存储。 这里所使用的不是Spring所提供的用户存储, 而是使用UserDetailsService的实现。
    另外一种方案就是修改
    Spitter, 让其实现UserDetails。 这样的话, loadUserByUsername()就能直接返回Spitter对象了, 而不必再将它的值复制到User对象中。  

    3、拦截请求  
    每个请求进行细粒度安全性控制的关键在于重载configure(HttpSecurity)方法。 如下的代码片段展现了重载的configure(HttpSecurity)方法, 它为不同的URL路径有选择地应用安全性:  
    configure()方法中得到的HttpSecurity对象可以在多个方面配置HTTP的安全性。 首先调用authorizeRequests(),然后调用该方法所返回的对象的方法来配置请求级别的安全性细节。 其中, 第一次调用antMatchers()指定了对“/spitters/me”路径的请求需要进行认证。 
    第二次调用antMatchers()更为具体, 说明对“/spittles”路径的HTTP POST请求必须要经过认证。 最后对anyRequests()的调用中, 说明其他所有的请求都是允许的, 不需要认证和任何的权限。
    antMatchers()方法中设定的路径支持Ant风格的通配符。 在这里我们并没有这样使用, 但是也可以使用通配符来指定路径, 如下所示:  

    除了路径选择, 我们还通过authenticated()和permitAll()来定义该如何保护路径。 authenticated()要求在执行该请求时, 必须已经登录了应用。 如果用户没有认证的话, Spring Security的Filter将会捕获该请求, 并将用户重定向到应用的登录页面。 同时, permitAll()方法允许请求没有任何的安全限制。
    除了
    authenticated()和permitAll()以外, 还有其他的一些方法能够用来定义该如何保护请求。  
    方 法 能够做什么
    access(String) 如果给定的SpEL表达式计算结果为true, 就允许访问
    anonymous() 允许匿名用户访问
    authenticated() 允许认证过的用户访问
    denyAll() 无条件拒绝所有访问
    fullyAuthenticated() 如果用户是完整认证的话(不是通过Remember-me功能认证的) , 就允许访问
    hasAnyAuthority(String...) 如果用户具备给定权限中的某一个的话, 就允许访问
    hasAnyRole(String...) 如果用户具备给定角色中的某一个的话, 就允许访问
    hasAuthority(String) 如果用户具备给定权限的话, 就允许访问
    hasIpAddress(String) 如果请求来自给定IP地址的话, 就允许访问
    hasRole(String) 如果用户具备给定角色的话, 就允许访问
    not() 对其他访问方法的结果求反
    permitAll() 无条件允许访问
    rememberMe() 如果用户是通过Remember-me功能认证的, 就允许访问
    要求用户不仅需要认证, 还要具备ROLE_SPITTER权限:

    使用Spring表达式进行安全保护  
    我们可以使用hasRole()限制某个特定的角色, 但是我们不能在相同的路径上同时通过hasIpAddress()限制特定的IP地址。  
    使用Spring表达式语言(Spring Expression Language, SpEL) , 将其作为装配bean属性的高级技术。 借助access()方法, 我可以将SpEL作为声明访问限制的一种方式。 例如, 如下就是使用SpEL表达式来声明具有“ROLE_SPITTER”角色才能访问“/spitter/me”URL:  
    安全表达式 计 算 结 果
    authentication 用户的认证对象
    denyAll 结果始终为false
    hasAnyRole(list of roles) 如果用户被授予了列表中任意的指定角色, 结果为true
    hasRole(role) 如果用户被授予了指定的角色, 结果为true
    hasIpAddress(IP Address) 如果请求来自指定IP的话, 结果为true
    isAnonymous() 如果当前用户为匿名用户, 结果为true
    isAuthenticated() 如果当前用户进行了认证的话, 结果为true
    isFullyAuthenticated() 如果当前用户进行了完整认证的话(不是通过Remember-me功能进行的认证) , 结果为true
    isRememberMe() 如果当前用户是通过Remember-me自动认证的, 结果为true
    permitAll 结果始终为true
    principal 用户的principal对象

    例如, 如果你想限制“/spitter/me” URL的访问, 不仅需要ROLE_SPITTER, 还需要来自指定的IP地址,如下的方式调用access()方法:  

    强制通道的安全性  
    HTTPS加密方式传输用户敏感信息。
    示例, 为了保证注册表单的数据通过HTTPS传送, 我们可以在配置中添加requiresChannel()方法, 如下所示:  
    只要是对“/spitter/form”的请求, Spring Security都视为需要安全通道(通过调用requiresChannel()确定的) 并自动将请求重定向到HTTPS上。  
    与之相反,可以使用requiresInsecure()代替requiresSecure()方法, 将首页声明为始终通过HTTP传送:  

    防止跨站请求伪造  
    当一个POST请求提交到“/spittles”上时, SpittleController将会为用户创建一个新的Spittle对象。  如果这个POST请求来源于其他站点的话, 会怎么样呢? 如果在其他站点提交如下表单, 这个POST请求会造成什么样的结果呢?  
    这是跨站请求伪造(cross-site request forgery, CSRF) 。 如果一个站点欺骗用户提交请求到其他服务器的话, 就会发生CSRF攻击, 这可能会带来消极的后果。
    从
    Spring Security 3.2开始, 默认就会启用CSRF防护。

    Spring Security通过一个同步token的方式来实现CSRF防护的功能。 它将会拦截状态变化的请求(例如,非GET、 HEAD、 OPTIONS和TRACE的请求) 并检查CSRF token。 如果请求中不包含CSRF token的话, 或者token不能与服务器端的token相匹配, 请求将会失败, 并抛出CsrfException异常。
    这意味着在你的应用中, 所有的表单必须在一个
    “_csrf”域中提交token, 而且这个token必须要与服务器端计算并存储的token一致, 这样的话当表单提交的时候, 才能进行匹配。

    可是,Spring Security已经简化了将token放到请求的属性中这个操作。 如果使用Thymeleaf, 只要<form>标签的action属性添加了Thymeleaf命名空间前缀, 那么就会自动生成一个“_csrf”隐藏域:  
    JSP
    如果使用Spring的表单绑定标签的话, <sf:form>标签会自动为我们添加隐藏的CSRF token标签。  

    4、认证用户
    最简单的Spring Security配置的话, 那么就能默认得到一个登录页。认证不通过就会转到默认登录页面。 
    实际上,  一旦重写了configure(HttpSecurity)方法, 就失去了这个简单的登录页面。  如果也需要使用这个默认的登录页面,在configure(HttpSecurity)方法中, 调用formLogin()。
    请注意, 和前面一样, 这里调用add()方法来将不同的配置指令连接在一起。

    如果我们访问应用的
    “/login”链接或者导航到需要认证的页面, 那么将会在浏览器中展现登录页面。

    添加自定义的登录页  
    直接把一些主要信息修改和默认登录页一样就可以。
    启用HTTP Basic认证  
    HTTP Basic认证(HTTP Basic Authentication) 会直接通过HTTP请求本身, 对要访问应用程序的用户进行认证。当在Web浏览器中使用时, 它将向用户弹出一个简单的模态对话框。
    但这只是
    Web浏览器的显示方式。 本质上, 这是一个HTTP 401响应, 表明必须要在请求中包含一个用户名和密码。 在REST客户端向它使用的服务进行认证的场景中, 这种方式比较适合。
    如果要启用
    HTTP Basic认证的话, 只需在configure()方法所传入的HttpSecurity对象上调用httpBasic()即可。 另外, 还可以通过调用realmName()方法指定域。 如下是在Spring Security中启用HTTP Basic认证的典型配置:  

    启用Remember-me功能  
    你只要登录过一次, 应用就会记住你, 当再次回到应用的时候你就不需要登录了。  
    启用这项功能, 只需在configure()方法所传入的HttpSecurity对象上调用rememberMe()即可。  
    默认情况下, 这个功能是通过在cookie中存储一个token完成的。

    存储在cookie中的token包含用户名、 密码、 过期时间和一个私钥——在写入cookie前都进行了MD5哈希。 默认情况下, 私钥的名为SpringSecured, 将其设置为spitterKey。
    Remember-me功能已经启用,但是需要用户来确认希望应用程序能够记住他们。 那么登录请求必须包含一个名为remember-me的参数。 在登录表单中,增加一个复选框即可:  
    退出的时候,如果你启用Remember-me功能的话,也要添加对应的功能。

    退出功能是通过Servlet容器中的Filter实现的(默认情况下) , 这个Filter会拦截针对“/logout”的请求。 
    当用户点击这个链接的时候, 会发起对“/logout”的请求, 请求会被Spring Security的LogoutFilter所处理。 用户会退出应用, 所有的Remember-me token都会被清除掉。 在退出完成后, 用户浏览器将会重定向到/login?logout,如果需要重定向到其他页面,在configure()中进行如下的配置:  

    5、保护视图  
    当浏览器渲染HTML内容时, 可能希望视图中能够反映安全限制和相关的信息(比如显示“您已经以……身份登录”) 或者你想根据用户被授予了什么权限, 有条件地渲染特定的视图元素。  
    Spring Security本身提供了一个JSP标签库, 而Thymeleaf通过特定的方言实现了与Spring Security的集成。  
    使用Spring Security的JSP标签库  
    只有三个标签
    JSP标签 作 用
    <security:accesscontrollist> 如果用户通过访问控制列表授予了指定的权限, 那么渲染该标签体中的内容
    <security:authentication> 渲染当前用户认证对象的详细信息
    <security:authorize> 如果用户被授予了特定的权限或者SpEL表达式的计算结果为true, 那么渲染该标签体中的内容
    使用标签库,需要在页面中引用。
    访问认证信息的细节  
    访问用户的认证信息。 在页面顶部以用户名标示显示“欢迎”或“您好”,使用<security:authentication>。
    认 证 属 性 描 述
    authorities 一组用于表示用户所授予权限的GrantedAuthority对象
    Credentials 用于核实用户的凭证(通常, 这会是用户的密码)
    details 认证的附加信息(IP地址、 证件序列号、 会话ID等)
    principal 用户的基本信息对象

    <security:authentication>将在视图中渲染属性的值。 赋值给一个变量, 只需要在var属性中指明变量的名字即可。 
    这个变量默认是定义在页面作用域内的。 如果在其他作用域也可以使用,例如请求或会话作用域(或者是能够在javax.servlet.jsp.PageContext中获取的其他作用域) , 那么可以通过scope属性来声明。 
    条件性的渲染内容  
    使用<security:authorize>标签来为具有ROLE_SPITTER角色的用户显示Spitter表单。  
    access属性被赋值为一个SpEL表达式, 这个表达式的值将确定<security: authorize>标签主体内的内容是否渲染。 这里我们使用了hasRole('ROLE_SPITTER')表达式来确保用户具有ROLE_SPITTER角色。 
    例如, 假设应用中有一些管理功能只能对用户名为habuma的用户可用。 也许你会像这样使用isAuthenticated()和principal表达式:  
    例如,只把部分功能链接给管理员,视图上阻止链接的渲染。 但是没有什么可以阻止别人在浏览器的地址栏手动输入“/admin”这个URL。  
    在安全配置中, 添加一个对antMatchers()方法的调用将会严格限制对“/admin”这个URL的访问  
    管理功能已经被锁定了,URL地址得到了保护, 并且到这个URL的链接在用户没有授权使用的情况下不会显示。  接下来,在在<security:authorize>标签的access属性中 设置显示就可以了。
    使用Thymeleaf的Spring Security方言  
    如果我们选择Thymeleaf而不是JSP作为视图方案的话,看一下Thymeleaf如何支持Spring Security。  
    与Spring Security的JSP标签库类似, Thymeleaf的安全方言提供了条件化渲染和显示认证细节的能力。  
    sec:authentication 渲染认证对象的属性。 类似于Spring Security的<sec:authentication/>JSP标签
    sec:authorize 基于表达式的计算结果, 条件性的渲染内容。 类似于Spring Security的<sec:authorize/>JSP标签
    sec:authorize-acl 基于表达式的计算结果, 条件性的渲染内容。 类似于Spring Security的<sec:accesscontrollist/> JSP标签
    sec:authorize-expr sec:authorize属性的别名
    sec:authorize-url 基于给定URL路径相关的安全规则, 条件性的渲染内容。 类似于Spring Security的<sec:authorize/> JSP标签使用url属性时的场景

    需要确保Thymeleaf Extras Spring Security已经位于应用的类路径下。 还需要在配置中使用SpringTemplateEngine来注册SpringSecurity Dialect。 
    安全方言注册完成之后, 我们就可以在Thymeleaf模板中使用它的属性了。 首先, 需要在使用这些属性的模板中声明安全命名空间:  
    标准的Thymeleaf方法依旧与之前一样, 使用th前缀, 安全方言则设置为使用sec前缀。  
    假设我们想要为认证用户渲染“Hello”文本。 
    sec:authorize属性会接受一个SpEL表达式。 如果表达式的计算结果为true, 那么元素的主体内容就会渲染。 在本例中, 表达式为isAuthenticated(), 所以只有用户已经进行了认证, 才会渲染<div>标签的主体内容。 

    例如, 如下Thymeleaf代码片段所实现的功能与之前<sec:authorize>JSP标签和url属性所实现的功能是相同的:

    如果用户有权限访问
    “/admin”的话, 那么到管理页面的链接就会渲染, 否则的话, 这个链接将不会渲染。  

    二、Spring Web之8-Web高级-跨重定向请求传递数据

    发表于 2017-09-20 | 分类于 Spring实战4版
    在处理完POST请求后, 通常来讲一个最佳实践就是执行一下重定向。 除了其他的一些因素外, 这样做能够防止用户点击浏览器的刷新按钮或后退箭头时, 客户端重新执行危险的POST请求。 
    使用redirect: 前缀能够让重定向功能变得非常简单。但是 Spring为重定向功能还提供了一些其他的辅助功能。  

    具体来讲, 正在发起重定向功能的方法该如何发送数据给重定向的目标方法呢? 一般来讲, 当一个处理器方法完成之后, 该方法所指定的模型数据将会复制到请求中, 并作为请求中的属性, 请求会转发(forward) 到视图上进行渲染。 因为控制器方法和视图所处理的是同一个请求, 所以在转发的过程中, 请求属性能够得以保存。
    但是重定向是不能保存原模型的数据信息,原始的请求就结束了, 并且会发起一个新的GET请求。 原始请求中所带有的模型数据也就随着请求一起消亡了。 

    对于重定向来说, 模型并不能用来传递数据。 但是我们也有一些其他方案, 能够从发起重定向的方法传递数据给处理重定向方法中:使用URL模板以路径变量和/或查询参数的形式传递数据;通过flash属性发送数据。

    首先, 我们看一下
    Spring如何帮助我们通过路径变量和/或查询参数的形式传递数据。  

    通过URL模板进行传递数据
    通过路径变量和查询参数传递数据看起来非常简单。 例如username的值是直接连接到重定向String上的。 这能够正常运行, 但是还远远不能说没有问题。 当构建URL或SQL
    查询语句的时候, 使用String连接是很危险的。  
    除了连接String的方式来构建重定向URL, Spring还提供了使用模板的方式来定义重定向URL。 例如:
    username作为占位符填充到了URL模板中, 而不是直接连接到重定向String中, 所以username中所有的不安全字符都会进行转义。 这样会更加安全, 这里允许用户输入任何想要的内容作为username, 并会将其附加到路径上。
    除此之外, 模型中所有其他的原始类型值都可以添加到
    URL中作为查询参数。 作为样例, 假设除了username以外, 模型中还要包含新创建Spitter对象的id属性, 那processRegistration()方法可以改写为如下的形式:  
    所返回的重定向String并没有太大的变化。 但是, 因为模型中的spitterId属性没有匹配重定向URL中的任何占位符, 所以它会自动以查询参数的形式附加到重定向URL上。
    如果
    username属性的值是habuma并且spitterId属性的值是42, 那么结果得到的重定向URL路径将会是“/spitter/habuma?spitterId=42”。
    通过路径变量和查询参数的形式跨重定向传递数据是很简单直接的方式, 但它也有一定的限制。 它只能用来发送简单的值, 如
    String和数字的值。 在URL中, 并没有办法发送更为复杂的值, 但这正是flash属性能够提供帮助的领域。

    使用flash属性  
    假设我们不想在重定向中发送username或ID了, 而是要发送实际的Spitter对象。
    有个方案是将Spitter放到会话中。 会话能够长期存在, 并且能够跨多个请求。 所以我们可以在重定向发生之前将Spitter放到会话中, 并在重定向后, 从会话中将其取出。 当然, 我们还要负责在重定向后在会话中将其清理掉。
    实际上, Spring也认为将跨重定向存活的数据放到会话中是一个很不错的方式。 但是, Spring认为我们并不需要管理这些数据, 相反, Spring提供了将数据发送为flash属性(flash attribute) 的功能。 按照定义, flash属性会一直携带这些数据直到下一次请求, 然后才会消失。

    Spring提供了通过RedirectAttributes设置flash属性的方法, 这是Spring 3.1引入的Model的一个子接口。 RedirectAttributes提供了Model的所有功能, 除此之外, 还有几个方法是用来设置flash属性的。具体来讲, RedirectAttributes提供了一组addFlashAttribute()方法来添加flash属性。 重新看一下processRegistration()方法, 我们可以使用addFlashAttribute()将Spitter对象添加到模型中: 
    可以直接不设置key,直接我们传递了一个Spitter对象给addFlashAttribute()方法, 所以推断得到的key将会是spitter。
    在重定向执行之前, 所有的
    flash属性都会复制到会话中。 在重定向后, 存在会话中的flash属性会被取出, 并从会话转移到模型之中。 处理重定向的方法就能从模型中访问Spitter对象了, 就像获取其他的模型对象一样。
    获取信息:
    showSpitterProfile()方法所做的第一件事就是检查是否存有key为spitter的model属性。 如果模型中包含spitter属性, 那就什么都不用做了。 这里面包含的Spitter对象将会传递到视图中进行渲染。 但是如果模型中不包含spitter属性的话, 那么showSpitterProfile()将会从Repository中查找Spitter, 并将其存放到模型中。  
     

    二、Spring Web之7-Web高级-为控制器添加通知

    发表于 2017-09-19 | 分类于 Spring实战4版
    如果控制器类的特定切面能够运用到整个应用程序的所有控制器中, 那么这将会便利很多。 举例来说, 如果要在多个控制器中处理异常,那@ExceptionHandler注解所标注的方法是很有用的。 不过, 如果多个控制器类中都会抛出某个特定的异常, 那么你可能会发现要在所有的控制器方法中重复相同的@ExceptionHandler方法。 或者, 为了避免重复, 我们会创建一个基础的控制器类, 所有控制器类要扩展这个类, 从而继承通用的@ExceptionHandler方法。
    Spring 3.2为这类问题引入了一个新的解决方案: 控制器通知。 控制器通知(controller advice) 是任意带有@ControllerAdvice注解的类,
    这个类会包含一个或多个如下类型的方法:
            @ExceptionHandler注解标注的方法;
            @InitBinder注解标注的方法;
            @ModelAttribute注解标注的方法。
    在带有@ControllerAdvice注解的类中, 以上所述的这些方法会运用到整个应用程序所有控制器中带有@RequestMapping注解的方法上。
    @ControllerAdvice注解本身已经使用了@Component, 因此@ControllerAdvice注解所标注的类将会自动被组件扫描获取到, 就像带有@Component注解的类一样。
    @ControllerAdvice最为实用的一个场景就是将所有的@ExceptionHandler方法收集到一个类中, 这样所有控制器的异常就能在一个地方进行一致的处理。 例如, 我们想将DuplicateSpittleException的处理方法用到整个应用程序的所有控制器上。 如下的程序清单展现的AppWideExceptionHandler就能完成这一任务, 这是一个带有@ControllerAdvice注解的类。
    任意的控制器方法抛出了DuplicateSpittleException, 不管这个方法位于哪个控制器中, 都会调用这个duplicateSpittleHandler()方法来处理异常。 我们可以像编@RequestMapping注解的方法那样来编写@ExceptionHandler注解的方法。 如程序清单7.10所示, 它返回“error/duplicate”作为逻辑视图名, 因此将会为用户展现一个友好的出错页面。

    二、Spring Web之6-Web高级-处理异常

    发表于 2017-09-18 | 分类于 Spring实战4版
    如果在请求处理的时候, 出现了异常, 那它的输出依然会是Servlet响应。 异常必须要以某种方式转换为响应。是Servlet响应。 异常必须要以某种方式转换为响应。
    Spring提供了多种方式将异常转换为响应:
    特定的Spring异常将会自动映射为指定的HTTP状态码;
    异常上可以添加@ResponseStatus注解, 从而将其映射为某一个HTTP状态码;
    在方法上可以添加@ExceptionHandler注解, 使其用来处理异常。
    处理异常的最简单方式就是将其映射到HTTP状态码上, 进而放到响应之中。 接下来, 我们看一下如何将异常映射为某一个HTTP状态码。
    Spring异常 HTTP状态码
    BindException 400 - Bad Request
    ConversionNotSupportedException 500 - Internal Server Error
    HttpMediaTypeNotAcceptableException 406 - Not Acceptable
    HttpMediaTypeNotSupportedException 415 - Unsupported Media Type
    HttpMessageNotReadableException 400 - Bad Request
    HttpMessageNotWritableException 500 - Internal Server Error
    HttpRequestMethodNotSupportedException 405 - Method Not Allowed
    MethodArgumentNotValidException 400 - Bad Request
    MissingServletRequestParameterException 400 - Bad Request
    MissingServletRequestPartException 400 - Bad Request
    NoSuchRequestHandlingMethodException 404 - Not Found
    TypeMismatchException 400 - Bad Request


    异常一般会由Spring自身抛出, 作为DispatcherServlet处理过程中或执行校验时出现问题的结果。 如果DispatcherServlet无法找到适合处理请求的控制器方法, 那么将会抛出NoSuchRequestHandlingMethodException异常, 最终的结果就是产生404状态码的响应(Not Found) 。
    尽管这些内置的映射是很有用的, 但是对于应用所抛出的异常它们就无能为力了。 所以,Spring提供了一种机制, 能够通过@ResponseStatus注解将异常映射为HTTP状态码。
    例如在控制器中抛出了这个的异常,这是一个自定义异常。
    这时候,可以通过ResponseStatus注解将异常信息映射为THHP状态码。

    异常处理
    在很多的场景下, 将异常映射为状态码是很简单的方案, 并且就功能来说也足够了。 但是如果我们想在响应中不仅要包括状态码, 还要包含所产生的错误, 此时的话, 我们就不能将异常视为HTTP错误了, 而是要按照处理请求的方式来处理异常了。
    一是直接在控制器中输出异常信息,如在处理请求的方法中直接处理异常 :

    它运行起来没什么问题, 但是这个方法有些复杂。 该方法可以有两个路径, 每个路径会有不同的输出。 如果能让saveSpittle()方法只关注正确的路径, 而让其他方法处理异常的话, 那么它就能简单一些。
    首先, 让我们首先将saveSpittle()方法中的异常处理方法剥离掉:  
    可以看到, saveSpittle()方法简单了许多。 因为它只关注成功保存Spittle的情况, 所以只有一个执行路径, 很容易理解(和测试) 。
    现在, 我们为
    SpittleController添加一个新的方法, 它会处理抛出DuplicateSpittleException的情况:

    handleDuplicateSpittle()方法上添加了@ExceptionHandler注解, 当抛出DuplicateSpittleException异常的时候, 将会委托该方法来处理。 它返回的是一个String, 这与处理请求的方法是一致的, 指定了要渲染的逻辑视图名, 它能够告诉用户他们正在试图创建一条重复的条目。

    对于
    @ExceptionHandler注解标注的方法来说, 它能处理同一个控制器中所有处理器方法所抛出的异常。 所以, 尽管我们从saveSpittle()中抽取代码创建了handleDuplicateSpittle()方法, 但是它能够处理SpittleController中所有方法所抛出的DuplicateSpittleException异常。 我们不用在每一个可能抛出DuplicateSpittleException的方法中添加异常处理代码,这一个方法就涵盖了所有的功能。
    既然
    @ExceptionHandler注解所标注的方法能够处理同一个控制器类中所有处理器方法的异常, 那么你可能会问有没有一种方法能够处理所有控制器中处理器方法所抛出的异常呢。 从Spring 3.2开始, 这肯定是能够实现的, 我们只需将其定义到控制器通知类中即可。详见,为控制器添加通知。

    二、Spring Web之5-Web高级-处理multipart

    发表于 2017-09-17 | 分类于 Spring实战4版
    根据http/1.1 rfc 2616的协议规定,我们的请求方式只有OPTIONS、GET、HEAD、POST、PUT、DELETE、TRACE等,那为为何我们还会有multipart/form-data请求之说呢?
     multipart格式的数据会将一个表单拆分为多个部分(part),每个部分对应一个输入域。在一般的表单输入域中,它所对应的部分中会放置文本型数据,但是如果上传文件的话,它所对应的部分可以是二进制。类似这样:

    http协议规定以ASCII码传输,建立在tcp,ip协议规范,把http请求分成3个部分,状态行,请求头,请求体。所有的方法,实现都是围绕如何使用和组织这三部分来完成了,万变不离其宗。
    既然上面请求方式里面没有multipart/form-data那这个请求又是怎么回事呢,其实multipart/form-data也是在post基础上演变而来的,具体如下:
    1、multipart/form-data的基础方式是post,也就是说通过post组合方式来实现的。
    2、multipart/form-data于post方法的不同之处在于请求头和请求体。
    3、multipart/form-data的请求头必须包含一个特殊的头信息:Content-Type,其值也必须为multipart/form-data,同时还需要规定一个内容分割用于分割请求提中多个post的内容,如文件内容和文本内容是需要分隔开来的,不然接收方就无法解析和还原这个文件了,具体的头信息如下:
    Content-Type: multipart/form-data; boundary=${bound} 
    其中${bound} 是一个占位符,代表我们规定的分割符,可以自己任意规定,但为了避免和正常文本重复了,尽量要使用复杂一点的内容。如:--------------------56423498738365
    4、multipart/form-data的请求体也是一个字符串,不过和post的请求提不同的是它的构造方式,post是简单的name=value键值连接,而multipart/form-data是添加了分隔符等内容的构造体。

    尽管multipart请求看起来很复杂, 但在Spring MVC中处理它们却很容易。 在编写控制器方法处理文件上传之前, 我们必须要配置一个multipart解析器, 通过它来告诉DispatcherServlet该如何读取multipart请求。

    配置multipart解析器
    DispatcherServlet并没有实现任何解析multipart请求数据的功能。 它将该任务委托给了Spring中MultipartResolver策略接口的实现, 通过这个实现类来解析multipart请求中的内容。 从Spring 3.1开始, Spring内置了两个MultipartResolver的实现供我们选择:
        CommonsMultipartResolver: 使用Jakarta Commons FileUpload解析multipart请求;
        StandardServletMultipartResolver: 依赖于Servlet 3.0对multipart请求的支持(始于Spring 3.1) 。
    一般来讲, 在这两者之间,
    StandardServletMultipartResolver可能会是优选的方案。 它使用Servlet所提供的功能支持, 并不需要依赖任何其他的项目。 如果我们需要将应用部署到Servlet 3.0之前的容器中, 或者还没有使用Spring 3.1或更高版本, 那么可能就需要CommonsMultipartResolver了。
    使用
    Servlet 3.0解析multipart请求
    兼容
    Servlet 3.0的StandardServletMultipartResolver没有构造器参数, 也没有要设置的属性。 在Spring应用上下文中, 将其声明为bean就会非常简单, 如下所示:  

    @Bean

    public MultipartResolver multipartResolver() throws IOException {

    return new StandardServletMultipartResolver();

    }

    限制用户上传文件的参数调节在web.xml或Servlet初始化类中, 将multipart的具体细节作为DispatcherServlet配置的一部分。

    如果我们采用Servlet初始化类的方式来配置DispatcherServlet的话, 这个初始化类应该已经实现了WebApplicationInitializer,那我们可以在Servlet registration上调用setMultipartConfig()方法, 传入一个MultipartConfig-Element实例。 如下是最基本的DispatcherServlet multipart配置, 它将临时路径设置为“/tmp/spittr/uploads”:

    class MyServletInitializer implements WebApplicationInitializer{

    @Override

    public void onStartup(ServletContext servletContext) throws ServletException {

    DispatcherServlet ds = new DispatcherServlet();

    ServletRegistration.Dynamic registration = servletContext.addServlet("appServlet",ds);

    registration.addMapping("/");

    registration.setMultipartConfig(new MultipartConfigElement("/tmp/spittr/uploads", 2097152, 4194304, 0));

    }

    }

    如果我们配置DispatcherServlet的Servlet初始化类继承了AbstractAnnotationConfigDispatcherServletInitializer或AbstractDispatcher-ServletInitializer的话, 那么我们不会直接创建DispatcherServlet实例并将其注册到Servlet上下文中。 这样的话, 将不会有对Dynamic Servlet registration的引用供我们使用了。 但是, 我们可以通过重载customizeRegistration()方法(它会得到一个Dynamic作为参数) 来配置multipart的具体细节:

    @Override

    protected void customizeRegistration(ServletRegistration.Dynamic registration) {

    registration.setMultipartConfig(new MultipartConfigElement("/tmp/spittr/uploads", 2097152, 4194304, 0));

    }

    还可以通过其他的构造器来限制上传文件的大小  

    如果我们使用更为传统的web.xml来配置MultipartConfigElement的话, 那么可以使用<servlet>中的<multipart-config>元素 。

    配置Jakarta Commons FileUpload multipart解析器  
    Servlet 3.0, StandardServletMultipartResolver会是最佳的选择, 如果在Servlet 3.0以下就需要替代的方案。  我们可以编写自己的MultipartResolver实现,除非想要在处理multipart请求的时候执行特定的逻辑, 否则的话, 没有必要这样做。 Spring内置了CommonsMultipartResolver, 可以作为StandardServletMultipartResolver的替代方案。
    具体见StandardServletMultipartResolver使用


    处理multipart请求
    前面配置好对mutipart请求的处理, 接下来进行接收请求处理,最常用方式就是在某个控制器方法参数上添加@RequestPart注解。
    假设我们允许用户在注册Spittr应用的时候上传一张图片, 那么我们需要修改表单, 以允许用户选择要上传的图片, 同时还需要修改SpitterController 中的processRegistration()方法来接收上传的图片。 
    <form>标签现在将enctype属性设置为multipart/form-data, 这会告诉浏览器以multipart数据的形式提交表单, 而不是以表单数据的形式进行提交。 
    在multipart中, 每个输入域都会对应一个part。

    除了注册表单中已有的输入域, 我们还添加了一个新的
    <input>域, 其type为file。 这能够让用户选择要上传的图片文件。 accept属性用来将文件类型限制为JPEG、 PNG以及GIF图片。 根据其name属性, 图片数据将会发送到multipart请求中的profilePicture part之中。

    现在, 我们需要修改
    processRegistration()方法, 使其能够接受上传的图片。 其中一种方式是添加byte数组参数, 并为其添加@RequestPart注解。 
    当注册表单提交的时候, profilePicture属性将会给定一个byte数组, 这个数组中包含了请求中对应part的数据(通过@RequestPart指定) 。 如果用户提交表单的时候没有选择文件, 那么这个数组会是空(而不是null) 。 获取到图片数据后, processRegistration()方法剩下的任务就是将文件保存到某个位置。

    控制器接收MultipartFile
    使用上传文件的原始byte比较简单但是功能有限。 因此, Spring还提供了MultipartFile接口, 它为处理multipart数据提供了内容更为丰富的对象。接口源码如下:

    public interface MultipartFile extends InputStreamSource {

    String getName();

    String getOriginalFilename();

    String getContentType();

    boolean isEmpty();

    long getSize();

    byte[] getBytes() throws IOException;

    InputStream getInputStream() throws IOException;

    void transferTo(File var1) throws IOException, IllegalStateException;

    }

    可以看到, MultipartFile提供了获取上传文件byte的方式,还能获得原始的文件名、 大小以及内容类型。 它还提供了一个InputStream, 用来将文件数据以流的方式进行读取。
    除此之外,
    MultipartFile还提供了一个便利的transferTo()方法, 它能够帮助我们将上传的文件写入到文件系统中。 例如:可以在process-Registration()方法中添加如下的几行代码, 从而将上传的图片文件写入到文件系统中:  
    profilePicture.transferTo(new File("/tmp/spittr/" + spitter.getUsername() + ".jpg"));

    将文件保存到本地文件系统中非常easy,但需要我们对这些文件进行管理。 我们需要确保有足够的空间, 确保当出现硬件故障时, 文件进行了备份, 还需要在集群的多个服务器之间处理这些图片文件的同步。

    另外一种方案就是让别人来负责处理这些事情。 多加几行代码, 我们就能将图片保存到云端。 例如, 如下的程序清单所展现的saveImage()方法能够将上传的文件保存到Amazon S3中, 我们在processRegistration()中可以调用该方法。

    Part的形式接受上传的文件
    如果你需要将应用部署到Servlet 3.0的容器中, 那么会有MultipartFile的一个替代方案。 Spring MVC也能接受javax.servlet.http.Part作为控制器方法的参数。 如果使用Part来替换MultipartFile的话, 那么processRegistration()的方法签名将会变成如下的形式:

    Part接口与MultipartFile并没有太大的差别。 

    如果在编写控制器方法的时候, 通过Part参数的形式接受文件上传, 那么就没有必要配置MultipartResolver了。 只有使用MultipartFile的时候, 我们才需要MultipartResolver。  

    二、Spring Web之4-Web高级-DispatcherServlet高级配置

    发表于 2017-09-16 | 分类于 Spring实战4版
    在之前的基础配置中, 只需要基本的DispatcherServlet和ContextLoaderListener环境, 并且Spring配置是使用Java的, 而不是XML。
    这对于很多Spring应用来说, 已经可以使用,但是不一定总能满足我们的要求。 除了DispatcherServlet以外, 我们可能还需要额外的Servlet和Filter; 我们可能还需要对DispatcherServlet本身做一些额外的配置; 或者, 如果我们需要将应用部署到Servlet 3.0之前的容器中, 那么还需要将DispatcherServlet配置到传统的web.xml中。

    自定义DispatcherServlet配置
    AbstractAnnotationConfigDispatcherServletInitializer所完成的事情其实比看上去要多。 在SpittrWebAppInitializer中我们所编写的三个方法仅仅是必须要重载的abstract方法。 实际四行还可以重载很多方法,如:customizeRegistration()。 在AbstractAnnotationConfigDispatcherServletInitializer将DispatcherServlet注册到Servlet容器中之后, 就会调用customizeRegistration(), 并将Servlet注册后得到的Registration.Dynamic传递进来。 通过重载customizeRegistration()方法, 我们可以对DispatcherServlet进行额外的配置。

    例如, 在Spring MVC中处理multipart请求和文件上传。 如果计划使用Servlet 3.0对multipart配置的支持, 那么需要使用DispatcherServlet的registration来启用multipart请求。 我们可以重载customizeRegistration()方法来
    设置MultipartConfigElement, 如下所示:

    @Override

    protected void customizeRegistration(Dynamic registration) {

    registration.setMultipartConfig(

    new MultipartConfigElement("/tmp/spittr/uploads", 2097152, 4194304, 0));

    }

    借助customizeRegistration()方法中的ServletRegistration.Dynamic, 我们能够完成多项任务, 包括通过调
    用setLoadOnStartup()设置load-on-startup优先级, 通过setInitParameter()设置初始化参数, 通过调
    用setMultipartConfig()配置Servlet 3.0对multipart的支持。 在前面的样例中, 我们设置了对multipart的支持, 将上传文件的临时存储目
    录设置在“/tmp/spittr/uploads”中。

    添加其他的Servlet和Filter
    按照AbstractAnnotationConfigDispatcherServletInitializer的定义, 它会创建DispatcherServlet和ContextLoaderListener。如果你想注册其他的Servlet、 Filter或Listener,通过以下办法。
    基于Java的初始化器(initializer) 的一个好处就在于我们可以定义任意数量的初始化器类。 因此, 如果我们想往Web容器中注册其他组件的话, 只需创建一个新的初始化器就可以了。 最简单的方式就是实现Spring的WebApplicationInitializer接口。
    例如, 如下的程序清单展现了如何创建WebApplicationInitializer实现并注册一个Servlet。

    class MyServletInitializer implements WebApplicationInitializer{

    @Override

    public void onStartup(ServletContext servletContext) throws ServletException {

    //注册filter

    FilterRegistration.Dynamic filter = servletContext.addFilter("myServlet", MyServlet.class);

    //映射路径

    filter.addMappingForUrlPatterns(null, false, "/my/**");

    }

    }

    如果要将应用部署到支持Servlet 3.0的容器中, 那么WebApplicationInitializer提供了一种通用的方式, 实现在Java中注册Servlet、Filter和Listener。 

    如果你只是注册Filter, 并且该Filter只会映射到DispatcherServlet上的话, 那么在AbstractAnnotationConfigDispatcherServletInitializer中还有一种快捷方式。
    为了注册
    Filter并将其映射到DispatcherServlet, 所需要做的仅仅是重载AbstractAnnotationConfigDispatcherServletInitializer的getServlet-Filters()方法。 
    例如, 在如下的代码中, 重载了AbstractAnnotationConfig-DispatcherServletInitializer的getServletFilters()方法以注册Filter:  

    @Override

    protected Filter[] getServletFilters() {

    return new Filter[]{new MyFilter()};

    }

    在这里没有必要声明它的映射路径, getServletFilters()方法返回的所有Filter都会映射到DispatcherServlet上。
    Servlet 3.0容器中, 那么Spring提供了多种方式来注册Servlet(包括DispatcherServlet) 、 Filter和Listener, 而不必创建web.xml文件。 

    如果你不想采取以上所述方案的话, 也是可以的。 假设你需要将应用部署到不支持Servlet 3.0的容器中(或者你只是希望使用web.xml文件) , 那么我们完全可以按照传统的方式, 通过web.xml配置Spring MVC。
    DispatcherServlet和ContextLoaderListener从XML中加载各自的应用上下文。 
    要在
    Spring MVC中使用基于Java的配置, 我们需要告诉DispatcherServlet和ContextLoaderListener使用AnnotationConfigWebApplicationContext, 这是一个WebApplicationContext的实现类, 它会加载Java配置类, 而不是使用XML。 要实现这种配置, 我们可以设置contextClass上下文参数以及DispatcherServlet的初始化参数。 如下的程序清单展现了一个新的web.xml, 在这个文件中, 它所搭建的Spring MVC使用基于Java的Spring:








    上一页1…192021…25下一页
    初晨

    初晨

    永远不要说你知道本质,更别说真相了。

    249 日志
    46 分类
    109 标签
    近期文章
    • WebSocket、Socket、TCP、HTTP区别
    • Springboot项目的接口防刷
    • 深入理解Volatile关键字及其实现原理
    • 使用vscode搭建个人笔记环境
    • HBase介绍安装与操作
    © 2018 — 2020 Copyright
    由 Hexo 强力驱动
    |
    主题 — NexT.Gemini v5.1.4