Jakarta Faces and Spring Boot

Spring Boot works with Tomcat Embed. Tomcat does not include support for Jakarta Faces and CDI. Despite this, it is possible to add the necessary dependencies and use Faces.

This article is about what configuration is needed to run Jakarta Faces with Spring Boot. I also described some of the errors that may occur.

tomcat

First of all, I want to see how a regular web application with Jakarta Faces will run on a full Tomcat.

For this case Mojarra README recommends add dependency for CDI

<dependency>
    <groupId>org.jboss.weld.servlet</groupId>
    <artifactId>weld-servlet-shaded</artifactId>
    <version>4.0.0.Final</version>
</dependency>

This library includes the Jakarta API. And this causes a problem. Tomcat already includes libraries with the Jakarta API. But the code there is different. During debugging, the IDE will show incorrect lines on these classes. Therefore, I have to exclude these APIs from dependencies. My version looks like this

<dependency>
    <groupId>org.apache.myfaces.core</groupId>
    <artifactId>myfaces-impl</artifactId>
    <version>4.0.0</version>
</dependency>
<dependency>
    <groupId>jakarta.servlet</groupId>
    <artifactId>jakarta.servlet-api</artifactId>
    <version>6.0.0</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>org.jboss.weld.servlet</groupId>
    <artifactId>weld-servlet-core</artifactId>
    <version>5.1.0.Final</version>
    <exclusions>
        <exclusion>
            <groupId>jakarta.el</groupId>
            <artifactId>jakarta.el-api</artifactId>
        </exclusion>
        <exclusion>
            <groupId>jakarta.annotation</groupId>
            <artifactId>jakarta.annotation-api</artifactId>
        </exclusion>
    </exclusions>
</dependency>

Addiction weld-servlet-core includes a listener for the container that initializes the CDI. Addiction myfaces-impl itself creates FacesServlet.

It remains for me to create an initializer to add parameters to the context.

import java.util.Set;

import jakarta.faces.application.ProjectStage;
import jakarta.faces.component.UIInput;
import jakarta.servlet.ServletContainerInitializer;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebListener;

@WebListener
public class ConfigureListener implements ServletContainerInitializer {

    @Override
    public void onStartup(Set<Class<?>> classes, ServletContext context) throws ServletException {
        context.setInitParameter(UIInput.EMPTY_STRING_AS_NULL_PARAM_NAME, Boolean.TRUE.toString());

        context.setInitParameter(ProjectStage.PROJECT_STAGE_PARAM_NAME, ProjectStage.Development.name());
    }
}

That’s all. Now I can create xhtml and beans.

spring boot

Now you can try Spring Boot. I used Spring Initializr to create a jar project.

I’ll add the spring dependency for the web:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

You can remove the provided dependency for servlet api

<dependency>
    <groupId>jakarta.servlet</groupId>
    <artifactId>jakarta.servlet-api</artifactId>
    <version>6.0.0</version>
    <scope>provided</scope>
</dependency>

WEBROOT for Tomcat embedded – /META-INF/resources. I added this directory to /src/main/resources. Now we can create a test page /src/main/resources/META-INF/resources/hello.xhtml

<!DOCTYPE html>
<html
    xmlns="https://jakarta.ee/xml/ns/jakartaee"
    xmlns:f="jakarta.faces.core"
    xmlns:h="jakarta.faces.html"
    xmlns:c="jakarta.tags.core">
<h:head>
    <title>Hello World</title>
    <h:outputScript library="js" name="demo.js" />
</h:head>
<h:body>
    <h1>Hello World</h1>
    <h:form id="helloForm">

        <h:inputText id="number"
            value="#{helloBacking.number}"
            onblur="demoLog(event.target.value)">
        </h:inputText>

        <h:commandButton id="submitBtn"
            value="Submit"
            type="button"
            action="#{helloBacking.submit}">
            <f:ajax execute="@form" render="output messages" />
        </h:commandButton>

        <div>
            <h:outputText id="output" value="#{helloBacking.number}" />
        </div>

        <h:messages id="messages" showSummary="true" showDetail="true"/>

    </h:form>
