Let's say I have this router definition:
#Component
class PersonRouter(private val handler: PersonHandler) {
#Bean
fun router(): RouterFunction<ServerResponse> = router {
("/api/people" and accept(MediaType.APPLICATION_JSON_UTF8)).nest {
GET("/{id}") { handler.findById(it) }
}
}
And then this handler:
#Component
#Transactional
class PersonHandler(private val repository: PersonRepository) {
private companion object : KLogging()
#Transactional(readOnly = true)
fun findById(req: ServerRequest): Mono<ServerResponse> {
logger.info { "${req.method()} ${req.path()}" }
val uuid = ? // req.pathContainer().elements().last().value()
return ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(BodyInserters.fromObject(repository.findById(uuid)))
.switchIfEmpty(ServerResponse.notFound().build())
}
}
How do I access the identifier (what would be a #PathVariable id: String on a typical #RestController) from ServerRequest without doing black magic with regular expressions, string-heavy-lifting, and such things?
Ah! Found it!
It is by doing: req.pathVariable("id")
It was there all the time...in the official Spring Framework (Web Reactive) documentation!
Related
Implementing a simple web application using REST Api using Spring Boot + Vaadin. Also, Security is connected in the project, a simple login with a login-password is carried out. Get() requests work fine, but a 403 "Forbidden" error occurs on PUT, POST, DELETE requests.
I tried disabling csrf using the http.httpBasic().and().csrf().disable() method, it does not help, and this is not recommended in production either.
I also tried adding to antMatchers() specifically a request type like this: http.httpBasic().and().authorizeRequests().antMatchers(HttpMethod.POST,"/**").permitAll(), also not helps.
Configuration class:
#EnableWebSecurity
#Configuration
public class SecurityConfig extends VaadinWebSecurity {
private static class SimpleInMemoryUserDetailsManager extends InMemoryUserDetailsManager {
public SimpleInMemoryUserDetailsManager() {
createUser(Manager.withUsername("manager1")
.password("{noop}123")
.roles(ROLE_MANAGER)
.build());
createUser(Manager.withUsername("manager2")
.password("{noop}123")
.roles(ROLE_MANAGER)
.build());
}
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic().and().authorizeRequests().antMatchers("/enterprises/\*\*").hasRole(ROLE_MANAGER);
super.configure(http);
setLoginView(http, LoginView.class);
}
#Bean
public InMemoryUserDetailsManager enterprisesService() {
return new SimpleInMemoryUserDetailsManager();
}
}
Rest-controller:
#org.springframework.web.bind.annotation.RestController
#RequestMapping(path = "/")
public class RestController {
#Autowired
private VehiclesRepository vehiclesRepository;
#Autowired
private EnterprisesRepository enterprisesRepository;
#Autowired
private DriversRepository driversRepository;
#Autowired
private ManagersRepository managersRepository;
#GetMapping(
path = "/vehicles",
produces = "application/json")
public VehiclesDto getVehicles() {
VehiclesDto vehiclesDto = new VehiclesDto();
for (Vehicle vehicle : vehiclesRepository.findAll()) {
vehiclesDto.getVehicles().add(vehicle);
}
return vehiclesDto;
}
#GetMapping(
path = "/enterprises",
produces = "application/json")
public #ResponseBody EnterprisesDto getEnterprises(#RequestParam("managerId") String managerId) {
Manager manager = null;
for (Manager managerFromRepo : managersRepository.findAll()) {
if (managerFromRepo.getId().equals(Long.parseLong(managerId))) {
manager = managerFromRepo;
break;
}
}
EnterprisesDto enterprisesDto = new EnterprisesDto();
if (manager == null) return enterprisesDto;
for (Enterprise enterprise : enterprisesRepository.findAll()) {
if (manager.getEnterprises().contains(enterprise.getId()))
enterprisesDto.getEnterprises().add(enterprise);
}
return enterprisesDto;
}
#GetMapping(
path = "/drivers",
produces = "application/json")
public DriversDto getDrivers() {
DriversDto driversDto = new DriversDto();
for (Driver driver : driversRepository.findAll()) {
driversDto.getDrivers().add(driver);
}
return driversDto;
}
#PostMapping("/createVehicle")
public #ResponseBody String createVehicle(#RequestBody String info) {
return "it works!!!";
}
#DeleteMapping("/deleteVehicle")
public #ResponseBody String deleteVehicle(){
return "it works!!!";
}
}
Testing requests through Postman using Basic Authentication.
You can disable CSRF just for your API:
http.csrf().ignoringRequestMatchers(new AntPathRequestMatcher("/enterprises/**"));
Suppose I have the following application with a WebFilter and I'm trying to pass some context back from the controller into the filter.
#RestController
class MyController {
#PostMapping("/test")
suspend fun postSomething(): ResponseEntity<Unit> {
val valueFromFilter = coroutineContext[ReactorContext.Key]?.context?.get<String>("myKey") ?: "EMPTY"
logger.info { "Inside handler = $valueFromFilter" } // this works since Reactor populate coroutineContext with respective ReactorContext
coroutineContext[ReactorContext.Key]?.context?.put("handlerKey", "hello")
return ResponseEntity.ok().build()
}
}
#Component
class MyFilter : WebFilter {
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
logger.info { "Before" }
return chain.filter(exchange)
.contextWrite(Context.of("myKey", "myValue"))
.doOnEach {
val valueFromHandler = if (it.contextView.hasKey("handlerKey")) it.contextView.get<String>("handlerKey") else "EMPTY"
logger.info { "After handler = $valueFromHandler" } // But this doesn't work since Reactor doesn't restore ReactorContext from respective coroutineContext
}
}
}
#SpringBootApplication
class MyApplication
fun main(args: Array<String>) {
run(MyApplication::class.java, *args)
}
In this example if we made a request to /test it would print the following
Before
Inside handler = myValue
After handler = EMPTY
I understand that when we call chain.filter(exchange) to proceed with the request and the handler is a Kotlin suspend function the framework fills in coroutineContext with ReactorContext.Key to actualReactorContext. Although my question is why doesn't Spring restore the context that I may have filled in the controller back, so I can use it after calling chain.filter(exchange). Moreover if there is anyway possible to do this currently.
Just figured out I was looking into the wrong way of doing it. Instead one can simply just use the attributes of ServerWebExchange to pass context around like
#RestController
class MyController {
#PostMapping("/test")
suspend fun postSomething(webExchange: ServerWebExchange): ResponseEntity<Unit> {
val valueFromFilter = webExchange.attributes["myKey"] as? String ?: "EMPTY"
logger.info { "Inside handler = $valueFromFilter" }
webExchange.attributes["handlerKey"] = "hello"
return ResponseEntity.ok().build()
}
}
#Component
class MyFilter : WebFilter {
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
logger.info { "Before" }
exchange.attributes["myKey"] = "myValue"
return chain.filter(exchange)
.doOnEach {
val valueFromHandler = exchange.attributes["handlerKey"] as? String ?: "EMPTY"
logger.info { "After handler = $valueFromHandler" }
}
}
}
This way we can pass context between both the filter and the handler.
I am working on webclient for various HTTP methods (GET,PATCH,POST,DELETE). These are created separately and invoke separately. I am looking to make it as a generic webclient component so minimum changes needs to do in the future. Below is the code for GET and POST. PATCH and Delete is also on similar lines. Please let me know how can I proceed to make generic webclient for various HTTP methods.
#Component
public class HuntsCreateNewCollectionAdapter {
#Autowired
AppProperties properties;
#Autowired
WebclientCollectionConfig webclientCollectionConfig;
public Mono<ResponseEntity<HuntsCollectionDto>> createCollectionAPI(RequestContext context, String title) {
LOGGER.debug("Create Collection API call");
CollectionInputDto collectionInput = CollectionInputDto.builder().title(title).build();
String json = AppJsonUtil.getJsonAsString(collectionInput);
Mono<ResponseEntity<HuntsCollectionDto>> result = webclientCollectionConfig.collectionBuilder().post()
.uri(properties.getCollectionUrl())
.headers(header -> header.addAll(webclientCollectionConfig.getHeaders(context)))
.body(BodyInserters.fromValue(json)).exchangeToMono(response -> {
return response.toEntity(HuntsCollectionDto.class);
});
return result;
}
}
#Component
public class HuntsGetCollectionAdapter {
#Autowired
AppProperties properties;
#Autowired
WebclientCollectionConfig webclientCollectionConfig;
public Mono<ResponseEntity<HuntsCollectionDto>> getCollectionAPI(RequestContext context, String collectionId) {
LOGGER.debug("Get Collection API call");
return webclientCollectionConfig.collectionBuilder().get()
.uri(properties.getCollectionUrl() + "/" + collectionId)
.headers(header -> header.addAll(webclientCollectionConfig.getHeaders(context)))
.exchangeToMono(response -> {
return response.toEntity(HuntsCollectionDto.class);
});
}
}
#Component
public class WebclientCollectionConfig {
#Autowired
AppProperties appProperties;
#Bean
public WebClient collectionBuilder() {
return WebClient.builder().baseUrl(appProperties.getCollectionbaseUrl())
.defaultHeader("Content-Type", "application/json").build();
}
}
I am working on a simple project which uses Spring Boot 2 with Spring WebFlux using Kotlin.
I wrote test for my handler function (in which I mock the dependencies using Mockito).
However, it seems like my route function does not trigger the handler, as all of my requests return HTTP 404 NOT FOUND (even though the route is correct).
I have looked at various other projects to find out what how these tests are supposed to be written (here, here), but the problem persists.
The code is as follows (and can also be found on GitHub):
UserRouterTest
#ExtendWith(SpringExtension::class, MockitoExtension::class)
#Import(UserHandler::class)
#WebFluxTest
class UserRouterTest {
#MockBean
private lateinit var userService: UserService
#Autowired
private lateinit var userHandler: UserHandler
#Test
fun givenExistingCustomer_whenGetCustomerByID_thenCustomerFound() {
val expectedCustomer = User("test", "test")
val id = expectedCustomer.userID
`when`(userService.getUserByID(id)).thenReturn(Optional.ofNullable(expectedCustomer))
val router = UserRouter().userRoutes(userHandler)
val client = WebTestClient.bindToRouterFunction(router).build()
client.get()
.uri("/users/$id")
.accept(MediaType.ALL)
.exchange()
.expectStatus().isOk
.expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
.expectBody(User::class.java)
}
}
User
#Entity
class User(var username : String, var password: String) {
#Id
val userID = UUID.randomUUID()
}
UserRepository
#Repository
interface UserRepository : JpaRepository<User, UUID>{
}
UserService
#Service
class UserService(
private val userRepository: UserRepository
) {
fun getUserByID(id: UUID): Optional<User> {
return Optional.of(
try {
userRepository.getOne(id)
} catch (e: EntityNotFoundException) {
User("test", "test")
}
)
}
fun addUser(user: User) {
userRepository.save(user)
}
}
UserHandler
#Component
class UserHandler(
private val userService: UserService
) {
fun getUserWithID(request: ServerRequest): Mono<ServerResponse> {
val id = try {
UUID.fromString(request.pathVariable("userID"))
} catch (e: IllegalArgumentException) {
return ServerResponse.badRequest().syncBody("Invalid user id")
}
val user = userService.getUserByID(id).get()
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8)
.body(BodyInserters.fromObject(user))
}
}
UserRouter
#Configuration
class UserRouter {
#Bean
fun userRoutes(userHandler: UserHandler) = router {
contentType(MediaType.APPLICATION_JSON_UTF8).nest {
GET("/users/{userID}", userHandler::getUserWithID)
GET("") { ServerResponse.ok().build() }
}
}
}
EDIT
To route based on the presence of one or more query parameter (regardless of their values), we can do the following:
UserRouter
#Configuration
class UserRouter {
#Bean
fun userRoutes(userHandler: UserHandler) = router {
GET("/users/{userID}", userHandler::getUserWithID)
(GET("/users/")
and queryParam("username") { true }
and queryParam("password") { true }
)
.invoke(userHandler::getUsers)
}
}
Note that GET("/users/?username={username}", userHandler::getUsersWithUsername) does not work.
The way the router is configured - contentType(MediaType.APPLICATION_JSON_UTF8).nest - will only match requests that have this content type, so you would have to either remove the contentType prerequisite or change the test to include it
client.get()
.uri("/users/$id")
.accept(MediaType.ALL)
.header("Content-Type", "application/json;charset=UTF-8")
.exchange()
.expectStatus().isOk
.expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
.expectBody(User::class.java)
I have two kinds of Webflux applications, annotation-based and route-based. These applications are called with a set of headers, some of which (Open Tracing) I need to propagate in downstream calls using WebClient.
If these were normal Spring WebMvc applications I would use a Filter to keep the selected headers in a ThreadLocal, access it in a RestTemplate interceptor to send them to subsequent services and clear the ThreadLocal.
What's the proper way to replicate this behaviour in WebFlux applications?
I solved it using Project Reactor's Context to store the headers in a WebFilter. Then they are gotten in the WebClient's ExchangeFilterFunction. Here's the whole solution:
WebFilter
class OpenTracingFilter(private val openTracingHeaders: Set<String>) : WebFilter {
private val logger = LoggerFactory.getLogger(javaClass)
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
return chain.filter(exchange)
.subscriberContext { ctx ->
var updatedContext = ctx
exchange.request.headers.forEach {
if (openTracingHeaders.contains(it.key.toLowerCase())) {
logger.debug("Found OpenTracing Header - key {} - value {}", it.key, it.value[0])
updatedContext = updatedContext.put(it.key, it.value[0])
}
}
updatedContext
}
}
}
OpenTracingExchangeFilterFunction
class OpenTracingExchangeFilterFunction(private val headers: Set<String>) : ExchangeFilterFunction {
private val logger = LoggerFactory.getLogger(javaClass)
override fun filter(request: ClientRequest, next: ExchangeFunction): Mono<ClientResponse> {
logger.debug("OpenTracingExchangeFilterFunction - filter()")
return OpenTracingClientResponseMono(request, next, headers)
}
}
OpenTracingClientResponseMono
class OpenTracingClientResponseMono(private val request: ClientRequest,
private val next: ExchangeFunction,
private val headersToPropagate: Set<String>) : Mono<ClientResponse>() {
private val logger = LoggerFactory.getLogger(javaClass)
override fun subscribe(subscriber: CoreSubscriber<in ClientResponse>) {
val context = subscriber.currentContext()
val requestBuilder = ClientRequest.from(request)
requestBuilder.headers { httpHeaders ->
headersToPropagate.forEach {
if(context.hasKey(it)) {
logger.debug("Propagating header key {} - value{}", it, context.get<String>(it))
httpHeaders[it] = context.get<String>(it)
}
}
}
val mutatedRequest = requestBuilder.build()
next.exchange(mutatedRequest).subscribe(subscriber)
}
}
OpenTracingConfiguration
#Configuration
class OpenTracingConfiguration(private val openTracingConfigurationProperties: OpenTracingConfigurationProperties) {
#Bean
fun webClient(): WebClient {
return WebClient.builder().filter(openTracingExchangeFilterFunction()).build()
}
#Bean
fun openTracingFilter(): WebFilter {
return OpenTracingFilter(openTracingConfigurationProperties.headers)
}
#Bean
fun openTracingExchangeFilterFunction(): OpenTracingExchangeFilterFunction {
return OpenTracingExchangeFilterFunction(openTracingConfigurationProperties.headers)
}
}
OpenTracingConfigurationProperties
#Configuration
#ConfigurationProperties("opentracing")
class OpenTracingConfigurationProperties {
lateinit var headers: Set<String>
}
application.yml
opentracing:
headers:
- x-request-id
- x-b3-traceid
- x-b3-spanid
- x-b3-parentspanid
- x-b3-sampled
- x-b3-flags
- x-ot-span-context
I needed to pass x-request-id header to a downstream service in my application. Achieved this by adding WebFilter that writes x-request-id to a reactor context
class ContextWebFilter : WebFilter {
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
val headers = exchange.request.headers
val xRequestId = headers[X_REQUEST_ID]?.firstOrNull() ?: ""
val requestId = xRequestId.ifBlank { UUID.randomUUID().toString() }
return chain
.filter(exchange)
.contextWrite { it.put(X_REQUEST_ID, requestId) }
}
companion object {
const val X_REQUEST_ID = "X-REQUEST-ID"
}
}
and updating WebClient with ExchangeFilterFunction that updates outgoing request
WebClient.builder()
.filter(
ExchangeFilterFunction.ofRequestProcessor { request ->
Mono.deferContextual { context ->
val xRId = context.getOrDefault<String>("X-REQUEST-ID", "")
logger.debug("Set X-REQUEST-ID={} as a header to outgoing call", xRId)
Mono.just(
ClientRequest.from(request)
.header("X-REQUEST-ID", xRId)
.build()
)
}
}
)
.baseUrl("http://localhost:8080")
.build()