Thursday, November 23, 2023

Using OmniFaces CDI @ViewScoped with unload/destroy in a Spring Boot project

Introduction

While working on a Spring Boot based Faces project having memory issues, one of the solutions is to migrate the existing @Scope("view") beans and/or @SessionScope beans disguised as view scoped beans to use OmniFaces @ViewScoped instead, so that they get destroyed immediately when the webpage unloads and hereby also immediately frees up memory occuppied by the managed bean state as well as the Faces view state instead of basically having them to accumulate during the rest of the HTTP session.

Install JoinFaces in any case

JoinFaces is a great library for easily integrating Jakarta Faces in a Spring Boot project. However it unfortunately only supports using the standard Faces view scope @jakarta.faces.view.ViewScoped on a Spring managed bean. On the other hand, though, JoinFaces is already kind of the standard approach in order to effortlessly be able to use Jakarta Faces, Jakarta CDI and OmniFaces (and PrimeFaces, of course) in a Spring Boot project. It really takes away a lot of potential boilerplate in order to properly configure them in a Spring Boot based project. It's a matter of adding the following dependency (in Maven flavor) when you're already on Spring Boot 3.x or newer:

<dependency>
    <groupId>org.joinfaces</groupId>
    <artifactId>omnifaces-spring-boot-starter</artifactId>
    <version>5.x.x</version>
</dependency>

Or when you're still on Spring Boot 2.x:

<dependency>
    <groupId>org.joinfaces</groupId>
    <artifactId>omnifaces3-spring-boot-starter</artifactId>
    <version>4.x.x</version>
</dependency>

Getting OmniFaces @ViewScoped to work

One way would be to create a custom Spring Scope for this and port all the existing logic behind OmniFaces @ViewScoped over there, including the auto-inclusion of the unload script. However this seems to be unnecessary (not DRY) and brittle (sensitive to breaking when something incompatibly changes in the original OmniFaces @ViewScoped implementation).

Another way would be to just use a CDI managed bean instead of a Spring managed bean. Thanks to JoinFaces, CDI is readily available. However, the project already has a lot of existing Spring managed beans/services/repositories which couldn't be injected in a CDI managed bean via CDI's own @Inject. So they had to be injectable in a CDI managed bean in some way, without the need to convert these Spring managed beans to CDI managed beans and possibly losing any Spring-induced advantages/features on them and then requiring us to unnecessarily rewrite/migrate yet another bunch of code.

The idea (with help of Arjan Tijms) is to autodetect fields annotated with Spring-specific @Autowired in a CDI managed bean and simply let CDI fetch it from Spring context while injecting. This can be done with help of a CDI Extension. The major advantage is that we can then fully leverage to the original implementation of the CDI scope, including all of its originally intended behavior and features.

Activate CDI

To activate CDI in a Spring Boot project after installing JoinFaces, which by default uses the Weld implementation, simply add a beans.xml file to the src/main/resources/META-INF folder of the Spring Boot project:

<?xml version="1.0" encoding="UTF-8"?>
<beans
    xmlns="https://jakarta.ee/xml/ns/jakartaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/beans_4_0.xsd"
    version="4.0" bean-discovery-mode="annotated">
</beans>

Or when you're still on Spring Boot 2.x:

<?xml version="1.0" encoding="UTF-8"?>
<beans
    xmlns="http://xmlns.jcp.org/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_2_0.xsd"
    version="2.0" bean-discovery-mode="annotated">
</beans>

Note that this doesn't affect any existing Spring managed bean configurations. By default, they still have higher precedence. Moreover, the key configuration is explicitly setting CDI bean discovery mode to annotated so that CDI only tries to scan for beans with CDI's own bean defining annotations.

Also note that when running the Spring Boot application as a (fat) JAR file, the beans.xml needs to be located in BOOT-INF/classes/META-INF folder of the generated JAR file, not in the root META-INF folder containing web resources.

Instruct Spring to ignore CDI managed beans