</h:body>
</html>

You can see the script here

<h:outputScript library="js" name="demo.js" />

That means it’s in the file /META-INF/resources/resources/js/demo.js. I call a function demoLog from this file to the blur event.

Bin for the page:

package com.example.demo.backing;

import jakarta.enterprise.context.SessionScoped;
import jakarta.inject.Named;

@Named(value = "helloWorld")
@SessionScoped
public class HelloWorld implements Serializable {
    private BigDecimal number;

    public void submit() {
        System.out.println(this.number);
    }

    public BigDecimal getNumber() {
        return number;
    }

    public void setNumber(BigDecimal number) {
        this.number = number;
    }
}

Two more configuration files

/src/main/resources/META-INF/beans.xml

<?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"
>
    <!-- CDI configuration here. -->
</beans>

/src/main/resources/META-INF/resources/WEB-INF/faces-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<faces-config
    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/web-facesconfig_4_0.xsd"
    version="4.0"
>
    <!-- Faces configuration here. -->
</faces-config>

And it doesn’t work.

The server returns my xhtml. This means there is no faces servlet. This is where the difference with Tomcat comes into play. Spring version does not cause classes jakarta.servlet.ServletContainerInitializer. I have to create a spring bean org.springframework.boot.web.servlet.ServletContextInitializer to run them on their own.

import org.springframework.boot.web.servlet.ServletContextInitializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class Config {
    @Bean
    public ServletContextInitializer jsfInitializer() {
        return new JsfInitializer();
    }
}

There are two ServletContainerInitializerto be launched. For CDI it is org.jboss.weld.environment.servlet.EnhancedListener. For MyFaces – org.apache.myfaces.webapp.MyFacesContainerInitializer.

import org.apache.myfaces.webapp.MyFacesContainerInitializer;
import org.jboss.weld.environment.servlet.EnhancedListener;
import org.springframework.boot.web.servlet.ServletContextInitializer;

import jakarta.faces.application.ProjectStage;
import jakarta.faces.component.UIInput;
import jakarta.servlet.ServletContainerInitializer;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;

public class JsfInitializer implements ServletContextInitializer {

    @Override
    public void onStartup(ServletContext context) throws ServletException {
        context.setInitParameter(UIInput.EMPTY_STRING_AS_NULL_PARAM_NAME, Boolean.TRUE.toString());
        context.setInitParameter(ProjectStage.PROJECT_STAGE_PARAM_NAME, ProjectStage.Development.name());

        EnhancedListener cdiInitializer = new EnhancedListener();
        cdiInitializer.onStartup(null, context);

        ServletContainerInitializer myFacesInitializer = new MyFacesContainerInitializer();
        myFacesInitializer.onStartup(null, context);
	}
}

After that a new error. Already from FacesServlet:

Servlet.init() for servlet [FacesServlet] threw exception
java.lang.IllegalStateException: No Factories configured for this Application. This happens if the faces-initialization does not work at all – make sure that you properly include all configuration settings necessary for a basic faces application and that all the necessary libs are included. Also check the logging output of your web application and your container for any exceptions!
If you did that and find nothing, the mistake might be due to the fact that you use some special web-containers which do not support registering context-listeners via TLD files and a context listener is not setup in your web.xml.
A typical config looks like this;

<listener>
    <listener-class>
        org.apache.myfaces.webapp.StartupServletContextListener
    </listener-class>
</listener>

This happens because Spring does not scan the classpath for servlet annotations and web-fragment.xml. So I have to add another spring bean to the config Config.java.

import org.apache.myfaces.webapp.StartupServletContextListener;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.context.annotation.Bean;
import jakarta.servlet.ServletContextListener;

@Bean
public ServletListenerRegistrationBean<ServletContextListener> facesStartupServletContextListener() {
    ServletListenerRegistrationBean<ServletContextListener> bean = new ServletListenerRegistrationBean<>();
    bean.setListener(new StartupServletContextListener());
    return bean;
}

