Spring Boot works with Tomcat embedded. Tomcat does not support Jakarta Faces. However, it is possible to add dependencies and use Faces. This article covers the additional configuration required to make Jakarta Faces functional with Spring Boot.
Tomcat
First, I want to see how to run WAR with Faces on standard Tomcat. In this case, Mojarra's README recommends to add this dependency
<dependency>
<groupId>org.jboss.weld.servlet</groupId>
<artifactId>weld-servlet-shaded</artifactId>
<version>4.0.0.Final</version>
</dependency>
It includes Jakarta API. And it is a problem. The Tomcat libraries include Jakarta API too. But their code is different. When I debug, the IDE shows me wrong lines. So I need to exclude from maven all the libs that Tomcat has. My version is
<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>
Dependency weld-servlet-core
includes a
listeners for servlet container to initialize CDI.
Dependency myfaces-impl
contains the
initializer to create FacesServlet.
I can add my initializer to set init parameters to servlet 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 is all. Now I can write xhtml and a backing beans.
Spring Boot
Now I will try Spring Boot. I used Spring Initializr to created project.
One more dependency for servlets:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
Delete provided 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 is
/META-INF/resources
. I added this directory
to /src/main/resources
. Let's 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 here
<h:outputScript library="js" name="demo.js" />
It means that file
/META-INF/resources/resources/js/demo.js
exists. I call demoLog
function from this
file on blur event.
HelloBacking.javapackage 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 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>
It doesn't work. Server returns my xhtml. It means there
is no faces servlet. There is difference from Tomcat.
Embedded version
does not execute
jakarta.servlet.ServletContainerInitializer
classes. I must implement
org.springframework.boot.web.servlet.ServletContextInitializer
bean to run the initialization.
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 initializers. For CDI is
org.jboss.weld.environment.servlet.EnhancedListener
. For Faces is
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);
}
}
Now I get error 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 classpath for
Servlet annotations and web-fragment.xml
. So
I must add new spring bean to my configuration.
Config.javaimport 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 it works.
Customize faces config
Let's try add something to faces-config.xml
.
I want to add a listener to ELContext and write an
evaluation events to Java Flight Recorder.
JFR Eventimport 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;
}
}
EvaluationListener.javaimport 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 should somehow add this listener to ELContext. I do this when the context is created. One more 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 should add this listener to Application
.
The third 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());
}
}
Now I can add this SystemEventListener
to
config.
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>
Now I can analyze the page rendering with Java Mission Control. It means the face-config.xml is loaded well.
Validator and converter
I can add something custom with 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 number input.
Validatorimport 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));
}
}
}
Converterimport 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>
But it does not work. I get errors
Could not find any registered converter-class by converterId : demo.BigDecimalConverter
Unknown validator id 'demo.NumberValidator'.
Because MyFaces searches classes with annotations in
/WEB-INF/classes
or in jars. It is not WAR
and there is no /WEB-INF/classes
. When I run
program from IDE, my classes is not in jar. If I package
and run jar, then it works. But I want run from IDE too.
There are three variants to solve problem.
First, I can write my
org.apache.myfaces.spi.AnnotationProvider
.
Register service by adding new file
src/main/resources/META-INF/services/org.apache.myfaces.spi.AnnotationProvidercom.example.demo.config.AnnotationProvider
Write provider with
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;
}
}
The second variant is add init parameter.
JsfInitializer.javaimport org.apache.myfaces.config.webparameters.MyfacesConfig;
context.setInitParameter(MyfacesConfig.SCAN_PACKAGES, "com.example.demo");
This parameter activates another scanning logic. It looks like in my provider but without Spring dependencies. Why are there two approaches? Because load all classes from all dependencies to check annotation is expensive. So the first approach is just read headers from binary *.class files and load only files with annotation. Or I must set one package to load.
The third variant is set parameter
context.setInitParameter(MyfacesConfig.USE_CDI_FOR_ANNOTATION_SCANNING, Boolean.TRUE.toString());
It means MyFaces calls CDI to search beans with
annotations. But our classes is not CDI beans. I should
add CDI annotation @Dependent
and this
variant works.
Inject beans to converter and validator
Let's move random generator to repository and inject it to 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;
I got NPE. DemoRepository is null. Because converter is
not CDI bean. If you use CDI for annotation scanning, it
doesn't matter. Scanning produces map of classes. When
request happens, MyFaces creates converter instance by
constructor without arguments. At most, it adds properties
from faces-config. To change that I should set
managed = true
to converter and validator
annotations. So MyFaces will ask them from CDI. I get
error shortly thereafter.
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 doesn't have this bean. The converter should has
@Dependent
annotation. Try again - same
error. Bean exists. But CDI can't find it. It happens
because the converter must has ID
<f:converter converterId="demo.BigDecimalConverter" />
So Myfaces requests the bean which has annotation
@FacesConverter(
value = "demo.BigDecimalConverter",
forClass = Object.class,
managed = true
)
But my bean has forClass = BigDecimal.class
.
That is why. I should remove forClass
from
annotation. Or I can remove tag
f:converter
and remove
value
from annotation. I removed
forClass
. It allows me to use my strange
converter for inputs with converter-tag and standard
converter for other BigDecimal inputs.
Now the converter and the validator work with injection.
Inject Faces Artifacts
Jakarta Faces adds faces artifacts like
FacesContext
, ExternalContext
,
ResourceHandler
, and others. Let's inject
init parameters to backing bean.
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 got 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<java.lang.String, java.lang.String>' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@jakarta.inject.Inject(), @jakarta.faces.annotation.InitParameterMap()}
Spring creates beans and tries to use Jakarta annotations
for scan.
ClassPathScanningCandidateComponentProvider
has includedFilters
, which in Spring Boot 3
contains AnnotationTypeFilters
, which are
these:
import org.springframework.stereotype.Component;
import jakarta.annotation.ManagedBean;
import jakarta.inject.Named;
I fix this by adding the
@ComponentScan
configuration to the Spring
application.
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 use Spring Boot, I guess you have Spring
components. And maybe you want to use them for Faces.
Let's convert DemoRepository
to Spring bean.
import org.springframework.stereotype.Repository;
@Repository
public class DemoRepository {
Now there is error
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 producer for Spring
ApplicationContext
and get bean from it 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;
}
}
BigDecimalConverter.javaimport 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 for spring beans? I tried that
BigDecimalConverter.javaimport com.example.demo.config.SpringBeanProducer.SpringBean;
@Inject
@SpringBean
private DemoRepository demoRepository;
SpringBeanProducer.javaimport 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);
}
This does not work. Because CDI does not match
DemoRepository.class
and
Object.class
.
@SpringBean
qualifier doesn't help. It means
I should inject bean in the converter like Object. So it
is better to use a setter injection.
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 inject Spring to CDI. Can I inject CDI to
Spring? I can do it with @PostConstruct
. For
example
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 as backing beans for my pages. I need ELResolver for that. Spring has one.
faces-config.xml<application>
<el-resolver>org.springframework.web.jsf.el.SpringBeanFacesELResolver</el-resolver>
</application>
Build JAR
This configuration is useful and I can test a build.
mvn package
The result jar will not work. Because Spring Boot makes custom jar with dependencies inside and uses custom ClassLoader to load all this.
I can replace
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
with
<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>
On my opinion it is the best variant. The Spring fat jar is complicated and adds points of failure. Not all libraries can work with that file structure.
If you want to keep a Spring Boot package, there are two problems to deal with.
1. CDI
CDI searches beans by crawling the jar file tree. And a
class name is builded by path. So the class name will be
BOOT-INF.classes.com.example.demo.converter.BigDecimalConverter
You should write your discovery strategy. I use
jandex
.
<dependency>
<groupId>org.jboss</groupId>
<artifactId>jandex</artifactId>
<version>3.1.1</version>
</dependency>
And I can add to that strategy my custom
BeanArchiveHandler
. I should register it as
service
META-INF/services/org.jboss.weld.environment.deployment.discovery.BeanArchiveHandlercom.example.demo.config.SpringBootFileSystemBeanArchiveHandler
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();
}
}
}
@Priority(100)
is important. Hight value
means that my handler will be sorted and used in the first
place.
2. Tomcat static resources
Tomcat creates static resources from jars. Tomcat asks all
urls from URLClassLoader and searches
/META-INF/resources
in that locations. Spring
Boot moves /src/main/resources
to root of
jar. But does not add the root to ClassLoader. Why does
spring not package resources to
/BOOT-INF/classes
? There is
issue. When they talk about jar, they forget that it is
executable. On their opinion, jar is a dependency for
another war project.
I wrote my
ConfigurableTomcatWebServerFactory
with a
listener. The listener adds my resources to Tomcat.
@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, change the dependencies.
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>jakarta.faces</artifactId>
<version>4.0.0</version>
</dependency>
<dependency> <!-- Optional, only when <f:websocket> is used. -->
<groupId>org.glassfish</groupId>
<artifactId>jakarta.json</artifactId>
<version>2.0.0</version>
</dependency>
Change ServletContainerInitializer
JsfInitializer.javaimport com.sun.faces.config.FacesInitializer;
ServletContainerInitializer facesInitializer = new FacesInitializer();
facesInitializer.onStartup(null, context);
Conclusion
It is as easy to run Faces with Spring Boot as to run initializers for embedded servlet container. But the packaging makes things worse.
The WAR package requires no additional configuration. Just
two initializers. And it works like Tomcat. The problem is
to run it from a IDE. Running the main class
DemoApplication from the IDE is not the same as running
java -jar demo.war
. DX is bad.
So I prefer to build JAR and to package it with dependencies outside. When I run the project from the IDE, I can change xhtml, java and changes are visible after page reload.