eigene Annotation erstellen die einen Konstruktor erzeugt:

MJannek

Bekanntes Mitglied
Ich habe in einer Java Library (lombok) gesehen, dass mit Annotations Konstruktoren erstellen kann. Das gefällt mir sehr. Jedoch besteht in dieser Bibliothek, nicht die Möglichkeit, mit einer Annotation folgendes zu erzeugen.
Java:
 // Constructor for setting id
    public ClassName(Integer id) {
        super(id);
    }
Ich habe mir dazu folgendes erstellt.
Java:
package org.mjannek.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface GenerateConstructor {
}


package org.mjannek.annotation;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.ElementFilter;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
import java.io.IOException;
import java.io.Writer;
import java.util.Set;

@SupportedAnnotationTypes("org.mjannek.annotation.GenerateConstructor")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class GenerateConstructorProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (Element element : roundEnv.getElementsAnnotatedWith(GenerateConstructor.class)) {
            if (element.getKind().isClass()) {
                TypeElement typeElement = (TypeElement) element;
                String className = typeElement.getSimpleName().toString();
                String packageName = processingEnv.getElementUtils().getPackageOf(typeElement).toString();

                if (!hasConstructor(typeElement)) {
                    String constructor = String.format("public %s(Integer id) { super(id); }", className);
                    String source = String.format("package %s;\n\npublic class %s {\n%s\n}", packageName, className, constructor);

                    try {
                        JavaFileObject file = processingEnv.getFiler().createSourceFile(packageName + "." + className);
                        try (Writer writer = file.openWriter()) {
                            writer.write(source);
                        }
                    } catch (IOException e) {
                        processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Error generating constructor: " + e.getMessage());
                    }
                }
            }
        }
        return true;
    }

    private boolean hasConstructor(TypeElement typeElement) {
        return ElementFilter.constructorsIn(typeElement.getEnclosedElements())
                .stream()
                .anyMatch(constructor -> constructor.getModifiers().contains(Modifier.PUBLIC));
    }
}

Ich habe folgende Basisklasse:
Java:
package org.mjannek.database.entity;


import java.io.*;
import java.sql.*;
import java.time.*;
import javax.persistence.*;
import javax.persistence.ForeignKey;
import javax.persistence.Table;
import lombok.*;
import org.hibernate.annotations.*;
import org.mjannek.sport.buli.database.table.*;

@MappedSuperclass
@FilterDef(name = "deletedFilter", parameters = @ParamDef(name = "isDeleted", type = "boolean"))
@Filter(name = "deletedFilter", condition = "deleted = :isDeleted")
@Getter
@Setter
@ToString
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public abstract class BaseEntity implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @EqualsAndHashCode.Include
    private Integer id;

    @Column(name = "createdat", updatable = false, nullable = false)
    private Timestamp createdAt;

    @Column(name = "updatedat", nullable = false)
    private Timestamp updatedAt;

    @Column(name = "deleted")
    private Boolean deleted = false;

    @ManyToOne
    @JoinColumn(name = "createdby", updatable = false, foreignKey = @ForeignKey(name = "fk_baseentity_createdby"))
    private User createdBy;

    @ManyToOne
    @JoinColumn(name = "updatedby", foreignKey = @ForeignKey(name = "fk_baseentity_updatedby"))
    private User updatedBy;

    @Column(name = "deletedat")
    private Timestamp deletedAt;

    @ManyToOne
    @JoinColumn(name = "deletedby", foreignKey = @ForeignKey(name = "fk_baseentity_deletedby"))
    private User deletedBy;

    @Version
    private Integer version;

    // Basis-Konstruktor
    public BaseEntity() {
        this.deleted = false;
    }

    public BaseEntity(Integer id) {
        this.id = id;
        this.deleted = false;
    }

    @PrePersist
    protected void onCreate() {
        this.createdAt = Timestamp.valueOf(LocalDateTime.now());
        this.updatedAt = Timestamp.valueOf(LocalDateTime.now());
    }

    @PreUpdate
    protected void onUpdate() {
        this.updatedAt = Timestamp.valueOf(LocalDateTime.now());
    }


    // Utility Methods
    public void markAsDeleted(User deletedByUser) {
        this.deleted = true;
        this.deletedAt = Timestamp.valueOf(LocalDateTime.now());
        this.deletedBy = deletedByUser;
    }

    public void restore() {
        this.deleted = false;
        this.deletedAt = null;
        this.deletedBy = null;
        this.updatedAt = Timestamp.valueOf(LocalDateTime.now());
    }
    public String getTableName() {
        // Überprüfen, ob die Annotation @Table vorhanden ist
        Table tableAnnotation = this.getClass().getAnnotation(Table.class);
        if (tableAnnotation != null) {
            return tableAnnotation.name(); // Gibt den Tabellennamen zurück
        }
        return this.getClass().getSimpleName(); // Fallback, falls keine @Table-Annotation vorhanden ist
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        BaseEntity that = (BaseEntity) o;
        return id != null && id.equals(that.id);
    }

    @Override
    public int hashCode() {
        return getClass().hashCode();
    }
}

