Cache 注解的 key、unless、condition 等都是支持 SpEL 的。而对这块的支持是在 CacheOperationExpressionEvaluator 中实现的。
首先可以看到 CacheAspectSupport 中有一个 evaluator
变量,其定义如下:
1 | private final CacheOperationExpressionEvaluator evaluator = new CacheOperationExpressionEvaluator(); |
接下来我们详细分析这个 CacheOperationExpressionEvaluator
。
最近工作有点忙,再次拖更,之后忙完这阵会补回来的。
定义了一个 protected 的静态内部类 ExpressionKey,实现了 Comparable 接口,包含 element(类型为 AnnotatedElementKey)和 expression(类型为 String)两个字段。
定义了一个 parser ,由父类继承构造的时候赋值了一个 new SpelExpressionParser()
。关于 SpelExpressionParser 此处不展开。
定义了一个 parameterNameDiscoverer,赋值为一个 new DefaultParameterNameDiscoverer()
。
关于 DefaultParameterNameDiscoverer,是为了兼容低于 Java 8 的版本。在 Java 8 之前是没有反射的,使用的是 LocalVariableTableParameterNameDiscoverer,内部依赖 ASM 库实现(详见其 inspectClass 方法,此处不展开);Java 8 以及之后使用的是 StandardReflectionParameterNameDiscoverer,直接使用反射即可。
CachedExpressionEvaluator 的关键代码只有一段:
1 | protected Expression getExpression(Map<ExpressionKey, Expression> cache, |
尝试从 cache 获取表达式,不存在则由 parser 执行 parseExpression 并将结果缓存起来。
1 | private final Collection<? extends Cache> caches; |
CacheEvaluationContext 继承自 MethodBasedEvaluationContext,在其基础上对不可用变量 unavailableVariables
额外做了一级缓存。
关于 MethodBasedEvaluationContext 的实现,是 SpEL 的部分,此处不展开。简单来说其将方法的参数注入上下文,提供了一个 lookupVariable 方法用于从上下文查找变量。
1 | public EvaluationContext createEvaluationContext(Collection<? extends Cache> caches, |
用 new 出来的 CacheExpressionRootObject 作为 rootObject,带上当前的方法和参数一起生成了一个 CacheEvaluationContext。
其提供了三处需要进行 SpEL 求值的函数:
1 | public Object key(String keyExpression, AnnotatedElementKey methodKey, EvaluationContext evalContext) { |
最终由 generateKey 等方法调用,由 SpEL 表达式结合上下文计算出实际值。
到这章为止,Spring Cache 的核心源码基本讲完了。
Spring cache 通过 ProxyCachingConfiguration 配置。由 AnnotationCacheOperationSource 使用 SpringCacheAnnotationParser 将注解解析为 CacheOperation 并缓存。
CacheOperationSourcePointcut 通过尝试使用 AnnotationCacheOperationSource 取 CacheOperation 来判断是否需要进入切面。
CacheAspectSupport 作为拦截类,使用 AnnotationCacheOperationSource 获取到 CacheOperation,然后依赖 CacheResolver 等进行 Cache 解析,结合 CacheOperationExpressionEvaluator 进行表达式求值,最终执行 cache 的增删查操作。
]]>先看看 CacheInterceptor 的继承关系:
CacheAspectSupport 又继承自 AbstractCacheInvoker,所以我们从 AbstractCacheInvoker 开始看起:
先看源码:
1 | public abstract class AbstractCacheInvoker { |
errorHandler 默认使用 SimpleCacheErrorHandler 实现,即直接抛出所有异常。
提供了四个 doXXX 方法(XXX = Get/Put/Evict/Clear)分别调用了 cache 的四个方法,并交给 errorHandler 处理异常。
AbstractCacheInvoker 的作用就是在最基本的 cache 的基本操作上包了一层异常处理。
先说此处有个有意思的地方。CacheAspectSupport 支持对 Optional 拆包。但由于 Optional 是 Java 8 才有的新特性,为了避免对 Java 8 的硬依赖,对 Optional 类用了一个特殊的加载方式:
1 | private static Class<?> javaUtilOptionalClass = null; |
1 |
|
简而言之就是:cacheOperationSources 和 errorHandler 必须设置、在没有定义 cacheResolver 的情况下会从容器取 cacheManager。
1 | protected Object execute(CacheOperationInvoker invoker, Object target, Method method, Object[] args) { |
首先由 CacheOperationSource 从 method 和 targetClass 的注解生成对应的 CacheOperation 集合,然后将操作、方法、参数等打包整理成一个缓存操作的上下文 CacheOperationContexts。
接下来就是 execute 的主逻辑:
1 | private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) { |
首先区分是否有同步标记,同步标记是在构造 CacheOperationContexts 时由 determineSyncFlag(method)
方法确定的。只有 Cacheable 操作有 sync 标记,且只能允许有这一个 Cache 操作,且只允许有一个 Cache,且不支持 unless 注解。
所以对于同步调用而言,取出这个 CacheableOperation,通过 isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)
判断是否符合条件,符合条件则进行缓存读写操作(尝试从缓存获取,没有则执行函数并写入缓存),否则直接执行函数。对于同步调用,此处已经结束了。
对于没有同步标记的情况,首先由 processCacheEvicts
执行调用前的缓存清除操作:
1 | private void processCacheEvicts(Collection<CacheOperationContext> contexts, boolean beforeInvocation, Object result) { |
关于 performCacheEvict 的细节有机会细讲。
接下来,由 findCachedItem(contexts.get(CacheableOperation.class))
获取 CacheableOperation 命中的缓存。
如果未命中,则聚合之后 Cacheable 操作需要的 put 请求,聚合函数如下:
1 | private void collectPutRequests(Collection<CacheOperationContext> contexts, |
接下来看 cacheHit 非空,如果也没有 CachePut 操作,那么可以直接取 cacheHit 作为返回值。否则需要调用方法作为返回值。
再将显式注解的 CachePutOperation 聚合到之前的 Put 操作集合中,依次调用 cachePutRequest.apply(cacheValue)
。
最后执行函数后清空缓存的 CacheEvicts 操作。然后返回之前的返回值。
在这个过程中,对返回值和异常穿插 wrap 和 unwrap 的处理,支持了有 Optional 的情况。
关于例如 generateKey 这些到的涉及表达式 Evaluation 的部分下章细讲。
]]>Operation 解析器最底层的接口是 CacheAnnotationParser,我们先来看下这个接口
接口的定义十分简单:
1 | public interface CacheAnnotationParser { |
要求了实现类计算出 目标类 或 方法 对应的 CacheOperation 集合。
SpringCacheAnnotationParser 实现了 CacheAnnotationParser 这个接口:
1 |
|
从目标类或方法的声明类上取得默认的 CacheConfig,再执行 parseCacheAnnotations 方法进行解析。
我们先看这个 getDefaultCacheConfig 方法:
1 | DefaultCacheConfig getDefaultCacheConfig(Class<?> target) { |
非常简单,就是从类的 @CacheConfig
注解中取出 cacheNames
、keyGenerator
、cacheManager
、cacheResolver
这四个属性值。
parseCacheAnnotations 方法就较为复杂了,需要解析 Cacheable
、CacheEvict
、CachePut
、Caching
四种注解:
1 | private Collection<CacheOperation> parseCacheAnnotations(DefaultCacheConfig cachingConfig, AnnotatedElement ae) { |
这个 lazyInit 就是数组的懒初始化,不解释了:
1 | private <T extends Annotation> Collection<CacheOperation> lazyInit(Collection<CacheOperation> ops) { |
这三者的注解解析大同小异,parseXXXAnnotation(XXX = Cacheable/Evict/Put)的方法实现基本一致:
1 | CacheXXXOperation parseXXXAnnotation(AnnotatedElement ae, DefaultCacheConfig defaultConfig, CacheXXX cacheXXX) { |
除了上文的 8 个字段外,Cacheable 独有 unless 和 sync 字段,Evict 独有 cacheWide(取自注解的 allEntries 字段) 和 beforeInvocation 字段,Put 独有 unless 字段。
因为 Caching 本身是一个聚合注解,其定义如下:
1 | ({ElementType.METHOD, ElementType.TYPE}) |
所以其需要执行类似 parseCacheAnnotations 的操作,将 Cacheable、CachePut、CacheEvict 三种注解分别转换再聚合起来:
1 | Collection<CacheOperation> parseCachingAnnotation(AnnotatedElement ae, DefaultCacheConfig defaultConfig, Caching caching) { |
SpringCacheAnnotationParser 本质上就是将 Cacheable、CacheEvict、CachePut 和 Caching 的注解参数转换成了 CacheOperation 集合的结构。
]]>上篇讲到,Spring Cache 在 AdviceMode.Proxy
模式会注入 AutoProxyRegistrar
和 ProxyCachingConfiguration
这两个类,其中 AutoProxyRegistrar
是一个与 cache 功能无关的 AOP 类,已经在上一篇中介绍过。
这篇将详细深入 ProxyCachingConfiguration
的配置类。
首先可以看到 ProxyCachingConfiguration
继承自 AbstractCachingConfiguration
,所以我们先看 AbstractCachingConfiguration
的源码。
注:这个 AbstractCachingConfiguration
也是其他代理模式下配置的基类
1 |
|
这里涉及到一个 CachingConfigurer
的接口,容器内应当注册了 0 个或 1 个 CachingConfigurer
,当有 CachingConfigurer
的时候,会为 Cache 功能提供 CacheManager
、CacheResolver
、KeyGenerator
、CacheErrorHandler
这四项配置。顺便从容器取到 @EnableCaching
注解的参数保存在 enableCaching
中。
如果有配置 CachingConfigurer,则将其中的 CacheManager
、CacheResolver
、KeyGenerator
、CacheErrorHandler
保存在类中。同时取到 @EnableCaching
注解的参数保存在类中。
1 |
|
注:所有的 Configuration 和 Bean 都加上了 @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
以便于被 AutoProxyRegistrar
过滤注册。具体逻辑见上一篇。
配置类一共注入了三个类:CacheOperationSource
、CacheInterceptor
、BeanFactoryCacheOperationSourceAdvisor
。额外做的操作就是将基类拿到的 CacheManager
、CacheResolver
、KeyGenerator
、CacheErrorHandler
尽可能注入 CacheInterceptor
中,将 EnableCaching
注解中的 order
注入到 BeanFactoryCacheOperationSourceAdvisor
中,并将这三者接线。
我们就按 CacheOperationSource
、CacheInterceptor
、BeanFactoryCacheOperationSourceAdvisor
的顺序看这三个类。
CacheOperationSource
只是一个接口,实际注册的是 AnnotationCacheOperationSource
。
CacheOperationSource
接口只包含一个 Collection<CacheOperation> getCacheOperations(Method method, Class<?> targetClass)
方法,targetClass 和 method 分别是调用者的类和调用的方法。接口方法应当将 method 上的 Cacheable
之类的注解,将其解析为 CacheOperation
集合并返回。关于 CacheOperation
的细节见下文,点击跳转。
AbstractFallbackCacheOperationSource
实现了 CacheOperationSource
,并做了一级 CacheOperations 的缓存:
1 | private final Map<Object, Collection<CacheOperation>> attributeCache = |
getCacheOperations 方法优先从 attributeCache 缓存拿 CacheOperation 集合,否则使用 computeCacheOperations 方法计算 CacheOperation 集合并放进缓存。
computeCacheOperations 源码如下,过程加了注解:
1 | private Collection<CacheOperation> computeCacheOperations(Method method, Class<?> targetClass) { |
至于 findCacheOperations(Class<?> clazz) 和 findCacheOperations(Method method) 是由子类实现的抽象方法。
AnnotationCacheOperationSource 继承自 AbstractFallbackCacheOperationSource,根据不同的构造方式可以指定 publicMethodsOnly 和 annotationParsers,这里以默认构造函数为例。
默认构造函数只指定了 publicMethodsOnly 为 true,annotationParsers 只包含一个 SpringCacheAnnotationParser。
下一章我们再来详细看一看 SpringCacheAnnotationParser 这个类,现在我们先暂时知道 SpringCacheAnnotationParser 是一个取到 method 或 clazz 上注解并解析整理成 CacheOperation 集合的解析器类就可以了。
AnnotationCacheOperationSource 关键的功能代码如下:
1 |
|
因为 annotationParsers 只有一个 SpringCacheAnnotationParser,所以本质上就是执行了一行 return springCacheAnnotationParser.parseCacheAnnotations(method);
或 return springCacheAnnotationParser.parseCacheAnnotations(clazz);
。
我们可以看出,AnnotationCacheOperationSource 最终实现的就是取到方法或目标类上注解并将其解析为 CacheOperation 集合。
CacheOperationSource
还有一个实现 —— CompositeCacheOperationSource
表示 CacheOperationSource 的聚合,有一个私有的 CacheOperationSource 数组,其 getCacheOperations 方法会将这些 CacheOperationSource 的 getCacheOperations 的结果聚合到一个集合中返回。
CacheOperation 有三个实现类:CacheEvictOperation
、CachePutOperation
、CacheableOperation
,对应三种注解的操作。其本身具有以下的字段:
1 | private final String name; |
CacheInterceptor 继承自 CacheAspectSupport,实现了 MethodInterceptor。
MethodInterceptor 的本质是一个增强 Advice。实现 MethodInterceptor 的目的是为了可以被 Advisor 调用。
关键代码如下:
1 |
|
可以看出,其实就是调用了基类 CacheAspectSupport
的 execute
方法并拆包了一下基类抛出的异常。
因为此处过于复杂,我们放在下下一章详细讲 CacheAspectSupport
这个类。现在我们只需要知道,CacheInterceptor 提供了一个 Advice,在方法执行前后使用 cacheOperationSource 取到 CacheOperation 集合并执行对应的缓存操作。
BeanFactoryCacheOperationSourceAdvisor 继承自 AbstractBeanFactoryPointcutAdvisor,其 pointcut 使用的是固定的 CacheOperationSourcePointcut。
CacheOperationSourcePointcut 继承自 StaticMethodMatcherPointcut,被 set 了上文的 CacheOperationSource,其 match 方法实现如下:
1 |
|
也就是说,能取出 CacheOperation 的方法都会作为切入点。
也就是说,BeanFactoryCacheOperationSourceAdvisor
会对所有能取出 CacheOperation
的方法执行 CacheInterceptor
这个 Advice。
换句话说,虽然聊的是 Spring Cache 源码,但这块其实主要聊的是 Spring Framework 的 Configuration 配置和动态代理相关的内容。
@EnableCaching
众所周知,Spring Cache 的启用方式是 @EnableCaching
注解,我们看下源码:
1 | (ElementType.TYPE) |
可以看到其 Import 了一个 CachingConfigurationSelector
,同时定义了三个字段。其中 proxyTargetClass 和 mode 是用于动态代理的配置项,order 是用于启动顺序的配置项。
CachingConfigurationSelector
CachingConfigurationSelector 实现于 AdviceModeImportSelector<EnableCaching>
,根据注解中 mode 代表的 Advice 模式选择 import 不同的 bean。
已经了解 AdviceModeImportSelector 或不关心细节的读者可以跳过下面这个子模块。
AdviceModeImportSelector<T>
要聊 AdviceModeImportSelector
,必须先聊聊他实现的接口 ImportSelector
。
先看看其源码:
1 | public interface ImportSelector { |
selectImports
方法需要实现根据 importingClassMetadata
的元信息返回不同的类名数组。
不妨看看 AdviceModeImportSelector
是怎么实现的:
1 |
|
我们一句句解析。
首先 GenericTypeResolver.resolveTypeArgument(getClass(), AdviceModeImportSelector.class)
的作用是取到该类在 AdviceModeImportSelector
上的泛型类型,对于 CachingConfigurationSelector 而言,其继承的是 AdviceModeImportSelector<EnableCaching>
,所以返回的 annType
就是 EnableCaching.class
。
第二句 AnnotationConfigUtils.attributesFor(importingClassMetadata, annType)
,importingClassMetadata
表示的是导入类的元信息(比如实际使用时我们可能将 @EnableCaching
注解在某个 Configuration 类或 Starter 类上,Spring Framework 中的 ConfigurationClassParser
会在处理这个 Configuration 类或 Starter 类的所有 @Import
注解时带入该配置类本身的元信息(可见于 org.springframework.context.annotation.ConfigurationClassParser#processImports
方法),并最终传递至 selectImports
方法的 importingClassMetadata
参数),该方法会返回配置类上 @EnableCaching
注解中的实际参数 Map(AnnotationAttributes
继承自 LinkedHashMap<String, Object>
)。
后面是对结果判空,跳过。
第四句,AdviceMode adviceMode = attributes.getEnum(getAdviceModeAttributeName())
,其中 getAdviceModeAttributeName()
返回一个常量字符串 mode
,所以这句话会取到 @EnableCaching
注解中 mode 的值,保存在 adviceMode
中。
第五句,根据 adviceMode
选择不同的 bean 的类名数组,这个 selectImports
方法是 AdviceModeImportSelector
的抽象方法,交给子类实现。
第六和第七句,判空并返回。
1 |
|
方法本身很简单,交给 getProxyImports
和 getAspectJImports
选择不同的动态代理模式的类名。
先看两个配置项:
1 | private static final String PROXY_JCACHE_CONFIGURATION_CLASS = |
关于什么是 JSR 107 标准和 JCache 详细的此处不再赘述,更多资料可自行搜索。简单来说就是 JSR 107 规定了一套 JCache API 规范,如下图所示:
jsr107Present 和 jcacheImplPresent 都为 true 时表示需要启用 jcache 支持。
对于 Proxy 模式,注入的是 AutoProxyRegistrar 和 ProxyCachingConfiguration(如果启用 JCache 还注入 ProxyJCacheConfiguration):
1 | private static final String PROXY_JCACHE_CONFIGURATION_CLASS = |
对于 AspectJ 模式,注入的是 AspectJCachingConfiguration (如果启用 JCache 还注入 AspectJJCacheConfiguration):
1 | private static final String CACHE_ASPECT_CONFIGURATION_CLASS_NAME = |
通常而言,我们使用的是默认的 Proxy 方式,且不会使用到 JCache。也就是说只注入了 AutoProxyRegistrar 和 ProxyCachingConfiguration。
再说一说这个 AutoProxyRegistrar
类,其实也是一个与 cache 功能无关的类,其功能源码如下:
1 |
|
文档的注释直接翻译过来是这样:
针对给定的注册表注册,升级和配置标准自动代理创建器(APC)。通过查找在
@Configuration
具有 mode 和 proxyTargetClass 属性的导入类上声明的最接近的注释来工作。如果 mode 设置为 PROXY,则注册 APC;如果 proxyTargetClass 设置为 true,则 APC 被强制使用子类(CGLIB)代理。几个
@Enable*
注释同时公开 mode 和 proxyTargetClass 属性。重要的是要注意,大多数这些功能最终都共享一个 APC。因此,此实现并不“在乎”它找到的批注的确切含义——只要它公开了权限 mode 和 proxyTargetClass 属性,就可以对 APC 进行相同的注册和配置。
看一下代码,头两行与 AdviceModeImportSelector 的 selectImports 实现类似,由 candidate 拿到注解里的参数。
接下来判断需要存在 mode 和 proxyTargetClass 参数且类型分别是 AdviceMode 和 Boolean,则置 candidateFound 标识位为 true,说明找到了对应的自动代理配置的注解。
如果 mode 是 PROXY 模式,则调用 AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry)
方法,如果 proxyTargetClass (为 true 时强制全部使用 CGLIB,为 false 时对实现了接口的使用 JDK 动态代理,没有接口的使用 CGLIB),再调用 AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry)
方法。
这个 AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry)
方法内部追踪下去就是调用了一句 registerOrEscalateApcAsRequired(InfrastructureAdvisorAutoProxyCreator.class, registry, null)
。内部功能代码:
1 | public static final String AUTO_PROXY_CREATOR_BEAN_NAME = |
其中这个 findPriorityForClass 就是取了 class 在数组里的下标:
1 | private static final List<Class<?>> APC_PRIORITY_LIST = new ArrayList<Class<?>>(3); |
也就是说,registerOrEscalateApcAsRequired
方法会将 cls
注册为 org.springframework.aop.config.internalAutoProxyCreator
,如果再次注册了 AspectJAwareAdvisorAutoProxyCreator
乃至 AnnotationAwareAspectJAutoProxyCreator
,那么后者会顶替前者成为 internalAutoProxyCreator。同级别乃至更低级别则不会再顶替。
注:AnnotationAwareAspectJAutoProxyCreator
会在启用 @EnableAspectJAutoProxy
注解时注入。
而被注册的这个 InfrastructureAdvisorAutoProxyCreator
不再深挖,简单来说他会只注册所有定义上有 role = BeanDefinition.ROLE_INFRASTRUCTURE
的 advisor bean。
如果 proxyTargetClass
时执行的 AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry)
定义如下:
1 | public static void forceAutoProxyCreatorToUseClassProxying(BeanDefinitionRegistry registry) { |
简单来说就是在 bean 定义上添加了一条 proxyTargetClass=true
的属性。
包含几个关键配置对象可供配置:
1 | protected AnnotationAttributes enableCaching; |
1 | (name = CacheManagementConfigUtils.CACHE_ADVISOR_BEAN_NAME) |
包含两个方法的声明。
1 | public interface CacheAnnotationParser { |
能够将 class 或 method 的 cache 注解解析为 CacheOperation
操作。
SpringCacheAnnotationParser
实现了 CacheAnnotationParser
。同时还实现了 Serializable
。
两个 parseCacheAnnotations
方法的内部实现,都是首先通过 getDefaultCacheConfig(type)
(对于 method
,参数取 method.getDeclaringClass()
)取一个默认的 cache 配置,然后走同一套 parseCacheAnnotations(defaultConfig, type)
逻辑。
基本上调用方只有 AnnotationCacheOperationSource
。
通过 getAllMergedAnnotations
方法,将类型(Class<?>
或 Method
)的 Cacheable
、CacheEvict
、CachePut
、Caching
注解分别取出,分别通过 parseCacheableAnnotation
、parseEvictAnnotation
、parsePutAnnotation
、parseCachingAnnotation
方法解析为成 CacheOperation,最终合并为一个集合。
前三个 parseXXXAnnotation 的方法基本类似,取出注解中的参数(name、cacheName、key 等等,前三个注解略有不同),与 defaultConfig 合并,校验参数并返回。parseCachingAnnotation
是前三个注解的组合注解,所以将其内部的三种分别取出,再执行前三个 parseXXXAnnotation 方法。代码如下:
1 | CacheableOperation parseCacheableAnnotation(AnnotatedElement ae, DefaultCacheConfig defaultConfig, Cacheable cacheable) { |
最终校验
CacheableOperation
时,key 和 keyGenerator 至少需要有一个定义,cacheManager 和 cacheResolver 至少需要有一个定义。
只定义了一个方法,用于获取 CacheOperation 集合
1 | Collection<CacheOperation> getCacheOperations(Method method, Class<?> targetClass); |
做了一层缓存。
由 computeCacheOperations(Method method, Class<?> targetClass)
计算 CacheOperation 集合。
取 class 的 CacheConfig 注解作为 DefaultCacheConfig
,没有则全为 null。
也就是说,Method 的 CacheConfig 定义需要在声明它的 class 上。
方法声明为:
1 | private Collection<CacheOperation> computeCacheOperations(Method method, Class<?> targetClass) |
首先,如果需要的话,过滤只考虑 public 方法。(默认需要过滤)
其次,通过 ClassUtils.getMostSpecificMethod
和 BridgeMethodResolver.findBridgedMethod
找到最合适的 method 定义。
桥接方法是为了兼容 Java 1.5 没有泛型语义时允许传递 Object 的问题而由编译器自动生成的方法。
先使用 findCacheOperations(Class<?> clazz)
尝试从方法上解析 cache 操作,有则直接返回。没有则尝试使用 findCacheOperations(Method method)
从定义方法的类上解析,有则直接返回。
都取不到则尝试从原方法上用相同的流程解析。
都解析不到则返回 null。
AnnotationCacheOperationSource
继承了 AbstractFallbackCacheOperationSource
并实现了 Serializable
。
AnnotationCacheOperationSource 会读取 Spring 的 Cacheable
、 CachePut
和 CacheEvict
批注,并将相应的 cache operation 定义公开给 Spring 的缓存基础结构。此类也可以用作自定义 CacheOperationSource 的基类。
内部定义了一个 Set<CacheAnnotationParser> annotationParsers
保存用到的注解分析器。
提供了自定义是否只使用 public 方法和自定义缓存注解解析器(CacheAnnotationParser
)的构造方法。默认只解析 public 方法,且解析器集合只有上文提到的 SpringCacheAnnotationParser
。
determineCacheOperations(provider)
确定给定 CacheOperationProvider
的 CacheOperation
。该实现委托配置的 CacheAnnotationParser
(默认为 SpringCacheAnnotationParser)来将已知的注解解析为 Spring 的元 attribute 类。可以重写以支持带有 caching metadata 的自定义注解。参数 provider
是要使用的 cache operation 提供者,类型为 CacheOperationProvider
。
定义了基类使用的 findCacheOperations
方法,实现是遍历所有 annotationParsers
,调用其 parseCacheAnnotations
,然后将 cache 操作合并为一个 set。
这个类,最终对外提供的是定义在 AbstractFallbackCacheOperationSource
下的 getCacheOperations(Method method, Class<?> targetClass)
方法。用于 CacheAspectSupport
和 CacheOperationSourcePointcut
。
StaticMethodMatcherPointcut -> Pointcut
StaticMethodMatcherPointcut -> StaticMethodMatcher -> MethodMatcher
是一个抽象类,继承自 StaticMethodMatcherPointcut,实现了 Serializable。
有一个待实现的 getCacheOperationSource
方法,以返回缓存操作源。
实现了基类的 match 方法。
1 |
|
也就是说,如果 cacheOperationSource
非空且 cacheOperationSource.getCacheOperations(method, targetClass)
取到了一个或多个 cache 操作则可以匹配上。
缓存操作的调用上下文类,提供四个接口:
1 | public interface CacheOperationInvocationContext<O extends BasicOperation> { |
泛型的基类定义如下:
1 | public interface BasicOperation { |
1 | public interface CacheResolver { |
抽象类,实现了 CacheResolver 接口,框架提供了两个默认实现 SimpleCacheResolver 和 NamedCacheResolver。
区别在于 SimpleCacheResolver 取 Operation 上的 cacheNames,NamedCacheResolver 手动指定 cacheNames。
而后都遍历 cacheNames 执行 CacheManager.getCache(cacheName) 并整合至同一集合并返回。
对外提供了 resolveCaches 方法,主要用在 CacheAspectSupport
。
在基本的 Cache
类的 doGet、doPut、doEvict 方法外包了一层 try-catch 并定义了异常日志的打印方式。默认直接抛出异常。
抽象类,CacheAspectSupport 继承自 AbstractCacheInvoker,还实现了 BeanFactoryAware, InitializingBean, SmartInitializingSingleton。
涉及到众多需要 set 的依赖:
1 | private final Map<CacheOperationCacheKey, CacheOperationMetadata> metadataCache = |
定义了一些对 Optional 的解包逻辑。
关键是 execute 方法。真正在方法执行前后对缓存执行对应操作。
1 | private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) { |
继承自 CacheAspectSupport,实现了 MethodInterceptor 和 Serializable
将 CacheAspectSupport 父类的 execute 方法包装为接口的 invoke 方法的实现。
AbstractBeanFactoryPointcutAdvisor -> AbstractPointcutAdvisor -> PointcutAdvisor -> Advisor
继承自 AbstractBeanFactoryPointcutAdvisor。
getPointcut 方法返回一个 CacheOperationSourcePointcut 的实现,getCacheOperationSource 返回用户自定义的 CacheOperationSource。
注册时代码如下:
1 | (name = CacheManagementConfigUtils.CACHE_ADVISOR_BEAN_NAME) |
再次声明:本文所述的 2PC、3PC、TCC 可能与网络上某些文章提到的不太一致(事实上这些文章之间也不统一),个人认为这种概念上的争论不是很重要,本文试图描述的的是一种实际的、可实现、具有实用价值的 2PC、3PC 或 TCC 框架。
个人认为,TCC 是一种实现,Github 上有诸多具体的实现,例如 https://github.com/changmingxie/tcc-transaction 。
TCC 指的就是 Try、Confirm、Cancel 三个操作,基本类似两阶段提交。由事务管理方发起向所有参与者发起 try 请求,根据 try 请求的结果决定全部 confirm 或是全部 cancel。
TCC 框架一般需要使用数据库持久化记录事务数据,跟踪整个事务的执行状态,并在事务失败后补偿重试。具有一定的容灾能力。
TCC 不仅可以认为是两阶段事务的实现,在之前加上资源检查的步骤(也就是上文所说的第 0 步),也同样可以认为是三阶段事务的实现。
在上一篇也提到过,所有操作 Try、Confirm、Cancel 三个方法均需满足保证幂等。一旦发生网络波动重试、或事务补偿执行,不幂等的接口重复执行后便会有数据正确性的风险。
在 TCC 框架内,所有的参与者的业务逻辑都需按照二阶段设计。一阶段锁定和预备资源、二阶段执行提交(confirm)或释放资源(cancel)。
仍然以用户购买商品举例,假设用户账户余额为 10 元,同时下了价值 2 和 3 元的两单。一阶段的锁定应当是分别进行的,也就是说,如果两单同时执行一阶段,一共会有 5 元成为冻结金额。(与数据库的事务隔离不同,所以称之为事务不隔离)
考虑一种情况,如果执行者由于网络问题并未收到过阶段 1 的 Try 请求,却收到了阶段 2 的 Cancel 请求(不可能是 Confirm,想想为什么)。这种情况下就是我们所说的“空取消”。应当跳过执行。
这种情况比较少见。在某些极端情况下,Cancel 可能比 Try 先到达(或先被处理)。由于先到达的 Cancel 请求被当做了“空取消”处理了,所以只要在“空取消”时短暂记录事务,对后到的 try 拒绝处理就可以了。
以 tcc-transaction 为例。
包含几个关键配置项:
0 */1 * * * ?
,每分钟执行一次。OptimisticLockException.class
和 SocketTimeoutException.class
由 Quartz 调度事务恢复定时任务,并禁止并发。
事务在重试时会乐观锁更新,同时只有一个应用节点能更新成功。
在需要极致响应的情况下,一阶段结束后可以立刻返回,将二阶段交给线程池或其他异步方式执行。因为一阶段收到所有返回后,就已经可以确认事务能否执行,接下来交给异步任务、失败重试和事务恢复机制即可。
]]>想聊聊分布式事务。
看了网上的一些说法,仔细思考之后感觉都不大统一,有些就是不对。本文加入了一些自己的思考,讨论了一些实现中的细节,如果不对欢迎指正。
先说说两阶段和三阶段提交吧。
我这里说的两阶段提交,区别于网络上某些文章里提到的显然不实用的两阶段实现,是考虑到超时、异常恢复的两阶段提交。
各系统的所有操作应当保证幂等。
具体的流程图不再画(网上随便搜搜就有),简单描述一下就是两步:
发起事务:事务的发起者提出一个请求(比如用户下单购买某个商品),要求其依赖服务(也就是事务的执行者)响应请求(比如通知优惠券业务锁定使用的优惠券、通知支付业务冻结付款金额、通知仓储服务冻结库存等等)
当所有依赖方都回复确认之后,事务的准备阶段完毕。
确认/取消事务:当请求得到所有依赖服务的成功确认后,事务的发起者通知所有执行者确认(confirm)事务;如果第一步中只要有一个执行者返回失败,则取消(cancel)事务。
对于第二步,有些文章中的简单的二阶段提交是不需要执行者回复的,个人认为这意味着发起者无法确认第二步事务(无论是 confirm 还是 cancel 操作)有没有成功执行,所以个人认为需要确认。下文按有确认进行讨论。
更进一步,对于执行者而言,因为第一步的锁定返回了成功,所以第二步的确认只能是成功,不允许失败。执行者应想办法重试并保证成功。如果失败则意味着出现了系统的数据不一致。
以上的流程在是理想情况下。
当考虑到网络异常等情况,会存在三个问题:
对执行者而言:如果没有收到第二步事务,该如何处理?此时的执行者会一直锁定资源等待第二步事务。
对发起者而言:如果第一步中没有收到回复,该如果处理?此时的发起者无法得知是否所有执行者都成功锁定了资源。
对发起者而言:如果第二步中没有收到回复,该如果处理?此时的发起者无法得知是否所有执行者都成功确认了事务。
执行者没有收到第一步事务,对执行者而言是无感知的。所以没有这个问题。
依次回答这三个问题:
执行者没有收到第二步事务,有三种处理方案:第一种是一直锁定资源等待,第二种是超时 confirm 事务,第三种是超时 cancel 事务。对于两阶段提交而言,其他执行者在第一步是有可能返回失败的,所以显然强行 confirm 会有风险,第三种更为合理。此处有“应当 confirm 但因为网络或其他问题而没有收到,最终执行了超时 cancel”的风险,会导致数据不一致。
发起者第一步中没有收到回复,也存在两种策略:要么超时重试(再次提出事务),要么超时后当做返回失败处理。这两种可以组合使用,即多次超时重试后仍无回复则当做返回失败处理。
发起者第二步中没有收到回复,和问题 2 的处理策略类似,多次超时重试后仍无返回说明出现了异常,但不同的是这个异常是一个无法回滚的异常,意味着系统中可能出现了数据不一致,可能需要其他(很可能是人工)方式修复数据。
对于问题 3 ,因为在问题 1 的回答中我们默认执行者超时会 cancel 事务,所以当发起者第二步提出的是 cancel 时不会有什么问题。换句话说,当发起者在第二步提出 confirm 而没有收到回复时可能会出现数据不一致。
当执行过程中发生异常(比如宕机),事务应当可以重试。
对发起者而言:如果在第一步发生异常:部分执行者锁定了资源,而另一部分从未收到过事务请求。由于执行者会默认超时 cancel,所以发起者发起 cancel 后(或不处理,直接等待超时)重新发起新事务即可。
对发起者而言:如果在第二步发生异常:如果执行的是 cancel,则无需重试,当做成功即可(当然也可以重试)。如果执行的是 confirm,则可能发生部分机器成功 confirm,部分机器由于没有收到 confirm,默认超时 cancel 请求,从而数据不一致的风险。
对执行者而言:如果在第一步发生异常:尽量返回失败即可,超时发起者会重试/cancel 请求。不会有什么风险。
对执行者而言:如果在第二步发生异常:尽量重试并保证成功。如果执行的是 confirm,说明第一步的锁定返回了成功,所以第二步的确认只能是成功。如果是 cancel,则更应当自行重试保证资源释放。
在思考前两章问题的过程中,我们意识到了这个流程所存在的问题:
最严重的风险,如果发起者在第二步 confirm 的过程中出现了异常、或由于网络问题部分执行者没有收到 confirm,那么会出现数据不一致的问题。
第一步操作会锁定资源,然而有可能操作不成功,需要释放资源。这种反复的“锁定-释放”降低了并发。
名词上的修改没太多意义(cancel -> rollback,confirm -> doCommit)
三阶段提交在两阶段提交上的改进就是在之前多了一步:
在锁定资源之前先进行查询,确认是否可提交。我们姑且将其称之为第 0 步。
好处是什么?
还是以之前“用户下单购买某个商品”为例。对于这个场景,第 0 步会检查向优惠券服务检查优惠券是否可用、向支付业务检查账户余额是否足够、向仓储服务检查库存是否足够等。
只有当第 0 步全部返回成功时,才会执行第一步的锁定资源。这时的第一步也几乎可以全部返回成功(只有并发情况下会失败)。
因此,对于执行者而言,如果一直没有收到第二步(实际上的第三步)的事务,超时可以默认执行 confirm 操作。大多数情况下都会成功避免数据不一致。(只有并发竞争情况下有可能失败)
简而言之,三阶段提交相比两阶段提交多了第 0 步检查是否可提交。执行者的默认超时行为从 cancel 改为 confirm。
我们可以看到,三阶段提交的确成功提高了并发,降低了反复的“锁定-释放”的可能。
然而他并没有完全解决二阶段提交数据不一致的问题,只是极大概率避免了数据不一致的可能性。在极端情况下:由于高并发,多个请求同时通过了第 0 步检查,部分却在第 1 步锁定失败,本应 cancel 却因为网络或其他问题导致部分(或全部)执行者没有收到 cancel 命令默认 confirm 了事务,导致了数据不一致。
下一章聊聊 TCC。
]]>接下来到 Spring framework core 的剩余部分 —— 空指针安全、数据缓冲区和编解码器
尽管 Java 不允许您使用其类型系统来表示空指针安全性,但 Spring 框架现在在 org.springframework.lang
包中提供了以下注释,以使您声明 API 和字段的空性:
@Nullable
:表示特定参数,返回值或字段可以为 null
的注释。
@NonNull
:表示特定参数,返回值或字段不能为 null
的注释(@NonNullApi
和 @NonNullFields
的参数/返回值和字段不需要)。
@NonNullApi
:程序包级别的注释,它声明非 null
为参数和返回值的默认语义。
@NonNullFields
:程序包级别的注释,它声明非 null 为字段的默认语义。
Spring 框架本身利用了这些注释,但是它们也可以在任何基于 Spring 的 Java 项目中使用,以声明 null 安全的 API 和可选的 null 安全的字段。尚不支持泛型类型参数,varargs 和数组元素的可空性,但应在即将发布的版本中使用它们,有关最新信息,请参见 SPR-15942。可空性声明预计将在 Spring Framework 版本之间进行微调,包括次要版本。在方法主体内部使用的类型的可空性超出了此功能的范围。
其他常见的库(如 Reactor 和 Spring Data)提供了使用类似可空性设置的空安全 API,从而为 Spring 应用程序开发人员提供了一致的总体体验。
除了为 Spring Framework API 可空性提供显式声明外,IDE(例如 IDEA 或 Eclipse)还可以使用这些批注提供与空安全有关的有用警告,以避免在运行时出现 NullPointerException
。
由于 Kotlin 原生支持 null 安全,因此它们还用于在 Kotlin 项目中使 Spring API 为 null 安全。 Kotlin 支持文档中提供了更多详细信息。
Spring 注释使用 JSR 305 注释(静止但广泛使用的 JSR)进行元注释。 JSR-305 元注释使工具供应商(如 IDEA 或 Kotlin)以通用方式提供了空安全支持,而无需对 Spring 注释进行硬编码支持。
既不需要也不建议向项目类路径添加 JSR-305 依赖项以利用 Spring 空安全 API。只有诸如在其代码库中使用空安全注释的基于 Spring 的库之类的项目,才应添加 com.google.code.findbugs:jsr305:3.0.2
(具有 compileOnly
Gradle 配置或 Maven provided
的范围),以避免编译警告。
Java NIO 提供了 ByteBuffer
,但是许多库在上层构建了自己的字节缓冲区 API,特别是对于网络操作,其中重用缓冲区和/或使用直接缓冲区对性能有利。例如,Netty 具有 ByteBuf
层次结构,Undertow 使用 XNIO,Jetty 使用具有要释放的回调的池化字节缓冲区,等等。spring-core
模块提供了一组抽象,可以与各种字节缓冲区 API 配合使用,如下所示:
DataBufferFactory
抽象数据缓冲区的创建。DataBuffer
表示一个字节缓冲区,可以将其合并。DataBufferUtils
提供了用于数据缓冲区的实用程序方法。DataBufferFactory
DataBufferFactory
用于通过以下两种方式之一创建数据缓冲区:
DataBuffer
的实现可以按需增长和缩小,该容量也会更有效。byte[]
或 java.nio.ByteBuffer
,它们用 DataBuffer
实现装饰给定的数据,并且不涉及分配。请注意,WebFlux 应用程序不会直接创建 DataBufferFactory
,而是通过客户端的 ServerHttpResponse
或 ClientHttpRequest
访问它。工厂的类型取决于基础客户端或服务器,例如 NettyDataBufferFactory
用于 Reactor Netty,DefaultDataBufferFactory
用于其他。
DataBuffer
DataBuffer
接口提供与 java.nio.ByteBuffer
类似的操作,但还带来了一些其他好处,其中一些是受 Netty ByteBuf
启发的。以下是部分好处:
flip()
在读取和写入之间交替。java.lang.StringBuilder
一样,容量可以按需扩展。PooledDataBuffer
进行缓冲池和引用计数。java.nio.ByteBuffer
,InputStream
或 OutputStream
查看。PooledDataBuffer
如 Javadoc 中针对 ByteBuffer
所述,字节缓冲区可以是直接的也可以是非直接的。直接缓冲区可以驻留在 Java 堆之外,从而无需复制本机 I/O 操作。这使得直接缓冲区对于通过套接字接收和发送数据特别有用,但直接创建和释放它们的成本也更高,这导致了缓冲池的想法。
PooledDataBuffer
是 DataBuffer
的扩展,可帮助进行引用计数,这对于字节缓冲区池至关重要。它是如何工作的?分配 PooledDataBuffer
时,引用计数为 1。调用 keep()
会增加计数,而调用 release()
会减少计数。只要计数大于 0,就保证不会释放缓冲区。当计数减少到 0 时,可以释放池中的缓冲区,这实际上意味着将为缓冲区保留的内存返回到内存池。
请注意,在大多数情况下,与其直接在 PooledDataBuffer
上进行操作,不如在 DataBufferUtils
中使用方便的方法,该方法仅在为 PooledDataBuffer
的实例时才将释放或保留应用于 DataBuffer
。
8.4。 DataBufferUtils
DataBufferUtils
提供了许多实用程序方法来对数据缓冲区进行操作:
InputStream
或 NIO channel
转换为 Flux<DataBuffer>
,反之亦然,将 Publisher<DataBuffer>
转换为 OutputStream
或 NIO channel
。PooledDataBuffer
的实例,则释放或保留 DataBuffer
的方法。org.springframework.core.codec 包提供以下策略接口:
Publisher<T>
编码为数据缓冲区流。Publisher<DataBuffer>
解码为更高级别的对象流。spring-core
模块提供 byte[]
,ByteBuffer
,DataBuffer
,Resource
和 String
编码器和解码器实现。spring-web
模块添加了 Jackson JSON,Jackson Smile,JAXB2,Protocol Buffers 和其他编码器和解码器。请参阅 WebFlux 部分中的编解码器。
DataBuffer
使用数据缓冲区时,必须特别小心以确保释放缓冲区,因为它们可能会被合并。我们将使用编解码器来说明其工作原理,但是这些概念会更普遍地应用。让我们看看编解码器在内部必须执行哪些操作来管理数据缓冲区。
在创建更高级别的对象之前,Decoder
是最后一个读取输入数据缓冲区的对象,因此,它必须按以下方式释放它们:
请注意,DataBufferUtils#join
提供了一种安全有效的方法来将数据缓冲区流聚合到单个数据缓冲区中。同样,skipUntilByteCount
和 takeUntilByteCount
是供解码器使用的其他安全方法。
Encoder
分配其他人必须读取(和释放)的数据缓冲区。因此,Encoder
无事可做。但是,如果在使用数据填充缓冲区时发生序列化错误,则 Encoder
必须小心释放数据缓冲区。例如:
1 | DataBuffer buffer = factory.allocateBuffer(); |
Encoder
的使用者负责释放其接收的数据缓冲区。在 WebFlux 应用程序中,Encoder
的输出用于写入 HTTP 服务器响应或客户端 HTTP 请求,在这种情况下,释放数据缓冲区是写入服务器响应或客户端请求的代码的责任。
请注意,在 Netty 上运行时,有用于调试缓冲区泄漏的调试选项。
]]>接下来到 Spring framework core 的第五大块 —— AOP
面向切面的编程(AOP)通过提供另一种思考程序结构的方式来补充面向对象的编程(OOP)。OOP 中模块化的关键单元是类,而在 AOP 中模块化是切面。切面使关注点(例如事务管理)的模块化可以跨越多种类型和对象。(这种关注在 AOP 文献中通常被称为“跨领域”关注。)
Spring 的关键组件之一是 AOP 框架。 尽管 Spring IoC 容器不依赖于 AOP(这意味着您不需要的话就不需要使用 AOP),但 AOP 是对 Spring IoC 的补充,以提供功能非常强大的中间件解决方案。
具有 AspectJ 切入点的 Spring AOP
Spring 提供了使用基于模式的方法或
@AspectJ
批注样式来编写自定义切面的简单而强大的方法。这两种样式都提供了完全类型化的 advice ,并使用了 AspectJ 切入点语言,同时仍使用 Spring AOP 进行编织。本章讨论基于架构和基于
@AspectJ
的 AOP 支持。 下一章将讨论较低级别的 AOP 支持。
AOP 在 Spring 框架中用于:
提供声明式企业服务。此类服务中最重要的是声明式事务管理。
让用户实现自定义切面,并用 AOP 补充其对 OOP 的使用。
如果您只对通用声明性服务或其他预包装的声明性中间件服务(例如池)感兴趣,则无需直接使用 Spring AOP,并且可以跳过本章的大部分内容。
让我们首先定义一些主要的 AOP 概念和术语。这些术语不是特定于 Spring 的。不幸的是,AOP 术语并不是特别直观。但是,如果使用 Spring 自己的术语,将会更加令人困惑。
Spring AOP 包括以下类型的 advice :
Around advice 是最通用的 advice 。由于 Spring AOP 与 AspectJ 一样,提供了各种 advice 类型,因此我们 advice 您使用功能最弱的 advice 类型,以实现所需的行为。例如,如果您只需要使用方法的返回值更新缓存,则最好使用 after returning advice 而不是 around advice ,尽管 around advice 可以完成相同的事情。使用最具体的 advice 类型可提供更简单的编程模型,并减少出错的可能性。例如,您不需要在用于 around advice 的 JoinPoint
上调用 proceed()
方法,因此,您不会失败。
所有 advice 参数都是静态类型的,因此您可以使用适当类型(例如,从方法执行返回的值的类型)而不是 Object
数组的 advice 参数。
切入点匹配的连接点的概念是 AOP 的关键,它与仅提供拦截功能的旧技术有所不同。切入点使 advice 的目标独立于面向对象的层次结构。例如,您可以将提供声明性事务管理的 around advice 应用于跨越多个对象(例如服务层中的所有业务操作)的一组方法。
Spring AOP 是用纯 Java 实现的。不需要特殊的编译过程。 Spring AOP 不需要控制类加载器的层次结构,因此适合在 Servlet 容器或应用程序服务器中使用。
Spring AOP 当前仅支持方法执行连接点( advice 在 Spring Bean 执行方法上)。尽管可以在不破坏核心 Spring AOP API 的情况下添加对字段拦截的支持,但并未实现字段拦截。如果需要 advice 字段访问和更新连接点,请考虑使用诸如 AspectJ 之类的语言。
Spring AOP 的 AOP 方法不同于大多数其他 AOP 框架。目的不是提供最完整的 AOP 实现(尽管 Spring AOP 相当强大)。相反,其目的是在 AOP 实现和 Spring IoC 之间提供紧密的集成,以帮助解决企业应用程序中的常见问题。
因此,例如,通常将 Spring Framework 的 AOP 功能与 Spring IoC 容器结合使用。通过使用常规 bean 定义语法来配置切面(尽管这允许强大的“自动代理”功能)。这是与其他 AOP 实现的关键区别。使用 Spring AOP 不能轻松或高效地完成某些事情,例如 advice 非常细粒度的对象(通常是域对象)。在这种情况下,AspectJ 是最佳选择。但是,我们的经验是,Spring AOP 为 AOP 可以解决的企业 Java 应用程序中的大多数问题提供了出色的解决方案。
Spring AOP 从未努力与 AspectJ 竞争以提供全面的 AOP 解决方案。我们认为,基于代理的框架(如 Spring AOP)和成熟的框架(如 AspectJ)都是有价值的,它们是互补的,而不是竞争。 Spring 无缝地将 Spring AOP 和 IoC 与 AspectJ 集成在一起,以在基于 Spring 的一致应用程序架构中支持 AOP 的所有使用。这种集成不会影响 Spring AOP API 或 AOP Alliance API。 Spring AOP 仍然向后兼容。请参阅下一章,以讨论 Spring AOP API。
Spring 框架的中心宗旨之一是非侵入性。这是一个想法,您不应被迫将特定于框架的类和接口引入业务或域模型。但是,在某些地方,Spring Framework 确实为您提供了将特定于 Spring Framework 的依赖项引入代码库的选项。提供此类选项的理由是,在某些情况下,以这种方式阅读或编码某些特定功能可能会变得更加容易。但是,Spring 框架(几乎)总是为您提供选择:您可以自由地就哪个选项最适合您的特定用例或场景做出明智的决定。
与本章相关的一种选择是选择哪种 AOP 框架(以及哪种 AOP 样式)。您可以选择 AspectJ 和/或 Spring AOP。您也可以选择 @AspectJ 注释样式方法或 Spring XML 配置样式方法。本章选择首先介绍 @AspectJ 风格的方法这一事实不应被视为表明 Spring 团队比 Spring XML 配置风格更喜欢 @AspectJ 注释风格的方法。
有关每种样式的“whys and wherefores”的更完整讨论,请参见[选择要使用的 AOP 声明样式](https://docs.spring.io/spring/docs/5.3.0-SNAPSHOT/spring-framework-reference/core.html#aop-choosing]。
Spring AOP 默认将标准 JDK 动态代理用于 AOP 代理。这使得可以代理任何接口(或一组接口)。
Spring AOP 也可以使用 CGLIB 代理。 这对于代理类而不是接口是必需的。默认情况下,如果业务对象未实现接口,则使用 CGLIB。由于对接口而不是对类进行编程是一种好习惯,因此业务类通常实现一个或多个业务接口。在那些需要 advice 在接口上未声明的方法或需要将代理对象作为具体类型传递给方法的情况下(在极少数情况下),可以强制使用 CGLIB。
掌握 Spring AOP 是基于代理的这一事实很重要。 请参阅了解 AOP 代理以全面了解此实现细节的实际含义。
@AspectJ 是一种将切面声明为带有注释的常规 Java 类的样式。@AspectJ 样式是 AspectJ 项目在 AspectJ 5 版本中引入的。 Spring 使用 AspectJ 提供的用于切入点解析和匹配的库来解释与 AspectJ 5 相同的注释。 但是,AOP 运行时仍然是纯 Spring AOP,并且不依赖于 AspectJ 编译器或编织器。
使用 AspectJ 编译器和 weaver 可以使用完整的 AspectJ 语言,有关内容在 在 Spring Applications 中使用 AspectJ 进行了讨论。
要在 Spring 配置中使用 @AspectJ 切面,您需要启用 Spring 支持以基于 @AspectJ
切面配置 Spring AOP,并基于这些切面是否 advice 对 Bean 进行自动代理。 通过自动代理,我们的意思是,如果 Spring 确定一个或多个切面 advice 一个 bean,它会自动为该 bean 生成一个代理来拦截方法调用并确保按需执行 advice 。
可以使用 XML 或 Java 样式的配置来启用 @AspectJ 支持。 无论哪种情况,您都需要确保 AspectJ 的 Aspectjweaver.jar
库位于应用程序的类路径(版本 1.8 或更高版本)上。 该库在 AspectJ 发行版的 lib 目录中或从 Maven Central 存储库中可用。
要通过 Java @Configuration
启用 @AspectJ
支持,请添加 @EnableAspectJAutoProxy
批注,如以下示例所示:
1 |
|
要通过基于 XML 的配置启用 @AspectJ 支持,请使用 aop:aspectj-autoproxy
元素,如以下示例所示:
1 | <aop:aspectj-autoproxy/> |
假定您使用基于 XML Schema 的配置中所述的架构支持。 有关如何在 aop 名称空间中导入标签的信息,请参见 AOP 模式。
启用 @AspectJ 支持后,Spring 会自动检测在应用程序上下文中使用 @AspectJ 切面(具有 @Aspect 批注)的类定义的任何 bean,并用于配置 Spring AOP。 接下来的两个示例显示了一个不太有用的切面所需的最小定义。
两个示例中的第一个示例显示了应用程序上下文中的常规 bean 定义,该定义指向具有 @Aspect 批注的 bean 类:
1 | <bean id="myAspect" class="org.xyz.NotVeryUsefulAspect"> |
这两个示例中的第二个示例显示了 NotVeryUsefulAspect
类定义,该类定义使用 org.aspectj.lang.annotation.Aspect
注释进行注释;
1 | package org.xyz; |
切面(使用 @Aspect 注释的类)可以具有方法和字段,与任何其他类相同。它们还可以包含切入点, advice 和介绍(类型间)声明。
通过组件扫描自动检测切面
您可以将切面类注册为 Spring XML 配置中的常规 bean,也可以通过类路径扫描自动检测它们——与其他任何 Spring 管理的 bean 一样。 但是,请注意,@Aspect 注释不足以在类路径中进行自动检测。为此,您需要添加一个单独的 @Component 批注(或者,或者,按照 Spring 的组件扫描程序的规则,有条件的自定义构造型批注)。
向其他切面提供 advice ?
在 Spring AOP 中,切面本身不能成为其他切面的 advice 目标。 类上的 @Aspect 注释将其标记为一个切面,因此将其从自动代理中排除。
切入点确定了感兴趣的连接点,从而使我们能够控制何时执行 advice 。 Spring AOP 仅支持 Spring Bean 的方法执行连接点,因此您可以将切入点视为与 Spring Bean 上的方法执行匹配。 切入点声明由两部分组成:一个包含名称和任何参数的签名,以及一个切入点表达式,该切入点表达式精确确定我们感兴趣的方法执行。在 AOP 的@AspectJ 批注样式中,常规方法定义提供了切入点签名。 ,并使用 @Pointcut
批注指示切入点表达式(用作切入点签名的方法必须具有 void
返回类型)。
一个示例可能有助于使切入点签名和切入点表达式之间的区别变得清晰。 下面的示例定义一个名为 anyOldTransfer
的切入点,该切入点与任何名为 transfer
的方法的执行相匹配:
1 | "execution(* transfer(..))") // the pointcut expression ( |
形成 @Pointcut
批注的值的切入点表达式是一个常规的 AspectJ 5 切入点表达式。 有关 AspectJ 的切入点语言的完整讨论,请参见 AspectJ 编程指南(以及扩展,请参见 AspectJ 5 开发者手册)或有关 AspectJ 的书籍之一(例如 Colyer 等人的 Eclipse AspectJ,或《AspectJ in Action》 ,由 Ramnivas Laddad 撰写)。
Spring AOP 支持以下在切入点表达式中使用的 AspectJ 切入点指示符(PCD):
execution
:用于匹配方法执行的连接点。这是使用 Spring AOP 时要使用的主要切入点指示符。within
:将匹配限制为某些类型内的连接点(使用 Spring AOP 时,在匹配类型内声明的方法的执行)。this
:限制匹配到连接点(使用 Spring AOP 时方法的执行),其中 bean 引用(Spring AOP 代理)是给定类型的实例。target
:限制匹配到连接点(使用 Spring AOP 时方法的执行),其中目标对象(代理的应用程序对象)是给定类型的实例。args
:将匹配限制为连接点(使用 Spring AOP 时方法的执行),其中参数是给定类型的实例。@target
:限制匹配到连接点(使用 Spring AOP 时方法的执行)的匹配,其中执行对象的类具有给定类型的注释。@args
:限制匹配的连接点(使用 Spring AOP 时方法的执行),其中传递的实际参数的运行时类型具有给定类型的注释。@within
:限制匹配到具有给定注释的类型内的连接点(使用 Spring AOP 时,使用给定注释的类型中声明的方法的执行)。@annotation
:将匹配限制为连接点的主题(在 Spring AOP 中正在执行的方法)具有给定注释的连接点。其他切入点类型
完整的 AspectJ 切入点语言支持 Spring 不支持的其他切入点指示符:
call
,get
,set
,preinitialization
,staticinitialization
,initialization
,handler
,adviceexecution
,withincode
,cflow
,cflowbelow
,if
,@this
, 和@withincode
。在 Spring AOP 解释的切入点表达式中使用这些切入点指示符会导致抛出IllegalArgumentException
。Spring AOP 支持的切入点指示符集合可能会在将来的版本中扩展,以支持更多的 AspectJ 切入点指示符。
由于 Spring AOP 仅将匹配限制为仅方法执行连接点,因此前面对切入点指示符的讨论所给出的定义比在 AspectJ 编程指南中所能找到的要窄。此外,AspectJ 本身具有基于类型的语义,并且在执行连接点处,此对象和目标都引用同一个对象:执行该方法的对象。 Spring AOP 是基于代理的系统,可区分代理对象本身(绑定到此对象)和代理后面的目标对象(绑定到目标)。
由于 Spring 的 AOP 框架基于代理的性质,因此根据定义,不会拦截目标对象内的调用。对于 JDK 代理,只能拦截代理上的公共接口方法调用。使用 CGLIB,将拦截代理上的公共方法和受保护的方法调用(必要时甚至包可见的方法)。但是,通常应通过公共签名设计通过代理进行的常见交互。
请注意,切入点定义通常与任何拦截方法匹配。如果严格地将切入点设置为仅公开使用,即使在 CGLIB 代理方案中通过代理可能存在非公开交互,也需要相应地进行定义。
如果您的拦截需要在目标类中包括方法调用甚至构造函数,请考虑使用 Spring 驱动的本机 AspectJ 编织,而不是 Spring 的基于代理的 AOP 框架。这构成了具有不同特征的 AOP 使用模式,因此请确保在做出决定之前先熟悉编织。
Spring AOP 还支持其他名为 bean
的 PCD。使用 PCD,可以将连接点的匹配限制为特定的命名 Spring Bean 或一组命名 Spring Bean(使用通配符时)。bean
PCD 具有以下形式:
1 | bean(idOrNameOfBean) |
idOrNameOfBean
令牌可以是任何 Spring bean 的名称。提供了使用 *
字符的有限通配符支持,因此,如果为 Spring bean 建立了一些命名约定,则可以编写 bean PCD 表达式来选择它们。与其他切入点指示符一样,bean PCD 可以和与或非运算符一起使用。
bean
PCD 仅在 Spring AOP 中受支持,而在本机 AspectJ 编织中不受支持。它是 AspectJ 定义的标准 PCD 的特定于 Spring 的扩展,因此不适用于@Aspect
模型中声明的切面。
bean
PCD 在实例级别(基于 Spring bean 名称概念构建)上运行,而不是仅在类型级别(基于编织的 AOP 受其限制)上运行。基于实例的切入点指示符是 Spring 基于代理的 AOP 框架及其与 Spring bean 工厂的紧密集成的一种特殊功能,可以自然而直接地识别特定对象。
您可以使用&&
、||
、!
组合切入点表达式,您也可以按名称引用切入点表达式。以下示例显示了三个切入点表达式:
1 | "execution(public * *(..))") ( |
如果方法执行连接点表示任何公共方法的执行,则 anyPublicOperation 匹配。
如果交易模块中有方法执行,则 inTrading 匹配。
如果方法执行代表交易模块中的任何公共方法,则 tradingOperation 匹配。
最佳实践是从较小的命名组件中构建更复杂的切入点表达式,如先前所示。按名称引用切入点时,将应用常规的 Java 可见性规则(您可以看到相同类型的私有切入点,层次结构中受保护的切入点,任何位置的公共切入点,等等)。可见性不影响切入点匹配。
在使用企业应用程序时,开发人员通常希望从多个切面引用应用程序的模块和特定的操作集。我们推荐为此定义一个 “SystemArchitecture” 切面,以捕获常见的切入点表达式。这样的切面通常类似于以下示例:
1 | package com.xyz.someapp; |
您可以在需要切入点表达式的任何地方引用在此切面定义的切入点。 例如,要使服务层具有事务性,您可以编写以下内容:
1 | <aop:config> |
在基于模式的 AOP 支持中讨论了 <aop:config>
和 <aop:advisor>
元素。事务管理中讨论了事务元素。
Spring AOP 用户可能最常使用执行切入点指示符。执行表达式的格式如下:
1 | execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?) |
除了返回类型模式(前面的代码片段中的 ret-type-pattern
),名称模式和参数模式以外的所有部分都是可选的。返回类型模式确定该方法的返回类型必须是什么才能使连接点匹配。 *
最常用作返回类型模式。它匹配任何返回类型。仅当方法返回给定类型时,标准类型名称才匹配。名称模式与方法名称匹配。您可以将 *
通配符用作名称模式的全部或一部分。如果指定了声明类型模式,请在其后加上 .
将其连接到名称模式组件。参数模式稍微复杂一些:()
匹配不带参数的方法,而 (..)
匹配任意数量(零个或多个)的参数。 (*)
模式与采用任何类型的一个参数的方法匹配。(*,String)
与采用两个参数的方法匹配。第一个可以是任何类型,而第二个必须是 String
。有关更多信息,请查阅 AspectJ 编程指南的语言语义部分。
以下示例显示了一些常用的切入点表达式:
1 | execution(public * *(..)) |
1 | execution(* set*(..)) |
1 | execution(* com.xyz.service.AccountService.*(..)) |
1 | execution(* com.xyz.service.*.*(..)) |
1 | execution(* com.xyz.service..*.*(..)) |
1 | within(com.xyz.service.*) |
1 | within(com.xyz.service..*) |
AccountService
接口的任何连接点(仅在 Spring AOP 中执行方法):1 | this(com.xyz.service.AccountService) |
this
通常以绑定形式使用。 有关如何在 advice 正文中使代理对象可用的信息,请参阅声明 advice部分。
1 | target(com.xyz.service.AccountService) |
target
通常以绑定形式使用。有关如何使目标对象在 advice 正文中可用的信息,请参见声明 advice部分。
Serializable
的连接点(仅在 Spring AOP 中执行方法):1 | args(java.io.Serializable) |
args
通常以绑定形式使用。 有关如何使方法参数在 advice 正文中可用的信息,请参见声明 advice部分。
请注意,此示例中给出的切入点与 execution(* *(java.io.Serializable))
不同。如果在运行时传递的参数为 Serializable
,则 args
版本匹配;如果方法签名声明单个类型为 Serializable
的参数,则 execution
版本匹配。
@Transactional
批注的任何连接点(仅在 Spring AOP 中是方法执行):1 | @target(org.springframework.transaction.annotation.Transactional) |
您也可以在绑定形式中使用
@target
。有关如何使注释对象在 advice 正文中可用的信息,请参见声明 advice部分。
@Transactional
批注的任何连接点(仅在 Spring AOP 中是方法执行)1 | @within(org.springframework.transaction.annotation.Transactional) |
您也可以在绑定形式中使用
@within
。有关如何使注释对象在 advice 正文中可用的信息,请参见声明 advice部分。
@Transactional
批注的联接点(仅在 Spring AOP 中是方法执行):1 | @annotation(org.springframework.transaction.annotation.Transactional) |
您也可以在绑定形式中使用
@annotation
。有关如何使注释对象在 advice 正文中可用的信息,请参见声明 advice部分。
1 | @args(com.xyz.security.Classified) |
您也可以在绑定形式中使用“ @args”。 请参阅“声明 advice”部分,如何使 advice 对象中的注释对象可用。
tradeService
的 Spring bean 上的任何连接点(仅在 Spring AOP 中执行方法):1 | bean(tradeService) |
Spring Bean 上具有与通配符表达式 *Service
匹配的名称的任何连接点(仅在 Spring AOP 中才执行方法):
1 | bean(*Service) |
在编译期间,AspectJ 处理切入点以优化匹配性能。检查代码并确定每个连接点是否(静态或动态)匹配给定的切入点是一个昂贵的过程。 (动态匹配意味着无法从静态分析中完全确定匹配,并且在代码中进行了测试以确定在运行代码时是否存在实际匹配)。首次遇到切入点声明时,AspectJ 将其重写为匹配过程的最佳形式。这是什么意思?基本上,切入点以 DNF(析取范式)重写,并且对切入点的组件进行排序,以便首先检查那些较便宜的组件。这意味着您不必担心理解各种切入点指示符的性能,并且可以在切入点声明中以任何顺序提供它们。
但是,AspectJ 只能使用所告诉的内容。为了获得最佳的匹配性能,您应该考虑他们试图达到的目标,并在定义中尽可能缩小匹配的搜索空间。现有的指示符自然分为三类之一:同类,作用域和上下文:
友好的指示者选择一种特殊的连接点:execution
, get
, set
, call
, 和 handler
作用域指定者选择一组感兴趣的连接点(可能是多种):within
和 withincode
上下文指示符根据上下文匹配(并可选地绑定):this
, target
, 和 @annotation
编写正确的切入点至少应包括前两种类型(种类和作用域)。您可以包括上下文指示符以根据连接点上下文进行匹配,也可以绑定该上下文以在 advice 中使用。仅提供同类的标识符或仅提供上下文的标识符是可行的,但是由于额外的处理和分析,可能会影响编织性能(使用的时间和内存)。范围指定符的匹配非常快,使用它们的使用意味着 AspectJ 可以非常迅速地消除不应进一步处理的连接点组。一个好的切入点应尽可能包括一个切入点。
advice 与切入点表达式关联,并且在切入点匹配的方法执行之前,之后或周围运行。 切入点表达式可以是对命名切入点的简单引用,也可以是就地声明的切入点表达式。
您可以使用 @Before
批注在一个切面中声明先 advice :
1 | import org.aspectj.lang.annotation.Aspect; |
如果使用就地切入点表达式,则可以将前面的示例重写为以下示例:
1 | import org.aspectj.lang.annotation.Aspect; |
当匹配的方法执行正常返回时,After returning advice 运行。 您可以使用 @AfterReturning
批注进行声明:
1 | import org.aspectj.lang.annotation.Aspect; |
您可以在同一切面内拥有多个 advice 声明(以及其他成员)。在这些示例中,我们仅显示单个 advice 声明,以集中每个 advice 的效果。
有时,您需要在 advice 正文中访问返回的实际值。 您可以使用 @AfterReturning
的形式绑定返回值以获取该访问,如以下示例所示:
1 | import org.aspectj.lang.annotation.Aspect; |
returning
属性中使用的名称必须与 advice 方法中的参数名称相对应。当方法执行返回时,返回值将作为相应的参数值传递到 advice 方法。返回子句也将匹配限制为仅返回指定类型值的方法执行(在这种情况下为 Object
,它匹配任何返回值)。
请注意,使用 after returning advice 时,不可能返回完全不同的引用。
当匹配的方法执行通过抛出异常退出时 after throwing advice 运行。 您可以使用 @AfterThrowing
批注进行声明,如以下示例所示:
1 | import org.aspectj.lang.annotation.Aspect; |
通常,您希望 advice 仅在引发给定类型的异常时才运行,并且您通常还需要访问 advice 正文中的引发异常。 您可以使用 throwing
属性来限制匹配(如果需要)(否则,请使用 Throwable
作为异常类型),并将抛出的异常绑定到 advice 参数。 以下示例显示了如何执行此操作:
1 | import org.aspectj.lang.annotation.Aspect; |
throwing
属性中使用的名称必须与 advice 方法中的参数名称相对应。 当通过抛出异常退出方法执行时,该异常将作为相应的参数值传递给 advice 方法。 throwing 子句还将匹配仅限制为抛出指定类型的异常(在这种情况下为 DataAccessException
)的方法执行。
当匹配的方法执行退出时,After (Finally) Advice 运行。 通过使用 @After
注释声明它。 之后必须准备处理正常和异常返回条件的 advice。它通常用于释放资源和类似目的。 以下示例显示了最终 advice 后的用法:
1 | import org.aspectj.lang.annotation.Aspect; |
最后一种 advice 是 around advice。around advice 在匹配方法的执行过程中“around”运行。它有机会在方法执行之前和之后进行工作,并确定何时,如何以及什至根本不执行该方法。如果需要以线程安全的方式(例如,启动和停止计时器)在方法执行之前和之后共享状态,则通常使用绕行 advice。始终使用最不可行的 advice 形式来满足您的要求(也就是说,在 advice 可以使用之前,请勿在 advice 周围使用)。
通过使用 @Around
批注来声明周围 advice。咨询方法的第一个参数必须是 ProceedingJoinPoint
类型。在 advice 的正文中,在 ProceedingJoinPoint
上调用 proceed()
会使基础方法执行。proceed
方法也可以传入 Object[]
。数组中的值用作方法执行时的参数。
当用
Object[]
调用proceed
时, 行为与 AspectJ 编译器所编译的 around advice 的行为略有不同。对于使用传统 AspectJ 语言编写的 around advice,传递给proceed
的参数数量必须与传递给 around advice 的参数数量(而不是基础连接点采用的参数数量)相匹配,并且传递给给定的参数位置会取代该值绑定到的实体的连接点处的原始值(不要担心,如果这现在没有意义)。 Spring 采取的方法更简单,并且更适合其基于代理的,仅执行的语义。如果您编译为 Spring 编写的@AspectJ 切面,并在 AspectJ 编译器和 weaver 中使用参数进行处理,则只需要意识到这种区别。有一种方法可以在 Spring AOP 和 AspectJ 之间 100%兼容,并且在下面有关 advice 参数的部分中对此进行了讨论。
以下示例显示了如何使用周围 advice:
1 | import org.aspectj.lang.annotation.Aspect; |
around advice 返回的值是该方法的调用者看到的返回值。例如,如果一个简单的缓存切面有一个值,则它可以从缓存中返回一个值,如果没有,则调用 proceed()
。请注意,在 around advice 的正文中,proceed
可能被调用一次,多次或完全不被调用。所有这些都是合法的。
Spring 提供了完全类型化的 advice,这意味着您可以在 advice 签名中声明所需的参数(如我们先前在返回和抛出示例中所看到的),而不是一直使用 Object[]
数组。 我们将在本节的后面部分介绍如何使参数和其他上下文值可用于 advice 主体。首先,我们看一下如何编写通用 advice,以了解该 advice 当前 advice 的方法。
任何 advice 方法都可以将 org.aspectj.lang.JoinPoint
类型的参数声明为它的第一个参数(请注意,需要 around advice 以声明 ProceedingJoinPoint
类型的第一个参数,该类型是 JoinPoint 的子类。JoinPoint 接口提供了一个 几种有用的方法:
getArgs()
:返回方法参数。
getThis()
:返回代理对象。
getTarget()
:返回目标对象。
getSignature()
:返回所 advice 方法的描述。
toString()
:打印有关所 advice 方法的有用描述。
有关更多详细信息,请参见 javadoc。
我们已经看到了如何绑定返回的值或异常值(在返回之后和引发 advice 之后使用)。要使参数值可用于 advice 正文,可以使用 args
的绑定形式。如果在 args
表达式中使用参数名称代替类型名称,则在调用 advice 时会将相应参数的值作为参数值传递。一个例子应该使这一点更清楚。假设您要 advice 以 Account
对象作为第一个参数的 DAO 操作的执行,并且您需要在 advice 正文中访问该帐户。您可以编写以下内容:
1 | "com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)") ( |
切入点表达式的 args(account,..)
部分有两个用途。首先,它将匹配限制为仅方法采用至少一个参数且传递给该参数的参数为 Account
实例的那些方法执行。其次,它通过 account
参数使 advice 的实际 Account
对象可用。
编写此代码的另一种方法是声明一个切入点,当切入点与匹配点匹配时“提供” Account
对象值,然后从通知中引用命名切入点。如下所示:
1 | "com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)") ( |
有关更多详细信息,请参见 AspectJ 编程指南。
代理对象(this
),目标对象(target
)和注释(@within
,@target
,@annotation
和 @args
)都可以以类似的方式绑定。接下来的两个示例显示如何匹配使用 @Auditable
注释注释的方法的执行并提取审计代码:
这两个示例中的第一个显示了 @Auditable
批注的定义:
1 | (RetentionPolicy.RUNTIME) |
这两个示例中的第二个示例显示了与 @Auditable
方法的执行相匹配的 advice:
1 | "com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)") ( |
Spring AOP 可以处理类声明和方法参数中使用的泛型。假设您具有如下通用类型:
1 | public interface Sample<T> { |
您可以通过在要拦截方法的参数类型中键入 advice 参数,将方法类型的拦截限制为某些参数类型:
1 | "execution(* ..Sample+.sampleGenericMethod(*)) && args(param)") ( |
这种方法不适用于通用集合。因此,您不能按以下方式定义切入点:
1 | "execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)") ( |
为了使这项工作有效,我们将不得不检查集合的每个元素,这是不合理的,因为我们也无法决定通常如何处理空值。要实现类似的目的,您必须定义参数 Collection<?>
并手动检查元素的类型。
通知调用中的参数绑定依赖于切入点表达式中使用的名称与通知和切入点方法签名中声明的参数名称的匹配。通过 Java 反射无法获得参数名称,因此 Spring AOP 使用以下策略来确定参数名称:
argNames
属性,您可以使用该属性来指定带注释的方法的参数名称。这些参数名称在运行时可用。以下示例显示如何使用 argNames
属性:1 | "com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)", (value= |
如果第一个参数是 JoinPoint
,ProceedingJoinPoint
或 JoinPoint.StaticPart
类型,则可以从 argNames
属性的值中忽略该参数的名称。例如,如果您修改前面的 advice 以接收连接点对象,则 argNames
属性不需要包括它:
1 | "com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)", (value= |
对 JoinPoint
,ProceedingJoinPoint
和 JoinPoint.StaticPart
类型的第一个参数给予的特殊处理对于不收集任何其他联接点上下文的 advice 实例特别方便。 在这种情况下,您可以省略 argNames
属性。 例如,以下 advice 无需声明 argNames
属性:
1 | "com.xyz.lib.Pointcuts.anyPublicMethod()") ( |
'argNames'
属性有点笨拙,因此,如果未指定 'argNames'
属性,Spring AOP 将查看该类的调试信息,并尝试从局部变量表中确定参数名称。只要已使用调试信息(至少是 '-g:vars'
)编译了类,此信息就会存在。启用此标志时进行编译的结果是:(1)您的代码更易于理解(反向工程),(2)类文件的大小略大(通常无关紧要),(3)删除未使用的本地代码的优化变量不适用于您的编译器。换句话说,通过启用该标志,您应该不会遇到任何困难。如果即使没有调试信息,AspectJ 编译器(ajc)都已编译 @AspectJ 切面,则无需添加
argNames
属性,因为编译器会保留所需的信息。
如果在没有必要调试信息的情况下编译了代码,Spring AOP 将尝试推断绑定变量与参数的配对(例如,如果切入点表达式中仅绑定了一个变量,并且 advice 方法仅接受一个参数,则配对很明显)。如果在给定可用信息的情况下变量的绑定不明确,则抛出 AmbiguousBindingException
。
如果以上所有策略均失败,则抛出 IllegalArgumentException
。
前面我们提到过,我们将描述如何编写一个在 Spring AOP 和 AspectJ 中始终有效的参数的 proceed
调用。 解决方案是确保 advice 签名按顺序绑定每个方法参数。 以下示例显示了如何执行此操作:
1 | "execution(List<Account> find*(..)) && " + ( |
在许多情况下,无论如何都要进行此绑定(如上例所示)。
当多条 advice 都希望在同一连接点上运行时会发生什么? Spring AOP 遵循与 AspectJ 相同的优先级规则来确定 advice 执行的顺序。优先级最高的 advice 首先“在途中”运行(因此,给定两条优先 advice,则优先级最高的 advice 首先运行)。从连接点“出路”时,优先级最高的 advice 将最后运行(因此,给定两条后置通知,优先级最高的 advice 将第二次运行)。
当在不同切面定义的两条 advice 都需要在同一连接点上运行时,除非另行指定,否则执行顺序是不确定的。您可以通过指定优先级来控制执行顺序。通过在切面类中实现 org.springframework.core.Ordered
接口或使用 Order 批注对其进行注释,可以通过常规的 Spring 方法来完成。给定两个切面,从 Ordered.getValue()
(或注释值)返回较低值的切面具有较高的优先级。
当在相同切面定义的两条 advice 都需要在同一连接点上运行时,其顺序是未定义的(因为无法通过反射为 javac 编译的类检索声明顺序)。考虑将这些 advice 方法折叠为每个切面类中每个连接点的一个 advice 方法,或将 advice 重构为单独的切面类,您可以在切面级别进行订购。
简介(在 AspectJ 中称为类型间声明)使切面可以声明 advice 对象实现给定的接口,并代表那些对象提供该接口的实现。
您可以使用 @DeclareParents 批注进行介绍。 此批注用于声明匹配类型具有新的父代(因此具有名称)。 例如,给定一个名为 UsageTracked 的接口和该接口名为 DefaultUsageTracked 的实现,以下切面声明服务接口的所有实现者也都实现了 UsageTracked 接口(例如,通过 JMX 公开统计信息):
1 |
|
要实现的接口由带注释的字段的类型确定。 @DeclareParents 批注的 value 属性是 AspectJ 类型的模式。 匹配类型的任何 bean 都实现 UsageTracked 接口。 请注意,在前面示例的之前 advice 中,服务 Bean 可以直接用作 UsageTracked 接口的实现。 如果以编程方式访问 bean,则应编写以下内容:
1 | UsageTracked usageTracked = (UsageTracked) context.getBean("myService"); |
这是一个高级主题。如果您刚开始使用 AOP,则可以放心地跳过它,直到以后。
默认情况下,应用程序上下文中每个切面都有一个实例。 AspectJ 将此称为单例实例化模型。 可以使用备用生命周期来定义切面。 Spring 支持 AspectJ 的 perthis
和 pertarget
实例化模型(当前不支持 percflow
,percflowbelow
和 pertypewithin
)。
您可以通过在 @Aspect
批注中指定 perthis
子句来声明 perthis
切面。 考虑以下示例:
1 | "perthis(com.xyz.myapp.SystemArchitecture.businessService())") ( |
在前面的示例中,'perthis'
子句的作用是为每个执行业务服务的唯一服务对象(每个与切入点表达式匹配的联接点绑定到 “this” 的唯一对象)创建一个切面实例。切面实例是在服务对象上首次调用方法时创建的。 当服务对象超出范围时,切面将超出范围。在创建切面实例之前,其中的任何 advice 都不会执行。创建切面实例后,在其中声明的 advice 将在匹配的连接点处执行,但是仅当服务对象是与此切面相关联的对象时才执行。有关 per
子句的更多信息,请参见 AspectJ 编程指南。
pertarget
实例化模型的工作方式与 perthis
完全相同,但是它在匹配的连接点为每个唯一目标对象创建一个切面实例。
既然您已经了解了所有组成部分是如何工作的,那么我们可以将它们放在一起做一些有用的事情。
有时由于并发问题(例如,死锁失败者),业务服务的执行可能会失败。如果重试该操作,则很可能在下一次尝试中成功。对于适合在这种情况下重试的业务(不需要为解决冲突而需要返回给用户的幂等操作),我们希望透明地重试该操作,以避免客户端看到 PessimisticLockingFailureException
。这项要求明确地跨越了服务层中的多个服务,因此非常适合通过一个切面实施。
因为我们想重试该操作,所以我们需要使用周围 advice,以便可以多次调用 proceed。 以下清单显示了基本切面的实现:
1 |
|
请注意,切面实现了 Ordered
接口,因此我们可以将切面的优先级设置为高于事务 advice(每次重试时都希望有新的事务)。 maxRetries 和 order 属性均由 Spring 配置。 advice 的主要动作发生在 doConcurrentOperation
中。 请注意,目前,我们将重试逻辑应用于每个 businessService()
。 我们尝试继续,如果失败并出现 PessimisticLockingFailureException
,则我们将再次尝试,除非我们用尽了所有重试尝试。
相应的 Spring 配置如下:
1 | <aop:aspectj-autoproxy/> |
为了完善切面,使其仅重试幂等运算,我们可以定义以下幂等注解:
1 | (RetentionPolicy.RUNTIME) |
然后,我们可以使用注释来注释服务操作的实现。切面更改为仅重试幂等操作涉及更改切入点表达式,以便仅 @Idempotent
操作匹配,如下所示:
1 | "com.xyz.myapp.SystemArchitecture.businessService() && " + ( |
如果您更喜欢基于 XML 的格式,Spring 还提供了使用新的 aop 名称空间标签定义切面的支持。支持与使用 @AspectJ
样式时完全相同的切入点表达式和 advice 类型。因此,在本节中,我们将重点放在新语法上,并使读者参考上一节中的讨论(@AspectJ
支持),以了解编写切入点表达式和 advice 参数的绑定。
要使用本节中描述的 aop 名称空间标签,您需要导入 spring-aop 模式,如基于 XML Schema 的配置中所述。有关如何在 aop 名称空间中导入标签的信息,请参见 AOP 模式。
在您的 Spring 配置中,所有切面和顾问程序元素都必须放在 <aop:config>
元素内(在应用程序上下文配置中可以有多个 <aop:config>
元素)。<aop:config>
元素可以包含切入点,顾问程序和 aspect 元素(请注意,必须按此顺序声明它们)。
<aop:config>
的配置样式大量使用了 Spring 的自动代理机制。如果您已经通过使用BeanNameAutoProxyCreator
或类似方法使用显式自动代理,则可能会导致问题(例如,未编制 advice)。推荐的用法模式是仅使用<aop:config>
样式或仅使用AutoProxyCreator
样式,并且不要混合使用。
因为我不喜欢 XML 格式,此段暂时跳过,原文参考 https://docs.spring.io/spring/docs/5.3.0-SNAPSHOT/spring-framework-reference/core.html#aop-schema
一旦确定切面是实现给定需求的最佳方法,您如何在使用 Spring AOP 或 AspectJ 以及在 Aspect 语言(代码)样式,@ AspectJ 批注样式或 Spring XML 样式之间做出选择?这些决定受许多因素影响,包括应用程序需求,开发工具和团队对 AOP 的熟悉程度。
使用最简单的方法即可。 Spring AOP 比使用完整的 AspectJ 更简单,因为不需要在开发和构建过程中引入 AspectJ 编译器/编织器。如果您只需要 advice 在 Spring bean 上执行操作,则 Spring AOP 是正确的选择。如果您需要 advice 不受 Spring 容器管理的对象(通常是域对象),则需要使用 AspectJ。如果您希望 advice 除简单方法执行之外的连接点(例如,字段 get 或设置连接点等),则还需要使用 AspectJ。
使用 AspectJ 时,可以选择 AspectJ 语言语法(也称为“代码样式”)或@AspectJ 注释样式。显然,如果您不使用 Java 5+,则已经为您做出了选择:使用代码样式。如果切面在您的设计中起着重要作用,并且您能够使用用于 Eclipse 的 AspectJ 开发工具(AJDT)插件,则 AspectJ 语言语法是首选。它更干净,更简单,因为该语言是专为编写切面而设计的。如果您不使用 Eclipse 或只有少数几个切面在您的应用程序中不起作用,那么您可能要考虑使用@AspectJ 样式,在 IDE 中坚持常规 Java 编译,并向其中添加切面编织阶段您的构建脚本。
如果您选择使用 Spring AOP,则可以选择 @AspectJ 或 XML 样式。有各种折衷考虑。
XML 样式可能是现有 Spring 用户最熟悉的,并且得到了真正的 POJO 的支持。当使用 AOP 作为配置企业服务的工具时,XML 是一个不错的选择(一个很好的测试是您是否将切入点表达式视为您可能希望独立更改的配置的一部分)。使用 XML 样式,可以说从您的配置中可以更清楚地了解系统中存在哪些切面。
XML 样式有两个缺点。首先,它没有完全将要解决的需求的实现封装在一个地方。 DRY 原则说,系统中的任何知识都应该有一个单一,明确,权威的表示形式。使用 XML 样式时,关于如何实现需求的知识会在配置文件中的后备 bean 类的声明和 XML 中分散。当您使用@AspectJ 样式时,此信息将封装在一个模块中:切面。其次,与@AspectJ 样式相比,XML 样式在表达能力上有更多限制:仅支持“单例”切面实例化模型,并且无法组合以 XML 声明的命名切入点。例如,使用@AspectJ 样式,您可以编写如下内容:
1 | "execution(* get*())") ( |
在 XML 样式中,您可以声明前两个切入点:
1 | <aop:pointcut id="propertyAccess" |
XML 方法的缺点是您无法通过组合这些定义来定义 accountPropertyAccess
切入点。
@AspectJ 样式支持其他实例化模型和更丰富的切入点组合。 它具有将切面保持为模块化单元的优势。 它还具有的优点是,Spring AOP 和 AspectJ 都可以理解 @AspectJ 切面。 因此,如果您以后决定需要 AspectJ 的功能来实现其他要求,则可以轻松地迁移到经典的 AspectJ 设置。 总而言之,Spring 团队在自定义切面更喜欢 @AspectJ 样式,而不是简单地配置企业服务。
通过使用自动代理支持,模式定义的<aop:aspect>
切面,<aop:advisor>
声明的顾问程序,甚至是同一配置中其他样式的代理和拦截器,完全可以混合 @AspectJ 样式的切面。 所有这些都是通过使用相同的基础支持机制实现的,并且可以毫无困难地共存。
Spring AOP 使用 JDK 动态代理或 CGLIB 创建给定目标对象的代理。 JDK 动态代理内置在 JDK 中,而 CGLIB 是常见的开源类定义库(重新包装到 spring-core
中)。
如果要代理的目标对象实现至少一个接口,则使用 JDK 动态代理。代理了由目标类型实现的所有接口。如果目标对象未实现任何接口,则将创建 CGLIB 代理。
如果要强制使用 CGLIB 代理(例如,代理为目标对象定义的每个方法,而不仅是由其接口实现的方法),都可以这样做。但是,您应该考虑以下问题:
要强制使用 CGLIB 代理,请将 <aop:config>
元素的 proxy-target-class
属性的值设置为 true,如下所示:
1 | <aop:config proxy-target-class="true"> |
要在使用 @AspectJ
自动代理支持时强制 CGLIB 代理,请将 <aop:aspectj-autoproxy>
元素的 proxy-target-class
属性设置为 true,如下所示:
1 | <aop:aspectj-autoproxy proxy-target-class="true"/> |
多个
<aop:config/>
部分在运行时折叠到一个统一的自动代理创建器中,该创建器将应用任何<aop:config/>
部分(通常来自不同的 XML bean 定义文件)指定的最强的代理设置。 这也适用于<tx:annotation-driven/>
和<aop:aspectj-autoproxy/>
元素。为了清楚起见,在
<tx:annotation-driven/>
,<aop:aspectj-autoproxy/>
或<aop:config/>
元素上使用proxy-target-class="true"
会强制对所有三个元素使用 CGLIB 代理。
Spring AOP 是基于代理的。 在编写自己的切面或使用 Spring Framework 随附的任何基于 Spring AOP 的切面之前,掌握最后一条语句实际含义的语义至关重要。
首先考虑以下情况:您有一个普通的,未经代理的,无特殊要求的直接对象引用,如以下代码片段所示:
1 | public class SimplePojo implements Pojo { |
如果在对象引用上调用方法,则直接在该对象引用上调用该方法,如下图所示:
1 | public class Main { |
当客户端代码具有的引用是代理时,情况会稍有变化。考虑以下图表和代码片段:
1 | public class Main { |
此处要理解的关键是,Main 类的 main(..)
方法内部的客户端代码具有对代理的引用。 这意味着该对象引用上的方法调用是代理上的调用。结果,代理可以委派给与该特定方法调用相关的所有拦截器(advice)。 但是,一旦调用最终到达目标对象(在此示例中为 SimplePojo
,则为引用),它可能对其自身进行的任何方法调用(例如 this.bar()
或 this.foo()
)都是针对 this 引用的调用,而不是代理。这具有重要意义。这意味着自调用不会导致与方法调用相关的 advice 得到执行的机会。
好吧,那么该怎么办? 最佳方法(在这里宽松地使用术语“最佳”)是重构代码,以免发生自调用。这确实需要您做一些工作,但这是最好的,侵入性最小的方法。下一种方法绝对可怕,我们正要指出这一点,恰恰是因为它是如此可怕。您可以(对我们来说是痛苦的)完全将类中的逻辑与 Spring AOP 绑定在一起,如以下示例所示:
1 | public class SimplePojo implements Pojo { |
这将您的代码完全耦合到 Spring AOP,并且使类本身意识到在 AOP 上下文中使用它的事实,而 AOP 上下文却是这样。创建代理时,还需要一些其他配置,如以下示例所示:
1 | public class Main { |
最后,必须注意,AspectJ 没有此自调用问题,因为它不是基于代理的 AOP 框架。
除了通过使用 <aop:config>
或 <aop:aspectj-autoproxy>
声明配置中的各个切面外,还可以通过编程方式创建建议目标对象的代理。有关 Spring 的 AOP API 的完整详细信息,请参阅下一章。在这里,我们要重点介绍使用 @AspectJ 切面自动创建代理的功能。
您可以使用 org.springframework.aop.aspectj.annotation.AspectJProxyFactory
类为一个或多个 @AspectJ 切面建议的目标对象创建代理。此类的基本用法非常简单,如以下示例所示:
1 | // create a factory that can generate a proxy for the given target object |
有关更多信息,请参见 javadoc。
到目前为止,本章介绍的所有内容都是纯 Spring AOP。 在本节中,我们将研究如果您的需求超出了 Spring AOP 所提供的功能,那么如何使用 AspectJ 编译器或 weaver 代替 Spring AOP 或除 Spring AOP 之外使用。
Spring 附带了一个小的 AspectJ 切面库,该库在您的发行版中可以作为 spring-aspects.jar
独立使用。您需要将其添加到类路径中才能使用其中的切面。使用 AspectJ 通过 Spring 依赖注入域对象
和 AspectJ 的其他 Spring 切面 讨论了该库的内容以及如何使用它。 使用 Spring IoC 配置 AspectJ 切面讨论了如何依赖注入使用 AspectJ 编译器编织的 AspectJ 切面。 最后,Spring Framework 中使用 AspectJ 进行的加载时编织为使用 AspectJ 的 Spring 应用程序提供了加载时编织的介绍。
Spring 容器实例化并配置在您的应用程序上下文中定义的 bean。 给定包含要应用的配置的 Bean 定义的名称,也可以要求 Bean 工厂配置预先存在的对象。 spring-aspects.jar
包含注释驱动的切面,该切面利用此功能允许依赖项注入任何对象。 该支撑旨在用于在任何容器的控制范围之外创建的对象。 域对象通常属于此类,因为它们通常是通过数据库查询的结果由 new 运算符或 ORM 工具以编程方式创建的。
@Configurable
批注将一个类标记为符合 Spring 驱动的配置。 在最简单的情况下,您可以将其纯粹用作标记注释,如以下示例所示:
1 | package com.xyz.myapp.domain; |
当以这种方式用作标记接口时,Spring 通过使用具有与完全限定类型名称(com.xyz.myapp.domain.Account
)相同名称的 bean 定义(通常为原型作用域)来配置带注释类型的新实例(在这种情况下为 Account)。由于 bean 的默认名称是其类型的全限定名,因此声明原型定义的便捷方法是省略 id 属性,如以下示例所示:
1 | <bean class="com.xyz.myapp.domain.Account" scope="prototype"> |
如果要显式指定要使用的原型 bean 定义的名称,则可以直接在批注中这样做,如以下示例所示:
1 | package com.xyz.myapp.domain; |
Spring 现在查找名为 account 的 bean 定义,并将其用作配置新 Account 实例的定义。
您也可以使用自动装配来避免完全指定专用的 bean 定义。要让 Spring 应用自动装配,请使用 @Configurable
批注的 autowire
属性。您可以指定 @Configurable(autowire=Autowire.BY_TYPE)
或 @Configurable(autowire=Autowire.BY_NAME)
分别按类型或名称进行自动装配。作为替代方案,最好为您的对象指定显式的,注释驱动的依赖项注入。通过 @Autowired
或 @Inject
在字段或方法级别上使用 @Configurable
bean(有关更多详细信息,请参见基于注释的容器配置)。
最后,您可以使用 dependencyCheck
属性(例如,@Configurable(autowire=Autowire.BY_NAME,dependencyCheck=true)
)为新创建和配置的对象中的对象引用启用 Spring 依赖检查。如果此属性设置为 true,则 Spring 在配置后验证是否已设置所有属性(不是基本类型或集合)。
请注意,单独使用注释不会执行任何操作。spring-aspects.jar
中的 AnnotationBeanConfigurerAspect
会对注释的存在起作用。从本质上讲,切面说:“在从带有 @Configurable
注释的类型的新对象的初始化返回之后,使用 Spring 根据注释的属性配置新创建的对象”。在这种情况下,“初始化”是指新实例化的对象(例如,用 new 运算符实例化的对象)以及正在进行反序列化(例如,通过 readResolve()
的可序列化的对象)。
上段中的关键短语之一是“本质上”。在大多数情况下,“从新对象的初始化返回后”的确切语义是可以的。在这种情况下,“初始化之后”是指在构造对象之后注入依赖项。这意味着该依赖项不可在类的构造函数体中使用。如果您希望在构造函数主体执行之前注入依赖项,从而可以在构造函数主体中使用这些依赖项,则需要在
@Configurable
声明中对此进行定义,如下所示:
1
2 > true) (preConstruction =
>
您可以在AspectJ 编程指南的此附录中找到有关各种切入点类型的语言语义的更多信息。
为此,必须将带注释的类型与 AspectJ 编织器编织在一起。您可以使用构建时的 Ant 或 Maven 任务来执行此操作(例如,参见《 AspectJ 开发环境指南》),也可以使用加载时编织(请参见 Spring Framework 中的使用 AspectJ 进行加载时编织)。 Spring 需要配置 AnnotationBeanConfigurerAspect
自身(以便获得对将用于配置新对象的 Bean 工厂的引用)。如果使用基于 Java 的配置,则可以将 @EnableSpringConfigured
添加到任何 @Configuration
类中,如下所示:
1 |
|
如果您更喜欢基于 XML 的配置,则 Spring 上下文名称空间定义了一个方便的 context:spring-configured
元素,您可以按以下方式使用它:
1 | <context:spring-configured/> |
除非您真的想在运行时依赖它的语义,否则不要通过 bean configurer 切面激活
@Configurable
处理。特别是,请确保不要在通过容器注册为常规 Spring bean 的 bean 类上使用@Configurable
。这样做会导致两次初始化,一次是通过容器,一次是通过切面。
@Configurable
支持的目标之一是实现域对象的独立单元测试,而不会遇到与硬编码查找相关的困难。如果 AspectJ 尚未编织 @Configurable
类型,则注释在单元测试期间不起作用。您可以在被测对象中设置模拟或存根属性引用,然后照常进行。如果 AspectJ 编织了 @Configurable
类型,您仍然可以像往常一样在容器外部进行单元测试,但是每次构造 @Configurable
对象时,您都会看到一条警告消息,指示该对象尚未由 Spring 配置。
用于实现 @Configurable
支持的 AnnotationBeanConfigurerAspect
是 AspectJ 单例切面。单例切面的范围与静态成员的范围相同:每个类加载器都有一个切面实例来定义类型。这意味着,如果您在同一个类加载器层次结构中定义多个应用程序上下文,则需要考虑在何处定义@EnableSpringConfigured bean,以及在哪里将 spring-aspects.jar 放置在类路径上。
考虑一个典型的 Spring Web 应用程序配置,该配置具有一个共享的父应用程序上下文,该上下文定义了通用的业务服务,支持那些服务所需的一切,以及每个 Servlet 的一个子应用程序上下文(其中包含该 Servlet 的特定定义)。所有这些上下文共存于同一类加载器层次结构中,因此 AnnotationBeanConfigurerAspect
只能保存对其中一个的引用。在这种情况下,我们建议在共享(父)应用程序上下文中定义@EnableSpringConfigured
bean。这定义了您可能想注入域对象的服务。结果是,您无法使用@Configurable 机制来配置域对象,该域对象引用的是在子(特定于 servlet 的)上下文中定义的 Bean 的引用(无论如何,这可能不是您想要做的)。
在同一容器中部署多个 Web 应用程序时,请确保每个 Web 应用程序通过使用其自己的类加载器(例如,将 spring-aspects.jar
放置在 'WEB-INF/lib'
中)将其类型加载到 spring-aspects.jar
中。如果将 spring-aspects.jar
仅添加到容器级的类路径中(并因此由共享的父类加载器加载),则所有 Web 应用程序都共享相同的切面实例(可能不是您想要的)。
除了 @Configurable
切面之外,spring-aspects.jar
还包含一个 AspectJ 切面,您可以使用该切面来驱动 Spring 的事务管理,以使用 @Transactional
批注来批注类型和方法。这主要适用于希望在 Spring 容器之外使用 Spring Framework 的事务支持的用户。
解释 @Transactional
批注的切面是 AnnotationTransactionAspect
。使用此切面时,必须注释实现类(或该类中的方法或两者),而不是注释该类所实现的接口(如果有)。AspectJ 遵循 Java 的规则,即不继承接口上的注释。
类上的 @Transactional
批注指定用于执行该类中任何公共操作的默认事务语义。
类中方法上的 @Transactional
注释将覆盖类注释(如果存在)给出的默认事务语义。可以注释任何可见性的方法,包括私有方法。直接注释非公共方法是执行此类方法而获得事务划分的唯一方法。
从 Spring Framework 4.2 开始,spring-aspects 提供了一个相似的切面,为标准
javax.transaction.Transactional
注释提供了完全相同的功能。检查JtaAnnotationTransactionAspect
了解更多详细信息。
对于希望使用 Spring 配置和事务管理支持但又不想(或不能)使用注释的 AspectJ 程序员,spring-aspects.jar
也包含抽象切面,您可以扩展它们以提供自己的切入点定义。有关更多信息,请参见 AbstractBeanConfigurerAspect
和 AbstractTransactionAspect
切面的资源。作为示例,以下摘录显示了如何编写切面来使用与完全限定的类名匹配的原型 bean 定义来配置域模型中定义的对象的所有实例:
1 | public aspect DomainObjectConfiguration extends AbstractBeanConfigurerAspect { |
当您将 AspectJ 切面与 Spring 应用程序一起使用时,既自然又希望能够使用 Spring 配置这些切面。 AspectJ 运行时本身负责切面的创建,并且通过 Spring 配置 AspectJ 创建的切面的方法取决于切面所使用的 AspectJ 实例化模型(per-xxx 子句)。
AspectJ 的大多数切面都是单例切面。这些切面的配置很容易。您可以创建一个 bean 定义,该 bean 定义按常规引用切面类型,并包括 factory-method =“ aspectOf” bean 属性。这样可以确保 Spring 通过向 AspectJ 索要长宽比实例,而不是尝试自己创建实例来获得长宽比实例。以下示例显示如何使用 factory-method =“ aspectOf”属性:
1 | <bean id="profiler" class="com.xyz.profiler.Profiler" |
注意 factory-method =“ aspectOf”属性
非单一切面很难配置。但是,可以通过创建原型 Bean 定义并使用 spring-aspects.jar 中的@Configurable 支持来实现,一旦它们由 AspectJ 运行时创建了 Bean,就可以配置切面实例。
如果您有一些要与 AspectJ 编织的@AspectJ 切面(例如,对域模型类型使用加载时编织)以及要与 Spring AOP 一起使用的其他@AspectJ 切面,那么这些切面都已在 Spring 中配置,您需要告诉 Spring AOP @AspectJ 自动代理支持,应使用配置中定义的@AspectJ 切面的确切子集进行自动代理。您可以通过在<aop:aspectj-autoproxy />
声明中使用一个或多个<include />
元素来做到这一点。每个<include />
元素都指定一个名称模式,只有名称与至少一个模式匹配的 bean 才可用于 Spring AOP 自动代理配置。以下示例显示了如何使用<include />
元素:
1 | <aop:aspectj-autoproxy> |
不要被
<aop:aspectj-autoproxy/>
元素的名称所迷惑。使用它可以创建 Spring AOP 代理。 这里使用的是 @AspectJ 样式的切面声明,但不涉及 AspectJ 运行时。
AspectJ 切面加载到应用程序的类文件中时将其编织到 Java 虚拟机(JVM)中的过程。本节的重点是在 Spring 框架的特定上下文中配置和使用 LTW。本节不是 LTW 的一般介绍。有关 LTW 的详细信息以及仅使用 AspectJ 配置 LTW(完全不涉及 Spring)的详细信息,请参阅《AspectJ 开发环境指南》的 LTW 部分。
Spring 框架为 AspectJ LTW 带来的价值在于能够对编织过程进行更精细的控制。 “Vanilla” AspectJ LTW 通过使用 Java(5+)代理来实现,该代理在启动 JVM 时通过指定 VM 参数来打开。因此,它是一个 JVM 范围的设置,在某些情况下可能很好,但通常有点过于粗糙。启用 Spring 的 LTW 可让您基于每个 ClassLoader 开启 LTW,它的粒度更细,并且在“单个 JVM-多个应用程序”环境(例如在典型的应用程序服务器中发现)中更有意义。环境)。
此外,在某些环境中,此支持无需在添加-javaagent:path/to/aspectjweaver.jar
或(如本节稍后所述)-javaagent:path/to/spring-instrument.jar
所需的应用程序服务器的启动脚本进行任何修改的情况下即可进行加载时编织。开发人员将应用程序上下文配置为启用加载时编织,而不是依赖通常负责部署配置(例如启动脚本)的管理员。
现在,销售工作已经结束,让我们首先浏览一个使用 Spring 的 AspectJ LTW 的快速示例,然后详细介绍示例中引入的元素。有关完整的示例,请参见 Petclinic 示例应用程序。
此处过于深入,暂时跳过。
可以在 AspectJ 网站上找到有关 AspectJ 的更多信息。
Eclipse AspectJ,作者:Adrian Colyer 等。(Addison-Wesley,2005 年)为 AspectJ 语言提供了全面的介绍和参考。
强烈推荐 Ramnivas Laddad 撰写的《AspectJ in Action》第二版(Manning,2009 年)。本书的重点是 AspectJ,但是(在一定程度上)探讨了许多通用的 AOP 主题。
]]>接下来到 Spring framework core 的第四大块 —— spring 表达式语言(SpEL)
Spring 表达式语言(简称“SpEL”)是一种功能强大的表达式语言,支持在运行时查询和操作对象图。语言语法与 Unified EL 相似,但提供了其他功能,最著名的是方法调用和基本的字符串模板功能。
尽管还有其他几种 Java 表达式语言可用-OGNL,MVEL 和 JBoss EL,仅举几例-Spring 表达式语言的创建是为了向 Spring 社区提供一种受良好支持的表达式语言,该语言可用于以下版本中的所有产品春季投资组合。它的语言功能受 Spring 产品组合中项目的要求驱动,包括 Spring Tools for Eclipse 中代码完成支持的工具要求。也就是说,SpEL 基于与技术无关的 API,如果需要,可以将其他表达语言实现集成在一起。
虽然 SpEL 是 Spring 产品组合中表达评估的基础,但它并不直接与 Spring 绑定,可以独立使用。为了自成一体,本章中的许多示例都将 SpEL 用作独立的表达语言。这需要创建一些自举基础结构类,例如解析器。 Spring 的大多数用户不需要处理这种基础结构,而只能编写表达式字符串进行评估。这种典型用法的一个示例是将 SpEL 集成到创建 XML 或基于注释的 Bean 定义中,如 Expression 支持中定义的 Bean 定义所示。
本章介绍了表达语言,其 API 和语言语法的功能。在许多地方,Inventor 和 Society 类都用作表达评估的目标对象。这些类声明和用于填充它们的数据在本章末尾列出。
表达式语言支持以下功能:
本节介绍 SpEL 接口及其表达语言的简单用法。 完整的语言参考可以在“语言参考”中找到。
以下代码介绍了 SpEL API,用于计算文字字符串表达式 Hello World
。
1 | ExpressionParser parser = new SpelExpressionParser(); |
message 变量的值为“ Hello World”。
您最可能使用的 SpEL 类和接口位于 org.springframework.expression
包及其子包中,例如 spel.support
。
ExpressionParser
接口负责解析表达式字符串。在前面的示例中,表达式字符串是由周围的单引号表示的字符串文字。Expression
接口负责评估先前定义的表达式字符串。分别调用 parser.parseExpression
和 exp.getValue
时,可以引发两个异常 ParseException
和 EvaluationException
。
SpEL 支持多种功能,例如调用方法,访问属性和调用构造函数。
在以下方法调用示例中,我们在字符串文字上调用 concat
方法:
1 | ExpressionParser parser = new SpelExpressionParser(); |
message == ‘Hello World!’
1 | ExpressionParser parser = new SpelExpressionParser(); |
SpEL 还通过使用标准的点符号(例如 prop1.prop2.prop3)以及相应的属性值设置来支持嵌套属性。 也可以访问公共字段。
下面的示例演示如何使用点表示法获取文字的长度:
1 | ExpressionParser parser = new SpelExpressionParser(); |
可以调用 String 的构造函数,而不是使用字符串文字,如以下示例所示:
1 | ExpressionParser parser = new SpelExpressionParser(); |
上例会从文字构造一个新的 String 并将其变为大写。
注意使用通用方法:public <T> T getValue(Class<T> desiredResultType)
。使用此方法无需将表达式的值强制转换为所需的结果类型。 如果该值不能转换为 T
类型或无法使用已注册的类型转换器转换,则将引发 EvaluationException
。
SpEL 的更常见用法是提供一个针对特定对象实例(称为根对象)进行评估的表达式字符串。 以下示例显示如何从 Inventor
类的实例检索 name
属性或如何创建布尔条件:
1 | // Create and set a calendar |
EvaluationContext
在评估表达式以解析属性,方法或字段并帮助执行类型转换时,使用 EvaluationContext
接口。 Spring 提供了两种实现。
SimpleEvaluationContext
:针对不需要全部 SpEL 语言语法范围且应受到有意义限制的表达式类别,公开了 SpEL 基本语言功能和配置选项的子集。示例包括但不限于数据绑定表达式和基于属性的过滤器。StandardEvaluationContext
:公开了全套 SpEL 语言功能和配置选项。您可以使用它来指定默认的根对象并配置每个可用的评估相关策略。SimpleEvaluationContext
设计为仅支持 SpEL 语言语法的子集。它不包括 Java 类型引用,构造函数和 Bean 引用。它还要求您明确选择对表达式中的属性和方法的支持级别。默认情况下,create()
静态工厂方法仅启用对属性的读取访问。您还可以获取构建器来配置所需的确切支持级别,并针对以下一种或某种组合:
PropertyAccessor
(无反射)默认情况下,SpEL 使用 Spring 核心中可用的转换服务(org.springframework.core.convert.ConversionService
)。此转换服务附带许多内置转换器,用于常见转换,但也可以完全扩展,以便您可以在类型之间添加自定义转换。此外,它是泛型感知的。这意味着,当您在表达式中使用泛型类型时,SpEL 会尝试进行转换以维护遇到的任何对象的类型正确性。
实际上这是什么意思?假设使用 setValue()
进行赋值来设置 List
属性。该属性的类型实际上是 List<Boolean>
。 SpEL 认识到列表中的元素在放入列表之前需要转换为布尔值。以下示例显示了如何执行此操作:
1 | class Simple { |
可以使用解析器配置对象(org.springframework.expression.spel.SpelParserConfiguration
)配置 SpEL 表达式解析器。 配置对象控制某些表达式组件的行为。 例如,如果您索引到数组或集合中并且指定索引处的元素为 null
,则可以自动创建该元素。 当使用由属性引用链组成的表达式时,这很有用。 如果您索引到数组或列表中并指定了超出数组或列表当前大小末尾的索引,则可以自动增长数组或列表以容纳该索引。下面的示例演示如何自动增加列表:
1 | class Demo { |
Spring Framework 4.1 包含一个基本的表达式编译器。通常对表达式进行解释,这样可以在评估过程中提供很大的动态灵活性,但不能提供最佳性能。对于偶尔使用表达式,这很好,但是,当与其他组件(例如 Spring Integration)一起使用时,性能可能非常重要,并且不需要动态性。
SpEL 编译器旨在满足这一需求。在评估过程中,编译器会生成一个 Java 类,该类体现了运行时的表达式行为,并使用该类来实现更快的表达式评估。由于缺少在表达式周围输入内容的信息,因此编译器在执行编译时会使用在表达式的解释式求值过程中收集的信息。例如,它不仅仅从表达式中就知道属性引用的类型,而是在第一次解释求值时就知道它是什么。当然,如果各种表达元素的类型随时间变化,则基于此类派生信息进行编译会在以后引起麻烦。因此,编译最适合类型信息在重复求值时不会改变的表达式。
考虑以下基本表达式:
1 | someArray[0].someProperty.someOtherProperty < 0.1 |
由于前面的表达式涉及数组访问,一些属性取消引用和数字运算,因此性能提升可能非常明显。在一个示例中,进行了 50000 次迭代的微基准测试,使用解释器评估需要 75 毫秒,而使用表达式的编译版本仅需要 3 毫秒。
两种不同的方式之一来打开它。当 SpEL 用法嵌入到另一个组件中时,可以使用解析器配置过程(前面讨论过)或使用系统属性来打开它。本节讨论这两个选项。
编译器可以在 org.springframework.expression.spel.SpelCompilerMode
枚举中捕获的三种模式之一进行操作。模式如下:
OFF
(默认):编译器已关闭。IMMEDIATE
:在立即模式下,将尽快编译表达式。通常是在第一次解释评估之后。如果编译的表达式失败(通常是由于类型更改,如前所述),则表达式求值的调用者将收到异常。MIXED
:在混合模式下,表达式会随着时间静默在解释模式和编译模式之间切换。经过一定数量的解释运行后,它们会切换到编译形式,如果编译形式出了问题(例如,如前面所述的类型更改),则表达式会自动再次切换回解释形式。稍后,它可能会生成另一个已编译的表单并切换到该表单。基本上,用户进入即时模式的异常是在内部处理的。存在 IMMEDIATE
模式是因为 MIXED
模式可能会导致具有副作用的表达式出现问题。如果已编译的表达式在部分成功后就崩溃了,则它可能已经完成了影响系统状态的操作。如果发生这种情况,调用者可能不希望它在解释模式下静默地重新运行,因为表达式的一部分可能运行了两次。
选择模式后,使用 SpelParserConfiguration
配置解析器。以下示例显示了如何执行此操作:
1 | SpelParserConfiguration config = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE, |
当指定编译器模式时,还可以指定一个类加载器(允许传递 null)。编译的表达式在提供的任何子类加载器中定义。重要的是要确保,如果指定了类加载器,则它可以查看表达式评估过程中涉及的所有类型。如果未指定类加载器,则使用默认的类加载器(通常是在表达式求值期间运行的线程的上下文类加载器)。
第二种配置编译器的方法是将 SpEL 嵌入到其他组件中,并且可能无法通过配置对象进行配置。 在这些情况下,可以使用系统属性。 您可以将 spring.expression.compiler.mode
属性设置为 SpelCompilerMode
枚举值之一(off
、immediate
或 mixed
)。
从 Spring Framework 4.1 开始,已经有了基本的编译框架。但是,该框架尚不支持编译每种表达式。最初的重点是可能在性能关键型上下文中使用的通用表达式。目前无法编译以下类型的表达式:
将来会编译更多类型的表达。
您可以将 SpEL 表达式与基于 XML 或基于注释的配置元数据一起使用,以定义 BeanDefinition
实例。 在这两种情况下,用于定义表达式的语法都采用 #{ <expression string> }
的形式。
可以使用表达式来设置属性或构造函数参数值,如以下示例所示:
1 | <bean id="numberGuess" class="org.spring.samples.NumberGuess"> |
应用程序上下文中的所有 bean 都可以使用其公共 bean 名称作为预定义变量使用。 这包括标准上下文 Bean,例如用于访问运行时环境的 environment
(类型为 org.springframework.core.env.Environment
)以及 systemProperties
和 systemEnvironment
(类型为 Map<String,Object>
)。
下面的示例显示了如何对 systemProperties
bean 作为 SpEL 变量的访问:
1 | <bean id="taxCalculator" class="org.spring.samples.TaxCalculator"> |
请注意,此处不必在预定义变量前加上 #
符号。
您还可以按名称引用其他 bean 属性,如以下示例所示:
1 | <bean id="numberGuess" class="org.spring.samples.NumberGuess"> |
若要指定默认值,可以将 @Value
批注放置在字段,方法以及方法或构造函数参数上。
下面的示例设置字段变量的默认值:
1 | public class FieldValueTestBean { |
以下示例显示了等效的但使用属性设置器方法的示例:
1 | public class PropertyValueTestBean { |
自动装配的方法和构造函数也可以使用 @Value 批注,如以下示例所示:
1 | public class SimpleMovieLister { |
1 | public class MovieRecommender { |
本节描述了 Spring Expression Language 的工作方式。 它涵盖以下主题:
支持的文字表达式的类型为字符串,数值(int,实数,十六进制),布尔值和 null。 字符串由单引号引起来。 要将单引号本身放在字符串中,请使用两个单引号字符。
以下清单显示了文字的简单用法。 通常,它们不是像这样孤立地使用,而是作为更复杂的表达式的一部分使用-例如,在逻辑比较运算符的一侧使用文字。
1 | ExpressionParser parser = new SpelExpressionParser(); |
数字支持使用负号,指数符号和小数点。 默认情况下,使用 Double.parseDouble()解析实数。
使用属性引用进行导航很容易。为此,请使用句点来指示嵌套的属性值。Inventor
类的实例 pupin
和 tesla
填充有 示例中使用的类 中列出的数据。要向下导航并获取特斯拉(Tesla)的出生年份和普平(Pupin)的出生城市,我们使用以下表达式:
1 | // evals to 1856 |
属性名称的首字母允许不区分大小写。 数组和列表的内容通过使用方括号表示法获得,如以下示例所示:
1 | ExpressionParser parser = new SpelExpressionParser(); |
通过在方括号内指定文字键值可以获取映射的内容。 在下面的示例中,由于 Officer 映射的键是字符串,因此我们可以指定字符串文字:
1 | // Officer's Dictionary |
您可以使用 {}
表示法在表达式中直接表达列表。
1 | // evaluates to a Java list containing the four numbers |
{}
本身表示一个空列表。出于性能原因,如果列表本身完全由固定文字组成,则会创建一个常量列表来表示该表达式(而不是在每次求值时都建立一个新列表)。
您也可以使用 {key:value}
表示法在表达式中直接表达映射。以下示例显示了如何执行此操作:
1 | // evaluates to a Java map containing the two entries |
{:}
本身意味着一个空的映射。出于性能原因,如果映射图本身由固定的文字或其他嵌套的常量结构(列表或映射)组成,则会创建一个常量映射来表示该表达式(而不是在每次求值时都构建一个新的映射)。映射键的引号是可选的。上面的示例使用的是不带引号的键。
您可以使用熟悉的 Java 语法来构建数组,可以选择提供一个初始化程序,以在构造时填充该数组。 以下示例显示了如何执行此操作:
1 | int[] numbers1 = (int[]) parser.parseExpression("new int[4]").getValue(context); |
构造多维数组时,当前无法提供初始化程序。
您可以使用典型的 Java 编程语法来调用方法。 您还可以在文字上调用方法。 还支持变量参数。 下面的示例演示如何调用方法:
1 | // string literal, evaluates to "bc" |
Spring 表达式语言支持以下几种运算符:
使用标准运算符表示法支持关系运算符(等于,不等于,小于,小于或等于,大于和大于或等于)。 以下清单显示了一些运算符示例:
1 | // evaluates to true |
对
null
的大于和小于比较遵循一个简单的规则:null
被视为无(不是零)。结果,任何其他值始终大于null
(X > null
始终为true
),并且其他任何值都不小于零(X < null
始终为false
)。如果您需要数字比较,请避免使用基于数字的
null
比较,而建议使用零进行比较(例如,X > 0
或X < 0
)。
除了标准的关系运算符外,SpEL 还支持 instanceof
和基于正则表达式的匹配运算符。 以下清单显示了两个示例:
1 | // evaluates to false |
请注意原始类型,因为它们会立即被包装为包装器类型,因此,按预期方式,
1 instanceof T(int)
的计算结果为false
,而1 instanceof T(Integer)
的计算结果为true
。
每个符号运算符也可以指定为纯字母等效项。 这样可以避免使用的符号对于嵌入表达式的文档类型具有特殊含义的问题(例如在 XML 文档中)。等效的文字是:
所有的文本运算符都不区分大小写。
SpEL 支持以下逻辑运算符:
&&
)||
)!
)下面的示例演示如何使用逻辑运算符
1 | // -- AND -- |
您可以在数字和字符串上使用加法运算符。 您只能对数字使用减法,乘法和除法运算符。 您还可以使用模数(%)和指数幂(^)运算符。 强制执行标准运算符优先级。 以下示例显示了正在使用的数学运算符:
1 | // Addition |
要设置属性,请使用赋值运算符(=)。 这通常在对 setValue
的调用内完成,但也可以在对 getValue
的调用内完成。 下面的清单显示了使用赋值运算符的两种方法:
1 | Inventor inventor = new Inventor(); |
您可以使用特殊的 T
运算符来指定 java.lang.Class
(类型)的实例。静态方法也可以通过使用此运算符来调用。StandardEvaluationContext
使用 TypeLocator
查找类型,而 StandardTypeLocator
(可以替换)是在了解 java.lang
包的情况下构建的。 这意味着对 Java.lang
中的类型的 T()
引用不需要完全限定,但是所有其他类型引用都必须是完全限定的。下面的示例演示如何使用 T
运算符:
1 | Class dateClass = parser.parseExpression("T(java.util.Date)").getValue(Class.class); |
您可以使用 new
运算符来调用构造函数。 除基本类型(int
,float
等)和 String
以外的所有其他类都应使用完全限定的类名。 下面的示例演示如何使用 new 运算符调用构造函数:
1 | Inventor einstein = p.parseExpression( |
您可以使用 #variableName
语法在表达式中引用变量。 通过在 EvaluationContext
实现上使用 setVariable
方法设置变量。
有效的变量名称必须由以下一个或多个受支持的字符组成。
- 字母:
A
到Z
和a
到z
- 数字:
0
到9
- 下划线:
_
- 美元符号:
$
以下示例显示了如何使用变量。
1 | Inventor tesla = new Inventor("Nikola Tesla", "Serbian"); |
#this
和 #root
变量#this
变量始终是定义的,并且引用当前的评估对象(反对解决不合格的引用)。#root
变量也是始终定义,并引用根上下文对象。尽管 #this
可能随表达式的组成部分的求值而变化,但 #root
始终引用根。 以下示例说明如何使用 #this
和 #root
变量:
1 | // create an array of integers |
您可以通过注册可以在表达式字符串中调用的用户定义函数来扩展 SpEL。该函数通过 EvaluationContext
注册。下面的示例显示如何注册用户定义的函数:
1 | Method method = ...; |
例如,考虑以下用于反转字符串的实用程序方法:
1 | public abstract class StringUtils { |
然后,您可以注册并使用前面的方法,如以下示例所示:
1 | ExpressionParser parser = new SpelExpressionParser(); |
如果评估上下文已使用 bean 解析器配置,则可以使用@符号从表达式中查找 bean。 以下示例显示了如何执行此操作:
1 | ExpressionParser parser = new SpelExpressionParser(); |
要访问工厂 bean 本身,您应该在 bean 名称前加上&符号。 以下示例显示了如何执行此操作:
1 | ExpressionParser parser = new SpelExpressionParser(); |
您可以使用三元运算符在表达式内部执行 if-then-else 条件逻辑。 以下清单显示了一个最小的示例:
1 | String falseString = parser.parseExpression( |
在这种情况下,布尔值 false 导致返回字符串值’falseExp’。 一个更现实的示例如下:
1 | parser.parseExpression("Name").setValue(societyContext, "IEEE"); |
有关三元运算符的更短语法,请参阅关于 Elvis 运算符的下一部分。
Elvis 运算符是三元运算符语法的简化,并且在 Groovy 语言中使用。 使用三元运算符语法,通常必须将变量重复两次,如以下示例所示:
1 | String name = "Elvis Presley"; |
相反,您可以使用 Elvis 运算符(其命名类似于 Elvis 的发型)。 以下示例显示了如何使用 Elvis 运算符:
1 | ExpressionParser parser = new SpelExpressionParser(); |
以下显示了一个更复杂的示例:
1 | ExpressionParser parser = new SpelExpressionParser(); |
您可以使用 Elvis 运算符在表达式中应用默认值。 以下示例显示了如何在
@Value
表达式中使用 Elvis 运算符:
1
2 > "#{systemProperties['pop3.port'] ?: 25}") (
>
上例将注入系统属性
pop3.port
,为空将注入 25。
安全导航运算符用于避免 NullPointerException
,它来自 Groovy 语言。通常,当您引用一个对象时,可能需要在访问该对象的方法或属性之前验证其是否为 null。为了避免这种情况,安全导航运算符返回 null 而不是引发异常。 下面的示例演示如何使用安全导航操作符:
1 | ExpressionParser parser = new SpelExpressionParser(); |
选择是一种强大的表达语言功能,可让您通过从源集合中进行选择来将其转换为另一个集合。
选择使用 .?[selectionExpression]
的语法。 它过滤集合并返回一个包含原始元素子集的新集合。例如,通过选择,我们可以轻松地获得 Serbian inventors 的列表,如以下示例所示:
1 | List<Inventor> list = (List<Inventor>) parser.parseExpression( |
在列表和映射上都可以选择。对于列表,将针对每个单独的列表元素评估选择标准。针对映射,针对每个映射条目(Java 类型 Map.Entry 的对象)评估选择标准。每个映射条目都有其键和值,可作为属性访问以供选择。
以下表达式返回一个新映射,该映射由原始映射中条目值小于 27 的那些元素组成:
1 | Map newMap = parser.parseExpression("map.?[value<27]").getValue(); |
除了返回所有选定的元素外,您只能检索第一个或最后一个值。为了获得与选择匹配的第一个条目,语法为 .^[selectionExpression]
。要获取最后一个匹配选择,语法为 .$[selectionExpression]
。
投影使集合可以驱动子表达式的求值,结果是一个新的集合。投影的语法为 .![projectionExpression]
。例如,假设我们有一个 inventor 列表,但是想要他们出生的城市列表。实际上,我们希望为 inventor 列表中的每个条目计算“placeOfBirth.city”。 下面的示例使用投影来做到这一点:
1 | // returns ['Smiljan', 'Idvor' ] |
您还可以使用映射来驱动投影,在这种情况下,将针对映射中的每个条目(表示为 Java Map.Entry)对投影表达式进行评估。 跨映射的投影结果是一个列表,其中包含针对每个映射条目的投影表达式的评估。
表达式模板允许将文字文本与一个或多个评估块混合。每个评估块均以您可以定义的前缀和后缀字符分隔。常见的选择是使用 #{ }
作为分隔符,如以下示例所示:
1 | String randomPhrase = parser.parseExpression( |
通过将文字文本 'random number is '
与评估 #{ }
分隔符内的表达式的结果(在本例中为调用 random()
方法的结果)相连接来评估字符串。parseExpression()
方法的第二个参数的类型为 ParserContext
。ParserContext
接口用于影响表达式的解析方式,以支持表达式模板功能。TemplateParserContext
的定义如下:
1 | public class TemplateParserContext implements ParserContext { |
本节列出了本章示例中使用的类。
Inventor.java
1 | package org.spring.samples.spel.inventor; |
PlaceOfBirth.java
1 | package org.spring.samples.spel.inventor; |
Society.java
1 | package org.spring.samples.spel.inventor; |
接下来到 Spring framework core 的第三大块 —— 验证、数据绑定和类型转换
考虑将验证作为业务逻辑有利有弊,Spring 提供了一种验证(和数据绑定)设计。具体来说,验证不应与 Web 层绑定,并且应该易于本地化,并且应该可以插入任何可用的验证器。考虑到这些问题,Spring 提供了一个 Validator
合同,该合同既基本又可以在应用程序的每个层中使用。
数据绑定对于使用户输入动态绑定到应用程序的域模型(或用于处理用户输入的任何对象)非常有用。Spring 提供了恰当地命名为 DataBinder
的功能。 Validator
和 DataBinder
组成了验证包,该验证包主要用于但不限于 Web 层。
BeanWrapper
是 Spring 框架中的基本概念,并在很多地方使用。但是,您可能不需要直接使用 BeanWrapper
。但是,因为这是参考文档,所以我们认为可能需要进行一些解释。我们将在本章中解释 BeanWrapper
,因为如果您要使用它,那么在尝试将数据绑定到对象时最有可能使用它。
Spring 的 DataBinder
和较低级别的 BeanWrapper
都使用 PropertyEditorSupport
实现来解析和格式化属性值。 PropertyEditor
和 PropertyEditorSupport
类型是 JavaBeans 规范的一部分,本章还将对此进行说明。 Spring 3 引入了 core.convert
包,该包提供了常规的类型转换工具,以及用于格式化 UI 字段值的高级“format”包。您可以将这些包用作 PropertyEditorSupport
实现的更简单替代方案。本章还将对它们进行讨论。
Spring 通过设置基础结构和 Spring 自己的 Validator
合同的适配器来支持 Java Bean 验证。应用程序可以全局启用一次 Bean 验证,如 Java Bean 验证中所述,并将其专用于所有验证需求。在 Web 层中,应用程序可以每个 DataBinder
进一步注册控制器本地的 Spring Validator
实例,如配置 DataBinder
中所述,这对于插入自定义验证逻辑很有用。
Spring 具有 Validator
接口,可用于验证对象。 Validator
接口通过使用 Errors
对象来工作,以便验证器在验证时可以将验证失败报告给 Errors
对象。
1 | public class Person { |
下一个示例通过实现 org.springframework.validation.Validator
接口的以下两个方法来提供 Person
类的验证行为:
supports(Class)
:此验证程序可以验证提供的 Class 的实例吗?validate(Object, org.springframework.validation.Errors)
:验证给定的对象,并在发生验证错误的情况下,向给定的 Errors
对象注册这些对象。实施 Validator
非常简单,尤其是当您知道 Spring Framework 也提供的 ValidationUtils
帮助器类时。 以下示例实现了用于 Person
实例的 Validator
:
1 | public class PersonValidator implements Validator { |
ValidationUtils
类上的静态 rejectIfEmpty(..)
方法用于拒绝 name 属性(如果该属性为 null 或为空字符串)。查看 ValidationUtils
javadoc,看看它除了提供前面显示的示例外还提供什么功能。
虽然可以实现单个 Validator
类来验证丰富对象中的每个嵌套对象,但最好在其自己的 Validator
实现中封装对象的每个嵌套类的验证逻辑。一个“丰富”对象的简单示例是一个 Customer
,它由两个 String 属性(第一个和第二个名称)和一个复杂的 Address
对象组成。地址对象可以独立于客户对象使用,因此已实现了不同的 AddressValidator
。如果希望 CustomerValidator
重用 AddressValidator
类中包含的逻辑而不求助于复制和粘贴,则可以在 CustomerValidator
中依赖注入或实例化一个 AddressValidator
,如以下示例所示:
1 | public class CustomerValidator implements Validator { |
验证错误将报告给传递给验证器的 Errors
对象。 对于 Spring Web MVC,可以使用 <spring:bind/>
标记检查错误消息,但是也可以自己检查 Errors
对象。关于它提供的方法的更多信息可以在 javadoc 中找到。
我们介绍了数据绑定和验证。本节介绍与验证错误相对应的输出消息。在上一节显示的示例中,我们拒绝了名称和年龄字段。如果要使用 MessageSource
输出错误消息,可以使用拒绝字段时提供的错误代码(在这种情况下为“名称”和“年龄”)来进行输出。当您从 Errors
接口调用(直接或间接通过使用诸如 ValidationUtils
类的直接或间接)rejectValue
或其他拒绝方法之一时,基础实现不仅注册您传入的代码,还注册许多其他错误代码。MessageCodesResolver
确定 Errors
接口寄存器中的哪个错误代码。默认情况下,使用 DefaultMessageCodesResolver
,它(例如)不仅使用您提供的代码注册消息,而且还注册包含传递给拒绝方法的字段名称的消息。因此,如果您通过使用 rejectValue("age", "too.darn.old")
拒绝字段,除了 too.darn.old
代码外,Spring 还会注册 too.darn.old.age
和 too.darn.old.age.int
(第一个包含字段名称,第二个包含字段类型)。这样做是为了方便开发人员在定位错误消息时提供帮助。
有关 MessageCodesResolver 和默认策略的更多信息,可以分别在 MessageCodesResolver
和 DefaultMessageCodesResolver
的 javadoc 中找到。
org.springframework.beans
包遵循 JavaBeans 标准。 JavaBean 是具有默认无参数构造函数的类,并且遵循命名约定,在该命名约定下,例如,名为 bingoMadness
的属性将具有 setter 方法 setBingoMadness(..)
和 getter 方法 getBingoMadness()
。有关 JavaBean 和规范的更多信息,请参见 javabeans。
Bean 包中的一个非常重要的类是 BeanWrapper
接口及其相应的实现(BeanWrapperImpl
)。就像从 Javadoc 引用的那样,BeanWrapper
提供了以下功能:设置和获取属性值(单独或批量),获取属性描述符以及查询属性以确定它们是否可读或可写。此外,BeanWrapper
还支持嵌套属性,从而可以将子属性上的属性设置为无限深度。 BeanWrapper
还支持添加标准 JavaBeans PropertyChangeListeners
和 VetoableChangeListeners
的功能,而无需在目标类中支持代码。最后但并非最不重要的一点是,BeanWrapper
支持设置索引属性。 BeanWrapper
通常不直接由应用程序代码使用,而是由 DataBinder
和 BeanFactory
使用。
BeanWrapper
的工作方式部分由其名称表示:它包装一个 Bean,以对该 Bean 执行操作,例如设置和检索属性。
设置和获取属性是通过 BeanWrapper
的 setPropertyValue
和 getPropertyValue
重载方法变体完成的。有关详细信息,请参见其 Javadoc。下表显示了这些约定的一些示例:
表达式 | 说明 |
---|---|
name | 表示与 getName() 或 isName() 和 setName(..) 方法相对应的 name 属性 。 |
account.name | 表示与 getAccount().setName() 或 getAccount().getName() 方法相对应的 account 属性的 name 嵌套属性。 |
account[2] | 表示索引属性的第三个元素 account 。索引属性可能是的 array ,list 或其它天然有序集合。 |
account[COMPANYNAME] | 表示account 这个 Map 由 COMPANYNAME 键索引的条目的值。 |
(如果您不打算直接使用 BeanWrapper
,那么下一部分对您而言并不是至关重要的。如果仅使用 DataBinder
和 BeanFactory
及其默认实现,则应跳到 PropertyEditors
的部分。)
以下两个示例类使用 BeanWrapper
来获取和设置属性:
1 | public class Company { |
1 | public class Employee { |
以下代码段显示了一些有关如何检索和操纵实例化的 Companies
和 Employees
的某些属性的示例:
1 | BeanWrapper company = new BeanWrapperImpl(new Company()); |
PropertyEditor
实现pring 使用 PropertyEditor
的概念来实现对象和字符串之间的转换。以不同于对象本身的方式表示属性可能很方便。例如,日期可以用人类可读的方式表示(如字符串:”2007-14-09”),而我们仍然可以将人类可读的形式转换回原始日期(或者更好的是,转换任何日期以人类可读的形式输入到 Date
对象)。通过注册类型为 java.beans.PropertyEditor
的自定义编辑器,可以实现此行为。在 BeanWrapper
上或在特定的 IoC 容器中注册自定义编辑器(如上一章所述),使它具有如何将属性转换为所需类型的知识。有关 PropertyEditor
的更多信息,请参见 Oracle 的 java.beans 包的 javadoc。
在 Spring 中使用属性编辑的两个示例:
PropertyEditor
实现在 bean 上设置属性。当使用 String
作为在 XML 文件中声明的某个 bean 的属性的值时,Spring(如果相应属性的设置器具有 Class
参数)将使用 ClassEditor
尝试将参数解析为 Class
对象。PropertyEditor
实现来解析 HTTP 请求参数,您可以在 CommandController
的所有子类中手动绑定这些实现。Spring 具有许多内置的 PropertyEditor
实现,以简化生活。它们都位于org.springframework.beans.propertyeditors
包中。默认情况下,大多数(但不是全部,如下表所示)由 BeanWrapperImpl
注册。如果可以通过某种方式配置属性编辑器,则仍可以注册自己的变体以覆盖默认变体。下表描述了 Spring 提供的各种 PropertyEditor
实现:
类 | 说明 |
---|---|
ByteArrayPropertyEditor |
字节数组的编辑器。将字符串转换为其相应的字节表示形式。默认情况下由 BeanWrapperImpl 注册。 |
ClassEditor |
将代表类的字符串解析为实际类,反之亦然。当找不到类时,将抛出 IllegalArgumentException 。默认情况下,由 BeanWrapperImpl 注册。 |
CustomBooleanEditor |
布尔属性的可定制属性编辑器。默认情况下,由 BeanWrapperImpl 注册,但是可以通过将其自定义实例注册为自定义编辑器来覆盖。 |
CustomCollectionEditor |
集合的属性编辑器,可将任何源 Collection 转换为给定的目标 Collection 类型。 |
CustomDateEditor |
java.util.Date 的可自定义属性编辑器,支持自定义 DateFormat 。默认未注册。必须根据需要以适当的格式进行用户注册。 |
CustomNumberEditor |
任何 Number 子类(例如 Integer ,Long ,Float 或 Double )的可自定义属性编辑器。默认情况下,由 BeanWrapperImpl 注册,但是可以通过将其自定义实例注册为自定义编辑器来覆盖。 |
FileEditor |
将字符串解析为 java.io.File 对象。默认情况下,由 BeanWrapperImpl 注册。 |
InputStreamEditor |
单向属性编辑器,它可以采用字符串并生成(通过中间的 ResourceEditor 和 Resource )一个 InputStream ,以便可以将 InputStream 属性直接设置为字符串。请注意,默认用法不会为您关闭 InputStream 。默认情况下,由 BeanWrapperImpl 注册。 |
LocaleEditor |
可以将字符串解析为 Locale 对象,反之亦然(字符串格式为 _[country] _[variant] ,与 Locale 的 toString() 方法相同)。默认情况下,由 BeanWrapperImpl 注册。 |
PatternEditor |
可以将字符串解析为 java.util.regex.Pattern 对象,反之亦然。 |
PropertiesEditor |
可以将字符串(以 java.util.Properties 类的 javadoc 中定义的格式格式化)转换为 Properties 对象。默认情况下,由 BeanWrapperImpl 注册。 |
StringTrimmerEditor |
修剪字符串的属性编辑器。(可选)允许将空字符串转换为空值。默认情况下未注册——必须是用户注册的。 |
URLEditor |
可以将 URL 的字符串表示形式解析为实际的 URL 对象。默认情况下,由 BeanWrapperImpl 注册 |
Spring 使用 java.beans.PropertyEditorManager
设置可能需要的属性编辑器的搜索路径。搜索路径还包括 sun.bean.editors
,其中包括针对诸如 Font
,Color
和大多数基本类型的类型的 PropertyEditor
实现。 还要注意,如果标准 JavaBeans 基础结构与它们处理的类在同一包中,并且与该类具有相同的名称,并附加了 Editor,则标准 JavaBeans 基础结构将自动发现 PropertyEditor
类(无需显式注册它们)。例如,可能具有以下类和包结构,足以使 SomethingEditor
类被识别并用作 Something
类型的属性的 PropertyEditor
。
1 | com |
注意,您也可以在此处使用标准的 BeanInfo
JavaBeans 机制(在某种程度上进行了描述)。 以下示例使用 BeanInfo
机制使用关联类的属性显式注册一个或多个 PropertyEditor
实例:
1 | com |
所引用的 SomethingBeanInfo
类的以下 Java 源代码将 CustomNumberEditor
与 Something
类的 age
属性相关联:
1 | public class SomethingBeanInfo extends SimpleBeanInfo { |
当将 bean 属性设置为字符串值时,Spring IoC 容器最终使用标准 JavaBeans PropertyEditor
实现将这些字符串转换为属性的复杂类型。Spring 预注册了许多自定义的 PropertyEditor
实现(例如,将表示为字符串的类名称转换为 Class
对象)。此外,Java 的标准 JavaBeans PropertyEditor
查找机制允许适当地命名类的 PropertyEditor
,并将其与提供支持的类放在同一包中,以便可以自动找到它。
如果需要注册其他自定义 PropertyEditor
,则可以使用几种机制。最手动的方法(通常不方便或不建议使用)是使用 ConfigurableBeanFactory
接口的 registerCustomEditor()
方法,并假设您有 BeanFactory
引用。另一种(稍微方便些)的机制是使用一种称为 CustomEditorConfigurer
的特殊 bean 工厂后处理器。尽管您可以将 Bean 工厂后处理器与 BeanFactory 实现一起使用,但 CustomEditorConfigurer
具有嵌套的属性设置,因此我们强烈建议您将其与 ApplicationContext
一起使用,在这里可以将其以与其他任何 Bean 相似的方式进行部署,并且可以在任何位置进行部署。自动检测并应用。
请注意,所有的 bean 工厂和应用程序上下文通过使用 BeanWrapper 来处理属性转换,都会自动使用许多内置的属性编辑器。上一节列出了 BeanWrapper 注册的标准属性编辑器。此外,ApplicationContext
还以适合特定应用程序上下文类型的方式重写或添加其他编辑器,以处理资源查找。
标准 JavaBeans PropertyEditor
实例用于将以字符串表示的属性值转换为该属性的实际复杂类型。您可以使用 bean 工厂的后处理器 CustomEditorConfigurer
来方便地将对其他 PropertyEditor
实例的支持添加到 ApplicationContext
。
考虑以下示例,该示例定义了一个名为 ExoticType
的用户类和另一个名为 DependsOnExoticType
的类,该类需要将 ExoticType
设置为属性:
1 | package example; |
正确设置之后,我们希望能够将 type 属性分配为字符串,PropertyEditor
会将其转换为实际的 ExoticType
实例。 以下 bean 定义显示了如何建立这种关系:
1 | <bean id="sample" class="example.DependsOnExoticType"> |
PropertyEditor
实现可能类似于以下内容:
1 | // converts string representation to ExoticType object |
Spring 3 引入了 core.convert
包,该包提供了通用的类型转换系统。系统定义了一个用于实现类型转换逻辑的 SPI 和一个用于在运行时执行类型转换的 API。在 Spring 容器中,可以使用此系统作为 PropertyEditor
实现的替代方法,以将外部化的 bean 属性值字符串转换为所需的属性类型。 您还可以在应用程序中需要类型转换的任何地方使用公共 API。
如以下接口定义所示,用于实现类型转换逻辑的 SPI 非常简单且具有强类型:
1 | package org.springframework.core.convert.converter; |
要创建自己的转换器,请实现 Converter 接口并将 S 设置为要转换的类型,并将 T 设置为要转换的类型。如果还需要注册一个委托数组或集合转换器(默认情况下 DefaultConversionService 会这样做),则也可以透明地应用此类转换器,如果需要将 S 的集合或数组转换为 T 的数组或集合。
对于每次对 convert(S)的调用,保证源参数不为 null。如果转换失败,您的转换器可能会引发任何未经检查的异常。具体来说,它应该抛出 IllegalArgumentException 以报告无效的源值。注意确保您的 Converter 实现是线程安全的。
为了方便起见,在 core.convert.support 软件包中提供了几种转换器实现。这些包括从字符串到数字和其他常见类型的转换器。下面的清单显示了 StringToInteger 类,它是一个典型的 Converter 实现:
1 | package org.springframework.core.convert.support; |
ConverterFactory
当需要集中整个类层次结构的转换逻辑时(例如,从 String 转换为 Enum 对象时),可以实现 ConverterFactory,如以下示例所示:
1 | package org.springframework.core.convert.converter; |
参数化 S 为您要转换的类型,参数化 R 为基类型,定义可以转换为的类的范围。 然后实现 getConverter(Class
以 StringToEnumConverterFactory 为例:
1 | package org.springframework.core.convert.support; |
GenericConverter
当您需要复杂的 Converter
实现时,请考虑使用 GenericConverter
接口。 与 Converter
相比,GenericConverter
具有比 Converter
更灵活但强度不高的签名,支持在多种源类型和目标类型之间进行转换。此外,GenericConverter
使您可以在实现转换逻辑时使用可用的源字段和目标字段上下文。 这种上下文允许类型转换由字段注释或在字段签名上声明的通用信息驱动。 以下清单显示了 GenericConverter
的接口定义:
1 | package org.springframework.core.convert.converter; |
要实现 GenericConverter
,请让 getConvertibleTypes()
返回支持的源 → 目标类型对。 然后实现 convert(Object,TypeDescriptor,TypeDescriptor)
包含您的转换逻辑。 源 TypeDescriptor
提供对包含正在转换的值的源字段的访问。 使用目标 TypeDescriptor
,可以访问要设置转换值的目标字段。
GenericConverter
的一个很好的例子是在 Java 数组和集合之间进行转换的转换器。 这样的 ArrayToCollectionConverter
会对声明目标集合类型的字段进行内省,以解析集合的元素类型。 这样就可以在将集合设置到目标字段上之前,将源数组中的每个元素转换为集合元素类型。
由于
GenericConverter
是一个更复杂的 SPI 接口,因此仅应在需要时使用它。 支持Converter
或ConverterFactory
以满足基本的类型转换需求。
ConditionalGenericConverter
有时,您希望 Converter 仅在满足特定条件时才运行。 例如,您可能只想在目标字段上存在特定注释时才运行 Converter,或者可能仅在目标类上定义了特定方法(例如静态 valueOf 方法)时才运行 Converter。 ConditionalGenericConverter 是 GenericConverter 和 ConditionalConverter 接口的联合,可让您定义以下自定义匹配条件:
1 | public interface ConditionalConverter { |
ConditionalGenericConverter
的一个很好的例子是 EntityConverter
,它在持久实体标识符和实体引用之间进行转换。仅当目标实体类型声明静态查找器方法(例如 findAccount(Long)
)时,此类 EntityConverter
才可能匹配。 您可以在 matchs(TypeDescriptor,TypeDescriptor)
的实现中执行这种 finder 方法检查。
ConversionService
APIConversionService
定义了一个统一的 API,用于在运行时执行类型转换逻辑。转换器通常在以下外观接口后面执行:
1 | package org.springframework.core.convert; |
大多数 ConversionService
实现也都实现 ConverterRegistry
,该转换器提供用于注册转换器的 SPI。在内部,ConversionService
实现委派其注册的转换器执行类型转换逻辑。
core.convert.support
软件包中提供了一个强大的 ConversionService
实现。 GenericConversionService
是适用于大多数环境的通用实现。ConversionServiceFactory
提供了一个方便的工厂来创建通用的 ConversionService
配置。
ConversionService
ConversionService
是无状态对象,旨在在应用程序启动时实例化,然后在多个线程之间共享。在 Spring 应用程序中,通常为每个 Spring 容器(或 ApplicationContext
)配置一个 ConversionService
实例。当框架需要执行类型转换时,Spring 会调用该 ConversionService
并使用它。 您还可以将此 ConversionService
注入到任何 bean 中,然后直接调用它。
如果未向 Spring 注册任何
ConversionService
,则使用原始的基于PropertyEditor
的系统。
要向 Spring 注册默认的 ConversionService
,请添加以下 bean 定义,其 id 为 conversionService
:
1 | <bean id="conversionService" |
默认的 ConversionService
可以在字符串,数字,枚举,集合,映射和其他常见类型之间进行转换。 要用您自己的自定义转换器补充或覆盖默认转换器,请设置 converters
属性。 属性值可以实现 Converter
,ConverterFactory
或 GenericConverter
接口中的任何一个。
1 | <bean id="conversionService" |
在 Spring MVC 应用程序中使用 ConversionService
也很常见。 参见 Spring MVC 一章中的转换和格式化。
在某些情况下,您可能希望在转换过程中应用格式设置。 有关使用 FormattingConversionServiceFactoryBean
的详细信息,请参见 FormatterRegistry
SPI。
要以编程方式使用 ConversionService 实例,可以像对其他任何 bean 一样注入对该实例的引用。 以下示例显示了如何执行此操作:
1 |
|
对于大多数用例,可以使用指定 targetType 的 convert 方法,但不适用于更复杂的类型,例如参数化元素的集合。 例如,如果要以编程方式将整数列表转换为字符串列表,则需要提供源类型和目标类型的正式定义。
幸运的是,如下面的示例所示,TypeDescriptor 提供了各种选项来使操作变得简单明了:
1 | DefaultConversionService cs = new DefaultConversionService(); |
请注意,DefaultConversionService
自动注册适用于大多数环境的转换器。 这包括集合转换器,标量转换器和基本的对象到字符串转换器。 您可以使用 DefaultConversionService
类上的静态 addDefaultConverters
方法向任何 ConverterRegistry
注册相同的转换器。
值类型的转换器可重用于数组和集合,因此,假设标准的集合处理适当,则无需创建特定的转换器即可将 S 的集合转换为 T 的集合。
如上一节所述,core.convert
是一种通用类型转换系统。它提供了统一的 ConversionService
API 和强类型的 Converter
SPI,用于实现从一种类型到另一种类型的转换逻辑。 Spring 容器使用此系统绑定 bean 属性值。此外,Spring Expression Language(SpEL)和 DataBinder
都使用此系统绑定字段值。例如,当 SpEL 需要强制将 Short
转换为 Long
来完成 expression.setValue(Object bean, Object value)
尝试时,core.convert
系统将执行强制转换。
现在考虑典型客户端环境(例如 Web 或桌面应用程序)的类型转换要求。在这样的环境中,您通常会从 String
转换为支持客户端回发过程,然后又转换为 String
以支持视图渲染过程。另外,您通常需要本地化 String
值。更通用的 core.convert
Converter
SPI 不能直接满足此类格式化要求。为了直接解决这些问题,Spring 3 引入了方便的 Formatter
SPI,它为客户端环境提供了 PropertyEditor
实现的简单而强大的替代方案。
通常,当您需要实现通用类型转换逻辑时(例如,用于在 java.util.Date
和 Long
之间进行转换),可以使用 Converter
SPI。在客户端环境(例如 Web 应用程序)中工作并且需要解析和打印本地化的字段值时,可以使用 Formatter
SPI。 ConversionService
为两个 SPI 提供统一的类型转换 API。
用于实现字段格式化逻辑的 Formatter
SPI 非常简单且类型严格。 以下清单显示了 Formatter
接口定义:
1 | package org.springframework.format; |
Formatter
从 Printer
和 Parser
构建块接口扩展。 以下清单显示了这两个接口的定义:
1 | public interface Printer<T> { |
1 | import java.text.ParseException; |
要创建自己的 Formatter
,请实现前面显示的 Formatter
接口。将 T 参数化为您希望格式化的对象的类型(例如 java.util.Date
)。实现 print()
操作以打印 T 的实例以在客户端语言环境中显示。实现 parse()
操作,以从客户端语言环境返回的格式化表示形式解析 T 的实例。如果解析尝试失败,则 Formatter
应该抛出 ParseException
或 IllegalArgumentException
。注意确保您的 Formatter
实现是线程安全的。
format
子包为方便起见提供了几种 Formatter
实现。数字程序包提供 NumberStyleFormatter``,CurrencyStyleFormatter
和 PercentStyleFormatter
来格式化使用 java.text.NumberFormat
的 Number
对象。datetime
包提供了一个 DateFormatter
,用于使用 java.text.DateFormat
格式化 java.util.Date
对象。datetime.joda
包基于 Joda-Time 库提供了全面的日期时间格式支持。
以下 DateFormatter
是 Formatter
实现的示例:
1 | package org.springframework.format.datetime; |
Spring 团队欢迎社区推动的 Formatter
贡献。 请参阅 GitHub 问题以做出贡献。
可以通过字段类型或注释配置字段格式。要将注释绑定到 Formatter
,请实现 AnnotationFormatterFactory
。以下显示了AnnotationFormatterFactory
接口的定义:
1 | package org.springframework.format; |
要创建一个实现:将 A 参数化为要与格式逻辑关联的字段注解类型,例如 org.springframework.format.annotation.DateTimeFormat
。让 getFieldTypes()
返回可在其上使用注释的字段类型。让 getPrinter()
返回 Printer
以打印带注释的字段的值。让 getParser()
返回解析器以解析带注释字段的 clientValue
。
以下示例 AnnotationFormatterFactory
实现将 @NumberFormat
批注绑定到格式化程序,以指定数字样式或模式:
1 | public final class NumberFormatAnnotationFormatterFactory |
要触发格式,可以使用 @NumberFormat
注释字段,如以下示例所示:
1 | public class MyModel { |
org.springframework.format.annotation
包中存在一个可移植的格式注释 API。 您可以使用 @NumberFormat
格式化数字字段(例如 Double 和 Long),并使用 @DateTimeFormat
格式化 java.util.Date
,java.util.Calendar
,Long
(用于毫秒时间戳)以及 JSR-310 java.time
和 Joda-Time 值类型。
以下示例使用 @DateTimeFormat
将 java.util.Date
格式化为 ISO 日期(yyyy-MM-dd):
1 | public class MyModel { |
FormatterRegistry
SPIFormatterRegistry
是用于注册格式器和转换器的 SPI。FormattingConversionService
是适用于大多数环境的 FormatterRegistry
的实现。 您可以通过编程方式或声明方式将此变体配置为 Spring Bean,例如通过使用 FormattingConversionServiceFactoryBean
。 由于此实现还实现了 ConversionService
,因此您可以直接将其配置为与 Spring 的 DataBinder
和 Spring 表达式语言(SpEL)一起使用。
1 | package org.springframework.format; |
如前面的清单所示,您可以按字段类型或批注注册格式化程序。
FormatterRegistry
SPI 使您可以集中配置格式设置规则,而不必在控制器之间复制此类配置。 例如,您可能要强制所有日期字段以某种方式设置格式或带有特定注释的字段以某种方式设置格式。 使用共享的 FormatterRegistry
,您可以一次定义这些规则,并在需要格式化时应用它们。
FormatterRegistrar
SPIFormatterRegistrar
是一个 SPI,用于通过 FormatterRegistry
注册格式器和转换器。 以下清单显示了其接口定义:
1 | package org.springframework.format; |
为给定的格式类别(例如日期格式)注册多个相关的转换器和格式器时,FormatterRegistrar
很有用。 在声明式注册不足的情况下(例如,当格式化程序需要在不同于其自身 <T>
的特定字段类型下建立索引或注册 Printer
/Parser
对时),它也很有用。 下一节将提供有关转换器和格式化程序注册的更多信息。
https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-config-conversion
默认情况下,未使用 @DateTimeFormat
注释的日期和时间字段是使用 DateFormat.SHORT
样式从字符串转换的。 如果愿意,可以通过定义自己的全局格式来更改此设置。
为此,请确保 Spring 不注册默认格式器。 相反,可以借助以下方法手动注册格式化程序:
org.springframework.format.datetime.standard.DateTimeFormatterRegistrar
org.springframework.format.datetime.DateFormatterRegistrar
或 joda-Time 的 org.springframework.format.datetime.joda.JodaTimeFormatterRegistrar
。例如,以下 Java 配置注册全局 yyyyMMdd 格式:
1 |
|
如果您喜欢基于 XML 的配置,则可以使用 FormattingConversionServiceFactoryBean。 以下示例显示了如何执行此操作(这次使用 Joda Time):
1 |
|
请注意,在 Web 应用程序中配置日期和时间格式时,还有其他注意事项。请参阅 WebMVC 转换和格式或 WebFlux 转换和格式。
Spring 框架提供了对 Java Bean 验证 API 的支持。
Bean 验证为 Java 应用程序提供了通过约束声明和元数据进行验证的通用方法。要使用它,您需要使用声明性验证约束对域模型属性进行注释,然后由运行时强制实施。有内置的约束,您也可以定义自己的自定义约束。
考虑以下示例,该示例显示了具有两个属性的简单 PersonForm
模型:
1 | public class PersonForm { |
Bean 验证使您可以声明约束,如以下示例所示:
1 | public class PersonForm { |
然后,Bean 验证验证器根据声明的约束来验证此类的实例。 有关该 API 的一般信息,请参见 Bean 验证。 有关特定限制,请参见 Hibernate Validator 文档。 要学习如何将 bean 验证提供程序设置为 Spring bean,请继续阅读。
Spring 提供了对 Bean 验证 API 的全面支持,包括将 Bean 验证提供程序作为 Spring Bean 进行引导。 这使您可以在应用程序中需要验证的任何地方注入 javax.validation.ValidatorFactory
或 javax.validation.Validator
。
您可以使用 LocalValidatorFactoryBean
将默认的 Validator
配置为 Spring Bean,如以下示例所示:
1 | import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; |
前面示例中的基本配置触发 Bean 验证以使用其默认引导机制进行初始化。 Bean 验证提供程序,例如 Hibernate Validator,应该存在于类路径中并被自动检测到。
LocalValidatorFactoryBean
同时实现 javax.validation.ValidatorFactory
和 javax.validation.Validator
以及 Spring 的 org.springframework.validation.Validator
。 您可以将对这些接口之一的引用注入需要调用验证逻辑的 bean 中。
如果您希望直接使用 Bean Validation API,则可以注入对 javax.validation.Validator
的引用,如以下示例所示:
1 | import javax.validation.Validator; |
如果您的 bean 需要使用 Spring Validation API,则可以注入对 org.springframework.validation.Validator
的引用,如以下示例所示:
1 | import org.springframework.validation.Validator; |
每个 bean 验证约束都包括两个部分:
@Constraint
批注,用于声明约束及其可配置属性。
javax.validation.ConstraintValidator
接口的实现,用于实现约束的行为。
要将声明与实现相关联,每个 @Constraint
批注都引用一个对应的 ConstraintValidator
实现类。 在运行时,当在域模型中遇到约束注释时,ConstraintValidatorFactory
实例化引用的实现。
默认情况下,LocalValidatorFactoryBean
配置一个 SpringConstraintValidatorFactory
,该工厂使用 Spring 创建 ConstraintValidator
实例。 这使您的自定义 ConstraintValidators
像其他任何 Spring bean 一样受益于依赖项注入。
以下示例显示了一个自定义 @Constraint
声明,后跟一个关联的 ConstraintValidator
实现,该实现使用 Spring 进行依赖项注入:
1 | ({ElementType.METHOD, ElementType.FIELD}) |
1 | import javax.validation.ConstraintValidator; |
如前面的示例所示,ConstraintValidator
实现可以像其他任何 Spring bean 一样具有其 @Autowired
依赖项。
您可以通过 MethodValidationPostProcessor
bean 定义将 Bean Validation 1.1(以及作为自定义扩展,还包括 Hibernate Validator 4.3)支持的方法验证功能集成到 Spring 上下文中:
1 | import org.springframework.validation.beanvalidation.MethodValidationPostProcessor; |
为了有资格进行 Spring 驱动的方法验证,所有目标类都必须使用 Spring 的 @Validated
注释进行注释,该注释也可以选择声明要使用的验证组。 有关使用 Hibernate Validator 和 Bean Validation 1.1 提供程序的设置详细信息,请参见 MethodValidationPostProcessor
。
方法验证依赖于目标类周围的 AOP 代理,即接口上方法的 JDK 动态代理或 CGLIB 代理。 代理的使用存在某些限制,《了解 AOP 代理》 中介绍了其中的一些限制。 另外,请记住在代理类上始终使用方法和访问器;直接现场字段将不起作用。
在大多数情况下,默认 LocalValidatorFactoryBean
配置就足够了。 从消息插值到遍历解析,有许多用于各种 Bean 验证构造的配置选项。 有关这些选项的更多信息,请参见 LocalValidatorFactoryBean
Javadoc。
DataBinder
从 Spring 3 开始,您可以使用 Validator 配置 DataBinder
实例。 配置完成后,您可以通过调用 binder.validate()
来调用 Validator
。任何验证 Errors
都会自动添加到活页夹的 BindingResult
中。
下面的示例演示如何在绑定到目标对象后,以编程方式使用 DataBinder
来调用验证逻辑:
1 | Foo target = new Foo(); |
您还可以通过 dataBinder.addValidators
和 dataBinder.replaceValidators
配置具有多个 Validator
实例的 DataBinder
。当将全局配置的 bean 验证与在 DataBinder
实例上本地配置的 Spring Validator 结合使用时,这很有用。 请参阅 Spring MVC 验证配置。
参见 Spring MVC 中的验证。
]]>接下来到 Spring framework core 的第二大块 —— 资源
本章介绍了 Spring 如何处理资源以及如何在 Spring 中使用资源。它包括以下主题:
ResourceLoaderAware
接口不幸的是,Java 的标准 java.net.URL
类和用于各种 URL 前缀的标准处理程序不足以满足所有对低级资源的访问。例如,没有标准化的 URL
实现可用于访问需要从类路径或相对于 ServletContext
获得的资源。 虽然可以注册用于特殊 URL
前缀的新处理程序(类似于用于诸如 http:
的现有前缀的处理程序),但这通常相当复杂,并且 URL
接口仍然缺少某些理想的功能,例如用于检查是否存在的方法指向的资源。
Spring 的 Resource
接口旨在成为一种功能更强大的接口,用于抽象化对低级资源的访问。以下清单显示了 Resource
接口定义:
1 | public interface Resource extends InputStreamSource { |
如 Resource
接口的定义所示,它扩展了 InputStreamSource
接口。以下清单显示了 InputStreamSource
接口的定义:
1 | public interface InputStreamSource { |
Resource 接口中一些最重要的方法是:
getInputStream()
:找到并打开资源,返回一个 InputStream
以便从资源中读取。预期每次调用都返回一个新的 InputStream
。调用方有责任关闭流。exist()
:返回一个布尔值,指示此资源是否实际以物理形式存在。isOpen()
:返回一个布尔值,指示此资源是否表示具有打开流的句柄。如果为 true,则不能多次读取 InputStream
,必须只读取一次,然后将其关闭以避免资源泄漏。对于所有常规资源实现,返回 false
,但 InputStreamResource
除外。getDescription()
:返回对此资源的描述,用于在处理资源时用于错误输出。这通常是标准文件名或资源的实际 URL。其他方法可让您获取代表资源的实际 URL
或 File
对象(如果基础实现兼容并且支持该功能)。
当需要资源时,Spring 本身广泛使用 Resource
抽象作为许多方法签名中的参数类型。一些 Spring API 中的其他方法(例如,各种 ApplicationContext
实现的构造函数)采用 String 形式,该字符串以未经修饰或简单的形式用于创建适合该上下文实现的 Resource
,或者通过 String 路径上的特殊前缀,让调用者指定必须创建并使用特定的资源实现。
尽管 Spring 经常使用 Resource
接口,但实际上,在您自己的代码中单独用作通用实用工具类来访问资源也非常有用,即使您的代码不了解也不关心 Spring 的其他任何部分。虽然这将您的代码耦合到 Spring,但实际上仅将其耦合到这套实用程序类,它们充当 URL
的更强大替代,并且可以被视为等同于您将用于此目的的任何其他库。
Resource
抽象不能替代功能。它尽可能地包装它。例如,UrlResource
包装一个URL
,然后使用包装的URL
进行工作。
Spring 包含以下 Resource 实现:
UrlResource
包装了 java.net.URL
,可用于访问通常可以通过 URL 访问的任何对象,例如文件、HTTP 目标、FTP 目标等。所有 URL 都具有标准化的 String 表示形式,因此使用适当的标准化前缀来指示另一种 URL 类型。其中包括 file:
用于访问文件系统路径,http:
用于通过 HTTP 协议访问资源,ftp:
用于通过 FTP 访问资源等。
UrlResource
是由 Java 代码通过显式使用 UrlResource
构造函数创建的,但通常在调用带有 String 参数表示路径的 API 方法时隐式创建。对于后一种情况,JavaBeans PropertyEditor
最终决定要创建哪种类型的资源。如果路径字符串包含众所周知的前缀(例如 classpath:
),则它将为该前缀创建适当的专用资源。但是,如果它不能识别前缀,则假定该字符串是标准 URL 字符串并创建一个 UrlResource
。
此类表示应从类路径获取的资源。它使用线程上下文类加载器,给定的类加载器或给定的类来加载资源。
如果类路径资源驻留在文件系统中,而不是驻留在 jar 中并且尚未(通过 servlet 引擎或任何环境将其扩展到)文件系统的类路径资源驻留在文件系统中,则此 Resource
实现以 java.io.File
支持解析。 。为了解决这个问题,各种 Resource
实现始终支持将解析作为 java.net.URL
。
Java 代码通过显式使用 ClassPathResource
构造函数来创建 ClassPathResource
,但通常在调用带有 String 参数表示路径的 API 方法时隐式创建 ClassPathResource
。对于后一种情况,JavaBeans PropertyEditor 可以识别字符串路径上的特殊前缀 classpath:
,并在这种情况下创建 ClassPathResource
。
这是 java.io.File
和 java.nio.file.Path
句柄的 Resource
实现。它支持解析为 File
和 URL
。
这是 ServletContext
资源的 Resource
实现,它解释相关 Web 应用程序根目录中的相对路径。
它始终支持流访问和 URL 访问,但仅在扩展 Web 应用程序档案且资源实际位于文件系统上时才允许 java.io.File 访问。它是在文件系统上扩展还是直接扩展,或者直接从 JAR 或其他类似数据库(可以想到的)中访问,实际上取决于 Servlet 容器。
InputStreamResource
是给定 InputStream
的 Resource
实现。仅当没有特定的资源实现适用时才应使用它。特别是,在可能的情况下,最好选择 ByteArrayResource
或任何基于文件的 Resource
实现。
与其他 Resource
实现相反,这是一个已经打开的资源的描述符。因此,它从 isOpen()
返回 true。如果需要将资源描述符保留在某个地方,或者需要多次读取流,请不要使用它。
这是给定字节数组的 Resource
实现。它为给定的字节数组创建一个 ByteArrayInputStream
。
这对于从任何给定的字节数组加载内容很有用,而不必求助于一次性的 InputStreamResource
。
ResourceLoader
接口旨在由可以返回(即加载)Resource
实例的对象实现。以下显示了 ResourceLoader
接口定义:
1 | public interface ResourceLoader { |
所有应用程序上下文均实现该 ResourceLoader
接口。因此,所有应用程序上下文都可用于获取 Resource
实例。
当您调用 getResource()
特定的应用程序上下文时,并且指定的位置路径没有特定的前缀时,您将获得 Resource
适合该特定应用程序上下文的类型。例如,假定针对 ClassPathXmlApplicationContext
实例执行了以下代码段:
1 | Resource template = ctx.getResource("some/resource/path/myTemplate.txt"); |
针对 ClassPathXmlApplicationContext
,该代码返回 ClassPathResource
。如果对 FileSystemXmlApplicationContext
实例执行相同的方法,则将返回 FileSystemResource
。对于 WebApplicationContext
,它将返回 ServletContextResource
。类似地,它将为每个上下文返回适当的对象。
所以,您可以以适合特定应用程序上下文的方式加载资源。
另一方面,ClassPathResource
无论应用程序上下文类型如何,您都可以通过指定特殊 classpath:
前缀来强制使用,如以下示例所示:
1 | Resource template = ctx.getResource("classpath:some/resource/path/myTemplate.txt"); |
同样,您可以通过指定任何标准 java.net.URL
前缀来强制使用 UrlResource
。 以下两个示例使用 file
和 http
前缀:
1 | Resource template = ctx.getResource("file:///some/resource/path/myTemplate.txt"); |
1 | Resource template = ctx.getResource("https://myhost.com/resource/path/myTemplate.txt"); |
下表总结了将 String 对象转换为 Resource 对象的策略:
前缀 | 举例 | 解释 |
---|---|---|
classpath: | classpath:com/myapp/config.xml | 从类路径加载。 |
file: | file:///data/config.xml | 从文件系统作为URL 加载。另请参见 FileSystemResource 警告 。 |
http: | https://myserver/logo.png | 加载为 URL 。 |
(无) | /data/config.xml | 取决于基础 ApplicationContext。 |
ResourceLoaderAware
接口ResourceLoaderAware
接口是一个特殊的回调接口,用于标识期望提供 ResourceLoader
引用的组件。 以下显示了 ResourceLoaderAware
接口的定义:
1 | public interface ResourceLoaderAware { |
当一个类实现 ResourceLoaderAware
并部署到应用程序上下文中(作为 Spring 托管的 bean)时,该类被应用程序上下文识别为 ResourceLoaderAware
。然后,应用程序上下文调用 setResourceLoader(ResourceLoader)
,将自身作为参数提供(请记住,Spring 中的所有应用程序上下文均实现 ResourceLoader
接口)。
由于 ApplicationContext
是 ResourceLoader,因此
Bean 也可以实现 ApplicationContextAware
接口,并直接使用提供的应用程序上下文来加载资源。但是,通常,如果需要的话,最好使用专用的 ResourceLoader
接口。该代码将仅耦合到资源加载接口(可以视为实用程序接口),而不耦合到整个 Spring ApplicationContext
接口。
在应用程序组件中,您还可以依靠自动装配 ResourceLoader
来实现 ResourceLoaderAware
接口。“传统”构造函数和 byType 自动装配模式(如“自动装配协作器”中所述)能够分别为构造函数参数或 setter 方法参数提供 ResourceLoader
。为了获得更大的灵活性(包括自动装配字段和多个参数方法的能力),请考虑使用基于注释的自动装配功能。在这种情况下,只要有问题的字段,构造函数或方法带有 @Autowired
批注,ResourceLoader
就会自动连接到需要 ResourceLoader
类型的字段,构造函数参数或方法参数中。有关更多信息,请参见前文。
如果 Bean 本身将通过某种动态过程来确定和提供资源路径,那么对于 Bean 来说,使用 ResourceLoader
接口加载资源可能是有意义的。例如,考虑加载某种模板,其中所需的特定资源取决于用户的角色。如果资源是静态的,则有必要完全消除对 ResourceLoader
接口的使用,让 Bean 公开所需的 Resource
属性,并期望将其注入其中。
然后注入这些属性的琐事是,所有应用程序上下文都注册并使用了特殊的 JavaBeans PropertyEditor,它可以将 String 路径转换为 Resource
对象。 因此,如果 myBean 具有资源类型的模板属性,则可以为该资源配置一个简单的字符串,如以下示例所示:
1 | <bean id="myBean" class="..."> |
请注意,资源路径没有前缀。因此,由于应用程序上下文本身将用作 ResourceLoader
,因此根据上下文的确切类型,通过 ClassPathResource
,FileSystemResource
或 ServletContextResource
加载资源本身。
如果需要强制使用特定的资源类型,则可以使用前缀。 以下两个示例显示了如何强制 ClassPathResource
和 UrlResource
(后者用于访问文件系统文件):
1 | <property name="template" value="classpath:some/resource/path/myTemplate.txt"> |
1 | <property name="template" value="file:///some/resource/path/myTemplate.txt"/> |
本节介绍如何使用资源创建应用程序上下文,包括使用 XML 的快捷方式,如何使用通配符以及其他详细信息。
应用程序上下文构造函数(针对特定的应用程序上下文类型)通常采用字符串或字符串数 组作为资源的位置路径,例如构成上下文定义的 XML 文件。
当这样的位置路径没有前缀时,从该路径构建并用于加载 Bean 定义的特定 Resource
类型取决于特定应用程序上下文,并且适用于该特定应用程序上下文。 例如,考虑以下示例,该示例创建一个 ClassPathXmlApplicationContext
:
1 | ApplicationContext ctx = new ClassPathXmlApplicationContext("conf/appContext.xml"); |
由于使用了 ClassPathResource
,因此从类路径中加载了 Bean 定义。 但是,请考虑以下示例,该示例创建一个 FileSystemXmlApplicationContext
:
1 | ApplicationContext ctx = |
现在,bean 定义是从文件系统位置(在这种情况下,是相对于当前工作目录)加载的。
请注意,在位置路径上使用特殊的类路径前缀或标准 URL 前缀会覆盖为加载定义而创建的默认资源类型。 考虑以下示例:
1 | ApplicationContext ctx = |
使用 FileSystemXmlApplicationContext
从类路径加载 bean 定义。 但是,它仍然是 FileSystemXmlApplicationContext
。 如果随后将其用作 ResourceLoader
,则任何未前缀的路径仍将视为文件系统路径。
ClassPathXmlApplicationContext
实例 —— 快捷方式ClassPathXmlApplicationContext
公开了许多构造函数以启用方便的实例化。基本思想是,您只能提供一个字符串数组,该字符串数组仅包含 XML 文件本身的文件名(不包含前导路径信息),并且还提供一个 Class。然后,ClassPathXmlApplicationContext
从提供的类中派生路径信息。
请考虑以下目录布局:
1 | com/ |
以下示例显示如何实例化由在名为 service.xml
和 daos.xml
(位于类路径中)的文件中定义的 bean 组成的 ClassPathXmlApplicationContext
实例:
1 | ApplicationContext ctx = new ClassPathXmlApplicationContext( |
有关各种构造函数的详细信息,请参见 ClassPathXmlApplicationContext
javadoc。
应用程序上下文构造函数值中的资源路径可以是简单路径(如先前所示),每个路径都具有到目标资源的一对一映射,或者可以包含特殊的classpath*:
前缀或内部 Ant 样式的正则表达式(通过使用 Spring 的 PathMatcher
实用程序进行匹配)。后者都是有效的通配符。
这种机制的一种用途是当您需要进行组件样式的应用程序组装时。所有组件都可以将上下文定义片段“发布”到一个众所周知的位置路径,并且当使用前缀为 classpath*:
的相同路径创建最终应用程序上下文时,所有组件片段都会被自动拾取。
请注意,此通配符特定于在应用程序上下文构造函数中使用资源路径(或当您直接使用 PathMatcher
实用工具类层次结构时),并且在构造时已解决。它与资源类型本身无关。您不能使用 classpath*:
前缀来构造实际的 Resource
,因为资源一次仅指向一个资源。
路径位置可以包含 Ant 样式的模式,如以下示例所示:
1 | /WEB-INF/*-context.xml |
当路径位置包含 Ant 样式的模式时,解析程序将遵循更复杂的过程来尝试解析通配符。 它为到达最后一个非通配符段的路径生成资源,并从中获取 URL。 如果此 URL 不是 jar:
URL 或特定于容器的变体(例如 WebLogic 中的 zip:
,WebSphere 中的 wsjar
等),则从中获取 java.io.File
并将其用于遍历文件系统。 对于 jar URL,解析器可以从中获取 java.net.JarURLConnection
,也可以手动解析 jar URL,然后遍历 jar 文件的内容以解析通配符。
如果指定的路径已经是一个文件 URL(由于基本 ResourceLoader
是一个文件系统,所以它是隐式的,或者是显式的),则保证通配符可以完全可移植的方式工作。
如果指定的路径是类路径位置,则解析器必须通过调用 Classloader.getResource()
获得最后的非通配符路径段 URL。由于这只是路径的一个节点(而不是末尾的文件),因此实际上(在 ClassLoader
javadoc 中)未定义确切返回的是哪种 URL。实际上,它始终是代表目录的 java.io.File
(类路径资源在其中解析到文件系统位置)或某种 jar URL(类路径资源在 jar 上解析)。尽管如此,此操作仍存在可移植性问题。
如果为最后一个非通配符段获取了 jar URL,则解析程序必须能够从中获取 java.net.JarURLConnection
或手动解析 jar URL,以便能够遍历 jar 的内容并解析通配符。这在大多数环境中确实有效,但在其他环境中则无效,因此我们强烈建议您在依赖特定环境之前,对来自 jars 的资源的通配符解析进行彻底测试。
classpath*:
前缀在构造基于 XML 的应用程序上下文时,位置字符串可以使用特殊的 classpath*:
前缀,如以下示例所示:
1 | ApplicationContext ctx = |
这个特殊的前缀指定必须获取与给定名称匹配的所有类路径资源(内部其实是通过调用 ClassLoader.getResources(…)
完成的),然后合并形成最终的应用程序上下文定义。
通配符类路径依赖于基础类加载器的 getResources()
方法。 由于当今大多数应用程序服务器都提供自己的类加载器实现,因此行为可能有所不同,尤其是在处理 jar 文件时。 检查 classpath*
是否有效的一个简单测试是使用 classloader
从 classpath 的 jar 中加载文件:getClass().getClassLoader().getResources("<someFileInsideTheJar>")
。 尝试对具有相同名称但位于两个不同位置的文件进行此测试。如果返回了不合适的结果,请检查应用程序服务器文档中可能会影响类加载器行为的设置。
您还可以在其余位置路径中将 classpath*:
前缀与 PathMatcher
模式结合使用(例如,classpath*:META-INF/*-beans.xml
)。在这种情况下,解析策略非常简单:在最后一个非通配符路径段上使用 ClassLoader.getResources()
调用,以获取类加载器层次结构中的所有匹配资源,然后从每个资源获取相同的 PathMatcher
解析 前面描述的策略用于通配符子路径。
请注意,当 classpath*:
与 Ant 样式的模式结合使用时,除非模式文件实际驻留在文件系统中,否则在模式启动之前,它只能与至少一个根目录可靠地一起工作。这意味着诸如 classpath*:*.xml
之类的模式可能不会从 jar 文件的根目录检索文件,而只会从扩展目录的根目录检索文件。
Spring 检索类路径条目的能力源自 JDK 的 ClassLoader.getResources()
方法,该方法仅返回文件系统中的空字符串位置(指示可能要搜索的根目录)。 Spring 还会评估 jar 文件中的 URLClassLoader
运行时配置和 java.class.path
清单,但这不能保证会导致可移植行为。
扫描类路径包需要在类路径中存在相应的目录条目。使用 Ant 构建 JAR 时,请勿激活 JAR 任务的仅文件开关。此外,在某些环境中,基于安全策略,可能不会公开类路径目录-例如,在 JDK 1.7.0_45 及更高版本上的独立应用程序(要求在清单中设置“受信任的库”。请参阅 https://stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources)。
在 JDK 9 的模块路径(Jigsaw)上,Spring 的类路径扫描通常可以正常进行。强烈建议在此处将资源放入专用目录,以避免在搜索 jar 文件根目录级别时出现上述可移植性问题。
具有 classpath:
的 Ant 样式模式要搜索的根包如果在多个类路径位置可用,则不能保证资源找到匹配的资源。考虑以下资源位置示例:
1 | com/mycompany/package1/service-context.xml |
现在考虑某人可能用来尝试找到该文件的 Ant 样式的路径:
1 | classpath:com/mycompany/**/service-context.xml |
这样的资源可能只在一个位置,但是当使用诸如上一示例的路径尝试对其进行解析时,解析器将处理 getResource("com/mycompany");
返回的(第一个)URL。如果此基本包节点存在于多个类加载器位置,则实际的最终资源可能不存在。因此,在这种情况下,您应该首选使用具有相同 Ant 样式模式的 classpath*:
,该模式将搜索包含根包的所有类路径位置。
FileSystemResource
警告未附加到 FileSystemApplicationContext
的 FileSystemResource
(即,当 FileSystemApplicationContext
不是实际的 ResourceLoader
时)将按您期望的方式处理绝对路径和相对路径。相对路径是相对于当前工作目录的,而绝对路径是相对于文件系统的根的。
但是,出于向后兼容性(历史)的原因,当 FileSystemApplicationContext
是 ResourceLoader
时,此情况会更改。FileSystemApplicationContext
强制所有附加的 FileSystemResource
实例将所有位置路径都视为相对位置,无论它们是否以前斜杠开头。 实际上,这意味着以下示例是等效的:
1 | ApplicationContext ctx = |
1 | ApplicationContext ctx = |
以下示例也是等效的(尽管使它们有所不同是有意义的,因为一种情况是相对的,另一种情况是绝对的):
1 | FileSystemXmlApplicationContext ctx = ...; |
1 | FileSystemXmlApplicationContext ctx = ...; |
在实践中,如果需要真正的绝对文件系统路径,则应避免将绝对路径与 FileSystemResource
或 FileSystemXmlApplicationContext
一起使用,并通过使用 file:
URL 前缀来强制使用 UrlResource
。以下示例显示了如何执行此操作:
1 | // actual context type doesn't matter, the Resource will always be UrlResource |
1 | // force this FileSystemXmlApplicationContext to load its definition via a UrlResource |
Spring Framework 提供了许多可用于自定义 bean 行为的接口。依次讲这三种接口:
想要介入容器的 bean 生命周期管理,可以实现 Spring InitializingBean 和 DisposableBean 接口。前者调用 afterPropertiesSet()初始化,后者调用 destroy() 销毁 bean 。
JSR-250 规范中的 @PostConstruct
和 @PreDestroy
注释通常被认为是在现代 Spring 应用程序中接收生命周期回调的最佳实践。使用这些注释可以使 bean 解耦于这些 Spring 的接口。详见下文。
如果不想使用 JSR-250 规范的注释但仍想解耦,可以使用 init-method 和 destroy-method。
在内部,Spring Framework 使用 BeanPostProcessor 实现来处理它可以找到的任何回调接口并调用对应的方法。如果需要自定义其他 Spring 默认不提供的功能或生命周期行为,可以 BeanPostProcessor 自己实现。
除了初始化和销毁 回调之外,Spring 管理的对象还可以实现 Lifecycle 接口,以便这些对象可以参与启动和关闭过程,这是由容器自身的生命周期驱动的。
接下来讲生命周期回调接口。
org.springframework.beans.factory.InitializingBean 接口允许在容器设置 bean 的所有必要属性后进行初始化工作。InitializingBean 接口规定了一个方法:
1 | void afterPropertiesSet() throws Exception; |
当然,上文说了,并不推荐使用 InitializingBean 接口,更好的做法是使用 @PostConstruct
方法或者指定 POJO 初始化方法。可以使用 XML 中的 init-method 属性或者 @Bean
注解的 initMethod 指定一个返回 void 且无参的方法作为初始化方法。
对应的 org.springframework.beans.factory.DisposableBean 接口规定了一个销毁前执行的回调:
1 | void destroy() throws Exception; |
同理,建议 @PreDestroy
和 destroy-method 的 XML 配置或 @Bean
的 destroyMethod 属性。
你可以将所有的初始化、销毁的方法使用相同的命名(比如 init、initialize、dispose 等),那么就可以定义默认初始化和析构方法。
举例:
1 | <beans default-init-method="init"> |
Spring 容器保证在为 bean 提供所有依赖项后立即调用已配置的初始化回调,所以初始化回调会在 AOP 作用之前。首先完全创建 bean,然后配置带有拦截器链的 AOP 代理。所以在 init 方法上使用拦截器可能会导致和预想不一致,因为这样会导致目标 bean 的生命周期与代理或拦截器耦合,代码与原始目标 bean 杂糅的语义就会难以预测。
如果使用了多种生命周期机制,他们的先后顺序如下:
@PostConstruct
afterPropertiesSet()
回调init()
方法Destroy 方法以相同的顺序调用:
@PreDestroy
destroy()
回调destroy()
方法如果是同一个函数,那么只会执行一次。
Lifecycle 接口为任何有生命周期要求的对象(例如启动和停止某些后台进程)定义了基本的方法:
1 | public interface Lifecycle { |
任何 Spring 管理的对象都可以实现 Lifecycle 接口。当 ApplicationContext 接收到启动和停止信号时(例如,对于运行时的停止/重启场景),它将这些调用级联到容器内定义的所有 Lifecycle 实现,这个过程由 LifecycleProcessor 实现:
1 | public interface LifecycleProcessor extends Lifecycle { |
LifecycleProcessor 是 Lifecycle 接口的扩展。它又添加了另外两种方法来响应刷新和关闭的容器。
值得注意的是,常规 org.springframework.context.Lifecycle
接口是显式启动和停止通知的简单合约,并不意味着在上下文刷新时自动启动。要对特定 bean 的自动启动(包括启动阶段)进行细粒度控制,需要实现 org.springframework.context.SmartLifecycle
。
如果要控制顺序,可以再继承 Phased 接口:
1 | public interface Phased { |
SmartLifecycle
就继承了这个接口:
1 | public interface SmartLifecycle extends Lifecycle, Phased { |
Phase 表示相位,启动时从低相位开始执行,停止时低相位最后停止。换句话说,Integer.MIN_VALUE
相位的第一个开始、最后一个停止,而 Integer.MAX_VALUE
最后启动并首先停止。未定义的相位默认为 0。
SmartLifecycle 定义了 stop 回调方法。任何实现在关闭过程完成之后都必须调用其 callback
的 run()
方法,这样就可以在必要时启用异步关闭。LifecycleProcessor 接口的默认实现 DefaultLifecycleProcessor 等待每个阶段内对象组的超时值来调用该回调,默认的每阶段超时为 30 秒。也可以通过定义名为 lifecycleProcessor 的 bean 来覆盖默认生命周期处理器实例(不定义就是默认的 DefaultLifecycleProcessor)。如果只想修改超时,则定义以下内容就足够了:
1 | <bean id="lifecycleProcessor" class="org.springframework.context.support.DefaultLifecycleProcessor"> |
Spring 的基于 Web 的 ApplicationContext 实现已经可以在相关 Web 应用程序关闭时正常关闭 Spring IoC 容器。本节仅适用于非 Web 应用程序。
如果在非 Web 应用程序环境中使用 Spring 的 IoC 容器(例如,在客户机桌面环境中),请使用 JVM 注册 shutdown hook。这样做可确保正常关闭并在单例 bean 上调用相关的 destroy 方法,以便释放所有资源。我们依然必须正确配置和实现这些 destroy 回调。
要注册 shutdown hook,需要调用接口 registerShutdownHook()
上声明的 ConfigurableApplicationContext 方法:
1 | import org.springframework.context.ConfigurableApplicationContext; |
当 ApplicationContext 创建实现 org.springframework.context.ApplicationContextAware 接口的对象实例时,将为该实例提供对该 ApplicationContext 的引用。 ApplicationContextAware 接口的定义如下:
1 | public interface ApplicationContextAware { |
因此,bean 可以通过 ApplicationContext 接口以编程方式操作创建它们的 ApplicationContext,或者通过将引用转换为此接口的已知子类(例如 ConfigurableApplicationContext,包括其他功能)。一种作用是可以操作其他 bean。有时这种能力很有用。但是通常我们应该避免使用它,因为它会将代码耦合到 Spring 且不遵循 IoC 规范,其中协作者作为属性提供给 bean。 ApplicationContext 还提供对文件资源的访问,发布应用程序事件和对 MessageSource 的访问,详见下文。
从 Spring 2.5 开始,自动装配是另一种获取 ApplicationContext 引用的方法。“传统”构造函数和 byType 自动装配模式(见上文的自动装配协作者)可以分别为构造函数参数或 setter 方法参数提供 ApplicationContext 类型的依赖关系。 为了获得更大的灵活性,包括自动装配字段和多参数方法的能力,我们可以使用基于注释的新自动装配功能。如果带有 @Autowired
注解的字段,构造函数或方法需要 ApplicationContext 类型的参数,那么 ApplicationContext 将会被注入。详见下文的 @Autowired
注解。
BeanNameAware 可以获取到该类实现的 bean 的 name,接口定义如下:
1 | public interface BeanNameAware { |
会在全体普通 bean 属性定义之后但在初始化回调之前(例如 InitializingBean,afterPropertiesSet 或自定义 init 方法)之前调用回调。
名称 | 注入的依赖 | 解释 |
---|---|---|
ApplicationContextAware | ApplicationContext | |
ApplicationEventPublisherAware | 封闭 ApplicationContext 的事件发布者 | |
BeanClassLoaderAware | 用于加载 bean 类的类加载器 | |
BeanFactoryAware | BeanFactory | |
BeanNameAware | 声明的 bean 的名称 | |
BootstrapContextAware | 运行容器的资源适配器 BootstrapContext,仅在 JCA-aware ApplicationContext 的实例中可用 | |
LoadTimeWeaverAware | 用于在加载时处理类定义的 weaver | |
MessageSourceAware | 用于解析消息的策略(支持参数化和国际化) | |
NotificationPublisherAware | Spring JMX 通知发布者 | |
ResourceLoaderAware | 用于对资源进行低级访问的加载器 | |
ServletConfigAware | 运行容器的 ServletConfig,仅在 web-aware Spring ApplicationContext 中可用 | |
ServletContextAware | 运行容器的 ServletContext,仅在 web-aware Spring ApplicationContext 中可用 |
再次提醒,使用这些接口会将代码耦合到 Spring API 且不遵循 IoC 规范。除非是需要以代码方式访问容器的基础架构 bean,否则不建议使用这些方式。
bean 定义可以包含许多配置信息,包括构造函数参数,属性值和特定于容器的信息,例如初始化方法,静态工厂方法名称等。子 bean 定义从父定义继承配置数据。子定义可以覆盖某些值或根据需要添加其他值。使用继承 bean 定义可以节省大量的输入。这是一种模板模型。
在 ApplicationContext 级别上,子 bean 定义使用 ChildBeanDefinition 类表示,大多数时候我们并不用在这个级别上去做定义。以下是一个简单定义:
1 | <bean id="inheritedTestBean" abstract="true" |
子 bean 定义从父级继承 scope,构造函数参数值,属性值和方法覆盖并带有添加新值的选项。指定的 scope,初始化方法,销毁方法或静态工厂方法设置都会覆盖相应的父设置。
也有些不会被继承:依赖、自动装配模式、依赖检查、单例、懒加载。
前面的示例通过使用 abstract 属性将父 bean 定义显式标记为 abstract。 如果父定义未指定类,则需要将父 bean 定义显式标记为 abstract,如以下示例所示:
1 | <bean id="inheritedTestBeanWithoutClass" abstract="true"> |
这个父 bean 不能单独实例化。标记为 abstract 的 bean 只做模板 bean 定义。如果尝试强行 getBean 则会报错。容器的 preInstantiateSingletons()
方法会忽略 abstract 的 bean 定义。
注:ApplicationContext 默认情况下预先实例化所有单例。因此,重要的是(至少对于单例 bean),如果你有一个(父)bean 定义,你只打算用作模板,并且这个定义指定了一个类,你必须确保将 abstract 属性设置为 true 否则应用程序上下文将实际(尝试)预先实例化 abstract bean。
通常,应用程序开发人员不需要继承 ApplicationContext 实现类。 相反,可以通过插入特殊集成接口的实现来扩展 Spring IoC 容器。 接下来的几节将介绍这些集成接口。
BeanPostProcessor 接口实现举例:
1 | package scripting; |
BeanPostProcessor 接口允许用户实现自己的(或覆盖容器的默认)实例化逻辑、依赖关系解析逻辑等。如果要在 Spring 容器完成实例化,配置和初始化 bean 之后实现某些自定义逻辑,则可以插入一个或多个自定义 BeanPostProcessor 实现。多个 BeanPostProcessor 实例可以通过实现 Ordered 接口并设置 order 属性来控制执行顺序。
BeanPostProcessor 实例的范围是每个容器的范围。 仅当我们使用容器层次结构时,这才是相关的。 如果在一个容器中定义 BeanPostProcessor,它只对该容器中的 bean 进行后处理。 换句话说,在一个容器中定义的 bean 不会被另一个容器中定义的 BeanPostProcessor 进行后处理,即使两个容器都是同一层次结构的一部分。
如果要更改的是 bean 定义,我们可以使用 BeanFactoryPostProcessor,详见下文。
org.springframework.beans.factory.config.BeanPostProcessor 接口由两个回调方法组成。当这样的类被注册为容器的后处理器(post-processor)时,对于容器创建的每个 bean 实例,后处理器在容器初始化方法之前从容器中获取回调(比如 InitializingBean.afterPropertiesSet()
或 bean 声明的 init 方法)。后处理器可以对 bean 实例执行任何操作,甚至可以让它忽略回调。bean 后处理器通常用于检查回调接口,或者用来将 bean 包装成代理。一些 Spring AOP 的基础设施类就是用 bean 后处理器实现的,以便提供代理包装逻辑。
ApplicationContext 会自动检测所有实现 BeanPostProcessor 接口的 bean,并将他们注册为后处理器,以便稍后在创建 bean 时调用。后处理器可以用任何 bean 注册方式部署在容器中。
注意,在配置类上使用 @Bean
工厂方法声明 BeanPostProcessor 时,工厂方法的返回类型应该是实现类本身,或者至少是 org.springframework.beans.factory.config.BeanPostProcessor 接口,指示该 bean 的后处理器性质。否则,ApplicationContext 无法在完全创建之前按类型自动检测到它。由于 BeanPostProcessor 需要尽早实例化以便应用于上下文中其他 bean 的初始化,因此这种早期类型检测至关重要。
虽然 BeanPostProcessor 注册的推荐方法是通过 ApplicationContext 自动检测(如前所述),但也可以使用 addBeanPostProcessor 方法以编程方式对 ConfigurableBeanFactory 注册它们。当您需要在注册前评估条件逻辑或甚至跨层次结构中的上下文复制 Bean 后处理器时,这非常有用。 但请注意,以编程方式添加的 BeanPostProcessor 实例不遵循 Ordered 接口。这里,注册的顺序决定了执行的顺序。另请注意,以编程方式注册的 BeanPostProcessor 实例始终在通过自动检测注册的实例之前处理,而不管任何显式排序。
BeanPostProcessor 是不能使用 AOP 的。实现 BeanPostProcessor 接口的类是特殊的,容器会对它们进行不同的处理。作为 ApplicationContext 的特殊启动阶段的一部分,它们直接引用的所有 BeanPostProcessor 实例和 bean 都在启动时实例化。接下来,所有 BeanPostProcessor 实例都以排序方式注册,并应用于容器中的所有其他 bean。因为 AOP 自动代理是作为 BeanPostProcessor 本身实现的,所以 BeanPostProcessor 实例和它们直接引用的 bean 都不符合自动代理的条件,因此没有编入方法。
举个例子:
1 | package scripting; |
1 |
|
1 | import org.springframework.context.ApplicationContext; |
输出:
1 | Bean'sensenger'创建:org.springframework.scripting.groovy.GroovyMessenger@272961 |
将回调接口或注释与自定义 BeanPostProcessor 实现结合使用是扩展 Spring IoC 容器的常用方法。一个例子是 Spring 的 RequiredAnnotationBeanPostProcessor —— 一个 Spring 提供的 BeanPostProcessor 实现,它保证用 @Required
(或者其他自定义的)注释标记的 bean 上的属性一定有值注入。
我们看到的下一个扩展点是 org.springframework.beans.factory.config.BeanFactoryPostProcessor。此接口的语义类似于 BeanPostProcessor 的语义,但有一个主要区别:BeanFactoryPostProcessor 对 bean 配置元数据进行操作。也就是说,Spring IoC 容器允许 BeanFactoryPostProcessor 读取配置元数据,并可能在容器实例化除 BeanFactoryPostProcessor 实例之外的任何 bean 之前更改它。
您可以配置多个 BeanFactoryPostProcessor 实例,并且可以通过设置 order 属性来控制这些 BeanFactoryPostProcessor 实例的运行顺序。但是,如果 BeanFactoryPostProcessor 实现 Ordered 接口,则只能设置此属性。如果编写自己的 BeanFactoryPostProcessor,则应考虑实现 Ordered 接口。有关更多详细信息,请参阅 BeanFactoryPostProcessor 和 Ordered 接口的 javadoc。
如果要更改实际的 bean 实例(即,从配置项元数据创建的对象),则需要使用 BeanPostProcessor(前面在使用 BeanPostProcessor 定制 Bean 中进行了描述)。虽然技术上可以在 BeanFactoryPostProcessor 中使用 bean 实例(例如,通过使用 BeanFactory.getBean()
),但这样做会导致过早的 bean 实例化,从而违反标准的容器生命周期。这可能会导致负面影响,例如绕过 bean 后期处理。
此外,BeanFactoryPostProcessor 实例的范围是每个容器的范围。仅当您使用容器层次结构时,这才有意义。如果在一个容器中定义 BeanFactoryPostProcessor,则它仅应用于该容器中的 bean 定义。一个容器中的 Bean 定义不会被另一个容器中的 BeanFactoryPostProcessor 实例后处理,即使两个容器都是同一层次结构的一部分。
BeanFactoryPostProcessor 在 ApplicationContext 中声明时自动执行,以便将更改应用于定义容器的配置元数据。Spring 包含许多预定义的 bean 工厂后处理器,例如 PropertyOverrideConfigurer 和 PropertyPlaceholderConfigurer。您还可以使用自定义 BeanFactoryPostProcessor —— 例如,注册自定义属性编辑器。
ApplicationContext 自动检测部署到其中的任何实现 BeanFactoryPostProcessor 接口的 bean。它在适当的时候使用这些 bean 作为 bean factory post-processor。您可以像配置其他 bean 一样配置这些 post-processor bean。
您可以使用 PropertyPlaceholderConfigurer 将 bean 定义中的属性值分离到外部 Java Properties 文件中。这样做可以使部署应用程序的人员自定义不同环境的属性,例如数据库 URL 和密码,而不会出现修改主 XML 定义文件或容器文件的复杂性或风险。
下面的基于 XML 的配置元数据片段,其中 DataSource 定义了占位符值:
1 | <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> |
该示例显示了从外部属性文件配置的属性。在运行时,PropertyPlaceholderConfigurer 应用于替换 DataSource 的某些属性的元数据。要替换的值被指定为 ${property-name}
形式的占位符,它遵循 Ant 和 log4j 以及 JSP EL 样式。
实际值来自标准 Java Properties 格式的另一个文件:
1 | jdbc.driverClassName=org.hsqldb.jdbcDriver |
然后 ${jdbc.username}
字符串在运行时将替换为值’sa’,其他占位符值也是类似。PropertyPlaceholderConfigurer 检查 bean 定义的大多数属性和属性中的占位符。此外,您可以自定义占位符前缀和后缀。
使用 Spring 2.5 中引入的 context 命名空间,您可以使用专用配置元素配置属性占位符。 您可以在 location 属性中以逗号分隔列表的形式提供一个或多个位置,如以下示例所示:
1 | <context:property-placeholder location="classpath:com/something/jdbc.properties"/> |
PropertyPlaceholderConfigurer 不仅在您指定的属性文件中查找属性。 默认情况下,如果它在指定的属性文件中找不到属性,它还会检查 Java System 属性。 您可以通过使用以下三个受支持的整数值之一设置 configurer 的 systemPropertiesMode 属性来自定义此行为:
有关 PropertyPlaceholderConfigurer 更多信息,请参阅 javadoc。
另,你甚至可以使用 PropertyPlaceholderConfigurer 替换类名称,这在您必须在运行时选择特定实现类时有时很有用。以下示例显示了如何执行此操作:
1 | <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> |
如果在运行时无法将类解析为有效类, 在即将创建 bean 时,bean 的解析将失败。对于非懒加载的 bean,这会发生在 ApplicationContext 的 preInstantiateSingletons()
阶段期间。
PropertyOverrideConfigurer 是另一个 bean 工厂后处理器,类似于 PropertyPlaceholderConfigurer,但与后者不同,原始定义可以具有默认值,或者根本不具有 bean 属性的值。如果重写的 Properties 文件没有某个 bean 属性的条目,则使用默认的上下文定义。
需要注意的是,bean 定义不会感知到被覆盖,因此无法从 XML 定义文件中立即看出正在使用覆盖配置器。 如果多个 PropertyOverrideConfigurer 实例为同一个 bean 属性定义了不同的值,那么根据覆盖机制,最后一个实例将生效。
属性文件配置行采用以下格式:
1 | beanName.property=value |
例如:
1 | dataSource.driverClassName=com.mysql.jdbc.Driver |
复合属性名称也是支持的,只要路径的每个组件(重写的最终属性除外)都已经非空(可能由构造函数初始化)。在下面的示例中,tom bean 的 fred 属性的 bob 属性的 sammy 属性设置为 123:
1 | tom.fred.bob.sammy = 123 |
使用 Spring 2.5 中引入的 context 命名空间,可以使用专用配置元素配置属性覆盖,如以下示例所示:
1 | <context:property-override location="classpath:override.properties"/> |
你可以为本身为工厂的对象实现 org.springframework.beans.factory.FactoryBean 接口。
FactoryBean 接口在 Spring IoC 容器实例化逻辑中是可拔插的。如果 bean 的初始化代码相对复杂,更适合以 Java 代码表达,而不是(可能)冗长的 XML,那么我们可以创建自己的 FactoryBean,在该类中编写复杂的初始化,然后将自定义 FactoryBean 插入容器中。
FactoryBean 接口有三个方法:
Object getObject()
:返回此工厂创建的对象的实例。可以共享实例,具体取决于此工厂是返回单例还是原型。
boolean isSingleton()
:如果此 FactoryBean 返回单例,则返回 true,否则返回 false。
Class getObjectType()
:返回 getObject()
方法返回的对象类型,如果事先不知道类型,则返回 null。
FactoryBean 概念和接口被用在 Spring Framework 中的许多位置。Spring 提供了 50 多个 FactoryBean 接口的实现。
如果需要向容器询问实际的 FactoryBean 实例本身,那么在调用 ApplicationContext 的 getBean()
方法时需要加上&符号作为 bean 的 id 前缀。比如,对于 id 为 myBean 的 FactoryBean,在容器上调用 getBean("myBean")
将返回 FactoryBean 的产品,调用 getBean(“&myBean”)将返回 FactoryBean 实例本身。
这里有一个讨论,配置 Spring 的注解是否比 XML 更好?
注解在其声明中提供了大量上下文,从而可以使配置更短更简洁。而 XML 可以在不触及源代码或重新编译它们的情况下连接组件。
有些人更喜欢将注入靠近源,也有人认为有注解的类不再是 POJO,而且配置变得分散且难以控制。
无论选择如何,Spring 都可以兼顾两种风格,甚至可以将它们混合在一起。值得指出的是,通过其 JavaConfig 选项,Spring 允许以非侵入方式使用注释,而无需触及目标组件源代码,并且在工具方面,Spring Tool Suite 支持所有配置样式。
基于注释的配置提供了 XML 设置的替代方案,该配置依赖于字节码元数据来连接组件而不是角括号声明。开发人员不是使用 XML 来描述 bean 连接,而是通过在相关的类,方法或字段声明上使用注释将配置移动到组件类本身。如示例中所述:RequiredAnnotationBeanPostProcessor,将 BeanPostProcessor 与注释结合使用是扩展 Spring IoC 容器的常用方法。例如,Spring 2.0 引入了使用 @Required
注释强制执行所需属性的可能性。 Spring 2.5 使得有可能采用相同的通用方法来驱动 Spring 的依赖注入。从本质上讲,@Autowired
注释提供的功能与自动装配协作者中描述的相同,但具有更细粒度的控制和更广泛的适用性。Spring 2.5 还增加了对 JSR-250 注释的支持,例如 @PostConstruct
和 @PreDestroy
。Spring 3.0 增加了对 javax.inject 包中包含的 JSR-330(Java 的依赖注入)注释的支持,例如 @Inject
和 @Named
。有关这些注释,详见下文。
注:注解注入在 XML 注入之前执行。因此,对于同事通过这两种方法注入的属性,XML 配置会覆盖注解的配置。
和之前一样,可以将它们注册为单独的 bean 定义,但也可以通过在基于 XML 的 Spring 配置中包含以下标记来隐式注册它们(请注意包含 context 命名空间)
1 | <beans xmlns="http://www.springframework.org/schema/beans" |
(隐式注册的 post-processors 包括 AutowiredAnnotationBeanPostProcessor, CommonAnnotationBeanPostProcessor, PersistenceAnnotationBeanPostProcessor,和前面提到的 RequiredAnnotationBeanPostProcessor。)
<context:annotation-config/>
仅在定义它的同一应用程序上下文中查找 bean 上的注释。这意味着,如果将 <context:annotation-config/>
放在 DispatcherServlet 的 WebApplicationContext 中,它只检查控制器中有 @Autowired
注解的 bean,而不检查您的服务。有关更多信息,请参阅 DispatcherServlet。
@Required 注释可以用于 bean 属性 setter 方法,如下面的例子:
1 | public class SimpleMovieLister { |
这个注解表示被修饰的 bean 属性必须在配置时通过 bean 定义中的显式属性值,或通过 autowiring 装配。 如果被修饰的 bean 属性没有被填充,容器就会抛出异常,以后避免以后抛出 NullPointerException 等。不过最好还是将断言放入 bean 类本身(比如 init 方法内),这样如果在容器外部使用类,这样做也会强制执行那些必需的引用和值。
注:@Required
注释从 Spring Framework 5.1 开始正式弃用,最好使用构造函数注入所需的设置(或者自定义实现 InitializingBean.afterPropertiesSet()
以及 bean 属性的 setter 方法)。
在本节所包含的示例中,可以使用 JSR 330 的@Inject 注释代替 Spring 的@Autowired 注释。
@Autowired 注解可以用于构造函数,如下所示:
1 | public class MovieRecommender { |
从 Spring Framework 4.3 开始,如果目标 bean 只定义了一个构造函数入口,则不再需要在这样的构造函数上使用@Autowired 注释。但是,如果有多个构造器可用,则必须注释至少一个构造器以告诉容器使用哪一个。
@Autowired 注解也可用于“传统” setter 方法:
1 | public class SimpleMovieLister { |
甚至是具有任意名称和多个参数的方法:
1 | public class MovieRecommender { |
也可以应用于字段,甚至可以将它与构造函数混合使用:
1 | public class MovieRecommender { |
注:确保目标组件(例如,MovieCatalog 或 CustomerPreferenceDao)始终按照用于@Autowired 注释注入点的类型声明。否则,由于在运行时未找到类型匹配,注入可能会失败。
对于通过类路径扫描找到的 XML 定义的 bean 或组件类,容器通常预先知道具体类型。但是,对于 @Bean
工厂方法,您需要确保声明的返回类型足够准确。对于实现多个接口的组件或可能由其实现类型引用的组件,请考虑在工厂方法上声明最具体的返回类型(至少与引用 bean 的注入点所需的特定类型一致)。
甚至可以用于数组,ApplicationContext 会提供符合类型的所有 bean:
1 | public class MovieRecommender { |
乃至 Collection 类型:
1 | public class MovieRecommender { |
如果希望数组或列表中的项按特定顺序排序,可以实现
org.springframework.core.Ordered
接口或使用@Order
或标准@Priority
注解。否则,它们的顺序遵循容器中相应目标 bean 定义的注册顺序。您可以在目标类级别和
@Bean
方法上声明@Order
注解,可能是通过单个 bean 定义(在多个定义使用相同 bean 类的情况下)。@Order
值可能影响注入时的优先级,但要注意这并不会影响启动的顺序,这是由依赖关系和@DependsOn
声明正交决定的。请注意,标准的
javax.annotation.Priority
注释在@Bean
级别不可用,因为它无法在方法上声明。它的语义可以通过@Order
值或是指定@Primary
在同类型的某一个 bean 上实现。
即使是 Map,只要 key 类型是 String,也可以被自动装配。Map 值包含所有期望类型的 bean,并且键包含相应的 bean 名称,如以下示例所示:
1 | public class MovieRecommender { |
默认情况下,当给定注入点没有匹配的候选 bean 时,自动装配会失败。对于声明的数组,集合或映射,至少需要一个匹配元素。
默认情况下 @Autowired
注解的方法和字段是必须的依赖项。我们也可以通过修改 required 参数来改为非必要,如以下示例所示:
1 | public class SimpleMovieLister { |
并且,如果依赖项(可能是多个参数依赖项中的某一个)无法注入,那么这个被注解的方法都完全不会被调用(比如上文的 setMovieFinder)。
注入构造函数和工厂方法参数是一种特殊情况,因为 @Autowired
上的 required
标志具有一些不同的含义,因为 Spring 的构造函数解析算法可能要处理多个构造函数。默认情况下构造函数和工厂方法参数需要保证有效,但在单构造函数场景中有一些特殊规则,例如,如果多元素注入点(数组,集合,Map)没有匹配的 bean 可用,则解析为空(empty)的实例。这样所有的依赖关系都可以在同一个多参的构造函数中声明,比如可以声明为一个没有 @Autowired
注解的公共构造函数。
每个类只能标记一个带 required 注解的构造函数,但可以标注多个非 required 注解的构造函数。在这种情况下,每个构造函数都是可选的,Spring 会使用满足依赖性的“最贪心”(具有最多参数的构造函数)的构造函数。构造函数解析算法与具有重载构造函数的非注释类相同,只是将候选者缩小到带注释的构造函数。
建议使用
@Autowired
的required
属性而不是 setter 方法的@Required
注释。required
属性表示该属性不是自动装配所必需的。如果无法自动装配,则会忽略该属性。另一方面,@Required
更强大,因为它要求容器必须通过任意容器支持的方式设置属性。如果未定义任何值,则会引发相应的异常。
另外,Java 8 提供了 java.util.Optional 特性来解决这个问题:
1 | public class SimpleMovieLister { |
从 Spring Framework 5.0 开始,还可以使用 @Nullable
注释(任何包中的任何类型的注释 - 比如 JSR-305 中的javax.annotation.Nullable
):
1 | public class SimpleMovieLister { |
您还可以将 @Autowired
用于众所周知的可解析依赖项的接口:BeanFactory,ApplicationContext,Environment,ResourceLoader,ApplicationEventPublisher 和 MessageSource。 这些接口及其扩展接口(如 ConfigurableApplicationContext 或 ResourcePatternResolver)将自动解析,无需特殊设置。 以下示例自动装配 ApplicationContext 对象:
1 | public class MovieRecommender { |
@Autowired
,@Inject
,@Value
和 @Resource
注解由 BeanPostProcessor 实现处理。 这意味着我们不能在自己的 BeanPostProcessor 或 BeanFactoryPostProcessor 类型中应用这些注释。必须使用 XML 或 Spring @Bean 方法显式地“连接”这些类型。
由于按类型自动装配可能会导致多个候选项,因此通常需要对选择过程进行更多控制。实现这一目标的一种方法是使用 Spring 的 @Primary
注释。@Primary
表示当多个 bean 可以自动装配到单值依赖项时,应该优先选择特定的 bean。如果候选项中只存在一个 primary 的 bean,那么它就是自动装配的值。
1 |
|
1 | public class MovieRecommender { |
MovieRecommender 将会自动装配 firstMovieCatalog
@Primary
可以确定一个主要候选者。当您需要更准确控制选择过程时,可以使用 Spring 的 @Qualifier
注释。您可以将限定符值与特定参数相关联,缩小类型匹配集,以便为每个参数选择特定的 bean。有个简单的例子:
1 | public class MovieRecommender { |
1 | public class MovieRecommender { |
作为降级,bean 名称被视为默认 qualifier 的值。因此,您可以使用 bean 的 id 得到相同的匹配结果。但是,虽然您可以使用此约定来按名称引用特定 bean,限定符值在类型匹配集中只是具有缩小的语义,它们在语义上不表示对唯一 bean id 的引用。良好的限定符值是 main 或 EMEA 或 persistent,类似这些,不同于自动生成的 bean id。
限定符也适用于类型化集合,比如前面的 Set<MovieCatalog>
。在这种情况下,根据声明的限定符,所有匹配的 bean 都作为集合注入。这意味着限定符不必是唯一的,它们只是一个过滤条件。例如,您可以使用相同的限定符值 action
定义多个 MovieCatalog bean,所有这些 bean 都注入到使用 @Qualifier("action")
注解的 Set<MovieCatalog>
中。
在类型匹配候选项中,根据目标 bean 名称选择限定符值,在注入点不需要
@Qualifier
注释。如果没有其他解析指示符(例如限定符或主要标记),则对于非唯一依赖性情况,Spring 会将注入点名称(即字段名称或参数名称)与目标 bean 名称进行匹配,然后选择同名的候选人,如果有的话。也就是说,如果您打算按名称表达注释驱动的注入,请不要主要使用
@Autowired
,即使它能够在类型匹配候选项中通过 bean 名称进行选择。相反,使用 JSR-250@Resource
注释,该注释在语义上定义为通过其唯一名称标识特定目标组件,声明的类型与匹配过程无关。@Autowired
具有相当不同的语义:在按类型选择候选 bean 之后,仅在那些类型选择的候选项中考虑指定的字符串限定符值(例如,将account
限定符与标记有相同限定符标签的 bean 匹配)。对于自身定义为集合,Map 或数组类型的 bean,
@Resource
是一个很好的解决方案,通过唯一名称引用特定的集合或数组 bean。也就是说,从 4.3 版本开始,只要元素类型信息保存在@Bean
返回类型签名或集合继承层次结构中,您就可以通过 Spring 的@Autowired
类型匹配算法匹配 Map 和数组类型。在这种情况下,您可以使用限定符值在相同类型的集合中进行选择,如上一段所述。从 4.3 开始,
@Autowired
还会考虑自引用注入(即,引用回到当前注入的 bean)。请注意,自我注入是一种后备。对其他组件的常规依赖性始终具有优先权。从这个意义上说,自我引用并不参与常规的候选人选择,因此从不是主要的。相反,它们总是最低优先级。在实践中,您应该仅使用自引用作为最后的手段(例如,通过 bean 的事务代理调用同一实例上的其他方法)。考虑在这种情况下将受影响的方法分解为单独的委托 bean。或者,您可以使用@Resource
,它可以通过其唯一名称获取代理回到当前 bean。
@Autowired
适用于字段,构造函数和多参数方法,允许在参数级别缩小限定符注释。相比之下,@Resource
仅支持字段和具有单个参数的 bean 属性 setter 方法。因此,如果注射目标是构造函数或多参数方法,则应该使用限定符。
我们可以创建自己的自定义限定符注释,需要定义注释并在定义中提供 @Qualifier
注释,如以下示例所示:
1 | ({ElementType.FIELD, ElementType.PARAMETER}) |
1 | public class MovieRecommender { |
接下来,您可以提供候选 bean 定义的信息。 您可以将 <qualifier/>
标记添加为 <bean/>
标记的子元素,然后指定与自定义限定符注释匹配的类型和值。 类型与注释的完全限定类名匹配。或者,为方便起见,如果不存在冲突名称的风险,您可以使用短类名称。以下示例演示了这两种方法:
1 |
|
也可以使用注解:
1 |
|
这个后文会细讲。
在某些情况下,使用没有值的注释可能就足够了。当注释用于更通用的目的并且可以应用于多种不同类型的依赖项时,这可能很有用。例如,您可以提供可在没有 Internet 连接时搜索的脱机目录:
1 | ({ElementType.FIELD, ElementType.PARAMETER}) |
1 | public class MovieRecommender { |
您还可以定义除简单值属性之外或代替简单值属性接受命名属性的自定义限定符注释。如果随后在要自动装配的字段或参数上指定了多个属性值,则 bean 定义必须匹配所有此类属性值才能被视为自动装配候选:
1 | ({ElementType.FIELD, ElementType.PARAMETER}) |
1 | public enum Format { |
1 | public class MovieRecommender { |
除了@Qualifier
注释之外,您还可以使用 Java 泛型类型作为隐式的限定形式。例如,假设您具有以下配置:
1 |
|
假设前面的 bean 实现了一个通用接口(即 Store<String>
和 Store<Integer>
),你可以 @Autowire
Store 接口,并将泛型用作限定符,如下例所示:
1 |
|
通用限定符也适用于自动装配列表,Map 实例和数组。以下示例自动装配通用 List:
1 | // Inject all Store beans as long as they have an <Integer> generic |
CustomAutowireConfigurer 是一个 BeanFactoryPostProcessor,它允许注册自己的自定义限定符注释类型,即使它们没有使用 Spring 的 @Qualifier
注释进行注释。 以下示例显示如何使用 CustomAutowireConfigurer:
1 | <bean id="customAutowireConfigurer" |
AutowireCandidateResolver 通过以下方式确定 autowire 候选者:
每个 bean 定义的 autowire-candidate 值
<beans/>
元素上可用的任何 default-autowire 候选模式
存在 @Qualifier
注释以及使用 CustomAutowireConfigurer 注册的任何自定义注释
当多个 bean 有资格作为 autowire 候选者时,“primary”的确定如下:当候选者中刚好只有一个 bean 定义的 primary 属性设置为 true,就选择它。
Spring 还通过对字段或 bean 属性 setter 方法使用 JSR-250 @Resource
注释(javax.annotation.Resource)来支持注入。这是 Java EE 中的常见模式:例如,在 JSF 管理的 bean 和 JAX-WS 端点中。Spring 也支持 Spring 管理对象的模式。
@Resource
采用名称属性。默认情况下,Spring 将该值解释为要注入的 bean 名称。换句话说,它遵循按名称语义,如以下示例所示:
1 | public class SimpleMovieLister { |
如果未明确指定名称,则默认名称是从字段名称或 setter 方法派生的。如果是字段,则采用字段名称。在 setter 方法的情况下,它采用 bean 属性名称。下面的例子将把 movieFinder bean 注入其 setter 方法:
1 | public class SimpleMovieLister { |
注释中的 name 由 ApplicationContext 解析为 bean 名称,由 CommonAnnotationBeanPostProcessor 拿到该名称。如果显式配置 Spring 的 SimpleJndiBeanFactory,则可以通过 JNDI 解析名称。但是,我们建议您依赖于默认行为并使用 Spring 的 JNDI 查找功能来保证间接级别。
在某些特殊情况下 @Resource
不指定明确的名称,与 @Autowired
类似,@Resource
会不看名称,使用主要类型匹配,这些情况包括以下类:BeanFactory, ApplicationContext,ResourceLoader,ApplicationEventPublisher 和 MessageSource 接口。
1 | public class MovieRecommender { |
CommonAnnotationBeanPostProcessor 不仅识别 @Resource
注释,还识别 JSR-250 生命周期注释:javax.annotation.PostConstruct
和javax.annotation.PreDestroy
。在 Spring 2.5 中引入,对这些注释的支持提供了初始化回调和销毁回调中描述的生命周期回调机制的替代方法。假如 CommonAnnotationBeanPostProcessor 在 ApplicationContext 中注册,那么带有这些注释的方法会在相应的 Spring 生命周期接口方法或显式声明的回调方法的同时被调用。在以下示例中,缓存在初始化时预填充并在销毁时清除:
1 | public class CachingMovieLister { |
有关组合各种生命周期机制的效果的详细信息,参见上文组合生命周期机制。
与
@Resource
一样,@PostConstruct
和@PreDestroy
注释类型是 JDK 6 到 8 的标准 Java 库的一部分。但是,整个 javax.annotation 包与 JDK 9 中的核心 Java 模块分离,最终在 JDK 11 中删除。如果需要,现在需要通过 Maven Central 获取 javax.annotation-api 组件,并添加到应用程序的类路径中。
本章中的大多数示例都使用 XML 来指定在 Spring 容器中生成每个 BeanDefinition 的配置。 上一节(基于注释的容器配置)演示了如何通过源级注释提供大量配置元数据。但是,即使在这些示例中,基本 bean 定义也在 XML 文件中显式定义,而注释仅驱动依赖项注入。本节介绍通过扫描类路径隐式检测候选组件的选项。候选组件是与筛选条件匹配的类,并且具有向容器注册的相应 bean 定义。这消除了使用 XML 执行 bean 注册的需要。相反,您可以使用注释(比如@Component
),AspectJ 类型表达式或您自己的自定义筛选条件来选择哪些类具有向容器注册的 bean 定义。
从 Spring 3.0 开始,Spring JavaConfig 项目提供的许多功能是核心 Spring Framework 的一部分。这使您可以使用 Java 而不是使用传统的 XML 文件来定义 bean。看看的@Configuration,@Bean, @Import,和@DependsOn 注释有关如何使用这些新功能的例子。
@Repository
注解是实现存储库的角色或构造型(也称为数据访问对象或 DAO)的任何类的标记。 该标记的用途包括自动翻译异常,详见“异常翻译”。
Spring 提供了进一步的构造型注释:@Component
,@Service
和 @Controller
。@Component
是任何 Spring 托管组件的通用构造型。 @Repository
,@Service
和@Controller
是 @Component
的特化,用于更具体的用例(分别在持久层,服务层和表示层中)。因此,您可以使用 @Component
来注释组件类,但是通过使用 @Repository
,@Service
或 @Controller
来注释组件类,您的类更适合于通过工具进行处理或与方面相关联。例如,这些构造型注释成为切入点的理想目标。 @Repository
,@Service
和 @Controller
在 Spring 框架的将来版本中也可以带有其他语义。因此,如果在服务层使用 @Component
或 @Service
之间进行选择,则 @Service
显然是更好的选择。同样,如前所述,@Repository
已被支持作为持久层中自动异常转换的标记。
Spring 提供的许多注释都可以在您自己的代码中用作元注释。元注释是可以应用于另一个注释的注释。例如,前面提到的 @Service
注释使用 @Component
进行元注释,如以下示例所示:
1 | (ElementType.TYPE) |
@Service
的处理方式与 @Component
相同。
您还可以组合元注释来创建“组合注释”。 例如,Spring MVC 中的 @RestController
批注由 @Controller
和 @ResponseBody
组成。
此外,组合注释可以选择从元注释中重新声明属性,以允许自定义。当您只希望公开元注释属性的子集时,此功能特别有用。例如,Spring 的 @SessionScope
注释将作用域名称硬编码为会话,但仍允许自定义 proxyMode。以下清单显示了 SessionScope
批注的定义:
1 | ({ElementType.TYPE, ElementType.METHOD}) |
使用时:
1 |
|
您还可以覆盖 proxyMode 的值,如以下示例所示:
1 |
|
Spring 可以自动检测构造型类,并向 ApplicationContext 注册相应的 BeanDefinition 实例。 例如,以下两个类别有资格进行这种自动检测:
1 |
|
1 |
|
要自动检测这些类并注册相应的 bean,您需要添加 @ComponentScan 到@Configuration 类中,其中 basePackages 属性是两个类的公共父包。(或者,您可以指定一个逗号分隔,分号分隔或空格分隔的列表,其中包括每个类的父包。)
1 |
|
为简便起见,前面的示例可能使用了 注释的 value 属性(即
@ComponentScan("org.example")
)。
扫描类路径包需要在类路径中存在相应的目录条目。使用 Ant 构建 JAR 时,请确保未激活 JAR 任务的仅文件开关。此外,在某些环境中,可能不会基于安全策略公开类路径目录,例如,在 JDK 1.7.0_45 及更高版本上的独立应用程序(这需要在清单中设置“受信任的库”-请参阅 https://stackoverflow.com/Questions/19394570/java-jre-7u45-breaks-classloader-getresources)。
在 JDK 9 的模块路径(Jigsaw)上,Spring 的类路径扫描通常可以按预期进行。但是,请确保将组件类导出到 module-info 描述符中。如果您期望 Spring 调用您的类的非公共成员,请确保它们是“打开的”(也就是说,它们使用 opens 声明而不是描述符中的 exports 声明 module-info)。
此外,当您使用 component-scan 元素时,AutowiredAnnotationBeanPostProcessor 和 CommonAnnotationBeanPostProcessor 都隐式包括在内。这意味着将自动检测这两个组件并将它们连接在一起,而这一切都不需要 XML 中提供的任何 bean 配置元数据。
您可以通过将注释配置属性包括为 false 来禁用 AutowiredAnnotationBeanPostProcessor 和 CommonAnnotationBeanPostProcessor 的注册。
默认情况下,只有使用 @Component
,@Repository
,@Service
,@Controller
进行注释的类或使用 @Component
进行注释的自定义注释是可以被检测到的候选组件。但是,您可以通过应用自定义过滤器来修改和扩展此行为。将它们添加为 @ComponentScan
批注的 includeFilters 或 excludeFilters 参数(或作为 component-scan 元素的 include-filter 或 exclude-filter 子元素)。每个过滤器元素都需要 type 和 expression 属性。 下表描述了过滤选项:
过滤器类型 | 范例表达 | 描述 |
---|---|---|
注释(默认) | org.example.SomeAnnotation | 在目标组件的类型级别上存在的注释。 |
分配 | org.example.SomeClass | 目标组件可分配给(扩展或实现)的类(或接口)。 |
AspectJ | org.example..*Service+ | 目标组件要匹配的 AspectJ 类型表达式。 |
正则表达式 | org.example.Default.* | 要与目标组件类名称匹配的正则表达式。 |
自定义 | org.example.MyTypeFilter | org.springframework.core.type.TypeFilter 接口的自定义实现。 |
以下示例显示了忽略所有 @Repository
注释并改为使用“存根”存储库的配置:
1 |
|
您还可以通过在注释上设置 useDefaultFilters = false
或通过将 use-default-filters=“false”
作为 <component-scan/>
元素的属性来禁用默认过滤器。这将禁用对@Component
,@Repository
,@Service
,@Controller
或 @Configuration
注释的类的自动检测。
Spring 组件还可以将 bean 定义元数据贡献给容器。您可以 @Bean
使用与在带 @Configuration
注释的类中定义 Bean 元数据相同的注释来执行此操作。以下示例显示了如何执行此操作:
1 |
|
上一类是 Spring 组件,在其 doWork()
方法中具有特定于应用程序的代码。 但是,它也提供了一个具有工厂方法的 bean 定义,该工厂方法引用了方法 publicInstance()
。@Bean
批注标识工厂方法和其他 bean 定义属性,例如通过 @Qualifier
批注的限定符。可以指定其他的方法级别注释比如 @Scope
,@Lazy
和自定义限定符注释。
除了用于组件初始化的角色外,您还可以将
@Lazy
批注放置在标有@Autowired
或@Inject
的注入点上。在这种情况下,它导致了惰性解析代理的注入。
如前所述,除了支持自动装配的字段和方法,还支持自动装配@Bean 方法。 以下示例显示了如何执行此操作:
1 |
|
该示例将 String 方法参数国家/地区自动连线到另一个名为 privateInstance 的 bean 上 age 属性的值。Spring Expression Language 元素通过符号 #{<expression>}
定义属性的值。对于 @Value
批注,表达式解析程序已预先配置为在解析表达式文本时查找 bean 名称。
从 Spring Framework 4.3 开始,您还可以声明类型为 InjectionPoint 的工厂方法参数(或更具体的子类:DependencyDescriptor),以访问触发当前 bean 创建的请求注入点。请注意,这仅适用于实际创建的 Bean 实例,不适用于注入现有实例。因此,此功能对原型范围的 bean 最有意义。对于其他作用域,factory 方法仅在给定作用域中看到触发创建新 bean 实例的注入点(例如,触发创建惰性单例 bean 的依赖项)。 在这种情况下,可以将提供的注入点元数据与语义一起使用。 以下示例显示了如何使用 InjectionPoint:
1 |
|
常规 Spring 组件中的 @Bean
方法的处理方式与 Spring @Configuration
类中的 @Bean
方法不同。区别在于,@Component
类没有使用 CGLIB 来拦截方法和字段的调用。 CGLIB 代理是一种调用 @Configuration
类中 @Bean
方法中的方法或字段的方法,用于创建 Bean 元数据引用以协作对象。此类方法不是使用常规 Java 语义调用的,而是通过容器进行的,以提供通常的生命周期管理和 Spring Bean 的代理,即使通过 @Bean
方法的编程调用引用其他 Bean 时也是如此。相反,在普通 @Component
类内的 @Bean
方法中调用方法或字段具有标准 Java 语义,而无需特殊的 CGLIB 处理或其他约束。
您可以将
@Bean
方法声明为静态方法,从而允许在不将其包含配置类创建为实例的情况下调用它们。在定义后处理器 Bean(例如 BeanFactoryPostProcessor 或 BeanPostProcessor 类型)时,这特别有意义,因为此类 Bean 在容器生命周期的早期进行了初始化,并且应避免在那时触发配置的其他部分。由于技术限制,对静态
@Bean
方法的调用永远不会被容器拦截,即使在@Configuration
类中也是如此(如本节前面所述),由于技术限制:CGLIB 子类只能覆盖非静态方法。结果,直接调用另一个@Bean
方法具有标准的 Java 语义,从而导致直接从工厂方法本身直接返回一个独立的实例。@Bean 方法的 Java 语言可见性不会对 Spring 容器中的最终 bean 定义产生直接影响。您可以在非
@Configuration
类中自由声明自己的工厂方法,也可以在任何地方声明静态方法。但是,@Configuration
类中的常规@Bean
方法必须是可重写的——即,不得将它们声明为 private 或 final。还可以在给定组件或配置类的基类上以及在由组件或配置类实现的接口中声明的 Java 8 默认方法上发现
@Bean
方法。这为组合复杂的配置安排提供了很大的灵活性,从 Spring 4.2 开始,通过 Java 8 默认方法甚至可以进行多重继承。最后,一个类可以为同一个 bean 保留多个
@Bean
方法,这取决于在运行时可用的依赖关系,从而可以使用多个工厂方法。这与在其他配置方案中选择“最贪婪”的构造函数或工厂方法的算法相同:在构造时选择具有最大可满足依赖关系数量的变量,类似于容器在多个@Autowired
构造函数之间进行选择的方式。
在扫描过程中自动检测到组件时,其 bean 名称由该扫描程序已知的 BeanNameGenerator 策略生成。 默认情况下,任何包含名称值的 Spring 构造型注释(@Component
,@Repository
,@Service
和 @Controller
)都会将该名称提供给相应的 bean 定义。
如果这样的注释不包含名称,value 或者不包含任何其他检测到的组件(例如,通过自定义过滤器发现的组件),则缺省 bean 名称生成器将返回未大写的非限定类名称。例如,如果检测到以下组件类,则名称为 myMovieLister 和 movieFinderImpl:
1 | "myMovieLister") ( |
1 |
|
如果您不想依赖默认的 Bean 命名策略,则可以提供自定义 Bean 命名策略。首先,实现 BeanNameGenerator 接口,并确保包括默认的无参构造函数。然后,在配置扫描器时提供完全限定的类名,如以下示例注释和 Bean 定义所示:
1 |
|
1 | <beans> |
作为一般规则,每当其他组件可能对其进行显式引用时,请考虑使用注释指定名称。另一方面,只要容器负责接线,自动生成的名称就足够了。
通常,与 Spring 管理的组件一样,自动检测到的组件的默认且最常见的作用域是单例。 但是,有时您需要使用@Scope 批注指定的其他范围。 您可以在注释中提供范围的名称,如以下示例所示:
1 | "prototype") ( |
@Scope
注释仅在具体的 bean 类(对于带注释的组件)或工厂方法(对于 @Bean
方法)上进行内省。 与 XML bean 定义相反,没有 bean 定义继承的概念,并且在类级别的继承层次结构与元数据目的无关。
有关特定于 Web 的范围的详细信息,例如 Spring 上下文中的“request”或“session”,请参见 Request,Session,Application 和 WebSocket Scope。与这些作用域的预构建批注一样,您也可以使用 Spring 的元注释方法来组成自己的作用域注释:例如,用@Scope(“prototype”),进行元注释的自定义注释,也可能会声明自定义作用域代理模式。
要提供用于范围解析的自定义策略,而不是依赖于基于注释的方法,可以实现该 ScopeMetadataResolver 接口。确保包括默认的无参数构造函数。然后,可以在配置扫描程序时提供完全限定的类名,如以下注释和 Bean 定义示例所示:
1 |
|
1 | <beans> |
使用某些非单作用域时,可能有必要为作用域对象生成代理。在范围 Bean 中将推理描述为依赖项。为此,在 component-scan 元素上可以使用 scoped-proxy 属性。三个可能的值是:no,interfaces,和 targetClass。例如,以下配置生成标准的 JDK 动态代理:
1 |
|
1 | <beans> |
在@Qualifier 注释中讨论与预选赛微调基于注解的自动连接。该部分中的示例演示了如何使用@Qualifier 注释和自定义限定符注释在解析自动装配候选时提供细粒度的控制。由于这些示例基于 XML Bean 定义,因此通过使用 XML 中的元素的 qualifier 或 meta 子元素,在候选 Bean 定义上提供了限定符元数据 bean。当依靠类路径扫描来自动检测组件时,可以在候选类上为限定符元数据提供类型级别的注释。下面的三个示例演示了此技术:
1 |
|
1 |
|
1 |
|
与大多数基于注释的替代方法一样,请记住,注释元数据绑定到类定义本身,而 XML 的使用允许相同类型的多个 bean 提供其限定符元数据的变体,因为该元数据是按-instance 而不是按类。
尽管类路径扫描非常快,但可以通过在编译时创建静态候选列表来提高大型应用程序的启动性能。在这种模式下,作为组件扫描目标的所有模块都必须使用此机制。
您现有的
@ComponentScan
或<context:component-scan>
指令必须保持原样,以请求上下文扫描某些软件包中的候选对象。当 ApplicationContext 检测到这样的索引时,它将自动使用它而不是扫描类路径。
要生成索引,请向每个包含组件的模块添加附加依赖关系,这些组件是组件扫描指令的目标。以下示例显示了如何使用 Maven 进行操作:
1 | <dependencies> |
对于 Gradle 4.5 和更早版本,应在 compileOnly
配置中声明依赖项,如以下示例所示:
1 | dependencies { |
对于 Gradle 4.6 及更高版本,应在 annotationProcessor
配置中声明依赖项,如以下示例所示:
1 | dependencies { |
该过程将生成一个 META-INF/spring.components
包含在 jar 文件中的文件。
在 IDE 中使用此模式时,
spring-context-indexer
必须将其注册为注释处理器,以确保在更新候选组件时索引是最新的。
META-INF/spring.components
在类路径上找到a
时,索引将自动启用。如果某个索引对于某些库(或用例)部分可用,但无法为整个应用程序构建,则可以通过将设置spring.index.ignore
为true
,来回退到常规的类路径安排(好像根本没有索引)属性或spring.properties
类路径根目录下的文件中。
从 Spring 3.0 开始,Spring 提供对 JSR-330 标准注释(依赖注入)的支持。这些注释的扫描方式与 Spring 注释的扫描方式相同。要使用它们,您需要在类路径中有相关的 jar。
如果使用 Maven,
javax.inject
则可以在标准 Maven 存储库(https://repo1.maven.org/maven2/javax/inject/javax.inject/1/)中找到该工件。您可以将以下依赖项添加到文件 pom.xml 中:
1
2
3
4
5
6 > <dependency>
> <groupId>javax.inject</groupId>
> <artifactId>javax.inject</artifactId>
> <version>1</version>
> </dependency>
>
除了 @Autowired
,您可以使用 @javax.inject.Inject
以下方法:
1 | import javax.inject.Inject; |
与一样 @Autowired
,您可以 @Inject
在字段级别,方法级别和构造函数参数级别使用。此外,您可以将注入点声明为 Provider
,以允许按需访问范围更短的 bean,或者通过 Provider.get()
调用延迟访问其他 bean 。以下示例提供了先前示例的变体:
1 | import javax.inject.Inject; |
如果要为应该注入的依赖项使用限定名称,则应使用 @Named
批注,如以下示例所示:
1 | import javax.inject.Inject; |
与一样 @Autowired
,@Inject
也可以与 java.util.Optional
或一起使用@Nullable
。这在这里更为适用,因为 @Inject
它没有 required
属性。以下示例展示了如何使用 @Inject
和 @Nullable
:
1 | public class SimpleMovieLister { |
1 | public class SimpleMovieLister { |
代替@Component,您可以使用@javax.inject.Named 或 javax.annotation.ManagedBean,如以下示例所示:
1 | import javax.inject.Inject; |
在 @Component
不指定组件名称的情况下使用非常常见。@Named
可以类似的方式使用,如以下示例所示:
1 | import javax.inject.Inject; |
当使用 @Named
或 @ManagedBean
时,可以使用与使用 Spring 注释完全相同的方式来使用组件扫描,如以下示例所示:
1 |
|
与相比@Component,JSR-330 @Named 和 JSR-250 ManagedBean 注释是不可组合的。您应该使用 Spring 的构造型模型来构建自定义组件注释。
当使用标准注释时,您应该知道某些重要功能不可用,如下表所示:
表 6. Spring 组件模型元素与 JSR-330 变体
Spring | javax.inject.* | javax.inject 限制/注释 |
---|---|---|
@Autowired | @Inject | @Inject 没有“必填”属性。可以与 Java 8 一起使用 Optional。 |
@Component | @Named / @ManagedBean | JSR-330 不提供可组合的模型,仅提供一种识别命名组件的方法。 |
@Scope(“singleton”) | @Singleton | JSR-330 的默认范围类似于 Spring 的 prototype。但是,为了使其与 Spring 的默认默认值保持一致,默认情况下,在 Spring 容器中声明的 JSR-330 bean 是 a singleton。为了使用之外的范围 singleton,您应该使用 Spring 的@Scope 注释。javax.inject 还提供了 @Scope 批注。不过,此仅用于创建自己的注释。 |
@Qualifier | @Qualifier / @Named | javax.inject.Qualifier 只是用于构建自定义限定符的元注释。具体的 String 限定词(例如@Qualifier 带有值的 Spring 的限定词)可以通过关联 javax.inject.Named。 |
@Value | - | 没有等效 |
@Required | - | 没有等效 |
@Lazy | - | 没有等效 |
ObjectFactory | Provider | javax.inject.Provider 是 Spring 的直接替代方法 ObjectFactory,只是 get()方法名称较短。它也可以与 Spring @Autowired 或非注释构造函数和 setter 方法结合使用。 |
本节介绍如何在 Java 代码中使用注释来配置 Spring 容器。它包括以下主题:
@Bean
和 @Configuration
@Bean
和 @Configuration
Spring 的新 Java 配置支持中的主要构件是 @Configuration
注释的类和 @Bean
注释的方法。
@Bean
批注用于指示方法实例化,配置和初始化要由 Spring IoC 容器管理的新对象。 对于那些熟悉 Spring 的 <beans/>
XML 配置的人来说,@Bean
注释与 <bean/>
元素具有相同的作用。 您可以将 @Bean
批注方法与任何 Spring @Component
一起使用。 但是,它们最常与 @Configuration
bean 一起使用。
用注释类 @Configuration
表示其主要目的是作为 Bean 定义的来源。此外,@Configuration
类允许通过调用 @Bean
同一类中的其他方法来定义 Bean 之间的依赖关系。最简单的 @Configuration
类如下:
1 |
|
上一 AppConfig
类等效于以下 Spring <beans/>
XML:
1 | <beans> |
完整的
@Configuration
与“精简”@Bean
模式?如果在未使用
@Configuration
注释的类中声明@Bean
方法,则将它们称为以“精简”模式进行处理。在@Component
或是甚至在简单的旧类中声明的 Bean 方法被认为是“精简版”,其中包含类的主要目的不同,而@Bean
方法在那里具有某种优势。例如,服务组件可以通过每个适用组件类上的其他@Bean
方法将管理视图公开给容器。在这种情况下,@Bean
方法是一种通用的工厂方法机制。与完整的
@Configuration
不同,精简@Bean
方法无法声明 Bean 之间的依赖关系。取而代之的是,它们在其包含组件的内部状态上进行操作,并且还可以根据其可能声明的参数进行操作。因此,此类@Bean
方法不应调用其他@Bean
方法。实际上,每个此类方法仅是用于特定 bean 引用的工厂方法,而没有任何特殊的运行时语义。这里的积极副作用是,不必在运行时应用 CGLIB 子类,因此在类设计方面没有任何限制(即,包含类可能是 final 修饰之类的)。在常见情况下,
@Bean
方法将在@Configuration
类中声明,以确保始终使用“完全”模式,因此跨方法引用将重定向到容器的生命周期管理。这样可以防止通过常规 Java 调用意外地调用同一@Bean
方法,从而有助于减少在“精简”模式下运行时难以追查的细微错误。
以下各节将详细讨论 @Bean
和 @Configuration
批注。 但是,首先,我们介绍了使用基于 Java 的配置来创建 spring 容器的各种方法。
以下各节介绍了 Spring 3.0 中引入的 Spring 的 AnnotationConfigApplicationContext。这种通用的 ApplicationContext 实现不仅能够接受 @Configuration
类作为输入,而且还可以接受普通的 @Component
类和带有 JSR-330 元数据注释的类。
当提供 @Configuration
类作为输入时,@Configuration
类本身将注册为 Bean 定义,并且该类中所有已声明的 @Bean
方法也将注册为 Bean 定义。
提供 @Component
和 JSR-330 类时,它们将注册为 bean 定义,并且假定在必要时在这些类中使用了诸如 @Autowired
或 @Inject
之类的 DI 元数据。
与实例化 ClassPathXmlApplicationContext 时将 Spring XML 文件用作输入的方式几乎相同,您可以在实例化 AnnotationConfigApplicationContext 时将 @Configuration
类用作输入。如下面的示例所示,这允许完全不使用 XML 来使用 Spring 容器:
1 | public static void main(String[] args) { |
如前所述,AnnotationConfigApplicationContext 不限于仅与 @Configuration
类一起使用。 可以将任何 @Component
或 JSR-330 带注释的类作为输入提供给构造函数,如以下示例所示:
1 | public static void main(String[] args) { |
前面的示例假定 MyServiceImpl,Dependency1 和 Dependency2 使用 Spring 依赖项注入注释,例如 @Autowired
。
您可以使用无参构造函数实例化 AnnotationConfigApplicationContext,然后使用 register() 方法对其进行配置。以编程方式构建 AnnotationConfigApplicationContext 时,此方法特别有用。以下示例显示了如何执行此操作:
1 | public static void main(String[] args) { |
1 |
|
等价于
1 | public static void main(String[] args) { |
值得注意的是,@Configuration
类带有 @Component
元注解,因此是组件扫描的候选对象。在前面的示例中,假定 AppConfig
在 com.acme
包(或下面的任何包)中声明,那么在调用 scan()
时,@Configuration
类被检测到并注册,但不会注册其中的 @Bean
。直到调用 refresh()
方法,其所有 @Bean
方法才都被处理并注册为容器内的 Bean 定义。
AnnotationConfigWebApplicationContext = WebApplicationContext + AnnotationConfigApplicationContext,这个实现可以配置 Spring 的 ContextLoaderListener
servlet 侦听器, Spring MVC 的 DispatcherServlet
等。以下 web.xml 代码片段配置了典型的 Spring MVC Web 应用程序 (注意 contextClass
context-param and init-param 的使用):
1 | <web-app> |
@Bean 是方法级别的注解,类似 XML 中的 <bean/>
元素。同时支持提供 <bean/>
的属性,例如:init-method、destroy-method、autowiring。
你可以使用在有 @Configuration
或 @Component
注解的类内使用 @Bean
注释。
要声明一个 bean,可以对方法进行 @Bean 注解。您可以使用此方法在 ApplicationContext 指定为该方法的返回值的类型内注册 Bean 定义。默认情况下,bean 名称与方法名称相同。以下示例显示了@Bean 方法声明:
1 |
|
前面的配置与下面的 Spring XML 完全等效:
1 | <beans> |
这两个声明都使一个名为 transferService 的 bean 在 ApplicationContext 中可用,并绑定到类型为 TransferServiceImpl 的对象实例,如以下文本镜像所示:
1 | transferService -> com.acme.TransferServiceImpl |
您还可以使用接口(或基类)返回类型声明@Bean 方法,如以下示例所示:
1 |
|
但是,这将高级类型预测的可见性限制为指定的接口类型(TransferService)。然后,使用仅一次使容器知道的完整类型(TransferServiceImpl),就可以实例化受影响的单例 bean。非惰性单例 bean 根据其声明顺序实例化,因此您可能会看到不同的类型匹配结果,具体取决于另一个组件何时尝试按非声明类型进行匹配(例如@Autowired TransferServiceImpl,仅 transferService 在实例化 bean 后才解析)。
如果您通过声明的服务接口一致地引用类型,则@Bean 返回类型可以安全地加入该设计决策。但是,对于实现多个接口的组件或由其实现类型潜在引用的组件,声明可能的最具体的返回类型(至少与引用您的 bean 的注入点所要求的具体类型一样)更为安全。
带@Bean 注释的方法可以具有任意数量的参数,这些参数描述构建该 bean 所需的依赖关系。例如,如果我们 TransferService 需要一个 AccountRepository,我们可以使用方法参数来实现该依赖关系,如以下示例所示:
1 |
|
解析机制与基于构造函数的依赖注入几乎相同。
用@Bean 注释定义的任何类都支持常规的生命周期回调,并且可以使用 JSR-250 中的@PostConstruct 和@PreDestroy 注释。有关更多详细信息,请参见 JSR-250 注释。
常规 Spring 生命周期回调也得到完全支持。如果 bean 实现 InitializingBean,DisposableBean 或 Lifecycle,则容器将调用它们各自的方法。
也完全支持标准*Aware 接口集(例如 BeanFactoryAware, BeanNameAware, MessageSourceAware, ApplicationContextAware 等)。
该@Bean 注释支持指定任意初始化和销毁回调方法,就像 Spring XML 中的 init-method 和 destroy-method 属性的 bean 元素,如下面的示例所示:
1 | public class BeanOne { |
默认情况下,使用 Java config 定义的具有公开关闭或停止方法的 bean 将自动加入销毁回调。如果你有一个公开的关闭或停止方法,但是你不希望在容器关闭时被调用,只需将@Bean(destroyMethod =””)添加到你的 bean 定义中即可禁用默认(推测)模式。 默认情况下,您可能希望通过 JNDI 获取资源,因为它的生命周期在应用程序之外进行管理。特别地,请确保始终为 DataSource 执行此操作,因为它已知在 Java EE 应用程序服务器上有问题。
默认情况下,您可能要对通过 JNDI 获取的资源执行此操作,因为其生命周期是在应用程序外部进行管理的。特别是,请确保始终对进行操作 DataSource,因为这在 Java EE 应用程序服务器上是有问题的。
以下示例显示了如何防止对的自动销毁回调 DataSource:
1 | "") (destroyMethod= |
另外,通过@Bean 方法,通常会选择使用编程来进行 JNDI 查找:要么使用 Spring 的 JndiTemplate/JndiLocatorDelegate 帮助类,要么直接使用 JNDI InitialContext,但不能使用 JndiObjectFactoryBean 变体来强制将返回类型声明为 FactoryBean 类型以代替目标的实际类型,它将使得在其他@Bean 方法中更难用于交叉引用调用这些在此引用提供资源的方法。
当然上面的 BeanOne 例子中,在构造期间直接调用 init()方法同样有效:
1 |
|
当您直接在 Java 中工作时,您可以对对象执行任何您喜欢的操作,并不总是需要依赖容器生命周期。
你可以指定@Bean 注解定义的 bean 应具有的特定作用域。你可以使用 Bean 作用域章节中的任何标准作用域。
默认的作用域是单例,但是你可以用@Scope 注解重写作用域。
1 |
|
Spring 提供了一个通过范围代理来处理范围依赖的便捷方法。使用 XML 配置创建此类代理的最简单方法是元素。使用@Scope 注解配置 Java 中的 bean 提供了与 proxyMode 属性相似的支持。默认是没有代理(ScopedProxyMode.NO),但您可以指定 ScopedProxyMode.TARGET_CLASS 或 ScopedProxyMode.INTERFACES。
如果你使用 Java 将 XML 参考文档(请参阅上述链接)到范围的@Bean 中移植范围限定的代理示例,则它将如下所示
如果你将 XML 参考文档的 scoped 代理示例转化为 Java @Bean,如下所示:
1 | // an HTTP Session-scoped bean exposed as a proxy |
默认情况下,配置类使用@Bean 方法的名称作为结果 bean 的名称。但是,可以使用 name 属性覆盖此功能,如以下示例所示:
1 |
|
如前文命名 bean 中所讨论的,有时希望为单个 Bean 提供多个名称,否则会导致 Bean 混淆。 为此 name,@Bean 注释的属性接受 String 数组。以下示例显示了如何为 bean 设置多个别名:
1 |
|
有时,提供有关 bean 的更详细的文本描述会很有帮助。当出于监视目的而暴露(可能通过 JMX)bean 时,这特别有用。
要将说明添加到@Bean,可以使用 @Description 批注,如以下示例所示:
1 |
|
1 |
@Configuration
注解@Configuration
是类级别的注释,指示对象是 Bean 定义的源。 @Configuration
类通过公共 @Bean
注释方法声明 bean。 对 @Configuration
类的 @Bean
方法的调用也可以用于定义 Bean 之间的依赖关系。 有关一般性介绍,请参见[基本概念:@Bean
和 @Configuration
](<#基本概念:@Bean
\ 和\ @Configuration
>)。
当 bean 相互依赖时,表示依赖关系就像让一个 bean 方法调用另一个依赖一样简单,如以下示例所示:
1 |
|
仅当
@Bean
在@Configuration
类中声明该方法时,此声明 bean 间依赖关系的方法才有效。您不能使用普通@Component
类声明 bean 间的依赖关系。
如前所述,查找方法注入是一项高级功能,您应该很少使用。在单例作用域的 bean 对原型作用域的 bean 有依赖性的情况下,这很有用。将 Java 用于这种类型的配置为实现这种模式提供了自然的方法。以下示例显示如何使用查找方法注入:
1 | public abstract class CommandManager { |
通过使用 Java 配置,可以创建一个覆盖 CommandManager
抽象 createCommand()
方法的子类,该方法将以某种方式查找新的(原型)命令对象。以下示例显示了如何执行此操作:
1 |
|
考虑以下示例,该示例显示了一个带 @Bean
注释的方法被调用两次:
1 |
|
clientDao()
在 clientService1()
中被调用一次,并在 clientService2()
中被调用一次。由于此方法会创建一个 ClientDaoImpl
的新实例并返回它,因此通常希望有两个实例(每个服务一个)。那肯定是有问题的:在 Spring 中,实例化的 bean 默认情况下具有单例作用域。这就是神奇之处所在:所有 @Configuration
类在启动时都使用 CGLIB 进行了子类化。在子类中,子方法在调用父方法并创建新实例之前,首先检查容器中是否有任何缓存(作用域)的 bean。
根据 bean 的作用域,行为可能有所不同。我们在这里只谈论单例。
从 Spring 3.2 开始,不再需要将 CGLIB 添加到您的类路径中,因为 CGLIB 类已经被重新打包
org.springframework.cglib
并直接包含在 spring-core JAR 中。
由于 CGLIB 在启动时会动态添加功能,因此存在一些限制。特别是,配置类不能是 final。但是,从 4.3 版本开始,配置类上允许使用任何构造函数,包括
@Autowired
对默认注入使用或单个非默认构造函数声明。如果您希望避免 CGLIB 施加的限制,请考虑
@Bean
在非@Configuration
类上声明您的方法(例如,在普通@Component
类上声明)。那么@Bean
就不会截获方法之间的跨方法调用,因此您必须专门依赖那里的构造函数或方法级别的依赖项注入。
Spring 的基于 Java 的配置功能使您可以编写批注,这可以降低配置的复杂性。
@Import
注释就像 <import/>
在 Spring XML 文件中使用该元素来帮助模块化配置一样,@Import
注释允许 @Bean
从另一个配置类加载定义,如以下示例所示:
1 |
|
现在,无需同时指定两者 ConfigA.class
和 ConfigB.class
实例化上下文,只需 ConfigB
显式提供,如以下示例所示
1 | public static void main(String[] args) { |
这种方法简化了容器的实例化,因为只需要处理一个类,而无需在构造过程中记住大量潜在的 @Configuration
类。
从 Spring Framework 4.2 开始,@Import
还支持对常规组件类的引用,类似于 AnnotationConfigApplicationContext.register
方法。如果要通过使用一些配置类作为入口点来显式定义所有组件,从而避免组件扫描,则此功能特别有用。
前面的示例有效,但过于简单。在大多数实际情况下,Bean 在配置类之间相互依赖。使用 XML 时,这不是问题,因为不涉及编译器,并且您可以声明 ref="someBean"
并信任 Spring 在容器初始化期间对其进行处理。使用 @Configuration
类时,Java 编译器会在配置模型上施加约束,因为对其他 bean 的引用必须是有效的 Java 语法。
幸运的是,解决这个问题很简单。正如我们已经讨论的那样,一个 @Bean
方法可以具有任意数量的描述 Bean 依赖关系的参数。考虑以下具有多个 @Configuration
类的更真实的场景,每个类都取决于其他类中声明的 bean:
1 |
|
还有另一种方法可以达到相同的结果。 请记住,@Configuration
类最终仅是容器中的另一个 bean:这意味着它们可以利用 @Autowired
和 @Value
注入以及与任何其他 bean 相同的其他功能。
确保以这种方式注入的依赖项只是最简单的一种。
@Configuration
类是在上下文初始化期间非常早地处理的,并且强制以这种方式注入依赖项可能导致意外的早期初始化。 如上例所示,尽可能使用基于参数的注入。另外,通过
@Bean
使用BeanPostProcessor
和BeanFactoryPostProcessor
定义时要特别小心。 通常应将这些声明为静态@Bean
方法,从而不触发其包含的配置类的实例化。 否则,@Autowired
和@Value
可能不适用于配置类本身,因为该 bean 实例的创建可能比AutowiredAnnotationBeanPostProcessor
要早。
以下示例说明如何将一个 bean 自动连接到另一个 bean:
1 |
|
仅从 Spring Framework 4.3 开始支持
@Configuration
类中的构造方法注入。还要注意,如果目标 bean 仅定义了一个构造函数,那么无需指定@Autowired
。
在前面的场景中,使用 @Autowired
效果很好,并提供了所需的模块化,但是要确切确定声明自动装配的 Bean 定义的位置仍然有些模棱两可。例如,当开发人员查看时 ServiceConfig,您如何确切知道该 @Autowired AccountRepository
bean 的声明位置?它在代码中不是明确的,这可能很好。请记住, Spring Tools for Eclipse 提供了可以渲染图形的工具,这些图形显示了所有接线的方式,这可能就是您所需要的。另外,您的 Java IDE 可以轻松找到该 AccountRepository 类型的所有声明和使用,并快速向您显示@Bean
返回该类型的方法的位置。
如果这种歧义是不可接受的,并且您希望从 IDE 内部直接从一个 @Configuration
类导航到另一个类,请考虑自动装配配置类本身。以下示例显示了如何执行此操作:
1 |
|
在上述情况下,在哪里定义了 AccountRepository
是完全显式的。但是,ServiceConfig
现在与 RepositoryConfig
紧密耦合。那是权衡。通过使用基于接口或基于抽象类的 @Configuration
类,可以在某种程度上缓解这种紧密耦合。考虑以下示例:
1 |
|
现在 ServiceConfig
就具体而言松散耦合 DefaultRepositoryConfig
,并且内置的 IDE 工具仍然有用:您可以轻松地获得实现的类型层次结构 RepositoryConfig
。通过这种方式,导航 @Configuration
类及其依赖项与导航基于接口的代码的通常过程没有什么不同。
如果要影响某些 Bean 的启动创建顺序,请考虑将其中一些声明为 @Lazy
(用于首次访问而不是在启动时创建)或声明为 @DependsOn
某些其他 Bean(确保在当前 Bean 之前创建特定的其他 Bean),后者的直接依赖意味着什么)。
@Configuration
类或 @Bean
方法根据某些系统状态,有条件地启用或禁用完整的 @Configuration
类甚至单个 @Bean
方法通常很有用。一个常见的示例是仅在 Spring 环境中启用了特定配置文件后,才使用@Profile
批注来激活 Bean(有关详细信息,请参见后文 Bean 定义配置文件)。
@Profile
批注实际上是通过使用更灵活的称为 @Conditional
的批注来实现的。 @Conditional
批注指示在注册 @Bean
之前应参考的特定 org.springframework.context.annotation.Condition
实现。
Condition
接口的实现提供了一个 matches(…)
方法,该方法返回 true
或 false
。例如,以下清单显示了用于 @Profile
的实际 Condition
实现:
1 |
|
有关 @Conditional
更多详细信息,请参见 javadoc。
Spring 的 @Configuration
类支持并非旨在 100% 完全替代 Spring XML。 某些工具(例如 Spring XML 名称空间)仍然是配置容器的理想方法。 在使用 XML 方便或有必要的情况下,您可以选择:使用“以 XML 为中心”的方式实例化容器,比如 ClassPathXmlApplicationContext
,或使用“以 Java 中心”的方式实例化容器,也就是使用 AnnotationConfigApplicationContext
和 @ImportResource
批注来根据需要导入 XML。
@Configuration
类使用最好从 XML 引导 Spring 容器并以 ad-hoc 方式包含 @Configuration
类。 例如,在使用 Spring XML 的大型现有代码库中,根据需要创建 @Configuration
类并从现有 XML 文件中将它们包含在内会变得更加容易。 在本节的后面,我们将介绍在这种“以 XML 为中心”的情况下使用 @Configuration
类的选项。
@Configuration
类声明为纯 Spring <bean/>
元素请记住,@Configuration
类最终是容器中的 bean 定义。在本系列示例中,我们创建一个名为 AppConfig
的 @Configuration
类, 并将其包含在system-test-config.xml
中作为 <bean/>
定义。因为 <context:annotation-config/>
已打开,所以容器会识别 @Configuration
注释并正确处理 AppConfig
中声明的 @Bean
方法。
以下示例显示了 Java 中的普通配置类:
1 |
|
以下示例显示了示例 system-test-config.xml
文件的一部分:
1 | <beans> |
以下示例显示了一个可能的 jdbc.properties
文件:
1 | jdbc.url=jdbc:hsqldb:hsql://localhost/xdb |
1 | public static void main(String[] args) { |
在
system-test-config.xml
文件中,AppConfig
<bean/>
没有声明id
元素。尽管申明一下是可以接受的,但由于没有其他 bean 引用过它,因此这是不必要的,并且不太可能通过名称从容器中显式获取。同样,DataSource
仅按类型自动对 Bean 进行接线,因此也并不严格要求id
定义。
<context:component-scan/>
拾取 @Configuration
类因为 @Configuration
用 @Component
进行元注释,所以 @Configuration
注释的类自动成为组件扫描的候选对象。使用与先前示例中描述的场景相同的场景,我们可以重新定义 system-test-config.xml
以利用组件扫描的优势。请注意,在这种情况下,我们无需显式声明 <context:annotation-config/>
,因为 <context:component-scan/>
可启用相同的功能。
以下示例显示了修改后的 system-test-config.xml
文件:
1 | <beans> |
@Configuration
以类为中心的 XML 与 @ImportResource
在 @Configuration
类是配置容器的主要机制的应用程序中,仍然有必要至少使用一些 XML。在这些情况下,您可以使用 @ImportResource
并仅定义所需的 XML。这样做实现了一种“以 Java 为中心”的方法来配置容器,并将 XML 保持在最低限度。以下示例(包括配置类,定义 Bean 的 XML 文件,属性文件和主类)显示了如何使用 @ImportResource
批注来实现按需使用 XML 的以 Java 为中心的配置:
1 |
|
properties-config.xml:
1 | <beans> |
jdbc.properties:
1 | jdbc.url=jdbc:hsqldb:hsql://localhost/xdb |
1 | public static void main(String[] args) { |
Environment
接口是集成在容器中的抽象,它对应用程序环境的两个关键方面进行建模:概要文件(profiles)和属性(properties)。
profile 是仅在给定概要文件处于活动状态时才向容器注册的 Bean 定义的命名逻辑组。可以将 Bean 通过 XML 定义还或注释方式分配给 profile。与 profile 相关的 Environment
对象的作用是确定当前哪些 profile(如果有)处于活动状态,以及默认情况下哪些 profile(如果有)应处于活动状态。
properties 在几乎所有应用程序中都起着重要作用,并且可能源自多种来源:属性文件,JVM 系统属性,系统环境变量,JNDI,Servlet 上下文参数,ad-hoc Properties
对象,Map
对象等。Environment
对象与 properties 相关的作用是为用户提供方便的服务接口,用于配置属性源并从中解析属性。
Bean 定义配置文件在核心容器中提供了一种机制,该机制允许在不同环境中注册不同的 Bean。“环境”一词对不同的用户可能具有不同的含义,并且此功能可以帮助解决许多用例,包括:
考虑实际应用中需要使用的第一个用例 DataSource
。在测试环境中,配置可能类似于以下内容:
1 |
|
现在,假设该应用程序的数据源已在生产应用程序服务器的 JNDI 目录中注册,请考虑如何将该应用程序部署到 QA 或生产环境中。 现在,我们的 dataSource
bean 看起来像下面的清单:
1 | "") (destroyMethod= |
问题是如何根据当前环境在使用这两种变体之间进行切换。 随着时间的流逝,Spring 用户已经设计出多种方法来完成此任务,通常依赖于系统环境变量和包含 ${placeholder}
令牌的 XML <import/>
语句的组合,这些语句根据值解析为正确的配置文件路径环境变量。 Bean 定义配置文件是一个核心容器功能,可提供此问题的解决方案。
如果我们概括前面特定于环境的 Bean 定义示例中所示的用例,那么最终需要在某些上下文中而不是在其他上下文中注册某些 Bean 定义。 您可能会说您要在情况 A 中注册一个特定的 bean 定义配置文件,在情况 B 中注册一个不同的配置文件。我们首先更新配置以反映这种需求。
@Profile
@Profile 批注可让您指示一个或多个指定的配置文件处于活动状态时有资格注册的组件。 使用前面的示例,我们可以如下重写 dataSource 配置:
1 |
|
1 |
|
如前所述,对于
@Bean
方法,通常选择使用程序化 JNDI 查找,方法是使用 Spring 的JndiTemplate
/JndiLocatorDelegate
帮助器或前面展示的直接 JNDIInitialContext
用法,而不是JndiObjectFactoryBean
变体,这将迫使您将返回类型声明为FactoryBean
类型。
配置文件字符串可以包含简单的配置文件名称(例如 production
)或配置文件表达式。配置文件表达式允许表达更复杂的配置文件逻辑(例如 production & us-east
)。概要文件表达式中支持以下运算符:
!
: 非&
: 与|
: 或您不能在不使用括号的情况下混合使用
&
和|
运算符。例如,production & us-east | eu-central
不是有效的表达式。它必须表示为production & (us-east | eu-central)
。
您可以将其@Profile
用作元注释,以创建自定义的组合注释。以下示例定义了一个自定义 @Production
批注,您可以将其用作替代品 @Profile("production")
:
1 | (ElementType.TYPE) |
如果用
@Configuration
标记了一个类,则除非一个或多个指定的配置文件处于活动状态,否则将忽略与该类关联的@Profile
所有@Bean
方法和@Import
注释。如果一个@Component
或@Configuration
类标记有@Profile({"p1", "p2"})
,则除非已激活配置文件“p1”或“p2”,否则该类不会注册或处理。如果给定的配置文件以 非 运算符(!
)为前缀,则仅在该配置文件不活动时才注册带注释的元素。例如,给定@Profile({"p1", "!p2"})
,如果配置文件“p1”处于活动状态或配置文件“p2”未处于活动状态,则会进行注册。
@Profile
也可以在方法级别声明为仅包括配置类的一个特定 Bean(例如,特定 Bean 的替代变体),如以下示例所示:
1 |
|
在
@Bean
方法上使用@Profile
时,可能会出现特殊情况:对于具有@Bean
相同 Java 方法名称的重载方法(类似于构造函数重载),@Profile
需要在所有重载方法上一致声明条件。如果条件不一致,则仅重载方法中第一个声明的条件有效。因此,@Profile
不能用于选择具有特定参数签名的重载方法。在创建时,相同 bean 的所有工厂方法之间的解析都遵循 Spring 的构造函数解析算法。如果要使用不同的概要文件条件定义备用 bean,
@Bean
name 属性需要使用不同的 Java 方法名称来指向相同的 bean 名称,如前面的示例所示。如果参数签名都相同(例如,所有变体都具有无参工厂方法),则这是首先在有效 Java 类中表示这种排列的唯一方法(因为只能有一个特定名称和参数签名的方法)。
XML 对应项是元素的 profile 属性 <beans>
。我们前面的示例配置可以用两个 XML 文件重写,如下所示:
1 | <beans profile="development" |
1 | <beans profile="production" |
也可以避免<beans/>
在同一文件中拆分和嵌套元素,如以下示例所示:
1 | <beans xmlns="http://www.springframework.org/schema/beans" |
spring-bean.xsd
已被限制为仅允许这些元素作为文件中的最后一个元素。这应该有助于提供灵活性,而不会引起 XML 文件混乱。
XML 对应项不支持前面描述的配置文件表达式。但是,可以通过使用
!
运算符来取消配置文件。也可以通过嵌套配置文件来应用逻辑“与”,如以下示例所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 > <beans xmlns="http://www.springframework.org/schema/beans"
> xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
> xmlns:jdbc="http://www.springframework.org/schema/jdbc"
> xmlns:jee="http://www.springframework.org/schema/jee"
> xsi:schemaLocation="...">
>
> <!-- other bean definitions -->
>
> <beans profile="production">
> <beans profile="us-east">
> <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
> </beans>
> </beans>
> </beans>
>
在前面的示例中,如果
production
和us-east
profiles 都处于活动状态,则将显示dataSource
bean。
现在我们已经更新了配置,我们仍然需要指示 Spring 哪个配置文件处于活动状态。如果我们现在启动示例应用程序,则会看到 NoSuchBeanDefinitionException
抛出的错误,因为容器找不到名为 dataSource
的 Spring bean。
可以通过多种方式来激活配置文件,但最直接的方法是针对可通过 ApplicationContext
获得的 Environment
API 以编程方式进行配置。以下示例显示了如何执行此操作:
1 | AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); |
另外,您还可以通过 spring.profiles.active
属性声明性地激活配置文件,可以通过系统环境变量,JVM 系统属性,web.xml
中的 servlet 上下文参数 或甚至作为 JNDI 中的条目来指定配置文件 (请参见 [PropertySource
抽象化](<#PropertySource
\ 抽象化>))。在集成测试中,可以通过使用 spring-test
模块中的 @ActiveProfiles
注释来声明活动配置文件 (请参阅环境配置文件的上下文配置)。
请注意,profile 不是“非此即彼”的。您可以一次激活多个配置文件。通过编程,您可以为 setActiveProfiles()
方法提供多个配置文件名称,该方法接受 String…
变长参数。以下示例激活多个配置文件:
1 | ctx.getEnvironment().setActiveProfiles("profile1", "profile2"); |
以声明方式,spring.profiles.active
可以接受以逗号分隔的配置文件名称列表,如以下示例所示:
1 | -Dspring.profiles.active="profile1,profile2" |
1 |
|
如果没有配置文件处于活动状态,则将创建上面的dataSource
。您可以看到这是为一个或多个 bean 提供默认定义的一种方法。如果启用了任何配置文件,则默认配置文件将不适用。
您可以通过Environment
上的setDefaultProfiles()
,或者声明 spring.profiles.default
属性,来更改默认的配置文件的名称,。
PropertySource
抽象Spring 的 Environment
抽象提供了对属性源可配置层次结构的搜索操作。考虑以下清单:
1 | ApplicationContext ctx = new GenericApplicationContext(); |
在前面的代码片段中,我们看到了一种高级方式来询问 Spring 是否为当前环境定义了 my-property
属性。 为了解决这个问题,Environment
对象在一组 PropertySource
对象上执行搜索。 PropertySource
是对键-值对源的简单抽象,Spring 的 StandardEnvironment
配置了两个 PropertySource
对象——一个代表 JVM 系统属性的集合(System.getProperties()
),另一个代表系统环境变量的集合(System.getenv()
)。
这些默认属性源存在于
StandardEnvironment
中,供在独立应用程序中使用。StandardServletEnvironment
用其他默认属性源(包括servlet
配置和servlet
上下文参数)填充。它可以有选择地启用JndiPropertySource
。有关详细信息,请参见 javadoc。
具体来说,当您使用StandardEnvironment
时,env.containsProperty("my-property")
如果运行时存在 my-property
系统属性或 my-property
环境变量,则对的调用将返回 true 。
执行的搜索是分层的。默认情况下,系统属性优先于环境变量。因此,如果
my-property
在调用期间在两个地方同时设置了env.getProperty("my-property")
该属性,则系统属性值将“获胜”并返回。请注意,属性值不会合并,而是会被前面的条目完全覆盖。对于常规
StandardServletEnvironment
,完整层次结构如下,最高优先级条目位于顶部:
- ServletConfig 参数(如果适用,例如在
DispatcherServlet
上下文的情况下)- ServletContext 参数(
web.xml
的context-param
条目)- JNDI 环境变量(
java:comp/env/
条目)- JVM 系统属性(-D 命令行参数)
- JVM 系统环境(操作系统环境变量)
最重要的是,整个机制是可配置的。也许您有一个要集成到此搜索中的自定义属性源。为此,请实现并实例化自己的 PropertySource
实例并将其添加到 Environment
的 PropertySourcescurrent
集合中。以下示例显示了如何执行此操作:
1 | ConfigurableApplicationContext ctx = new GenericApplicationContext(); |
在前面的代码中,MyPropertySource
已在搜索中添加了最高优先级。如果它包含一个my-property
属性,则检测并返回该属性,超越其他 PropertySource
中的my-property
属性 。MutablePropertySources
API 公开了许多方法,这些方法允许对属性源集进行精确操作。
@PropertySource
注解提供了一种添加PropertySource
到 Spring 的Environment
的便利和声明性的机制。
给定一个名为 app.properties
的文件,包含键-值对 testbean.name=myTestBean
,下面的 @Configuration
类使用 @PropertySource
,其方式是对 testBean.getName()
的调用返回 myTestBean
:
1 |
|
@PropertySource
资源位置中存在的任何 ${…}
占位符都是根据已经针对该环境注册的一组属性源来解析的,如以下示例所示:
1 |
|
假设 my.placeholder
存在于已注册的属性源之一(例如,系统属性或环境变量)中,则占位符将解析为相应的值。如果不是,则 default/path
用作默认值。如果未指定默认值并且无法解析属性,则抛出 IllegalArgumentException
。
根据 Java 8 的约定, @PropertySource
注释是可重复的。但是,所有此类@PropertySource
批注都需要在同一级别上声明,可以直接在配置类上声明,也可以在同一自定义批注中声明为元批注。不建议将直接注释和元注释混合使用,因为直接注释会覆盖元注释。
从历史上看,元素中占位符的值只能根据 JVM 系统属性或环境变量来解析。这已不再是这种情况。由于 Environment
抽象是在整个容器中集成的,因此很容易通过它路由占位符的解析。这意味着您可以按照自己喜欢的任何方式配置解析过程。您可以更改搜索系统属性和环境变量的优先级,也可以完全删除它们。您还可以根据需要将自己的属性源添加到混合中。
具体而言,无论该 customer 属性在何处定义,只要该属性在 Environment
可用,以下语句均有效:
1 | <beans> |
LoadTimeWeaver
Spring 使用 LoadTimeWeaver
在将类加载到 Java 虚拟机(JVM)中时对其进行动态转换。
要启用加载时编织(load-time weaving),可以将 @EnableLoadTimeWeaving
添加到您的 @Configuration
类之一,如以下示例所示:
1 |
|
另外,对于 XML 配置,可以使用 context:load-time-weaver 元素:
1 | <beans> |
为 ApplicationContext
配置后,该 ApplicationContext
中的任何 bean 都可以实现 LoadTimeWeaverAware
,从而接收对加载时 weaver 实例的引用。 与 Spring 的 JPA 支持结合使用时,该功能特别有用,因为在 JPA 类转换中可能需要进行加载时编织。 有关更多详细信息,请查阅 LocalContainerEntityManagerFactoryBean
javadoc。 有关 AspectJ 加载时编织的更多信息,请参见 Spring 框架中的 AspectJ 加载时编织。
如本章介绍中所讨论的,org.springframework.beans.factory
包提供了用于管理和操纵 bean 的基本功能,包括以编程方式。org.springframework.context
包添加了 ApplicationContext
接口,该接口扩展了 BeanFactory
接口,此外还扩展了其他接口以提供更多面向应用程序框架的样式的附加功能。许多人以完全声明性的方式使用 ApplicationContext
,甚至没有以编程方式创建它,而是依靠诸如 ContextLoader
之类的支持类来自动实例化 ApplicationContext
作为 Java EE Web 应用程序正常启动过程的一部分。
为了以更加面向框架的方式增强 BeanFactory
的功能,上下文包还提供以下功能:
通过 MessageSource
界面访问 i18n 样式的消息。
通过 ResourceLoader
接口访问资源,例如 URL 和文件。
通过使用 ApplicationEventPublisher
接口,将事件发布到实现 ApplicationListener
接口的 bean。
加载多个(分层)上下文,使每个上下文都通过 HierarchicalBeanFactory
接口集中在一个特定层上,例如应用程序的 Web 层。
ApplicationContext
接口扩展了一个称为 MessageSource
的接口,因此提供了国际化(“i18n”)功能。Spring 还提供了 HierarchicalMessageSource
接口,该接口可以分层解析消息。这些接口一起提供了 Spring 影响消息解析的基础。 这些接口上定义的方法包括:
String getMessage(String code, Object[] args, String default, Locale loc)
:用于从 MessageSource
检索消息的基本方法。如果找不到针对指定语言环境的消息,则使用默认消息。使用标准库提供的 MessageFormat 功能,传入的所有参数都将成为替换值。String getMessage(String code, Object[] args, Locale loc)
:与先前的方法基本相同,但有一个区别:无法指定默认消息。如果找不到该消息,则抛出 NoSuchMessageException。
String getMessage(MessageSourceResolvable resolvable, Locale locale)
:前面方法中使用的所有属性也都包装在一个名为 MessageSourceResolvable
的类中,您可以在此方法中使用该类。加载 ApplicationContext
时,它将自动搜索在上下文中定义的 MessageSource
bean。Bean 的 name 必须是 messageSource
。如果找到了这样的 bean,则对先前方法的所有调用都将委派给消息源。 如果找不到消息源,则 ApplicationContext
尝试查找包含同名 bean 的父级。如果找到了,它将使用该 bean 作为 MessageSource
。如果 ApplicationContext
找不到任何消息源,则将实例化一个空的 DelegatingMessageSource
,以便能够接受对上述方法的调用。
Spring 提供了两个 MessageSource
实现,即 ResourceBundleMessageSource
和 StaticMessageSource
。两者都实现 HierarchicalMessageSource
以便进行嵌套消息传递。 StaticMessageSource
很少使用,但是提供了将消息添加到源中的编程方式。下面的示例显示 ResourceBundleMessageSource
:
1 | <beans> |
该示例假设您在类路径中定义了三个资源包,分别称为 format,exceptions 和 windows。解析消息的任何请求都通过 JDK 标准的通过 ResourceBundle
对象解析消息的方式来处理。就本示例而言,假定上述两个资源束文件的内容如下:
1 | # in format.properties |
1 | # in exceptions.properties |
下一个示例显示了执行 MessageSource
功能的程序。请记住,所有 ApplicationContext
实现也是 MessageSource
实现,因此可以转换为 MessageSource
接口。
1 | public static void main(String[] args) { |
以上程序的结果输出如下:
1 | Alligators rock! |
总而言之,MessageSource
是在名为 beans.xml
的文件中定义的,该文件位于类路径的根目录下。 messageSource
bean 定义通过其 basenames
属性引用了许多资源包。列表中传递给 basenames
属性的三个文件在类路径的根目录下以文件形式存在,分别称为 format.properties
,exceptions.properties
和 windows.properties
。
下一个示例显示了传递给消息查找的参数。这些参数将转换为 String
对象,并插入到查找消息中的占位符中。
1 | <beans> |
1 | public class Example { |
execute()
方法调用的结果输出如下:
1 | The userDao argument is required. |
关于国际化(“i18n”),Spring 的各种 MessageSource
实现遵循与标准 JDK ResourceBundle
相同的语言环境解析和后备规则。简而言之,继续前面定义的示例 messageSource
,如果要针对英国(en-GB)语言环境解析消息,则可以分别创建名为 format_en_GB.properties
,exceptions_en_GB.properties
和 windows_en_GB.properties
的文件。
通常,语言环境解析由应用程序的周围环境管理。在以下示例中,手动指定了针对其解析(英国)消息的语言环境:
1 | # in exceptions_en_GB.properties |
1 | public static void main(final String[] args) { |
运行上述程序的结果输出如下:
1 | Ebagum lad, the 'userDao' argument is required, I say, required. |
您还可以使用 MessageSourceAware
接口获取对已定义的任何 MessageSource
的引用。 创建和配置 bean 时,在 ApplicationContext
中实现 MessageSourceAware
接口的所有 bean 都会与应用程序上下文的 MessageSource
一起注入。
作为
ResourceBundleMessageSource
的替代,Spring 提供了ReloadableResourceBundleMessageSource
类。 此变体支持相同的包文件格式,但比基于标准 JDK 的ResourceBundleMessageSource
实现更灵活。特别是,它允许从任何 Spring 资源位置(不仅从类路径)读取文件,并且支持捆绑属性文件的热重载(同时在它们之间有效地进行缓存)。有关详细信息,请参见ReloadableResourceBundleMessageSource
javadoc。
通过 ApplicationEvent
类和 ApplicationListener
接口提供 ApplicationContext
中的事件处理。 如果将实现 ApplicationListener
接口的 bean 部署到上下文中,则每次将 ApplicationEvent
发布到 ApplicationContext
时,都会通知该 bean。 本质上,这是标准的 Observer 设计模式。
从 Spring 4.2 开始,事件基础结构得到了显着改进,并提供了基于注释的模型以及发布任意事件(即不一定从 ApplicationEvent 扩展的对象)的功能。发布此类对象后,我们会为您包装一个事件。
下表描述了 Spring 提供的标准事件:
事件 | 说明 |
---|---|
ContextRefreshedEvent |
在初始化或刷新 ApplicationContext 时发布(例如,通过使用 ConfigurableApplicationContext 接口上的 refresh() 方法)。 在这里,“已初始化”是指所有 Bean 均已加载,检测到并激活了后处理器 Bean,已预先实例化单例并且可以使用 ApplicationContext 对象。 只要尚未关闭上下文,只要选定的 ApplicationContext 实际上支持这种“热”刷新,就可以多次触发刷新。例如,XmlWebApplicationContext 支持热刷新,但 GenericApplicationContext 不支持。 |
ContextStartedEvent |
在 ConfigurableApplicationContext 接口上使用 start() 方法启动 ApplicationContext 时发布。在这里,“已启动”表示所有 Lifecycle bean 都收到一个明确的启动信号。通常,此信号用于在显式停止后重新启动 Bean,但也可以用于启动尚未配置为自动启动的组件(例如,尚未在初始化时启动的组件)。 |
ContextStoppedEvent |
通过使用 ConfigurableApplicationContext 接口上的 stop() 方法停止 ApplicationContext 时发布。 在这里,“已停止”表示所有 Lifecycle bean 都收到一个明确的停止信号。 停止的上下文可以通过 start() 调用重新启动。 |
ContextClosedEvent |
通过使用 ConfigurableApplicationContext 接口上的 close() 方法或通过 JVM 关闭钩子关闭 ApplicationContext 时发布。 在这里,“已关闭”意味着所有单例 bean 将被销毁。 关闭上下文后,它将达到使用寿命,无法刷新或重新启动。 |
RequestHandledEvent |
一个特定于 Web 的事件,告诉所有 Bean HTTP 请求已得到服务。请求完成后,将发布此事件。此事件仅适用于使用 Spring 的 DispatcherServlet 的 Web 应用程序。 |
ServletRequestHandledEvent |
RequestHandledEvent 的子类,用于添加特定于 Servlet 的上下文信息。 |
您还可以创建和发布自己的自定义事件。以下示例显示了一个简单的类,该类扩展了 Spring 的 ApplicationEvent
基类:
1 | public class BlackListEvent extends ApplicationEvent { |
要发布自定义的 ApplicationEvent
,请在 publishEvent()
上调用方法 ApplicationEventPublisher
。通常,这是通过创建一个实现 ApplicationEventPublisherAware
并注册为 Spring bean 的类来完成的。以下示例显示了此类:
1 | public class EmailService implements ApplicationEventPublisherAware { |
在配置时,Spring 容器检测到 EmailService
实现了 ApplicationEventPublisherAware
并自动调用 setApplicationEventPublisher()
。实际上,传入的参数是 Spring 容器本身。您正在通过其 ApplicationEventPublisher
接口与应用程序上下文进行交互。
要接收自定义 ApplicationEvent
,可以创建一个实现 ApplicationListener
的类并将其注册为 Spring Bean。 以下示例显示了此类:
1 | public class BlackListNotifier implements ApplicationListener<BlackListEvent> { |
请注意,ApplicationListener
通常使用您的自定义事件的类型(上一示例中的 BlackListEvent
)进行参数化。这意味着 onApplicationEvent()
方法可以保持类型安全,从而避免了任何向下转换的需求。您可以根据需要注册任意数量的事件侦听器,但是请注意,默认情况下,事件侦听器会同步接收事件。这意味着 publishEvent()
方法将阻塞,直到所有侦听器都已完成对事件的处理为止。这种同步和单线程方法的一个优点是,当侦听器接收到事件时,如果有可用的事务上下文,它将在发布者的事务上下文内部进行操作。如果有必要采用其他发布事件的策略,请参阅 Spring 的 ApplicationEventMulticaster
接口的 javadoc 和配置选项的 SimpleApplicationEventMulticaster
实现。
以下示例显示了用于注册和配置上述每个类的 Bean 定义:
1 | <bean id="emailService" class="example.EmailService"> |
总而言之,当调用 emailService
bean 的 sendEmail()
方法时,如果有任何电子邮件应列入黑名单,则将发布 BlackListEvent
类型的自定义事件。 blackListNotifier
bean 被注册为 ApplicationListener
并接收 BlackListEvent
,这时它可以通知适当的参与者。
Spring 的事件机制旨在在同一应用程序上下文内在 Spring bean 之间进行简单的通信。 但是,对于更复杂的企业集成需求,单独维护的 Spring Integration 项目为基于著名的 Spring 编程模型构建轻量级,面向模式,事件驱动的架构提供了完整的支持。
从 Spring 4.2 开始,您可以使用 @EventListener
注释在托管 Bean 的任何公共方法上注册事件侦听器。BlackListNotifier
可改写如下:
1 | public class BlackListNotifier { |
方法签名再次声明其侦听的事件类型,但是这次使用灵活的名称,并且没有实现特定的侦听器接口。只要实际事件类型在其实现层次结构中解析您的通用参数,也可以通过通用类型来缩小事件类型。
如果您的方法应该侦听多个事件,或者您要完全不使用任何参数来定义它,则事件类型也可以在注释本身上指定。以下示例显示了如何执行此操作:
1 | ({ContextStartedEvent.class, ContextRefreshedEvent.class}) |
也可以通过使用定义 SpEL 表达式的注释的 condition
属性来添加其他运行时过滤,该注释应匹配以针对特定事件实际调用该方法。
以下示例显示了仅当事件的 content
属性等于 my-event
时,才可以重写我们的通知程序以进行调用:
1 | "#blEvent.content == 'my-event'") (condition = |
每个 SpEL 表达式都会根据专用上下文进行求值。 下表列出了可用于上下文的项目,以便您可以将它们用于条件事件处理:
名称 | 位置 | 描述 | 例 |
---|---|---|---|
事件 | 根对象 | 实际的 ApplicationEvent 。 |
#root.event 或 event |
参数数组 | 根对象 | 用于调用方法的参数(作为对象数组)。 | #root.args 或 args ; args[0] 访问第一个参数,等等。 |
参数名称 | 求值上下文 | 任何方法参数的名称。如果由于某种原因这些名称不可用(例如,由于在编译的字节码中没有调试信息),则也可以使用#a<#arg> 语法(其中<#arg> 代表参数索引(从 0 开始)。 |
#blEvent 或 #a0 (您也可以使用 #p0 或 #p<#arg> 参数符号作为别名 |
请注意,即使您的方法签名实际上引用了已发布的任意对象,root.event
也使您可以访问基础事件。
如果由于处理一个事件而需要发布另一个事件,则可以更改方法签名以返回应发布的事件,如以下示例所示:
1 |
|
异步侦听器不支持此功能。
此新方法为上述方法处理的每个 BlackListEvent
发布一个新的 ListUpdateEvent
。如果您需要发布多个事件,则可以返回事件的 Collection
。
如果希望特定的侦听器异步处理事件,则可以重用常规的 @Async
支持。 以下示例显示了如何执行此操作:
1 |
|
使用异步事件时,请注意以下限制:
如果异步事件侦听器引发 Exception
,则不会将其传播到调用方。有关更多详细信息,请参见 AsyncUncaughtExceptionHandler
。
异步事件侦听器方法无法通过返回值来发布后续事件。如果您需要发布另一个事件作为处理的结果,请注入 ApplicationEventPublisher
以手动发布事件。
如果需要先调用一个侦听器,则可以将 @Order
注释添加到方法声明中,如以下示例所示:
1 |
|
您还可以使用泛型来进一步定义事件的结构。考虑使用 EntityCreatedEvent<T>
,其中 T 是已创建的实际实体的类型。例如,您可以创建以下侦听器定义以仅接收 Person
的 EntityCreatedEvent
:
1 |
|
由于类型擦除,只有在触发的事件解析了事件侦听器所依据的通用参数(即 class PersonCreatedEvent extends EntityCreatedEvent<Person> {…}
)时,此方法才起作用。
在某些情况下,如果所有事件都遵循相同的结构,这可能会变得很乏味(就像前面示例中的事件一样)。 在这种情况下,您可以实现 ResolvableTypeProvider
来指导框架超出运行时环境提供的范围。以下事件显示了如何执行此操作:
1 | public class EntityCreatedEvent<T> extends ApplicationEvent implements ResolvableTypeProvider { |
这不仅适用于
ApplicationEvent
,而且适用于您作为事件发送的任何任意对象。
为了获得最佳用法和对应用程序上下文的理解,您应该熟悉 Spring 的 Resource
抽象,如参考资料所述。
应用程序上下文是 ResourceLoader
,可用于加载 Resource
对象。Resource
本质上是 JDK java.net.URL
类的功能更丰富的版本。实际上,资源的实现在适当的地方包装了 java.net.URL
的实例。资源可以以透明的方式从几乎任何位置获取低级资源,包括从类路径、文件系统位置、可使用标准 URL 描述的任何位置以及一些其他变体。如果资源位置字符串是没有任何特殊前缀的简单路径,则这些资源的来源是特定的,并且适合于实际的应用程序上下文类型。
您可以配置部署到应用程序上下文中的 bean,以实现特殊的回调接口 ResourceLoaderAware
,以便在初始化时自动调用,并将应用程序上下文本身作为 ResourceLoader
传入。您还可以公开 Resource
类型的属性,以用于访问静态资源。它们像其他任何属性一样注入其中。您可以将那些 Resource
属性指定为简单的 String 路径,并在部署 bean 时依靠从这些文本字符串到实际 Resource
对象的自动转换。
提供给 ApplicationContext
构造函数的一个或多个位置路径实际上是资源字符串,并且根据特定的上下文实现以简单的形式对其进行适当处理。例如,ClassPathXmlApplicationContext
将简单的位置路径视为类路径位置。您也可以使用带有特殊前缀的位置路径(资源字符串)来强制从类路径或 URL 中加载定义,而不管实际的上下文类型如何。
您可以使用例如 ContextLoader
声明性地创建 ApplicationContext
实例。 当然,您还可以使用 ApplicationContext
实现之一以编程方式创建 ApplicationContext
实例。
您可以使用 ApplicationContext
来注册一个 ContextLoaderListener
,如以下示例所示:
1 | <context-param> |
侦听器检查 contextConfigLocation
参数。 如果参数不存在,那么侦听器将使用 /WEB-INF/applicationContext.xml
作为默认值。 当参数确实存在时,侦听器将使用预定义的定界符(逗号,分号和空格)来分隔 String,并将这些值用作搜索应用程序上下文的位置。此外还支持蚂 Ant 风格的路径模式。示例包括 /WEB-INF/*Context.xml
(适用于所有名称以 Context.xml
结尾且位于 WEB-INF
目录中的文件)和 /WEB-INF/**/*Context.xml
(适用于所有此类在 WEB-INF
的任何子目录中的文件)。
ApplicationContext
部署为 Java EE RAR 文件可以将 Spring ApplicationContext
部署为 RAR 文件,并将上下文及其所有必需的 bean 类和库 JAR 封装在 Java EE RAR 部署单元中。这等效于引导独立的 ApplicationContext
(仅托管在 Java EE 环境中)能够访问 Java EE 服务器功能。 RAR 部署是部署无头 WAR 文件的方案的一种更自然的选择——实际上,这种 WAR 文件没有任何 HTTP 入口点,仅用于在 Java EE 环境中引导 Spring ApplicationContext
。
对于不需要 HTTP 入口点而仅由消息端点和计划的作业组成的应用程序上下文,RAR 部署是理想的选择。在这样的上下文中,Bean 可以使用应用程序服务器资源,例如 JTA 事务管理器和 JNDI 绑定的 JDBC DataSource
实例以及 JMS ConnectionFactory
实例,并且还可以在平台的 JMX 服务器上注册-通过 Spring 的标准事务管理以及 JNDI 和 JMX 支持工具。应用程序组件还可以通过 Spring 的 TaskExecutor
抽象与应用程序服务器的 JCA WorkManager
进行交互。
有关 RAR 部署中涉及的配置详细信息,请参见 SpringContextResourceAdapter
类的 javadoc。
对于将 Spring ApplicationContext 作为 Java EE RAR 文件的简单部署:
将所有应用程序类打包到 RAR 文件(这是具有不同文件扩展名的标准 JAR 文件)中。将所有必需的库 JAR 添加到 RAR 归档文件的根目录中。添加一个 META-INF/ra.xml
部署描述符(如 javadoc 中的 SpringContextResourceAdapter
所示)和相应的 Spring XML bean 定义文件(通常为 META-INF/applicationContext.xml
)。
将生成的 RAR 文件拖放到应用程序服务器的部署目录中。
此类 RAR 部署单元通常是独立的。它们不会将组件暴露给外界,甚至不会暴露给同一应用程序的其他模块。与基于 RAR 的
ApplicationContext
的交互通常是通过与其他模块共享的 JMS 目标进行的。例如,基于 RAR 的ApplicationContext
还可以安排一些作业或对文件系统(或类似文件)中的新文件做出反应。如果需要允许来自外部的同步访问,则可以(例如)导出 RMI 端点,该端点可以由同一台计算机上的其他应用程序模块使用。
BeanFactory
API 为 Spring 的 IoC 功能提供了基础。它的特定合同主要用于与 Spring 的其他部分以及相关的第三方框架集成,并且它的 DefaultListableBeanFactory
实现是更高级别的 GenericApplicationContext
容器中的关键委托。
BeanFactory
和相关接口(例如 BeanFactoryAware
,InitializingBean
,DisposableBean
)是其他框架组件的重要集成点。通过不需要任何注释,甚至不需要反射,它们可以在容器及其组件之间进行非常有效的交互。应用程序级 Bean 可以使用相同的回调接口,但通常更喜欢通过注释或通过编程配置进行声明式依赖注入。
请注意,核心 BeanFactory
API 级别及其 DefaultListableBeanFactory
实现不对配置格式或要使用的任何组件注释进行假设。所有这些风味都是通过扩展(例如 XmlBeanDefinitionReader
和 AutowiredAnnotationBeanPostProcessor
)引入的,并以核心元数据表示形式对共享 BeanDefinition
对象进行操作。这就是使 Spring 的容器如此灵活和可扩展的本质。
BeanFactory
还是 ApplicationContext
?本节说明 BeanFactory
和 ApplicationContext
容器级别之间的区别以及对引导的影响。
除非有充分的理由,否则应使用 ApplicationContext
,除非将 GenericApplicationContext
及其子类 AnnotationConfigApplicationContext
作为自定义引导的常见实现,否则应使用 ApplicationContext
。这些是用于所有常见目的的 Spring 核心容器的主要入口点:加载配置文件,触发类路径扫描,以编程方式注册 Bean 定义和带注释的类,以及(从 5.0 版本开始)注册功能性 Bean 定义。
因为 ApplicationContext
包含 BeanFactory
的所有功能,所以通常建议在普通 BeanFactory
上使用,除非需要完全控制 Bean 处理的方案。在 ApplicationContext
(例如 GenericApplicationContext
实现)中,按照约定(即,按 Bean 名称或 Bean 类型(尤其是后处理器))检测到几种 Bean,而普通的 DefaultListableBeanFactory
不知道任何特殊的 Bean。
对于许多扩展的容器功能,例如注释处理和 AOP 代理,BeanPostProcessor
扩展点是必不可少的。如果仅使用普通的 DefaultListableBeanFactory
,则默认情况下不会检测到此类后处理器并将其激活。这种情况可能会造成混淆,因为您的 bean 配置实际上并没有错。而是在这种情况下,需要通过其他设置完全引导容器。
下表列出了 BeanFactory
和 ApplicationContext
接口和实现所提供的功能。
特征 | BeanFactory | ApplicationContext |
---|---|---|
Bean 实例化/接线 | 是 | 是 |
集成生命周期管理 | 没有 | 是 |
自动 BeanPostProcessor 注册 | 没有 | 是 |
自动 BeanFactoryPostProcessor 注册 | 没有 | 是 |
方便的 MessageSource 访问(用于国际化) | 没有 | 是 |
内置 ApplicationEvent 发布机制 | 没有 | 是 |
要向 DefaultListableBeanFactory
显式注册 Bean 后处理器,需要以编程方式调用 addBeanPostProcessor
,如以下示例所示:
1 | DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); |
要将 BeanFactoryPostProcessor
应用于普通的 DefaultListableBeanFactory
,您需要调用其 postProcessBeanFactory
方法,如以下示例所示:
1 | DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); |
在这两种情况下,显式的注册步骤都是不方便的,这就是为什么在 Spring 支持的应用程序中,各种 ApplicationContext
变量比普通的 DefaultListableBeanFactory
更为可取的原因,尤其是在典型企业设置中依赖 BeanFactoryPostProcessor
和 BeanPostProcessor
实例来扩展容器功能时。
]]>
AnnotationConfigApplicationContext
已注册了所有常见的注释后处理器,并且可以通过配置注释(例如@EnableTransactionManagement
)在幕后引入其他处理器。 在 Spring 基于注释的配置模型的抽象级别上,bean 后处理器的概念仅是内部容器详细信息。
一些简单概念略过不讲,希望阅读这篇文章的你对 Spring 有一定了解。
本文讲第一部分——IoC 容器、bean 概述、依赖注入和 bean 的作用域。
Spring Framework 的 IoC 容器主要在 org.springframework.beans 和 org.springframework.context 这两个包里,BeanFactory 接口提供了更高级的注册机制能够管理任意类型的对象。 ApplicationContext 接口继承了 BeanFactory 接口,并添加了更易与 Spring AOP 集成的特性、消息资源管理(用于国际化)、事件发布、应用层特定上下文(例如用于 web 应用的 WebApplicationContext)。简而言之,BeanFactory 提供了注册框架和基础功能,ApplicationContext 增加了更多企业级应用的特定功能。本章重点讲 ApplicationContext,关于 BeanFactory 详见下文。
ApplicationContext 接口表示 Spring 的 IoC 容器,它负责实例化(instantiate)、配置和组装 beans。ApplicationContext 通过从 XML、注解或是代码得知哪些类需要被加载、以何种方式加载、依赖关系是什么。
Spring 提供了一些 ApplicationContext 的实现,常见的例如 ClassPathXmlApplicationContext 和 FileSystemXmlApplicationContext,都是通过 XML 配置来实现的。大多数场景下,用户代码并不需要自己实例化出一个 Spring IoC 容器,可以通过一个简单的 web.xml 便可实现。
1 | <context-param> |
下图简要展示了 Spring IoC 容器的工作原理,你的 POJOs 结合配置项元数据,这样,ApplicationContext 创建并初始化后,你就拥有了一个配置完全且可运行的系统/应用。
上文说了,Spring IoC 容器使用配置项元数据的形式配置,支持传统的 XML 格式,从 Spring 2.5 开始支持基于注解的配置项元数据,从 3.0 开始,Spring JavaConfig 提供的很多特性使你可以通过一些类来定义 beans,包括 @Configuration、 @Bean、@Import 以及 @DependsOn 等注解。
Spring 配置项包含一个或多个容器管理的 bean 定义(definition)。在 XML 中使用<beans/>
中的<bean/>
标签,代码方式则在@Configuration
注解的类中使用@Bean
注解的方法。通常我们用其定义服务层对象、DAOs、展示对象、基础设置对象(例如一些框架工厂类)等等。通常在容器中不注册细粒度的域对象,因为这通常是 DAOs 的责任,应当又业务逻辑创建和加载这些对象。不过,你可以使用 Spring 的 AspectJ 集成来注册在 IoC 容器控制之外创建的对象。详见下文。
以下是一个典型的 XML 配置项元数据。
1 |
|
See Dependencies for more information.
我们可以从各种外部资源(本地文件系统、Java CLASSPATH
等)指定 ApplicationContext 构造时使用的资源文件。官方文档此处给了一个简单的例子:
1 | ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml"); |
services.xml:
1 |
|
daos.xml:
1 |
|
从其他 XML 文件导入 beans
1 | <beans> |
namespace 也提供了 import、context 和 util 等功能,此处不再详细解释说明。
注:虽然可以使用../
这样的相对路径,但是不推荐,这可能会导致引用了应用之外的文件,例如classpath:../services.xml
ApplicationContext 是一个高级工厂,其维护了不同 beans 的注册项和他们的依赖。通过使用方法T getBean(String name, Class<T> requiredType)
你可以获取到这些 bean 的实例。
举例:
1 | // create and configure beans |
有一种最灵活的方式是GenericApplicationContext
结合 reader 委托,以 XmlBeanDefinitionReader 为例:
1 | GenericApplicationContext context = new GenericApplicationContext(); |
你可以混合使用不同的 reader 委托到同一个ApplicationContext
上,以便从不同的源注册 bean。
尽管提供了 getBean 方法来获取 bean,ApplicationContext 也提供了一些其他方法来获取,但是事实上应用程序代码不应当通过这些 Spring API 来获取 bean。
一个 IoC 容器管理一个或多个 bean,这些 bean 根据你提供给容器的配置项元数据来构建。
在容器内部,这些 bean 定义以BeanDefinition
对象的形式存在,它通常包含以下元数据:
属性 | 解释 |
---|---|
Class | [实例化](<#bean\ 的实例化>) |
Name | [命名](<#bean\ 的命名>) |
Scope | 作用域 |
Constructor arguments | 依赖注入 |
Properties | 依赖注入 |
Autowiring mode | 自动注入合作项 |
Lazy initialization mode | 懒加载 |
Initialization method | 初始化回调 |
Destruction method | 销毁回调 |
除了 bean 定义说明了怎样创建一个指定的 bean,ApplicationContext 也允许注册用户在容器外创建好的对象。通过 ApplicationContext 下 BeanFactory 的getBeanFactory()
方法可以拿到 BeanFactory 的默认 DefaultListableBeanFactory 实现。DefaultListableBeanFactory 支持通过registerSingleton(...)
和registerBeanDefinition(..)
方法注册 bean。当然,通常我们只使用常规 bean 定义元数据。
注:我们应当尽早注册 bean 元数据和人工指定的单例,这样才能保证容器实例化时正确注入他们。尽管某种程度上支持重写已经存在的元数据和单例,但是运行时注册新的 bean(同时有并行访问工厂)官方并不支持,这可能导致并行获取异常以及容器的状态异常等。
每个 bean 都有一个或多个标识符,标识符在容器内必须唯一。通常 bean 只有一个标识符,但有时有些 bean 也需要别名(aliase)。
bean 命名公约
公约使用标准 Java 公约的实例字段命名规则。bean 名称以小写字母开头,驼峰命名,例如
accountManager
、accountService
、userDao
、loginController
等。一致的命名风格可以使你的配置项更容易阅读和理解,并且如果你使用 AOP 的话,在为一系列以命名关联的 bean 配置 advice 时会很有帮助。
你可以使用 name 指定名称,alias 指定别名,此处过于简单不展开细讲。
bean 定义本质上是如何创建一个或多个对象的方法的定义,当容器需要拿一个 bean 时它会寻找这个定义并使用定义里保存的配置项元数据创建(或获取)一个实际的对象。通常情况下,bean 需要指定 class(除非工厂方法和 bean 定义继承)。我们通常有两种方式指定 class:
内部类名
如果你想配置一个静态内部类的 bean,你需要使用内部类的二进制名称;
比如在 com.example 包中有一个 SomeThing 类,SomeThing 类下有一个 OtherThing 静态内部类,那么这个 bean 定义的 class 应当是
com.example.SomeThing$OtherThing
。使用$
符号分隔内外的类名。
当我们通过构造方法创建 bean 时,所有普通类都可以使用并与 Spring 兼容。也就是说,开发的类不需要实现任何特定接口或以特定方式编码。 只需指定 bean 类就足够了。但是,根据为该特定 bean 使用的 IoC 类型,我们可能需要一个默认无参构造函数。
Spring IoC 容器几乎可以管理你希望它管理的任何类。 它不仅可以管理真正的 JavaBeans。大多数情况下我们更喜欢实际只有一个默认无参构造函数的 JavaBeans,提供一些适当的 setter 和 getter。当然你也可以在容器中指定其他外部的非 bean 风格的类。例如,如果你需要使用完全不符合 JavaBean 规范的旧连接池,Spring 也可以对其进行管理。
至于为构造函数提供参数的机制以及如何在构造对象后设置对象实例属性的,详见下文的依赖注入。
定义使用静态工厂方法创建的 bean 时,需要使用 class 属性指定包含静态工厂方法的类并使用 factory-method 指定对应的工厂方法。此工厂方法(可以有参数)会创建一个对象,容器会把它当做构造函数创建的对象。这种 bean 定义的一个用途是在老式代码中调用静态工厂。
举例:
1 | <bean id="clientService" |
1 | public class ClientService { |
与通过静态工厂方法实例化类似,使用实例工厂方法进行实例化会从容器调用现有 bean 的非静态方法来创建新 bean。 要使用此机制,请将 class 属性保留为空,并在 factory-bean 属性中指定当前(或父级或祖先)容器中 bean 的名称,该容器包含要调用以创建对象的实例方法。 使用 factory-method 属性设置工厂方法本身的名称。 以下示例显示如何配置此类 bean:
1 | <!-- the factory bean, which contains a method called createInstance() --> |
1 | public class DefaultServiceLocator { |
当然,一个工厂类可以有多个工厂方法。
注:在 Spring 文档中,“工厂 bean”是指在 Spring 容器中配置并通过实例或静态工厂方法创建对象的 bean。 相比之下,FactoryBean(注意大小写)是指特定于 Spring 的 FactoryBean。
典型的企业应用程序不会只包含单个对象(或 Spring 说法中的 bean)。即使是最简单的应用程序也有一些对象可以协同工作,以呈现最终用户所看到的连贯应用程序。这节讲如何定义多个独立的 bean 定义,以及对象协作实现目标的完全实现的应用程序。
依赖注入主要有两种方式:基于构造函数和[基于 Setter](<#基于\ Setter\ 的依赖注入>)。
基于构造函数的 DI 由容器调用具有多个参数的构造函数来完成,每个参数表示一个依赖项。这跟调用具有相同参数的静态工厂方法来构造 bean 是等效的,下面的解释同样适用于静态工厂方法。举个简单的 POJO 的例子:
1 | public class SimpleMovieLister { |
容器会通过参数的类型与 bean 进行匹配,如果 bean 定义的构造函数参数中不存在歧义,那么在 bean 实例化时,这些参数按照定义中构造函数参数的顺序提供给对应的构造函数。
例如:
1 | package x.y; |
假如 ThingTwo 和 ThingThree 类没有继承上的关联,也就是不存在歧义,那么以下的配置就可以了,不需要在 <constructor-arg/>
元素中显式指定构造函数参数下标或类型。
1 | <beans> |
当使用简单类型时,例如 <value>true</value>
,Spring 无法确定值的类型,因此无法在没有帮助的情况下按类型进行匹配。例如:
1 | package examples; |
为解决前面的问题,需要使用 type 属性显式指定构造函数参数的类型,如下:
1 | <bean id="exampleBean" class="examples.ExampleBean"> |
可以使用 index 属性显式指定构造函数参数的下标:
1 | <bean id="exampleBean" class="examples.ExampleBean"> |
除了解决多个简单值的歧义之外,指定索引还可以解决构造函数具有相同类型的多参数的歧义。
注:下标从 0 开始。
还可以使用构造函数参数名称进行值消歧:
1 | <bean id="exampleBean" class="examples.ExampleBean"> |
但这种方式只在 debug 模式下有效(release 模式会改变参数名),或者使用 @ConstructorProperties
注解指定参数名:
1 | package examples; |
基于 Setter 的依赖注入指在调用无参数构造函数或无参静态工厂方法来实例化 bean 之后,容器调用 setter 方法注入 bean。
1 | public class SimpleMovieLister { |
ApplicationContext
不仅支持基于构造函数和基于 Setter 的依赖注入,甚至支持两者同时使用。如果使用了 Setter 注意添加 @Required 注解使得 bean 必须被注入。
Spring 团队通常提倡构造函数注入,因为它允许我们将应用程序组件实现为不可变对象,并确保所需的依赖项不是 null。值得一提的是,如果构造函数有太多的参数参数说明该类可能有太多的责任,最好拆分责任以更好地分离关注点。
Setter 注入应主要仅用于可在类中指定合理默认值的可选依赖项。否则,必须在代码使用依赖项的任何位置执行非空检查。setter 注入的一个好处是 setter 方法使该类的对象可以在以后重新配置或重新注入。JMX 最常用这种模式。
容器执行 bean 依赖性解析,如下所示:
ApplicationContext
创建和初始化所有 bean 的配置项元数据。配置项元数据可以由 XML,Java 代码或注解指定。Spring 容器在创建容器时验证每个 bean 的配置,但是直到实际创建 bean 的时候才会设置这些 bean 属性。创建时容器会创建单例作用域且需要预加载(默认)的 Bean,其他的仅在请求时才创建 bean。创建 bean 时可能会构造一张 bean 的图,因为 bean 的依赖及其依赖的依赖(等等)都需要创建和分配。这些依赖项之间的不匹配可能较晚才会被发现 —— 也就是第一次创建受影响的 bean 时。
循环依赖
使用构造函数注入时可能出现无法解析循环依赖关系的情况。
例如:类 A 通过构造函数注入类 B 的实例,而类 B 通过构造函数注入类 A 的实例。如果将 A 类和 B 类的 bean 配置为相互注入,则 Spring IoC 容器会在运行时检测到此循环引用,并抛出一个 BeanCurrentlyInCreationException。
一种可能的解决方案是改用 setter 方式注入。换句话说,尽管不推荐,但可以使用 setter 注入配置循环依赖项。
与典型情况(没有循环依赖)不同,bean A 和 bean B 之间的循环依赖强制其中一个 bean 在完全初始化之前被注入另一个 bean(类似鸡与蛋的场景)。
你通常可以相信 Spring 没有问题。容器会在加载时检测配置问题,例如对不存在的 bean 和循环依赖关系的引用。当实际创建 bean 时,Spring 会尽可能晚地设置属性并解析依赖关系。这意味着如果创建该对象或其中一个依赖项时出现问题(比如 bean 因缺失或无效属性而抛出异常)那么在请求对象时,正确加载的 Spring 容器才会抛出异常。这可能会导致无法及时暴露一些配置的问题,这就是默认情况下 ApplicationContext 需要预加载单例 bean 的原因。以一些前期时间和内存为代价,ApplicationContext 会在创建时就发现配置问题。当然我们也可以指定 bean 懒加载来代替预加载。
如果不存在循环依赖关系,当一个或多个协作 bean 被注入依赖 bean 时,每个协作 bean 在被注入依赖 bean 之前被初始化。这意味着,如果 bean A 依赖于 bean B,那么 Spring IoC 容器在调用 bean A 上的 setter 方法之前需要初始化 bean B。也就是说,bean 会被实例化(如果它不是预先实例化的单例),设置依赖项,相关的生命周期方法(如配置的 init 方法 或 InitializingBean 回调方法)也会被调用。静态工厂方法也是一样。
bean 属性和构造函数参数可以是其他 bean 的引用,也可以是内联定义的值,因此 Spring 的 XML 配置 <property/>
和 <constructor-arg/>
支持以下类型。
<property />
元素的 value 属性将属性或构造函数参数指定为人类可读的字符串表示形式。 Spring 的转换服务用于将这些值从 String 转换为属性或参数的实际类型。 以下示例显示了要设置的各种值:
1 | <bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> |
以下示例使用 p-namespace 进行更简洁的 XML 配置:
1 | <beans xmlns="http://www.springframework.org/schema/beans" |
也可以配置 java.util.Properties 实例,如下所示:
1 | <bean id="mappings" |
Spring 容器通过使用 JavaBeans 机制将 <value/>
元素内的文本转换为 java.util.Properties 实例 PropertyEditor。这是一个很好的快捷方式,也是 Spring 团队支持 <value/>
在 value 属性样式上使用嵌套元素的少数几个地方之一。
idref 标签只是一种防错方法,可以将容器中另一个 bean 的 id(字符串值 - 而不是引用)传递给 <constructor-arg/>
或 <property/>
标签。如下:
1 | <bean id="theTargetBean" class="..."/> |
前面的 bean 定义代码段与以下代码段完全等效(在运行时):
1 | <bean id="theTargetBean" class="..." /> |
第一种形式优于第二种形式,因为使用 idref 标记允许容器在部署时验证引用的命名 bean 实际存在。 第二种形式不会对传递给客户端 bean 的 targetName 属性的值执行验证,只有当客户端 bean 实际被实例化时才会发现错误(很可能是致命的错误)。 如果客户端 bean 是原型(prototype) bean,那就只能在部署容器后很长时间才能发现错误并抛出异常。
注:4.0 beans XSD 中不再支持 idref 元素的 local 属性,因为它不再提供常规 bean 引用的值。 升级到 4.0 架构时,需要将现有的 idref 本地引用更改为 idref bean。
bean 属性的值需要和目标 bean 的 id 或 name 相同:
1 | <ref bean="someBean"/> |
通过 parent 属性指定目标 bean 会创建对当前容器的父容器中的 bean 的引用。 parent 属性的值可以与目标 bean 的 id 属性或目标 bean 的 name 属性中的值之一相同。 目标 bean 必须位于当前 bean 的父容器中。 我们最好使用此 bean 引用方式,主要是当我们有容器层次结构并且希望将现有 bean 包装在父容器中时,该容器具有与父 bean 同名的代理。 以下显示了如何使用父属性:
1 | <!-- in the parent context --> |
1 | <!-- in the child (descendant) context --> |
注:4.0 beans XSD 不再支持 ref 元素的 local 属性,因为它不再提供常规 bean 引用的值。升级到 4.0 架构时,将现有的 ref 本地引用更改为 ref bean。
直接看例子:
1 | <bean id="outer" class="..."> |
<list/>
,<set/>
,<map/>
和<props/>
元素分别对应 Java Collection 类型 List,Set,Map 和 Properties 的属性和参数。 如下:
1 | <bean id="moreComplexObject" class="example.ComplexObject"> |
map 的键和值、set 的值也可以是以下类型:
1 | bean | ref | idref | list | set | map | props | value | null |
Spring 容器还支持合并集合。你可以定义父<list/>
,<map/>
,<set/>
或<props/>
元素,然后定义子<list/>
,<map/>
,<set/>
或<props/>
元素继承和覆盖父集合中的值。 也就是说,子集合的值是合并父集合和子集合的元素的结果,相同时子集合的元素覆盖父集合中指定的值。
1 | <beans> |
得到
1 | administrator=administrator@example.com |
<list/>
,<map/>
和<set/>
集合同样会合并。 使用<list/>
元素的时候,list 是有序的,且 父级的值在所有子级列表的值之前。 对于 Map,Set 和 Properties 集合类型,不存在排序。 因此,对于作为容器在内部使用的关联 Map,Set 和 Properties 实现类型的基础的集合类型,顺序是没有意义的。
我们无法合并不同的集合类型(例如 Map 和 List)。 如果不小心这么做了,则会抛出相应的异常。必须在较低的继承子定义上指定 merge 属性。 在父集合定义上指定 merge 属性是多余的,并且不会导致所需的合并。
通过在 Java 5 中引入泛型类型,我们可以使用强类型集合。也就是说,可以声明一种 Collection 类型,使得它只能包含(例如)String 元素。如果使用 Spring 将强类型依赖注入 Collection 到 bean 中,则可以利用 Spring 的类型转换支持,以便强类型 Collection 实例的元素在添加到之前转换为适当的 Collection 类。以下 Java 类和 bean 定义显示了如何执行此操作:
1 | public class SomeClass { |
1 | <beans> |
当为注入准备 bean 的 accounts 属性时,通过反射可获得 something 关于强类型的元素类型的泛型信息 Map<String, Float>。因此,Spring 的类型转换基础结构将各种值元素识别为类型 Float,并将字符串值(9.99, 2.75 和 3.99)转换为实际 Float 类型。
1 | <bean class="ExampleBean"> |
等价于
1 | exampleBean.setEmail(""); |
用 <null/>
标签标示 null:
1 | <bean class="ExampleBean"> |
等价于
1 | exampleBean.setEmail(null); |
举例 1:
1 | <bean name="classic" class="com.example.ExampleBean"> |
等价于
1 | <bean name="p-namespace" class="com.example.ExampleBean" |
举例 2:
1 | <bean name="john-classic" class="com.example.Person"> |
等价于
1 | <bean name="john-modern" |
Spring 3.1 中引入的 c-namespace 允许使用内联属性来配置构造函数参数,无需嵌套 constructor-arg 标签。
1 | <beans xmlns="http://www.springframework.org/schema/beans" |
对于构造函数参数名称不可用的情况(例如非 debug 模式),也可以使用参数索引,如下所示:
1 | <!-- c-namespace index declaration --> |
注:由于 XML 语法,索引表示法要求存在前导 _
,因为 XML 属性名称不能以数字开头(即使某些 IDE 允许)。对于<constructor-arg>
元素也可以使用相应的索引符号,但不常用,因为通常的声明顺序通常就足够了。\
设置 bean 属性时,可以使用复合或嵌套属性名称,只要除最终属性名称之外的路径的所有组件都不是 null,例如:
1 | <bean id="something" class="things.ThingOne"> |
该 somethingbean 具有一个 fred 属性,该属性具有属性,该 bob 属性具有 sammy 属性,并且最终 sammy 属性的值设置为 123。在构造 bean 之后,fred 属性 something 和 bob 属性 fred 不得为 null,否则会抛出 NullPointerException。
1 | <bean id="beanOne" class="ExampleBean" depends-on="manager"/> |
那么 beanOne 会依赖 manager,只有 manager 初始化完成才会构建 beanOne
1 | <bean id="lazy" class="com.something.ExpensiveToCreateBean" lazy-init="true"/> |
ApplicationContext 启动时不会初始化 lazy 的 bean。
但是,当延迟初始化的 bean 是未进行延迟初始化的单例 bean 的依赖项时,ApplicationContext 会在启动时创建延迟初始化的 bean,因为它必须满足单例的依赖关系。
Spring 容器可以自动连接协作 bean 之间的关系。 我们可以通过检查 ApplicationContext 的内容让 Spring 自动为 bean 解析协作者(其他 bean)。 自动装配具有以下优点:
自动装配可以显着减少指定属性或构造函数参数的需要。
自动装配可以随着对象的发展更新配置。例如,如果需要向类添加依赖项,则可以自动满足该依赖项,而无需修改配置。因此,自动装配在开发期间尤其有用,而不会在代码库变得更稳定时否定切换到显式布线的选项。
使用基于 XML 的配置元数据(请参阅依赖注入)时,可以使用<bean/>
元素的 autowire 属性为 bean 定义指定 autowire 模式。自动装配功能有四种模式。 下表描述了四种自动装配模式:
模式 | 解释 |
---|---|
no | (默认)无自动装配。Bean 引用必须由 ref 元素定义。不建议对较大的部署更改默认设置,因为明确指定协作者可以提供更好的控制和清晰度。在某种程度上,它记录了系统的结构。 |
byName | 按属性名称自动装配。Spring 查找与需要自动装配的属性同名的 bean。例如,如果 bean 定义按名称设置为 autowire 并且它包含一个 master 属性(即,它有一个 setMaster(..)方法),则 Spring 会查找名为 bean 的定义 master 并使用它来设置属性。 |
byType | 如果容器中只存在一个属性类型的 bean,则允许属性自动装配。如果存在多个,则抛出致命异常,且可能不会根据类型对该 bean 使用自动装配。如果没有匹配的 bean,则不会发生任何事情(该属性未设置)。 |
constructor | 类似 byType 但适用于构造函数参数。如果容器中没有构造函数参数类型的一个 bean,则会引发致命错误。 |
显式依赖项 property 和 constructor-arg 设置始终覆盖自动装配,且不能自动装配简单属性,例如基元 Strings 和 Classes(以及此类简单属性的数组)。这种限制是设计决定的。
自动装配不如显式布线精确。虽然如前面的表中所述,但 Spring 会谨慎地避免在可能产生意外结果的模糊性的情况下进行猜测。我们不再明确清楚 Spring 管理对象之间的关系。
可能无法为可能从 Spring 容器生成文档的工具提供装配信息。
容器中的多个 bean 定义可以匹配 setter 方法或构造函数参数指定的类型以进行自动装配。对于数组,集合或 Map 实例,这不一定是个问题。但是,对于期望单个值的依赖关系,这种模糊性不是任意解决的。如果没有可用的唯一 bean 定义,则抛出异常。
在这种情况下,你有一些选择:
放弃自动装配,支持显式装载。
通过将其 autowire-candidate 属性设置为 bean,可以避免对 bean 定义进行自动装配 false,如下一节所述。
通过将其 <bean/>
元素的 primary 属性设置为 true,将单个 bean 定义指定为主要候选者。
实现基于注释的配置可用的更细粒度的控件,如基于注释的容器配置。
在每个 bean 的基础上,我们可以从自动装配中排除 bean。在 Spring 的 XML 格式中,将元素的 autowire-candidate 属性设置<bean/>
为 false。容器使特定的 bean 定义对自动装配基础结构不可用(包括@Autowired 等注解配置)。
我们还可以根据与 bean 名称的模式匹配来限制 autowire 候选者。 <beans/>
元素在其 default-autowire-candidates 属性中接受一个或多个模式。 例如,要将 autowire 候选状态限制为名称以 Repository 结尾的任何 bean,请提供值* Repository。 要提供多个模式,请在逗号分隔的列表中定义它们。 bean 定义的 autowire-candidate 属性的显式值 true 或 false 始终优先。 对于此类 bean,模式匹配规则不适用。
这些技术对于永远不希望通过自动装配注入其他 bean 的 bean 非常有用。这并不意味着排除的 bean 本身不能使用自动装配进行配置。相反,bean 本身不是自动装配其他 bean 的候选者。
在大多数应用程序场景中,容器中的大多数 bean 都是单例。当单例 bean 需要与另一个单例 bean 协作或非单例 bean 需要与另一个非单例 bean 协作时,通常通过将一个 bean 定义为另一个 bean 的属性来处理依赖关系。但是当 bean 生命周期不同时会出现问题。假设单例 bean A 需要使用非单例(原型)bean B,可能是在 A 上的每个方法都调用。但是容器只创建一次单例 bean A,因此只有一次机会来设置属性。每次需要时,容器都不能为 bean A 提供 bean B 的新实例。
有一种不太好解决方案是放弃一些 IoC,你可以通过实现 ApplicationContextAware 接口使 bean A 意识到容器,每次通过一个 getBean("B")
调用来请求一个新的 bean B 实例,就像下面这样:
1 | // a class that uses a stateful Command-style class to perform some processing |
但是这么做会使得业务代码知道并耦合到 Spring Framework,正确的做法应当是使用方法注入。方法注入是 Spring IoC 容器的一个高级功能,可以让开发者干净地处理这个用例。
查找方法注入是指容器可以重写容器管理的 bean 上的方法并返回查找结果给容器中另一个命名 bean。查找通常涉及原型 bean,如上一节中描述的场景。Spring Framework 通过使用 CGLIB 库中的字节码生成来动态生成重写该方法的子类来实现此方法注入。
- 为了使这个动态子类生效,Spring bean 容器子类不能是
final
的类,要覆盖的方法也不能是final
。- 对具有抽象方法的类进行单元测试需要自己对类进行子类化,并提供抽象方法的桩实现。
- 组件扫描也需要具体的方法,这需要具体的类来获取。
- 另一个关键限制是查找方法不适用于工厂方法,特别是配置类中的
@Bean
方法,因为在这种情况下,容器不负责创建实例,因此无法在运行时生成子类。
对于之前的 CommandManager,Spring 容器需要动态地覆盖 createCommand() 方法的实现:
1 | package fiona.apple; |
在这个例子中,要注入的方法需要以下形式的签名:
1 | <public|protected> [abstract] <return-type> theMethodName(no-arguments); |
我们进行如下的配置:
1 | <!-- a stateful bean deployed as a prototype (non-singleton) --> |
这样,myCommand 会被动态注入。也可以使用 @Lookup
注解:
1 | public abstract class CommandManager { |
或者直接根据返回值推断:
1 | public abstract class CommandManager { |
请注意,我们通常应该使用具体的桩实现来声明这种带注释的查找方法,以使它们与 Spring 的组件扫描规则兼容,默认情况下抽象类被忽略。此限制不适用于显式注册或显式导入的 bean 类。(这段话我没看懂)
另一种解决方案是使用 ObjectFactory / Provider 注入点,详见下文。
与查找方法注入相比,一种不太有用的方法注入形式是能够使用另一个方法实现替换托管 bean 中的任意方法。这部分不重要,跳过也没关系。
使用基于 XML 的配置元数据,我们可以使用 replaced-method 元素将已存在的方法实现替换为已部署的 bean。比如下面的例子,我们想覆盖它的 computeValue 方法:
1 | public class MyValueCalculator { |
实现 org.springframework.beans.factory.support.MethodReplacer 接口的类提供了新的方法定义:
1 | /** |
部署原始类并指定方法覆盖的 bean 定义类:
1 | <bean id="myValueCalculator" class="x.y.z.MyValueCalculator"> |
可以使用 <arg-type/>
元素中的一个或多个元素 <replaced-method/>
来指示被覆盖的方法的方法签名。仅当方法重载且类中存在多个变体时,才需要参数的签名。为方便起见,参数的类型字符串可以是完全限定类型名称的子字符串。
比如java.lang.String
、String
、Str
都可以匹配java.lang.String
。
Spring Framework 支持六种作用域,其中后四种作用域仅在使用 Web 感知的 ApplicationContext 才可用。
作用域 | 描述 |
---|---|
单例 | (默认)将单个 bean 定义范围限定为每个 Spring IoC 容器的单个对象实例 |
原型(prototype) | 每次都创建新的对象 |
请求(request) | 作用域为单个 HTTP 请求,也就是说,每个 HTTP 请求都有自己的 bean 实例 |
会话(session) | 作用域为单个 HTTP Session |
应用(application) | 作用域为整个 ServletContext |
websocket | 作用域为单个 WebSocket 期间 |
前两个比较简单,不再详细解释。
值得一提的是,使用具有依赖于原型 bean 的单例作用域 bean 时,依赖项会在单例 bean 初始化的时候被注入,此后拿到的永远是同一个原型 bean 而并未创建新的。如果需要每次都能在运行时注入新的原型 bean,参见上文方法注入。
仅当使用 Web 感知的 Spring ApplicationContext 实现(例如 XmlWebApplicationContext)时,Request、Session、Application 和 websocket 作用域才可用。如果将这些作用域与常规的 Spring IoC 容器(例如 ClassPathXmlApplicationContext)一起使用,则会引发由于未知 bean 作用域的 IllegalStateException。
要在请求,会话,应用程序和 websocket 级别(统称 Web 作用域)支持 bean 的作用域,在定义 bean 之前需要做一些初始配置。(单例和原型这两种标准范围不需要此初始设置)
如何初始设置取决于具体的 Servlet 环境。
如果在 Spring Web MVC 中访问 scoped bean,实际上是在 Spring DispatcherServlet 处理的请求中,则无需进行特殊设置。 DispatcherServlet 已经公开了所有相关状态。
如果使用 Servlet 2.5 的 Web 容器,并且在 Spring 的 DispatcherServlet 之外处理请求(例如,使用 JSF 或 Struts 时),则需要注册 org.springframework.web.context.request.RequestContextListener 的 ServletRequestListener。对于 Servlet 3.0+,可以使用该 WebApplicationInitializer 接口以编程方式完成。或者对于旧容器可以添加以下声明到 web.xml:
1 | <web-app> |
如果不方便设置 Listener,也可以使用 RequestContextFilter:
1 | <web-app> |
DispatcherServlet,RequestContextListener 和 RequestContextFilter 都可以做同样的事情,也就是将 HTTP 请求对象绑定到为该请求提供服务的 Thread。这使得请求和会话范围的 bean 可以在调用链的下游进一步使用。
1 | <bean id="loginAction" class="com.something.LoginAction" scope="request"/> |
也可以使用 @RequestScope
注解。
Spring IoC 容器不仅管理对象(bean)的实例化,还管理协作者(或依赖关系)的连接。如果要将(例如)HTTP 请求范围的 bean 注入到寿命较长范围的另一个 bean 中,我们可以选择注入 AOP 代理来代替范围内的 bean。也就是说,需要注入一个代理对象,该对象公开与范围对象相同的公共接口,但也可以从相关范围(例如 HTTP 请求)中检索真实目标对象,并将方法调用委托给真实对象。
具体的操作涉及到 AOP,之后详细解释。
注:CGLIB 只拦截公共方法的调用。
bean 的作用域机制是可扩展的。我们可以定义自己的范围,甚至可以重新定义现有的作用域,尽管后者被认为是不好的做法且我们也无法重写内置的 singleton 和 prototype 范围。
要将自定义作用域集成到 Spring 容器中,需要实现 org.springframework.beans.factory.config.Scope 接口,本节将对此进行介绍。 有关如何实现自己的作用域的想法,可以参阅 Spring Framework 和 Scope javadoc 提供的 Scope 实现,它解释了需要更详细地实现的方法。
Scope 接口有四个方法可以从作用域获取、删除对象,以及销毁对象。
1 | Object get(String name, ObjectFactory objectFactory); |
最后一个方法可以获取作用域下的对话(conversation) ID。
不同的作用域下的 id 互不相同,同一个作用域下使用同一个 ID。
在写好一个或多个自定义 Scope 实现之后,我们需要让 Spring 容器知道这个新的作用域。这个方法是使用 Spring 容器注册新 Scope 的核心方法:
1 | void registerScope(String scopeName, Scope scope); |
此方法在 ConfigurableBeanFactory 接口上声明,该接口可通过 Spring 随附的大多数具体 ApplicationContext 实现上的 BeanFactory 属性获得。
例如:
1 | Scope threadScope = new SimpleThreadScope(); |
接下来就可以使用
1 | <bean id="..." class="..." scope="thread"> |
此外也可以使用 CustomScopeConfigurer 来注册一个 Scope:
1 |
|
注:在 FactoryBean 实现中放置 <aop:scoped-proxy/>
时,被限制作用域的工厂 bean 本身,而不是从 getObject()
返回的对象。
先看一个当前使用的 jvm 配置(为方便阅读我加了换行和适当注释)
1 | /usr/local/jdk8/bin/java |
除去一些日志和路径配置,其他主要包含两种配置
1 | -XX:MetaspaceSize=256M // 初始元空间大小(也是初始的阈值,即初始的high-water-mark),达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值 |
可以看到我们目前使用的参数是 4G 内存,其中新生代指定 1G,老年代则为剩下的 3G。元数据区的大小为 256M。单线程最大 256k。
这边值得一提的是,从 JAVA 8 开始,永久代被移除出 JVM,改为元数据(metadata)区。
1 | -XX:SurvivorRatio=8 // Eden 区与 Survivor 区的大小比值 1:1:8 |
首先可以看到使用的收集器为 ParNew + CMS,这里简单介绍一下两种 GC 策略
ParNew 常被用作新生代的收集器,具体策略如下:
CMS 是目前最常用的老年代收集器,其主要步骤如下:
再回头来看这套配置:
注:此篇尚未整理完成,虽然代码已经完成,但因一些变故,日后再继续整理
Paxos 作为一套可以解决分布式场景下高容错(半数以内的机器挂掉)的共识算法被广泛接受,然而由于其难以被理解所以少有直接的实现。Raft 的本质其实是对 Paxos 协议加强后的一种实现方式,对诸多细节进行了定义,使得这套系统变得易于实现。(个人认为,其实不得不承认的是,这样的代价是降低了效率)。
Raft 类似于其他的共识算法,但它有几个独有的特征———
看不懂没关系,往下看。
首先讲一讲共识算法的一些基本概念(不仅仅是 raft 协议)。
共识算法基本讲的都是基于可复制状态机的共识算法,服务器集合中的所有状态机都可以通过相同的日志拷贝计算得到相同的状态,即使一些(不超过半数)服务器出现异常也一样能保持一致。可复制状态机是众多大型分布式系统的基础。例如包括 GFS、HDFS、RAMCloud 等通常使用分布式的可复制状态机管理 leader 选举和保存配置信息。常见的可复制状态机的例子包括 Chubby 和 ZooKeeper。
可复制状态机通常通过可复制 log 实现。每台服务器保存了相同的所有历史 command 的 log,状态机依次执行这些 command 可以得到相同的状态和输出。(类比 git)
在共识协议中通常节点服务器有三种状态——leader、candidate 和 follower。
服务器节点的角色会在三种状态间根据规则切换。
在 raft 中,follower 是完全被动的:follower 不主动发送任何请求,只对 leader 和 candidate 的请求做回应。(如果客户端向 follower 发送了 command,follower 将其重定向到 leader)只有 leader 拥有 log 的写入权限,所有的 command 由 leader 先接受,然后分发给所有 follower,不一致时可覆盖 follower 内容。下图简要展示了各角色切换的条件。
raft 采用任期制,任期用递增的整数表示。leader 由选举选出,在竞选时会有一到多个 candidate 尝试成为 leader,当 candidate 赢得竞选后,它会转变为该任期的 leader 直到新的任期。在某些情况下,投票可能无法产生大多数同意的情况(比如 3 个 candidate 每人获得了总数 1/3 节点的支持),那么很快新的任期(新的选举)会再次开始以打破局面。由此可以看出,raft 保证了每一任期至多有一个 leader。
然而作为分布式系统,不同的节点可能并不总是同时能观察到任期的更迭,甚至某些情况下某个节点可能会错过整个选举甚至整个任期,所以对于每个节点而言,都会存有一份当前任期和当前 leader。所有的节点间通信都会带上任期信息以校验传来的或自己节点的数据是否过期,并更新或通知更新。当 candidate 发现自己的 term 已经过旧则立刻回归 follower 状态。对于过旧的请求服务器直接拒绝执行。
raft 节点间通过 RPC 通信。具体的 RPC 格式在下文解释。
这些状态信息需要在每次响应 RPC 前更新到静态存储
选举后重新初始化
AppendEntries RPC
由 leader 调用以复制日志条目; 也用作心跳。
]]>本章主要看内存相关操作。
定义各种内存申请操作。
提供了三种库,一种是google的tcmalloc,一种是jemalloc,还有一种就是malloc
最简单的申请内存。加了一个size_t
的空间,记录当前申请的空间的大小。
1 | void *zmalloc(size_t size) { |
如果未提供zmalloc_size
函数,默认方法是向前取PREFIX_SIZE
拿到size
,相加得到真正的size
(在刚才的情况中并不会被调用到)
1 |
|
其中update_zmalloc_stat_alloc
作用是更新used_memory
。首先将n
用long
对齐,然后原子加used_memory
1 |
|
atomicIncr
定义如下,__atomic_add_fetch
是内建函数,原型为type __atomic_add_fetch (type *ptr, type val, int memorder)
,详见文档。
1 |
释放内存
1 | void zfree(void *ptr) { |
获取RSS(resident set size,常驻内存大小)
1 |
|
碎片率
1 | /* Fragmentation = RSS / allocated-bytes */ |
按字段名计算总字节数,字段名需要以”:”结尾,pid为-1表示当前进程。若不支持直接返回0。(利用"proc/<pid>/smaps"
或"proc/self/smaps"
信息)
1 | /* Get the sum of the specified field (converted form kb to bytes) in |
返回物理内存字节数。(声称虽然看起来丑但是已经是最干净的方式)
通过sysctl
或sysconf
查询物理内存使用
1 | size_t zmalloc_get_memory_size(void) { |
他们可以通过调用 WaitOne()
方法阻塞当前线程,直到其他线程上调用了 Set()
方法。
详细的使用方法如下所示:
1 | using System; |
运行结果如下:
1 | task1 : Before WaitOne() |
首先需要初始化一个 ManualResetEvent
或是 AutoResetEvent
。构造函数带一个参数,类型为 bool ,表示初始状态是否设置为终止。这个状态之后可以通过 Set()
和 Reset()
方法来改变。
换句话说,类似于 Event 中有一个开关,表示是否 WaitOne
时是否阻塞。
如果初始化为 true ,或是调用过 Set()
,那么这个开关就是打开的状态,当 Event 调用 WaitOne()
方法时,线程不会暂停,会继续执行下去。
而当初始化为 false ,或是调用过 Reset()
时,那么这个开关就是关闭的状态,当 Event 调用 WaitOne()
方法时线程会被阻塞,直到有其他线程通过 Set()
打开了开关。
ManualResetEvent
的 Set()
为打开,Reset()
为关闭。一旦打开,所有阻塞在 WaitOne()
的线程都会继续执行。
1 | using System; |
运行结果:
1 | task2 : Before WaitOne() |
AutoResetEvent
的 Set()
和 Reset()
与 ManualResetEvent
一致。不同的是,每有一个阻塞在 WaitOne()
的线程由于开关打开而继续执行,都会自动回弹开关。(这也就是 AutoReset 的含义)
1 | using System; |
运行结果:
1 | task1 : Before WaitOne() |
保持阻塞,无法继续执行。
原因是 task1 被唤醒的同时关闭了开关, task2 无法通过。
需要唤醒的线程只有一个时,两种没有区别。
会有多个线程同时等待,而每次只希望唤醒一个线程时,用 AutoResetEvent ,
希望一次唤醒所有线程永久可以通过时,用 ManualResetEvent 。
]]>swagger 是一套框架,作用是自动化生成 .NET 的 Web API 项目的 API 文档。
ASP.NET Core 官方提供了简单的 Swagger 使用文档:ASP.NET Web API Help Pages using Swagger
首先我们要安装 Swashbuckle.AspNetCore 的 Nuget 包
1 | Install-Package Swashbuckle.AspNetCore -Pre |
然后我们可以在 Startup.cs 中的 ConfigureServices 方法中注册 Swagger 文档生成器,这里可以定义一个或多个需要生成的文档
1 | services.AddMvc(); |
我们需要确保所有的 API 方法和非路径的参数都有明确的 Http 和 From 绑定修饰符
1 | [ ] |
注: 省略参数绑定修饰符则默认为请求 (query) 字段
在 Configure 方法中添加中间件来暴露 Swagger 生成的文档 JSON
1 | app.UseSwagger(); |
此时你可以启动应用并在 “/swagger/v1/swagger.json.” 下看到 Swagger 生成的 JSON
(可选)如果你想得到交互式的文档,可以添加 swagger-ui 中间件。需要指定 Swagger JSON 源
1 | app.UseSwaggerUI(c => |
在 “/swagger” 下可以看到这个交互式的页面
Swashbuckle 十分依赖 ApiExplorer 。ApiExplorer 是一项位于 ASP.NET Core 之上的 Metadata 层的服务。如果服务集使用的是 AddMvc 方法引导 MVC 栈的话那么会自动注册 ApiExplorer_。然而如果是用 _AddMvcCore 来自行引导 MVC stack 的话你需要手动添加 Api Explorer 服务:
1 | services.AddMvcCore() |
Swashbuckle 包含三个包:Swagger 生成器, 暴露 JSON 格式 Swagger 文档的中间件和使用这个 JSON 暴露 swagger-ui 的中间件。 你可以通过 “Swashbuckle.AspNetCore” 包一起下载这些包或根据自己的需要独立下载。详细说明如下表所示
Package | Description |
---|---|
Swashbuckle.AspNetCore.Swagger | 用一个 JSON API 暴露 SwaggerDocument 对象。在返回一个序列化的 JSON 之前,这个包需要注册一个 ISwaggerProvider 的实现用于生成 Swagger 文档 |
Swashbuckle.AspNetCore.SwaggerGen | 用于注入第一个组件需要的 ISwaggerProvider 的实现。这个特定的实现可以用你的路由(routes)、控制器(controllers)和模型(models)自动生成 Swagger 文档 |
Swashbuckle.AspNetCore.SwaggerUI | 暴露一个嵌入版本的 swagger-ui。你可以指定 ui 从哪个 API 获取Swagger JSON,然后 ui 会使用 JSON 生成交互式文档 |
Swagger JSON 默认暴露在 /swagger/{documentName}/swagger.json
路径下。在启用中间件时我们可以自行修改这个路径。自定义的路径必须包含 {documentName}
字段。
1 | app.UseSwagger(c => |
NOTE: 如果同时也使用了 SwaggerUI 中间件,那么我们还需要更新 Swagger UI 的配置:
1 | app.UseSwaggerUI(c => |
如果我们想使用当前请求的某些信息设置 Swagger metadata,那么可以注册一个过滤器。
1 | app.UseSwagger(c => |
SwaggerDocument 和当前的 HttpRequest 都会被传递至过滤器。这个方法提供了很大的灵活性。例如你可以赋值给 host
属性(如上所示),你也可以检查 session 信息或者是 Authoriation header 来验证用户权限。
Swashbuckle 默认会为所有方法生成 200
responses。如果这个方法返回一个 DTO,那么这个 DTO 将会被用来生成 HTTP responses body 的 schema,例如:
1 | [ ] |
Will produce the following response metadata:
1 | responses: { |
如果你想要指定一个状态码和/或其他 responses,或者需要返回的不是 DTO 而是 IActionResult_,你可以用 ASP.NET Core 中的 _ProducesResponseTypeAttribute 描述特定的 response,例如:
1 | [ ] |
这种写法会得到的 response metadata:
1 | responses: { |
为了增强可读性,控制器和模型可以添加 XML 文档注释,并在注册 Swashbuckle 时将这些注释包含进 Swagger JSON:
打开项目的“属性”选项,选择“生成”标签页并勾上“ XML 文档文件”。然后当你生成项目时可以它会自动生成一个包含所有 XML 注释的文件。
此时如果某个类或方法没有使用 XML 注释,那么会产生一个生成警告。如果想要去掉这个警告,在此页的“禁止显示警告”选项中添加警告码“1591”即可。
在注册 Swashbuckle 时引用 XML 注释文件来生成 Swagger JSON:
1 | services.AddSwaggerGen(c => |
方法注释应当有 summary、 remarks 和 response 标签。
1 | /// <summary> |
重新生成项目,XML 注释文件会被更新。你可以打开 Swagger JSON 页面看看这些注释是怎么映射进对应的 Swagger 文档的。
注:你也可以通过给 model 以及它们的属性添加 summary 标签的方式生成 Swagger 文档。如果你有多个 XML 注释文件(例如控制器和 model 是独立的类库),你可以多次调用 IncludeXmlComments 方法,他们会被合并进输出的 Swagger JSON.
除了 Paths_, _Operations 和 _Responses_,Swashbuckle 还提供了全局 metadata (详见 http://swagger.io/specification/#swaggerObject)。例如,你可以为你的 API、服务项、甚至是联系方式和证书信息提供一个完整的描述:
1 | c.SwaggerDoc("v1", |
使用 IntelliSense 可以看到哪些字段是可用的。
未完待更。
]]>OAuth 是一套第三方认证框架,即认证服务器和资源服务器分开,用户向认证服务器授权,资源服务器才有权使用认证服务器认证用户身份,认证通过后用户才有权使用资源服务器中的权限资源。2.0 是目前的版本号。
以下的 OAuth 代指 OAuth 2.0 框架。
目前微信、QQ、新浪微博、人人网等众多网站都支持这样的第三方登录,基本都是使用的 OAuth 框架。其中 Github 的较为标准。所以我们可以以此为例。
对于小型应用而言,希望降低使用者的门槛,无需经过复杂的注册;另一方面可以直接使用腾讯、新浪等大网站的客户资源;性能和成本上讲也省掉了自己维护大量用户数据的烦恼。
对于大型网站而言,方便自己解耦,可以更方便地扩展新的应用;也可以吸引用户粘性,让用户更多的应用关联自己的用户资源;同时这种关联信息也是日后数据挖掘的一项原料。
当然,上面的这些其实都是我瞎猜的。
自己的项目之所以要将授权服务器拆开纯粹是为了解耦,可以多应用使用相同的用户资源而不会建立多余数据库连接。
OAuth 框架定义了四种角色:
+--------+ +---------------+
| |--(A)- Authorization Request ->| Resource |
| | | Owner |
| |<-(B)-- Authorization Grant ---| |
| | +---------------+
| |
| | +---------------+
| |--(C)-- Authorization Grant -->| Authorization |
| Client | | Server |
| |<-(D)----- Access Token -------| |
| | +---------------+
| |
| | +---------------+
| |--(E)----- Access Token ------>| Resource |
| | | Server |
| |<-(F)--- Protected Resource ---| |
+--------+ +---------------+
客户端需要资源拥有者授权。授权请求可以直接对资源拥有者请求,或者更好的做法是通过授权服务器作为媒介。
客户端收到来自资源拥有者的授权许可。许可的格式通常使用以下定义的四种授权模式的一种(当然你也可以自行定义授权模式)。具体使用何种模式取决于客户端使用了哪种且授权服务器是否支持。
客户端向授权服务器请求一个令牌(token)表示授权许可。
授权服务器验证客户端并确认授权许可。如果授权许可通过,那么分配给客户端一个 token。
客户端通过 token 验证身份并从资源服务器请求资源。
资源服务器验证 token 有效性后处理请求。
有四种常见的授权模式:
这种授权模式将授权服务器作为客户端和资源拥有者的中间媒介。用户不向客户端直接授权,而是将用户指引到授权服务器,然后将得到授权码的用户定向回客户端(原文 1.3.1 中提到通过 user-agent 的方式引导,我没有太懂是什么意思,欢迎看懂的小伙伴给我解答一下)
简化模式是为使用脚本语言(例如 JavaScript)的客户端优化设计的一种授权模式。简化模式中不分配授权码,而是直接分配 token(作为资源拥有者的身份验证结果)。因为没有中间验证信息(例如授权码),所以这种授权模式称为简化模式。
在简化模式中,授权服务器并不验证客户端就直接分配了 token。在一些情况下,客户端身份可以通过重定向到返回 token 的 URI 来验证客户端。这个 token 可能会被暴露给用户或者其他用户 UA 层面的应用。
简化模式对于一些比如浏览器客户端可以提高效率,当然也会导致一些安全上的问题。
第一点是可能 token 被抓,所以必须使用 TLS(即使用 HTTPS)。
第二点是可能存在“点击挟持”(clickjacking),即使用隐形的不可见的 button 覆盖在授权页面上,让用户在不知情情况下误授权。所以本地应用应当使用外部浏览器而不是应用内的嵌入浏览器。
密码模式即直接使用账号密码(或其他验证信息)登录。只应当用于用户对客户端高度信任(例如是设备操作系统的一部分)且其他授权模式都不可用的情况。
即使使用这种模式,客户端也不应当存储账号密码,而是每次都使用验证信息去交换 token。
将客户端证书(或其他形式的客户端凭证)看做授权凭证。在已经授权的资源范围内,客户端可以被看做用户本人,拥有获取权限资源的权限。
简单来说,token 就是表示授权的一个临时证明,通常是一个字符串。这个字符串通常对客户端是不透明(opaque)的(根据我的理解,应该指的是看不见)。用户授权后,资源服务器和授权服务器才可以通过 token 来验证对某一指定域的权限。当超越已授权的权限时,可能需要提供额外的验证信息来获得权限。
token 本身就已经足够表明授权者身份。
The OAuth 2.0 Authorization Framework - Internet Engineering Task Force (IETF)
]]>需事先安装OpenSSL。
前几篇我们已经讲过,证书相当于是权威机构对个人公钥的认证,而个人持有对应的私钥并保密。那么我们如何创建证书呢?
先建目录:
1 | mkdir rootCA |
再创建序号和记录文件
1 | touch index.txt |
首先我们需要生成一个私钥:
1 | openssl genrsa -out private/ca.key 2048 |
这样生成的私钥是不加密的,当然我们也可以选择一种加密方式(可选的加密方式可以使用 openssl help
查询)
以des3为例,我们生成私钥的命令:
1 | openssl genrsa -des3 -out private/ca.key 2048 |
输入一个不少于4位的密码即可。
注:这样加密的意义是一旦私钥被复制可以不让攻击者直接就能使用私钥,然而只能拖延不能完全防止。原则上私钥被复制需要更换私钥。
所谓请求就是将自己的信息和自己需要被签名的公钥打包成的一个文件。这个请求会被提交给CA确认。确认无误后CA会用自己的私钥签名。
接下来我们需要生成一个请求,我们有两种可选方式:
1 | openssl req -x509 -key private/ca.key -out ca.crt |
接下来按照指引填入相关信息。
这样就生成了一张自签名证书。
其中 -x509
表示生成x509格式的自签名证书。(待会我们生成请求时会使用 -new
而不是 -x509
)
另外,我们可以通过 -day
参数指定过期时间, -set_serial
参数设置特定序列号。
具体可以使用 openssl req -help
命令查看详细。
至于配置文件的详细解释可以查阅官方文档。
以下只给一个样例:
1 | [ default ] |
保存为 conf/default.cnf
。
接下来在生成请求时带上配置文件即可。
1 | openssl req -x509 -key private/ca.key -out ca.crt -config conf/default.cnf |
注:此处如果不带 -key private/ca.key
参数,OpenSSL会在对应目录下创建一个新的private/ca.key文件。也就是说,你也可以选择跳过第一步直接使用配置文件的方式一条命令生成私钥及请求。
现在我们已经有了一个根证书ca.crt,从某种程度上,我们现在已经搭建出了一个根CA。
通常情况下,一级CA(即根CA)不直接签发服务器证书,而是签发二级CA。由二级CA签发服务器证书。
同搭建根CA:
1 | cd .. |
同搭建根CA:
1 | openssl genrsa -out private/ca.key 2048 |
除 -x509
被换成 -new
以外其他基本一致。
我们以方式二(使用配置文件)为例。
首先写一个配置文件。(和前一个类似,不再举例)
然后:
1 | openssl req -new -key private/ca.key -out ../rootCA/requests/secondCA.csr -config conf/default.cnf |
这一步我们需要使用根CA的身份,用根CA的私钥签发二级CA的证书
1 | cd ../rootCA |
确认后,证书签发完成。
至此,二级CA搭建完成
类似二级CA:
1 | cd .. |
1 | openssl genrsa -out private/ca.key 2048 |
写好配置文件后:
1 | openssl req -new -key private/ca.key -out ../secondCA/requests/serverCA.csr -config conf/default.cnf |
注:之后ServerCA签发的客户端证书不具备CA权限,所以注意修改 basicConstraints
1 | cd ../secondCA |
好吧都是一样的。。。
1 | cd ../serverCA |
1 | openssl genrsa -out Client.%x%/client.key 2048 |
1 | openssl req -new -key Client.%x%/client.key -out requests/client.%x%.csr -config conf/openssl.cnf |
1 | openssl ca -in requests/client.%x%.csr -out Client.%x%/client.crt -config conf/default.cnf |
为了客户端方便导入,最后需要打包成p12文件
1 | openssl pkcs12 -export -clcerts -in Client.%x%/client.crt -inkey Client.%x%/client.key -out Client.%x%/client.p12 |
写的可能有点草率,很多东西没说清楚,之后有空再完善。
]]>首先,SSL是一种Web安全机制,他的作用是加密双方通信保证双方在不安全网络上的信息私密且不被篡改。
简单来说,双方先使用公钥体制互相验证身份并协商一个密钥,然后双方使用这个密钥作为之后通信的对称密钥来加密会话。
由于协商密钥是使用公钥体制进行的,所以密钥本身是不会被其他人截获的,所以保证了之后的通信过程加密的可靠性。
而之后的会话使用对称密钥是因为每次都使用非对称密钥则开销过大,使用对称密钥可以大大节省开销。
有人说,SSL是在TCP和HTTP层之间的,也有人说,SSL层是和HTTP层并列在TCP层之上的。
我觉得应该是这样的:
记录协议将要发送的数据分块、压缩(可选)、加上消息认证代码(MAC)、加密、再加上一个SSL头,将最终的到的数据放入一个TCP段。
一个SSL记录的格式如下图所示:
这两个协议对我们开发来说关系不大。
修改密码规范协议仅一个字节,值为1,用于更新连接所使用的的密码套件。
警报协议两个字节,第一个字节表示等级,1表示警报(warning),2表示致命错误(error),第二个字节是警报信息的描述码,这里不详细列举。
这是SSL协议的重点,SSL的复杂性和安全性基本依赖于握手协议。
握手协议一共分四个阶段:
第一阶段的主要任务是协商SSL版本、加密算法、压缩算法等。具体流程如下:
客户端发出一个ClientHello,包括
那么服务器返回一个ServerHello,包括
至此,客户端服务端已经协商确认了:
第二阶段双方之间做的事主要就是,服务器向客户端发送相关信息。
一般先后发送四条消息:
第三阶段是客户端向服务器发送信息的阶段。在此之前,客户端会先检验服务端证书的有效性、参数是否可接受。
一般先后有三条消息:
此阶段完成安全连接的设置。
过程如下:
至此,握手完成,客户端和服务器可以用对称加密交换信息了。
至于主密钥的计算由于和我们关系不大,我们只需要知道是用次密钥、服务器随机数、客户端随机数这三者生成的就可以了。有兴趣的小伙伴可以自行查阅相关书籍或资料。
我们先选择任意一个HTTPS的网站(例如https://www.baidu.com/),在Chrome浏览器中用F12调出开发者工具,选择Security标签页。
点击View certificate,可以看到一张证书。
这张证书是一张服务器证书,在详细信息中我们可以看到这张证书的基本信息。
所谓HTTPS,其实就是HTTP + SSL。
SSL的详细协议在下一篇讲,目前只需知道SSL所使用的数字证书就是这里的证书。而HTTPS就是用对方的公钥加密自己的通话,与对方协商出一个安全的对称密钥和加密方式,然后将双方通话过程全部用对称密钥加密(使用对称密钥是为了节省开销,每次都非对称加密会极大加重系统负担),由于协商过程是使用了公钥机制加密的,所以对称密钥也是安全的。
数字证书就是互联网通讯中标识通讯各方身份信息的一系列数据,提供了一种在Internet上验证身份的方式,其作用类似于驾驶执照或身份证。
数字证书通常由数字证书授权中心(Certificate Authority — CA)签发,带有CA的数字签名。其中包含公开密钥拥有者信息以及公开密钥的文件。
最简单的证书包含一个公开密钥、名称以及证书授权中心的数字签名。
其实说完最简单的证书,我们就可以知道数字证书最基本的功能是什么了。
目前的证书通常遵循 ITUT X.509国际标准,证书主要包括以下信息:
认证机构签名是将主体部分hash后用认证机构私钥加密,如果能够被认证机构公钥解密就说明了证书内容没有被篡改。
验证方式如下图:
首先,PKI有两条基本原则:
所以,通常我们看到的证书都是一条证书链。通常 根CA 不签发末端的客户证书,只负责签发 CA 证书,而 CA 根据他们证书中被授予的权限,有些可以签发下级CA有些只可以签发客户证书。
每张证书都会有过期时间和一个证书撤销列表(CRL)的URL。如果一张证书过期或被写进了CRL,那么证书就会作废(这往往是由私钥泄露导致的)。
PKI(Public Key Infrastructure)全称是公钥基础设施,主要提供公钥加密和数字签名服务。PKI技术以公钥技术为基础,以数字证书为媒介,结合对称加密和非对称加密技术,将个人、组织、设备的标识信息与各自的公钥捆绑在一起。其主要目的是通过自动管理密钥和证书,为用户建立起一个安全、可信的网络运行环境,使用户可以在多种应用环境下方便地使用加密和数字签名技术,在互联网上验证用户的身份,从而保证了互联网上所传输信息的真实性、完整性、机密性和不可否认性。
目前常规的加密方式通常只有两种:对称和非对称加密。
所谓对称加密,就是指加密和解密使用的密钥是相同的,加密和解密的过程对称,所以被称为对称加密。
例如异或加密就是一种很简单的对称加密算法。(即明文用密钥加密一次成为密文,密文用同一密钥解密一次成为明文)
在对称加密算法中常用的算法有:DES、3DES、TDEA、Blowfish、RC2、RC4、RC5、IDEA、SKIPJACK、AES等。
相对的,非对称加密指加密和解密所需的密钥并不相同,用 加密密钥 加密的密文是无法再次用 加密密钥 解出明文的,只能用与加密密钥对应的 解密密钥 来得到明文。
当然,加密密钥 和 解密密钥 是相对的,他们可以互相加解密。
也就是说,在非对称加密中,有一个密钥对,其中一个用来加密的时候,还要得到另一个才能解密。
所以,在非对称加密中有两个密钥,只要自己保密两个中的一个,那么就能保证通话的安全。也就是说其中一个即使泄露也不会影响安全性,是可公开的。
于是这种体制又被称为 公钥机制 。
每个人都有两把密钥,其中一把密钥由自己保密,称为 私钥 ,另一把任何人都可以拥有,向所有人公开,称为 公钥 。
公钥体制主要能做的两件事就是加密和验证。
加密是保证对方发送给自己的数据不会被他人破解。
验证是保证自己发送给对方的数据无法被他人篡改,且自己也不可否认。
加密的实现方式是,对方用公钥加密信息并发送给自己。解密密文的私钥只有自己有,所以保证了密文的机密性。
验证的实现方式是,将自己要发送的信息用自己的私钥加密。由于自己的公钥是公开的,所以密文本身没有机密性,可被任何人解密,但是对方能否用公钥解密这一点就保证了文本的完整性和不可否认。
其他人如果想修改原文,那么修改后是无法再次用私钥进行加密的,也就无法让密文可以被公钥解密。
事实上,由于正文过长,我们往往会将信息hash后再用私钥加密,附在正文后用以验证。
好久不刷题不写题解了。想想自己也大三了。看到子楚兄说自己退役的一瞬间,突然发现自己的时间其实也只剩下一年不到了。
到现在ACM连个铜都没拿过,CCPC也是打铁。再不好好刷题再也没时间了。
跟队友开玩笑说明年要拿金。但我真的希望不是玩笑。
刷题,我生命中最美的两个字!
水题,模拟就行。
我取了个巧,把0换成-1,然后乘就可以。
1 |
|
两天最难的题了吧。一开始想的是骗分。思路是网上看来的。
首先所有的路径一定是从起点经过LCA(最小公共祖先)到达终点,先上后下(也有可能只上或只下)。
于是我们把s到t的路径拆分成s到top(向上)再由top到t(向下)。
如果把节点v深度记为d[v]的话,那么在向上的过程中 时间+深度 是定值d[s]。
同样,向下过程中 深度-时间 是定值d[t]-len(len = d[s]+d[t]-2*d[top],是路径总长度,也是总时间)。
换句话说,对于从s到top这条路径上所有的点v,如果它的观察时间w[v]满足w[v]+d[v] == d[s];同理,向下满足d[v]-w[v] == d[t]-len,那么它的答案数就要+1。
有几个点吧,第一个是LCA,第二个是树的前缀和。
LCA有人说用Tarjan,我比较菜,不会。我能接受的一种O(nlogn)的方法就是开一个f[0..N][0..logN]的数组,f[i][j]表示结点i的第2^j级父节点。这样找LCA的时候就是O(logn)级别的了。
然后说到,在给s到top或者top到t做标记的时候我们是不能遍历一个个做标记的,所以我们先O(1)的做标记。
比如s到top时,我们在s处做+1标记,在top处做-1标记,之后在求结果的时候深搜求子树和。
用两个数组(桶)up[i]、down[i]记录当前搜索路径下定值为i的路径数(为了避免负数,down数组下标需要加上一个MAXN)
然后就没有了。
1 |
|
dp,几乎算是模板题。
递推式自己看代码吧。
1 |
|
杨辉三角性质+二维前缀和。
1 |
|
一开始用的queue,结果爆常数了。一怒之下开个10^6的数组好了。
由于切的比例p是定值,所以先被切的一定比后被切的长,所以直接把原长排序、再把切下来的两端分别放进两个队列,每次取队首的比,最长的拿来切。
每回合所有的增长转化为被切的两个缩短。需要注意的是切之前要算一下原长再切。
1 |
|
状压dp。
i看成二进制,由低到高第x位表示第x只猪。0为未击中,1为击中。
先两两初始化出由这两个点确定的抛物线可以击中的所有的猪(击中就或上这一位),然后O(2^n)的dp就可以了。
有几个坑,一个是a有可能大于等于0,另一个是double由于精度的问题等于要用abs(a-b)<EXP的方式判断。
1 |
|
有 n 本书, 每本书的格式为CATEGORY 1/CATEGORY 2/..../CATEGORY n/BOOKNAME
, 现在要重新格式化这些书的格式. 第 n 个 category 前面需要有 4(n-1) 个空格, 如果这本书在第 n 个 category 上, 那么它前面要有 4n 个空格. 同一 category 里面, category 和书名都按照字典序排序, 但是 category 要排在书前面. 第一个 category 需要按照字典序排列。
1 | B/A |
1 | Case 1: |
hzy解了这题。建树并排序就可以了。
1 |
|
给出n个数p1,p2,…,pn, 你要把把这nn个数划分成若干段(每段都是连续的), 每段的代价这么计算:
从每段中选出m(不够m对的话, 选出最多的对数)对,计算每对数之间的差值,然后求平方和,代价是所有选法的平方和的最大值,记为SPD。
你要划分成最少的段,使得每段的SPD都不大于k。
1 | B/A |
1 | Case 1: |
hzy解了这题。建树并排序就可以了。
1 |
|
#
]]>微软的 ASP.NET Web Service 是一套基于XML扩展标记语言,使用Soap简单对象访问协议实现的网络数据交互服务。它使用 WSDL 来描述服务相关的接口。
ASP.NET Web Service 必须依赖于 IIS,是一种无状态的通讯协议。
从某种程度上来说,Web Service 是 WCF(Windows Communication Foundation)的子集,它支持 Web Service 的所有标准。当然它不仅仅支持 Web Service,它是微软为整合.NET平台下所有的和分布式系统有关的技术而创建的统一框架。
WCF 相对 Web Service 的优势很多,不在此一一举例。
在 .NET Core 的正式版中,大概是由于跨平台的需要,微软抛弃了 Web Service,所以我们只能用 WCF 来添加网络服务。
首先可以在这里下载安装 Visual Studio WCF Connected Service 的扩展。
其次,创建一个 .NET Core 项目(以类库项目为例)。
在项目中添加 WCF 服务:
添加对应的 Webservice.asmx,修改相关参数(其实只要改名字就好),一路 next 然后 Finish:
对应的 Web Service 就添加成功了~
.NET Core 的正式版终于发布了。.NET Core 是一个开源项目。根据官方的说法,.NET Core 是跨平台的(当然它的确是跨平台的)。
我们常说的 .NET 通常指的是.NET Framework,通常只运行于 Windows。两者的关系用官方的图来表示就是这样:
他们有一个共同的子集。
恩,据说这玩意以后还要收费?不知道是不是我理解错了。要是我理解错了求英语好的来给我解释一下。原文如下:
Finally, .NET Core will be “pay-for-play” and performant. One goal of the .NET Core effort is to make the cost of abstraction clear to developers, by implementing a pay-for-play model that makes obvious the costs that come from employing a higher-level abstraction to solve a problem. Abstractions don’t come for free, and that truth should never be hidden from developers. Additionally, .NET Core will favor performance with a standard library that minimizes allocations and the overall memory footprint of your system.
.NET Core 提供了 .NET CLI(Command Line Interface),可以通过命令行来完成程序的编译,相关命令如下:
命令 | 说明 |
---|---|
dotnet new | 使用 C# 语言初始化用于类库或控制台应用程序的有效项目。 |
dotnet restore | 还原在指定项目的 project.json 文件中定义的依赖项。依赖项通常是您在应用程序中使用的 NuGet 包。 |
dotnet build | 生成您的代码! 此命令将生成适用于您的项目的中间语言 (IL) 二进制。如果项目是控制台应用程序,则产生的输出是可执行的,您可以立即运行。默认情况下,生成命令会将生成的程序集和可执行文件(如果适用)输出到调用位置目录的 bin 目录中。 |
dotnet test | 如果不支持运行测试,则不会出现适合的工具。此命令让您可以使用在 project.json 文件中指定的运行程序运行一组测试。目前支持 xUnit 和 NUnit 测试运行程序。 |
dotnet publish | 发布在目标计算机上运行的应用程序。 |
dotnet pack | pack 命令会把您的项目打包成 NuGet 包。输出一组 nupkg 文件后,您可以将其上载至您的源,或使用本地文件夹替代将其用于还原操作。 |
dotnet run | 运行命令将编译并运行您的应用程序。您可以将其看作没有 Visual Studio 的 Ctrl+F5 模拟。 |
.NET Core 依赖 NuGet 提供的各种包。开发者也可以把自己的类库打包成 NuGet 包共享到 NuGet。
一不小心,竟然都大三了,这些天看着新生群里陆续加进来的心生们,想到自己已经大三了,不禁有点恍惚。又想起那句,去年我大一,明年我就大四了,一阵心慌。
感觉自己还很弱,感觉自己还有很多想做,感觉自己还有好多好多想要在进入社会之前完成的事情都还没用做。
真的好心慌。
时间根本不够用啊摔!
上次比完蓝桥杯是想写一份题解来着,然而奈何下笔什么都不会。好像从那之后就再也没有更新了。
期末倒是挺忙,各种整理、复习、预习,偶尔还穿插一点比赛什么的。
一不小心错过了暑期国际课程的选课。
假期来了开悦科技这边实习,写写东西,学习学习。主要是做做 .NET,挣点学费。
这些天学到的东西尽量整理出来。
7 月 9-10 日参加了一个全国大学生信息安全的 CTF 比赛,抱着队友的大腿进了决赛。只是很水的帮队友做了一道签到题,还有做了一半的破译,最后队友全转大写过掉了,也算是 2333。
其他的都是胡乱折腾了好久,其实也没有帮到太大忙。(这里有本队 WriteUp,这是官方题解)
最后还能混个 i 春秋的 VIP,其实这波也不亏。
7 月 16-17 日去打了一场中国高校计算机大赛-团体程序设计天梯赛,然叔看的起,放在了一队。然而成绩不忍直视。说真我都不好意思提。
希望自己加油吧。
]]>通常我们讨论的都是对称旅行商问题(SYMMETRIC traveling-salesman problem),即距离矩阵保持对称(A 到 B 与 B 到 A 距离相等)。
严格意义上的旅行商问题的要求是:遍历所有点,保证每个点刚好访问一次,求最短的遍历路径。
解决这种问题的算法分为两种:精确算法(Exact algorithms)和启发式方法(heuristic methods)。
精确算法的复杂度过高(n的指数级),短时间可解决的问题约在60个点(1971)。
一般采用启发式方法去解决能得到一个相对满意的结果。
解决这个问题的灵感来自于另一个问题:图分割问题。
问题描述很简单:将图分为两个点的个数相同的部分,并满足两个部分间的距离最短。
我们可以将这两个问题抽象成同一种问题:
从集合 S 中寻找满足限制条件 C 并能使目标函数 f 最小化的子集 T 。
接下来,我们就可以将启发式算法的思路总结出来:
在 TSP 问题中,这个问题的具体化就是:
这种算法好坏主要看两点:
KL算法重点关注第二个点。
第二步中有一个难题,是 k 的值具体应该取多少?
如果遍历 k 那无疑是一件很恐怖的事,所以我们尝试用一些方法:
那我们采用怎样的方法去找“最不合适”对呢?
首先,我们要保证每次交换后都能得到一个可行的交换,所以我们保证 x1、y1、x2、y2 …… 依次首尾相连(且 yn 最后一个节点为 x1 的第一个节点),如下图所示:
其次,假设 gi 表示 xi 与 yi 交换后减少的代价,那么我们不必在某一个 gi 为负数时立刻停止,我们只在 g1+g2+……+gi ≤ 0 时认为找不到“不合适”对。
当然,我们要保证不重新选取被去掉的边。
生成的最终路径:
如果回路 T 无法再次被改进,则 T 是一个局部最优解。当下次再次得到回路 T 时,避免再次检验。
可节约 30-50% 的运行时间。
为避免出现选择的 yi 使得 x(i+1) 太小最终导致无用的搜索,在选择 yi 时选择使得 |x(i+1)|-|yi| 最大的 yi。
因为很多时候,大部分重要的优化都是相同的,所以我们在找出至少两个局部最优解后,求这些最优解的交集,并以这些边作为初始回路的一部分。
数据测试发现,两个普通的局部最优解有 85% 是相似的,甚至 7-8 个最优解也有 60-80% 是相似的。
无法进行不连续交换。
e.g.:
根据初始随机解的生成方式的不同,衍生出不同的方法,其中包括:
An Effective Heuristic Algorithm for the Traveling-Salesman Problem
—— S. Lin and B. W. Kernighan
Lin-Kernighan 算法初始解的启发式构造策略
—— 曾华,崔文,付连宁,吴耀华
首先纠正上一篇中的一处错误,空间可以压缩到 O(n) ,然而同时就意味着不能得到具体序列。当不在乎具体序列时可以简单使用对 i 模2的方法压缩空间。
此文提供了一种方法在不提高复杂度的前提下压缩空间并能回求序列的方案。
将上一篇所提到的 f[0..n][0..m] 中的第一维用 i mod 2 替换,则只有0和1,然而不影响正确性。
将 A 二分,用不回求序列的压缩法求 A[0..n/2] 和 B[0..m] 的公共子序列长度,将所有的 f[(n/2)%2][0..m] 的值(共 m+1 个)保存为 L1[0..m]。
同理,将 A[n/2+1..n] 和 B[0..m] 倒序后得到长度 L2[m..0]。
找到 B 数组的中间值 k ,使得 L1[k]+L2[n-k] 取得最大值。
那么我们就知道了,用 A[0..n/2] 和 B[0..k] 匹配,A[n/2+1..n] 和 B[k+1..m] 匹配便可得到最长的公共子序列。
那我们递归处理A[0..n/2] 和 B[0..k] 以及 A[n/2+1..n] 和 B[k+1..m] 的匹配就可以得到最终字符串了。
当然,递归的最后需要特判 A 序列长度为0和1的情况。
注:ALG B指的是上文所说的 i mod 2 的压缩空间的方法。
显然空间只有O(m);
时间略微有些复杂。用了两次 不回求序列的压缩法 ,复杂度为 O(mn),然而每次递归复杂度递减为上一次的一半,求和后可以知道总复杂度只有O(2mn),常数不看,即O(mn).
A Liner Space Algorithm for Computing Maximal Common Subsequences
—— D.S. Hirschberg (Princeton University)
结掉了几门课,停掉了几门课。现在还上着的基本就是英语和计组了。
计组这些天在做 MIPS ,挺有意思的。博客的其他文章也有提到在做的这件事。
算法研讨下周一要讲DP,刚整理了一篇论文。今天晚上必须搞懂另一篇并做好 PPT。
算是结掉了一份工作,虽然似乎没有做到很好。以后的锅可能就甩给学弟了。
又是一拖再拖下来了。得抓紧时间赶工了,中期检查快到了。
这两天终于差(bing)不(mei)多(you)补完了上周的 解题报告。还差 spfa 和 莫比乌斯函数 的坑,接下来接着填。
下午会打一个很奇葩的天梯赛。下下周末可能要出去打蓝桥,可是好像和算法考试重了。
报了一个信息安全竞赛,也是这两天。真的想弃了蓝桥杯了。
万一真的去实习没法来补考岂不是很尴尬?
选课系统的万年老坑啊啊啊啊啊啊啊啊。
看样子我要接一口大锅啦啊啊啊啊啊啊啊。
好吧接下来又要开始动工了,争取学期结束前完工。
日子过得越来越充实。
祝自己越来越优秀吖~
单周期的 Mips 处理器
只支持 add、sub、and、or、lw、sw、slt、beq、j 这9条指令
采用 Verilog 语言开发
Window 10 环境下
使用了 Sublime Text + ModelSim 的方式进行开发(ModelSim 自带的编辑器真的难用一个tab竟然是8个空格啊啊啊啊啊)
具体分为两大部分和处理器部分
定义了各种元部件
模块名:alu
说明:算逻部件
输入接口:op(4位,运算符编码), a, b(32位,运算数)
输出接口:zero(结果是否为0), dout(32位,运算结果)
op的说明:
0010: dout = a + b
0110: dout = a - b
0001: dout = a | b
0000: dout = a & b
0111: dout = a < b ? 1 : 0
模块名:dm_4k
说明:数据寄存器,大小为4k(时钟上升沿触发)
输入接口:addr(10位,数据地址), din(32位,写数据时的数据端), we(写数据使能端), re(读数据使能端), clk(时钟端)
输出接口:dout(32位,读数据时的数据输出端)
模块名:ext
说明:符号扩展部件(W 表示输入数据宽度)
输入接口:din(W 位)
输出接口:dout(32位)
模块名:im_4k
说明:指令存储器,大小为4k
输入接口:addr(10位,运算符编码)
输出接口:dout(32位,对应指令)
模块名:mux2
说明:二路选择器(W 表示输入数据宽度)
输入接口:a, b(W 位,表示0和1对应的数据源), s(选择信号)
输出接口:dout(W 位,选择结果)
模块名:pc
说明:程序计数器(时钟上升沿触发)
输入接口:clk(时钟端), rst(重置信号), data(32位,下一指令地址)
输出接口:dout(32位,当前指令地址)
模块名:regheap
说明:寄存器堆(时钟上升沿触发)
输入接口:clk(时钟端), we(写寄存器使能端), rreg1(5位,读寄存器1地址), rreg2(5位,读寄存器2地址), wreg(5位,写寄存器的地址), wdata(写入寄存器的数据)
输出接口:rdata1(32位,读寄存器1的数据), rdata2(32位,读寄存器2的数据)
另:有部分为方便测试而添加的初始化寄存器的值的代码。
解析指令,生成对应的控制信号
模块名:ALUctrl
说明:算逻部件控制器
输入接口:ALUOp(2位), funct(6位,指令的5-0位)
输出接口:op(4位,对应的 alu 运算符编码)
模块名:ctrl
说明:算逻部件控制器
输入接口:op(6位,指令的31-26位)
输出接口:RegDst, RegWrite, ALUSrc, MemRead, MemWrite, MemtoReg, Jump, Branch, ALUOp(各种控制信号,其中 ALUOp 为2位)
模块名:mips
说明:单周期处理器(时钟上升沿触发)
输入接口:clk(时钟端), rst(重置信号)
模块名:testbench
说明:生成时钟信号测试部件可行性
最长公共子序列(LCS)问题是一种经典的动态规划(DP)问题。
假设两个序列为 s1 和 s2 (假设下标从 0 开始),原先我们的算法是用 f[i][j] 表示 s1[1..i] 和 s2[1..j] 的最长公共子序列长度。
那么,我们可以得到状态转移方程:
f[i][j] = max{f[i-1][j], f[i][j-1]) (i>0 && j>0 && s1[i] != s2[j])
f[i][j] = max{f[i-1][j], f[i][j-1], f[i-1][j-1] + 1} (i>0 && j>0 && s1[i] == s2[j])
f[i][j] = 0 (i==0 || j==0)
递推即可得到答案。
假设两个序列长度分别为 n 和 m ,则原算法的时间复杂度显然可见是 O(n·m) 的。
空间复杂度上由于开了一个 f[0..n][0..m] 的数组,所以复杂度也是 O(n·m) 的。
当然,由于每一维度的 f 数组只和上一维度有关,所以空间复杂度可以压缩到 O(m) 。(此处的分析有误,在下一文中对空间压缩的方法给出了具体讨论)
定义一种阈值数组 T[0..n][0..n],T[i][k] 表示在序列 s2 中匹配 s1[1..i],寻找到 k 项匹配的最小下标值。
换句话说,即满足 s1[1..i] 和 s2[1..j] 有 k 项匹配的最小 j 值。
e.g.:
s1 = “abcbdda”
s2 = “badbabd”
则 T[5,1] 表示在 s2 中找到能与 s1[1..5] 公共子序列长度为1的最小下标。显然第一个即可满足,所以 T[5,1] = 1 。
同理,T[5,2] = 3,T[5,3] = 6,T[5,4] = 7,T[5,5] = ∞ 。
显然,数组 T 在第二维上是递增的。且 T[i][k-1] < T[i+1][k] ≤ T[i][k] 。
则可得递推方程:
T[i+1][k] = min{j} (s1[i+1]==s2[j] && T[i][k-1] < j ≤ T[i][k])
T[i+1][k] = T[i][k] (不存在上述条件的 j 时)
算法正确性在此不做证明。
据此我们找到最后一列中满足 T[n][k] != ∞ 的最大 k 值即为 LCS 答案。
对应的算法伪代码:
此时的时间复杂度为O((n^2)log n),空间复杂度一样是可以压缩到 O(n)。
建立一个匹配表,事先将 s1 数组中的元素去和 s2 进行匹配,得到匹配表。
e.g.:
s1 = “abcbdda”
s2 = “badbabd”
则对应匹配表为:
MATCHLIST[1] = <5,2>
MATCHLIST[2] = <6,4,1>
MATCHLIST[3] = <>
MATCHLIST[4] = MATCHLIST[2]
MATCHLIST[5] = <7,3>
MATCHLIST[6] = MATCHLIST[5]
MATCHLIST[7] = MATCHLIST[1]
对应的算法伪代码:
时间复杂度 O((r+n)log n) ( n 表示字符串长度,r 表示两个字符串间能匹配的次数),最坏复杂度为 O(n^2 log n)。
空间复杂度 O(r+n)。
具体分析如下:
可使用带序号的排序。时间复杂度 O(nlog n),空间复杂度 O(n)。
时间:O(n)
时间:O(n + rlog n)
时间:最坏 O(n),空间:O(r)
A Fast Algorithm for Computer Longest Common Subsequences
—— James W.Hunt (Stanford University) & Thomas G.Szymanski (Princeton University)
首先去 官网 找到 ModelSim ,正版太贵又不愿用盗版,所以我们使用 Student Edition 。
找到学生版的链接:https://www.mentor.com/company/higher_ed/modelsim-student-edition,找到 Download Student Edition 下载安装包(也可以戳这里)。
记得仔细阅读页面上的 Additional Information 。
下载完成后照着 .exe 文件的步骤安装,注意:
安装路径中不可出现空格!
别问我是怎么知道的。
血的教训!
安装完成后会弹出一个窗口,如果你仔细阅读了 Additional Information 应该就知道了,我们需要在这个页面中填写个人信息和邮箱,申请一个 license 文件。
要注意有些邮箱会过滤国外信件,避免使用(比如我航)。
填写完成后,过一会(大概 5 min)你会收到一个邮件,把邮件的附件(一个 .dat 文件)复制进你的安装目录。
完成~
这是一道模拟题,翻译莫斯密码。
先是强行手打莫斯密码表(只含 ‘.’ 和 ‘-‘ )
然后一个 ‘.’ 为一个 ‘=’ ;一个 ‘-‘ 转为 ‘===’;每两个符号间加一个 ‘.’;
然后每两个字母间加’…’;
每两个单词间加’…….’;
Over
//代码过于繁琐并且没什么价值不再手打。
n 个结点二叉树( i 的子节点是 2i 和 2i+1 )
求中序遍历的第 x 个值。(1 ≤ x ≤ n ≤ 10000)
由于 n 的范围不算大,所以强行中序就好。
1 |
|
给 n (n ≤ 10)个格子涂色(颜色少于等于3种),如果考虑旋转对称可以涂多少种结果。(例如001和010和100算同一种)
由于数据量都不大(3^10 = 59049),所以考虑直接暴力。
扫一遍,如果这个数还没有被考虑过就轮换一圈做上标记,并结果数 +1 。
1 |
|
将四维的 1×1×1×2 的方块放进 2×2×4×n 四维空间(可旋转),问有多少种放置的方法。
暂时还是不会 QAQ
在 2×n 的网格里每次随机放置一个矩阵,问覆盖全图所需次数的数学期望。
同上
有 n 个人,每个人初始有 a[i](1≤i≤n) 的钱,相互间通过给钱来似的所有人钱一样多,然而每次给钱需要交倍率为 k 的税(交易额 × k ),问最终每人最多多少钱。
我们队使用的是二分。
答案 ans 应该满足 (1-k)*∑(a[i]-ans)(1≤i≤n && a[i]>ans) == ∑(ans-a[i])(1≤i≤n && ans>a[i])
1 |
|
猜数游戏。 B 想一个数(1 到 n),A 猜对了可以从 B 那里赢1刀,如果 A 猜了 x 而 B 想的是 x+1 则 A 要支付1刀给 B 。
B 使用随机数发生器,同时 B 能决定每个数字的分布。而 A 知道 B 的决定,所以 A 会挑选一个获利期望最大的选择。
B 现在要将 A 的获利最小化,问最小化的 A 的获利期望是多少。
问题模型简化为:
∑(P[i]) = 1, 求 max{ P[i]-P[i+1] , P[n] } (1 ≤ i < n) 的最小值。
记 a[i]=P[i]-P[i+1](1 ≤ i < n), a[n]=P[n]
,则有
1 | a[1] + 2·a[2] + …… + (n-1)·a[n-1] + n·a[n] = 1, |
显然取等号时有最小值,最小值为 2/(n·(n+1)).
所以输出2/(n·(n+1))即可。
给定 n 和 m ,求∑∑(i^2·j^2·gcd(i,j)) (1 ≤ i ≤ n, 1 ≤ j ≤ m).
要用到一个叫莫比乌斯函数的东西。暂时还没搞明白,搞清楚以后会在单独写一篇。
有两种公交线路,一种是每次2刀(A 类),一种是免费的(B 类)。给出所有线路和起点终点,求解出最小花费。
思路是将所有站点和线路相间建立无向图。若站点在线路上则连接站点和线路。若与 A 类线路连接,则边权为 1,若与 B 类线路连接则为 0.
然后一遍spfa过了。
代码再过两天补吧。
这题真的是有意思,人肉机器学习。
题意大概是给了一段西班牙语,一段中文拼音,然后将会给一段 100 - 500 词的文本(从报纸、杂志上摘取),判断这段文本是什么语言(英语 / 西班牙语 / 拼音)。
我们起初用一些 y、es 这样的单词来判断西班牙语;the、be 动词这些来判断英语,然而 WA 了。
队友后来开始统计词频,然而还是呵呵了。
最后的处理方式是:先查英文,出现特征词直接认定为 English;
接下来统计字长,出现长度大于6的单词认定不是中文拼音(拼音最长为6个字母);
然后很诡异的作了一步判断首字母是不是元音字母的单词比例,大于 0.8 则断定为中文拼音(也不知道是不是这个操作起了效果我觉得不是然而队友这么写了并且过了那现在就无从得知了);
接下来再用西班牙语特征词判断西班牙语;
还判断不出来就返回英文。
具体代码不写了,没什么意思。
其实现在想想这个逻辑还是有问题的。但是不管怎么说,过了、拿到分了就行。
]]>Android Studio (Eclipse)
我们这里使用 Android Studio.
Android Studio
SDK
Android Virtual Device
Gradle
此处保存主要的 app 支持的 Android 版本号。compileSdkVersion
指编译所用的 SDK 版本,一般要求高于 targetSdkVersionminSdkVersion
指支持的最小 SDK 版本targetSdkVersion
指目标SDK版本,即能保证支持的最高版本
classpath
表示使用的 gradle 版本,保存后自动下载,保持网络♂畅通。
sdk.dir
表示 SDK 的路径
以 Gradle 2.1.0 为例:
程序的各种声明
1 | <manifest xmlns:android="http://schemas.android.com/apk/res/android" |
申请权限
1 | <uses-permission android:name="android.permission.GET_ACCOUNTS" /> |
Application 基本信息
1 | <application |
声明 Activity 和 Service
放在 application 中:
1 | <activity android:name="PackageName.ActivityName" /> |
四大(五大)基本组件:
App的程序,即逻辑代码,被放在不同的包中(类似 C# 中的 namespace )
所有的 Activity 均继承于 Activity 类,所有的 Service 均继承于 Service 类。
事件驱动,通过 On动作 方法处理事件。
不同的 Activity 和 Service 之间通过 Intend 去连接。
Activity 使用 setContentView 获取 res 中对应的界面文件来设置界面。
使用 R 来获取 res 中的具体值。
存放所有资源文件
真的是捡了个大便宜吧,莫名其妙就成了 CCF 南航学生分会的主席。
一个学会的会旗竟然放在我的柜子里,想想真的是有一点不可思议。
肩上的担子重啊。
祝我这次真的能做好吧。
顺便也重新思考了一下之后的路。开发?科研?好像又开始犹豫了。
终于还是下定决心买了电池,断电后不能用电脑真的很麻烦。
这几天事情其实还不算少,下周三的动态规划又该讲了,还有周二晚上要考概率论,周末还得给大一的讲 Android 。
其他时间可能会好好复习刷题了。
祝我顺利。
]]>在一对多关系中,例如一个 Class 对多个 Student ,使用 ToList() 取出一个 List 的 Student 对象,
那么 List 中的 Student 对象外键对应的 Class 到底是各自独立,还是会共用一个对象?
那么分开取出的 Student 们对应的 Class 共用吗?
加上 AsNoTracking() 方法呢?
1 | public class Class |
1 | public class AppDbContext : DbContext |
1 | public static void Main(string[] args) |
1 | AsNoTracking: |
不加 AsNoTracking() 的情况下,所有的同一外键指向同一对象;
加 AsNoTracking() 的情况下,分开取出的同一外键指向不同的对象,通过 List 方式取出的同一外键指向相同对象。
首先,我们从 Visual Studio 2015 新建一个控制台应用程序(如图)。
我们此处以Entity Framework For Microsoft SQL Server 为例。
可以手动在 工具 ‣ NuGet 包管理器 ‣ 管理解决方案的 NuGet 包 中搜索 EntityFramework.MicrosoftSqlServer 并选择安装,也可以选择 工具 ‣ NuGet 包管理器 ‣ 程序包管理器控制台 ,通过 Install-Package 命令 手动添加:
1 | PM> Install-Package EntityFramework.MicrosoftSqlServer –Pre |
这样就安装好了 EF 7 For SQL Server 的依赖库。
需要在自己定义的 DbContext 类中重写 OnConfiguring 方法。
此处以 在 (localdb)\mssqllocaldb 上创建一个名叫 EFGetStarted.ConsoleAppFortest.DbForTest 的数据库 为例。
1 | public class AppDbContext : DbContext |
接下来有两种方案,代码创建和手动添加 Migration。
在创建完自己的 DbContext 对象后,通过调用该对象的 Database.EnsureCreated() 方法保证 Migration 被创建。
1 | public static void Main(string[] args) |
这时配置便已经完成,可以运行了。
手动添加 Migration 需要安装一个包并注册一个命令:
1 | PM> Install-Package EntityFramework.Commands –Pre |
并在 project.json 的 commands 里添加
1 | "ef": "EntityFramework.Commands" |
接着就可以在 NuGet 命令行里添加 Migration 了(新的 Migration 命名为 MyFirstMigration ):
1 | PM> Add-Migration MyFirstMigration |
我在执行 Add-Migration 的时候出现了一点意外,无法识别这一命令,于是我直接打开了 工作目录/src/项目名称/ 文件夹,在这里使用了命令行:
1 | 工作目录/src/项目名称>dnx ef migrations add MyFirstMigration |
此时就在用户文件夹下生成了对应的数据库文件。
这时,配置工作全部完成。
当下次修改了数据库结构时,需重新创建 Migration 并 Update 。
]]>斐波那契堆是一种相对松散的堆结构。它的存储结构并不是一棵树,而是一个森林,并且每棵树都是一个符合堆结构的多叉树。
它的特点是只在删除掉顶点以后整理堆的结构。并且通过做标记的形式保持堆的平衡。
直接插入对应结点,松散排列。
删除顶点后先直接将所有子节点作为一棵树直接放进堆,然后进行整理。
整理的方式是,将rank(即根节点的孩子数)相同的树合并为一棵树。
//别问我为什么没做成 gif ,因为没有 PS ,而且懒。
去掉顶点并直接将子节点作为独立的树:
开始合并操作:
将树的根节点按照rank依次放进一个指针数组:
发现已经有相同rank的根节点则进行合并:
不断合并直到rank唯一:
继续:
合并:
到这里,整个堆的根节点都放进数组,整理完成。
此过程需要用到之前定义的 mark 属性,它表示这一结点是否已经被删除过子节点。通过这一标记来尽量保证树的平衡,避免出现“链”的结构。
具体操作如下:
首先减小某一顶点的值,然后观察其是否是小于父节点:
若无父节点,则看是否要更新 min 指针;
若依然大于父节点,则不做修改;
若小于父节点,则剪断改分支,并尝试对父节点做标记;
若父节点已经有标记,则剪断父节点并递归对其父节点做标记,直到可以做标记或已经是根节点为止。
将46减小为29:
小于父节点,无需修改
将29减小为15:
小于父节点,剪断,并做标记:
24被标记:
将35减小为5:
小于父节点,剪断,并做标记:
父节点已经被标记一次了,所以剪断父节点,并对其父节点做标记:
发现其父节点也已经被标记了,所以再次剪断父节点并对其父节点做标记:
由于父节点是根节点,所以不再做标记:
符号 | 含义 |
---|---|
n | 节点数 |
rank(x) | 结点 x 的孩子数 |
rank(H) | 堆 H 的最大 rank |
trees(H) | 堆 H 中树的数量 |
marks(H) | 堆 H 中已标记的点数 |
定义一个势函数: Φ(H) = trees(H) + 2 marks(H)
Insert:
Delete Min:
O(rank(H)) + O(trees(H))
势函数变化:O(rank(H)) - trees(H)
均摊时间复杂度: O(rank(H))
Decrease Key:
O(c) (c 表示剪断次数)
势函数变化:O(1) - c
均摊时间复杂度: O(1)
可以证明,rank(H) ≤ log Φ (|V|)(其中Φ表示(1 + √5) / 2 ≈ 1.618),由于证明过程有点复杂这里不再说明。
所以最终复杂度为O( ( 1 + log Φ (|V|) )|V| + |E| )
简单点说,即将原本堆的实现形式从二叉树改为 k 叉树。
k 叉树的 Insert 和 DecreaseKey 操作的复杂度为 O(log k (V)),DeleteMin 的复杂度为 O(k log k (V)),具体的对比如下表所示:
数据结构 | Insert,DecreaseKey | DeleteMin |
---|---|---|
链表 | O(1) | O(V) |
二叉堆 | O(log (V)) | O(log (V)) |
k 叉堆 | O(log k (V)) | O(k log k (V)) |
斐波那契堆 | O(1) amortized | O(log V) |
对一个 Dijkstra 算法来说,如果图为 G(V,E),一般需要 |V| 次 Insert、|V| 次 DeleteMin 和 |E| 次 DecreaseKey,所以对二叉堆来说总复杂度为 O( (2|V|+|E|) log (V) ),而 k 叉堆为 O( ( (k+1)|V|+|E| ) log k (V) )。
通常我们 k 的值会取 |E| / |V|,那么k 叉堆复杂度就是 O( ( |V|+2|E| ) log (|E| / |V|) (V) )。
在 |E| / |V| > 2 的情况下,显然k叉堆是可以起到优化复杂度的效果的。
核心思想是一次考虑一位(bit),本质是一种按位的 Dijkstra 。
先将图按最高位(i = log2( max{ range(w) } ), i表示当前位) Dijkstra 一次,接下来每次用前 i 位的距离进行 Dijkstra 并左移一位。
因为可以反复寻找最短路径,所以保证了最终的路径最短。
好吧我放弃了。因为实在是不了解并行化编程,花了两天也实在无法吃透这篇论文。
有人后来明白了欢迎留言评论。
我算是在这留了个坑吧。【摊手】
放弃这一篇,转而去写 k 叉堆了。接下来会写两篇,一篇是 k 叉堆,一篇是斐波那契堆。
原谅我实在太弱。
Dijkstra 算法的具体细节不再赘述,本质上是一种贪心算法,通过不断添加距离源点最短的点并刷新距离来求解。
如图,通过添加点减少边的数量(子图必须是二部图)
如图,合并相似点(邻接点相同、距离均相近)
先将所有点按度数排序(压进优先队列),每次弹出一个顶点,把它的 Dijkstra 最短路的所有路径加入新图G’中。
当G’的压缩率(G’的边数/G的边数)达到某一要求值时结束循环。
讲道理,看到这里的时候我是崩溃的。这压缩能用?
边数:
随机两点间最短距离:
答案准确度(随机两点间最短路和原图的比):
和原图的 Dijkstra 运行时间比:
感觉作用不大啊。。
试图压缩稀疏图的想法并不靠谱。压缩率在0.9的时候还能接受。然而做一次压缩所需要的时间耗费太过巨大,所以并不推荐使用图压缩。
]]>前几天在 Facebook 上说自己太水,要开始写博客。
上午(其实已经是昨天上午了)的时候准备给大一的讲 git ,偶然发现 Github 可以搭网站。
傍晚的时候PM突然也用 Github 搭出了 Blog 。
那就动手咯~
照着 https://hexo.io/zh-cn/docs/ 文档;
装上 Node.js 和 Git ;
然后
1 | $ npm install -g hexo-cli |
再然后
1 | $ hexo init <folder> |
一通乱搞。
换个 theme ,改改 _config.yml ;
git 一下,再部署一下 deploy ;
跑一下
1 | $ hexo g |
然后就开始创建博客了
1 | $ hexo new "My New Post" |
看起来好像挺简单。
虽然是搭好了,但还是得好好看看 md ,毕竟以后都得用这语言写博客。
搭建这个 blog 算是一个开始吧。今后好好写代码。
Android & .NET & C/C++ & 算法
祝我这次不再断更。
]]>