I need to configure default coroutine context for all requests in Spring MVC. For example MDCContext (similar question as this but for MVC not WebFlux).
What I have tried
Hook into Spring - the coroutine code is here but there is no way to change the default behavior (need to change InvocableHandlerMethod.doInvoke implementation)
Use AOP - AOP and coroutines do not play well together
Any ideas?
This seems to work:
#Configuration
class ContextConfig: WebMvcRegistrations {
override fun getRequestMappingHandlerAdapter(): RequestMappingHandlerAdapter {
return object: RequestMappingHandlerAdapter() {
override fun createInvocableHandlerMethod(handlerMethod: HandlerMethod): ServletInvocableHandlerMethod {
return object : ServletInvocableHandlerMethod(handlerMethod) {
override fun doInvoke(vararg args: Any?): Any? {
val method = bridgedMethod
ReflectionUtils.makeAccessible(method)
if (KotlinDetector.isSuspendingFunction(method)) {
// Exception handling skipped for brevity, copy it from super.doInvoke()
return invokeSuspendingFunctionX(method, bean, *args)
}
return super.doInvoke(*args)
}
/**
* Copied from CoroutinesUtils in order to be able to set CoroutineContext
*/
#Suppress("UNCHECKED_CAST")
private fun invokeSuspendingFunctionX(method: Method, target: Any, vararg args: Any?): Publisher<*> {
val function = method.kotlinFunction!!
val mono = mono(YOUR_CONTEXT_HERE) {
function.callSuspend(target, *args.sliceArray(0..(args.size-2))).let { if (it == Unit) null else it }
}.onErrorMap(InvocationTargetException::class.java) { it.targetException }
return if (function.returnType.classifier == Flow::class) {
mono.flatMapMany { (it as Flow<Any>).asFlux() }
}
else {
mono
}
}
}
}
}
}
}
Related
Here is what I like to achieve.
My Quarkus app is using a homemade Quarkus extension that is securing my API endpoints with a custom annotation:
#NameBinding
#Retention(RetentionPolicy.RUNTIME)
public #interface AuthorizationSecured {
#Nonbinding String[] permissions() default{};
}
I want this custom annotation to automatically annotate my endpoints with this openapi annotation:
// org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement
#SecurityRequirement(name = "jwt")
So that in swagger-ui, I can have the padlock icon displayed and specify my token when clicking on the icon.
Any idea?
EDIT:
I tried #Ladicek suggestion, here is my processor:
class AuthorizeProcessor {
#BuildStep
FeatureBuildItem feature() {
return new FeatureBuildItem("authorize");
}
#BuildStep
AdditionalBeanBuildItem registerConfigValidator() {
// some stuff here...
}
#BuildStep
AnnotationsTransformerBuildItem transform() {
return new AnnotationsTransformerBuildItem(new AnnotationsTransformer() {
public boolean appliesTo(org.jboss.jandex.AnnotationTarget.Kind kind) {
return kind == AnnotationTarget.Kind.METHOD;
}
public void transform(TransformationContext context) {
if (context.getTarget().asMethod().hasAnnotation(DotName.createSimple("com.software.company.AuthorizationSecured"))) {
context.transform().add(DotName.createSimple("org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement"), AnnotationValue.createStringValue("name", "jwt")).done();
}
}
});
}
#BuildStep
void registerAuthorizeFilter(
BuildProducer<AdditionalBeanBuildItem> additionalBeanProducer,
BuildProducer<AdditionalIndexedClassesBuildItem> additionalIndexedClassesProducer,
BuildProducer<ResteasyJaxrsProviderBuildItem> resteasyJaxrsProviderProducer) {
// some stuff here...
}
}
The annotation transformer seems to do its job, I have printed some stuff in the console:
************* Found method name = secured
************* annotations before > [#GET, #Path(value = "/secured"), #Produces(value = ["text/plain"]), #AuthorizationSecured]
************* annotations after > [#GET, #Path(value = "/secured"), #Produces(value = ["text/plain"]), #AuthorizationSecured, #org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement(name = "jwt")]
But actually, I don't see any effect at runtime on the app that is using the extension. What am I missing here?
I have test that works properly with Spring 2.4.0-M2 but after upgrading to 2.4.0-M3 it breaks - returns 404 for a route that is registered.
My app:
#SpringBootApplication(proxyBeanMethods = false)
class ExampleApp
fun main(args: Array<String>) {
runApplication<ExampleApp>(
init = {
addInitializers(BeansInitializer())
},
args = args
)
}
beans:
class BeansInitializer : ApplicationContextInitializer<GenericApplicationContext> {
#Suppress("LongMethod")
override fun initialize(applicationContext: GenericApplicationContext) {
beans {
bean {
router {
"/routes".nest {
GET("/{id}") { ServerResponse.ok().bodyValue(Foo("ok")) }
POST("/") { ServerResponse.ok().bodyValue(Foo("ok")) }
}
}
}
}
.initialize(applicationContext)
}
}
data class Foo(val status: String)
My test:
#SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = [
ExampleApp::class
]
)
class FailingTest #Autowired constructor(
context: ApplicationContext,
) {
val webTestClient: WebTestClient = WebTestClient.bindToApplicationContext(context)
.configureClient()
.build()
#Test
fun `should interact with routes`() {
webTestClient
.post()
.uri("/routes")
.bodyValue(SampleBody("123"))
.exchange()
.expectStatus()
.isOk // returns 404 on 2.4.0-M3 / passes on 2.4.0-M2
}
data class SampleBody(val id: String)
}
test application.yml
context:
initializer:
classes: com.example.BeansInitializer
On 2.4.0-M3 tests fail with following message:
java.lang.AssertionError: Status expected:<200 OK> but was:<404 NOT_FOUND>
On 2.4.0-M2 they pass.
Is there something that changed through the versions? Or this is a bug?
The change in behaviour that you are seeing is due to an improvement in Spring Framework during the development of 5.3.
By default, Spring Framework will match an optional trailing path separator (/). This optional / should be in addition to the path specified in your routes.
You have two routes:
GET /routes/{id}
POST /routes/
The support for an optional trailing path separator means that you could make a get request to /routes/56/ (an additional trailing /), but it should not mean that you can make a request to POST /routes (removal of a trailing /).
If you want to be able to make POST requests to both /routes and /routes/, you should define the route as /routes:
beans {
bean {
router {
"/routes".nest {
GET("/{id}") { ServerResponse.ok().bodyValue(Foo("ok")) }
POST("") { ServerResponse.ok().bodyValue(Foo("ok")) }
}
}
}
}
I am playing around with Kofu functional Bean DSL. I am using Spring-Data-JDBC with Spring-MVC and trying to autowire NamedParameterJdbcTemplate. However, I am have been receiving this error that no beans found for it while running tests. In a annotation based approach, we don’t have to supply an explicit NamedParameterJdbcTemplate. My sample app here: https://github.com/overfullstack/kofu-mvc-jdbc. And PFB some code snippets from it:
val app = application(WebApplicationType.SERVLET) {
beans {
bean<SampleService>()
bean<UserHandler>()
}
enable(dataConfig)
enable(webConfig)
}
val dataConfig = configuration {
beans {
bean<UserRepository>()
}
listener<ApplicationReadyEvent> {
ref<UserRepository>().init()
}
}
val webConfig = configuration {
webMvc {
port = if (profiles.contains("test")) 8181 else 8080
router {
val handler = ref<UserHandler>()
GET("/", handler::hello)
GET("/api", handler::json)
}
converters {
string()
jackson()
}
}
}
class UserRepository(private val client: NamedParameterJdbcTemplate) {
fun count() =
client.queryForObject("SELECT COUNT(*) FROM users", emptyMap<String, String>(), Int::class.java)
}
open class UserRepositoryTests {
private val dataApp = application(WebApplicationType.NONE) {
enable(dataConfig)
}
private lateinit var context: ConfigurableApplicationContext
#BeforeAll
fun beforeAll() {
context = dataApp.run(profiles = "test")
}
#Test
fun count() {
val repository = context.getBean<UserRepository>()
assertEquals(3, repository.count())
}
#AfterAll
fun afterAll() {
context.close()
}
}
This is the error:
Parameter 0 of constructor in com.sample.UserRepository required a bean of type 'org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate' that could not be found.
Action:
Consider defining a bean of type 'org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate' in your configuration.
Please help, thanks
Apparently Kofu doesn't pick datasource from application.properties file. Everything is meant to be declarative and no implicit derivations. (Basically no Spring magic 🙂). This worked for me:
val dataConfig = configuration {
beans {
bean {
val dataSourceBuilder = DataSourceBuilder.create()
dataSourceBuilder.driverClassName(“org.h2.Driver”)
dataSourceBuilder.url(“jdbc:h2:mem:test”)
dataSourceBuilder.username(“SA”)
dataSourceBuilder.password(“”)
dataSourceBuilder.build()
}
bean<NamedParameterJdbcTemplate>()
bean<UserRepository>()
}
listener<ApplicationReadyEvent> {
ref<UserRepository>().init()
}
}
I know this kind of question has been asked before.
I have a method which is annotated with #PostConstruct.
The methods assumes that all Flyway scripts have been executed before invocation.
It seems that Flyway also uses #PostConstruct annotated methods and that these methods are called after my method.
I tried to annotate my method with #DependOn and different flyway beennames.
Unfortunately without success. Can anybody help me.
Solution:
I would set a dependency on the FlywayMigrationInitializer in the constructor. When the Initializer is created and set up, the migrations are run.
Or you can depend on the flywayInitializer bean (#DependsOn("flywayInitializer")). The bean is named flywayInitializer, of the class FlywayMigrationInitializer and it is created in FlywayAutoConfiguration.java.
FlywayMigrationInitializer implements InitializingBean and calls the migrate method in the afterPropertiesSet method.
Example:
#Component
// #DependsOn("flywayInitializer")
#Slf4j
public class TestPostConstruct {
public TestPostConstruct(FlywayMigrationInitializer flywayForceInitialization) {
}
#PostConstruct
public void testPostConstruct() {
log.info("----> in testPostConstruct");
}
}
The Spring Boot log:
INFO 4760 --- [main] o.f.core.internal.command.DbMigrate : Successfully applied 1 migration to schema "PUBLIC" (execution time 00:00.130s)
INFO 4760 --- [main] c.example.flywayinit.TestPostConstruct : ----> in testPostConstruct
For new Flyway this work (use Flyway callbacks)
#Configuration
class FlywayConfig(env: Environment) {
private val env: Environment
init {
this.env = env
}
#Bean(initMethod = "migrate")
fun flyway(dbLoadService: DbLoadService): Flyway {
return Flyway(
Flyway.configure()
.baselineOnMigrate(true)
.dataSource(
env.getRequiredProperty("spring.datasource.url"),
env.getRequiredProperty("spring.datasource.username"),
env.getRequiredProperty("spring.datasource.password")
)
//запуск загрузки из базы после окончания миграции
.callbacks(FlywayMigrationsCompleteCallback {
dbLoadService.loadAllCertificateInformation()
})
)
}
class FlywayMigrationsCompleteCallback(private val callback: () -> Unit) : Callback {
override fun supports(event: Event?, context: Context?): Boolean {
return event == Event.AFTER_MIGRATE
}
override fun canHandleInTransaction(event: Event?, context: Context?): Boolean {
return true
}
override fun handle(event: Event?, context: Context?) {
callback()
}
override fun getCallbackName(): String {
return FlywayMigrationsCompleteCallback::class.simpleName!!
}
}
#Component
class DbLoadService(private val certificateRepository:CertificateRepository) {
#Volatile var certificate: List<Certificate>?=null
fun loadAllCertificateInformation(){
val findAll = certificateRepository.findAll()
runBlocking {
certificate = findAll.toList()
}
}
}
I'm trying to configure subscription mapping for stomp over websockets in a spring boot application without any luck. I'm fairly certian I have the stomp/websocket stuff configured correctly as I am able to subscribe to topics that are being published to by a kafka consumer, but using the #SubscribeMapping is not working at all.
Here is my controller
#Controller
class TestController {
#SubscribeMapping("/topic/test")
fun testMapping(): String {
return "THIS IS A TEST"
}
}
And here is my configuration
#Configuration
#EnableWebSocketMessageBroker
#Order(Ordered.HIGHEST_PRECEDENCE + 99)
class WebSocketConfig : AbstractWebSocketMessageBrokerConfigurer() {
override fun configureMessageBroker(config: MessageBrokerRegistry) {
config.setApplicationDestinationPrefixes("/app", "/topic")
config.enableSimpleBroker("/queue", "/topic")
config.setUserDestinationPrefix("/user")
}
override fun registerStompEndpoints(registry:StompEndpointRegistry) {
registry.addEndpoint("/ws").setAllowedOrigins("*")
}
override fun configureClientInboundChannel(registration: ChannelRegistration?) {
registration?.setInterceptors(object: ChannelInterceptorAdapter() {
override fun preSend(message: Message<*>, channel: MessageChannel): Message<*> {
val accessor: StompHeaderAccessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor::class.java)
if (StompCommand.CONNECT.equals(accessor.command)) {
Optional.ofNullable(accessor.getNativeHeader("authorization")).ifPresent {
val token = it[0]
val keyReader = KeyReader()
val creds = Jwts.parser().setSigningKey(keyReader.key).parseClaimsJws(token).body
val groups = creds.get("groups", List::class.java)
val authorities = groups.map { SimpleGrantedAuthority(it as String) }
val authResult = UsernamePasswordAuthenticationToken(creds.subject, token, authorities)
SecurityContextHolder.getContext().authentication = authResult
accessor.user = authResult
}
}
return message
}
})
}
}
And then in the UI code, I'm using angular with a stompjs wrapper to subscribe to it like this:
this.stompService.subscribe('/topic/test')
.map(data => data.body)
.subscribe(data => console.log(data));
Subscribing like this to topics that I know are emitting data works perfectly but the subscribemapping does nothing. I've also tried adding an event listener to my websocket config to test that the UI is actually sending a subscription event to the back end like this:
#EventListener
fun handleSubscribeEvent(event: SessionSubscribeEvent) {
println("Subscription event: $event")
}
#EventListener
fun handleConnectEvent(event: SessionConnectEvent) {
println("Connection event: $event")
}
#EventListener
fun handleDisconnectEvent(event: SessionDisconnectEvent) {
println("Disconnection event: $event")
}
Adding these event listeners I can see that all the events that I'm expecting from the UI are coming through in the kotlin layer, but my controller method never gets called. Is there anything obvious that I'm missing?
Try the following:
#Controller
class TestController {
#SubscribeMapping("/test")
fun testMapping(): String {
return "THIS IS A TEST"
}
}