Now the page is working.

Customize faces config

I will add something to faces-config.xml for check. I want to make a listener for ELContext and add an event to Java Flight Recorder.

import jdk.jfr.Event;
import jdk.jfr.Label;
import jdk.jfr.Name;

@Name("com.example.faces.EvaluateExpression")
@Label("Evaluate Expression")
public class EvaluateExpressionEvent extends Event {
	
	@Label("Expression")
	private String expression;

	public String getExpression() {
		return expression;
	}

	public void setExpression(String expression) {
		this.expression = expression;
	}
}
import jakarta.el.ELContext;
import com.example.demo.event.EvaluateExpressionEvent;

public class EvaluationListener extends jakarta.el.EvaluationListener {
	
	private ThreadLocal<EvaluateExpressionEvent> event = new ThreadLocal<>();

	@Override
    public void beforeEvaluation(ELContext context, String expression) {
		EvaluateExpressionEvent event = new EvaluateExpressionEvent();
		event.setExpression(expression);
		event.begin();
		this.event.set(event);
    }

	@Override
	public void afterEvaluation(ELContext context, String expression) {
		event.get().commit();
		event.remove();
    }
}

Now I have to somehow add it to ELContext. I will do it during creation. Another listener.

import jakarta.el.ELContext;
import jakarta.el.ELContextEvent;

public class ELContextListener implements jakarta.el.ELContextListener {

	@Override
	public void contextCreated(ELContextEvent event) {
		ELContext elContext = event.getELContext();
		elContext.addEvaluationListener(new EvaluationListener());
	}
}

I have to add it to Application. Another listener:

import jakarta.faces.application.Application;
import jakarta.faces.event.AbortProcessingException;
import jakarta.faces.event.SystemEvent;
import jakarta.faces.event.SystemEventListener;

public class ApplicationCreatedListener implements SystemEventListener {

	@Override
	public boolean isListenerForSource(Object source) {
		return source instanceof Application;
	}

	@Override
	public void processEvent(SystemEvent event) throws AbortProcessingException {
		Application application = (Application) event.getSource();
		application.addELContextListener(new ELContextListener());
	}
}

Already I can now prescribe it in faces-config.xml.

<application>
    <system-event-listener>
        <system-event-listener-class>com.example.demo.listener.ApplicationCreatedListener</system-event-listener-class>
        <system-event-class>jakarta.faces.event.PostConstructApplicationEvent</system-event-class>
        <source-class>jakarta.faces.application.Application</source-class>
    </system-event-listener>
</application>

I am now observing value calculations when rendering a page in a Java Mission Control. This means face-config.xml works.

Validator and Converter

I can add any of the annotations:

import jakarta.faces.component.FacesComponent;
import jakarta.faces.component.behavior.FacesBehavior;
import jakarta.faces.convert.FacesConverter;
import jakarta.faces.event.NamedEvent;
import jakarta.faces.render.FacesBehaviorRenderer;
import jakarta.faces.render.FacesRenderer;
import jakarta.faces.validator.FacesValidator;

I will try FacesValidator And FacesConverter for my input.

Validator:

import jakarta.faces.application.FacesMessage;
import jakarta.faces.component.UIComponent;
import jakarta.faces.context.FacesContext;
import jakarta.faces.validator.FacesValidator;
import jakarta.faces.validator.Validator;
import jakarta.faces.validator.ValidatorException;

@FacesValidator(value = "demo.NumberValidator")
public class NumberValidator implements Validator<BigDecimal>{

    private Random random = new Random();

    private BigDecimal getNumber() {
        return new BigDecimal(random.nextDouble(420)).setScale(2, RoundingMode.HALF_UP);
    }

    @Override
    public void validate(FacesContext context, UIComponent component, BigDecimal value)
            throws ValidatorException {
        if (value == null) {
            return;
        }
        BigDecimal orig = getNumber();
        if (value.compareTo(orig) < 0) {
            throw new ValidatorException(
                    new FacesMessage(FacesMessage.SEVERITY_ERROR,
                            "Wrong number",
                            "Number " + value + " less than " + orig));
        }
    }
}

