Skip to content

Commit 7b349ea

Browse files
authored
#749 Add support for a @PreDestroy method on a factory, alternative to @bean(destroyMethod) (#756)
Example: ``` @factory final class MyFactory { @bean MyComponent myComponent() { ... } @PreDestroy(priority = 3000) void destroyMyComponent(MyComponent bean) { ... } ``` - ONLY valid on factory components - An alternative to @bean(destroyMethod) - The @PreDestroy method matches to a @bean by type only (ignores qualifiers, only 1 argument to the destroy method)
1 parent e6255b8 commit 7b349ea

File tree

11 files changed

+196
-20
lines changed

11 files changed

+196
-20
lines changed

blackbox-test-inject/src/main/java/org/example/myapp/MyNestedDestroy.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@
44

55
public class MyNestedDestroy {
66

7-
public static AtomicInteger started = new AtomicInteger();
8-
public static AtomicInteger stopped = new AtomicInteger();
7+
public static AtomicInteger STARTED = new AtomicInteger();
8+
public static AtomicInteger STOPPED = new AtomicInteger();
99

1010
public static void reset() {
11-
started.set(0);
12-
stopped.set(0);
11+
STARTED.set(0);
12+
STOPPED.set(0);
1313
}
1414

1515
public void start() {
16-
started.incrementAndGet();
16+
STARTED.incrementAndGet();
1717
}
1818

1919
public Reaper reaper() {
@@ -23,7 +23,7 @@ public Reaper reaper() {
2323
public static class Reaper {
2424

2525
public void stop() {
26-
stopped.incrementAndGet();
26+
STOPPED.incrementAndGet();
2727
}
2828
}
2929
}

blackbox-test-inject/src/main/java/org/example/myapp/config/AFactory.java

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,49 @@
22

33
import io.avaje.inject.Bean;
44
import io.avaje.inject.Factory;
5+
import io.avaje.inject.PreDestroy;
56
import org.example.myapp.MyNestedDestroy;
67

78
import java.io.IOException;
9+
import java.util.concurrent.atomic.AtomicInteger;
810

911
@Factory
10-
class AFactory {
12+
public class AFactory {
13+
14+
public static AtomicInteger DESTROY_COUNT_BEAN = new AtomicInteger();
15+
public static AtomicInteger DESTROY_COUNT_COMPONENT = new AtomicInteger();
16+
public static AtomicInteger DESTROY_COUNT_AFOO = new AtomicInteger();
17+
18+
public static void reset() {
19+
DESTROY_COUNT_BEAN.set(0);
20+
DESTROY_COUNT_AFOO.set(0);
21+
DESTROY_COUNT_COMPONENT.set(0);
22+
}
1123

1224
@Bean(initMethod = "start", destroyMethod = "reaper().stop()")
1325
MyNestedDestroy lifecycle2() {
1426
return new MyNestedDestroy();
1527
}
1628

29+
@PreDestroy(priority = 3000)
30+
void dest2(MyNestedDestroy bean) {
31+
DESTROY_COUNT_BEAN.incrementAndGet();
32+
}
33+
34+
// // compiler error when type does not match any @Bean method
35+
// @PreDestroy(priority = 3001)
36+
// void destErr(Object bean) {
37+
//
38+
// }
39+
40+
/**
41+
* NO args so this is the normal factory component destroy method.
42+
*/
43+
@PreDestroy
44+
void factoryDestroy() {
45+
DESTROY_COUNT_COMPONENT.incrementAndGet();
46+
}
47+
1748
@Bean
1849
A0.Builder build0() {
1950
return new I0();
@@ -40,6 +71,11 @@ AFoo buildAfoo() throws IOException {
4071
return new AFoo();
4172
}
4273

74+
@PreDestroy
75+
void destAfoo(AFoo bean) {
76+
DESTROY_COUNT_AFOO.incrementAndGet();
77+
}
78+
4379
static class I0 implements A0.Builder {
4480

4581
}

blackbox-test-inject/src/test/java/org/example/myapp/HelloServiceTest.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import io.avaje.inject.aop.InvocationException;
55
import org.example.myapp.aspect.MyAroundAspect;
66
import org.example.myapp.aspect.MyMultiInvokeAspect;
7+
import org.example.myapp.config.AFactory;
78
import org.junit.jupiter.api.Test;
89

910
import java.io.IOException;
@@ -17,12 +18,19 @@ class HelloServiceTest {
1718
@Test
1819
void lifecycles() {
1920
MyNestedDestroy.reset();
21+
AFactory.reset();
2022
try (BeanScope beanScope = BeanScope.builder().build()) {
2123
assertThat(beanScope.get(MyNestedDestroy.class)).isNotNull();
22-
assertThat(MyNestedDestroy.started.get()).isEqualTo(1);
23-
assertThat(MyNestedDestroy.stopped.get()).isEqualTo(0);
24+
assertThat(MyNestedDestroy.STARTED.get()).isEqualTo(1);
25+
assertThat(MyNestedDestroy.STOPPED.get()).isEqualTo(0);
26+
assertThat(AFactory.DESTROY_COUNT_BEAN.get()).isEqualTo(0);
27+
assertThat(AFactory.DESTROY_COUNT_AFOO.get()).isEqualTo(0);
28+
assertThat(AFactory.DESTROY_COUNT_COMPONENT.get()).isEqualTo(0);
2429
}
25-
assertThat(MyNestedDestroy.stopped.get()).isEqualTo(1);
30+
assertThat(MyNestedDestroy.STOPPED.get()).isEqualTo(1);
31+
assertThat(AFactory.DESTROY_COUNT_BEAN.get()).isEqualTo(1);
32+
assertThat(AFactory.DESTROY_COUNT_AFOO.get()).isEqualTo(1);
33+
assertThat(AFactory.DESTROY_COUNT_COMPONENT.get()).isEqualTo(1);
2634
}
2735

2836
/**

inject-generator/src/main/java/io/avaje/inject/generator/BeanReader.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,4 +583,8 @@ boolean needsTryForMethodInjection() {
583583
boolean isDelayed() {
584584
return delayed;
585585
}
586+
587+
void validate() {
588+
typeReader.validate();
589+
}
586590
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package io.avaje.inject.generator;
2+
3+
import javax.lang.model.element.Element;
4+
import javax.lang.model.element.ExecutableElement;
5+
import java.util.*;
6+
import java.util.stream.Collectors;
7+
8+
/**
9+
* Holds extra PreDestroy methods for a factory.
10+
* <p>
11+
* These methods are expected to relate back to a {@code @Bean} method
12+
* on the same factory.
13+
*/
14+
final class DestroyMethods {
15+
16+
private final Map<String, DestroyMethod> methods = new HashMap<>();
17+
private final Set<String> matchedTypes = new HashSet<>();
18+
19+
void add(ExecutableElement element) {
20+
Integer priority = PreDestroyPrism.getOptionalOn(element)
21+
.map(PreDestroyPrism::priority)
22+
.orElse(null);
23+
24+
var destroyMethod = new DestroyMethod(element, priority);
25+
methods.put(destroyMethod.matchType, destroyMethod);
26+
}
27+
28+
DestroyMethod match(String returnTypeRaw) {
29+
var match = methods.get(returnTypeRaw);
30+
if (match != null) {
31+
matchedTypes.add(returnTypeRaw);
32+
}
33+
return match;
34+
}
35+
36+
/**
37+
* Return PreDestroy methods that were not matched to a {@code @Bean} method
38+
* on the same factory.
39+
*/
40+
List<DestroyMethod> unmatched() {
41+
return methods.values()
42+
.stream()
43+
.filter(entry -> !matchedTypes.contains(entry.matchType()))
44+
.collect(Collectors.toList());
45+
}
46+
47+
static final class DestroyMethod {
48+
49+
private final String method;
50+
private final Integer priority;
51+
private final String matchType;
52+
private final ExecutableElement element;
53+
54+
DestroyMethod(ExecutableElement element, Integer priority) {
55+
this.element = element;
56+
this.method = element.getSimpleName().toString();
57+
this.matchType = element.getParameters().get(0).asType().toString();
58+
this.priority = priority;
59+
}
60+
61+
String method() {
62+
return method;
63+
}
64+
65+
Integer priority() {
66+
return priority;
67+
}
68+
69+
String matchType() {
70+
return matchType;
71+
}
72+
73+
Element element() {
74+
return element;
75+
}
76+
}
77+
}

inject-generator/src/main/java/io/avaje/inject/generator/MethodReader.java

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ final class MethodReader {
1515

1616
private static final String CODE_COMMENT_BUILD_FACTORYBEAN = " /**\n * Create and register %s via factory bean method %s#%s().\n */";
1717

18+
private final TypeExtendsInjection factory;
1819
private final ExecutableElement element;
1920
private final String factoryType;
2021
private final String methodName;
@@ -40,10 +41,11 @@ final class MethodReader {
4041
private MethodParam observeParameter;
4142

4243
MethodReader(ExecutableElement element, TypeElement beanType, ImportTypeMap importTypes) {
43-
this(element, beanType, null, null, importTypes);
44+
this(null, element, beanType, null, null, importTypes);
4445
}
4546

46-
MethodReader(ExecutableElement element, TypeElement beanType, BeanPrism bean, String qualifierName, ImportTypeMap importTypes) {
47+
MethodReader(TypeExtendsInjection factory, ExecutableElement element, TypeElement beanType, BeanPrism bean, String qualifierName, ImportTypeMap importTypes) {
48+
this.factory = factory;
4749
this.element = element;
4850
if (bean != null) {
4951
prototype = PrototypePrism.isPresent(element);
@@ -255,8 +257,8 @@ void builderBuildAddBean(Append writer) {
255257
}
256258
String indent = optionalType ? " " : " ";
257259
writer.indent(indent);
258-
var hasLifecycleMethods = hasLifecycleMethods();
259-
260+
var matchedPreDestroyMethod = factory.matchPreDestroy(returnTypeRaw);
261+
var hasLifecycleMethods = matchedPreDestroyMethod != null || hasLifecycleMethods();
260262
if (hasLifecycleMethods && multiRegister) {
261263
writer.append("bean.stream()").eol().indent(indent).append(" .map(");
262264
} else if (hasLifecycleMethods) {
@@ -292,7 +294,7 @@ void builderBuildAddBean(Append writer) {
292294
writer.indent(indent).append(addPostConstruct, initMethod).eol();
293295
}
294296

295-
var priority = destroyPriority == null || destroyPriority == 1000 ? "" : ", " + destroyPriority;
297+
var priority = priority(destroyPriority);
296298
if (notEmpty(destroyMethod)) {
297299
var addPreDestroy =
298300
multiRegister
@@ -317,13 +319,27 @@ void builderBuildAddBean(Append writer) {
317319
} else if (multiRegister && hasInitMethod) {
318320
writer.indent(indent).append(" .forEach(x -> {});").eol();
319321
}
322+
if (matchedPreDestroyMethod != null) {
323+
// PreDestroy method on the factory
324+
var addPreDestroy =
325+
multiRegister
326+
? " .forEach($bean -> builder.addPreDestroy(%s%s));"
327+
: "builder.addPreDestroy(%s%s);";
328+
var methodPriority = priority(matchedPreDestroyMethod.priority());
329+
var method = String.format("() -> factory.%s($bean)", matchedPreDestroyMethod.method());
330+
writer.indent(indent).append(addPreDestroy, method, methodPriority).eol();
331+
}
320332

321333
if (optionalType) {
322334
writer.append(" }").eol();
323335
}
324336
}
325337
}
326338

339+
static String priority(Integer priority) {
340+
return priority == null || priority == 1000 ? "" : ", " + priority;
341+
}
342+
327343
static String addPreDestroy(String destroyMethod) {
328344
if (!destroyMethod.contains(".")) {
329345
return "$bean::" + destroyMethod;

inject-generator/src/main/java/io/avaje/inject/generator/SimpleBeanWriter.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ private void writeStaticFactoryBeanMethods() {
135135
for (MethodReader factoryMethod : beanReader.factoryMethods()) {
136136
writeFactoryBeanMethod(factoryMethod);
137137
}
138+
beanReader.validate();
138139
}
139140

140141
private void writeFactoryBeanMethod(MethodReader method) {

inject-generator/src/main/java/io/avaje/inject/generator/TypeExtendsInjection.java

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.avaje.inject.generator;
22

3+
import static io.avaje.inject.generator.APContext.logError;
34
import static io.avaje.inject.generator.ProcessingContext.getImportedAspect;
45

56
import javax.lang.model.element.AnnotationMirror;
@@ -30,6 +31,7 @@ final class TypeExtendsInjection {
3031
private final TypeElement baseType;
3132
private final boolean factory;
3233
private final List<AspectPair> typeAspects;
34+
private final DestroyMethods factoryPreDestroyMethods = new DestroyMethods();
3335
private Optional<MethodReader> postConstructMethod = Optional.empty();
3436
private Element preDestroyMethod;
3537
private Integer preDestroyPriority;
@@ -129,9 +131,17 @@ private void readMethod(Element element, TypeElement type) {
129131
checkAspect = false;
130132
}
131133
if (AnnotationUtil.hasAnnotationWithName(element, "PreDestroy")) {
132-
preDestroyMethod = element;
133-
checkAspect = false;
134-
PreDestroyPrism.getOptionalOn(element).ifPresent(preDestroy -> preDestroyPriority = preDestroy.priority());
134+
int paramCount = methodElement.getParameters().size();
135+
if (paramCount == 0) {
136+
// normal component PreDestroy method
137+
preDestroyMethod = element;
138+
checkAspect = false;
139+
PreDestroyPrism.getOptionalOn(element).ifPresent(preDestroy -> preDestroyPriority = preDestroy.priority());
140+
} else if (paramCount == 1 && factory) {
141+
// additional factory PreDestroy to match a @Bean method
142+
checkAspect = false;
143+
factoryPreDestroyMethods.add(methodElement);
144+
}
135145
}
136146
if (checkAspect) {
137147
checkForAspect(methodElement);
@@ -163,10 +173,9 @@ private void checkForAspect(ExecutableElement methodElement) {
163173
}
164174
}
165175

166-
167176
private void addFactoryMethod(ExecutableElement methodElement, BeanPrism bean) {
168177
String qualifierName = Util.named(methodElement);
169-
factoryMethods.add(new MethodReader(methodElement, baseType, bean, qualifierName, importTypes).read());
178+
factoryMethods.add(new MethodReader(this, methodElement, baseType, bean, qualifierName, importTypes).read());
170179
}
171180

172181
BeanAspects hasAspects() {
@@ -238,4 +247,14 @@ MethodReader constructor() {
238247
}
239248
return null;
240249
}
250+
251+
DestroyMethods.DestroyMethod matchPreDestroy(String returnTypeRaw) {
252+
return factoryPreDestroyMethods.match(returnTypeRaw);
253+
}
254+
255+
void validate() {
256+
for (DestroyMethods.DestroyMethod destroyMethod : factoryPreDestroyMethods.unmatched()) {
257+
logError(destroyMethod.element(), "Unused @PreDestroy method, no matching @Bean method for type " + destroyMethod.matchType());
258+
}
259+
}
241260
}

inject-generator/src/main/java/io/avaje/inject/generator/TypeExtendsReader.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,4 +263,8 @@ private void readInterfacesOf(TypeMirror anInterface) {
263263
private boolean isPublic(Element element) {
264264
return element != null && element.getModifiers().contains(Modifier.PUBLIC);
265265
}
266+
267+
void validate() {
268+
extendsInjection.validate();
269+
}
266270
}

inject-generator/src/main/java/io/avaje/inject/generator/TypeReader.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,4 +165,8 @@ void extraImports(ImportTypeMap importTypes) {
165165
genericTypes.forEach(t -> importTypes.addAll(t.importTypes()));
166166
}
167167
}
168+
169+
void validate() {
170+
extendsReader.validate();
171+
}
168172
}

inject-generator/src/test/java/io/avaje/inject/generator/MethodReaderTest.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,11 @@ void addPreDestroy() {
1616
void addPreDestroyNested() {
1717
assertThat(MethodReader.addPreDestroy("foo().bar()")).isEqualTo("() -> $bean.foo().bar()");
1818
}
19+
20+
@Test
21+
void priority() {
22+
assertThat(MethodReader.priority(null)).isEqualTo("");
23+
assertThat(MethodReader.priority(1000)).isEqualTo("");
24+
assertThat(MethodReader.priority(52)).isEqualTo(", 52");
25+
}
1926
}

0 commit comments

Comments
 (0)