Java8: @Repeatable注解原理和使用技巧
Posted 琦彦
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java8: @Repeatable注解原理和使用技巧相关的知识,希望对你有一定的参考价值。
目录
Spring 中 @PropertySources 与 @PropertySource区别是?
前言
@Repeatable
是java8为了解决同一个注解, 不能重复在同一类/方法/属性上使用的问题。也就是说该注解可以重复的注解某个元素。
Spring 中 @PropertySources 与 @PropertySource区别是?
@PropertySources
/*
* Copyright 2002-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.context.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Container annotation that aggregates several {@link PropertySource} annotations.
*
* <p>Can be used natively, declaring several nested {@link PropertySource} annotations.
* Can also be used in conjunction with Java 8's support for <em>repeatable annotations</em>,
* where {@link PropertySource} can simply be declared several times on the same
* {@linkplain ElementType#TYPE type}, implicitly generating this container annotation.
*
* @author Phillip Webb
* @since 4.0
* @see PropertySource
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PropertySources {
PropertySource[] value();
}
@PropertySource
package org.springframework.context.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.io.support.PropertySourceFactory;
/**
* Annotation providing a convenient and declarative mechanism for adding a
* {@link org.springframework.core.env.PropertySource PropertySource} to Spring's
* {@link org.springframework.core.env.Environment Environment}. To be used in
* conjunction with @{@link Configuration} classes.
*
* <h3>Example usage</h3>
*
* <p>Given a file {@code app.properties} containing the key/value pair
* {@code testbean.name=myTestBean}, the following {@code @Configuration} class
* uses {@code @PropertySource} to contribute {@code app.properties} to the
* {@code Environment}'s set of {@code PropertySources}.
*
* <pre class="code">
* @Configuration
* @PropertySource("classpath:/com/myco/app.properties")
* public class AppConfig {
*
* @Autowired
* Environment env;
*
* @Bean
* public TestBean testBean() {
* TestBean testBean = new TestBean();
* testBean.setName(env.getProperty("testbean.name"));
* return testBean;
* }
* }</pre>
*
* <p>Notice that the {@code Environment} object is
* {@link org.springframework.beans.factory.annotation.Autowired @Autowired} into the
* configuration class and then used when populating the {@code TestBean} object. Given
* the configuration above, a call to {@code testBean.getName()} will return "myTestBean".
*
* <h3>Resolving <code>${...}</code> placeholders in {@code <bean>} and {@code @Value} annotations</h3>
*
* <p>In order to resolve ${...} placeholders in {@code <bean>} definitions or {@code @Value}
* annotations using properties from a {@code PropertySource}, you must ensure that an
* appropriate <em>embedded value resolver</em> is registered in the {@code BeanFactory}
* used by the {@code ApplicationContext}. This happens automatically when using
* {@code <context:property-placeholder>} in XML. When using {@code @Configuration} classes
* this can be achieved by explicitly registering a {@code PropertySourcesPlaceholderConfigurer}
* via a {@code static} {@code @Bean} method. Note, however, that explicit registration
* of a {@code PropertySourcesPlaceholderConfigurer} via a {@code static} {@code @Bean}
* method is typically only required if you need to customize configuration such as the
* placeholder syntax, etc. See the "Working with externalized values" section of
* {@link Configuration @Configuration}'s javadocs and "a note on
* BeanFactoryPostProcessor-returning {@code @Bean} methods" of {@link Bean @Bean}'s
* javadocs for details and examples.
*
* <h3>Resolving ${...} placeholders within {@code @PropertySource} resource locations</h3>
*
* <p>Any ${...} placeholders present in a {@code @PropertySource} {@linkplain #value()
* resource location} will be resolved against the set of property sources already
* registered against the environment. For example:
*
* <pre class="code">
* @Configuration
* @PropertySource("classpath:/com/${my.placeholder:default/path}/app.properties")
* public class AppConfig {
*
* @Autowired
* Environment env;
*
* @Bean
* public TestBean testBean() {
* TestBean testBean = new TestBean();
* testBean.setName(env.getProperty("testbean.name"));
* return testBean;
* }
* }</pre>
*
* <p>Assuming that "my.placeholder" is present in one of the property sources already
* registered, e.g. system properties or environment variables, the placeholder will
* be resolved to the corresponding value. If not, then "default/path" will be used as a
* default. Expressing a default value (delimited by colon ":") is optional. If no
* default is specified and a property cannot be resolved, an {@code
* IllegalArgumentException} will be thrown.
*
* <h3>A note on property overriding with @PropertySource</h3>
*
* <p>In cases where a given property key exists in more than one {@code .properties}
* file, the last {@code @PropertySource} annotation processed will 'win' and override.
*
* <p>For example, given two properties files {@code a.properties} and
* {@code b.properties}, consider the following two configuration classes
* that reference them with {@code @PropertySource} annotations:
*
* <pre class="code">
* @Configuration
* @PropertySource("classpath:/com/myco/a.properties")
* public class ConfigA { }
*
* @Configuration
* @PropertySource("classpath:/com/myco/b.properties")
* public class ConfigB { }
* </pre>
*
* <p>The override ordering depends on the order in which these classes are registered
* with the application context.
*
* <pre class="code">
* AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
* ctx.register(ConfigA.class);
* ctx.register(ConfigB.class);
* ctx.refresh();
* </pre>
*
* <p>In the scenario above, the properties in {@code b.properties} will override any
* duplicates that exist in {@code a.properties}, because {@code ConfigB} was registered
* last.
*
* <p>In certain situations, it may not be possible or practical to tightly control
* property source ordering when using {@code @PropertySource} annotations. For example,
* if the {@code @Configuration} classes above were registered via component-scanning,
* the ordering is difficult to predict. In such cases - and if overriding is important -
* it is recommended that the user fall back to using the programmatic PropertySource API.
* See {@link org.springframework.core.env.ConfigurableEnvironment ConfigurableEnvironment}
* and {@link org.springframework.core.env.MutablePropertySources MutablePropertySources}
* javadocs for details.
*
* <p><b>NOTE: This annotation is repeatable according to Java 8 conventions.</b>
* However, all such {@code @PropertySource} annotations need to be declared at the same
* level: either directly on the configuration class or as meta-annotations within the
* same custom annotation. Mixing of direct annotations and meta-annotations is not
* recommended since direct annotations will effectively override meta-annotations.
*
* @author Chris Beams
* @author Juergen Hoeller
* @author Phillip Webb
* @author Sam Brannen
* @since 3.1
* @see PropertySources
* @see Configuration
* @see org.springframework.core.env.PropertySource
* @see org.springframework.core.env.ConfigurableEnvironment#getPropertySources()
* @see org.springframework.core.env.MutablePropertySources
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(PropertySources.class)
public @interface PropertySource {
/**
* Indicate the name of this property source. If omitted, the {@link #factory()}
* will generate a name based on the underlying resource (in the case of
* {@link org.springframework.core.io.support.DefaultPropertySourceFactory}:
* derived from the resource description through a corresponding name-less
* {@link org.springframework.core.io.support.ResourcePropertySource} constructor).
* @see org.springframework.core.env.PropertySource#getName()
* @see org.springframework.core.io.Resource#getDescription()
*/
String name() default "";
/**
* Indicate the resource location(s) of the properties file to be loaded.
* <p>Both traditional and XML-based properties file formats are supported
* — for example, {@code "classpath:/com/myco/app.properties"}
* or {@code "file:/path/to/file.xml"}.
* <p>Resource location wildcards (e.g. **/*.properties) are not permitted;
* each location must evaluate to exactly one {@code .properties} resource.
* <p>${...} placeholders will be resolved against any/all property sources already
* registered with the {@code Environment}. See {@linkplain PropertySource above}
* for examples.
* <p>Each location will be added to the enclosing {@code Environment} as its own
* property source, and in the order declared.
*/
String[] value();
/**
* Indicate if failure to find the a {@link #value() property resource} should be
* ignored.
* <p>{@code true} is appropriate if the properties file is completely optional.
* Default is {@code false}.
* @since 4.0
*/
boolean ignoreResourceNotFound() default false;
/**
* A specific character encoding for the given resources, e.g. "UTF-8".
* @since 4.3
*/
String encoding() default "";
/**
* Specify a custom {@link PropertySourceFactory}, if any.
* <p>By default, a default factory for standard resource files will be used.
* @since 4.3
* @see org.springframework.core.io.support.DefaultPropertySourceFactory
* @see org.springframework.core.io.support.ResourcePropertySource
*/
Class<? extends PropertySourceFactory> factory() default PropertySourceFactory.class;
}
下面, 我们先从应用场景说起
应用场景
举一个比较贴近开发的例子,在spring/springboot我们引入资源文件可以使用注解@PropertySource
@PropertySource("classpath:sso.properties")
public class Application {
}
那要引入多个资源文件怎么办,没事,我把PropertySource中的value设置成String[]数组就行了
public @interface PropertySource {
...
String[] value();
}
那么就能这么用
@PropertySource({"classpath:sso.properties","classpath:dubbo.properties","classpath:systemInfo.properties"})
public class Application {
}
就spring引入配置文件来讲,肯定是没事问题的。但是如果注解中2个值存在依赖关系,那么这样就不行了。比如下面这个
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
//@Repeatable(Validates.class)
public @interface Validate {
/**
* 业务类型
* @return
*/
String bizCode();
/**
* 订单类型
* @return
*/
int orderType();
}
我玩策略模式的时候喜欢用注解和spring结合做自动策略路由
上面的@validate注解,bizcode和orderType是一对一的关系,我希望可以添加如下的注解
@Validate(bizCode = "fruit",orderType = 1)
@Validate(bizCode = "fruit",orderType = 2)
@Validate(bizCode = "vegetable",orderType = 2)
public class BizLogic2 {
}
很抱歉在java8之前,这种方式不行,不过你可以这么做,新建一个如下的注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
public @interface Validates {
Validate[] value();
}
注意这个聚合注解默认约束value来存储子注解
然后对应代码修改如下
@Validates(value = {
@Validate(bizCode = "fruit",orderType = 1),
@Validate(bizCode = "fruit",orderType = 2),
@Validate(bizCode = "vegetable",orderType = 2)
})
public class BizLogic2 {
}
在java8的@Repeatable出来之后,我们在不改动@Validates的情况下,对@Validate进行修改,增加@Repeatable(Validates.class)
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Repeatable(Validates.class)
public @interface Validate {
/**
* 业务类型
* @return
*/
String bizCode();
/**
* 订单类型
* @return
*/
int orderType();
}
那么就可以在类上使用多个@Validate注解了。
回过头来看下@PropertySource和@PropertySources也是如此
package org.springframework.context.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Container annotation that aggregates several {@link PropertySource} annotations.
*
* <p>Can be used natively, declaring several nested {@link PropertySource} annotations.
* Can also be used in conjunction with Java 8's support for <em>repeatable annotations</em>,
* where {@link PropertySource} can simply be declared several times on the same
* {@linkplain ElementType#TYPE type}, implicitly generating this container annotation.
*
* @author Phillip Webb
* @since 4.0
* @see PropertySource
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PropertySources {
PropertySource[] value();
}
从上面的注释中可以看到,spring在4.0支持了java8这个特性
原理
/**
* The annotation type {@code java.lang.annotation.Repeatable} is
* used to indicate that the annotation type whose declaration it
* (meta-)annotates is <em>repeatable</em>. The value of
* {@code @Repeatable} indicates the <em>containing annotation
* type</em> for the repeatable annotation type.
*
* @since 1.8
* @jls 9.6 Annotation Types
* @jls 9.7 Annotations
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Repeatable {
/**
* Indicates the <em>containing annotation type</em> for the
* repeatable annotation type.
* @return the containing annotation type
*/
Class<? extends Annotation> value();
}
那么@Repeatable的原理是啥?
语法糖而已!
首先我们需要定一个容器注解来包含这个能够重复的注解,并且这个容器注解的必须拥有一个返回需要重复注解数组的value方法。
咋一看好像有点绕,其实Repeatable的原理和我们原先的实现方式是一样的,原先我们是在另一个注解的value方法显示的另一个返回注解数组,而使用Repeatable之后,我们则可以直接在元素上重复使用某个注解。
我们反编译下面代码
@Validates(value = {
@Validate(bizCode = "fruit",orderType = 1),
@Validate(bizCode = "fruit",orderType = 2),
@Validate(bizCode = "vegetable",orderType = 2)
})
public class BizLogic2 {
}
和
@Validate(bizCode = "fruit", orderType = 1)
@Validate(bizCode = "fruit", orderType = 2)
@Validate(bizCode = "vegetable", orderType = 2)
public class BizLogic2 {
}
发现反编译变成了
@Validates({@Validate(
bizCode = "fruit",
orderType = 1
), @Validate(
bizCode = "fruit",
orderType = 2
), @Validate(
bizCode = "vegetable",
orderType = 2
)})
public class BizLogic2 {
public BizLogic2() {
}
}
语法糖无疑了。
问题来了
那么再反编译下面代码试试
@Validate(bizCode = "fruit", orderType = 1)
public class BizLogic2 {
}
生成
@Validate(
bizCode = "fruit",
orderType = 1
)
public class BizLogic2 {
public BizLogic2() {
}
}
那这样就存在问题了,我以为加了@Repeatable之后@Validate都会生成被语法糖@Validates包裹。没想到它居然这么智能,只有一个@Validate注解的时候居然不转换。
其中有一些需要注意的细节,可重复注解最终是以另一个注解的value值的数组形式存在于class中的,所以此时通过反射的getDeclaredAnnotation方法获取可重复注解时,获取到的是容器注解。
同时如果元素上的可重复注解只有一个时,最终的class中重复注解并不会被存放到容器注解,而是以直接的形式存放在元素上,此时通过反射的getDeclaredAnnotation方法是能获取到该注解的,所以当我们使用反射获取可重复注解时需要额外的关注这些细节。
所以使用多个@Validate的时候就会留坑,你需要兼容1个或多个的场景。
推荐方式
直接使用@Validates读取注解代码如下
Validates validates = AnnotationUtils.getAnnotation(BizLogic2.class,Validates.class);
Arrays.stream(validates.value()).forEach(a->{
System.out.println(a.bizCode());
});
带兼容的逻辑如下
Validate validate = AnnotationUtils.getAnnotation(BizLogic.class,Validate.class);
if(Objects.nonNull(validate)){
System.out.println(validate.bizCode());
}
Validates validates = AnnotationUtils.getAnnotation(BizLogic2.class,Validates.class);
Arrays.stream(validates.value()).forEach(a->{
System.out.println(a.bizCode());
});
如果你是自己写业务的,我觉得第一种方式更加方便。
但是如果你是开发中间件的,那么必须兼容。
还有一点需要注意,我的@Validate是被@Component元注解,当多个@Validate语法糖转换成@Validates之后,由于@Validates上没@Component,导致我的bean加载不到spring容器中去
参考链接:
https://docs.oracle.com/javase/tutorial/java/annotations/repeating.html
以上是关于Java8: @Repeatable注解原理和使用技巧的主要内容,如果未能解决你的问题,请参考以下文章
Hibernate 和 PostgreSQL:REPEATABLE_READ 和使用@Version 注解来避免写入倾斜和其他现象