Converter:

import jakarta.faces.component.UIComponent;
import jakarta.faces.context.FacesContext;
import jakarta.faces.convert.Converter;
import jakarta.faces.convert.ConverterException;
import jakarta.faces.convert.FacesConverter;

@FacesConverter(
        value = "demo.BigDecimalConverter",
        forClass = BigDecimal.class
        )
public class BigDecimalConverter implements Converter<BigDecimal> {

    private Random random = new Random();

    private BigDecimal getNumber() {
        return new BigDecimal(random.nextDouble(420)).setScale(2, RoundingMode.HALF_UP);
    }

    @Override
    public BigDecimal getAsObject(FacesContext context, UIComponent component, String value)
            throws ConverterException {
        if (value == null || value.trim().length() == 0) {
            return getNumber();
        }
        try {
            return new BigDecimal(value.trim()).setScale(2, RoundingMode.HALF_UP);
        } catch (NumberFormatException e) {
            throw new ConverterException(e.getMessage());
        }
    }

    @Override
    public String getAsString(FacesContext context, UIComponent component, BigDecimal value)
            throws ConverterException {
        if (value == null) {
            return "";
        }
        return value.toPlainString();
    }
}

hello.xhtml

<h:inputText id="number"
            value="#{helloBacking.number}"
            onblur="demoLog(event.target.value)">
    <f:converter converterId="demo.BigDecimalConverter" />
    <f:validator validatorId="demo.NumberValidator" /> 
</h:inputText>

And it doesn’t work. New bugs:

Could not find any registered converter-class by converterId : demo.BigDecimalConverter

Unknown validator id ‘demo.NumberValidator’.

Because MyFaces looks for classes with annotations in /WEB-INF/classes or jar files.
It’s not WAR and it’s not here /WEB-INF/classes. I am running the program from an IDE so my classes are not in a jar. If I build the project and run the jar, then it will work. But I need to run in the IDE too. There are three options for solving the problem.

First, I can implement org.apache.myfaces.spi.AnnotationProvider applying ClassPathScanningCandidateComponentProvider from spring.

import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.type.filter.AnnotationTypeFilter;

import jakarta.faces.component.FacesComponent;
import jakarta.faces.component.behavior.FacesBehavior;
import jakarta.faces.context.ExternalContext;
import jakarta.faces.convert.FacesConverter;
import jakarta.faces.event.NamedEvent;
import jakarta.faces.render.FacesBehaviorRenderer;
import jakarta.faces.render.FacesRenderer;
import jakarta.faces.validator.FacesValidator;

public class AnnotationProvider extends org.apache.myfaces.spi.AnnotationProvider {
    
    private static Set<Class<? extends Annotation>> annotationsToScan;

    static {
        annotationsToScan = new HashSet<>(7, 1f);
        annotationsToScan.add(FacesComponent.class);
        annotationsToScan.add(FacesBehavior.class);
        annotationsToScan.add(FacesConverter.class);
        annotationsToScan.add(FacesValidator.class);
        annotationsToScan.add(FacesRenderer.class);
        annotationsToScan.add(NamedEvent.class);
        annotationsToScan.add(FacesBehaviorRenderer.class);
    }

    @Override
    public Map<Class<? extends Annotation>, Set<Class<?>>> getAnnotatedClasses(ExternalContext ctx) {
        Map<Class<? extends Annotation>, Set<Class<?>>> result = new HashMap<>();
        
        ClassLoader cl = getClass().getClassLoader();
        Package[] packages = cl.getDefinedPackages();
        for (Package pack : packages) {
            pack.getName();
        }
        ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false);
        for (Class<? extends Annotation> a : annotationsToScan) {
            provider.addIncludeFilter(new AnnotationTypeFilter(a));
        }
        Set<BeanDefinition> components = provider.findCandidateComponents("com.example.demo");
        for (BeanDefinition c : components) {
            Class<?> clazz;
            try {
                clazz = cl.loadClass(c.getBeanClassName());
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
                continue;
            }
            for (Class<? extends Annotation> a : annotationsToScan) {
                Annotation an = clazz.getDeclaredAnnotation(a);
                if (an != null) {
                    Set<Class<?>> annotationSet = result.get(a);
                    if (annotationSet == null) {
                        annotationSet = new HashSet<>();
                        result.put(a, annotationSet);
                    }
                    annotationSet.add(clazz);
                }
            }
        }
        return result;
    }

    @Override
    public Set<URL> getBaseUrls(ExternalContext ctx) throws IOException {
        return null;
    }
}

