Java8: @Repeatable注解原理和使用技巧

Posted 琦彦

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java8: @Repeatable注解原理和使用技巧相关的知识,希望对你有一定的参考价值。

目录

前言

Spring 中 @PropertySources 与 @PropertySource区别是?

@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">
 * &#064;Configuration
 * &#064;PropertySource("classpath:/com/myco/app.properties")
 * public class AppConfig {
 *
 *     &#064;Autowired
 *     Environment env;
 *
 *     &#064;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">
 * &#064;Configuration
 * &#064;PropertySource("classpath:/com/${my.placeholder:default/path}/app.properties")
 * public class AppConfig {
 *
 *     &#064;Autowired
 *     Environment env;
 *
 *     &#064;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">
 * &#064;Configuration
 * &#064;PropertySource("classpath:/com/myco/a.properties")
 * public class ConfigA { }
 *
 * &#064;Configuration
 * &#064;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
	 * &mdash; for example, {@code "classpath:/com/myco/app.properties"}
	 * or {@code "file:/path/to/file.xml"}.
	 * <p>Resource location wildcards (e.g. *&#42;/*.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注解原理和使用技巧的主要内容,如果未能解决你的问题,请参考以下文章

Java8新特性 重复注解与类型注解

java8的特性都有哪些

Java:@Repeatable注解的使用和反射获取

Hibernate 和 PostgreSQL:REPEATABLE_READ 和使用@Version 注解来避免写入倾斜和其他现象

spring常用注解作用与常用接口与后置处理器

Java8新特性