Spring bean creation conditionally #ConditionalOn - java

I would like to create a Bean only when httpPort value not equals -1. I tried below code but It is complaining something wrong with the expression. Could you please fix this? or how to achieve my requirements.
I am using Java 8 and Spring Boot 1.5.4
I tried below options
#ConditionalOnExpression("'${httpPort}' ne '-1'")
#ConditionalOnExpression("'${httpPort}' != '-1'")
#ConditionalOnExpression("!'${httpPort}'=='-1'")
#ConditionalOnExpression("!'${httpPort == -1}'")
#ConditionalOnExpression("!${httpPort == -1}")
Most of the cases error is EL1041E: After parsing a valid expression,
there is still more data in the expression: 'lcurly({)'
#Configuration
public class TomcatConfig {
#Value("${server.http.port:-1}")
private int httpPort;
#Bean
#ConditionalOnExpression("'${httpPort}' ne '-1'")
public EmbeddedServletContainerCustomizer containerCustomizer() {
return new EmbeddedServletContainerCustomizer() {
#Override
public void customize(ConfigurableEmbeddedServletContainer container) {
if (container instanceof TomcatEmbeddedServletContainerFactory) {
TomcatEmbeddedServletContainerFactory containerFactory =
(TomcatEmbeddedServletContainerFactory) container;
Connector connector = new Connector(TomcatEmbeddedServletContainerFactory.DEFAULT_PROTOCOL);
connector.setPort(httpPort);
containerFactory.addAdditionalTomcatConnectors(connector);
}
}
};
}
}

Solved this by #ConditionalOnProperty("server.http.port")
#ConditionalOnProperty("server.http.port")
public EmbeddedServletContainerCustomizer containerCustomizer() {
}

Related

Disable 1/yes/on and 0/no/off and allow only true/false to be passed via query-param or path-param