Register my provider in src/main/resources/META-INF/services/org.apache.myfaces.spi.AnnotationProvider

com.example.demo.config.AnnotationProvider

The second option is to add a parameter to the context.

import org.apache.myfaces.config.webparameters.MyfacesConfig;

context.setInitParameter(MyfacesConfig.SCAN_PACKAGES, "com.example.demo");

This option activates other scanning logic. Approximately as in my provider, but without spring. Why are there two ways to scan? Because loading classes from all dependencies to check annotations is expensive. Therefore, by default, MyFaces only reads headers from compiled *.class files and only loads classes with the required annotations. For the second method, you must explicitly specify the packages to be scanned.

The third option is to set a parameter:

context.setInitParameter(
    MyfacesConfig.USE_CDI_FOR_ANNOTATION_SCANNING, Boolean.TRUE.toString());

Now MyFaces calls CDI to find beans with annotations. But my classes are not CDI beans. I have to add an annotation @Dependent and then this option will work.

Bean Injection into Converter and Validator

For testing, I will move the number generator to the repository and inject it into the converter and validator.

@Named
@ApplicationScoped
public class DemoRepository {

	private Random random = new Random();

	public BigDecimal getNumber() {
		return new BigDecimal(random.nextDouble(420)).setScale(2, RoundingMode.HALF_UP);
	}
}
public class BigDecimalConverter implements Converter<BigDecimal> {
    @Inject
	private DemoRepository demoRepository;

Now I have NPE. DemoRepository = null. Because the converter is not a CDI bean. If you are using CDI for scanning, this is not enough. Scanning only fills the map with classes. During the request, MyFaces creates a converter using the no-argument constructor. You can also add properties from faces-config when the converter was registered in the config. This is not my case. In order for the converter instance to be requested from CDI, I have to add managed = true to the converter annotation.

Right after that I get a new error:

Cannot invoke “jakarta.faces.convert.Converter.getAsString(jakarta.faces.context.FacesContext, jakarta.faces.component.UIComponent, Object)” because the return value of “org.apache.myfaces.cdi.wrapper.FacesConverterCDIWrapper. getWrapped()” is null

CDI does not know such beans. The converter must be marked with an annotation @Dependent. I try again – same error. The bean exists, but CDI does not find it. Because the converter must have an ID

<f:converter converterId="demo.BigDecimalConverter" />

This means Myfaces is looking for a converter by annotation

@FacesConverter(
        value = "demo.BigDecimalConverter",
        forClass = Object.class,
        managed = true
        )

But my converter has forClass = BigDecimal.class. This is a mistake. I have to delete forClass at the converter. Or can I remove the tag f:converter and delete accordingly value at the converter. Only one thing. I deleted forClass. This will allow me to use my weird converter for tagged inputs and leave the default converter for other BigDecimal fields.

Now my injection works.

Faces Entity Injection

Jakarta Faces adds entities such as FacesContext, ExternalContext, ResourceHandler, and others. Let’s try to populate the bean with the init parameters that I added in my initializer.

import jakarta.faces.annotation.InitParameterMap;

@Inject
@InitParameterMap
private Map<String, String> initParam;

System.out.println("Empty as null = "
    + this.initParam.get(UIInput.EMPTY_STRING_AS_NULL_PARAM_NAME));

I get an error:

ConfigServletWebServerApplicationContext : Exception encountered during context initialization – cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name ‘helloBacking’: Unsatisfied dependency expressed through field ‘initParam’: No qualifying bean of type ‘java.util. Map‘ available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: @jakartaa.inject.Inject(), @jakarta.faces.annotation.InitParameterMap()}