Meine Klasse, die BaseEntity erweitert sieht wie folgt aus:
Java:
package org.mjannek.sport.buli.database.table;

import javax.persistence.*;
import lombok.*;
import org.mjannek.annotation.*;
import org.mjannek.database.entity.*;

@GenerateConstructor
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@Getter
@Setter
@Entity
@Table(name = "clubtypes")
public class ClubType extends BaseEntity {

    @Column(name = "name", nullable = false)
    private String name;

    @Column(name = "description", columnDefinition = "varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci")
    private String description;

    // Constructor for setting id
    public ClubType(Integer id) {
        super(id);
    }
}

Ich habe außerdem och im Verzeichnis main/ressources/META-INF/services: eine Datei namens: javax.annotation.processing.Processor, die folgendes enthält:
Java:
org.mjannek.annotation.GenerateConstructorProcessor
Meine Build.gradle.kts sieht wiefolgt aus:
Code:
plugins {
    id("java")
    id("application")
}

group = "org.mjannek"
version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.hibernate:hibernate-core:5.+")
    implementation("org.reflections:reflections:0.10.2")
    implementation("javax.persistence:javax.persistence-api:2.2")
    implementation("org.jsoup:jsoup:1.18.3")
    implementation("com.sun.mail:javax.mail:1.6.2")
    implementation("com.mchange:c3p0:0.9.5.5")
    implementation("jakarta.validation:jakarta.validation-api:2.0.2")
    implementation("ch.qos.logback:logback-classic:1.4.12")
    implementation("mysql:mysql-connector-java:8.+")
    implementation("org.projectlombok:lombok:1.18.22")
    testImplementation(platform("org.junit:junit-bom:5.10.0"))
    testImplementation("org.junit.jupiter:junit-jupiter")
}

tasks.test {
    useJUnitPlatform()
}

Wenn ich in der Main Klasse folgendes aufrufe erhalte ich trotz mehrfachem build folgende Fehlermeldung:
Java:
package org.mjannek;

import org.mjannek.sport.buli.database.table.*;

public class Main {
    public static void main(String[] args) {
        ClubType ct = new ClubType(1);
}
}

Fehlerneldung:
Fehler: Konstruktor ClubType in Klasse ClubType kann nicht auf die angegebenen Typen angewendet werden.
ClubType ct = new ClubType(1);
^
Erforderlich: keine Argumente
Ermittelt: int
Grund: Liste der tatsächlichen Argumente hat eine andere Länge als die der formalen Argumente

Wo ist mein Fehler?
Vielen Dank im Voraus.
 
Bei dir ist irgendwie etwas falsch 😉 - Ich sage es einfach, wie es ist.

Du findest eine LIP, die dir ENORM viel Tipparbeit abnimmt und schaffst es dann eine Funktion zu finden, die diese API wiederrum sehr aufwändig nur schafft umsusetzen.

Falls man möchte: https://projectlombok.org/features/constructor

Wie dem auh sei: Deine Kentnisse von Java würde ich eher als "besser" einsortieren. Immerhin produzierst du innerhalb kürzerster Zeit sehr viel Quellcode, der auch nicht mega trivial ist.

Jetzt möchtest du einen KOnstruktur, der nur EIN Paramter hat. Und jetzt nimmst du das komplizierterste Verfahren, um so einen Konstruktor zu erzeugen, den es gibt.

Entweder du schreibst: Mein Projekt ist mir egal: Ich möchte alles über Annotationen heraussuchen, was geht.

oder (und das habe ich verstanden) soll auch das Projekt irgendwie den Fokus haben.

Dann schreib den Konstruktor doch einfach hin. In der IDE: Rechter Mausklick -> Refactoring -> Create Construcor.

Feddich.

----

In dem anderen Fall: Das ist kompliziert: Das funktioniert bei Lombok nicht über Reflections, sondern einem Zusatz beim Compiler, der dann actually byte-Code hinzufügt. Dies dann wiederrum im Tandem mit der IDE, die dann diese nicht existierenden Methoden auch finden kann.

Ehrlich gesagt das ist alles , was ich weiß dazu.
 
Das Problem, welches ich mit so einer Annotation, die Code generieren soll, habe, ist einfach, dass die Entwicklungsumgebungen dieses erst einmal nicht unterstützen. Angenommen Du machst, was Lombok macht und dann hast Du ein Compiler Plugin, welches den Code für Dich generiert:
Das wird technisch funktionieren.

Aber:
Alle Tools, die das eben nicht kennen, werden schimpfen. Du rufst einen Konstruktor auf, den es aus Ihrer Sicht nicht gibt.

Also müsstest Du dann auch entsprechende Unterstützung schreiben - so wie es das auch bei Lombok gibt.

