Caratteristica bandiere con la primavera

1. Panoramica

In questo articolo, definiremo brevemente i flag di funzionalità e proporremo un approccio supponente e pragmatico per implementarli nelle applicazioni Spring Boot. Quindi, approfondiremo iterazioni più sofisticate sfruttando le diverse funzionalità di Spring Boot.

Discuteremo vari scenari che potrebbero richiedere il contrassegno delle funzionalità e parleremo delle possibili soluzioni. Lo faremo utilizzando un'applicazione di esempio Bitcoin Miner.

2. Flag di funzionalità

I flag di funzionalità, a volte chiamati toggle di funzionalità, sono un meccanismo che ci consente di abilitare o disabilitare funzionalità specifiche della nostra applicazione senza dover modificare il codice o, idealmente, ridistribuire la nostra app.

A seconda delle dinamiche richieste da un determinato flag di funzionalità, potrebbe essere necessario configurarle a livello globale, per istanza dell'app o in modo più granulare, magari per utente o richiesta.

Come in molte situazioni nell'ingegneria del software, è importante provare a utilizzare l'approccio più diretto che affronti il ​​problema in questione senza aggiungere complessità inutili.

I flag di funzionalità sono uno strumento potente che, se usato con saggezza, può portare affidabilità e stabilità al nostro sistema. Tuttavia, quando vengono utilizzati in modo improprio o sottoposte a manutenzione, possono diventare rapidamente fonti di complessità e mal di testa.

Esistono molti scenari in cui i flag di funzionalità potrebbero tornare utili:

Sviluppo basato su trunk e funzionalità non banali

Nello sviluppo basato su trunk, in particolare quando vogliamo continuare a integrarci frequentemente, potremmo non essere pronti a rilasciare una certa funzionalità. I flag delle funzionalità possono tornare utili per consentirci di continuare a rilasciare senza rendere disponibili le nostre modifiche fino al completamento.

Configurazione specifica per l'ambiente

Potremmo trovarci a richiedere determinate funzionalità per ripristinare il nostro DB per un ambiente di test E2E.

In alternativa, potrebbe essere necessario utilizzare una configurazione di sicurezza diversa per gli ambienti non di produzione da quella utilizzata nell'ambiente di produzione.

Quindi, potremmo trarre vantaggio dai flag delle funzionalità per attivare o disattivare la giusta configurazione nell'ambiente giusto.

Test A / B

Rilasciare più soluzioni per lo stesso problema e misurare l'impatto è una tecnica convincente che potremmo implementare utilizzando i flag di funzionalità.

Rilascio di Canary

Quando si distribuiscono nuove funzionalità, potremmo decidere di farlo gradualmente, iniziando con un piccolo gruppo di utenti ed espandendo la sua adozione mentre convalidiamo la correttezza del suo comportamento. I flag di funzionalità ci consentono di raggiungere questo obiettivo.

Nelle sezioni seguenti, proveremo a fornire un approccio pratico per affrontare gli scenari sopra menzionati.

Analizziamo diverse strategie per caratterizzare la segnalazione, iniziando dallo scenario più semplice per poi passare a una configurazione più granulare e più complessa.

3. Flag di funzionalità a livello di applicazione

Se dobbiamo affrontare uno dei primi due casi d'uso, i flag delle funzionalità a livello di applicazione sono un modo semplice per far funzionare le cose.

Un semplice flag di funzionalità normalmente coinvolge una proprietà e una configurazione basata sul valore di quella proprietà.

3.1. Flag di funzionalità che utilizzano profili a molla

In primavera possiamo sfruttare i profili. Convenientemente, i profili ci consentono di configurare determinati bean in modo selettivo. Con pochi costrutti attorno a loro, possiamo creare rapidamente una soluzione semplice ed elegante per i flag di funzionalità a livello di applicazione.

Facciamo finta di costruire un sistema di mining BitCoin. Il nostro software è già in produzione e abbiamo il compito di creare un algoritmo di mining sperimentale e migliorato.

Nella nostra JavaConfig potremmo profilare i nostri componenti:

@Configuration public class ProfiledMiningConfig { @Bean @Profile("!experimental-miner") public BitcoinMiner defaultMiner() { return new DefaultBitcoinMiner(); } @Bean @Profile("experimental-miner") public BitcoinMiner experimentalMiner() { return new ExperimentalBitcoinMiner(); } }

Quindi, con la configurazione precedente, dobbiamo semplicemente includere il nostro profilo per attivare la nostra nuova funzionalità. Ci sono tantissimi modi per configurare la nostra app in generale e abilitare i profili in particolare. Allo stesso modo, ci sono utilità di test per semplificarci la vita.

Finché il nostro sistema è abbastanza semplice, potremmo quindi creare una configurazione basata sull'ambiente per determinare quali flag di funzionalità applicare e quali ignorare.

Immaginiamo di avere una nuova interfaccia utente basata su carte invece che su tabelle, insieme al precedente minatore sperimentale.

Vorremmo abilitare entrambe le funzionalità nel nostro ambiente di accettazione (UAT). Potremmo creare un file application-uat.yml :

spring: profiles: include: experimental-miner,ui-cards # More config here

Con il file precedente in posizione, avremmo solo bisogno di abilitare il profilo UAT nell'ambiente UAT per ottenere il set di funzionalità desiderato.

È anche importante capire come sfruttare i vantaggi di spring.profiles.include. Rispetto a spring.profiles.active, il primo ci consente di includere i profili in modo additivo.