This is from the spring. Spring creates beans and uses Jakarta annotations to scan bean candidates.
ClassPathScanningCandidateComponentProvider It has includedFilterswhich in Spring Boot 3 contains the following annotations by default:

    import org.springframework.stereotype.Component;
    import jakarta.annotation.ManagedBean;
    import jakarta.inject.Named;

I will fix this conflict by adding @ComponentScan to the application configuration. I’ll leave it for the spring only @Component.

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Component;

@ComponentScan(
        useDefaultFilters = false,
        includeFilters = {
            @ComponentScan.Filter(
                type = FilterType.ANNOTATION,
                value = Component.class)
        }
    )
@SpringBootApplication

Using Spring Beans

If you are using Spring Boot, then most likely you have spring beans. And most likely you will want to use them for Faces. Let’s turn DemoRepository into a spring bin.

import org.springframework.stereotype.Repository;

@Repository
public class DemoRepository {

Now I have an error. CDI does not have the correct bean to inject into the converter.

Caused by: org.jboss.weld.exceptions.DeploymentException: WELD-001408: Unsatisfied dependencies for type DemoRepository with qualifiers @Default
at injection point [BackedAnnotatedField] @Inject private com.example.demo.converter.BigDecimalConverter.demoRepository
at com.example.demo.converter.BigDecimalConverter.demoRepository(BigDecimalConverter.java:0)

I can write a CDI producer for spring ApplicationContext. Inject it. And ask him for the desired bin in @PostConstruct method.

import org.springframework.context.ApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

import jakarta.enterprise.context.Dependent;
import jakarta.enterprise.inject.Produces;
import jakarta.servlet.ServletContext;

@Dependent
public class SpringBeanProducer {

