New posts
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.ankurm</groupId>
|
||||
<artifactId>annotation-processor</artifactId>
|
||||
<version>1.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>builder-demo</artifactId>
|
||||
<name>builder-demo</name>
|
||||
<description>A consumer of @GenerateBuilder, plus the retention / reflection demos.</description>
|
||||
|
||||
<dependencies>
|
||||
<!-- Needed on the compile classpath so javac can resolve the @GenerateBuilder type.
|
||||
It is SOURCE-retention, so nothing needs it at run time: scope "provided". -->
|
||||
<dependency>
|
||||
<groupId>com.ankurm</groupId>
|
||||
<artifactId>builder-processor</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<!-- The processor is named here, on the PROCESSOR path - not just on the classpath.
|
||||
On JDK 23+ a processor found only on the classpath is not run by default. -->
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>com.ankurm</groupId>
|
||||
<artifactId>builder-processor</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<profiles>
|
||||
<!-- For the article only: drops <annotationProcessorPaths> so the processor is on the classpath
|
||||
(it is a "provided" dependency) but NOT on the processor path. Used by scripts/run-all.sh. -->
|
||||
<profile>
|
||||
<id>classpath-only</id>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<annotationProcessorPaths combine.self="override"/>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
<!-- Same, plus an explicit request to run the processors found on the classpath. -->
|
||||
<profile>
|
||||
<id>classpath-proc-full</id>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<annotationProcessorPaths combine.self="override"/>
|
||||
<proc>full</proc>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
</project>
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.ankurm.demo;
|
||||
|
||||
/** Uses the generated PersonBuilder and PizzaMaker. Neither class exists in src/main/java. */
|
||||
public final class BuilderUsageDemo {
|
||||
public static void main(String[] args) {
|
||||
Person p = new PersonBuilder()
|
||||
.name("Asha")
|
||||
.age(34)
|
||||
.addToNicknames("Ash")
|
||||
.addToNicknames("A")
|
||||
.build();
|
||||
System.out.println(p);
|
||||
|
||||
Pizza pizza = new PizzaMaker()
|
||||
.withSize("large")
|
||||
.withExtraCheese(true)
|
||||
.addToToppings("olives")
|
||||
.build();
|
||||
System.out.println(pizza);
|
||||
|
||||
// Unset properties take the Java default for their type: null for references, 0 for int.
|
||||
System.out.println(new PersonBuilder().build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.ankurm.demo;
|
||||
|
||||
import com.ankurm.builder.GenerateBuilder;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** A record: the builder is generated from its canonical constructor. */
|
||||
@GenerateBuilder
|
||||
public record Person(String name, int age, List<String> nicknames) {
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.ankurm.demo;
|
||||
|
||||
import com.ankurm.builder.GenerateBuilder;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** A plain class, with options on the annotation: builder name and a "with" prefix. */
|
||||
@GenerateBuilder(name = "PizzaMaker", prefix = "with")
|
||||
public final class Pizza {
|
||||
private final String size;
|
||||
private final boolean extraCheese;
|
||||
private final List<String> toppings;
|
||||
|
||||
public Pizza(String size, boolean extraCheese, List<String> toppings) {
|
||||
this.size = size;
|
||||
this.extraCheese = extraCheese;
|
||||
this.toppings = toppings == null ? List.of() : List.copyOf(toppings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Pizza[size=" + size + ", extraCheese=" + extraCheese + ", toppings=" + toppings + "]";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.ankurm.demo;
|
||||
|
||||
import com.ankurm.demo.annotations.ClassOnly;
|
||||
import com.ankurm.demo.annotations.Level;
|
||||
import com.ankurm.demo.annotations.Role;
|
||||
import com.ankurm.demo.annotations.Roles;
|
||||
import com.ankurm.demo.annotations.SourceOnly;
|
||||
import com.ankurm.demo.annotations.Tracked;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/** What reflection can and cannot see, plus members, defaults, @Inherited and @Repeatable. */
|
||||
public final class RetentionDemo {
|
||||
|
||||
@SourceOnly @ClassOnly @Tracked
|
||||
static class Base { }
|
||||
|
||||
static class Child extends Base { }
|
||||
|
||||
@Role("admin") @Role("auditor")
|
||||
static class Staff { }
|
||||
|
||||
@Role("admin")
|
||||
static class Solo { }
|
||||
|
||||
@Tracked(value = "billing", priority = 9, level = Level.HIGH, tags = {"pci", "money"})
|
||||
static class Configured { }
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println("-- retention: annotations reflection can see on Base");
|
||||
System.out.println(Arrays.toString(Base.class.getAnnotations()));
|
||||
|
||||
System.out.println("-- members and defaults");
|
||||
Tracked t = Base.class.getAnnotation(Tracked.class);
|
||||
System.out.println("default value=" + quote(t.value()) + " priority=" + t.priority()
|
||||
+ " level=" + t.level() + " tags=" + Arrays.toString(t.tags()));
|
||||
Tracked c = Configured.class.getAnnotation(Tracked.class);
|
||||
System.out.println("explicit value=" + quote(c.value()) + " priority=" + c.priority()
|
||||
+ " level=" + c.level() + " tags=" + Arrays.toString(c.tags()));
|
||||
|
||||
System.out.println("-- @Inherited: Child extends Base, Child itself has no annotations");
|
||||
System.out.println("getAnnotation(Tracked) = " + (Child.class.getAnnotation(Tracked.class) != null));
|
||||
System.out.println("getDeclaredAnnotation(Tracked) = " + (Child.class.getDeclaredAnnotation(Tracked.class) != null));
|
||||
|
||||
System.out.println("-- @Repeatable");
|
||||
System.out.println("Staff direct getAnnotations().length = " + Staff.class.getAnnotations().length
|
||||
+ " (" + Staff.class.getAnnotations()[0].annotationType().getSimpleName() + " container)");
|
||||
System.out.println("Staff getAnnotationsByType(Role) = " + roleValues(Staff.class.getAnnotationsByType(Role.class)));
|
||||
System.out.println("Solo getAnnotation(Role) != null = " + (Solo.class.getAnnotation(Role.class) != null));
|
||||
System.out.println("Solo getAnnotation(Roles) != null = " + (Solo.class.getAnnotation(Roles.class) != null));
|
||||
System.out.println("Solo getAnnotationsByType(Role) = " + roleValues(Solo.class.getAnnotationsByType(Role.class)));
|
||||
}
|
||||
|
||||
private static String quote(String s) {
|
||||
return "\"" + s + "\"";
|
||||
}
|
||||
|
||||
private static String roleValues(Role[] roles) {
|
||||
return Arrays.stream(roles).map(Role::value).toList().toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.ankurm.demo;
|
||||
|
||||
import com.ankurm.demo.annotations.NotBlank;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** The runtime-reflection style: discover annotated fields while the program runs. */
|
||||
public final class RuntimeValidatorDemo {
|
||||
|
||||
static class Signup {
|
||||
@NotBlank String email;
|
||||
@NotBlank(message = "name is required") String name;
|
||||
String note; // not annotated: ignored
|
||||
Signup(String email, String name, String note) { this.email = email; this.name = name; this.note = note; }
|
||||
}
|
||||
|
||||
static List<String> validate(Object o) throws IllegalAccessException {
|
||||
List<String> problems = new ArrayList<>();
|
||||
for (Field f : o.getClass().getDeclaredFields()) {
|
||||
NotBlank nb = f.getAnnotation(NotBlank.class);
|
||||
if (nb == null) continue;
|
||||
f.setAccessible(true);
|
||||
Object v = f.get(o);
|
||||
if (v == null || v.toString().isBlank()) {
|
||||
problems.add(f.getName() + ": " + nb.message());
|
||||
}
|
||||
}
|
||||
return problems;
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
System.out.println(validate(new Signup("[email protected]", "Asha", null)));
|
||||
System.out.println(validate(new Signup("", " ", null)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.ankurm.demo.annotations;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
/** CLASS (also the default when @Retention is omitted): in the .class file, invisible to reflection. */
|
||||
@Retention(RetentionPolicy.CLASS)
|
||||
public @interface ClassOnly {
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.ankurm.demo.annotations;
|
||||
|
||||
public enum Level { LOW, NORMAL, HIGH }
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.ankurm.demo.annotations;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/** Marker for the reflection-based validator in {@code RuntimeValidatorDemo}. */
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.FIELD)
|
||||
public @interface NotBlank {
|
||||
String message() default "must not be blank";
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.ankurm.demo.annotations;
|
||||
|
||||
import java.lang.annotation.Repeatable;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
/** Repeatable: javac wraps two or more of these into one {@link Roles} container. */
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Repeatable(Roles.class)
|
||||
public @interface Role {
|
||||
String value();
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.ankurm.demo.annotations;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
/** The container annotation that {@link Role} is repeatable into. */
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface Roles {
|
||||
Role[] value();
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.ankurm.demo.annotations;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
/** SOURCE: discarded by javac after processing. Not in the .class file at all. */
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
public @interface SourceOnly {
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.ankurm.demo.annotations;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Inherited;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* RUNTIME: readable through reflection. Shows member types and defaults, plus
|
||||
* {@code @Documented}, {@code @Inherited} and {@code @Target}.
|
||||
*/
|
||||
@Documented
|
||||
@Inherited
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
|
||||
public @interface Tracked {
|
||||
String value() default ""; // "value" lets you write @Tracked("x")
|
||||
int priority() default 5;
|
||||
Level level() default Level.NORMAL;
|
||||
String[] tags() default {};
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.ankurm.demo;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/** Runtime tests of classes that the annotation processor wrote. They would not compile if it had not run. */
|
||||
class GeneratedBuilderTest {
|
||||
|
||||
@Test
|
||||
void recordBuilderSetsEveryComponent() {
|
||||
Person p = new PersonBuilder().name("Asha").age(34).addToNicknames("Ash").addToNicknames("A").build();
|
||||
assertEquals("Asha", p.name());
|
||||
assertEquals(34, p.age());
|
||||
assertEquals(List.of("Ash", "A"), p.nicknames());
|
||||
}
|
||||
|
||||
@Test
|
||||
void unsetPropertiesTakeJavaDefaults() {
|
||||
Person p = new PersonBuilder().build();
|
||||
assertNull(p.name());
|
||||
assertEquals(0, p.age());
|
||||
assertNull(p.nicknames());
|
||||
}
|
||||
|
||||
@Test
|
||||
void prefixAndCustomNameFromAnnotationMembersAreHonoured() {
|
||||
Pizza pizza = new PizzaMaker().withSize("large").withExtraCheese(true).addToToppings("olives").build();
|
||||
assertEquals("Pizza[size=large, extraCheese=true, toppings=[olives]]", pizza.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void addToDoesNotMutateAListHandedToTheSetter() {
|
||||
List<String> original = List.of("x");
|
||||
Person p = new PersonBuilder().nicknames(original).addToNicknames("y").build();
|
||||
assertEquals(List.of("x"), original);
|
||||
assertEquals(List.of("x", "y"), p.nicknames());
|
||||
}
|
||||
|
||||
@Test
|
||||
void sourceRetentionLeavesNoTraceInTheClassFile() throws Exception {
|
||||
assertNotNull(Class.forName("com.ankurm.demo.PersonBuilder"));
|
||||
assertTrue(Person.class.getAnnotations().length == 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.ankurm.demo;
|
||||
|
||||
import com.ankurm.demo.annotations.Level;
|
||||
import com.ankurm.demo.annotations.Role;
|
||||
import com.ankurm.demo.annotations.Roles;
|
||||
import com.ankurm.demo.annotations.Tracked;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
class RetentionReflectionTest {
|
||||
|
||||
@Test
|
||||
void onlyRuntimeAnnotationsAreVisibleToReflection() {
|
||||
var annotations = RetentionDemo.Base.class.getAnnotations();
|
||||
assertEquals(1, annotations.length);
|
||||
assertEquals(Tracked.class, annotations[0].annotationType());
|
||||
}
|
||||
|
||||
@Test
|
||||
void membersFallBackToDefaults() {
|
||||
Tracked t = RetentionDemo.Base.class.getAnnotation(Tracked.class);
|
||||
assertEquals("", t.value());
|
||||
assertEquals(5, t.priority());
|
||||
assertEquals(Level.NORMAL, t.level());
|
||||
assertEquals(0, t.tags().length);
|
||||
}
|
||||
|
||||
@Test
|
||||
void inheritedAnnotationsAreVisibleOnSubclassesButNotDeclaredThere() {
|
||||
assertNotNull(RetentionDemo.Child.class.getAnnotation(Tracked.class));
|
||||
assertNull(RetentionDemo.Child.class.getDeclaredAnnotation(Tracked.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void repeatedAnnotationsAreWrappedInTheContainer() {
|
||||
assertEquals(Roles.class, RetentionDemo.Staff.class.getAnnotations()[0].annotationType());
|
||||
assertNull(RetentionDemo.Staff.class.getAnnotation(Role.class));
|
||||
assertEquals(List.of("admin", "auditor"),
|
||||
java.util.Arrays.stream(RetentionDemo.Staff.class.getAnnotationsByType(Role.class)).map(Role::value).toList());
|
||||
assertNotNull(RetentionDemo.Solo.class.getAnnotation(Role.class)); // a single one is not wrapped
|
||||
}
|
||||
|
||||
@Test
|
||||
void runtimeValidatorFindsBlankFields() throws Exception {
|
||||
var s = new RuntimeValidatorDemo.Signup("", " ", null);
|
||||
assertEquals(List.of("email: must not be blank", "name: name is required"), RuntimeValidatorDemo.validate(s));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user