В пригороде далекого города Нью-Дели жил простой индийский паренек со сложным именем Чандракант. Любил он маму, Кришну и общаться с волшебными говорящими грибами.

Три грани безумия на одной картинке.
Три грани безумия на одной картинке.

Однажды после особо глубокого погружения в нирвану, волшебный гриб рассказал индийскому пареньку его предназначение, что рожден Чандракант был ради великой цели:

мстить белым варварам за годы рабства и угнетения индийского народа.

— О юный Чандракант! — молвил гриб.

— Помни что «вера без дел мертва». Будет непросто.

 — Враг хитер и коварен, сражаться предстоит тайно и его же оружием.

Прошли годы, затем десятилетия.

Паренек выучил английский, закончил хороший индийский ВУЗ и поступил на работу в крупную индийскую компанию, которой «белые варвары» из далекой страны за океаном заказывали разработку программного обеспечения.

Но не забыл храбрый Чандракант — верный сын индийского народа наставления волшебного гриба и дослужившись до должности системного архитектора начал вершить жестокую месть, сражаясь с «белыми варварами» их же собственным информационным оружием.


Прошу прощения за столь поэтическую вводную, но проект о котором пойдет речь  — настолько лют, что его появление на свет может быть обосновано только экспериментами по созданию психотронного оружия.

Если код это изложение мысли в виде набора логических конструкций, то этот проект — программная реализация шизофрении.

Единственное связанное с этим проектом хорошее событие: коллега, который первым узрел это навсегда бросил пить и находится в полной завязке до сих пор. Седьмой год подряд.

К серьезному безумию надо уметь подводить, поэтому прежде чем показывать код, немного раскрою теорию и матчасть — для непричастных и невинных.

Аннотации

У современных языков вроде Java и C# есть такая замечательная вещь как аннотации — метаданные, которые содержат различные метки и дополнительные настройки, связанные с конкретным полем, методом или классом.

Выглядит это обычно так:

@Configuration
@EnableCaching
public class CacheConfig {   
    @Bean
    public HibernatePropertiesCustomizer hibernatePropertiesCustomizer(
        javax.cache.CacheManager cacheManager) {
        return hibernateProperties -> hibernateProperties
                .put(ConfigSettings.CACHE_MANAGER, cacheManager);
    }
    ..
}

Все что начинается с символа @ — те самые аннотации, содержащие определенную единицу смысла, в данном случае посвященную настройке Spring Boot.

Но куда чаще аннотации привязываются к полям класса и используются для хранения метаданных, связанных с конкретным полем:

/**
 * Сущность 'заказ'
 * @since 1.0
 * @author 0x08
 */
@Entity
@Table(name = "strm_orders")
@Indexed
public class Order extends AbstractAuditingEntity implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, 
                    generator = "order_seq")
    @SequenceGenerator(name = "order_seq")
    @GenericField
    private Long id;
    @Column(name = "customer_id", nullable = false)
    private long customerId;
    @Column(name = "sender_id", nullable = false)
    private long senderId;
    @Enumerated(EnumType.STRING)
    @Column(name = "order_status", nullable = false)
    private OrderStatus orderStatus = OrderStatus.NEW;
..

В примере выше, аннотация @Column, которой помечено практически каждое поле класса, отвечает за связывание этого поля с колонкой в таблице базы данных. Атрибут name собственно хранит название колонки.

Несмотря на внешнюю громоздкость, аннотации всегда были лишь дополнением к классу — дополнительным инструментом, призванным упростить работу и сократить объем кода внутри самого класса, необходимый для поддержки аналогичного функционала.

Никто в своем уме не воспринимал аннотации как полную замену всей логики приложения и соответственно не пытался программировать исключительно с помощью аннотаций — запомните этот важный момент.

Неожиданный но предсказуемый результат поиска по словам "SOLID rocks"
Неожиданный но предсказуемый результат поиска по словам "SOLID rocks"

SOLID

В современном программировании есть такой термин SOLID — акроним, каждая буква которого обозначает отдельную концепцию:

Как и любая другая абстрактная концепция, SOLID — «за все хорошее против всего плохого», красиво выглядит на бумаге (в книгах) и в виде строчки резюме, но при реальном использовании становится сильно сложнее и не таким красивой.

Как и любую другую концепцию, созданную человеком, SOLID можно извратить до неузнаваемости, превратив в психотронное оружие для пожирания мозга несчастных программистов.

Особенно когда волшебный говорящий гриб нарекает избранным и готовит к великой миссии — мстить «белым варварам» за годы угнетения их же собственным высокотехнологичным оружием.

Та самая корпоративная IBM Websphere, на которой работал неповторимый оригинал.
Та самая корпоративная IBM Websphere, на которой работал неповторимый оригинал.

Часть 0. Архитектурный джихад

Первая встреча с неведомым произошла как обычно абсолютно случайно, но навсегда изменила нашу скучную жизнь и отношение к индийской школе программирования и ее талантам:

к нам обратился (через посредников) классический английский джентельмен с лаконичной просьбой «посмотреть проект и если возможно оценить состояние».

Послать джентельмена сразу мы сочли неприемлемым — все же культурные люди из культурной столицы, тем более у него оказался приятный бюджет при весьма скромных запросах. Однако несмотря на постоянную практику в подобных задачах, к такому оказались не готовы даже мы — настолько все оказалось люто завернуто внутри.

На этом месте думаю стоит начать выкладывать конкретные примеры этой «индийской истории ужасов», чтобы дорогие читатели начали наконец понимать о чем идет речь.

К сожалению ввиду подписанных документов о неразглашении давно убитой сборки проекта и объемов исходников, не могу хочу показывать оригинальный индийский код, поэтому все что ниже — вольное изложение «по мотивам» и адаптация, хотя и занявшая в итоге семь лет подготовки.

Что никак не уменьшает убойность архитектурных талантов смелого Чандраканта (да не тронет GC его классы), помноженных на мощь его веры в святое дело борьбы с «белыми угнетателями» путем сжигания их мозгов.

Оригинальный проект представлял собой связку из нескольких десятков JEE-приложений, разворачиваемых на «большой» IBM Websphere и соответственно Java 8, но ради упрощения мы адаптировали реализацию для более современной Jakarta 10, со всеми наворотами новых версий Java (см. ниже).

Начнем демонстрацию с чего‑то более‑менее безобидного, хоть как‑то попадающего в рамки адекватности:

..
@WebFilter("/*")
@WebListener
public class Foo implements Filter,ServletContextListener{
  
  // важная переменная
  private int someImportantValue = 42;