	@Produces
	public ApplicationContext springContext(ServletContext sctx) {
		ApplicationContext ctx = WebApplicationContextUtils.getRequiredWebApplicationContext(sctx);
		return ctx;
	}
}

In the converter:

import jakarta.annotation.PostConstruct;

private DemoRepository demoRepository;

@Inject
private ApplicationContext springContext;

@PostConstruct
private void init() {
    this.demoRepository = springContext.getBean(DemoRepository.class);
} 

Can I write a producer directly for the bins? I’ll try to do the following.

In the converter I will add an annotation:

import com.example.demo.config.SpringBeanProducer.SpringBean;

@Inject
@SpringBean
@Qualifier("demoRepository")
private DemoRepository demoRepository;

And a new producer that searches for a bean by class and name:

import jakarta.enterprise.inject.spi.InjectionPoint;
import jakarta.inject.Qualifier;

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface SpringBean {
}

@Produces
@SpringBean
public Object create(InjectionPoint ip, ServletContext sctx) {
    Class beanClass = (Class) ip.getType();
    Set<Annotation> qualifiers = ip.getAnnotated().getAnnotations();
    String beanName = null;
    for (Annotation a : qualifiers) {
        if (a instanceof org.springframework.beans.factory.annotation.Qualifier q) {
            beanName = q.value();
        }
    }

    ApplicationContext ctx = WebApplicationContextUtils.getRequiredWebApplicationContext(sctx);
    if (beanName != null && !beanName.isBlank()) {
        return ctx.getBean(beanName, beanClass);
    }
        
    return ctx.getBean(beanClass);
}

But it doesn’t work. Because CDI can’t match DemoRepository.class And Object.class. It will only use the producer if it returns the correct class. Annotations @SpringBean few. This means that I have to write my own producer for each bean. If there are a lot of them, then this is not a solution. Or should I inject the bean into the converter as Object. It is more convenient to write a setter for each injection than a producer for each bean.

import org.springframework.beans.factory.annotation.Qualifier;

private DemoRepository demoRepository;

@Inject
public void setDemoRepository(
    @SpringBean @Qualifier("demoRepository") Object repository) {
    this.demoRepository = (DemoRepository) repository;
}

Now I can use Spring in CDI.

Is it possible to inject dependencies from CDI into Spring? I can do it in @PostConstruct in the following way:

import jakarta.enterprise.inject.spi.CDI;
import jakarta.enterprise.util.TypeLiteral;
import jakarta.faces.annotation.InitParameterMap;

private Map<String, String> initParam;

@PostConstruct
private void init() {
    this.initParam = CDI.current().select(
            new TypeLiteral<Map<String, String>>() {},
            new InitParameterMap.Literal()
        ).get();
}

The last step is to use Spring beans directly in xhtml. This requires ELResolver. Spring already has one. It remains to register it in faces-config.xml.

<application>
    <el-resolver>
        org.springframework.web.jsf.el.SpringBeanFacesELResolver
    </el-resolver>
</application>

Building a JAR

The configuration looks to be working. can be collected and tested.

mvn package

As a result, the compiled jar does not work. Because Spring Boot makes a special fat jar with dependencies inside and makes its own ClassLoader to load all this.

I can change it

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
</plugin>

on this

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-dependency-plugin</artifactId>
    <executions>
        <execution>
            <id>copy-dependencies</id>
            <phase>prepare-package</phase>
            <goals>
                <goal>copy-dependencies</goal>
            </goals>
            <configuration>
                <outputDirectory>
                    ${project.build.directory}/libs
                </outputDirectory>
            </configuration>
        </execution>
    </executions>
</plugin>

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-jar-plugin</artifactId>
    <configuration>
        <archive>
            <manifest>
                <addClasspath>true</addClasspath>
                <classpathPrefix>libs/</classpathPrefix>
                <mainClass>
                    com.example.demo.DemoApplication
                </mainClass>
            </manifest>
        </archive>
    </configuration>
</plugin>

In my opinion this is the best option. The spring assembly is tricky and is another point of failure. Not all libraries can work with this archive structure.

If you want to keep packaging from Spring Boot, then you have to solve two problems.

1. CDI

CDI looks for bins by bypassing the file structure of the archive. And the class name is eventually collected from the path. So the class name will be
BOOT-INF.classes.com.example.demo.converter.BigDecimalConverter

To fix this, you need to write your search strategy. I use jandex. I will redefine the strategy from this library.

<dependency>
    <groupId>org.jboss</groupId>
    <artifactId>jandex</artifactId>
    <version>3.1.1</version>
</dependency>

I implement my org.jboss.weld.environment.deployment.discovery.BeanArchiveHandler. And register it in META-INF/services.

import org.jboss.weld.environment.deployment.discovery.BeanArchiveBuilder;
import org.jboss.weld.environment.deployment.discovery.jandex.JandexFileSystemBeanArchiveHandler;
import jakarta.annotation.Priority;

@Priority(100)
public class SpringBootFileSystemBeanArchiveHandler extends JandexFileSystemBeanArchiveHandler {

    private static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";

    @Override
    public BeanArchiveBuilder handle(String path) {
        // use only for spring boot build
        if (!path.toLowerCase().endsWith(".jar")) {
            // try other handlers
            return null;
        }
        return super.handle(path);
    }

    @Override
    protected void add(Entry entry, BeanArchiveBuilder builder) throws MalformedURLException {
        if (!entry.getName().startsWith(BOOT_INF_CLASSES)
                || !entry.getName().endsWith(CLASS_FILE_EXTENSION)) {
            // skip spring classes for loader and other resources 
            return;
        }
        entry = new SpringBootEntry(entry);
        super.add(entry, builder);
    }
    
    protected class SpringBootEntry implements Entry {

        private Entry delegate;
        
        public SpringBootEntry(Entry entry) {
            this.delegate = entry;
        }

        @Override
        public String getName() {
            String name = delegate.getName();
            // cut off prefix from class name
            name = name.substring(BOOT_INF_CLASSES.length());
            return name;
        }