We need to explicitly instruct Spring to ignore CDI managed beans annotated by @Named. Otherwise the Spring bean management facility may still have higher precedence to find and manage them, particularly if they are in the same package which is covered by @ComponentScan. This can be achieved by adding excludeFilters=@Filter(Named.class) to the @ComponentScan annotation on your Spring @Configuration class.

@Configuration
@ComponentScan(basePackages = YourSpringConfiguration.BEANS_PACKAGE, excludeFilters = @Filter(Named.class))
public class YourSpringConfiguration implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {

    public static final String BEANS_PACKAGE = "com.example.beans";

    // ...
}

Make ApplicationContext available in static context

Next step is to make sure that Spring's ApplicationContext is available in static context, so that the CDI extension can access it while trying to fetch Spring managed beans. There's namely no dedicated API for this in Spring Boot (yet?) such as Faces has with FacesContext.getCurrentInstance() and as CDI has with CDI.current().getBeanManager(). In a Spring Boot application, the canonical approach is to have a Spring managed bean implementing ApplicationContextAware. The average Spring Boot project should already have one somewhere, but in case yours doesn't seem to have one, then you can add one as follows:

@Component
public class Spring implements ApplicationContextAware {
 
    private static ApplicationContext context;
 
    @Override
    public void setApplicationContext(ApplicationContext context) throws BeansException {
        this.context = context;
    }
 
    public static ApplicationContext getContext() {
        return context;
    }
}

Create CDI extension

Now we can create the CDI extension which autodetects fields annotated with Spring's @Autowired in CDI managed beans annotated with @Named and makes sure that these fields are injected with beans obtained from Spring context.

public class SpringAutowiredExtension implements Extension {

    private Map<Class<?>, String> autowiredFields = new ConcurrentHashMap<>();

    public <T> void processAnnotatedType(@Observes @WithAnnotations(Named.class) ProcessAnnotatedType<T> processAnnotatedType, BeanManager beanManager) {
        Class<T> beanClass = processAnnotatedType.getAnnotatedType().getJavaClass();

        if (!beanClass.getPackage().getName().startsWith(YourSpringConfiguration.BEANS_PACKAGE)) {
            return; // This should filter out any CDI managed beans provided by e.g. Faces and OmniFaces themselves.
        }

        processAnnotatedType.configureAnnotatedType()
            .filterFields(field -> field.isAnnotationPresent(Autowired.class))
            .forEach(this::registerAutowiredField);
    }

    private void registerAutowiredField(AnnotatedFieldConfigurator<?> fieldConfigurator) {
        fieldConfigurator.add(InjectLiteral.INSTANCE);
        Field field = fieldConfigurator.getAnnotated().getJavaMember();
        autowiredFields.put(field.getType(), field.getName());
    }

    public void afterBeanDiscovery(@Observes AfterBeanDiscovery event) {
        autowiredFields.entrySet().forEach(autowiredField -> injectBeanViaSpringContext(event, autowiredField.getKey(), autowiredField.getValue()));
    }

    private static void injectBeanViaSpringContext(AfterBeanDiscovery event, Class<?> type, String name) {
        event.addBean().addType(type).createWith(ignoreCdiContext -> getBeanFromSpringContext(type, name));
    }

    private static Object getBeanFromSpringContext(Class<?> type, String name) {
        try {
            try {
                return Spring.getContext().getBean(type);
            }
            catch (NoUniqueBeanDefinitionException ignore) {
                return Spring.getContext().getBean(name);
            }
        }
        catch (Exception e) {
            throw new IllegalStateException("Cannot get bean from Spring context", e);
        }
    }
}

Register CDI extension

In order to get it to run, create a file with the exact name jakarta.enterprise.inject.spi.Extension in the src/main/resources/META-INF/services folder of the Spring Boot project and add a line to it representing the FQN of the CDI extension:

com.example.cdi.SpringAutowiredExtension

