Initial commit
This commit is contained in:
@@ -1,14 +0,0 @@
|
||||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
|
||||
[*.yml]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
17
.github/ISSUE_TEMPLATE/bug_report.md
vendored
17
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,17 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: waiting-for-triage
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
Please provide details of the problem, including the version of Spring Cloud that you
|
||||
are using.
|
||||
|
||||
**Sample**
|
||||
If possible, please provide a test case or sample application that reproduces
|
||||
the problem. This makes it much easier for us to diagnose the problem and to verify that
|
||||
we have fixed it.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: waiting-for-triage
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
53
.github/workflows/deploy-docs.yml
vendored
Normal file
53
.github/workflows/deploy-docs.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
name: Deploy Docs
|
||||
run-name: ${{ format('{0} ({1})', github.workflow, github.event.inputs.build-refname || 'all') }}
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
build-refname:
|
||||
description: Enter git refname to build (e.g., 5.7.x).
|
||||
required: false
|
||||
push:
|
||||
branches: docs-build
|
||||
env:
|
||||
GRADLE_ENTERPRISE_SECRET_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
|
||||
permissions:
|
||||
contents: write
|
||||
jobs:
|
||||
build:
|
||||
if: github.repository_owner == 'spring-cloud'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 5
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
- name: Set up refname build
|
||||
if: github.event.inputs.build-refname
|
||||
run: |
|
||||
git fetch --depth 1 https://github.com/$GITHUB_REPOSITORY ${{ github.event.inputs.build-refname }}
|
||||
export BUILD_REFNAME=${{ github.event.inputs.build-refname }}
|
||||
echo "BUILD_REFNAME=$BUILD_REFNAME" >> $GITHUB_ENV
|
||||
export BUILD_VERSION=$(git cat-file --textconv FETCH_HEAD:pom.xml | python3 -c "import xml.etree.ElementTree as xml; from sys import stdin; print(xml.parse(stdin).getroot().find('{http://maven.apache.org/POM/4.0.0}version').text)")
|
||||
echo BUILD_VERSION=$BUILD_VERSION >> $GITHUB_ENV
|
||||
- name: Run Antora
|
||||
run: |
|
||||
./mvnw --no-transfer-progress -B antora
|
||||
- name: Publish Docs
|
||||
uses: rwinch/spring-doc-actions/rsync-antora-reference@httpdocs-path
|
||||
with:
|
||||
docs-username: ${{ secrets.DOCS_USERNAME }}
|
||||
docs-host: ${{ secrets.DOCS_HOST }}
|
||||
docs-ssh-key: ${{ secrets.DOCS_SSH_KEY }}
|
||||
docs-ssh-host-key: ${{ secrets.DOCS_SSH_HOST_KEY }}
|
||||
site-path: target/antora/site
|
||||
- name: Bust Cloudflare Cache
|
||||
uses: spring-io/spring-doc-actions/bust-cloudflare-antora-cache@v0.0.11
|
||||
with:
|
||||
context-root: spring-cloud-function
|
||||
cloudflare-zone-id: ${{ secrets.CLOUDFLARE_ZONE_ID }}
|
||||
cloudflare-cache-token: ${{ secrets.CLOUDFLARE_CACHE_TOKEN }}
|
||||
40
.gitignore
vendored
40
.gitignore
vendored
@@ -1,32 +1,22 @@
|
||||
/application.yml
|
||||
/application.properties
|
||||
asciidoctor.css
|
||||
*~
|
||||
.#*
|
||||
*#
|
||||
target/
|
||||
build/
|
||||
bin/
|
||||
.sts4-cache/
|
||||
.attach_pid*
|
||||
.m2/
|
||||
.gradle/
|
||||
_site/
|
||||
.vscode/
|
||||
.classpath
|
||||
.project
|
||||
.settings/
|
||||
.project
|
||||
.classpath
|
||||
*.orig
|
||||
.springBeans
|
||||
.factorypath
|
||||
.sts4-cache
|
||||
.ant-targets-build.xml
|
||||
src/ant/.ant-targets-upload-dist.xml
|
||||
*.sonar4clipse*
|
||||
.DS_Store
|
||||
*.sw*
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
.factorypath
|
||||
.checkstyle
|
||||
coverage-error.log
|
||||
.apt_generated
|
||||
credentials.yml
|
||||
.flattened-pom.xml
|
||||
pom.xml.versionsBackup
|
||||
/.idea/
|
||||
*.graphml
|
||||
node
|
||||
node_modules
|
||||
build
|
||||
package.json
|
||||
package-lock.json
|
||||
|
||||
@@ -1 +1 @@
|
||||
-Xmx1024m -XX:CICompilerCount=1 -noverify -XX:TieredStopAtLevel=1 -Djava.security.egd=file:/dev/./urandom
|
||||
-Xmx1024m -XX:CICompilerCount=1 -XX:TieredStopAtLevel=1 -Djava.security.egd=file:/dev/./urandom
|
||||
@@ -1 +0,0 @@
|
||||
-DaltSnapshotDeploymentRepository=repo.spring.io::default::https://repo.spring.io/libs-snapshot-local -P spring
|
||||
@@ -1,15 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<settings>
|
||||
<servers>
|
||||
<server>
|
||||
<id>sonatype-nexus-staging</id>
|
||||
<username>${env.sonatype_username}</username>
|
||||
<password>${env.sonatype_password}</password>
|
||||
</server>
|
||||
<server>
|
||||
<id>repo.spring.io</id>
|
||||
<username>${env.spring_username}</username>
|
||||
<password>${env.spring_password}</password>
|
||||
</server>
|
||||
</servers>
|
||||
</settings>
|
||||
11
.travis.yml
11
.travis.yml
@@ -1,11 +0,0 @@
|
||||
language: java
|
||||
jdk:
|
||||
- oraclejdk8
|
||||
sudo: required
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.m2
|
||||
install: true
|
||||
before_script: ./mvnw install -q -U -DskipTests=true -Dmaven.test.redirectTestOutputToFile=true
|
||||
script: ./mvnw install -q -nsu -Dmaven.test.redirectTestOutputToFile=true
|
||||
dist: trusty
|
||||
@@ -1,3 +0,0 @@
|
||||
If you have not previously done so, please fill out and
|
||||
submit the https://cla.pivotal.io/sign/spring[Contributor License Agreement].
|
||||
|
||||
12
Guardfile
12
Guardfile
@@ -1,12 +0,0 @@
|
||||
require 'asciidoctor'
|
||||
require 'erb'
|
||||
require './src/main/ruby/readme.rb'
|
||||
|
||||
options = {:mkdirs => true, :safe => :unsafe, :attributes => ['linkcss', 'allow-uri-read']}
|
||||
|
||||
guard 'shell' do
|
||||
watch(/^src\/[A-Z-a-z][^#]*\.adoc$/) {|m|
|
||||
SpringCloud::Build.render_file('src/main/asciidoc/README.adoc', :to_file => './README.adoc')
|
||||
Asciidoctor.render_file('src/main/asciidoc/spring-cloud-cli.adoc', options.merge(:to_dir => 'target/generated-docs'))
|
||||
}
|
||||
end
|
||||
202
LICENSE
202
LICENSE
@@ -1,202 +0,0 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
https://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
412
README.adoc
412
README.adoc
@@ -1,411 +1,23 @@
|
||||
////
|
||||
DO NOT EDIT THIS FILE. IT WAS GENERATED.
|
||||
Manual changes to this file will be lost when it is generated again.
|
||||
Edit the files in the src/main/asciidoc/ directory instead.
|
||||
////
|
||||
= Spring Cloud Function Docs Build
|
||||
|
||||
You're currently viewing the Antora playbook branch.
|
||||
The playbook branch hosts the docs build that is used to build and publish the production docs site.
|
||||
|
||||
:branch: master
|
||||
The Spring Cloud Function reference docs are built using https://antora.org[Antora].
|
||||
This README covers how to build the docs in a software branch as well as how to build the production docs site locally.
|
||||
|
||||
image::https://travis-ci.org/spring-cloud/spring-cloud-function.svg?branch={branch}[Build Status, link=https://travis-ci.org/spring-cloud/spring-cloud-function]
|
||||
== Building the Site
|
||||
|
||||
== Introduction
|
||||
You can build the entire site by invoking the following on the docs-build branch and then viewing the site at `target/site/index.html`
|
||||
|
||||
Spring Cloud Function is a project with the following high-level goals:
|
||||
|
||||
* Promote the implementation of business logic via functions.
|
||||
* Decouple the development lifecycle of business logic from any specific runtime target so that the same code can run as a web endpoint, a stream processor, or a task.
|
||||
* Support a uniform programming model across serverless providers, as well as the ability to run standalone (locally or in a PaaS).
|
||||
* Enable Spring Boot features (auto-configuration, dependency injection, metrics) on serverless providers.
|
||||
|
||||
It abstracts away all of the transport details and
|
||||
infrastructure, allowing the developer to keep all the familiar tools
|
||||
and processes, and focus firmly on business logic.
|
||||
|
||||
Here's a complete, executable, testable Spring Boot application
|
||||
(implementing a simple string manipulation):
|
||||
|
||||
[source,java]
|
||||
[source,bash]
|
||||
----
|
||||
@SpringBootApplication
|
||||
public class Application {
|
||||
|
||||
@Bean
|
||||
public Function<String, String> uppercase() {
|
||||
return value -> value.toUpperCase();
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(Application.class, args);
|
||||
}
|
||||
}
|
||||
./mvnw antora
|
||||
----
|
||||
|
||||
It's just a Spring Boot application, so it can be built, run and
|
||||
tested, locally and in a CI build, the same way as any other Spring
|
||||
Boot application. The `Function` is from `java.util` and `Flux` is a
|
||||
https://www.reactive-streams.org/[Reactive Streams] `Publisher` from
|
||||
https://projectreactor.io/[Project Reactor]. The function can be
|
||||
accessed over HTTP or messaging.
|
||||
|
||||
Spring Cloud Function has the following features:
|
||||
|
||||
* _Choice of programming styles - reactive, imperative or hybrid._
|
||||
* _Function composition and adaptation (e.g., composing imperative functions with reactive)._
|
||||
* _Support for reactive function with multiple inputs and outputs allowing merging, joining and other complex streaming operation to be handled by functions._
|
||||
* _Transparent type conversion of inputs and outputs._
|
||||
* _Packaging functions for deployments, specific to the target platform (e.g., Project Riff, AWS Lambda and more)_
|
||||
* _Adapters to expose function to the outside world as HTTP endpoints etc._
|
||||
* _Deploying a JAR file containing such an application context with an isolated classloader, so that you can pack them together in a single JVM._
|
||||
* _Adapters for https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-adapters/spring-cloud-function-adapter-aws[AWS Lambda], https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-adapters/spring-cloud-function-adapter-azure[Azure], https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp[Google Cloud Functions], and possibly other "serverless" service providers._
|
||||
|
||||
== Getting Started
|
||||
|
||||
Build from the command line (and "install" the samples):
|
||||
== Building a Specific Branch
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
$ ./mvnw clean install
|
||||
./mvnw antora
|
||||
----
|
||||
|
||||
(If you like to YOLO add `-DskipTests`.)
|
||||
|
||||
Run one of the samples, e.g.
|
||||
|
||||
----
|
||||
$ java -jar spring-cloud-function-samples/function-sample/target/*.jar
|
||||
----
|
||||
|
||||
This runs the app and exposes its functions over HTTP, so you can
|
||||
convert a string to uppercase, like this:
|
||||
|
||||
----
|
||||
$ curl -H "Content-Type: text/plain" localhost:8080/uppercase -d Hello
|
||||
HELLO
|
||||
----
|
||||
|
||||
You can convert multiple strings (a `Flux<String>`) by separating them
|
||||
with new lines
|
||||
|
||||
----
|
||||
$ curl -H "Content-Type: text/plain" localhost:8080/uppercase -d 'Hello
|
||||
> World'
|
||||
HELLOWORLD
|
||||
----
|
||||
|
||||
(You can use `^Q^J` in a terminal to insert a new line in a literal
|
||||
string like that.)
|
||||
|
||||
== Building
|
||||
|
||||
:jdkversion: 17
|
||||
|
||||
=== Basic Compile and Test
|
||||
|
||||
To build the source you will need to install JDK {jdkversion}.
|
||||
|
||||
Spring Cloud uses Maven for most build-related activities, and you
|
||||
should be able to get off the ground quite quickly by cloning the
|
||||
project you are interested in and typing
|
||||
|
||||
----
|
||||
$ ./mvnw install
|
||||
----
|
||||
|
||||
NOTE: You can also install Maven (>=3.3.3) yourself and run the `mvn` command
|
||||
in place of `./mvnw` in the examples below. If you do that you also
|
||||
might need to add `-P spring` if your local Maven settings do not
|
||||
contain repository declarations for spring pre-release artifacts.
|
||||
|
||||
NOTE: Be aware that you might need to increase the amount of memory
|
||||
available to Maven by setting a `MAVEN_OPTS` environment variable with
|
||||
a value like `-Xmx512m -XX:MaxPermSize=128m`. We try to cover this in
|
||||
the `.mvn` configuration, so if you find you have to do it to make a
|
||||
build succeed, please raise a ticket to get the settings added to
|
||||
source control.
|
||||
|
||||
The projects that require middleware (i.e. Redis) for testing generally
|
||||
require that a local instance of [Docker](https://www.docker.com/get-started) is installed and running.
|
||||
|
||||
|
||||
=== Documentation
|
||||
|
||||
The spring-cloud-build module has a "docs" profile, and if you switch
|
||||
that on it will try to build asciidoc sources from
|
||||
`src/main/asciidoc`. As part of that process it will look for a
|
||||
`README.adoc` and process it by loading all the includes, but not
|
||||
parsing or rendering it, just copying it to `${main.basedir}`
|
||||
(defaults to `${basedir}`, i.e. the root of the project). If there are
|
||||
any changes in the README it will then show up after a Maven build as
|
||||
a modified file in the correct place. Just commit it and push the change.
|
||||
|
||||
=== Working with the code
|
||||
If you don't have an IDE preference we would recommend that you use
|
||||
https://www.springsource.com/developer/sts[Spring Tools Suite] or
|
||||
https://eclipse.org[Eclipse] when working with the code. We use the
|
||||
https://eclipse.org/m2e/[m2eclipse] eclipse plugin for maven support. Other IDEs and tools
|
||||
should also work without issue as long as they use Maven 3.3.3 or better.
|
||||
|
||||
==== Activate the Spring Maven profile
|
||||
Spring Cloud projects require the 'spring' Maven profile to be activated to resolve
|
||||
the spring milestone and snapshot repositories. Use your preferred IDE to set this
|
||||
profile to be active, or you may experience build errors.
|
||||
|
||||
==== Importing into eclipse with m2eclipse
|
||||
We recommend the https://eclipse.org/m2e/[m2eclipse] eclipse plugin when working with
|
||||
eclipse. If you don't already have m2eclipse installed it is available from the "eclipse
|
||||
marketplace".
|
||||
|
||||
NOTE: Older versions of m2e do not support Maven 3.3, so once the
|
||||
projects are imported into Eclipse you will also need to tell
|
||||
m2eclipse to use the right profile for the projects. If you
|
||||
see many different errors related to the POMs in the projects, check
|
||||
that you have an up to date installation. If you can't upgrade m2e,
|
||||
add the "spring" profile to your `settings.xml`. Alternatively you can
|
||||
copy the repository settings from the "spring" profile of the parent
|
||||
pom into your `settings.xml`.
|
||||
|
||||
==== Importing into eclipse without m2eclipse
|
||||
If you prefer not to use m2eclipse you can generate eclipse project metadata using the
|
||||
following command:
|
||||
|
||||
[indent=0]
|
||||
----
|
||||
$ ./mvnw eclipse:eclipse
|
||||
----
|
||||
|
||||
The generated eclipse projects can be imported by selecting `import existing projects`
|
||||
from the `file` menu.
|
||||
|
||||
|
||||
== Contributing
|
||||
|
||||
:spring-cloud-build-branch: master
|
||||
|
||||
Spring Cloud is released under the non-restrictive Apache 2.0 license,
|
||||
and follows a very standard Github development process, using Github
|
||||
tracker for issues and merging pull requests into master. If you want
|
||||
to contribute even something trivial please do not hesitate, but
|
||||
follow the guidelines below.
|
||||
|
||||
=== Sign the Contributor License Agreement
|
||||
Before we accept a non-trivial patch or pull request we will need you to sign the
|
||||
https://cla.pivotal.io/sign/spring[Contributor License Agreement].
|
||||
Signing the contributor's agreement does not grant anyone commit rights to the main
|
||||
repository, but it does mean that we can accept your contributions, and you will get an
|
||||
author credit if we do. Active contributors might be asked to join the core team, and
|
||||
given the ability to merge pull requests.
|
||||
|
||||
=== Code of Conduct
|
||||
This project adheres to the Contributor Covenant https://github.com/spring-cloud/spring-cloud-build/blob/master/docs/src/main/asciidoc/code-of-conduct.adoc[code of
|
||||
conduct]. By participating, you are expected to uphold this code. Please report
|
||||
unacceptable behavior to spring-code-of-conduct@pivotal.io.
|
||||
|
||||
=== Code Conventions and Housekeeping
|
||||
None of these is essential for a pull request, but they will all help. They can also be
|
||||
added after the original pull request but before a merge.
|
||||
|
||||
* Use the Spring Framework code format conventions. If you use Eclipse
|
||||
you can import formatter settings using the
|
||||
`eclipse-code-formatter.xml` file from the
|
||||
https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-dependencies-parent/eclipse-code-formatter.xml[Spring
|
||||
Cloud Build] project. If using IntelliJ, you can use the
|
||||
https://plugins.jetbrains.com/plugin/6546[Eclipse Code Formatter
|
||||
Plugin] to import the same file.
|
||||
* Make sure all new `.java` files to have a simple Javadoc class comment with at least an
|
||||
`@author` tag identifying you, and preferably at least a paragraph on what the class is
|
||||
for.
|
||||
* Add the ASF license header comment to all new `.java` files (copy from existing files
|
||||
in the project)
|
||||
* Add yourself as an `@author` to the .java files that you modify substantially (more
|
||||
than cosmetic changes).
|
||||
* Add some Javadocs and, if you change the namespace, some XSD doc elements.
|
||||
* A few unit tests would help a lot as well -- someone has to do it.
|
||||
* If no-one else is using your branch, please rebase it against the current master (or
|
||||
other target branch in the main project).
|
||||
* When writing a commit message please follow https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html[these conventions],
|
||||
if you are fixing an existing issue please add `Fixes gh-XXXX` at the end of the commit
|
||||
message (where XXXX is the issue number).
|
||||
|
||||
=== Checkstyle
|
||||
|
||||
Spring Cloud Build comes with a set of checkstyle rules. You can find them in the `spring-cloud-build-tools` module. The most notable files under the module are:
|
||||
|
||||
.spring-cloud-build-tools/
|
||||
----
|
||||
└── src
|
||||
├── checkstyle
|
||||
│ └── checkstyle-suppressions.xml <3>
|
||||
└── main
|
||||
└── resources
|
||||
├── checkstyle-header.txt <2>
|
||||
└── checkstyle.xml <1>
|
||||
----
|
||||
<1> Default Checkstyle rules
|
||||
<2> File header setup
|
||||
<3> Default suppression rules
|
||||
|
||||
==== Checkstyle configuration
|
||||
|
||||
Checkstyle rules are *disabled by default*. To add checkstyle to your project just define the following properties and plugins.
|
||||
|
||||
.pom.xml
|
||||
----
|
||||
<properties>
|
||||
<maven-checkstyle-plugin.failsOnError>true</maven-checkstyle-plugin.failsOnError> <1>
|
||||
<maven-checkstyle-plugin.failsOnViolation>true
|
||||
</maven-checkstyle-plugin.failsOnViolation> <2>
|
||||
<maven-checkstyle-plugin.includeTestSourceDirectory>true
|
||||
</maven-checkstyle-plugin.includeTestSourceDirectory> <3>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin> <4>
|
||||
<groupId>io.spring.javaformat</groupId>
|
||||
<artifactId>spring-javaformat-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin> <5>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-checkstyle-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
|
||||
<reporting>
|
||||
<plugins>
|
||||
<plugin> <5>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-checkstyle-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</reporting>
|
||||
</build>
|
||||
----
|
||||
<1> Fails the build upon Checkstyle errors
|
||||
<2> Fails the build upon Checkstyle violations
|
||||
<3> Checkstyle analyzes also the test sources
|
||||
<4> Add the Spring Java Format plugin that will reformat your code to pass most of the Checkstyle formatting rules
|
||||
<5> Add checkstyle plugin to your build and reporting phases
|
||||
|
||||
If you need to suppress some rules (e.g. line length needs to be longer), then it's enough for you to define a file under `${project.root}/src/checkstyle/checkstyle-suppressions.xml` with your suppressions. Example:
|
||||
|
||||
.projectRoot/src/checkstyle/checkstyle-suppresions.xml
|
||||
----
|
||||
<?xml version="1.0"?>
|
||||
<!DOCTYPE suppressions PUBLIC
|
||||
"-//Puppy Crawl//DTD Suppressions 1.1//EN"
|
||||
"https://www.puppycrawl.com/dtds/suppressions_1_1.dtd">
|
||||
<suppressions>
|
||||
<suppress files=".*ConfigServerApplication\.java" checks="HideUtilityClassConstructor"/>
|
||||
<suppress files=".*ConfigClientWatch\.java" checks="LineLengthCheck"/>
|
||||
</suppressions>
|
||||
----
|
||||
|
||||
It's advisable to copy the `${spring-cloud-build.rootFolder}/.editorconfig` and `${spring-cloud-build.rootFolder}/.springformat` to your project. That way, some default formatting rules will be applied. You can do so by running this script:
|
||||
|
||||
```bash
|
||||
$ curl https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/.editorconfig -o .editorconfig
|
||||
$ touch .springformat
|
||||
```
|
||||
|
||||
=== IDE setup
|
||||
|
||||
==== Intellij IDEA
|
||||
|
||||
In order to setup Intellij you should import our coding conventions, inspection profiles and set up the checkstyle plugin.
|
||||
The following files can be found in the https://github.com/spring-cloud/spring-cloud-build/tree/master/spring-cloud-build-tools[Spring Cloud Build] project.
|
||||
|
||||
.spring-cloud-build-tools/
|
||||
----
|
||||
└── src
|
||||
├── checkstyle
|
||||
│ └── checkstyle-suppressions.xml <3>
|
||||
└── main
|
||||
└── resources
|
||||
├── checkstyle-header.txt <2>
|
||||
├── checkstyle.xml <1>
|
||||
└── intellij
|
||||
├── Intellij_Project_Defaults.xml <4>
|
||||
└── Intellij_Spring_Boot_Java_Conventions.xml <5>
|
||||
----
|
||||
<1> Default Checkstyle rules
|
||||
<2> File header setup
|
||||
<3> Default suppression rules
|
||||
<4> Project defaults for Intellij that apply most of Checkstyle rules
|
||||
<5> Project style conventions for Intellij that apply most of Checkstyle rules
|
||||
|
||||
.Code style
|
||||
|
||||
image::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/{spring-cloud-build-branch}/docs/src/main/asciidoc/images/intellij-code-style.png[Code style]
|
||||
|
||||
Go to `File` -> `Settings` -> `Editor` -> `Code style`. There click on the icon next to the `Scheme` section. There, click on the `Import Scheme` value and pick the `Intellij IDEA code style XML` option. Import the `spring-cloud-build-tools/src/main/resources/intellij/Intellij_Spring_Boot_Java_Conventions.xml` file.
|
||||
|
||||
.Inspection profiles
|
||||
|
||||
image::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/{spring-cloud-build-branch}/docs/src/main/asciidoc/images/intellij-inspections.png[Code style]
|
||||
|
||||
Go to `File` -> `Settings` -> `Editor` -> `Inspections`. There click on the icon next to the `Profile` section. There, click on the `Import Profile` and import the `spring-cloud-build-tools/src/main/resources/intellij/Intellij_Project_Defaults.xml` file.
|
||||
|
||||
.Checkstyle
|
||||
|
||||
To have Intellij work with Checkstyle, you have to install the `Checkstyle` plugin. It's advisable to also install the `Assertions2Assertj` to automatically convert the JUnit assertions
|
||||
|
||||
image::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/{spring-cloud-build-branch}/docs/src/main/asciidoc/images/intellij-checkstyle.png[Checkstyle]
|
||||
|
||||
Go to `File` -> `Settings` -> `Other settings` -> `Checkstyle`. There click on the `+` icon in the `Configuration file` section. There, you'll have to define where the checkstyle rules should be picked from. In the image above, we've picked the rules from the cloned Spring Cloud Build repository. However, you can point to the Spring Cloud Build's GitHub repository (e.g. for the `checkstyle.xml` : `https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-build-tools/src/main/resources/checkstyle.xml`). We need to provide the following variables:
|
||||
|
||||
- `checkstyle.header.file` - please point it to the Spring Cloud Build's, `spring-cloud-build-tools/src/main/resources/checkstyle-header.txt` file either in your cloned repo or via the `https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-build-tools/src/main/resources/checkstyle-header.txt` URL.
|
||||
- `checkstyle.suppressions.file` - default suppressions. Please point it to the Spring Cloud Build's, `spring-cloud-build-tools/src/checkstyle/checkstyle-suppressions.xml` file either in your cloned repo or via the `https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-build-tools/src/checkstyle/checkstyle-suppressions.xml` URL.
|
||||
- `checkstyle.additional.suppressions.file` - this variable corresponds to suppressions in your local project. E.g. you're working on `spring-cloud-contract`. Then point to the `project-root/src/checkstyle/checkstyle-suppressions.xml` folder. Example for `spring-cloud-contract` would be: `/home/username/spring-cloud-contract/src/checkstyle/checkstyle-suppressions.xml`.
|
||||
|
||||
IMPORTANT: Remember to set the `Scan Scope` to `All sources` since we apply checkstyle rules for production and test sources.
|
||||
|
||||
=== Duplicate Finder
|
||||
|
||||
Spring Cloud Build brings along the `basepom:duplicate-finder-maven-plugin`, that enables flagging duplicate and conflicting classes and resources on the java classpath.
|
||||
|
||||
==== Duplicate Finder configuration
|
||||
|
||||
Duplicate finder is *enabled by default* and will run in the `verify` phase of your Maven build, but it will only take effect in your project if you add the `duplicate-finder-maven-plugin` to the `build` section of the projecst's `pom.xml`.
|
||||
|
||||
.pom.xml
|
||||
[source,xml]
|
||||
----
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.basepom.maven</groupId>
|
||||
<artifactId>duplicate-finder-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
----
|
||||
|
||||
For other properties, we have set defaults as listed in the https://github.com/basepom/duplicate-finder-maven-plugin/wiki[plugin documentation].
|
||||
|
||||
You can easily override them but setting the value of the selected property prefixed with `duplicate-finder-maven-plugin`. For example, set `duplicate-finder-maven-plugin.skip` to `true` in order to skip duplicates check in your build.
|
||||
|
||||
If you need to add `ignoredClassPatterns` or `ignoredResourcePatterns` to your setup, make sure to add them in the plugin configuration section of your project:
|
||||
|
||||
[source,xml]
|
||||
----
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.basepom.maven</groupId>
|
||||
<artifactId>duplicate-finder-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<ignoredClassPatterns>
|
||||
<ignoredClassPattern>org.joda.time.base.BaseDateTime</ignoredClassPattern>
|
||||
<ignoredClassPattern>.*module-info</ignoredClassPattern>
|
||||
</ignoredClassPatterns>
|
||||
<ignoredResourcePatterns>
|
||||
<ignoredResourcePattern>changelog.txt</ignoredResourcePattern>
|
||||
</ignoredResourcePatterns>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
|
||||
----
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
**Describe the bug**
|
||||
Please provide details of the problem, including the version of Spring Cloud that you
|
||||
are using.
|
||||
|
||||
**Sample**
|
||||
If possible, please provide a test case or sample application that reproduces
|
||||
the problem. This makes it much easier for us to diagnose the problem and to verify that
|
||||
we have fixed it.
|
||||
@@ -6,38 +6,39 @@ antora:
|
||||
- '@antora/collector-extension'
|
||||
- '@antora/atlas-extension'
|
||||
- require: '@springio/antora-extensions/root-component-extension'
|
||||
root_component_name: 'PROJECT_WITHOUT_SPRING'
|
||||
# FIXME: Run antora once using this extension to migrate to the Asciidoc Tabs syntax
|
||||
# and then remove this extension
|
||||
- require: '@springio/antora-extensions/tabs-migration-extension'
|
||||
unwrap_example_block: always
|
||||
save_result: true
|
||||
root_component_name: 'cloud-function'
|
||||
site:
|
||||
title: PROJECT_FULL_NAME
|
||||
url: https://docs.spring.io/PROJECT_NAME/reference/
|
||||
title: Spring Cloud Function
|
||||
url: https://docs.spring.io/spring-cloud-function/reference
|
||||
robots: allow
|
||||
git:
|
||||
ensure_git_suffix: false
|
||||
content:
|
||||
sources:
|
||||
- url: ./..
|
||||
branches: HEAD
|
||||
- url: https://github.com/spring-cloud/spring-cloud-function
|
||||
# Refname matching:
|
||||
# https://docs.antora.org/antora/latest/playbook/content-refname-matching/
|
||||
branches: [ main ]
|
||||
tags: [ '({4..9}).+({1..9}).+({0..9})?(-{RC,M}+({0..9}))', '!4.1.0-M1' ]
|
||||
start_path: docs
|
||||
worktrees: true
|
||||
asciidoc:
|
||||
attributes:
|
||||
page-stackoverflow-url: https://stackoverflow.com/tags/spring-cloud
|
||||
page-pagination: ''
|
||||
hide-uri-scheme: '@'
|
||||
tabs-sync-option: '@'
|
||||
chomp: 'all'
|
||||
extensions:
|
||||
- '@asciidoctor/tabs'
|
||||
- '@springio/asciidoctor-extensions'
|
||||
sourcemap: true
|
||||
urls:
|
||||
latest_version_segment_strategy: redirect:to
|
||||
latest_version_segment: ''
|
||||
redirect_facility: httpd
|
||||
ui:
|
||||
bundle:
|
||||
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.3.5/ui-bundle.zip
|
||||
snapshot: true
|
||||
runtime:
|
||||
log:
|
||||
failure_level: warn
|
||||
format: pretty
|
||||
ui:
|
||||
bundle:
|
||||
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.3.5/ui-bundle.zip
|
||||
32
docs/.github/workflows/deploy-docs.yml
vendored
32
docs/.github/workflows/deploy-docs.yml
vendored
@@ -1,32 +0,0 @@
|
||||
name: Deploy Docs
|
||||
on:
|
||||
push:
|
||||
branches-ignore: [ gh-pages ]
|
||||
tags: '**'
|
||||
repository_dispatch:
|
||||
types: request-build-reference # legacy
|
||||
#schedule:
|
||||
#- cron: '0 10 * * *' # Once per day at 10am UTC
|
||||
workflow_dispatch:
|
||||
permissions:
|
||||
actions: write
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
# if: github.repository_owner == 'spring-cloud'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: docs-build
|
||||
fetch-depth: 1
|
||||
- name: Dispatch (partial build)
|
||||
if: github.ref_type == 'branch'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: gh workflow run deploy-docs.yml -r $(git rev-parse --abbrev-ref HEAD) -f build-refname=${{ github.ref_name }}
|
||||
- name: Dispatch (full build)
|
||||
if: github.ref_type == 'tag'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: gh workflow run deploy-docs.yml -r $(git rev-parse --abbrev-ref HEAD)
|
||||
@@ -1,12 +0,0 @@
|
||||
name: PROJECT_WITHOUT_SPRING
|
||||
version: true
|
||||
title: PROJECT_NAME
|
||||
nav:
|
||||
- modules/ROOT/nav.adoc
|
||||
ext:
|
||||
collector:
|
||||
run:
|
||||
command: ./mvnw --no-transfer-progress -B process-resources -Pdocs -pl docs -Dantora-maven-plugin.phase=none -Dgenerate-docs.phase=none -Dgenerate-readme.phase=none -Dgenerate-cloud-resources.phase=none -Dmaven-dependency-plugin-for-docs.phase=none -Dmaven-dependency-plugin-for-docs-classes.phase=none -DskipTests
|
||||
local: true
|
||||
scan:
|
||||
dir: ./target/classes/antora-resources/
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 589 KiB |
@@ -1 +0,0 @@
|
||||
please remove once this directory is not empty
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 158 KiB |
@@ -1,22 +0,0 @@
|
||||
* xref:index.adoc[]
|
||||
* xref:spring-integration.adoc[]
|
||||
* xref:README.adoc[]
|
||||
* xref:_intro.adoc[]
|
||||
** xref:adapters/aws-intro.adoc[]
|
||||
** xref:adapters/aws.adoc[]
|
||||
** xref:adapters/azure-intro.adoc[]
|
||||
** xref:adapters/azure.adoc[]
|
||||
** xref:adapters/gcp-intro.adoc[]
|
||||
** xref:adapters/gcp.adoc[]
|
||||
* xref:functional.adoc[]
|
||||
* xref:getting-started.adoc[]
|
||||
* xref:sagan-index.adoc[]
|
||||
* xref:spring-cloud-function.adoc[]
|
||||
** xref:spring-cloud-function/introduction.adoc[]
|
||||
** xref:spring-cloud-function/getting-started.adoc[]
|
||||
** xref:spring-cloud-function/programming-model.adoc[]
|
||||
** xref:spring-cloud-function/standalone-web-applications.adoc[]
|
||||
** xref:spring-cloud-function/standalone-streaming-applications.adoc[]
|
||||
** xref:spring-cloud-function/deploying-a-packaged.adoc[]
|
||||
** xref:spring-cloud-function/functional-bean-definitions.adoc[]
|
||||
** xref:spring-cloud-function/serverless-platform-adapters.adoc[]
|
||||
@@ -1,25 +0,0 @@
|
||||
:branch: master
|
||||
|
||||
image::https://travis-ci.org/spring-cloud/spring-cloud-function.svg?branch={branch}[Build Status, link=https://travis-ci.org/spring-cloud/spring-cloud-function]
|
||||
|
||||
[[introduction]]
|
||||
= Introduction
|
||||
:page-section-summary-toc: 1
|
||||
|
||||
|
||||
[[getting-started]]
|
||||
= Getting Started
|
||||
:page-section-summary-toc: 1
|
||||
|
||||
|
||||
[[building]]
|
||||
= Building
|
||||
:page-section-summary-toc: 1
|
||||
|
||||
include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/docs/src/main/asciidoc/building.adoc[]
|
||||
|
||||
[[contributing]]
|
||||
= Contributing
|
||||
:page-section-summary-toc: 1
|
||||
|
||||
include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/docs/src/main/asciidoc/contributing.adoc[]
|
||||
@@ -1,47 +0,0 @@
|
||||
Spring Cloud Function is a project with the following high-level goals:
|
||||
|
||||
* Promote the implementation of business logic via functions.
|
||||
* Decouple the development lifecycle of business logic from any specific runtime target so that the same code can run as a web endpoint, a stream processor, or a task.
|
||||
* Support a uniform programming model across serverless providers, as well as the ability to run standalone (locally or in a PaaS).
|
||||
* Enable Spring Boot features (auto-configuration, dependency injection, metrics) on serverless providers.
|
||||
|
||||
It abstracts away all of the transport details and
|
||||
infrastructure, allowing the developer to keep all the familiar tools
|
||||
and processes, and focus firmly on business logic.
|
||||
|
||||
Here's a complete, executable, testable Spring Boot application
|
||||
(implementing a simple string manipulation):
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@SpringBootApplication
|
||||
public class Application {
|
||||
|
||||
@Bean
|
||||
public Function<String, String> uppercase() {
|
||||
return value -> value.toUpperCase();
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(Application.class, args);
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
It's just a Spring Boot application, so it can be built, run and
|
||||
tested, locally and in a CI build, the same way as any other Spring
|
||||
Boot application. The `Function` is from `java.util` and `Flux` is a
|
||||
https://www.reactive-streams.org/[Reactive Streams] `Publisher` from
|
||||
https://projectreactor.io/[Project Reactor]. The function can be
|
||||
accessed over HTTP or messaging.
|
||||
|
||||
Spring Cloud Function has the following features:
|
||||
|
||||
* _Choice of programming styles - reactive, imperative or hybrid._
|
||||
* _Function composition and adaptation (e.g., composing imperative functions with reactive)._
|
||||
* _Support for reactive function with multiple inputs and outputs allowing merging, joining and other complex streaming operation to be handled by functions._
|
||||
* _Transparent type conversion of inputs and outputs._
|
||||
* _Packaging functions for deployments, specific to the target platform (e.g., Project Riff, AWS Lambda and more)_
|
||||
* _Adapters to expose function to the outside world as HTTP endpoints etc._
|
||||
* _Deploying a JAR file containing such an application context with an isolated classloader, so that you can pack them together in a single JVM._
|
||||
* _Adapters for https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-adapters/spring-cloud-function-adapter-aws[AWS Lambda], https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-adapters/spring-cloud-function-adapter-azure[Azure], https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp[Google Cloud Functions], and possibly other "serverless" service providers._
|
||||
@@ -1,330 +0,0 @@
|
||||
:branch: master
|
||||
|
||||
[[aws-lambda]]
|
||||
= AWS Lambda
|
||||
|
||||
The https://aws.amazon.com/[AWS] adapter takes a Spring Cloud Function app and converts it to a form that can run in AWS Lambda.
|
||||
|
||||
The details of how to get stared with AWS Lambda is out of scope of this document, so the expectation is that user has some familiarity with
|
||||
AWS and AWS Lambda and wants to learn what additional value spring provides.
|
||||
|
||||
[[getting-started]]
|
||||
== Getting Started
|
||||
|
||||
One of the goals of Spring Cloud Function framework is to provide necessary infrastructure elements to enable a _simple function application_
|
||||
to interact in a certain way in a particular environment.
|
||||
A simple function application (in context or Spring) is an application that contains beans of type Supplier, Function or Consumer.
|
||||
So, with AWS it means that a simple function bean should somehow be recognised and executed in AWS Lambda environment.
|
||||
|
||||
Let’s look at the example:
|
||||
|
||||
[source, java]
|
||||
----
|
||||
@SpringBootApplication
|
||||
public class FunctionConfiguration {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(FunctionConfiguration.class, args);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Function<String, String> uppercase() {
|
||||
return value -> value.toUpperCase();
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
It shows a complete Spring Boot application with a function bean defined in it. What’s interesting is that on the surface this is just
|
||||
another boot app, but in the context of AWS Adapter it is also a perfectly valid AWS Lambda application. No other code or configuration
|
||||
is required. All you need to do is package it and deploy it, so let’s look how we can do that.
|
||||
|
||||
To make things simpler we’ve provided a sample project ready to be built and deployed and you can access it
|
||||
https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-samples/function-sample-aws[here].
|
||||
|
||||
You simply execute `./mvnw clean package` to generate JAR file. All the necessary maven plugins have already been setup to generate
|
||||
appropriate AWS deployable JAR file. (You can read more details about JAR layout in <<Notes on JAR Layout>>).
|
||||
|
||||
Then you have to upload the JAR file (via AWS dashboard or AWS CLI) to AWS.
|
||||
|
||||
When ask about _handler_ you specify `org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest` which is a generic request handler.
|
||||
|
||||
image::{github-raw}/docs/src/main/asciidoc/images/AWS-deploy.png[width=800,scaledwidth="75%",align="center"]
|
||||
|
||||
That is all. Save and execute the function with some sample data which for this function is expected to be a
|
||||
String which function will uppercase and return back.
|
||||
|
||||
While `org.springframework.cloud.function.adapter.aws.FunctionInvoker` is a general purpose AWS's `RequestHandler` implementation aimed at completely
|
||||
isolating you from the specifics of AWS Lambda API, for some cases you may want to specify which specific AWS's `RequestHandler` you want
|
||||
to use. The next section will explain you how you can accomplish just that.
|
||||
|
||||
|
||||
[[aws-request-handlers]]
|
||||
== AWS Request Handlers
|
||||
|
||||
The adapter has a couple of generic request handlers that you can use. The most generic is (and the one we used in the Getting Started section)
|
||||
is `org.springframework.cloud.function.adapter.aws.FunctionInvoker` which is the implementation of AWS's `RequestStreamHandler`.
|
||||
User doesn't need to do anything other then specify it as 'handler' on AWS dashboard when deploying function.
|
||||
It will handle most of the case including Kinesis, streaming etc. .
|
||||
|
||||
|
||||
If your app has more than one `@Bean` of type `Function` etc. then you can choose the one to use by configuring `spring.cloud.function.definition`
|
||||
property or environment variable. The functions are extracted from the Spring Cloud `FunctionCatalog`. In the event you don't specify `spring.cloud.function.definition`
|
||||
the framework will attempt to find a default following the search order where it searches first for `Function` then `Consumer` and finally `Supplier`).
|
||||
|
||||
|
||||
[[aws-function-routing]]
|
||||
== AWS Function Routing
|
||||
|
||||
One of the core features of Spring Cloud Function is https://docs.spring.io/spring-cloud-function/docs/{project-version}/reference/html/spring-cloud-function.html#_function_routing_and_filtering[routing]
|
||||
- an ability to have one special function to delegate to other functions based on the user provided routing instructions.
|
||||
|
||||
In AWS Lambda environment this feature provides one additional benefit, as it allows you to bind a single function (Routing Function)
|
||||
as AWS Lambda and thus a single HTTP endpoint for API Gateway. So in the end you only manage one function and one endpoint, while benefiting
|
||||
from many function that can be part of your application.
|
||||
|
||||
More details are available in the provided https://github.com/spring-cloud/spring-cloud-function/tree/main/spring-cloud-function-samples/function-sample-aws-routing[sample],
|
||||
yet few general things worth mentioning.
|
||||
|
||||
Routing capabilities will be enabled by default whenever there is more then one function in your application as `org.springframework.cloud.function.adapter.aws.FunctionInvoker`
|
||||
can not determine which function to bind as AWS Lambda, so it defaults to `RoutingFunction`.
|
||||
This means that all you need to do is provide routing instructions which you can do https://docs.spring.io/spring-cloud-function/docs/{project-version}/reference/html/spring-cloud-function.html#_function_routing_and_filtering[using several mechanisms]
|
||||
(see https://github.com/spring-cloud/spring-cloud-function/tree/main/spring-cloud-function-samples/function-sample-aws-routing[sample] for more details).
|
||||
|
||||
Also, note that since AWS does not allow dots `.` and/or hyphens`-` in the name of the environment variable, you can benefit from boot support and simply substitute
|
||||
dots with underscores and hyphens with camel case. So for example `spring.cloud.function.definition` becomes `spring_cloud_function_definition`
|
||||
and `spring.cloud.function.routing-expression` becomes `spring_cloud_function_routingExpression`.
|
||||
|
||||
[[aws-function-routing-with-custom-runtime]]
|
||||
=== AWS Function Routing with Custom Runtime
|
||||
|
||||
When using <<Custom Runtime>> Function Routing works the same way. All you need is to specify `functionRouter` as AWS Handler the same way you would use the name of the function as handler.
|
||||
|
||||
[[notes-on-jar-layout]]
|
||||
== Notes on JAR Layout
|
||||
|
||||
You don't need the Spring Cloud Function Web or Stream adapter at runtime in Lambda, so you might
|
||||
need to exclude those before you create the JAR you send to AWS. A Lambda application has to be
|
||||
shaded, but a Spring Boot standalone application does not, so you can run the same app using 2
|
||||
separate jars (as per the sample). The sample app creates 2 jar files, one with an `aws`
|
||||
classifier for deploying in Lambda, and one [[thin-jar,thin jar]] executable (thin) jar that includes `spring-cloud-function-web`
|
||||
at runtime. Spring Cloud Function will try and locate a "main class" for you from the JAR file
|
||||
manifest, using the `Start-Class` attribute (which will be added for you by the Spring Boot
|
||||
tooling if you use the starter parent). If there is no `Start-Class` in your manifest you can
|
||||
use an environment variable or system property `MAIN_CLASS` when you deploy the function to AWS.
|
||||
|
||||
If you are not using the functional bean definitions but relying on Spring Boot's auto-configuration,
|
||||
and are not depending on `spring-boot-starter-parent`,
|
||||
then additional transformers must be configured as part of the maven-shade-plugin execution.
|
||||
|
||||
[[shade-plugin-setup]]
|
||||
[source, xml]
|
||||
----
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<version>2.7.4</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||
<shadedArtifactAttached>true</shadedArtifactAttached>
|
||||
<shadedClassifierName>aws</shadedClassifierName>
|
||||
<transformers>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
|
||||
<resource>META-INF/spring.handlers</resource>
|
||||
</transformer>
|
||||
<transformer implementation="org.springframework.boot.maven.PropertiesMergingResourceTransformer">
|
||||
<resource>META-INF/spring.factories</resource>
|
||||
</transformer>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
|
||||
<resource>META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports</resource>
|
||||
</transformer>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
|
||||
<resource>META-INF/spring/org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration.imports</resource>
|
||||
</transformer>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
|
||||
<resource>META-INF/spring.schemas</resource>
|
||||
</transformer>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
|
||||
<resource>META-INF/spring.components</resource>
|
||||
</transformer>
|
||||
</transformers>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
----
|
||||
|
||||
[[build-file-setup]]
|
||||
== Build file setup
|
||||
|
||||
In order to run Spring Cloud Function applications on AWS Lambda, you can leverage Maven or Gradle
|
||||
plugins offered by the cloud platform provider.
|
||||
|
||||
|
||||
[[maven]]
|
||||
=== Maven
|
||||
|
||||
In order to use the adapter plugin for Maven, add the plugin dependency to your `pom.xml`
|
||||
file:
|
||||
|
||||
[source,xml]
|
||||
----
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-function-adapter-aws</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
----
|
||||
|
||||
As pointed out in the <<Notes on JAR Layout>>, you will need a shaded jar in order to upload it
|
||||
to AWS Lambda. You can use the https://maven.apache.org/plugins/maven-shade-plugin/[Maven Shade Plugin] for that.
|
||||
The example of the xref:adapters/aws-intro.adoc#shade-plugin-setup[setup] can be found above.
|
||||
|
||||
You can use the Spring Boot Maven Plugin to generate the <<thin-jar>>.
|
||||
[source,xml]
|
||||
----
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot.experimental</groupId>
|
||||
<artifactId>spring-boot-thin-layout</artifactId>
|
||||
<version>${wrapper.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</plugin>
|
||||
----
|
||||
|
||||
You can find the entire sample `pom.xml` file for deploying Spring Cloud Function
|
||||
applications to AWS Lambda with Maven https://github.com/spring-cloud/spring-cloud-function/blob/{branch}/spring-cloud-function-samples/function-sample-aws/pom.xml[here].
|
||||
|
||||
[[gradle]]
|
||||
=== Gradle
|
||||
|
||||
In order to use the adapter plugin for Gradle, add the dependency to your `build.gradle` file:
|
||||
|
||||
[source,groovy]
|
||||
----
|
||||
|
||||
dependencies {
|
||||
compile("org.springframework.cloud:spring-cloud-function-adapter-aws:${version}")
|
||||
}
|
||||
----
|
||||
|
||||
As pointed out in <<Notes on JAR Layout>>, you will need a shaded jar in order to upload it
|
||||
to AWS Lambda. You can use the https://plugins.gradle.org/plugin/com.github.johnrengelman.shadow/[Gradle Shadow Plugin] for that:
|
||||
|
||||
[source,groovy]
|
||||
----
|
||||
buildscript {
|
||||
dependencies {
|
||||
classpath "com.github.jengelman.gradle.plugins:shadow:${shadowPluginVersion}"
|
||||
}
|
||||
}
|
||||
apply plugin: 'com.github.johnrengelman.shadow'
|
||||
|
||||
assemble.dependsOn = [shadowJar]
|
||||
|
||||
import com.github.jengelman.gradle.plugins.shadow.transformers.*
|
||||
|
||||
shadowJar {
|
||||
classifier = 'aws'
|
||||
dependencies {
|
||||
exclude(
|
||||
dependency("org.springframework.cloud:spring-cloud-function-web:${springCloudFunctionVersion}"))
|
||||
}
|
||||
// Required for Spring
|
||||
mergeServiceFiles()
|
||||
append 'META-INF/spring.handlers'
|
||||
append 'META-INF/spring.schemas'
|
||||
append 'META-INF/spring.tooling'
|
||||
append 'META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports'
|
||||
append 'META-INF/spring/org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration.imports'
|
||||
transform(PropertiesFileTransformer) {
|
||||
paths = ['META-INF/spring.factories']
|
||||
mergeStrategy = "append"
|
||||
}
|
||||
}
|
||||
|
||||
----
|
||||
|
||||
You can use the Spring Boot Gradle Plugin and Spring Boot Thin Gradle Plugin to generate
|
||||
the <<thin-jar>>.
|
||||
|
||||
[source,groovy]
|
||||
----
|
||||
buildscript {
|
||||
dependencies {
|
||||
classpath("org.springframework.boot.experimental:spring-boot-thin-gradle-plugin:${wrapperVersion}")
|
||||
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
|
||||
}
|
||||
}
|
||||
apply plugin: 'org.springframework.boot'
|
||||
apply plugin: 'org.springframework.boot.experimental.thin-launcher'
|
||||
assemble.dependsOn = [thinJar]
|
||||
----
|
||||
|
||||
You can find the entire sample `build.gradle` file for deploying Spring Cloud Function
|
||||
applications to AWS Lambda with Gradle https://github.com/spring-cloud/spring-cloud-function/blob/{branch}/spring-cloud-function-samples/function-sample-aws/build.gradle[here].
|
||||
|
||||
[[upload]]
|
||||
== Upload
|
||||
|
||||
Build the sample under `spring-cloud-function-samples/function-sample-aws` and upload the `-aws` jar file to Lambda. The handler can be `example.Handler` or `org.springframework.cloud.function.adapter.aws.SpringBootStreamHandler` (FQN of the class, _not_ a method reference, although Lambda does accept method references).
|
||||
|
||||
----
|
||||
./mvnw -U clean package
|
||||
----
|
||||
|
||||
Using the AWS command line tools it looks like this:
|
||||
|
||||
----
|
||||
aws lambda create-function --function-name Uppercase --role arn:aws:iam::[USERID]:role/service-role/[ROLE] --zip-file fileb://function-sample-aws/target/function-sample-aws-2.0.0.BUILD-SNAPSHOT-aws.jar --handler org.springframework.cloud.function.adapter.aws.SpringBootStreamHandler --description "Spring Cloud Function Adapter Example" --runtime java8 --region us-east-1 --timeout 30 --memory-size 1024 --publish
|
||||
----
|
||||
|
||||
The input type for the function in the AWS sample is a Foo with a single property called "value". So you would need this to test it:
|
||||
|
||||
----
|
||||
{
|
||||
"value": "test"
|
||||
}
|
||||
----
|
||||
|
||||
NOTE: The AWS sample app is written in the "functional" style (as an `ApplicationContextInitializer`). This is much faster on startup in Lambda than the traditional `@Bean` style, so if you don't need `@Beans` (or `@EnableAutoConfiguration`) it's a good choice. Warm starts are not affected.
|
||||
|
||||
|
||||
[[type-conversion]]
|
||||
== Type Conversion
|
||||
|
||||
Spring Cloud Function will attempt to transparently handle type conversion between the raw
|
||||
input stream and types declared by your function.
|
||||
|
||||
For example, if your function signature is as such `Function<Foo, Bar>` we will attempt to convert
|
||||
incoming stream event to an instance of `Foo`.
|
||||
|
||||
In the event type is not known or can not be determined (e.g., `Function<?, ?>`) we will attempt to
|
||||
convert an incoming stream event to a generic `Map`.
|
||||
|
||||
[[raw-input]]
|
||||
==== Raw Input
|
||||
|
||||
There are times when you may want to have access to a raw input. In this case all you need is to declare your
|
||||
function signature to accept `InputStream`. For example, `Function<InputStream, ?>`. In this case
|
||||
we will not attempt any conversion and will pass the raw input directly to a function.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
*{project-version}*
|
||||
|
||||
|
||||
The https://aws.amazon.com/[AWS] adapter takes a Spring Cloud Function app and converts it to a form that can run in AWS Lambda.
|
||||
|
||||
[[introduction]]
|
||||
= Introduction
|
||||
|
||||
|
||||
[[functional-bean-definitions]]
|
||||
= Functional Bean Definitions
|
||||
|
||||
Your functions will start much quicker if you can use functional bean definitions instead of `@Bean`. To do this make your main class
|
||||
an `ApplicationContextInitializer<GenericApplicationContext>` and use the `registerBean()` methods in `GenericApplicationContext` to
|
||||
create all the beans you need. You function need to be registered as a bean of type `FunctionRegistration` so that the input and
|
||||
output types can be accessed by the framework. There is an example in github (the AWS sample is written in this style). It would
|
||||
look something like this:
|
||||
|
||||
```java
|
||||
@SpringBootConfiguration
|
||||
public class FuncApplication implements ApplicationContextInitializer<GenericApplicationContext> {
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
FunctionalSpringApplication.run(FuncApplication.class, args);
|
||||
}
|
||||
|
||||
public Function<Foo, Bar> function() {
|
||||
return value -> new Bar(value.uppercase()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(GenericApplicationContext context) {
|
||||
context.registerBean("function", FunctionRegistration.class,
|
||||
() -> new FunctionRegistration<Function<Foo, Bar>>(function())
|
||||
.type(FunctionTypeUtils.functionType(Foo.class, Bar.class)));
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
[[aws-context]]
|
||||
= AWS Context
|
||||
|
||||
In a typical implementation of AWS Handler user has access to AWS _context_ object. With function approach you can have the same experience if you need it.
|
||||
Upon each invocation the framework will add `aws-context` message header containing the AWS _context_ instance for that particular invocation. So if you need to access it
|
||||
you can simply have `Message<YourPojo>` as an input parameter to your function and then access `aws-context` from message headers.
|
||||
For convenience we provide AWSLambdaUtils.AWS_CONTEXT constant.
|
||||
|
||||
[[platform-specific-features]]
|
||||
= Platform Specific Features
|
||||
|
||||
[[http-and-api-gateway]]
|
||||
== HTTP and API Gateway
|
||||
|
||||
AWS has some platform-specific data types, including batching of messages, which is much more efficient than processing each one individually. To make use of these types you can write a function that depends on those types. Or you can rely on Spring to extract the data from the AWS types and convert it to a Spring `Message`. To do this you tell AWS that the function is of a specific generic handler type (depending on the AWS service) and provide a bean of type `Function<Message<S>,Message<T>>`, where `S` and `T` are your business data types. If there is more than one bean of type `Function` you may also need to configure the Spring Boot property `function.name` to be the name of the target bean (e.g. use `FUNCTION_NAME` as an environment variable).
|
||||
|
||||
The supported AWS services and generic handler types are listed below:
|
||||
|
||||
|===
|
||||
| Service | AWS Types | Generic Handler |
|
||||
|
||||
| API Gateway | `APIGatewayProxyRequestEvent`, `APIGatewayProxyResponseEvent` | `org.springframework.cloud.function.adapter.aws.SpringBootApiGatewayRequestHandler` |
|
||||
| Kinesis | KinesisEvent | org.springframework.cloud.function.adapter.aws.SpringBootKinesisEventHandler |
|
||||
|===
|
||||
|
||||
|
||||
For example, to deploy behind an API Gateway, use `--handler org.springframework.cloud.function.adapter.aws.SpringBootApiGatewayRequestHandler` in your AWS command line (in via the UI) and define a `@Bean` of type `Function<Message<Foo>,Message<Bar>>` where `Foo` and `Bar` are POJO types (the data will be marshalled and unmarshalled by AWS using Jackson).
|
||||
|
||||
[[custom-runtime]]
|
||||
= Custom Runtime
|
||||
|
||||
You can also benefit from https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html[AWS Lambda custom runtime] feature of AWS Lambda
|
||||
and Spring Cloud Function provides all the necessary components to make it easy.
|
||||
|
||||
From the code perspective the application should look no different then any other Spring Cloud Function application.
|
||||
The only thing you need to do is to provide a `bootstrap` script in the root of your zip/jar that runs the Spring Boot application.
|
||||
and select "Custom Runtime" when creating a function in AWS.
|
||||
Here is an example 'bootstrap' file:
|
||||
```text
|
||||
#!/bin/sh
|
||||
|
||||
cd ${LAMBDA_TASK_ROOT:-.}
|
||||
|
||||
java -Dspring.main.web-application-type=none -Dspring.jmx.enabled=false \
|
||||
-noverify -XX:TieredStopAtLevel=1 -Xss256K -XX:MaxMetaspaceSize=128M \
|
||||
-Djava.security.egd=file:/dev/./urandom \
|
||||
-cp .:`echo lib/*.jar | tr ' ' :` com.example.LambdaApplication
|
||||
```
|
||||
The `com.example.LambdaApplication` represents your application which contains function beans.
|
||||
|
||||
Set the handler name in AWS to the name of your function. You can use function composition here as well (e.g., `uppecrase|reverse`).
|
||||
That is pretty much all. Once you upload your zip/jar to AWS your function will run in custom runtime.
|
||||
We provide a https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-samples/function-sample-aws-custom-new[sample project]
|
||||
where you can also see how to configure yoru POM to properly generate the zip file.
|
||||
|
||||
The functional bean definition style works for custom runtimes as well, and is
|
||||
faster than the `@Bean` style. A custom runtime can start up much quicker even than a functional bean implementation
|
||||
of a Java lambda - it depends mostly on the number of classes you need to load at runtime.
|
||||
Spring doesn't do very much here, so you can reduce the cold start time by only using primitive types in your function, for instance,
|
||||
and not doing any work in custom `@PostConstruct` initializers.
|
||||
@@ -1,548 +0,0 @@
|
||||
:branch: master
|
||||
|
||||
[[microsoft-azure-functions]]
|
||||
= Microsoft Azure Functions
|
||||
:sectnums:
|
||||
|
||||
https://azure.microsoft.com[Azure] function adapter for deploying `Spring Cloud Function` applications as native Azure Java Functions.
|
||||
|
||||
The `Azure Functions` https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-java[programming model] relays, extensively, on Java https://learn.microsoft.com/en-us/java/api/com.microsoft.azure.functions.annotation?view=azure-java-stable[annotations] for defining the function's handler methods and their input and output types.
|
||||
At compile time the annotated classes are processed by the provided Azure Maven/Gradle plugins to generate the necessary Azure Function binding files, configurations and package artifacts.
|
||||
The Azure annotations are just a type-safe way to configure your java function to be recognized as Azure function.
|
||||
|
||||
The https://github.com/spring-cloud/spring-cloud-function/tree/main/spring-cloud-function-adapters/spring-cloud-function-adapter-azure[spring-cloud-function-adapter-azure] extends the basic programming model to provide Spring and Spring Cloud Function support.
|
||||
With the adapter you can build your Spring Cloud Function application using dependency injections and then auto-wire the necessary services into your Azure handler methods.
|
||||
|
||||
image::../images/scf-azure-adapter.svg[width=800,scaledwidth="75%",align="center"]
|
||||
|
||||
TIP: For Web-based function applications, you can replace the generic `adapter-azure` with the specialized https://github.com/spring-cloud/spring-cloud-function/tree/main/spring-cloud-function-adapters/spring-cloud-function-adapter-azure-web[spring-cloud-function-adapter-azure-web].
|
||||
With the Azure Web Adapter you can deploy any Spring Web application as an Azure, HttpTrigger, function.
|
||||
This adapter hides the Azure annotations complexity and uses the familiar https://docs.spring.io/spring-boot/docs/current/reference/html/web.html[Spring Web] programming model instead.
|
||||
For further information follow the xref:adapters/azure-intro.adoc#azure.web.adapter[Azure Web Adapter] section below.
|
||||
|
||||
[[azure-adapter]]
|
||||
= Azure Adapter
|
||||
|
||||
Provides `Spring` & `Spring Cloud Function` integration for Azure Functions.
|
||||
|
||||
[[dependencies]]
|
||||
== Dependencies
|
||||
|
||||
In order to enable the Azure Function integration add the azure adapter dependency to your `pom.xml` or `build.gradle`
|
||||
files:
|
||||
|
||||
====
|
||||
[source,xml,indent=0,subs="verbatim,attributes",role="primary"]
|
||||
.Maven
|
||||
----
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-function-adapter-azure</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
----
|
||||
|
||||
[source,groovy,indent=0,subs="verbatim,attributes",role="secondary"]
|
||||
.Gradle
|
||||
----
|
||||
dependencies {
|
||||
implementation 'org.springframework.cloud:spring-cloud-function-adapter-azure'
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
NOTE: version `4.0.0+` is required. Having the adapter on the classpath activates the Azure Java Worker integration.
|
||||
|
||||
[[azure.development.guidelines]]
|
||||
== Development Guidelines
|
||||
|
||||
Use the `@Component` (or `@Service`) annotation to turn any exiting Azure Function class (e.g. with `@FunctionName` handlers) into a Spring component.
|
||||
Then you can auto-wire the required dependencies (or the xref:spring-cloud-function/programming-model.adoc#function.catalog[Function Catalog] for Spring Cloud Function composition) and use those inside the Azure function handlers.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Component // <1>
|
||||
public class MyAzureFunction {
|
||||
|
||||
// Plain Spring bean - not a Spring Cloud Functions!
|
||||
@Autowired private Function<String, String> uppercase; // <2>
|
||||
|
||||
// The FunctionCatalog leverages the Spring Cloud Function framework.
|
||||
@Autowired private FunctionCatalog functionCatalog; // <2>
|
||||
|
||||
@FunctionName("spring") // <3>
|
||||
public String plainBean( // <4>
|
||||
@HttpTrigger(name = "req", authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage<Optional<String>> request,
|
||||
ExecutionContext context) {
|
||||
|
||||
return this.uppercase.apply(request.getBody().get());
|
||||
}
|
||||
|
||||
@FunctionName("scf") // <3>
|
||||
public String springCloudFunction( // <5>
|
||||
@HttpTrigger(name = "req", authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage<Optional<String>> request,
|
||||
ExecutionContext context) {
|
||||
|
||||
// Use SCF composition. Composed functions are not just spring beans but SCF such.
|
||||
Function composed = this.functionCatalog.lookup("reverse|uppercase"); // <6>
|
||||
|
||||
return (String) composed.apply(request.getBody().get());
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
<1> Indicates that the `MyAzureFunction` class is a "component" to be considered by the Spring Framework as a candidate for auto-detection and classpath scanning.
|
||||
<2> Auto-wire the `uppercase` and `functionCatalog` beans defined in the `HttpTriggerDemoApplication` (below).
|
||||
<3> The https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-java?tabs=bash%2Cconsumption#java-function-basics[@FunctionName] annotation identifies the designated Azure function handlers.
|
||||
When invoked by a trigger (such as `@HttpTrigger`), functions process that trigger, and any other inputs, to produce one or more outputs.
|
||||
<4> The `plainBean` method handler is mapped to an Azure function that uses of the auto-wired `uppercase` spring bean to compute the result.
|
||||
It demonstrates how to use "plain" Spring components in your Azure handlers.
|
||||
<5> The `springCloudFunction` method handler is mapped to another Azure function, that uses the auto-wired `FunctionCatalog` instance to compute the result.
|
||||
<6> Shows how to leverage the Spring Cloud Function xref:spring-cloud-function/programming-model.adoc#function.catalog[Function Catalog] composition API.
|
||||
|
||||
TIP: Use the Java annotations included in the https://learn.microsoft.com/en-us/java/api/com.microsoft.azure.functions.annotation?view=azure-java-stable[com.microsoft.azure.functions.annotation.*] package to bind input and outputs to your methods.
|
||||
|
||||
The implementation of the business logic used inside the Azure handlers looks like a common Spring application:
|
||||
|
||||
[[HttpTriggerDemoApplication]]
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@SpringBootApplication // <1>
|
||||
public class HttpTriggerDemoApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(HttpTriggerDemoApplication.class, args);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Function<String, String> uppercase() { // <2>
|
||||
return payload -> payload.toUpperCase();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Function<String, String> reverse() { // <2>
|
||||
return payload -> new StringBuilder(payload).reverse().toString();
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> The `@SpringBootApplication` annotated class is used as a `Main-Class` as explained in xref:adapters/azure-intro.adoc#star-class-configuration[main class configuration].
|
||||
<2> Functions auto-wired and used in the Azure function handlers.
|
||||
|
||||
[[function-catalog]]
|
||||
=== Function Catalog
|
||||
|
||||
The Spring Cloud Function supports a range of type signatures for user-defined functions, while providing a consistent execution model.
|
||||
For this it uses the xref:spring-cloud-function/programming-model.adoc#function.catalog[Function Catalog] to transform all user defined functions into a canonical representation.
|
||||
|
||||
The Azure adapter can auto-wire any Spring component, such as the `uppercase` above.
|
||||
But those are treated as plain Java class instances, not as a canonical Spring Cloud Functions!
|
||||
|
||||
To leverage Spring Cloud Function and have access to the canonical function representations, you need to auto-wire the `FunctionCatalog` and use it in your handler, like the `functionCatalog` instance the `springCloudFunction()` handler above.
|
||||
|
||||
[[accessing-azure-executioncontext]]
|
||||
=== Accessing Azure ExecutionContext
|
||||
|
||||
Some time there is a need to access the target execution context provided by the Azure runtime in the form of `com.microsoft.azure.functions.ExecutionContext`.
|
||||
For example one of such needs is logging, so it can appear in the Azure console.
|
||||
|
||||
For that purpose the `AzureFunctionUtil.enhanceInputIfNecessary` allow you to add an instance of the `ExecutionContext` as a Message header so you can retrieve it via `executionContext` key.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@FunctionName("myfunction")
|
||||
public String execute(
|
||||
@HttpTrigger(name = "req", authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage<Optional<String>> request,
|
||||
ExecutionContext context) {
|
||||
|
||||
Message message =
|
||||
(Message) AzureFunctionUtil.enhanceInputIfNecessary(request.getBody().get(), context); // <1>
|
||||
|
||||
return this.uppercase.apply(message);
|
||||
}
|
||||
----
|
||||
<1> Leverages the `AzureFunctionUtil` utility to inline the `context` as message header using the `AzureFunctionUtil.EXECUTION_CONTEXT` header key.
|
||||
|
||||
Now you can retrieve the ExecutionContext from message headers:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
public Function<Message<String>, String> uppercase(JsonMapper mapper) {
|
||||
return message -> {
|
||||
String value = message.getPayload();
|
||||
ExecutionContext context =
|
||||
(ExecutionContext) message.getHeaders().get(AzureFunctionUtil.EXECUTION_CONTEXT); // <1>
|
||||
. . .
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Retrieve the ExecutionContext instance from the header.
|
||||
|
||||
|
||||
[[azure.configuration]]
|
||||
== Configuration
|
||||
|
||||
To run your function applications on Microsoft Azure, you have to provide the necessary configurations, such as `function.json` and `host.json`, and adhere to the compulsory https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-java?tabs=bash%2Cconsumption#folder-structure[packaging format].
|
||||
|
||||
Usually the Azure Maven (or Gradle) plugins are used to generate the necessary configurations from the annotated classes and to produce the required package format.
|
||||
|
||||
IMPORTANT: The Azure https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-java?tabs=bash%2Cconsumption#folder-structure[packaging format] is not compatible with the default Spring Boot packaging (e.g. `uber jar`).
|
||||
The xref:adapters/azure-intro.adoc#disable.spring.boot.plugin[Disable Spring Boot Plugin] section below explains how to handle this.
|
||||
|
||||
[[azure-maven/gradle-plugins]]
|
||||
=== Azure Maven/Gradle Plugins
|
||||
|
||||
Azure provides https://github.com/microsoft/azure-maven-plugins/tree/develop/azure-functions-maven-plugin[Maven] and https://github.com/microsoft/azure-gradle-plugins/tree/master/azure-functions-gradle-plugin[Gradle] plugins to process the annotated classes, generate the necessary configurations and produce the expected package layout.
|
||||
Plugins are used to set the platform, runtime and app-settings properties like this:
|
||||
|
||||
====
|
||||
[source,xml,indent=0,subs="verbatim,attributes",role="primary"]
|
||||
.Maven
|
||||
----
|
||||
<plugin>
|
||||
<groupId>com.microsoft.azure</groupId>
|
||||
<artifactId>azure-functions-maven-plugin</artifactId>
|
||||
<version>1.22.0 or higher</version>
|
||||
|
||||
<configuration>
|
||||
<appName>YOUR-AZURE-FUNCTION-APP-NAME</appName>
|
||||
<resourceGroup>YOUR-AZURE-FUNCTION-RESOURCE-GROUP</resourceGroup>
|
||||
<region>YOUR-AZURE-FUNCTION-APP-REGION</region>
|
||||
<appServicePlanName>YOUR-AZURE-FUNCTION-APP-SERVICE-PLANE-NAME</appServicePlanName>
|
||||
<pricingTier>YOUR-AZURE-FUNCTION-PRICING-TIER</pricingTier>
|
||||
|
||||
<hostJson>${project.basedir}/src/main/resources/host.json</hostJson>
|
||||
|
||||
<runtime>
|
||||
<os>linux</os>
|
||||
<javaVersion>11</javaVersion>
|
||||
</runtime>
|
||||
|
||||
<appSettings>
|
||||
<property>
|
||||
<name>FUNCTIONS_EXTENSION_VERSION</name>
|
||||
<value>~4</value>
|
||||
</property>
|
||||
</appSettings>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>package-functions</id>
|
||||
<goals>
|
||||
<goal>package</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
----
|
||||
|
||||
[source,groovy,indent=0,subs="verbatim,attributes",role="secondary"]
|
||||
.Gradle
|
||||
----
|
||||
plugins {
|
||||
id "com.microsoft.azure.azurefunctions" version "1.11.0"
|
||||
// ...
|
||||
}
|
||||
|
||||
apply plugin: "com.microsoft.azure.azurefunctions"
|
||||
|
||||
azurefunctions {
|
||||
appName = 'YOUR-AZURE-FUNCTION-APP-NAME'
|
||||
resourceGroup = 'YOUR-AZURE-FUNCTION-RESOURCE-GROUP'
|
||||
region = 'YOUR-AZURE-FUNCTION-APP-REGION'
|
||||
appServicePlanName = 'YOUR-AZURE-FUNCTION-APP-SERVICE-PLANE-NAME'
|
||||
pricingTier = 'YOUR-AZURE-FUNCTION-APP-SERVICE-PLANE-NAME'
|
||||
|
||||
runtime {
|
||||
os = 'linux'
|
||||
javaVersion = '11'
|
||||
}
|
||||
|
||||
auth {
|
||||
type = 'azure_cli'
|
||||
}
|
||||
|
||||
appSettings {
|
||||
FUNCTIONS_EXTENSION_VERSION = '~4'
|
||||
}
|
||||
// Uncomment to enable local debug
|
||||
// localDebug = "transport=dt_socket,server=y,suspend=n,address=5005"
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
More information about the runtime configurations: https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-java?tabs=bash%2Cconsumption#java-versions[Java Versions], https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-java?tabs=bash%2Cconsumption#specify-the-deployment-os[Deployment OS].
|
||||
|
||||
[[disable.spring.boot.plugin]]
|
||||
=== Disable Spring Boot Plugin
|
||||
|
||||
Expectedly, the Azure Functions run inside the Azure execution runtime, not inside the SpringBoot runtime!
|
||||
Furthermore, Azure expects a specific packaging format, generated by the Azure Maven/Gradle plugins, that is not compatible with the default Spring Boot packaging.
|
||||
|
||||
You have to either disable the SpringBoot Maven/Gradle plugin or use the https://github.com/dsyer/spring-boot-thin-launcher[Spring Boot Thin Launcher] as shown in this Maven snippet:
|
||||
|
||||
[source,xml]
|
||||
----
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot.experimental</groupId>
|
||||
<artifactId>spring-boot-thin-layout</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</plugin>
|
||||
----
|
||||
|
||||
[[star-class-configuration]]
|
||||
=== Main-Class Configuration
|
||||
|
||||
Specify the `Main-Class`/`Start-Class` to point to your Spring application entry point, such as the xref:adapters/azure-intro.adoc#HttpTriggerDemoApplication[HttpTriggerDemoApplication] class in the example above.
|
||||
|
||||
You can use the Maven `start-class` property or set the `Main-Class` attribute of your `MANIFEST/META-INFO`:
|
||||
|
||||
====
|
||||
[source,xml,indent=0,subs="verbatim,attributes",role="primary"]
|
||||
.Maven
|
||||
----
|
||||
<properties>
|
||||
<start-class>YOUR APP MAIN CLASS</start-class>
|
||||
...
|
||||
</properties>
|
||||
----
|
||||
|
||||
[source,groovy,indent=0,subs="verbatim,attributes",role="secondary"]
|
||||
.Gradle
|
||||
----
|
||||
jar {
|
||||
manifest {
|
||||
attributes(
|
||||
"Main-Class": "YOUR-APP-MAIN-CLASS"
|
||||
)
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
TIP: Alternatively you can use the `MAIN_CLASS` environment variable to set the class name explicitly.
|
||||
For local runs, add the `MAIN_CLASS` variable to your `local.settings.json` file and for Azure portal deployment set the variable in the https://learn.microsoft.com/en-us/azure/azure-functions/functions-how-to-use-azure-function-app-settings?tabs=portal#get-started-in-the-azure-portal[App Settings].
|
||||
|
||||
|
||||
IMPORTANT: If the `MAIN_CLASS` variable is not set, the Azure adapter lookups the `MANIFEST/META-INFO` attributes from the jars found on the classpath and selects the first `Main-Class:` annotated with either a `@SpringBootApplication` or `@SpringBootConfiguration` annotation.
|
||||
|
||||
[[metadata-configuration]]
|
||||
=== Metadata Configuration
|
||||
|
||||
You can use a shared https://learn.microsoft.com/en-us/azure/azure-functions/functions-host-json[host.json] file to configure the function app.
|
||||
|
||||
[source,json]
|
||||
----
|
||||
{
|
||||
"version": "2.0",
|
||||
"extensionBundle": {
|
||||
"id": "Microsoft.Azure.Functions.ExtensionBundle",
|
||||
"version": "[4.*, 5.0.0)"
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
The host.json metadata file contains configuration options that affect all functions in a function app instance.
|
||||
|
||||
TIP: If the file is not in the project top folder you need to configure your plugins accordingly (like `hostJson` maven attribute).
|
||||
|
||||
[[samples]]
|
||||
== Samples
|
||||
|
||||
Here is a list of various Spring Cloud Function Azure Adapter samples you can explore:
|
||||
|
||||
- https://github.com/spring-cloud/spring-cloud-function/tree/main/spring-cloud-function-samples/function-sample-azure-http-trigger[Http Trigger (Maven)]
|
||||
- https://github.com/spring-cloud/spring-cloud-function/tree/main/spring-cloud-function-samples/function-sample-azure-http-trigger-gradle[Http Trigger (Gradle)]
|
||||
- https://github.com/spring-cloud/spring-cloud-function/tree/main/spring-cloud-function-samples/function-sample-azure-blob-trigger[Blob Trigger (Maven)]
|
||||
- https://github.com/spring-cloud/spring-cloud-function/tree/main/spring-cloud-function-samples/function-sample-azure-timer-trigger[Timer Trigger (Maven)]
|
||||
- https://github.com/spring-cloud/spring-cloud-function/tree/main/spring-cloud-function-samples/function-sample-azure-kafka-trigger[ Kafka Trigger & Output Binding (Maven)].
|
||||
|
||||
[[azure.web.adapter]]
|
||||
= Azure Web Adapter
|
||||
|
||||
For, pure, Web-based function applications, you can replace the generic `adapter-azure` with the specialized https://github.com/spring-cloud/spring-cloud-function/tree/main/spring-cloud-function-adapters/spring-cloud-function-adapter-azure-web[spring-cloud-function-adapter-azure-web].
|
||||
The Azure Web Adapter can deploy any Spring Web application as a native Azure function, using the HttpTrigger internally.
|
||||
It hides the Azure annotations complexity and relies on the familiar https://docs.spring.io/spring-boot/docs/current/reference/html/web.html[Spring Web] programming model instead.
|
||||
|
||||
To enable the Azure Web Adapter, add the adapter dependency to your `pom.xml` or `build.gradle` files:
|
||||
|
||||
====
|
||||
[source,xml,indent=0,subs="verbatim,attributes",role="primary"]
|
||||
.Maven
|
||||
----
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-function-adapter-azure-web</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
----
|
||||
|
||||
[source,groovy,indent=0,subs="verbatim,attributes",role="secondary"]
|
||||
.Gradle
|
||||
----
|
||||
dependencies {
|
||||
implementation 'org.springframework.cloud:spring-cloud-function-adapter-azure-web'
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
The same xref:adapters/azure-intro.adoc#azure.configuration[Configuration] and xref:adapters/azure-intro.adoc#azure.usage[Usage] instructions apply to the `Azure Web Adapter` as well.
|
||||
|
||||
|
||||
[[samples]]
|
||||
== Samples
|
||||
|
||||
For further information, explore the following, Azure Web Adapter, sample:
|
||||
|
||||
- https://github.com/spring-cloud/spring-cloud-function/tree/main/spring-cloud-function-samples/function-sample-azure-web[ Azure Web Adapter (Maven)].
|
||||
|
||||
[[azure.usage]]
|
||||
= Usage
|
||||
|
||||
Common instructions for building and deploying both, `Azure Adapter` and `Azure Web Adapter` type of applications.
|
||||
|
||||
[[build]]
|
||||
== Build
|
||||
|
||||
====
|
||||
[source,xml,indent=0,subs="verbatim,attributes",role="primary"]
|
||||
.Maven
|
||||
----
|
||||
./mvnw -U clean package
|
||||
----
|
||||
|
||||
[source,groovy,indent=0,subs="verbatim,attributes",role="secondary"]
|
||||
.Gradle
|
||||
----
|
||||
./gradlew azureFunctionsPackage
|
||||
----
|
||||
====
|
||||
|
||||
[[running-locally]]
|
||||
== Running locally
|
||||
|
||||
To run locally on top of `Azure Functions`, and to deploy to your live Azure environment, you will need `Azure Functions Core Tools` installed along with the Azure CLI (see https://docs.microsoft.com/en-us/azure/azure-functions/create-first-function-cli-java?tabs=bash%2Cazure-cli%2Cbrowser#configure-your-local-environment[here]).
|
||||
For some configuration you would need the https://learn.microsoft.com/en-us/azure/storage/common/storage-use-emulator[Azurite emulator] as well.
|
||||
|
||||
Then run the sample:
|
||||
|
||||
====
|
||||
[source,xml,indent=0,subs="verbatim,attributes",role="primary"]
|
||||
.Maven
|
||||
----
|
||||
./mvnw azure-functions:run
|
||||
----
|
||||
|
||||
[source,groovy,indent=0,subs="verbatim,attributes",role="secondary"]
|
||||
.Gradle
|
||||
----
|
||||
./gradlew azureFunctionsRun
|
||||
----
|
||||
====
|
||||
|
||||
[[running-on-azure]]
|
||||
== Running on Azure
|
||||
|
||||
Make sure you are logged in your Azure account.
|
||||
|
||||
----
|
||||
az login
|
||||
----
|
||||
|
||||
and deploy
|
||||
|
||||
====
|
||||
[source,xml,indent=0,subs="verbatim,attributes",role="primary"]
|
||||
.Maven
|
||||
----
|
||||
./mvnw azure-functions:deploy
|
||||
----
|
||||
|
||||
[source,groovy,indent=0,subs="verbatim,attributes",role="secondary"]
|
||||
.Gradle
|
||||
----
|
||||
./gradlew azureFunctionsDeploy
|
||||
----
|
||||
====
|
||||
|
||||
[[debug-locally]]
|
||||
== Debug locally
|
||||
|
||||
Run the function in debug mode.
|
||||
|
||||
====
|
||||
[source,xml,indent=0,subs="verbatim,attributes",role="primary"]
|
||||
.Maven
|
||||
----
|
||||
./mvnw azure-functions:run -DenableDebug
|
||||
----
|
||||
[source,groovy,indent=0,subs="verbatim,attributes",role="secondary"]
|
||||
.Gradle
|
||||
|
||||
----
|
||||
// If you want to debug your functions, please add the following line
|
||||
// to the azurefunctions section of your build.gradle.
|
||||
azurefunctions {
|
||||
...
|
||||
localDebug = "transport=dt_socket,server=y,suspend=n,address=5005"
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
Alternatively and the `JAVA_OPTS` value to your `local.settings.json` like this:
|
||||
|
||||
[source,json]
|
||||
----
|
||||
{
|
||||
"IsEncrypted": false,
|
||||
"Values": {
|
||||
...
|
||||
"FUNCTIONS_WORKER_RUNTIME": "java",
|
||||
"JAVA_OPTS": "-Djava.net.preferIPv4Stack=true -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=127.0.0.1:5005"
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Here is snippet for a `VSCode` remote debugging configuration:
|
||||
|
||||
[source,json]
|
||||
----
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "java",
|
||||
"name": "Attach to Remote Program",
|
||||
"request": "attach",
|
||||
"hostName": "localhost",
|
||||
"port": "5005"
|
||||
},
|
||||
]
|
||||
}
|
||||
----
|
||||
|
||||
[[functioninvoker-deprecated]]
|
||||
= FunctionInvoker (deprecated)
|
||||
|
||||
WARNING: The legacy `FunctionInvoker` programming model is deprecated and will not be supported going forward.
|
||||
|
||||
For additional documentation and samples about the Function Integration approach follow the https://github.com/spring-cloud/spring-cloud-function/tree/main/spring-cloud-function-samples/function-sample-azure/[azure-sample] README and code.
|
||||
|
||||
[[relevant-links]]
|
||||
= Relevant Links
|
||||
|
||||
- https://learn.microsoft.com/en-us/azure/developer/java/spring-framework/getting-started-with-spring-cloud-function-in-azure[Spring Cloud Function in Azure]
|
||||
- https://spring.io/blog/2023/02/24/spring-cloud-function-for-azure-function[Spring Cloud Function for Azure Function (blog)]
|
||||
- <<spring-cloud-function.adoc#,Spring Cloud Function - Reference Guide>>
|
||||
- https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-java?tabs=bash%2Cconsumption[Azure Functions Java developer guide]
|
||||
- https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference?tabs=blob[Azure Functions developer guide]
|
||||
|
||||
:sectnums!:
|
||||
@@ -1,2 +0,0 @@
|
||||
*{project-version}*
|
||||
|
||||
@@ -1,304 +0,0 @@
|
||||
:branch: master
|
||||
|
||||
[[google-cloud-functions]]
|
||||
= Google Cloud Functions
|
||||
|
||||
The Google Cloud Functions adapter enables Spring Cloud Function apps to run on the https://cloud.google.com/functions[Google Cloud Functions] serverless platform.
|
||||
You can either run the function locally using the open source https://github.com/GoogleCloudPlatform/functions-framework-java[Google Functions Framework for Java] or on GCP.
|
||||
|
||||
[[project-dependencies]]
|
||||
== Project Dependencies
|
||||
|
||||
Start by adding the `spring-cloud-function-adapter-gcp` dependency to your project.
|
||||
|
||||
[source, xml]
|
||||
----
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-function-adapter-gcp</artifactId>
|
||||
</dependency>
|
||||
|
||||
...
|
||||
</dependencies>
|
||||
----
|
||||
|
||||
In addition, add the `spring-boot-maven-plugin` which will build the JAR of the function to deploy.
|
||||
|
||||
NOTE: Notice that we also reference `spring-cloud-function-adapter-gcp` as a dependency of the `spring-boot-maven-plugin`. This is necessary because it modifies the plugin to package your function in the correct JAR format for deployment on Google Cloud Functions.
|
||||
|
||||
[source, xml]
|
||||
----
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<outputDirectory>target/deploy</outputDirectory>
|
||||
</configuration>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-function-adapter-gcp</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</plugin>
|
||||
----
|
||||
|
||||
Finally, add the Maven plugin provided as part of the Google Functions Framework for Java.
|
||||
This allows you to test your functions locally via `mvn function:run`.
|
||||
|
||||
NOTE: The function target should always be set to `org.springframework.cloud.function.adapter.gcp.GcfJarLauncher`; this is an adapter class which acts as the entry point to your Spring Cloud Function from the Google Cloud Functions platform.
|
||||
|
||||
[source,xml]
|
||||
----
|
||||
<plugin>
|
||||
<groupId>com.google.cloud.functions</groupId>
|
||||
<artifactId>function-maven-plugin</artifactId>
|
||||
<version>0.9.1</version>
|
||||
<configuration>
|
||||
<functionTarget>org.springframework.cloud.function.adapter.gcp.GcfJarLauncher</functionTarget>
|
||||
<port>8080</port>
|
||||
</configuration>
|
||||
</plugin>
|
||||
----
|
||||
|
||||
A full example of a working `pom.xml` can be found in the https://github.com/spring-cloud/spring-cloud-function/blob/master/spring-cloud-function-samples/function-sample-gcp-http/pom.xml[Spring Cloud Functions GCP sample].
|
||||
|
||||
[[http-functions]]
|
||||
== HTTP Functions
|
||||
|
||||
Google Cloud Functions supports deploying https://cloud.google.com/functions/docs/writing/http[HTTP Functions], which are functions that are invoked by HTTP request. The sections below describe instructions for deploying a Spring Cloud Function as an HTTP Function.
|
||||
|
||||
[[getting-started]]
|
||||
=== Getting Started
|
||||
|
||||
Let’s start with a simple Spring Cloud Function example:
|
||||
|
||||
[source, java]
|
||||
----
|
||||
@SpringBootApplication
|
||||
public class CloudFunctionMain {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(CloudFunctionMain.class, args);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Function<String, String> uppercase() {
|
||||
return value -> value.toUpperCase();
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Specify your configuration main class in `resources/META-INF/MANIFEST.MF`.
|
||||
|
||||
[source]
|
||||
----
|
||||
Main-Class: com.example.CloudFunctionMain
|
||||
----
|
||||
|
||||
Then run the function locally.
|
||||
This is provided by the Google Cloud Functions `function-maven-plugin` described in the project dependencies section.
|
||||
|
||||
----
|
||||
mvn function:run
|
||||
----
|
||||
|
||||
Invoke the HTTP function:
|
||||
|
||||
----
|
||||
curl http://localhost:8080/ -d "hello"
|
||||
----
|
||||
|
||||
[[deploy-to-gcp]]
|
||||
=== Deploy to GCP
|
||||
|
||||
Start by packaging your application.
|
||||
|
||||
----
|
||||
mvn package
|
||||
----
|
||||
|
||||
If you added the custom `spring-boot-maven-plugin` plugin defined above, you should see the resulting JAR in `target/deploy` directory.
|
||||
This JAR is correctly formatted for deployment to Google Cloud Functions.
|
||||
|
||||
Next, make sure that you have the https://cloud.google.com/sdk/install[Cloud SDK CLI] installed.
|
||||
|
||||
From the project base directory run the following command to deploy.
|
||||
|
||||
----
|
||||
gcloud functions deploy function-sample-gcp-http \
|
||||
--entry-point org.springframework.cloud.function.adapter.gcp.GcfJarLauncher \
|
||||
--runtime java11 \
|
||||
--trigger-http \
|
||||
--source target/deploy \
|
||||
--memory 512MB
|
||||
----
|
||||
|
||||
Invoke the HTTP function:
|
||||
|
||||
----
|
||||
curl https://REGION-PROJECT_ID.cloudfunctions.net/function-sample-gcp-http -d "hello"
|
||||
----
|
||||
|
||||
|
||||
Setting custom HTTP statusCode:
|
||||
|
||||
----
|
||||
Functions can specify a custom HTTP response code by setting the `FunctionInvoker.HTTP_STATUS_CODE` header.
|
||||
----
|
||||
|
||||
[source, java]
|
||||
----
|
||||
@Bean
|
||||
public Function<String, Message<String>> function() {
|
||||
|
||||
String payload = "hello";
|
||||
|
||||
Message<String> message = MessageBuilder.withPayload(payload).setHeader(FunctionInvoker.HTTP_STATUS_CODE, 404).build();
|
||||
|
||||
return input -> message;
|
||||
};
|
||||
----
|
||||
|
||||
|
||||
|
||||
[[background-functions]]
|
||||
== Background Functions
|
||||
|
||||
Google Cloud Functions also supports deploying https://cloud.google.com/functions/docs/writing/background[Background Functions] which are invoked indirectly in response to an event, such as a message on a https://cloud.google.com/pubsub[Cloud Pub/Sub] topic, a change in a https://cloud.google.com/storage[Cloud Storage] bucket, or a https://firebase.google.com/[Firebase] event.
|
||||
|
||||
The `spring-cloud-function-adapter-gcp` allows for functions to be deployed as background functions as well.
|
||||
|
||||
The sections below describe the process for writing a Cloud Pub/Sub topic background function.
|
||||
However, there are a number of different event types that can trigger a background function to execute which are not discussed here; these are described in the https://cloud.google.com/functions/docs/calling[Background Function triggers documentation].
|
||||
|
||||
[[getting-started]]
|
||||
=== Getting Started
|
||||
|
||||
Let’s start with a simple Spring Cloud Function which will run as a GCF background function:
|
||||
|
||||
[source, java]
|
||||
----
|
||||
@SpringBootApplication
|
||||
public class BackgroundFunctionMain {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(BackgroundFunctionMain.class, args);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Consumer<PubSubMessage> pubSubFunction() {
|
||||
return message -> System.out.println("The Pub/Sub message data: " + message.getData());
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
In addition, create `PubSubMessage` class in the project with the below definition.
|
||||
This class represents the https://cloud.google.com/functions/docs/calling/pubsub#event_structure[Pub/Sub event structure] which gets passed to your function on a Pub/Sub topic event.
|
||||
|
||||
[source, java]
|
||||
----
|
||||
public class PubSubMessage {
|
||||
|
||||
private String data;
|
||||
|
||||
private Map<String, String> attributes;
|
||||
|
||||
private String messageId;
|
||||
|
||||
private String publishTime;
|
||||
|
||||
public String getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public void setData(String data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public Map<String, String> getAttributes() {
|
||||
return attributes;
|
||||
}
|
||||
|
||||
public void setAttributes(Map<String, String> attributes) {
|
||||
this.attributes = attributes;
|
||||
}
|
||||
|
||||
public String getMessageId() {
|
||||
return messageId;
|
||||
}
|
||||
|
||||
public void setMessageId(String messageId) {
|
||||
this.messageId = messageId;
|
||||
}
|
||||
|
||||
public String getPublishTime() {
|
||||
return publishTime;
|
||||
}
|
||||
|
||||
public void setPublishTime(String publishTime) {
|
||||
this.publishTime = publishTime;
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
Specify your configuration main class in `resources/META-INF/MANIFEST.MF`.
|
||||
|
||||
[source]
|
||||
----
|
||||
Main-Class: com.example.BackgroundFunctionMain
|
||||
----
|
||||
|
||||
Then run the function locally.
|
||||
This is provided by the Google Cloud Functions `function-maven-plugin` described in the project dependencies section.
|
||||
|
||||
----
|
||||
mvn function:run
|
||||
----
|
||||
|
||||
Invoke the HTTP function:
|
||||
|
||||
----
|
||||
curl localhost:8080 -H "Content-Type: application/json" -d '{"data":"hello"}'
|
||||
----
|
||||
|
||||
Verify that the function was invoked by viewing the logs.
|
||||
|
||||
[[deploy-to-gcp]]
|
||||
=== Deploy to GCP
|
||||
|
||||
In order to deploy your background function to GCP, first package your application.
|
||||
|
||||
----
|
||||
mvn package
|
||||
----
|
||||
|
||||
If you added the custom `spring-boot-maven-plugin` plugin defined above, you should see the resulting JAR in `target/deploy` directory.
|
||||
This JAR is correctly formatted for deployment to Google Cloud Functions.
|
||||
|
||||
Next, make sure that you have the https://cloud.google.com/sdk/install[Cloud SDK CLI] installed.
|
||||
|
||||
From the project base directory run the following command to deploy.
|
||||
|
||||
----
|
||||
gcloud functions deploy function-sample-gcp-background \
|
||||
--entry-point org.springframework.cloud.function.adapter.gcp.GcfJarLauncher \
|
||||
--runtime java11 \
|
||||
--trigger-topic my-functions-topic \
|
||||
--source target/deploy \
|
||||
--memory 512MB
|
||||
----
|
||||
|
||||
Google Cloud Function will now invoke the function every time a message is published to the topic specified by `--trigger-topic`.
|
||||
|
||||
For a walkthrough on testing and verifying your background function, see the instructions for running the https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-samples/function-sample-gcp-background/[GCF Background Function sample].
|
||||
|
||||
[[sample-functions]]
|
||||
== Sample Functions
|
||||
|
||||
The project provides the following sample functions as reference:
|
||||
|
||||
* The https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-samples/function-sample-gcp-http/[function-sample-gcp-http] is an HTTP Function which you can test locally and try deploying.
|
||||
* The https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-samples/function-sample-gcp-background/[function-sample-gcp-background] shows an example of a background function that is triggered by a message being published to a specified Pub/Sub topic.
|
||||
@@ -1,2 +0,0 @@
|
||||
*{project-version}*
|
||||
|
||||
@@ -1,321 +0,0 @@
|
||||
Spring Cloud Function supports a "functional" style of bean declarations for small apps where you need fast startup. The functional style of bean declaration was a feature of Spring Framework 5.0 with significant enhancements in 5.1.
|
||||
|
||||
[[comparing-functional-with-traditional-bean-definitions]]
|
||||
= Comparing Functional with Traditional Bean Definitions
|
||||
|
||||
Here's a vanilla Spring Cloud Function application from with the
|
||||
familiar `@Configuration` and `@Bean` declaration style:
|
||||
|
||||
```java
|
||||
@SpringBootApplication
|
||||
public class DemoApplication {
|
||||
|
||||
@Bean
|
||||
public Function<String, String> uppercase() {
|
||||
return value -> value.toUpperCase();
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(DemoApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
Now for the functional beans: the user application code can be recast into "functional"
|
||||
form, like this:
|
||||
|
||||
```java
|
||||
@SpringBootConfiguration
|
||||
public class DemoApplication implements ApplicationContextInitializer<GenericApplicationContext> {
|
||||
|
||||
public static void main(String[] args) {
|
||||
FunctionalSpringApplication.run(DemoApplication.class, args);
|
||||
}
|
||||
|
||||
public Function<String, String> uppercase() {
|
||||
return value -> value.toUpperCase();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(GenericApplicationContext context) {
|
||||
context.registerBean("demo", FunctionRegistration.class,
|
||||
() -> new FunctionRegistration<>(uppercase())
|
||||
.type(FunctionTypeUtils.functionType(String.class, String.class)));
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
The main differences are:
|
||||
|
||||
* The main class is an `ApplicationContextInitializer`.
|
||||
|
||||
* The `@Bean` methods have been converted to calls to `context.registerBean()`
|
||||
|
||||
* The `@SpringBootApplication` has been replaced with
|
||||
`@SpringBootConfiguration` to signify that we are not enabling Spring
|
||||
Boot autoconfiguration, and yet still marking the class as an "entry
|
||||
point".
|
||||
|
||||
* The `SpringApplication` from Spring Boot has been replaced with a
|
||||
`FunctionalSpringApplication` from Spring Cloud Function (it's a
|
||||
subclass).
|
||||
|
||||
The business logic beans that you register in a Spring Cloud Function app are of type `FunctionRegistration`.
|
||||
This is a wrapper that contains both the function and information about the input and output types. In the `@Bean`
|
||||
form of the application that information can be derived reflectively, but in a functional bean registration some of
|
||||
it is lost unless we use a `FunctionRegistration`.
|
||||
|
||||
An alternative to using an `ApplicationContextInitializer` and `FunctionRegistration` is to make the application
|
||||
itself implement `Function` (or `Consumer` or `Supplier`). Example (equivalent to the above):
|
||||
|
||||
```java
|
||||
@SpringBootConfiguration
|
||||
public class DemoApplication implements Function<String, String> {
|
||||
|
||||
public static void main(String[] args) {
|
||||
FunctionalSpringApplication.run(DemoApplication.class, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String apply(String value) {
|
||||
return value.toUpperCase();
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
It would also work if you add a separate, standalone class of type `Function` and register it with
|
||||
the `SpringApplication` using an alternative form of the `run()` method. The main thing is that the generic
|
||||
type information is available at runtime through the class declaration.
|
||||
|
||||
Suppose you have
|
||||
[source, java]
|
||||
----
|
||||
@Component
|
||||
public class CustomFunction implements Function<Flux<Foo>, Flux<Bar>> {
|
||||
@Override
|
||||
public Flux<Bar> apply(Flux<Foo> flux) {
|
||||
return flux.map(foo -> new Bar("This is a Bar object from Foo value: " + foo.getValue()));
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
You register it as such:
|
||||
|
||||
[source, java]
|
||||
----
|
||||
@Override
|
||||
public void initialize(GenericApplicationContext context) {
|
||||
context.registerBean("function", FunctionRegistration.class,
|
||||
() -> new FunctionRegistration<>(new CustomFunction()).type(CustomFunction.class));
|
||||
}
|
||||
----
|
||||
|
||||
[[limitations-of-functional-bean-declaration]]
|
||||
= Limitations of Functional Bean Declaration
|
||||
|
||||
Most Spring Cloud Function apps have a relatively small scope compared to the whole of Spring Boot,
|
||||
so we are able to adapt it to these functional bean definitions easily. If you step outside that limited scope,
|
||||
you can extend your Spring Cloud Function app by switching back to `@Bean` style configuration, or by using a hybrid
|
||||
approach. If you want to take advantage of Spring Boot autoconfiguration for integrations with external datastores,
|
||||
for example, you will need to use `@EnableAutoConfiguration`. Your functions can still be defined using the functional
|
||||
declarations if you want (i.e. the "hybrid" style), but in that case you will need to explicitly switch off the "full
|
||||
functional mode" using `spring.functional.enabled=false` so that Spring Boot can take back control.
|
||||
|
||||
[[function_visualization]]
|
||||
= Function visualization and control
|
||||
|
||||
Spring Cloud Function supports visualization of functions available in `FunctionCatalog` through Actuator endpoints as well as programmatic way.
|
||||
|
||||
[[programmatic-way]]
|
||||
=== Programmatic way
|
||||
|
||||
To see function available within your application context programmatically all you need is access to `FunctionCatalog`. There you can
|
||||
finds methods to get the size of the catalog, lookup functions as well as list the names of all the available functions.
|
||||
|
||||
For example,
|
||||
|
||||
[source,java]
|
||||
----
|
||||
FunctionCatalog functionCatalog = context.getBean(FunctionCatalog.class);
|
||||
int size = functionCatalog.size(); // will tell you how many functions available in catalog
|
||||
Set<String> names = functionCatalog.getNames(null); will list the names of all the Function, Suppliers and Consumers available in catalog
|
||||
. . .
|
||||
----
|
||||
|
||||
[[actuator]]
|
||||
=== Actuator
|
||||
Since actuator and web are optional, you must first add one of the web dependencies as well as add the actuator dependency manually.
|
||||
The following example shows how to add the dependency for the Web framework:
|
||||
|
||||
[source,xml]
|
||||
----
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
----
|
||||
|
||||
The following example shows how to add the dependency for the WebFlux framework:
|
||||
|
||||
[source,xml]
|
||||
----
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||
</dependency>
|
||||
----
|
||||
|
||||
You can add the Actuator dependency as follows:
|
||||
[source,xml]
|
||||
----
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
----
|
||||
|
||||
You must also enable the `functions` actuator endpoints by setting the following property: `--management.endpoints.web.exposure.include=functions`.
|
||||
|
||||
Access the following URL to see the functions in FunctionCatalog:
|
||||
`http://<host>:<port>/actuator/functions`
|
||||
|
||||
For example,
|
||||
[source,text]
|
||||
----
|
||||
curl http://localhost:8080/actuator/functions
|
||||
----
|
||||
|
||||
Your output should look something like this:
|
||||
[source,text]
|
||||
----
|
||||
{"charCounter":
|
||||
{"type":"FUNCTION","input-type":"string","output-type":"integer"},
|
||||
"logger":
|
||||
{"type":"CONSUMER","input-type":"string"},
|
||||
"functionRouter":
|
||||
{"type":"FUNCTION","input-type":"object","output-type":"object"},
|
||||
"words":
|
||||
{"type":"SUPPLIER","output-type":"string"}. . .
|
||||
----
|
||||
|
||||
[[testing-functional-applications]]
|
||||
= Testing Functional Applications
|
||||
|
||||
Spring Cloud Function also has some utilities for integration testing that will be very familiar to Spring Boot users.
|
||||
|
||||
Suppose this is your application:
|
||||
|
||||
[source, java]
|
||||
----
|
||||
@SpringBootApplication
|
||||
public class SampleFunctionApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SampleFunctionApplication.class, args);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Function<String, String> uppercase() {
|
||||
return v -> v.toUpperCase();
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Here is an integration test for the HTTP server wrapping this application:
|
||||
|
||||
[source, java]
|
||||
----
|
||||
@SpringBootTest(classes = SampleFunctionApplication.class,
|
||||
webEnvironment = WebEnvironment.RANDOM_PORT)
|
||||
public class WebFunctionTests {
|
||||
|
||||
@Autowired
|
||||
private TestRestTemplate rest;
|
||||
|
||||
@Test
|
||||
public void test() throws Exception {
|
||||
ResponseEntity<String> result = this.rest.exchange(
|
||||
RequestEntity.post(new URI("/uppercase")).body("hello"), String.class);
|
||||
System.out.println(result.getBody());
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
or when function bean definition style is used:
|
||||
|
||||
[source, java]
|
||||
----
|
||||
@FunctionalSpringBootTest
|
||||
public class WebFunctionTests {
|
||||
|
||||
@Autowired
|
||||
private TestRestTemplate rest;
|
||||
|
||||
@Test
|
||||
public void test() throws Exception {
|
||||
ResponseEntity<String> result = this.rest.exchange(
|
||||
RequestEntity.post(new URI("/uppercase")).body("hello"), String.class);
|
||||
System.out.println(result.getBody());
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
This test is almost identical to the one you would write for the `@Bean` version of the same app - the only difference
|
||||
is the `@FunctionalSpringBootTest` annotation, instead of the regular `@SpringBootTest`. All the other pieces,
|
||||
like the `@Autowired` `TestRestTemplate`, are standard Spring Boot features.
|
||||
|
||||
And to help with correct dependencies here is the excerpt from POM
|
||||
|
||||
[source, xml, subs=attributes+]
|
||||
----
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>{spring-boot-version}</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
. . . .
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-function-web</artifactId>
|
||||
<version>{project-version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
----
|
||||
|
||||
Or you could write a test for a non-HTTP app using just the `FunctionCatalog`. For example:
|
||||
|
||||
[source, java]
|
||||
----
|
||||
@FunctionalSpringBootTest
|
||||
public class FunctionalTests {
|
||||
|
||||
@Autowired
|
||||
private FunctionCatalog catalog;
|
||||
|
||||
@Test
|
||||
public void words() {
|
||||
Function<String, String> function = catalog.lookup(Function.class,
|
||||
"uppercase");
|
||||
assertThat(function.apply("hello")).isEqualTo("HELLO");
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
@@ -1,33 +0,0 @@
|
||||
Build from the command line (and "install" the samples):
|
||||
|
||||
----
|
||||
$ ./mvnw clean install
|
||||
----
|
||||
|
||||
(If you like to YOLO add `-DskipTests`.)
|
||||
|
||||
Run one of the samples, e.g.
|
||||
|
||||
----
|
||||
$ java -jar spring-cloud-function-samples/function-sample/target/*.jar
|
||||
----
|
||||
|
||||
This runs the app and exposes its functions over HTTP, so you can
|
||||
convert a string to uppercase, like this:
|
||||
|
||||
----
|
||||
$ curl -H "Content-Type: text/plain" localhost:8080/uppercase -d Hello
|
||||
HELLO
|
||||
----
|
||||
|
||||
You can convert multiple strings (a `Flux<String>`) by separating them
|
||||
with new lines
|
||||
|
||||
----
|
||||
$ curl -H "Content-Type: text/plain" localhost:8080/uppercase -d 'Hello
|
||||
> World'
|
||||
HELLOWORLD
|
||||
----
|
||||
|
||||
(You can use `^Q^J` in a terminal to insert a new line in a literal
|
||||
string like that.)
|
||||
@@ -1,25 +0,0 @@
|
||||
[[spring-cloud-function-reference-documentation]]
|
||||
= Spring Cloud Function Reference Documentation
|
||||
:page-section-summary-toc: 1
|
||||
Mark Fisher, Dave Syer, Oleg Zhurakousky, Anshul Mehra, Dan Dobrin, Chris Bono, Artem Bilan
|
||||
|
||||
*{project-version}*
|
||||
|
||||
:docinfo: shared
|
||||
|
||||
The reference documentation consists of the following sections:
|
||||
|
||||
[horizontal]
|
||||
<<spring-cloud-function.adoc#,Reference Guide>> :: Spring Cloud Function Reference
|
||||
https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-samples/function-sample-cloudevent[Cloud Events] :: Cloud Events
|
||||
https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-rsocket[RSocket] :: RSocket
|
||||
xref:spring-integration.adoc[Spring Integration] :: Spring Integration Framework Interaction
|
||||
<<aws.adoc#,AWS Adapter>> :: AWS Adapter Reference
|
||||
<<azure.adoc#, Azure Adapter>> :: Azure Adapter Reference
|
||||
<<gcp.adoc#, GCP Adapter>> :: GCP Adapter Reference
|
||||
|
||||
|
||||
Relevant Links:
|
||||
|
||||
[horizontal]
|
||||
https://projectreactor.io/[Reactor] :: Project Reactor
|
||||
@@ -1,46 +0,0 @@
|
||||
Spring Cloud Function is a project with the following high-level goals:
|
||||
|
||||
* Promote the implementation of business logic via functions.
|
||||
* Decouple the development lifecycle of business logic from any specific runtime target so that the same code can run as a web endpoint, a stream processor, or a task.
|
||||
* Support a uniform programming model across serverless providers, as well as the ability to run standalone (locally or in a PaaS).
|
||||
* Enable Spring Boot features (auto-configuration, dependency injection, metrics) on serverless providers.
|
||||
|
||||
It abstracts away all of the transport details and infrastructure, allowing the developer to keep all the familiar tools and processes, and focus firmly on business logic.
|
||||
|
||||
## Features
|
||||
|
||||
Spring Cloud Function features:
|
||||
|
||||
* _Choice of programming styles - reactive, imperative or hybrid._
|
||||
* _Function composition and adaptation (e.g., composing imperative functions with reactive)._
|
||||
* _Support for reactive function with multiple inputs and outputs allowing merging, joining and other complex streaming operation to be handled by functions._
|
||||
* _Transparent type conversion of inputs and outputs._
|
||||
* _Packaging functions for deployments, specific to the target platform (e.g., Project Riff, AWS Lambda and more)_
|
||||
* _Adapters to expose function to the outside world as HTTP endpoints etc._
|
||||
* _Deploying a JAR file containing such an application context with an isolated classloader, so that you can pack them together in a single JVM._
|
||||
* _Adapters for https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-adapters/spring-cloud-function-adapter-aws[AWS Lambda], https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-adapters/spring-cloud-function-adapter-azure[Microsoft Azure], https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp[Google Cloud Functions], and possibly other "serverless" service providers._
|
||||
|
||||
Here's a complete, executable, testable Spring Boot application (implementing a simple string manipulation):
|
||||
|
||||
```java
|
||||
@SpringBootApplication
|
||||
public class Application {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(Application.class, args);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Function<Flux<String>, Flux<String>> uppercase() {
|
||||
return flux -> flux.map(value -> value.toUpperCase());
|
||||
}
|
||||
}
|
||||
```
|
||||
### Sample Projects
|
||||
|
||||
* https://github.com/spring-cloud/spring-cloud-function/blob/master/spring-cloud-function-samples/function-sample[Vanilla]
|
||||
* https://github.com/spring-cloud/spring-cloud-function/blob/master/spring-cloud-function-samples/function-sample-pof[Plain Old Function]
|
||||
* https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-samples/function-sample-aws[AWS Lambda]
|
||||
* https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-samples/function-sample-azure[Microsoft Azure]
|
||||
* https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-samples/function-sample-gcp-http[Google Cloud Functions]
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
[[spring-cloud-function]]
|
||||
= Spring Cloud Function
|
||||
:page-section-summary-toc: 1
|
||||
|
||||
Mark Fisher, Dave Syer, Oleg Zhurakousky, Anshul Mehra, Dan Dobrin
|
||||
|
||||
*{project-version}*
|
||||
|
||||
---
|
||||
|
||||
:github: https://github.com/spring-cloud/spring-cloud-function
|
||||
:githubmaster: {github}/tree/master
|
||||
:docslink: {githubmaster}/docs/src/main/asciidoc
|
||||
:nofooter:
|
||||
:branch: master
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
[[deploying-a-packaged-function]]
|
||||
= Deploying a Packaged Function
|
||||
|
||||
Spring Cloud Function provides a "deployer" library that allows you to launch a jar file (or exploded archive, or set of jar files) with an isolated class loader and expose the functions defined in it. This is quite a powerful tool that would allow you to, for instance, adapt a function to a range of different input-output adapters without changing the target jar file. Serverless platforms often have this kind of feature built in, so you could see it as a building block for a function invoker in such a platform (indeed the https://projectriff.io[Riff] Java function invoker uses this library).
|
||||
|
||||
The standard entry point is to add `spring-cloud-function-deployer` to the classpath, the deployer kicks in and looks for some configuration to tell it where to find the function jar.
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-function-deployer</artifactId>
|
||||
<version>${spring.cloud.function.version}</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
|
||||
At a minimum the user has to provide a `spring.cloud.function.location` which is a URL or resource location for the archive containing
|
||||
the functions. It can optionally use a `maven:` prefix to locate the artifact via a dependency lookup (see `FunctionProperties`
|
||||
for complete details). A Spring Boot application is bootstrapped from the jar file, using the `MANIFEST.MF` to locate a start class, so
|
||||
that a standard Spring Boot fat jar works well, for example. If the target jar can be launched successfully then the result is a function
|
||||
registered in the main application's `FunctionCatalog`. The registered function can be applied by code in the main application, even though
|
||||
it was created in an isolated class loader (by deault).
|
||||
|
||||
Here is the example of deploying a JAR which contains an 'uppercase' function and invoking it .
|
||||
|
||||
```java
|
||||
@SpringBootApplication
|
||||
public class DeployFunctionDemo {
|
||||
|
||||
public static void main(String[] args) {
|
||||
ApplicationContext context = SpringApplication.run(DeployFunctionDemo.class,
|
||||
"--spring.cloud.function.location=..../target/uppercase-0.0.1-SNAPSHOT.jar",
|
||||
"--spring.cloud.function.definition=uppercase");
|
||||
|
||||
FunctionCatalog catalog = context.getBean(FunctionCatalog.class);
|
||||
Function<String, String> function = catalog.lookup("uppercase");
|
||||
System.out.println(function.apply("hello"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
And here is the example using Maven URI (taken from one of the tests in `FunctionDeployerTests`):
|
||||
|
||||
```java
|
||||
@SpringBootApplication
|
||||
public class DeployFunctionDemo {
|
||||
|
||||
public static void main(String[] args) {
|
||||
String[] args = new String[] {
|
||||
"--spring.cloud.function.location=maven://oz.demo:demo-uppercase:0.0.1-SNAPSHOT",
|
||||
"--spring.cloud.function.function-class=oz.demo.uppercase.MyFunction" };
|
||||
|
||||
ApplicationContext context = SpringApplication.run(DeployerApplication.class, args);
|
||||
FunctionCatalog catalog = context.getBean(FunctionCatalog.class);
|
||||
Function<String, String> function = catalog.lookup("myFunction");
|
||||
|
||||
assertThat(function.apply("bob")).isEqualTo("BOB");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Keep in mind that Maven resource such as local and remote repositories, user, password and more are resolved using default MavenProperties which
|
||||
effectively use local defaults and will work for majority of cases. However if you need to customize you can simply provide a bean of type
|
||||
`MavenProperties` where you can set additional properties (see example below).
|
||||
|
||||
```java
|
||||
@Bean
|
||||
public MavenProperties mavenProperties() {
|
||||
MavenProperties properties = new MavenProperties();
|
||||
properties.setLocalRepository("target/it/");
|
||||
return properties;
|
||||
}
|
||||
```
|
||||
|
||||
[[supported-packaging-scenarios]]
|
||||
== Supported Packaging Scenarios
|
||||
|
||||
Currently Spring Cloud Function supports several packaging scenarios to give you the most flexibility when it comes to deploying functions.
|
||||
|
||||
[[simple-jar]]
|
||||
=== Simple JAR
|
||||
|
||||
This packaging option implies no dependency on anything related to Spring.
|
||||
For example; Consider that such JAR contains the following class:
|
||||
```java
|
||||
package function.example;
|
||||
. . .
|
||||
public class UpperCaseFunction implements Function<String, String> {
|
||||
@Override
|
||||
public String apply(String value) {
|
||||
return value.toUpperCase();
|
||||
}
|
||||
}
|
||||
```
|
||||
All you need to do is specify `location` and `function-class` properties when deploying such package:
|
||||
|
||||
```
|
||||
--spring.cloud.function.location=target/it/simplestjar/target/simplestjar-1.0.0.RELEASE.jar
|
||||
--spring.cloud.function.function-class=function.example.UpperCaseFunction
|
||||
```
|
||||
|
||||
It's conceivable in some cases that you might want to package multiple functions together. For such scenarios you can use
|
||||
`spring.cloud.function.function-class` property to list several classes delimiting them by `;`.
|
||||
|
||||
For example,
|
||||
|
||||
```
|
||||
--spring.cloud.function.function-class=function.example.UpperCaseFunction;function.example.ReverseFunction
|
||||
```
|
||||
|
||||
Here we are identifying two functions to deploy, which we can now access in function catalog by name (e.g., `catalog.lookup("reverseFunction");`).
|
||||
|
||||
|
||||
For more details please reference the complete sample available https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-deployer/src/it/simplestjar[here].
|
||||
You can also find a corresponding test in https://github.com/spring-cloud/spring-cloud-function/blob/master/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/FunctionDeployerTests.java#L70[FunctionDeployerTests].
|
||||
|
||||
*** Component Scanning ***
|
||||
|
||||
Since version 3.1.4 you can simplify your configuration thru component scanning feature described in <<Function Component Scan>>. If you place your functional class in
|
||||
package named `functions`, you can omit `spring.cloud.function.function-class` property as framework will auto-discover functional classes loading them in function catalog.
|
||||
Keep in mind the naming convention to follow when doing function lookup. For example function class `functions.UpperCaseFunction` will be available in `FunctionCatalog`
|
||||
under the name `upperCaseFunction`.
|
||||
|
||||
[[spring-boot-jar]]
|
||||
=== Spring Boot JAR
|
||||
|
||||
This packaging option implies there is a dependency on Spring Boot and that the JAR was generated as Spring Boot JAR. That said, given that the deployed JAR
|
||||
runs in the isolated class loader, there will not be any version conflict with the Spring Boot version used by the actual deployer.
|
||||
For example; Consider that such JAR contains the following class (which could have some additional Spring dependencies providing Spring/Spring Boot is on the classpath):
|
||||
```java
|
||||
package function.example;
|
||||
. . .
|
||||
public class UpperCaseFunction implements Function<String, String> {
|
||||
@Override
|
||||
public String apply(String value) {
|
||||
return value.toUpperCase();
|
||||
}
|
||||
}
|
||||
```
|
||||
As before all you need to do is specify `location` and `function-class` properties when deploying such package:
|
||||
|
||||
```
|
||||
--spring.cloud.function.location=target/it/simplestjar/target/simplestjar-1.0.0.RELEASE.jar
|
||||
--spring.cloud.function.function-class=function.example.UpperCaseFunction
|
||||
```
|
||||
For more details please reference the complete sample available https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-deployer/src/it/bootjar[here].
|
||||
You can also find a corresponding test in https://github.com/spring-cloud/spring-cloud-function/blob/master/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/FunctionDeployerTests.java#L50[FunctionDeployerTests].
|
||||
|
||||
[[spring-boot-application]]
|
||||
=== Spring Boot Application
|
||||
|
||||
This packaging option implies your JAR is complete stand alone Spring Boot application with functions as managed Spring beans.
|
||||
As before there is an obvious assumption that there is a dependency on Spring Boot and that the JAR was generated as Spring Boot JAR. That said, given that the deployed JAR
|
||||
runs in the isolated class loader, there will not be any version conflict with the Spring Boot version used by the actual deployer.
|
||||
For example; Consider that such JAR contains the following class:
|
||||
```java
|
||||
package function.example;
|
||||
. . .
|
||||
@SpringBootApplication
|
||||
public class SimpleFunctionAppApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SimpleFunctionAppApplication.class, args);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Function<String, String> uppercase() {
|
||||
return value -> value.toUpperCase();
|
||||
}
|
||||
}
|
||||
```
|
||||
Given that we're effectively dealing with another Spring Application context and that functions are spring managed beans,
|
||||
in addition to the `location` property we also specify `definition` property instead of `function-class`.
|
||||
|
||||
```
|
||||
--spring.cloud.function.location=target/it/bootapp/target/bootapp-1.0.0.RELEASE-exec.jar
|
||||
--spring.cloud.function.definition=uppercase
|
||||
```
|
||||
For more details please reference the complete sample available https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-deployer/src/it/bootapp[here].
|
||||
You can also find a corresponding test in https://github.com/spring-cloud/spring-cloud-function/blob/master/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/FunctionDeployerTests.java#L164[FunctionDeployerTests].
|
||||
|
||||
NOTE: This particular deployment option may or may not have Spring Cloud Function on it's classpath. From the deployer perspective this doesn't matter.
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
[[functional-bean-definitions]]
|
||||
= Functional Bean Definitions
|
||||
:page-section-summary-toc: 1
|
||||
|
||||
include:../:functional.adoc[leveloffset=+1]
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
[[getting-started]]
|
||||
= Getting Started
|
||||
:page-section-summary-toc: 1
|
||||
|
||||
include:../:getting-started.adoc[]
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
[[introduction]]
|
||||
= Introduction
|
||||
:page-section-summary-toc: 1
|
||||
|
||||
include:../:_intro.adoc[]
|
||||
|
||||
include:../:https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/docs/src/main/asciidoc/contributing-docs.adoc[]
|
||||
|
||||
@@ -1,717 +0,0 @@
|
||||
[[programming-model]]
|
||||
= Programming model
|
||||
|
||||
[[function.catalog]]
|
||||
|
||||
[[function-catalog-and-flexible-function-signatures]]
|
||||
== Function Catalog and Flexible Function Signatures
|
||||
|
||||
One of the main features of Spring Cloud Function is to adapt and support a range of type signatures for user-defined functions,
|
||||
while providing a consistent execution model.
|
||||
That's why all user defined functions are transformed into a canonical representation by `FunctionCatalog`.
|
||||
|
||||
While users don't normally have to care about the `FunctionCatalog` at all, it is useful to know what
|
||||
kind of functions are supported in user code.
|
||||
|
||||
It is also important to understand that Spring Cloud Function provides first class support for reactive API
|
||||
provided by https://projectreactor.io/[Project Reactor] allowing reactive primitives such as `Mono` and `Flux`
|
||||
to be used as types in user defined functions providing greater flexibility when choosing programming model for
|
||||
your function implementation.
|
||||
Reactive programming model also enables functional support for features that would be otherwise difficult to impossible to implement
|
||||
using imperative programming style. For more on this please read <<Function Arity>> section.
|
||||
|
||||
[[java-8-function-support]]
|
||||
== Java 8 function support
|
||||
|
||||
Spring Cloud Function embraces and builds on top of the 3 core functional interfaces defined by Java
|
||||
and available to us since Java 8.
|
||||
|
||||
- Supplier<O>
|
||||
- Function<I, O>
|
||||
- Consumer<I>
|
||||
|
||||
To avoid constantly mentioning `Supplier`, `Function` and `Consumer` we’ll refer to them a Functional beans for the rest of this manual where appropriate.
|
||||
|
||||
In a nutshell, any bean in your Application Context that is Functional bean will lazily be registered with `FunctionCatalog`.
|
||||
This means that it could benefit from all of the additional features described in this reference manual.
|
||||
|
||||
In a simplest of application all you need to do is to declare `@Bean` of type `Supplier`, `Function` or `Consumer` in your application configuration.
|
||||
Then you can access `FunctionCatalog` and lookup a particular function based on its name.
|
||||
|
||||
For example:
|
||||
|
||||
|
||||
[source, test]
|
||||
----
|
||||
@Bean
|
||||
public Function<String, String> uppercase() {
|
||||
return value -> value.toUpperCase();
|
||||
}
|
||||
|
||||
. . .
|
||||
|
||||
FunctionCatalog catalog = applicationContext.getBean(FunctionCatalog.class);
|
||||
Function uppercase = catalog.lookup(“uppercase”);
|
||||
----
|
||||
|
||||
Important to understand that given that `uppercase` is a bean, you can certainly get it form the `ApplicationContext` directly, but all you will get is just your bean as you declared it without any extra features provided by SCF. When you do lookup of a function via `FunctionCatalog`, the instance you will receive is wrapped (instrumented) with additional features (i.e., type conversion, composition etc.) described in this manual. Also, it is important to understand that a typical user does not use Spring Cloud Function directly. Instead a typical user implements Java `Function/Supplier/Consumer` with the idea of using it in different execution contexts without additional work. For example the same java function could be represented as _REST endpoint_ or _Streaming message handler_ or _AWS Lambda_ and more via Spring Cloud Function provided
|
||||
adapters as well as other frameworks using Spring Cloud Function as the core programming model (e.g., https://spring.io/projects/spring-cloud-stream[Spring Cloud Stream])
|
||||
So in summary Spring Cloud Function instruments java functions with additional features to be utilised in variety of execution contexts.
|
||||
|
||||
|
||||
[[function-definition]]
|
||||
=== Function definition
|
||||
While the previous example shows you how to lookup function in FunctionCatalog programmatically, in a typical integration case where Spring Cloud Function used as programming model by another framework (e.fg. Spring Cloud Stream), you declare which functions to use via `spring.cloud.function.definition` property. Knowing that it is important to understand some default behaviour when it comes to discovering functions in `FunctionCatalog`. For example, if you only have one Functional bean in your `ApplicationContext`, the `spring.cloud.function.definition` property typically will not be required, since a single function in `FunctionCatalog` can be looked up by an empty name or any name. For example, assuming that `uppercase` is the only function in your catalog, it can be looked up as `catalog.lookup(null)`, `catalog.lookup(“”)`, `catalog.lookup(“foo”)`
|
||||
That said, for cases where you are using framework such as Spring Cloud Stream which uses `spring.cloud.function.definition` it is best practice and recommended to always use `spring.cloud.function.definition` property.
|
||||
|
||||
For example,
|
||||
|
||||
[source, test]
|
||||
----
|
||||
spring.cloud.function.definition=uppercase
|
||||
----
|
||||
|
||||
[[filtering-ineligible-functions]]
|
||||
=== Filtering ineligible functions
|
||||
A typical Application Context may include beans that are valid java functions, but not intended to be candidates to be registered with `FunctionCatalog`.
|
||||
Such beans could be auto-configurations from other projects or any other beans that qualify to be Java functions.
|
||||
The framework provides default filtering of known beans that should not be candidates for registration with function catalog.
|
||||
You can also add to this list additional beans by providing coma delimited list of bean definition names using
|
||||
`spring.cloud.function.ineligible-definitions` property.
|
||||
|
||||
For example,
|
||||
|
||||
[source, test]
|
||||
----
|
||||
spring.cloud.function.ineligible-definitions=foo,bar
|
||||
----
|
||||
|
||||
[[supplier]]
|
||||
=== Supplier
|
||||
Supplier can be _reactive_ - `Supplier<Flux<T>>`
|
||||
or _imperative_ - `Supplier<T>`. From the invocation standpoint this should make no difference
|
||||
to the implementor of such Supplier. However, when used within frameworks
|
||||
(e.g., https://spring.io/projects/spring-cloud-stream[Spring Cloud Stream]), Suppliers, especially reactive,
|
||||
often used to represent the source of the stream, therefore they are invoked once to get the stream (e.g., Flux)
|
||||
to which consumers can subscribe to. In other words such suppliers represent an equivalent of an _infinite stream_.
|
||||
However, the same reactive suppliers can also represent _finite_ stream(s) (e.g., result set on the polled JDBC data).
|
||||
In those cases such reactive suppliers must be hooked up to some polling mechanism of the underlying framework.
|
||||
|
||||
To assist with that Spring Cloud Function provides a marker annotation
|
||||
`org.springframework.cloud.function.context.PollableBean` to signal that such supplier produces a
|
||||
finite stream and may need to be polled again. That said, it is important to understand that Spring Cloud Function itself
|
||||
provides no behavior for this annotation.
|
||||
|
||||
In addition `PollableBean` annotation exposes a _splittable_ attribute to signal that produced stream
|
||||
needs to be split (see https://www.enterpriseintegrationpatterns.com/patterns/messaging/Sequencer.html[Splitter EIP])
|
||||
|
||||
Here is the example:
|
||||
|
||||
[source, java]
|
||||
----
|
||||
@PollableBean(splittable = true)
|
||||
public Supplier<Flux<String>> someSupplier() {
|
||||
return () -> {
|
||||
String v1 = String.valueOf(System.nanoTime());
|
||||
String v2 = String.valueOf(System.nanoTime());
|
||||
String v3 = String.valueOf(System.nanoTime());
|
||||
return Flux.just(v1, v2, v3);
|
||||
};
|
||||
}
|
||||
----
|
||||
|
||||
[[function]]
|
||||
=== Function
|
||||
Function can also be written in imperative or reactive way, yet unlike Supplier and Consumer there are
|
||||
no special considerations for the implementor other then understanding that when used within frameworks
|
||||
such as https://spring.io/projects/spring-cloud-stream[Spring Cloud Stream] and others, reactive function is
|
||||
invoked only once to pass a reference to the stream (Flux or Mono) and imperative is invoked once per event.
|
||||
|
||||
[[consumer]]
|
||||
=== Consumer
|
||||
Consumer is a little bit special because it has a `void` return type,
|
||||
which implies blocking, at least potentially. Most likely you will not
|
||||
need to write `Consumer<Flux<?>>`, but if you do need to do that,
|
||||
remember to subscribe to the input flux.
|
||||
|
||||
[[function-composition]]
|
||||
== Function Composition
|
||||
Function Composition is a feature that allows one to compose several functions into one.
|
||||
The core support is based on function composition feature available with https://docs.oracle.com/javase/8/docs/api/java/util/function/Function.html#andThen-java.util.function.Function-[Function.andThen(..)]
|
||||
support available since Java 8. However on top of it, we provide few additional features.
|
||||
|
||||
[[declarative-function-composition]]
|
||||
=== Declarative Function Composition
|
||||
|
||||
This feature allows you to provide composition instruction in a declarative way using `|` (pipe) or `,` (comma) delimiter
|
||||
when providing `spring.cloud.function.definition` property.
|
||||
|
||||
For example
|
||||
----
|
||||
--spring.cloud.function.definition=uppercase|reverse
|
||||
----
|
||||
Here we effectively provided a definition of a single function which itself is a composition of
|
||||
function `uppercase` and function `reverse`. In fact that is one of the reasons why the property name is _definition_ and not _name_,
|
||||
since the definition of a function can be a composition of several named functions.
|
||||
And as mentioned you can use `,` instead of pipe (such as `...definition=uppercase,reverse`).
|
||||
|
||||
[[composing-non-functions]]
|
||||
=== Composing non-Functions
|
||||
Spring Cloud Function also supports composing Supplier with `Consumer` or `Function` as well as `Function` with `Consumer`.
|
||||
What's important here is to understand the end product of such definitions.
|
||||
Composing Supplier with Function still results in Supplier while composing Supplier with Consumer will effectively render Runnable.
|
||||
Following the same logic composing Function with Consumer will result in Consumer.
|
||||
|
||||
And of course you can't compose uncomposable such as Consumer and Function, Consumer and Supplier etc.
|
||||
|
||||
|
||||
[[function-routing-and-filtering]]
|
||||
== Function Routing and Filtering
|
||||
|
||||
Since version 2.2 Spring Cloud Function provides routing feature allowing
|
||||
you to invoke a single function which acts as a router to an actual function you wish to invoke
|
||||
This feature is very useful in certain FAAS environments where maintaining configurations
|
||||
for several functions could be cumbersome or exposing more than one function is not possible.
|
||||
|
||||
The `RoutingFunction` is registered in _FunctionCatalog_ under the name `functionRouter`. For simplicity
|
||||
and consistency you can also refer to `RoutingFunction.FUNCTION_NAME` constant.
|
||||
|
||||
This function has the following signature:
|
||||
|
||||
[source, java]
|
||||
----
|
||||
public class RoutingFunction implements Function<Object, Object> {
|
||||
. . .
|
||||
}
|
||||
----
|
||||
The routing instructions could be communicated in several ways. We support providing instructions via Message headers, System
|
||||
properties as well as pluggable strategy. So let's look at some of the details
|
||||
|
||||
[[messageroutingcallback]]
|
||||
=== MessageRoutingCallback
|
||||
|
||||
The `MessageRoutingCallback` is a strategy to assist with determining the name of the route-to function definition.
|
||||
|
||||
[source, java]
|
||||
----
|
||||
public interface MessageRoutingCallback {
|
||||
FunctionRoutingResult routingResult(Message<?> message);
|
||||
. . .
|
||||
}
|
||||
----
|
||||
|
||||
All you need to do is implement and register it as a bean to be picked up by the `RoutingFunction`.
|
||||
For example:
|
||||
|
||||
[source, java]
|
||||
----
|
||||
@Bean
|
||||
public MessageRoutingCallback customRouter() {
|
||||
return new MessageRoutingCallback() {
|
||||
@Override
|
||||
public FunctionRoutingResult routingResult(Message<?> message) {
|
||||
return new FunctionRoutingResult((String) message.getHeaders().get("func_name"));
|
||||
}
|
||||
};
|
||||
}
|
||||
----
|
||||
|
||||
In the preceding example you can see a very simple implementation of `MessageRoutingCallback` which determines the function definition from
|
||||
`func_name` Message header of the incoming Message and returns the instance of `FunctionRoutingResult` containing the definition of function to invoke.
|
||||
|
||||
*Message Headers*
|
||||
|
||||
If the input argument is of type `Message<?>`, you can communicate routing instruction by setting one of
|
||||
`spring.cloud.function.definition` or `spring.cloud.function.routing-expression` Message headers.
|
||||
As the name of the property suggests `spring.cloud.function.routing-expression` relies on Spring Expression Language (SpEL).
|
||||
For more static cases you can use `spring.cloud.function.definition` header which allows you to provide
|
||||
the name of a single function (e.g., `...definition=foo`) or a composition instruction (e.g., `...definition=foo|bar|baz`).
|
||||
For more dynamic cases you can use `spring.cloud.function.routing-expression` header and provide SpEL expression that should resolve
|
||||
into definition of a function (as described above).
|
||||
|
||||
NOTE: SpEL evaluation context's root object is the
|
||||
actual input argument, so in the case of `Message<?>` you can construct expression that has access
|
||||
to both `payload` and `headers` (e.g., `spring.cloud.function.routing-expression=headers.function_name`).
|
||||
|
||||
IMPORTANT: SpEL allows user to provide string representation of Java code to be executed. Given that the `spring.cloud.function.routing-expression` could be provided via Message headers means that ability to set such expression could be exposed to the end user (i.e., HTTP Headers when using web module) which could result in some problems (e.g., malicious code). To manage that, all expressions coming via Message headers will only be evaluated against `SimpleEvaluationContext` which has limited functionality and designed to only evaluate the context object (Message in our case). On the other hand, all expressions that are set via property or system variable are evaluated against `StandardEvaluationContext`, which allows for full flexibility of Java language.
|
||||
While setting expression via system/application property or environment variable is generally considered to be secure as it is not exposed to the end user in normal cases, there are cases where visibility as well as capability to update system, application and environment variables are indeed exposed to the end user via Spring Boot Actuator endpoints provided either by some of the Spring projects or third parties or custom implementation by the end user. Such endpoints must be secured using industry standard web security practices.
|
||||
Spring Cloud Function does not expose any of such endpoints.
|
||||
|
||||
In specific execution environments/models the adapters are responsible to translate and communicate
|
||||
`spring.cloud.function.definition` and/or `spring.cloud.function.routing-expression` via Message header.
|
||||
For example, when using _spring-cloud-function-web_ you can provide `spring.cloud.function.definition` as an HTTP
|
||||
header and the framework will propagate it as well as other HTTP headers as Message headers.
|
||||
|
||||
*Application Properties*
|
||||
|
||||
Routing instruction can also be communicated via `spring.cloud.function.definition`
|
||||
or `spring.cloud.function.routing-expression` as application properties. The rules described in the
|
||||
previous section apply here as well. The only difference is you provide these instructions as
|
||||
application properties (e.g., `--spring.cloud.function.definition=foo`).
|
||||
|
||||
NOTE: It is important to understand that providing `spring.cloud.function.definition`
|
||||
or `spring.cloud.function.routing-expression` as Message headers will only work for imperative functions (e.g., `Function<Foo, Bar>`).
|
||||
That is to say that we can _only_ route ***per-message*** with imperative functions. With reactive functions we can not route
|
||||
***per-message***. Therefore you can only provide your routing instructions as Application Properties.
|
||||
It's all about unit-of-work. In imperative function unit of work is Message so we can route based on such unit-of-work.
|
||||
With reactive function unit-of-work is the entire stream, so we'll act only on the instruction provided via application
|
||||
properties and route the entire stream.
|
||||
|
||||
*Order of priority for routing instructions*
|
||||
|
||||
Given that we have several mechanisms of providing routing instructions it is important to understand the priorities for
|
||||
conflict resolutions in the event multiple mechanisms are used at the same time, so here is the order:
|
||||
|
||||
1. `MessageRoutingCallback` (If function is imperative will take over regardless if anything else is defined)
|
||||
2. Message Headers (If function is imperative and no `MessageRoutingCallback` provided)
|
||||
3. Application Properties (Any function)
|
||||
|
||||
*Unroutable Messages*
|
||||
|
||||
In the event route-to function is not available in catalog you will get an exception stating that.
|
||||
|
||||
There are cases when such behavior is not desired and you may want to have some "catch-all" type function which can handle such messages.
|
||||
To accomplish that, framework provides `org.springframework.cloud.function.context.DefaultMessageRoutingHandler` strategy. All you need to do is register it as a bean.
|
||||
Its default implementation will simply log the fact that the message is un-routable, but will allow message flow to proceed without the exception, effectively dropping the un-routable message.
|
||||
If you want something more sophisticated all you need to do is provide your own implementation of this strategy and register it as a bean.
|
||||
|
||||
[source, java]
|
||||
----
|
||||
@Bean
|
||||
public DefaultMessageRoutingHandler defaultRoutingHandler() {
|
||||
return new DefaultMessageRoutingHandler() {
|
||||
@Override
|
||||
public void accept(Message<?> message) {
|
||||
// do something really cool
|
||||
}
|
||||
};
|
||||
}
|
||||
----
|
||||
|
||||
[[function-filtering]]
|
||||
=== Function Filtering
|
||||
Filtering is the type of routing where there are only two paths - 'go' or 'discard'. In terms of functions it mean
|
||||
you only want to invoke a certain function if some condition returns 'true', otherwise you want to discard input.
|
||||
However, when it comes to discarding input there are many interpretation of what it could mean in the context of your application.
|
||||
For example, you may want to log it, or you may want to maintain the counter of discarded messages. you may also want to do nothing at all.
|
||||
Because of these different paths, we do not provide a general configuration option for how to deal with discarded messages.
|
||||
Instead we simply recommend to define a simple Consumer which would signify the 'discard' path:
|
||||
|
||||
[source, java]
|
||||
----
|
||||
@Bean
|
||||
public Consumer<?> devNull() {
|
||||
// log, count or whatever
|
||||
}
|
||||
----
|
||||
Now you can have routing expression that really only has two paths effectively becoming a filter. For example:
|
||||
|
||||
[source, text]
|
||||
----
|
||||
--spring.cloud.function.routing-expression=headers.contentType.toString().equals('text/plain') ? 'echo' : 'devNull'
|
||||
----
|
||||
Every message that does not fit criteria to go to 'echo' function will go to 'devNull' where you can simply do nothing with it.
|
||||
The signature `Consumer<?>` will also ensure that no type conversion will be attempted resulting in almost no execution overhead.
|
||||
|
||||
|
||||
IMPORTANT: When dealing with reactive inputs (e.g., Publisher), routing instructions must only be provided via Function properties. This is
|
||||
due to the nature of the reactive functions which are invoked only once to pass a Publisher and the rest
|
||||
is handled by the reactor, hence we can not access and/or rely on the routing instructions communicated via individual
|
||||
values (e.g., Message).
|
||||
|
||||
[[multiple-routers]]
|
||||
=== Multiple Routers
|
||||
|
||||
By default the framework will always have a single routing function configured as described in previous sections. However, there are times when you may need more than one routing function.
|
||||
In that case you can create your own instance of the `RoutingFunction` bean in addition to the existing one as long as you give it a name other than `functionRouter`.
|
||||
|
||||
You can pass `spring.cloud.function.routing-expression` or `spring.cloud.function.definition` to RoutinFunction as key/value pairs in the map.
|
||||
|
||||
Here is a simple example
|
||||
|
||||
----
|
||||
@Configuration
|
||||
protected static class MultipleRouterConfiguration {
|
||||
|
||||
@Bean
|
||||
RoutingFunction mySpecialRouter(FunctionCatalog functionCatalog, BeanFactory beanFactory, @Nullable MessageRoutingCallback routingCallback) {
|
||||
Map<String, String> propertiesMap = new HashMap<>();
|
||||
propertiesMap.put(FunctionProperties.PREFIX + ".routing-expression", "'reverse'");
|
||||
return new RoutingFunction(functionCatalog, propertiesMap, new BeanFactoryResolver(beanFactory), routingCallback);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Function<String, String> reverse() {
|
||||
return v -> new StringBuilder(v).reverse().toString();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Function<String, String> uppercase() {
|
||||
return String::toUpperCase;
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
and a test that demonstrates how it works
|
||||
|
||||
`
|
||||
----
|
||||
@Test
|
||||
public void testMultipleRouters() {
|
||||
System.setProperty(FunctionProperties.PREFIX + ".routing-expression", "'uppercase'");
|
||||
FunctionCatalog functionCatalog = this.configureCatalog(MultipleRouterConfiguration.class);
|
||||
Function function = functionCatalog.lookup(RoutingFunction.FUNCTION_NAME);
|
||||
assertThat(function).isNotNull();
|
||||
Message<String> message = MessageBuilder.withPayload("hello").build();
|
||||
assertThat(function.apply(message)).isEqualTo("HELLO");
|
||||
|
||||
function = functionCatalog.lookup("mySpecialRouter");
|
||||
assertThat(function).isNotNull();
|
||||
message = MessageBuilder.withPayload("hello").build();
|
||||
assertThat(function.apply(message)).isEqualTo("olleh");
|
||||
}
|
||||
----
|
||||
|
||||
[[input/output-enrichment]]
|
||||
== Input/Output Enrichment
|
||||
|
||||
There are often times when you need to modify or refine an incoming or outgoing Message and to keep your code clean of non-functional concerns. You don’t want to do it inside of your business logic.
|
||||
|
||||
You can always accomplish it via <<Function Composition>>. Such approach provides several benefits:
|
||||
|
||||
- It allows you to isolate this non-functional concern into a separate function which you can compose with the business function as function definition.
|
||||
- It provides you with complete freedom (and danger) as to what you can modify before incoming message reaches the actual business function.
|
||||
|
||||
[source, java]
|
||||
----
|
||||
@Bean
|
||||
public Function<Message<?>, Message<?>> enrich() {
|
||||
return message -> MessageBuilder.fromMessage(message).setHeader("foo", "bar").build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Function<Message<?>, Message<?>> myBusinessFunction() {
|
||||
// do whatever
|
||||
}
|
||||
----
|
||||
|
||||
And then compose your function by providing the following function definition `enrich|myBusinessFunction`.
|
||||
|
||||
While the described approach is the most flexible, it is also the most involved as it requires you to write some code, make it a bean or
|
||||
manually register it as a function before you can compose it with the business function as you can see from the preceding example.
|
||||
|
||||
But what if modifications (enrichments) you are trying to make are trivial as they are in the preceding example? Is there a simpler and more dynamic and configurable
|
||||
mechanism to accomplish the same?
|
||||
|
||||
Since version 3.1.3, the framework allows you to provide SpEL expression to enrich individual message headers for both input going into function and
|
||||
and output coming out of it. Let’s look at one of the tests as the example.
|
||||
|
||||
[source, java]
|
||||
----
|
||||
@Test
|
||||
public void testMixedInputOutputHeaderMapping() throws Exception {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
SampleFunctionConfiguration.class).web(WebApplicationType.NONE).run(
|
||||
"--logging.level.org.springframework.cloud.function=DEBUG",
|
||||
"--spring.main.lazy-initialization=true",
|
||||
"--spring.cloud.function.configuration.split.output-header-mapping-expression.keyOut1='hello1'",
|
||||
"--spring.cloud.function.configuration.split.output-header-mapping-expression.keyOut2=headers.contentType",
|
||||
"--spring.cloud.function.configuration.split.input-header-mapping-expression.key1=headers.path.split('/')[0]",
|
||||
"--spring.cloud.function.configuration.split.input-header-mapping-expression.key2=headers.path.split('/')[1]",
|
||||
"--spring.cloud.function.configuration.split.input-header-mapping-expression.key3=headers.path")) {
|
||||
|
||||
FunctionCatalog functionCatalog = context.getBean(FunctionCatalog.class);
|
||||
FunctionInvocationWrapper function = functionCatalog.lookup("split");
|
||||
Message<byte[]> result = (Message<byte[]>) function.apply(MessageBuilder.withPayload("helo")
|
||||
.setHeader(MessageHeaders.CONTENT_TYPE, "application/json")
|
||||
.setHeader("path", "foo/bar/baz").build());
|
||||
assertThat(result.getHeaders().containsKey("keyOut1")).isTrue();
|
||||
assertThat(result.getHeaders().get("keyOut1")).isEqualTo("hello1");
|
||||
assertThat(result.getHeaders().containsKey("keyOut2")).isTrue();
|
||||
assertThat(result.getHeaders().get("keyOut2")).isEqualTo("application/json");
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Here you see a properties called `input-header-mapping-expression` and `output-header-mapping-expression` preceded by the name of the function (i.e., `split`) and followed by the name of the message header key you want to set and the value as SpEL expression. The first expression (for 'keyOut1') is literal SpEL expressions enclosed in single quotes, effectively setting 'keyOut1' to value `hello1`. The `keyOut2` is set to the value of existing 'contentType' header.
|
||||
|
||||
You can also observe some interesting features in the input header mapping where we actually splitting a value of the existing header 'path', setting individual values of key1 and key2 to the values of split elements based on the index.
|
||||
|
||||
NOTE: if for whatever reason the provided expression evaluation fails, the execution of the function will proceed as if nothing ever happen.
|
||||
However you will see the WARN message in your logs informing you about it
|
||||
|
||||
[source, text]
|
||||
----
|
||||
o.s.c.f.context.catalog.InputEnricher : Failed while evaluating expression "hello1" on incoming message. . .
|
||||
----
|
||||
|
||||
In the event you are dealing with functions that have multiple inputs (next section), you can use index immediately after `input-header-mapping-expression`
|
||||
|
||||
[source, text]
|
||||
----
|
||||
--spring.cloud.function.configuration.echo.input-header-mapping-expression[0].key1=‘hello1'
|
||||
--spring.cloud.function.configuration.echo.input-header-mapping-expression[1].key2='hello2'
|
||||
----
|
||||
|
||||
[[function-arity]]
|
||||
== Function Arity
|
||||
|
||||
There are times when a stream of data needs to be categorized and organized. For example,
|
||||
consider a classic big-data use case of dealing with unorganized data containing, let’s say,
|
||||
‘orders’ and ‘invoices’, and you want each to go into a separate data store.
|
||||
This is where function arity (functions with multiple inputs and outputs) support
|
||||
comes to play.
|
||||
|
||||
Let’s look at an example of such a function (full implementation details are available
|
||||
https://github.com/spring-cloud/spring-cloud-stream/blob/master/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/function/MultipleInputOutputFunctionTests.java#L342[here]),
|
||||
|
||||
[source, java]
|
||||
----
|
||||
@Bean
|
||||
public Function<Flux<Integer>, Tuple2<Flux<String>, Flux<String>>> organise() {
|
||||
return flux -> ...;
|
||||
}
|
||||
----
|
||||
|
||||
Given that Project Reactor is a core dependency of SCF, we are using its Tuple library.
|
||||
Tuples give us a unique advantage by communicating to us both _cardinality_ and _type_ information.
|
||||
Both are extremely important in the context of SCSt. Cardinality lets us know
|
||||
how many input and output bindings need to be created and bound to the corresponding
|
||||
inputs and outputs of a function. Awareness of the type information ensures proper type
|
||||
conversion.
|
||||
|
||||
Also, this is where the ‘index’ part of the naming convention for binding
|
||||
names comes into play, since, in this function, the two output binding
|
||||
names are `organise-out-0` and `organise-out-1`.
|
||||
|
||||
IMPORTANT: IMPORTANT: At the moment, function arity is *only* supported for reactive functions
|
||||
(`Function<TupleN<Flux<?>...>, TupleN<Flux<?>...>>`) centered on Complex event processing
|
||||
where evaluation and computation on confluence of events typically requires view into a
|
||||
stream of events rather than single event.
|
||||
|
||||
[[input-header-propagation]]
|
||||
== Input Header propagation
|
||||
|
||||
In a typical scenario input Message headers are not propagated to output and rightfully so, since the output of a function may be an input to something else requiring it's own set of Message headers.
|
||||
However, there are times when such propagation may be necessary so Spring Cloud Function provides several mechanisms to accomplish this.
|
||||
|
||||
First you can always copy headers manually. For example, if you have a Function with the signature that takes `Message` and returns `Message` (i.e., `Function<Message, Message>`), you can simply and selectively copy headers yourselves. Remember, if your function returns Message, the framework will not do anything to it other then properly converting its payload.
|
||||
However, such approach may prove to be a bit tedious, especially in cases when you simply want to copy all headers.
|
||||
To assist with cases like this we provide a simple property that would allow you to set a boolean flag on a function where you want input headers to be propagated.
|
||||
The property is `copy-input-headers`.
|
||||
|
||||
For example, let's assume you have the following configuration:
|
||||
|
||||
[source, java]
|
||||
----
|
||||
@EnableAutoConfiguration
|
||||
@Configuration
|
||||
protected static class InputHeaderPropagationConfiguration {
|
||||
|
||||
@Bean
|
||||
public Function<String, String> uppercase() {
|
||||
return x -> x.toUpperCase();
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
As you know you can still invoke this function by sending a Message to it (framework will take care of type conversion and payload extraction)
|
||||
|
||||
By simply setting `spring.cloud.function.configuration.uppercase.copy-input-headers` to `true`, the following assertion will be true as well
|
||||
|
||||
----
|
||||
Function<Message<String>, Message<byte[]>> uppercase = catalog.lookup("uppercase", "application/json");
|
||||
Message<byte[]> result = uppercase.apply(MessageBuilder.withPayload("bob").setHeader("foo", "bar").build());
|
||||
assertThat(result.getHeaders()).containsKey("foo");
|
||||
----
|
||||
|
||||
[[type-conversion-content-type-negotiation]]
|
||||
== Type conversion (Content-Type negotiation)
|
||||
|
||||
Content-Type negotiation is one of the core features of Spring Cloud Function as it allows to not only transform the incoming data to the types declared
|
||||
by the function signature, but to do the same transformation during function composition making otherwise un-composable (by type) functions composable.
|
||||
|
||||
To better understand the mechanics and the necessity behind content-type negotiation, we take a look at a very simple use case by
|
||||
using the following function as an example:
|
||||
|
||||
[source, java]
|
||||
----
|
||||
@Bean
|
||||
public Function<Person, String> personFunction {..}
|
||||
----
|
||||
|
||||
The function shown in the preceding example expects a `Person` object as an argument and produces a String type as an output. If such function is
|
||||
invoked with the type `Person`, than all works fine. But typically function plays a role of a handler for the incoming data which most often comes
|
||||
in the raw format such as `byte[]`, `JSON String` etc. In order for the framework to succeed in passing the incoming data as an argument to
|
||||
this function, it has to somehow transform the incoming data to a `Person` type.
|
||||
|
||||
Spring Cloud Function relies on two native to Spring mechanisms to accomplish that.
|
||||
|
||||
. _MessageConverter_ - to convert from incoming Message data to a type declared by the function.
|
||||
. _ConversionService_ - to convert from incoming non-Message data to a type declared by the function.
|
||||
|
||||
This means that depending on the type of the raw data (Message or non-Message) Spring Cloud Function will apply one or the other mechanisms.
|
||||
|
||||
For most cases when dealing with functions that are invoked as part of some other request (e.g., HTTP, Messaging etc) the framework relies on `MessageConverters`,
|
||||
since such requests already converted to Spring `Message`. In other words, the framework locates and applies the appropriate `MessageConverter`.
|
||||
To accomplish that, the framework needs some instructions from the user. One of these instructions is already provided by the signature of the function
|
||||
itself (Person type). Consequently, in theory, that should be (and, in some cases, is) enough. However, for the majority of use cases, in order to
|
||||
select the appropriate `MessageConverter`, the framework needs an additional piece of information. That missing piece is `contentType` header.
|
||||
|
||||
Such header usually comes as part of the Message where it is injected by the corresponding adapter that created such Message in the first place.
|
||||
For example, HTTP POST request will have its content-type HTTP header copied to `contentType` header of the Message.
|
||||
|
||||
For cases when such header does not exist framework relies on the default content type as `application/json`.
|
||||
|
||||
|
||||
[[content-type-versus-argument-type]]
|
||||
=== Content Type versus Argument Type
|
||||
|
||||
As mentioned earlier, for the framework to select the appropriate `MessageConverter`, it requires argument type and, optionally, content type information.
|
||||
The logic for selecting the appropriate `MessageConverter` resides with the argument resolvers which trigger right before the invocation of the user-defined
|
||||
function (which is when the actual argument type is known to the framework).
|
||||
If the argument type does not match the type of the current payload, the framework delegates to the stack of the
|
||||
pre-configured `MessageConverters` to see if any one of them can convert the payload.
|
||||
|
||||
The combination of `contentType` and argument type is the mechanism by which framework determines if message can be converted to a target type by locating
|
||||
the appropriate `MessageConverter`.
|
||||
If no appropriate `MessageConverter` is found, an exception is thrown, which you can handle by adding a custom `MessageConverter`
|
||||
(see `xref:spring-cloud-function/programming-model.adoc#user-defined-message-converters[User-defined Message Converters]`).
|
||||
|
||||
NOTE: Do not expect `Message` to be converted into some other type based only on the `contentType`.
|
||||
Remember that the `contentType` is complementary to the target type.
|
||||
It is a hint, which `MessageConverter` may or may not take into consideration.
|
||||
|
||||
[[message-converters]]
|
||||
=== Message Converters
|
||||
|
||||
`MessageConverters` define two methods:
|
||||
|
||||
[source, java]
|
||||
----
|
||||
Object fromMessage(Message<?> message, Class<?> targetClass);
|
||||
|
||||
Message<?> toMessage(Object payload, @Nullable MessageHeaders headers);
|
||||
----
|
||||
|
||||
It is important to understand the contract of these methods and their usage, specifically in the context of Spring Cloud Stream.
|
||||
|
||||
The `fromMessage` method converts an incoming `Message` to an argument type.
|
||||
The payload of the `Message` could be any type, and it is
|
||||
up to the actual implementation of the `MessageConverter` to support multiple types.
|
||||
|
||||
|
||||
[[provided-messageconverters]]
|
||||
=== Provided MessageConverters
|
||||
|
||||
As mentioned earlier, the framework already provides a stack of `MessageConverters` to handle most common use cases.
|
||||
The following list describes the provided `MessageConverters`, in order of precedence (the first `MessageConverter` that works is used):
|
||||
|
||||
. `JsonMessageConverter`: Supports conversion of the payload of the `Message` to/from POJO for cases when `contentType` is `application/json` using Jackson (DEFAULT) or Gson libraries. This message converter also aware of `type` parameter (e.g., _application/json;type=foo.bar.Person_). This is useful for cases where types may not be known at the time when function is developed, hence function signature may look like `Function<?, ?>` or `Function` or `Function<Object, Object>`. In other words for type conversion we typically derive type from function signature. Having, mime-type parameter allows you to communicate type in a more dynamic way.
|
||||
. `ByteArrayMessageConverter`: Supports conversion of the payload of the `Message` from `byte[]` to `byte[]` for cases when `contentType` is `application/octet-stream`. It is essentially a pass through and exists primarily for backward compatibility.
|
||||
. `StringMessageConverter`: Supports conversion of any type to a `String` when `contentType` is `text/plain`.
|
||||
|
||||
When no appropriate converter is found, the framework throws an exception. When that happens, you should check your code and configuration and ensure you did
|
||||
not miss anything (that is, ensure that you provided a `contentType` by using a binding or a header).
|
||||
However, most likely, you found some uncommon case (such as a custom `contentType` perhaps) and the current stack of provided `MessageConverters`
|
||||
does not know how to convert. If that is the case, you can add custom `MessageConverter`. See xref:spring-cloud-function/programming-model.adoc#user-defined-message-converters[User-defined Message Converters].
|
||||
|
||||
[[user-defined-message-converters]]
|
||||
=== User-defined Message Converters
|
||||
|
||||
Spring Cloud Function exposes a mechanism to define and register additional `MessageConverters`.
|
||||
To use it, implement `org.springframework.messaging.converter.MessageConverter`, configure it as a `@Bean`.
|
||||
It is then appended to the existing stack of `MessageConverter`s.
|
||||
|
||||
NOTE: It is important to understand that custom `MessageConverter` implementations are added to the head of the existing stack.
|
||||
Consequently, custom `MessageConverter` implementations take precedence over the existing ones, which lets you override as well as add to the existing converters.
|
||||
|
||||
The following example shows how to create a message converter bean to support a new content type called `application/bar`:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@SpringBootApplication
|
||||
public static class SinkApplication {
|
||||
|
||||
...
|
||||
|
||||
@Bean
|
||||
public MessageConverter customMessageConverter() {
|
||||
return new MyCustomMessageConverter();
|
||||
}
|
||||
}
|
||||
|
||||
public class MyCustomMessageConverter extends AbstractMessageConverter {
|
||||
|
||||
public MyCustomMessageConverter() {
|
||||
super(new MimeType("application", "bar"));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean supports(Class<?> clazz) {
|
||||
return (Bar.class.equals(clazz));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object convertFromInternal(Message<?> message, Class<?> targetClass, Object conversionHint) {
|
||||
Object payload = message.getPayload();
|
||||
return (payload instanceof Bar ? payload : new Bar((byte[]) payload));
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
[[note-on-json-options]]
|
||||
=== Note on JSON options
|
||||
|
||||
In Spring Cloud Function we support Jackson and Gson mechanisms to deal with JSON.
|
||||
And for your benefit have abstracted it under `org.springframework.cloud.function.json.JsonMapper` which itself is aware of two mechanisms and will use the one selected
|
||||
by you or following the default rule.
|
||||
The default rules are as follows:
|
||||
|
||||
* Whichever library is on the classpath that is the mechanism that is going to be used. So if you have `com.fasterxml.jackson.*` to the classpath, Jackson is going to be used and if you have `com.google.code.gson`, then Gson will be used.
|
||||
* If you have both, then Gson will be the default, or you can set `spring.cloud.function.preferred-json-mapper` property with either of two values: `gson` or `jackson`.
|
||||
|
||||
|
||||
That said, the type conversion is usually transparent to the developer, however given that `org.springframework.cloud.function.json.JsonMapper` is also registered as a bean
|
||||
you can easily inject it into your code if needed.
|
||||
|
||||
|
||||
[[kotlin-lambda-support]]
|
||||
== Kotlin Lambda support
|
||||
|
||||
We also provide support for Kotlin lambdas (since v2.0).
|
||||
Consider the following:
|
||||
|
||||
[source, java]
|
||||
----
|
||||
@Bean
|
||||
open fun kotlinSupplier(): () -> String {
|
||||
return { "Hello from Kotlin" }
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun kotlinFunction(): (String) -> String {
|
||||
return { it.toUpperCase() }
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun kotlinConsumer(): (String) -> Unit {
|
||||
return { println(it) }
|
||||
}
|
||||
|
||||
----
|
||||
The above represents Kotlin lambdas configured as Spring beans. The signature of each maps to a Java equivalent of
|
||||
`Supplier`, `Function` and `Consumer`, and thus supported/recognized signatures by the framework.
|
||||
While mechanics of Kotlin-to-Java mapping are outside of the scope of this documentation, it is important to understand that the
|
||||
same rules for signature transformation outlined in "Java 8 function support" section are applied here as well.
|
||||
|
||||
To enable Kotlin support all you need is to add Kotlin SDK libraries on the classpath which will trigger appropriate
|
||||
autoconfiguration and supporting classes.
|
||||
|
||||
[[function-component-scan]]
|
||||
== Function Component Scan
|
||||
|
||||
Spring Cloud Function will scan for implementations of `Function`, `Consumer` and `Supplier` in a package called `functions` if it exists. Using this
|
||||
feature you can write functions that have no dependencies on Spring - not even the `@Component` annotation is needed. If you want to use a different
|
||||
package, you can set `spring.cloud.function.scan.packages`. You can also use `spring.cloud.function.scan.enabled=false` to switch off the scan completely.
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
[[serverless-platform-adapters]]
|
||||
= Serverless Platform Adapters
|
||||
:page-section-summary-toc: 1
|
||||
|
||||
As well as being able to run as a standalone process, a Spring Cloud
|
||||
Function application can be adapted to run one of the existing
|
||||
serverless platforms. In the project there are adapters for
|
||||
https://github.com/spring-cloud/spring-cloud-function/tree/{branch}/spring-cloud-function-adapters/spring-cloud-function-adapter-aws[AWS
|
||||
Lambda], and https://github.com/spring-cloud/spring-cloud-function/tree/{branch}/spring-cloud-function-adapters/spring-cloud-function-adapter-azure[Azure]. The https://github.com/fnproject/fn[Oracle Fn platform] has its own Spring Cloud Function adapter. And https://projectriff.io[Riff] supports Java functions and its
|
||||
https://github.com/projectriff/java-function-invoker[Java Function Invoker] acts natively is an adapter for Spring Cloud Function jars.
|
||||
|
||||
include:../:adapters/aws-intro.adoc[]
|
||||
include:../:adapters/azure-intro.adoc[leveloffset=+1]
|
||||
include:../:adapters/gcp-intro.adoc[]
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
[[standalone-streaming-applications]]
|
||||
= Standalone Streaming Applications
|
||||
:page-section-summary-toc: 1
|
||||
|
||||
To send or receive messages from a broker (such as RabbitMQ or Kafka) you can leverage `spring-cloud-stream` project and it's integration with Spring Cloud Function.
|
||||
Please refer to https://cloud.spring.io/spring-cloud-static/spring-cloud-stream/current/reference/html/spring-cloud-stream.html#spring_cloud_function[Spring Cloud Function] section of the https://spring.io/projects/spring-cloud-stream[Spring Cloud Stream] reference manual for more details and examples.
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
[[standalone-web-applications]]
|
||||
= Standalone Web Applications
|
||||
|
||||
Functions could be automatically exported as HTTP endpoints.
|
||||
|
||||
The `spring-cloud-function-web` module has autoconfiguration that
|
||||
activates when it is included in a Spring Boot web application (with
|
||||
MVC support). There is also a `spring-cloud-starter-function-web` to
|
||||
collect all the optional dependencies in case you just want a simple
|
||||
getting started experience.
|
||||
|
||||
With the web configurations activated your app will have an MVC
|
||||
endpoint (on "/" by default, but configurable with
|
||||
`spring.cloud.function.web.path`) that can be used to access the
|
||||
functions in the application context where function name becomes part of the URL path. The supported content types are
|
||||
plain text and JSON.
|
||||
|
||||
IMPORTANT: It is important to understand that while SCF provides ability to export Functional beans as REST endpoints it is NOT a replacement for Spring MVC/WebFlux etc.
|
||||
It is primarily to accommodate _stateless serverless patterns_ where you simply want to have some stateless functionality to be exposed via HTTP.
|
||||
|
||||
|===
|
||||
| Method | Path | Request | Response | Status
|
||||
|
||||
| GET | /{supplier} | - | Items from the named supplier | 200 OK
|
||||
| POST | /{consumer} | JSON object or text | Mirrors input and pushes request body into consumer | 202 Accepted
|
||||
| PUT | /{consumer} | JSON object or text | Mirrors input and pushes request body into consumer | 202 Accepted
|
||||
| DELETE | /{consumer} | JSON object or text | - | 204 NO CONTENT
|
||||
| POST | /{function} | JSON object or text | The result of applying the named function | 200 OK
|
||||
| PUT | /{function} | JSON object or text | The result of applying the named function | 200 OK
|
||||
| GET | /{function}/{item} | - | Convert the item into an object and return the result of applying the function | 200 OK
|
||||
|
||||
|===
|
||||
|
||||
As the table above shows the behavior of the endpoint depends on the method and also the type of incoming request data. When the incoming data is single valued, and the target function is declared as obviously single valued (i.e. not returning a collection or `Flux`), then the response will also contain a single value.
|
||||
For multi-valued responses the client can ask for a server-sent event stream by sending `Accept: text/event-stream`.
|
||||
|
||||
Functions and consumers that are declared with input and output in `Message<?>` will see the request headers as _message headers_, and the output _message headers_ will be converted to HTTP headers.
|
||||
The _payload_ of the Message will be a `body` or empty string if there is no `body` or it is null.
|
||||
|
||||
When POSTing text the response format might be different with Spring Boot 2.0 and older versions, depending on the content negotiation (provide content type and accept headers for the best results).
|
||||
|
||||
See <<Testing Functional Applications>> to see the details and example on how to test such application.
|
||||
|
||||
[[http-request-parameters]]
|
||||
=== HTTP Request Parameters
|
||||
As you have noticed from the previous table, you can pass an argument to a function as path variable (i.e., `/{function}/{item}`).
|
||||
For example, `http://localhost:8080/uppercase/foo` will result in calling `uppercase` function with its input parameter being `foo`.
|
||||
|
||||
While this is the recommended approach and the one that fits most use cases cases, there are times when you have to deal with HTTP request parameters (e.g., `http://localhost:8080/uppercase/foo?name=Bill`)
|
||||
The framework will treat HTTP request parameters similar to the HTTP headers by storing them in the `Message` headers under the header key `http_request_param`
|
||||
with its value being a `Map` of request parameters, so in order to access them your function input signature should accept `Message` type (e.g., `Function<Message<String>, String>`). For convenience we provide `HeaderUtils.HTTP_REQUEST_PARAM` constant.
|
||||
|
||||
[[function-mapping-rules]]
|
||||
== Function Mapping rules
|
||||
|
||||
If there is only a single function (consumer etc.) in the catalog, the name in the path is optional.
|
||||
In other words, providing you only have `uppercase` function in catalog
|
||||
`curl -H "Content-Type: text/plain" localhost:8080/uppercase -d hello` and `curl -H "Content-Type: text/plain" localhost:8080/ -d hello` calls are identical.
|
||||
|
||||
Composite functions can be addressed using pipes or commas to separate function names (pipes are legal in URL paths, but a bit awkward to type on the command line).
|
||||
For example, `curl -H "Content-Type: text/plain" localhost:8080/uppercase,reverse -d hello`.
|
||||
|
||||
For cases where there is more than a single function in catalog, each function will be exported and mapped with function name being
|
||||
part of the path (e.g., `localhost:8080/uppercase`).
|
||||
In this scenario you can still map specific function or function composition to the root path by providing
|
||||
`spring.cloud.function.definition` property
|
||||
|
||||
For example,
|
||||
----
|
||||
--spring.cloud.function.definition=foo|bar
|
||||
----
|
||||
|
||||
The above property will compose 'foo' and 'bar' function and map the composed function to the "/" path.
|
||||
|
||||
The same property will also work for cases where function can not be resolved via URL. For example, your URL may be `localhost:8080/uppercase`, but there is no `uppercase` function.
|
||||
However there are function `foo` and `bar`. So, in this case `localhost:8080/uppercase` will resolve to `foo|bar`.
|
||||
This could be useful especially for cases when URL is used to communicate certain information since there will be Message header called `uri` with the value
|
||||
of the actual URL, giving user ability to use it for evaluation and computation.
|
||||
|
||||
[[function-filtering-rules]]
|
||||
== Function Filtering rules
|
||||
|
||||
In situations where there are more than one function in catalog there may be a need to only export certain functions or function compositions. In that case you can use
|
||||
the same `spring.cloud.function.definition` property listing functions you intend to export delimited by `;`.
|
||||
Note that in this case nothing will be mapped to the root path and functions that are not listed (including compositions) are not going to be exported
|
||||
|
||||
For example,
|
||||
|
||||
----
|
||||
--spring.cloud.function.definition=foo;bar
|
||||
----
|
||||
|
||||
This will only export function `foo` and function `bar` regardless how many functions are available in catalog (e.g., `localhost:8080/foo`).
|
||||
|
||||
----
|
||||
--spring.cloud.function.definition=foo|bar;baz
|
||||
----
|
||||
|
||||
This will only export function composition `foo|bar` and function `baz` regardless how many functions are available in catalog (e.g., `localhost:8080/foo,bar`).
|
||||
|
||||
[[crud-rest-with-spring-cloud-function]]
|
||||
== CRUD REST with Spring Cloud Function
|
||||
|
||||
By now it should be clear that functions are exported as REST endpoints and can be invoked using various HTTP methods. In other words a single
|
||||
function could be triggered via GET, POST, PUT etc.
|
||||
|
||||
However, it is not always desirable and certainly does not fit the CRUD concept. And while SCF does not support and has no intention of supporting
|
||||
all the features of Spring web stack, the framework does provide support for CRUD mappings where a single function could be mapped to a particular HTTP method(s).
|
||||
It is done via spring.cloud.function.http.<method-name> property.
|
||||
|
||||
For example,
|
||||
|
||||
----
|
||||
spring.cloud.function.http.GET=uppercase;reverse;foo|bar
|
||||
spring.cloud.function.http.POST=reverse
|
||||
spring.cloud.function.http.DELETE=deleteById
|
||||
----
|
||||
|
||||
As you can see, here we’re mapping functions to various HTTP methods using the same rules as `spring.cloud.function.definition` property where “;” allows us to define several functions and “|” signifies function composition.
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
[[spring-integration]]
|
||||
= Spring Integration Interaction
|
||||
|
||||
https://spring.io/projects/spring-integration[Spring Integration Framework] extends the Spring programming model to support the well-known Enterprise Integration Patterns.
|
||||
It enables lightweight messaging within Spring-based applications and supports integration with external systems via declarative adapters.
|
||||
It also provides a high-level DSL to compose various operations (endpoints) into a logical integration flow.
|
||||
With a lambda style of this DSL configuration, Spring Integration already has a good level of `java.util.function` interfaces adoption.
|
||||
The `@MessagingGateway` proxy interface can also be as a `Function` or `Consumer`, which according to the Spring Cloud Function environment can be registered into a function catalog.
|
||||
See more information in Spring Integration https://docs.spring.io/spring-integration/docs/current/reference/html/messaging-endpoints.html#functions-support[ReferenceManual] about its support for functions.
|
||||
|
||||
On the other hand, starting with version `4.0.3`, Spring Cloud Function introduces a `spring-cloud-function-integration` module which provides deeper, more cloud-specific and auto-configuration based API for interaction with a `FunctionCatalog` from Spring Integration DSL perspective.
|
||||
The `FunctionFlowBuilder` is auto-configured and autowired with a `FunctionCatalog` and represents an entry point for function-specific DSL for target `IntegrationFlow` instance.
|
||||
In addition to standard `IntegrationFlow.from()` factories (for convenience), the `FunctionFlowBuilder` exposes a `fromSupplier(String supplierDefinition)` factory to lookup the target `Supplier` in the provided `FunctionCatalog`.
|
||||
Then this `FunctionFlowBuilder` leads to the `FunctionFlowDefinition`.
|
||||
This `FunctionFlowDefinition` is an implementation of the `IntegrationFlowExtension` and exposes `apply(String functionDefinition)` and `accept(String consumerDefinition)` operators to lookup `Function` or `Consumer` from the `FunctionCatalog`, respectively.
|
||||
See their Javadocs for more information.
|
||||
|
||||
The following example demonstrates the `FunctionFlowBuilder` in action alongside with the power of the rest of `IntegrationFlow` API:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Configuration
|
||||
public class IntegrationConfiguration {
|
||||
|
||||
@Bean
|
||||
Supplier<byte[]> simpleByteArraySupplier() {
|
||||
return "simple test data"::getBytes;
|
||||
}
|
||||
|
||||
@Bean
|
||||
Function<String, String> upperCaseFunction() {
|
||||
return String::toUpperCase;
|
||||
}
|
||||
|
||||
@Bean
|
||||
BlockingQueue<String> results() {
|
||||
return new LinkedBlockingQueue<>();
|
||||
}
|
||||
|
||||
@Bean
|
||||
Consumer<String> simpleStringConsumer(BlockingQueue<String> results) {
|
||||
return results::add;
|
||||
}
|
||||
|
||||
@Bean
|
||||
QueueChannel wireTapChannel() {
|
||||
return new QueueChannel();
|
||||
}
|
||||
|
||||
@Bean
|
||||
IntegrationFlow someFunctionFlow(FunctionFlowBuilder functionFlowBuilder) {
|
||||
return functionFlowBuilder
|
||||
.fromSupplier("simpleByteArraySupplier")
|
||||
.wireTap("wireTapChannel")
|
||||
.apply("upperCaseFunction")
|
||||
.log(LoggingHandler.Level.WARN)
|
||||
.accept("simpleStringConsumer");
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
Since the `FunctionCatalog.lookup()` functionality is not limited just to simple function names, a function composition feature can also be used in the mentioned `apply()` and `accept()` operators:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
IntegrationFlow functionCompositionFlow(FunctionFlowBuilder functionFlowBuilder) {
|
||||
return functionFlowBuilder
|
||||
.from("functionCompositionInput")
|
||||
.accept("upperCaseFunction|simpleStringConsumer");
|
||||
}
|
||||
----
|
||||
|
||||
This API becomes more relevant, when we add into our Spring Cloud applications auto-configuration dependencies for predefined functions.
|
||||
For example https://spring.io/projects/spring-cloud-stream-applications[Stream Applications] project, in addition to application images, provides artifacts with functions for various integration use-case, e.g. `debezium-supplier`, `elasticsearch-consumer`, `aggregator-function` etc.
|
||||
|
||||
The following configuration is based on the `http-supplier`, `spel-function` and `file-consumer`, respectively:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
IntegrationFlow someFunctionFlow(FunctionFlowBuilder functionFlowBuilder) {
|
||||
return functionFlowBuilder
|
||||
.fromSupplier("httpSupplier", e -> e.poller(Pollers.trigger(new OnlyOnceTrigger())))
|
||||
.<Flux<?>>handle((fluxPayload, headers) -> fluxPayload, e -> e.async(true))
|
||||
.channel(c -> c.flux())
|
||||
.apply("spelFunction")
|
||||
.<String, String>transform(String::toUpperCase)
|
||||
.accept("fileConsumer");
|
||||
}
|
||||
----
|
||||
|
||||
What we would need else is just to add their configuration into an `application.properties` (if necessary):
|
||||
|
||||
[source,properties]
|
||||
----
|
||||
http.path-pattern=/testPath
|
||||
spel.function.expression=new String(payload)
|
||||
file.consumer.name=test-data.txt
|
||||
----
|
||||
68
docs/pom.xml
68
docs/pom.xml
@@ -1,68 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>spring-cloud-function-docs</artifactId>
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-function-parent</artifactId>
|
||||
<version>4.1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<packaging>pom</packaging>
|
||||
<name>Spring Cloud Function Docs</name>
|
||||
<description>Spring Cloud Function Docs</description>
|
||||
<properties>
|
||||
<docs.main>spring-cloud-function</docs.main>
|
||||
<main.basedir>${basedir}/..</main.basedir>
|
||||
<maven.plugin.plugin.version>3.4</maven.plugin.plugin.version>
|
||||
<upload-docs-zip.phase>deploy</upload-docs-zip.phase>
|
||||
</properties>
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>docs</id>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>pl.project13.maven</groupId>
|
||||
<artifactId>git-commit-id-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-resources-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.asciidoctor</groupId>
|
||||
<artifactId>asciidoctor-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<attributes>
|
||||
<project-version>${project.version}</project-version>
|
||||
<spring-boot-version>${spring-boot.version}</spring-boot-version>
|
||||
</attributes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-antrun-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>build-helper-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-deploy-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
</project>
|
||||
@@ -1,330 +0,0 @@
|
||||
#!/bin/bash -x
|
||||
|
||||
set -e
|
||||
|
||||
# Set default props like MAVEN_PATH, ROOT_FOLDER etc.
|
||||
function set_default_props() {
|
||||
# The script should be executed from the root folder
|
||||
ROOT_FOLDER=`pwd`
|
||||
echo "Current folder is ${ROOT_FOLDER}"
|
||||
|
||||
if [[ ! -e "${ROOT_FOLDER}/.git" ]]; then
|
||||
echo "You're not in the root folder of the project!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Prop that will let commit the changes
|
||||
COMMIT_CHANGES="no"
|
||||
MAVEN_PATH=${MAVEN_PATH:-}
|
||||
echo "Path to Maven is [${MAVEN_PATH}]"
|
||||
REPO_NAME=${PWD##*/}
|
||||
echo "Repo name is [${REPO_NAME}]"
|
||||
SPRING_CLOUD_STATIC_REPO=${SPRING_CLOUD_STATIC_REPO:-git@github.com:spring-cloud/spring-cloud-static.git}
|
||||
echo "Spring Cloud Static repo is [${SPRING_CLOUD_STATIC_REPO}"
|
||||
}
|
||||
|
||||
# Check if gh-pages exists and docs have been built
|
||||
function check_if_anything_to_sync() {
|
||||
git remote set-url --push origin `git config remote.origin.url | sed -e 's/^git:/https:/'`
|
||||
|
||||
if ! (git remote set-branches --add origin gh-pages && git fetch -q); then
|
||||
echo "No gh-pages, so not syncing"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! [ -d docs/target/generated-docs ] && ! [ "${BUILD}" == "yes" ]; then
|
||||
echo "No gh-pages sources in docs/target/generated-docs, so not syncing"
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
function retrieve_current_branch() {
|
||||
# Code getting the name of the current branch. For master we want to publish as we did until now
|
||||
# https://stackoverflow.com/questions/1593051/how-to-programmatically-determine-the-current-checked-out-git-branch
|
||||
# If there is a branch already passed will reuse it - otherwise will try to find it
|
||||
CURRENT_BRANCH=${BRANCH}
|
||||
if [[ -z "${CURRENT_BRANCH}" ]] ; then
|
||||
CURRENT_BRANCH=$(git symbolic-ref -q HEAD)
|
||||
CURRENT_BRANCH=${CURRENT_BRANCH##refs/heads/}
|
||||
CURRENT_BRANCH=${CURRENT_BRANCH:-HEAD}
|
||||
fi
|
||||
echo "Current branch is [${CURRENT_BRANCH}]"
|
||||
git checkout ${CURRENT_BRANCH} || echo "Failed to check the branch... continuing with the script"
|
||||
}
|
||||
|
||||
# Switches to the provided value of the release version. We always prefix it with `v`
|
||||
function switch_to_tag() {
|
||||
git checkout v${VERSION}
|
||||
}
|
||||
|
||||
# Build the docs if switch is on
|
||||
function build_docs_if_applicable() {
|
||||
if [[ "${BUILD}" == "yes" ]] ; then
|
||||
./mvnw clean install -P docs -pl docs -DskipTests
|
||||
fi
|
||||
}
|
||||
|
||||
# Get the name of the `docs.main` property
|
||||
# Get whitelisted branches - assumes that a `docs` module is available under `docs` profile
|
||||
function retrieve_doc_properties() {
|
||||
MAIN_ADOC_VALUE=$("${MAVEN_PATH}"mvn -q \
|
||||
-Dexec.executable="echo" \
|
||||
-Dexec.args='${docs.main}' \
|
||||
--non-recursive \
|
||||
org.codehaus.mojo:exec-maven-plugin:1.3.1:exec)
|
||||
echo "Extracted 'main.adoc' from Maven build [${MAIN_ADOC_VALUE}]"
|
||||
|
||||
|
||||
WHITELIST_PROPERTY=${WHITELIST_PROPERTY:-"docs.whitelisted.branches"}
|
||||
WHITELISTED_BRANCHES_VALUE=$("${MAVEN_PATH}"mvn -q \
|
||||
-Dexec.executable="echo" \
|
||||
-Dexec.args="\${${WHITELIST_PROPERTY}}" \
|
||||
org.codehaus.mojo:exec-maven-plugin:1.3.1:exec \
|
||||
-P docs \
|
||||
-pl docs)
|
||||
echo "Extracted '${WHITELIST_PROPERTY}' from Maven build [${WHITELISTED_BRANCHES_VALUE}]"
|
||||
}
|
||||
|
||||
# Stash any outstanding changes
|
||||
function stash_changes() {
|
||||
git diff-index --quiet HEAD && dirty=$? || (echo "Failed to check if the current repo is dirty. Assuming that it is." && dirty="1")
|
||||
if [ "$dirty" != "0" ]; then git stash; fi
|
||||
}
|
||||
|
||||
# Switch to gh-pages branch to sync it with current branch
|
||||
function add_docs_from_target() {
|
||||
local DESTINATION_REPO_FOLDER
|
||||
if [[ -z "${DESTINATION}" && -z "${CLONE}" ]] ; then
|
||||
DESTINATION_REPO_FOLDER=${ROOT_FOLDER}
|
||||
elif [[ "${CLONE}" == "yes" ]]; then
|
||||
mkdir -p ${ROOT_FOLDER}/target
|
||||
local clonedStatic=${ROOT_FOLDER}/target/spring-cloud-static
|
||||
if [[ ! -e "${clonedStatic}/.git" ]]; then
|
||||
echo "Cloning Spring Cloud Static to target"
|
||||
git clone ${SPRING_CLOUD_STATIC_REPO} ${clonedStatic} && git checkout gh-pages
|
||||
else
|
||||
echo "Spring Cloud Static already cloned - will pull changes"
|
||||
cd ${clonedStatic} && git checkout gh-pages && git pull origin gh-pages
|
||||
fi
|
||||
DESTINATION_REPO_FOLDER=${clonedStatic}/${REPO_NAME}
|
||||
mkdir -p ${DESTINATION_REPO_FOLDER}
|
||||
else
|
||||
if [[ ! -e "${DESTINATION}/.git" ]]; then
|
||||
echo "[${DESTINATION}] is not a git repository"
|
||||
exit 1
|
||||
fi
|
||||
DESTINATION_REPO_FOLDER=${DESTINATION}/${REPO_NAME}
|
||||
mkdir -p ${DESTINATION_REPO_FOLDER}
|
||||
echo "Destination was provided [${DESTINATION}]"
|
||||
fi
|
||||
cd ${DESTINATION_REPO_FOLDER}
|
||||
git checkout gh-pages
|
||||
git pull origin gh-pages
|
||||
|
||||
# Add git branches
|
||||
###################################################################
|
||||
if [[ -z "${VERSION}" ]] ; then
|
||||
copy_docs_for_current_version
|
||||
else
|
||||
copy_docs_for_provided_version
|
||||
fi
|
||||
commit_changes_if_applicable
|
||||
}
|
||||
|
||||
|
||||
# Copies the docs by using the retrieved properties from Maven build
|
||||
function copy_docs_for_current_version() {
|
||||
if [[ "${CURRENT_BRANCH}" == "master" ]] ; then
|
||||
echo -e "Current branch is master - will copy the current docs only to the root folder"
|
||||
for f in docs/target/generated-docs/*; do
|
||||
file=${f#docs/target/generated-docs/*}
|
||||
if ! git ls-files -i -o --exclude-standard --directory | grep -q ^$file$; then
|
||||
# Not ignored...
|
||||
cp -rf $f ${ROOT_FOLDER}/
|
||||
git add -A ${ROOT_FOLDER}/$file
|
||||
fi
|
||||
done
|
||||
COMMIT_CHANGES="yes"
|
||||
else
|
||||
echo -e "Current branch is [${CURRENT_BRANCH}]"
|
||||
# https://stackoverflow.com/questions/29300806/a-bash-script-to-check-if-a-string-is-present-in-a-comma-separated-list-of-strin
|
||||
if [[ ",${WHITELISTED_BRANCHES_VALUE}," = *",${CURRENT_BRANCH},"* ]] ; then
|
||||
mkdir -p ${ROOT_FOLDER}/${CURRENT_BRANCH}
|
||||
echo -e "Branch [${CURRENT_BRANCH}] is whitelisted! Will copy the current docs to the [${CURRENT_BRANCH}] folder"
|
||||
for f in docs/target/generated-docs/*; do
|
||||
file=${f#docs/target/generated-docs/*}
|
||||
if ! git ls-files -i -o --exclude-standard --directory | grep -q ^$file$; then
|
||||
# Not ignored...
|
||||
# We want users to access 2.0.0.BUILD-SNAPSHOT/ instead of 1.0.0.RELEASE/spring-cloud.sleuth.html
|
||||
if [[ "${file}" == "${MAIN_ADOC_VALUE}.html" ]] ; then
|
||||
# We don't want to copy the spring-cloud-sleuth.html
|
||||
# we want it to be converted to index.html
|
||||
cp -rf $f ${ROOT_FOLDER}/${CURRENT_BRANCH}/index.html
|
||||
git add -A ${ROOT_FOLDER}/${CURRENT_BRANCH}/index.html
|
||||
else
|
||||
cp -rf $f ${ROOT_FOLDER}/${CURRENT_BRANCH}
|
||||
git add -A ${ROOT_FOLDER}/${CURRENT_BRANCH}/$file
|
||||
fi
|
||||
fi
|
||||
done
|
||||
COMMIT_CHANGES="yes"
|
||||
else
|
||||
echo -e "Branch [${CURRENT_BRANCH}] is not on the white list! Check out the Maven [${WHITELIST_PROPERTY}] property in
|
||||
[docs] module available under [docs] profile. Won't commit any changes to gh-pages for this branch."
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Copies the docs by using the explicitly provided version
|
||||
function copy_docs_for_provided_version() {
|
||||
local FOLDER=${DESTINATION_REPO_FOLDER}/${VERSION}
|
||||
mkdir -p ${FOLDER}
|
||||
echo -e "Current tag is [v${VERSION}] Will copy the current docs to the [${FOLDER}] folder"
|
||||
for f in ${ROOT_FOLDER}/docs/target/generated-docs/*; do
|
||||
file=${f#${ROOT_FOLDER}/docs/target/generated-docs/*}
|
||||
copy_docs_for_branch ${file} ${FOLDER}
|
||||
done
|
||||
COMMIT_CHANGES="yes"
|
||||
CURRENT_BRANCH="v${VERSION}"
|
||||
}
|
||||
|
||||
# Copies the docs from target to the provided destination
|
||||
# Params:
|
||||
# $1 - file from target
|
||||
# $2 - destination to which copy the files
|
||||
function copy_docs_for_branch() {
|
||||
local file=$1
|
||||
local destination=$2
|
||||
if ! git ls-files -i -o --exclude-standard --directory | grep -q ^${file}$; then
|
||||
# Not ignored...
|
||||
# We want users to access 2.0.0.BUILD-SNAPSHOT/ instead of 1.0.0.RELEASE/spring-cloud.sleuth.html
|
||||
if [[ ("${file}" == "${MAIN_ADOC_VALUE}.html") || ("${file}" == "${REPO_NAME}.html") ]] ; then
|
||||
# We don't want to copy the spring-cloud-sleuth.html
|
||||
# we want it to be converted to index.html
|
||||
cp -rf $f ${destination}/index.html
|
||||
git add -A ${destination}/index.html
|
||||
else
|
||||
cp -rf $f ${destination}
|
||||
git add -A ${destination}/$file
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
function commit_changes_if_applicable() {
|
||||
if [[ "${COMMIT_CHANGES}" == "yes" ]] ; then
|
||||
COMMIT_SUCCESSFUL="no"
|
||||
git commit -a -m "Sync docs from ${CURRENT_BRANCH} to gh-pages" && COMMIT_SUCCESSFUL="yes" || echo "Failed to commit changes"
|
||||
|
||||
# Uncomment the following push if you want to auto push to
|
||||
# the gh-pages branch whenever you commit to master locally.
|
||||
# This is a little extreme. Use with care!
|
||||
###################################################################
|
||||
if [[ "${COMMIT_SUCCESSFUL}" == "yes" ]] ; then
|
||||
git push origin gh-pages
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Switch back to the previous branch and exit block
|
||||
function checkout_previous_branch() {
|
||||
# If -version was provided we need to come back to root project
|
||||
cd ${ROOT_FOLDER}
|
||||
git checkout ${CURRENT_BRANCH} || echo "Failed to check the branch... continuing with the script"
|
||||
if [ "$dirty" != "0" ]; then git stash pop; fi
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Assert if properties have been properly passed
|
||||
function assert_properties() {
|
||||
echo "VERSION [${VERSION}], DESTINATION [${DESTINATION}], CLONE [${CLONE}]"
|
||||
if [[ "${VERSION}" != "" && (-z "${DESTINATION}" && -z "${CLONE}") ]] ; then echo "Version was set but destination / clone was not!"; exit 1;fi
|
||||
if [[ ("${DESTINATION}" != "" && "${CLONE}" != "") && -z "${VERSION}" ]] ; then echo "Destination / clone was set but version was not!"; exit 1;fi
|
||||
if [[ "${DESTINATION}" != "" && "${CLONE}" == "yes" ]] ; then echo "Destination and clone was set. Pick one!"; exit 1;fi
|
||||
}
|
||||
|
||||
# Prints the usage
|
||||
function print_usage() {
|
||||
cat <<EOF
|
||||
The idea of this script is to update gh-pages branch with the generated docs. Without any options
|
||||
the script will work in the following manner:
|
||||
|
||||
- if there's no gh-pages / target for docs module then the script ends
|
||||
- for master branch the generated docs are copied to the root of gh-pages branch
|
||||
- for any other branch (if that branch is whitelisted) a subfolder with branch name is created
|
||||
and docs are copied there
|
||||
- if the version switch is passed (-v) then a tag with (v) prefix will be retrieved and a folder
|
||||
with that version number will be created in the gh-pages branch. WARNING! No whitelist verification will take place
|
||||
- if the destination switch is passed (-d) then the script will check if the provided dir is a git repo and then will
|
||||
switch to gh-pages of that repo and copy the generated docs to `docs/<project-name>/<version>`
|
||||
- if the destination switch is passed (-d) then the script will check if the provided dir is a git repo and then will
|
||||
switch to gh-pages of that repo and copy the generated docs to `docs/<project-name>/<version>`
|
||||
|
||||
USAGE:
|
||||
|
||||
You can use the following options:
|
||||
|
||||
-v|--version - the script will apply the whole procedure for a particular library version
|
||||
-d|--destination - the root of destination folder where the docs should be copied. You have to use the full path.
|
||||
E.g. point to spring-cloud-static folder. Can't be used with (-c)
|
||||
-b|--build - will run the standard build process after checking out the branch
|
||||
-c|--clone - will automatically clone the spring-cloud-static repo instead of providing the destination.
|
||||
Obviously can't be used with (-d)
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
|
||||
# ==========================================
|
||||
# ____ ____ _____ _____ _____ _______
|
||||
# / ____|/ ____| __ \|_ _| __ \__ __|
|
||||
# | (___ | | | |__) | | | | |__) | | |
|
||||
# \___ \| | | _ / | | | ___/ | |
|
||||
# ____) | |____| | \ \ _| |_| | | |
|
||||
# |_____/ \_____|_| \_\_____|_| |_|
|
||||
#
|
||||
# ==========================================
|
||||
|
||||
while [[ $# > 0 ]]
|
||||
do
|
||||
key="$1"
|
||||
case ${key} in
|
||||
-v|--version)
|
||||
VERSION="$2"
|
||||
shift # past argument
|
||||
;;
|
||||
-d|--destination)
|
||||
DESTINATION="$2"
|
||||
shift # past argument
|
||||
;;
|
||||
-b|--build)
|
||||
BUILD="yes"
|
||||
;;
|
||||
-c|--clone)
|
||||
CLONE="yes"
|
||||
;;
|
||||
-h|--help)
|
||||
print_usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Invalid option: [$1]"
|
||||
print_usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
shift # past argument or value
|
||||
done
|
||||
|
||||
assert_properties
|
||||
set_default_props
|
||||
check_if_anything_to_sync
|
||||
if [[ -z "${VERSION}" ]] ; then
|
||||
retrieve_current_branch
|
||||
else
|
||||
switch_to_tag
|
||||
fi
|
||||
build_docs_if_applicable
|
||||
retrieve_doc_properties
|
||||
stash_changes
|
||||
add_docs_from_target
|
||||
checkout_previous_branch
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 589 KiB |
@@ -1 +0,0 @@
|
||||
please remove once this directory is not empty
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 158 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 51 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 45 KiB |
@@ -1,37 +0,0 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
base_dir = File.join(File.dirname(__FILE__),'../../..')
|
||||
src_dir = File.join(base_dir, "/src/main/asciidoc")
|
||||
require 'asciidoctor'
|
||||
require 'optparse'
|
||||
|
||||
options = {}
|
||||
file = "#{src_dir}/README.adoc"
|
||||
|
||||
OptionParser.new do |o|
|
||||
o.on('-o OUTPUT_FILE', 'Output file (default is stdout)') { |file| options[:to_file] = file unless file=='-' }
|
||||
o.on('-h', '--help') { puts o; exit }
|
||||
o.parse!
|
||||
end
|
||||
|
||||
file = ARGV[0] if ARGV.length>0
|
||||
|
||||
# Copied from https://github.com/asciidoctor/asciidoctor-extensions-lab/blob/master/scripts/asciidoc-coalescer.rb
|
||||
doc = Asciidoctor.load_file file, safe: :unsafe, header_only: true, attributes: options[:attributes]
|
||||
header_attr_names = (doc.instance_variable_get :@attributes_modified).to_a
|
||||
header_attr_names.each {|k| doc.attributes[%(#{k}!)] = '' unless doc.attr? k }
|
||||
attrs = doc.attributes
|
||||
attrs['allow-uri-read'] = true
|
||||
puts attrs
|
||||
|
||||
out = "// Do not edit this file (e.g. go instead to src/main/asciidoc)\n\n"
|
||||
doc = Asciidoctor.load_file file, safe: :unsafe, parse: false, attributes: attrs
|
||||
out << doc.reader.read
|
||||
|
||||
unless options[:to_file]
|
||||
puts out
|
||||
else
|
||||
File.open(options[:to_file],'w+') do |file|
|
||||
file.write(out)
|
||||
end
|
||||
end
|
||||
76
pipeline.yml
76
pipeline.yml
@@ -1,76 +0,0 @@
|
||||
# fly --target cloud login --concourse-url https://ci.spring.io --team-name spring-cloud
|
||||
# fly --target cloud set-pipeline --config pipeline.yml --pipeline spring-cloud-function --load-vars-from credentials.yml
|
||||
---
|
||||
jobs:
|
||||
- name: build
|
||||
plan:
|
||||
- get: source
|
||||
trigger: true
|
||||
- task: maven
|
||||
config:
|
||||
platform: linux
|
||||
image_resource:
|
||||
type: docker-image
|
||||
source:
|
||||
repository: springio/maven-base
|
||||
inputs:
|
||||
- name: source
|
||||
caches:
|
||||
- path: source/.m2
|
||||
run:
|
||||
dir: source
|
||||
path: sh
|
||||
args:
|
||||
- -c
|
||||
- |
|
||||
rm -rf ~/.m2
|
||||
ln -s $(pwd)/.m2 ~/.m2
|
||||
./mvnw deploy -s .mvn/settings.xml -Dgpg.passphrase="${passphrase}"
|
||||
params:
|
||||
TERM: -dumb
|
||||
passphrase: {{passphrase}}
|
||||
sonatype_username: {{sonatype-username}}
|
||||
sonatype_password: {{sonatype-password}}
|
||||
spring_username: {{spring-username}}
|
||||
spring_password: {{spring-password}}
|
||||
on_failure: *slack-failure
|
||||
on_success: *slack-success
|
||||
|
||||
slack-failure: &slack-failure
|
||||
put: slack
|
||||
params:
|
||||
channel: spring-cloud-firehose
|
||||
attachments:
|
||||
- color: danger
|
||||
fallback: "$BUILD_PIPELINE_NAME/$BUILD_JOB_NAME #$BUILD_NAME <FAILURE>"
|
||||
text: "Build has failed"
|
||||
title: "$BUILD_PIPELINE_NAME/$BUILD_JOB_NAME #$BUILD_NAME"
|
||||
title_link: $ATC_EXTERNAL_URL/teams/$BUILD_TEAM_NAME/pipelines/$BUILD_PIPELINE_NAME/jobs/$BUILD_JOB_NAME/builds/$BUILD_NAME
|
||||
|
||||
slack-success: &slack-success
|
||||
put: slack
|
||||
params:
|
||||
channel: spring-cloud-firehose
|
||||
attachments:
|
||||
- color: good
|
||||
fallback: "$BUILD_PIPELINE_NAME/$BUILD_JOB_NAME #$BUILD_NAME <SUCCESS>"
|
||||
text: "Build has succeeded"
|
||||
title: "$BUILD_PIPELINE_NAME/$BUILD_JOB_NAME #$BUILD_NAME"
|
||||
title_link: $ATC_EXTERNAL_URL/teams/$BUILD_TEAM_NAME/pipelines/$BUILD_PIPELINE_NAME/jobs/$BUILD_JOB_NAME/builds/$BUILD_NAME
|
||||
|
||||
resources:
|
||||
- name: source
|
||||
type: git
|
||||
source:
|
||||
uri: https://github.com/spring-cloud/spring-cloud-function.git
|
||||
- name: slack
|
||||
type: slack-notification
|
||||
source:
|
||||
url: {{slack-url}}
|
||||
|
||||
resource_types:
|
||||
- name: slack-notification
|
||||
type: docker-image
|
||||
source:
|
||||
repository: nebhale/slack-notification-resource
|
||||
|
||||
241
pom.xml
241
pom.xml
@@ -1,177 +1,58 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>spring-cloud-function-parent</artifactId>
|
||||
<name>Spring Cloud Function Parent</name>
|
||||
<version>4.1.0-SNAPSHOT</version>
|
||||
<packaging>pom</packaging>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-function-docs-build</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-build</artifactId>
|
||||
<version>4.1.0-SNAPSHOT</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
<name>Spring Cloud Function Docs Build</name>
|
||||
<description>Builds Spring Cloud Function Docs.</description>
|
||||
<url>https://spring.io/projects/spring-cloud-function</url>
|
||||
<scm>
|
||||
<connection>scm:git:https://github.com/spring-cloud/spring-cloud-function.git
|
||||
</connection>
|
||||
<developerConnection>
|
||||
scm:git:git@github.com:spring-cloud/spring-cloud-function.git
|
||||
</developerConnection>
|
||||
<url>https://github.com/spring-cloud/spring-cloud-function</url>
|
||||
</scm>
|
||||
<issueManagement>
|
||||
<url>https://github.com/spring-cloud/spring-cloud-function/issues</url>
|
||||
</issueManagement>
|
||||
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
<maven.compiler.source>${java.version}</maven.compiler.source>
|
||||
<maven.compiler.target>${java.version}</maven.compiler.target>
|
||||
<wrapper.version>1.0.27.RELEASE</wrapper.version>
|
||||
<docs.main>spring-cloud-function</docs.main>
|
||||
<maven-checkstyle-plugin.failsOnError>true</maven-checkstyle-plugin.failsOnError>
|
||||
<maven-checkstyle-plugin.failsOnViolation>true
|
||||
</maven-checkstyle-plugin.failsOnViolation>
|
||||
<maven-checkstyle-plugin.includeTestSourceDirectory>true
|
||||
</maven-checkstyle-plugin.includeTestSourceDirectory>
|
||||
<io.spring.maven.antora-version>0.0.3</io.spring.maven.antora-version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-function-dependencies</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-checkstyle-plugin</artifactId>
|
||||
<groupId>io.spring.maven.antora</groupId>
|
||||
<artifactId>antora-maven-plugin</artifactId>
|
||||
<version>${io.spring.maven.antora-version}</version>
|
||||
<extensions>true</extensions>
|
||||
<configuration>
|
||||
<options>
|
||||
<option>--to-dir=target/antora/site</option>
|
||||
<option>--stacktrace</option>
|
||||
<option>--fetch</option>
|
||||
</options>
|
||||
<environment>
|
||||
<ALGOLIA_API_KEY>9d489079e5ec46dbb238909fee5c9c29</ALGOLIA_API_KEY>
|
||||
<ALGOLIA_APP_ID>WB1FQYI187</ALGOLIA_APP_ID>
|
||||
<ALGOLIA_INDEX_NAME>springcloudfunction</ALGOLIA_INDEX_NAME>
|
||||
</environment>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<argLine>--add-opens java.base/java.util=ALL-UNNAMED</argLine>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
<pluginManagement>
|
||||
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>flatten-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<inherited>false</inherited>
|
||||
<configuration>
|
||||
<aggregate>true</aggregate>
|
||||
<excludePackageNames>com.example,functions,example
|
||||
</excludePackageNames>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>aggregate</id>
|
||||
<goals>
|
||||
<goal>aggregate-jar</goal>
|
||||
</goals>
|
||||
<phase>package</phase>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<version>${spring-boot.version}</version>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot.experimental</groupId>
|
||||
<artifactId>spring-boot-thin-layout</artifactId>
|
||||
<version>${wrapper.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.eclipse.m2e</groupId>
|
||||
<artifactId>lifecycle-mapping</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<configuration>
|
||||
<lifecycleMappingMetadata>
|
||||
<pluginExecutions>
|
||||
<pluginExecution>
|
||||
<pluginExecutionFilter>
|
||||
<groupId>
|
||||
org.codehaus.mojo
|
||||
</groupId>
|
||||
<artifactId>
|
||||
flatten-maven-plugin
|
||||
</artifactId>
|
||||
<versionRange>
|
||||
[1.0.0,)
|
||||
</versionRange>
|
||||
<goals>
|
||||
<goal>flatten</goal>
|
||||
</goals>
|
||||
</pluginExecutionFilter>
|
||||
<action>
|
||||
<ignore></ignore>
|
||||
</action>
|
||||
</pluginExecution>
|
||||
</pluginExecutions>
|
||||
</lifecycleMappingMetadata>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
</build>
|
||||
|
||||
<reporting>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-checkstyle-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</reporting>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>core</id>
|
||||
<modules>
|
||||
<module>spring-cloud-function-dependencies</module>
|
||||
<module>spring-cloud-function-core</module>
|
||||
<module>spring-cloud-function-context</module>
|
||||
<module>spring-cloud-function-web</module>
|
||||
</modules>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>all</id>
|
||||
<activation>
|
||||
<activeByDefault>true</activeByDefault>
|
||||
</activation>
|
||||
<modules>
|
||||
<module>spring-cloud-function-dependencies</module>
|
||||
<module>spring-cloud-function-core</module>
|
||||
<module>spring-cloud-function-context</module>
|
||||
<module>spring-cloud-function-web</module>
|
||||
<module>spring-cloud-starter-function-web</module>
|
||||
<module>spring-cloud-starter-function-webflux</module>
|
||||
<module>spring-cloud-function-samples</module>
|
||||
<module>spring-cloud-function-deployer</module>
|
||||
<module>spring-cloud-function-adapters</module>
|
||||
<module>spring-cloud-function-integration</module>
|
||||
<module>spring-cloud-function-rsocket</module>
|
||||
<module>spring-cloud-function-kotlin</module>
|
||||
<module>docs</module>
|
||||
</modules>
|
||||
</profile>
|
||||
</profiles>
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>spring-snapshots</id>
|
||||
<name>Spring Snapshots</name>
|
||||
<id>spring-snapshot</id>
|
||||
<url>https://repo.spring.io/snapshot</url>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
@@ -181,49 +62,9 @@
|
||||
</releases>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>spring-milestones</id>
|
||||
<name>Spring Milestones</name>
|
||||
<id>spring-milestone</id>
|
||||
<url>https://repo.spring.io/milestone</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>spring-releases</id>
|
||||
<name>Spring Releases</name>
|
||||
<url>https://repo.spring.io/release</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
<pluginRepositories>
|
||||
<pluginRepository>
|
||||
<id>spring-snapshots</id>
|
||||
<name>Spring Snapshots</name>
|
||||
<url>https://repo.spring.io/snapshot</url>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
<releases>
|
||||
<enabled>false</enabled>
|
||||
</releases>
|
||||
</pluginRepository>
|
||||
<pluginRepository>
|
||||
<id>spring-milestones</id>
|
||||
<name>Spring Milestones</name>
|
||||
<url>https://repo.spring.io/milestone</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</pluginRepository>
|
||||
<pluginRepository>
|
||||
<id>spring-releases</id>
|
||||
<name>Spring Releases</name>
|
||||
<url>https://repo.spring.io/release</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</pluginRepository>
|
||||
</pluginRepositories>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>spring-cloud-function-adapter-parent</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-function-parent</artifactId>
|
||||
<version>4.1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<name>spring-cloud-function-adapter-parent</name>
|
||||
|
||||
<modules>
|
||||
<module>spring-cloud-function-adapter-aws</module>
|
||||
<!-- <module>spring-cloud-function-adapter-azure-web/sample/pet-store</module>-->
|
||||
<module>spring-cloud-function-adapter-azure</module>
|
||||
<module>spring-cloud-function-adapter-gcp</module>
|
||||
<module>spring-cloud-function-grpc</module>
|
||||
<module>spring-cloud-function-grpc-cloudevent-ext</module>
|
||||
<module>spring-cloud-function-serverless-web</module>
|
||||
<module>spring-cloud-function-adapter-azure-web</module>
|
||||
</modules>
|
||||
|
||||
</project>
|
||||
@@ -1,7 +0,0 @@
|
||||
////
|
||||
DO NOT EDIT THIS FILE. IT WAS GENERATED.
|
||||
Manual changes to this file will be lost when it is generated again.
|
||||
Edit the files in the src/main/asciidoc/ directory instead.
|
||||
////
|
||||
|
||||
This project provides an adapter layer for a Spring Cloud Function application onto AWS Lambda. You can write an app with a single `@Bean` of type `Function`, `Consumer` or `Supplier` and it will be deployable in AWS if you get the JAR file laid out right. The best way to make it work is to include `spring-cloud-function-context` as a dependency, but not the higher level adapters (e.g. `spring-cloud-function-stream`).
|
||||
@@ -1,162 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>spring-cloud-function-adapter-aws</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>spring-cloud-function-adapter-aws</name>
|
||||
<description>AWS Lambda Adapter for Spring Cloud Function</description>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-function-adapter-parent</artifactId>
|
||||
<version>4.1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<aws-lambda-events.version>3.9.0</aws-lambda-events.version>
|
||||
<aws-java-sdk.version>1.12.29</aws-java-sdk.version>
|
||||
<aws-kinesis-deaggregator.version>1.0.3</aws-kinesis-deaggregator.version>
|
||||
<aws-kinesis-aggregator.version>1.0.3</aws-kinesis-aggregator.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-function-context</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>aws-lambda-java-log4j</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>aws-lambda-java-serialization</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>aws-lambda-java-core</artifactId>
|
||||
<version>1.2.1</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>amazon-kinesis-client</artifactId>
|
||||
<version>1.14.4</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>aws-lambda-java-events</artifactId>
|
||||
<version>${aws-lambda-events.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>aws-java-sdk-kinesis</artifactId>
|
||||
<version>${aws-java-sdk.version}</version>
|
||||
<scope>provided</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>amazon-kinesis-deaggregator</artifactId>
|
||||
<version>${aws-kinesis-deaggregator.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>aws-lambda-java-events</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
<optional>true</optional>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>org.springframework</groupId>-->
|
||||
<!-- <artifactId>spring-web</artifactId>-->
|
||||
<!-- </dependency>-->
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-webmvc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-function-web</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>amazon-kinesis-aggregator</artifactId>
|
||||
<version>${aws-kinesis-aggregator.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>spring-snapshots</id>
|
||||
<name>Spring Snapshots</name>
|
||||
<url>https://repo.spring.io/snapshot</url>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>spring-milestones</id>
|
||||
<name>Spring Milestones</name>
|
||||
<url>https://repo.spring.io/milestone</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
<pluginRepositories>
|
||||
<pluginRepository>
|
||||
<id>spring-snapshots</id>
|
||||
<name>Spring Snapshots</name>
|
||||
<url>https://repo.spring.io/snapshot</url>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</pluginRepository>
|
||||
<pluginRepository>
|
||||
<id>spring-milestones</id>
|
||||
<name>Spring Milestones</name>
|
||||
<url>https://repo.spring.io/milestone</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</pluginRepository>
|
||||
</pluginRepositories>
|
||||
|
||||
</project>
|
||||
@@ -1,43 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.function.adapter.aws;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import org.springframework.cloud.function.json.JacksonMapper;
|
||||
import org.springframework.cloud.function.json.JsonMapper;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Oleg Zhurakousky
|
||||
* @since 3.2
|
||||
*
|
||||
*/
|
||||
public class AWSCompanionAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public AWSTypesMessageConverter awsTypesMessageConverter(GenericApplicationContext applicationContext) {
|
||||
JsonMapper jsonMapper = CollectionUtils.isEmpty(applicationContext.getBeansOfType(JsonMapper.class).values())
|
||||
? new JacksonMapper(new ObjectMapper())
|
||||
: applicationContext.getBean(JsonMapper.class);
|
||||
return new AWSTypesMessageConverter(jsonMapper);
|
||||
}
|
||||
}
|
||||
@@ -1,253 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.function.adapter.aws;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import com.amazonaws.services.lambda.runtime.Context;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.cloud.function.context.catalog.FunctionTypeUtils;
|
||||
import org.springframework.cloud.function.json.JsonMapper;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Oleg Zhurakousky
|
||||
*
|
||||
*/
|
||||
public final class AWSLambdaUtils {
|
||||
|
||||
private static Log logger = LogFactory.getLog(AWSLambdaUtils.class);
|
||||
|
||||
static final String AWS_API_GATEWAY = "aws-api-gateway";
|
||||
|
||||
static final String AWS_EVENT = "aws-event";
|
||||
|
||||
static final String IS_BASE64_ENCODED = "isBase64Encoded";
|
||||
|
||||
static final String STATUS_CODE = "statusCode";
|
||||
|
||||
static final String BODY = "body";
|
||||
|
||||
static final String HEADERS = "headers";
|
||||
|
||||
/**
|
||||
* The name of the headers that stores AWS Context object.
|
||||
*/
|
||||
public static final String AWS_CONTEXT = "aws-context";
|
||||
|
||||
private AWSLambdaUtils() {
|
||||
|
||||
}
|
||||
|
||||
static boolean isSupportedAWSType(Type inputType) {
|
||||
if (FunctionTypeUtils.isMessage(inputType) || FunctionTypeUtils.isPublisher(inputType)) {
|
||||
inputType = FunctionTypeUtils.getImmediateGenericType(inputType, 0);
|
||||
}
|
||||
return FunctionTypeUtils.getRawType(inputType).getPackage().getName().startsWith("com.amazonaws.services.lambda.runtime.events");
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
public static Message generateMessage(InputStream payload, Type inputType, boolean isSupplier, JsonMapper jsonMapper, Context context) throws IOException {
|
||||
if (inputType != null && FunctionTypeUtils.isMessage(inputType)) {
|
||||
inputType = FunctionTypeUtils.getImmediateGenericType(inputType, 0);
|
||||
}
|
||||
if (inputType != null && InputStream.class.isAssignableFrom(FunctionTypeUtils.getRawType(inputType))) {
|
||||
MessageBuilder msgBuilder = MessageBuilder.withPayload(payload);
|
||||
if (context != null) {
|
||||
msgBuilder.setHeader(AWSLambdaUtils.AWS_CONTEXT, context);
|
||||
}
|
||||
return msgBuilder.build();
|
||||
}
|
||||
else {
|
||||
return generateMessage(StreamUtils.copyToByteArray(payload), inputType, isSupplier, jsonMapper, context);
|
||||
}
|
||||
}
|
||||
|
||||
public static Message<byte[]> generateMessage(byte[] payload, Type inputType, boolean isSupplier, JsonMapper jsonMapper) {
|
||||
return generateMessage(payload, inputType, isSupplier, jsonMapper, null);
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
public static Message<byte[]> generateMessage(byte[] payload, Type inputType, boolean isSupplier, JsonMapper jsonMapper, Context context) {
|
||||
if (logger.isInfoEnabled()) {
|
||||
logger.info("Received: " + new String(payload, StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
|
||||
Object structMessage = jsonMapper.fromJson(payload, Object.class);
|
||||
boolean isApiGateway = structMessage instanceof Map
|
||||
&& (((Map<String, Object>) structMessage).containsKey("httpMethod") ||
|
||||
(((Map<String, Object>) structMessage).containsKey("routeKey") && ((Map) structMessage).containsKey("version")));
|
||||
|
||||
Message<byte[]> requestMessage;
|
||||
MessageBuilder<byte[]> builder = MessageBuilder.withPayload(payload);
|
||||
if (isApiGateway) {
|
||||
builder.setHeader(AWSLambdaUtils.AWS_API_GATEWAY, true);
|
||||
if (JsonMapper.isJsonStringRepresentsCollection(((Map) structMessage).get("body"))) {
|
||||
builder.setHeader("payload", ((Map) structMessage).get("body"));
|
||||
}
|
||||
}
|
||||
if (!isSupplier && AWSLambdaUtils.isSupportedAWSType(inputType)) {
|
||||
builder.setHeader(AWSLambdaUtils.AWS_EVENT, true);
|
||||
}
|
||||
if (context != null) {
|
||||
builder.setHeader(AWSLambdaUtils.AWS_CONTEXT, context);
|
||||
}
|
||||
//
|
||||
if (structMessage instanceof Map && ((Map<String, Object>) structMessage).containsKey("headers")) {
|
||||
builder.copyHeaders((Map<String, Object>) ((Map<String, Object>) structMessage).get("headers"));
|
||||
}
|
||||
requestMessage = builder.build();
|
||||
return requestMessage;
|
||||
}
|
||||
|
||||
private static byte[] extractPayload(Message<Object> msg, JsonMapper objectMapper) {
|
||||
if (msg.getPayload() instanceof byte[]) {
|
||||
return (byte[]) msg.getPayload();
|
||||
}
|
||||
else {
|
||||
return objectMapper.toJson(msg.getPayload());
|
||||
}
|
||||
}
|
||||
|
||||
private static Object convertFromJsonIfNecessary(Object value, JsonMapper objectMapper) {
|
||||
if (JsonMapper.isJsonString(value)) {
|
||||
return objectMapper.fromJson(value, Object.class);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static byte[] generateOutputFromObject(Message<?> requestMessage, Object output, JsonMapper objectMapper, Type functionOutputType) {
|
||||
Message<byte[]> responseMessage = null;
|
||||
if (output instanceof Publisher<?>) {
|
||||
List<Object> result = new ArrayList<>();
|
||||
Message<?> lastMessage = null;
|
||||
for (Object item : Flux.from((Publisher<?>) output).toIterable()) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Response value: " + item);
|
||||
}
|
||||
if (item instanceof Message<?> message) {
|
||||
result.add(convertFromJsonIfNecessary(message.getPayload(), objectMapper));
|
||||
lastMessage = message;
|
||||
}
|
||||
else {
|
||||
result.add(convertFromJsonIfNecessary(item, objectMapper));
|
||||
}
|
||||
}
|
||||
|
||||
byte[] resultPayload;
|
||||
if (result.size() == 1) {
|
||||
resultPayload = objectMapper.toJson(result.get(0));
|
||||
}
|
||||
else if (result.size() > 1) {
|
||||
resultPayload = objectMapper.toJson(result);
|
||||
}
|
||||
else {
|
||||
resultPayload = null;
|
||||
}
|
||||
|
||||
if (resultPayload != null) {
|
||||
MessageBuilder<byte[]> messageBuilder = MessageBuilder.withPayload(resultPayload);
|
||||
if (lastMessage != null) {
|
||||
messageBuilder.copyHeaders(lastMessage.getHeaders());
|
||||
}
|
||||
responseMessage = messageBuilder.build();
|
||||
}
|
||||
}
|
||||
else {
|
||||
responseMessage = (Message<byte[]>) output;
|
||||
}
|
||||
return generateOutput(requestMessage, responseMessage, objectMapper, functionOutputType);
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
public static byte[] generateOutput(Message requestMessage, Message<?> responseMessage,
|
||||
JsonMapper objectMapper, Type functionOutputType) {
|
||||
|
||||
Class<?> outputClass = FunctionTypeUtils.getRawType(functionOutputType);
|
||||
if (outputClass != null) {
|
||||
String outputClassName = outputClass.getName();
|
||||
if (outputClassName.equals("com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPResponse") ||
|
||||
outputClassName.equals("com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent") ||
|
||||
outputClassName.equals("com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerResponseEvent") ||
|
||||
outputClassName.equals("com.amazonaws.services.lambda.runtime.events.IamPolicyResponse")) {
|
||||
return extractPayload((Message<Object>) responseMessage, objectMapper);
|
||||
}
|
||||
}
|
||||
|
||||
byte[] responseBytes = responseMessage == null ? "\"OK\"".getBytes() : extractPayload((Message<Object>) responseMessage, objectMapper);
|
||||
if (requestMessage.getHeaders().containsKey(AWS_API_GATEWAY) && ((boolean) requestMessage.getHeaders().get(AWS_API_GATEWAY))) {
|
||||
Map<String, Object> response = new HashMap<String, Object>();
|
||||
response.put(IS_BASE64_ENCODED, responseMessage != null && responseMessage.getHeaders().containsKey(IS_BASE64_ENCODED)
|
||||
? responseMessage.getHeaders().get(IS_BASE64_ENCODED) : false);
|
||||
|
||||
AtomicReference<MessageHeaders> headers = new AtomicReference<>();
|
||||
int statusCode = HttpStatus.OK.value();
|
||||
if (responseMessage != null) {
|
||||
headers.set(responseMessage.getHeaders());
|
||||
statusCode = headers.get().containsKey(STATUS_CODE)
|
||||
? (int) headers.get().get(STATUS_CODE)
|
||||
: HttpStatus.OK.value();
|
||||
}
|
||||
|
||||
response.put(STATUS_CODE, statusCode);
|
||||
if (isRequestKinesis(requestMessage)) {
|
||||
HttpStatus httpStatus = HttpStatus.valueOf(statusCode);
|
||||
response.put("statusDescription", httpStatus.toString());
|
||||
}
|
||||
|
||||
String body = responseMessage == null
|
||||
? "\"OK\"" : new String(extractPayload((Message<Object>) responseMessage, objectMapper), StandardCharsets.UTF_8);
|
||||
response.put(BODY, body);
|
||||
if (responseMessage != null) {
|
||||
Map<String, String> responseHeaders = new HashMap<>();
|
||||
headers.get().keySet().forEach(key -> responseHeaders.put(key, headers.get().get(key).toString()));
|
||||
response.put(HEADERS, responseHeaders);
|
||||
}
|
||||
|
||||
try {
|
||||
responseBytes = objectMapper.toJson(response);
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new IllegalStateException("Failed to serialize AWS Lambda output", e);
|
||||
}
|
||||
}
|
||||
return responseBytes;
|
||||
}
|
||||
|
||||
private static boolean isRequestKinesis(Message<Object> requestMessage) {
|
||||
return requestMessage.getHeaders().containsKey("Records");
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.function.adapter.aws;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
|
||||
import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer;
|
||||
import com.amazonaws.services.lambda.runtime.serialization.events.LambdaEventSerializers;
|
||||
|
||||
import org.springframework.cloud.function.cloudevent.CloudEventMessageUtils;
|
||||
import org.springframework.cloud.function.context.config.JsonMessageConverter;
|
||||
import org.springframework.cloud.function.json.JsonMapper;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.messaging.converter.MessageConverter;
|
||||
import org.springframework.util.MimeType;
|
||||
|
||||
/**
|
||||
* Implementation of {@link MessageConverter} which uses Jackson or Gson libraries to do the
|
||||
* actual conversion via {@link JsonMapper} instance.
|
||||
*
|
||||
* @author Oleg Zhurakousky
|
||||
*
|
||||
* @since 3.2
|
||||
*/
|
||||
class AWSTypesMessageConverter extends JsonMessageConverter {
|
||||
|
||||
private final JsonMapper jsonMapper;
|
||||
|
||||
AWSTypesMessageConverter(JsonMapper jsonMapper) {
|
||||
this(jsonMapper, new MimeType("application", "json"), new MimeType(CloudEventMessageUtils.APPLICATION_CLOUDEVENTS.getType(),
|
||||
CloudEventMessageUtils.APPLICATION_CLOUDEVENTS.getSubtype() + "+json"));
|
||||
}
|
||||
|
||||
AWSTypesMessageConverter(JsonMapper jsonMapper, MimeType... supportedMimeTypes) {
|
||||
super(jsonMapper, supportedMimeTypes);
|
||||
this.jsonMapper = jsonMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean canConvertFrom(Message<?> message, @Nullable Class<?> targetClass) {
|
||||
if (message.getHeaders().containsKey(AWSLambdaUtils.AWS_API_GATEWAY)) {
|
||||
return ((boolean) message.getHeaders().get(AWSLambdaUtils.AWS_API_GATEWAY));
|
||||
}
|
||||
if (message.getHeaders().containsKey(AWSLambdaUtils.AWS_EVENT)) {
|
||||
return ((boolean) message.getHeaders().get(AWSLambdaUtils.AWS_EVENT));
|
||||
}
|
||||
//TODO Do we really need the ^^ above? It seems like the line below dows the trick
|
||||
else if (targetClass.getPackage().getName().startsWith("com.amazonaws.services.lambda.runtime.events")) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object convertFromInternal(Message<?> message, Class<?> targetClass, @Nullable Object conversionHint) {
|
||||
if (message.getPayload().getClass().isAssignableFrom(targetClass)) {
|
||||
return message.getPayload();
|
||||
}
|
||||
|
||||
if (targetClass.getPackage().getName().startsWith("com.amazonaws.services.lambda.runtime.events")) {
|
||||
PojoSerializer<?> serializer = LambdaEventSerializers.serializerFor(targetClass, Thread.currentThread().getContextClassLoader());
|
||||
Object event = serializer.fromJson(new ByteArrayInputStream((byte[]) message.getPayload()));
|
||||
return event;
|
||||
}
|
||||
else {
|
||||
Map<String, String> structMessage = this.jsonMapper.fromJson(message.getPayload(), Map.class);
|
||||
if (targetClass.isAssignableFrom(Map.class)) {
|
||||
return structMessage;
|
||||
}
|
||||
else {
|
||||
Object body;
|
||||
if (message.getHeaders().containsKey("payload")) {
|
||||
body = message.getPayload();
|
||||
}
|
||||
else {
|
||||
body = structMessage.get("body");
|
||||
}
|
||||
Object convertedResult = this.jsonMapper.fromJson(body, targetClass);
|
||||
return convertedResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean canConvertTo(Object payload, @Nullable MessageHeaders headers) {
|
||||
if (!supportsMimeType(headers)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected Object convertToInternal(Object payload, @Nullable MessageHeaders headers,
|
||||
@Nullable Object conversionHint) {
|
||||
if (payload instanceof String && headers.containsKey(AWSLambdaUtils.IS_BASE64_ENCODED) && (boolean) headers.get(AWSLambdaUtils.IS_BASE64_ENCODED)) {
|
||||
return ((String) payload).getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
return jsonMapper.toJson(payload);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,279 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.function.adapter.aws;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.net.SocketException;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
|
||||
import org.springframework.cloud.function.context.FunctionCatalog;
|
||||
import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper;
|
||||
import org.springframework.cloud.function.context.config.RoutingFunction;
|
||||
import org.springframework.cloud.function.json.JsonMapper;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.SmartLifecycle;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.RequestEntity;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import static org.apache.http.HttpHeaders.USER_AGENT;
|
||||
|
||||
/**
|
||||
* Event loop and necessary configurations to support AWS Lambda
|
||||
* Custom Runtime - https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html.
|
||||
*
|
||||
* @author Oleg Zhurakousky
|
||||
* @author Mark Sailes
|
||||
* @author Rahul Lokurte
|
||||
* @since 3.1.1
|
||||
*
|
||||
*/
|
||||
public final class CustomRuntimeEventLoop implements SmartLifecycle {
|
||||
|
||||
private static Log logger = LogFactory.getLog(CustomRuntimeEventLoop.class);
|
||||
|
||||
static final String LAMBDA_VERSION_DATE = "2018-06-01";
|
||||
private static final String LAMBDA_ERROR_URL_TEMPLATE = "http://{0}/{1}/runtime/invocation/{2}/error";
|
||||
private static final String LAMBDA_RUNTIME_URL_TEMPLATE = "http://{0}/{1}/runtime/invocation/next";
|
||||
private static final String LAMBDA_INVOCATION_URL_TEMPLATE = "http://{0}/{1}/runtime/invocation/{2}/response";
|
||||
private static final String USER_AGENT_VALUE = String.format(
|
||||
"spring-cloud-function/%s-%s",
|
||||
System.getProperty("java.runtime.version"),
|
||||
extractVersion());
|
||||
|
||||
private final ConfigurableApplicationContext applicationContext;
|
||||
|
||||
private volatile boolean running;
|
||||
|
||||
private ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
|
||||
public CustomRuntimeEventLoop(ConfigurableApplicationContext applicationContext) {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
this.running = true;
|
||||
this.executor.execute(() -> {
|
||||
eventLoop(this.applicationContext);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
this.run();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
this.executor.shutdownNow();
|
||||
this.running = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return this.running;
|
||||
}
|
||||
|
||||
private void eventLoop(ConfigurableApplicationContext context) {
|
||||
Environment environment = context.getEnvironment();
|
||||
logger.info("Starting spring-cloud-function CustomRuntimeEventLoop");
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("AWS LAMBDA ENVIRONMENT: " + System.getenv());
|
||||
}
|
||||
|
||||
String runtimeApi = environment.getProperty("AWS_LAMBDA_RUNTIME_API");
|
||||
String eventUri = MessageFormat.format(LAMBDA_RUNTIME_URL_TEMPLATE, runtimeApi, LAMBDA_VERSION_DATE);
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Event URI: " + eventUri);
|
||||
}
|
||||
|
||||
RequestEntity<Void> requestEntity = RequestEntity.get(URI.create(eventUri)).header(USER_AGENT, USER_AGENT_VALUE).build();
|
||||
FunctionCatalog functionCatalog = context.getBean(FunctionCatalog.class);
|
||||
RestTemplate rest = new RestTemplate();
|
||||
JsonMapper mapper = context.getBean(JsonMapper.class);
|
||||
|
||||
logger.info("Entering event loop");
|
||||
while (this.isRunning()) {
|
||||
logger.debug("Attempting to get new event");
|
||||
ResponseEntity<String> response = this.pollForData(rest, requestEntity);
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("New Event received: " + response);
|
||||
}
|
||||
|
||||
if (response != null && response.hasBody()) {
|
||||
String requestId = response.getHeaders().getFirst("Lambda-Runtime-Aws-Request-Id");
|
||||
try {
|
||||
FunctionInvocationWrapper function = locateFunction(environment, functionCatalog, response.getHeaders());
|
||||
|
||||
ByteArrayInputStream is = new ByteArrayInputStream(response.getBody().getBytes(StandardCharsets.UTF_8));
|
||||
Message<?> requestMessage = AWSLambdaUtils.generateMessage(is, function.getInputType(), function.isSupplier(), mapper, null);
|
||||
|
||||
Object functionResponse = function.apply(requestMessage);
|
||||
byte[] responseBytes = AWSLambdaUtils.generateOutputFromObject(requestMessage, functionResponse, mapper, function.getOutputType());
|
||||
|
||||
String invocationUrl = MessageFormat
|
||||
.format(LAMBDA_INVOCATION_URL_TEMPLATE, runtimeApi, LAMBDA_VERSION_DATE, requestId);
|
||||
|
||||
ResponseEntity<Object> result = rest.exchange(RequestEntity.post(URI.create(invocationUrl))
|
||||
.header(USER_AGENT, USER_AGENT_VALUE)
|
||||
.body(responseBytes), Object.class);
|
||||
|
||||
if (logger.isInfoEnabled()) {
|
||||
logger.info("Result POST status: " + result);
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
this.propagateAwsError(requestId, e, mapper, runtimeApi, rest);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void propagateAwsError(String requestId, Exception e, JsonMapper mapper, String runtimeApi, RestTemplate rest) {
|
||||
String errorMessage = e.getMessage();
|
||||
String errorType = e.getClass().getSimpleName();
|
||||
StringWriter sw = new StringWriter();
|
||||
PrintWriter pw = new PrintWriter(sw);
|
||||
e.printStackTrace(pw);
|
||||
String stackTrace = sw.toString();
|
||||
Map<String, String> em = new HashMap<>();
|
||||
em.put("errorMessage", errorMessage);
|
||||
em.put("errorType", errorType);
|
||||
em.put("stackTrace", stackTrace);
|
||||
byte[] outputBody = mapper.toJson(em);
|
||||
try {
|
||||
String errorUrl = MessageFormat.format(LAMBDA_ERROR_URL_TEMPLATE, runtimeApi, LAMBDA_VERSION_DATE, requestId);
|
||||
ResponseEntity<Object> result = rest.exchange(RequestEntity.post(URI.create(errorUrl))
|
||||
.header(USER_AGENT, USER_AGENT_VALUE)
|
||||
.body(outputBody), Object.class);
|
||||
if (logger.isInfoEnabled()) {
|
||||
logger.info("Result ERROR status: " + result.getStatusCode());
|
||||
}
|
||||
}
|
||||
catch (Exception e2) {
|
||||
throw new IllegalArgumentException("Failed to report error", e2);
|
||||
}
|
||||
}
|
||||
|
||||
private ResponseEntity<String> pollForData(RestTemplate rest, RequestEntity<Void> requestEntity) {
|
||||
try {
|
||||
return rest.exchange(requestEntity, String.class);
|
||||
}
|
||||
catch (Exception e) {
|
||||
if (e instanceof SocketException) {
|
||||
this.stop();
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private FunctionInvocationWrapper locateFunction(Environment environment, FunctionCatalog functionCatalog,
|
||||
HttpHeaders httpHeaders) {
|
||||
MediaType contentType = httpHeaders.getContentType();
|
||||
String handlerName = environment.getProperty("DEFAULT_HANDLER");
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Value of DEFAULT_HANDLER env: " + handlerName);
|
||||
}
|
||||
FunctionInvocationWrapper function = functionCatalog.lookup(handlerName, contentType.toString());
|
||||
if (function == null) {
|
||||
logger.debug("Could not locate function under DEFAULT_HANDLER");
|
||||
handlerName = environment.getProperty("_HANDLER");
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Value of _HANDLER env: " + handlerName);
|
||||
}
|
||||
function = functionCatalog.lookup(handlerName, contentType.toString());
|
||||
}
|
||||
|
||||
if (function == null) {
|
||||
logger.debug("Could not locate function under _HANDLER");
|
||||
function = functionCatalog.lookup((String) null, contentType.toString());
|
||||
}
|
||||
|
||||
if (function == null) {
|
||||
logger.info("Could not determine default function");
|
||||
handlerName = environment.getProperty("spring.cloud.function.definition");
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Value of 'spring.cloud.function.definition' env: " + handlerName);
|
||||
}
|
||||
function = functionCatalog.lookup(handlerName, contentType.toString());
|
||||
}
|
||||
|
||||
if (function == null) {
|
||||
logger.info("Could not determine DEFAULT_HANDLER, _HANDLER or 'spring.cloud.function.definition'");
|
||||
handlerName = httpHeaders.getFirst("spring.cloud.function.definition");
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Value of 'spring.cloud.function.definition' header: " + handlerName);
|
||||
}
|
||||
function = functionCatalog.lookup(handlerName, contentType.toString());
|
||||
}
|
||||
|
||||
if (function == null) {
|
||||
function = functionCatalog.lookup(RoutingFunction.FUNCTION_NAME, "application/json");
|
||||
if (function != null && logger.isInfoEnabled()) {
|
||||
logger.info("Will default to RoutingFunction, since multiple functions available in FunctionCatalog."
|
||||
+ "Expecting 'spring.cloud.function.definition' or 'spring.cloud.function.routing-expression' as Message headers. "
|
||||
+ "If invocation is over API Gateway, Message headers can be provided as HTTP headers.");
|
||||
}
|
||||
}
|
||||
|
||||
Assert.notNull(function, "Failed to locate function. Tried locating default function, "
|
||||
+ "function by 'DEFAULT_HANDLER', '_HANDLER' env variable as well as'spring.cloud.function.definition'. "
|
||||
+ "Functions available in catalog are: " + functionCatalog.getNames(null));
|
||||
if (function != null && logger.isInfoEnabled()) {
|
||||
logger.info("Located function " + function.getFunctionDefinition());
|
||||
}
|
||||
return function;
|
||||
}
|
||||
|
||||
private static String extractVersion() {
|
||||
try {
|
||||
String path = CustomRuntimeEventLoop.class.getProtectionDomain().getCodeSource().getLocation().toString();
|
||||
int endIndex = path.lastIndexOf('.');
|
||||
if (endIndex < 0) {
|
||||
return "UNKNOWN-VERSION";
|
||||
}
|
||||
int startIndex = path.lastIndexOf("/") + 1;
|
||||
return path.substring(startIndex, endIndex).replace("spring-cloud-function-adapter-aws-", "");
|
||||
}
|
||||
catch (Exception e) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Failed to detect version", e);
|
||||
}
|
||||
return "UNKNOWN-VERSION";
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
/*
|
||||
* Copyright 2018-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.function.adapter.aws;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.context.ApplicationContextInitializer;
|
||||
import org.springframework.context.SmartLifecycle;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
* @author Oleg Zhurakousky
|
||||
*/
|
||||
public class CustomRuntimeInitializer implements ApplicationContextInitializer<GenericApplicationContext> {
|
||||
|
||||
private static Log logger = LogFactory.getLog(CustomRuntimeInitializer.class);
|
||||
|
||||
@Override
|
||||
public void initialize(GenericApplicationContext context) {
|
||||
Environment environment = context.getEnvironment();
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("AWS Environment: " + System.getenv());
|
||||
}
|
||||
|
||||
if (!this.isWebExportEnabled(context) && isCustomRuntime(environment)) {
|
||||
if (context.getBeanFactory().getBeanNamesForType(CustomRuntimeEventLoop.class, false, false).length == 0) {
|
||||
context.registerBean(StringUtils.uncapitalize(CustomRuntimeEventLoop.class.getSimpleName()),
|
||||
SmartLifecycle.class, () -> new CustomRuntimeEventLoop(context));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isCustomRuntime(Environment environment) {
|
||||
String handler = environment.getProperty("_HANDLER");
|
||||
if (StringUtils.hasText(handler)) {
|
||||
handler = handler.split(":")[0];
|
||||
logger.info("AWS Handler: " + handler);
|
||||
try {
|
||||
Class<?> clazz = Thread.currentThread().getContextClassLoader().loadClass(handler);
|
||||
if (FunctionInvoker.class.isAssignableFrom(clazz)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
logger.debug("Will execute Lambda in Custom Runtime");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private boolean isWebExportEnabled(GenericApplicationContext context) {
|
||||
Boolean enabled = context.getEnvironment()
|
||||
.getProperty("spring.cloud.function.web.export.enabled", Boolean.class);
|
||||
return enabled != null && enabled;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.function.adapter.aws;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Set;
|
||||
|
||||
import com.amazonaws.services.lambda.runtime.Context;
|
||||
import com.amazonaws.services.lambda.runtime.RequestStreamHandler;
|
||||
import com.fasterxml.jackson.databind.MapperFeature;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.cloud.function.context.FunctionCatalog;
|
||||
import org.springframework.cloud.function.context.FunctionProperties;
|
||||
import org.springframework.cloud.function.context.FunctionalSpringApplication;
|
||||
import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper;
|
||||
import org.springframework.cloud.function.context.config.RoutingFunction;
|
||||
import org.springframework.cloud.function.json.JacksonMapper;
|
||||
import org.springframework.cloud.function.json.JsonMapper;
|
||||
import org.springframework.cloud.function.utils.FunctionClassUtils;
|
||||
import org.springframework.context.ApplicationContextInitializer;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StreamUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Oleg Zhurakousky
|
||||
* @since 3.1
|
||||
*
|
||||
* see
|
||||
* https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-output-format
|
||||
*/
|
||||
public class FunctionInvoker implements RequestStreamHandler {
|
||||
|
||||
private static Log logger = LogFactory.getLog(FunctionInvoker.class);
|
||||
|
||||
private JsonMapper jsonMapper;
|
||||
|
||||
private FunctionInvocationWrapper function;
|
||||
|
||||
private volatile String functionDefinition;
|
||||
|
||||
private boolean started;
|
||||
|
||||
public FunctionInvoker(String functionDefinition) {
|
||||
this.functionDefinition = functionDefinition;
|
||||
String lateInitialization = System.getenv("FUNCTION_INVOKER_LATE_INITIALIZATION");
|
||||
if (!StringUtils.hasText(lateInitialization) || !Boolean.parseBoolean(lateInitialization)) {
|
||||
this.start();
|
||||
}
|
||||
else {
|
||||
logger.info("Spring Application Context will be initialized on first request");
|
||||
}
|
||||
}
|
||||
|
||||
public FunctionInvoker() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes" })
|
||||
@Override
|
||||
public void handleRequest(InputStream input, OutputStream output, Context context) throws IOException {
|
||||
if (!this.started) {
|
||||
this.start();
|
||||
}
|
||||
Message requestMessage = AWSLambdaUtils
|
||||
.generateMessage(input, this.function.getInputType(), this.function.isSupplier(), jsonMapper, context);
|
||||
|
||||
Object response = this.function.apply(requestMessage);
|
||||
byte[] responseBytes = AWSLambdaUtils.generateOutputFromObject(requestMessage, response, this.jsonMapper, function.getOutputType());
|
||||
StreamUtils.copy(responseBytes, output);
|
||||
// any exception should propagate
|
||||
}
|
||||
|
||||
private void start() {
|
||||
Class<?> startClass = FunctionClassUtils.getStartClass();
|
||||
String[] properties = new String[] {"--spring.cloud.function.web.export.enabled=false", "--spring.main.web-application-type=none"};
|
||||
ConfigurableApplicationContext context = ApplicationContextInitializer.class.isAssignableFrom(startClass)
|
||||
? FunctionalSpringApplication.run(new Class[] {startClass, AWSCompanionAutoConfiguration.class}, properties)
|
||||
: SpringApplication.run(new Class[] {startClass, AWSCompanionAutoConfiguration.class}, properties);
|
||||
|
||||
Environment environment = context.getEnvironment();
|
||||
if (!StringUtils.hasText(this.functionDefinition)) {
|
||||
this.functionDefinition = environment.getProperty(FunctionProperties.FUNCTION_DEFINITION);
|
||||
}
|
||||
|
||||
FunctionCatalog functionCatalog = context.getBean(FunctionCatalog.class);
|
||||
this.jsonMapper = context.getBean(JsonMapper.class);
|
||||
if (this.jsonMapper instanceof JacksonMapper) {
|
||||
((JacksonMapper) this.jsonMapper).configureObjectMapper(objectMapper -> {
|
||||
if (!objectMapper.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES)) {
|
||||
objectMapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (logger.isInfoEnabled()) {
|
||||
logger.info("Locating function: '" + this.functionDefinition + "'");
|
||||
}
|
||||
|
||||
this.function = functionCatalog.lookup(this.functionDefinition, "application/json");
|
||||
|
||||
if (this.function == null) {
|
||||
if (logger.isInfoEnabled()) {
|
||||
if (!StringUtils.hasText(this.functionDefinition)) {
|
||||
logger.info("Failed to determine default function. Please use 'spring.cloud.function.definition' property "
|
||||
+ "or pass function definition as a constructir argument to this FunctionInvoker");
|
||||
}
|
||||
Set<String> names = functionCatalog.getNames(null);
|
||||
if (names.size() == 1) {
|
||||
logger.info("Will default to RoutingFunction, since it is the only function available in FunctionCatalog."
|
||||
+ "Expecting 'spring.cloud.function.definition' or 'spring.cloud.function.routing-expression' as Message headers. "
|
||||
+ "If invocation is over API Gateway, Message headers can be provided as HTTP headers.");
|
||||
}
|
||||
else {
|
||||
logger.info("More then one function is available in FunctionCatalog. " + names
|
||||
+ " Will default to RoutingFunction, "
|
||||
+ "Expecting 'spring.cloud.function.definition' or 'spring.cloud.function.routing-expression' as Message headers. "
|
||||
+ "If invocation is over API Gateway, Message headers can be provided as HTTP headers.");
|
||||
}
|
||||
}
|
||||
this.function = functionCatalog.lookup(RoutingFunction.FUNCTION_NAME, "application/json");
|
||||
}
|
||||
|
||||
if (this.function.isOutputTypePublisher()) {
|
||||
this.function.setSkipOutputConversion(true);
|
||||
}
|
||||
Assert.notNull(this.function, "Failed to lookup function " + this.functionDefinition);
|
||||
|
||||
this.functionDefinition = this.function.getFunctionDefinition();
|
||||
if (logger.isInfoEnabled()) {
|
||||
logger.info("Located function: '" + this.functionDefinition + "'");
|
||||
}
|
||||
this.started = true;
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
* Copyright 2018-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.function.adapter.aws;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.cloud.function.web.source.DestinationResolver;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
|
||||
/**
|
||||
* Implementation of {@link DestinationResolver}for AWS Lambda which resolves destination
|
||||
* from `lambda-runtime-aws-request-id` message header.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Oleg Zhurakousky
|
||||
*
|
||||
*/
|
||||
public class LambdaDestinationResolver implements DestinationResolver {
|
||||
|
||||
private static Log logger = LogFactory.getLog(LambdaDestinationResolver.class);
|
||||
|
||||
@Override
|
||||
public String destination(Supplier<?> supplier, String name, Object value) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Lambda invoming value: " + value);
|
||||
}
|
||||
String destination = "unknown";
|
||||
if (value instanceof Message) {
|
||||
Message<?> message = (Message<?>) value;
|
||||
MessageHeaders headers = message.getHeaders();
|
||||
if (headers.containsKey("lambda-runtime-aws-request-id")) {
|
||||
destination = (String) headers.get("lambda-runtime-aws-request-id");
|
||||
}
|
||||
}
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Lambda destination resolved to: " + destination);
|
||||
}
|
||||
return destination;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.function.adapter.test.aws;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.env.MapPropertySource;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
|
||||
/**
|
||||
* AWS Custom Runtime emulator to be used for testing.
|
||||
*
|
||||
* @author Oleg Zhurakousky
|
||||
* @since 3.2
|
||||
*/
|
||||
@EnableAutoConfiguration
|
||||
public class AWSCustomRuntime {
|
||||
|
||||
BlockingQueue<Object> inputQueue = new ArrayBlockingQueue<>(3);
|
||||
|
||||
BlockingQueue<Message<String>> outputQueue = new ArrayBlockingQueue<>(3);
|
||||
|
||||
public AWSCustomRuntime(ConfigurableApplicationContext context) {
|
||||
context.getEnvironment().getPropertySources().addFirst(
|
||||
new MapPropertySource("AWSCustomRuntime",
|
||||
Map.of("AWS_LAMBDA_RUNTIME_API", "localhost:${local.server.port}")));
|
||||
}
|
||||
|
||||
@Bean("2018-06-01/runtime/invocation/consume/response")
|
||||
Consumer<Message<String>> consume() {
|
||||
return v -> outputQueue.offer(v);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Bean("2018-06-01/runtime/invocation/next")
|
||||
Supplier<Message<String>> supply() {
|
||||
|
||||
return () -> {
|
||||
try {
|
||||
Object value = inputQueue.poll(1L, TimeUnit.SECONDS);
|
||||
if (value == null) {
|
||||
return MessageBuilder.withPayload("").build();
|
||||
}
|
||||
if (!(value instanceof Message)) {
|
||||
return MessageBuilder.withPayload((String) value)
|
||||
.setHeader("Lambda-Runtime-Aws-Request-Id", "consume")
|
||||
.setHeader("Content-Type",
|
||||
MimeTypeUtils.APPLICATION_JSON)
|
||||
.build();
|
||||
}
|
||||
else {
|
||||
return (Message<String>) value;
|
||||
}
|
||||
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public Message<String> exchange(Object input) {
|
||||
inputQueue.offer(input);
|
||||
try {
|
||||
return outputQueue.poll(5000, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
org.springframework.context.ApplicationContextInitializer=\
|
||||
org.springframework.cloud.function.adapter.aws.CustomRuntimeInitializer
|
||||
@@ -1 +0,0 @@
|
||||
org.springframework.cloud.function.adapter.aws.AWSCompanionAutoConfiguration
|
||||
@@ -1,297 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.function.adapter.aws;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.boot.WebApplicationType;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.cloud.function.adapter.test.aws.AWSCustomRuntime;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
/**
|
||||
*
|
||||
* @author Oleg Zhurakousky
|
||||
*/
|
||||
public class CustomRuntimeEventLoopTest {
|
||||
|
||||
private String API_EVENT = "{\n"
|
||||
+ " \"version\": \"1.0\",\n"
|
||||
+ " \"resource\": \"$default\",\n"
|
||||
+ " \"path\": \"/question\",\n"
|
||||
+ " \"httpMethod\": \"POST\",\n"
|
||||
+ " \"headers\": {\n"
|
||||
+ " \"Content-Length\": \"40\",\n"
|
||||
+ " \"Content-Type\": \"application/json\",\n"
|
||||
+ " \"Host\": \"emcdxu5ijj.execute-api.us-east-2.amazonaws.com\",\n"
|
||||
+ " \"User-Agent\": \"curl/7.88.1\",\n"
|
||||
+ " \"X-Amzn-Trace-Id\": \"Root=1-64ad9787-4c89d5af7607eb9e522e01d5\",\n"
|
||||
+ " \"X-Forwarded-For\": \"109.210.252.44\",\n"
|
||||
+ " \"X-Forwarded-Port\": \"443\",\n"
|
||||
+ " \"X-Forwarded-Proto\": \"https\",\n"
|
||||
+ " \"accept\": \"*/*\"\n"
|
||||
+ " },\n"
|
||||
+ " \"multiValueHeaders\": {\n"
|
||||
+ " \"Content-Length\": [\n"
|
||||
+ " \"40\"\n"
|
||||
+ " ],\n"
|
||||
+ " \"Content-Type\": [\n"
|
||||
+ " \"application/json\"\n"
|
||||
+ " ],\n"
|
||||
+ " \"Host\": [\n"
|
||||
+ " \"emcdxu5ijj.execute-api.us-east-2.amazonaws.com\"\n"
|
||||
+ " ],\n"
|
||||
+ " \"User-Agent\": [\n"
|
||||
+ " \"curl/7.88.1\"\n"
|
||||
+ " ],\n"
|
||||
+ " \"X-Amzn-Trace-Id\": [\n"
|
||||
+ " \"Root=1-64ad9787-4c89d5af7607eb9e522e01d5\"\n"
|
||||
+ " ],\n"
|
||||
+ " \"X-Forwarded-For\": [\n"
|
||||
+ " \"109.210.252.44\"\n"
|
||||
+ " ],\n"
|
||||
+ " \"X-Forwarded-Port\": [\n"
|
||||
+ " \"443\"\n"
|
||||
+ " ],\n"
|
||||
+ " \"X-Forwarded-Proto\": [\n"
|
||||
+ " \"https\"\n"
|
||||
+ " ],\n"
|
||||
+ " \"accept\": [\n"
|
||||
+ " \"*/*\"\n"
|
||||
+ " ]\n"
|
||||
+ " },\n"
|
||||
+ " \"queryStringParameters\": null,\n"
|
||||
+ " \"multiValueQueryStringParameters\": null,\n"
|
||||
+ " \"requestContext\": {\n"
|
||||
+ " \"accountId\": \"313369169943\",\n"
|
||||
+ " \"apiId\": \"emcdxu5ijj\",\n"
|
||||
+ " \"domainName\": \"emcdxu5ijj.execute-api.us-east-2.amazonaws.com\",\n"
|
||||
+ " \"domainPrefix\": \"emcdxu5ijj\",\n"
|
||||
+ " \"extendedRequestId\": \"H6SdPgXtiYcEP1w=\",\n"
|
||||
+ " \"httpMethod\": \"POST\",\n"
|
||||
+ " \"identity\": {\n"
|
||||
+ " \"accessKey\": null,\n"
|
||||
+ " \"accountId\": null,\n"
|
||||
+ " \"caller\": null,\n"
|
||||
+ " \"cognitoAmr\": null,\n"
|
||||
+ " \"cognitoAuthenticationProvider\": null,\n"
|
||||
+ " \"cognitoAuthenticationType\": null,\n"
|
||||
+ " \"cognitoIdentityId\": null,\n"
|
||||
+ " \"cognitoIdentityPoolId\": null,\n"
|
||||
+ " \"principalOrgId\": null,\n"
|
||||
+ " \"sourceIp\": \"109.210.252.44\",\n"
|
||||
+ " \"user\": null,\n"
|
||||
+ " \"userAgent\": \"curl/7.88.1\",\n"
|
||||
+ " \"userArn\": null\n"
|
||||
+ " },\n"
|
||||
+ " \"path\": \"/question\",\n"
|
||||
+ " \"protocol\": \"HTTP/1.1\",\n"
|
||||
+ " \"requestId\": \"H6SdPgXtiYcEP1w=\",\n"
|
||||
+ " \"requestTime\": \"11/Jul/2023:17:55:19 +0000\",\n"
|
||||
+ " \"requestTimeEpoch\": 1689098119662,\n"
|
||||
+ " \"resourceId\": \"$default\",\n"
|
||||
+ " \"resourcePath\": \"$default\",\n"
|
||||
+ " \"stage\": \"$default\"\n"
|
||||
+ " },\n"
|
||||
+ " \"pathParameters\": null,\n"
|
||||
+ " \"stageVariables\": null,\n"
|
||||
+ " \"body\": \"[{\\\"latitude\\\": 41.34, \\\"longitude\\\": 2.78},{\\\"latitude\\\": 43.24, \\\"longitude\\\": 3.78}]\",\n"
|
||||
+ " \"isBase64Encoded\": false\n"
|
||||
+ "}";
|
||||
|
||||
@Test
|
||||
public void testDefaultFunctionLookup() throws Exception {
|
||||
testDefaultFunctionLookup("uppercase", SingleFunctionConfiguration.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDefaultFunctionLookupReactive() throws Exception {
|
||||
testDefaultFunctionLookup("uppercase", SingleFunctionConfigurationReactive.class);
|
||||
}
|
||||
|
||||
private void testDefaultFunctionLookup(String handler, Class<?> context) throws Exception {
|
||||
try (ConfigurableApplicationContext userContext =
|
||||
new SpringApplicationBuilder(context, AWSCustomRuntime.class)
|
||||
.web(WebApplicationType.SERVLET)
|
||||
.properties("_HANDLER=" + handler, "server.port=0")
|
||||
.run()) {
|
||||
|
||||
AWSCustomRuntime aws = userContext.getBean(AWSCustomRuntime.class);
|
||||
Message<String> replyMessage = aws.exchange("\"ricky\"");
|
||||
assertThat(replyMessage.getHeaders()).containsKey("user-agent");
|
||||
assertThat(((String) replyMessage.getHeaders().get("user-agent"))).startsWith("spring-cloud-function");
|
||||
assertThat(aws.exchange("\"ricky\"").getPayload()).isEqualTo("\"RICKY\"");
|
||||
assertThat(aws.exchange("\"julien\"").getPayload()).isEqualTo("\"JULIEN\"");
|
||||
assertThat(aws.exchange("\"bubbles\"").getPayload()).isEqualTo("\"BUBBLES\"");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDefaultFunctionAsComponentLookup() throws Exception {
|
||||
try (ConfigurableApplicationContext userContext =
|
||||
new SpringApplicationBuilder(PersonFunction.class, AWSCustomRuntime.class)
|
||||
.web(WebApplicationType.SERVLET)
|
||||
.properties("_HANDLER=personFunction", "server.port=0")
|
||||
.run()) {
|
||||
|
||||
AWSCustomRuntime aws = userContext.getBean(AWSCustomRuntime.class);
|
||||
|
||||
assertThat(aws.exchange("\"ricky\"").getPayload()).isEqualTo("{\"name\":\"RICKY\"}");
|
||||
assertThat(aws.exchange("\"julien\"").getPayload()).isEqualTo("{\"name\":\"JULIEN\"}");
|
||||
assertThat(aws.exchange("\"bubbles\"").getPayload()).isEqualTo("{\"name\":\"BUBBLES\"}");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_HANDLERlookupAndPojoFunction() throws Exception {
|
||||
try (ConfigurableApplicationContext userContext =
|
||||
new SpringApplicationBuilder(MultipleFunctionConfiguration.class, AWSCustomRuntime.class)
|
||||
.web(WebApplicationType.SERVLET)
|
||||
.properties("_HANDLER=uppercasePerson", "server.port=0")
|
||||
.run()) {
|
||||
|
||||
AWSCustomRuntime aws = userContext.getBean(AWSCustomRuntime.class);
|
||||
|
||||
assertThat(aws.exchange("\"ricky\"").getPayload()).isEqualTo("{\"name\":\"RICKY\"}");
|
||||
assertThat(aws.exchange("\"julien\"").getPayload()).isEqualTo("{\"name\":\"JULIEN\"}");
|
||||
assertThat(aws.exchange("\"bubbles\"").getPayload()).isEqualTo("{\"name\":\"BUBBLES\"}");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_HANDLERWithApiGatewayRequestAndFlux() throws Exception {
|
||||
try (ConfigurableApplicationContext userContext =
|
||||
new SpringApplicationBuilder(MultipleFunctionConfiguration.class, AWSCustomRuntime.class)
|
||||
.web(WebApplicationType.SERVLET)
|
||||
.properties("_HANDLER=echoFlux", "server.port=0")
|
||||
.run()) {
|
||||
|
||||
AWSCustomRuntime aws = userContext.getBean(AWSCustomRuntime.class);
|
||||
String response = aws.exchange(API_EVENT).getPayload();
|
||||
assertThat(response).contains("{\\\"latitude\\\":2.78,\\\"longitude\\\":41.34}");
|
||||
assertThat(response).contains("{\\\"latitude\\\":3.78,\\\"longitude\\\":43.24}");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DirtiesContext
|
||||
public void test_definitionLookupAndComposition() throws Exception {
|
||||
try (ConfigurableApplicationContext userContext =
|
||||
new SpringApplicationBuilder(MultipleFunctionConfiguration.class, AWSCustomRuntime.class)
|
||||
.web(WebApplicationType.SERVLET)
|
||||
.properties("_HANDLER=toPersonJson|uppercasePerson", "server.port=0")
|
||||
.run()) {
|
||||
|
||||
AWSCustomRuntime aws = userContext.getBean(AWSCustomRuntime.class);
|
||||
|
||||
assertThat(aws.exchange("\"ricky\"").getPayload()).isEqualTo("{\"name\":\"RICKY\"}");
|
||||
assertThat(aws.exchange("\"julien\"").getPayload()).isEqualTo("{\"name\":\"JULIEN\"}");
|
||||
assertThat(aws.exchange("\"bubbles\"").getPayload()).isEqualTo("{\"name\":\"BUBBLES\"}");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
protected static class SingleFunctionConfiguration {
|
||||
@Bean
|
||||
public Function<String, String> uppercase() {
|
||||
return v -> v.toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
protected static class SingleFunctionConfigurationReactive {
|
||||
@Bean
|
||||
public Function<Flux<String>, Flux<String>> uppercase() {
|
||||
return v -> v.map(String::toUpperCase);
|
||||
}
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
protected static class MultipleFunctionConfiguration {
|
||||
@Bean
|
||||
public Function<String, String> uppercase() {
|
||||
return v -> v.toUpperCase();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Function<String, String> toPersonJson() {
|
||||
return v -> "{\"name\":\"" + v + "\"}";
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Function<Person, Person> uppercasePerson() {
|
||||
return p -> new Person(p.getName().toUpperCase());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Function<Flux<GeoLocation>, Flux<GeoLocation>> echoFlux() {
|
||||
return flux -> flux.map(g -> {
|
||||
return new GeoLocation(g.longitude(), g.latitude());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@Component("personFunction") // need in test explicitly since it is inner class and name wil be `customRuntimeEventLoopTest.PersonFunction`
|
||||
public static class PersonFunction implements Function<Person, Person> {
|
||||
|
||||
public PersonFunction() {
|
||||
System.out.println();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Person apply(Person input) {
|
||||
return new Person(input.getName().toUpperCase());
|
||||
}
|
||||
}
|
||||
|
||||
public static class Person {
|
||||
private String name;
|
||||
|
||||
public Person() {
|
||||
|
||||
}
|
||||
|
||||
public Person(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public record GeoLocation(Float latitude, Float longitude) {
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,97 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.function.adapter.aws;
|
||||
|
||||
import com.amazonaws.services.lambda.runtime.ClientContext;
|
||||
import com.amazonaws.services.lambda.runtime.CognitoIdentity;
|
||||
import com.amazonaws.services.lambda.runtime.Context;
|
||||
import com.amazonaws.services.lambda.runtime.LambdaLogger;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Oleg Zhurakousky
|
||||
*
|
||||
*/
|
||||
public class TestContext implements Context {
|
||||
|
||||
@Override
|
||||
public String getAwsRequestId() {
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLogGroupName() {
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLogStreamName() {
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFunctionName() {
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFunctionVersion() {
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getInvokedFunctionArn() {
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CognitoIdentity getIdentity() {
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientContext getClientContext() {
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRemainingTimeInMillis() {
|
||||
// TODO Auto-generated method stub
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMemoryLimitInMB() {
|
||||
// TODO Auto-generated method stub
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LambdaLogger getLogger() {
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
=== Introduction
|
||||
|
||||
Light weight Azure Function forwarding proxy which can deploy any existing Spring Boot web application as Azure Functions.
|
||||
Infernally uses the Azure Http Trigger mapping.
|
||||
|
||||
This module is identified as the only additional dependency to the existing web-app.
|
||||
|
||||
A sample is provided in https://github.com/spring-cloud/spring-cloud-function/tree/main/spring-cloud-function-samples/function-sample-azure-web[azure-web sample]
|
||||
|
||||
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>spring-cloud-function-adapter-azure-web</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>spring-cloud-function-adapter-azure-web</name>
|
||||
<description>Azure Function Adapter for Spring Cloud Function</description>
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-function-adapter-parent</artifactId>
|
||||
<version>4.1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<java.version>17</java.version>
|
||||
<azure.functions.java.core.version>3.0.0</azure.functions.java.core.version>
|
||||
<azure.functions.java.spi.version>1.0.0</azure.functions.java.spi.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.google.code.findbugs</groupId>
|
||||
<artifactId>annotations</artifactId>
|
||||
<version>3.0.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-function-context</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-function-serverless-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-autoconfigure</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.azure.functions</groupId>
|
||||
<artifactId>azure-functions-java-library</artifactId>
|
||||
<version>${azure.functions.java.core.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.microsoft.azure.functions</groupId>
|
||||
<artifactId>azure-functions-java-spi</artifactId>
|
||||
<version>${azure.functions.java.spi.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>jakarta.servlet</groupId>
|
||||
<artifactId>jakarta.servlet-api</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-webmvc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -1,144 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.function.adapter.azure.web;
|
||||
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Optional;
|
||||
|
||||
import com.microsoft.azure.functions.ExecutionContext;
|
||||
import com.microsoft.azure.functions.HttpMethod;
|
||||
import com.microsoft.azure.functions.HttpRequestMessage;
|
||||
import com.microsoft.azure.functions.HttpResponseMessage;
|
||||
import com.microsoft.azure.functions.HttpResponseMessage.Builder;
|
||||
import com.microsoft.azure.functions.HttpStatus;
|
||||
import com.microsoft.azure.functions.annotation.AuthorizationLevel;
|
||||
import com.microsoft.azure.functions.annotation.FunctionName;
|
||||
import com.microsoft.azure.functions.annotation.HttpTrigger;
|
||||
import com.microsoft.azure.functions.spi.inject.FunctionInstanceInjector;
|
||||
import jakarta.servlet.ServletContext;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.cloud.function.serverless.web.ProxyHttpServletRequest;
|
||||
import org.springframework.cloud.function.serverless.web.ProxyHttpServletResponse;
|
||||
import org.springframework.cloud.function.serverless.web.ProxyMvc;
|
||||
import org.springframework.cloud.function.utils.FunctionClassUtils;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
* @author Oleg Zhurakousky
|
||||
*
|
||||
*/
|
||||
public class AzureWebProxyInvoker implements FunctionInstanceInjector {
|
||||
|
||||
private static Log logger = LogFactory.getLog(AzureWebProxyInvoker.class);
|
||||
|
||||
private static final String AZURE_WEB_ADAPTER_NAME = "AzureWebAdapter";
|
||||
private static final String AZURE_WEB_ADAPTER_ROUTE = AZURE_WEB_ADAPTER_NAME
|
||||
+ "/{e?}/{e2?}/{e3?}/{e4?}/{e5?}/{e6?}/{e7?}/{e8?}/{e9?}/{e10?}/{e11?}/{e12?}/{e13?}/{e14?}/{e15?}";
|
||||
|
||||
private ProxyMvc mvc;
|
||||
|
||||
private ServletContext servletContext;
|
||||
|
||||
@Override
|
||||
public <T> T getInstance(Class<T> functionClass) throws Exception {
|
||||
this.initialize();
|
||||
return (T) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Because the getInstance is called by Azure Java Function on every function request we need to cache the Spring
|
||||
* context initialization on the first function call.
|
||||
* @throws ServletException error.
|
||||
*/
|
||||
private void initialize() throws ServletException {
|
||||
synchronized (AzureWebProxyInvoker.class.getName()) {
|
||||
if (mvc == null) {
|
||||
Class<?> startClass = FunctionClassUtils.getStartClass();
|
||||
this.mvc = ProxyMvc.INSTANCE(startClass);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private HttpServletRequest prepareRequest(HttpRequestMessage<Optional<String>> request) {
|
||||
|
||||
int pathOffset = request.getUri().getPath().indexOf(AZURE_WEB_ADAPTER_NAME) + AZURE_WEB_ADAPTER_NAME.length();
|
||||
|
||||
String path = request.getUri().getPath().substring(pathOffset);
|
||||
|
||||
ProxyHttpServletRequest httpRequest = new ProxyHttpServletRequest(servletContext,
|
||||
request.getHttpMethod().toString(), path);
|
||||
|
||||
request.getBody().ifPresent(body -> {
|
||||
httpRequest.setContent(body.getBytes());
|
||||
});
|
||||
|
||||
if (!CollectionUtils.isEmpty(request.getQueryParameters())) {
|
||||
httpRequest.setParameters(request.getQueryParameters());
|
||||
}
|
||||
|
||||
if (!CollectionUtils.isEmpty(request.getHeaders())) {
|
||||
for (Entry<String, String> entry : request.getHeaders().entrySet()) {
|
||||
httpRequest.addHeader(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
return httpRequest;
|
||||
}
|
||||
|
||||
@FunctionName(AZURE_WEB_ADAPTER_NAME)
|
||||
public HttpResponseMessage execute(
|
||||
@HttpTrigger(name = "req", methods = { HttpMethod.GET,
|
||||
HttpMethod.POST }, authLevel = AuthorizationLevel.ANONYMOUS, route = AZURE_WEB_ADAPTER_ROUTE) HttpRequestMessage<Optional<String>> request,
|
||||
ExecutionContext context) {
|
||||
|
||||
context.getLogger().info("Request body is: " + request.getBody().orElse("[empty]"));
|
||||
|
||||
HttpServletRequest httpRequest = this.prepareRequest(request);
|
||||
|
||||
ProxyHttpServletResponse httpResponse = new ProxyHttpServletResponse();
|
||||
try {
|
||||
this.mvc.service(httpRequest, httpResponse);
|
||||
|
||||
Builder responseBuilder = request.createResponseBuilder(HttpStatus.OK);
|
||||
for (String headerName : httpResponse.getHeaderNames()) {
|
||||
responseBuilder.header(headerName, httpResponse.getHeader(headerName));
|
||||
}
|
||||
|
||||
String responseString = httpResponse.getContentAsString();
|
||||
if (StringUtils.hasText(responseString)) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Response: " + responseString);
|
||||
}
|
||||
responseBuilder.body(responseString);
|
||||
} // TODO: what to do with bodyless response?
|
||||
|
||||
return responseBuilder.build();
|
||||
}
|
||||
catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
org.springframework.cloud.function.adapter.azure.web.AzureWebProxyInvoker
|
||||
@@ -1 +0,0 @@
|
||||
spring.banner.location=classpath:/spring-azure-function-banner.txt
|
||||
@@ -1,8 +0,0 @@
|
||||
____ _ _ _____ _ _
|
||||
/ ___| _ __ _ __(_)_ __ __ _ / \ _____ _ _ __ ___ | ___| _ _ __ ___| |_(_) ___ _ __ ___
|
||||
\___ \| '_ \| '__| | '_ \ / _` | / _ \ |_ / | | | '__/ _ \ | |_ | | | | '_ \ / __| __| |/ _ \| '_ \/ __|
|
||||
___) | |_) | | | | | | | (_| | / ___ \ / /| |_| | | | __/ | _|| |_| | | | | (__| |_| | (_) | | | \__ \
|
||||
|____/| .__/|_| |_|_| |_|\__, | /_/ \_\/___|\__,_|_| \___| |_| \__,_|_| |_|\___|\__|_|\___/|_| |_|___/
|
||||
|_| |___/
|
||||
${application.title} ${application.version}
|
||||
Powered by Spring Boot ${spring-boot.version} and Azure Functions
|
||||
@@ -1,181 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.function.adapter.azure.web;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import com.microsoft.azure.functions.HttpMethod;
|
||||
import com.microsoft.azure.functions.HttpRequestMessage;
|
||||
import com.microsoft.azure.functions.HttpResponseMessage;
|
||||
import com.microsoft.azure.functions.HttpResponseMessage.Builder;
|
||||
import com.microsoft.azure.functions.HttpStatus;
|
||||
import com.microsoft.azure.functions.HttpStatusType;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class AzureWebProxyInvokerTests {
|
||||
|
||||
@Test
|
||||
public void test() throws Exception {
|
||||
System.setProperty("MAIN_CLASS", PetStoreSpringAppConfig.class.getName());
|
||||
AzureWebProxyInvoker proxyInvoker = new AzureWebProxyInvoker();
|
||||
AzureWebProxyInvoker instance = proxyInvoker.getInstance(AzureWebProxyInvoker.class);
|
||||
|
||||
HttpRequestMessageStub<Optional<String>> request = new HttpRequestMessageStub<Optional<String>>();
|
||||
|
||||
request.setHttpMethod(HttpMethod.GET);
|
||||
|
||||
request.setUri(new URI(
|
||||
"http://localhost:7072/api/AzureWebAdapter/pets"));
|
||||
|
||||
request.setBody(Optional.of("{\"id\":\"535932f1-d18b-488a-ad8f-8d50b9678492\"" +
|
||||
"\"breed\":\"Beagle\",\"name\":\"Murphy\",\"dateOfBirth\":1591682824313}"));
|
||||
|
||||
HttpResponseMessage response = instance.execute(request, new TestExecutionContext("execute"));
|
||||
|
||||
System.out.println(response.getBody());
|
||||
|
||||
}
|
||||
|
||||
public static class HttpRequestMessageStub<I> implements HttpRequestMessage<I> {
|
||||
|
||||
private URI uri;
|
||||
private HttpMethod httpMethod;
|
||||
private Map<String, String> headers;
|
||||
private Map<String, String> queryParameters;
|
||||
private I body;
|
||||
|
||||
public void setUri(URI uri) {
|
||||
this.uri = uri;
|
||||
}
|
||||
|
||||
public void setHttpMethod(HttpMethod httpMethod) {
|
||||
this.httpMethod = httpMethod;
|
||||
}
|
||||
|
||||
public void setHeaders(Map<String, String> headers) {
|
||||
this.headers = headers;
|
||||
}
|
||||
|
||||
public void setQueryParameters(Map<String, String> queryParameters) {
|
||||
this.queryParameters = queryParameters;
|
||||
}
|
||||
|
||||
public void setBody(I body) {
|
||||
this.body = body;
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI getUri() {
|
||||
return this.uri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpMethod getHttpMethod() {
|
||||
return this.httpMethod;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getHeaders() {
|
||||
return this.headers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getQueryParameters() {
|
||||
return this.queryParameters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public I getBody() {
|
||||
return this.body;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpResponseMessage.Builder createResponseBuilder(HttpStatusType status) {
|
||||
return new BuilderStub().status(status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder createResponseBuilder(HttpStatus status) {
|
||||
return new BuilderStub().status(status);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class BuilderStub implements Builder {
|
||||
|
||||
private HttpStatusType status;
|
||||
private Map<String, String> headers = new HashMap<>();
|
||||
private Object body;
|
||||
|
||||
@Override
|
||||
public Builder status(HttpStatusType status) {
|
||||
this.status = status;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder header(String key, String value) {
|
||||
headers.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder body(Object body) {
|
||||
this.body = body;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpResponseMessage build() {
|
||||
return new HttpResponseMessageStub(this.status, this.headers, this.body);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class HttpResponseMessageStub implements HttpResponseMessage {
|
||||
|
||||
private HttpStatusType status;
|
||||
private Map<String, String> headers = new HashMap<>();
|
||||
private Object body;
|
||||
|
||||
HttpResponseMessageStub(HttpStatusType status, Map<String, String> headers,
|
||||
Object body) {
|
||||
this.status = status;
|
||||
this.headers = headers;
|
||||
this.body = body;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpStatusType getStatus() {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHeader(String key) {
|
||||
return this.headers.get(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getBody() {
|
||||
return this.body;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.function.adapter.azure.web;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
public class Pet {
|
||||
private String id;
|
||||
private String breed;
|
||||
private String name;
|
||||
private Date dateOfBirth;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getBreed() {
|
||||
return breed;
|
||||
}
|
||||
|
||||
public void setBreed(String breed) {
|
||||
this.breed = breed;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Date getDateOfBirth() {
|
||||
return dateOfBirth;
|
||||
}
|
||||
|
||||
public void setDateOfBirth(Date dateOfBirth) {
|
||||
this.dateOfBirth = dateOfBirth;
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.function.adapter.azure.web;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
public class PetData {
|
||||
private static List<String> breeds = new ArrayList<>();
|
||||
static {
|
||||
breeds.add("Afghan Hound");
|
||||
breeds.add("Beagle");
|
||||
breeds.add("Bernese Mountain Dog");
|
||||
breeds.add("Bloodhound");
|
||||
breeds.add("Dalmatian");
|
||||
breeds.add("Jack Russell Terrier");
|
||||
breeds.add("Norwegian Elkhound");
|
||||
}
|
||||
|
||||
private static List<String> names = new ArrayList<>();
|
||||
static {
|
||||
names.add("Bailey");
|
||||
names.add("Bella");
|
||||
names.add("Max");
|
||||
names.add("Lucy");
|
||||
names.add("Charlie");
|
||||
names.add("Molly");
|
||||
names.add("Buddy");
|
||||
names.add("Daisy");
|
||||
names.add("Rocky");
|
||||
names.add("Maggie");
|
||||
names.add("Jake");
|
||||
names.add("Sophie");
|
||||
names.add("Jack");
|
||||
names.add("Sadie");
|
||||
names.add("Toby");
|
||||
names.add("Chloe");
|
||||
names.add("Cody");
|
||||
names.add("Bailey");
|
||||
names.add("Buster");
|
||||
names.add("Lola");
|
||||
names.add("Duke");
|
||||
names.add("Zoe");
|
||||
names.add("Cooper");
|
||||
names.add("Abby");
|
||||
names.add("Riley");
|
||||
names.add("Ginger");
|
||||
names.add("Harley");
|
||||
names.add("Roxy");
|
||||
names.add("Bear");
|
||||
names.add("Gracie");
|
||||
names.add("Tucker");
|
||||
names.add("Coco");
|
||||
names.add("Murphy");
|
||||
names.add("Sasha");
|
||||
names.add("Lucky");
|
||||
names.add("Lily");
|
||||
names.add("Oliver");
|
||||
names.add("Angel");
|
||||
names.add("Sam");
|
||||
names.add("Princess");
|
||||
names.add("Oscar");
|
||||
names.add("Emma");
|
||||
names.add("Teddy");
|
||||
names.add("Annie");
|
||||
names.add("Winston");
|
||||
names.add("Rosie");
|
||||
}
|
||||
|
||||
public static List<String> getBreeds() {
|
||||
return breeds;
|
||||
}
|
||||
|
||||
public static List<String> getNames() {
|
||||
return names;
|
||||
}
|
||||
|
||||
public static String getRandomBreed() {
|
||||
return breeds.get(ThreadLocalRandom.current().nextInt(0, breeds.size() - 1));
|
||||
}
|
||||
|
||||
public static String getRandomName() {
|
||||
return names.get(ThreadLocalRandom.current().nextInt(0, names.size() - 1));
|
||||
}
|
||||
|
||||
public static Date getRandomDoB() {
|
||||
GregorianCalendar gc = new GregorianCalendar();
|
||||
|
||||
int year = ThreadLocalRandom.current().nextInt(Calendar.getInstance().get(Calendar.YEAR) - 15,
|
||||
Calendar.getInstance().get(Calendar.YEAR));
|
||||
|
||||
gc.set(Calendar.YEAR, year);
|
||||
|
||||
int dayOfYear = ThreadLocalRandom.current().nextInt(1, gc.getActualMaximum(Calendar.DAY_OF_YEAR));
|
||||
|
||||
gc.set(Calendar.DAY_OF_YEAR, dayOfYear);
|
||||
return gc.getTime();
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.function.adapter.azure.web;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import jakarta.servlet.Filter;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.web.servlet.HandlerAdapter;
|
||||
import org.springframework.web.servlet.HandlerMapping;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||
|
||||
@Configuration
|
||||
@Import({ PetsController.class })
|
||||
public class PetStoreSpringAppConfig {
|
||||
/*
|
||||
* Create required HandlerMapping, to avoid several default HandlerMapping
|
||||
* instances being created
|
||||
*/
|
||||
@Bean
|
||||
public HandlerMapping handlerMapping() {
|
||||
return new RequestMappingHandlerMapping();
|
||||
}
|
||||
|
||||
/*
|
||||
* Create required HandlerAdapter, to avoid several default HandlerAdapter
|
||||
* instances being created
|
||||
*/
|
||||
@Bean
|
||||
public HandlerAdapter handlerAdapter() {
|
||||
return new RequestMappingHandlerAdapter();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Filter filter() {
|
||||
return new Filter() {
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
System.out.println("FILTER ===> Hello from: " + request.getLocalAddr());
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.function.adapter.azure.web;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||
|
||||
@RestController
|
||||
@EnableWebMvc
|
||||
public class PetsController {
|
||||
@RequestMapping(path = "/pets", method = RequestMethod.POST)
|
||||
public Pet createPet(@RequestBody Pet newPet) {
|
||||
if (newPet.getName() == null || newPet.getBreed() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Pet dbPet = newPet;
|
||||
dbPet.setId(UUID.randomUUID().toString());
|
||||
return dbPet;
|
||||
}
|
||||
|
||||
@RequestMapping(path = "/pets", method = RequestMethod.GET)
|
||||
public Pet[] listPets(@RequestParam("limit") Optional<Integer> limit, Principal principal) {
|
||||
System.out.println("=====> EXECUTING");
|
||||
int queryLimit = 10;
|
||||
if (limit.isPresent()) {
|
||||
queryLimit = limit.get();
|
||||
}
|
||||
|
||||
Pet[] outputPets = new Pet[queryLimit];
|
||||
|
||||
for (int i = 0; i < queryLimit; i++) {
|
||||
Pet newPet = new Pet();
|
||||
newPet.setId(UUID.randomUUID().toString());
|
||||
newPet.setName(PetData.getRandomName());
|
||||
newPet.setBreed(PetData.getRandomBreed());
|
||||
newPet.setDateOfBirth(PetData.getRandomDoB());
|
||||
outputPets[i] = newPet;
|
||||
}
|
||||
|
||||
return outputPets;
|
||||
}
|
||||
|
||||
@RequestMapping(path = "/pets/{petId}", method = RequestMethod.GET)
|
||||
public Pet listPets() {
|
||||
System.out.println("=====> Getting pet by id");
|
||||
Pet newPet = new Pet();
|
||||
newPet.setId(UUID.randomUUID().toString());
|
||||
newPet.setBreed(PetData.getRandomBreed());
|
||||
newPet.setDateOfBirth(PetData.getRandomDoB());
|
||||
newPet.setName(PetData.getRandomName());
|
||||
return newPet;
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.function.adapter.azure.web;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import com.microsoft.azure.functions.ExecutionContext;
|
||||
|
||||
public class TestExecutionContext implements ExecutionContext {
|
||||
|
||||
private String name;
|
||||
|
||||
public TestExecutionContext(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Logger getLogger() {
|
||||
return Logger.getLogger(TestExecutionContext.class.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getInvocationId() {
|
||||
return UUID.randomUUID().toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFunctionName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
////
|
||||
DO NOT EDIT THIS FILE. IT WAS GENERATED.
|
||||
Manual changes to this file will be lost when it is generated again.
|
||||
Edit the files in the src/main/asciidoc/ directory instead.
|
||||
////
|
||||
|
||||
This project provides an adapter layer for a Spring Cloud Function application onto Azure.
|
||||
You can write an app with a single `@Bean` of type `Function` and it will be deployable in Azure if you get the JAR file laid out right.
|
||||
|
||||
== Sample Functions
|
||||
|
||||
- ../../spring-cloud-function-samples/function-sample-azure-http-trigger[Azure HTTP Trigger (Maven)]
|
||||
- ../../spring-cloud-function-samples/function-sample-azure-http-trigger-gradle[Azure HTTP Trigger (Gradle)]
|
||||
- ../../spring-cloud-function-samples/function-sample-azure-blob-trigger[Azure Blob Trigger (Maven)]
|
||||
- ../../spring-cloud-function-samples/function-sample-azure-timer-trigger[Azure Timer Trigger (Maven)]
|
||||
- ../../spring-cloud-function-samples/function-sample-azure-kafka-trigger[Azure Kafka Trigger & Output Binding (Maven)]
|
||||
- ../../spring-cloud-function-samples/function-sample-azure/[(Legacy - FunctionInvoker) Azure HTTP Trigger (Maven)]
|
||||
@@ -1,110 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>spring-cloud-function-adapter-azure</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>spring-cloud-function-adapter-azure</name>
|
||||
<description>Azure Function Adapter for Spring Cloud Function</description>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-function-adapter-parent</artifactId>
|
||||
<version>4.1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<java.version>17</java.version>
|
||||
|
||||
<azure.functions.java.core.version>3.0.0</azure.functions.java.core.version>
|
||||
<azure.functions.java.spi.version>1.0.0</azure.functions.java.spi.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-function-context</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<artifactId>spring-boot-starter-logging</artifactId>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.microsoft.azure.functions</groupId>
|
||||
<artifactId>azure-functions-java-library</artifactId>
|
||||
<version>${azure.functions.java.core.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.microsoft.azure.functions</groupId>
|
||||
<artifactId>azure-functions-java-spi</artifactId>
|
||||
<version>${azure.functions.java.spi.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-log4j12</artifactId>
|
||||
<version>2.0.7</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>spring-snapshots</id>
|
||||
<name>Spring Snapshots</name>
|
||||
<url>https://repo.spring.io/snapshot</url>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>spring-milestones</id>
|
||||
<name>Spring Milestones</name>
|
||||
<url>https://repo.spring.io/milestone</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
<pluginRepositories>
|
||||
<pluginRepository>
|
||||
<id>spring-snapshots</id>
|
||||
<name>Spring Snapshots</name>
|
||||
<url>https://repo.spring.io/snapshot</url>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</pluginRepository>
|
||||
<pluginRepository>
|
||||
<id>spring-milestones</id>
|
||||
<name>Spring Milestones</name>
|
||||
<url>https://repo.spring.io/milestone</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</pluginRepository>
|
||||
</pluginRepositories>
|
||||
|
||||
</project>
|
||||
@@ -1,105 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.function.adapter.azure;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import com.microsoft.azure.functions.spi.inject.FunctionInstanceInjector;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.boot.ResourceBanner;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.WebApplicationType;
|
||||
import org.springframework.cloud.function.utils.FunctionClassUtils;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* The instance factory used by the Spring framework to initialize Azure function instance. It is waived with the Azure
|
||||
* Java Worker through the META-INFO/services/com.microsoft.azure.functions.spi.inject.FunctionInstanceInjector service
|
||||
* hook. The Azure Java Worker delegates scans the classpath for service definition and delegates the function class
|
||||
* creation to this instance factory.
|
||||
* @author Christian Tzolov
|
||||
* @since 3.2.9
|
||||
*/
|
||||
public class AzureFunctionInstanceInjector implements FunctionInstanceInjector {
|
||||
|
||||
private static Log logger = LogFactory.getLog(AzureFunctionInstanceInjector.class);
|
||||
|
||||
private static ConfigurableApplicationContext APPLICATION_CONTEXT;
|
||||
|
||||
/**
|
||||
* This method is called by the Azure Java Worker on every function invocation. The Worker sends in the classes
|
||||
* annotated with @FunctionName annotations and allows the Spring framework to initialize the function instance as a
|
||||
* Spring Bean and return the instance. Then the Worker uses the created instance to invoke the function.
|
||||
* @param functionClass the class that contains customer Azure functions (e.g. @FunctionName annotated )
|
||||
* @param <T> customer Azure functions class type
|
||||
* @return the instance that will be invoked on by azure functions java worker
|
||||
* @throws Exception any exception that is thrown by the Spring framework during instance creation
|
||||
*/
|
||||
@Override
|
||||
public <T> T getInstance(Class<T> functionClass) throws Exception {
|
||||
try {
|
||||
// Backward compatibility workaround. If the function class is of type FunctionInvoker then create plain
|
||||
// Java instance and delegate to FunctionInvoker adaptor approach.
|
||||
if (ClassUtils.isAssignable(FunctionInvoker.class, functionClass)) {
|
||||
return functionClass.getDeclaredConstructor().newInstance();
|
||||
}
|
||||
|
||||
initialize();
|
||||
|
||||
Map<String, T> azureFunctionBean = APPLICATION_CONTEXT.getBeansOfType(functionClass);
|
||||
if (CollectionUtils.isEmpty(azureFunctionBean)) {
|
||||
throw new IllegalStateException(
|
||||
"Failed to retrieve Bean instance for: " + functionClass
|
||||
+ ". The class should be annotated with @Component to let the Spring framework initialize it!");
|
||||
}
|
||||
return azureFunctionBean.entrySet().iterator().next().getValue();
|
||||
}
|
||||
catch (Exception e) {
|
||||
if (APPLICATION_CONTEXT != null) {
|
||||
APPLICATION_CONTEXT.close();
|
||||
}
|
||||
throw new IllegalStateException("Failed to initialize", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a static Application Context instance shared between multiple function invocations.
|
||||
*/
|
||||
private static void initialize() {
|
||||
synchronized (AzureFunctionInstanceInjector.class.getName()) {
|
||||
if (APPLICATION_CONTEXT == null) {
|
||||
Class<?> springConfigurationClass = FunctionClassUtils.getStartClass();
|
||||
logger.info("Initializing: " + springConfigurationClass);
|
||||
APPLICATION_CONTEXT = springApplication(springConfigurationClass).run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static SpringApplication springApplication(Class<?> configurationClass) {
|
||||
SpringApplication application = new org.springframework.cloud.function.context.FunctionalSpringApplication(
|
||||
configurationClass);
|
||||
application.setWebApplicationType(WebApplicationType.NONE);
|
||||
application.setBanner(new ResourceBanner(
|
||||
new DefaultResourceLoader().getResource("classpath:/spring-azure-function-banner.txt")));
|
||||
return application;
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.function.adapter.azure;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import com.microsoft.azure.functions.ExecutionContext;
|
||||
import com.microsoft.azure.functions.HttpMethod;
|
||||
import com.microsoft.azure.functions.HttpRequestMessage;
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
|
||||
/**
|
||||
* @author Christian Tzolov
|
||||
* @author Oleg Zhurakousky
|
||||
* @author Chris Bono
|
||||
* @since 4.0
|
||||
*/
|
||||
public final class AzureFunctionUtil {
|
||||
|
||||
/**
|
||||
* Message header key name used to store and extract the ExecutionContext.
|
||||
*/
|
||||
public static String EXECUTION_CONTEXT = "executionContext";
|
||||
|
||||
private AzureFunctionUtil() {
|
||||
};
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
public static <I> Object enhanceInputIfNecessary(Object input, ExecutionContext executionContext) {
|
||||
if (input == null) { // Supplier
|
||||
return input;
|
||||
}
|
||||
if (input instanceof Publisher) {
|
||||
return Flux.from((Publisher) input).map(item -> {
|
||||
if (item instanceof Message) {
|
||||
return MessageBuilder.fromMessage((Message<I>) item)
|
||||
.setHeaderIfAbsent(EXECUTION_CONTEXT, executionContext).build();
|
||||
}
|
||||
else {
|
||||
return constructInputMessageFromItem(input, executionContext);
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (input instanceof Message) {
|
||||
return MessageBuilder.fromMessage((Message<I>) input)
|
||||
.setHeaderIfAbsent(EXECUTION_CONTEXT, executionContext).build();
|
||||
}
|
||||
else if (input instanceof Iterable) {
|
||||
return Flux.fromIterable((Iterable) input).map(item -> {
|
||||
return constructInputMessageFromItem(item, executionContext);
|
||||
});
|
||||
}
|
||||
return constructInputMessageFromItem(input, executionContext);
|
||||
}
|
||||
|
||||
private static <I> Message<?> constructInputMessageFromItem(Object input, ExecutionContext executionContext) {
|
||||
MessageBuilder<?> messageBuilder = null;
|
||||
if (input instanceof HttpRequestMessage) {
|
||||
HttpRequestMessage<I> requestMessage = (HttpRequestMessage<I>) input;
|
||||
Object payload = requestMessage.getHttpMethod() != null
|
||||
&& requestMessage.getHttpMethod().equals(HttpMethod.GET)
|
||||
? requestMessage.getQueryParameters()
|
||||
: requestMessage.getBody();
|
||||
|
||||
if (payload == null) {
|
||||
payload = Optional.empty();
|
||||
}
|
||||
messageBuilder = MessageBuilder.withPayload(payload).copyHeaders(getHeaders(requestMessage));
|
||||
}
|
||||
else {
|
||||
messageBuilder = MessageBuilder.withPayload(input);
|
||||
}
|
||||
return messageBuilder.setHeaderIfAbsent(EXECUTION_CONTEXT, executionContext).build();
|
||||
}
|
||||
|
||||
private static <I> MessageHeaders getHeaders(HttpRequestMessage<I> event) {
|
||||
Map<String, Object> headers = new HashMap<String, Object>();
|
||||
|
||||
if (event.getHeaders() != null) {
|
||||
headers.putAll(event.getHeaders());
|
||||
}
|
||||
if (event.getQueryParameters() != null) {
|
||||
headers.putAll(event.getQueryParameters());
|
||||
}
|
||||
if (event.getUri() != null) {
|
||||
headers.put("path", event.getUri().getPath());
|
||||
}
|
||||
|
||||
if (event.getHttpMethod() != null) {
|
||||
headers.put("httpMethod", event.getHttpMethod().toString());
|
||||
}
|
||||
|
||||
headers.put("request", event.getBody());
|
||||
return new MessageHeaders(headers);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,390 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.function.adapter.azure;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Optional;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.microsoft.azure.functions.ExecutionContext;
|
||||
import com.microsoft.azure.functions.HttpMethod;
|
||||
import com.microsoft.azure.functions.HttpRequestMessage;
|
||||
import com.microsoft.azure.functions.HttpResponseMessage.Builder;
|
||||
import com.microsoft.azure.functions.HttpStatus;
|
||||
import com.microsoft.azure.functions.OutputBinding;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.WebApplicationType;
|
||||
import org.springframework.cloud.function.context.FunctionCatalog;
|
||||
import org.springframework.cloud.function.context.FunctionRegistration;
|
||||
import org.springframework.cloud.function.context.FunctionRegistry;
|
||||
import org.springframework.cloud.function.context.catalog.FunctionTypeUtils;
|
||||
import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry;
|
||||
import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper;
|
||||
import org.springframework.cloud.function.context.config.FunctionContextUtils;
|
||||
import org.springframework.cloud.function.context.config.JsonMessageConverter;
|
||||
import org.springframework.cloud.function.context.config.SmartCompositeMessageConverter;
|
||||
import org.springframework.cloud.function.json.JacksonMapper;
|
||||
import org.springframework.cloud.function.json.JsonMapper;
|
||||
import org.springframework.cloud.function.utils.FunctionClassUtils;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* @param <I> input type
|
||||
* @param <O> result type
|
||||
* @author Oleg Zhurakousky
|
||||
* @author Chris Bono
|
||||
* @author Christian Tzolov
|
||||
*
|
||||
* @since 3.2
|
||||
*
|
||||
* @deprecated since 4.0.0 in favor of the dependency injection implementation {@link AzureFunctionInstanceInjector}.
|
||||
* Follow the official documentation for further information.
|
||||
*/
|
||||
@Deprecated
|
||||
public class FunctionInvoker<I, O> {
|
||||
|
||||
private static Log logger = LogFactory.getLog(FunctionInvoker.class);
|
||||
|
||||
private static String EXECUTION_CONTEXT = "executionContext";
|
||||
|
||||
private static FunctionCatalog FUNCTION_CATALOG;
|
||||
|
||||
private static ConfigurableApplicationContext APPLICATION_CONTEXT;
|
||||
|
||||
private static JsonMapper OBJECT_MAPPER;
|
||||
|
||||
public FunctionInvoker(Class<?> configurationClass) {
|
||||
try {
|
||||
initialize(configurationClass);
|
||||
}
|
||||
catch (Exception e) {
|
||||
this.close();
|
||||
throw new IllegalStateException("Failed to initialize", e);
|
||||
}
|
||||
}
|
||||
|
||||
public FunctionInvoker() {
|
||||
this(FunctionClassUtils.getStartClass());
|
||||
}
|
||||
|
||||
public O handleRequest(ExecutionContext context) {
|
||||
return this.handleRequest(null, context);
|
||||
}
|
||||
|
||||
public void close() {
|
||||
FUNCTION_CATALOG = null;
|
||||
}
|
||||
|
||||
public void handleOutput(I input, OutputBinding<O> binding,
|
||||
ExecutionContext context) {
|
||||
O result = handleRequest(input, context);
|
||||
binding.setValue(result);
|
||||
}
|
||||
|
||||
private FunctionInvocationWrapper discoverFunction(String functionDefinition) {
|
||||
FunctionInvocationWrapper function = FUNCTION_CATALOG.lookup(functionDefinition);
|
||||
if (function != null && StringUtils.hasText(functionDefinition)
|
||||
&& !function.getFunctionDefinition().equals(functionDefinition)) {
|
||||
this.registerFunction(functionDefinition);
|
||||
function = FUNCTION_CATALOG.lookup(functionDefinition);
|
||||
}
|
||||
else if (function == null && StringUtils.hasText(functionDefinition)
|
||||
&& APPLICATION_CONTEXT.containsBean(functionDefinition)) {
|
||||
this.registerFunction(functionDefinition);
|
||||
function = FUNCTION_CATALOG.lookup(functionDefinition);
|
||||
}
|
||||
return function;
|
||||
}
|
||||
|
||||
public O handleRequest(I input, ExecutionContext executionContext) {
|
||||
String functionDefinition = executionContext.getFunctionName();
|
||||
FunctionInvocationWrapper function = this.discoverFunction(functionDefinition);
|
||||
Object enhancedInput = enhanceInputIfNecessary(input, executionContext);
|
||||
|
||||
Object functionResult = function.apply(enhancedInput);
|
||||
|
||||
if (functionResult instanceof Publisher) {
|
||||
return postProcessReactiveFunctionResult(input, enhancedInput, (Publisher<?>) functionResult, function,
|
||||
executionContext);
|
||||
}
|
||||
return postProcessImperativeFunctionResult(input, enhancedInput, functionResult, function, executionContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Post-processes the result from a non-reactive function invocation before returning it to the Azure runtime. The
|
||||
* default behavior is to {@link #convertOutputIfNecessary possibly convert} the result.
|
||||
*
|
||||
* <p>
|
||||
* Provides a hook for custom function invokers to extend/modify the function results handling.
|
||||
*
|
||||
* @param rawInputs the inputs passed in from the Azure runtime
|
||||
* @param functionInputs the actual inputs used for the function invocation; may be {@link #enhanceInputIfNecessary
|
||||
* different} from the {@literal rawInputs}
|
||||
* @param functionResult the result from the function invocation
|
||||
* @param function the invoked function
|
||||
* @param executionContext the Azure execution context
|
||||
* @return the possibly modified function results
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
protected O postProcessImperativeFunctionResult(I rawInputs, Object functionInputs, Object functionResult,
|
||||
FunctionInvocationWrapper function, ExecutionContext executionContext) {
|
||||
return (O) this.convertOutputIfNecessary(rawInputs, functionResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* Post-processes the result from a reactive function invocation before returning it to the Azure runtime. The
|
||||
* default behavior is to delegate to {@link #postProcessMonoFunctionResult} or
|
||||
* {@link #postProcessFluxFunctionResult} based on the result type.
|
||||
*
|
||||
* <p>
|
||||
* Provides a hook for custom function invokers to extend/modify the function results handling.
|
||||
*
|
||||
* @param rawInputs the inputs passed in from the Azure runtime
|
||||
* @param functionInputs the actual inputs used for the function invocation; may be {@link #enhanceInputIfNecessary
|
||||
* different} from the {@literal rawInputs}
|
||||
* @param functionResult the result from the function invocation
|
||||
* @param function the invoked function
|
||||
* @param executionContext the Azure execution context
|
||||
* @return the possibly modified function results
|
||||
*/
|
||||
protected O postProcessReactiveFunctionResult(I rawInputs, Object functionInputs, Publisher<?> functionResult,
|
||||
FunctionInvocationWrapper function, ExecutionContext executionContext) {
|
||||
if (FunctionTypeUtils.isMono(function.getOutputType())) {
|
||||
return postProcessMonoFunctionResult(rawInputs, functionInputs, Mono.from(functionResult), function,
|
||||
executionContext);
|
||||
}
|
||||
return postProcessFluxFunctionResult(rawInputs, functionInputs, Flux.from(functionResult), function,
|
||||
executionContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Post-processes the {@code Mono} result from a reactive function invocation before returning it to the Azure
|
||||
* runtime. The default behavior is to {@link Mono#blockOptional()} and {@link #convertOutputIfNecessary possibly
|
||||
* convert} the result.
|
||||
*
|
||||
* <p>
|
||||
* Provides a hook for custom function invokers to extend/modify the function results handling.
|
||||
*
|
||||
* @param rawInputs the inputs passed in from the Azure runtime
|
||||
* @param functionInputs the actual inputs used for the function invocation; may be {@link #enhanceInputIfNecessary
|
||||
* different} from the {@literal rawInputs}
|
||||
* @param functionResult the Mono result from the function invocation
|
||||
* @param function the invoked function
|
||||
* @param executionContext the Azure execution context
|
||||
* @return the possibly modified function results
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
protected O postProcessMonoFunctionResult(I rawInputs, Object functionInputs, Mono<?> functionResult,
|
||||
FunctionInvocationWrapper function, ExecutionContext executionContext) {
|
||||
return (O) this.convertOutputIfNecessary(rawInputs, functionResult.blockOptional().get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Post-processes the {@code Flux} result from a reactive function invocation before returning it to the Azure
|
||||
* runtime. The default behavior is to {@link Flux#toIterable() block} and {@link #convertOutputIfNecessary possibly
|
||||
* convert} the results.
|
||||
*
|
||||
* <p>
|
||||
* Provides a hook for custom function invokers to extend/modify the function results handling.
|
||||
*
|
||||
* @param rawInputs the inputs passed in from the Azure runtime
|
||||
* @param functionInputs the actual inputs used for the function invocation; may be {@link #enhanceInputIfNecessary
|
||||
* different} from the {@literal rawInputs}
|
||||
* @param functionResult the Mono result from the function invocation
|
||||
* @param function the invoked function
|
||||
* @param executionContext the Azure execution context
|
||||
* @return the possibly modified function results
|
||||
*/
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
protected O postProcessFluxFunctionResult(I rawInputs, Object functionInputs, Flux<?> functionResult,
|
||||
FunctionInvocationWrapper function, ExecutionContext executionContext) {
|
||||
List resultList = new ArrayList<>();
|
||||
for (Object resultItem : functionResult.toIterable()) {
|
||||
if (resultItem instanceof Collection) {
|
||||
resultList.addAll((Collection) resultItem);
|
||||
}
|
||||
else {
|
||||
if (!function.isSupplier()
|
||||
&& Collection.class.isAssignableFrom(FunctionTypeUtils.getRawType(function.getInputType()))
|
||||
&& !Collection.class.isAssignableFrom(FunctionTypeUtils.getRawType(function.getOutputType()))) {
|
||||
return (O) this.convertOutputIfNecessary(rawInputs, resultItem);
|
||||
}
|
||||
else {
|
||||
resultList.add(resultItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
return (O) this.convertOutputIfNecessary(rawInputs, resultList);
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
private void registerFunction(String functionDefinition) {
|
||||
if (APPLICATION_CONTEXT.containsBean(functionDefinition)) {
|
||||
FunctionRegistration functionRegistration = new FunctionRegistration(
|
||||
APPLICATION_CONTEXT.getBean(functionDefinition), functionDefinition);
|
||||
|
||||
Type type = FunctionContextUtils.findType(functionDefinition, APPLICATION_CONTEXT.getBeanFactory());
|
||||
|
||||
functionRegistration = functionRegistration.type(type);
|
||||
|
||||
((FunctionRegistry) FUNCTION_CATALOG).register(functionRegistration);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
private Object enhanceInputIfNecessary(Object input, ExecutionContext executionContext) {
|
||||
if (input == null) { // Supplier
|
||||
return input;
|
||||
}
|
||||
if (input instanceof Publisher) {
|
||||
return Flux.from((Publisher) input).map(item -> {
|
||||
if (item instanceof Message) {
|
||||
return MessageBuilder.fromMessage((Message<I>) item)
|
||||
.setHeaderIfAbsent(EXECUTION_CONTEXT, executionContext).build();
|
||||
}
|
||||
else {
|
||||
return constructInputMessageFromItem(input, executionContext);
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (input instanceof Message) {
|
||||
return MessageBuilder.fromMessage((Message<I>) input)
|
||||
.setHeaderIfAbsent(EXECUTION_CONTEXT, executionContext).build();
|
||||
}
|
||||
else if (input instanceof Iterable) {
|
||||
return Flux.fromIterable((Iterable) input).map(item -> {
|
||||
return constructInputMessageFromItem(item, executionContext);
|
||||
});
|
||||
}
|
||||
return constructInputMessageFromItem(input, executionContext);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Object convertOutputIfNecessary(Object input, Object output) {
|
||||
if (input instanceof HttpRequestMessage) {
|
||||
HttpRequestMessage<I> requestMessage = (HttpRequestMessage<I>) input;
|
||||
Map<String, Object> headers = null;
|
||||
if (output instanceof Message) {
|
||||
headers = ((Message<I>) output).getHeaders();
|
||||
output = ((Message<I>) output).getPayload();
|
||||
}
|
||||
Builder responseBuilder = requestMessage.createResponseBuilder(HttpStatus.OK).body(output);
|
||||
if (headers != null) {
|
||||
for (Entry<String, Object> headersEntry : headers.entrySet()) {
|
||||
if (headersEntry.getValue() != null) {
|
||||
responseBuilder.header(headersEntry.getKey(), headersEntry.getValue().toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
return responseBuilder.build();
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Message<?> constructInputMessageFromItem(Object input, ExecutionContext executionContext) {
|
||||
MessageBuilder<?> messageBuilder = null;
|
||||
if (input instanceof HttpRequestMessage) {
|
||||
HttpRequestMessage<I> requestMessage = (HttpRequestMessage<I>) input;
|
||||
Object payload = requestMessage.getHttpMethod() != null
|
||||
&& requestMessage.getHttpMethod().equals(HttpMethod.GET)
|
||||
? requestMessage.getQueryParameters()
|
||||
: requestMessage.getBody();
|
||||
|
||||
if (payload == null) {
|
||||
payload = Optional.empty();
|
||||
}
|
||||
messageBuilder = MessageBuilder.withPayload(payload).copyHeaders(this.getHeaders(requestMessage));
|
||||
}
|
||||
else {
|
||||
messageBuilder = MessageBuilder.withPayload(input);
|
||||
}
|
||||
return messageBuilder.setHeaderIfAbsent(EXECUTION_CONTEXT, executionContext).build();
|
||||
}
|
||||
|
||||
private MessageHeaders getHeaders(HttpRequestMessage<I> event) {
|
||||
Map<String, Object> headers = new HashMap<String, Object>();
|
||||
|
||||
if (event.getHeaders() != null) {
|
||||
headers.putAll(event.getHeaders());
|
||||
}
|
||||
if (event.getQueryParameters() != null) {
|
||||
headers.putAll(event.getQueryParameters());
|
||||
}
|
||||
if (event.getUri() != null) {
|
||||
headers.put("path", event.getUri().getPath());
|
||||
}
|
||||
|
||||
if (event.getHttpMethod() != null) {
|
||||
headers.put("httpMethod", event.getHttpMethod().toString());
|
||||
}
|
||||
|
||||
headers.put("request", event.getBody());
|
||||
return new MessageHeaders(headers);
|
||||
}
|
||||
|
||||
private static void initialize(Class<?> configurationClass) {
|
||||
synchronized (FunctionInvoker.class.getName()) {
|
||||
if (FUNCTION_CATALOG == null) {
|
||||
logger.info("Initializing: " + configurationClass);
|
||||
SpringApplication builder = springApplication(configurationClass);
|
||||
APPLICATION_CONTEXT = builder.run();
|
||||
|
||||
Map<String, FunctionCatalog> mf = APPLICATION_CONTEXT.getBeansOfType(FunctionCatalog.class);
|
||||
if (CollectionUtils.isEmpty(mf)) {
|
||||
OBJECT_MAPPER = new JacksonMapper(new ObjectMapper());
|
||||
JsonMessageConverter jsonConverter = new JsonMessageConverter(OBJECT_MAPPER);
|
||||
SmartCompositeMessageConverter messageConverter = new SmartCompositeMessageConverter(
|
||||
Collections.singletonList(jsonConverter));
|
||||
FUNCTION_CATALOG = new SimpleFunctionRegistry(
|
||||
APPLICATION_CONTEXT.getBeanFactory().getConversionService(),
|
||||
messageConverter, OBJECT_MAPPER);
|
||||
}
|
||||
else {
|
||||
OBJECT_MAPPER = APPLICATION_CONTEXT.getBean(JsonMapper.class);
|
||||
FUNCTION_CATALOG = mf.values().iterator().next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static SpringApplication springApplication(Class<?> configurationClass) {
|
||||
SpringApplication application = new org.springframework.cloud.function.context.FunctionalSpringApplication(
|
||||
configurationClass);
|
||||
application.setWebApplicationType(WebApplicationType.NONE);
|
||||
return application;
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.function.adapter.azure;
|
||||
|
||||
import com.microsoft.azure.functions.HttpRequestMessage;
|
||||
import com.microsoft.azure.functions.HttpResponseMessage;
|
||||
|
||||
|
||||
/**
|
||||
* Implementation of HTTP Request Handler for Azure which supports
|
||||
* HttpRequestMessage and HttpResponseMessage the types required by
|
||||
* Azure Functions for HTTP-triggered functions.
|
||||
*
|
||||
* @param <I> input type
|
||||
* @author Oleg Zhurakousky
|
||||
*
|
||||
* @since 3.2
|
||||
*
|
||||
* @deprecated since 4.0.0 in favor of the dependency injection implementation {@link AzureFunctionInstanceInjector}.
|
||||
* Follow the official documentation for further information.
|
||||
*/
|
||||
@Deprecated
|
||||
public class HttpFunctionInvoker<I> extends
|
||||
FunctionInvoker<HttpRequestMessage<I>, HttpResponseMessage> {
|
||||
|
||||
public HttpFunctionInvoker(Class<?> configurationClass) {
|
||||
super(configurationClass);
|
||||
}
|
||||
|
||||
public HttpFunctionInvoker() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
org.springframework.cloud.function.adapter.azure.AzureFunctionInstanceInjector
|
||||
@@ -1,8 +0,0 @@
|
||||
____ _ _ _____ _ _
|
||||
/ ___| _ __ _ __(_)_ __ __ _ / \ _____ _ _ __ ___ | ___| _ _ __ ___| |_(_) ___ _ __ ___
|
||||
\___ \| '_ \| '__| | '_ \ / _` | / _ \ |_ / | | | '__/ _ \ | |_ | | | | '_ \ / __| __| |/ _ \| '_ \/ __|
|
||||
___) | |_) | | | | | | | (_| | / ___ \ / /| |_| | | | __/ | _|| |_| | | | | (__| |_| | (_) | | | \__ \
|
||||
|____/| .__/|_| |_|_| |_|\__, | /_/ \_\/___|\__,_|_| \___| |_| \__,_|_| |_|\___|\__|_|\___/|_| |_|___/
|
||||
|_| |___/
|
||||
${application.title} ${application.version}
|
||||
Powered by Spring Boot ${spring-boot.version} and Azure Functions
|
||||
@@ -1,201 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.function.adapter.azure;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.microsoft.azure.functions.ExecutionContext;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.cloud.function.adapter.azure.helper.TestExecutionContext;
|
||||
import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.util.Lists.list;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyList;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.same;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link FunctionInvoker} custom result handling.
|
||||
*
|
||||
* @author Chris Bono
|
||||
*/
|
||||
class CustomFunctionInvokerTests {
|
||||
|
||||
private FunctionInvoker<?, ?> currentInvoker;
|
||||
|
||||
@AfterEach
|
||||
void closeCurrentInvoker() {
|
||||
if (this.currentInvoker != null) {
|
||||
this.currentInvoker.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies custom result handling and proper post-process callback invocation for an imperative function.
|
||||
*/
|
||||
@Test
|
||||
void customImperativeResultHandling() {
|
||||
FunctionInvoker<String, String> invoker = new FunctionInvoker<String, String>(TestFunctionsConfig.class) {
|
||||
@Override
|
||||
protected String postProcessImperativeFunctionResult(String rawInputs, Object functionInputs,
|
||||
Object functionResult, FunctionInvocationWrapper function, ExecutionContext executionContext
|
||||
) {
|
||||
return functionResult + "+imperative";
|
||||
}
|
||||
};
|
||||
invoker = spyOnAndCloseAfterTest(invoker);
|
||||
ExecutionContext executionContext = new TestExecutionContext("imperativeUppercase");
|
||||
String result = invoker.handleRequest("foo", executionContext);
|
||||
assertThat(result).isEqualTo("FOO+imperative");
|
||||
|
||||
// Below here verifies that the expected callback(s) were invoked w/ the expected arguments
|
||||
|
||||
// Only imperative post-process callback should be called
|
||||
verify(invoker, never()).postProcessReactiveFunctionResult(anyString(), any(), any(Publisher.class), any(), same(executionContext));
|
||||
verify(invoker, never()).postProcessMonoFunctionResult(anyString(), any(), any(Mono.class), any(), same(executionContext));
|
||||
verify(invoker, never()).postProcessFluxFunctionResult(anyString(), any(), any(Flux.class), any(), same(executionContext));
|
||||
|
||||
// Only sniff-test the payload of the input message (the other fields are problematic to verify and no value doing that here)
|
||||
ArgumentCaptor<GenericMessage> functionInputsCaptor = ArgumentCaptor.forClass(GenericMessage.class);
|
||||
verify(invoker).postProcessImperativeFunctionResult(eq("foo"), functionInputsCaptor.capture(), eq("FOO"), any(), same(executionContext));
|
||||
assertThat(functionInputsCaptor.getValue()).extracting(GenericMessage::getPayload).isEqualTo("foo");
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies custom result handling and proper post-process callback invocation for a reactive Mono function.
|
||||
*/
|
||||
@Test
|
||||
void customReactiveMonoResultHandling() {
|
||||
FunctionInvoker<String, String> invoker = new FunctionInvoker<String, String>(TestFunctionsConfig.class) {
|
||||
@Override
|
||||
protected String postProcessMonoFunctionResult(String rawInputs, Object functionInputs, Mono<?> functionResult,
|
||||
FunctionInvocationWrapper function, ExecutionContext executionContext
|
||||
) {
|
||||
return functionResult.block().toString() + "+mono";
|
||||
}
|
||||
};
|
||||
invoker = spyOnAndCloseAfterTest(invoker);
|
||||
ExecutionContext executionContext = new TestExecutionContext("reactiveMonoUppercase");
|
||||
String result = invoker.handleRequest("foo", executionContext);
|
||||
assertThat(result).isEqualTo("FOO+mono");
|
||||
|
||||
// Below here verifies that the expected callback(s) were invoked w/ the expected arguments
|
||||
|
||||
// Only publisher->mono post-process callbacks should be called
|
||||
verify(invoker, never()).postProcessImperativeFunctionResult(anyString(), any(), any(), any(), same(executionContext));
|
||||
verify(invoker, never()).postProcessFluxFunctionResult(anyString(), any(), any(Flux.class), any(), same(executionContext));
|
||||
|
||||
// Only sniff-test the payload of the input message and the mono (the other fields are problematic to verify and no value doing that here)
|
||||
ArgumentCaptor<GenericMessage> functionInputsCaptor = ArgumentCaptor.forClass(GenericMessage.class);
|
||||
ArgumentCaptor<Mono> functionResultCaptor = ArgumentCaptor.forClass(Mono.class);
|
||||
verify(invoker).postProcessReactiveFunctionResult(eq("foo"), functionInputsCaptor.capture(), functionResultCaptor.capture(), any(), same(executionContext));
|
||||
verify(invoker).postProcessMonoFunctionResult(eq("foo"), functionInputsCaptor.capture(), functionResultCaptor.capture(), any(), same(executionContext));
|
||||
// NOTE: The captors get called twice as the args are just delegated from publisher->mono callback
|
||||
assertThat(functionInputsCaptor.getAllValues()).extracting(GenericMessage::getPayload).containsExactly("foo", "foo");
|
||||
assertThat(functionResultCaptor.getAllValues()).extracting(Mono::block).containsExactly("FOO", "FOO");
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies custom result handling and proper post-process callback invocation for a reactive Flux function.
|
||||
*/
|
||||
@Test
|
||||
void customReactiveFluxResultHandling() {
|
||||
FunctionInvoker<List<String>, String> invoker = new FunctionInvoker<List<String>, String>(TestFunctionsConfig.class) {
|
||||
@Override
|
||||
protected String postProcessFluxFunctionResult(List<String> rawInputs, Object functionInputs,
|
||||
Flux<?> functionResult, FunctionInvocationWrapper function, ExecutionContext executionContext
|
||||
) {
|
||||
return functionResult.map(o -> o.toString() + "+flux").collectList().block().stream().collect(Collectors.joining("/"));
|
||||
}
|
||||
};
|
||||
invoker = spyOnAndCloseAfterTest(invoker);
|
||||
ExecutionContext executionContext = new TestExecutionContext("reactiveFluxUppercase");
|
||||
List<String> rawInputs = Arrays.asList("foo", "bar");
|
||||
String result = invoker.handleRequest(rawInputs, executionContext);
|
||||
assertThat(result).isEqualTo("FOO+flux/BAR+flux");
|
||||
|
||||
// Below here verifies that the expected callback(s) were invoked w/ the expected arguments
|
||||
|
||||
// Only publisher->flux post-process callbacks should be called
|
||||
verify(invoker, never()).postProcessImperativeFunctionResult(anyList(), any(), any(), any(), same(executionContext));
|
||||
verify(invoker, never()).postProcessMonoFunctionResult(anyList(), any(), any(Mono.class), any(), same(executionContext));
|
||||
|
||||
// Only sniff-test the payload of the input message and the mono (the other fields are problematic to verify and no value doing that here)
|
||||
ArgumentCaptor<Flux<GenericMessage>> functionInputsCaptor = ArgumentCaptor.forClass(Flux.class);
|
||||
ArgumentCaptor<Flux> functionResultCaptor = ArgumentCaptor.forClass(Flux.class);
|
||||
verify(invoker).postProcessReactiveFunctionResult(same(rawInputs), functionInputsCaptor.capture(), functionResultCaptor.capture(), any(), same(executionContext));
|
||||
verify(invoker).postProcessFluxFunctionResult(same(rawInputs), functionInputsCaptor.capture(), functionResultCaptor.capture(), any(), same(executionContext));
|
||||
|
||||
// NOTE: The captors get called twice as the args are just delegated from publisher->flux callback
|
||||
|
||||
// The functionInputs for each call is Flux<GreetingMessage> with 2 items - one for 'foo' and one for 'bar'
|
||||
assertThat(functionInputsCaptor.getAllValues())
|
||||
.extracting(Flux::collectList).extracting(Mono::block)
|
||||
.flatExtracting(fluxAsList -> fluxAsList.stream().collect(Collectors.toList()))
|
||||
.extracting(GenericMessage::getPayload).containsExactlyInAnyOrder("foo", "bar", "foo", "bar");
|
||||
|
||||
// The functionResult for each call is a Flux<String> w/ 2 items { "FOO", "BAR" }
|
||||
assertThat(functionResultCaptor.getAllValues())
|
||||
.extracting(Flux::collectList).extracting(Mono::block)
|
||||
.containsExactlyInAnyOrder(list("FOO", "BAR"), list("FOO", "BAR"));
|
||||
}
|
||||
|
||||
private <I, O> FunctionInvoker<I, O> spyOnAndCloseAfterTest(FunctionInvoker<I, O> invoker) {
|
||||
this.currentInvoker = invoker;
|
||||
return spy(invoker);
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableAutoConfiguration
|
||||
static class TestFunctionsConfig {
|
||||
|
||||
@Bean
|
||||
public Function<String, String> imperativeUppercase() {
|
||||
return (s) -> s.toUpperCase();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Function<Mono<String>, Mono<String>> reactiveMonoUppercase() {
|
||||
return (m) -> m.map(String::toUpperCase);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Function<Flux<String>, Flux<String>> reactiveFluxUppercase() {
|
||||
return (f) -> f.map(String::toUpperCase);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,369 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.function.adapter.azure;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.microsoft.azure.functions.ExecutionContext;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.cloud.function.adapter.azure.helper.TestExecutionContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.messaging.Message;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
* @author Oleg Zhurakousky
|
||||
*/
|
||||
public class FunctionInvokerTests {
|
||||
|
||||
private FunctionInvoker<?, ?> handler = null;
|
||||
|
||||
<I, O> FunctionInvoker<I, O> handler(Class<?> config) {
|
||||
FunctionInvoker<I, O> handler = new FunctionInvoker<I, O>(
|
||||
config);
|
||||
this.handler = handler;
|
||||
return handler;
|
||||
}
|
||||
|
||||
// @Test // this is wrong too since function is Flux, Flux and while input may be single value, the output can still be multiple
|
||||
public void bareConfig() {
|
||||
FunctionInvoker<Foo, Bar> handler = handler(BareConfig.class);
|
||||
Bar bar = handler.handleRequest(new Foo("bar"),
|
||||
new TestExecutionContext("uppercase"));
|
||||
assertThat(bar.getValue()).isEqualTo("BAR");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void autoConfig() {
|
||||
FunctionInvoker<Foo, Bar> handler = handler(AutoConfig.class);
|
||||
Bar bar = handler.handleRequest(new Foo("bar"),
|
||||
new TestExecutionContext("uppercase"));
|
||||
assertThat(bar.getValue()).isEqualTo("BAR");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multiConfig() {
|
||||
FunctionInvoker<Foo, Bar> handler = handler(MultiConfig.class);
|
||||
Bar bar = handler.handleRequest(new Foo("bar"),
|
||||
new TestExecutionContext("uppercase"));
|
||||
assertThat(bar.getValue()).isEqualTo("BAR");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void implicitListConfig() {
|
||||
FunctionInvoker<List<Foo>, List<Bar>> handler = handler(
|
||||
AutoConfig.class);
|
||||
List<Bar> bar = handler.handleRequest(Arrays.asList(new Foo("bar"), new Foo("baz")),
|
||||
new TestExecutionContext("uppercase"));
|
||||
assertThat(bar).hasSize(2);
|
||||
assertThat(bar.get(0).getValue()).isEqualTo("BAR");
|
||||
assertThat(bar.get(1).getValue()).isEqualTo("BAZ");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listToListConfig() {
|
||||
FunctionInvoker<List<Foo>, List<Bar>> handler = handler(
|
||||
ListConfig.class);
|
||||
List<Bar> bar = handler.handleRequest(
|
||||
Arrays.asList(new Foo("bar"), new Foo("baz")),
|
||||
new TestExecutionContext("uppercase"));
|
||||
assertThat(bar).hasSize(2);
|
||||
assertThat(bar.get(0).getValue()).isEqualTo("BAR");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listToListSingleConfig() {
|
||||
FunctionInvoker<List<Foo>, List<Bar>> handler = handler(
|
||||
ListConfig.class);
|
||||
List<Bar> bar = handler.handleRequest(Arrays.asList(new Foo("bar")),
|
||||
new TestExecutionContext("uppercase"));
|
||||
assertThat(bar).hasSize(1);
|
||||
assertThat(bar.get(0).getValue()).isEqualTo("BAR");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void collectConfig() {
|
||||
FunctionInvoker<List<Foo>, Bar> handler = handler(
|
||||
CollectConfig.class);
|
||||
Bar bar = handler.handleRequest(Arrays.asList(new Foo("bar")),
|
||||
new TestExecutionContext("uppercase"));
|
||||
assertThat(bar.getValue()).isEqualTo("BAR");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void functionNonFluxBean() {
|
||||
FunctionInvoker<Foo, Bar> handler = handler(NonFluxFunctionConfig.class);
|
||||
Bar bar = handler.handleRequest(new Foo("bar"), new TestExecutionContext("function"));
|
||||
assertThat(bar).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void supplierNonFluxBean() {
|
||||
FunctionInvoker<Void, List<String>> handler = handler(NonFluxSupplierConfig.class);
|
||||
List<String> result = handler.handleRequest(new TestExecutionContext("supplier"));
|
||||
|
||||
assertThat(result).isNotEmpty();
|
||||
assertThat(result.toString()).isEqualTo("[foo1, foo2]");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void supplierPublisherBean() {
|
||||
FunctionInvoker<Void, ?> handler = handler(ReactiveSupplierConfig.class);
|
||||
Foo resultSingle = (Foo) handler.handleRequest(new TestExecutionContext("suppliermono"));
|
||||
assertThat(resultSingle.getValue()).isEqualTo("hello");
|
||||
|
||||
List<Foo> resultList = (List<Foo>) handler.handleRequest(new TestExecutionContext("supplierflux"));
|
||||
assertThat(resultList.size()).isEqualTo(2);
|
||||
}
|
||||
|
||||
private static String consumerResult;
|
||||
|
||||
@Test
|
||||
public void consumerNonFluxBean() {
|
||||
FunctionInvoker<String, Void> handler = handler(NonFluxConsumerConfig.class);
|
||||
Object result = handler.handleRequest("foo1", new TestExecutionContext("consumer"));
|
||||
|
||||
assertThat(result).isNull();
|
||||
assertThat(consumerResult).isEqualTo("foo1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReactiveFunctions() {
|
||||
FunctionInvoker<String, String> handler = handler(ReactiveFunctionConfiguration.class);
|
||||
String result = handler.handleRequest("hello", new TestExecutionContext("uppercaseMono"));
|
||||
|
||||
System.out.println(result);
|
||||
|
||||
// assertThat(result).isNull();
|
||||
// assertThat(consumerResult).isEqualTo("foo1");
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void close() throws IOException {
|
||||
if (this.handler != null) {
|
||||
this.handler.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
protected static class NonFluxFunctionConfig {
|
||||
|
||||
@Bean
|
||||
public Function<Foo, Bar> function() {
|
||||
return foo -> new Bar();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableAutoConfiguration
|
||||
protected static class ReactiveFunctionConfiguration {
|
||||
|
||||
@Bean
|
||||
public Function<Flux<String>, Flux<String>> echoStream() {
|
||||
return f -> f;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Function<Mono<String>, Mono<String>> uppercaseMono() {
|
||||
return f -> f.map(v -> v.toUpperCase());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
protected static class NonFluxSupplierConfig {
|
||||
|
||||
@Bean
|
||||
public Supplier<List<String>> supplier() {
|
||||
return () -> Arrays.asList("foo1", "foo2");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
protected static class NonFluxConsumerConfig {
|
||||
|
||||
@Bean
|
||||
public Consumer<String> consumer() {
|
||||
return (v) -> consumerResult = v;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
// @EnableAutoConfiguration
|
||||
protected static class ReactiveSupplierConfig {
|
||||
|
||||
@Bean
|
||||
public Supplier<Mono<Foo>> suppliermono() {
|
||||
return () -> Mono.just(new Foo("hello"));
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Supplier<Flux<Foo>> supplierflux() {
|
||||
return () -> Flux.just(new Foo("hello"), new Foo("bye"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
protected static class BareConfig {
|
||||
|
||||
@Bean("uppercase")
|
||||
public Function<Flux<Foo>, Flux<Bar>> function() {
|
||||
return foos -> foos.map(foo -> new Bar(foo.getValue().toUpperCase()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableAutoConfiguration
|
||||
protected static class AutoConfig {
|
||||
|
||||
@Bean
|
||||
public Function<Message<Foo>, Bar> uppercase() {
|
||||
return message -> {
|
||||
Foo foo = message.getPayload();
|
||||
ExecutionContext targetContext = (ExecutionContext) message.getHeaders().get("executionContext");
|
||||
targetContext.getLogger().info("Invoking 'uppercase' on " + foo.getValue());
|
||||
return new Bar(foo.getValue().toUpperCase());
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableAutoConfiguration
|
||||
protected static class ListConfig {
|
||||
|
||||
@Bean
|
||||
public Function<List<Foo>, List<Bar>> uppercase() {
|
||||
return foos -> {
|
||||
List<Bar> bars = foos.stream().map(foo -> new Bar(foo.getValue().toUpperCase()))
|
||||
.collect(Collectors.toList());
|
||||
return bars;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableAutoConfiguration
|
||||
protected static class CollectConfig {
|
||||
|
||||
@Bean
|
||||
public Function<List<Foo>, Bar> uppercase() {
|
||||
return foos -> new Bar(foos.stream().map(foo -> foo.getValue().toUpperCase())
|
||||
.collect(Collectors.joining(",")));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableAutoConfiguration
|
||||
protected static class MultiConfig {
|
||||
|
||||
@Bean
|
||||
public Function<Message<Foo>, Bar> uppercase() {
|
||||
|
||||
return message -> {
|
||||
ExecutionContext context = (ExecutionContext) message.getHeaders().get("executionContext");
|
||||
Foo foo = message.getPayload();
|
||||
context.getLogger().info("Executing uppercase function");
|
||||
return new Bar(foo.getValue().toUpperCase());
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Function<Message<Bar>, Foo> lowercase() {
|
||||
return message -> {
|
||||
ExecutionContext context = (ExecutionContext) message.getHeaders().get("executionContext");
|
||||
Bar bar = message.getPayload();
|
||||
context.getLogger().info("Executing lowercase function");
|
||||
return new Foo(bar.getValue().toLowerCase());
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Foo {
|
||||
|
||||
private String value;
|
||||
|
||||
Foo() {
|
||||
}
|
||||
|
||||
Foo(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String lowercase() {
|
||||
return this.value.toLowerCase();
|
||||
}
|
||||
|
||||
public String uppercase() {
|
||||
return this.value.toUpperCase();
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
public void setValue(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Bar {
|
||||
|
||||
private String value;
|
||||
|
||||
Bar() {
|
||||
}
|
||||
|
||||
Bar(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
public void setValue(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.function.adapter.azure;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.microsoft.azure.functions.HttpMethod;
|
||||
import com.microsoft.azure.functions.HttpResponseMessage;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.cloud.function.adapter.azure.helper.HttpRequestMessageStub;
|
||||
import org.springframework.cloud.function.adapter.azure.helper.TestExecutionContext;
|
||||
import org.springframework.cloud.function.context.config.ContextFunctionCatalogAutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Markus Gulden
|
||||
*/
|
||||
public class HttpFunctionInvokerTests {
|
||||
|
||||
private HttpFunctionInvoker<?> handler = null;
|
||||
|
||||
<I> HttpFunctionInvoker<I> handler(Class<?> config) {
|
||||
HttpFunctionInvoker<I> handler = new HttpFunctionInvoker<I>(
|
||||
config);
|
||||
this.handler = handler;
|
||||
return handler;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithBody() {
|
||||
HttpFunctionInvoker<Foo> handler = handler(
|
||||
FunctionMessageBodyConfig.class);
|
||||
HttpRequestMessageStub<Foo> request = new HttpRequestMessageStub<Foo>();
|
||||
request.setBody(new Foo("foo"));
|
||||
|
||||
HttpResponseMessage response = handler.handleRequest(request,
|
||||
new TestExecutionContext("uppercase"));
|
||||
|
||||
assertThat(response.getBody()).isInstanceOf(Bar.class);
|
||||
assertThat(response.getStatusCode()).isEqualTo(200);
|
||||
Bar body = (Bar) response.getBody();
|
||||
assertThat(body.getValue()).isEqualTo("FOO");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithRequestParameters() throws URISyntaxException {
|
||||
HttpFunctionInvoker<Foo> handler = handler(
|
||||
FunctionMessageEchoReqParametersConfig.class);
|
||||
HttpRequestMessageStub<Foo> request = new HttpRequestMessageStub<Foo>();
|
||||
request.setUri(new URI("http://localhost:8080/pathValue"));
|
||||
request.setHeaders(Collections.singletonMap("test-header", "headerValue"));
|
||||
request.setQueryParameters(Collections.singletonMap("query", "queryValue"));
|
||||
request.setHttpMethod(HttpMethod.GET);
|
||||
|
||||
HttpResponseMessage response = handler.handleRequest(request,
|
||||
new TestExecutionContext("uppercase"));
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(200);
|
||||
assertThat(response.getHeader("path")).isEqualTo("/pathValue");
|
||||
assertThat(response.getHeader("query")).isEqualTo("queryValue");
|
||||
assertThat(response.getHeader("test-header")).isEqualTo("headerValue");
|
||||
Bar body = (Bar) response.getBody();
|
||||
assertThat(body.getValue()).isEqualTo("body");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithEmptyBody() {
|
||||
HttpFunctionInvoker<Foo> handler = handler(
|
||||
FunctionMessageConsumerConfig.class);
|
||||
HttpRequestMessageStub<Foo> request = new HttpRequestMessageStub<Foo>();
|
||||
|
||||
HttpResponseMessage response = handler.handleRequest(request,
|
||||
new TestExecutionContext("uppercase"));
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(200);
|
||||
Bar body = (Bar) response.getBody();
|
||||
assertThat(body).isNull();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void close() throws IOException {
|
||||
if (this.handler != null) {
|
||||
this.handler.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import({ ContextFunctionCatalogAutoConfiguration.class })
|
||||
protected static class FunctionMessageBodyConfig {
|
||||
|
||||
@Bean
|
||||
public Function<Message<Foo>, Message<Bar>> function() {
|
||||
return (foo -> {
|
||||
Map<String, Object> headers = new HashMap<>();
|
||||
return new GenericMessage<>(
|
||||
new Bar(foo.getPayload().getValue().toUpperCase()), headers);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import({ ContextFunctionCatalogAutoConfiguration.class })
|
||||
protected static class FunctionMessageEchoReqParametersConfig {
|
||||
|
||||
@Bean
|
||||
public Function<Message<Void>, Message<Bar>> function() {
|
||||
return (message -> {
|
||||
Map<String, Object> headers = new HashMap<>();
|
||||
headers.put("path", message.getHeaders().get("path"));
|
||||
headers.put("query", message.getHeaders().get("query"));
|
||||
headers.put("test-header", message.getHeaders().get("test-header"));
|
||||
headers.put("httpMethod", message.getHeaders().get("httpMethod"));
|
||||
return new GenericMessage<>(new Bar("body"), headers);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import({ ContextFunctionCatalogAutoConfiguration.class })
|
||||
protected static class FunctionMessageConsumerConfig {
|
||||
|
||||
@Bean
|
||||
public Consumer<Message<Foo>> function() {
|
||||
return (foo -> {
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.function.adapter.azure.helper;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.microsoft.azure.functions.HttpResponseMessage;
|
||||
import com.microsoft.azure.functions.HttpResponseMessage.Builder;
|
||||
import com.microsoft.azure.functions.HttpStatusType;
|
||||
|
||||
public class BuilderStub implements Builder {
|
||||
|
||||
private HttpStatusType status;
|
||||
private Map<String, String> headers = new HashMap<>();
|
||||
private Object body;
|
||||
|
||||
@Override
|
||||
public Builder status(HttpStatusType status) {
|
||||
this.status = status;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder header(String key, String value) {
|
||||
headers.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder body(Object body) {
|
||||
this.body = body;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpResponseMessage build() {
|
||||
return new HttpResponseMessageStub(this.status, this.headers, this.body);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.function.adapter.azure.helper;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Map;
|
||||
|
||||
import com.microsoft.azure.functions.HttpMethod;
|
||||
import com.microsoft.azure.functions.HttpRequestMessage;
|
||||
import com.microsoft.azure.functions.HttpResponseMessage;
|
||||
import com.microsoft.azure.functions.HttpResponseMessage.Builder;
|
||||
import com.microsoft.azure.functions.HttpStatus;
|
||||
import com.microsoft.azure.functions.HttpStatusType;
|
||||
|
||||
public class HttpRequestMessageStub<I> implements HttpRequestMessage<I> {
|
||||
|
||||
private URI uri;
|
||||
private HttpMethod httpMethod;
|
||||
private Map<String, String> headers;
|
||||
private Map<String, String> queryParameters;
|
||||
private I body;
|
||||
|
||||
public void setUri(URI uri) {
|
||||
this.uri = uri;
|
||||
}
|
||||
|
||||
public void setHttpMethod(HttpMethod httpMethod) {
|
||||
this.httpMethod = httpMethod;
|
||||
}
|
||||
|
||||
public void setHeaders(Map<String, String> headers) {
|
||||
this.headers = headers;
|
||||
}
|
||||
|
||||
public void setQueryParameters(Map<String, String> queryParameters) {
|
||||
this.queryParameters = queryParameters;
|
||||
}
|
||||
|
||||
public void setBody(I body) {
|
||||
this.body = body;
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI getUri() {
|
||||
return this.uri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpMethod getHttpMethod() {
|
||||
return this.httpMethod;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getHeaders() {
|
||||
return this.headers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getQueryParameters() {
|
||||
return this.queryParameters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public I getBody() {
|
||||
return this.body;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpResponseMessage.Builder createResponseBuilder(HttpStatusType status) {
|
||||
return new BuilderStub().status(status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder createResponseBuilder(HttpStatus status) {
|
||||
return new BuilderStub().status(status);
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.function.adapter.azure.helper;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.microsoft.azure.functions.HttpResponseMessage;
|
||||
import com.microsoft.azure.functions.HttpStatusType;
|
||||
|
||||
public class HttpResponseMessageStub implements HttpResponseMessage {
|
||||
|
||||
private HttpStatusType status;
|
||||
private Map<String, String> headers = new HashMap<>();
|
||||
private Object body;
|
||||
|
||||
public HttpResponseMessageStub(HttpStatusType status, Map<String, String> headers,
|
||||
Object body) {
|
||||
this.status = status;
|
||||
this.headers = headers;
|
||||
this.body = body;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpStatusType getStatus() {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHeader(String key) {
|
||||
return this.headers.get(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getBody() {
|
||||
return this.body;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.function.adapter.azure.helper;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import com.microsoft.azure.functions.ExecutionContext;
|
||||
|
||||
public class TestExecutionContext implements ExecutionContext {
|
||||
|
||||
private String name;
|
||||
|
||||
public TestExecutionContext(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Logger getLogger() {
|
||||
return Logger.getLogger(TestExecutionContext.class.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getInvocationId() {
|
||||
return UUID.randomUUID().toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFunctionName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.function.adapter.azure.injector;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.microsoft.azure.functions.ExecutionContext;
|
||||
import com.microsoft.azure.functions.HttpRequestMessage;
|
||||
import com.microsoft.azure.functions.annotation.AuthorizationLevel;
|
||||
import com.microsoft.azure.functions.annotation.FunctionName;
|
||||
import com.microsoft.azure.functions.annotation.HttpTrigger;
|
||||
import com.microsoft.azure.functions.spi.inject.FunctionInstanceInjector;
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.cloud.function.adapter.azure.AzureFunctionInstanceInjector;
|
||||
import org.springframework.cloud.function.adapter.azure.AzureFunctionUtil;
|
||||
import org.springframework.cloud.function.adapter.azure.helper.HttpRequestMessageStub;
|
||||
import org.springframework.cloud.function.adapter.azure.helper.TestExecutionContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* This is an example JUnit test for Azure Adapter.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
public class AzureFunctionInstanceInjectorTest {
|
||||
|
||||
static ExecutionContext TEST_EXECUTION_CONTEXT = new TestExecutionContext("hello");
|
||||
|
||||
@Test
|
||||
public void testFunctionInjector() throws Exception {
|
||||
|
||||
// The SpringBootApplication class
|
||||
System.setProperty("MAIN_CLASS", MySpringConfig.class.getName());
|
||||
|
||||
FunctionInstanceInjector injector = new AzureFunctionInstanceInjector();
|
||||
|
||||
// Emulates the Azure Function Java Runtime DI call
|
||||
MyAzureFunction azureFunction = injector.getInstance(MyAzureFunction.class);
|
||||
|
||||
Assertions.assertThat(azureFunction).isNotNull();
|
||||
|
||||
HttpRequestMessageStub<Optional<String>> requestStub = new HttpRequestMessageStub<Optional<String>>();
|
||||
|
||||
requestStub.setBody(Optional.of("payload"));
|
||||
|
||||
String result = azureFunction.execute(requestStub, TEST_EXECUTION_CONTEXT);
|
||||
|
||||
Assertions.assertThat(result).isEqualTo("PAYLOAD");
|
||||
|
||||
Assertions.assertThat(azureFunction).isInstanceOf(MyAzureFunction.class);
|
||||
}
|
||||
|
||||
@Component
|
||||
public static class MyAzureFunction {
|
||||
|
||||
@Autowired
|
||||
private Function<Message<String>, String> uppercase;
|
||||
|
||||
@FunctionName("hello")
|
||||
public String execute(
|
||||
@HttpTrigger(name = "req", authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage<Optional<String>> request,
|
||||
ExecutionContext context) {
|
||||
|
||||
Message<String> enhancedRequest = (Message<String>) AzureFunctionUtil.enhanceInputIfNecessary(
|
||||
request.getBody().get(),
|
||||
context);
|
||||
|
||||
return uppercase.apply(enhancedRequest);
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@ComponentScan
|
||||
public static class MySpringConfig {
|
||||
|
||||
@Bean
|
||||
public Function<Message<String>, String> uppercaseBean() {
|
||||
return message -> {
|
||||
ExecutionContext context = (ExecutionContext) message.getHeaders()
|
||||
.get(AzureFunctionUtil.EXECUTION_CONTEXT);
|
||||
|
||||
Assertions.assertThat(context).isNotNull();
|
||||
Assertions.assertThat(context.getFunctionName()).isEqualTo("hello");
|
||||
|
||||
return message.getPayload().toUpperCase();
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.function.adapter.azure.injector;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.Optional;
|
||||
import java.util.ServiceLoader;
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.microsoft.azure.functions.ExecutionContext;
|
||||
import com.microsoft.azure.functions.HttpMethod;
|
||||
import com.microsoft.azure.functions.HttpRequestMessage;
|
||||
import com.microsoft.azure.functions.annotation.AuthorizationLevel;
|
||||
import com.microsoft.azure.functions.annotation.FunctionName;
|
||||
import com.microsoft.azure.functions.annotation.HttpTrigger;
|
||||
import com.microsoft.azure.functions.spi.inject.FunctionInstanceInjector;
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurationExcludeFilter;
|
||||
import org.springframework.cloud.function.adapter.azure.AzureFunctionInstanceInjector;
|
||||
import org.springframework.cloud.function.adapter.azure.AzureFunctionUtil;
|
||||
import org.springframework.cloud.function.adapter.azure.helper.HttpRequestMessageStub;
|
||||
import org.springframework.cloud.function.adapter.azure.helper.TestExecutionContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.ComponentScan.Filter;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.FilterType;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
public class FunctionInstanceInjectorServiceLoadingTest {
|
||||
|
||||
static ExecutionContext executionContext = new TestExecutionContext("hello");
|
||||
|
||||
@Test
|
||||
public void testFunctionInjector() throws Exception {
|
||||
|
||||
FunctionInstanceInjector injector = initializeFunctionInstanceInjector();
|
||||
Assertions.assertThat(injector).isNotNull();
|
||||
Assertions.assertThat(injector).isInstanceOf(AzureFunctionInstanceInjector.class);
|
||||
|
||||
System.setProperty("MAIN_CLASS", MyMainConfig.class.getName());
|
||||
|
||||
MyAzureTestFunction functionInstance = injector.getInstance(MyAzureTestFunction.class);
|
||||
|
||||
HttpRequestMessageStub<Optional<String>> request = new HttpRequestMessageStub<Optional<String>>();
|
||||
|
||||
request.setBody(Optional.of("test"));
|
||||
|
||||
String result = functionInstance.execute(request, executionContext);
|
||||
|
||||
Assertions.assertThat(result).isEqualTo("TEST");
|
||||
|
||||
Assertions.assertThat(functionInstance).isNotNull();
|
||||
Assertions.assertThat(functionInstance).isInstanceOf(MyAzureTestFunction.class);
|
||||
}
|
||||
|
||||
private static FunctionInstanceInjector initializeFunctionInstanceInjector() {
|
||||
FunctionInstanceInjector functionInstanceInjector = null;
|
||||
ClassLoader prevContextClassLoader = Thread.currentThread().getContextClassLoader();
|
||||
try {
|
||||
Iterator<FunctionInstanceInjector> iterator = ServiceLoader.load(FunctionInstanceInjector.class).iterator();
|
||||
if (iterator.hasNext()) {
|
||||
functionInstanceInjector = iterator.next();
|
||||
if (iterator.hasNext()) {
|
||||
throw new RuntimeException(
|
||||
"Customer function app has multiple FunctionInstanceInjector implementations");
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new IllegalStateException("Failed to resolve the AzureFunctionInstanceInjector as java service!");
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Thread.currentThread().setContextClassLoader(prevContextClassLoader);
|
||||
}
|
||||
return functionInstanceInjector;
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class)})
|
||||
public static class MyMainConfig {
|
||||
|
||||
@Bean
|
||||
public Function<Message<String>, String> uppercase() {
|
||||
return message -> {
|
||||
ExecutionContext context = (ExecutionContext) message.getHeaders()
|
||||
.get(AzureFunctionUtil.EXECUTION_CONTEXT);
|
||||
Assertions.assertThat(context).isNotNull();
|
||||
Assertions.assertThat(context.getFunctionName()).isEqualTo("hello");
|
||||
return message.getPayload().toUpperCase();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
public static class MyAzureTestFunction {
|
||||
|
||||
@Autowired
|
||||
private Function<Message<String>, String> uppercase;
|
||||
|
||||
@FunctionName("ditest")
|
||||
public String execute(
|
||||
@HttpTrigger(name = "req", methods = { HttpMethod.GET,
|
||||
HttpMethod.POST }, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage<Optional<String>> request,
|
||||
ExecutionContext context) {
|
||||
|
||||
Message<String> enhancedRequest = (Message<String>) AzureFunctionUtil.enhanceInputIfNecessary(
|
||||
request.getBody().get(),
|
||||
context);
|
||||
return uppercase.apply(enhancedRequest);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user