        @Override
        public URL getUrl() throws MalformedURLException {
            return delegate.getUrl();
        }
        
    }
}

annotation @Priority(100) important. The class with the highest value will be used first. If another BeanArchiveBuilder finds something first, then the next ones are not called at all.

2. Tomcat static resources

Tomcat reads static from jar. Tomcat polls all urls from URLClassLoader for /META-INF/resources. And uses these directories as WEBROOT for static. Spring Boot packages /src/main/resources to the root of the archive. But does not add a root to the ClassLoader it creates. There are only /BOOT-INF/classes and all dependency archives. Why does spring not put resources in /BOOT-INF/classes Then? Eat issue on this. And when spring people talk about jar, they forget that it can be executable. In their opinion jar is a dependency for another web project.

As you understand, this problem can be solved by separating everything for Faces into a separate module and connecting it as a dependency. This can also solve the problem with CDI.

I’ll write mine ConfigurableTomcatWebServerFactory with a tomcat event listener. The listener will add another resource for the static.

@Bean
public TomcatServletWebServerFactory tomcatFactory() {
    TomcatFactory factory = new TomcatFactory();
    return factory;
}
import org.apache.catalina.Context;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleEvent;
import org.apache.catalina.LifecycleListener;
import org.apache.catalina.WebResourceRoot.ResourceSetType;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;

public class TomcatFactory extends TomcatServletWebServerFactory {

    @Override
    protected void postProcessContext(Context context) {
        context.addLifecycleListener(new WebResourcesConfigurer(context));
    }
    
    public class WebResourcesConfigurer implements LifecycleListener {
        
        private static final String META_INF_RESOURCES = "META-INF/resources";
        private Context context;

        public WebResourcesConfigurer(Context context) {
            this.context = context;
        }

        @Override
        public void lifecycleEvent(LifecycleEvent event) {
            if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
                ClassLoader classLoader = getClass().getClassLoader();
                if (!(classLoader instanceof URLClassLoader)) {
                    return;
                }
                URL jarRoot = classLoader.getResource(META_INF_RESOURCES);
                if (jarRoot == null) {
                    logger.warn("Web resources not found");
                    return;
                }

                try {
                    int innerRootIndex = jarRoot.getPath().indexOf("!/");
                    String path = jarRoot.getPath().substring(0, innerRootIndex);
                    jarRoot = new URL(path);
                } catch (Exception e) {
                    logger.warn("Web resources URL error", e);
                    return;
                }
                context.getResources().createWebResourceSet(ResourceSetType.RESOURCE_JAR,
                        "/", jarRoot, "/" + META_INF_RESOURCES);
                logger.info("Web resources were added to Tomcat");
            }
        }
    }
}

Mojarra

Above I experimented with Apache MyFaces. If you want to use Eclipse Mojarra, there are two edits to make.

Replace dependency with

<dependency>
    <groupId>org.glassfish</groupId>
    <artifactId>jakarta.faces</artifactId>
    <version>4.0.0</version>
</dependency>
<dependency> <!-- Опционально для вебсокетов -->
    <groupId>org.glassfish</groupId>
    <artifactId>jakarta.json</artifactId>
    <version>2.0.0</version>
</dependency>

Replace ServletContainerInitializer.

import com.sun.faces.config.FacesInitializer;

ServletContainerInitializer facesInitializer = new FacesInitializer();
facesInitializer.onStartup(null, context);

Conclusion

Running Jakarta Faces with Spring Boot is as easy as running the initializers for the built-in servlet container.

But packaging complicates things.

The WAR package does not require any additional settings. Only initializers, and everything works as if deployed in a full-fledged Tomcat. The problem is running this from the IDE. Running main-class DemoApplication from the IDE is not the same as running

java -jar demo.war

DX will be bad.

Therefore, I prefer to build JARs. And put the dependencies outside in a separate directory. And when working from the IDE, I can edit xhtml, java, and the changes are visible without restarting. You just need to refresh the page.

Links

  1. Face specification

  2. CDI specification

  3. Servlet specification

  4. spring boot

  5. Mojarra

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *