Wątek przeniesiony 2023-07-01 09:22 z Java przez Riddle.

Procesor adnotacji

0

Cześć.

Jest to mój pierwszy post o programowaniu w ogóle. Uczę się od początku 2022r. z 6 miesięczną przerwą, czyli przeszło rok. Wcześniej nie miałem styczności z programowaniem. Jakiś czas temu przeszedłem z trybu nauki (bo mi się już brzydło) do płodzenia.
Coś tam skleciłem i prosił bym o code review. Przepraszam za readme ale godzine temu je spłodziłem. Oczywiście wszystko jest w fazie rozwoju ale udało mi się jedną funkcjonalność - w zasadzie tą która przyczyniła się do napisania procesora adnotacji - doprowadzić do jakiegoś tam końca. Po prostu działa. Testów nie ma....nie miałem pojęcia jak w TDD pisać procesor adnotacji (scenariusze sprawdzałem sobie Messagerem). Ale jakieś będą żeby nie podpaść.
Co on robi:
Przy pomocy adnotacji @AutoConfig na polach statycznych klasy tworzy plik properties który następnie sobie wypełniamy. Przy pomocy instrukcji. AutoInitializer.ConfigurationHandler.initialize(); pola są inicjalizowane propertisami.
Wcześniej trochę się bawiłem Mavenem buszując po jego zakamarkach. No i rozkminianie Tomcata i jego przepływu sterowania zajęło mi dużo czasu.
Jest on w pełni na zielonych wątkach (jedynie utilityexecutor jest na platformowym).
Za wszelkie porady będę bardzo wdzięczny. Jest to moja pierwsza poważna rzecz którą napisałem. Wcześniej były to jakieś głupiutkie konsolowe snippety które i tak dawały mi mnóstwo frajdy.
Branch release:

https://github.com/aujee84/bike-model-proto-vanilla/tree/release

Sam nie wiem jak na to wpadłem żeby to pisać. Miało być w Springu i co innego ;).

1

Czyli w sumie co to ma być? Bo nie do końca rozumiem jaki jest zamierzony efekt?

0

W klasie anotujemy pola statyczne niezainicjalizowane typu String, primitive i wrapper adnotacją @AutoConfig. Na podstawie tych pól tworzony jest plik application.properties gdzie key
to pakiet.klasa.nazwapola. Value jest w ${key}. Ten plik kopiujemy do src/main/resources ( czyli tam gdzie ma być). Uzupełniamy o wartości jak keyStoreFile, db password, url, hostname - czyli to co zazwyczaj ląduje w propertisach. Inicjalizacja tych pól odbywa się w runtimie podaną komendą. Jeśli readme jest niezrozumiałe to proszę o jakieś wskazówki. Pisałem je dosłownie 30 min przed umieszczeniem posta. Już mnie rozrywało żeby podać się ocenie.
Dodam jeszcze obsługę char[] i Character[] dla propertisów które nie powinny pływać w constant pool.
Zależy mi na ocenie tego jak piszę kod, czy jest to dobry styl. Co można by zmienić. Czy są jakiejś architektoniczne "ALE". Wiem że to jest bibliotekarstwo, ale zaczynając pisać coś poważniejszego w końcu stwierdziłem że zejdę do najniższego poziomu aby będąc na wyższych nie dostać lęku wysokości.
Miała być wyszukiwarka rowerów po różnych jego parametrach z publicznym api jako mikroserwis z jak największym throughput z db schema optymalizowanym w runtimie (postgres - bo wspiera Loom - brak native code i synchronized code block w najnowszym sterowniku). Poczytałem Twelve app factor i zacząłem radośnie tworzyć zaczynając od konfiguracji. I tyle.
Robie to aby wprawić się w javie. Mógłbym skorzystać z bibliotek, ale na pierwszy strzał nie...Jak najmniej bibliotek.
Powoli to idzie bo dużo muszę doczytywać. Samo wyszukanie czegoś takiego jak -J przed enable preview dla javaca zajęło mi cały dzień. (nigdzie nie ma informacji na temat puszczania jara z enable-preview)
Pakiet java.lang.invoke jest ciężki....bardzo ciężki. To już javax annotation processor jest lżejszy. Java poet jest fajna. Na tyle zrozumiałe że napisałem cały pakiet autoconfigurablegroup, ElementsProvider i ProcEnvironment i mały refactor z messegerem z pały i odpaliło za pierwszym razem. A już AutoConfigBase i AutoInitializer doprowadzał mnie do psychozy - cały dzień to uruchamiałem.