When you're still on Spring Boot 2.x, then the file should be named javax.enterprise.inject.spi.Extension

To confirm, when running the Spring Boot application as a (fat) JAR file, this file needs to be located in BOOT-INF/classes/META-INF/services folder of the generated JAR file.

Profit

That's basically all! You should now be able to use CDI managed beans with OmniFaces @ViewScoped and still therein have Spring managed beans at your availability via @Autowired.

@Named
@ViewScoped
public class YourViewScopedBean implements Serializable {
 
    private static final long serialVersionUID = 1L;

    @Autowired
    private SomeSpringComponent someSpringComponent;

    @PostConstruct
    public void init() {
        // SomeSpringComponent should be readily available at this point.
        // This is the key task of the CDI extension.
    }
 
    @PreDestroy
    public void destroy() {
        // This should be correctly invoked on page unload.
        // This is the key advantage over standard Faces @ViewScoped annotation.
    }
}

Saturday, September 23, 2023

OmniFaces 4.3 / 3.14.4 / 2.7.24 released!

OmniFaces 4.3 has been released!

A couple of new things were introduced:

  • <o:importConstants> and <o:importFunctions> got a new loader attribute where you can specify an object whose class loader will be used to load the class specified in the type attribute. In the end this should allow you to use a more specific class when there are duplicate instances in the runtime classpath, e.g. via multiple (plugin) libraries.
  • Inspired by this new loader attribute, a new <o:loadBundle> taghandler was introduced which also allows you to use loader attribute to load the bundle from a more specific library in case there are duplicate instances in the runtime classpath.
  • Faces(Local)#isAuthenticated() so that you can with less code check whether the current request is authenticated.

You can find the complete list of additions, changes and fixes at What's new in OmniFaces 4.3? list in showcase.

Installation

Non-Maven users: download OmniFaces 4.3 JAR and drop it in /WEB-INF/lib the usual way, replacing the older version if any.

Maven users: use <version>4.3</version>.

<dependency>
    <groupId>org.omnifaces</groupId>
    <artifactId>omnifaces</artifactId>
    <version>4.3</version>
</dependency>

How about OmniFaces 3.x, 2.x and 1.1x?

OmniFaces 3.x got the same bugfixes as 4.3 and has been released as 3.14.4. This version is for JSF 2.3 users with CDI. In case you've already migrated to Faces 3.0 or 4.0, please use OmniFaces 4.x instead. OmniFaces 2.x got the same bugfixes as well and has been released as 2.7.24. This version is for JSF 2.2 users with CDI. In case you've already migrated to JSF 2.3, please use OmniFaces 3.x instead.

The 1.1x is basically already since 2.5 in maintenance mode. I.e. only critical bugfix versions will be released. It's currently still at 1.14.1 (May 2017), basically featuring the same features as OmniFaces 2.4, but without any JSF 2.2 and CDI things and therefore compatible with CDI-less JSF 2.0/2.1.

Saturday, June 24, 2023

OmniFaces 4.2 / 3.14.3 / 2.7.23 released!

OmniFaces 4.2 has been released!

The PWAResourceHandler can now also be triggered via manifest.webmanifest instead of manifest.json. The manifest.webmanifest will eventually become the default resource name as per latest W3C draft on this.

The Components utility class got a bunch of new methods:

  • getRenderedValue(ValueHolder valueHolder) so that you can easily grab the to-be-rendered value of any ValueHolder component, particularly taking into account the internal state of any UIInput component, without the need to do all the checks if there's a submitted value or a local value or a converter etc
  • invalidateInputs(String... clientIds) so that you can explicitly invalidate specific UIInput components by (relative) client ID, e.g. when a specific DB constraint violation exception was thrown during a bean action which was unavoidable by a validator
  • invalidateInput(String clientId, String message, String... messageParams) which does basically the same but then allows you to add a faces message

The Messages utility class got a little brother who doesn't anywhere invoke FacesContext.getCurrentInstance(), the MessagesLocal. It has the same philosophy as FacesLocal.

