Skip to content

Commit 1183182

Browse files
authored
feat: Parameters injection (#201)
* Parameters injection annotation * update doc on annotation usage and installation
1 parent 6f1bd75 commit 1183182

File tree

10 files changed

+372
-37
lines changed

10 files changed

+372
-37
lines changed

docs/content/utilities/parameters.mdx

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,4 +264,106 @@ And then use it like this :
264264
S3Provider provider = new S3Provider(ParamManager.getCacheManager());
265265
provider.setTransformationManager(ParamManager.getTransformationManager()); // optional, needed for transformations
266266
String value = provider.withBucket("myBucket").get("myKey");
267-
```
267+
```
268+
269+
## Annotation
270+
You can make use of the annotation ```@Param``` to inject a parameter value in a variable.
271+
272+
```java
273+
@Param(key = "/my/parameter")
274+
private String value;
275+
```
276+
By default it will use ```SSMProvider``` to retrieve the value from AWS System Manager Parameter Store.
277+
You could specify a different provider as long as it extends ```BaseProvider``` and/or a ```Transformer```.
278+
For example:
279+
280+
```java
281+
@Param(key = "/my/parameter/json", provider = SecretsProvider.class, transformer = JsonTransformer.class)
282+
private ObjectToDeserialize value;
283+
```
284+
285+
In this case ```SecretsProvider``` will be used to retrieve a raw value that is then trasformed into the target Object by using ```JsonTransformer```.
286+
To show the convenience of the annotation compare the following two code snippets.
287+
288+
```java:title=AppWithoutAnnotation.java
289+
290+
public class AppWithoutAnnotation implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
291+
292+
// Get an instance of the SSM Provider
293+
SSMProvider ssmProvider = ParamManager.getSsmProvider();
294+
295+
// Retrieve a single parameter
296+
ObjectToDeserialize value = ssmProvider
297+
.withTransformation(Transformer.json)
298+
.get("/my/parameter/json");
299+
300+
}
301+
```
302+
And with the usage of ```@Param```
303+
304+
```java:title=AppWithAnnotation.java
305+
public class AppWithAnnotation implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
306+
307+
@Param(key = "/my/parameter/json" transformer = JsonTransformer.class)
308+
ObjectToDeserialize value;
309+
310+
}
311+
```
312+
313+
### Install
314+
315+
If you want to use the ```@Param``` annotation in your project add configuration to compile-time weave (CTW) the powertools-parameters aspects into your project.
316+
317+
* [maven](https://maven.apache.org/):
318+
```xml
319+
<build>
320+
<plugins>
321+
...
322+
<plugin>
323+
<groupId>org.codehaus.mojo</groupId>
324+
<artifactId>aspectj-maven-plugin</artifactId>
325+
<version>1.11</version>
326+
<configuration>
327+
...
328+
<aspectLibraries>
329+
...
330+
<aspectLibrary>
331+
<groupId>software.amazon.lambda</groupId>
332+
<artifactId>powertools-parameters</artifactId>
333+
</aspectLibrary>
334+
</aspectLibraries>
335+
</configuration>
336+
<executions>
337+
<execution>
338+
<goals>
339+
<goal>compile</goal>
340+
</goals>
341+
</execution>
342+
</executions>
343+
</plugin>
344+
...
345+
</plugins>
346+
</build>
347+
```
348+
**Note:** If you are working with lambda function on runtime post java8, please refer [issue](https://github.com/awslabs/aws-lambda-powertools-java/issues/50) for workaround
349+
350+
* [gradle](https://gradle.org):
351+
```groovy
352+
plugins{
353+
id 'java'
354+
id 'aspectj.AspectjGradlePlugin' version '0.0.6'
355+
}
356+
repositories {
357+
jcenter()
358+
}
359+
dependencies {
360+
...
361+
implementation 'software.amazon.lambda:powertools-parameters:1.0.1'
362+
aspectpath 'software.amazon.lambda:powertools-parameters:1.0.1'
363+
}
364+
```
365+
366+
**Note:**
367+
368+
Please add `aspectjVersion = '1.9.6'` to the `gradle.properties` file. The aspectj plugin works at the moment with gradle 5.x only if
369+
you are using `java 8` as runtime. Please refer to [open issue](https://github.com/awslabs/aws-lambda-powertools-java/issues/146) for more details.

powertools-parameters/pom.xml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@
8282
<artifactId>aspectjrt</artifactId>
8383
<scope>compile</scope>
8484
</dependency>
85-
8685
<!-- Test dependencies -->
8786
<dependency>
8887
<groupId>org.junit.jupiter</groupId>
@@ -99,6 +98,11 @@
9998
<artifactId>mockito-core</artifactId>
10099
<scope>test</scope>
101100
</dependency>
101+
<dependency>
102+
<groupId>org.mockito</groupId>
103+
<artifactId>mockito-inline</artifactId>
104+
<scope>test</scope>
105+
</dependency>
102106
<dependency>
103107
<groupId>org.apache.commons</groupId>
104108
<artifactId>commons-lang3</artifactId>
@@ -109,6 +113,11 @@
109113
<artifactId>assertj-core</artifactId>
110114
<scope>test</scope>
111115
</dependency>
116+
<dependency>
117+
<groupId>org.aspectj</groupId>
118+
<artifactId>aspectjweaver</artifactId>
119+
<scope>test</scope>
120+
</dependency>
112121
</dependencies>
113122

114123
</project>

powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/BaseProvider.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ public BaseProvider withMaxAge(int maxAge, ChronoUnit unit) {
104104
* @param transformerClass Class of the transformer to apply. For convenience, you can use {@link Transformer#json} or {@link Transformer#base64} shortcuts.
105105
* @return the provider itself in order to chain calls (eg. <pre>provider.withTransformation(json).get("key", MyObject.class)</pre>).
106106
*/
107-
protected BaseProvider withTransformation(Class<? extends Transformer> transformerClass) {
107+
public BaseProvider withTransformation(Class<? extends Transformer> transformerClass) {
108108
if (transformationManager == null) {
109109
throw new IllegalStateException("Trying to add transformation while no TransformationManager has been provided.");
110110
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package software.amazon.lambda.powertools.parameters;
2+
3+
import software.amazon.lambda.powertools.parameters.transform.Transformer;
4+
5+
import java.lang.annotation.ElementType;
6+
import java.lang.annotation.Retention;
7+
import java.lang.annotation.RetentionPolicy;
8+
import java.lang.annotation.Target;
9+
10+
/**
11+
* {@code Param} is used to signal that the annotated field should be
12+
* populated with a value retrieved from a parameter store through a {@link ParamProvider}.
13+
*
14+
* <p>By default {@code Param} use {@link SSMProvider} as parameter provider. This can be overridden specifying
15+
* the annotation variable {@code Param(provider = <Class-of-the-provider>)}.<br/>
16+
* The library provide a provider for AWS System Manager Parameters Store ({@link SSMProvider}) and a provider
17+
* for AWS Secrets Manager ({@link SecretsProvider}).
18+
* The user can implement a custom provider by extending the abstract class {@link BaseProvider}.</p>
19+
*
20+
* <p>If the parameter value requires transformation before being assigned to the annotated field
21+
* users can specify a {@link Transformer}
22+
* </p>
23+
*/
24+
@Retention(RetentionPolicy.RUNTIME)
25+
@Target(ElementType.FIELD)
26+
public @interface Param {
27+
String key();
28+
Class<? extends BaseProvider> provider() default SSMProvider.class;
29+
Class<? extends Transformer> transformer() default Transformer.class;
30+
}

powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/ParamManager.java

Lines changed: 43 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
import software.amazon.lambda.powertools.parameters.cache.CacheManager;
1919
import software.amazon.lambda.powertools.parameters.transform.TransformationManager;
2020

21+
import java.lang.reflect.Constructor;
22+
import java.lang.reflect.InvocationTargetException;
23+
import java.util.concurrent.ConcurrentHashMap;
24+
2125
/**
2226
* Utility class to retrieve instances of parameter providers.
2327
* Each instance is unique (singleton).
@@ -27,22 +31,28 @@ public final class ParamManager {
2731
private static final CacheManager cacheManager = new CacheManager();
2832
private static final TransformationManager transformationManager = new TransformationManager();
2933

30-
private static SecretsProvider secretsProvider;
31-
private static SSMProvider ssmProvider;
34+
private static ConcurrentHashMap<Class<? extends BaseProvider>, BaseProvider> providers = new ConcurrentHashMap<>();
35+
36+
/**
37+
* Get a concrete implementation of {@link BaseProvider}.<br/>
38+
* You can specify {@link SecretsProvider} or {@link SSMProvider} or create your custom provider
39+
* by extending {@link BaseProvider} if you need to integrate with a different parameter store.
40+
* @return a {@link SecretsProvider}
41+
*/
42+
public static <T extends BaseProvider> T getProvider(Class<T> providerClass) {
43+
if (providerClass == null) {
44+
throw new IllegalStateException("providerClass cannot be null.");
45+
}
46+
return (T) providers.computeIfAbsent(providerClass, (k) -> createProvider(k));
47+
}
3248

3349
/**
3450
* Get a {@link SecretsProvider} with default {@link SecretsManagerClient}.<br/>
3551
* If you need to customize the region, or other part of the client, use {@link ParamManager#getSecretsProvider(SecretsManagerClient)} instead.
3652
* @return a {@link SecretsProvider}
3753
*/
3854
public static SecretsProvider getSecretsProvider() {
39-
if (secretsProvider == null) {
40-
secretsProvider = SecretsProvider.builder()
41-
.withCacheManager(cacheManager)
42-
.withTransformationManager(transformationManager)
43-
.build();
44-
}
45-
return secretsProvider;
55+
return getProvider(SecretsProvider.class);
4656
}
4757

4858
/**
@@ -51,13 +61,7 @@ public static SecretsProvider getSecretsProvider() {
5161
* @return a {@link SSMProvider}
5262
*/
5363
public static SSMProvider getSsmProvider() {
54-
if (ssmProvider == null) {
55-
ssmProvider = SSMProvider.builder()
56-
.withCacheManager(cacheManager)
57-
.withTransformationManager(transformationManager)
58-
.build();
59-
}
60-
return ssmProvider;
64+
return getProvider(SSMProvider.class);
6165
}
6266

6367
/**
@@ -66,14 +70,11 @@ public static SSMProvider getSsmProvider() {
6670
* @return a {@link SecretsProvider}
6771
*/
6872
public static SecretsProvider getSecretsProvider(SecretsManagerClient client) {
69-
if (secretsProvider == null) {
70-
secretsProvider = SecretsProvider.builder()
71-
.withClient(client)
72-
.withCacheManager(cacheManager)
73-
.withTransformationManager(transformationManager)
74-
.build();
75-
}
76-
return secretsProvider;
73+
return (SecretsProvider) providers.computeIfAbsent(SecretsProvider.class, (k) -> SecretsProvider.builder()
74+
.withClient(client)
75+
.withCacheManager(cacheManager)
76+
.withTransformationManager(transformationManager)
77+
.build());
7778
}
7879

7980
/**
@@ -82,14 +83,11 @@ public static SecretsProvider getSecretsProvider(SecretsManagerClient client) {
8283
* @return a {@link SSMProvider}
8384
*/
8485
public static SSMProvider getSsmProvider(SsmClient client) {
85-
if (ssmProvider == null) {
86-
ssmProvider = SSMProvider.builder()
87-
.withClient(client)
88-
.withCacheManager(cacheManager)
89-
.withTransformationManager(transformationManager)
90-
.build();
91-
}
92-
return ssmProvider;
86+
return (SSMProvider) providers.computeIfAbsent(SSMProvider.class, (k) -> SSMProvider.builder()
87+
.withClient(client)
88+
.withCacheManager(cacheManager)
89+
.withTransformationManager(transformationManager)
90+
.build());
9391
}
9492

9593
public static CacheManager getCacheManager() {
@@ -99,4 +97,17 @@ public static CacheManager getCacheManager() {
9997
public static TransformationManager getTransformationManager() {
10098
return transformationManager;
10199
}
100+
101+
private static <T extends BaseProvider> T createProvider(Class<T> providerClass) {
102+
try {
103+
Constructor<T> constructor = providerClass.getDeclaredConstructor(CacheManager.class);
104+
T provider = constructor.newInstance(cacheManager);
105+
provider.setTransformationManager(transformationManager);
106+
return provider;
107+
} catch (NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException e) {
108+
throw new RuntimeException("Unexpected error occurred. Please raise issue at " +
109+
"https://github.com/awslabs/aws-lambda-powertools-java/issues", e);
110+
}
111+
}
112+
102113
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package software.amazon.lambda.powertools.parameters.internal;
2+
3+
import org.aspectj.lang.ProceedingJoinPoint;
4+
import org.aspectj.lang.annotation.Around;
5+
import org.aspectj.lang.annotation.Aspect;
6+
import org.aspectj.lang.annotation.Pointcut;
7+
import org.aspectj.lang.reflect.FieldSignature;
8+
import software.amazon.lambda.powertools.parameters.*;
9+
10+
@Aspect
11+
public class LambdaParametersAspect {
12+
13+
@Pointcut("get(* *) && @annotation(paramAnnotation)")
14+
public void getParam(Param paramAnnotation) {
15+
}
16+
17+
@Around("getParam(paramAnnotation)")
18+
public Object injectParam(final ProceedingJoinPoint joinPoint, final Param paramAnnotation) {
19+
if(null == paramAnnotation.provider()) {
20+
throw new IllegalArgumentException("provider for Param annotation cannot be null!");
21+
}
22+
BaseProvider provider = ParamManager.getProvider(paramAnnotation.provider());
23+
24+
if(paramAnnotation.transformer().isInterface()) {
25+
// No transformation
26+
return provider.get(paramAnnotation.key());
27+
} else {
28+
FieldSignature s = (FieldSignature) joinPoint.getSignature();
29+
if(String.class.isAssignableFrom(s.getFieldType())) {
30+
// Basic transformation
31+
return provider
32+
.withTransformation(paramAnnotation.transformer())
33+
.get(paramAnnotation.key());
34+
} else {
35+
// Complex transformation
36+
return provider
37+
.withTransformation(paramAnnotation.transformer())
38+
.get(paramAnnotation.key(), s.getFieldType());
39+
}
40+
}
41+
}
42+
43+
}

powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/ParamManagerIntegrationTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import java.util.ArrayList;
2929
import java.util.List;
3030
import java.util.Map;
31+
import java.util.concurrent.ConcurrentHashMap;
3132

3233
import static org.apache.commons.lang3.reflect.FieldUtils.writeStaticField;
3334
import static org.assertj.core.api.Assertions.assertThat;
@@ -57,8 +58,7 @@ public class ParamManagerIntegrationTest {
5758
public void setup() throws IllegalAccessException {
5859
openMocks(this);
5960

60-
writeStaticField(ParamManager.class, "ssmProvider", null, true);
61-
writeStaticField(ParamManager.class, "secretsProvider", null, true);
61+
writeStaticField(ParamManager.class, "providers", new ConcurrentHashMap<>(), true);
6262
}
6363

6464
@Test
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package software.amazon.lambda.powertools.parameters.internal;
2+
3+
public class AnotherObject {
4+
5+
public AnotherObject() {}
6+
7+
private String another;
8+
private int object;
9+
10+
public String getAnother() {
11+
return another;
12+
}
13+
14+
public void setAnother(String another) {
15+
this.another = another;
16+
}
17+
18+
public int getObject() {
19+
return object;
20+
}
21+
22+
public void setObject(int object) {
23+
this.object = object;
24+
}
25+
}

0 commit comments

Comments
 (0)