0

Było takie coś w pakiecie Javolution, co ciekawe też mocno dotyczyło pól statycznych, choć akurat nie polegało na adnotacjach a na parametryzowanej typem klasie
TAMTO było ciekawym soluszynem do dostarczania konfiguracji bez silnika wstrzykiwania, np wieki temu w desktopowych aplikacjach. W pewnym czasie znałem te 100-300 linii kodu na pamięć, proste a fajne to było
Statyczne dało prostotę implemenatcji, ale zwłaszcza w webie kłopotliwe / niewystarczające

Twoje ? .... kucze nie wiem, ten github zlewa wszystko Tomcat, adnotacje

0

Tak, sorry. Moja niecierpliwość wzięła górę. Zwyczajnie chciałem się poddać ocenie. Tomcat jest bo zrobiłem go wcześniej, a nie wydzieliłem procesora do osobnego projektu bo ...nie wiem, a powinienem i będzie. Na razie jest mi wygodnie, testuję sobie procesor na module tomcat. Nie dostarcza na razie nic poza generowaniem pliku properties i inicjalizowania pól statycznych. To co chciałem osiągnąłem - Postaram się w najbliższym czasie dostarczyć kompletny osobny projekt procesora adnotacji z testami end to end jako zależność. Do jakiej wersji javy zejść, 17?
Mam zamiar wprowadzić inicjalizacje pól, oprócz static on init, również w momencie tworzenia obiektu - na polach instancyjnych, może hiden class (dla security) , możliwość rozdzielania propertisów np osobno do db, osobno do serwera, wprowadzenie własnych nazw dla value oprócz defaultowego, oraz możliwość generowania nie tylko propertisów ale również yamla, tomla, confa, jsona.
Prosił bym o skupienie się na module lib.
Wyjaśnię może pokrótce dlaczego jest to tak rozbudowane i podzielone.
Przede wszystkim już w momencie tworzenia procesora wiedziałem że chcę mieć bezproblemową możliwość rozbudowy - dlatego też pojawiła się klasa SupportDispatcher która dostarcza odpowiednią implementację w zależności od wyszukanych przez roundenvironment anotowanych elementów dla obsługiwanej przez procesor adnotacji. Rozbudowa klasy rozszerzającej AbstractProcesor polegała na dostarczeniu jak największej ilości informacji użytkownikowi procesora. SupportDestiny prawdopodobnie nie będzie potrzebne. SupportDestinyPUBLIC odnosi się do generowanych klas z których użytkownik korzysta zanim zostały utworzone. Czyli coś w stylu - piszemy sobie kod i wszystko świeci na czerwono. Nie podoba mi się coś takiego, wprowadza chaos, ale zaimplementowałem taką możliwość - nic wielkiego ( wyczytałem od jakiegoś annotation processor guru na stackoverflow że nie należy przeszkadzać javacowi żadnymi wyjątkami kiedy generuje publicznie dostępny kod nawet jeśli użytkownik popełnił błędy. Javac a w zasadzie procesor w kolejnych rundach będzie podejmował kolejne próby wygenerowania jak największej ilości kodu. Użytkownik jak coś spieprzy to dostanie trochę mniej funkcjonalności - ale dostanie.)
UtilBucket zawiera niepowiązane z żadną klasą helper metody - pojawił się później w momencie refactoringu i rozdzielenia odpowiedzialności. ProcLogger również, chciałem wywalić z klas wszystkie messegery (zwyczajnie opakować w coś znanego i od razu kojarzonego jak Logger) . ProcEnvironment również pojawił się później. Pojawił się dlatego że wydziela środowisko dla obsługi wyszukanych elementów które jest tylko jedno w cyklu życia procesora - jest thread safe. SourceBuilder jak sama nazwa wskazuje odpowiada tylko za zbudowanie klasy na podstawie specyfikacji albo pliku - używa do tego Fillera który jest jedynym współdzielonym elementem (dla tego też ewentualna współbieżność została oddelegowana do osobnej klasy - aby wyglądało na prosto i zrozumiale - ale chyba tak nie jest - proszę o komentarz). W końcu odpowiedzialność - implementacje tego jak tworzyć source spoczywa na klasach pakietu autoconfigurablegroup (obsługującego adnotację @Autoconfig). Tam i tylko tam jest zaimplementowane tworzenie specyfikacji klasy (TypeSpec) oraz pliku propierties . ElementProvider dostarcza prawidłowe elementy dla klas ich używających tak aby zdjąc z nich odpowiedzialność za wyselekcjonowanie prawidłowo anotowanych elementów. W zasadzie lepiej jak by ElementProvider był bezstanowy ( nazywał by się wtedy ElementSelector) a za stan odpowiadał by jakiś obiekt cachujący prawidłowe elementy ( po selekcji) . Na ten moment implementacja takiego rozwiązanie nie żadnego merytorycznego uzasadnienia
Jeśli to mało mogę zagłębić się w szczegóły.
To co chciałem wstępnie zrobiłem - podzieliłem na elementy reużywalne, oddelegowałem odpowiedzialności do wyspecjalizowanych klas, ograniczyłem zależności, wskazałem gdzie leży kwestia ewentualnej współbieżności. Teraz dodawanie kolejnych funkcjonalności w ramach tej samej adnotacji polega na kopaniu tylko w pakiecie obsługującym tą adnotację, a dodanie kolejnej adnotacji polega na utworzeniu pakietu obsługującego ją gdzie implementacja korzysta z reużywalnych elementów czyli SourceBuilder, ProcEnvironment, ElementProvider, ProcLogger. Do StartupProcessor wystarczy dodać adnotację którą ma obsługiwać, do SupportDispatchera również i tyle.
Droga do współbieżnego generowania źródeł też jest otwarta. Metoda generate może być uruchamiana na wszystkich wyspecjalizowanych klasach jednocześnie ( wystarczy kilka modyfikacji w strukturach danych na thread safe StartupProcessora), create też.
Słowo na temat AutoConfigBase (internal bez dostępu)- jest to klasa która implementuje wszystko to czego procesor adnotacji nie musi tworzyć ( bo po co ). Żyje sobie w tym samym pakiecie co wygenerowana klasa procesorem adnotacji.
To co jest dostępne dla użytkownika jest w pakiecie api ( adnotacja i handler z 1 metodą). AutoInitialize wygląda tak a nie inaczej ze względu na organizację kodu. Dla mnie jest to przejrzyste - użytkownik ma tylko dostęp do jednej metody.
Natomiast ktoś kto chciał by się pobawić procesorem ma dostęp do tych klas pakietu processor które są publiczne.
Przykład wygenerowanej klasy:

public final class AutoConfig {
  private static final Map<String, String[]> autoGenMap = new HashMap<>();

  static {
    String key;
    String[] autoGenVal;

    autoGenVal = new String[3];
    autoGenVal[0] = "org.aujee.com.searchengine.TomcatServer";
    autoGenVal[1] = "inSecurePort";
    autoGenVal[2] = "Integer";
    key = autoGenVal[0] + "." + autoGenVal[1];
    autoGenMap.put(key, autoGenVal);

    autoGenVal = new String[3];
    autoGenVal[0] = "org.aujee.com.searchengine.TomcatServer";
    autoGenVal[1] = "keyStoreFile";
    autoGenVal[2] = "String";
    key = autoGenVal[0] + "." + autoGenVal[1];
    autoGenMap.put(key, autoGenVal);

    autoGenVal = new String[3];
    autoGenVal[0] = "org.aujee.com.searchengine.TomcatServer";
    autoGenVal[1] = "hostName";
    autoGenVal[2] = "String";
    key = autoGenVal[0] + "." + autoGenVal[1];
    autoGenMap.put(key, autoGenVal);

    autoGenVal = new String[3];
    autoGenVal[0] = "org.aujee.com.searchengine.TomcatServer";
    autoGenVal[1] = "keyAlias";
    autoGenVal[2] = "String";
    key = autoGenVal[0] + "." + autoGenVal[1];
    autoGenMap.put(key, autoGenVal);

    autoGenVal = new String[3];
    autoGenVal[0] = "org.aujee.com.searchengine.TomcatServer";
    autoGenVal[1] = "securePort";
    autoGenVal[2] = "int";
    key = autoGenVal[0] + "." + autoGenVal[1];
    autoGenMap.put(key, autoGenVal);

    autoGenVal = new String[3];
    autoGenVal[0] = "org.aujee.com.searchengine.TomcatServer";
    autoGenVal[1] = "keystorePassword";
    autoGenVal[2] = "String";
    key = autoGenVal[0] + "." + autoGenVal[1];
    autoGenMap.put(key, autoGenVal);
  }