Da wäre es dann eher interessant, wenn man etwas baut, das Code generell baut. Du hast also keine Java Klassen in dem eigentlichen Sinne sondern irgend etwas aus das dann zur Build Zeit erst die Klassen werden. Das kann natürlich Java sein aber auch beliebige andere Formate wie XML oder YAML oder was auch immer Du haben willst. (Das ist gar nicht so unüblich. Bei Entities, die über Webservices übertragen werden, hat man dann eine Beschreibung und aus der kann man dann sehr viel generieren. SOAP Webservices (veraltet) hatten dazu das WSDL und heutzutage hat man die OpenAPI / Swagger, das man nutzen kann um alles zu beschreiben und um dann Code zu generieren.

Und dann ist die Fragestellung natürlich auch die fachliche Seite. Was soll ein Konstruktor, der nur eine id entgegen nimmt. Wie ist denn da das Szenario bezüglich Vergabe der IDs? Da Du etwas eigenes baust, sollte man das durchaus betrachten. Bei Hibernate und Co ist es oft so, dass Ids von der Datenbank durch eine Sequence vergeben wird. Eine Entität hat keine id bis diese dann gespeichert wurde und ab da gibt es dann die id.
Denn man muss ja sicher stellen, dass ids sauber vergeben werden. Wenn Du jetzt eine id schon zur Erstellung einer Entity brauchst, ist die Frage: Woher nimmst Du diese?
Wenn man keine zentral verwalteten Ids haben möchte, dann wäre ein anderer Datentyp wie UUID evtl. sinnvoller, so dass mehrere Systeme paralle IDs vergeben können ohne dass es zu Problemen durch doppelte IDs führt.
 
Bezüglich deinem Ansatz: Was Du machen willst, ist doch eine Transformation von dem Code, ehe er analysiert wird. Was Du aber statt dessen versuchst, ist eine Datei zu schreiben, die aber doch schon da ist und was - so Du alles richtig machen würdest - zu einer Exception wie
javax.annotation.processing.FilerException: Attempt to recreate a file for type test.DerivedEntity

Wie so eine AST Transformation aussehen könnte im Rahmen des Annotation Processing ist z.B. hier beschrieben:

Das kann man aber auch bei Lombok näher ansehen, aber da ist das halt alles etwas modularisiert mit diversen Handlern, die auch über Services eingebunden werden. Aber das würde man z.B. hier finden:
Aber es gibt halt viele zusätzliche Klassen, die diverse Aspekte behandeln wie z.B.
nur um da mal ein paar erste Ansätze aufzuzeigen.

Aber das nur um den technischen Weg etwas aufzuzeigen, wie es gehen würde. Der Weg von Dir mit JavaFileObject wäre nur dann interessant, wenn Du ein neues Sourcefile schreiben wolltest.
 
Bei dir ist irgendwie etwas falsch 😉 - Ich sage es einfach, wie es ist.

Du findest eine LIP, die dir ENORM viel Tipparbeit abnimmt und schaffst es dann eine Funktion zu finden, die diese API wiederrum sehr aufwändig nur schafft umsusetzen.

Falls man möchte: https://projectlombok.org/features/constructor

Wie dem auh sei: Deine Kentnisse von Java würde ich eher als "besser" einsortieren. Immerhin produzierst du innerhalb kürzerster Zeit sehr viel Quellcode, der auch nicht mega trivial ist.

Jetzt möchtest du einen KOnstruktur, der nur EIN Paramter hat. Und jetzt nimmst du das komplizierterste Verfahren, um so einen Konstruktor zu erzeugen, den es gibt.

Entweder du schreibst: Mein Projekt ist mir egal: Ich möchte alles über Annotationen heraussuchen, was geht.

oder (und das habe ich verstanden) soll auch das Projekt irgendwie den Fokus haben.

Dann schreib den Konstruktor doch einfach hin. In der IDE: Rechter Mausklick -> Refactoring -> Create Construcor.

Feddich.

----

In dem anderen Fall: Das ist kompliziert: Das funktioniert bei Lombok nicht über Reflections, sondern einem Zusatz beim Compiler, der dann actually byte-Code hinzufügt. Dies dann wiederrum im Tandem mit der IDE, die dann diese nicht existierenden Methoden auch finden kann.

Ehrlich gesagt das ist alles , was ich weiß dazu.
Ich nutze projectlombok. Mir ist lediglich auf, dass der von mir gewünschte Konstruktor, den ich in jeder Subklasse von BaseEntity benötige, nicht über eine Annotation dieser Bibliothek erzeugt werden kann. Daher habe ich versucht es selbst zu implementieren, bin dabei auf beschrieben Probleme gestoßen und habe deshalb nach Hinweisen gefragt. Es tut mir leid, wenn ich das falsch kommuniziert habe.
 
Wenn du lombok verwendest, dann kannst du Builder verwenden. Somit brauchst du den Aufwand für die "Vererbung" der Konstruktoren nicht zu betreiben:
MyEntity entity = MyEntity.builder().id(1)...build();
 

Zurück
Oben