Skip to content

Commit

Permalink
feature: v0.5.0: Spring Cloud Function Support
Browse files Browse the repository at this point in the history
  • Loading branch information
tschuehly committed Apr 5, 2023
1 parent 022e599 commit aeba511
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 4 deletions.
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,26 @@ class Router(
)
}
```
### Serverless components - Spring Cloud Function support

If you want to deploy your application on a serverless platform such as AWS Lambda or Azure Functions you can easily do that with the Spring Cloud Function support.

Just add the dependency `implementation("org.springframework.cloud:spring-cloud-function-context")` to your build.gradle.kts.

Create a @ViewComponent that implements the functional interface `Supplier<ViewContext>`. Instead of the render() function we will now override the get method of the Supplier interface.

If you start your application the component should be automatically rendered on http://localhost:8080
```kotlin
@ViewComponent
class HomeViewComponent(
private val exampleService: ExampleService,
) : Supplier<ViewContext> {
override fun get() = ViewContext(
"helloWorld" toProperty exampleService.getHelloWorld(),
"coffee" toProperty exampleService.getCoffee()
)
}
```

### Local Development

Expand Down Expand Up @@ -347,6 +367,38 @@ public class Router {
}
```

### Serverless components - Spring Cloud Function support

If you want to deploy your application on a serverless platform such as AWS Lambda or Azure Functions you can easily do that with the Spring Cloud Function support.

Just add the dependency `implementation("org.springframework.cloud:spring-cloud-function-context")` to your build.gradle.kts.

Create a @ViewComponent that implements the functional interface `Supplier<ViewContext>`. Instead of the render() function we will now override the get method of the Supplier interface.

If you start your application the component should be automatically rendered on http://localhost:8080


```java
// HomeViewComponent.java
@ViewComponent
public class HomeViewComponent implements Supplier<ViewContext> {
private final ExampleService exampleService;

public HomeViewComponent(ExampleService exampleService) {
this.exampleService = exampleService;
}

@Override
public ViewContext get() {
return new ViewContext(
ViewProperty.of("helloWorld", "Hello World"),
ViewProperty.of("coffee", exampleService.getCoffee()),
ViewProperty.of("office", exampleService.getOfficeHours())
);
}
}
```

### Local Development

To enable live reload of the components on each save without rebuilding the application add this configuration:
Expand Down
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-aop")
implementation("org.springframework.boot:spring-boot-devtools")
implementation("io.projectreactor:reactor-core")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package de.tschuehly.thymeleafviewcomponent
import org.aspectj.lang.ProceedingJoinPoint
import org.aspectj.lang.annotation.Around
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Pointcut
import org.springframework.stereotype.Component


Expand All @@ -14,7 +15,17 @@ annotation class ViewComponent
@Aspect
@Component
class ViewComponentAspect {
@Around("execution(* render(..)) && @within(de.tschuehly.thymeleafviewcomponent.ViewComponent)")
@Pointcut("@within(de.tschuehly.thymeleafviewcomponent.ViewComponent)")
fun isViewComponent(){
//
}

@Pointcut("execution(* render(..)) || execution(* get(..))")
fun isRenderOrGetMethod(){
//
}

@Around("isViewComponent() && isRenderOrGetMethod()")
fun renderInject(joinPoint: ProceedingJoinPoint): ViewContext {

val returnValue = joinPoint.proceed()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer

@Component
class ViewComponentMvcConfigurer(
private val viewContextContainerMethodReturnValueHandler: ViewContextContainerMethodReturnValueHandler
private val viewContextContainerMethodReturnValueHandler: ViewContextContainerMethodReturnValueHandler,
) : WebMvcConfigurer {

override fun addReturnValueHandlers(handlers: MutableList<HandlerMethodReturnValueHandler>) {
handlers.add(ViewContextAsyncHandlerMethodReturnValueHandler())
handlers.add(viewContextContainerMethodReturnValueHandler)
handlers.add(ViewContextMethodReturnValueHandler())
super.addReturnValueHandlers(handlers)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,13 @@ class ViewComponentProcessor(dialectPrefix: String) :
val viewContext = try {
expression.execute(webContext) as ViewContext
} catch (e: TemplateProcessingException) {
throw ViewComponentProcessingException(e.message, e.cause)
try{
val supplierExpression = parser.parseExpression(webContext,expressionString.replace(".render(",".get("))
supplierExpression.execute(webContext) as ViewContext
}catch (e: TemplateProcessingException){
throw ViewComponentProcessingException(e.message, e.cause)
}

}
val engine = appCtx.getBean(SpringTemplateEngine::class.java)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package de.tschuehly.thymeleafviewcomponent

import org.springframework.core.MethodParameter
import org.springframework.http.ResponseEntity
import org.springframework.web.context.request.NativeWebRequest
import org.springframework.web.method.support.AsyncHandlerMethodReturnValueHandler
import org.springframework.web.method.support.ModelAndViewContainer
import reactor.core.publisher.Mono

class ViewContextAsyncHandlerMethodReturnValueHandler() : AsyncHandlerMethodReturnValueHandler {
override fun supportsReturnType(returnType: MethodParameter): Boolean {
return Mono::class.java.isAssignableFrom(returnType.parameterType)
}

override fun handleReturnValue(
returnValue: Any?,
returnType: MethodParameter,
mavContainer: ModelAndViewContainer,
webRequest: NativeWebRequest
) {
if(returnValue == null){
throw Exception("Return Value is null")
}
val mono= returnValue.takeIf {
(Mono::class.java.isAssignableFrom(it.javaClass))
} as Mono<*>
val responseEntity = mono.block()?.takeIf {
ResponseEntity::class.java.isAssignableFrom(it.javaClass)
} as ResponseEntity<*>
val viewContext = responseEntity.body?.takeIf {
ViewContext::class.java.isAssignableFrom(it.javaClass)
} as ViewContext
mavContainer.view = viewContext.componentTemplate
mavContainer.addAllAttributes(viewContext.contextAttributes.toMap())
}

override fun isAsyncReturnValue(returnValue: Any?, returnType: MethodParameter): Boolean {
return returnValue != null && supportsReturnType(returnType)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ class ViewContextMethodReturnValueHandler : HandlerMethodReturnValueHandler {
mavContainer: ModelAndViewContainer,
webRequest: NativeWebRequest
) {
val viewContext = returnValue as ViewContext
val viewContext = returnValue?.takeIf {
(ViewContext::class.java.isAssignableFrom(it.javaClass))
} as ViewContext
mavContainer.view = viewContext.componentTemplate
mavContainer.addAllAttributes(viewContext.contextAttributes.toMap())
}
Expand Down

0 comments on commit aeba511

Please sign in to comment.