  private AutoConfig() {
  }

  public static void initialize() {
    AutoConfBase.load(autoGenMap);
    AutoConfBase.initialize();
  }
}

Wsółbieżne tworzenie źródeł nie za bardzo ma sens w obecnym stanie, ale ewentualna implementacja jest bezproblemowa.
Co istotne implementacja kolejnych funkcjonalnosći nie wymaga zastanawiania się czy kontrakt z api zostanie złamany.
Mam kilka pomysłów na kolejne wyspecjalizowane funkcjonalności, jednak na razie nie chce o nich gadać -nie wiem czy jest sens robienia tego ( zwyczajnie po roku nauki jeszcze nie wiem co ma sens a co nie). na pewno było by to związane z brakiem stanu, lambdami, niemutowalnością, współbieżnością...

Jak na razie muszę coś porobić w springu (chociaż ciągnię mnie do robienia niskopoziomowo) - bo chcę się przebranżowić (chociaż jak na IT stary już jestem).
Jeśli ktoś mógłby się pochylić nad tym i ocenić mój kod - rozwiązania, styl, zależności, spójność, architektura, organizacja kodu, SOLID, nazewnictwo, wyjątki, czy mam już szerokie pojęcie o Javie, czy jeszcze nie - to był bym niezmiernie wdzięczny. (rozumiem że od procesorów adnotacji prawdopodobnie nie jest tu za wiele specjalistów - dlatego też nie proszę o to żeby ktoś siedział i poświęcał wieczór na rozkminianie przepływu sterowania i tego czy w ogóle dobrze rozpoznałem javax annotation processor)

0

Fajne forum, najlepsze w polszy. Dzięki dzięki za ocenę. Tuzy z kilkunastoletnim doświadczeniem odpowiadają na jakiej springopierwszorocznesnipety, a w kilka klas się zerknąć nie chce. Chyba jednak trzeba będzie rozsupłać sakwę i poszukać mentora.

1

Myślę nad dodaniem kolejnych funkcjonalności( dynamiczne tworzenie pliku - in memory file system , ładowanie pola instancji w runtimie po indeksie/pointerze bez mapowania/castingu a nie tylko statyczne). Wskazówki co jest przydatne, a co nie były by pomocne. Nie chcę robić czegoś co nikomu się nie przyda. — szmeterling 34 minuty temu

Po prostu jezykowo nie rozumiem. Jakbyś opisywał jak to zaimplementujesz, a nie co "mi" jako użytkownikowi biblioteki by to dało.

Z pewnością dobrze obiektowo zaprojektowany mechanizm zasilania tej konfiguracji.
A jak dobrze zaprojektowany, stanie się nieważne czy akurat "in memory file system", bo ty i inni użytkownicy będa mogli implementować mechanizmy odczytu ":skądś" i ustawiania konfiguracji dosłownie taśmowo.
Np taki Reader, aby konfigurację wczytywać z bazy w filozofii tego projektu, który jest na warsztacie (a odmiennej od innych)

Nie jesteśmy na pustyni, są dziesiątki bibliotek (czy części frameworków) do obsługi konfiguracji w javie (w dowolnym języku JVM). Z analizy tych rozwiązań byś może odniósł lepszy pożytek niż usilnie implantować własny, zwłaszcza nie mając oglądu co u konkurencji na horyzoncie widać.

