Статьи

Grails Goodness: условно загруженные компоненты в конфигурации Java на основе среды Grails

В  предыдущем посте  мы видели, что мы можем использовать функцию конфигурации Java в Spring для загрузки bean-компонентов в наше приложение Grails. Мы можем использовать  @Profile аннотацию для условной загрузки бинов на основе текущего активного профиля Spring. Например, мы можем использовать системное свойство Java  spring.profiles.active при запуске приложения Grails, чтобы сделать профиль активным. Но разве не было бы неплохо, если бы мы могли использовать параметр среды Grails для условной загрузки bean-компонентов из конфигурации Java?

Оказывается, это не так сложно достичь. Мы должны реализовать  matches метод из  Condition интерфейса из  org.springframework.context.annotation пакета. Затем мы создаем новый интерфейс аннотации, где мы делегируем в основном нашему классу реализации.

Начнем с написания реализации  Condition интерфейса:

// File: src/groovy/com/mrhaki/grails/context/annotation/GrailsEnvCondition.groovy
package com.mrhaki.grails.context.annotation

import grails.util.Environment
import org.springframework.context.annotation.Condition
import org.springframework.context.annotation.ConditionContext
import org.springframework.core.type.AnnotatedTypeMetadata
import org.springframework.util.Assert
import org.springframework.util.MultiValueMap

/**
 * <p>{@link Condition} that matches based on the value of
 * a {@link GrailsEnv @GrailsEnv} annotation.</p>
 *
 * <p>The value of the current Grails environment is compared to given
 * Grails environments set via the {@link GrailsEnv @GrailsEnv} annotation.</p>
 *
 * @author Hubert A. Klein Ikkink aka mrhaki
 * @see GrailsEnv
 */
class GrailsEnvCondition implements Condition {

    @Override
    public boolean matches(final ConditionContext context, final AnnotatedTypeMetadata metadata) {
        final MultiValueMap<String, Object> attributes = metadata.getAllAnnotationAttributes(GrailsEnv.class.name)
        if (attributes != null) {
            final List<String> value = attributes.get('value')
            return value ? value.any { acceptsEnvironments(it) } : false
        }
        return true
    }

    protected boolean acceptsEnvironments(final String... environments) {
        Assert.notEmpty(environments, "Must specify at least one environment")
        for (environment in environments) {
            if (isNegateEnvironment(environment)) {
                if (!isProfileActive(environment[1..-1])) {
                    return true
                }
            } else if (isProfileActive(environment)) {
                return true
            }
        }
        return false
    }

    private boolean isNegateEnvironment(final String environment) {
        environment != null && environment.length() > 0 && environment[0] == '!'
    }

    protected boolean isProfileActive(final String profile) {
        validateEnvironment(profile);
        final String currentEnvironment = Environment.current.name
        profile == currentEnvironment
    }

    protected void validateEnvironment(final String environment) {
        if (!environment || environment.allWhitespace) {
            throw new IllegalArgumentException("Invalid environment [$environment]: must contain text");
        }
        if (environment[0] == '!') {
            throw new IllegalArgumentException("Invalid environment [$environment]: must not begin with ! operator");
        }
    }

}

Далее мы создаем новую аннотацию  @GrailsEnv. Аннотация принимает  String значение или массив  String значений с именами сред Grails, для которых bean-компонент должен быть зарегистрирован или исключен из:

// File: src/java/com/mrhaki/grails/context/annotation/GrailsEnv.java
package com.mrhaki.grails.context.annotation;

import org.springframework.context.annotation.Conditional;

import java.lang.annotation.*;

/**
 * <p>Indicates that a component is eligible for registration when one
 * or more {@linkplain #value specified Grails environments} are active.</p>
 *
 * <p>The Grails environment can be set via the Java system property
 * <em>grails.env</em>.</p>
 *
 * <p>The {@code @GrailsEnv} annotation may be used in any of the following ways:
 * <ul>
 * <li>as a type-level annotation on any class directly or indirectly annotated with
 * {@code @Component},
 * including {@link org.springframework.context.annotation.Configuration @Configuration} classes</li>
 * <li>as a meta-annotation, for the purpose of composing custom stereotype annotations</li>
 * <li>as a method-level annotation on
 * any {@link org.springframework.context.annotation.Bean @Bean} method</li>
 * </ul>
 *
 * <p>If a {@code @Configuration} class is marked with {@code @GrailEnv},
 * all of the {@code @Bean} methods and
 * {@link org.springframework.context.annotation.Import @Import} annotations associated with that class
 * will be bypassed unless one or more of the specified Grails environments are active.</p>
 *
 * <p>If a given Grails environment is prefixed with the NOT operator ({@code !}),
 * the annotated beans or components will be registered if the Grails environment
 * is <em>not</em> active. e.g., for {@code @GrailsEnv("!production")}, registration will occur
 * if Grails environment 'production' is not active.</p>
 *
 * <p>If the {@code @GrailsEnv} annotation is omitted, registration will occur, regardless
 * of which Grails environment is active.
 *
 * @author Hubert A. Klein Ikkink aka mrhaki
 * @see org.springframework.context.annotation.Profile
 * @see com.mrhaki.grails.annotation.context.GrailsEnvCondition
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Conditional(GrailsEnvCondition.class)
public @interface GrailsEnv {

    /**
     * Annotation attribute value to set Grails environments.
     * Use an array of String value to determine Grails environments
     * or use a single String value (is automatically put in array).
     * Environment maybe prefixed with {@code !} to register component when environment
     * is NOT active.
     */
    String[] value();

}

Мы можем использовать нашу новую аннотацию в классе конфигурации Spring Java. В следующем примере показаны разные значения для  @GrailsEnv аннотации. Аннотация применяется к методам в примере кода, но также может применяться к классу.

// File: src/groovy/com/mrhaki/grails/configuration/BeansConfiguration.groovy
package com.mrhaki.grails.configuration

import com.mrhaki.grails.context.annotation.GrailsEnv
import org.springframework.context.annotation.*

@Configuration
class BeansConfiguration {

    // Load for Grails environments 
    // development or test
    @Bean
    @GrailsEnv(['development', 'test'])
    Sample sampleBean() {
        new Sample('sampleBean')
    }

    // Load for every Grails environment NOT
    // being production.
    @Bean
    @GrailsEnv('!production')
    Sample sample() {
        new Sample('sample')
    }

    // Load for custom environment name qa.
    @Bean
    @GrailsEnv('qa')
    Sample sampleQA() {
        new Sample('QA')
    }

}

Мы также можем использовать аннотацию для классов, аннотированных  @Component аннотацией:

// File: src/groovy/com/mrhaki/grails/Person.groovy
package 

import com.mrhaki.grails.context.annotation.GrailsEnv
import org.springframework.stereotype.Component

@Component
@GrailsEnv('development')
class Person {
    String name
    String email
}

Реализация для  GrailsEnv и  GrailsEnvCondition основана на существующих классах Spring  Profile и  ProfileCondition.

Код, написанный с помощью Grails 2.4.2.