Spring Boot JettyServerCustomizer | HTTPS port not opened | A ServletContext is required to configure default servlet handling - https

I am trying to use Spring Boot with embedded. I want the embedded Jetty to open a HTTPS port at 443.
After referring the answer posted here, I came up with this configuration:-
import java.io.FileNotFoundException;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ssl.SslSocketConnector;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.embedded.jetty.JettyServerCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
import org.springframework.util.ResourceUtils;
import com.samsoft.expunto.service.UserService;
/**
* #author Kumar Sambhav Jain
*
*/
#Configuration
#EnableWebMvcSecurity
#EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private UserService userService;
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeRequests().antMatchers("/", "/resources/**")
.permitAll().anyRequest().authenticated().and().formLogin()
.loginPage("/").defaultSuccessUrl("/home", false).and()
.requiresChannel().anyRequest().requiresSecure().and().logout()
.invalidateHttpSession(true).logoutUrl("/logout")
.logoutSuccessUrl("/").and().userDetailsService(userService);
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth)
throws Exception {
}
#Bean
public JettyServerCustomizer jettyCutomizer() {
return new JettyServerCustomizer() {
#Override
public void customize(Server server) {
SslContextFactory sslContextFactory = new SslContextFactory();
sslContextFactory.setKeyStorePassword("jetty6");
try {
sslContextFactory.setKeyStorePath(ResourceUtils.getFile(
"classpath:jetty-ssl.keystore").getAbsolutePath());
} catch (FileNotFoundException ex) {
throw new IllegalStateException("Could not load keystore",
ex);
}
SslSocketConnector sslConnector = new SslSocketConnector(
sslContextFactory);
sslConnector.setPort(443);
sslConnector.setMaxIdleTime(60000);
server.addConnector(sslConnector);
}
};
}
}
Trying to run the application using spring-boot:run, I can see in logs that port 80 is opened but no HTTPS port:-
2014-06-10 23:41:56.932 INFO 196 --- [lication.main()] /
: Initializing Spring FrameworkServlet 'dispatcherServlet' 2014-06-10
23:41:56.932 INFO 196 --- [lication.main()]
o.s.web.servlet.DispatcherServlet : FrameworkServlet
'dispatcherServlet': initialization started 2014-06-10 23:41:56.960
INFO 196 --- [lication.main()] o.s.web.servlet.DispatcherServlet
: FrameworkServlet 'dispatcherServlet': initialization completed in 26
ms 2014-06-10 23:41:57.037 INFO 196 --- [lication.main()]
o.e.jetty.server.AbstractConnector : Started
SelectChannelConnector#0.0.0.0:80 2014-06-10 23:41:57.043 INFO 196
--- [lication.main()] .s.b.c.e.j.JettyEmbeddedServletContainer : Jetty started on port: 80 2014-06-10 23:41:57.045 INFO 196 ---
[lication.main()] c.s.expunto.web.config.Application : Started
Application in 7.628 seconds (JVM running for 16.509)
UPDATE
Using this configuration:-
#Configuration
#EnableWebMvcSecurity
#EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter
implements EmbeddedServletContainerCustomizer {
#Autowired
private UserService userService;
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeRequests().antMatchers("/", "/resources/**")
.permitAll().anyRequest().authenticated().and().formLogin()
.loginPage("/").defaultSuccessUrl("/home", false).and()
.requiresChannel().anyRequest().requiresSecure().and().logout()
.invalidateHttpSession(true).logoutUrl("/logout")
.logoutSuccessUrl("/").and().userDetailsService(userService);
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth)
throws Exception {
}
public JettyServerCustomizer jettyServerCustomizer() {
return new JettyServerCustomizer() {
#Override
public void customize(Server server) {
SslContextFactory sslContextFactory = new SslContextFactory();
sslContextFactory.setKeyStorePassword("jetty6");
try {
sslContextFactory.setKeyStorePath(ResourceUtils.getFile(
"classpath:jetty-ssl.keystore").getAbsolutePath());
} catch (FileNotFoundException ex) {
throw new IllegalStateException("Could not load keystore",
ex);
}
SslSocketConnector sslConnector = new SslSocketConnector(
sslContextFactory);
sslConnector.setPort(443);
sslConnector.setMaxIdleTime(60000);
server.addConnector(sslConnector);
}
};
}
public void customizeJetty(
JettyEmbeddedServletContainerFactory containerFactory) {
containerFactory.addServerCustomizers(jettyServerCustomizer());
}
#Override
public void customize(ConfigurableEmbeddedServletContainer container) {
if (container instanceof JettyEmbeddedServletContainerFactory) {
customizeJetty((JettyEmbeddedServletContainerFactory) container);
}
container.setContextPath("");
}
}
I get this error:-
: java.lang.IllegalArgumentException: A ServletContext is required to configure default servlet handling
at org.springframework.util.Assert.notNull(Assert.java:112)
at org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer.<init>(DefaultServletHandlerConfigurer.java:54)
at org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport.defaultServletHandlerMapping(WebMvcConfigurationSupport.java:346)
at org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration$$EnhancerBySpringCGLIB$$d7014349.CGLIB$defaultServletHandlerMapping$24(<generated>)
at org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration$$EnhancerBySpringCGLIB$$d7014349$$FastClassBySpringCGLIB$$ec8be680.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:228)
at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:312)
at org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration$$EnhancerBySpringCGLIB$$d7014349.defaultServletHandlerMapping(<generated>)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:601)
at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:166)

As per M. Deinum advice, moving the customizers to class annotated with #EnableAutoConfiguration did the trick.
This is what worked for me:-
#Configuration
#EnableAutoConfiguration
public class Application implements EmbeddedServletContainerCustomizer {
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
#Bean
public JettyServerCustomizer jettyServerCustomizer() {
return new JettyServerCustomizer() {
#Override
public void customize(Server server) {
SslContextFactory sslContextFactory = new SslContextFactory();
sslContextFactory.setKeyStorePassword("jetty6");
try {
sslContextFactory.setKeyStorePath(ResourceUtils.getFile(
"classpath:jetty-ssl.keystore").getAbsolutePath());
} catch (FileNotFoundException ex) {
throw new IllegalStateException("Could not load keystore",
ex);
}
SslSocketConnector sslConnector = new SslSocketConnector(
sslContextFactory);
sslConnector.setPort(443);
sslConnector.setMaxIdleTime(60000);
server.addConnector(sslConnector);
}
};
}
public void customizeJetty(
JettyEmbeddedServletContainerFactory containerFactory) {
containerFactory.addServerCustomizers(jettyServerCustomizer());
}
#Override
public void customize(ConfigurableEmbeddedServletContainer container) {
if (container instanceof JettyEmbeddedServletContainerFactory) {
customizeJetty((JettyEmbeddedServletContainerFactory) container);
}
}
}

Related

How to override SecurityFilterChain in Spring Boot context?

I am facing the issue which is not obvious to resolve just by reading the documentation. While migrating to Spring Boot v2.7.4 / Spring Security v5.7.3 I have refactored the configuration not to extend WebSecurityConfigurerAdapter and to look like below:
#Configuration
#EnableWebSecurity
public class CustomSecurityConfig {
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.
csrf().disable().
logout().disable().
authorizeRequests().anyRequest().permitAll();
return http.build();
}
}
The above method is called, however has no effect as SecurityFilterChain instance created by OAuth2SecurityFilterChainConfiguration is used instead (I see that from debug by inspecting the list of filter in the stack that has e.g. LogoutFilter which should be disabled by above configuration). Debug log:
2022-10-20 15:49:48.790 [main] o.s.b.a.s.DefaultWebSecurityCondition : Condition DefaultWebSecurityCondition on org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerJwtConfiguration$OAuth2SecurityFilterChainConfiguration matched due to AllNestedConditions 2 matched 0 did not; NestedCondition on DefaultWebSecurityCondition.Beans #ConditionalOnMissingBean (types: org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter,org.springframework.security.web.SecurityFilterChain; SearchStrategy: all) did not find any beans; NestedCondition on DefaultWebSecurityCondition.Classes #ConditionalOnClass found required classes 'org.springframework.security.web.SecurityFilterChain', 'org.springframework.security.config.annotation.web.builders.HttpSecurity'
2022-10-20 15:49:48.791 [main] a.ConfigurationClassBeanDefinitionReader : Registered bean definition for imported class 'org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerJwtConfiguration$OAuth2SecurityFilterChainConfiguration'
2022-10-20 15:49:48.792 [main] o.s.b.a.condition.OnBeanCondition : Condition OnBeanCondition on org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerJwtConfiguration$OAuth2SecurityFilterChainConfiguration#jwtSecurityFilterChain matched due to #ConditionalOnBean (types: org.springframework.security.oauth2.jwt.JwtDecoder; SearchStrategy: all) found bean 'jwtDecoderByJwkKeySetUri'
...
2022-10-20 15:49:49.082 [main] a.ConfigurationClassBeanDefinitionReader : Registering bean definition for #Bean method com.mycompany.CustomSecurityConfig.filterChain()
...
2022-10-20 15:49:52.276 [main] edFilterInvocationSecurityMetadataSource : Adding web access control expression [authenticated] for any request
2022-10-20 15:50:13.348 [main] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with [org.springframework.security.web.session.DisableEncodeUrlFilter#33502cfe, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter#729d1428, org.springframework.security.web.context.SecurityContextPersistenceFilter#7d0312a, org.springframework.security.web.header.HeaderWriterFilter#6ca97ddf, org.springframework.security.web.csrf.CsrfFilter#38f569d, org.springframework.security.web.authentication.logout.LogoutFilter#1104ad6a, org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter#74ab8610, org.springframework.security.web.savedrequest.RequestCacheAwareFilter#7833407, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter#66acaa54, org.springframework.security.web.authentication.AnonymousAuthenticationFilter#115924ba, org.springframework.security.web.session.SessionManagementFilter#6a905513, org.springframework.security.web.access.ExceptionTranslationFilter#5749e633, org.springframework.security.web.access.intercept.FilterSecurityInterceptor#49741e80]
...
2022-10-20 15:50:13.384 [main] edFilterInvocationSecurityMetadataSource : Adding web access control expression [permitAll] for any request
2022-10-20 15:50:17.641 [main] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with [org.springframework.security.web.session.DisableEncodeUrlFilter#4a0f4282, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter#19d3f4fb, org.springframework.security.web.context.SecurityContextPersistenceFilter#99f75e4, org.springframework.security.web.header.HeaderWriterFilter#118c1faa, org.springframework.security.web.savedrequest.RequestCacheAwareFilter#2b6ff016, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter#5aefdb9e, org.springframework.security.web.authentication.AnonymousAuthenticationFilter#43cf97a8, org.springframework.security.web.session.SessionManagementFilter#da5b46f, org.springframework.security.web.access.ExceptionTranslationFilter#11267e87, org.springframework.security.web.access.intercept.FilterSecurityInterceptor#7827cdfc]
Is it expected that the bean CustomSecurityConfig.filterChain participates in DefaultWebSecurityCondition evaluation and OAuth2SecurityFilterChainConfiguration.jwtSecurityFilterChain is not created. Or the issue with DefaultWebSecurityCondition is that the instance of WebSecurityConfigurerAdapter is not in the context anymore (as to issue #10822 it is deprecated)?
The suggestion to add #Order() annotation didn't work:
#Configuration
#EnableWebSecurity
public class CustomSecurityConfig {
#Bean
#Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { ...
as well as further attempts to exclude the autoconfiguration class like this:
#SpringBootApplication(excludeName = "org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerJwtConfiguration.OAuth2SecurityFilterChainConfiguration")
public class Application extends SpringBootServletInitializer { ...
failed probably due to issue #5427 with the following error
java.lang.IllegalStateException: The following classes could not be excluded because they are not auto-configuration classes:
- org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerJwtConfiguration.OAuth2SecurityFilterChainConfiguration
at org.springframework.boot.autoconfigure.AutoConfigurationImportSelector.handleInvalidExcludes(AutoConfigurationImportSelector.java:222) ~[spring-boot-autoconfigure-2.7.4.jar!/:2.7.4]
This way also does not work:
#ComponentScan(excludeFilters = {#ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*OAuth2ResourceServerJwtConfiguration.*")})
public class Application extends SpringBootServletInitializer { ...
The documentation I read before posting:
Spring Security without the WebSecurityConfigurerAdapter
Spring Security: Upgrading the Deprecated WebSecurityConfigurerAdapter
Spring Security - How to Fix WebSecurityConfigurerAdapter Deprecated
Update
I have created a small Maven project that demonstrates the issue. After project is started, request the controller like this:
$ wget -nv -O - 'http://localhost:8080/spring/test'
Username/Password Authentication Failed.
As one can see, the custom configured SecurityFilterChain is not active because otherwise the access would be granted (as to antMatchers( "/**/test").permitAll()). ContextRefreshedEvent listener dumps two SecurityFilterChain instances (jwtSecurityFilterChain and filterChain), the priority of them is not possible to configure reliably.
As follows from issue #33103, the beans imported from XML via #ImportResource do not participate in Spring Boot autoconfiguration, hence beans scanning should be performed using annotation i.e. #SpringBootApplication(scanBasePackages = "some.package") – this basically solves the issue. –
dma_k
Nov 17, 2022 at 9:21
Thanks to this reply I found the solution.
But since we are using different auth method so it might not work for you. I am using DaoAuthenticationProvider from this tutorial. Then rewrite to a new version without using "WebSecurityConfigurerAdapter".
Hope this solution helps. I got tortured 3-4 hours finding solution.
I created a subpackage called "config", on the same level with "models, controller, service, etc"(or whatever you name them).
Then, manully import this package by "scanBasePackages" along with others.
#SpringBootApplication(scanBasePackages = {"com.example.test.controller", "com.example.test.model", "com.example.test.repo", "com.example.test.service","com.example.test.config"})
public class TestApplication {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
}
Here are 3 rewritten files in my "config" folder. All rewrote from the tutorial mentioned above.
a.CustomUserDetails
package com.example.test.config;
import java.util.Collection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import com.example.test.model.UserModel;
public class CustomUserDetails implements UserDetails {
//#Autowired no need cuz this is not a bean
private UserModel user;
public CustomUserDetails(UserModel user) {
this.user = user;
}
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
#Override
public String getPassword() {
return user.getPassword();
}
#Override
public String getUsername() {
return user.getEmail();
}
#Override
public boolean isAccountNonExpired() {
return true;
}
#Override
public boolean isAccountNonLocked() {
return true;
}
#Override
public boolean isCredentialsNonExpired() {
return true;
}
#Override
public boolean isEnabled() {
return true;
}
public String getFullName() {
return user.getFirstName() + " " + user.getLastName();
}
}
b. CustomUserDetailsService
package com.example.test.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import com.example.test.repo.UserRepo;
import com.example.test.model.UserModel;
import org.springframework.stereotype.Service;
#Service
public class CustomUserDetailsService implements UserDetailsService {
#Autowired
private UserRepo userRepo;
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserModel user = userRepo.findByEmail(username);
if (user == null) {
throw new UsernameNotFoundException("User not found");
}
return new CustomUserDetails(user);
}
}
c. SecurityConfiguration
package com.example.test.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
#Configuration
public class SecurityConfiguration{
#Bean
public UserDetailsService userDetailsService() {
return new CustomUserDetailsService();
}
#Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService());
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
#Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests ((requests)->requests
.requestMatchers("/", "/register", "/try2Register").permitAll()
.anyRequest().authenticated()
)
.formLogin((form)->form
.usernameParameter("email")
.defaultSuccessUrl("/users")
.permitAll()
)
.logout((logout) -> logout
.logoutSuccessUrl("/").permitAll());
http.authenticationProvider(authenticationProvider());
return http.build();
}
#Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().requestMatchers("/images/**", "/js/**", "/webjars/**");
}
}

Spring State Machine does not maintain order in which actions are triggered when configuration is read from DB

StateMachineFactory is configured to read configuration from db repository. However then statemachine is created, and events are send to machine, the order of actions executed is not maintained. I suspect, it's because the results returned from DB are not in any particular order.
Following is a POC for the same. Logs are also attached
#Configuration
#EnableStateMachineFactory
public class Config extends StateMachineConfigurerAdapter<String, String> {
#Autowired
private StateRepository<? extends RepositoryState> stateRepository;
#Autowired
private TransitionRepository<? extends RepositoryTransition> transitionRepository;
#Override
public void configure(StateMachineModelConfigurer<String, String> model) throws Exception {
model
.withModel()
.factory(modelFactory());
}
#Bean
public StateMachineModelFactory<String, String> modelFactory() {
return new RepositoryStateMachineModelFactory(stateRepository, transitionRepository);
}
#Bean
public Action<String, String> action1() {
return context -> System.out.println("Action1");
}
#Bean
public Action<String, String> action2() {
return context -> System.out.println("Action2");
}
}
#SpringBootApplication
public class Main implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(Main.class, args);
}
#Autowired
StateMachineFactory<String, String> factory;
#Autowired
ApplicationContext context;
#Override
public void run(String... args) throws Exception {
JpaStateRepository stateRepository = context.getBean(JpaStateRepository.class);
JpaTransitionRepository transitionRepository = context.getBean(JpaTransitionRepository.class);
JpaActionRepository actionRepository = context.getBean(JpaActionRepository.class);
JpaRepositoryState stateS1 = new JpaRepositoryState("UNPAID", true);
JpaRepositoryState stateS2 = new JpaRepositoryState("DONE");
stateRepository.save(stateS1);
stateRepository.save(stateS2);
var action1 = new JpaRepositoryAction();
action1.setName("action1");
var action2 = new JpaRepositoryAction();
action2.setName("action2");
actionRepository.save(action1);
actionRepository.save(action2);
JpaRepositoryTransition transitionS1ToS2 = new JpaRepositoryTransition(null, stateS1, stateS1, "TEST", Set.of(action1, action2));
transitionRepository.save(transitionS1ToS2);
for (int i = 0; i < 10; i++) {
var sm = factory.getStateMachine();
sm.startReactively().block();
sm.sendEvent("TEST");
}
}
}
Logs:
2022-10-12 22:01:19.474 INFO 58704 --- [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 7082 (http) with context path ''
2022-10-12 22:01:19.482 INFO 58704 --- [ restartedMain] com.poc.Main : Started Main in 2.076 seconds (JVM running for 2.288)
Action2
Action1
Action1
Action2
Is there any way to achieve order when done configuration is read from DB?

Spring AOP - Determine whether method was invoked by #Scheduled

I have a runtime annotation #MyAnnotation, and I would like to write an Aspect that determines whether the test() method below was called by:
Spring's #Scheduled framework
normal method invocation
#SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
#Scheduled(cron = "*/1 * * * * *") // scheduled to invoke every second
#MyAnnotation
public void test() {
// business logic
}
}
aspect code (pointcut + advice)
#Around(value="#annotation(myAnnotation)")
public Object featureToggle(ProceedingJoinPoint joinPoint, MyAnnotation myAnnotation) throws Throwable {
Boolean isInvoked = // TODO - is invoked by #Scheduled or not
}
Inspecting stack traces is always ugly, but of course you can do it:
package de.scrum_master.spring.q65397019;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
#Retention(RetentionPolicy.RUNTIME)
public #interface MyAnnotation {}
package de.scrum_master.spring.q65397019;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
#Component
public class MyComponent {
#Scheduled(fixedRate = 1000)
// #Async
#MyAnnotation
public void doSomething() {}
}
package de.scrum_master.spring.q65397019;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
#SpringBootApplication
#Configuration
#EnableScheduling
#EnableAsync
public class DemoApplication {
public static void main(String[] args) throws InterruptedException {
try (ConfigurableApplicationContext appContext = SpringApplication.run(DemoApplication.class, args)) {
doStuff(appContext);
}
}
private static void doStuff(ConfigurableApplicationContext appContext) throws InterruptedException {
MyComponent myComponent = appContext.getBean(MyComponent.class);
myComponent.doSomething();
Thread.sleep(1000);
myComponent.doSomething();
}
}
package de.scrum_master.spring.q65397019;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import java.util.Arrays;
#Aspect
#Component
public class MyAspect {
#Around("#annotation(myAnnotation)")
public Object advice2(ProceedingJoinPoint joinPoint, MyAnnotation myAnnotation) throws Throwable {
if (
Arrays.stream(new Exception().getStackTrace())
.map(StackTraceElement::toString)
.anyMatch(string -> string.contains("scheduling.support.ScheduledMethodRunnable.run("))
)
System.out.println(joinPoint + " -> scheduled");
else
System.out.println(joinPoint + " -> normal");
return joinPoint.proceed();
}
}
This will print something like:
(...)
2020-12-22 10:00:59.372 INFO 1620 --- [ main] o.s.s.c.ThreadPoolTaskScheduler : Initializing ExecutorService 'taskScheduler'
execution(void de.scrum_master.spring.q65397019.MyComponent.doSomething()) -> scheduled
2020-12-22 10:00:59.456 INFO 1620 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2020-12-22 10:00:59.456 INFO 1620 --- [ main] d.s.spring.q65397019.DemoApplication : Started DemoApplication in 6.534 seconds (JVM running for 8.329)
execution(void de.scrum_master.spring.q65397019.MyComponent.doSomething()) -> normal
execution(void de.scrum_master.spring.q65397019.MyComponent.doSomething()) -> scheduled
execution(void de.scrum_master.spring.q65397019.MyComponent.doSomething()) -> normal
2020-12-22 10:01:00.475 INFO 1620 --- [ main] o.s.s.c.ThreadPoolTaskScheduler : Shutting down ExecutorService 'taskScheduler'
2020-12-22 10:01:00.477 INFO 1620 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService 'applicationTaskExecutor'
(...)
On Java 9+ you could use the stack walking API which would be more efficient than creating full stack traces from exception instances or querying them from the currently running thread.
Caveat: If you also annotate your scheduled method with #Async, then this will not work anymore because then the asynchronously running method does not have a stack trace in which you could identify that it was triggered by a ScheduledMethodRunnable or an application class.
Maybe you would like to achieve something like that:
#Slf4j
#Component
public class ScheduledTask {
#Scheduled(cron = "0/1 * * * * *")
#ScheduledTaskAnnotation(message = "ScheduledTaskMessage", number = 10)
public void doAction() {
log.debug("Task scheduled");
}
}
#Slf4j
#Aspect
#Component
public class ScheduledTaskAspect {
#Around("execution(public * *(..)) && #annotation(hu.gaszabo.sample.schedule.ScheduledTaskAnnotation)")
public void logScheduledTaskAction(final ProceedingJoinPoint p) {
log.debug("Aspect");
parameters(p).ifPresent(a -> {
log.debug("message: {}", a.message());
log.debug("number: {}", a.number());
});
try {
p.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
}
private Optional<ScheduledTaskAnnotation> parameters(final ProceedingJoinPoint p) {
final Method method = ((MethodSignature) p.getSignature()).getMethod();
return Optional.ofNullable(AnnotationUtils.findAnnotation(method, ScheduledTaskAnnotation.class));
}
}
#Retention(RetentionPolicy.RUNTIME)
#Target(value = { ElementType.METHOD })
public #interface ScheduledTaskAnnotation {
String message() default "Message";
long number() default 0L;
}

Can not execute controller test with #SpringBootTest

I have a Spring Boot application. Version is 2.3.1.
Main Application looks like:
#AllArgsConstructor
#SpringBootApplication
public class LocalServiceApplication implements CommandLineRunner {
private final DataService dataService;
private final QrReaderServer qrReaderServer;
private final MonitoringService monitoringService;
#Override
public void run(String... args) {
dataService.fetchData();
monitoringService.launchMonitoring();
qrReaderServer.launchServer();
}
public static void main(String[] args) {
SpringApplication.run(LocalServiceApplication.class, args);
}
}
After the application is started I have to execute 3 distinct steps which have done with CommandLineRunner:
first gets remote data and store it locally (for test profile this step is skipped)
start async folder monitoring for file uploads with WatchService.
launch TCP server
I have a controller like:
#Slf4j
#RestController
#AllArgsConstructor
#RequestMapping("/v1/permissions")
public class CarParkController {
private final PermissionService permissionService;
#PostMapping
public CarParkPermission createPermission(#RequestBody #Valid CarParkPermission permission) {
return permissionService.createPermission(permission);
}
}
Ant test with Junit 5 looks like:
#ActiveProfiles("test")
#AutoConfigureMockMvc
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
class CarParkControllerIntegrationTest {
#Autowired
private MockMvc mockMvc;
#MockBean
private PermissionService permissionService;
private final Gson gson = new Gson();
#Test
void testCreatingNewPermissionSuccess() throws Exception {
CarParkPermission permission = CarParkPermission.builder()
.id(56)
.permissionCode("1234")
.build();
when(permissionService.createPermission(refEq(permission))).thenReturn(permission);
postPermission(permission).andExpect(status().isOk());
}
private <T> ResultActions postPermission(T instance) throws Exception {
return this.mockMvc.perform(post("/v1/permissions")
.contentType(MediaType.APPLICATION_JSON)
.content(gson.toJson(instance)));
}
}
Looks like it should work fine.
However, the test isn't executed:
2020-08-27 14:42:30.308 INFO 21800 --- [ main] c.s.i.CarParkControllerIntegrationTest : Started CarParkControllerIntegrationTest in 8.593 seconds (JVM running for 10.03)
2020-08-27 14:42:30.334 INFO 21800 --- [ main] c.s.s.s.DataServiceTestImpl : Fetch data for test profile is skipped
2020-08-27 14:42:30.336 DEBUG 21800 --- [ carpark-ex-1] c.s.monitoring.MonitoringServiceImpl : START_MONITORING Results from Cameras for folder: D:\results-from-camera
2020-08-27 14:42:30.751 DEBUG 21800 --- [ main] c.s.netty.TCPServer : TCP Server is STARTED : port 9090
After those lines execution hangs up forever.
UPDATE
Here are details for monitoring task:
#Async
#Override
public void launchMonitoring() {
log.debug("START_MONITORING Results from Cameras for folder: {}", properties.getFolder());
try {
WatchKey key;
while ((key = watchService.take()) != null) {
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
if (kind == ENTRY_CREATE) {
log.info("FILE_CREATED: {}", event.context());
// processing resource
deleteResource(zipFullPath);
} else if (kind == ENTRY_DELETE) {
log.info("RESOURCE_DELETED: {}", event.context());
}
}
key.reset();
}
} catch (InterruptedException e) {
log.error("interrupted exception for monitoring service", e);
Thread.currentThread().interrupt();
}
}
Also AsyncConfiguration is configured with TaskExecutor.
Launch method from TCPServer looks:
#Override
public void launchServer() {
try {
ChannelFuture serverChannelFuture = serverBootstrap.bind(hostAddress).sync();
log.debug("TCP Server is STARTED : port {}", hostAddress.getPort());
serverChannel = serverChannelFuture.channel().closeFuture().sync().channel();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
shutdownQuietly();
}
}
How to solve this issue?
Have understood that execution is blocked (thanks to M.Deinum).
So changed the last method for:
#Async
#Override
public void launchServer() {
// ...
}
And shifted to ObjectMapper instead of Gson for converting instance to JSON format:
#SpringBootTest
#AutoConfigureMockMvc
#ActiveProfiles("test")
class CarParkControllerIntegrationTest {
#Autowired
private MockMvc mockMvc;
#Autowired
private ObjectMapper mapper;
#Test
void testCreatingNewPermissionSuccess() throws Exception {
CarParkPermission permission = CarParkPermission.builder()
.id(444)
.permissionCode("1234")
.build();
postPermission(permission).andExpect(status().isOk());
}
private <T> ResultActions postPermission(T instance) throws Exception {
return this.mockMvc.perform(post("/v1/permissions")
.contentType(MediaType.APPLICATION_JSON)
.content(mapper.writeValueAsString(instance)));
}
}
And finally, it works fine.

Strange situation with #Autowired

I have strange situation with #Autowired
App's main class:
#Configuration
#EnableAutoConfiguration
#SpringBootApplication
#ComponentScan({"khartn", "khartn.torrentsuploader.processor"})
public class NewMain implements CommandLineRunner {
public static void main(String[] args) {
SpringApplicationBuilder builder = new SpringApplicationBuilder(NewMain.class);
builder.headless(false);
ConfigurableApplicationContext context = builder.run(args);
}
#Override
public void run(String... args) throws Exception {
}
}
Component class:
#Component("MyDirectoryReader")
public class MyDirectoryReader {
public MyDirectoryReader ( ) {
System.out.println("qqqqqqqqqqqqqqq");
}
public void readDir() {
try {
String initialPathStr = NewJFrame.jTextField1.getText();
System.out.println("initialPathStr " + initialPathStr);
Path dir = FileSystems.getDefault().getPath(initialPathStr);
DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.{torrent}");
for (Path path : stream) {
System.out.println(path.getFileName());
}
stream.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
When application starting, I see, what MyDirectoryReader class is initialized:
2015-04-11 21:42:29.405 INFO 9375 --- [.NewMain.main()] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext#13d6044f: startup date [Sat Apr 11 21:42:29 SAMT 2015]; root of context hierarchy
qqqqqqqqqqqqqqq
Config class:
#Configuration
#ComponentScan({"khartn", "khartn.torrentsuploader.processor"})
public class AppConfig {
#Bean(initMethod = "init")
public NewJFrame mainForm() {
System.out.println("init mainForm");
return new NewJFrame();
}
}
And in NewJFrame class have autowired field
public class NewJFrame extends javax.swing.JFrame {
#Autowired
#Qualifier(value = "MyDirectoryReader")
MyDirectoryReader myDirectoryReader;
But when NewJFrame showed and button pressed,
private void jButton2ActionPerformed(java.awt.event.ActionEvent evt) {
if (myDirectoryReader == null) {
System.out.println("myDirectoryReader is null");
}
// myDirectoryReader.readDir();
}
then myDirectoryReader is null.
Why MyDirectoryReader is initialized as Component, but not autowired to the field?
According to the code from the init method, you make the call new NewJFrame().setVisible(true);, however, when you create an object yourself, Spring doesn't know that and autowiring doesn't work. You have to use the Spring bean object itself. I think, if you change that line to this.setVisible(true), it should work correctly (since the init method is executed when the bean is instantiated).

Resources