  @Override
  public void contextInitialized(ServletContextEvent sce) {
  
        final ServletContext sc = sce.getServletContext();
        // прокидывание в контекст выполнения собственный инстанс
        sc.setAttribute("foo", this);
 }
 ..
 
  @Override
  public void doFilter(ServletRequest sr, 
                      ServletResponse sr1, FilterChain fc) 
                            throws IOException, ServletException {
       
        final ServletContext sc = sr.getServletContext();
        // вытаскивание своего же инстанса из контекста
        Foo foo = (Foo)sc.getAttribute("foo");
        
        if (foo.someImportantValue == 42) {
           // дальнейшая обработка      
           ..   
        }
    }
  ..
}

В примере выше храбрый индийский архитектор Чандракант с помощью двух аннотаций, общего контекста и молитв Кришне использовал одну и ту же копию основного объекта в контексте двух разных сущностей в Servlet API (Filter и Listener), с разным жизненным циклом.

Связав их состояние воедино с подлинно индийской хитростью — ради нанесения побоев мозгу неподготовленного «белого варвара».

Но пример выше это еще цветочки, по сравнению со следующим шедевром индийской инженерной мысли, из-за которого мой бедный коллега навсегда бросил пить:

@Entity
@Table(name = "t_foo")
@NamedQueries({
    @NamedQuery(name = "Foo.fetchAll",
            query = "SELECT f FROM Foo f order by f.id desc")
})
@Named
@RequestScoped
public class Foo {

    @Id
    @SequenceGenerator
    @GeneratedValue(strategy = GenerationType.SEQUENCE, 
                    generator = "default_gen")
    private Long id;
    
    @Size(min = 3, max = 255)
    @Pattern(regexp = "[a-zA-Z0-9._ -?!]+")
    private String title;
    
    ..

    private transient Foo instance;
    
    @Transient
    @PersistenceContext
    private EntityManager em;
    
    ..
    
    public List<Foo> fetchRecords() {
        return this.em.createNamedQuery("Foo.fetchAll",Foo.class).getResultList();
    }
..
}

Если вы занимались разработкой для J2EE/JEE/Jakarta и видели все эти JPA, JTA, JSF в работе — уже должны были бежать за валидолом.

Но если ужасы корпоративной Java-разработки обошли стороной, то наверное стоит немного объяснить суть и смысл.

На класс Foo в примере выше навешано два типа аннотаций, каждая из которых помечает его использование в разных контекстах и с разным жизненным циклом:

  • в качестве сущности (Entity) JPA, отвечающей за связывание с таблицей в базе данных;

  • в качестве бина CDI, который может быть использован непосредственно со страницы JSF.

Cо страницы это выглядит это как-то так:

 <p>
    <ui:repeat value="#{foo.fetchRecords()}" var="record">          
             <div style="padding-left: 2em; font-style: italic;">
                            <h:outputText value="#{record.title}"/>
              </div>
              ..
    </ui:repeat>
</p>

Но на самом деле тут все еще веселее — посмотрите на поле instance:

private transient Foo instance;

которое великий индийский гуру использовал в качестве.. DTO!

Считаю что такая реализация — верх дословной интерпретации принципов SOLID, доведенный до безумия, в качестве мести белым угнетателям по заказу волшебных грибов.

Таких интересных классов в проекте было не один и не два, а около 700 — целая армия автономных бинов, каждый из которых отвечал только за себя от начала и до конца.

Временами бины вызывали друг-друга, временами делали это по сети, иногда — через очереди сообщений.

Сложно сказать было ли это попыткой создать нейронную сеть с помощью Java-бинов и если да то насколько успешной, но JEE-приложение в те времена еще не успело осознать себя как личность.

Несмотря на всю описанную выше жесть, гордый сын индийского народа — архитектор Чандракант (да будут вечно стабильными его сборки) не успокоился, решив окончательно добить белых варваров, живущих за океаном и сжечь им психику таким замечательным API:

..
@WebMethod
@POST
@Path("addMessage")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.TEXT_PLAIN)
@Transactional(Transactional.TxType.REQUIRED)
public String addMessage(
            @FormParam("title") String title,
            @FormParam("author") String author,
            @FormParam("message") String message) {
            ..
            }
            
..

Если вы отличаете SOAP от REST и тем более знаете что такое JAX-WS и JAX-RS — с кода выше уже должно было морально поплохеть, поскольку сие есть самое натуральное оскорбление чувств верующих в высокие архитектурные принципы.

Для непричастных объясняю:

API состоит из методов, вызываемых удаленно, вебсервис — вариация API, вызываемая через веб, с использованием классических протоколов веба: HTTP/HTTPS.

REST и SOAP — два разных стандарта вебсервисов, один использует JSON, другой — XML (если упрощенно) для обмена сообщениями между клиентом и сервером. JAX‑WS и JAX‑RS — два разных стандарта для разметки методов вебсервиса с помощью аннотаций.

Так что в примере выше один и тот же метод используется одновременно в вебсервисе SOAP и как метод RESTful вебсервиса.

Особую пикантность и новые ощущения во время отладки придает тот факт, что каждый вебсервис — отдельный инстанс класса, со своим жизненным циклом.

А связывались между собой они через все тот же контекст CDI.

Или атрибут ServletContext.

Или через статический синглтон.

Или через контекст EJB.

Или по сети.

Или через очереди сообщений (JMS).

Гордый сын индийского народа любил разнообразие.

Резюмируя:

для того чтобы хотя-бы понять как это все работает, потребовался весь наш многолетний опыт разгребания корпоративных говен — ни в каких поисковиках, wiki, stackoverflow, форумах и досках объявлений ничего подобного не находилось.

Такова сила и мощь индийской инженерной школы.

Часть 1. Одноклассовый энтерпраиз

Мы решили сохранить подвиг храброго индийского архитектора Чандраканта (да никогда не упадут его юнит-тесты) для будущих поколений, чтобы его великие архитектурные идеи стали примером и образцом того как правильно мстить «белым угнетателям», сжигая вражеские мозги их собственным оружием.

мы отобрали самую лютую дичь из этого отбитого индийского проекта и свели в одно небольшое приложение, реализующее простейшую гостевую книгу.

Фишка в том что вся реализация это один класс на Java, без вложенных или анонимных классов. И очень много аннотаций.

Затем мы смогли развернуть столь упоротое приложение на всех основных серверах приложений, реализующих API Jakarta 10:

  • WildFly

  • Open Liberty / IBM WebSphere Liberty

  • Eclipse GlassFish

  • Payara