Nel nostro caso, vogliamo che il profilo uat includa anche il miner sperimentale e le schede ui .

3.2. Flag di funzionalità che utilizzano proprietà personalizzate

I profili sono un modo semplice e fantastico per portare a termine il lavoro. Tuttavia, potremmo richiedere profili per altri scopi. O forse, potremmo voler costruire un'infrastruttura di flag di funzionalità più strutturata.

Per questi scenari, le proprietà personalizzate potrebbero essere un'opzione desiderabile.

Riscriviamo il nostro esempio precedente sfruttando @ConditionalOnProperty e il nostro spazio dei nomi :

@Configuration public class CustomPropsMiningConfig { @Bean @ConditionalOnProperty( name = "features.miner.experimental", matchIfMissing = true) public BitcoinMiner defaultMiner() { return new DefaultBitcoinMiner(); } @Bean @ConditionalOnProperty( name = "features.miner.experimental") public BitcoinMiner experimentalMiner() { return new ExperimentalBitcoinMiner(); } }

L'esempio precedente si basa sulla configurazione condizionale di Spring Boot e configura un componente o un altro, a seconda che la proprietà sia impostata su true o false (o omessa del tutto).

The result is very similar to the one in 3.1, but now, we have our namespace. Having our namespace allows us to create meaningful YAML/properties files:

#[...] Some Spring config features: miner: experimental: true ui: cards: true #[...] Other feature flags

Also, this new setup allows us to prefix our feature flags – in our case, using the features prefix.

It might seem like a small detail, but as our application grows and complexity increases, this simple iteration will help us keep our feature flags under control.

Let's talk about other benefits of this approach.

3.3. Using @ConfigurationProperties

As soon as we get a prefixed set of properties, we can create a POJO decorated with @ConfigurationProperties to get a programmatic handle in our code.

Following our ongoing example:

@Component @ConfigurationProperties(prefix = "features") public class ConfigProperties { private MinerProperties miner; private UIProperties ui; // standard getters and setters public static class MinerProperties { private boolean experimental; // standard getters and setters } public static class UIProperties { private boolean cards; // standard getters and setters } }

By putting our feature flags' state in a cohesive unit, we open up new possibilities, allowing us to easily expose that information to other parts of our system, such as the UI, or to downstream systems.

3.4. Exposing Feature Configuration

Our Bitcoin mining system got a UI upgrade which is not entirely ready yet. For that reason, we decided to feature-flag it. We might have a single-page app using React, Angular, or Vue.

Regardless of the technology, we need to know what features are enabled so that we can render our page accordingly.

Let's create a simple endpoint to serve our configuration so that our UI can query the backend when needed:

@RestController public class FeaturesConfigController { private ConfigProperties properties; // constructor @GetMapping("/feature-flags") public ConfigProperties getProperties() { return properties; } }

There might be more sophisticated ways of serving this information, such as creating custom actuator endpoints. But for the sake of this guide, a controller endpoint feels like good enough a solution.

3.5. Keeping the Camp Clean

Although it might sound obvious, once we've implemented our feature flags thoughtfully, it's equally important to remain disciplined in getting rid of them once they're no longer needed.

Feature flags for the first use case – trunk-based development and non-trivial features – are typically short-lived. This means that we're going to need to make sure that our ConfigProperties, our Java configuration, and our YAML files stay clean and up-to-date.

4. More Granular Feature Flags

Sometimes we find ourselves in more complex scenarios. For A/B testing or canary releases, our previous approach is simply not enough.

To get feature flags at a more granular level, we may need to create our solution. This could involve customizing our user entity to include feature-specific information, or perhaps extending our web framework.

Polluting our users with feature flags might not be an appealing idea for everybody, however, and there are other solutions.

As an alternative, we could take advantage of some built-in tools such as Togglz. This tool adds some complexity but offers a nice out-of-the-box solution and provides first-class integration with Spring Boot.

Togglz supports different activation strategies:

  1. Username: Flags associated with specific users
  2. Gradual rollout: Flags enabled for a percentage of the user base. This is useful for Canary releases, for example, when we want to validate the behavior of our features
  3. Release date: We could schedule flags to be enabled at a certain date and time. This might be useful for a product launch, a coordinated release, or offers and discounts
  4. Client IP: Flagged features based on clients IPs. These might come in handy when applying the specific configuration to specific customers, given they have static IPs
  5. Server IP: In this case, the IP of the server is used to determine whether a feature should be enabled or not. This might be useful for canary releases too, with a slightly different approach than the gradual rollout – like when we want to assess performance impact in our instances
  6. ScriptEngine: We could enable feature flags based on arbitrary scripts. This is arguably the most flexible option
  7. System Properties: We could set certain system properties to determine the state of a feature flag. This would be quite similar to what we achieved with our most straightforward approach

5. Summary

In this article, we had a chance to talk about feature flags. Additionally, we discussed how Spring could help us achieve some of this functionality without adding new libraries.

Abbiamo iniziato definendo come questo modello può aiutarci con alcuni casi d'uso comuni.

Successivamente, abbiamo creato alcune semplici soluzioni utilizzando gli strumenti Spring e Spring Boot pronti all'uso. Con ciò, abbiamo creato un costrutto di segnalazione delle funzionalità semplice ma potente.

In basso, abbiamo confrontato un paio di alternative. Passare dalla soluzione più semplice e meno flessibile a un modello più sofisticato, anche se più complesso.

Infine, abbiamo brevemente fornito alcune linee guida per creare soluzioni più solide. Ciò è utile quando è necessario un grado di granularità più elevato.