You can find the complete list of additions, changes and fixes at What's new in OmniFaces 4.2? list in showcase.

Installation

Non-Maven users: download OmniFaces 4.2 JAR and drop it in /WEB-INF/lib the usual way, replacing the older version if any.

Maven users: use <version>4.2</version>.

<dependency>
    <groupId>org.omnifaces</groupId>
    <artifactId>omnifaces</artifactId>
    <version>4.2</version>
</dependency>

How about OmniFaces 3.x, 2.x and 1.1x?

OmniFaces 3.x got the same bugfixes as 4.2 and has been released as 3.14.3. This version is for JSF 2.3 users with CDI. In case you've already migrated to Faces 3.0 or 4.0, please use OmniFaces 4.x instead. OmniFaces 2.x got the same bugfixes as well and has been released as 2.7.23. This version is for JSF 2.2 users with CDI. In case you've already migrated to JSF 2.3, please use OmniFaces 3.x instead.

The 1.1x is basically already since 2.5 in maintenance mode. I.e. only critical bugfix versions will be released. It's currently still at 1.14.1 (May 2017), basically featuring the same features as OmniFaces 2.4, but without any JSF 2.2 and CDI things and therefore compatible with CDI-less JSF 2.0/2.1.

Sunday, February 26, 2023

OmniFaces 4.1 / 3.14.2 / 2.7.22 now available

OmniFaces 4.1 has been released!

Nothing shocking. Just one new tag attribute and a bunch of improvements/fixes as compared to 4.0. As usual all these improvements/fixes have been backported into 3.14.2 / 2.7.22.

You can find the complete list of additions, changes and fixes at What's new in OmniFaces 4.1? list in showcase.

Readonly hidden input

The new tag attribute is the <o:inputHidden readonly="true">. In standard Faces <h:inputHidden> this attribute is unspecified. In OmniFaces <o:inputHidden> this attribute is entirely server side. It will basically ignore the submitted value and grab the model value instead for validation. This can be very useful in cases when you intend to block the form submit via a validator attached to the <o:inputHidden> which validates the model value.

Below is an illustrative example:

<h:selectOneMenu binding="#{paymentMethod}" value="#{bean.paymentMethod}">
    <f:selectItem itemValue="bank" itemLabel="Bank" />
    <f:selectItem itemValue="card" itemLabel="Credit Card" />
    <f:selectItem itemValue="paypal" itemLabel="PayPal" />
</h:selectOneMenu>
<o:inputHidden value="#{bean.bankAccount.id}"
    readonly="true"
    required="#{param[paymentMethod.clientId] eq 'bank'}"
    requiredMessage="In order to pay with bank you need to create a valid bank account first" />

This removes the need to create a custom validator for such case or to awkwardly implement it in the action method.

Installation

Non-Maven users: download OmniFaces 4.1 JAR and drop it in /WEB-INF/lib the usual way, replacing the older version if any.

Maven users: use <version>4.1</version>.

<dependency>
    <groupId>org.omnifaces</groupId>
    <artifactId>omnifaces</artifactId>
    <version>4.1</version>
</dependency>

How about OmniFaces 3.x, 2.x and 1.1x?

OmniFaces 3.x got the same bugfixes as 4.1 and has been released as 3.14.2. This version is for JSF 2.3 users with CDI. In case you've already migrated to Faces 3.0 or 4.0, please use OmniFaces 4.x instead. OmniFaces 2.x got the same bugfixes as well and has been released as 2.7.22. This version is for JSF 2.2 users with CDI. In case you've already migrated to JSF 2.3, please use OmniFaces 3.x instead.

The 1.1x is basically already since 2.5 in maintenance mode. I.e. only critical bugfix versions will be released. It's currently still at 1.14.1 (May 2017), basically featuring the same features as OmniFaces 2.4, but without any JSF 2.2 and CDI things and therefore compatible with CDI-less JSF 2.0/2.1.