Так оно выглядит в работе (основной экран):

Для оформления на этот раз был взят индийский CSS-микрофреймворк с интересным для отечественного слуха названием.

Учим хинди вместе с автором:

FWIW, "choṭā" means "small" in Hindi

Так выглядит основной функционал гостевой книги — добавление новой записи в действии:

Так выглядит авторизация, полноценная авторизация с сессиями:

Теперь показываю работу с API, напоминаю это все еще один и тот же класс на Java.

JAX-WS

Вызов JAX-WS (SOAP) из клиента на Python:

Так выглядит тестовый клиент на Python:

from zeep import Client

# ссылка на wsdl файл
client = Client('http://localhost:9080/madjavaee-1.0.1-RELEASE/MegaBeanService?wsdl')
# вызов тестового метода
result = client.service.doPing()
print(result)

# создание объекта DTO 
factory = client.type_factory('http://madjavaee.experiments.Ox08.com/')
message = factory.megaBean(title='new title', author='test@test.com', message='test message')
# вызов метода API для добавления записи 
result = client.service.addMessage(message)
print(result)

# получить записи гостевой через API
result = client.service.fetchRecords()
print(result)
# получить количество записей через API
result = client.service.fetchRecordsCount()
print(result)

JAX-RS

Так выглядят в действии вызовы вебсервиса JAX-RS (REST) с помощью curl и браузера:

Команда для добавления поста с помощью curl:

curl -H "Content-Type: application/json" -X POST http://localhost:9080/madjavaee-1.0.1-RELEASE/api/addMessage -d '{"title":"test title2","author":"user@test.com", "message":"some message"}'

Часть 2. Психотронное оружие

То что в итоге у нас получилось представляет собой самое настоящее психотронное оружие, которым можно выжигать мозг неподготовленным к такому накалу дичи программистам.

Если никогда не видели код, за который приличные люди с высшим техническим образованием могут избить ногами прямо на рабочем месте — ниже вас ждут удивительные открытия.

Но сначала немного статистики:

~600 строк из которых ~200 — комментарии, итого ~400 строк на все приложение.

Смотрим и наслаждаемся шедевром:

package com.Ox08.experiments.madjavaee;
// Common Java
import java.io.*;
import java.util.*;
import java.util.logging.*;
// CDI
import jakarta.enterprise.context.*;
import jakarta.inject.*;
// JPA
import jakarta.persistence.*;
import jakarta.persistence.criteria.*;
// JSR 303 Validation API
import jakarta.validation.constraints.*;
// JSF
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.*;
// JSR 375
import jakarta.security.enterprise.AuthenticationStatus;
import jakarta.security.enterprise.authentication.mechanism.http.*;
import jakarta.security.enterprise.credential.*;
import jakarta.security.enterprise.identitystore.*;
// Servlet API
import jakarta.servlet.*;
import jakarta.servlet.annotation.*;
import jakarta.servlet.http.*;
// JTA
import jakarta.transaction.Transactional;
// JAX-RS
import jakarta.ws.rs.core.*;
import jakarta.ws.rs.ext.*;
import jakarta.ws.rs.*;
// JAX-WS
import jakarta.jws.*;
/**
 * This is single class CRUD application, based on recent Java EE stack.
 *
 * @author <a href="mailto:alex3.145@gmail.com">Alex Chernyshev</a>
 */
// ordinary JPA entity annotations
@Entity
@Table(name = "t_records")
@NamedQueries({
    @NamedQuery(name = "MegaBean.getAllRecords",
            query = "SELECT m FROM MegaBean m order by m.id desc")
})
// CDI bean annotation, which used to register instance of this class as CDI managed bean
// This is required for EntityManager injection
@Named
// Java Faces annotation, required to trigger JSF initialization on some servers
@jakarta.faces.annotation.FacesConfig()
        //(version = FacesConfig.Version.JSF_2_3) - deprecated in Faces 4.0 and upper