Co nie znaczy, że osobiście nie lubię małych fajnych podobnych bibliotek czy zrobić na kolanie, czy zaimplementować im Reader (w Javie /C# a dawniej w C++ też się zdarzyło)

1

Jest trochę inaczej niż to widzisz. Ja to robię raz że mi się podoba, dwa że nie przesiąkając zbyt mocno innymi rozwiązaniami zrobię to po swojemu i wystawie na ocenę (a to że są i z jakich mechanizmów korzystają to się orientuję), która odzwierciedli jak myślę, a nie jak kopiuję, trzy wiele mnie to nauczy. A przy okazji wolałbym żeby to chociaż w minimalnym stopniu było przydatne, więc pytam.
No rzeczywiście, napisałem po części jak chciałbym to zaimplementować. A co u konkurencji to z niedawno zrobionego małego rekonesansu mogę powiedzieć że np snakeyaml potrzebuje castingu/mapowania i utworzenia w runtimie obiektu żeby wyciągnąć wartość, trochę podobnie w properties. Więc pytam co by się przydało, nie prosząc o ocenę intencji i pobocznych rozkmin aby trochę się odciążyć od buszowania po necie i szukania za i przeciw.

1

KIlka myśli przetrawiłem.
Pierwsza: najłatwiejsze są pola statyczne, trudniejsze pola instancyjne (ale maja większe walory w razie rozwiazania problemów), najtrudniejsze zmienne w blokach (fajne, ale niekonieczne)

Druga: wstrzykiwanie. Nie taki diabłeł kosztowny. Oczywiscie każdy z nas słowo kojarzy z je...m frameworkiem do wstrzykiwania, code wearing, aspekty itd... Tak to się ukształtowało, natomiast nie jest to istotą idei wstrzykiwania.. Wielu programistów ma swoje doświadczenia z budowanym in-house mechanizmem, ktory zaczynał się static Manager.GetValue(Class Interface) a doprowadził do jakiejś formy nienazwanego jawnie wstrzykiwania. Jest wiele form wstrzykiwania.

Trzy Wiązanie pola/zmiennej z nazwą atomu konfiguracji (lewa strona property w pliku). Wiodące dla libki do konfiguracji.
Dla pól statycznych odkryłeś, podobnie jak twórcy w/w Configurable. Dla pól instancyjnych teoria jest tak samo łątwa, kodowanie trudniejsze.

Inną ścieżką są adnotacje ... hmmm... mam dylemat sumienia a) czy łączyć z kwalifikowaną nazwą klasy+pola jak w/w b) czy jedynie adnotacja, ale wtedy przymus dość bogatej. Gdzieś na githubie była bardzo sprytna libka konfiguracyjna i to Polaka, gdzie wykorzystywał adnotacje @Named CDI, a całe czary w providerze CDI badajacym injection point (czy jak to sie nazywa)
O ile compile-time (a cos istotnego wyszło w javie 15-16 czy w tych okolicach) to jedno.
Adnotacje runtime to z pewną ostrożnością. Pewnie suluszym (dla mnie) musiało by być nie konfliktowe ze Springiem / EE, ale zdolne do pracy bez w/w środowsisk

0

Ciekawe czy "problem z rzutowaniem" to coś pokrewnego co jak nazywam "problem z generyczną deserializacją". Gdzieś trzeba generycznie przejść z ciągu literek na wartość konkretnego typu, np określnego przez Class: prymityw, ale najprostsze rzeczy z datą, kwotą.

0

@ZrobieDobrze: Dużo wskazówek za które dzięki. Na razie dowiozę kwestie statyczne. Potem skupię się na wyszukiwarce (mam spore braki w SQL), a w międzyczasie będę pracował nad procesorem. Odnośnie samej koncepcji chyba najciężej będzie to powiązać ze springiem, aby mu nie przeszkadzało, albo współpracowało. Nie celuję również w budowę jakiś rozbudowanych mechanizmów wstrzykiwania ( raz że za mało doświadczenia, dwa coś innego chce też robić, 3 po co znowu to samo). Twój post jest bardzo pomocny, na pewno do niego wrócę jak będę rozbudowywał procesor. Na tą chwilę dużo jeszcze muszę się douczyć aby wybrać słuszną koncepcję (vector api, java.lang.invoke, simd, a może ringbuffer, jimfs..? na pewno jak najmniej generowania w runtimie na rzecz celowania w powiązania wytworzone procesorem, albo jakiś agent i instrumentalizacja - grube tematy).
Nie będę dalej rozkminiał tylko zabieram się do pisania.
Jak by ktoś jakieś luźne myśli miał to niech wpisuje. Na pewno się przyda.

1 użytkowników online, w tym zalogowanych: 0, gości: 1