I want to disable Spring built-in deserialization of integer 0/1 and the strings
no/off yes/on that are mapped to false/true respectively when they are
passed via query-parameter or path-variable.
When any value that is not true/false is passed I want to throw IllegalArgumentException.
First I've implemented my own StringToBooleanConverter:
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
#Component
public class StringToBooleanConverter implements Converter<String, Boolean> {
#Override
public Boolean convert(String source) {
String value = source.trim().toLowerCase();
if ("true".equals(value)) {
return Boolean.TRUE;
} else if ("false".equals(value)) {
return Boolean.FALSE;
}
throw new IllegalArgumentException("Invalid boolean value '" + source + "'");
}
}
And since I've provided an implementation of similar class spring will use my StringToBooleanConverter and not the one from org.springframework.core.convert.support
But here is where my problem arise. When I send e.g. 0 code is reaching to the IllegalArgumentException and then execution passed to CustomBooleanEditor in the
org.springframework.beans.propertyeditors and the method public void setAsText(#Nullable String text) throws IllegalArgumentException {} is invoked and does almost the same logic
as StringToBooleanConverter.
How can I disable Spring from calling this method and use only the StringToBooleanConverter?
Try registering your Converter as follows:
#Configuration
public class WebConfig implements WebMvcConfigurer {
#Override
public void addFormatters(FormatterRegistry registry) {
registry.removeConvertible(String.class, Boolean.class);
registry.addConverter(new StringToBooleanConverter());
}
}
If this does not work, which I suspect. Try creating your own CustomBooleanEditor as follows:
#Configuration
public class Config {
#Bean
public CustomBooleanEditor getCustomBooleanEditor() {
return new CustomBooleanEditor("true", "false", false);
}
}
Configuring ObjectMapper disabling the following feature helped to solve the problem:
#Bean
#Primary
public ObjectMapper objectMapper() {
return JsonMapper.builder()
.disable(MapperFeature.ALLOW_COERCION_OF_SCALARS)
.build();
}

spring boot doesn't shows custom error pages

I added spring-boot-starter-thymeleaf dependency to my project that using spring boot 2.3.1.RELEASE and placed error.html file inside src/main/resources/templates with name error.html and other custom error pages inside src/main/resources/templates/error` as you can see in below image:
and add this configuration in application.yml:
server:
error:
whitelabel:
enabled: false
and exclude ErrorMvcAutoConfiguration by add #SpringBootApplication(exclude = {ErrorMvcAutoConfiguration.class}) into Application class.
But, unfortunately i see this below page when error occurs, for example for 404 error!
How can i solve this problem? i also googled for this but didn't find anything to help.
Try to use WebServerFactoryCustomizer:
#Configuration
public class WebConfig implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {
#Override
public void customize(ConfigurableServletWebServerFactory factory) {
factory.addErrorPages(
new ErrorPage(HttpStatus.FORBIDDEN, "/403"),
new ErrorPage(HttpStatus.NOT_FOUND, "/404"),
new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/500"));
}
}
And ErrorController:
#Controller
public class ErrorController {
#GetMapping("/403")
public String forbidden(Model model) {
return "error/403";
}
#GetMapping("/404")
public String notFound(Model model) {
return "error/404";
}
#GetMapping("/500")
public String internal(Model model) {
return "error/500";
}
#GetMapping("/access-denied")
public String accessDenied() {
return "error/access-denied";
}
}
I have the same structure and it works for me:
Example: Customize the Error Messages
PS: in mine application.yml i don't have any properties for error handling
You don't need to exclude ErrorMvcAutoConfiguration.
Simply put error pages to these places.
see also:
https://docs.spring.io/spring-boot/docs/2.3.1.RELEASE/reference/htmlsingle/#boot-features-error-handling-custom-error-pages
add this configuration in application.yml/application.properties:
server.error.path=/error
creat your error pages inder error path
error/400.html
error/500.html
error/error.html
create class implements ErrorController:
#Controller
public class MyErrorController implements ErrorController {
#RequestMapping("/error")
public String handleError(HttpServletRequest request) {
Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
if (status != null) {
Integer statusCode = Integer.valueOf(status.toString());
if(statusCode == HttpStatus.NOT_FOUND.value()) {
return "/error/404";
}
else if(statusCode == HttpStatus.INTERNAL_SERVER_ERROR.value()) {
return "/error/500";
}
}
return "/error/error";
}
}

Set jvmRoute in spring boot 2.0.0

For sticky session i need to set the jvmRoute of the embedded tomcat.
Actually only a
System.setProperty("jvmRoute", "node1");
is required, but i want to set a via application.properties configurable property. I don't know how and when to set this with #Value annotated property.
With #PostConstruct as described here, it does not work (at least not in spring boot 2.0.0.RELEASE)
The only way i found so far is
#Component
public class TomcatInitializer implements ApplicationListener<ServletWebServerInitializedEvent> {
#Value("${tomcat.jvmroute}")
private String jvmRoute;
#Override
public void onApplicationEvent(final ServletWebServerInitializedEvent event) {
final WebServer ws = event.getApplicationContext().getWebServer();
if (ws instanceof TomcatWebServer) {
final TomcatWebServer tws = (TomcatWebServer) ws;
final Context context = (Context) tws.getTomcat().getHost().findChildren()[0];
context.getManager().getSessionIdGenerator().setJvmRoute(jvmRoute);
}
}
}
It works, but it does not look like much elegant...
Any suggestions are very appreciated.
You can customise Tomcat's Context a little more elegantly by using a context customiser. It's a functional interface so you can use a lambda:
#Bean
public WebServerFactoryCustomizer<TomcatServletWebServerFactory> tomcatCustomizer() {
return (tomcat) -> tomcat.addContextCustomizers((context) -> {
Manager manager = context.getManager();
if (manager == null) {
manager = new StandardManager();
context.setManager(manager);
}
manager.getSessionIdGenerator().setJvmRoute(jvmRoute);
});
}
I'm using Spring Boot 2.0.4. The above answer did not work for me all the way. I had to update it this way:
#Bean
public WebServerFactoryCustomizer<TomcatServletWebServerFactory> servletContainer() {
return (tomcat) -> {
tomcat.addContextCustomizers((context) -> {
Manager manager = context.getManager();
if (manager == null) {
manager = new StandardManager();
context.setManager(manager);
}
((ManagerBase) context.getManager()).getEngine().setJvmRoute("tomcatJvmRoute");
});
};
}

Spring MVC Missing matrix variable

I'm trying to add a matrix parameter (or matrix variable) to my Rest Controller using SpringMVC (from Spring boot 1.2.3.RELEASE)
Here is my code :
#RestController
public class SubAgentsController {
#RequestMapping(value = "/{subagents}", method = RequestMethod.GET)
public SubAgent subAgents(#MatrixVariable(value="agentName", pathVar="subagents") String agentName) {
System.out.println(agentName);
}
}
Unfortunately, when I try to get :
http://localhost:8080/subagents;agentName=hello
that is the answer I receive :
There was an unexpected error (type=Bad Request, status=400).
Missing matrix variable 'agentName' for method parameter of type String
What did I do wrong ? According to http://docs.spring.io/spring-framework/docs/3.2.0.M2/reference/html/mvc.html that should work :-(
Thanks for your answers!
In SpringBoot Application In order to enable Matrix variables you need to define below override code
#Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
#Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
}
Otherwise, they’re disabled by default
As the documentation you linked to states,
Note that to enable the use of matrix variables, you must set the
removeSemicolonContent property of RequestMappingHandlerMapping to
false. By default it is set to true with the exception of the MVC
namespace and the MVC Java config both of which automatically enable
the use of matrix variables.
If you're configuring your application by extending WebMvcConfigurationSupport, then override the requestMappingHandlerMapping method which prepares the RequestMappingHandlerMapping and set its appropriate property.
#Override
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
final RequestMappingHandlerMapping requestMappingHandlerMapping = super.requestMappingHandlerMapping();
requestMappingHandlerMapping.setRemoveSemicolonContent(false); // <<< this
return requestMappingHandlerMapping;
}
You'll then be all set.
With Spring Boot, I think all you need is to declare a #Bean method with the above, ie. that returns a RequestMappingHandlerMapping instance.
If you are using Spring Data and its controller mapping, try something like this also
#Configuration
public class DataMvcConfig extends RepositoryRestMvcConfiguration {
#Override
public DelegatingHandlerMapping restHandlerMapping() {
final DelegatingHandlerMapping delegatingHandlerMapping = super.restHandlerMapping();
for (HandlerMapping delegate : delegatingHandlerMapping.getDelegates()) {
if (delegate instanceof AbstractHandlerMapping) {
((AbstractHandlerMapping)delegate).setRemoveSemicolonContent(false);
}
}
return delegatingHandlerMapping;
}
}

Spring: How to do AND in Profiles?

Spring Profile annotation allows you to select profiles. However if you read documentation it only allows you to select more than one profile with OR operation. If you specify #Profile("A", "B") then your bean will be up if either profile A or profile B is active.
Our use case is different we want to support TEST and PROD versions of multiple configurations. Therefore sometimes we want to autowire the bean only if both profiles TEST and CONFIG1 are active.
Is there any way to do it with Spring? What would be the simplest way?
Since Spring 5.1 (incorporated in Spring Boot 2.1) it is possible to use a profile expression inside profile string annotation. So:
In Spring 5.1 (Spring Boot 2.1) and above it is as easy as:
#Component
#Profile("TEST & CONFIG1")
public class MyComponent {}
Spring 4.x and 5.0.x:
Approach 1: answered by #Mithun, it covers perfectly your case of converting OR into AND in your profile annotation whenever you annotate the Spring Bean also with his Condition class implementation. But I want to offer another approach that nobody proposed that has its pro's and con's.
Approach 2:
Just use #Conditional and create as many Condition implementations as combinations needed. It has the con of having to create as many implementations as combinations but if you don't have many combinations, in my opinion, it is a more concise solution and it offers more flexibility and the chance of implementing more complex logical resolutions.
The implementation of Approach 2 would be as follows.
Your Spring Bean:
#Component
#Conditional(value = { TestAndConfig1Profiles.class })
public class MyComponent {}
TestAndConfig1Profiles implementation:
public class TestAndConfig1Profiles implements Condition {
#Override
public boolean matches(final ConditionContext context, final AnnotatedTypeMetadata metadata) {
return context.getEnvironment().acceptsProfiles("TEST")
&& context.getEnvironment().acceptsProfiles("CONFIG1");
}
}
With this approach you could easily cover more complex logical situations like for example:
(TEST & CONFIG1) | (TEST & CONFIG3)
Just wanted to give an updated answer to your question and complement other answers.
Since Spring does not provide the AND feature out of the box. I would suggest the following strategy:
Currently #Profile annotation has a conditional annotation #Conditional(ProfileCondition.class). In ProfileCondition.class it iterates through the profiles and checks if the profile is active. Similarly you could create your own conditional implementation and restrict registering the bean. e.g.
public class MyProfileCondition implements Condition {
#Override
public boolean matches(final ConditionContext context,
final AnnotatedTypeMetadata metadata) {
if (context.getEnvironment() != null) {
final MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
if (attrs != null) {
for (final Object value : attrs.get("value")) {
final String activeProfiles = context.getEnvironment().getProperty("spring.profiles.active");
for (final String profile : (String[]) value) {
if (!activeProfiles.contains(profile)) {
return false;
}
}
}
return true;
}
}
return true;
}
}
In your class:
#Component
#Profile("dev")
#Conditional(value = { MyProfileCondition.class })
public class DevDatasourceConfig
NOTE: I have not checked for all the corner cases (like null, length checks etc). But, this direction could help.
A little bit improved version of #Mithun answer:
public class AndProfilesCondition implements Condition {
public static final String VALUE = "value";
public static final String DEFAULT_PROFILE = "default";
#Override
public boolean matches(final ConditionContext context, final AnnotatedTypeMetadata metadata) {
if (context.getEnvironment() == null) {
return true;
}
MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
if (attrs == null) {
return true;
}
String[] activeProfiles = context.getEnvironment().getActiveProfiles();
String[] definedProfiles = (String[]) attrs.getFirst(VALUE);
Set<String> allowedProfiles = new HashSet<>(1);
Set<String> restrictedProfiles = new HashSet<>(1);
for (String nextDefinedProfile : definedProfiles) {
if (!nextDefinedProfile.isEmpty() && nextDefinedProfile.charAt(0) == '!') {
restrictedProfiles.add(nextDefinedProfile.substring(1, nextDefinedProfile.length()));
continue;
}
allowedProfiles.add(nextDefinedProfile);
}
int activeAllowedCount = 0;
for (String nextActiveProfile : activeProfiles) {
// quick exit when default profile is active and allowed profiles is empty
if (DEFAULT_PROFILE.equals(nextActiveProfile) && allowedProfiles.isEmpty()) {
continue;
}
// quick exit when one of active profiles is restricted
if (restrictedProfiles.contains(nextActiveProfile)) {
return false;
}
// just go ahead when there is no allowed profiles (just need to check that there is no active restricted profiles)
if (allowedProfiles.isEmpty()) {
continue;
}
if (allowedProfiles.contains(nextActiveProfile)) {
activeAllowedCount++;
}
}
return activeAllowedCount == allowedProfiles.size();
}
}
Was unable to post it in the comments.
Yet another option is to play on the Class/Method level allowed by the #Profile annotation. Not as flexible as implementing MyProfileCondition but quick and clean if it suits your case.
e.g. this won't start when FAST & DEV are both active, but will if only DEV is:
#Configuration
#Profile("!" + SPRING_PROFILE_FAST)
public class TomcatLogbackAccessConfiguration {
#Bean
#Profile({SPRING_PROFILE_DEVELOPMENT, SPRING_PROFILE_STAGING})
public EmbeddedServletContainerCustomizer containerCustomizer() {
Another kind of trick but might work in many scenarios is put #Profile annotation on #Configuration and the other #Profile on #Bean - that creates logical AND between 2 profiles in java-based spring config.
#Configuration
#Profile("Profile1")
public class TomcatLogbackAccessConfiguration {
#Bean
#Profile("Profile2")
public EmbeddedServletContainerCustomizer containerCustomizer() {
If you have already marked a configuration class or bean method with #Profile annotation, it is simple to check for additional profiles (e.g. for AND condition) with Environment.acceptsProfiles()
#Autowired Environment env;
#Profile("profile1")
#Bean
public MyBean myBean() {
if( env.acceptsProfiles("profile2") ) {
return new MyBean();
}
else {
return null;
}
}
I improved #rozhoc's answer since that answer did not account for the fact that no profile is equivalent to 'default' when it comes to using #Profile. Also, conditions that I wanted were !default && !a which #rozhoc's code did not handle properly. Finally I used some Java8 and show only the matches method for brevity.
#Override
public boolean matches(final ConditionContext context, final AnnotatedTypeMetadata metadata) {
if (context.getEnvironment() == null) {
return true;
}
MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
if (attrs == null) {
return true;
}
Set<String> activeProfilesSet = Arrays.stream(context.getEnvironment().getActiveProfiles()).collect(Collectors.toSet());
String[] definedProfiles = (String[]) attrs.getFirst(VALUE);
Set<String> allowedProfiles = new HashSet<>(1);
Set<String> restrictedProfiles = new HashSet<>(1);
if (activeProfilesSet.size() == 0) {
activeProfilesSet.add(DEFAULT_PROFILE); // no profile is equivalent in #Profile terms to "default"
}
for (String nextDefinedProfile : definedProfiles) {
if (!nextDefinedProfile.isEmpty() && nextDefinedProfile.charAt(0) == '!') {
restrictedProfiles.add(nextDefinedProfile.substring(1, nextDefinedProfile.length()));
continue;
}
allowedProfiles.add(nextDefinedProfile);
}
boolean allowed = true;
for (String allowedProfile : allowedProfiles) {
allowed = allowed && activeProfilesSet.contains(allowedProfile);
}
boolean restricted = true;
for (String restrictedProfile : restrictedProfiles) {
restricted = restricted && !activeProfilesSet.contains(restrictedProfile);
}
return allowed && restricted;
}
Here is how you actually use it in case that was confusing as well:
#Profile({"!default", "!a"})
#Conditional(value={AndProfilesCondition.class})

Categories