// JSF annotation, required to bypass jsr299 validation see WebContainer.validateJSR299Scope
@Dependent
//@ApplicationScoped or @RequestScoped are not allowed, because of @WebFilter/@WebListener annotations presence
// Servlet 3.0 annotations
// Servlet Filter - another instance of this class will be registered as servlet filter
@WebFilter("/*")
// One more instance will be registered as servlet context listener, to be used as initialization point.
// All because we can't use @ApplicationScoped and @Observes here
@WebListener
// See JSR375 spec for details
@CustomFormAuthenticationMechanismDefinition(
        loginToContinue = @LoginToContinue(
                loginPage = "/index.xhtml?login=true",
                useForwardToLogin = false,
                errorPage = "/index.xhtml?login=true&error=true"
        )
)
// used only when embedded IdentityStore in use
@jakarta.annotation.security.DeclareRoles({"admin", "user", "demo"})
// JAX-RS annotations
@ApplicationPath("api")
@jakarta.ws.rs.Path("")
// this is required for ExceptionMapper
@jakarta.ws.rs.ext.Provider
// JAX-WS binding (SOAP) Warning: conflicts with JAX-RS on OpenLiberty and Wildfly!
//@WebService
public class MegaBean extends Application implements Serializable,
        jakarta.servlet.Filter,
        ServletContextListener,
        // Because OpenLiberty/IBM Websphere Liberty does not support  combination of
        // CustomFormAuthenticationMechanismDefinition and HttpAuthenticationMechanism,
        // I was required to remove HttpAuthenticationMechanism interface
        // Custom IdentityStore does not work without @ApplicationScoped on OpenLiberty
        IdentityStore, // see JSR375
        ExceptionMapper<Exception> {
    public MegaBean() {
        // call for JAX-RS parent class
        super();
        /*
         * we need to set some default values to bypass JSR 303 bean validation for
         * JAX-RS bean, otherwise, JAX-RS service will not work.
         */
        this.author = "no@no.org";
        this.createdAt = new Date();
        this.message = "no no no";
        this.title = "test title";
    }
    /**
     * We need to have an instance of this class as DTO - to transfer data from
     * html form
     */
    private transient MegaBean current;
    /**
     * This class is also a CDI managed bean, remember? So here we will inject
     * EntityManager
     */
    @Transient
    @PersistenceContext(unitName = "megaPU")
    private EntityManager em;
    /**
     * Security context maybe null when JAAS API was not initialized, so it's wrapped with @Instance
     */
    @Transient
    @Inject
    private jakarta.enterprise.inject.Instance<jakarta.security.enterprise.SecurityContext> securityContext;
    @Transient
    @Context
    private ServletContext servletContext;
    /**
     * Ordinary JPA fields
     */
    @Id
    @SequenceGenerator(name = "default_gen", sequenceName = "w_default_pk_seq")
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "default_gen")
    private Long id; // unique id, this sequence will be created automatically too
    @Size(min = 3, max = 255)
    @Pattern(regexp = "[a-zA-Z0-9._ -?!]+")
    private String title; // used also as 'login' field for auth
    @Size(min = 3, max = 30)
    @Email
    private String author; // used also as 'password' field for auth
    @Lob
    @Column(length = Integer.MAX_VALUE)
    @NotBlank(message = "message may not be blank")
    private String message; //message body, CLOB/TEXT/BLOB type will be used in database
    @Column(name = "created_date", nullable = false)
    @Temporal(TemporalType.TIMESTAMP)
    @NotNull
    protected Date createdAt;
    /**
     * this called from JSF page to clean up fields on page reload
     */
    @WebMethod(exclude = true)
    public void init() { resetFields(this); }
    /**
     * Each and every interface methods should be implemented and marked with
     * Annotation WebMethod(exclude = true) used to avoid bug in Apache CXF (Wildfly/OpenLiberty)
     * <a href="https://issues.apache.org/jira/browse/CXF-4916">...</a>
     * Method 'contextDestroyed' is part of ServletContextListener interface, so must be implemented
     */
    @WebMethod(exclude = true)
    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        // not used
    }
    /**
     * Part of ServletContextListener API, used on app start/reload
     */
    @Override
    @WebMethod(exclude = true)
    // transactional is required to let EntityManager do his job
    @Transactional(Transactional.TxType.REQUIRED)
    public void contextInitialized(ServletContextEvent sce) {
        final ServletContext sc = sce.getServletContext();
        // due to CDI vs servlet conflict
        sc.setAttribute("mega", this);
        // we can make it only here due to stackoverflow error in eclipselink
        this.current = new MegaBean();
        // reset fields back to nulls - to have working JSR303 validation
        resetFields(this.current);
        // populate JSF version details
        addVersionEnv(sc);
        // try to add some initial data if database is empty
        try {
            if (fetchRecordsCount() == 0) {
                //create test entity
                final MegaBean r = new MegaBean();
                r.setCreatedAt(new Date());
                r.setAuthor("system@test.org");
                r.setMessage("Test message");
                r.setTitle("Test title");
                em.merge(r);
                LOG.info(String.format("automatically added default record: %d", r.getId()));
            }
        } catch (Exception e) {
            LOG.log(Level.WARNING,
                    String.format("Exception on startup: %s", e.getMessage()), e);
        }
    }
    /**
     * JSF bean method, used to save form (from itself)
     */
    @WebMethod(exclude = true)
    @Transactional(value=Transactional.TxType.REQUIRED,rollbackOn = Exception.class)
    public String save() {
        // set creation date&time
        current.setCreatedAt(new Date());
        em.merge(current);
        // this is required to reset form fields
        this.current = new MegaBean();
        resetFields(this.current);
        // does redirect
        return "/index.xhtml?faces-redirect=true";
    }
    /**
     * Does login action from JSF page
     * @throws IOException
     *          if God was not on our side
     */
    @WebMethod(exclude = true)
    public void login() throws IOException {
        // we need to re-use 2 existing fields, present in this class: 'author for username and 'title' for password
        final Credential credential = new UsernamePasswordCredential(author, new Password(title));
        final FacesContext facesContext =FacesContext.getCurrentInstance();
        final ExternalContext ec =  facesContext.getExternalContext();
        // should not happen, this is used to avoid class cast
        if (!(ec.getRequest() instanceof HttpServletRequest req)
                || !(ec.getResponse() instanceof HttpServletResponse res)) {
            ec.getRequestMap().put("login", "true");
            facesContext.addMessage(null,
                    new FacesMessage(FacesMessage.SEVERITY_ERROR, "Login failed", null));
            return;
        }
        // check if JAAS initialized
        if (!securityContext.isResolvable()) {
            LOG.warning("SecurityContext cannot be resolved!");
            return;
        }
        // try to authenticate programmatically
        final AuthenticationStatus status = securityContext.get()
                .authenticate(
                        req, res,
                        AuthenticationParameters.withParams().credential(credential));
        if (status == null) {
            LOG.warning("JAAS not initialized!");
            return;
        }
        LOG.fine(String.format("auth status: %s",status));
        switch (status) {
            case SEND_CONTINUE: {
                facesContext.responseComplete();
                break;
            }
            case SEND_FAILURE: {
                ec.getRequestMap().put("login", "true");
                facesContext.addMessage(null,
                        new FacesMessage(FacesMessage.SEVERITY_ERROR, "Login failed", null));
                break;
            }
            case SUCCESS: {
                putCurrentUser(current);
                LOG.info(String.format("logged in as %s",current.author));
                facesContext.addMessage(null,
                        new FacesMessage(FacesMessage.SEVERITY_INFO, "Login succeed", null));
                // after redirect there will be full page reload
                ec.redirect(ec.getRequestContextPath() + "/index.xhtml?ok=true");
                break;
            }
            case NOT_DONE:
                facesContext.responseComplete();
                break;
        }
    }
    /**
     * Does logout action from JSF page
     *
     * @return
     * @throws ServletException
     */
    @WebMethod(exclude = true)
    public String logout() throws ServletException {
        final FacesContext facesContext =FacesContext.getCurrentInstance();
        final ExternalContext ec = facesContext.getExternalContext();
        // check for impossible state
        if (!(ec.getRequest() instanceof HttpServletRequest req)) {
            facesContext.addMessage(null,
                    new FacesMessage(FacesMessage.SEVERITY_ERROR, "Logout failed", null));
            return "";
        }
        req.logout();
        ec.invalidateSession();
        return "/index.xhtml?faces-redirect=true";
    }
    /**
     * JPA Entity fields
     * -------------------------------------------------------------------------------------------
     */
    @WebMethod(exclude = true)
    public String getAuthor() { return author; }
    @WebMethod(exclude = true)
    public void setAuthor(String author) { this.author = author; }
    @WebMethod(exclude = true)
    @jakarta.json.bind.annotation.JsonbTransient
    public MegaBean getCurrent() {
        return current;
    }
    @WebMethod(exclude = true)
    public Date getCreatedAt() {
        return createdAt;
    }
    @WebMethod(exclude = true)
    public void setCreatedAt(Date createdAt) {
        this.createdAt = createdAt;
    }
    @WebMethod(exclude = true)
    public Long getId() {
        return id;
    }
    @WebMethod(exclude = true)
    public void setId(Long id) {
        this.id = id;
    }
    @WebMethod(exclude = true)
    public String getTitle() {
        return title;
    }
    @WebMethod(exclude = true)
    public void setTitle(String title) { this.title = title; }
    @WebMethod(exclude = true)
    public String getMessage() { return message; }
    @WebMethod(exclude = true)
    public void setMessage(String message) { this.message = message; }
    /**
     * JAX-RS & JAX-WS Methods
     * -----------------------------------------------------------
     * Each method serves for both APIs
     * Ping is a test method, which respond plain text
     */
    @GET
    @jakarta.ws.rs.Path("ping")
    @Produces(MediaType.TEXT_PLAIN)
    @WebMethod
    public String doPing() { return "pong: " + System.currentTimeMillis(); }
    /**
     * Adds new message to guestbook from API
     * @param dto
     *          new message data
     * @return
     */
    @WebMethod
    @POST
    @jakarta.ws.rs.Path("addMessage")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.TEXT_PLAIN)
    // for JAX-WS only
    @Transactional
    public String addMessage(MegaBean dto) {
        LOG.info(String.format("prepare to add record %s , %s , %s", author, title, message));
        final MegaBean r = new MegaBean();
        r.setCreatedAt(new Date());
        r.setAuthor(dto.author);
        r.setMessage(dto.message);
        r.setTitle(dto.title);
        // for JAX-RS, EntityManager should be injected
        if (em!=null && em.isJoinedToTransaction())
            return addMessageImpl(r);
        // otherwise, take EntityManager from servlet context
        // note: access to servletContext from JAX-RS will trigger exception:
        // RESTEASY003880: Unable to find contextual data of type: jakarta.servlet.ServletContext
        else {
            final MegaBean mb = (MegaBean) servletContext.getAttribute("mega");
            return mb.addMessageImpl(r);
        }
    }

    /**
     * This 'black magic' is required, because JAX-WS does not allow transaction injection on service method
     */
    @Transactional(Transactional.TxType.REQUIRED)
    @WebMethod(exclude = true)
    public String addMessageImpl(MegaBean r) {
        try {
            r=em.merge(r);
            LOG.info(String.format("saved record %d", r.id));
            return String.format("Message added: %d %n", r.id);
        } catch (Exception e) {
            LOG.log(Level.WARNING, e.getMessage(), e);
            return String.format("Error on saving: %s", e.getMessage());
        }
    }

    /**
     * Uses Criteria API to retrieve count of records
     */
    @WebMethod
    @GET
    @jakarta.ws.rs.Path("recordsCount")
    @Produces(MediaType.TEXT_PLAIN)
    public Long fetchRecordsCount() {
        final EntityManager em = selectEm();
        final CriteriaBuilder qb = em.getCriteriaBuilder();
        final CriteriaQuery<Long> cq = qb.createQuery(Long.class);
        cq.select(qb.count(cq.from(MegaBean.class)));
        return em.createQuery(cq).getSingleResult();
    }

    /**
     * API method to retrieve all guestbook records
     */
    @WebMethod
    @GET
    @jakarta.ws.rs.Path("records")
    @Produces(MediaType.APPLICATION_JSON + "; charset=UTF-8")
    public List<MegaBean> fetchRecords() {
        return selectEm().createNamedQuery("MegaBean.getAllRecords", MegaBean.class).getResultList();
    }

    /**
     * API method to get currently authenticated user details
     */
    @GET
    @Produces(MediaType.APPLICATION_JSON + "; charset=UTF-8")
    @jakarta.ws.rs.Path("details")
    @WebMethod(exclude = true)
    public Response userDetails(@Context SecurityContext sc) {
        final java.security.Principal p = sc.getUserPrincipal(); // see sc.getCallerPrincipal() in Jakarta EE;
        return p != null ? Response.ok(p.getName()).build() :
                Response.status(Response.Status.UNAUTHORIZED).build();
    }
    /**
     * Methods below are required , due to re-use of same class for both JAX-WS
     * and JAX-RS
     * -------------------------------------------------------------------------------------------------------
     *
     */
    // part of IdentityStore API, not used
    @Override
    @WebMethod(exclude = true)
    public Set<String> getCallerGroups(CredentialValidationResult validationResult) {
        return Collections.emptySet();
    }
    @Override
    @WebMethod(exclude = true)
    public int priority() {
        return 100;
    }
    @Override
    @WebMethod(exclude = true)
    public Set<ValidationType> validationTypes() {
        return DEFAULT_VALIDATION_TYPES;
    }
    @WebMethod(exclude = true)
    @Override
    public void init(FilterConfig filterConfig) { }
    @WebMethod(exclude = true)
    @Override
    public void destroy() { }
    /**
     * this filter is used to redirect from / to actual jsf page
     *
     */
    @Override
    @WebMethod(exclude = true)
    public void doFilter(ServletRequest sr, ServletResponse sr1, FilterChain fc)
            throws IOException, ServletException {
        final HttpServletRequest request = (HttpServletRequest) sr;
        LOG.info(String.format("got request: %s", request.getRequestURI()));
        // required for correct characters encoding
        request.setCharacterEncoding("UTF-8");
        final String p = request.getRequestURI(),
                cp = request.getServletContext().getContextPath();
        String url = p;
        if (p.startsWith(cp)) url = p.substring(cp.length());
        if ("/".equals(url) && sr1 instanceof HttpServletResponse hsr)
            hsr.sendRedirect(cp + "/index.xhtml");
        else fc.doFilter(sr, sr1);
    }
    /**
     * Custom JSR375 validation
     * Used in combination with IdentityStore
     * @param credential
     * @return 
     */
    @Override
    @WebMethod(exclude = true)
    public CredentialValidationResult validate(Credential credential) {
        // should not happen
        if (!(credential instanceof UsernamePasswordCredential userCredential))
            return CredentialValidationResult.INVALID_RESULT;
        final String login = userCredential.getCaller();
        LOG.info(String.format("called validate for %s", login));
        if (!USERS.containsKey(login))
            return CredentialValidationResult.INVALID_RESULT;
        final Map<String, Object> user = USERS.get(login);
        // dumb password check
        if (!userCredential.compareTo(login, (String) user.get("password")))
            return CredentialValidationResult.INVALID_RESULT;
        LOG.info(String.format("user %s validated", login));
        return new CredentialValidationResult(login,
                new HashSet<>(Arrays.asList((String[]) user.get("roles"))));
    }

    /**
     * JAX-RS exception handler
     * @param e
     * @return
     */
    @Override
    @WebMethod(exclude = true)
    public Response toResponse(Exception e) {
        LOG.log(Level.WARNING, String.format("Exception on call : %s", e.getMessage()), e);
        return Response.status(400).entity(e.getMessage())
                .type("text/plain").build();
    }
    // !! required for YASSON parser, otherwise exception will raise:
    //  Error accessing getter 'getEnclosingConstructor' declared in 'class java.lang.Class'
    @Override
    @WebMethod(exclude = true)
    @jakarta.json.bind.annotation.JsonbTransient
    public Set<Class<?>> getClasses() { return Collections.emptySet();}

    /*
        remove from JAX-RS/JAX-WS output
    */
    @Override
    @WebMethod(exclude = true)
    @jakarta.json.bind.annotation.JsonbTransient
    public Set<Object> getSingletons() { return Collections.emptySet();}
    /*
        remove from JAX-RS/JAX-WS output
    */
    @Override
    @WebMethod(exclude = true)
    @jakarta.json.bind.annotation.JsonbTransient
    public Map<String,Object> getProperties() { return Collections.emptyMap();}
    /**
     * Clean fields for provided instance
     * @param m
     *          bean instance
     */
    private void resetFields(MegaBean m) {
        m.setAuthor(null);
        putCurrentUser(m);
        m.setCreatedAt(null);
        m.setId(null);
        m.setMessage(null);
        m.setTitle(null);
    }
    /**
     * JAX-RS and JAX-WS APIs have different lifecycle, for JAX-WS, an EntityManager will be injected by CDI,
     *  but for JAX-RS is not (not for all servers).
     *  So we need some selection logic here
     */
    private EntityManager selectEm() {
        // if EntityManager was not injected
        if (this.em!=null) return this.em;
        // take instance from servlet context
        return ((MegaBean) servletContext.getAttribute("mega")).em;
    }
    /**
     * Get current user from principal
     * @return
     *          current user's name
     */
    public static String getCurrentUser() {
        final FacesContext ctx = FacesContext.getCurrentInstance();
        // if there is no faces context - could happen if current bean was not created by JSF
        if (ctx==null || ctx.getExternalContext()==null)
            return null;
        // get principal (the standard way) from current context
        final java.security.Principal p = ctx.getExternalContext().getUserPrincipal();
        return p == null ? null : p.getName();
    }
    /**
     * Set current user's name to author field of our bean instance
     * @param instance
     *          an instance of MegaBean, used as DTO
     */
    private static void putCurrentUser(MegaBean instance) {
        final String username = getCurrentUser();
        if (username!=null)
            instance.setAuthor(username);
    }
    /**
     * Reads JSF version details and store as attribute of ServletContext
     * @param sc
     */
    private static void addVersionEnv(ServletContext sc) {
        final Package facesPackage = FacesContext.class.getPackage();
        final StringBuilder sb = new StringBuilder();
        if (sc.getServerInfo() !=null)
            sb.append(sc.getServerInfo());
        if (facesPackage.getImplementationVersion()!=null)
            sb.append(facesPackage.getImplementationVersion());
        sc.setAttribute("versionLine", sb.toString());
        LOG.info(sb.toString());
    }
    // credentials store, not used under Wildfly/OpenLiberty
    private static final Map<String, Map<String, Object>> USERS = new TreeMap<>();
    static {
        final Map<String, Object> admin_user = new HashMap<>();
        admin_user.put("password", "admin");
        admin_user.put("roles", new String[]{"admin", "user", "demo"});
        USERS.put("admin@test.org", admin_user);
        final Map<String, Object> s_user = new HashMap<>();
        s_user.put("password", "user");
        s_user.put("roles", new String[]{"user"});
        USERS.put("user@test.org", s_user);
    }
     // ordinary JUL logger, will not be serialized/persisted
    private static final Logger LOG = Logger.getLogger("MEGA");
}

Выдохнули, перекрестились и хлебнули валидола? Значит самое время рассказать как эта космическая дичь вообще работает.

Часть 3. Препарируя дичь

Начнем с жемчужины индийской архитектурной мысли — использования JPA Entity и работы с Entity в одном и том же классе:

@Entity
@Table(name = "t_records")
@NamedQueries({
    @NamedQuery(name = "MegaBean.getAllRecords",
            query = "SELECT m FROM MegaBean m order by m.id desc")
})
@Named
..
// ниже в этом же классе
@Transient
@PersistenceContext(unitName = "megaPU")
private EntityManager em;
..
// еще ниже в этом же классе
final MegaBean r = new MegaBean();
r.setCreatedAt(new Date());
r.setAuthor("system@test.org");
r.setMessage("Test message");
r.setTitle("Test title");
em.merge(r);
..

Сама мысль о возможности такого не могла придти в голову психически здоровому инженеру, поэтому индийскому архитектору явно помогали волшебные говорящие грибы.

Но работает тем не менее все довольно просто:

во время сканирования аннотаций, создаются несколько разных контекстов выполнения для одного и того же класса

Да, оказывается «так можно было».

Аннотация @Entity регистрирует класс в качестве entity JPA, @Table указывает на конкретную таблицу, @NamedQueries и @NamedQuery описывает именованное JPQL-выражение для получения записей из базы - все как в других, нормальных проектах.

А затем начинается чистая шиза:

в этом же классе указывается аннотация @Named, которая превращает класс в управляемый бин CDI, с возможностью связывания зависимых полей.

Контейнер CDI честно отрабатывает свою пайку и вставляет инстанс EntityManager, через который происходит работа с сущностями JPA в качестве поля этого управляемого бина. Который является тем же самым классом что и сама сущность.

Вставляет сюда:

@Transient
@PersistenceContext(unitName = "megaPU")
private EntityManager em;

Аннотация @Transient нужна для того чтобы скрыть вставляемое через CDI поле от механизма, отвечающего за сохранение данных в JPA.

Servlet API

Следующий интересный с точки зрения клинической психиатрии блок аннотаций отвечает за надругательство над Servlet API:

@Dependent
@WebFilter("/*")
@WebListener

Конечно же так тоже делать нельзя, более того — если попытаетесь комбинировать @RequestScoped,@SessionScoped или @ApplicationScopedи аннотации Servlet API получите отлуп а приложение упадет при установке.

Единственная причина, по которой эта дичь вообще работает — «волшебная» аннотация @Dependent, про которую никто (из моих коллег) никогда не слышал.

Согласно официальному описанию:

The default scope if none is specified; it means that an object exists to serve exactly one client (bean) and has the same lifecycle as that client (bean).

Во всех остальных случаях (для всех остальных scope) будет выбрасываться ошибка при установке. Разгадка находится в методе validateJSR299Scope, аналог которого есть в любой реализации Jakarta API.

Так что суммнарно аннотации @Named и @Dependent позволяют использовать класс в качестве бина для Jakarta Faces и одновременно использовать аннотации из Servlet API — на одном и том же классе.

Тут помимо аннотаций становятся нужны интерфейсы:

..
public class MegaBean extends Application implements Serializable,
        jakarta.servlet.Filter,
        ServletContextListener,
..      

Поэтому у класса появляются обязательные методы, которые необходимо реализовать.

Для интерфейса ServletContextListener это метод contextInitialized, ради которого собственно интерфейс и использовался:

..
@Override
@WebMethod(exclude = true)
@Transactional(Transactional.TxType.REQUIRED)
public void contextInitialized(ServletContextEvent sce) {
        final ServletContext sc = sce.getServletContext();
        // due to CDI vs servlet conflict
        sc.setAttribute("mega", this);
        // we can make it only here due to stackoverflow error 
        // in eclipselink
        this.current = new MegaBean();
        // reset fields back to nulls - to have working JSR303 validation
        resetFields(this.current);
        // populate JSF version details
        addVersionEnv(sc);
        // try to add some initial data if database is empty
        try {
            if (fetchRecordsCount() == 0) {
                //create test entity
                final MegaBean r = new MegaBean();
                r.setCreatedAt(new Date());
                r.setAuthor("system@test.org");
                r.setMessage("Test message");
                r.setTitle("Test title");
                em.merge(r);
                LOG.info(String
                .format("automatically added default record: %d", 
                r.getId()));
            }
        } catch (Exception e) {
            LOG.log(Level.WARNING,
                    String.format("Exception on startup: %s", 
                    e.getMessage()), e);
        }
}
..   

Напомню, что метод contextInitialized вызывается при запуске приложения, что используется для начальной загрузки данных в базу.

Кастомная авторизация

Следующий уровень отбитости, хотя и более слабый чем идея с JPA описанная выше:

полностью программная настройка авторизации, силами одних лишь аннотаций.

Конечно после красот Spring Boot это смотрится как жалкая пародия уже не так, но не забываем что Jakarta — API, у которого есть разные реализации.

И все они обязаны поддерживать вот такое:

..
@CustomFormAuthenticationMechanismDefinition(
        loginToContinue = @LoginToContinue(
                loginPage = "/index.xhtml?login=true",
                useForwardToLogin = false,
                errorPage = "/index.xhtml?login=true&error=true"
        )
)
// used only when embedded IdentityStore in use
@jakarta.annotation.security.DeclareRoles({"admin", "user", "demo"})
..

Верхняя аннотация «лошадиного размера» отвечает за настройку механизма авторизации — указывает на использование form-based авторизации с дополнительными настройками.

Нижняя @DeclareRoles описывает набор ролей, используемых приложением.

К сожалению аннотация @DeclareRoles работает только в сочетании с IdentityStore, который актвируется не всеми серверами приложений.

Отбитый API

Напоследок стоит рассказать и про реализацию двух вебсервисов в одном классе, средствами черной магии аннотаций.

На класс навешано два набора аннотаций, первая отвечает за инициализацию бина в качестве вебсервиса JAX-WS (старый добрый SOAP с XML):

@WebService

Каждый публичный метод, который должен быть скрыт от генератора вебсервисов должен быть помечен специальным образом:

@WebMethod(exclude = true)
public String getAuthor() { return author; }

Также будут пропущены все статичные поля.

Второй набор аннотаций отвечает за инициализацию JAX-RS и с ним все несколько сложнее:

@ApplicationPath("api")
@Path("")
@jakarta.ws.rs.ext.Provider

Помимо стандартных @ApplicationPath и @Path, которые вы точно видели в официальных примерах и более нормальных проектах, тут используется аннотация @Provider.

Нужна она ради метода toResponse, отвечающего за обработку исключений:

@Override
@WebMethod(exclude = true)
public Response toResponse(Exception e) {
    LOG.log(Level.WARNING, String.format("Exception on call : %s", 
                                                   e.getMessage()), e);
        return Response.status(400).entity(e.getMessage())
                .type("text/plain").build();
}

С помощью этого обработчика можно отдать клиенту вебсервиса красивое сообщение об ошибке, а не адовый треш трейс, который вы чаще всего и видите при ошибках в других проектах.

Часть 4. Сборка и деплой отбитой дичи

Проект целиком выложен в репозиторий на Github, сборка осуществляется с помощью обычного Apache Maven:

mvn clean package

Для сборки и запуска использовалась стандартная OpenJDK 21, которую поддерживают все используемые в статье серверы приложений.

К сожалению установка имеет свою специфику в каждом сервере приложений и временами требует дополнительных шагов настройки.

Самый простой способ увидеть наше «чудо-приложение» в действии — запустить специальный шаг Maven:

mvn liberty:run

После чего запустится скачивание сервера приложений OpenLiberty, его запуск и развертывание туда нашего приложения.

В качестве СУБД на всех серверах приложений использовался встраиваемый Apache Derby, за исключением Payara, где по-умолчанию используется H2.

Open Liberty / IBM Websphere Liberty

Проект OpenLiberty это открытая реализация сервера приложений, активно разрабатываемая IBM. Ее коммерческая версия IBM Websphere Liberty позиционируется как замена «большой» Websphere и основа всех будущих продуктов IBM, создаваемых на базе Websphere.

Вся основная разработка и развитие происходят в Open Liberty, затем переносятся в IBM Websphere Liberty, для которой потом оказывается коммерческая и долговременная поддержка.

Все описанные шаги по развертыванию актуальны и применимы для IBM Websphere Liberty.

Для статьи использовалась Open Liberty версии 25.0.0.5 с профилем Jakarta 10, скачать архив со сборкой можно по ссылке.

Так выглядит наша «адская гостевая» будучи запущенной в Open Liberty:

Прежде чем запускать приложение, необходимо настроить сервер — задать хотя-бы одного тестового пользователя и пул подключений к базе.

К сожалению OpenLiberty не подхватывает программный IdentityStore, который мы реализовывали в самом бине ради авторизации.

Для обоих действий надо изменить файл server.xml в каталоге:

wlp/usr/servers/defaultServer/server.xml

где wlp — корневой каталог распакованного Open Liberty.

Для регистрации тестового пользователя необходимо добавить блок:

<basicRegistry id="basic" realm="WebRealm">
        <user name="admin@test.org" password="admin"/>
        <group name="admin">
            <member name="admin"/>
        </group>      
 </basicRegistry>

Для пула подключений к базе:

<dataSource id="DefaultDataSource">
        <jdbcDriver libraryRef="phlegethLib"/>
        <properties.derby.embedded createDatabase="create" 
                                       databaseName="shuggDB"/>
        <containerAuthData password="y'hah" user="tharanak"/>
</dataSource>

<library id="phlegethLib">
        <file name="${server.config.dir}/lib/global/jdbc/derby.jar"/>
        <file name="${server.config.dir}/lib/global/jdbc/derbyshared.jar"/>
</library>

Также будет необходимо скопировать JDBC-драйвер для Derby в указанный выше каталог:

wlp/usr/servers/defaultServer/lib/global/jdbc

WAR-файл с нашей адской гостевой копируется в каталог dropins:

cp /opt/work/serial-experiments/madjpa/target/*.war wlp/sr/servers/defaultServer/dropins/

Запущенное приложение доступно по адресу:

http://localhost:9080/madjavaee-1.0.1-RELEASE/index.xhtml

Как уже упоминал выше:

Текущая версия OpenLiberty не дает использовать одновременно JAX-RS и JAX-WS вебсервис, основанный на одном и том же классе.

Хотя в предыдущих версиях прокатывало.

Поэтому придется либо закомментировать аннотацию @WebService для отключения JAX-WS либо набор аннотаций JAX-RS:

@ApplicationPath("api")
@Path("")
@jakarta.ws.rs.ext.Provider

Еще OpenLiberty не умеет генерировать WADL-файл с описанием JAX-RS сервиса, поэтому ссылка на него будет битой.

Wildfly

Wildfly (в девичестве JBoss) — один из самых известных серверов приложений с очень долгой историей.

Хотя это изначально открытый проект, как и в случае с разработкой IBM (Websphere Liberty) существует отдельный коммерческий продукт на его основе — Red Hat JBoss Enterprise Application Platform.

Как и в случае с OpenLiberty, смысл существования Wildfly — разработка и обкатка новых фич с помощью коммьюнити, с последующей продажей гоям в виде готового коммерческого продукта за серьезный прайс.

Для статьи использовался Wildfly 36.0.1.Final, архив с которым можно скачать по ссылке.

Так выглядит наше чудо-приложение в работе под управлением Wildfly:

Установка приложения на Wildfly заметно проще — достаточно скопировать WAR-файл с приложением в каталог:

wildfly/standalone/deployments

Поддержка Apache Derby, JDBC-драйвер и пул подключений по-умолчанию уже присутствуют.

К сожалению текущая версия Wildfly (на момент написания статьи) также не активирует программный IdentityStore — тестовый домен JASPIC убрали в настройке по-умолчанию.

Поэтому тестовых пользователей придется создавать вручную с помощью скрипта bin/add-user.sh

Запущенное приложение доступно по адресу:

http://localhost:8080/madjpa/index.xhtml

Eclipse Glassfish

Еще один очень известный проект с длинной историей, некогда разрабатываемый самой компанией Sun Microsystems в качестве эталонной реализации J2EE.

Для статьи использовалась версия 7.0.25 с профилем Jakarta EE Platform, скачать архив со сборкой можно по ссылке.

Так выглядит развертывание и запуск нашего приложения в Glassfish:

Запускается сервер командой bin/startserv

Glassfish поддерживает и похватывает при установке программный IdentityStore (будут работать встроенные в бин учетки) и пул по-умолчанию с Apache Derby и сочетание JAX-WS и JAX-RS вебсервисов на одном и том же классе.

И даже генерацию WADL-файла он тоже поддерживает:

Единственная яркая дичь — за каким-то хером в последних версиях перестал автоматически запускаться сервер Apache Derby, теперь для его запуска надо запустить консоль управления asadmin и выполнить команду:

start-database

Результат выполнения:

Payara

Наконец последний в сегодняшнем списке и наименее известный в наших краях проект Payara:

When commercial support for GlassFish ended in 2014, Payara Server was created as a fully-supported drop-in replacement. Payara Services was born in 2016 to offer support solutions for the application server.

Да, теперь вы тоже знаете что у Glassifsh оказывается были коммерческие пользователи, по зову которых и появилась Payara.

Для статьи использовался Payara Server 6.2025.5 с профилем Full, скачать сборку можно по ссылке.

Запускается командой:

bin/asadmin start-domain

Так выглядит в действии развертывание и запуск нашего приложения:

Тут все совсем хорошо и никаких дополнительных шагов не требуется совсем, база (H2) запускается по-умолчанию, программный IdentityStore подхватывается из бина и определяются оба типа вебсервисов — JAX-RS и JAX-WS, которые спокойно работают одновременно.

Эпилог

Перед вами одна из статей, материал для которой автор собирал несколько лет и до последнего не был уверен, что подобную «техноересь» вообще стоит показывать психически здоровым людям.

Храбрый индийский архитектор Чандракант (да будут всегда выделяться ресурсы его процессам) все же творил свою лютую месть довольно давно.

Поэтому адаптация его «архитектурного джихада» под современные реалии Java заняла в итоге несколько лет экспериментов и тестов — все ради того чтобы дорогие читатели ощутили на себе с какими ужасами от мира разработки временами приходится иметь дело.

Не буду просить читателей никогда не применять описанные выше техники в реальных проектах — все равно не получится, поскольку для такого нужен праведный гнев, избранность и помощь волшебных грибов.

Но если столкнетесь с таким на практике и захотите сохранить психику ваших программистов в девственном виде — теперь будете знать кому написать.

Контакты по прежнему в профиле, менее цензурный оригинал с саундтреком и дополнительными материалами в нашем блоге.

Комментарии (13)


  1. Mox
    07.06.2025 09:44

    Все что я думаю, читая этот код - это что вообще JavaEE и JSF прокляты by design.
    Я ушел с этого стека как только увидел Ruby On Rails.


    1. alex0x08 Автор
      07.06.2025 09:44

      Про Руби и «дружную команду рубистов из Днепра» тоже есть что рассказать — следите за анонсами:)


  1. Dhwtj
    07.06.2025 09:44

    "а вот как я умею!"

    Код написан чтобы все 100500 технологий упомянуть в резюме.

    Resume driven development в терминальной стадии


    1. alex0x08 Автор
      07.06.2025 09:44

      автору уже несколько поздновато врать в резюме, по той простой причине что даже в его реальные навыки и опыт теперь мало кто верит )