diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 5cdaa4ba7117..36717b4858e5 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,5 +1,5 @@ **Affects:** \ @@ -14,4 +14,4 @@ Thanks for taking the time to create an issue. Please read the following: Issue or Pull Request? Create only one, not both. GitHub treats them as the same. If unsure, start with an issue, and if you submit a pull request later, the issue will be closed as superseded. ---> \ No newline at end of file +--> diff --git a/.github/workflows/backport-bot.yml b/.github/workflows/backport-bot.yml new file mode 100644 index 000000000000..2c3d36fd7c49 --- /dev/null +++ b/.github/workflows/backport-bot.yml @@ -0,0 +1,29 @@ +name: Backport Bot + +on: + issues: + types: [labeled] + push: + branches: + - '*.x' +permissions: + contents: read +jobs: + build: + permissions: + contents: read + issues: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '17' + - name: Download BackportBot + run: wget https://github.com/spring-io/backport-bot/releases/download/latest/backport-bot-0.0.1-SNAPSHOT.jar + - name: Backport + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_EVENT: ${{ toJSON(github.event) }} + run: java -jar backport-bot-0.0.1-SNAPSHOT.jar --github.accessToken="$GITHUB_TOKEN" --github.event_name "$GITHUB_EVENT_NAME" --github.event "$GITHUB_EVENT" diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml index 405a2b306592..c80a7e5278d0 100644 --- a/.github/workflows/gradle-wrapper-validation.yml +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -1,10 +1,13 @@ name: "Validate Gradle Wrapper" on: [push, pull_request] +permissions: + contents: read + jobs: validation: name: "Validation" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: gradle/wrapper-validation-action@v1 diff --git a/.gitignore b/.gitignore index 3f904904f76f..e21dcb18ebca 100644 --- a/.gitignore +++ b/.gitignore @@ -1,35 +1,42 @@ +# Miscellaneous *.java.hsp *.sonarj *.sw* .DS_Store -.settings -.springBeans -bin build.sh integration-repo ivy-cache +argfile* +activemq-data/ +classes/ + +# Log files jxl.log jmx.log derby.log -spring-test/test-output/ -.gradle -argfile* -pom.xml -activemq-data/ -classes/ +# Gradle artifacts +.gradle +.gradletasknamecache /build buildSrc/build /spring-*/build -/spring-core/kotlin-coroutines/build /framework-bom/build +/framework-docs/build /integration-tests/build /src/asciidoc/build +spring-test/test-output/ + +# Maven artifacts +pom.xml target/ # Eclipse artifacts, including WTP generated manifests +bin .classpath .project +.settings +.springBeans spring-*/src/main/java/META-INF/MANIFEST.MF # IDEA artifacts and output dirs @@ -40,4 +47,6 @@ spring-*/src/main/java/META-INF/MANIFEST.MF out test-output atlassian-ide-plugin.xml -.gradletasknamecache + +# VS Code +.vscode/ diff --git a/.mailmap b/.mailmap index 92baa202dab8..ab65af2e4c98 100644 --- a/.mailmap +++ b/.mailmap @@ -1,23 +1,34 @@ -Juergen Hoeller jhoeller - - - - - - - - - - - - - - - - - - - - +Juergen Hoeller +Juergen Hoeller +Juergen Hoeller +Rossen Stoyanchev +Rossen Stoyanchev +Rossen Stoyanchev +Phillip Webb +Phillip Webb +Phillip Webb +Chris Beams +Chris Beams +Chris Beams +Arjen Poutsma +Arjen Poutsma +Arjen Poutsma +Arjen Poutsma +Arjen Poutsma +Oliver Drotbohm +Oliver Drotbohm +Oliver Drotbohm +Oliver Drotbohm +Dave Syer +Dave Syer +Dave Syer +Dave Syer +Andy Clement +Andy Clement +Andy Clement +Andy Clement +Sam Brannen +Sam Brannen +Sam Brannen -Nick Williams Nicholas Williams +Nick Williams diff --git a/.sdkmanrc b/.sdkmanrc new file mode 100644 index 000000000000..95c6084f66f9 --- /dev/null +++ b/.sdkmanrc @@ -0,0 +1,3 @@ +# Enable auto-env through the sdkman_auto_env config +# Add key=value pairs of SDKs to use below +java=17.0.5-librca diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2295a0928826..6edef2962b98 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ First off, thank you for taking the time to contribute! :+1: :tada: * [Code of Conduct](#code-of-conduct) * [How to Contribute](#how-to-contribute) - * [Discuss](#discuss) + * [Ask questions](#ask-questions) * [Create an Issue](#create-an-issue) * [Issue Lifecycle](#issue-lifecycle) * [Submit a Pull Request](#submit-a-pull-request) @@ -22,11 +22,10 @@ Please report unacceptable behavior to spring-code-of-conduct@pivotal.io. ### How to Contribute -#### Discuss +#### Ask questions If you have a question, check Stack Overflow using -[this list of tags](https://stackoverflow.com/questions/tagged/spring+or+spring-mvc+or+spring-aop+or+spring-jdbc+or+spring-transactions+or+spring-annotations+or+spring-jms+or+spring-el+or+spring-test+or+spring+or+spring-remoting+or+spring-orm+or+spring-jmx+or+spring-cache+or+spring-webflux?tab=Newest). -Find an existing discussion, or start a new one if necessary. +[this list of tags](https://stackoverflow.com/questions/tagged/spring+or+spring-mvc+or+spring-aop+or+spring-jdbc+or+spring-transactions+or+spring-annotations+or+spring-jms+or+spring-el+or+spring-test+or+spring+or+spring-remoting+or+spring-orm+or+spring-jmx+or+spring-cache+or+spring-webflux?tab=Newest). Find an existing discussion, or start a new one if necessary. If you believe there is an issue, search through [existing issues](https://github.com/spring-projects/spring-framework/issues) trying a @@ -39,14 +38,18 @@ decision. Reporting an issue or making a feature request is a great way to contribute. Your feedback and the conversations that result from it provide a continuous flow of ideas. However, -before creating a ticket, please take the time to [discuss and research](#discuss) first. +before creating a ticket, please take the time to [ask and research](#ask-questions) first. -If creating an issue after a discussion on Stack Overflow, please provide a description +If you create an issue after a discussion on Stack Overflow, please provide a description in the issue instead of simply referring to Stack Overflow. The issue tracker is an important place of record for design discussions and should be self-sufficient. -Once you're ready, create an issue on -[GitHub](https://github.com/spring-projects/spring-framework/issues). +Once you're ready, create an issue on [GitHub](https://github.com/spring-projects/spring-framework/issues). + +Many issues are caused by subtle behavior, typos, and unintended configuration. +Creating a [Minimal Reproducible Example](https://stackoverflow.com/help/minimal-reproducible-example) +(starting with https://start.spring.io for example) of the problem helps the team +quickly triage your issue and get to the core of the problem. #### Issue Lifecycle @@ -63,7 +66,7 @@ follow-up reports will need to be created as new issues with a fresh description #### Submit a Pull Request 1. If you have not previously done so, please sign the -[Contributor License Agreement](https://cla.pivotal.io/sign/spring). You will be reminded +[Contributor License Agreement](https://cla.spring.io/sign/spring). You will be reminded automatically when you submit the PR. 1. Should you create an issue first? No, just create the pull request and use the @@ -72,7 +75,7 @@ to start a discussion first or have already created an issue, once a pull reques created, we will close the issue as superseded by the pull request, and the discussion about the issue will continue under the pull request. -1. Always check out the `master` branch and submit pull requests against it +1. Always check out the `main` branch and submit pull requests against it (for target version see [settings.gradle](settings.gradle)). Backports to prior versions will be considered on a case-by-case basis and reflected as the fix version in the issue tracker. @@ -120,15 +123,13 @@ define the source file coding standards we use along with some IDEA editor setti ### Reference Docs -The reference documentation is in the [src/docs/asciidoc](src/docs/asciidoc) directory, in +The reference documentation is in the [framework-docs/src/docs/asciidoc](framework-docs/src/docs/asciidoc) directory, in [Asciidoctor](https://asciidoctor.org/) format. For trivial changes, you may be able to browse, edit source files, and submit directly from GitHub. -When making changes locally, execute `./gradlew asciidoctor` and then browse the result under -`build/asciidoc/html5/index.html`. +When making changes locally, execute `./gradlew :framework-docs:asciidoctor` and then browse the result under +`framework-docs/build/docs/ref-docs/html5/index.html`. + +Asciidoctor also supports live editing. For more details see +[AsciiDoc Tooling](https://docs.asciidoctor.org/asciidoctor/latest/tooling/). -Asciidoctor also supports live editing. For more details read -[Editing AsciiDoc with Live Preview](https://asciidoctor.org/docs/editing-asciidoc-with-live-preview/). -Note that if you choose the -[System Monitor](https://asciidoctor.org/docs/editing-asciidoc-with-live-preview/#using-a-system-monitor) -option, you can find a Guardfile under `src/docs/asciidoc`. diff --git a/README.md b/README.md index 43094aa5919f..6fdaa56337d6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Spring Framework [![Build Status](https://ci.spring.io/api/v1/teams/spring-framework/pipelines/spring-framework-5.3.x/jobs/build/badge)](https://ci.spring.io/teams/spring-framework/pipelines/spring-framework-5.3.x?groups=Build") +# Spring Framework [![Build Status](https://ci.spring.io/api/v1/teams/spring-framework/pipelines/spring-framework-5.3.x/jobs/build/badge)](https://ci.spring.io/teams/spring-framework/pipelines/spring-framework-5.3.x?groups=Build") [![Revved up by Gradle Enterprise](https://img.shields.io/badge/Revved%20up%20by-Gradle%20Enterprise-06A0CE?logo=Gradle&labelColor=02303A)](https://ge.spring.io/scans?search.rootProjectNames=spring) This is the home of the Spring Framework: the foundation for all [Spring projects](https://spring.io/projects). Collectively the Spring Framework and the family of Spring projects are often referred to simply as "Spring". @@ -14,16 +14,20 @@ For access to artifacts or a distribution zip, see the [Spring Framework Artifac ## Documentation -The Spring Framework maintains reference documentation ([published](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/) and [source](src/docs/asciidoc)), Github [wiki pages](https://github.com/spring-projects/spring-framework/wiki), and an +The Spring Framework maintains reference documentation ([published](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/) and [source](framework-docs/src/docs/asciidoc)), GitHub [wiki pages](https://github.com/spring-projects/spring-framework/wiki), and an [API reference](https://docs.spring.io/spring-framework/docs/current/javadoc-api/). There are also [guides and tutorials](https://spring.io/guides) across Spring projects. ## Micro-Benchmarks -See the [Micro-Benchmarks](https://github.com/spring-projects/spring-framework/wiki/Micro-Benchmarks) Wiki page. +See the [Micro-Benchmarks](https://github.com/spring-projects/spring-framework/wiki/Micro-Benchmarks) wiki page. ## Build from Source -See the [Build from Source](https://github.com/spring-projects/spring-framework/wiki/Build-from-Source) Wiki page and the [CONTRIBUTING.md](CONTRIBUTING.md) file. +See the [Build from Source](https://github.com/spring-projects/spring-framework/wiki/Build-from-Source) wiki page and the [CONTRIBUTING.md](CONTRIBUTING.md) file. + +## Continuous Integration Builds + +Information regarding CI builds can be found in the [Spring Framework Concourse pipeline](ci/README.adoc) documentation. ## Stay in Touch diff --git a/SECURITY.md b/SECURITY.md index 8ed0ff412377..038a36b56539 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -8,4 +8,4 @@ wiki page. ## Reporting a Vulnerability -Please see https://pivotal.io/security. +Please see https://spring.io/security-policy. diff --git a/build.gradle b/build.gradle index 49e9569a6189..8f7736ee0e25 100644 --- a/build.gradle +++ b/build.gradle @@ -1,296 +1,39 @@ plugins { - id 'io.spring.dependency-management' version '1.0.9.RELEASE' apply false - id 'io.spring.nohttp' version '0.0.5.RELEASE' - id 'org.jetbrains.kotlin.jvm' version '1.4.10' apply false - id 'org.jetbrains.dokka' version '0.10.1' apply false - id 'org.asciidoctor.jvm.convert' version '3.1.0' - id 'org.asciidoctor.jvm.pdf' version '3.1.0' - id 'de.undercouch.download' version '4.1.1' - id "io.freefair.aspectj" version '5.1.1' apply false - id "com.github.ben-manes.versions" version '0.28.0' - id "me.champeau.gradle.jmh" version "0.5.0" apply false - id "org.jetbrains.kotlin.plugin.serialization" version "1.4.10" apply false + id 'io.spring.nohttp' version '0.0.10' + id 'io.freefair.aspectj' version '6.5.0.3' apply false + // kotlinVersion is managed in gradle.properties + id 'org.jetbrains.kotlin.plugin.serialization' version "${kotlinVersion}" apply false + id 'org.jetbrains.dokka' version '1.7.20' + id 'org.asciidoctor.jvm.convert' version '3.3.2' apply false + id 'org.asciidoctor.jvm.pdf' version '3.3.2' apply false + id 'org.unbroken-dome.xjc' version '2.0.0' apply false + id 'com.github.ben-manes.versions' version '0.42.0' + id 'com.github.johnrengelman.shadow' version '7.1.2' apply false + id 'de.undercouch.download' version '5.1.0' + id 'me.champeau.jmh' version '0.6.6' apply false } ext { moduleProjects = subprojects.findAll { it.name.startsWith("spring-") } - javaProjects = subprojects - project(":framework-bom") - withoutJclOverSlf4j = { - exclude group: "org.slf4j", name: "jcl-over-slf4j" - } + javaProjects = subprojects - project(":framework-bom") - project(":framework-platform") } configure(allprojects) { project -> - apply plugin: "io.spring.dependency-management" - - dependencyManagement { - imports { - mavenBom "com.fasterxml.jackson:jackson-bom:2.11.3" - mavenBom "io.netty:netty-bom:4.1.53.Final" - mavenBom "io.projectreactor:reactor-bom:2020.0.0" - mavenBom "io.r2dbc:r2dbc-bom:Arabba-SR8" - mavenBom "io.rsocket:rsocket-bom:1.1.0" - mavenBom "org.eclipse.jetty:jetty-bom:9.4.34.v20201102" - mavenBom "org.jetbrains.kotlin:kotlin-bom:1.4.10" - mavenBom "org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.4.1" - mavenBom "org.junit:junit-bom:5.7.0" - } - dependencies { - dependencySet(group: 'org.apache.logging.log4j', version: '2.13.3') { - entry 'log4j-api' - entry 'log4j-core' - entry 'log4j-jul' - entry 'log4j-slf4j-impl' - } - dependency "org.slf4j:slf4j-api:1.7.30" - dependency("com.google.code.findbugs:findbugs:3.0.1") { - exclude group: "dom4j", name: "dom4j" - } - dependency "com.google.code.findbugs:jsr305:3.0.2" - - dependencySet(group: 'org.aspectj', version: '1.9.6') { - entry 'aspectjrt' - entry 'aspectjtools' - entry 'aspectjweaver' - } - dependencySet(group: 'org.codehaus.groovy', version: '3.0.6') { - entry 'groovy' - entry 'groovy-jsr223' - entry 'groovy-templates' // requires findbugs for warning-free compilation - entry 'groovy-test' - entry 'groovy-xml' - } - - dependency "io.reactivex:rxjava:1.3.8" - dependency "io.reactivex:rxjava-reactive-streams:1.2.1" - dependency "io.reactivex.rxjava2:rxjava:2.2.19" - dependency "io.reactivex.rxjava3:rxjava:3.0.7" - dependency "io.projectreactor.tools:blockhound:1.0.4.RELEASE" - - dependency "com.caucho:hessian:4.0.63" - dependency "com.fasterxml:aalto-xml:1.2.2" - dependency("com.fasterxml.woodstox:woodstox-core:6.2.3") { - exclude group: "stax", name: "stax-api" - } - dependency "com.google.code.gson:gson:2.8.6" - dependency "com.google.protobuf:protobuf-java-util:3.13.0" - dependency "com.googlecode.protobuf-java-format:protobuf-java-format:1.4" - dependency("com.thoughtworks.xstream:xstream:1.4.13") { - exclude group: "xpp3", name: "xpp3_min" - exclude group: "xmlpull", name: "xmlpull" - } - dependency "org.apache.johnzon:johnzon-jsonb:1.2.8" - dependency("org.codehaus.jettison:jettison:1.3.8") { - exclude group: "stax", name: "stax-api" - } - dependencySet(group: 'org.jibx', version: '1.3.3') { - entry 'jibx-bind' - entry 'jibx-run' - } - dependency "org.ogce:xpp3:1.1.6" - dependency "org.yaml:snakeyaml:1.27" - dependencySet(group: 'org.jetbrains.kotlinx', version: '1.0.0') { - entry 'kotlinx-serialization-core' - entry 'kotlinx-serialization-json' - } - - dependency "com.h2database:h2:1.4.200" - dependency "com.github.ben-manes.caffeine:caffeine:2.8.6" - dependency "com.github.librepdf:openpdf:1.3.22" - dependency "com.rometools:rome:1.15.0" - dependency "commons-io:commons-io:2.5" - dependency "io.vavr:vavr:0.10.3" - dependency "net.sf.jopt-simple:jopt-simple:5.0.4" - dependencySet(group: 'org.apache.activemq', version: '5.16.0') { - entry 'activemq-broker' - entry('activemq-kahadb-store') { - exclude group: "org.springframework", name: "spring-context" - } - entry 'activemq-stomp' + repositories { + mavenCentral() + maven { + url "https://repo.spring.io/milestone" + content { + // Netty 5 optional support + includeGroup 'io.projectreactor.netty' } - dependency "org.apache.bcel:bcel:6.0" - dependency "org.apache.commons:commons-pool2:2.9.0" - dependencySet(group: 'org.apache.derby', version: '10.14.2.0') { - entry 'derby' - entry 'derbyclient' - } - dependency "org.apache.poi:poi-ooxml:4.1.2" - dependency "org.apache-extras.beanshell:bsh:2.0b6" - dependency "org.freemarker:freemarker:2.3.30" - dependency "org.hsqldb:hsqldb:2.5.1" - dependency "org.quartz-scheduler:quartz:2.3.2" - dependency "org.codehaus.fabric3.api:commonj:1.1.0" - dependency "net.sf.ehcache:ehcache:2.10.6" - dependency "org.ehcache:jcache:1.0.1" - dependency "org.ehcache:ehcache:3.4.0" - dependency "org.hibernate:hibernate-core:5.4.23.Final" - dependency "org.hibernate:hibernate-validator:6.1.6.Final" - dependency "org.webjars:webjars-locator-core:0.46" - dependency "org.webjars:underscorejs:1.8.3" - - dependencySet(group: 'org.apache.tomcat', version: '9.0.39') { - entry 'tomcat-util' - entry('tomcat-websocket') { - exclude group: "org.apache.tomcat", name: "tomcat-websocket-api" - exclude group: "org.apache.tomcat", name: "tomcat-servlet-api" - } - } - dependencySet(group: 'org.apache.tomcat.embed', version: '9.0.39') { - entry 'tomcat-embed-core' - entry 'tomcat-embed-websocket' - } - dependencySet(group: 'io.undertow', version: '2.2.2.Final') { - entry 'undertow-core' - entry('undertow-websockets-jsr') { - exclude group: "org.jboss.spec.javax.websocket", name: "jboss-websocket-api_1.1_spec" - } - entry('undertow-servlet') { - exclude group: "org.jboss.spec.javax.servlet", name: "jboss-servlet-api_3.1_spec" - exclude group: "org.jboss.spec.javax.annotation", name: "jboss-annotations-api_1.2_spec" - } - } - - dependencySet(group: 'com.squareup.okhttp3', version: '3.14.9') { - entry 'okhttp' - entry 'mockwebserver' - } - dependency("org.apache.httpcomponents:httpclient:4.5.12") { - exclude group: "commons-logging", name: "commons-logging" - } - dependency("org.apache.httpcomponents:httpasyncclient:4.1.4") { - exclude group: "commons-logging", name: "commons-logging" - } - dependency 'org.apache.httpcomponents.client5:httpclient5:5.0.3' - dependency 'org.apache.httpcomponents.core5:httpcore5-reactive:5.0.2' - dependency "org.eclipse.jetty:jetty-reactive-httpclient:1.1.4" - - dependency "org.jruby:jruby:9.2.13.0" - dependency "org.python:jython-standalone:2.7.1" - dependency "org.mozilla:rhino:1.7.11" - - dependency "commons-fileupload:commons-fileupload:1.4" - dependency "org.synchronoss.cloud:nio-multipart-parser:1.1.0" - - dependency("org.dom4j:dom4j:2.1.3") { - exclude group: "jaxen", name: "jaxen" - exclude group: "net.java.dev.msv", name: "xsdlib" - exclude group: "pull-parser", name: "pull-parser" - exclude group: "xpp3", name: "xpp3" - } - dependency("jaxen:jaxen:1.2.0") { - exclude group: "dom4j", name: "dom4j" - } - - dependency("junit:junit:4.13.1") { - exclude group: "org.hamcrest", name: "hamcrest-core" - } - dependency("de.bechte.junit:junit-hierarchicalcontextrunner:4.12.1") { - exclude group: "junit", name: "junit" - } - dependency "org.testng:testng:7.3.0" - dependency "org.hamcrest:hamcrest:2.1" - dependency "org.awaitility:awaitility:3.1.6" - dependency "org.assertj:assertj-core:3.18.0" - dependencySet(group: 'org.xmlunit', version: '2.6.2') { - entry 'xmlunit-assertj' - entry('xmlunit-matchers') { - exclude group: "org.hamcrest", name: "hamcrest-core" - } - } - dependencySet(group: 'org.mockito', version: '3.6.0') { - entry('mockito-core') { - exclude group: "org.hamcrest", name: "hamcrest-core" - } - entry 'mockito-junit-jupiter' - } - dependency "io.mockk:mockk:1.10.0" - - dependency("net.sourceforge.htmlunit:htmlunit:2.44.0") { - exclude group: "commons-logging", name: "commons-logging" - } - dependency("org.seleniumhq.selenium:htmlunit-driver:2.44.0") { - exclude group: "commons-logging", name: "commons-logging" - } - dependency("org.seleniumhq.selenium:selenium-java:3.141.59") { - exclude group: "commons-logging", name: "commons-logging" - exclude group: "io.netty", name: "netty" - } - dependency "org.skyscreamer:jsonassert:1.5.0" - dependency "com.jayway.jsonpath:json-path:2.4.0" - dependency "org.bouncycastle:bcpkix-jdk15on:1.66" - - dependencySet(group: 'org.apache.tiles', version: '3.0.8') { - entry 'tiles-api' - entry('tiles-core', withoutJclOverSlf4j) - entry('tiles-servlet', withoutJclOverSlf4j) - entry('tiles-jsp', withoutJclOverSlf4j) - entry('tiles-el', withoutJclOverSlf4j) - entry('tiles-extras') { - exclude group: "org.springframework", name: "spring-web" - exclude group: "org.slf4j", name: "jcl-over-slf4j" - } - } - dependency("org.apache.taglibs:taglibs-standard-jstlel:1.2.5") { - exclude group: "org.apache.taglibs", name: "taglibs-standard-spec" - } - - dependency "com.ibm.websphere:uow:6.0.2.17" - dependency "com.jamonapi:jamon:2.82" - dependency "joda-time:joda-time:2.10.6" - dependency "org.eclipse.persistence:org.eclipse.persistence.jpa:2.7.7" - dependency "org.javamoney:moneta:1.3" - - dependency "com.sun.activation:javax.activation:1.2.0" - dependency "com.sun.mail:javax.mail:1.6.2" - dependencySet(group: 'com.sun.xml.bind', version: '2.3.0.1') { - entry 'jaxb-core' - entry 'jaxb-impl' - entry 'jaxb-xjc' - } - - dependency "javax.activation:javax.activation-api:1.2.0" - dependency "javax.annotation:javax.annotation-api:1.3.2" - dependency "javax.cache:cache-api:1.1.0" - dependency "javax.ejb:javax.ejb-api:3.2" - dependency "javax.el:javax.el-api:3.0.1-b04" - dependency "javax.enterprise.concurrent:javax.enterprise.concurrent-api:1.0" - dependency "javax.faces:javax.faces-api:2.2" - dependency "javax.inject:javax.inject:1" - dependency "javax.inject:javax.inject-tck:1" - dependency "javax.interceptor:javax.interceptor-api:1.2.2" - dependency "javax.jms:javax.jms-api:2.0.1" - dependency "javax.json:javax.json-api:1.1.4" - dependency "javax.json.bind:javax.json.bind-api:1.0" - dependency "javax.mail:javax.mail-api:1.6.2" - dependency "javax.money:money-api:1.0.3" - dependency "javax.resource:javax.resource-api:1.7.1" - dependency "javax.servlet:javax.servlet-api:4.0.1" - dependency "javax.servlet.jsp:javax.servlet.jsp-api:2.3.2-b02" - dependency "javax.servlet.jsp.jstl:javax.servlet.jsp.jstl-api:1.2.1" - dependency "javax.transaction:javax.transaction-api:1.3" - dependency "javax.validation:validation-api:2.0.1.Final" - dependency "javax.websocket:javax.websocket-api:1.1" - dependency "javax.xml.bind:jaxb-api:2.3.1" - dependency "javax.xml.ws:jaxws-api:2.3.1" - - dependency "org.eclipse.persistence:javax.persistence:2.2.0" - - // Substitute for "javax.management:jmxremote_optional:1.0.1_04" which - // is not available on Maven Central - dependency "org.glassfish.external:opendmk_jmxremote_optional_jar:1.0-b01-ea" - dependency "org.glassfish:javax.el:3.0.1-b08" - dependency "org.glassfish.main:javax.jws:4.0-b33" - dependency "org.glassfish.tyrus:tyrus-container-servlet:1.13.1" } - generatedPomCustomization { - enabled = false - } - resolutionStrategy { - cacheChangingModulesFor 0, "seconds" + maven { url "https://repo.spring.io/libs-spring-framework-build" } + if (version.contains('-')) { + maven { url "https://repo.spring.io/milestone" } } - repositories { - mavenCentral() - maven { url "https://repo.spring.io/libs-spring-framework-build" } + if (version.endsWith('-SNAPSHOT')) { + maven { url "https://repo.spring.io/snapshot" } } } configurations.all { @@ -307,27 +50,17 @@ configure([rootProject] + javaProjects) { project -> apply plugin: "java" apply plugin: "java-test-fixtures" apply plugin: "checkstyle" - apply plugin: 'org.springframework.build.compile' - apply from: "${rootDir}/gradle/custom-java-home.gradle" + apply plugin: 'org.springframework.build.conventions' + apply from: "${rootDir}/gradle/toolchains.gradle" apply from: "${rootDir}/gradle/ide.gradle" - pluginManager.withPlugin("kotlin") { - apply plugin: "org.jetbrains.dokka" - compileKotlin { - kotlinOptions { - jvmTarget = "1.8" - languageVersion = "1.3" - apiVersion = "1.3" - freeCompilerArgs = ["-Xjsr305=strict"] - allWarningsAsErrors = true - } - } - compileTestKotlin { - kotlinOptions { - jvmTarget = "1.8" - freeCompilerArgs = ["-Xjsr305=strict"] - } + configurations { + dependencyManagement { + canBeConsumed = false + canBeResolved = false + visible = false } + matching { it.name.endsWith("Classpath") }.all { it.extendsFrom(dependencyManagement) } } test { @@ -336,52 +69,79 @@ configure([rootProject] + javaProjects) { project -> systemProperty("java.awt.headless", "true") systemProperty("testGroups", project.properties.get("testGroups")) systemProperty("io.netty.leakDetection.level", "paranoid") + systemProperty("io.netty5.leakDetectionLevel", "paranoid") + systemProperty("io.netty5.leakDetection.targetRecords", "32") + systemProperty("io.netty5.buffer.lifecycleTracingEnabled", "true") + systemProperty("io.netty5.buffer.leakDetectionEnabled", "true") + jvmArgs(["--add-opens=java.base/java.lang=ALL-UNNAMED", + "--add-opens=java.base/java.util=ALL-UNNAMED"]) } checkstyle { - toolVersion = "8.36.2" + toolVersion = "10.4" configDirectory.set(rootProject.file("src/checkstyle")) } + tasks.named("checkstyleMain").configure { + maxHeapSize = "1g" + } + + tasks.named("checkstyleTest").configure { + maxHeapSize = "1g" + } + dependencies { - testCompile("org.junit.jupiter:junit-jupiter-api") - testCompile("org.junit.jupiter:junit-jupiter-params") - testCompile("org.mockito:mockito-core") - testCompile("org.mockito:mockito-junit-jupiter") - testCompile("io.mockk:mockk") - testCompile("org.assertj:assertj-core") + dependencyManagement(enforcedPlatform(dependencies.project(path: ":framework-platform"))) + testImplementation("org.junit.jupiter:junit-jupiter-api") + testImplementation("org.junit.jupiter:junit-jupiter-params") + testImplementation("org.junit.platform:junit-platform-suite-api") + testImplementation("org.mockito:mockito-core") + testImplementation("org.mockito:mockito-junit-jupiter") + testImplementation("io.mockk:mockk") + testImplementation("org.assertj:assertj-core") // Pull in the latest JUnit 5 Launcher API to ensure proper support in IDEs. - testRuntime("org.junit.platform:junit-platform-launcher") - testRuntime("org.junit.jupiter:junit-jupiter-engine") - testRuntime("org.apache.logging.log4j:log4j-core") - testRuntime("org.apache.logging.log4j:log4j-slf4j-impl") - testRuntime("org.apache.logging.log4j:log4j-jul") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") + testRuntimeOnly("org.junit.platform:junit-platform-suite-engine") + testRuntimeOnly("org.apache.logging.log4j:log4j-core") + testRuntimeOnly("org.apache.logging.log4j:log4j-jul") + testRuntimeOnly("org.apache.logging.log4j:log4j-slf4j-impl") // JSR-305 only used for non-required meta-annotations compileOnly("com.google.code.findbugs:jsr305") testCompileOnly("com.google.code.findbugs:jsr305") - checkstyle("io.spring.javaformat:spring-javaformat-checkstyle:0.0.15") + checkstyle("io.spring.javaformat:spring-javaformat-checkstyle:0.0.31") } ext.javadocLinks = [ - "https://docs.oracle.com/javase/8/docs/api/", - "https://docs.oracle.com/javaee/7/api/", + "https://docs.oracle.com/en/java/javase/17/docs/api/", + "https://jakarta.ee/specifications/platform/9/apidocs/", "https://docs.oracle.com/cd/E13222_01/wls/docs90/javadocs/", // CommonJ - "https://www.ibm.com/support/knowledgecenter/SS7JFU_8.5.5/com.ibm.websphere.javadoc.doc/web/apidocs/", - "https://glassfish.java.net/nonav/docs/v3/api/", + "https://www.ibm.com/docs/api/v1/content/SSEQTP_8.5.5/com.ibm.websphere.javadoc.doc/web/apidocs/", "https://docs.jboss.org/jbossas/javadoc/4.0.5/connector/", "https://docs.jboss.org/jbossas/javadoc/7.1.2.Final/", - "https://tiles.apache.org/tiles-request/apidocs/", - "https://tiles.apache.org/framework/apidocs/", "https://www.eclipse.org/aspectj/doc/released/aspectj5rt-api/", - "https://www.ehcache.org/apidocs/2.10.4/", "https://www.quartz-scheduler.org/api/2.3.0/", "https://fasterxml.github.io/jackson-core/javadoc/2.10/", "https://fasterxml.github.io/jackson-databind/javadoc/2.10/", "https://fasterxml.github.io/jackson-dataformat-xml/javadoc/2.10/", - "https://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/", + "https://hc.apache.org/httpcomponents-client-5.2.x/current/httpclient5/apidocs/", "https://projectreactor.io/docs/test/release/api/", - "https://junit.org/junit4/javadoc/4.13.1/", - "https://junit.org/junit5/docs/5.7.0/api/" + "https://junit.org/junit4/javadoc/4.13.2/", + // TODO Uncomment link to JUnit 5 docs once we have sorted out + // the following warning in the build. + // + // warning: The code being documented uses packages in the unnamed module, but the packages defined in https://junit.org/junit5/docs/5.9.1/api/ are in named modules. + // + // "https://junit.org/junit5/docs/5.9.1/api/", + "https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/", + "https://javadoc.io/static/io.rsocket/rsocket-core/1.1.1/", + "https://r2dbc.io/spec/1.0.0.RELEASE/api/", + // The external Javadoc link for JSR 305 must come last to ensure that types from + // JSR 250 (such as @PostConstruct) are still supported. This is due to the fact + // that JSR 250 and JSR 305 both define types in javax.annotation, which results + // in a split package, and the javadoc tool does not support split packages + // across multiple external Javadoc sites. + "https://www.javadoc.io/doc/com.google.code.findbugs/jsr305/3.0.2/" ] as String[] } @@ -392,12 +152,8 @@ configure(moduleProjects) { project -> configure(rootProject) { description = "Spring Framework" - apply plugin: "groovy" - apply plugin: "kotlin" apply plugin: "io.spring.nohttp" apply plugin: 'org.springframework.build.api-diff' - apply from: "${rootDir}/gradle/publications.gradle" - apply from: "${rootDir}/gradle/docs.gradle" nohttp { source.exclude "**/test-output/**" @@ -414,13 +170,8 @@ configure(rootProject) { } } - publishing { - publications { - mavenJava(MavenPublication) { - artifact docsZip - artifact schemaZip - artifact distZip - } - } + tasks.named("checkstyleNohttp").configure { + maxHeapSize = "1g" } + } diff --git a/buildSrc/README.md b/buildSrc/README.md index f48339e6d61f..90dfdd23db84 100644 --- a/buildSrc/README.md +++ b/buildSrc/README.md @@ -5,19 +5,16 @@ They are declared in the `build.gradle` file in this folder. ## Build Conventions -### Compiler conventions +The `org.springframework.build.conventions` plugin applies all conventions to the Framework build: -The `org.springframework.build.compile` plugin applies the Java compiler conventions to the build. -By default, the build compiles sources with Java `1.8` source and target compatibility. -You can test a different source compatibility version on the CLI with a project property like: +* Configuring the Java compiler, see `JavaConventions` +* Configuring the Kotlin compiler, see `KotlinConventions` +* Configuring testing in the build with `TestConventions` -``` -./gradlew test -PjavaSourceVersion=11 -``` ## Build Plugins -## Optional dependencies +### Optional dependencies The `org.springframework.build.optional-dependencies` plugin creates a new `optional` Gradle configuration - it adds the dependencies to the project's compile and runtime classpath @@ -25,7 +22,7 @@ but doesn't affect the classpath of dependent projects. This plugin does not provide a `provided` configuration, as the native `compileOnly` and `testCompileOnly` configurations are preferred. -## API Diff +### API Diff This plugin uses the [Gradle JApiCmp](https://github.com/melix/japicmp-gradle-plugin) plugin to generate API Diff reports for each Spring Framework module. This plugin is applied once on the root @@ -39,3 +36,40 @@ current working version with. You can generate the reports for all modules or a ``` The reports are located under `build/reports/api-diff/$OLDVERSION_to_$NEWVERSION/`. + + +### RuntimeHints Java Agent + +The `spring-core-test` project module contributes the `RuntimeHintsAgent` Java agent. + +The `RuntimeHintsAgentPlugin` Gradle plugin creates a dedicated `"runtimeHintsTest"` test task for each project. +This task will detect and execute [tests tagged](https://junit.org/junit5/docs/current/user-guide/#running-tests-build-gradle) +with the `"RuntimeHintsTests"` [JUnit tag](https://junit.org/junit5/docs/current/user-guide/#running-tests-tags). +In the Spring Framework test suite, those are usually annotated with the `@EnabledIfRuntimeHintsAgent` annotation. + +By default, the agent will instrument all classes located in the `"org.springframework"` package, as they are loaded. +The `RuntimeHintsAgentExtension` allows to customize this using a DSL: + +```groovy +// this applies the `RuntimeHintsAgentPlugin` to the project +plugins { + id 'org.springframework.build.runtimehints-agent' +} + +// You can configure the agent to include and exclude packages from the instrumentation process. +runtimeHintsAgent { + includedPackages = ["org.springframework", "io.spring"] + excludedPackages = ["org.example"] +} + +dependencies { + // to use the test infrastructure, the project should also depend on the "spring-core-test" module + testImplementation(project(":spring-core-test")) +} +``` + +With this configuration, `./gradlew runtimeHintsTest` will run all tests instrumented by this java agent. +The global `./gradlew check` task depends on `runtimeHintsTest`. + +NOTE: the "spring-core-test" module doesn't shade "spring-core" by design, so the agent should never instrument +code that doesn't have "spring-core" on its classpath. \ No newline at end of file diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 03f629496a28..e21f9231a98a 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -7,9 +7,20 @@ repositories { gradlePluginPortal() } +ext { + def propertiesFile = new File(new File("$projectDir").parentFile, "gradle.properties") + propertiesFile.withInputStream { + def properties = new Properties() + properties.load(it) + set("kotlinVersion", properties["kotlinVersion"]) + } +} + dependencies { - implementation "me.champeau.gradle:japicmp-gradle-plugin:0.2.8" - implementation "com.google.guava:guava:28.2-jre" // required by japicmp-gradle-plugin + implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}") + implementation("org.jetbrains.kotlin:kotlin-compiler-embeddable:${kotlinVersion}") + implementation "me.champeau.gradle:japicmp-gradle-plugin:0.3.0" + implementation "org.gradle:test-retry-gradle-plugin:1.4.1" } gradlePlugin { @@ -18,13 +29,17 @@ gradlePlugin { id = "org.springframework.build.api-diff" implementationClass = "org.springframework.build.api.ApiDiffPlugin" } - compileConventionsPlugin { - id = "org.springframework.build.compile" - implementationClass = "org.springframework.build.compile.CompilerConventionsPlugin" + conventionsPlugin { + id = "org.springframework.build.conventions" + implementationClass = "org.springframework.build.ConventionsPlugin" } optionalDependenciesPlugin { id = "org.springframework.build.optional-dependencies" implementationClass = "org.springframework.build.optional.OptionalDependenciesPlugin" } + runtimeHintsAgentPlugin { + id = "org.springframework.build.runtimehints-agent" + implementationClass = "org.springframework.build.hint.RuntimeHintsAgentPlugin" + } } } diff --git a/buildSrc/src/main/java/org/springframework/build/ConventionsPlugin.java b/buildSrc/src/main/java/org/springframework/build/ConventionsPlugin.java new file mode 100644 index 000000000000..e54ddce55974 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/build/ConventionsPlugin.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-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.build; + +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.plugins.JavaBasePlugin; +import org.jetbrains.kotlin.gradle.plugin.KotlinBasePlugin; + +/** + * Plugin to apply conventions to projects that are part of Spring Framework's build. + * Conventions are applied in response to various plugins being applied. + * + * When the {@link JavaBasePlugin} is applied, the conventions in {@link TestConventions} + * are applied. + * When the {@link JavaBasePlugin} is applied, the conventions in {@link JavaConventions} + * are applied. + * When the {@link KotlinBasePlugin} is applied, the conventions in {@link KotlinConventions} + * are applied. + * + * @author Brian Clozel + */ +public class ConventionsPlugin implements Plugin { + + @Override + public void apply(Project project) { + new JavaConventions().apply(project); + new KotlinConventions().apply(project); + new TestConventions().apply(project); + } +} diff --git a/buildSrc/src/main/java/org/springframework/build/JavaConventions.java b/buildSrc/src/main/java/org/springframework/build/JavaConventions.java new file mode 100644 index 000000000000..134f99fe9fac --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/build/JavaConventions.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-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.build; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.plugins.JavaBasePlugin; +import org.gradle.api.plugins.JavaPlugin; +import org.gradle.api.tasks.compile.JavaCompile; + +/** + * {@link Plugin} that applies conventions for compiling Java sources in Spring Framework. + * + * @author Brian Clozel + * @author Sam Brannen + * @author Sebastien Deleuze + */ +public class JavaConventions { + + private static final List COMPILER_ARGS; + + private static final List TEST_COMPILER_ARGS; + + static { + List commonCompilerArgs = Arrays.asList( + "-Xlint:serial", "-Xlint:cast", "-Xlint:classfile", "-Xlint:dep-ann", + "-Xlint:divzero", "-Xlint:empty", "-Xlint:finally", "-Xlint:overrides", + "-Xlint:path", "-Xlint:processing", "-Xlint:static", "-Xlint:try", "-Xlint:-options", + "-parameters" + ); + COMPILER_ARGS = new ArrayList<>(); + COMPILER_ARGS.addAll(commonCompilerArgs); + COMPILER_ARGS.addAll(Arrays.asList( + "-Xlint:varargs", "-Xlint:fallthrough", "-Xlint:rawtypes", "-Xlint:deprecation", + "-Xlint:unchecked", "-Werror" + )); + TEST_COMPILER_ARGS = new ArrayList<>(); + TEST_COMPILER_ARGS.addAll(commonCompilerArgs); + TEST_COMPILER_ARGS.addAll(Arrays.asList("-Xlint:-varargs", "-Xlint:-fallthrough", "-Xlint:-rawtypes", + "-Xlint:-deprecation", "-Xlint:-unchecked")); + } + + public void apply(Project project) { + project.getPlugins().withType(JavaBasePlugin.class, javaPlugin -> applyJavaCompileConventions(project)); + } + + /** + * Applies the common Java compiler options for main sources, test fixture sources, and + * test sources. + * @param project the current project + */ + private void applyJavaCompileConventions(Project project) { + project.getTasks().withType(JavaCompile.class) + .matching(compileTask -> compileTask.getName().equals(JavaPlugin.COMPILE_JAVA_TASK_NAME)) + .forEach(compileTask -> { + compileTask.getOptions().setCompilerArgs(COMPILER_ARGS); + compileTask.getOptions().setEncoding("UTF-8"); + }); + project.getTasks().withType(JavaCompile.class) + .matching(compileTask -> compileTask.getName().equals(JavaPlugin.COMPILE_TEST_JAVA_TASK_NAME) + || compileTask.getName().equals("compileTestFixturesJava")) + .forEach(compileTask -> { + compileTask.getOptions().setCompilerArgs(TEST_COMPILER_ARGS); + compileTask.getOptions().setEncoding("UTF-8"); + }); + } + +} diff --git a/buildSrc/src/main/java/org/springframework/build/KotlinConventions.java b/buildSrc/src/main/java/org/springframework/build/KotlinConventions.java new file mode 100644 index 000000000000..f0ef7f3d59c7 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/build/KotlinConventions.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-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.build; + +import java.util.ArrayList; +import java.util.List; + +import org.gradle.api.Project; +import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions; +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile; + +/** + * @author Brian Clozel + */ +public class KotlinConventions { + + void apply(Project project) { + project.getPlugins().withId("org.jetbrains.kotlin.jvm", + (plugin) -> project.getTasks().withType(KotlinCompile.class, this::configure)); + } + + private void configure(KotlinCompile compile) { + KotlinJvmOptions kotlinOptions = compile.getKotlinOptions(); + kotlinOptions.setApiVersion("1.7"); + kotlinOptions.setLanguageVersion("1.7"); + kotlinOptions.setJvmTarget("17"); + kotlinOptions.setAllWarningsAsErrors(true); + List freeCompilerArgs = new ArrayList<>(compile.getKotlinOptions().getFreeCompilerArgs()); + freeCompilerArgs.addAll(List.of("-Xsuppress-version-warnings", "-Xjsr305=strict", "-opt-in=kotlin.RequiresOptIn")); + compile.getKotlinOptions().setFreeCompilerArgs(freeCompilerArgs); + } + +} diff --git a/buildSrc/src/main/java/org/springframework/build/TestConventions.java b/buildSrc/src/main/java/org/springframework/build/TestConventions.java new file mode 100644 index 000000000000..42f710cf7833 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/build/TestConventions.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-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.build; + +import org.gradle.api.Project; +import org.gradle.api.plugins.JavaBasePlugin; +import org.gradle.api.tasks.testing.Test; +import org.gradle.testretry.TestRetryPlugin; +import org.gradle.testretry.TestRetryTaskExtension; + +/** + * Conventions that are applied in the presence of the {@link JavaBasePlugin}. When the + * plugin is applied: + *
    + *
  • The {@link TestRetryPlugin Test Retry} plugins is applied so that flaky tests + * are retried 3 times when running on the CI. + *
+ * + * @author Brian Clozel + * @author Andy Wilkinson + */ +class TestConventions { + + void apply(Project project) { + project.getPlugins().withType(JavaBasePlugin.class, (java) -> configureTestConventions(project)); + } + + private void configureTestConventions(Project project) { + project.getPlugins().apply(TestRetryPlugin.class); + project.getTasks().withType(Test.class, + (test) -> project.getPlugins().withType(TestRetryPlugin.class, (testRetryPlugin) -> { + TestRetryTaskExtension testRetry = test.getExtensions().getByType(TestRetryTaskExtension.class); + testRetry.getFailOnPassedAfterRetry().set(true); + testRetry.getMaxRetries().set(isCi() ? 3 : 0); + })); + } + + private boolean isCi() { + return Boolean.parseBoolean(System.getenv("CI")); + } + +} diff --git a/buildSrc/src/main/java/org/springframework/build/api/ApiDiffPlugin.java b/buildSrc/src/main/java/org/springframework/build/api/ApiDiffPlugin.java index 931fd1022116..eee01370494b 100644 --- a/buildSrc/src/main/java/org/springframework/build/api/ApiDiffPlugin.java +++ b/buildSrc/src/main/java/org/springframework/build/api/ApiDiffPlugin.java @@ -16,6 +16,7 @@ package org.springframework.build.api; import java.io.File; +import java.net.URI; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Collections; @@ -23,6 +24,7 @@ import me.champeau.gradle.japicmp.JapicmpPlugin; import me.champeau.gradle.japicmp.JapicmpTask; +import org.gradle.api.GradleException; import org.gradle.api.Plugin; import org.gradle.api.Project; import org.gradle.api.artifacts.Configuration; @@ -32,10 +34,12 @@ import org.gradle.api.publish.maven.plugins.MavenPublishPlugin; import org.gradle.api.tasks.TaskProvider; import org.gradle.jvm.tasks.Jar; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * {@link Plugin} that applies the {@code "japicmp-gradle-plugin"} - * and create tasks for all subprojects, diffing the public API one by one + * and create tasks for all subprojects named {@code "spring-*"}, diffing the public API one by one * and creating the reports in {@code "build/reports/api-diff/$OLDVERSION_to_$NEWVERSION/"}. *

{@code "./gradlew apiDiff -PbaselineVersion=5.1.0.RELEASE"} will output the * reports for the API diff between the baseline version and the current one for all modules. @@ -46,12 +50,16 @@ */ public class ApiDiffPlugin implements Plugin { + private static final Logger logger = LoggerFactory.getLogger(ApiDiffPlugin.class); + public static final String TASK_NAME = "apiDiff"; private static final String BASELINE_VERSION_PROPERTY = "baselineVersion"; private static final List PACKAGE_INCLUDES = Collections.singletonList("org.springframework.*"); + private static final URI SPRING_MILESTONE_REPOSITORY = URI.create("https://repo.spring.io/milestone"); + @Override public void apply(Project project) { if (project.hasProperty(BASELINE_VERSION_PROPERTY) && project.equals(project.getRootProject())) { @@ -63,16 +71,24 @@ public void apply(Project project) { private void applyApiDiffConventions(Project project) { String baselineVersion = project.property(BASELINE_VERSION_PROPERTY).toString(); - project.subprojects(subProject -> createApiDiffTask(baselineVersion, subProject)); + project.subprojects(subProject -> { + if (subProject.getName().startsWith("spring-")) { + createApiDiffTask(baselineVersion, subProject); + } + }); } private void createApiDiffTask(String baselineVersion, Project project) { if (isProjectEligible(project)) { + // Add Spring Milestone repository for generating diffs against previous milestones + project.getRootProject() + .getRepositories() + .maven(mavenArtifactRepository -> mavenArtifactRepository.setUrl(SPRING_MILESTONE_REPOSITORY)); JapicmpTask apiDiff = project.getTasks().create(TASK_NAME, JapicmpTask.class); apiDiff.setDescription("Generates an API diff report with japicmp"); apiDiff.setGroup(JavaBasePlugin.DOCUMENTATION_GROUP); - apiDiff.setOldClasspath(project.files(createBaselineConfiguration(baselineVersion, project))); + apiDiff.setOldClasspath(createBaselineConfiguration(baselineVersion, project)); TaskProvider jar = project.getTasks().withType(Jar.class).named("jar"); apiDiff.setNewArchives(project.getLayout().files(jar.get().getArchiveFile().get().getAsFile())); apiDiff.setNewClasspath(getRuntimeClassPath(project)); @@ -98,7 +114,16 @@ private Configuration createBaselineConfiguration(String baselineVersion, Projec String baseline = String.join(":", project.getGroup().toString(), project.getName(), baselineVersion); Dependency baselineDependency = project.getDependencies().create(baseline + "@jar"); - return project.getRootProject().getConfigurations().detachedConfiguration(baselineDependency); + Configuration baselineConfiguration = project.getRootProject().getConfigurations().detachedConfiguration(baselineDependency); + try { + // eagerly resolve the baseline configuration to check whether this is a new Spring module + baselineConfiguration.resolve(); + return baselineConfiguration; + } + catch (GradleException exception) { + logger.warn("Could not resolve {} - assuming this is a new Spring module.", baseline); + } + return project.getRootProject().getConfigurations().detachedConfiguration(); } private Configuration getRuntimeClassPath(Project project) { diff --git a/buildSrc/src/main/java/org/springframework/build/compile/CompilerConventionsPlugin.java b/buildSrc/src/main/java/org/springframework/build/compile/CompilerConventionsPlugin.java deleted file mode 100644 index db51666f74b5..000000000000 --- a/buildSrc/src/main/java/org/springframework/build/compile/CompilerConventionsPlugin.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright 2002-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.build.compile; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import org.gradle.api.JavaVersion; -import org.gradle.api.Plugin; -import org.gradle.api.Project; -import org.gradle.api.plugins.JavaPlugin; -import org.gradle.api.plugins.JavaPluginConvention; -import org.gradle.api.tasks.compile.JavaCompile; - -/** - * {@link Plugin} that applies conventions for compiling Java sources in Spring Framework. - *

One can override the default Java source compatibility version - * with a dedicated property on the CLI: {@code "./gradlew test -PjavaSourceVersion=11"}. - * - * @author Brian Clozel - * @author Sam Brannen - */ -public class CompilerConventionsPlugin implements Plugin { - - /** - * The project property that can be used to switch the Java source - * compatibility version for building source and test classes. - */ - public static final String JAVA_SOURCE_VERSION_PROPERTY = "javaSourceVersion"; - - public static final JavaVersion DEFAULT_COMPILER_VERSION = JavaVersion.VERSION_1_8; - - private static final List COMPILER_ARGS; - - private static final List TEST_COMPILER_ARGS; - - static { - List commonCompilerArgs = Arrays.asList( - "-Xlint:serial", "-Xlint:cast", "-Xlint:classfile", "-Xlint:dep-ann", - "-Xlint:divzero", "-Xlint:empty", "-Xlint:finally", "-Xlint:overrides", - "-Xlint:path", "-Xlint:processing", "-Xlint:static", "-Xlint:try", "-Xlint:-options" - ); - COMPILER_ARGS = new ArrayList<>(); - COMPILER_ARGS.addAll(commonCompilerArgs); - COMPILER_ARGS.addAll(Arrays.asList( - "-Xlint:varargs", "-Xlint:fallthrough", "-Xlint:rawtypes", "-Xlint:deprecation", - "-Xlint:unchecked", "-Werror" - )); - TEST_COMPILER_ARGS = new ArrayList<>(); - TEST_COMPILER_ARGS.addAll(commonCompilerArgs); - TEST_COMPILER_ARGS.addAll(Arrays.asList("-Xlint:-varargs", "-Xlint:-fallthrough", "-Xlint:-rawtypes", - "-Xlint:-deprecation", "-Xlint:-unchecked", "-parameters")); - } - - @Override - public void apply(Project project) { - project.getPlugins().withType(JavaPlugin.class, javaPlugin -> applyJavaCompileConventions(project)); - } - - /** - * Applies the common Java compiler options for main sources, test fixture sources, and - * test sources. - * @param project the current project - */ - private void applyJavaCompileConventions(Project project) { - JavaPluginConvention java = project.getConvention().getPlugin(JavaPluginConvention.class); - if (project.hasProperty(JAVA_SOURCE_VERSION_PROPERTY)) { - JavaVersion javaSourceVersion = JavaVersion.toVersion(project.property(JAVA_SOURCE_VERSION_PROPERTY)); - java.setSourceCompatibility(javaSourceVersion); - } - else { - java.setSourceCompatibility(DEFAULT_COMPILER_VERSION); - } - java.setTargetCompatibility(DEFAULT_COMPILER_VERSION); - - project.getTasks().withType(JavaCompile.class) - .matching(compileTask -> compileTask.getName().equals(JavaPlugin.COMPILE_JAVA_TASK_NAME)) - .forEach(compileTask -> { - compileTask.getOptions().setCompilerArgs(COMPILER_ARGS); - compileTask.getOptions().setEncoding("UTF-8"); - }); - project.getTasks().withType(JavaCompile.class) - .matching(compileTask -> compileTask.getName().equals(JavaPlugin.COMPILE_TEST_JAVA_TASK_NAME) - || compileTask.getName().equals("compileTestFixturesJava")) - .forEach(compileTask -> { - compileTask.getOptions().setCompilerArgs(TEST_COMPILER_ARGS); - compileTask.getOptions().setEncoding("UTF-8"); - }); - } - -} diff --git a/buildSrc/src/main/java/org/springframework/build/hint/RuntimeHintsAgentExtension.java b/buildSrc/src/main/java/org/springframework/build/hint/RuntimeHintsAgentExtension.java new file mode 100644 index 000000000000..45d7aad32c20 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/build/hint/RuntimeHintsAgentExtension.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-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.build.hint; + +import java.util.Collections; + +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.SetProperty; + +/** + * Entry point to the DSL extension for the {@link RuntimeHintsAgentPlugin} Gradle plugin. + * @author Brian Clozel + */ +public class RuntimeHintsAgentExtension { + + private final SetProperty includedPackages; + + private final SetProperty excludedPackages; + + public RuntimeHintsAgentExtension(ObjectFactory objectFactory) { + this.includedPackages = objectFactory.setProperty(String.class).convention(Collections.singleton("org.springframework")); + this.excludedPackages = objectFactory.setProperty(String.class).convention(Collections.emptySet()); + } + + public SetProperty getIncludedPackages() { + return this.includedPackages; + } + + public SetProperty getExcludedPackages() { + return this.excludedPackages; + } + + String asJavaAgentArgument() { + StringBuilder builder = new StringBuilder(); + this.includedPackages.get().forEach(packageName -> builder.append('+').append(packageName).append(',')); + this.excludedPackages.get().forEach(packageName -> builder.append('-').append(packageName).append(',')); + return builder.toString(); + } +} diff --git a/buildSrc/src/main/java/org/springframework/build/hint/RuntimeHintsAgentPlugin.java b/buildSrc/src/main/java/org/springframework/build/hint/RuntimeHintsAgentPlugin.java new file mode 100644 index 000000000000..f8f4a513f5e2 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/build/hint/RuntimeHintsAgentPlugin.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-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.build.hint; + +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.plugins.JavaPlugin; +import org.gradle.api.tasks.bundling.Jar; +import org.gradle.api.tasks.testing.Test; + +/** + * {@link Plugin} that configures the {@code RuntimeHints} Java agent to test tasks. + * + * @author Brian Clozel + */ +public class RuntimeHintsAgentPlugin implements Plugin { + + public static final String RUNTIMEHINTS_TEST_TASK = "runtimeHintsTest"; + private static final String EXTENSION_NAME = "runtimeHintsAgent"; + + + @Override + public void apply(Project project) { + + project.getPlugins().withType(JavaPlugin.class, javaPlugin -> { + RuntimeHintsAgentExtension agentExtension = project.getExtensions().create(EXTENSION_NAME, + RuntimeHintsAgentExtension.class, project.getObjects()); + Test agentTest = project.getTasks().create(RUNTIMEHINTS_TEST_TASK, Test.class, test -> { + test.useJUnitPlatform(options -> { + options.includeTags("RuntimeHintsTests"); + }); + test.include("**/*Tests.class", "**/*Test.class"); + test.systemProperty("java.awt.headless", "true"); + }); + project.afterEvaluate(p -> { + Jar jar = project.getRootProject().project("spring-core-test").getTasks().withType(Jar.class).named("jar").get(); + agentTest.jvmArgs("-javaagent:" + jar.getArchiveFile().get().getAsFile() + "=" + agentExtension.asJavaAgentArgument()); + }); + project.getTasks().getByName("check", task -> task.dependsOn(agentTest)); + }); + } +} diff --git a/buildSrc/src/main/java/org/springframework/build/optional/OptionalDependenciesPlugin.java b/buildSrc/src/main/java/org/springframework/build/optional/OptionalDependenciesPlugin.java index 55537c2d8259..d8c188f4ce26 100644 --- a/buildSrc/src/main/java/org/springframework/build/optional/OptionalDependenciesPlugin.java +++ b/buildSrc/src/main/java/org/springframework/build/optional/OptionalDependenciesPlugin.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -20,15 +20,13 @@ import org.gradle.api.Project; import org.gradle.api.artifacts.Configuration; import org.gradle.api.plugins.JavaPlugin; -import org.gradle.api.plugins.JavaPluginConvention; +import org.gradle.api.plugins.JavaPluginExtension; import org.gradle.api.tasks.SourceSetContainer; -import org.gradle.plugins.ide.eclipse.EclipsePlugin; -import org.gradle.plugins.ide.eclipse.model.EclipseModel; /** * A {@code Plugin} that adds support for Maven-style optional dependencies. Creates a new * {@code optional} configuration. The {@code optional} configuration is part of the - * project's compile and runtime classpath's but does not affect the classpath of + * project's compile and runtime classpaths but does not affect the classpath of * dependent projects. * * @author Andy Wilkinson @@ -43,22 +41,16 @@ public class OptionalDependenciesPlugin implements Plugin { @Override public void apply(Project project) { Configuration optional = project.getConfigurations().create("optional"); + optional.setCanBeConsumed(false); + optional.setCanBeResolved(false); project.getPlugins().withType(JavaPlugin.class, (javaPlugin) -> { - SourceSetContainer sourceSets = project.getConvention() - .getPlugin(JavaPluginConvention.class).getSourceSets(); + SourceSetContainer sourceSets = project.getExtensions().getByType(JavaPluginExtension.class) + .getSourceSets(); sourceSets.all((sourceSet) -> { - sourceSet.setCompileClasspath( - sourceSet.getCompileClasspath().plus(optional)); - sourceSet.setRuntimeClasspath( - sourceSet.getRuntimeClasspath().plus(optional)); + project.getConfigurations().getByName(sourceSet.getCompileClasspathConfigurationName()).extendsFrom(optional); + project.getConfigurations().getByName(sourceSet.getRuntimeClasspathConfigurationName()).extendsFrom(optional); }); }); - project.getPlugins().withType(EclipsePlugin.class, (eclipePlugin) -> { - project.getExtensions().getByType(EclipseModel.class) - .classpath((classpath) -> { - classpath.getPlusConfigurations().add(optional); - }); - }); } } \ No newline at end of file diff --git a/buildSrc/src/main/java/org/springframework/build/shadow/ShadowSource.java b/buildSrc/src/main/java/org/springframework/build/shadow/ShadowSource.java new file mode 100644 index 000000000000..de14fd691011 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/build/shadow/ShadowSource.java @@ -0,0 +1,175 @@ +package org.springframework.build.shadow; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import org.gradle.api.DefaultTask; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.component.ModuleComponentSelector; +import org.gradle.api.artifacts.query.ArtifactResolutionQuery; +import org.gradle.api.artifacts.result.ArtifactResolutionResult; +import org.gradle.api.artifacts.result.ComponentArtifactsResult; +import org.gradle.api.artifacts.result.DependencyResult; +import org.gradle.api.artifacts.result.ResolutionResult; +import org.gradle.api.artifacts.result.ResolvedArtifactResult; +import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.file.FileCopyDetails; +import org.gradle.api.file.FileTree; +import org.gradle.api.tasks.Classpath; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Nested; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.OutputDirectory; +import org.gradle.api.tasks.TaskAction; +import org.gradle.jvm.JvmLibrary; +import org.gradle.language.base.artifact.SourcesArtifact; + +/** + * Gradle task to add source from shadowed jars into our own source jars. + * + * @author Phillip Webb + * @author Andy Wilkinson + */ +public class ShadowSource extends DefaultTask { + + private final DirectoryProperty outputDirectory = getProject().getObjects().directoryProperty(); + + private List configurations = new ArrayList<>(); + + private final List relocations = new ArrayList<>(); + + + @Classpath + @Optional + public List getConfigurations() { + return this.configurations; + } + + public void setConfigurations(List configurations) { + this.configurations = configurations; + } + + @Nested + public List getRelocations() { + return this.relocations; + } + + public void relocate(String pattern, String destination) { + this.relocations.add(new Relocation(pattern, destination)); + } + + @OutputDirectory + DirectoryProperty getOutputDirectory() { + return this.outputDirectory; + } + + @TaskAction + void syncSourceJarFiles() { + sync(getSourceJarFiles()); + } + + private List getSourceJarFiles() { + List sourceJarFiles = new ArrayList<>(); + for (Configuration configuration : this.configurations) { + ResolutionResult resolutionResult = configuration.getIncoming().getResolutionResult(); + resolutionResult.getRootComponent().get().getDependencies().forEach(dependency -> { + Set artifactsResults = resolveSourceArtifacts(dependency); + for (ComponentArtifactsResult artifactResult : artifactsResults) { + artifactResult.getArtifacts(SourcesArtifact.class).forEach(sourceArtifact -> { + sourceJarFiles.add(((ResolvedArtifactResult) sourceArtifact).getFile()); + }); + } + }); + } + return Collections.unmodifiableList(sourceJarFiles); + } + + private Set resolveSourceArtifacts(DependencyResult dependency) { + ModuleComponentSelector componentSelector = (ModuleComponentSelector) dependency.getRequested(); + ArtifactResolutionQuery query = getProject().getDependencies().createArtifactResolutionQuery() + .forModule(componentSelector.getGroup(), componentSelector.getModule(), componentSelector.getVersion()); + return executeQuery(query).getResolvedComponents(); + } + + @SuppressWarnings("unchecked") + private ArtifactResolutionResult executeQuery(ArtifactResolutionQuery query) { + return query.withArtifacts(JvmLibrary.class, SourcesArtifact.class).execute(); + } + + private void sync(List sourceJarFiles) { + getProject().sync(spec -> { + spec.into(this.outputDirectory); + spec.eachFile(this::relocateFile); + spec.filter(this::transformContent); + spec.exclude("META-INF/**"); + spec.setIncludeEmptyDirs(false); + sourceJarFiles.forEach(sourceJar -> spec.from(zipTree(sourceJar))); + }); + } + + private void relocateFile(FileCopyDetails details) { + String path = details.getPath(); + for (Relocation relocation : this.relocations) { + path = relocation.relocatePath(path); + } + details.setPath(path); + } + + private String transformContent(String content) { + for (Relocation relocation : this.relocations) { + content = relocation.transformContent(content); + } + return content; + } + + private FileTree zipTree(File sourceJar) { + return getProject().zipTree(sourceJar); + } + + + /** + * A single relocation. + */ + static class Relocation { + + private final String pattern; + + private final String pathPattern; + + private final String destination; + + private final String pathDestination; + + + Relocation(String pattern, String destination) { + this.pattern = pattern; + this.pathPattern = pattern.replace('.', '/'); + this.destination = destination; + this.pathDestination = destination.replace('.', '/'); + } + + + @Input + public String getPattern() { + return this.pattern; + } + + @Input + public String getDestination() { + return this.destination; + } + + String relocatePath(String path) { + return path.replace(this.pathPattern, this.pathDestination); + } + + public String transformContent(String content) { + return content.replaceAll("\\b" + this.pattern, this.destination); + } + + } + +} diff --git a/ci/README.adoc b/ci/README.adoc index c2c154acca90..9ff0d5b1e86c 100644 --- a/ci/README.adoc +++ b/ci/README.adoc @@ -1,7 +1,8 @@ == Spring Framework Concourse pipeline -The Spring Framework is using https://concourse-ci.org/[Concourse] for its CI build and other automated tasks. -The Spring team has a dedicated Concourse instance available at https://ci.spring.io. +The Spring Framework uses https://concourse-ci.org/[Concourse] for its CI build and other automated tasks. +The Spring team has a dedicated Concourse instance available at https://ci.spring.io with a build pipeline +for https://ci.spring.io/teams/spring-framework/pipelines/spring-framework-6.0.x[Spring Framework 6.0.x]. === Setting up your development environment @@ -25,13 +26,17 @@ spring https://ci.spring.io spring-framework Wed, 25 Mar 20 ---- === Pipeline configuration and structure + The build pipelines are described in `pipeline.yml` file. + This file is listing Concourse resources, i.e. build inputs and outputs such as container images, artifact repositories, source repositories, notification services, etc. + It also describes jobs (a job is a sequence of inputs, tasks and outputs); jobs are organized by groups. The `pipeline.yml` definition contains `((parameters))` which are loaded from the `parameters.yml` file or from our https://docs.cloudfoundry.org/credhub/[credhub instance]. You'll find in this folder the following resources: + * `pipeline.yml` the build pipeline * `parameters.yml` the build parameters used for the pipeline * `images/` holds the container images definitions used in this pipeline @@ -41,11 +46,12 @@ You'll find in this folder the following resources: === Updating the build pipeline Updating files on the repository is not enough to update the build pipeline, as changes need to be applied. + The pipeline can be deployed using the following command: [source] ---- -$ fly -t spring set-pipeline -p spring-framework-5.3.x -c ci/pipeline.yml -l ci/parameters.yml +$ fly -t spring set-pipeline -p spring-framework-6.0.x -c ci/pipeline.yml -l ci/parameters.yml ---- -NOTE: This assumes that you have credhub integration configured with the appropriate secrets. \ No newline at end of file +NOTE: This assumes that you have credhub integration configured with the appropriate secrets. diff --git a/ci/config/changelog-generator.yml b/ci/config/changelog-generator.yml index 248e58db739e..fce2a3a860b1 100644 --- a/ci/config/changelog-generator.yml +++ b/ci/config/changelog-generator.yml @@ -4,7 +4,7 @@ changelog: - title: ":star: New Features" labels: - "type: enhancement" - - title: ":beetle: Bug Fixes" + - title: ":lady_beetle: Bug Fixes" labels: - "type: bug" - "type: regression" @@ -15,3 +15,6 @@ changelog: sort: "title" labels: - "type: dependency-upgrade" + contributors: + exclude: + names: ["bclozel", "jhoeller", "poutsma", "rstoyanchev", "sbrannen", "sdeleuze", "snicoll"] diff --git a/ci/config/release-scripts.yml b/ci/config/release-scripts.yml index 032a42d67161..d31f8cba00dc 100644 --- a/ci/config/release-scripts.yml +++ b/ci/config/release-scripts.yml @@ -1,9 +1,10 @@ logging: level: io.spring.concourse: DEBUG -distribute: - optional-deployments: - - '.*\\.zip' spring: main: - banner-mode: off \ No newline at end of file + banner-mode: off +sonatype: + exclude: + - 'build-info\.json' + - '.*\.zip' diff --git a/ci/images/ci-image/Dockerfile b/ci/images/ci-image/Dockerfile new file mode 100644 index 000000000000..cfb4fcb479af --- /dev/null +++ b/ci/images/ci-image/Dockerfile @@ -0,0 +1,12 @@ +FROM ubuntu:focal-20220922 + +ADD setup.sh /setup.sh +ADD get-jdk-url.sh /get-jdk-url.sh +RUN ./setup.sh + +ENV JAVA_HOME /opt/openjdk/java17 +ENV JDK17 /opt/openjdk/java17 +ENV JDK18 /opt/openjdk/java18 +ENV JDK19 /opt/openjdk/java19 + +ENV PATH $JAVA_HOME/bin:$PATH diff --git a/ci/images/get-jdk-url.sh b/ci/images/get-jdk-url.sh index 25a9daea4cdb..6bf034579429 100755 --- a/ci/images/get-jdk-url.sh +++ b/ci/images/get-jdk-url.sh @@ -2,19 +2,16 @@ set -e case "$1" in - java8) - echo "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u265-b01/OpenJDK8U-jdk_x64_linux_hotspot_8u265b01.tar.gz" + java17) + echo "https://github.com/bell-sw/Liberica/releases/download/17.0.5+8/bellsoft-jdk17.0.5+8-linux-amd64.tar.gz" ;; - java11) - echo "https://github.com/AdoptOpenJDK/openjdk11-binaries/releases/download/jdk-11.0.8%2B10/OpenJDK11U-jdk_x64_linux_hotspot_11.0.8_10.tar.gz" + java18) + echo "https://github.com/bell-sw/Liberica/releases/download/18.0.2.1%2B1/bellsoft-jdk18.0.2.1+1-linux-amd64.tar.gz" ;; - java14) - echo "https://github.com/AdoptOpenJDK/openjdk14-binaries/releases/download/jdk-14.0.2%2B12/OpenJDK14U-jdk_x64_linux_hotspot_14.0.2_12.tar.gz" + java19) + echo "https://github.com/bell-sw/Liberica/releases/download/19.0.1%2B11/bellsoft-jdk19.0.1+11-linux-amd64.tar.gz" ;; - java15) - echo "https://github.com/AdoptOpenJDK/openjdk15-binaries/releases/download/jdk-15%2B36/OpenJDK15U-jdk_x64_linux_hotspot_15_36.tar.gz" - ;; - *) + *) echo $"Unknown java version" exit 1 esac diff --git a/ci/images/setup.sh b/ci/images/setup.sh index ffc3d86d89d3..043dca9371f5 100755 --- a/ci/images/setup.sh +++ b/ci/images/setup.sh @@ -5,24 +5,32 @@ set -ex # UTILS ########################################################### +export DEBIAN_FRONTEND=noninteractive apt-get update -apt-get install --no-install-recommends -y ca-certificates net-tools libxml2-utils git curl libudev1 libxml2-utils iptables iproute2 jq fontconfig +apt-get install --no-install-recommends -y tzdata ca-certificates net-tools libxml2-utils git curl libudev1 libxml2-utils iptables iproute2 jq fontconfig +ln -fs /usr/share/zoneinfo/UTC /etc/localtime +dpkg-reconfigure --frontend noninteractive tzdata rm -rf /var/lib/apt/lists/* -curl https://raw.githubusercontent.com/spring-io/concourse-java-scripts/v0.0.3/concourse-java.sh > /opt/concourse-java.sh - -curl --output /opt/concourse-release-scripts.jar https://repo.spring.io/release/io/spring/concourse/releasescripts/concourse-release-scripts/0.2.1/concourse-release-scripts-0.2.1.jar +curl https://raw.githubusercontent.com/spring-io/concourse-java-scripts/v0.0.4/concourse-java.sh > /opt/concourse-java.sh ########################################################### # JAVA ########################################################### -JDK_URL=$( ./get-jdk-url.sh $1 ) mkdir -p /opt/openjdk -cd /opt/openjdk -curl -L ${JDK_URL} | tar zx --strip-components=1 -test -f /opt/openjdk/bin/java -test -f /opt/openjdk/bin/javac +pushd /opt/openjdk > /dev/null +for jdk in java17 java18 java19 +do + JDK_URL=$( /get-jdk-url.sh $jdk ) + mkdir $jdk + pushd $jdk > /dev/null + curl -L ${JDK_URL} | tar zx --strip-components=1 + test -f bin/java + test -f bin/javac + popd > /dev/null +done +popd ########################################################### # GRADLE ENTERPRISE diff --git a/ci/images/spring-framework-ci-image/Dockerfile b/ci/images/spring-framework-ci-image/Dockerfile deleted file mode 100644 index 2e237054650b..000000000000 --- a/ci/images/spring-framework-ci-image/Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -FROM ubuntu:bionic-20200713 - -ADD setup.sh /setup.sh -ADD get-jdk-url.sh /get-jdk-url.sh -RUN ./setup.sh java8 - -ENV JAVA_HOME /opt/openjdk -ENV PATH $JAVA_HOME/bin:$PATH diff --git a/ci/images/spring-framework-jdk11-ci-image/Dockerfile b/ci/images/spring-framework-jdk11-ci-image/Dockerfile deleted file mode 100644 index 29ac39677553..000000000000 --- a/ci/images/spring-framework-jdk11-ci-image/Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -FROM ubuntu:bionic-20200713 - -ADD setup.sh /setup.sh -ADD get-jdk-url.sh /get-jdk-url.sh -RUN ./setup.sh java11 - -ENV JAVA_HOME /opt/openjdk -ENV PATH $JAVA_HOME/bin:$PATH diff --git a/ci/images/spring-framework-jdk14-ci-image/Dockerfile b/ci/images/spring-framework-jdk14-ci-image/Dockerfile deleted file mode 100644 index bd7a467de227..000000000000 --- a/ci/images/spring-framework-jdk14-ci-image/Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -FROM ubuntu:bionic-20200713 - -ADD setup.sh /setup.sh -ADD get-jdk-url.sh /get-jdk-url.sh -RUN ./setup.sh java14 - -ENV JAVA_HOME /opt/openjdk -ENV PATH $JAVA_HOME/bin:$PATH diff --git a/ci/images/spring-framework-jdk15-ci-image/Dockerfile b/ci/images/spring-framework-jdk15-ci-image/Dockerfile deleted file mode 100644 index c3b544e01cca..000000000000 --- a/ci/images/spring-framework-jdk15-ci-image/Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -FROM ubuntu:bionic-20200713 - -ADD setup.sh /setup.sh -ADD get-jdk-url.sh /get-jdk-url.sh -RUN ./setup.sh java15 - -ENV JAVA_HOME /opt/openjdk -ENV PATH $JAVA_HOME/bin:$PATH diff --git a/ci/parameters.yml b/ci/parameters.yml index 3e09f785ebf8..74f41d365c95 100644 --- a/ci/parameters.yml +++ b/ci/parameters.yml @@ -1,14 +1,14 @@ -email-server: "smtp.svc.pivotal.io" -email-from: "ci@spring.io" -email-to: ["spring-framework-dev@pivotal.io"] github-repo: "https://github.com/spring-projects/spring-framework.git" github-repo-name: "spring-projects/spring-framework" +sonatype-staging-profile: "org.springframework" docker-hub-organization: "springci" artifactory-server: "https://repo.spring.io/" -branch: "master" +branch: "main" +milestone: "6.0.x" build-name: "spring-framework" pipeline-name: "spring-framework" concourse-url: "https://ci.spring.io/" -bintray-subject: "spring" -bintray-repo: "jars" -task-timeout: 1h00m \ No newline at end of file +registry-mirror-host: docker.repo.spring.io +registry-mirror-username: ((artifactory-username)) +registry-mirror-password: ((artifactory-password)) +task-timeout: 1h00m diff --git a/ci/pipeline.yml b/ci/pipeline.yml index 38e7a4f4d8be..51166cfbf58c 100644 --- a/ci/pipeline.yml +++ b/ci/pipeline.yml @@ -1,21 +1,33 @@ anchors: + git-repo-resource-source: &git-repo-resource-source + uri: ((github-repo)) + username: ((github-username)) + password: ((github-ci-release-token)) + branch: ((branch)) + gradle-enterprise-task-params: &gradle-enterprise-task-params + GRADLE_ENTERPRISE_ACCESS_KEY: ((gradle_enterprise_secret_access_key)) + GRADLE_ENTERPRISE_CACHE_USERNAME: ((gradle_enterprise_cache_user.username)) + GRADLE_ENTERPRISE_CACHE_PASSWORD: ((gradle_enterprise_cache_user.password)) + sonatype-task-params: &sonatype-task-params + SONATYPE_USERNAME: ((sonatype-username)) + SONATYPE_PASSWORD: ((sonatype-password)) + SONATYPE_URL: ((sonatype-url)) + SONATYPE_STAGING_PROFILE: ((sonatype-staging-profile)) artifactory-task-params: &artifactory-task-params ARTIFACTORY_SERVER: ((artifactory-server)) ARTIFACTORY_USERNAME: ((artifactory-username)) ARTIFACTORY_PASSWORD: ((artifactory-password)) - bintray-task-params: &bintray-task-params - BINTRAY_SUBJECT: ((bintray-subject)) - BINTRAY_REPO: ((bintray-repo)) - BINTRAY_USERNAME: ((bintray-username)) - BINTRAY_API_KEY: ((bintray-api-key)) + build-project-task-params: &build-project-task-params + BRANCH: ((branch)) + <<: *gradle-enterprise-task-params docker-resource-source: &docker-resource-source username: ((docker-hub-username)) password: ((docker-hub-password)) - tag: 5.3.x - gradle-enterprise-task-params: &gradle-enterprise-task-params - GRADLE_ENTERPRISE_ACCESS_KEY: ((gradle_enterprise_secret_access_key)) - GRADLE_ENTERPRISE_CACHE_USERNAME: ((gradle_enterprise_cache_user.username)) - GRADLE_ENTERPRISE_CACHE_PASSWORD: ((gradle_enterprise_cache_user.password)) + tag: ((milestone)) + registry-mirror-vars: ®istry-mirror-vars + registry-mirror-host: ((registry-mirror-host)) + registry-mirror-username: ((registry-mirror-username)) + registry-mirror-password: ((registry-mirror-password)) slack-fail-params: &slack-fail-params text: > :concourse-failed: @@ -24,9 +36,6 @@ anchors: silent: true icon_emoji: ":concourse:" username: concourse-ci - sonatype-task-params: &sonatype-task-params - SONATYPE_USER_TOKEN: ((sonatype-user-token)) - SONATYPE_PASSWORD_TOKEN: ((sonatype-user-token-password)) changelog-task-params: &changelog-task-params name: generated-changelog/tag tag: generated-changelog/tag @@ -36,38 +45,42 @@ anchors: GITHUB_TOKEN: ((github-ci-release-token)) resource_types: +- name: registry-image + type: registry-image + source: + repository: concourse/registry-image-resource + tag: 1.5.0 - name: artifactory-resource - type: docker-image + type: registry-image source: repository: springio/artifactory-resource - tag: 0.0.12 + tag: 0.0.17 +- name: github-release + type: registry-image + source: + repository: concourse/github-release-resource + tag: 1.5.5 - name: github-status-resource - type: docker-image + type: registry-image source: repository: dpb587/github-status-resource tag: master +- name: pull-request + type: registry-image + source: + repository: teliaoss/github-pr-resource + tag: v0.23.0 - name: slack-notification - type: docker-image + type: registry-image source: repository: cfcommunity/slack-notification-resource tag: latest - resources: - name: git-repo type: git icon: github source: - uri: ((github-repo)) - username: ((github-username)) - password: ((github-password)) - branch: ((branch)) -- name: every-morning - type: time - icon: alarm - source: - start: 8:00 AM - stop: 9:00 AM - location: Europe/Vienna + <<: *git-repo-resource-source - name: ci-images-git-repo type: git icon: github @@ -75,30 +88,19 @@ resources: uri: ((github-repo)) branch: ((branch)) paths: ["ci/images/*"] -- name: spring-framework-ci-image - type: docker-image - icon: docker - source: - <<: *docker-resource-source - repository: ((docker-hub-organization))/spring-framework-ci-image -- name: spring-framework-jdk11-ci-image - type: docker-image - icon: docker - source: - <<: *docker-resource-source - repository: ((docker-hub-organization))/spring-framework-jdk11-ci-image -- name: spring-framework-jdk14-ci-image - type: docker-image +- name: ci-image + type: registry-image icon: docker source: <<: *docker-resource-source - repository: ((docker-hub-organization))/spring-framework-jdk14-ci-image -- name: spring-framework-jdk15-ci-image - type: docker-image - icon: docker + repository: ((docker-hub-organization))/spring-framework-ci +- name: every-morning + type: time + icon: alarm source: - <<: *docker-resource-source - repository: ((docker-hub-organization))/spring-framework-jdk15-ci-image + start: 8:00 AM + stop: 9:00 AM + location: Europe/Vienna - name: artifactory-repo type: artifactory-resource icon: package-variant @@ -107,38 +109,38 @@ resources: username: ((artifactory-username)) password: ((artifactory-password)) build_name: ((build-name)) -- name: repo-status-build - type: github-status-resource - icon: eye-check-outline +- name: git-pull-request + type: pull-request + icon: source-pull source: + access_token: ((github-ci-pull-request-token)) repository: ((github-repo-name)) - access_token: ((github-ci-status-token)) - branch: ((branch)) - context: build -- name: repo-status-jdk11-build + base_branch: ((branch)) + ignore_paths: ["ci/*"] +- name: repo-status-build type: github-status-resource icon: eye-check-outline source: repository: ((github-repo-name)) access_token: ((github-ci-status-token)) branch: ((branch)) - context: jdk11-build -- name: repo-status-jdk14-build + context: build +- name: repo-status-jdk18-build type: github-status-resource icon: eye-check-outline source: repository: ((github-repo-name)) access_token: ((github-ci-status-token)) branch: ((branch)) - context: jdk14-build -- name: repo-status-jdk15-build + context: jdk18-build +- name: repo-status-jdk19-build type: github-status-resource icon: eye-check-outline source: repository: ((github-repo-name)) access_token: ((github-ci-status-token)) branch: ((branch)) - context: jdk15-build + context: jdk19-build - name: slack-alert type: slack-notification icon: slack @@ -161,47 +163,40 @@ resources: repository: spring-framework access_token: ((github-ci-release-token)) pre_release: false - jobs: -- name: build-spring-framework-ci-images +- name: build-ci-images plan: - - get: ci-images-git-repo - trigger: true - - in_parallel: - - put: spring-framework-ci-image - params: - build: ci-images-git-repo/ci/images - dockerfile: ci-images-git-repo/ci/images/spring-framework-ci-image/Dockerfile - - put: spring-framework-jdk11-ci-image - params: - build: ci-images-git-repo/ci/images - dockerfile: ci-images-git-repo/ci/images/spring-framework-jdk11-ci-image/Dockerfile - - put: spring-framework-jdk14-ci-image - params: - build: ci-images-git-repo/ci/images - dockerfile: ci-images-git-repo/ci/images/spring-framework-jdk14-ci-image/Dockerfile - - put: spring-framework-jdk15-ci-image + - get: git-repo + - get: ci-images-git-repo + trigger: true + - task: build-ci-image + privileged: true + file: git-repo/ci/tasks/build-ci-image.yml + output_mapping: + image: ci-image + vars: + ci-image-name: ci-image + <<: *registry-mirror-vars + - put: ci-image params: - build: ci-images-git-repo/ci/images - dockerfile: ci-images-git-repo/ci/images/spring-framework-jdk15-ci-image/Dockerfile + image: ci-image/image.tar - name: build serial: true public: true plan: - - get: spring-framework-ci-image + - get: ci-image - get: git-repo trigger: true - put: repo-status-build params: { state: "pending", commit: "git-repo" } - do: - task: build-project + image: ci-image + file: git-repo/ci/tasks/build-project.yml privileged: true timeout: ((task-timeout)) - image: spring-framework-ci-image - file: git-repo/ci/tasks/build-project.yml params: - BRANCH: ((branch)) - <<: *gradle-enterprise-task-params + <<: *build-project-task-params on_failure: do: - put: repo-status-build @@ -213,6 +208,8 @@ jobs: params: { state: "success", commit: "git-repo" } - put: artifactory-repo params: &artifactory-params + signing_key: ((signing-key)) + signing_passphrase: ((signing-passphrase)) repo: libs-snapshot-local folder: distribution-repository build_uri: "https://ci.spring.io/teams/$%7BBUILD_TEAM_NAME%7D/pipelines/$%7BBUILD_PIPELINE_NAME%7D/jobs/$%7BBUILD_JOB_NAME%7D/builds/$%7BBUILD_NAME%7D" @@ -221,117 +218,120 @@ jobs: threads: 8 artifact_set: - include: - - "/**/spring-*.zip" + - "/**/framework-docs-*.zip" properties: "zip.name": "spring-framework" "zip.displayname": "Spring Framework" "zip.deployed": "false" - include: - - "/**/spring-*-docs.zip" + - "/**/framework-docs-*-docs.zip" properties: "zip.type": "docs" - include: - - "/**/spring-*-dist.zip" + - "/**/framework-docs-*-dist.zip" properties: "zip.type": "dist" - include: - - "/**/spring-*-schema.zip" + - "/**/framework-docs-*-schema.zip" properties: "zip.type": "schema" get_params: threads: 8 -- name: jdk11-build +- name: jdk18-build serial: true public: true plan: - - get: spring-framework-jdk11-ci-image - - get: git-repo - - get: every-morning - trigger: true - - put: repo-status-jdk11-build - params: { state: "pending", commit: "git-repo" } - - do: - - task: check-project - privileged: true - timeout: ((task-timeout)) - image: spring-framework-jdk11-ci-image - file: git-repo/ci/tasks/check-project.yml - params: - BRANCH: ((branch)) - <<: *gradle-enterprise-task-params - on_failure: - do: - - put: repo-status-jdk11-build - params: { state: "failure", commit: "git-repo" } - - put: slack-alert - params: - <<: *slack-fail-params - - put: repo-status-jdk11-build - params: { state: "success", commit: "git-repo" } -- name: jdk14-build - serial: true - public: true - plan: - - get: spring-framework-jdk14-ci-image + - get: ci-image - get: git-repo - get: every-morning trigger: true - - put: repo-status-jdk14-build + - put: repo-status-jdk18-build params: { state: "pending", commit: "git-repo" } - do: - - task: check-project - privileged: true - timeout: ((task-timeout)) - image: spring-framework-jdk14-ci-image - file: git-repo/ci/tasks/check-project.yml - params: - BRANCH: ((branch)) - <<: *gradle-enterprise-task-params + - task: check-project + image: ci-image + file: git-repo/ci/tasks/check-project.yml + privileged: true + timeout: ((task-timeout)) + params: + TEST_TOOLCHAIN: 18 + <<: *build-project-task-params on_failure: do: - - put: repo-status-jdk14-build + - put: repo-status-jdk18-build params: { state: "failure", commit: "git-repo" } - put: slack-alert params: <<: *slack-fail-params - - put: repo-status-jdk14-build + - put: repo-status-jdk18-build params: { state: "success", commit: "git-repo" } -- name: jdk15-build +- name: jdk19-build serial: true public: true plan: - - get: spring-framework-jdk15-ci-image + - get: ci-image - get: git-repo - get: every-morning trigger: true - - put: repo-status-jdk15-build + - put: repo-status-jdk19-build params: { state: "pending", commit: "git-repo" } - do: - task: check-project + image: ci-image + file: git-repo/ci/tasks/check-project.yml privileged: true timeout: ((task-timeout)) - image: spring-framework-jdk15-ci-image - file: git-repo/ci/tasks/check-project.yml params: - BRANCH: ((branch)) - <<: *gradle-enterprise-task-params + TEST_TOOLCHAIN: 19 + <<: *build-project-task-params on_failure: do: - - put: repo-status-jdk15-build + - put: repo-status-jdk19-build params: { state: "failure", commit: "git-repo" } - put: slack-alert params: <<: *slack-fail-params - - put: repo-status-jdk15-build + - put: repo-status-jdk19-build params: { state: "success", commit: "git-repo" } +- name: build-pull-requests + serial: true + public: true + plan: + - get: ci-image + - get: git-repo + resource: git-pull-request + trigger: true + version: every + - do: + - put: git-pull-request + params: + path: git-repo + status: pending + - task: build-pr + image: ci-image + file: git-repo/ci/tasks/build-pr.yml + privileged: true + timeout: ((task-timeout)) + params: + BRANCH: ((branch)) + on_success: + put: git-pull-request + params: + path: git-repo + status: success + on_failure: + put: git-pull-request + params: + path: git-repo + status: failure - name: stage-milestone serial: true plan: - - get: spring-framework-ci-image + - get: ci-image - get: git-repo trigger: false - task: stage - image: spring-framework-ci-image + image: ci-image file: git-repo/ci/tasks/stage-version.yml params: RELEASE_TYPE: M @@ -346,7 +346,7 @@ jobs: - name: promote-milestone serial: true plan: - - get: spring-framework-ci-image + - get: ci-image - get: git-repo trigger: false - get: artifactory-repo @@ -356,7 +356,6 @@ jobs: download_artifacts: false save_build_info: true - task: promote - image: spring-framework-ci-image file: git-repo/ci/tasks/promote-version.yml params: RELEASE_TYPE: M @@ -372,11 +371,11 @@ jobs: - name: stage-rc serial: true plan: - - get: spring-framework-ci-image + - get: ci-image - get: git-repo trigger: false - task: stage - image: spring-framework-ci-image + image: ci-image file: git-repo/ci/tasks/stage-version.yml params: RELEASE_TYPE: RC @@ -391,7 +390,7 @@ jobs: - name: promote-rc serial: true plan: - - get: spring-framework-ci-image + - get: ci-image - get: git-repo trigger: false - get: artifactory-repo @@ -401,7 +400,6 @@ jobs: download_artifacts: false save_build_info: true - task: promote - image: spring-framework-ci-image file: git-repo/ci/tasks/promote-version.yml params: RELEASE_TYPE: RC @@ -417,11 +415,11 @@ jobs: - name: stage-release serial: true plan: - - get: spring-framework-ci-image + - get: ci-image - get: git-repo trigger: false - task: stage - image: spring-framework-ci-image + image: ci-image file: git-repo/ci/tasks/stage-version.yml params: RELEASE_TYPE: RELEASE @@ -436,26 +434,25 @@ jobs: - name: promote-release serial: true plan: - - get: spring-framework-ci-image + - get: ci-image - get: git-repo trigger: false - get: artifactory-repo trigger: false passed: [stage-release] params: - download_artifacts: false + download_artifacts: true save_build_info: true - task: promote - image: spring-framework-ci-image file: git-repo/ci/tasks/promote-version.yml params: RELEASE_TYPE: RELEASE <<: *artifactory-task-params - <<: *bintray-task-params -- name: sync-to-maven-central + <<: *sonatype-task-params +- name: create-github-release serial: true plan: - - get: spring-framework-ci-image + - get: ci-image - get: git-repo - get: artifactory-repo trigger: true @@ -463,12 +460,6 @@ jobs: params: download_artifacts: false save_build_info: true - - task: sync-to-maven-central - image: spring-framework-ci-image - file: git-repo/ci/tasks/sync-to-maven-central.yml - params: - <<: *bintray-task-params - <<: *sonatype-task-params - task: generate-changelog file: git-repo/ci/tasks/generate-changelog.yml params: @@ -480,8 +471,10 @@ jobs: groups: - name: "builds" - jobs: ["build", "jdk11-build", "jdk14-build", "jdk15-build"] + jobs: ["build", "jdk18-build", "jdk19-build"] - name: "releases" - jobs: ["stage-milestone", "stage-rc", "stage-release", "promote-milestone","promote-rc", "promote-release", "sync-to-maven-central"] + jobs: ["stage-milestone", "stage-rc", "stage-release", "promote-milestone", "promote-rc", "promote-release", "create-github-release"] - name: "ci-images" - jobs: ["build-spring-framework-ci-images"] + jobs: ["build-ci-images"] +- name: "pull-requests" + jobs: [ "build-pull-requests" ] diff --git a/ci/scripts/build-pr.sh b/ci/scripts/build-pr.sh new file mode 100755 index 000000000000..94c4e8df65b4 --- /dev/null +++ b/ci/scripts/build-pr.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -e + +source $(dirname $0)/common.sh + +pushd git-repo > /dev/null +./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false --no-daemon --max-workers=4 check +popd > /dev/null diff --git a/ci/scripts/check-project.sh b/ci/scripts/check-project.sh index 94c4e8df65b4..c0b66efc7d3d 100755 --- a/ci/scripts/check-project.sh +++ b/ci/scripts/check-project.sh @@ -4,5 +4,6 @@ set -e source $(dirname $0)/common.sh pushd git-repo > /dev/null -./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false --no-daemon --max-workers=4 check +./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false -Porg.gradle.java.installations.fromEnv=JDK17,JDK18 \ + -PmainToolchain=${MAIN_TOOLCHAIN} -PtestToolchain=${TEST_TOOLCHAIN} --no-daemon --max-workers=4 check popd > /dev/null diff --git a/ci/scripts/generate-changelog.sh b/ci/scripts/generate-changelog.sh index b0bc952a33a4..d3d2b97e5dba 100755 --- a/ci/scripts/generate-changelog.sh +++ b/ci/scripts/generate-changelog.sh @@ -2,7 +2,7 @@ set -e CONFIG_DIR=git-repo/ci/config -version=$( cat version/version ) +version=$( cat artifactory-repo/build-info.json | jq -r '.buildInfo.modules[0].id' | sed 's/.*:.*:\(.*\)/\1/' ) java -jar /github-changelog-generator.jar \ --spring.config.location=${CONFIG_DIR}/changelog-generator.yml \ diff --git a/ci/scripts/promote-version.sh b/ci/scripts/promote-version.sh index 020db4f300b3..bd1600191a79 100755 --- a/ci/scripts/promote-version.sh +++ b/ci/scripts/promote-version.sh @@ -1,16 +1,17 @@ #!/bin/bash -source $(dirname $0)/common.sh CONFIG_DIR=git-repo/ci/config version=$( cat artifactory-repo/build-info.json | jq -r '.buildInfo.modules[0].id' | sed 's/.*:.*:\(.*\)/\1/' ) export BUILD_INFO_LOCATION=$(pwd)/artifactory-repo/build-info.json -java -jar /opt/concourse-release-scripts.jar promote $RELEASE_TYPE $BUILD_INFO_LOCATION > /dev/null || { exit 1; } +java -jar /concourse-release-scripts.jar \ + --spring.config.location=${CONFIG_DIR}/release-scripts.yml \ + publishToCentral $RELEASE_TYPE $BUILD_INFO_LOCATION artifactory-repo || { exit 1; } -java -jar /opt/concourse-release-scripts.jar \ +java -jar /concourse-release-scripts.jar \ --spring.config.location=${CONFIG_DIR}/release-scripts.yml \ - distribute $RELEASE_TYPE $BUILD_INFO_LOCATION > /dev/null || { exit 1; } + promote $RELEASE_TYPE $BUILD_INFO_LOCATION || { exit 1; } echo "Promotion complete" echo $version > version/version diff --git a/ci/scripts/stage-version.sh b/ci/scripts/stage-version.sh index 0b285de014bf..73c57755451c 100755 --- a/ci/scripts/stage-version.sh +++ b/ci/scripts/stage-version.sh @@ -29,8 +29,8 @@ fi echo "Staging $stageVersion (next version will be $nextVersion)" sed -i "s/version=$snapshotVersion/version=$stageVersion/" gradle.properties -git config user.name "Spring Buildmaster" > /dev/null -git config user.email "buildmaster@springframework.org" > /dev/null +git config user.name "Spring Builds" > /dev/null +git config user.email "spring-builds@users.noreply.github.com" > /dev/null git add gradle.properties > /dev/null git commit -m"Release v$stageVersion" > /dev/null git tag -a "v$stageVersion" -m"Release v$stageVersion" > /dev/null diff --git a/ci/scripts/sync-to-maven-central.sh b/ci/scripts/sync-to-maven-central.sh deleted file mode 100755 index b42631164ed5..000000000000 --- a/ci/scripts/sync-to-maven-central.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -export BUILD_INFO_LOCATION=$(pwd)/artifactory-repo/build-info.json -version=$( cat artifactory-repo/build-info.json | jq -r '.buildInfo.modules[0].id' | sed 's/.*:.*:\(.*\)/\1/' ) -java -jar /opt/concourse-release-scripts.jar syncToCentral "RELEASE" $BUILD_INFO_LOCATION || { exit 1; } - -echo "Sync complete" -echo $version > version/version diff --git a/ci/tasks/build-ci-image.yml b/ci/tasks/build-ci-image.yml new file mode 100644 index 000000000000..db0ad5d05175 --- /dev/null +++ b/ci/tasks/build-ci-image.yml @@ -0,0 +1,32 @@ +--- +platform: linux +image_resource: + type: registry-image + source: + repository: concourse/oci-build-task + tag: 0.10.0 + registry_mirror: + host: ((registry-mirror-host)) + username: ((registry-mirror-username)) + password: ((registry-mirror-password)) +inputs: + - name: ci-images-git-repo +outputs: + - name: image +caches: + - path: ci-image-cache +params: + CONTEXT: ci-images-git-repo/ci/images + DOCKERFILE: ci-images-git-repo/ci/images/ci-image/Dockerfile + DOCKER_HUB_AUTH: ((docker-hub-auth)) +run: + path: /bin/sh + args: + - "-c" + - | + mkdir -p /root/.docker + cat > /root/.docker/config.json < + project.sourceSets.main.allJava + } + maxMemory = "1024m" + destinationDir = file("$buildDir/docs/javadoc") +} + +/** + * Produce KDoc for all Spring Framework modules in "build/docs/kdoc" + */ +rootProject.tasks.dokkaHtmlMultiModule.configure { + dependsOn { + tasks.getByName("api") + } + moduleName.set("spring-framework") + outputDirectory.set(project.file("$buildDir/docs/kdoc")) +} + +asciidoctorj { + def docRoot = 'https://docs.spring.io/' + def docsSpringFramework = "${docRoot}/spring-framework/docs/${project.version}" + version = '2.4.3' + fatalWarnings ".*" + options doctype: 'book', eruby: 'erubis' + attributes([ + icons: 'font', + idprefix: '', + idseparator: '-', + revnumber: project.version, + sectanchors: '', + sectnums: '', + 'spring-version': project.version, + 'spring-framework-main-code': 'https://github.com/spring-projects/spring-framework/tree/main', + 'doc-root': docRoot, + 'docs-spring-framework': docsSpringFramework, + 'api-spring-framework': "${docsSpringFramework}/javadoc-api/org/springframework" + ]) +} + +/** + * Generate the Spring Framework Reference documentation from + * "src/docs/asciidoc" in "build/docs/ref-docs/html5". + */ +asciidoctor { + baseDirFollowsSourceDir() + configurations "asciidoctorExtensions" + sources { + include '*.adoc' + } + outputDir "$buildDir/docs/ref-docs/html5" + outputOptions { + backends "spring-html" + } + logDocuments = true + resources { + from(sourceDir) { + include 'images/*.png' + } + } +} + +/** + * Generate the Spring Framework Reference documentation from "src/docs/asciidoc" + * in "build/docs/ref-docs/pdf". + */ +asciidoctorPdf { + baseDirFollowsSourceDir() + configurations 'asciidoctorExtensions' + sources { + include 'spring-framework.adocbook' + } + outputDir "$buildDir/docs/ref-docs/pdf" + forkOptions { + jvmArgs += ["--add-opens", "java.base/sun.nio.ch=ALL-UNNAMED", "--add-opens", "java.base/java.io=ALL-UNNAMED"] + } + logDocuments = true +} + +/** + * Zip all docs (API and reference) into a single archive + */ +task docsZip(type: Zip, dependsOn: ['api', 'asciidoctor', 'asciidoctorPdf', rootProject.tasks.dokkaHtmlMultiModule]) { + group = "Distribution" + description = "Builds -${archiveClassifier} archive containing api and reference " + + "for deployment at https://docs.spring.io/spring-framework/docs/." + + archiveBaseName.set("spring-framework") + archiveClassifier.set("docs") + from("src/dist") { + include "changelog.txt" + } + from (api) { + into "javadoc-api" + } + from ("$asciidoctor.outputDir") { + into "reference/html" + } + from ("$asciidoctorPdf.outputDir") { + into "reference/pdf" + } + from (rootProject.tasks.dokkaHtmlMultiModule.outputDirectory) { + into "kdoc-api" + } +} + +/** + * Zip all Spring Framework schemas into a single archive + */ +task schemaZip(type: Zip) { + group = "Distribution" + archiveBaseName.set("spring-framework") + archiveClassifier.set("schema") + description = "Builds -${archiveClassifier} archive containing all " + + "XSDs for deployment at https://springframework.org/schema." + duplicatesStrategy DuplicatesStrategy.EXCLUDE + moduleProjects.each { module -> + def Properties schemas = new Properties(); + + module.sourceSets.main.resources.find { + (it.path.endsWith("META-INF/spring.schemas") || it.path.endsWith("META-INF\\spring.schemas")) + }?.withInputStream { schemas.load(it) } + + for (def key : schemas.keySet()) { + def shortName = key.replaceAll(/http.*schema.(.*).spring-.*/, '$1') + assert shortName != key + File xsdFile = module.sourceSets.main.resources.find { + (it.path.endsWith(schemas.get(key)) || it.path.endsWith(schemas.get(key).replaceAll('\\/','\\\\'))) + } + assert xsdFile != null + into (shortName) { + from xsdFile.path + } + } + } +} + +/** + * Create a distribution zip with everything: + * docs, schemas, jars, source jars, javadoc jars + */ +task distZip(type: Zip, dependsOn: [docsZip, schemaZip]) { + group = "Distribution" + archiveBaseName.set("spring-framework") + archiveClassifier.set("dist") + description = "Builds -${archiveClassifier} archive, containing all jars and docs, " + + "suitable for community download page." + + ext.baseDir = "spring-framework-${project.version}"; + + from("src/docs/dist") { + include "readme.txt" + include "license.txt" + include "notice.txt" + into "${baseDir}" + expand(copyright: new Date().format("yyyy"), version: project.version) + } + + from(zipTree(docsZip.archiveFile)) { + into "${baseDir}/docs" + } + + from(zipTree(schemaZip.archiveFile)) { + into "${baseDir}/schema" + } + + moduleProjects.each { module -> + into ("${baseDir}/libs") { + from module.jar + if (module.tasks.findByPath("sourcesJar")) { + from module.sourcesJar + } + if (module.tasks.findByPath("javadocJar")) { + from module.javadocJar + } + } + } +} + +distZip.mustRunAfter moduleProjects.check + + +publishing { + publications { + mavenJava(MavenPublication) { + artifact docsZip + artifact schemaZip + artifact distZip + } + } +} \ No newline at end of file diff --git a/framework-docs/src/docs/api/overview.html b/framework-docs/src/docs/api/overview.html new file mode 100644 index 000000000000..e6086dc458d1 --- /dev/null +++ b/framework-docs/src/docs/api/overview.html @@ -0,0 +1,7 @@ + + +

+This is the public API documentation for the Spring Framework. +

+ + diff --git a/framework-docs/src/docs/asciidoc/appendix.adoc b/framework-docs/src/docs/asciidoc/appendix.adoc new file mode 100644 index 000000000000..8f11dd941a81 --- /dev/null +++ b/framework-docs/src/docs/asciidoc/appendix.adoc @@ -0,0 +1,82 @@ +[[appendix]] += Appendix +:toc: left +:toclevels: 4 +:tabsize: 4 +:docinfo1: + +This part of the reference documentation covers topics that apply to multiple modules +within the core Spring Framework. + + +[[appendix-spring-properties]] +== Spring Properties + +{api-spring-framework}/core/SpringProperties.html[`SpringProperties`] is a static holder +for properties that control certain low-level aspects of the Spring Framework. Users can +configure these properties via JVM system properties or programmatically via the +`SpringProperties.setProperty(String key, String value)` method. The latter may be +necessary if the deployment environment disallows custom JVM system properties. As an +alternative, these properties may be configured in a `spring.properties` file in the root +of the classpath -- for example, deployed within the application's JAR file. + +The following table lists all currently supported Spring properties. + +.Supported Spring Properties +|=== +| Name | Description + +| `spring.beaninfo.ignore` +| Instructs Spring to use the `Introspector.IGNORE_ALL_BEANINFO` mode when calling the +JavaBeans `Introspector`. See +{api-spring-framework}++/beans/CachedIntrospectionResults.html#IGNORE_BEANINFO_PROPERTY_NAME++[`CachedIntrospectionResults`] +for details. + +| `spring.expression.compiler.mode` +| The mode to use when compiling expressions for the +<>. + +| `spring.getenv.ignore` +| Instructs Spring to ignore operating system environment variables if a Spring +`Environment` property -- for example, a placeholder in a configuration String -- isn't +resolvable otherwise. See +{api-spring-framework}++/core/env/AbstractEnvironment.html#IGNORE_GETENV_PROPERTY_NAME++[`AbstractEnvironment`] +for details. + +| `spring.index.ignore` +| Instructs Spring to ignore the components index located in +`META-INF/spring.components`. See <>. + +| `spring.jdbc.getParameterType.ignore` +| Instructs Spring to ignore `java.sql.ParameterMetaData.getParameterType` completely. +See the note in <>. + +| `spring.jndi.ignore` +| Instructs Spring to ignore a default JNDI environment, as an optimization for scenarios +where nothing is ever to be found for such JNDI fallback searches to begin with, avoiding +the repeated JNDI lookup overhead. See +{api-spring-framework}++/jndi/JndiLocatorDelegate.html#IGNORE_JNDI_PROPERTY_NAME++[`JndiLocatorDelegate`] +for details. + +| `spring.objenesis.ignore` +| Instructs Spring to ignore Objenesis, not even attempting to use it. See +{api-spring-framework}++/objenesis/SpringObjenesis.html#IGNORE_OBJENESIS_PROPERTY_NAME++[`SpringObjenesis`] +for details. + +| `spring.test.constructor.autowire.mode` +| The default _test constructor autowire mode_ to use if `@TestConstructor` is not present +on a test class. See <>. + +| `spring.test.context.cache.maxSize` +| The maximum size of the context cache in the _Spring TestContext Framework_. See +<>. + +| `spring.test.enclosing.configuration` +| The default _enclosing configuration inheritance mode_ to use if +`@NestedTestConfiguration` is not present on a test class. See +<>. + +|=== diff --git a/src/docs/asciidoc/core.adoc b/framework-docs/src/docs/asciidoc/core.adoc similarity index 88% rename from src/docs/asciidoc/core.adoc rename to framework-docs/src/docs/asciidoc/core.adoc index b9441028a901..1801504c0edf 100644 --- a/src/docs/asciidoc/core.adoc +++ b/framework-docs/src/docs/asciidoc/core.adoc @@ -1,7 +1,5 @@ [[spring-core]] = Core Technologies -:doc-root: https://docs.spring.io -:api-spring-framework: {doc-root}/spring-framework/docs/{spring-version}/javadoc-api/org/springframework :toc: left :toclevels: 4 :tabsize: 4 @@ -21,6 +19,9 @@ Coverage of Spring's integration with AspectJ (currently the richest -- in terms features -- and certainly most mature AOP implementation in the Java enterprise space) is also provided. +AOT processing can be used to optimize your application ahead-of-time. It is typically +used for native image deployment using GraalVM. + include::core/core-beans.adoc[leveloffset=+1] include::core/core-resources.adoc[leveloffset=+1] @@ -39,4 +40,6 @@ include::core/core-databuffer-codec.adoc[leveloffset=+1] include::core/core-spring-jcl.adoc[leveloffset=+1] +include::core/core-aot.adoc[leveloffset=+1] + include::core/core-appendix.adoc[leveloffset=+1] diff --git a/src/docs/asciidoc/core/core-aop-api.adoc b/framework-docs/src/docs/asciidoc/core/core-aop-api.adoc similarity index 96% rename from src/docs/asciidoc/core/core-aop-api.adoc rename to framework-docs/src/docs/asciidoc/core/core-aop-api.adoc index cbfbab9e0bc6..26e91ea39b18 100644 --- a/src/docs/asciidoc/core/core-aop-api.adoc +++ b/framework-docs/src/docs/asciidoc/core/core-aop-api.adoc @@ -25,26 +25,13 @@ target different advice with the same pointcut. The `org.springframework.aop.Pointcut` interface is the central interface, used to target advices to particular classes and methods. The complete interface follows: -[source,java,indent=0,subs="verbatim,quotes",role="primary"] -.Java +[source,java,indent=0,subs="verbatim,quotes"] ---- public interface Pointcut { ClassFilter getClassFilter(); MethodMatcher getMethodMatcher(); - - } ----- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] -.Kotlin ----- - interface Pointcut { - - fun getClassFilter(): ClassFilter - - fun getMethodMatcher(): MethodMatcher - } ---- @@ -56,47 +43,25 @@ The `ClassFilter` interface is used to restrict the pointcut to a given set of t classes. If the `matches()` method always returns true, all target classes are matched. The following listing shows the `ClassFilter` interface definition: -[source,java,indent=0,subs="verbatim,quotes",role="primary"] -.Java +[source,java,indent=0,subs="verbatim,quotes"] ---- public interface ClassFilter { boolean matches(Class clazz); } ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] -.Kotlin ----- - interface ClassFilter { - - fun matches(clazz: Class<*>): Boolean - } ----- The `MethodMatcher` interface is normally more important. The complete interface follows: -[source,java,indent=0,subs="verbatim,quotes",role="primary"] -.Java +[source,java,indent=0,subs="verbatim,quotes"] ---- public interface MethodMatcher { - boolean matches(Method m, Class targetClass); + boolean matches(Method m, Class targetClass); boolean isRuntime(); - boolean matches(Method m, Class targetClass, Object[] args); - } ----- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] -.Kotlin ----- - interface MethodMatcher { - - val isRuntime: Boolean - - fun matches(m: Method, targetClass: Class<*>): Boolean - - fun matches(m: Method, targetClass: Class<*>, args: Array): Boolean + boolean matches(Method m, Class targetClass, Object... args); } ---- @@ -335,22 +300,13 @@ Spring is compliant with the AOP `Alliance` interface for around advice that use interception. Classes that implement `MethodInterceptor` and that implement around advice should also implement the following interface: -[source,java,indent=0,subs="verbatim,quotes",role="primary"] -.Java +[source,java,indent=0,subs="verbatim,quotes"] ---- public interface MethodInterceptor extends Interceptor { Object invoke(MethodInvocation invocation) throws Throwable; } ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] -.Kotlin ----- - interface MethodInterceptor : Interceptor { - - fun invoke(invocation: MethodInvocation) : Any - } ----- The `MethodInvocation` argument to the `invoke()` method exposes the method being invoked, the target join point, the AOP proxy, and the arguments to the method. The @@ -413,22 +369,13 @@ interceptor chain. The following listing shows the `MethodBeforeAdvice` interface: -[source,java,indent=0,subs="verbatim,quotes",role="primary"] -.Java +[source,java,indent=0,subs="verbatim,quotes"] ---- public interface MethodBeforeAdvice extends BeforeAdvice { void before(Method m, Object[] args, Object target) throws Throwable; } ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] -.Kotlin ----- -interface MethodBeforeAdvice : BeforeAdvice { - - fun before(m: Method, args: Array, target: Any) -} ----- (Spring's API design would allow for field before advice, although the usual objects apply to field interception and it is @@ -591,8 +538,7 @@ TIP: Throws advice can be used with any pointcut. An after returning advice in Spring must implement the `org.springframework.aop.AfterReturningAdvice` interface, which the following listing shows: -[source,java,indent=0,subs="verbatim,quotes",role="primary"] -.Java +[source,java,indent=0,subs="verbatim,quotes"] ---- public interface AfterReturningAdvice extends Advice { @@ -600,14 +546,6 @@ An after returning advice in Spring must implement the throws Throwable; } ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] -.Kotlin ----- - interface AfterReturningAdvice : Advice { - - fun afterReturning(returnValue: Any, m: Method, args: Array, target: Any) - } ----- An after returning advice has access to the return value (which it cannot modify), the invoked method, the method's arguments, and the target. @@ -660,22 +598,13 @@ Spring treats introduction advice as a special kind of interception advice. Introduction requires an `IntroductionAdvisor` and an `IntroductionInterceptor` that implement the following interface: -[source,java,indent=0,subs="verbatim,quotes",role="primary"] -.Java +[source,java,indent=0,subs="verbatim,quotes"] ---- public interface IntroductionInterceptor extends MethodInterceptor { boolean implementsInterface(Class intf); } ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] -.Kotlin ----- - interface IntroductionInterceptor : MethodInterceptor { - - fun implementsInterface(intf: Class<*>): Boolean - } ----- The `invoke()` method inherited from the AOP Alliance `MethodInterceptor` interface must implement the introduction. That is, if the invoked method is on an introduced @@ -686,8 +615,7 @@ Introduction advice cannot be used with any pointcut, as it applies only at the rather than the method, level. You can only use introduction advice with the `IntroductionAdvisor`, which has the following methods: -[source,java,indent=0,subs="verbatim,quotes",role="primary"] -.Java +[source,java,indent=0,subs="verbatim,quotes"] ---- public interface IntroductionAdvisor extends Advisor, IntroductionInfo { @@ -701,22 +629,6 @@ rather than the method, level. You can only use introduction advice with the Class[] getInterfaces(); } ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] -.Kotlin ----- - interface IntroductionAdvisor : Advisor, IntroductionInfo { - - val classFilter: ClassFilter - - @Throws(IllegalArgumentException::class) - fun validateInterfaces() - } - - interface IntroductionInfo { - - val interfaces: Array> - } ----- There is no `MethodMatcher` and, hence, no `Pointcut` associated with introduction advice. Only class filtering is logical. @@ -1725,7 +1637,7 @@ The following listing shows an example configuration: Note that the target object (`businessObjectTarget` in the preceding example) must be a prototype. This lets the `PoolingTargetSource` implementation create new instances -of the target to grow the pool as necessary. See the {api-spring-framework}aop/target/AbstractPoolingTargetSource.html[javadoc of +of the target to grow the pool as necessary. See the {api-spring-framework}/aop/target/AbstractPoolingTargetSource.html[javadoc of `AbstractPoolingTargetSource`] and the concrete subclass you wish to use for information about its properties. `maxSize` is the most basic and is always guaranteed to be present. diff --git a/src/docs/asciidoc/core/core-aop.adoc b/framework-docs/src/docs/asciidoc/core/core-aop.adoc similarity index 93% rename from src/docs/asciidoc/core/core-aop.adoc rename to framework-docs/src/docs/asciidoc/core/core-aop.adoc index ffc3be35770a..61d2135f7ab5 100644 --- a/src/docs/asciidoc/core/core-aop.adoc +++ b/framework-docs/src/docs/asciidoc/core/core-aop.adoc @@ -262,8 +262,8 @@ element, as the following example shows: ---- This assumes that you use schema support as described in -<>. -See <> for how to +<>. +See <> for how to import the tags in the `aop` namespace. @@ -316,17 +316,17 @@ other class. They can also contain pointcut, advice, and introduction (inter-typ declarations. .Autodetecting aspects through component scanning -NOTE: You can register aspect classes as regular beans in your Spring XML configuration or -autodetect them through classpath scanning -- the same as any other Spring-managed bean. -However, note that the `@Aspect` annotation is not sufficient for autodetection in -the classpath. For that purpose, you need to add a separate `@Component` annotation -(or, alternatively, a custom stereotype annotation that qualifies, as per the rules of -Spring's component scanner). +NOTE: You can register aspect classes as regular beans in your Spring XML configuration, +via `@Bean` methods in `@Configuration` classes, or have Spring autodetect them through +classpath scanning -- the same as any other Spring-managed bean. However, note that the +`@Aspect` annotation is not sufficient for autodetection in the classpath. For that +purpose, you need to add a separate `@Component` annotation (or, alternatively, a custom +stereotype annotation that qualifies, as per the rules of Spring's component scanner). .Advising aspects with other aspects? -NOTE: In Spring AOP, aspects themselves cannot be the targets of advice -from other aspects. The `@Aspect` annotation on a class marks it as an aspect and, -hence, excludes it from auto-proxying. +NOTE: In Spring AOP, aspects themselves cannot be the targets of advice from other +aspects. The `@Aspect` annotation on a class marks it as an aspect and, hence, excludes +it from auto-proxying. @@ -361,12 +361,12 @@ matches the execution of any method named `transfer`: ---- The pointcut expression that forms the value of the `@Pointcut` annotation is a regular -AspectJ 5 pointcut expression. For a full discussion of AspectJ's pointcut language, see +AspectJ pointcut expression. For a full discussion of AspectJ's pointcut language, see the https://www.eclipse.org/aspectj/doc/released/progguide/index.html[AspectJ Programming Guide] (and, for extensions, the https://www.eclipse.org/aspectj/doc/released/adk15notebook/index.html[AspectJ 5 Developer's Notebook]) or one of the books on AspectJ (such as _Eclipse AspectJ_, by Colyer -et. al., or _AspectJ in Action_, by Ramnivas Laddad). +et al., or _AspectJ in Action_, by Ramnivas Laddad). [[aop-pointcuts-designators]] @@ -770,7 +770,7 @@ sub-packages: this(com.xyz.service.AccountService) ---- + -NOTE: 'this' is more commonly used in a binding form. See the section on <> +NOTE: `this` is more commonly used in a binding form. See the section on <> for how to make the proxy object available in the advice body. * Any join point (method execution only in Spring AOP) where the target object @@ -781,7 +781,7 @@ implements the `AccountService` interface: target(com.xyz.service.AccountService) ---- + -NOTE: 'target' is more commonly used in a binding form. See the <> section +NOTE: `target` is more commonly used in a binding form. See the <> section for how to make the target object available in the advice body. * Any join point (method execution only in Spring AOP) that takes a single parameter @@ -792,7 +792,7 @@ and where the argument passed at runtime is `Serializable`: args(java.io.Serializable) ---- + -NOTE: 'args' is more commonly used in a binding form. See the <> section +NOTE: `args` is more commonly used in a binding form. See the <> section for how to make the method arguments available in the advice body. + Note that the pointcut given in this example is different from `execution(* @@ -808,7 +808,7 @@ parameter of type `Serializable`. @target(org.springframework.transaction.annotation.Transactional) ---- + -NOTE: You can also use '@target' in a binding form. See the <> section for +NOTE: You can also use `@target` in a binding form. See the <> section for how to make the annotation object available in the advice body. * Any join point (method execution only in Spring AOP) where the declared type of the @@ -819,7 +819,7 @@ target object has an `@Transactional` annotation: @within(org.springframework.transaction.annotation.Transactional) ---- + -NOTE: You can also use '@within' in a binding form. See the <> section for +NOTE: You can also use `@within` in a binding form. See the <> section for how to make the annotation object available in the advice body. * Any join point (method execution only in Spring AOP) where the executing method has an @@ -830,7 +830,7 @@ how to make the annotation object available in the advice body. @annotation(org.springframework.transaction.annotation.Transactional) ---- + -NOTE: You can also use '@annotation' in a binding form. See the <> section +NOTE: You can also use `@annotation` in a binding form. See the <> section for how to make the annotation object available in the advice body. * Any join point (method execution only in Spring AOP) which takes a single parameter, @@ -841,7 +841,7 @@ and where the runtime type of the argument passed has the `@Classified` annotati @args(com.xyz.security.Classified) ---- + -NOTE: You can also use '@args' in a binding form. See the <> section +NOTE: You can also use `@args` in a binding form. See the <> section how to make the annotation object(s) available in the advice body. * Any join point (method execution only in Spring AOP) on a Spring bean named @@ -893,7 +893,7 @@ scoping). You can include the contextual designators to match based on join point context or bind that context for use in the advice. Supplying only a kinded designator or only a contextual designator works but could affect weaving performance (time and memory used), due to extra processing and analysis. Scoping -designators are very fast to match, and using them usage means AspectJ can very quickly +designators are very fast to match, and using them means AspectJ can very quickly dismiss groups of join points that should not be further processed. A good pointcut should always include one if possible. @@ -925,7 +925,6 @@ You can declare before advice in an aspect by using the `@Before` annotation: public void doAccessCheck() { // ... } - } ---- [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] @@ -941,7 +940,6 @@ You can declare before advice in an aspect by using the `@Before` annotation: fun doAccessCheck() { // ... } - } ---- @@ -961,7 +959,6 @@ following example: public void doAccessCheck() { // ... } - } ---- [source,kotlin,indent=0,subs="verbatim",role="secondary"] @@ -977,7 +974,6 @@ following example: fun doAccessCheck() { // ... } - } ---- @@ -985,8 +981,8 @@ following example: [[aop-advice-after-returning]] ==== After Returning Advice -After returning advice runs when a matched method execution returns normally. You can -declare it by using the `@AfterReturning` annotation: +After returning advice runs when a matched method execution returns normally. +You can declare it by using the `@AfterReturning` annotation: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1001,7 +997,6 @@ declare it by using the `@AfterReturning` annotation: public void doAccessCheck() { // ... } - } ---- [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] @@ -1017,16 +1012,16 @@ declare it by using the `@AfterReturning` annotation: fun doAccessCheck() { // ... } - + } ---- -NOTE: You can have multiple advice declarations (and other members -as well), all inside the same aspect. We show only a single advice declaration in -these examples to focus the effect of each one. +NOTE: You can have multiple advice declarations (and other members as well), +all inside the same aspect. We show only a single advice declaration in these +examples to focus the effect of each one. -Sometimes, you need access in the advice body to the actual value that was returned. You -can use the form of `@AfterReturning` that binds the return value to get that access, as -the following example shows: +Sometimes, you need access in the advice body to the actual value that was returned. +You can use the form of `@AfterReturning` that binds the return value to get that +access, as the following example shows: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1043,7 +1038,6 @@ the following example shows: public void doAccessCheck(Object retVal) { // ... } - } ---- [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] @@ -1061,15 +1055,14 @@ the following example shows: fun doAccessCheck(retVal: Any) { // ... } - } ---- -The name used in the `returning` attribute must correspond to the name of a parameter in -the advice method. When a method execution returns, the return value is passed to +The name used in the `returning` attribute must correspond to the name of a parameter +in the advice method. When a method execution returns, the return value is passed to the advice method as the corresponding argument value. A `returning` clause also -restricts matching to only those method executions that return a value of the specified -type (in this case, `Object`, which matches any return value). +restricts matching to only those method executions that return a value of the +specified type (in this case, `Object`, which matches any return value). Please note that it is not possible to return a totally different reference when using after returning advice. @@ -1079,8 +1072,8 @@ using after returning advice. ==== After Throwing Advice After throwing advice runs when a matched method execution exits by throwing an -exception. You can declare it by using the `@AfterThrowing` annotation, as the following -example shows: +exception. You can declare it by using the `@AfterThrowing` annotation, as the +following example shows: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1095,7 +1088,6 @@ example shows: public void doRecoveryActions() { // ... } - } ---- [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] @@ -1111,15 +1103,14 @@ example shows: fun doRecoveryActions() { // ... } - } ---- -Often, you want the advice to run only when exceptions of a given type are thrown, and -you also often need access to the thrown exception in the advice body. You can use the -`throwing` attribute to both restrict matching (if desired -- use `Throwable` as the -exception type otherwise) and bind the thrown exception to an advice parameter. The -following example shows how to do so: +Often, you want the advice to run only when exceptions of a given type are thrown, +and you also often need access to the thrown exception in the advice body. You can +use the `throwing` attribute to both restrict matching (if desired -- use `Throwable` +as the exception type otherwise) and bind the thrown exception to an advice parameter. +The following example shows how to do so: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1136,7 +1127,6 @@ following example shows how to do so: public void doRecoveryActions(DataAccessException ex) { // ... } - } ---- [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] @@ -1154,15 +1144,22 @@ following example shows how to do so: fun doRecoveryActions(ex: DataAccessException) { // ... } - } ---- The name used in the `throwing` attribute must correspond to the name of a parameter in the advice method. When a method execution exits by throwing an exception, the exception -is passed to the advice method as the corresponding argument value. A `throwing` -clause also restricts matching to only those method executions that throw an exception -of the specified type ( `DataAccessException`, in this case). +is passed to the advice method as the corresponding argument value. A `throwing` clause +also restricts matching to only those method executions that throw an exception of the +specified type (`DataAccessException`, in this case). + +[NOTE] +==== +Note that `@AfterThrowing` does not indicate a general exception handling callback. +Specifically, an `@AfterThrowing` advice method is only supposed to receive exceptions +from the join point (user-declared target method) itself but not from an accompanying +`@After`/`@AfterReturning` method. +==== [[aop-advice-after-finally]] @@ -1170,8 +1167,8 @@ of the specified type ( `DataAccessException`, in this case). After (finally) advice runs when a matched method execution exits. It is declared by using the `@After` annotation. After advice must be prepared to handle both normal and -exception return conditions. It is typically used for releasing resources and similar purposes. -The following example shows how to use after finally advice: +exception return conditions. It is typically used for releasing resources and similar +purposes. The following example shows how to use after finally advice: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1186,7 +1183,6 @@ The following example shows how to use after finally advice: public void doReleaseLock() { // ... } - } ---- [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] @@ -1202,41 +1198,75 @@ The following example shows how to use after finally advice: fun doReleaseLock() { // ... } - } ---- +[NOTE] +==== +Note that `@After` advice in AspectJ is defined as "after finally advice", analogous +to a finally block in a try-catch statement. It will be invoked for any outcome, +normal return or exception thrown from the join point (user-declared target method), +in contrast to `@AfterReturning` which only applies to successful normal returns. +==== + [[aop-ataspectj-around-advice]] ==== Around Advice -The last kind of advice is around advice. Around advice runs "`around`" a matched method's -execution. It has the opportunity to do work both before and after the method runs -and to determine when, how, and even if the method actually gets to run at all. +The last kind of advice is _around_ advice. Around advice runs "around" a matched +method's execution. It has the opportunity to do work both before and after the method +runs and to determine when, how, and even if the method actually gets to run at all. Around advice is often used if you need to share state before and after a method -execution in a thread-safe manner (starting and stopping a timer, for example). Always -use the least powerful form of advice that meets your requirements (that is, do not use -around advice if before advice would do). +execution in a thread-safe manner – for example, starting and stopping a timer. + +[TIP] +==== +Always use the least powerful form of advice that meets your requirements. -Around advice is declared by using the `@Around` annotation. The first parameter of the -advice method must be of type `ProceedingJoinPoint`. Within the body of the advice, -calling `proceed()` on the `ProceedingJoinPoint` causes the underlying method to -run. The `proceed` method can also pass in an `Object[]`. The values -in the array are used as the arguments to the method execution when it proceeds. +For example, do not use _around_ advice if _before_ advice is sufficient for your needs. +==== -NOTE: The behavior of `proceed` when called with an `Object[]` is a little different than the +Around advice is declared by annotating a method with the `@Around` annotation. The +method should declare `Object` as its return type, and the first parameter of the method +must be of type `ProceedingJoinPoint`. Within the body of the advice method, you must +invoke `proceed()` on the `ProceedingJoinPoint` in order for the underlying method to +run. Invoking `proceed()` without arguments will result in the caller's original +arguments being supplied to the underlying method when it is invoked. For advanced use +cases, there is an overloaded variant of the `proceed()` method which accepts an array of +arguments (`Object[]`). The values in the array will be used as the arguments to the +underlying method when it is invoked. + +[NOTE] +==== +The behavior of `proceed` when called with an `Object[]` is a little different than the behavior of `proceed` for around advice compiled by the AspectJ compiler. For around advice written using the traditional AspectJ language, the number of arguments passed to `proceed` must match the number of arguments passed to the around advice (not the number of arguments taken by the underlying join point), and the value passed to proceed in a -given argument position supplants the original value at the join point for the entity -the value was bound to (do not worry if this does not make sense right now). The approach -taken by Spring is simpler and a better match to its proxy-based, execution-only -semantics. You only need to be aware of this difference if you compile @AspectJ -aspects written for Spring and use `proceed` with arguments with the AspectJ compiler -and weaver. There is a way to write such aspects that is 100% compatible across both -Spring AOP and AspectJ, and this is discussed in the -<>. +given argument position supplants the original value at the join point for the entity the +value was bound to (do not worry if this does not make sense right now). + +The approach taken by Spring is simpler and a better match to its proxy-based, +execution-only semantics. You only need to be aware of this difference if you compile +`@AspectJ` aspects written for Spring and use `proceed` with arguments with the AspectJ +compiler and weaver. There is a way to write such aspects that is 100% compatible across +both Spring AOP and AspectJ, and this is discussed in the +<>. +==== + +The value returned by the around advice is the return value seen by the caller of the +method. For example, a simple caching aspect could return a value from a cache if it has +one or invoke `proceed()` (and return that value) if it does not. Note that `proceed` +may be invoked once, many times, or not at all within the body of the around advice. All +of these are legal. + +WARNING: If you declare the return type of your around advice method as `void`, `null` +will always be returned to the caller, effectively ignoring the result of any invocation +of `proceed()`. It is therefore recommended that an around advice method declare a return +type of `Object`. The advice method should typically return the value returned from an +invocation of `proceed()`, even if the underlying method has a `void` return type. +However, the advice may optionally return a cached value, a wrapped value, or some other +value depending on the use case. The following example shows how to use around advice: @@ -1257,7 +1287,6 @@ The following example shows how to use around advice: // stop stopwatch return retVal; } - } ---- [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] @@ -1277,34 +1306,26 @@ The following example shows how to use around advice: // stop stopwatch return retVal } - } ---- -The value returned by the around advice is the return value seen by the caller of -the method. For example, a simple caching aspect could return a value from a cache if it -has one and invoke `proceed()` if it does not. Note that `proceed` may be invoked once, -many times, or not at all within the body of the around advice. All of these are -legal. - - [[aop-ataspectj-advice-params]] ==== Advice Parameters -Spring offers fully typed advice, meaning that you declare the parameters you need -in the advice signature (as we saw earlier for the returning and throwing examples) rather -than work with `Object[]` arrays all the time. We see how to make argument and other -contextual values available to the advice body later in this section. First, we take a look at -how to write generic advice that can find out about the method the advice is currently -advising. +Spring offers fully typed advice, meaning that you declare the parameters you need in the +advice signature (as we saw earlier for the returning and throwing examples) rather than +work with `Object[]` arrays all the time. We see how to make argument and other contextual +values available to the advice body later in this section. First, we take a look at how to +write generic advice that can find out about the method the advice is currently advising. [[aop-ataspectj-advice-params-the-joinpoint]] ===== Access to the Current `JoinPoint` Any advice method may declare, as its first parameter, a parameter of type -`org.aspectj.lang.JoinPoint` (note that around advice is required to declare -a first parameter of type `ProceedingJoinPoint`, which is a subclass of `JoinPoint`. The -`JoinPoint` interface provides a number of useful methods: +`org.aspectj.lang.JoinPoint`. Note that around advice is required to declare a first +parameter of type `ProceedingJoinPoint`, which is a subclass of `JoinPoint`. + +The `JoinPoint` interface provides a number of useful methods: * `getArgs()`: Returns the method arguments. * `getThis()`: Returns the proxy object. @@ -1320,9 +1341,9 @@ See the https://www.eclipse.org/aspectj/doc/released/runtime-api/org/aspectj/lan We have already seen how to bind the returned value or exception value (using after returning and after throwing advice). To make argument values available to the advice body, you can use the binding form of `args`. If you use a parameter name in place of a -type name in an args expression, the value of the corresponding argument is -passed as the parameter value when the advice is invoked. An example should make this -clearer. Suppose you want to advise the execution of DAO operations that take an `Account` +type name in an `args` expression, the value of the corresponding argument is passed as +the parameter value when the advice is invoked. An example should make this clearer. +Suppose you want to advise the execution of DAO operations that take an `Account` object as the first parameter, and you need access to the account in the advice body. You could write the following: @@ -1349,7 +1370,7 @@ parameter, and the argument passed to that parameter is an instance of `Account` Second, it makes the actual `Account` object available to the advice through the `account` parameter. -Another way of writing this is to declare a pointcut that "`provides`" the `Account` +Another way of writing this is to declare a pointcut that "provides" the `Account` object value when it matches a join point, and then refer to the named pointcut from the advice. This would look as follows: @@ -1377,13 +1398,12 @@ from the advice. This would look as follows: } ---- -See the AspectJ programming guide for more -details. +See the AspectJ programming guide for more details. -The proxy object ( `this`), target object ( `target`), and annotations ( `@within`, -`@target`, `@annotation`, and `@args`) can all be bound in a similar fashion. The next two -examples show how to match the execution of methods annotated with an -`@Auditable` annotation and extract the audit code: +The proxy object (`this`), target object (`target`), and annotations (`@within`, +`@target`, `@annotation`, and `@args`) can all be bound in a similar fashion. The next +two examples show how to match the execution of methods annotated with an `@Auditable` +annotation and extract the audit code: The first of the two examples shows the definition of the `@Auditable` annotation: @@ -1449,7 +1469,7 @@ you have a generic type like the following: ---- You can restrict interception of method types to certain parameter types by -typing the advice parameter to the parameter type for which you want to intercept the method: +tying the advice parameter to the parameter type for which you want to intercept the method: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1576,18 +1596,18 @@ the `argNames` attribute: } ---- -* Using the `'argNames'` attribute is a little clumsy, so if the `'argNames'` attribute +* Using the `argNames` attribute is a little clumsy, so if the `argNames` attribute has not been specified, Spring AOP looks at the debug information for the class and tries to determine the parameter names from the local variable table. This information is present as long as the classes have been compiled with debug - information ( `'-g:vars'` at a minimum). The consequences of compiling with this flag + information (`-g:vars` at a minimum). The consequences of compiling with this flag on are: (1) your code is slightly easier to understand (reverse engineer), (2) the class file sizes are very slightly bigger (typically inconsequential), (3) the - optimization to remove unused local variables is not applied by your compiler. In + optimization to remove unused local variables is not applied by your compiler. In other words, you should encounter no difficulties by building with this flag on. + -NOTE: If an @AspectJ aspect has been compiled by the AspectJ compiler (ajc) even without the -debug information, you need not add the `argNames` attribute, as the compiler +NOTE: If an @AspectJ aspect has been compiled by the AspectJ compiler (`ajc`) even +without the debug information, you need not add the `argNames` attribute, as the compiler retain the needed information. * If the code has been compiled without the necessary debug information, Spring AOP @@ -1654,20 +1674,23 @@ the higher precedence. [NOTE] ==== +Each of the distinct advice types of a particular aspect is conceptually meant to apply +to the join point directly. As a consequence, an `@AfterThrowing` advice method is not +supposed to receive an exception from an accompanying `@After`/`@AfterReturning` method. + As of Spring Framework 5.2.7, advice methods defined in the same `@Aspect` class that need to run at the same join point are assigned precedence based on their advice type in the following order, from highest to lowest precedence: `@Around`, `@Before`, `@After`, -`@AfterReturning`, `@AfterThrowing`. Note, however, that due to the implementation style -in Spring's `AspectJAfterAdvice`, an `@After` advice method will effectively be invoked -after any `@AfterReturning` or `@AfterThrowing` advice methods in the same aspect. +`@AfterReturning`, `@AfterThrowing`. Note, however, that an `@After` advice method will +effectively be invoked after any `@AfterReturning` or `@AfterThrowing` advice methods +in the same aspect, following AspectJ's "after finally advice" semantics for `@After`. When two pieces of the same type of advice (for example, two `@After` advice methods) defined in the same `@Aspect` class both need to run at the same join point, the ordering is undefined (since there is no way to retrieve the source code declaration order through reflection for javac-compiled classes). Consider collapsing such advice methods into one -advice method per join point in each `@Aspect` class or refactor the pieces of advice -into separate `@Aspect` classes that you can order at the aspect level via `Ordered` or -`@Order`. +advice method per join point in each `@Aspect` class or refactor the pieces of advice into +separate `@Aspect` classes that you can order at the aspect level via `Ordered` or `@Order`. ==== @@ -1678,11 +1701,11 @@ Introductions (known as inter-type declarations in AspectJ) enable an aspect to that advised objects implement a given interface, and to provide an implementation of that interface on behalf of those objects. -You can make an introduction by using the `@DeclareParents` annotation. This annotation is used -to declare that matching types have a new parent (hence the name). For example, given an -interface named `UsageTracked` and an implementation of that interface named `DefaultUsageTracked`, -the following aspect declares that all implementors of service interfaces also implement -the `UsageTracked` interface (to expose statistics via JMX for example): +You can make an introduction by using the `@DeclareParents` annotation. This annotation +is used to declare that matching types have a new parent (hence the name). For example, +given an interface named `UsageTracked` and an implementation of that interface named +`DefaultUsageTracked`, the following aspect declares that all implementors of service +interfaces also implement the `UsageTracked` interface (e.g. for statistics via JMX): [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1764,7 +1787,6 @@ annotation. Consider the following example: public void recordServiceUsage() { // ... } - } ---- [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] @@ -1779,7 +1801,6 @@ annotation. Consider the following example: fun recordServiceUsage() { // ... } - } ---- @@ -1854,7 +1875,6 @@ call `proceed` multiple times. The following listing shows the basic aspect impl } while(numAttempts <= this.maxRetries); throw lockFailureException; } - } ---- [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] @@ -1971,8 +1991,8 @@ that syntax and refer the reader to the discussion in the previous section of advice parameters. To use the aop namespace tags described in this section, you need to import the -`spring-aop` schema, as described in <>. See <> +`spring-aop` schema, as described in <>. See <> for how to import the tags in the `aop` namespace. Within your Spring configurations, all aspect and advisor elements must be placed within @@ -2066,7 +2086,6 @@ as the following example shows: expression="execution(* com.xyz.myapp.service.*.*(..))"/> ... - @@ -2088,7 +2107,6 @@ collects the `this` object as the join point context and passes it to the advice ... - @@ -2179,7 +2197,6 @@ a `pointcut` attribute, as follows: method="doAccessCheck"/> ... - ---- @@ -2209,7 +2226,6 @@ shows how to declare it: method="doAccessCheck"/> ... - ---- @@ -2227,7 +2243,6 @@ the return value should be passed, as the following example shows: method="doAccessCheck"/> ... - ---- @@ -2263,7 +2278,6 @@ as the following example shows: method="doRecoveryActions"/> ... - ---- @@ -2281,13 +2295,12 @@ which the exception should be passed as the following example shows: method="doRecoveryActions"/> ... - ---- -The `doRecoveryActions` method must declare a parameter named `dataAccessEx`. The type of -this parameter constrains matching in the same way as described for `@AfterThrowing`. For -example, the method signature may be declared as follows: +The `doRecoveryActions` method must declare a parameter named `dataAccessEx`. +The type of this parameter constrains matching in the same way as described for +`@AfterThrowing`. For example, the method signature may be declared as follows: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -2304,8 +2317,8 @@ example, the method signature may be declared as follows: [[aop-schema-advice-after-finally]] ==== After (Finally) Advice -After (finally) advice runs no matter how a matched method execution exits. You can declare it -by using the `after` element, as the following example shows: +After (finally) advice runs no matter how a matched method execution exits. +You can declare it by using the `after` element, as the following example shows: [source,xml,indent=0,subs="verbatim,quotes"] ---- @@ -2316,7 +2329,6 @@ by using the `after` element, as the following example shows: method="doReleaseLock"/> ... - ---- @@ -2324,20 +2336,30 @@ by using the `after` element, as the following example shows: [[aop-schema-advice-around]] ==== Around Advice -The last kind of advice is around advice. Around advice runs "around" a matched method -execution. It has the opportunity to do work both before and after the method runs -and to determine when, how, and even if the method actually gets to run at all. -Around advice is often used to share state before and after a method -execution in a thread-safe manner (starting and stopping a timer, for example). Always -use the least powerful form of advice that meets your requirements. Do not use around -advice if before advice can do the job. - -You can declare around advice by using the `aop:around` element. The first parameter of the -advice method must be of type `ProceedingJoinPoint`. Within the body of the advice, -calling `proceed()` on the `ProceedingJoinPoint` causes the underlying method to -run. The `proceed` method may also be called with an `Object[]`. The values -in the array are used as the arguments to the method execution when it proceeds. See -<> for notes on calling `proceed` with an `Object[]`. +The last kind of advice is _around_ advice. Around advice runs "around" a matched +method's execution. It has the opportunity to do work both before and after the method +runs and to determine when, how, and even if the method actually gets to run at all. +Around advice is often used if you need to share state before and after a method +execution in a thread-safe manner – for example, starting and stopping a timer. + +[TIP] +==== +Always use the least powerful form of advice that meets your requirements. + +For example, do not use _around_ advice if _before_ advice is sufficient for your needs. +==== + +You can declare around advice by using the `aop:around` element. The advice method should +declare `Object` as its return type, and the first parameter of the method must be of +type `ProceedingJoinPoint`. Within the body of the advice method, you must invoke +`proceed()` on the `ProceedingJoinPoint` in order for the underlying method to run. +Invoking `proceed()` without arguments will result in the caller's original arguments +being supplied to the underlying method when it is invoked. For advanced use cases, there +is an overloaded variant of the `proceed()` method which accepts an array of arguments +(`Object[]`). The values in the array will be used as the arguments to the underlying +method when it is invoked. See <> for notes on calling +`proceed` with an `Object[]`. + The following example shows how to declare around advice in XML: [source,xml,indent=0,subs="verbatim,quotes"] @@ -2349,7 +2371,6 @@ The following example shows how to declare around advice in XML: method="doBasicProfiling"/> ... - ---- @@ -2552,7 +2573,7 @@ With such a Boot class, we would get output similar to the following on standard [literal,subs="verbatim,quotes"] ---- -StopWatch 'Profiling for 'Pengo' and '12'': running time (millis) = 0 +StopWatch 'Profiling for 'Pengo' and '12': running time (millis) = 0 ----------------------------------------- ms % Task name ----------------------------------------- @@ -2763,7 +2784,6 @@ call `proceed` multiple times. The following listing shows the basic aspect impl } while(numAttempts <= this.maxRetries); throw lockFailureException; } - } ---- [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] @@ -2841,7 +2861,7 @@ The corresponding Spring configuration is as follows: ---- -Notice that, for the time, being we assume that all business services are idempotent. If +Notice that, for the time being, we assume that all business services are idempotent. If this is not the case, we can refine the aspect so that it retries only genuinely idempotent operations, by introducing an `Idempotent` annotation and using the annotation to annotate the implementation of service operations, as the following example shows: @@ -3405,7 +3425,7 @@ definition to configure new `Account` instances. You can also use autowiring to avoid having to specify a dedicated bean definition at all. To have Spring apply autowiring, use the `autowire` property of the `@Configurable` annotation. You can specify either `@Configurable(autowire=Autowire.BY_TYPE)` or -`@Configurable(autowire=Autowire.BY_NAME` for autowiring by type or by name, +`@Configurable(autowire=Autowire.BY_NAME)` for autowiring by type or by name, respectively. As an alternative, it is preferable to specify explicit, annotation-driven dependency injection for your `@Configurable` beans through `@Autowired` or `@Inject` at the field or method level (see <> for further details). @@ -3482,7 +3502,7 @@ use Java-based configuration, you can add `@EnableSpringConfigured` to any ---- If you prefer XML based configuration, the Spring -<> +<> defines a convenient `context:spring-configured` element, which you can use as follows: [source,xml,indent=0,subs="verbatim,quotes"] @@ -3551,7 +3571,7 @@ with references to beans defined in the child (servlet-specific) contexts by usi When deploying multiple web applications within the same container, ensure that each web application loads the types in `spring-aspects.jar` by using its own classloader -(for example, by placing `spring-aspects.jar` in `'WEB-INF/lib'`). If `spring-aspects.jar` +(for example, by placing `spring-aspects.jar` in `WEB-INF/lib`). If `spring-aspects.jar` is added only to the container-wide classpath (and hence loaded by the shared parent classloader), all web applications share the same aspect instance (which is probably not what you want). @@ -3581,7 +3601,7 @@ visibility may be annotated, including private methods. Annotating non-public me directly is the only way to get transaction demarcation for the execution of such methods. TIP: Since Spring Framework 4.2, `spring-aspects` provides a similar aspect that offers the -exact same features for the standard `javax.transaction.Transactional` annotation. Check +exact same features for the standard `jakarta.transaction.Transactional` annotation. Check `JtaAnnotationTransactionAspect` for more details. For AspectJ programmers who want to use the Spring configuration and transaction diff --git a/framework-docs/src/docs/asciidoc/core/core-aot.adoc b/framework-docs/src/docs/asciidoc/core/core-aot.adoc new file mode 100644 index 000000000000..c6bf1fa9dc1f --- /dev/null +++ b/framework-docs/src/docs/asciidoc/core/core-aot.adoc @@ -0,0 +1,292 @@ +[[aot]] += Ahead of Time Optimizations + +This chapter covers Spring's Ahead of Time (AOT) optimizations. + +For AOT support specific to integration tests, see <>. + +[[aot-introduction]] +== Introduction to Ahead of Time Optimizations + +Spring's support for AOT optimizations is meant to inspect an `ApplicationContext` at build time and apply decisions and discovery logic that usually happens at runtime. +Doing so allows building an application startup arrangement that is more straightforward and focused on a fixed set of features based mainly on the classpath and the `Environment`. + +Applying such optimizations early implies the following restrictions: + +* The classpath is fixed and fully defined at build time. +* The beans defined in your application cannot change at runtime, meaning: +** `@Profile`, in particular profile-specific configuration needs to be chosen at build time. +** Environment properties that impact the presence of a bean (`@Conditional`) are only considered at build time. + +When these restrictions are in place, it becomes possible to perform ahead-of-time processing at build time and generate additional assets. +A Spring AOT processed application typically generates: + +* Java source code +* Bytecode (usually for dynamic proxies) +* {api-spring-framework}/aot/hint/RuntimeHints.html[`RuntimeHints`] for the use of reflection, resource loading, serialization, and JDK proxies. + +NOTE: At the moment, AOT is focused on allowing Spring applications to be deployed as native images using GraalVM. +We intend to offer more JVM-based use cases in future generations. + +[[aot-basics]] +== AOT engine overview + +The entry point of the AOT engine for processing an `ApplicationContext` arrangement is `ApplicationContextAotGenerator`. It takes care of the following steps, based on a `GenericApplicationContext` that represents the application to optimize and a {api-spring-framework}/aot/generate/GenerationContext.html[`GenerationContext`]: + +* Refresh an `ApplicationContext` for AOT processing. Contrary to a traditional refresh, this version only creates bean definitions, not bean instances. +* Invoke the available `BeanFactoryInitializationAotProcessor` implementations and apply their contributions against the `GenerationContext`. +For instance, a core implementation iterates over all candidate bean definitions and generates the necessary code to restore the state of the `BeanFactory`. + +Once this process completes, the `GenerationContext` has been updated with the generated code, resources, and classes that are necessary for the application to run. +The `RuntimeHints` instance can also be used to generate the relevant GraalVM native image configuration files. + +`ApplicationContextAotGenerator#processAheadOfTime` returns the class name of the `ApplicationContextInitializer` entry point that allows the context to be started with AOT optimizations. + +Those steps are covered in more details in the sections below. + +[[aot-refresh]] +== Refresh for AOT Processing + +Refresh for AOT processing is supported on all `GenericApplicationContext` implementations. +An application context is created with any number of entry points, usually in the form of `@Configuration`-annotated classes. + +Let's look at a basic example: + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- +@Configuration(proxyBeanMethods=false) +@ComponentScan +@Import({DataSourceConfiguration.class, ContainerConfiguration.class}) +public class MyApplication { +} +---- + +Starting this application with the regular runtime involves a number of steps including classpath scanning, configuration class parsing, bean instantiation, and lifecycle callback handling. +Refresh for AOT processing only applies a subset of what happens with a <>. +AOT processing can be triggered as follows: + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- +GenericApplicationContext applicationContext = new AnnotatedConfigApplicationContext(); +context.register(MyApplication.class); +context.refreshForAotProcessing(); +---- + +In this mode, <> are invoked as usual. +This includes configuration class parsing, import selectors, classpath scanning, etc. +Such steps make sure that the `BeanRegistry` contains the relevant bean definitions for the application. +If bean definitions are guarded by conditions (such as `@Profile`), these are discarded at this stage. + +Because this mode does not actually create bean instances, `BeanPostProcessor` implementations are not invoked, except for specific variants that are relevant for AOT processing. +These are: + +* `MergedBeanDefinitionPostProcessor` implementations post-process bean definitions to extract additional settings, such as `init` and `destroy` methods. +* `SmartInstantiationAwareBeanPostProcessor` implementations determine a more precise bean type if necessary. +This makes sure to create any proxy that is required at runtime. + +One this part completes, the `BeanFactory` contains the bean definitions that are necessary for the application to run. It does not trigger bean instantiation but allows the AOT engine to inspect the beans that would be created at runtime. + +[[aot-bean-factory-initialization-contributions]] +== Bean Factory Initialization AOT Contributions + +Components that want to participate in this step can implement the {api-spring-framework}/beans/factory/aot/BeanFactoryInitializationAotProcessor.html[`BeanFactoryInitializationAotProcessor`] interface. +Each implementation can return an AOT contribution, based on the state of the bean factory. + +An AOT contribution is a component that contributes generated code that reproduces a particular behavior. +It can also contribute `RuntimeHints` to indicate the need for reflection, resource loading, serialization, or JDK proxies. + +A `BeanFactoryInitializationAotProcessor` implementation can be registered in `META-INF/spring/aot.factories` with a key equal to the fully qualified name of the interface. + +A `BeanFactoryInitializationAotProcessor` can also be implemented directly by a bean. +In this mode, the bean provides an AOT contribution equivalent to the feature it provides with a regular runtime. +Consequently, such a bean is automatically excluded from the AOT-optimized context. + +[NOTE] +==== +If a bean implements the `BeanFactoryInitializationAotProcessor` interface, the bean and **all** of its dependencies will be initialized during AOT processing. +We generally recommend that this interface is only implemented by infrastructure beans such as `BeanFactoryPostProcessor` which have limited dependencies and are already initialized early in the bean factory lifecycle. +If such a bean is registered using an `@Bean` factory method, ensure the method is `static` so that its enclosing `@Configuration` class does not have to be initialized. +==== + + +[[aot-bean-registration-contributions]] +=== Bean Registration AOT Contributions + +A core `BeanFactoryInitializationAotProcessor` implementation is responsible for collecting the necessary contributions for each candidate `BeanDefinition`. +It does so using a dedicated `BeanRegistrationAotProcessor`. + +This interface is used as follows: + +* Implemented by a `BeanPostProcessor` bean, to replace its runtime behavior. +For instance <> implements this interface to generate code that injects members annotated with `@Autowired`. +* Implemented by a type registered in `META-INF/spring/aot.factories` with a key equal to the fully qualified name of the interface. +Typically used when the bean definition needs to be tuned for specific features of the core framework. + +[NOTE] +==== +If a bean implements the `BeanRegistrationAotProcessor` interface, the bean and **all** of its dependencies will be initialized during AOT processing. +We generally recommend that this interface is only implemented by infrastructure beans such as `BeanFactoryPostProcessor` which have limited dependencies and are already initialized early in the bean factory lifecycle. +If such a bean is registered using an `@Bean` factory method, ensure the method is `static` so that its enclosing `@Configuration` class does not have to be initialized. +==== + +If no `BeanRegistrationAotProcessor` handles a particular registered bean, a default implementation processes it. +This is the default behavior, since tuning the generated code for a bean definition should be restricted to corner cases. + +Taking our previous example, let's assume that `DataSourceConfiguration` is as follows: + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- + @Configuration(proxyBeanMethods = false) + public class DataSourceConfiguration { + + @Bean + public SimpleDataSource dataSource() { + return new SimpleDataSource(); + } + + } +---- + +Since there isn't any particular condition on this class, `dataSourceConfiguration` and `dataSource` are identified as candidates. +The AOT engine will convert the configuration class above to code similar to the following: + +[source,java,indent=0,role="primary"] +.Java +---- + /** + * Bean definitions for {@link DataSourceConfiguration} + */ + public class DataSourceConfiguration__BeanDefinitions { + /** + * Get the bean definition for 'dataSourceConfiguration' + */ + public static BeanDefinition getDataSourceConfigurationBeanDefinition() { + Class beanType = DataSourceConfiguration.class; + RootBeanDefinition beanDefinition = new RootBeanDefinition(beanType); + beanDefinition.setInstanceSupplier(DataSourceConfiguration::new); + return beanDefinition; + } + + /** + * Get the bean instance supplier for 'dataSource'. + */ + private static BeanInstanceSupplier getDataSourceInstanceSupplier() { + return BeanInstanceSupplier.forFactoryMethod(DataSourceConfiguration.class, "dataSource") + .withGenerator((registeredBean) -> registeredBean.getBeanFactory().getBean(DataSourceConfiguration.class).dataSource()); + } + + /** + * Get the bean definition for 'dataSource' + */ + public static BeanDefinition getDataSourceBeanDefinition() { + Class beanType = SimpleDataSource.class; + RootBeanDefinition beanDefinition = new RootBeanDefinition(beanType); + beanDefinition.setInstanceSupplier(getDataSourceInstanceSupplier()); + return beanDefinition; + } + } +---- + +NOTE: The exact code generated may differ depending on the exact nature of your bean definitions. + +The generated code above creates bean definitions equivalent to the `@Configuration` class, but in a direct way and without the use of reflection if at all possible. +There is a bean definition for `dataSourceConfiguration` and one for `dataSourceBean`. +When a `datasource` instance is required, a `BeanInstanceSupplier` is called. +This supplier invokes the `dataSource()` method on the `dataSourceConfiguration` bean. + + +[[aot-hints]] +== Runtime Hints + +Running an application as a native image requires additional information compared to a regular JVM runtime. +For instance, GraalVM needs to know ahead of time if a component uses reflection. +Similarly, classpath resources are not shipped in a native image unless specified explicitly. +Consequently, if the application needs to load a resource, it must be referenced from the corresponding GraalVM native image configuration file. + +The {api-spring-framework}/aot/hint/RuntimeHints.html[`RuntimeHints`] API collects the need for reflection, resource loading, serialization, and JDK proxies at runtime. +The following example makes sure that `config/app.properties` can be loaded from the classpath at runtime within a native image: + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- + runtimeHints.resources().registerPattern("config/app.properties"); +---- + +A number of contracts are handled automatically during AOT processing. +For instance, the return type of a `@Controller` method is inspected, and relevant reflection hints are added if Spring detects that the type should be serialized (typically to JSON). + +For cases that the core container cannot infer, you can register such hints programmatically. +A number of convenient annotations are also provided for common use cases. + + +[[aot-hints-import-runtime-hints]] +=== `@ImportRuntimeHints` + +`RuntimeHintsRegistrar` implementations allow you to get a callback to the `RuntimeHints` instance managed by the AOT engine. +Implementations of this interface can be registered using `@ImportRuntimeHints` on any Spring bean or `@Bean` factory method. +`RuntimeHintsRegistrar` implementations are detected and invoked at build time. + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- + @Component + @ImportRuntimeHints(MyComponentRuntimeHints.class) + public class MyComponent { + + // ... + + private static class MyComponentRuntimeHints implements RuntimeHintsRegistrar { + + @Override + public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + // ... + } + } + } +---- + +If at all possible, `@ImportRuntimeHints` should be used as close as possible to the component that requires the hints. +This way, if the component is not contributed to the `BeanFactory`, the hints won't be contributed either. + +It is also possible to register an implementation statically by adding an entry in `META-INF/spring/aot.factories` with a key equal to the fully qualified name of the `RuntimeHintsRegistrar` interface. + + +[[aot-hints-reflective]] +=== `@Reflective` + +{api-spring-framework}/aot/hint/annotation/Reflective.html[`@Reflective`] provides an idiomatic way to flag the need for reflection on an annotated element. +For instance, `@EventListener` is meta-annotated with `@Reflective` since the underlying implementation invokes the annotated method using reflection. + +By default, only Spring beans are considered and an invocation hint is registered for the annotated element. +This can be tuned by specifying a custom `ReflectiveProcessor` implementation via the +`@Reflective` annotation. + +Library authors can reuse this annotation for their own purposes. +If components other than Spring beans need to be processed, a `BeanFactoryInitializationAotProcessor` can detect the relevant types and use `ReflectiveRuntimeHintsRegistrar` to process them. + + +[[aot-hints-register-reflection-for-binding]] +=== `@RegisterReflectionForBinding` + +{api-spring-framework}/aot/hint/annotation/RegisterReflectionForBinding.html[`@RegisterReflectionForBinding`] is a specialization of `@Reflective` that registers the need for serializing arbitrary types. +A typical use case is the use of DTOs that the container cannot infer, such as using a web client within a method body. + +`@RegisterReflectionForBinding` can be applied to any Spring bean at the class level, but it can also be applied directly to a method, field, or constructor to better indicate where the hints are actually required. +The following example registers `Account` for serialization. + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- + @Component + public class OrderService { + + @RegisterReflectionForBinding(Account.class) + public void process(Order order) { + // ... + } + + } +---- diff --git a/src/docs/asciidoc/core/core-appendix.adoc b/framework-docs/src/docs/asciidoc/core/core-appendix.adoc similarity index 95% rename from src/docs/asciidoc/core/core-appendix.adoc rename to framework-docs/src/docs/asciidoc/core/core-appendix.adoc index ce31eb55131a..9ecc84772896 100644 --- a/src/docs/asciidoc/core/core-appendix.adoc +++ b/framework-docs/src/docs/asciidoc/core/core-appendix.adoc @@ -1,19 +1,17 @@ -:doc-root: https://docs.spring.io -:api-spring-framework: {doc-root}/spring-framework/docs/{spring-version}/javadoc-api/org/springframework - +[[core.appendix]] = Appendix -[[xsd-schemas]] +[[core.appendix.xsd-schemas]] == XML Schemas This part of the appendix lists XML schemas related to the core container. -[[xsd-schemas-util]] +[[core.appendix.xsd-schemas-util]] === The `util` Schema As the name implies, the `util` tags deal with common, utility configuration @@ -38,7 +36,7 @@ correct schema so that the tags in the `util` namespace are available to you): ---- -[[xsd-schemas-util-constant]] +[[core.appendix.xsd-schemas-util-constant]] ==== Using `` Consider the following bean definition: @@ -71,7 +69,7 @@ developer's intent ("`inject this constant value`"), and it reads better: ---- -[[xsd-schemas-util-frfb]] +[[core.appendix.xsd-schemas-util-frfb]] ===== Setting a Bean Property or Constructor Argument from a Field Value {api-spring-framework}/beans/factory/config/FieldRetrievingFactoryBean.html[`FieldRetrievingFactoryBean`] @@ -128,7 +126,7 @@ The following example enumeration shows how easy injecting an enum value is: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java ---- - package javax.persistence; + package jakarta.persistence; public enum PersistenceContextType { @@ -139,7 +137,7 @@ The following example enumeration shows how easy injecting an enum value is: [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] .Kotlin ---- - package javax.persistence + package jakarta.persistence enum class PersistenceContextType { @@ -183,7 +181,7 @@ Now consider the following setter of type `PersistenceContextType` and the corre ---- -[[xsd-schemas-util-property-path]] +[[core.appendix.xsd-schemas-util-property-path]] ==== Using `` Consider the following example: @@ -230,7 +228,7 @@ The value of the `path` attribute of the `` element follows the `beanName.beanProperty`. In this case, it picks up the `age` property of the bean named `testBean`. The value of that `age` property is `10`. -[[xsd-schemas-util-property-path-dependency]] +[[core.appendix.xsd-schemas-util-property-path-dependency]] ===== Using `` to Set a Bean Property or Constructor Argument `PropertyPathFactoryBean` is a `FactoryBean` that evaluates a property path on a given @@ -305,7 +303,7 @@ for most use cases, but it can sometimes be useful. See the javadoc for more inf this feature. -[[xsd-schemas-util-properties]] +[[core.appendix.xsd-schemas-util-properties]] ==== Using `` Consider the following example: @@ -331,7 +329,7 @@ The following example uses a `util:properties` element to make a more concise re ---- -[[xsd-schemas-util-list]] +[[core.appendix.xsd-schemas-util-list]] ==== Using `` Consider the following example: @@ -386,7 +384,7 @@ following configuration: If no `list-class` attribute is supplied, the container chooses a `List` implementation. -[[xsd-schemas-util-map]] +[[core.appendix.xsd-schemas-util-map]] ==== Using `` Consider the following example: @@ -441,7 +439,7 @@ following configuration: If no `'map-class'` attribute is supplied, the container chooses a `Map` implementation. -[[xsd-schemas-util-set]] +[[core.appendix.xsd-schemas-util-set]] ==== Using `` Consider the following example: @@ -497,7 +495,7 @@ If no `set-class` attribute is supplied, the container chooses a `Set` implement -[[xsd-schemas-aop]] +[[core.appendix.xsd-schemas-aop]] === The `aop` Schema The `aop` tags deal with configuring all things AOP in Spring, including Spring's @@ -527,7 +525,7 @@ are available to you): -[[xsd-schemas-context]] +[[core.appendix.xsd-schemas-context]] === The `context` Schema The `context` tags deal with `ApplicationContext` configuration that relates to plumbing @@ -552,7 +550,7 @@ available to you: ---- -[[xsd-schemas-context-pphc]] +[[core.appendix.xsd-schemas-context-pphc]] ==== Using `` This element activates the replacement of `${...}` placeholders, which are resolved against a @@ -562,14 +560,15 @@ is a convenience mechanism that sets up a <` This element activates the Spring infrastructure to detect annotations in bean classes: * Spring's <> model -* <> and `@Value` -* JSR-250's `@Resource`, `@PostConstruct` and `@PreDestroy` (if available) +* <>, `@Value`, and `@Lookup` +* JSR-250's `@Resource`, `@PostConstruct`, and `@PreDestroy` (if available) +* JAX-WS's `@WebServiceRef` and EJB 3's `@EJB` (if available) * JPA's `@PersistenceContext` and `@PersistenceUnit` (if available) * Spring's <> @@ -584,28 +583,28 @@ element for that purpose. Similarly, Spring's <> as well. -[[xsd-schemas-context-component-scan]] +[[core.appendix.xsd-schemas-context-component-scan]] ==== Using `` This element is detailed in the section on <>. -[[xsd-schemas-context-ltw]] +[[core.appendix.xsd-schemas-context-ltw]] ==== Using `` This element is detailed in the section on <>. -[[xsd-schemas-context-sc]] +[[core.appendix.xsd-schemas-context-sc]] ==== Using `` This element is detailed in the section on <>. -[[xsd-schemas-context-mbe]] +[[core.appendix.xsd-schemas-context-mbe]] ==== Using `` This element is detailed in the section on <>. -[[xsd-schemas-beans]] +[[core.appendix.xsd-schemas-beans]] === The Beans Schema Last but not least, we have the elements in the `beans` schema. These elements @@ -625,7 +624,7 @@ in <` XML definitions. What, if anything, is done with this extra metadata is totally up to your own custom logic (and so is typically only of use if you write your own custom elements as described -in the appendix entitled <>). +in the appendix entitled <>). The following example shows the `` element in the context of a surrounding `` (note that, without any logic to interpret it, the metadata is effectively useless @@ -654,10 +653,10 @@ the bean definition and sets up some caching infrastructure that uses the suppli -[[xml-custom]] +[[core.appendix.xml-custom]] == XML Schema Authoring -[[xsd-custom-introduction]] +[[core.appendix.xsd-custom-introduction]] Since version 2.0, Spring has featured a mechanism for adding schema-based extensions to the basic Spring XML format for defining and configuring beans. This section covers how to write your own custom XML bean definition parsers and @@ -666,23 +665,23 @@ integrate such parsers into the Spring IoC container. To facilitate authoring configuration files that use a schema-aware XML editor, Spring's extensible XML configuration mechanism is based on XML Schema. If you are not familiar with Spring's current XML configuration extensions that come with the standard -Spring distribution, you should first read the appendix entitled <>. +Spring distribution, you should first read the previous section on <>. + To create new XML configuration extensions: -. <> an XML schema to describe your custom element(s). -. <> a custom `NamespaceHandler` implementation. -. <> one or more `BeanDefinitionParser` implementations +. <> an XML schema to describe your custom element(s). +. <> a custom `NamespaceHandler` implementation. +. <> one or more `BeanDefinitionParser` implementations (this is where the real work is done). -. <> your new artifacts with Spring. +. <> your new artifacts with Spring. For a unified example, we create an XML extension (a custom XML element) that lets us configure objects of the type `SimpleDateFormat` (from the `java.text` package). When we are done, we will be able to define bean definitions of type `SimpleDateFormat` as follows: -[source,xml,indent=0] -[subs="verbatim,quotes"] +[source,xml,indent=0,subs="verbatim,quotes"] ---- - + ---- @@ -767,7 +766,7 @@ defined in the enumeration. -[[xsd-custom-namespacehandler]] +[[core.appendix.xsd-custom-namespacehandler]] === Coding a `NamespaceHandler` In addition to the schema, we need a `NamespaceHandler` to parse all elements of @@ -838,7 +837,7 @@ custom element, as we can see in the next step. -[[xsd-custom-parser]] +[[core.appendix.xsd-custom-parser]] === Using `BeanDefinitionParser` A `BeanDefinitionParser` is used if the `NamespaceHandler` encounters an XML @@ -928,7 +927,7 @@ is the extraction and setting of the bean definition's unique identifier. -[[xsd-custom-registration]] +[[core.appendix.xsd-custom-registration]] === Registering the Handler and the Schema The coding is finished. All that remains to be done is to make the Spring XML @@ -940,7 +939,7 @@ XML parsing infrastructure automatically picks up your new extension by consumin these special properties files, the formats of which are detailed in the next two sections. -[[xsd-custom-registration-spring-handlers]] +[[core.appendix.xsd-custom-registration-spring-handlers]] ==== Writing `META-INF/spring.handlers` The properties file called `spring.handlers` contains a mapping of XML Schema URIs to @@ -959,7 +958,7 @@ namespace extension and needs to exactly match exactly the value of the `targetN attribute, as specified in your custom XSD schema. -[[xsd-custom-registration-spring-schemas]] +[[core.appendix.xsd-custom-registration-spring-schemas]] ==== Writing 'META-INF/spring.schemas' The properties file called `spring.schemas` contains a mapping of XML Schema locations @@ -983,7 +982,7 @@ the `NamespaceHandler` and `BeanDefinitionParser` classes on the classpath. -[[xsd-custom-using]] +[[core.appendix.xsd-custom-using]] === Using a Custom Extension in Your Spring XML Configuration Using a custom extension that you yourself have implemented is no different from using @@ -1017,13 +1016,13 @@ in a Spring XML configuration file: -[[xsd-custom-meat]] +[[core.appendix.xsd-custom-meat]] === More Detailed Examples This section presents some more detailed examples of custom XML extensions. -[[xsd-custom-custom-nested]] +[[core.appendix.xsd-custom-custom-nested]] ==== Nesting Custom Elements within Custom Elements The example presented in this section shows how you to write the various artifacts required @@ -1197,7 +1196,7 @@ setter property for the `components` property. The following listing shows such This works nicely, but it exposes a lot of Spring plumbing to the end user. What we are going to do is write a custom extension that hides away all of this Spring plumbing. -If we stick to <>, we start off +If we stick to <>, we start off by creating the XSD schema to define the structure of our custom tag, as the following listing shows: @@ -1224,7 +1223,7 @@ listing shows: ---- -Again following <>, +Again following <>, we then create a custom `NamespaceHandler`: [source,java,indent=0,subs="verbatim,quotes",role="primary"] @@ -1375,7 +1374,7 @@ http\://www.foo.example/schema/component/component.xsd=com/foo/component.xsd ---- -[[xsd-custom-custom-just-attributes]] +[[core.appendix.xsd-custom-custom-just-attributes]] ==== Custom Attributes on "`Normal`" Elements Writing your own custom parser and the associated artifacts is not hard. However, @@ -1612,7 +1611,7 @@ http\://www.foo.example/schema/jcache/jcache.xsd=com/foo/jcache.xsd ---- -[[application-startup-steps]] +[[core.appendix.application-startup-steps]] == Application Startup Steps This part of the appendix lists the existing `StartupSteps` that the core container is instrumented with. @@ -1668,8 +1667,4 @@ its behavior changes. | `spring.context.refresh` | Application context refresh phase. | - -| `spring.event.invoke-listener` -| Invocation of event listeners, if done in the main thread. -| `event` the current application event, `eventType` its type and `listener` the listener processing this event. |=== \ No newline at end of file diff --git a/src/docs/asciidoc/core/core-beans.adoc b/framework-docs/src/docs/asciidoc/core/core-beans.adoc similarity index 96% rename from src/docs/asciidoc/core/core-beans.adoc rename to framework-docs/src/docs/asciidoc/core/core-beans.adoc index df0f1f33f29d..544817bce341 100644 --- a/src/docs/asciidoc/core/core-beans.adoc +++ b/framework-docs/src/docs/asciidoc/core/core-beans.adoc @@ -33,12 +33,12 @@ is a sub-interface of `BeanFactory`. It adds: * Application-layer specific contexts such as the `WebApplicationContext` for use in web applications. -In short, the `BeanFactory` provides the configuration framework and basic -functionality, and the `ApplicationContext` adds more enterprise-specific functionality. -The `ApplicationContext` is a complete superset of the `BeanFactory` and is used -exclusively in this chapter in descriptions of Spring's IoC container. For more -information on using the `BeanFactory` instead of the `ApplicationContext,` see -<>. +In short, the `BeanFactory` provides the configuration framework and basic functionality, +and the `ApplicationContext` adds more enterprise-specific functionality. The +`ApplicationContext` is a complete superset of the `BeanFactory` and is used exclusively +in this chapter in descriptions of Spring's IoC container. For more information on using +the `BeanFactory` instead of the `ApplicationContext,` see the section covering the +<>. In Spring, the objects that form the backbone of your application and that are managed by the Spring IoC container are called beans. A bean is an object that is @@ -135,8 +135,7 @@ dependency-inject domain objects with Spring>>. The following example shows the basic structure of XML-based configuration metadata: -[source,xml,indent=0] -[subs="verbatim,quotes"] +[source,xml,indent=0,subs="verbatim,quotes"] ---- >. -NOTE: In Spring documentation, "`factory bean`" refers to a bean that is configured in -the Spring container and that creates objects through an +NOTE: In Spring documentation, "factory bean" refers to a bean that is configured in the +Spring container and that creates objects through an <> or <> factory method. By contrast, `FactoryBean` (notice the capitalization) refers to a Spring-specific -<> implementation class. +<> implementation class. [[beans-factory-type-determination]] @@ -925,7 +925,7 @@ injection: public class SimpleMovieLister { // the SimpleMovieLister has a dependency on a MovieFinder - private MovieFinder movieFinder; + private final MovieFinder movieFinder; // a constructor so that the Spring container can inject a MovieFinder public SimpleMovieLister(MovieFinder movieFinder) { @@ -945,7 +945,7 @@ injection: ---- Notice that there is nothing special about this class. It is a POJO that -has no dependencies on container specific interfaces, base classes or annotations. +has no dependencies on container specific interfaces, base classes, or annotations. [[beans-factory-ctor-arguments-resolution]] ===== Constructor Argument Resolution @@ -976,10 +976,10 @@ being instantiated. Consider the following class: class ThingOne(thingTwo: ThingTwo, thingThree: ThingThree) ---- -Assuming that `ThingTwo` and `ThingThree` classes are not related by inheritance, no potential -ambiguity exists. Thus, the following configuration works fine, and you do not need to specify -the constructor argument indexes or types explicitly in the `` -element. +Assuming that the `ThingTwo` and `ThingThree` classes are not related by inheritance, no +potential ambiguity exists. Thus, the following configuration works fine, and you do not +need to specify the constructor argument indexes or types explicitly in the +`` element. [source,xml,indent=0,subs="verbatim,quotes"] ---- @@ -1008,10 +1008,10 @@ by type without help. Consider the following class: public class ExampleBean { // Number of years to calculate the Ultimate Answer - private int years; + private final int years; // The Answer to Life, the Universe, and Everything - private String ultimateAnswer; + private final String ultimateAnswer; public ExampleBean(int years, String ultimateAnswer) { this.years = years; @@ -1026,14 +1026,14 @@ by type without help. Consider the following class: class ExampleBean( private val years: Int, // Number of years to calculate the Ultimate Answer - private val ultimateAnswer: String// The Answer to Life, the Universe, and Everything + private val ultimateAnswer: String // The Answer to Life, the Universe, and Everything ) ---- .[[beans-factory-ctor-arguments-type]]Constructor argument type matching -- In the preceding scenario, the container can use type matching with simple types if -you explicitly specify the type of the constructor argument by using the `type` attribute. +you explicitly specify the type of the constructor argument by using the `type` attribute, as the following example shows: [source,xml,indent=0,subs="verbatim,quotes"] @@ -1168,7 +1168,7 @@ load an entire Spring IoC container instance. **** Since you can mix constructor-based and setter-based DI, it is a good rule of thumb to use constructors for mandatory dependencies and setter methods or configuration methods -for optional dependencies. Note that use of the <> +for optional dependencies. Note that use of the <> annotation on a setter method can be used to make the property be a required dependency; however, constructor injection with programmatic validation of arguments is preferable. @@ -1253,7 +1253,8 @@ visibility of some configuration issues is why `ApplicationContext` implementati default pre-instantiate singleton beans. At the cost of some upfront time and memory to create these beans before they are actually needed, you discover configuration issues when the `ApplicationContext` is created, not later. You can still override this default -behavior so that singleton beans initialize lazily, rather than being pre-instantiated. +behavior so that singleton beans initialize lazily, rather than being eagerly +pre-instantiated. If no circular dependencies exist, when one or more collaborating beans are being injected into a dependent bean, each collaborating bean is totally configured prior @@ -1367,7 +1368,7 @@ The following example shows the corresponding `ExampleBean` class: } } ---- -[source,java,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] .Kotlin ---- class ExampleBean( @@ -1426,6 +1427,7 @@ The following example shows the corresponding `ExampleBean` class: // a static factory method; the arguments to this method can be // considered the dependencies of the bean that is returned, // regardless of how those arguments are actually used. + @JvmStatic fun createInstance(anotherBean: AnotherBean, yetAnotherBean: YetAnotherBean, i: Int): ExampleBean { val eb = ExampleBean (...) // some other operations... @@ -1609,7 +1611,7 @@ listings shows how to use the `parent` attribute: ---- - + ---- @@ -1691,7 +1693,7 @@ respectively. The following example shows how to use them: - + @@ -1789,7 +1791,7 @@ a parent collection definition is redundant and does not result in the desired m [[beans-collection-elements-strongly-typed]] ===== Strongly-typed collection -With the introduction of generic types in Java 5, you can use strongly typed collections. +Thanks to Java's support for generic types, you can use strongly typed collections. That is, it is possible to declare a `Collection` type such that it can only contain (for example) `String` elements. If you use Spring to dependency-inject a strongly-typed `Collection` into a bean, you can take advantage of Spring's @@ -1835,7 +1837,7 @@ class SomeClass { When the `accounts` property of the `something` bean is prepared for injection, the generics information about the element type of the strongly-typed `Map` is available by reflection. Thus, Spring's type conversion infrastructure recognizes the -various value elements as being of type `Float`, and the string values (`9.99, 2.75`, and +various value elements as being of type `Float`, and the string values (`9.99`, `2.75`, and `3.99`) are converted into an actual `Float` type. @@ -1898,7 +1900,7 @@ The preceding configuration is equivalent to the following Java code: The p-namespace lets you use the `bean` element's attributes (instead of nested `` elements) to describe your property values collaborating beans, or both. -Spring supports extensible configuration formats <>, +Spring supports extensible configuration formats <>, which are based on an XML Schema definition. The `beans` configuration format discussed in this chapter is defined in an XML Schema document. However, the p-namespace is not defined in an XSD file and exists only in the core of Spring. @@ -1980,8 +1982,7 @@ then nested `constructor-arg` elements. The following example uses the `c:` namespace to do the same thing as the from <>: -[source,xml,indent=0] -[subs="verbatim,quotes"] +[source,xml,indent=0,subs="verbatim,quotes"] ---- > is quite efficient in matching arguments, so unless you really need to, we recommend using the name notation -through-out your configuration. +throughout your configuration. [[beans-compound-property-names]] @@ -2505,13 +2506,13 @@ declared return type of the lookup method: public abstract class CommandManager { public Object process(Object commandState) { - MyCommand command = createCommand(); + Command command = createCommand(); command.setState(commandState); return command.execute(); } @Lookup - protected abstract MyCommand createCommand(); + protected abstract Command createCommand(); } ---- [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] @@ -2606,9 +2607,9 @@ interface provides the new method definition, as the following example shows: .Kotlin ---- /** - * meant to be used to override the existing computeValue(String) - * implementation in MyValueCalculator - */ + * meant to be used to override the existing computeValue(String) + * implementation in MyValueCalculator + */ class ReplacementComputeValue : MethodReplacer { override fun reimplement(obj: Any, method: Method, args: Array): Any { @@ -2808,7 +2809,7 @@ prototype-scoped bean repeatedly at runtime. You cannot dependency-inject a prototype-scoped bean into your singleton bean, because that injection occurs only once, when the Spring container instantiates the singleton bean and resolves and injects its dependencies. If you need a new instance of a prototype bean at -runtime more than once, see <> +runtime more than once, see <>. @@ -2837,12 +2838,11 @@ If you access scoped beans within Spring Web MVC, in effect, within a request th processed by the Spring `DispatcherServlet`, no special setup is necessary. `DispatcherServlet` already exposes all relevant state. -If you use a Servlet 2.5 web container, with requests processed outside of Spring's +If you use a Servlet web container, with requests processed outside of Spring's `DispatcherServlet` (for example, when using JSF or Struts), you need to register the `org.springframework.web.context.request.RequestContextListener` `ServletRequestListener`. -For Servlet 3.0+, this can be done programmatically by using the `WebApplicationInitializer` -interface. Alternatively, or for older containers, add the following declaration to -your web application's `web.xml` file: +This can be done programmatically by using the `WebApplicationInitializer` interface. +Alternatively, add the following declaration to your web application's `web.xml` file: [source,xml,indent=0,subs="verbatim,quotes"] ---- @@ -2971,6 +2971,8 @@ When using annotation-driven components or Java configuration, you can use the ---- + + [[beans-factory-scopes-application]] ==== Application Scope @@ -2986,7 +2988,7 @@ The Spring container creates a new instance of the `AppPreferences` bean by usin `appPreferences` bean is scoped at the `ServletContext` level and stored as a regular `ServletContext` attribute. This is somewhat similar to a Spring singleton bean but differs in two important ways: It is a singleton per `ServletContext`, not per Spring -'ApplicationContext' (for which there may be several in any given web application), +`ApplicationContext` (for which there may be several in any given web application), and it is actually exposed and therefore visible as a `ServletContext` attribute. When using annotation-driven components or Java configuration, you can use the @@ -3014,6 +3016,17 @@ following example shows how to do so: + +[[beans-factory-scopes-websocket]] +==== WebSocket Scope + +WebSocket scope is associated with the lifecycle of a WebSocket session and applies to +STOMP over WebSocket applications, see +<> for more details. + + + + [[beans-factory-scopes-other-injection]] ==== Scoped Beans as Dependencies @@ -3041,7 +3054,7 @@ constructor or setter argument or autowired field) as `ObjectFactory`, which delivers +As an extended variant, you may declare `ObjectProvider` which delivers several additional access variants, including `getIfAvailable` and `getIfUnique`. The JSR-330 variant of this is called `Provider` and is used with a `Provider` @@ -3081,7 +3094,7 @@ understand the "`why`" as well as the "`how`" behind it: To create such a proxy, you insert a child `` element into a scoped bean definition (see <> and -<>). +<>). Why do definitions of beans scoped at the `request`, `session` and custom-scope levels require the `` element? Consider the following singleton bean definition and contrast it with @@ -3102,7 +3115,7 @@ to the HTTP `Session`-scoped bean (`userPreferences`). The salient point here is `userManager` bean is a singleton: it is instantiated exactly once per container, and its dependencies (in this case only one, the `userPreferences` bean) are also injected only once. This means that the `userManager` bean operates only on the -exact same `userPreferences` object (that is, the one with which it was originally injected. +exact same `userPreferences` object (that is, the one with which it was originally injected). This is not the behavior you want when injecting a shorter-lived scoped bean into a longer-lived scoped bean (for example, injecting an HTTP `Session`-scoped collaborating @@ -3352,8 +3365,9 @@ of the scope. You can also do the `Scope` registration declaratively, by using t ---- -NOTE: When you place `` in a `FactoryBean` implementation, it is the factory -bean itself that is scoped, not the object returned from `getObject()`. +NOTE: When you place `` within a `` declaration for a +`FactoryBean` implementation, it is the factory bean itself that is scoped, not the object +returned from `getObject()`. @@ -3409,16 +3423,10 @@ The `org.springframework.beans.factory.InitializingBean` interface lets a bean perform initialization work after the container has set all necessary properties on the bean. The `InitializingBean` interface specifies a single method: -[source,java,indent=0,subs="verbatim,quotes",role="primary"] -.Java +[source,java,indent=0,subs="verbatim,quotes"] ---- void afterPropertiesSet() throws Exception; ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] -.Kotlin ----- - fun afterPropertiesSet() ----- We recommend that you do not use the `InitializingBean` interface, because it unnecessarily couples the code to Spring. Alternatively, we suggest using @@ -3428,8 +3436,7 @@ you can use the `init-method` attribute to specify the name of the method that h no-argument signature. With Java configuration, you can use the `initMethod` attribute of `@Bean`. See <>. Consider the following example: -[source,xml,indent=0] -[subs="verbatim,quotes"] +[source,xml,indent=0,subs="verbatim,quotes"] ---- ---- @@ -3495,16 +3502,10 @@ Implementing the `org.springframework.beans.factory.DisposableBean` interface le bean get a callback when the container that contains it is destroyed. The `DisposableBean` interface specifies a single method: -[source,java,indent=0,subs="verbatim,quotes",role="primary"] -.Java +[source,java,indent=0,subs="verbatim,quotes"] ---- void destroy() throws Exception; ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] -.Kotlin ----- - fun destroy() ----- We recommend that you do not use the `DisposableBean` callback interface, because it unnecessarily couples the code to Spring. Alternatively, we suggest using @@ -3715,8 +3716,7 @@ Destroy methods are called in the same order: The `Lifecycle` interface defines the essential methods for any object that has its own lifecycle requirements (such as starting and stopping some background process): -[source,java,indent=0,subs="verbatim,quotes",role="primary"] -.Java +[source,java,indent=0,subs="verbatim,quotes"] ---- public interface Lifecycle { @@ -3727,18 +3727,6 @@ lifecycle requirements (such as starting and stopping some background process): boolean isRunning(); } ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] -.Kotlin ----- - interface Lifecycle { - - fun start() - - fun stop() - - val isRunning: Boolean - } ----- Any Spring-managed object may implement the `Lifecycle` interface. Then, when the `ApplicationContext` itself receives start and stop signals (for example, for a stop/restart @@ -3746,8 +3734,7 @@ scenario at runtime), it cascades those calls to all `Lifecycle` implementations defined within that context. It does this by delegating to a `LifecycleProcessor`, shown in the following listing: -[source,java,indent=0,subs="verbatim,quotes",role="primary"] -.Java +[source,java,indent=0,subs="verbatim,quotes"] ---- public interface LifecycleProcessor extends Lifecycle { @@ -3756,16 +3743,6 @@ in the following listing: void onClose(); } ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] -.Kotlin ----- - interface LifecycleProcessor : Lifecycle { - - fun onRefresh() - - fun onClose() - } ----- Notice that the `LifecycleProcessor` is itself an extension of the `Lifecycle` interface. It also adds two other methods for reacting to the context being refreshed @@ -3792,27 +3769,17 @@ prior to objects of another type. In those cases, the `SmartLifecycle` interface another option, namely the `getPhase()` method as defined on its super-interface, `Phased`. The following listing shows the definition of the `Phased` interface: -[source,java,indent=0,subs="verbatim,quotes",role="primary"] -.Java +[source,java,indent=0,subs="verbatim,quotes"] ---- public interface Phased { int getPhase(); } ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] -.Kotlin ----- - interface Phased { - - val phase: Int - } ----- The following listing shows the definition of the `SmartLifecycle` interface: -[source,java,indent=0,subs="verbatim,quotes",role="primary"] -.Java +[source,java,indent=0,subs="verbatim,quotes"] ---- public interface SmartLifecycle extends Lifecycle, Phased { @@ -3821,16 +3788,6 @@ The following listing shows the definition of the `SmartLifecycle` interface: void stop(Runnable callback); } ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] -.Kotlin ----- - interface SmartLifecycle : Lifecycle, Phased { - - val isAutoStartup: Boolean - - fun stop(callback: Runnable) - } ----- When starting, the objects with the lowest phase start first. When stopping, the reverse order is followed. Therefore, an object that implements `SmartLifecycle` and @@ -3942,23 +3899,13 @@ When an `ApplicationContext` creates an object instance that implements the with a reference to that `ApplicationContext`. The following listing shows the definition of the `ApplicationContextAware` interface: -[source,java,indent=0,subs="verbatim,quotes",role="primary"] -.Java +[source,java,indent=0,subs="verbatim,quotes"] ---- public interface ApplicationContextAware { void setApplicationContext(ApplicationContext applicationContext) throws BeansException; } ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] -.Kotlin ----- - interface ApplicationContextAware { - - @Throws(BeansException::class) - fun setApplicationContext(applicationContext: ApplicationContext) - } ----- Thus, beans can programmatically manipulate the `ApplicationContext` that created them, through the `ApplicationContext` interface or by casting the reference to a known @@ -3987,26 +3934,16 @@ When an `ApplicationContext` creates a class that implements the a reference to the name defined in its associated object definition. The following listing shows the definition of the BeanNameAware interface: -[source,java,indent=0,subs="verbatim,quotes",role="primary"] -.Java +[source,java,indent=0,subs="verbatim,quotes"] ---- public interface BeanNameAware { void setBeanName(String name) throws BeansException; } ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] -.Kotlin ----- - interface BeanNameAware { - - @Throws(BeansException::class) - fun setBeanName(name: String) - } ----- The callback is invoked after population of normal bean properties but before an -initialization callback such as `InitializingBean`, `afterPropertiesSet`, or a custom +initialization callback such as `InitializingBean.afterPropertiesSet()` or a custom init-method. @@ -4038,7 +3975,7 @@ dependency type. The following table summarizes the most important `Aware` inter | `BeanFactoryAware` | Declaring `BeanFactory`. -| <> +| <> | `BeanNameAware` | Name of the declaring bean. @@ -4220,7 +4157,7 @@ or it may wrap a bean with a proxy. Some Spring AOP infrastructure classes are implemented as bean post-processors in order to provide proxy-wrapping logic. An `ApplicationContext` automatically detects any beans that are defined in the -configuration metadata that implements the `BeanPostProcessor` interface. The +configuration metadata that implement the `BeanPostProcessor` interface. The `ApplicationContext` registers these beans as post-processors so that they can be called later, upon bean creation. Bean post-processors can be deployed in the container in the same fashion as any other beans. @@ -4396,15 +4333,14 @@ org.springframework.scripting.groovy.GroovyMessenger@272961 ---- -[[beans-factory-extension-bpp-examples-rabpp]] -==== Example: The `RequiredAnnotationBeanPostProcessor` +[[beans-factory-extension-bpp-examples-aabpp]] +==== Example: The `AutowiredAnnotationBeanPostProcessor` -Using callback interfaces or annotations in conjunction with a custom -`BeanPostProcessor` implementation is a common means of extending the Spring IoC -container. An example is Spring's `RequiredAnnotationBeanPostProcessor` -- a -`BeanPostProcessor` implementation that ships with the Spring distribution and that ensures -that JavaBean properties on beans that are marked with an (arbitrary) annotation are -actually (configured to be) dependency-injected with a value. +Using callback interfaces or annotations in conjunction with a custom `BeanPostProcessor` +implementation is a common means of extending the Spring IoC container. An example is +Spring's `AutowiredAnnotationBeanPostProcessor` -- a `BeanPostProcessor` implementation +that ships with the Spring distribution and autowires annotated fields, setter methods, +and arbitrary config methods. @@ -4619,22 +4555,22 @@ Java as opposed to a (potentially) verbose amount of XML, you can create your ow `FactoryBean`, write the complex initialization inside that class, and then plug your custom `FactoryBean` into the container. -The `FactoryBean` interface provides three methods: +The `FactoryBean` interface provides three methods: -* `Object getObject()`: Returns an instance of the object this factory creates. The +* `T getObject()`: Returns an instance of the object this factory creates. The instance can possibly be shared, depending on whether this factory returns singletons or prototypes. * `boolean isSingleton()`: Returns `true` if this `FactoryBean` returns singletons or - `false` otherwise. -* `Class getObjectType()`: Returns the object type returned by the `getObject()` method + `false` otherwise. The default implementation of this method returns `true`. +* `Class getObjectType()`: Returns the object type returned by the `getObject()` method or `null` if the type is not known in advance. -The `FactoryBean` concept and interface is used in a number of places within the Spring +The `FactoryBean` concept and interface are used in a number of places within the Spring Framework. More than 50 implementations of the `FactoryBean` interface ship with Spring itself. When you need to ask a container for an actual `FactoryBean` instance itself instead of -the bean it produces, preface the bean's `id` with the ampersand symbol (`&`) when +the bean it produces, prefix the bean's `id` with the ampersand symbol (`&`) when calling the `getBean()` method of the `ApplicationContext`. So, for a given `FactoryBean` with an `id` of `myBean`, invoking `getBean("myBean")` on the container returns the product of the `FactoryBean`, whereas invoking `getBean("&myBean")` returns the @@ -4664,22 +4600,21 @@ source code and that, in terms of tooling, all configuration styles are supporte https://spring.io/tools[Spring Tools for Eclipse]. **** -An alternative to XML setup is provided by annotation-based configuration, which relies on -the bytecode metadata for wiring up components instead of angle-bracket declarations. +An alternative to XML setup is provided by annotation-based configuration, which relies +on the bytecode metadata for wiring up components instead of angle-bracket declarations. Instead of using XML to describe a bean wiring, the developer moves the configuration into the component class itself by using annotations on the relevant class, method, or -field declaration. As mentioned in <>, using +field declaration. As mentioned in <>, using a `BeanPostProcessor` in conjunction with annotations is a common means of extending the -Spring IoC container. For example, Spring 2.0 introduced the possibility of enforcing -required properties with the <> annotation. Spring -2.5 made it possible to follow that same general approach to drive Spring's dependency -injection. Essentially, the `@Autowired` annotation provides the same capabilities as -described in <> but with more fine-grained control and wider -applicability. Spring 2.5 also added support for JSR-250 annotations, such as -`@PostConstruct` and `@PreDestroy`. Spring 3.0 added support for JSR-330 (Dependency -Injection for Java) annotations contained in the `javax.inject` package such as `@Inject` -and `@Named`. Details about those annotations can be found in the -<>. +Spring IoC container. For example, Spring 2.5 introduced an annotation-based approach to +drive Spring's dependency injection. Essentially, the <> annotation provides the same capabilities as described in +<> but with more fine-grained control and wider applicability. +Spring 2.5 also added support for JSR-250 annotations, such as `@PostConstruct` and +`@PreDestroy`. Spring 3.0 added support for JSR-330 (Dependency Injection for Java) +annotations contained in the `jakarta.inject` package such as `@Inject` and `@Named`. +Details about those annotations can be found in the <>. [NOTE] ==== @@ -4687,8 +4622,8 @@ Annotation injection is performed before XML injection. Thus, the XML configurat overrides the annotations for properties wired through both approaches. ==== -As always, you can register them as individual bean definitions, but they can also be -implicitly registered by including the following tag in an XML-based Spring +As always, you can register the post-processors as individual bean definitions, but they +can also be implicitly registered by including the following tag in an XML-based Spring configuration (notice the inclusion of the `context` namespace): [source,xml,indent=0,subs="verbatim,quotes"] @@ -4707,12 +4642,13 @@ configuration (notice the inclusion of the `context` namespace): ---- -(The implicitly registered post-processors include -{api-spring-framework}/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.html[`AutowiredAnnotationBeanPostProcessor`], -{api-spring-framework}/context/annotation/CommonAnnotationBeanPostProcessor.html[`CommonAnnotationBeanPostProcessor`], -{api-spring-framework}/orm/jpa/support/PersistenceAnnotationBeanPostProcessor.html[`PersistenceAnnotationBeanPostProcessor`], -and the aforementioned -{api-spring-framework}/beans/factory/annotation/RequiredAnnotationBeanPostProcessor.html[`RequiredAnnotationBeanPostProcessor`].) +The `` element implicitly registers the following post-processors: + +* {api-spring-framework}/context/annotation/ConfigurationClassPostProcessor.html[`ConfigurationClassPostProcessor`] +* {api-spring-framework}/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.html[`AutowiredAnnotationBeanPostProcessor`] +* {api-spring-framework}/context/annotation/CommonAnnotationBeanPostProcessor.html[`CommonAnnotationBeanPostProcessor`] +* {api-spring-framework}/orm/jpa/support/PersistenceAnnotationBeanPostProcessor.html[`PersistenceAnnotationBeanPostProcessor`] +* {api-spring-framework}/context/event/EventListenerMethodProcessor.html[`EventListenerMethodProcessor`] [NOTE] ==== @@ -4725,57 +4661,6 @@ it only checks for `@Autowired` beans in your controllers, and not your services -[[beans-required-annotation]] -=== @Required - -The `@Required` annotation applies to bean property setter methods, as in the following -example: - -[source,java,indent=0,subs="verbatim,quotes",role="primary"] -.Java ----- - public class SimpleMovieLister { - - private MovieFinder movieFinder; - - @Required - public void setMovieFinder(MovieFinder movieFinder) { - this.movieFinder = movieFinder; - } - - // ... - } ----- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] -.Kotlin ----- - class SimpleMovieLister { - - @Required - lateinit var movieFinder: MovieFinder - - // ... -} ----- - - -This annotation indicates that the affected bean property must be populated at -configuration time, through an explicit property value in a bean definition or through -autowiring. The container throws an exception if the affected bean property has not been -populated. This allows for eager and explicit failure, avoiding `NullPointerException` -instances or the like later on. We still recommend that you put assertions into the -bean class itself (for example, into an init method). Doing so enforces those required -references and values even when you use the class outside of a container. - -[NOTE] -==== -The `@Required` annotation is formally deprecated as of Spring Framework 5.1, in favor -of using constructor injection for required settings (or a custom implementation of -`InitializingBean.afterPropertiesSet()` along with bean property setter methods). -==== - - - [[beans-autowired-annotation]] === Using `@Autowired` @@ -4842,7 +4727,7 @@ as the following example shows: ---- class SimpleMovieLister { - @Autowired + @set:Autowired lateinit var movieFinder: MovieFinder // ... @@ -4996,6 +4881,7 @@ The same applies for typed collections, as the following example shows: } ---- +[[beans-factory-ordered]] [TIP] ==== Your target beans can implement the `org.springframework.core.Ordered` interface or use @@ -5009,7 +4895,7 @@ use the same bean class). `@Order` values may influence priorities at injection but be aware that they do not influence singleton startup order, which is an orthogonal concern determined by dependency relationships and `@DependsOn` declarations. -Note that the standard `javax.annotation.Priority` annotation is not available at the +Note that the standard `jakarta.annotation.Priority` annotation is not available at the `@Bean` level, since it cannot be declared on methods. Its semantics can be modeled through `@Order` values in combination with `@Primary` on a single bean for each type. ==== @@ -5081,10 +4967,19 @@ non-required (i.e., by setting the `required` attribute in `@Autowired` to `fals } ---- +[NOTE] +==== A non-required method will not be called at all if its dependency (or one of its dependencies, in case of multiple arguments) is not available. A non-required field will not get populated at all in such cases, leaving its default value in place. +In other words, setting the `required` attribute to `false` indicates that the +corresponding property is _optional_ for autowiring purposes, and the property will be +ignored if it cannot be autowired. This allows properties to be assigned default values +that can be optionally overridden via dependency injection. +==== + + [[beans-autowired-annotation-constructor-resolution]] Injected constructor and factory method arguments are a special case since the `required` @@ -5112,13 +5007,6 @@ declares multiple constructors but none of them is annotated with `@Autowired`, primary/default constructor (if present) will be used. If a class only declares a single constructor to begin with, it will always be used, even if not annotated. Note that an annotated constructor does not have to be public. - -The `required` attribute of `@Autowired` is recommended over the deprecated `@Required` -annotation on setter methods. Setting the `required` attribute to `false` indicates that -the property is not required for autowiring purposes, and the property is ignored if it -cannot be autowired. `@Required`, on the other hand, is stronger in that it enforces the -property to be set by any means supported by the container, and if no value is defined, -a corresponding exception is raised. ==== Alternatively, you can express the non-required nature of a particular dependency @@ -5444,7 +5332,7 @@ Letting qualifier values select against target bean names, within the type-match candidates, does not require a `@Qualifier` annotation at the injection point. If there is no other resolution indicator (such as a qualifier or a primary marker), for a non-unique dependency situation, Spring matches the injection point name -(that is, the field name or parameter name) against the target bean names and choose the +(that is, the field name or parameter name) against the target bean names and chooses the same-named candidate, if any. ==== @@ -5459,7 +5347,7 @@ matching an `account` qualifier against beans marked with the same qualifier lab For beans that are themselves defined as a collection, `Map`, or array type, `@Resource` is a fine solution, referring to the specific collection or array bean by unique name. -That said, as of 4.3, collection, you can match `Map`, and array types through Spring's +That said, as of 4.3, you can match collection, `Map`, and array types through Spring's `@Autowired` type matching algorithm as well, as long as the element type information is preserved in `@Bean` return type signatures or collection inheritance hierarchies. In this case, you can use qualifier values to select among same-typed collections, @@ -5934,8 +5822,8 @@ attribute set to `true`, it is selected. === Injection with `@Resource` Spring also supports injection by using the JSR-250 `@Resource` annotation -(`javax.annotation.Resource`) on fields or bean property setter methods. -This is a common pattern in Java EE: for example, in JSF-managed beans and JAX-WS +(`jakarta.annotation.Resource`) on fields or bean property setter methods. +This is a common pattern in Jakarta EE: for example, in JSF-managed beans and JAX-WS endpoints. Spring supports this pattern for Spring-managed objects as well. `@Resource` takes a name attribute. By default, Spring interprets that value as @@ -5992,7 +5880,7 @@ named `movieFinder` injected into its setter method: ---- class SimpleMovieLister { - @Resource + @set:Resource private lateinit var movieFinder: MovieFinder } @@ -6113,14 +6001,14 @@ example shows: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java ---- - @Configuration - public class AppConfig { + @Configuration + public class AppConfig { - @Bean - public static PropertySourcesPlaceholderConfigurer propertyPlaceholderConfigurer() { - return new PropertySourcesPlaceholderConfigurer(); - } - } + @Bean + public static PropertySourcesPlaceholderConfigurer propertyPlaceholderConfigurer() { + return new PropertySourcesPlaceholderConfigurer(); + } + } ---- [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] .Kotlin @@ -6146,7 +6034,7 @@ will get properties from `application.properties` and `application.yml` files. Built-in converter support provided by Spring allows simple type conversion (to `Integer` or `int` for example) to be automatically handled. Multiple comma-separated values can be -automatically converted to String array without extra effort. +automatically converted to `String` array without extra effort. It is possible to provide a default value as following: @@ -6170,8 +6058,8 @@ It is possible to provide a default value as following: class MovieRecommender(@Value("\${catalog.name:defaultCatalog}") private val catalog: String) ---- -A Spring `BeanPostProcessor` uses a `ConversionService` behind the scene to handle the -process for converting the String value in `@Value` to the target type. If you want to +A Spring `BeanPostProcessor` uses a `ConversionService` behind the scenes to handle the +process for converting the `String` value in `@Value` to the target type. If you want to provide conversion support for your own custom type, you can provide your own `ConversionService` bean instance as the following example shows: @@ -6197,7 +6085,7 @@ provide conversion support for your own custom type, you can provide your own @Bean fun conversionService(): ConversionService { - return DefaultFormattingConversionService().apply { + return DefaultFormattingConversionService().apply { addConverter(MyCustomConverter()) } } @@ -6257,8 +6145,8 @@ SpEL also enables the use of more complex data structures: === Using `@PostConstruct` and `@PreDestroy` The `CommonAnnotationBeanPostProcessor` not only recognizes the `@Resource` annotation -but also the JSR-250 lifecycle annotations: `javax.annotation.PostConstruct` and -`javax.annotation.PreDestroy`. Introduced in Spring 2.5, the support for these +but also the JSR-250 lifecycle annotations: `jakarta.annotation.PostConstruct` and +`jakarta.annotation.PreDestroy`. Introduced in Spring 2.5, the support for these annotations offers an alternative to the lifecycle callback mechanism described in <> and <>. Provided that the @@ -6309,8 +6197,9 @@ For details about the effects of combining various lifecycle mechanisms, see Like `@Resource`, the `@PostConstruct` and `@PreDestroy` annotation types were a part of the standard Java libraries from JDK 6 to 8. However, the entire `javax.annotation` package got separated from the core Java modules in JDK 9 and eventually removed in -JDK 11. If needed, the `javax.annotation-api` artifact needs to be obtained via Maven -Central now, simply to be added to the application's classpath like any other library. +JDK 11. As of Jakarta EE 9, the package lives in `jakarta.annotation` now. If needed, +the `jakarta.annotation-api` artifact needs to be obtained via Maven Central now, +simply to be added to the application's classpath like any other library. ==== @@ -6386,7 +6275,7 @@ is meta-annotated with `@Component`, as the following example shows: // ... } ---- -<1> The `Component` causes `@Service` to be treated in the same way as `@Component`. +<1> The `@Component` causes `@Service` to be treated in the same way as `@Component`. [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] .Kotlin @@ -6400,7 +6289,7 @@ is meta-annotated with `@Component`, as the following example shows: // ... } ---- -<1> The `Component` causes `@Service` to be treated in the same way as `@Component`. +<1> The `@Component` causes `@Service` to be treated in the same way as `@Component`. You can also combine meta-annotations to create "`composed annotations`". For example, the `@RestController` annotation from Spring MVC is composed of `@Controller` and @@ -6662,14 +6551,14 @@ and using "`stub`" repositories instead: includeFilters = @Filter(type = FilterType.REGEX, pattern = ".*Stub.*Repository"), excludeFilters = @Filter(Repository.class)) public class AppConfig { - ... + // ... } ---- [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] .Kotlin ---- @Configuration - @ComponentScan(basePackages = "org.example", + @ComponentScan(basePackages = ["org.example"], includeFilters = [Filter(type = FilterType.REGEX, pattern = [".*Stub.*Repository"])], excludeFilters = [Filter(Repository::class)]) class AppConfig { @@ -6746,9 +6635,11 @@ factory method and other bean definition properties, such as a qualifier value t the `@Qualifier` annotation. Other method-level annotations that can be specified are `@Scope`, `@Lazy`, and custom qualifier annotations. -TIP: In addition to its role for component initialization, you can also place the `@Lazy` annotation -on injection points marked with `@Autowired` or `@Inject`. In this context, it -leads to the injection of a lazy-resolution proxy. +TIP: In addition to its role for component initialization, you can also place the `@Lazy` +annotation on injection points marked with `@Autowired` or `@Inject`. In this context, +it leads to the injection of a lazy-resolution proxy. However, such a proxy approach +is rather limited. For sophisticated lazy interactions, in particular in combination +with optional dependencies, we recommend `ObjectProvider` instead. Autowired fields and methods are supported, as previously discussed, with additional support for autowiring of `@Bean` methods. The following example shows how to do so: @@ -7192,10 +7083,10 @@ metadata is provided per-instance rather than per-class. While classpath scanning is very fast, it is possible to improve the startup performance of large applications by creating a static list of candidates at compilation time. In this -mode, all modules that are target of component scan must use this mechanism. +mode, all modules that are targets of component scanning must use this mechanism. -NOTE: Your existing `@ComponentScan` or `` directives must remain +unchanged to request the context to scan candidates in certain packages. When the `ApplicationContext` detects such an index, it automatically uses it rather than scanning the classpath. @@ -7224,32 +7115,31 @@ configuration, as shown in the following example: compileOnly "org.springframework:spring-context-indexer:{spring-version}" } ---- -==== With Gradle 4.6 and later, the dependency should be declared in the `annotationProcessor` configuration, as shown in the following example: -==== -[source,groovy,indent=0subs="verbatim,quotes,attributes"] +[source,groovy,indent=0,subs="verbatim,quotes,attributes"] ---- dependencies { annotationProcessor "org.springframework:spring-context-indexer:{spring-version}" } ---- -That process generates a `META-INF/spring.components` file that is -included in the jar file. +The `spring-context-indexer` artifact generates a `META-INF/spring.components` file that +is included in the jar file. NOTE: When working with this mode in your IDE, the `spring-context-indexer` must be registered as an annotation processor to make sure the index is up-to-date when candidate components are updated. -TIP: The index is enabled automatically when a `META-INF/spring.components` is found +TIP: The index is enabled automatically when a `META-INF/spring.components` file is found on the classpath. If an index is partially available for some libraries (or use cases) -but could not be built for the whole application, you can fallback to a regular classpath -arrangement (as though no index was present at all) by setting `spring.index.ignore` to -`true`, either as a system property or in a `spring.properties` file at the root of the -classpath. +but could not be built for the whole application, you can fall back to a regular classpath +arrangement (as though no index were present at all) by setting `spring.index.ignore` to +`true`, either as a JVM system property or via the +<> mechanism. + @@ -7262,16 +7152,16 @@ annotations. To use them, you need to have the relevant jars in your classpath. [NOTE] ===== -If you use Maven, the `javax.inject` artifact is available in the standard Maven +If you use Maven, the `jakarta.inject` artifact is available in the standard Maven repository ( -https://repo1.maven.org/maven2/javax/inject/javax.inject/1/[https://repo1.maven.org/maven2/javax/inject/javax.inject/1/]). +https://repo1.maven.org/maven2/jakarta/inject/jakarta.inject-api/2.0.0/[https://repo1.maven.org/maven2/jakarta/inject/jakarta.inject-api/2.0.0/]). You can add the following dependency to your file pom.xml: [source,xml,indent=0,subs="verbatim,quotes"] ---- - javax.inject - javax.inject + jakarta.inject + jakarta.inject-api 1 ---- @@ -7282,12 +7172,12 @@ You can add the following dependency to your file pom.xml: [[beans-inject-named]] === Dependency Injection with `@Inject` and `@Named` -Instead of `@Autowired`, you can use `@javax.inject.Inject` as follows: +Instead of `@Autowired`, you can use `@jakarta.inject.Inject` as follows: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java ---- - import javax.inject.Inject; + import jakarta.inject.Inject; public class SimpleMovieLister { @@ -7307,7 +7197,7 @@ Instead of `@Autowired`, you can use `@javax.inject.Inject` as follows: [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] .Kotlin ---- - import javax.inject.Inject + import jakarta.inject.Inject class SimpleMovieLister { @@ -7331,8 +7221,8 @@ preceding example: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java ---- - import javax.inject.Inject; - import javax.inject.Provider; + import jakarta.inject.Inject; + import jakarta.inject.Provider; public class SimpleMovieLister { @@ -7352,16 +7242,16 @@ preceding example: [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] .Kotlin ---- - import javax.inject.Inject + import jakarta.inject.Inject class SimpleMovieLister { @Inject - lateinit var movieFinder: MovieFinder + lateinit var movieFinder: Provider fun listMovies() { - movieFinder.findMovies(...) + movieFinder.get().findMovies(...) // ... } } @@ -7373,8 +7263,8 @@ you should use the `@Named` annotation, as the following example shows: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java ---- - import javax.inject.Inject; - import javax.inject.Named; + import jakarta.inject.Inject; + import jakarta.inject.Named; public class SimpleMovieLister { @@ -7391,8 +7281,8 @@ you should use the `@Named` annotation, as the following example shows: [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] .Kotlin ---- - import javax.inject.Inject - import javax.inject.Named + import jakarta.inject.Inject + import jakarta.inject.Named class SimpleMovieLister { @@ -7449,14 +7339,14 @@ a `required` attribute. The following pair of examples show how to use `@Inject` [[beans-named]] === `@Named` and `@ManagedBean`: Standard Equivalents to the `@Component` Annotation -Instead of `@Component`, you can use `@javax.inject.Named` or `javax.annotation.ManagedBean`, +Instead of `@Component`, you can use `@jakarta.inject.Named` or `jakarta.annotation.ManagedBean`, as the following example shows: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java ---- - import javax.inject.Inject; - import javax.inject.Named; + import jakarta.inject.Inject; + import jakarta.inject.Named; @Named("movieListener") // @ManagedBean("movieListener") could be used as well public class SimpleMovieLister { @@ -7474,8 +7364,8 @@ as the following example shows: [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] .Kotlin ---- - import javax.inject.Inject - import javax.inject.Named + import jakarta.inject.Inject + import jakarta.inject.Named @Named("movieListener") // @ManagedBean("movieListener") could be used as well class SimpleMovieLister { @@ -7493,8 +7383,8 @@ It is very common to use `@Component` without specifying a name for the componen [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java ---- - import javax.inject.Inject; - import javax.inject.Named; + import jakarta.inject.Inject; + import jakarta.inject.Named; @Named public class SimpleMovieLister { @@ -7512,8 +7402,8 @@ It is very common to use `@Component` without specifying a name for the componen [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] .Kotlin ---- - import javax.inject.Inject - import javax.inject.Named + import jakarta.inject.Inject + import jakarta.inject.Named @Named class SimpleMovieLister { @@ -7547,7 +7437,7 @@ exact same way as when you use Spring annotations, as the following example show } ---- -NOTE: In contrast to `@Component`, the JSR-330 `@Named` and the JSR-250 `ManagedBean` +NOTE: In contrast to `@Component`, the JSR-330 `@Named` and the JSR-250 `@ManagedBean` annotations are not composable. You should use Spring's stereotype model for building custom component annotations. @@ -7562,7 +7452,7 @@ features are not available, as the following table shows: [[annotations-comparison]] .Spring component model elements versus JSR-330 variants |=== -| Spring| javax.inject.*| javax.inject restrictions / comments +| Spring| jakarta.inject.*| jakarta.inject restrictions / comments | @Autowired | @Inject @@ -7577,31 +7467,27 @@ features are not available, as the following table shows: | The JSR-330 default scope is like Spring's `prototype`. However, in order to keep it consistent with Spring's general defaults, a JSR-330 bean declared in the Spring container is a `singleton` by default. In order to use a scope other than `singleton`, - you should use Spring's `@Scope` annotation. `javax.inject` also provides a - https://download.oracle.com/javaee/6/api/javax/inject/Scope.html[@Scope] annotation. - Nevertheless, this one is only intended to be used for creating your own annotations. + you should use Spring's `@Scope` annotation. `jakarta.inject` also provides a + `jakarta.inject.Scope` annotation: however, this one is only intended to be used + for creating custom annotations. | @Qualifier | @Qualifier / @Named -| `javax.inject.Qualifier` is just a meta-annotation for building custom qualifiers. +| `jakarta.inject.Qualifier` is just a meta-annotation for building custom qualifiers. Concrete `String` qualifiers (like Spring's `@Qualifier` with a value) can be associated - through `javax.inject.Named`. + through `jakarta.inject.Named`. | @Value | - | no equivalent -| @Required -| - -| no equivalent - | @Lazy | - | no equivalent | ObjectFactory | Provider -| `javax.inject.Provider` is a direct alternative to Spring's `ObjectFactory`, +| `jakarta.inject.Provider` is a direct alternative to Spring's `ObjectFactory`, only with a shorter `get()` method name. It can also be used in combination with Spring's `@Autowired` or with non-annotated constructors and setter methods. |=== @@ -7703,7 +7589,7 @@ to reduce subtle bugs that can be hard to track down when operating in "`lite`" **** The `@Bean` and `@Configuration` annotations are discussed in depth in the following sections. -First, however, we cover the various ways of creating a spring container using by +First, however, we cover the various ways of creating a spring container by using Java-based configuration. @@ -7830,7 +7716,7 @@ To enable component scanning, you can annotate your `@Configuration` class as fo @Configuration @ComponentScan(basePackages = "com.acme") // <1> public class AppConfig { - ... + // ... } ---- <1> This annotation enables component scanning. @@ -7852,8 +7738,7 @@ To enable component scanning, you can annotate your `@Configuration` class as fo Experienced Spring users may be familiar with the XML declaration equivalent from Spring's `context:` namespace, shown in the following example: -[source,xml,indent=0] -[subs="verbatim,quotes"] +[source,xml,indent=0,subs="verbatim,quotes"] ---- @@ -7958,6 +7843,10 @@ init-param): ---- +NOTE: For programmatic use cases, a `GenericWebApplicationContext` can be used as an +alternative to `AnnotationConfigWebApplicationContext`. See the +{api-spring-framework}/web/context/support/GenericWebApplicationContext.html[`GenericWebApplicationContext`] +javadoc for details. [[beans-java-bean-annotation]] @@ -7965,6 +7854,7 @@ init-param): `@Bean` is a method-level annotation and a direct analog of the XML `` element. The annotation supports some of the attributes offered by ``, such as: + * <> * <> * <> @@ -8023,6 +7913,26 @@ following text image shows: transferService -> com.acme.TransferServiceImpl ---- +You can also use default methods to define beans. This allows composition of bean +configurations by implementing interfaces with bean definitions on default methods. + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- + public interface BaseConfig { + + @Bean + default TransferServiceImpl transferService() { + return new TransferServiceImpl(); + } + } + + @Configuration + public class AppConfig implements BaseConfig { + + } +---- + You can also declare your `@Bean` method with an interface (or base class) return type, as the following example shows: @@ -8053,7 +7963,7 @@ return type, as the following example shows: However, this limits the visibility for advance type prediction to the specified interface type (`TransferService`). Then, with the full type (`TransferServiceImpl`) -known to the container only once, the affected singleton bean has been instantiated. +known to the container only once the affected singleton bean has been instantiated. Non-lazy singleton beans get instantiated according to their declaration order, so you may see different type matching results depending on when another component tries to match by a non-declared type (such as `@Autowired TransferServiceImpl`, @@ -8194,7 +8104,7 @@ default `(inferred)` mode. You may want to do that by default for a resource that you acquire with JNDI, as its lifecycle is managed outside the application. In particular, make sure to always do it -for a `DataSource`, as it is known to be problematic on Java EE application servers. +for a `DataSource`, as it is known to be problematic on Jakarta EE application servers. The following example shows how to prevent an automatic destruction callback for a `DataSource`: @@ -8311,8 +8221,10 @@ Spring offers a convenient way of working with scoped dependencies through <>. The easiest way to create such a proxy when using the XML configuration is the `` element. Configuring your beans in Java with a `@Scope` annotation offers equivalent support -with the `proxyMode` attribute. The default is no proxy (`ScopedProxyMode.NO`), -but you can specify `ScopedProxyMode.TARGET_CLASS` or `ScopedProxyMode.INTERFACES`. +with the `proxyMode` attribute. The default is `ScopedProxyMode.DEFAULT`, which +typically indicates that no scoped proxy should be created unless a different default +has been configured at the component-scan instruction level. You can specify +`ScopedProxyMode.TARGET_CLASS`, `ScopedProxyMode.INTERFACES` or `ScopedProxyMode.NO`. If you port the scoped proxy example from the XML reference documentation (see <>) to our `@Bean` using Java, @@ -8348,7 +8260,7 @@ it resembles the following: fun userService(): Service { return SimpleUserService().apply { // a reference to the proxied userPreferences bean - setUserPreferences(userPreferences() + setUserPreferences(userPreferences()) } } ---- @@ -8366,7 +8278,7 @@ as the following example shows: @Configuration public class AppConfig { - @Bean(name = "myThing") + @Bean("myThing") public Thing thing() { return new Thing(); } @@ -8459,7 +8371,7 @@ annotation, as the following example shows: === Using the `@Configuration` annotation `@Configuration` is a class-level annotation indicating that an object is a source of -bean definitions. `@Configuration` classes declare beans through public `@Bean` annotated +bean definitions. `@Configuration` classes declare beans through `@Bean`-annotated methods. Calls to `@Bean` methods on `@Configuration` classes can also be used to define inter-bean dependencies. See <> for a general introduction. @@ -9208,7 +9120,7 @@ method that returns `true` or `false`. For example, the following listing shows val attrs = metadata.getAllAnnotationAttributes(Profile::class.java.name) if (attrs != null) { for (value in attrs["value"]!!) { - if (context.environment.acceptsProfiles(Profiles .of(*value as Array))) { + if (context.environment.acceptsProfiles(Profiles.of(*value as Array))) { return true } } @@ -9351,7 +9263,7 @@ is not strictly required. -- Because `@Configuration` is meta-annotated with `@Component`, `@Configuration`-annotated classes are automatically candidates for component scanning. Using the same scenario as -describe in the previous example, we can redefine `system-test-config.xml` to take advantage of component-scanning. +described in the previous example, we can redefine `system-test-config.xml` to take advantage of component-scanning. Note that, in this case, we need not explicitly declare ``, because `` enables the same functionality. @@ -9682,7 +9594,7 @@ of creating a custom composed annotation. The following example defines a custom [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] .Kotlin ---- - @Target(AnnotationTarget.TYPE) + @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) @Profile("production") annotation class Production @@ -9972,7 +9884,7 @@ as a way to provide a default definition for one or more beans. If any profile is enabled, the default profile does not apply. You can change the name of the default profile by using `setDefaultProfiles()` on -the `Environment` or ,declaratively, by using the `spring.profiles.default` property. +the `Environment` or, declaratively, by using the `spring.profiles.default` property. @@ -10010,9 +9922,9 @@ is configured with two PropertySource objects -- one representing the set of JVM NOTE: These default property sources are present for `StandardEnvironment`, for use in standalone applications. {api-spring-framework}/web/context/support/StandardServletEnvironment.html[`StandardServletEnvironment`] -is populated with additional default property sources including servlet config and servlet -context parameters. It can optionally enable a {api-spring-framework}/jndi/JndiPropertySource.html[`JndiPropertySource`]. -See the javadoc for details. +is populated with additional default property sources including servlet config, servlet +context parameters, and a {api-spring-framework}/jndi/JndiPropertySource.html[`JndiPropertySource`] +if JNDI is available. Concretely, when you use the `StandardEnvironment`, the call to `env.containsProperty("my-property")` returns true if a `my-property` system property or `my-property` environment variable is present at @@ -10246,7 +10158,7 @@ interfaces to provide additional functionality in a more application framework-oriented style. Many people use the `ApplicationContext` in a completely declarative fashion, not even creating it programmatically, but instead relying on support classes such as `ContextLoader` to automatically instantiate an -`ApplicationContext` as part of the normal startup process of a Java EE web application. +`ApplicationContext` as part of the normal startup process of a Jakarta EE web application. To enhance `BeanFactory` functionality in a more framework-oriented style, the context package also provides the following functionality: @@ -10291,8 +10203,8 @@ bean with the same name. If it does, it uses that bean as the `MessageSource`. I `DelegatingMessageSource` is instantiated in order to be able to accept calls to the methods defined above. -Spring provides two `MessageSource` implementations, `ResourceBundleMessageSource` and -`StaticMessageSource`. Both implement `HierarchicalMessageSource` in order to do nested +Spring provides three `MessageSource` implementations, `ResourceBundleMessageSource`, `ReloadableResourceBundleMessageSource` +and `StaticMessageSource`. All of them implement `HierarchicalMessageSource` in order to do nested messaging. The `StaticMessageSource` is rarely used but provides programmatic ways to add messages to the source. The following example shows `ResourceBundleMessageSource`: @@ -10477,6 +10389,10 @@ You can also use the `MessageSourceAware` interface to acquire a reference to an `ApplicationContext` that implements the `MessageSourceAware` interface is injected with the application context's `MessageSource` when the bean is created and configured. +NOTE: Because Spring's `MessageSource` is based on Java's `ResourceBundle`, it does not merge +bundles with the same base name, but will only use the first bundle found. +Subsequent message bundles with the same base name are ignored. + NOTE: As an alternative to `ResourceBundleMessageSource`, Spring provides a `ReloadableResourceBundleMessageSource` class. This variant supports the same bundle file format but is more flexible than the standard JDK based @@ -10666,7 +10582,7 @@ shows such a class: ---- class BlockedListNotifier : ApplicationListener { - lateinit var notificationAddres: String + lateinit var notificationAddress: String override fun onApplicationEvent(event: BlockedListEvent) { // notify appropriate parties via notificationAddress... @@ -10726,9 +10642,8 @@ architectures that build upon the well-known Spring programming model. [[context-functionality-events-annotation]] ==== Annotation-based Event Listeners -As of Spring 4.2, you can register an event listener on any public method of a managed -bean by using the `@EventListener` annotation. The `BlockedListNotifier` can be rewritten as -follows: +You can register an event listener on any method of a managed bean by using the +`@EventListener` annotation. The `BlockedListNotifier` can be rewritten as follows: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -10788,7 +10703,7 @@ following example shows how to do so: ---- It is also possible to add additional runtime filtering by using the `condition` attribute -of the annotation that defines a <> , which should match +of the annotation that defines a <>, which should match to actually invoke the method for a particular event. The following example shows how our notifier can be rewritten to be invoked only if the @@ -10798,7 +10713,7 @@ The following example shows how our notifier can be rewritten to be invoked only .Java ---- @EventListener(condition = "#blEvent.content == 'my-event'") - public void processBlockedListEvent(BlockedListEvent blockedListEvent) { + public void processBlockedListEvent(BlockedListEvent blEvent) { // notify appropriate parties via notificationAddress... } ---- @@ -10806,7 +10721,7 @@ The following example shows how our notifier can be rewritten to be invoked only .Kotlin ---- @EventListener(condition = "#blEvent.content == 'my-event'") - fun processBlockedListEvent(blockedListEvent: BlockedListEvent) { + fun processBlockedListEvent(blEvent: BlockedListEvent) { // notify appropriate parties via notificationAddress... } ---- @@ -10866,9 +10781,9 @@ method signature to return the event that should be published, as the following NOTE: This feature is not supported for <>. -This new method publishes a new `ListUpdateEvent` for every `BlockedListEvent` handled by the -method above. If you need to publish several events, you can return a `Collection` of events -instead. +The `handleBlockedListEvent()` method publishes a new `ListUpdateEvent` for every +`BlockedListEvent` that it handles. If you need to publish several events, you can return +a `Collection` or an array of events instead. [[context-functionality-events-async]] @@ -10900,10 +10815,12 @@ The following example shows how to do so: Be aware of the following limitations when using asynchronous events: * If an asynchronous event listener throws an `Exception`, it is not propagated to the - caller. See `AsyncUncaughtExceptionHandler` for more details. + caller. See + {api-spring-framework}/aop/interceptor/AsyncUncaughtExceptionHandler.html[`AsyncUncaughtExceptionHandler`] + for more details. * Asynchronous event listener methods cannot publish a subsequent event by returning a value. If you need to publish another event as the result of the processing, inject an - {api-spring-framework}/aop/interceptor/AsyncUncaughtExceptionHandler.html[`ApplicationEventPublisher`] + {api-spring-framework}/context/ApplicationEventPublisher.html[`ApplicationEventPublisher`] to publish the event manually. @@ -11028,8 +10945,10 @@ location path as a classpath location. You can also use location paths (resource with special prefixes to force loading of definitions from the classpath or a URL, regardless of the actual context type. + + [[context-functionality-startup]] -=== Application Startup tracking +=== Application Startup Tracking The `ApplicationContext` manages the lifecycle of Spring applications and provides a rich programming model around components. As a result, complex applications can have equally @@ -11064,19 +10983,19 @@ Here is an example of instrumentation in the `AnnotationConfigApplicationContext .Kotlin ---- // create a startup step and start recording - val scanPackages = this.getApplicationStartup().start("spring.context.base-packages.scan"); + val scanPackages = this.getApplicationStartup().start("spring.context.base-packages.scan") // add tagging information to the current step - scanPackages.tag("packages", () -> Arrays.toString(basePackages)); + scanPackages.tag("packages", () -> Arrays.toString(basePackages)) // perform the actual phase we're instrumenting - this.scanner.scan(basePackages); + this.scanner.scan(basePackages) // end the current step - scanPackages.end(); + scanPackages.end() ---- The application context is already instrumented with multiple steps. Once recorded, these startup steps can be collected, displayed and analyzed with specific tools. For a complete list of existing startup steps, you can check out the -<>. +<>. The default `ApplicationStartup` implementation is a no-op variant, for minimal overhead. This means no metrics will be collected during application startup by default. @@ -11132,15 +11051,15 @@ Examples are `/WEB-INF/{asterisk}Context.xml` (for all files with names that end [[context-deploy-rar]] -=== Deploying a Spring `ApplicationContext` as a Java EE RAR File +=== Deploying a Spring `ApplicationContext` as a Jakarta EE RAR File It is possible to deploy a Spring `ApplicationContext` as a RAR file, encapsulating the -context and all of its required bean classes and library JARs in a Java EE RAR deployment +context and all of its required bean classes and library JARs in a Jakarta EE RAR deployment unit. This is the equivalent of bootstrapping a stand-alone `ApplicationContext` (only hosted -in Java EE environment) being able to access the Java EE servers facilities. RAR deployment +in Jakarta EE environment) being able to access the Jakarta EE servers facilities. RAR deployment is a more natural alternative to a scenario of deploying a headless WAR file -- in effect, a WAR file without any HTTP entry points that is used only for bootstrapping a Spring -`ApplicationContext` in a Java EE environment. +`ApplicationContext` in a Jakarta EE environment. RAR deployment is ideal for application contexts that do not need HTTP entry points but rather consist only of message endpoints and scheduled jobs. Beans in such a context can @@ -11154,13 +11073,13 @@ See the javadoc of the {api-spring-framework}/jca/context/SpringContextResourceAdapter.html[`SpringContextResourceAdapter`] class for the configuration details involved in RAR deployment. -For a simple deployment of a Spring ApplicationContext as a Java EE RAR file: +For a simple deployment of a Spring ApplicationContext as a Jakarta EE RAR file: . Package all application classes into a RAR file (which is a standard JAR file with a different file extension). -.Add all required library JARs into the root of the RAR archive. -.Add a +. Add all required library JARs into the root of the RAR archive. +. Add a `META-INF/ra.xml` deployment descriptor (as shown in the {api-spring-framework}/jca/context/SpringContextResourceAdapter.html[javadoc for `SpringContextResourceAdapter`]) and the corresponding Spring XML bean definition file(s) (typically `META-INF/applicationContext.xml`). @@ -11179,7 +11098,7 @@ by other application modules on the same machine. [[beans-beanfactory]] -== The `BeanFactory` +== The `BeanFactory` API The `BeanFactory` API provides the underlying basis for Spring's IoC functionality. Its specific contracts are mostly used in integration with other parts of Spring and @@ -11254,7 +11173,7 @@ The following table lists features provided by the `BeanFactory` and | No | Yes -| Convenient `MessageSource` access (for internalization) +| Convenient `MessageSource` access (for internationalization) | No | Yes diff --git a/src/docs/asciidoc/core/core-databuffer-codec.adoc b/framework-docs/src/docs/asciidoc/core/core-databuffer-codec.adoc similarity index 95% rename from src/docs/asciidoc/core/core-databuffer-codec.adoc rename to framework-docs/src/docs/asciidoc/core/core-databuffer-codec.adoc index ca5065144961..1f9f6e995a07 100644 --- a/src/docs/asciidoc/core/core-databuffer-codec.adoc +++ b/framework-docs/src/docs/asciidoc/core/core-databuffer-codec.adoc @@ -12,7 +12,7 @@ APIs as follows: * <> represents a byte buffer, which may be <>. * <> offers utility methods for data buffers. -* <> decode or encode streams data buffer streams into higher level objects. +* <> decode or encode data buffer streams into higher level objects. @@ -125,11 +125,11 @@ release it immediately, it can do so via `DataBufferUtils.release(dataBuffer)`. . If a `Decoder` is using `Flux` or `Mono` operators such as `flatMap`, `reduce`, and others that prefetch and cache data items internally, or is using operators such as `filter`, `skip`, and others that leave out items, then -`doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release)` must be added to the +`doOnDiscard(DataBuffer.class, DataBufferUtils::release)` must be added to the composition chain to ensure such buffers are released prior to being discarded, possibly -also as a result an error or cancellation signal. +also as a result of an error or cancellation signal. . If a `Decoder` holds on to one or more data buffers in any other way, it must -ensure they are released when fully read, or in case an error or cancellation signals that +ensure they are released when fully read, or in case of an error or cancellation signals that take place before the cached data buffers have been read and released. Note that `DataBufferUtils#join` offers a safe and efficient way to aggregate a data diff --git a/src/docs/asciidoc/core/core-expressions.adoc b/framework-docs/src/docs/asciidoc/core/core-expressions.adoc similarity index 93% rename from src/docs/asciidoc/core/core-expressions.adoc rename to framework-docs/src/docs/asciidoc/core/core-expressions.adoc index 5a1399dd831b..5ee6f2f71722 100644 --- a/src/docs/asciidoc/core/core-expressions.adoc +++ b/framework-docs/src/docs/asciidoc/core/core-expressions.adoc @@ -357,7 +357,7 @@ the list: // Turn on: // - auto null reference initialization // - auto collection growing - SpelParserConfiguration config = new SpelParserConfiguration(true,true); + SpelParserConfiguration config = new SpelParserConfiguration(true, true); ExpressionParser parser = new SpelExpressionParser(config); @@ -433,9 +433,9 @@ interpreter and only 3ms using the compiled version of the expression. The compiler is not turned on by default, but you can turn it on in either of two different ways. You can turn it on by using the parser configuration process -(<>) or by using a system -property when SpEL usage is embedded inside another component. This section -discusses both of these options. +(<>) or by using a Spring property +when SpEL usage is embedded inside another component. This section discusses both of +these options. The compiler can operate in one of three modes, which are captured in the `org.springframework.expression.spel.SpelCompilerMode` enum. The modes are as follows: @@ -465,7 +465,7 @@ following example shows how to do so: .Java ---- SpelParserConfiguration config = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE, - this.getClass().getClassLoader()); + this.getClass().getClassLoader()); SpelExpressionParser parser = new SpelExpressionParser(config); @@ -496,10 +496,12 @@ It is important to ensure that, if a classloader is specified, it can see all th the expression evaluation process. If you do not specify a classloader, a default classloader is used (typically the context classloader for the thread that is running during expression evaluation). -The second way to configure the compiler is for use when SpEL is embedded inside some other -component and it may not be possible to configure it through a configuration object. In these -cases, it is possible to use a system property. You can set the `spring.expression.compiler.mode` -property to one of the `SpelCompilerMode` enum values (`off`, `immediate`, or `mixed`). +The second way to configure the compiler is for use when SpEL is embedded inside some +other component and it may not be possible to configure it through a configuration +object. In these cases, it is possible to set the `spring.expression.compiler.mode` +property via a JVM system property (or via the +<> mechanism) to one of the +`SpelCompilerMode` enum values (`off`, `immediate`, or `mixed`). [[expressions-compiler-limitations]] @@ -515,7 +517,7 @@ kinds of expression cannot be compiled at the moment: * Expressions using custom resolvers or accessors * Expressions using selection or projection -More types of expression will be compilable in the future. +More types of expressions will be compilable in the future. @@ -587,7 +589,7 @@ You can also refer to other bean properties by name, as the following example sh To specify a default value, you can place the `@Value` annotation on fields, methods, and method or constructor parameters. -The following example sets the default value of a field variable: +The following example sets the default value of a field: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -786,7 +788,7 @@ using a literal on one side of a logical comparison operator. ---- Numbers support the use of the negative sign, exponential notation, and decimal points. -By default, real numbers are parsed by using Double.parseDouble(). +By default, real numbers are parsed by using `Double.parseDouble()`. @@ -794,10 +796,10 @@ By default, real numbers are parsed by using Double.parseDouble(). === Properties, Arrays, Lists, Maps, and Indexers Navigating with property references is easy. To do so, use a period to indicate a nested -property value. The instances of the `Inventor` class, `pupin` and `tesla`, were populated with -data listed in the <> section. -To navigate "`down`" and get Tesla's year of birth and Pupin's city of birth, we use the following -expressions: +property value. The instances of the `Inventor` class, `pupin` and `tesla`, were +populated with data listed in the <> section. To navigate "down" the object graph and get Tesla's year of birth and +Pupin's city of birth, we use the following expressions: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -937,7 +939,7 @@ You can directly express lists in an expression by using `{}` notation. ---- `{}` by itself means an empty list. For performance reasons, if the list is itself -entirely composed of fixed literals, a constant list is created to represent the +entirely composed of fixed literals, a constant list is created to represent the expression (rather than building a new list on each evaluation). @@ -956,7 +958,7 @@ following example shows how to do so: Map mapOfMaps = (Map) parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context); ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim",role="secondary"] .Kotlin ---- // evaluates to a Java map containing the two entries @@ -965,10 +967,11 @@ following example shows how to do so: val mapOfMaps = parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context) as Map<*, *> ---- -`{:}` by itself means an empty map. For performance reasons, if the map is itself composed -of fixed literals or other nested constant structures (lists or maps), a constant map is created -to represent the expression (rather than building a new map on each evaluation). Quoting of the map keys -is optional. The examples above do not use quoted keys. +`{:}` by itself means an empty map. For performance reasons, if the map is itself +composed of fixed literals or other nested constant structures (lists or maps), a +constant map is created to represent the expression (rather than building a new map on +each evaluation). Quoting of the map keys is optional (unless the key contains a period +(`.`)). The examples above do not use quoted keys. @@ -1001,8 +1004,7 @@ to have the array populated at construction time. The following example shows ho val numbers3 = parser.parseExpression("new int[4][5]").getValue(context) as Array ---- -You cannot currently supply an initializer when you construct -multi-dimensional array. +You cannot currently supply an initializer when you construct a multi-dimensional array. @@ -1050,8 +1052,9 @@ The Spring Expression Language supports the following kinds of operators: ==== Relational Operators The relational operators (equal, not equal, less than, less than or equal, greater than, -and greater than or equal) are supported by using standard operator notation. The -following listing shows a few examples of operators: +and greater than or equal) are supported by using standard operator notation. +These operators work on `Number` types as well as types implementing `Comparable`. +The following listing shows a few examples of operators: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1064,6 +1067,9 @@ following listing shows a few examples of operators: // evaluates to true boolean trueValue = parser.parseExpression("'black' < 'block'").getValue(Boolean.class); + + // uses CustomValue:::compareTo + boolean trueValue = parser.parseExpression("new CustomValue(1) < new CustomValue(2)").getValue(Boolean.class); ---- [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] .Kotlin @@ -1076,6 +1082,9 @@ following listing shows a few examples of operators: // evaluates to true val trueValue = parser.parseExpression("'black' < 'block'").getValue(Boolean::class.java) + + // uses CustomValue:::compareTo + val trueValue = parser.parseExpression("new CustomValue(1) < new CustomValue(2)").getValue(Boolean::class.java); ---- [NOTE] @@ -1103,7 +1112,7 @@ expression-based `matches` operator. The following listing shows examples of bot boolean trueValue = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); - //evaluates to false + // evaluates to false boolean falseValue = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); ---- @@ -1118,14 +1127,14 @@ expression-based `matches` operator. The following listing shows examples of bot val trueValue = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) - //evaluates to false + // evaluates to false val falseValue = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) ---- -CAUTION: Be careful with primitive types, as they are immediately boxed up to the wrapper type, -so `1 instanceof T(int)` evaluates to `false` while `1 instanceof T(Integer)` -evaluates to `true`, as expected. +CAUTION: Be careful with primitive types, as they are immediately boxed up to their +wrapper types. For example, `1 instanceof T(int)` evaluates to `false`, while +`1 instanceof T(Integer)` evaluates to `true`, as expected. Each symbolic operator can also be specified as a purely alphabetic equivalent. This avoids problems where the symbols used have special meaning for the document type in @@ -1153,7 +1162,7 @@ SpEL supports the following logical operators: * `or` (`||`) * `not` (`!`) -The following example shows how to use the logical operators +The following example shows how to use the logical operators: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1220,10 +1229,11 @@ The following example shows how to use the logical operators [[expressions-operators-mathematical]] ==== Mathematical Operators -You can use the addition operator on both numbers and strings. You can use the subtraction, multiplication, -and division operators only on numbers. You can also use -the modulus (%) and exponential power (^) operators. Standard operator precedence is enforced. The -following example shows the mathematical operators in use: +You can use the addition operator (`+`) on both numbers and strings. You can use the +subtraction (`-`), multiplication (`*`), and division (`/`) operators only on numbers. +You can also use the modulus (`%`) and exponential power (`^`) operators on numbers. +Standard operator precedence is enforced. The following example shows the mathematical +operators in use: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1294,9 +1304,9 @@ following example shows the mathematical operators in use: [[expressions-assignment]] ==== The Assignment Operator -To setting a property, use the assignment operator (`=`). This is typically -done within a call to `setValue` but can also be done inside a call to `getValue`. The -following listing shows both ways to use the assignment operator: +To set a property, use the assignment operator (`=`). This is typically done within a +call to `setValue` but can also be done inside a call to `getValue`. The following +listing shows both ways to use the assignment operator: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1331,9 +1341,9 @@ You can use the special `T` operator to specify an instance of `java.lang.Class` type). Static methods are invoked by using this operator as well. The `StandardEvaluationContext` uses a `TypeLocator` to find types, and the `StandardTypeLocator` (which can be replaced) is built with an understanding of the -`java.lang` package. This means that `T()` references to types within `java.lang` do not need to be -fully qualified, but all other type references must be. The following example shows how -to use the `T` operator: +`java.lang` package. This means that `T()` references to types within the `java.lang` +package do not need to be fully qualified, but all other type references must be. The +following example shows how to use the `T` operator: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1363,9 +1373,10 @@ to use the `T` operator: [[expressions-constructors]] === Constructors -You can invoke constructors by using the `new` operator. You should use the fully qualified class name -for all but the primitive types (`int`, `float`, and so on) and String. The following -example shows how to use the `new` operator to invoke constructors: +You can invoke constructors by using the `new` operator. You should use the fully +qualified class name for all types except those located in the `java.lang` package +(`Integer`, `Float`, `String`, and so on). The following example shows how to use the +`new` operator to invoke constructors: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1374,7 +1385,7 @@ example shows how to use the `new` operator to invoke constructors: "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor.class); - //create new inventor instance within add method of List + // create new Inventor instance within the add() method of List p.parseExpression( "Members.add(new org.spring.samples.spel.inventor.Inventor( 'Albert Einstein', 'German'))").getValue(societyContext); @@ -1386,7 +1397,7 @@ example shows how to use the `new` operator to invoke constructors: "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor::class.java) - //create new inventor instance within add method of List + // create new Inventor instance within the add() method of List p.parseExpression( "Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))") .getValue(societyContext) @@ -1673,7 +1684,7 @@ ternary operator. === The Elvis Operator The Elvis operator is a shortening of the ternary operator syntax and is used in the -http://www.groovy-lang.org/operators.html#_elvis_operator[Groovy] language. +https://www.groovy-lang.org/operators.html#_elvis_operator[Groovy] language. With the ternary operator syntax, you usually have to repeat a variable twice, as the following example shows: @@ -1752,7 +1763,7 @@ This will inject a system property `pop3.port` if it is defined or 25 if not. === Safe Navigation Operator The safe navigation operator is used to avoid a `NullPointerException` and comes from -the http://www.groovy-lang.org/operators.html#_safe_navigation_operator[Groovy] +the https://www.groovy-lang.org/operators.html#_safe_navigation_operator[Groovy] language. Typically, when you have a reference to an object, you might need to verify that it is not null before accessing methods or properties of the object. To avoid this, the safe navigation operator returns null instead of throwing an exception. The following @@ -1800,7 +1811,7 @@ Selection is a powerful expression language feature that lets you transform a source collection into another collection by selecting from its entries. Selection uses a syntax of `.?[selectionExpression]`. It filters the collection and -returns a new collection that contain a subset of the original elements. For example, +returns a new collection that contains a subset of the original elements. For example, selection lets us easily get a list of Serbian inventors, as the following example shows: [source,java,indent=0,subs="verbatim,quotes",role="primary"] @@ -1816,14 +1827,14 @@ selection lets us easily get a list of Serbian inventors, as the following examp "members.?[nationality == 'Serbian']").getValue(societyContext) as List ---- -Selection is possible upon both lists and maps. For a list, the selection -criteria is evaluated against each individual list element. Against a map, the -selection criteria is evaluated against each map entry (objects of the Java type -`Map.Entry`). Each map entry has its key and value accessible as properties for use in -the selection. +Selection is supported for arrays and anything that implements `java.lang.Iterable` or +`java.util.Map`. For a list or array, the selection criteria is evaluated against each +individual element. Against a map, the selection criteria is evaluated against each map +entry (objects of the Java type `Map.Entry`). Each map entry has its `key` and `value` +accessible as properties for use in the selection. -The following expression returns a new map that consists of those elements of the original map -where the entry value is less than 27: +The following expression returns a new map that consists of those elements of the +original map where the entry's value is less than 27: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1836,9 +1847,8 @@ where the entry value is less than 27: val newMap = parser.parseExpression("map.?[value<27]").getValue() ---- - -In addition to returning all the selected elements, you can retrieve only the -first or the last value. To obtain the first entry matching the selection, the syntax is +In addition to returning all the selected elements, you can retrieve only the first or +the last element. To obtain the first element matching the selection, the syntax is `.^[selectionExpression]`. To obtain the last matching selection, the syntax is `.$[selectionExpression]`. @@ -1847,11 +1857,11 @@ first or the last value. To obtain the first entry matching the selection, the s [[expressions-collection-projection]] === Collection Projection -Projection lets a collection drive the evaluation of a sub-expression, and the -result is a new collection. The syntax for projection is `.![projectionExpression]`. For -example, suppose we have a list of inventors but want the list of -cities where they were born. Effectively, we want to evaluate 'placeOfBirth.city' for -every entry in the inventor list. The following example uses projection to do so: +Projection lets a collection drive the evaluation of a sub-expression, and the result is +a new collection. The syntax for projection is `.![projectionExpression]`. For example, +suppose we have a list of inventors but want the list of cities where they were born. +Effectively, we want to evaluate 'placeOfBirth.city' for every entry in the inventor +list. The following example uses projection to do so: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1866,7 +1876,8 @@ every entry in the inventor list. The following example uses projection to do so val placesOfBirth = parser.parseExpression("members.![placeOfBirth.city]") as List<*> ---- -You can also use a map to drive projection and, in this case, the projection expression is +Projection is supported for arrays and anything that implements `java.lang.Iterable` or +`java.util.Map`. When using a map to drive projection, the projection expression is evaluated against each entry in the map (represented as a Java `Map.Entry`). The result of a projection across a map is a list that consists of the evaluation of the projection expression against each map entry. diff --git a/src/docs/asciidoc/core/core-null-safety.adoc b/framework-docs/src/docs/asciidoc/core/core-null-safety.adoc similarity index 100% rename from src/docs/asciidoc/core/core-null-safety.adoc rename to framework-docs/src/docs/asciidoc/core/core-null-safety.adoc diff --git a/framework-docs/src/docs/asciidoc/core/core-resources.adoc b/framework-docs/src/docs/asciidoc/core/core-resources.adoc new file mode 100644 index 000000000000..4aaa4575c875 --- /dev/null +++ b/framework-docs/src/docs/asciidoc/core/core-resources.adoc @@ -0,0 +1,965 @@ +[[resources]] += Resources + +This chapter covers how Spring handles resources and how you can work with resources in +Spring. It includes the following topics: + +* <> +* <> +* <> +* <> +* <> +* <> +* <> +* <> + + + + +[[resources-introduction]] +== Introduction + +Java's standard `java.net.URL` class and standard handlers for various URL prefixes, +unfortunately, are not quite adequate enough for all access to low-level resources. For +example, there is no standardized `URL` implementation that may be used to access a +resource that needs to be obtained from the classpath or relative to a +`ServletContext`. While it is possible to register new handlers for specialized `URL` +prefixes (similar to existing handlers for prefixes such as `http:`), this is generally +quite complicated, and the `URL` interface still lacks some desirable functionality, +such as a method to check for the existence of the resource being pointed to. + + + + +[[resources-resource]] +== The `Resource` Interface + +Spring's `Resource` interface located in the `org.springframework.core.io.` package is +meant to be a more capable interface for abstracting access to low-level resources. The +following listing provides an overview of the `Resource` interface. See the +{api-spring-framework}/core/io/Resource.html[`Resource`] javadoc for further details. + + +[source,java,indent=0,subs="verbatim,quotes"] +---- + public interface Resource extends InputStreamSource { + + boolean exists(); + + boolean isReadable(); + + boolean isOpen(); + + boolean isFile(); + + URL getURL() throws IOException; + + URI getURI() throws IOException; + + File getFile() throws IOException; + + ReadableByteChannel readableChannel() throws IOException; + + long contentLength() throws IOException; + + long lastModified() throws IOException; + + Resource createRelative(String relativePath) throws IOException; + + String getFilename(); + + String getDescription(); + } +---- + +As the definition of the `Resource` interface shows, it extends the `InputStreamSource` +interface. The following listing shows the definition of the `InputStreamSource` +interface: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + public interface InputStreamSource { + + InputStream getInputStream() throws IOException; + } +---- + +Some of the most important methods from the `Resource` interface are: + +* `getInputStream()`: Locates and opens the resource, returning an `InputStream` for + reading from the resource. It is expected that each invocation returns a fresh + `InputStream`. It is the responsibility of the caller to close the stream. +* `exists()`: Returns a `boolean` indicating whether this resource actually exists in + physical form. +* `isOpen()`: Returns a `boolean` indicating whether this resource represents a handle + with an open stream. If `true`, the `InputStream` cannot be read multiple times and + must be read once only and then closed to avoid resource leaks. Returns `false` for + all usual resource implementations, with the exception of `InputStreamResource`. +* `getDescription()`: Returns a description for this resource, to be used for error + output when working with the resource. This is often the fully qualified file name or + the actual URL of the resource. + +Other methods let you obtain an actual `URL` or `File` object representing the +resource (if the underlying implementation is compatible and supports that +functionality). + +Some implementations of the `Resource` interface also implement the extended +{api-spring-framework}/core/io/WritableResource.html[`WritableResource`] interface +for a resource that supports writing to it. + +Spring itself uses the `Resource` abstraction extensively, as an argument type in +many method signatures when a resource is needed. Other methods in some Spring APIs +(such as the constructors to various `ApplicationContext` implementations) take a +`String` which in unadorned or simple form is used to create a `Resource` appropriate to +that context implementation or, via special prefixes on the `String` path, let the +caller specify that a specific `Resource` implementation must be created and used. + +While the `Resource` interface is used a lot with Spring and by Spring, it is actually +very convenient to use as a general utility class by itself in your own code, for access +to resources, even when your code does not know or care about any other parts of Spring. +While this couples your code to Spring, it really only couples it to this small set of +utility classes, which serves as a more capable replacement for `URL` and can be +considered equivalent to any other library you would use for this purpose. + +NOTE: The `Resource` abstraction does not replace functionality. It wraps it where +possible. For example, a `UrlResource` wraps a URL and uses the wrapped `URL` to do its +work. + + + + +[[resources-implementations]] +== Built-in `Resource` Implementations + +Spring includes several built-in `Resource` implementations: + +* <> +* <> +* <> +* <> +* <> +* <> +* <> + +For a complete list of `Resource` implementations available in Spring, consult the +"All Known Implementing Classes" section of the +{api-spring-framework}/core/io/Resource.html[`Resource`] javadoc. + + + +[[resources-implementations-urlresource]] +=== `UrlResource` + +`UrlResource` wraps a `java.net.URL` and can be used to access any object that is +normally accessible with a URL, such as files, an HTTPS target, an FTP target, and +others. All URLs have a standardized `String` representation, such that appropriate +standardized prefixes are used to indicate one URL type from another. This includes +`file:` for accessing filesystem paths, `https:` for accessing resources through the +HTTPS protocol, `ftp:` for accessing resources through FTP, and others. + +A `UrlResource` is created by Java code by explicitly using the `UrlResource` constructor +but is often created implicitly when you call an API method that takes a `String` +argument meant to represent a path. For the latter case, a JavaBeans `PropertyEditor` +ultimately decides which type of `Resource` to create. If the path string contains a +well-known (to property editor, that is) prefix (such as `classpath:`), it creates an +appropriate specialized `Resource` for that prefix. However, if it does not recognize the +prefix, it assumes the string is a standard URL string and creates a `UrlResource`. + + + +[[resources-implementations-classpathresource]] +=== `ClassPathResource` + +This class represents a resource that should be obtained from the classpath. It uses +either the thread context class loader, a given class loader, or a given class for +loading resources. + +This `Resource` implementation supports resolution as a `java.io.File` if the class path +resource resides in the file system but not for classpath resources that reside in a +jar and have not been expanded (by the servlet engine or whatever the environment is) +to the filesystem. To address this, the various `Resource` implementations always support +resolution as a `java.net.URL`. + +A `ClassPathResource` is created by Java code by explicitly using the `ClassPathResource` +constructor but is often created implicitly when you call an API method that takes a +`String` argument meant to represent a path. For the latter case, a JavaBeans +`PropertyEditor` recognizes the special prefix, `classpath:`, on the string path and +creates a `ClassPathResource` in that case. + + + +[[resources-implementations-filesystemresource]] +=== `FileSystemResource` + +This is a `Resource` implementation for `java.io.File` handles. It also supports +`java.nio.file.Path` handles, applying Spring's standard String-based path +transformations but performing all operations via the `java.nio.file.Files` API. For pure +`java.nio.path.Path` based support use a `PathResource` instead. `FileSystemResource` +supports resolution as a `File` and as a `URL`. + + + +[[resources-implementations-pathresource]] +=== `PathResource` + +This is a `Resource` implementation for `java.nio.file.Path` handles, performing all +operations and transformations via the `Path` API. It supports resolution as a `File` and +as a `URL` and also implements the extended `WritableResource` interface. `PathResource` +is effectively a pure `java.nio.path.Path` based alternative to `FileSystemResource` with +different `createRelative` behavior. + + + +[[resources-implementations-servletcontextresource]] +=== `ServletContextResource` + +This is a `Resource` implementation for `ServletContext` resources that interprets +relative paths within the relevant web application's root directory. + +It always supports stream access and URL access but allows `java.io.File` access only +when the web application archive is expanded and the resource is physically on the +filesystem. Whether or not it is expanded and on the filesystem or accessed +directly from the JAR or somewhere else like a database (which is conceivable) is actually +dependent on the Servlet container. + + + +[[resources-implementations-inputstreamresource]] +=== `InputStreamResource` + +An `InputStreamResource` is a `Resource` implementation for a given `InputStream`. It +should be used only if no specific `Resource` implementation is applicable. In +particular, prefer `ByteArrayResource` or any of the file-based `Resource` +implementations where possible. + +In contrast to other `Resource` implementations, this is a descriptor for an +already-opened resource. Therefore, it returns `true` from `isOpen()`. Do not use it if +you need to keep the resource descriptor somewhere or if you need to read a stream +multiple times. + + + +[[resources-implementations-bytearrayresource]] +=== `ByteArrayResource` + +This is a `Resource` implementation for a given byte array. It creates a +`ByteArrayInputStream` for the given byte array. + +It is useful for loading content from any given byte array without having to resort to a +single-use `InputStreamResource`. + + + + +[[resources-resourceloader]] +== The `ResourceLoader` Interface + +The `ResourceLoader` interface is meant to be implemented by objects that can return +(that is, load) `Resource` instances. The following listing shows the `ResourceLoader` +interface definition: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + public interface ResourceLoader { + + Resource getResource(String location); + + ClassLoader getClassLoader(); + } +---- + +All application contexts implement the `ResourceLoader` interface. Therefore, all +application contexts may be used to obtain `Resource` instances. + +When you call `getResource()` on a specific application context, and the location path +specified doesn't have a specific prefix, you get back a `Resource` type that is +appropriate to that particular application context. For example, assume the following +snippet of code was run against a `ClassPathXmlApplicationContext` instance: + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- + Resource template = ctx.getResource("some/resource/path/myTemplate.txt"); +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val template = ctx.getResource("some/resource/path/myTemplate.txt") +---- + +Against a `ClassPathXmlApplicationContext`, that code returns a `ClassPathResource`. If +the same method were run against a `FileSystemXmlApplicationContext` instance, it would +return a `FileSystemResource`. For a `WebApplicationContext`, it would return a +`ServletContextResource`. It would similarly return appropriate objects for each context. + +As a result, you can load resources in a fashion appropriate to the particular application +context. + +On the other hand, you may also force `ClassPathResource` to be used, regardless of the +application context type, by specifying the special `classpath:` prefix, as the following +example shows: + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- + Resource template = ctx.getResource("classpath:some/resource/path/myTemplate.txt"); +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val template = ctx.getResource("classpath:some/resource/path/myTemplate.txt") +---- + +Similarly, you can force a `UrlResource` to be used by specifying any of the standard +`java.net.URL` prefixes. The following examples use the `file` and `https` prefixes: + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- + Resource template = ctx.getResource("file:///some/resource/path/myTemplate.txt"); +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val template = ctx.getResource("file:///some/resource/path/myTemplate.txt") +---- + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- + Resource template = ctx.getResource("https://myhost.com/resource/path/myTemplate.txt"); +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val template = ctx.getResource("https://myhost.com/resource/path/myTemplate.txt") +---- + +The following table summarizes the strategy for converting `String` objects to `Resource` +objects: + +[[resources-resource-strings]] +.Resource strings +|=== +| Prefix| Example| Explanation + +| classpath: +| `classpath:com/myapp/config.xml` +| Loaded from the classpath. + +| file: +| `\file:///data/config.xml` +| Loaded as a `URL` from the filesystem. See also <>. + +| https: +| `\https://myserver/logo.png` +| Loaded as a `URL`. + +| (none) +| `/data/config.xml` +| Depends on the underlying `ApplicationContext`. +|=== + + + + +[[resources-resourcepatternresolver]] +== The `ResourcePatternResolver` Interface + +The `ResourcePatternResolver` interface is an extension to the `ResourceLoader` interface +which defines a strategy for resolving a location pattern (for example, an Ant-style path +pattern) into `Resource` objects. + +[source,java,indent=0,subs="verbatim,quotes"] +---- + public interface ResourcePatternResolver extends ResourceLoader { + + String CLASSPATH_ALL_URL_PREFIX = "classpath*:"; + + Resource[] getResources(String locationPattern) throws IOException; + } +---- + +As can be seen above, this interface also defines a special `classpath*:` resource prefix +for all matching resources from the class path. Note that the resource location is +expected to be a path without placeholders in this case -- for example, +`classpath*:/config/beans.xml`. JAR files or different directories in the class path can +contain multiple files with the same path and the same name. See +<> and its subsections for further details +on wildcard support with the `classpath*:` resource prefix. + +A passed-in `ResourceLoader` (for example, one supplied via +<> semantics) can be checked whether +it implements this extended interface too. + +`PathMatchingResourcePatternResolver` is a standalone implementation that is usable +outside an `ApplicationContext` and is also used by `ResourceArrayPropertyEditor` for +populating `Resource[]` bean properties. `PathMatchingResourcePatternResolver` is able to +resolve a specified resource location path into one or more matching `Resource` objects. +The source path may be a simple path which has a one-to-one mapping to a target +`Resource`, or alternatively may contain the special `classpath*:` prefix and/or internal +Ant-style regular expressions (matched using Spring's +`org.springframework.util.AntPathMatcher` utility). Both of the latter are effectively +wildcards. + +[NOTE] +==== +The default `ResourceLoader` in any standard `ApplicationContext` is in fact an instance +of `PathMatchingResourcePatternResolver` which implements the `ResourcePatternResolver` +interface. The same is true for the `ApplicationContext` instance itself which also +implements the `ResourcePatternResolver` interface and delegates to the default +`PathMatchingResourcePatternResolver`. +==== + + + + +[[resources-resourceloaderaware]] +== The `ResourceLoaderAware` Interface + +The `ResourceLoaderAware` interface is a special callback interface which identifies +components that expect to be provided a `ResourceLoader` reference. The following listing +shows the definition of the `ResourceLoaderAware` interface: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + public interface ResourceLoaderAware { + + void setResourceLoader(ResourceLoader resourceLoader); + } +---- + +When a class implements `ResourceLoaderAware` and is deployed into an application context +(as a Spring-managed bean), it is recognized as `ResourceLoaderAware` by the application +context. The application context then invokes `setResourceLoader(ResourceLoader)`, +supplying itself as the argument (remember, all application contexts in Spring implement +the `ResourceLoader` interface). + +Since an `ApplicationContext` is a `ResourceLoader`, the bean could also implement the +`ApplicationContextAware` interface and use the supplied application context directly to +load resources. However, in general, it is better to use the specialized `ResourceLoader` +interface if that is all you need. The code would be coupled only to the resource loading +interface (which can be considered a utility interface) and not to the whole Spring +`ApplicationContext` interface. + +In application components, you may also rely upon autowiring of the `ResourceLoader` as +an alternative to implementing the `ResourceLoaderAware` interface. The _traditional_ +`constructor` and `byType` autowiring modes (as described in <>) +are capable of providing a `ResourceLoader` for either a constructor argument or a +setter method parameter, respectively. For more flexibility (including the ability to +autowire fields and multiple parameter methods), consider using the annotation-based +autowiring features. In that case, the `ResourceLoader` is autowired into a field, +constructor argument, or method parameter that expects the `ResourceLoader` type as long +as the field, constructor, or method in question carries the `@Autowired` annotation. +For more information, see <>. + +NOTE: To load one or more `Resource` objects for a resource path that contains wildcards +or makes use of the special `classpath*:` resource prefix, consider having an instance of +<> autowired into your +application components instead of `ResourceLoader`. + + + + +[[resources-as-dependencies]] +== Resources as Dependencies + +If the bean itself is going to determine and supply the resource path through some sort +of dynamic process, it probably makes sense for the bean to use the `ResourceLoader` or +`ResourcePatternResolver` interface to load resources. For example, consider the loading +of a template of some sort, where the specific resource that is needed depends on the +role of the user. If the resources are static, it makes sense to eliminate the use of the +`ResourceLoader` interface (or `ResourcePatternResolver` interface) completely, have the +bean expose the `Resource` properties it needs, and expect them to be injected into it. + +What makes it trivial to then inject these properties is that all application contexts +register and use a special JavaBeans `PropertyEditor`, which can convert `String` paths +to `Resource` objects. For example, the following `MyBean` class has a `template` +property of type `Resource`. + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- + package example; + + public class MyBean { + + private Resource template; + + public setTemplate(Resource template) { + this.template = template; + } + + // ... + } +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + class MyBean(var template: Resource) +---- + +In an XML configuration file, the `template` property can be configured with a simple +string for that resource, as the following example shows: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + +---- + +Note that the resource path has no prefix. Consequently, because the application context +itself is going to be used as the `ResourceLoader`, the resource is loaded through a +`ClassPathResource`, a `FileSystemResource`, or a `ServletContextResource`, depending on +the exact type of the application context. + +If you need to force a specific `Resource` type to be used, you can use a prefix. The +following two examples show how to force a `ClassPathResource` and a `UrlResource` (the +latter being used to access a file in the filesystem): + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + +---- + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + +---- + +If the `MyBean` class is refactored for use with annotation-driven configuration, the +path to `myTemplate.txt` can be stored under a key named `template.path` -- for example, +in a properties file made available to the Spring `Environment` (see +<>). The template path can then be referenced via the `@Value` +annotation using a property placeholder (see <>). Spring will +retrieve the value of the template path as a string, and a special `PropertyEditor` will +convert the string to a `Resource` object to be injected into the `MyBean` constructor. +The following example demonstrates how to achieve this. + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- + @Component + public class MyBean { + + private final Resource template; + + public MyBean(@Value("${template.path}") Resource template) { + this.template = template; + } + + // ... + } +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @Component + class MyBean(@Value("\${template.path}") private val template: Resource) +---- + +If we want to support multiple templates discovered under the same path in multiple +locations in the classpath -- for example, in multiple jars in the classpath -- we can +use the special `classpath*:` prefix and wildcarding to define a `templates.path` key as +`classpath*:/config/templates/*.txt`. If we redefine the `MyBean` class as follows, +Spring will convert the template path pattern into an array of `Resource` objects that +can be injected into the `MyBean` constructor. + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- + @Component + public class MyBean { + + private final Resource[] templates; + + public MyBean(@Value("${templates.path}") Resource[] templates) { + this.templates = templates; + } + + // ... + } +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @Component + class MyBean(@Value("\${templates.path}") private val templates: Resource[]) +---- + + + + +[[resources-app-ctx]] +== Application Contexts and Resource Paths + +This section covers how to create application contexts with resources, including shortcuts +that work with XML, how to use wildcards, and other details. + + + +[[resources-app-ctx-construction]] +=== Constructing Application Contexts + +An application context constructor (for a specific application context type) generally +takes a string or array of strings as the location paths of the resources, such as +XML files that make up the definition of the context. + +When such a location path does not have a prefix, the specific `Resource` type built from +that path and used to load the bean definitions depends on and is appropriate to the +specific application context. For example, consider the following example, which creates a +`ClassPathXmlApplicationContext`: + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- + ApplicationContext ctx = new ClassPathXmlApplicationContext("conf/appContext.xml"); +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val ctx = ClassPathXmlApplicationContext("conf/appContext.xml") +---- + +The bean definitions are loaded from the classpath, because a `ClassPathResource` is +used. However, consider the following example, which creates a `FileSystemXmlApplicationContext`: + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- + ApplicationContext ctx = + new FileSystemXmlApplicationContext("conf/appContext.xml"); +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val ctx = FileSystemXmlApplicationContext("conf/appContext.xml") +---- + +Now the bean definitions are loaded from a filesystem location (in this case, relative to +the current working directory). + +Note that the use of the special `classpath` prefix or a standard URL prefix on the +location path overrides the default type of `Resource` created to load the bean +definitions. Consider the following example: + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- + ApplicationContext ctx = + new FileSystemXmlApplicationContext("classpath:conf/appContext.xml"); +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val ctx = FileSystemXmlApplicationContext("classpath:conf/appContext.xml") +---- + +Using `FileSystemXmlApplicationContext` loads the bean definitions from the classpath. +However, it is still a `FileSystemXmlApplicationContext`. If it is subsequently used as a +`ResourceLoader`, any unprefixed paths are still treated as filesystem paths. + + +[[resources-app-ctx-classpathxml]] +==== Constructing `ClassPathXmlApplicationContext` Instances -- Shortcuts + +The `ClassPathXmlApplicationContext` exposes a number of constructors to enable +convenient instantiation. The basic idea is that you can supply merely a string array +that contains only the filenames of the XML files themselves (without the leading path +information) and also supply a `Class`. The `ClassPathXmlApplicationContext` then derives +the path information from the supplied class. + +Consider the following directory layout: + +[literal,subs="verbatim,quotes"] +---- +com/ + example/ + services.xml + repositories.xml + MessengerService.class +---- + +The following example shows how a `ClassPathXmlApplicationContext` instance composed of +the beans defined in files named `services.xml` and `repositories.xml` (which are on the +classpath) can be instantiated: + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- + ApplicationContext ctx = new ClassPathXmlApplicationContext( + new String[] {"services.xml", "repositories.xml"}, MessengerService.class); +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val ctx = ClassPathXmlApplicationContext(arrayOf("services.xml", "repositories.xml"), MessengerService::class.java) +---- + +See the {api-spring-framework}/context/support/ClassPathXmlApplicationContext.html[`ClassPathXmlApplicationContext`] +javadoc for details on the various constructors. + + + +[[resources-app-ctx-wildcards-in-resource-paths]] +=== Wildcards in Application Context Constructor Resource Paths + +The resource paths in application context constructor values may be simple paths (as +shown earlier), each of which has a one-to-one mapping to a target `Resource` or, +alternately, may contain the special `classpath*:` prefix or internal Ant-style patterns +(matched by using Spring's `PathMatcher` utility). Both of the latter are effectively +wildcards. + +One use for this mechanism is when you need to do component-style application assembly. All +components can _publish_ context definition fragments to a well-known location path, and, +when the final application context is created using the same path prefixed with +`classpath*:`, all component fragments are automatically picked up. + +Note that this wildcarding is specific to the use of resource paths in application context +constructors (or when you use the `PathMatcher` utility class hierarchy directly) and is +resolved at construction time. It has nothing to do with the `Resource` type itself. +You cannot use the `classpath*:` prefix to construct an actual `Resource`, as +a resource points to just one resource at a time. + + +[[resources-app-ctx-ant-patterns-in-paths]] +==== Ant-style Patterns + +Path locations can contain Ant-style patterns, as the following example shows: + +[literal,subs="verbatim,quotes"] +---- +/WEB-INF/\*-context.xml +com/mycompany/\**/applicationContext.xml +file:C:/some/path/\*-context.xml +classpath:com/mycompany/**/applicationContext.xml +---- + +When the path location contains an Ant-style pattern, the resolver follows a more complex +procedure to try to resolve the wildcard. It produces a `Resource` for the path up to the +last non-wildcard segment and obtains a URL from it. If this URL is not a `jar:` URL or +container-specific variant (such as `zip:` in WebLogic, `wsjar` in WebSphere, and so on), +a `java.io.File` is obtained from it and used to resolve the wildcard by traversing the +filesystem. In the case of a jar URL, the resolver either gets a +`java.net.JarURLConnection` from it or manually parses the jar URL and then traverses the +contents of the jar file to resolve the wildcards. + +[[resources-app-ctx-portability]] +===== Implications on Portability + +If the specified path is already a `file` URL (either implicitly because the base +`ResourceLoader` is a filesystem one or explicitly), wildcarding is guaranteed to +work in a completely portable fashion. + +If the specified path is a `classpath` location, the resolver must obtain the last +non-wildcard path segment URL by making a `Classloader.getResource()` call. Since this +is just a node of the path (not the file at the end), it is actually undefined (in the +`ClassLoader` javadoc) exactly what sort of a URL is returned in this case. In practice, +it is always a `java.io.File` representing the directory (where the classpath resource +resolves to a filesystem location) or a jar URL of some sort (where the classpath resource +resolves to a jar location). Still, there is a portability concern on this operation. + +If a jar URL is obtained for the last non-wildcard segment, the resolver must be able to +get a `java.net.JarURLConnection` from it or manually parse the jar URL, to be able to +walk the contents of the jar and resolve the wildcard. This does work in most environments +but fails in others, and we strongly recommend that the wildcard resolution of resources +coming from jars be thoroughly tested in your specific environment before you rely on it. + + +[[resources-classpath-wildcards]] +==== The `classpath*:` Prefix + +When constructing an XML-based application context, a location string may use the +special `classpath*:` prefix, as the following example shows: + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- + ApplicationContext ctx = + new ClassPathXmlApplicationContext("classpath*:conf/appContext.xml"); +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val ctx = ClassPathXmlApplicationContext("classpath*:conf/appContext.xml") +---- + +This special prefix specifies that all classpath resources that match the given name +must be obtained (internally, this essentially happens through a call to +`ClassLoader.getResources(...)`) and then merged to form the final application +context definition. + +NOTE: The wildcard classpath relies on the `getResources()` method of the underlying +`ClassLoader`. As most application servers nowadays supply their own `ClassLoader` +implementation, the behavior might differ, especially when dealing with jar files. A +simple test to check if `classpath*` works is to use the `ClassLoader` to load a file from +within a jar on the classpath: +`getClass().getClassLoader().getResources("")`. Try this test with +files that have the same name but reside in two different locations -- for example, files +with the same name and same path but in different jars on the classpath. In case an +inappropriate result is returned, check the application server documentation for settings +that might affect the `ClassLoader` behavior. + +You can also combine the `classpath*:` prefix with a `PathMatcher` pattern in the +rest of the location path (for example, `classpath*:META-INF/*-beans.xml`). In this +case, the resolution strategy is fairly simple: A `ClassLoader.getResources()` call is +used on the last non-wildcard path segment to get all the matching resources in the +class loader hierarchy and then, off each resource, the same `PathMatcher` resolution +strategy described earlier is used for the wildcard subpath. + + +[[resources-wildcards-in-path-other-stuff]] +==== Other Notes Relating to Wildcards + +Note that `classpath*:`, when combined with Ant-style patterns, only works +reliably with at least one root directory before the pattern starts, unless the actual +target files reside in the file system. This means that a pattern such as +`classpath*:*.xml` might not retrieve files from the root of jar files but rather only +from the root of expanded directories. + +Spring's ability to retrieve classpath entries originates from the JDK's +`ClassLoader.getResources()` method, which only returns file system locations for an +empty string (indicating potential roots to search). Spring evaluates +`URLClassLoader` runtime configuration and the `java.class.path` manifest in jar files +as well, but this is not guaranteed to lead to portable behavior. + +[NOTE] +==== +The scanning of classpath packages requires the presence of corresponding directory +entries in the classpath. When you build JARs with Ant, do not activate the `files-only` +switch of the JAR task. Also, classpath directories may not get exposed based on security +policies in some environments -- for example, stand-alone applications on JDK 1.7.0_45 +and higher (which requires 'Trusted-Library' to be set up in your manifests. See +https://stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources). + +On JDK 9's module path (Jigsaw), Spring's classpath scanning generally works as expected. +Putting resources into a dedicated directory is highly recommendable here as well, +avoiding the aforementioned portability problems with searching the jar file root level. +==== + +Ant-style patterns with `classpath:` resources are not guaranteed to find matching +resources if the root package to search is available in multiple classpath locations. +Consider the following example of a resource location: + +[literal,subs="verbatim,quotes"] +---- +com/mycompany/package1/service-context.xml +---- + +Now consider an Ant-style path that someone might use to try to find that file: + +[literal,subs="verbatim,quotes"] +---- +classpath:com/mycompany/**/service-context.xml +---- + +Such a resource may exist in only one location in the classpath, but when a path such as +the preceding example is used to try to resolve it, the resolver works off the (first) +URL returned by `getResource("com/mycompany");`. If this base package node exists in +multiple `ClassLoader` locations, the desired resource may not exist in the first +location found. Therefore, in such cases you should prefer using `classpath*:` with the +same Ant-style pattern, which searches all classpath locations that contain the +`com.mycompany` base package: `classpath*:com/mycompany/**/service-context.xml`. + + + +[[resources-filesystemresource-caveats]] +=== `FileSystemResource` Caveats + +A `FileSystemResource` that is not attached to a `FileSystemApplicationContext` (that +is, when a `FileSystemApplicationContext` is not the actual `ResourceLoader`) treats +absolute and relative paths as you would expect. Relative paths are relative to the +current working directory, while absolute paths are relative to the root of the +filesystem. + +For backwards compatibility (historical) reasons however, this changes when the +`FileSystemApplicationContext` is the `ResourceLoader`. The +`FileSystemApplicationContext` forces all attached `FileSystemResource` instances +to treat all location paths as relative, whether they start with a leading slash or not. +In practice, this means the following examples are equivalent: + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- + ApplicationContext ctx = + new FileSystemXmlApplicationContext("conf/context.xml"); +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val ctx = FileSystemXmlApplicationContext("conf/context.xml") +---- + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- + ApplicationContext ctx = + new FileSystemXmlApplicationContext("/conf/context.xml"); +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val ctx = FileSystemXmlApplicationContext("/conf/context.xml") +---- + +The following examples are also equivalent (even though it would make sense for them to be different, as one +case is relative and the other absolute): + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- + FileSystemXmlApplicationContext ctx = ...; + ctx.getResource("some/resource/path/myTemplate.txt"); +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val ctx: FileSystemXmlApplicationContext = ... + ctx.getResource("some/resource/path/myTemplate.txt") +---- + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- + FileSystemXmlApplicationContext ctx = ...; + ctx.getResource("/some/resource/path/myTemplate.txt"); +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val ctx: FileSystemXmlApplicationContext = ... + ctx.getResource("/some/resource/path/myTemplate.txt") +---- + +In practice, if you need true absolute filesystem paths, you should avoid using +absolute paths with `FileSystemResource` or `FileSystemXmlApplicationContext` and +force the use of a `UrlResource` by using the `file:` URL prefix. The following examples +show how to do so: + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- + // actual context type doesn't matter, the Resource will always be UrlResource + ctx.getResource("file:///some/resource/path/myTemplate.txt"); +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + // actual context type doesn't matter, the Resource will always be UrlResource + ctx.getResource("file:///some/resource/path/myTemplate.txt") +---- + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- + // force this FileSystemXmlApplicationContext to load its definition via a UrlResource + ApplicationContext ctx = + new FileSystemXmlApplicationContext("file:///conf/context.xml"); +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + // force this FileSystemXmlApplicationContext to load its definition via a UrlResource + val ctx = FileSystemXmlApplicationContext("file:///conf/context.xml") +---- diff --git a/src/docs/asciidoc/core/core-spring-jcl.adoc b/framework-docs/src/docs/asciidoc/core/core-spring-jcl.adoc similarity index 100% rename from src/docs/asciidoc/core/core-spring-jcl.adoc rename to framework-docs/src/docs/asciidoc/core/core-spring-jcl.adoc diff --git a/src/docs/asciidoc/core/core-validation.adoc b/framework-docs/src/docs/asciidoc/core/core-validation.adoc similarity index 91% rename from src/docs/asciidoc/core/core-validation.adoc rename to framework-docs/src/docs/asciidoc/core/core-validation.adoc index dc8287172453..7afadfa8a97d 100644 --- a/src/docs/asciidoc/core/core-validation.adoc +++ b/framework-docs/src/docs/asciidoc/core/core-validation.adoc @@ -103,7 +103,7 @@ example implements `Validator` for `Person` instances: ---- class PersonValidator : Validator { - /** + /\** * This Validator validates only Person instances */ override fun supports(clazz: Class<*>): Boolean { @@ -215,7 +215,7 @@ as the following example shows: Validation errors are reported to the `Errors` object passed to the validator. In the case of Spring Web MVC, you can use the `` tag to inspect the error messages, but you can also inspect the `Errors` object yourself. More information about the -methods it offers can be found in the {api-spring-framework}validation/Errors.html[javadoc]. +methods it offers can be found in the {api-spring-framework}/validation/Errors.html[javadoc]. @@ -382,7 +382,7 @@ properties: ---- The following code snippets show some examples of how to retrieve and manipulate some of -the properties of instantiated `Companies` and `Employees`: +the properties of instantiated ``Company``s and ``Employee``s: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -500,8 +500,9 @@ the various `PropertyEditor` implementations that Spring provides: | `LocaleEditor` | Can resolve strings to `Locale` objects and vice-versa (the string format is - `[language]_[country]_[variant]`, same as the `toString()` method of - `Locale`). By default, registered by `BeanWrapperImpl`. + `[language]\_[country]_[variant]`, same as the `toString()` method of + `Locale`). Also accepts spaces as separators, as an alternative to underscores. + By default, registered by `BeanWrapperImpl`. | `PatternEditor` | Can resolve strings to `java.util.regex.Pattern` objects and vice-versa. @@ -541,10 +542,9 @@ com Note that you can also use the standard `BeanInfo` JavaBeans mechanism here as well (described to some extent -https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html[ -here]). The following example use the `BeanInfo` mechanism to -explicitly register one or more `PropertyEditor` instances with the properties of an -associated class: +https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html[here]). The +following example uses the `BeanInfo` mechanism to explicitly register one or more +`PropertyEditor` instances with the properties of an associated class: [literal,subs="verbatim,quotes"] ---- @@ -567,9 +567,10 @@ associates a `CustomNumberEditor` with the `age` property of the `Something` cla try { final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true); PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Something.class) { + @Override public PropertyEditor createPropertyEditor(Object bean) { return numberPE; - }; + } }; return new PropertyDescriptor[] { ageDescriptor }; } @@ -625,10 +626,10 @@ nested property setup, so we strongly recommend that you use it with the where it can be automatically detected and applied. Note that all bean factories and application contexts automatically use a number of -built-in property editors, through their use a `BeanWrapper` to +built-in property editors, through their use of a `BeanWrapper` to handle property conversions. The standard property editors that the `BeanWrapper` registers are listed in the <>. -Additionally, `ApplicationContexts` also override or add additional editors to handle +Additionally, ``ApplicationContext``s also override or add additional editors to handle resource lookups in a manner appropriate to the specific application context type. Standard JavaBeans `PropertyEditor` instances are used to convert property values @@ -789,10 +790,10 @@ The following example shows how to create your own `PropertyEditorRegistrar` imp See also the `org.springframework.beans.support.ResourceEditorRegistrar` for an example `PropertyEditorRegistrar` implementation. Notice how in its implementation of the -`registerCustomEditors(..)` method ,it creates new instances of each property editor. +`registerCustomEditors(..)` method, it creates new instances of each property editor. -The next example shows how to configure a `CustomEditorConfigurer` and inject an instance of our -`CustomPropertyEditorRegistrar` into it: +The next example shows how to configure a `CustomEditorConfigurer` and inject an instance +of our `CustomPropertyEditorRegistrar` into it: [source,xml,indent=0,subs="verbatim,quotes"] ---- @@ -808,50 +809,51 @@ The next example shows how to configure a `CustomEditorConfigurer` and inject an class="com.foo.editors.spring.CustomPropertyEditorRegistrar"/> ---- -Finally (and in a bit of a departure from the focus of this chapter for those of you -using <>), using `PropertyEditorRegistrars` in -conjunction with data-binding `Controllers` (such as `SimpleFormController`) can be very -convenient. The following example uses a `PropertyEditorRegistrar` in the -implementation of an `initBinder(..)` method: +Finally (and in a bit of a departure from the focus of this chapter) for those of you +using <>, using a `PropertyEditorRegistrar` in +conjunction with data-binding web controllers can be very convenient. The following +example uses a `PropertyEditorRegistrar` in the implementation of an `@InitBinder` method: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java ---- - public final class RegisterUserController extends SimpleFormController { + @Controller + public class RegisterUserController { private final PropertyEditorRegistrar customPropertyEditorRegistrar; - public RegisterUserController(PropertyEditorRegistrar propertyEditorRegistrar) { + RegisterUserController(PropertyEditorRegistrar propertyEditorRegistrar) { this.customPropertyEditorRegistrar = propertyEditorRegistrar; } - protected void initBinder(HttpServletRequest request, - ServletRequestDataBinder binder) throws Exception { + @InitBinder + void initBinder(WebDataBinder binder) { this.customPropertyEditorRegistrar.registerCustomEditors(binder); } - // other methods to do with registering a User + // other methods related to registering a User } ---- [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] .Kotlin ---- + @Controller class RegisterUserController( - private val customPropertyEditorRegistrar: PropertyEditorRegistrar) : SimpleFormController() { + private val customPropertyEditorRegistrar: PropertyEditorRegistrar) { - protected fun initBinder(request: HttpServletRequest, - binder: ServletRequestDataBinder) { + @InitBinder + fun initBinder(binder: WebDataBinder) { this.customPropertyEditorRegistrar.registerCustomEditors(binder) } - // other methods to do with registering a User + // other methods related to registering a User } ---- This style of `PropertyEditor` registration can lead to concise code (the implementation -of `initBinder(..)` is only one line long) and lets common `PropertyEditor` -registration code be encapsulated in a class and then shared amongst as many -`Controllers` as needed. +of the `@InitBinder` method is only one line long) and lets common `PropertyEditor` +registration code be encapsulated in a class and then shared amongst as many controllers +as needed. @@ -874,8 +876,7 @@ application where type conversion is needed. The SPI to implement type conversion logic is simple and strongly typed, as the following interface definition shows: -[source,java,indent=0,subs="verbatim,quotes",role="primary"] -.Java +[source,java,indent=0,subs="verbatim,quotes"] ---- package org.springframework.core.convert.converter; @@ -884,16 +885,6 @@ interface definition shows: T convert(S source); } ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] -.Kotlin ----- - package org.springframework.core.convert.converter - - interface Converter { - - fun convert(source: S): T - } ----- To create your own converter, implement the `Converter` interface and parameterize `S` as the type you are converting from and `T` as the type you are converting to. You can also transparently apply such a @@ -910,8 +901,7 @@ Several converter implementations are provided in the `core.convert.support` pac a convenience. These include converters from strings to numbers and other common types. The following listing shows the `StringToInteger` class, which is a typical `Converter` implementation: -[source,java,indent=0,subs="verbatim,quotes",role="primary"] -.Java +[source,java,indent=0,subs="verbatim,quotes"] ---- package org.springframework.core.convert.support; @@ -922,20 +912,6 @@ The following listing shows the `StringToInteger` class, which is a typical `Con } } ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] -.Kotlin ----- - package org.springframework.core.convert.support - - import org.springframework.core.convert.converter.Converter - - internal class StringToInteger : Converter { - - override fun convert(source: String): Int? { - return Integer.valueOf(source) - } - } ----- @@ -946,8 +922,7 @@ When you need to centralize the conversion logic for an entire class hierarchy (for example, when converting from `String` to `Enum` objects), you can implement `ConverterFactory`, as the following example shows: -[source,java,indent=0,subs="verbatim,quotes",role="primary"] -.Java +[source,java,indent=0,subs="verbatim,quotes"] ---- package org.springframework.core.convert.converter; @@ -956,16 +931,6 @@ When you need to centralize the conversion logic for an entire class hierarchy Converter getConverter(Class targetType); } ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] -.Kotlin ----- - package org.springframework.core.convert.converter - - interface ConverterFactory { - - fun getConverter(targetType: Class): Converter - } ----- Parameterize S to be the type you are converting from and R to be the base type defining the __range__ of classes you can convert to. Then implement `getConverter(Class)`, @@ -974,7 +939,6 @@ where T is a subclass of R. Consider the `StringToEnumConverterFactory` as an example: [source,java,indent=0,subs="verbatim,quotes"] -.Java ---- package org.springframework.core.convert.support; @@ -1011,8 +975,7 @@ context that you can use when you implement your conversion logic. Such context type conversion be driven by a field annotation or by generic information declared on a field signature. The following listing shows the interface definition of `GenericConverter`: -[source,java,indent=0,subs="verbatim,quotes",role="primary"] -.Java +[source,java,indent=0,subs="verbatim,quotes"] ---- package org.springframework.core.convert.converter; @@ -1023,18 +986,6 @@ field signature. The following listing shows the interface definition of `Generi Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType); } ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] -.Kotlin ----- - package org.springframework.core.convert.converter - - interface GenericConverter { - - fun getConvertibleTypes(): Set? - - fun convert(@Nullable source: Any?, sourceType: TypeDescriptor, targetType: TypeDescriptor): Any? - } ----- To implement a `GenericConverter`, have `getConvertibleTypes()` return the supported source->target type pairs. Then implement `convert(Object, TypeDescriptor, @@ -1063,8 +1014,7 @@ on the target field, or you might want to run a `Converter` only if a specific m `ConditionalGenericConverter` is the union of the `GenericConverter` and `ConditionalConverter` interfaces that lets you define such custom matching criteria: -[source,java,indent=0,subs="verbatim,quotes",role="primary"] -.Java +[source,java,indent=0,subs="verbatim,quotes"] ---- public interface ConditionalConverter { @@ -1074,19 +1024,9 @@ on the target field, or you might want to run a `Converter` only if a specific m public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter { } ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] -.Kotlin ----- - interface ConditionalConverter { - - fun matches(sourceType: TypeDescriptor, targetType: TypeDescriptor): Boolean - } - - interface ConditionalGenericConverter : GenericConverter, ConditionalConverter ----- -A good example of a `ConditionalGenericConverter` is an `EntityConverter` that converts -between a persistent entity identifier and an entity reference. Such an `EntityConverter` +A good example of a `ConditionalGenericConverter` is an `IdToEntityConverter` that converts +between a persistent entity identifier and an entity reference. Such an `IdToEntityConverter` might match only if the target entity type declares a static finder method (for example, `findAccount(Long)`). You might perform such a finder method check in the implementation of `matches(TypeDescriptor, TypeDescriptor)`. @@ -1099,8 +1039,7 @@ might match only if the target entity type declares a static finder method (for `ConversionService` defines a unified API for executing type conversion logic at runtime. Converters are often run behind the following facade interface: -[source,java,indent=0,subs="verbatim,quotes",role="primary"] -.Java +[source,java,indent=0,subs="verbatim,quotes"] ---- package org.springframework.core.convert; @@ -1113,24 +1052,6 @@ runtime. Converters are often run behind the following facade interface: boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType); Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType); - - } ----- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] -.Kotlin ----- - package org.springframework.core.convert - - interface ConversionService { - - fun canConvert(sourceType: Class<*>, targetType: Class<*>): Boolean - - fun convert(source: Any, targetType: Class): T - - fun canConvert(sourceType: TypeDescriptor, targetType: TypeDescriptor): Boolean - - fun convert(source: Any, sourceType: TypeDescriptor, targetType: TypeDescriptor): Any - } ---- @@ -1301,8 +1222,7 @@ provides a unified type conversion API for both SPIs. The `Formatter` SPI to implement field formatting logic is simple and strongly typed. The following listing shows the `Formatter` interface definition: -[source,java,indent=0,subs="verbatim,quotes",role="primary"] -.Java +[source,java,indent=0,subs="verbatim,quotes"] ---- package org.springframework.format; @@ -1313,25 +1233,15 @@ following listing shows the `Formatter` interface definition: `Formatter` extends from the `Printer` and `Parser` building-block interfaces. The following listing shows the definitions of those two interfaces: -[source,java,indent=0,subs="verbatim,quotes",role="primary"] -.Java +[source,java,indent=0,subs="verbatim,quotes"] ---- public interface Printer { String print(T fieldValue, Locale locale); } ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] -.Kotlin ----- - interface Printer { - fun print(fieldValue: T, locale: Locale): String - } ----- - -[source,java,indent=0,subs="verbatim,quotes",role="primary"] -.Java +[source,java,indent=0,subs="verbatim,quotes"] ---- import java.text.ParseException; @@ -1340,15 +1250,6 @@ following listing shows the definitions of those two interfaces: T parse(String clientValue, Locale locale) throws ParseException; } ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] -.Kotlin ----- - interface Parser { - - @Throws(ParseException::class) - fun parse(clientValue: String, locale: Locale): T - } ----- To create your own `Formatter`, implement the `Formatter` interface shown earlier. Parameterize `T` to be the type of object you wish to format -- for example, @@ -1432,8 +1333,7 @@ Field formatting can be configured by field type or annotation. To bind an annotation to a `Formatter`, implement `AnnotationFormatterFactory`. The following listing shows the definition of the `AnnotationFormatterFactory` interface: -[source,java,indent=0,subs="verbatim,quotes",role="primary"] -.Java +[source,java,indent=0,subs="verbatim,quotes"] ---- package org.springframework.format; @@ -1446,22 +1346,9 @@ listing shows the definition of the `AnnotationFormatterFactory` interface: Parser getParser(A annotation, Class fieldType); } ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] -.Kotlin ----- - package org.springframework.format - - interface AnnotationFormatterFactory { - - val fieldTypes: Set> - - fun getPrinter(annotation: A, fieldType: Class<*>): Printer<*> - - fun getParser(annotation: A, fieldType: Class<*>): Parser<*> - } ----- To create an implementation: + . Parameterize A to be the field `annotationType` with which you wish to associate formatting logic -- for example `org.springframework.format.annotation.DateTimeFormat`. . Have `getFieldTypes()` return the types of fields on which the annotation can be used. @@ -1585,7 +1472,7 @@ The following example uses `@DateTimeFormat` to format a `java.util.Date` as an .Kotlin ---- class MyModel( - @DateTimeFormat(iso= ISO.DATE) private val date: Date + @DateTimeFormat(iso=ISO.DATE) private val date: Date ) ---- @@ -1602,36 +1489,23 @@ for use with Spring's `DataBinder` and the Spring Expression Language (SpEL). The following listing shows the `FormatterRegistry` SPI: -[source,java,indent=0,subs="verbatim,quotes",role="primary"] -.Java +[source,java,indent=0,subs="verbatim,quotes"] ---- package org.springframework.format; public interface FormatterRegistry extends ConverterRegistry { - void addFormatterForFieldType(Class fieldType, Printer printer, Parser parser); + void addPrinter(Printer printer); - void addFormatterForFieldType(Class fieldType, Formatter formatter); + void addParser(Parser parser); - void addFormatterForFieldType(Formatter formatter); + void addFormatter(Formatter formatter); - void addFormatterForAnnotation(AnnotationFormatterFactory factory); - } ----- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] -.Kotlin ----- - package org.springframework.format - - interface FormatterRegistry : ConverterRegistry { - - fun addFormatterForFieldType(fieldType: Class<*>, printer: Printer<*>, parser: Parser<*>) - - fun addFormatterForFieldType(fieldType: Class<*>, formatter: Formatter<*>) + void addFormatterForFieldType(Class fieldType, Formatter formatter); - fun addFormatterForFieldType(formatter: Formatter<*>) + void addFormatterForFieldType(Class fieldType, Printer printer, Parser parser); - fun addFormatterForAnnotation(factory: AnnotationFormatterFactory<*>) + void addFormatterForFieldAnnotation(AnnotationFormatterFactory annotationFormatterFactory); } ---- @@ -1651,8 +1525,7 @@ these rules once, and they are applied whenever formatting is needed. `FormatterRegistrar` is an SPI for registering formatters and converters through the FormatterRegistry. The following listing shows its interface definition: -[source,java,indent=0,subs="verbatim,quotes",role="primary"] -.Java +[source,java,indent=0,subs="verbatim,quotes"] ---- package org.springframework.format; @@ -1661,16 +1534,6 @@ FormatterRegistry. The following listing shows its interface definition: void registerFormatters(FormatterRegistry registry); } ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] -.Kotlin ----- - package org.springframework.format - - interface FormatterRegistrar { - - fun registerFormatters(registry: FormatterRegistry) - } ----- A `FormatterRegistrar` is useful when registering multiple related converters and formatters for a given formatting category, such as date formatting. It can also be @@ -1876,8 +1739,8 @@ bean, keep reading. Spring provides full support for the Bean Validation API including the bootstrapping of a Bean Validation provider as a Spring bean. This lets you inject a -`javax.validation.ValidatorFactory` or `javax.validation.Validator` wherever validation is -needed in your application. +`jakarta.validation.ValidatorFactory` or `jakarta.validation.Validator` wherever validation +is needed in your application. You can use the `LocalValidatorFactoryBean` to configure a default Validator as a Spring bean, as the following example shows: @@ -1911,18 +1774,18 @@ Validator, is expected to be present in the classpath and is automatically detec [[validation-beanvalidation-spring-inject]] ==== Injecting a Validator -`LocalValidatorFactoryBean` implements both `javax.validation.ValidatorFactory` and -`javax.validation.Validator`, as well as Spring's `org.springframework.validation.Validator`. +`LocalValidatorFactoryBean` implements both `jakarta.validation.ValidatorFactory` and +`jakarta.validation.Validator`, as well as Spring's `org.springframework.validation.Validator`. You can inject a reference to either of these interfaces into beans that need to invoke validation logic. -You can inject a reference to `javax.validation.Validator` if you prefer to work with the Bean +You can inject a reference to `jakarta.validation.Validator` if you prefer to work with the Bean Validation API directly, as the following example shows: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java ---- - import javax.validation.Validator; + import jakarta.validation.Validator; @Service public class MyService { @@ -1934,7 +1797,7 @@ Validation API directly, as the following example shows: [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] .Kotlin ---- - import javax.validation.Validator; + import jakarta.validation.Validator; @Service class MyService(@Autowired private val validator: Validator) @@ -1971,7 +1834,7 @@ requires the Spring Validation API, as the following example shows: Each bean validation constraint consists of two parts: * A `@Constraint` annotation that declares the constraint and its configurable properties. -* An implementation of the `javax.validation.ConstraintValidator` interface that implements +* An implementation of the `jakarta.validation.ConstraintValidator` interface that implements the constraint's behavior. To associate a declaration with an implementation, each `@Constraint` annotation @@ -2007,7 +1870,7 @@ The following example shows a custom `@Constraint` declaration followed by an as [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java ---- - import javax.validation.ConstraintValidator; + import jakarta.validation.ConstraintValidator; public class MyConstraintValidator implements ConstraintValidator { @@ -2020,7 +1883,7 @@ The following example shows a custom `@Constraint` declaration followed by an as [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] .Kotlin ---- - import javax.validation.ConstraintValidator + import jakarta.validation.ConstraintValidator class MyConstraintValidator(private val aDependency: Foo) : ConstraintValidator { diff --git a/src/docs/asciidoc/data-access.adoc b/framework-docs/src/docs/asciidoc/data-access.adoc similarity index 95% rename from src/docs/asciidoc/data-access.adoc rename to framework-docs/src/docs/asciidoc/data-access.adoc index ed552dc63dc2..b9a2453022c4 100644 --- a/src/docs/asciidoc/data-access.adoc +++ b/framework-docs/src/docs/asciidoc/data-access.adoc @@ -1,7 +1,5 @@ [[spring-data-tier]] = Data Access -:doc-root: https://docs.spring.io -:api-spring-framework: {doc-root}/spring-framework/docs/{spring-version}/javadoc-api/org/springframework :toc: left :toclevels: 4 :tabsize: 4 @@ -60,7 +58,7 @@ and <>. [[transaction-motivation]] === Advantages of the Spring Framework's Transaction Support Model -Traditionally, Java EE developers have had two choices for transaction management: +Traditionally, EE application developers have had two choices for transaction management: global or local transactions, both of which have profound limitations. Global and local transaction management is reviewed in the next two sections, followed by a discussion of how the Spring Framework's transaction management support addresses the @@ -133,9 +131,9 @@ Typically, you need an application server's JTA capability only if your applicat to handle transactions across multiple resources, which is not a requirement for many applications. Many high-end applications use a single, highly scalable database (such as Oracle RAC) instead. Stand-alone transaction managers (such as -https://www.atomikos.com/[Atomikos Transactions] and http://jotm.objectweb.org/[JOTM]) +https://www.atomikos.com/[Atomikos Transactions] and https://jotm.ow2.org/[JOTM]) are other options. Of course, you may need other application server capabilities, such as -Java Message Service (JMS) and Java EE Connector Architecture (JCA). +Java Message Service (JMS) and Jakarta EE Connector Architecture (JCA). The Spring Framework gives you the choice of when to scale your application to a fully loaded application server. Gone are the days when the only alternative to using EJB @@ -158,8 +156,7 @@ transaction management and the transaction management. The following listing shows the definition of the `PlatformTransactionManager` API: -[source,java,indent=0,subs="verbatim,quotes",role="primary"] -.Java +[source,java,indent=0,subs="verbatim,quotes"] ---- public interface PlatformTransactionManager extends TransactionManager { @@ -170,21 +167,6 @@ transaction management. The following listing shows the definition of the void rollback(TransactionStatus status) throws TransactionException; } ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] -.Kotlin ----- - interface PlatformTransactionManager : TransactionManager { - - @Throws(TransactionException::class) - fun getTransaction(definition: TransactionDefinition): TransactionStatus - - @Throws(TransactionException::class) - fun commit(status: TransactionStatus) - - @Throws(TransactionException::class) - fun rollback(status: TransactionStatus) - } ----- This is primarily a service provider interface (SPI), although you can use it <> from your application code. Because @@ -207,7 +189,7 @@ The `getTransaction(..)` method returns a `TransactionStatus` object, depending `TransactionDefinition` parameter. The returned `TransactionStatus` might represent a new transaction or can represent an existing transaction, if a matching transaction exists in the current call stack. The implication in this latter case is that, as with -Java EE transaction contexts, a `TransactionStatus` is associated with a thread of +Jakarta EE transaction contexts, a `TransactionStatus` is associated with a thread of execution. As of Spring Framework 5.2, Spring also provides a transaction management abstraction for @@ -215,8 +197,7 @@ reactive applications that make use of reactive types or Kotlin Coroutines. The listing shows the transaction strategy defined by `org.springframework.transaction.ReactiveTransactionManager`: -[source,java,indent=0,subs="verbatim,quotes",role="primary"] -.Java +[source,java,indent=0,subs="verbatim,quotes"] ---- public interface ReactiveTransactionManager extends TransactionManager { @@ -227,21 +208,6 @@ listing shows the transaction strategy defined by Mono rollback(ReactiveTransaction status) throws TransactionException; } ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] -.Kotlin ----- - interface ReactiveTransactionManager : TransactionManager { - - @Throws(TransactionException::class) - fun getReactiveTransaction(definition: TransactionDefinition): Mono - - @Throws(TransactionException::class) - fun commit(status: ReactiveTransaction): Mono - - @Throws(TransactionException::class) - fun rollback(status: ReactiveTransaction): Mono - } ----- The reactive transaction manager is primarily a service provider interface (SPI), although you can use it <> from your @@ -276,8 +242,7 @@ control transaction execution and query transaction status. The concepts should familiar, as they are common to all transaction APIs. The following listing shows the `TransactionStatus` interface: -[source,java,indent=0,subs="verbatim,quotes",role="primary"] -.Java +[source,java,indent=0,subs="verbatim,quotes"] ---- public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable { @@ -298,24 +263,6 @@ familiar, as they are common to all transaction APIs. The following listing show boolean isCompleted(); } ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] -.Kotlin ----- - interface TransactionStatus : TransactionExecution, SavepointManager, Flushable { - - override fun isNewTransaction(): Boolean - - fun hasSavepoint(): Boolean - - override fun setRollbackOnly() - - override fun isRollbackOnly(): Boolean - - fun flush() - - override fun isCompleted(): Boolean - } ----- Regardless of whether you opt for declarative or programmatic transaction management in Spring, defining the correct `TransactionManager` implementation is absolutely essential. @@ -348,7 +295,7 @@ The related `PlatformTransactionManager` bean definition then has a reference to ---- -If you use JTA in a Java EE container, then you use a container `DataSource`, obtained +If you use JTA in a Jakarta EE container, then you use a container `DataSource`, obtained through JNDI, in conjunction with Spring's `JtaTransactionManager`. The following example shows what the JTA and JNDI lookup version would look like: @@ -379,7 +326,7 @@ infrastructure. NOTE: The preceding definition of the `dataSource` bean uses the `` tag from the `jee` namespace. For more information see -<>. +<>. NOTE: If you use JTA, your transaction manager definition should look the same, regardless of what data access technology you use, be it JDBC, Hibernate JPA, or any other supported @@ -402,8 +349,8 @@ The `DataSource` bean definition is similar to the local JDBC example shown prev and, thus, is not shown in the following example. NOTE: If the `DataSource` (used by any non-JTA transaction manager) is looked up through -JNDI and managed by a Java EE container, it should be non-transactional, because the -Spring Framework (rather than the Java EE container) manages the transactions. +JNDI and managed by a Jakarta EE container, it should be non-transactional, because the +Spring Framework (rather than the Jakarta EE container) manages the transactions. The `txManager` bean in this case is of the `HibernateTransactionManager` type. In the same way as the `DataSourceTransactionManager` needs a reference to the `DataSource`, the @@ -431,7 +378,7 @@ example declares `sessionFactory` and `txManager` beans: ---- -If you use Hibernate and Java EE container-managed JTA transactions, you should use the +If you use Hibernate and Jakarta EE container-managed JTA transactions, you should use the same `JtaTransactionManager` as in the previous JTA example for JDBC, as the following example shows. Also, it is recommended to make Hibernate aware of JTA through its transaction coordinator and possibly also its connection release mode configuration: @@ -498,7 +445,7 @@ relevant `TransactionManager`. [[tx-resource-synchronization-high]] ==== High-level Synchronization Approach -The preferred approach is to use Spring's highest-level template based persistence +The preferred approach is to use Spring's highest-level template-based persistence integration APIs or to use native ORM APIs with transaction-aware factory beans or proxies for managing the native resource factories. These transaction-aware solutions internally handle resource creation and reuse, cleanup, optional transaction @@ -555,7 +502,7 @@ behind the scenes and you need not write any special code. At the very lowest level exists the `TransactionAwareDataSourceProxy` class. This is a proxy for a target `DataSource`, which wraps the target `DataSource` to add awareness of Spring-managed transactions. In this respect, it is similar to a transactional JNDI -`DataSource`, as provided by a Java EE server. +`DataSource`, as provided by a Jakarta EE server. You should almost never need or want to use this class, except when existing code must be called and passed a standard JDBC `DataSource` interface implementation. In @@ -566,7 +513,7 @@ abstractions mentioned earlier. [[transaction-declarative]] -=== Declarative transaction management +=== Declarative Transaction Management NOTE: Most Spring Framework users choose declarative transaction management. This option has the least impact on application code and, hence, is most consistent with the ideals of a @@ -637,7 +584,7 @@ around method invocations. NOTE: Spring AOP is covered in <>. -Spring Frameworks's `TransactionInterceptor` provides transaction management for +Spring Framework's `TransactionInterceptor` provides transaction management for imperative and reactive programming models. The interceptor detects the desired flavor of transaction management by inspecting the method return type. Methods returning a reactive type such as `Publisher` or Kotlin `Flow` (or a subtype of those) qualify for reactive @@ -648,6 +595,18 @@ Transaction management flavors impact which transaction manager is required. Imp transactions require a `PlatformTransactionManager`, while reactive transactions use `ReactiveTransactionManager` implementations. +[NOTE] +==== +`@Transactional` commonly works with thread-bound transactions managed by +`PlatformTransactionManager`, exposing a transaction to all data access operations within +the current execution thread. Note: This does _not_ propagate to newly started threads +within the method. + +A reactive transaction managed by `ReactiveTransactionManager` uses the Reactor context +instead of thread-local attributes. As a consequence, all participating data access +operations need to execute within the same Reactor context in the same reactive pipeline. +==== + The following image shows a conceptual view of calling a method on a transactional proxy: image::images/tx.png[] @@ -876,9 +835,9 @@ that test drives the configuration shown earlier: public final class Boot { public static void main(final String[] args) throws Exception { - ApplicationContext ctx = new ClassPathXmlApplicationContext("context.xml", Boot.class); - FooService fooService = (FooService) ctx.getBean("fooService"); - fooService.insertFoo (new Foo()); + ApplicationContext ctx = new ClassPathXmlApplicationContext("context.xml"); + FooService fooService = ctx.getBean(FooService.class); + fooService.insertFoo(new Foo()); } } ---- @@ -1056,9 +1015,11 @@ to ensure completion and buffer results in the calling code. ==== Rolling Back a Declarative Transaction The previous section outlined the basics of how to specify transactional settings for -classes, typically service layer classes, declaratively in your application. This -section describes how you can control the rollback of transactions in a simple, -declarative fashion. +classes, typically service layer classes, declaratively in your application. This section +describes how you can control the rollback of transactions in a simple, declarative +fashion in XML configuration. For details on controlling rollback semantics declaratively +with the `@Transactional` annotation, see +<>. The recommended way to indicate to the Spring Framework's transaction infrastructure that a transaction's work is to be rolled back is to throw an `Exception` from code that @@ -1068,14 +1029,64 @@ the call stack and makes a determination whether to mark the transaction for rol In its default configuration, the Spring Framework's transaction infrastructure code marks a transaction for rollback only in the case of runtime, unchecked exceptions. -That is, when the thrown exception is an instance or subclass of `RuntimeException`. ( -`Error` instances also, by default, result in a rollback). Checked exceptions that are +That is, when the thrown exception is an instance or subclass of `RuntimeException`. +(`Error` instances also, by default, result in a rollback). Checked exceptions that are thrown from a transactional method do not result in rollback in the default configuration. You can configure exactly which `Exception` types mark a transaction for rollback, -including checked exceptions. The following XML snippet demonstrates how you configure -rollback for a checked, application-specific `Exception` type: +including checked exceptions by specifying _rollback rules_. + +.Rollback rules +[[transaction-declarative-rollback-rules]] +[NOTE] +==== +Rollback rules determine if a transaction should be rolled back when a given exception is +thrown, and the rules are based on exception types or exception patterns. + +Rollback rules may be configured in XML via the `rollback-for` and `no-rollback-for` +attributes, which allow rules to be defined as patterns. When using +<>, rollback rules may +be configured via the `rollbackFor`/`noRollbackFor` and +`rollbackForClassName`/`noRollbackForClassName` attributes, which allow rules to be +defined based on exception types or patterns, respectively. + +When a rollback rule is defined with an exception type, that type will be used to match +against the type of a thrown exception and its super types, providing type safety and +avoiding any unintentional matches that may occur when using a pattern. For example, a +value of `jakarta.servlet.ServletException.class` will only match thrown exceptions of +type `jakarta.servlet.ServletException` and its subclasses. + +When a rollback rule is defined with an exception pattern, the pattern can be a fully +qualified class name or a substring of a fully qualified class name for an exception type +(which must be a subclass of `Throwable`), with no wildcard support at present. For +example, a value of `"jakarta.servlet.ServletException"` or `"ServletException"` will +match `jakarta.servlet.ServletException` and its subclasses. + +[WARNING] +===== +You must carefully consider how specific a pattern is and whether to include package +information (which isn't mandatory). For example, `"Exception"` will match nearly +anything and will probably hide other rules. `"java.lang.Exception"` would be correct if +`"Exception"` were meant to define a rule for all checked exceptions. With more unique +exception names such as `"BaseBusinessException"` there is likely no need to use the +fully qualified class name for the exception pattern. + +Furthermore, pattern-based rollback rules may result in unintentional matches for +similarly named exceptions and nested classes. This is due to the fact that a thrown +exception is considered to be a match for a given pattern-based rollback rule if the name +of the thrown exception contains the exception pattern configured for the rollback rule. +For example, given a rule configured to match on `"com.example.CustomException"`, that +rule will match against an exception named `com.example.CustomExceptionV2` (an exception +in the same package as `CustomException` but with an additional suffix) or an exception +named `com.example.CustomException$AnotherException` (an exception declared as a nested +class in `CustomException`). +===== +==== + +The following XML snippet demonstrates how to configure rollback for a checked, +application-specific `Exception` type by supplying an _exception pattern_ via the +`rollback-for` attribute: [source,xml,indent=0,subs="verbatim,quotes"] ---- @@ -1087,8 +1098,8 @@ rollback for a checked, application-specific `Exception` type: ---- -If you do not want a transaction rolled -back when an exception is thrown, you can also specify 'no rollback rules'. The following example tells the Spring Framework's +If you do not want a transaction rolled back when an exception is thrown, you can also +specify 'no rollback' rules. The following example tells the Spring Framework's transaction infrastructure to commit the attendant transaction even in the face of an unhandled `InstrumentNotFoundException`: @@ -1102,11 +1113,11 @@ unhandled `InstrumentNotFoundException`: ---- -When the Spring Framework's transaction infrastructure catches an exception and it -consults the configured rollback rules to determine whether to mark the transaction for -rollback, the strongest matching rule wins. So, in the case of the following -configuration, any exception other than an `InstrumentNotFoundException` results in a -rollback of the attendant transaction: +When the Spring Framework's transaction infrastructure catches an exception and consults +the configured rollback rules to determine whether to mark the transaction for rollback, +the strongest matching rule wins. So, in the case of the following configuration, any +exception other than an `InstrumentNotFoundException` results in a rollback of the +attendant transaction: [source,xml,indent=0,subs="verbatim,quotes"] ---- @@ -1117,10 +1128,10 @@ rollback of the attendant transaction: ---- -You can also indicate a required rollback programmatically. Although simple, -this process is quite invasive and tightly couples your code to the Spring Framework's -transaction infrastructure. The following example shows how to programmatically indicate -a required rollback: +You can also indicate a required rollback programmatically. Although simple, this process +is quite invasive and tightly couples your code to the Spring Framework's transaction +infrastructure. The following example shows how to programmatically indicate a required +rollback: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1338,8 +1349,8 @@ source code puts the declarations much closer to the affected code. There is not danger of undue coupling, because code that is meant to be used transactionally is almost always deployed that way anyway. -NOTE: The standard `javax.transaction.Transactional` annotation is also supported as a -drop-in replacement to Spring's own annotation. Please refer to JTA 1.2 documentation +NOTE: The standard `jakarta.transaction.Transactional` annotation is also supported as +a drop-in replacement to Spring's own annotation. Please refer to the JTA documentation for more details. The ease-of-use afforded by the use of the `@Transactional` annotation is best @@ -1353,19 +1364,23 @@ Consider the following class definition: @Transactional public class DefaultFooService implements FooService { - Foo getFoo(String fooName) { + @Override + public Foo getFoo(String fooName) { // ... } - Foo getFoo(String fooName, String barName) { + @Override + public Foo getFoo(String fooName, String barName) { // ... } - void insertFoo(Foo foo) { + @Override + public void insertFoo(Foo foo) { // ... } - void updateFoo(Foo foo) { + @Override + public void updateFoo(Foo foo) { // ... } } @@ -1395,11 +1410,13 @@ Consider the following class definition: } ---- -Used at the class level as above, the annotation indicates a default for all methods -of the declaring class (as well as its subclasses). Alternatively, each method can -get annotated individually. Note that a class-level annotation does not apply to -ancestor classes up the class hierarchy; in such a scenario, methods need to be -locally redeclared in order to participate in a subclass-level annotation. +Used at the class level as above, the annotation indicates a default for all methods of +the declaring class (as well as its subclasses). Alternatively, each method can be +annotated individually. See <> for +further details on which methods Spring considers transactional. Note that a class-level +annotation does not apply to ancestor classes up the class hierarchy; in such a scenario, +inherited methods need to be locally redeclared in order to participate in a +subclass-level annotation. When a POJO class such as the one above is defined as a bean in a Spring context, you can make the bean instance transactional through an `@EnableTransactionManagement` @@ -1429,7 +1446,8 @@ In XML configuration, the `` tag provides similar conveni - <1> + + <1> @@ -1444,7 +1462,7 @@ In XML configuration, the `` tag provides similar conveni TIP: You can omit the `transaction-manager` attribute in the `` -tag if the bean name of the `TransactionManager` that you want to wire in has the name, +tag if the bean name of the `TransactionManager` that you want to wire in has the name `transactionManager`. If the `TransactionManager` bean that you want to dependency-inject has any other name, you have to use the `transaction-manager` attribute, as in the preceding example. @@ -1459,19 +1477,23 @@ programming arrangements as the following listing shows: @Transactional public class DefaultFooService implements FooService { - Publisher getFoo(String fooName) { + @Override + public Publisher getFoo(String fooName) { // ... } - Mono getFoo(String fooName, String barName) { + @Override + public Mono getFoo(String fooName, String barName) { // ... } - Mono insertFoo(Foo foo) { + @Override + public Mono insertFoo(Foo foo) { // ... } - Mono updateFoo(Foo foo) { + @Override + public Mono updateFoo(Foo foo) { // ... } } @@ -1503,20 +1525,50 @@ programming arrangements as the following listing shows: Note that there are special considerations for the returned `Publisher` with regards to Reactive Streams cancellation signals. See the <> section under -"Using the TransactionOperator" for more details. +"Using the TransactionalOperator" for more details. +[[transaction-declarative-annotations-method-visibility]] .Method visibility and `@Transactional` -**** -When you use proxies, you should apply the `@Transactional` annotation only to methods -with public visibility. If you do annotate protected, private or package-visible -methods with the `@Transactional` annotation, no error is raised, but the annotated -method does not exhibit the configured transactional settings. If you need to annotate -non-public methods, consider using AspectJ (described later). -**** +[NOTE] +==== +When you use transactional proxies with Spring's standard configuration, you should apply +the `@Transactional` annotation only to methods with `public` visibility. If you do +annotate `protected`, `private`, or package-visible methods with the `@Transactional` +annotation, no error is raised, but the annotated method does not exhibit the configured +transactional settings. If you need to annotate non-public methods, consider the tip in +the following paragraph for class-based proxies or consider using AspectJ compile-time or +load-time weaving (described later). + +When using `@EnableTransactionManagement` in a `@Configuration` class, `protected` or +package-visible methods can also be made transactional for class-based proxies by +registering a custom `transactionAttributeSource` bean like in the following example. +Note, however, that transactional methods in interface-based proxies must always be +`public` and defined in the proxied interface. + +[source,java,indent=0,subs="verbatim,quotes"] +---- + /** + * Register a custom AnnotationTransactionAttributeSource with the + * publicMethodsOnly flag set to false to enable support for + * protected and package-private @Transactional methods in + * class-based proxies. + * + * @see ProxyTransactionManagementConfiguration#transactionAttributeSource() + */ + @Bean + TransactionAttributeSource transactionAttributeSource() { + return new AnnotationTransactionAttributeSource(false); + } +---- + +The _Spring TestContext Framework_ supports non-private `@Transactional` test methods by +default. See <> in the testing +chapter for examples. +==== You can apply the `@Transactional` annotation to an interface definition, a method -on an interface, a class definition, or a public method on a class. However, the +on an interface, a class definition, or a method on a class. However, the mere presence of the `@Transactional` annotation is not enough to activate the transactional behavior. The `@Transactional` annotation is merely metadata that can be consumed by some runtime infrastructure that is `@Transactional`-aware and that @@ -1538,12 +1590,13 @@ the proxy are intercepted. This means that self-invocation (in effect, a method the target object calling another method of the target object) does not lead to an actual transaction at runtime even if the invoked method is marked with `@Transactional`. Also, the proxy must be fully initialized to provide the expected behavior, so you should not -rely on this feature in your initialization code (that is, `@PostConstruct`). +rely on this feature in your initialization code -- for example, in a `@PostConstruct` +method. -Consider using of AspectJ mode (see the `mode` attribute in the following table) if you -expect self-invocations to be wrapped with transactions as well. In this case, there no -proxy in the first place. Instead, the target class is woven (that is, its byte code is -modified) to turn `@Transactional` into runtime behavior on any kind of method. +Consider using AspectJ mode (see the `mode` attribute in the following table) if you +expect self-invocations to be wrapped with transactions as well. In this case, there is +no proxy in the first place. Instead, the target class is woven (that is, its byte code +is modified) to support `@Transactional` runtime behavior on any kind of method. [[tx-annotation-driven-settings]] .Annotation driven transaction settings @@ -1596,14 +1649,14 @@ NOTE: The `proxy-target-class` attribute controls what type of transactional pro created for classes annotated with the `@Transactional` annotation. If `proxy-target-class` is set to `true`, class-based proxies are created. If `proxy-target-class` is `false` or if the attribute is omitted, standard JDK -interface-based proxies are created. (See <> for a discussion of the -different proxy types.) +interface-based proxies are created. (See <> +for a discussion of the different proxy types.) -NOTE: `@EnableTransactionManagement` and `` looks for +NOTE: `@EnableTransactionManagement` and `` look for `@Transactional` only on beans in the same application context in which they are defined. This means that, if you put annotation-driven configuration in a `WebApplicationContext` for a `DispatcherServlet`, it checks for `@Transactional` beans only in your controllers -and not your services. See <> for more information. +and not in your services. See <> for more information. The most derived location takes precedence when evaluating the transactional settings for a method. In the case of the following example, the `DefaultFooService` class is @@ -1651,8 +1704,8 @@ precedence over the transactional settings defined at the class level. ===== `@Transactional` Settings The `@Transactional` annotation is metadata that specifies that an interface, class, -or method must have transactional semantics (for example, "`start a brand new read-only -transaction when this method is invoked, suspending any existing transaction`"). +or method must have transactional semantics (for example, "start a brand new read-only +transaction when this method is invoked, suspending any existing transaction"). The default `@Transactional` settings are as follows: * The propagation setting is `PROPAGATION_REQUIRED.` @@ -1660,7 +1713,8 @@ The default `@Transactional` settings are as follows: * The transaction is read-write. * The transaction timeout defaults to the default timeout of the underlying transaction system, or to none if timeouts are not supported. -* Any `RuntimeException` triggers rollback, and any checked `Exception` does not. +* Any `RuntimeException` or `Error` triggers rollback, and any checked `Exception` does + not. You can change these default settings. The following table summarizes the various properties of the `@Transactional` annotation: @@ -1674,6 +1728,14 @@ properties of the `@Transactional` annotation: | `String` | Optional qualifier that specifies the transaction manager to be used. +| `transactionManager` +| `String` +| Alias for `value`. + +| `label` +| Array of `String` labels to add an expressive description to the transaction. +| Labels may be evaluated by transaction managers to associate implementation-specific behavior with the actual transaction. + | <> | `enum`: `Propagation` | Optional propagation setting. @@ -1686,32 +1748,35 @@ properties of the `@Transactional` annotation: | `int` (in seconds of granularity) | Optional transaction timeout. Applies only to propagation values of `REQUIRED` or `REQUIRES_NEW`. +| `timeoutString` +| `String` (in seconds of granularity) +| Alternative for specifying the `timeout` in seconds as a `String` value -- for example, as a placeholder. + | `readOnly` | `boolean` | Read-write versus read-only transaction. Only applicable to values of `REQUIRED` or `REQUIRES_NEW`. | `rollbackFor` | Array of `Class` objects, which must be derived from `Throwable.` -| Optional array of exception classes that must cause rollback. +| Optional array of exception types that must cause rollback. | `rollbackForClassName` -| Array of class names. The classes must be derived from `Throwable.` -| Optional array of names of exception classes that must cause rollback. +| Array of exception name patterns. +| Optional array of exception name patterns that must cause rollback. | `noRollbackFor` | Array of `Class` objects, which must be derived from `Throwable.` -| Optional array of exception classes that must not cause rollback. +| Optional array of exception types that must not cause rollback. | `noRollbackForClassName` -| Array of `String` class names, which must be derived from `Throwable.` -| Optional array of names of exception classes that must not cause rollback. - -| `label` -| Array of `String` labels to add an expressive description to the transaction. -| Labels may be evaluated by transaction managers to associate -implementation-specific behavior with the actual transaction. +| Array of exception name patterns. +| Optional array of exception name patterns that must not cause rollback. |=== +TIP: See <> for further details +on rollback rule semantics, patterns, and warnings regarding possible unintentional +matches for pattern-based rollback rules. + Currently, you cannot have explicit control over the name of a transaction, where 'name' means the transaction name that appears in a transaction monitor, if applicable (for example, WebLogic's transaction monitor), and in logging output. For declarative @@ -1877,7 +1942,7 @@ rollback rules, timeouts, and other features. ==== Transaction Propagation This section describes some semantics of transaction propagation in Spring. Note -that this section is not an introduction to transaction propagation proper. Rather, it +that this section is not a proper introduction to transaction propagation. Rather, it details some of the semantics regarding transaction propagation in Spring. In Spring-managed transactions, be aware of the difference between physical and @@ -2120,7 +2185,7 @@ declarative approach: - + @@ -2163,7 +2228,7 @@ container by means of an AspectJ aspect. To do so, first annotate your classes (and optionally your classes' methods) with the `@Transactional` annotation, and then link (weave) your application with the `org.springframework.transaction.aspectj.AnnotationTransactionAspect` defined in the -`spring-aspects.jar` file. You must also configure The aspect with a transaction +`spring-aspects.jar` file. You must also configure the aspect with a transaction manager. You can use the Spring Framework's IoC container to take care of dependency-injecting the aspect. The simplest way to configure the transaction management aspect is to use the `` element and specify the `mode` @@ -2414,39 +2479,39 @@ different settings (for example, a different isolation level), you need to creat two distinct `TransactionTemplate` instances. [[tx-prog-operator]] -==== Using the `TransactionOperator` +==== Using the `TransactionalOperator` -The `TransactionOperator` follows an operator design that is similar to other reactive +The `TransactionalOperator` follows an operator design that is similar to other reactive operators. It uses a callback approach (to free application code from having to do the boilerplate acquisition and release transactional resources) and results in code that is intention driven, in that your code focuses solely on what you want to do. -NOTE: As the examples that follow show, using the `TransactionOperator` absolutely +NOTE: As the examples that follow show, using the `TransactionalOperator` absolutely couples you to Spring's transaction infrastructure and APIs. Whether or not programmatic transaction management is suitable for your development needs is a decision that you have to make yourself. Application code that must run in a transactional context and that explicitly uses -the `TransactionOperator` resembles the next example: +the `TransactionalOperator` resembles the next example: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java ---- public class SimpleService implements Service { - // single TransactionOperator shared amongst all methods in this instance + // single TransactionalOperator shared amongst all methods in this instance private final TransactionalOperator transactionalOperator; // use constructor-injection to supply the ReactiveTransactionManager public SimpleService(ReactiveTransactionManager transactionManager) { - this.transactionOperator = TransactionalOperator.create(transactionManager); + this.transactionalOperator = TransactionalOperator.create(transactionManager); } public Mono someServiceMethod() { - + // the code in this method runs in a transactional context - - Mono update = updateOperation1(); + + Mono update = updateOperation1(); return update.then(resultOfUpdateOperation2).as(transactionalOperator::transactional); } @@ -2584,8 +2649,7 @@ following example shows how to do so: TransactionStatus status = txManager.getTransaction(def); try { // put your business logic here - } - catch (MyException ex) { + } catch (MyException ex) { txManager.rollback(status); throw ex; } @@ -2750,7 +2814,7 @@ supporting transaction suspension. See the {api-spring-framework}/transaction/jta/JtaTransactionManager.html[`JtaTransactionManager`] javadoc for details. -Spring's `JtaTransactionManager` is the standard choice to run on Java EE application +Spring's `JtaTransactionManager` is the standard choice to run on Jakarta EE application servers and is known to work on all common servers. Advanced functionality, such as transaction suspension, works on many servers as well (including GlassFish, JBoss and Geronimo) without any special configuration required. However, for fully supported @@ -2816,7 +2880,7 @@ treats them as errors. For more information about the Spring Framework's transaction support, see: -* https://www.javaworld.com/javaworld/jw-01-2009/jw-01-spring-transactions.html[Distributed +* https://www.infoworld.com/article/2077963/distributed-transactions-in-spring--with-and-without-xa.html[Distributed transactions in Spring, with and without XA] is a JavaWorld presentation in which Spring's David Syer guides you through seven patterns for distributed transactions in Spring applications, three of them with XA and four without. @@ -2844,30 +2908,29 @@ specific to each technology. Spring provides a convenient translation from technology-specific exceptions, such as `SQLException` to its own exception class hierarchy, which has `DataAccessException` as -the root exception. These exceptions wrap the original exception so that there is never any -risk that you might lose any information about what might have gone wrong. +the root exception. These exceptions wrap the original exception so that there is never +any risk that you might lose any information about what might have gone wrong. In addition to JDBC exceptions, Spring can also wrap JPA- and Hibernate-specific exceptions, -converting them to a set of focused runtime exceptions. -This lets you handle most non-recoverable persistence exceptions -in only the appropriate layers, without having annoying boilerplate -catch-and-throw blocks and exception declarations in your DAOs. (You can still trap -and handle exceptions anywhere you need to though.) As mentioned above, JDBC -exceptions (including database-specific dialects) are also converted to the same +converting them to a set of focused runtime exceptions. This lets you handle most +non-recoverable persistence exceptions in only the appropriate layers, without having +annoying boilerplate catch-and-throw blocks and exception declarations in your DAOs. +(You can still trap and handle exceptions anywhere you need to though.) As mentioned above, +JDBC exceptions (including database-specific dialects) are also converted to the same hierarchy, meaning that you can perform some operations with JDBC within a consistent programming model. -The preceding discussion holds true for the various template classes in Spring's support for various ORM -frameworks. If you use the interceptor-based classes, the application must care -about handling `HibernateExceptions` and `PersistenceExceptions` itself, preferably by -delegating to the `convertHibernateAccessException(..)` or -`convertJpaAccessException()` methods, respectively, of `SessionFactoryUtils`. These methods convert the exceptions +The preceding discussion holds true for the various template classes in Spring's support +for various ORM frameworks. If you use the interceptor-based classes, the application must +care about handling `HibernateExceptions` and `PersistenceExceptions` itself, preferably by +delegating to the `convertHibernateAccessException(..)` or `convertJpaAccessException(..)` +methods, respectively, of `SessionFactoryUtils`. These methods convert the exceptions to exceptions that are compatible with the exceptions in the `org.springframework.dao` -exception hierarchy. As `PersistenceExceptions` are unchecked, they can get -thrown, too (sacrificing generic DAO abstraction in terms of exceptions, though). +exception hierarchy. As `PersistenceExceptions` are unchecked, they can get thrown, too +(sacrificing generic DAO abstraction in terms of exceptions, though). -The following image shows the exception hierarchy that Spring provides. (Note that the -class hierarchy detailed in the image shows only a subset of the entire +The following image shows the exception hierarchy that Spring provides. +(Note that the class hierarchy detailed in the image shows only a subset of the entire `DataAccessException` hierarchy.) image::images/DataAccessException.png[] @@ -3109,7 +3172,7 @@ class and the related support classes. See <>, <> * `datasource`: The `org.springframework.jdbc.datasource` package contains a utility class for easy `DataSource` access and various simple `DataSource` implementations that you can use for -testing and running unmodified JDBC code outside of a Java EE container. A subpackage +testing and running unmodified JDBC code outside of a Jakarta EE container. A subpackage named `org.springfamework.jdbc.datasource.embedded` provides support for creating embedded databases by using Java database engines, such as HSQL, H2, and Derby. See <> and <>. @@ -3295,7 +3358,7 @@ For example, it may be better to write the preceding code snippet as follows: }; public List findAllActors() { - return this.jdbcTemplate.query( "select first_name, last_name from t_actor", actorRowMapper); + return this.jdbcTemplate.query("select first_name, last_name from t_actor", actorRowMapper); } ---- [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] @@ -3605,7 +3668,7 @@ variable and the corresponding value that is plugged into the `namedParameters` variable (of type `MapSqlParameterSource`). Alternatively, you can pass along named parameters and their corresponding values to a -`NamedParameterJdbcTemplate` instance by using the `Map`-based style.The remaining +`NamedParameterJdbcTemplate` instance by using the `Map`-based style. The remaining methods exposed by the `NamedParameterJdbcOperations` and implemented by the `NamedParameterJdbcTemplate` class follow a similar pattern and are not covered here. @@ -3743,7 +3806,7 @@ See also <> for guidelines on using the ==== Using `SQLExceptionTranslator` `SQLExceptionTranslator` is an interface to be implemented by classes that can translate -between `SQLExceptions` and Spring's own `org.springframework.dao.DataAccessException`, +between ``SQLException``s and Spring's own `org.springframework.dao.DataAccessException`, which is agnostic in regard to data access strategy. Implementations can be generic (for example, using SQLState codes for JDBC) or proprietary (for example, using Oracle error codes) for greater precision. @@ -3799,9 +3862,9 @@ You can extend `SQLErrorCodeSQLExceptionTranslator`, as the following example sh override fun customTranslate(task: String, sql: String?, sqlEx: SQLException): DataAccessException? { if (sqlEx.errorCode == -12345) { - return DeadlockLoserDataAccessException(task, sqlEx) - } - return null; + return DeadlockLoserDataAccessException(task, sqlEx) + } + return null } } ---- @@ -4034,7 +4097,7 @@ The following example updates a column for a certain primary key: In the preceding example, an SQL statement has placeholders for row parameters. You can pass the parameter values -in as varargs or ,alternatively, as an array of objects. Thus, you should explicitly wrap primitives +in as varargs or, alternatively, as an array of objects. Thus, you should explicitly wrap primitives in the primitive wrapper classes, or you should use auto-boxing. @@ -4232,14 +4295,14 @@ interface that wraps a single `Connection` that is not closed after each use. This is not multi-threading capable. If any client code calls `close` on the assumption of a pooled connection (as when using -persistence tools), you should set the `suppressClose` property to `true`. This setting returns a -close-suppressing proxy that wraps the physical connection. Note that you can no longer -cast this to a native Oracle `Connection` or a similar object. +persistence tools), you should set the `suppressClose` property to `true`. This setting +returns a close-suppressing proxy that wraps the physical connection. Note that you can +no longer cast this to a native Oracle `Connection` or a similar object. -`SingleConnectionDataSource` is primarily a test class. For example, it enables easy testing of code outside an -application server, in conjunction with a simple JNDI environment. In contrast to -`DriverManagerDataSource`, it reuses the same connection all the time, avoiding -excessive creation of physical connections. +`SingleConnectionDataSource` is primarily a test class. It typically enables easy testing +of code outside an application server, in conjunction with a simple JNDI environment. +In contrast to `DriverManagerDataSource`, it reuses the same connection all the time, +avoiding excessive creation of physical connections. @@ -4250,7 +4313,7 @@ The `DriverManagerDataSource` class is an implementation of the standard `DataSo interface that configures a plain JDBC driver through bean properties and returns a new `Connection` every time. -This implementation is useful for test and stand-alone environments outside of a Java EE +This implementation is useful for test and stand-alone environments outside of a Jakarta EE container, either as a `DataSource` bean in a Spring IoC container or in conjunction with a simple JNDI environment. Pool-assuming `Connection.close()` calls close the connection, so any `DataSource`-aware persistence code should work. However, @@ -4264,7 +4327,7 @@ environment, that it is almost always preferable to use such a connection pool o `TransactionAwareDataSourceProxy` is a proxy for a target `DataSource`. The proxy wraps that target `DataSource` to add awareness of Spring-managed transactions. In this respect, it -is similar to a transactional JNDI `DataSource`, as provided by a Java EE server. +is similar to a transactional JNDI `DataSource`, as provided by a Jakarta EE server. NOTE: It is rarely desirable to use this class, except when already existing code must be called and passed a standard JDBC `DataSource` interface implementation. In this case, @@ -4286,7 +4349,7 @@ specified data source to the currently executing thread, potentially allowing fo thread connection per data source. Application code is required to retrieve the JDBC connection through -`DataSourceUtils.getConnection(DataSource)` instead of Java EE's standard +`DataSourceUtils.getConnection(DataSource)` instead of Jakarta EE's standard `DataSource.getConnection`. It throws unchecked `org.springframework.dao` exceptions instead of checked `SQLExceptions`. All framework classes (such as `JdbcTemplate`) use this strategy implicitly. If not used with this transaction manager, the lookup strategy @@ -4503,13 +4566,14 @@ While this usually works well, there is a potential for issues (for example, wit `null` values). Spring, by default, calls `ParameterMetaData.getParameterType` in such a case, which can be expensive with your JDBC driver. You should use a recent driver version and consider setting the `spring.jdbc.getParameterType.ignore` property to `true` -(as a JVM system property or in a `spring.properties` file in the root of your classpath) -if you encounter a performance issue (as reported on Oracle 12c, JBoss and PostgreSQL). +(as a JVM system property or via the +<> mechanism) if you encounter +a performance issue (as reported on Oracle 12c, JBoss, and PostgreSQL). Alternatively, you might consider specifying the corresponding JDBC types explicitly, -either through a 'BatchPreparedStatementSetter' (as shown earlier), through an explicit type -array given to a 'List' based call, through 'registerSqlType' calls on a -custom 'MapSqlParameterSource' instance, or through a 'BeanPropertySqlParameterSource' +either through a `BatchPreparedStatementSetter` (as shown earlier), through an explicit type +array given to a `List` based call, through `registerSqlType` calls on a +custom `MapSqlParameterSource` instance, or through a `BeanPropertySqlParameterSource` that derives the SQL type from the Java-declared property type even for a null value. ==== @@ -4576,7 +4640,7 @@ The following example shows a batch update that uses a batch size of 100: } ---- -The batch update methods for this call returns an array of `int` arrays that contains an +The batch update method for this call returns an array of `int` arrays that contains an array entry for each batch with an array of the number of affected rows for each update. The top-level array's length indicates the number of batches run, and the second level array's length indicates the number of updates in that batch. The number of updates in @@ -5172,11 +5236,9 @@ as the following example shows: ---- public class JdbcActorDao implements ActorDao { - private JdbcTemplate jdbcTemplate; private SimpleJdbcCall funcGetActorName; public void setDataSource(DataSource dataSource) { - this.jdbcTemplate = new JdbcTemplate(dataSource); JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); jdbcTemplate.setResultsMapCaseInsensitive(true); this.funcGetActorName = new SimpleJdbcCall(jdbcTemplate) @@ -5442,7 +5504,7 @@ example shows such a method: The `SqlUpdate` class encapsulates an SQL update. As with a query, an update object is reusable, and, as with all `RdbmsOperation` classes, an update can have parameters and is defined in SQL. This class provides a number of `update(..)` methods analogous to the -`execute(..)` methods of query objects. The `SQLUpdate` class is concrete. It can be +`execute(..)` methods of query objects. The `SqlUpdate` class is concrete. It can be subclassed -- for example, to add a custom update method. However, you do not have to subclass the `SqlUpdate` class, since it can easily be parameterized by setting SQL and declaring parameters. @@ -5495,10 +5557,10 @@ The following example creates a custom update method named `execute`: } /** - * @param id for the Customer to be updated - * @param rating the new value for credit rating - * @return number of rows updated - */ + * @param id for the Customer to be updated + * @param rating the new value for credit rating + * @return number of rows updated + */ fun execute(id: Int, rating: Int): Int { return update(rating, id) } @@ -5509,10 +5571,8 @@ The following example creates a custom update method named `execute`: [[jdbc-StoredProcedure]] ==== Using `StoredProcedure` -The `StoredProcedure` class is a superclass for object abstractions of RDBMS stored -procedures. This class is `abstract`, and its various `execute(..)` methods have -`protected` access, preventing use other than through a subclass that offers tighter -typing. +The `StoredProcedure` class is an `abstract` superclass for object abstractions of RDBMS +stored procedures. The inherited `sql` property is the name of the stored procedure in the RDBMS. @@ -6031,8 +6091,8 @@ limit is 1000. In addition to the primitive values in the value list, you can create a `java.util.List` of object arrays. This list can support multiple expressions being defined for the `in` -clause, such as `select * from T_ACTOR where (id, last_name) in \((1, 'Johnson'), (2, -'Harrop'\))`. This, of course, requires that your database supports this syntax. +clause, such as `+++select * from T_ACTOR where (id, last_name) in ((1, 'Johnson'), (2, +'Harrop'))+++`. This, of course, requires that your database supports this syntax. [[jdbc-complex-types]] @@ -6169,7 +6229,7 @@ it with values from the Java `ARRAY`, as the following example shows: === Embedded Database Support The `org.springframework.jdbc.datasource.embedded` package provides support for embedded -Java database engines. Support for http://www.hsqldb.org[HSQL], +Java database engines. Support for https://www.hsqldb.org[HSQL], https://www.h2database.com[H2], and https://db.apache.org/derby[Derby] is provided natively. You can also use an extensible API to plug in new embedded database types and `DataSource` implementations. @@ -6792,20 +6852,20 @@ You can take control over result mapping by supplying a `Function` that called for each `Row` so it can can return arbitrary values (singular values, collections and maps, and objects). -The following example extracts the `id` column and emits its value: +The following example extracts the `name` column and emits its value: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java ---- Flux names = client.sql("SELECT name FROM person") - .map(row -> row.get("id", String.class)) + .map(row -> row.get("name", String.class)) .all(); ---- [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] .Kotlin ---- val names = client.sql("SELECT name FROM person") - .map{ row: Row -> row.get("id", String.class) } + .map{ row: Row -> row.get("name", String.class) } .flow() ---- @@ -7094,7 +7154,7 @@ databases, you may want multiple `DatabaseClient` instances, which requires mult instances. [[r2dbc-auto-generated-keys]] -== Retrieving Auto-generated Keys +=== Retrieving Auto-generated Keys `INSERT` statements may generate keys when inserting rows into a table that defines an auto-increment or identity column. To get full control over @@ -7388,7 +7448,9 @@ examples (one for Java configuration and one for XML configuration) show how to ---- @Repository class ProductDaoImpl : ProductDao { + // class body here... + } ---- @@ -7419,16 +7481,17 @@ exception hierarchies. [[orm-hibernate]] === Hibernate -We start with a coverage of https://hibernate.org/[Hibernate 5] in a Spring -environment, using it to demonstrate the approach that Spring takes towards integrating -OR mappers. This section covers many issues in detail and shows different variations -of DAO implementations and transaction demarcation. Most of these patterns can be -directly translated to all other supported ORM tools. The later sections in this -chapter then cover the other ORM technologies and show brief examples. +We start with a coverage of https://hibernate.org/[Hibernate 5] in a Spring environment, +using it to demonstrate the approach that Spring takes towards integrating OR mappers. +This section covers many issues in detail and shows different variations of DAO +implementations and transaction demarcation. Most of these patterns can be directly +translated to all other supported ORM tools. The later sections in this chapter then +cover the other ORM technologies and show brief examples. NOTE: As of Spring Framework 5.3, Spring requires Hibernate ORM 5.2+ for Spring's `HibernateJpaVendorAdapter` as well as for a native Hibernate `SessionFactory` setup. -Is is strongly recommended to go with Hibernate ORM 5.4 for a newly started application. +It is strongly recommended to go with Hibernate ORM 5.4 for a newly started application. +For use with `HibernateJpaVendorAdapter`, Hibernate Search needs to be upgraded to 5.11.6. [[orm-session-factory-setup]] @@ -7810,7 +7873,7 @@ resource definitions in the container or locally within the application is mainl matter of the transaction strategy that you use. Compared to a Spring-defined local `SessionFactory`, a manually registered JNDI `SessionFactory` does not provide any benefits. Deploying a `SessionFactory` through Hibernate's JCA connector provides the -added value of participating in the Java EE server's management infrastructure, but does +added value of participating in the Jakarta EE server's management infrastructure, but does not add actual value beyond that. Spring's transaction support is not bound to a container. When configured with any strategy @@ -7820,11 +7883,7 @@ local transaction support is a lightweight and powerful alternative to JTA. When local EJB stateless session beans to drive transactions, you depend both on an EJB container and on JTA, even if you access only a single database and use only stateless session beans to provide declarative transactions through container-managed -transactions. Direct use of JTA programmatically also requires a Java EE environment. -JTA does not involve only container dependencies in terms of JTA itself and of -JNDI `DataSource` instances. For non-Spring, JTA-driven Hibernate transactions, you have -to use the Hibernate JCA connector or extra Hibernate transaction code with the -`TransactionManagerLookup` configured for proper JVM-level caching. +transactions. Direct use of JTA programmatically also requires a Jakarta EE environment. Spring-driven transactions can work as well with a locally defined Hibernate `SessionFactory` as they do with a local JDBC `DataSource`, provided they access a @@ -7832,12 +7891,7 @@ single database. Thus, you need only use Spring's JTA transaction strategy when have distributed transaction requirements. A JCA connector requires container-specific deployment steps, and (obviously) JCA support in the first place. This configuration requires more work than deploying a simple web application with local resource -definitions and Spring-driven transactions. Also, you often need the Enterprise Edition -of your container if you use, for example, WebLogic Express, which does not -provide JCA. A Spring application with local resources and transactions that span one -single database works in any Java EE web container (without JTA, JCA, or EJB), such as -Tomcat, Resin, or even plain Jetty. Additionally, you can easily reuse such a middle -tier in desktop applications or test suites. +definitions and Spring-driven transactions. All things considered, if you do not use EJBs, stick with local `SessionFactory` setup and Spring's `HibernateTransactionManager` or `JtaTransactionManager`. You get all of @@ -7958,18 +8012,18 @@ persistence unit name. The following XML example configures such a bean: This form of JPA deployment is the simplest and the most limited. You cannot refer to an existing JDBC `DataSource` bean definition, and no support for global transactions exists. Furthermore, weaving (byte-code transformation) of persistent classes is -provider-specific, often requiring a specific JVM agent to specified on startup. This +provider-specific, often requiring a specific JVM agent to be specified on startup. This option is sufficient only for stand-alone applications and test environments, for which the JPA specification is designed. [[orm-jpa-setup-jndi]] ===== Obtaining an EntityManagerFactory from JNDI -You can use this option when deploying to a Java EE server. Check your server's documentation +You can use this option when deploying to a Jakarta EE server. Check your server's documentation on how to deploy a custom JPA provider into your server, allowing for a different provider than the server's default. -Obtaining an `EntityManagerFactory` from JNDI (for example in a Java EE environment), +Obtaining an `EntityManagerFactory` from JNDI (for example in a Jakarta EE environment), is a matter of changing the XML configuration, as the following example shows: [source,xml,indent=0,subs="verbatim,quotes"] @@ -7979,13 +8033,13 @@ is a matter of changing the XML configuration, as the following example shows: ---- -This action assumes standard Java EE bootstrapping. The Java EE server auto-detects +This action assumes standard Jakarta EE bootstrapping. The Jakarta EE server auto-detects persistence units (in effect, `META-INF/persistence.xml` files in application jars) and -`persistence-unit-ref` entries in the Java EE deployment descriptor (for example, +`persistence-unit-ref` entries in the Jakarta EE deployment descriptor (for example, `web.xml`) and defines environment naming context locations for those persistence units. In such a scenario, the entire persistence unit deployment, including the weaving -(byte-code transformation) of persistent classes, is up to the Java EE server. The JDBC +(byte-code transformation) of persistent classes, is up to the Jakarta EE server. The JDBC `DataSource` is defined through a JNDI location in the `META-INF/persistence.xml` file. `EntityManager` transactions are integrated with the server's JTA subsystem. Spring merely uses the obtained `EntityManagerFactory`, passing it on to application objects through @@ -8057,12 +8111,12 @@ so on. However, it also imposes requirements on the runtime environment, such as availability of a weaving-capable class loader if the persistence provider demands byte-code transformation. -This option may conflict with the built-in JPA capabilities of a Java EE server. In a -full Java EE environment, consider obtaining your `EntityManagerFactory` from JNDI. +This option may conflict with the built-in JPA capabilities of a Jakarta EE server. In a +full Jakarta EE environment, consider obtaining your `EntityManagerFactory` from JNDI. Alternatively, specify a custom `persistenceXmlLocation` on your `LocalContainerEntityManagerFactoryBean` definition (for example, META-INF/my-persistence.xml) and include only a descriptor with that name in your -application jar files. Because the Java EE server looks only for default +application jar files. Because the Jakarta EE server looks only for default `META-INF/persistence.xml` files, it ignores such custom persistence units and, hence, avoids conflicts with a Spring-driven JPA setup upfront. (This applies to Resin 3.1, for example.) @@ -8093,7 +8147,7 @@ more insight regarding the `LoadTimeWeaver` implementations and their setup, eit generic or customized to various platforms (such as Tomcat, JBoss and WebSphere). As described in <>, you can configure -a context-wide `LoadTimeWeaver` by using the `@EnableLoadTimeWeaving` annotation of the +a context-wide `LoadTimeWeaver` by using the `@EnableLoadTimeWeaving` annotation or the `context:load-time-weaver` XML element. Such a global weaver is automatically picked up by all JPA `LocalContainerEntityManagerFactoryBean` instances. The following example shows the preferred way of setting up a load-time weaver, delivering auto-detection @@ -8222,11 +8276,17 @@ that uses the `@PersistenceUnit` annotation: } public Collection loadProductsByCategory(String category) { - try (EntityManager em = this.emf.createEntityManager()) { + EntityManager em = this.emf.createEntityManager(); + try { Query query = em.createQuery("from Product as p where p.category = ?1"); query.setParameter(1, category); return query.getResultList(); } + finally { + if (em != null) { + em.close(); + } + } } } ---- @@ -8344,7 +8404,7 @@ protected, or private) does not matter. What about class-level annotations? -On the Java EE platform, they are used for dependency declaration and not for resource +On the Jakarta EE platform, they are used for dependency declaration and not for resource injection. **** @@ -8417,13 +8477,13 @@ more details of its operations and how they are used within Spring's JPA support ==== Setting up JPA with JTA Transaction Management As an alternative to `JpaTransactionManager`, Spring also allows for multi-resource -transaction coordination through JTA, either in a Java EE environment or with a +transaction coordination through JTA, either in a Jakarta EE environment or with a stand-alone transaction coordinator, such as Atomikos. Aside from choosing Spring's `JtaTransactionManager` instead of `JpaTransactionManager`, you need to take few further steps: * The underlying JDBC connection pools need to be XA-capable and be integrated with -your transaction coordinator. This is usually straightforward in a Java EE environment, +your transaction coordinator. This is usually straightforward in a Jakarta EE environment, exposing a different kind of `DataSource` through JNDI. See your application server documentation for details. Analogously, a standalone transaction coordinator usually comes with special XA-integrated `DataSource` variants. Again, check its documentation. @@ -8555,8 +8615,7 @@ the two Spring interfaces used for this purpose. Spring abstracts all marshalling operations behind the `org.springframework.oxm.Marshaller` interface, the main method of which follows: -[source,java,indent=0,subs="verbatim,quotes",role="primary"] -.Java +[source,java,indent=0,subs="verbatim,quotes"] ---- public interface Marshaller { @@ -8566,22 +8625,6 @@ Spring abstracts all marshalling operations behind the void marshal(Object graph, Result result) throws XmlMappingException, IOException; } ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] -.Kotlin ----- - interface Marshaller { - - /** - * Marshal the object graph with the given root into the provided Result. - */ - @Throws(XmlMappingException::class, IOException::class) - fun marshal( - graph: Any, - result: Result - ) - } ----- - The `Marshaller` interface has one main method, which marshals the given object to a given `javax.xml.transform.Result`. The result is a tagging interface that basically @@ -8615,8 +8658,7 @@ to determine how your O-X technology manages this. Similar to the `Marshaller`, we have the `org.springframework.oxm.Unmarshaller` interface, which the following listing shows: -[source,java,indent=0,subs="verbatim,quotes",role="primary"] -.Java +[source,java,indent=0,subs="verbatim,quotes"] ---- public interface Unmarshaller { @@ -8626,18 +8668,6 @@ interface, which the following listing shows: Object unmarshal(Source source) throws XmlMappingException, IOException; } ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] -.Kotlin ----- - interface Unmarshaller { - - /** - * Unmarshal the given provided Source into an object graph. - */ - @Throws(XmlMappingException::class, IOException::class) - fun unmarshal(source: Source): Any - } ----- This interface also has one method, which reads from the given `javax.xml.transform.Source` (an XML input abstraction) and returns the object read. As @@ -8809,8 +8839,8 @@ can do so by using the following `applicationContext.xml`: ---- This application context uses XStream, but we could have used any of the other marshaller -instances described later in this chapter. Note that, by default, XStream does not require any further -configuration, so the bean definition is rather simple. Also note that the +instances described later in this chapter. Note that, by default, XStream does not require +any further configuration, so the bean definition is rather simple. Also note that the `XStreamMarshaller` implements both `Marshaller` and `Unmarshaller`, so we can refer to the `xstreamMarshaller` bean in both the `marshaller` and `unmarshaller` property of the application. @@ -8828,12 +8858,11 @@ This sample application produces the following `settings.xml` file: [[oxm-schema-based-config]] === XML Configuration Namespace -You can configure marshallers more concisely by using tags from the OXM namespace. To -make these tags available, you must first reference the appropriate schema in the +You can configure marshallers more concisely by using tags from the OXM namespace. +To make these tags available, you must first reference the appropriate schema in the preamble of the XML configuration file. The following example shows how to do so: -[source,xml,indent=0] -[subs="verbatim,quotes"] +[source,xml,indent=0,subs="verbatim,quotes"] ---- +

Spring Framework Documentation

+ +{revnumber} + diff --git a/framework-docs/src/docs/asciidoc/index.adoc b/framework-docs/src/docs/asciidoc/index.adoc new file mode 100644 index 000000000000..15e7db515548 --- /dev/null +++ b/framework-docs/src/docs/asciidoc/index.adoc @@ -0,0 +1,37 @@ +:noheader: += Spring Framework Documentation + +[horizontal] +<> :: history, design philosophy, feedback, +getting started. +<> :: IoC Container, Events, Resources, i18n, +Validation, Data Binding, Type Conversion, SpEL, AOP, AOT. +<> :: Mock Objects, TestContext Framework, +Spring MVC Test, WebTestClient. +<> :: Transactions, DAO Support, +JDBC, R2DBC, O/R Mapping, XML Marshalling. +<> :: Spring MVC, WebSocket, SockJS, +STOMP Messaging. +<> :: Spring WebFlux, WebClient, +WebSocket, RSocket. +<> :: REST Clients, JMS, JCA, JMX, +Email, Tasks, Scheduling, Caching. +<> :: Kotlin, Groovy, Dynamic Languages. +<> :: Spring properties. +https://github.com/spring-projects/spring-framework/wiki[*Wiki*] :: What's New, +Upgrade Notes, Supported Versions, and other cross-version information. + +NOTE: This documentation is available in {docs-spring-framework}/reference/html/index.html[HTML] and {docs-spring-framework}/reference/pdf/spring-framework.pdf[PDF] formats. + +Rod Johnson, Juergen Hoeller, Keith Donald, Colin Sampaleanu, Rob Harrop, Thomas Risberg, +Alef Arendsen, Darren Davison, Dmitriy Kopylenko, Mark Pollack, Thierry Templier, Erwin +Vervaet, Portia Tung, Ben Hale, Adrian Colyer, John Lewis, Costin Leau, Mark Fisher, Sam +Brannen, Ramnivas Laddad, Arjen Poutsma, Chris Beams, Tareq Abedrabbo, Andy Clement, Dave +Syer, Oliver Gierke, Rossen Stoyanchev, Phillip Webb, Rob Winch, Brian Clozel, Stephane +Nicoll, Sebastien Deleuze, Jay Bryant, Mark Paluch + +Copyright © 2002 - 2022 VMware, Inc. All Rights Reserved. + +Copies of this document may be made for your own use and for distribution to others, +provided that you do not charge any fee for such copies and further provided that each +copy contains this Copyright Notice, whether distributed in print or electronically. diff --git a/framework-docs/src/docs/asciidoc/integration.adoc b/framework-docs/src/docs/asciidoc/integration.adoc new file mode 100644 index 000000000000..0bc29cf8dbf6 --- /dev/null +++ b/framework-docs/src/docs/asciidoc/integration.adoc @@ -0,0 +1,5737 @@ +[[spring-integration]] += Integration +:doc-spring-amqp: {doc-root}/spring-amqp/docs/current/reference +:doc-spring-gemfire: {doc-root}/spring-gemfire/docs/current/reference +:toc: left +:toclevels: 4 +:tabsize: 4 +:docinfo1: + +This part of the reference documentation covers Spring Framework's integration with +a number of technologies. + + +[[rest-client-access]] +== REST Clients + +The Spring Framework provides the following choices for making calls to REST endpoints: + +* <> - non-blocking, reactive client w fluent API. +* <> - synchronous client with template method API. +* <> - annotated interface with generated, dynamic proxy implementation. + + +[[rest-webclient]] +=== `WebClient` + +`WebClient` is a non-blocking, reactive client to perform HTTP requests. It was +introduced in 5.0 and offers an alternative to the `RestTemplate`, with support for +synchronous, asynchronous, and streaming scenarios. + +`WebClient` supports the following: + +* Non-blocking I/O. +* Reactive Streams back pressure. +* High concurrency with fewer hardware resources. +* Functional-style, fluent API that takes advantage of Java 8 lambdas. +* Synchronous and asynchronous interactions. +* Streaming up to or streaming down from a server. + +See <> for more details. + + + + +[[rest-resttemplate]] +=== `RestTemplate` + +The `RestTemplate` provides a higher level API over HTTP client libraries. It makes it +easy to invoke REST endpoints in a single line. It exposes the following groups of +overloaded methods: + +NOTE: `RestTemplate` is in maintenance mode, with only requests for minor +changes and bugs to be accepted. Please, consider using the +<> instead. + +[[rest-overview-of-resttemplate-methods-tbl]] +.RestTemplate methods +[cols="1,3"] +|=== +| Method group | Description + +| `getForObject` +| Retrieves a representation via GET. + +| `getForEntity` +| Retrieves a `ResponseEntity` (that is, status, headers, and body) by using GET. + +| `headForHeaders` +| Retrieves all headers for a resource by using HEAD. + +| `postForLocation` +| Creates a new resource by using POST and returns the `Location` header from the response. + +| `postForObject` +| Creates a new resource by using POST and returns the representation from the response. + +| `postForEntity` +| Creates a new resource by using POST and returns the representation from the response. + +| `put` +| Creates or updates a resource by using PUT. + +| `patchForObject` +| Updates a resource by using PATCH and returns the representation from the response. +Note that the JDK `HttpURLConnection` does not support `PATCH`, but Apache +HttpComponents and others do. + +| `delete` +| Deletes the resources at the specified URI by using DELETE. + +| `optionsForAllow` +| Retrieves allowed HTTP methods for a resource by using ALLOW. + +| `exchange` +| More generalized (and less opinionated) version of the preceding methods that provides extra +flexibility when needed. It accepts a `RequestEntity` (including HTTP method, URL, headers, +and body as input) and returns a `ResponseEntity`. + +These methods allow the use of `ParameterizedTypeReference` instead of `Class` to specify +a response type with generics. + +| `execute` +| The most generalized way to perform a request, with full control over request +preparation and response extraction through callback interfaces. + +|=== + +[[rest-resttemplate-create]] +==== Initialization + +The default constructor uses `java.net.HttpURLConnection` to perform requests. You can +switch to a different HTTP library with an implementation of `ClientHttpRequestFactory`. +There is built-in support for the following: + +* Apache HttpComponents +* Netty +* OkHttp + +For example, to switch to Apache HttpComponents, you can use the following: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + RestTemplate template = new RestTemplate(new HttpComponentsClientHttpRequestFactory()); +---- + +Each `ClientHttpRequestFactory` exposes configuration options specific to the underlying +HTTP client library -- for example, for credentials, connection pooling, and other details. + +TIP: Note that the `java.net` implementation for HTTP requests can raise an exception when +accessing the status of a response that represents an error (such as 401). If this is an +issue, switch to another HTTP client library. + +[[rest-resttemplate-uri]] +===== URIs + +Many of the `RestTemplate` methods accept a URI template and URI template variables, +either as a `String` variable argument, or as `Map`. + +The following example uses a `String` variable argument: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + String result = restTemplate.getForObject( + "https://example.com/hotels/%7Bhotel%7D/bookings/%7Bbooking%7D", String.class, "42", "21"); +---- + +The following example uses a `Map`: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + Map vars = Collections.singletonMap("hotel", "42"); + + String result = restTemplate.getForObject( + "https://example.com/hotels/%7Bhotel%7D/rooms/%7Bhotel%7D", String.class, vars); +---- + +Keep in mind URI templates are automatically encoded, as the following example shows: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + restTemplate.getForObject("https://example.com/hotel%20list", String.class); + + // Results in request to "https://example.com/hotel%20list" +---- + +You can use the `uriTemplateHandler` property of `RestTemplate` to customize how URIs +are encoded. Alternatively, you can prepare a `java.net.URI` and pass it into one of +the `RestTemplate` methods that accepts a `URI`. + +For more details on working with and encoding URIs, see <>. + +[[rest-template-headers]] +===== Headers + +You can use the `exchange()` methods to specify request headers, as the following example shows: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + String uriTemplate = "https://example.com/hotels/%7Bhotel%7D"; + URI uri = UriComponentsBuilder.fromUriString(uriTemplate).build(42); + + RequestEntity requestEntity = RequestEntity.get(uri) + .header("MyRequestHeader", "MyValue") + .build(); + + ResponseEntity response = template.exchange(requestEntity, String.class); + + String responseHeader = response.getHeaders().getFirst("MyResponseHeader"); + String body = response.getBody(); +---- + +You can obtain response headers through many `RestTemplate` method variants that return +`ResponseEntity`. + +[[rest-template-body]] +==== Body + +Objects passed into and returned from `RestTemplate` methods are converted to and from raw +content with the help of an `HttpMessageConverter`. + +On a POST, an input object is serialized to the request body, as the following example shows: + +---- +URI location = template.postForLocation("https://example.com/people", person); +---- + +You need not explicitly set the Content-Type header of the request. In most cases, +you can find a compatible message converter based on the source `Object` type, and the chosen +message converter sets the content type accordingly. If necessary, you can use the +`exchange` methods to explicitly provide the `Content-Type` request header, and that, in +turn, influences what message converter is selected. + +On a GET, the body of the response is deserialized to an output `Object`, as the following example shows: + +---- +Person person = restTemplate.getForObject("https://example.com/people/%7Bid%7D", Person.class, 42); +---- + +The `Accept` header of the request does not need to be explicitly set. In most cases, +a compatible message converter can be found based on the expected response type, which +then helps to populate the `Accept` header. If necessary, you can use the `exchange` +methods to provide the `Accept` header explicitly. + +By default, `RestTemplate` registers all built-in +<>, depending on classpath checks that help +to determine what optional conversion libraries are present. You can also set the message +converters to use explicitly. + +[[rest-message-conversion]] +==== Message Conversion +[.small]#<># + +The `spring-web` module contains the `HttpMessageConverter` contract for reading and +writing the body of HTTP requests and responses through `InputStream` and `OutputStream`. +`HttpMessageConverter` instances are used on the client side (for example, in the `RestTemplate`) and +on the server side (for example, in Spring MVC REST controllers). + +Concrete implementations for the main media (MIME) types are provided in the framework +and are, by default, registered with the `RestTemplate` on the client side and with +`RequestMappingHandlerAdapter` on the server side (see +<>). + +The implementations of `HttpMessageConverter` are described in the following sections. +For all converters, a default media type is used, but you can override it by setting the +`supportedMediaTypes` bean property. The following table describes each implementation: + +[[rest-message-converters-tbl]] +.HttpMessageConverter Implementations +[cols="1,3"] +|=== +| MessageConverter | Description + +| `StringHttpMessageConverter` +| An `HttpMessageConverter` implementation that can read and write `String` instances from the HTTP +request and response. By default, this converter supports all text media types +(`text/{asterisk}`) and writes with a `Content-Type` of `text/plain`. + +| `FormHttpMessageConverter` +| An `HttpMessageConverter` implementation that can read and write form data from the HTTP +request and response. By default, this converter reads and writes the +`application/x-www-form-urlencoded` media type. Form data is read from and written into a +`MultiValueMap`. The converter can also write (but not read) multipart +data read from a `MultiValueMap`. By default, `multipart/form-data` is +supported. As of Spring Framework 5.2, additional multipart subtypes can be supported for +writing form data. Consult the javadoc for `FormHttpMessageConverter` for further details. + +| `ByteArrayHttpMessageConverter` +| An `HttpMessageConverter` implementation that can read and write byte arrays from the +HTTP request and response. By default, this converter supports all media types (`{asterisk}/{asterisk}`) +and writes with a `Content-Type` of `application/octet-stream`. You can override this +by setting the `supportedMediaTypes` property and overriding `getContentType(byte[])`. + +| `MarshallingHttpMessageConverter` +| An `HttpMessageConverter` implementation that can read and write XML by using Spring's +`Marshaller` and `Unmarshaller` abstractions from the `org.springframework.oxm` package. +This converter requires a `Marshaller` and `Unmarshaller` before it can be used. You can inject these +through constructor or bean properties. By default, this converter supports +`text/xml` and `application/xml`. + +| `MappingJackson2HttpMessageConverter` +| An `HttpMessageConverter` implementation that can read and write JSON by using Jackson's +`ObjectMapper`. You can customize JSON mapping as needed through the use of Jackson's +provided annotations. When you need further control (for cases where custom JSON +serializers/deserializers need to be provided for specific types), you can inject a custom `ObjectMapper` +through the `ObjectMapper` property. By default, this +converter supports `application/json`. + +| `MappingJackson2XmlHttpMessageConverter` +| An `HttpMessageConverter` implementation that can read and write XML by using +https://github.com/FasterXML/jackson-dataformat-xml[Jackson XML] extension's +`XmlMapper`. You can customize XML mapping as needed through the use of JAXB +or Jackson's provided annotations. When you need further control (for cases where custom XML +serializers/deserializers need to be provided for specific types), you can inject a custom `XmlMapper` +through the `ObjectMapper` property. By default, this +converter supports `application/xml`. + +| `SourceHttpMessageConverter` +| An `HttpMessageConverter` implementation that can read and write +`javax.xml.transform.Source` from the HTTP request and response. Only `DOMSource`, +`SAXSource`, and `StreamSource` are supported. By default, this converter supports +`text/xml` and `application/xml`. + +| `BufferedImageHttpMessageConverter` +| An `HttpMessageConverter` implementation that can read and write +`java.awt.image.BufferedImage` from the HTTP request and response. This converter reads +and writes the media type supported by the Java I/O API. + +|=== + +[[rest-template-jsonview]] +==== Jackson JSON Views + +You can specify a https://www.baeldung.com/jackson-json-view-annotation[Jackson JSON View] +to serialize only a subset of the object properties, as the following example shows: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + MappingJacksonValue value = new MappingJacksonValue(new User("eric", "7!jd#h23")); + value.setSerializationView(User.WithoutPasswordView.class); + + RequestEntity requestEntity = + RequestEntity.post(new URI("https://example.com/user")).body(value); + + ResponseEntity response = template.exchange(requestEntity, String.class); +---- + +[[rest-template-multipart]] +==== Multipart + +To send multipart data, you need to provide a `MultiValueMap` whose values +may be an `Object` for part content, a `Resource` for a file part, or an `HttpEntity` for +part content with headers. For example: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + MultiValueMap parts = new LinkedMultiValueMap<>(); + + parts.add("fieldPart", "fieldValue"); + parts.add("filePart", new FileSystemResource("...logo.png")); + parts.add("jsonPart", new Person("Jason")); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_XML); + parts.add("xmlPart", new HttpEntity<>(myBean, headers)); +---- + +In most cases, you do not have to specify the `Content-Type` for each part. The content +type is determined automatically based on the `HttpMessageConverter` chosen to serialize +it or, in the case of a `Resource` based on the file extension. If necessary, you can +explicitly provide the `MediaType` with an `HttpEntity` wrapper. + +Once the `MultiValueMap` is ready, you can pass it to the `RestTemplate`, as show below: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + MultiValueMap parts = ...; + template.postForObject("https://example.com/upload", parts, Void.class); +---- + +If the `MultiValueMap` contains at least one non-`String` value, the `Content-Type` is set +to `multipart/form-data` by the `FormHttpMessageConverter`. If the `MultiValueMap` has +`String` values the `Content-Type` is defaulted to `application/x-www-form-urlencoded`. +If necessary the `Content-Type` may also be set explicitly. + + +[[rest-http-interface]] +=== HTTP Interface + +The Spring Framework lets you define an HTTP service as a Java interface with annotated +methods for HTTP exchanges. You can then generate a proxy that implements this interface +and performs the exchanges. This helps to simplify HTTP remote access which often +involves a facade that wraps the details of using the underlying HTTP client. + +One, declare an interface with `@HttpExchange` methods: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + interface RepositoryService { + + @GetExchange("/repos/{owner}/{repo}") + Repository getRepository(@PathVariable String owner, @PathVariable String repo); + + // more HTTP exchange methods... + + } +---- + +Two, create a proxy that will perform the declared HTTP exchanges: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + WebClient client = WebClient.builder().baseUrl("https://api.github.com/").build(); + HttpServiceProxyFactory factory = HttpServiceProxyFactory.builder(WebClientAdapter.forClient(client)).build(); + + RepositoryService service = factory.createClient(RepositoryService.class); +---- + +`@HttpExchange` is supported at the type level where it applies to all methods: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @HttpExchange(url = "/repos/{owner}/{repo}", accept = "application/vnd.github.v3+json") + interface RepositoryService { + + @GetExchange + Repository getRepository(@PathVariable String owner, @PathVariable String repo); + + @PatchExchange(contentType = MediaType.APPLICATION_FORM_URLENCODED_VALUE) + void updateRepository(@PathVariable String owner, @PathVariable String repo, + @RequestParam String name, @RequestParam String description, @RequestParam String homepage); + + } +---- + + +[[rest-http-interface-method-parameters]] +==== Method Parameters + +Annotated, HTTP exchange methods support flexible method signatures with the following +method parameters: + +[cols="1,2", options="header"] +|=== +| Method argument | Description + +| `URI` +| Dynamically set the URL for the request, overriding the annotation's `url` attribute. + +| `HttpMethod` +| Dynamically set the HTTP method for the request, overriding the annotation's `method` attribute + +| `@RequestHeader` +| Add a request header or mutliple headers. The argument may be a `Map` or + `MultiValueMap` with multiple headers, a `Collection` of values, or an + individual value. Type conversion is supported for non-String values. + +| `@PathVariable` +| Add a variable for expand a placeholder in the request URL. The argument may be a + `Map` with multiple variables, or an individual value. Type conversion + is supported for non-String values. + +| `@RequestBody` +| Provide the body of the request either as an Object to be serialized, or a + Reactive Streams `Publisher` such as `Mono`, `Flux`, or any other async type supported + through the configured `ReactiveAdapterRegistry`. + +| `@RequestParam` +| Add a request parameter or mutliple parameters. The argument may be a `Map` + or `MultiValueMap` with multiple parameters, a `Collection` of values, or + an individual value. Type conversion is supported for non-String values. + + When `"content-type"` is set to `"application/x-www-form-urlencoded"`, request + parameters are encoded in the request body. Otherwise, they are added as URL query + parameters. + +| `@RequestPart` +| Add a request part, which may be a String (form field), `Resource` (file part), + Object (entity to be encoded, e.g. as JSON), `HttpEntity` (part content and headers), + a Spring `Part`, or Reactive Streams `Publisher` of any of the above. + +| `@CookieValue` +| Add a cookie or mutliple cookies. The argument may be a `Map` or + `MultiValueMap` with multiple cookies, a `Collection` of values, or an + individual value. Type conversion is supported for non-String values. + +|=== + + +[[rest-http-interface-return-values]] +==== Return Values + +Annotated, HTTP exchange methods support the following return values: + +[cols="1,2", options="header"] +|=== +| Method return value | Description + +| `void`, `Mono` +| Perform the given request, and release the response content, if any. + +| `HttpHeaders`, `Mono` +| Perform the given request, release the response content, if any, and return the + response headers. + +| ``, `Mono` +| Perform the given request and decode the response content to the declared return type. + +| ``, `Flux` +| Perform the given request and decode the response content to a stream of the declared + element type. + +| `ResponseEntity`, `Mono>` +| Perform the given request, and release the response content, if any, and return a + `ResponseEntity` with the status and headers. + +| `ResponseEntity`, `Mono>` +| Perform the given request, decode the response content to the declared return type, and + return a `ResponseEntity` with the status, headers, and the decoded body. + +| `Mono>` +| Perform the given request, decode the response content to a stream of the declared + element type, and return a `ResponseEntity` with the status, headers, and the decoded + response body stream. + +|=== + +TIP: You can also use any other async or reactive types registered in the +`ReactiveAdapterRegistry`. + + +[[rest-http-interface-exceptions]] +==== Exception Handling + +By default, `WebClient` raises `WebClientResponseException` for 4xx and 5xx HTTP status +codes. To customize this, you can register a response status handler that applies to all +responses performed through the client: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + WebClient webClient = WebClient.builder() + .defaultStatusHandler(HttpStatusCode::isError, resp -> ...) + .build(); + + WebClientAdapter clientAdapter = WebClientAdapter.forClient(webClient); + HttpServiceProxyFactory factory = HttpServiceProxyFactory + .builder(clientAdapter).build(); +---- + +For more details and options, such as suppressing error status codes, see the Javadoc of +`defaultStatusHandler` in `WebClient.Builder`. + + + + +[[jms]] +== JMS (Java Message Service) + +Spring provides a JMS integration framework that simplifies the use of the JMS API in much +the same way as Spring's integration does for the JDBC API. + +JMS can be roughly divided into two areas of functionality, namely the production and +consumption of messages. The `JmsTemplate` class is used for message production and +synchronous message reception. For asynchronous reception similar to Jakarta EE's +message-driven bean style, Spring provides a number of message-listener containers that +you can use to create Message-Driven POJOs (MDPs). Spring also provides a declarative way +to create message listeners. + +The `org.springframework.jms.core` package provides the core functionality for using +JMS. It contains JMS template classes that simplify the use of the JMS by handling the +creation and release of resources, much like the `JdbcTemplate` does for JDBC. The +design principle common to Spring template classes is to provide helper methods to +perform common operations and, for more sophisticated usage, delegate the essence of the +processing task to user-implemented callback interfaces. The JMS template follows the +same design. The classes offer various convenience methods for sending messages, +consuming messages synchronously, and exposing the JMS session and message producer to +the user. + +The `org.springframework.jms.support` package provides `JMSException` translation +functionality. The translation converts the checked `JMSException` hierarchy to a +mirrored hierarchy of unchecked exceptions. If any provider-specific subclasses +of the checked `jakarta.jms.JMSException` exist, this exception is wrapped in the +unchecked `UncategorizedJmsException`. + +The `org.springframework.jms.support.converter` package provides a `MessageConverter` +abstraction to convert between Java objects and JMS messages. + +The `org.springframework.jms.support.destination` package provides various strategies +for managing JMS destinations, such as providing a service locator for destinations +stored in JNDI. + +The `org.springframework.jms.annotation` package provides the necessary infrastructure +to support annotation-driven listener endpoints by using `@JmsListener`. + +The `org.springframework.jms.config` package provides the parser implementation for the +`jms` namespace as well as the java config support to configure listener containers and +create listener endpoints. + +Finally, the `org.springframework.jms.connection` package provides an implementation of +the `ConnectionFactory` suitable for use in standalone applications. It also contains an +implementation of Spring's `PlatformTransactionManager` for JMS (the cunningly named +`JmsTransactionManager`). This allows for seamless integration of JMS as a transactional +resource into Spring's transaction management mechanisms. + +[NOTE] +==== +As of Spring Framework 5, Spring's JMS package fully supports JMS 2.0 and requires the +JMS 2.0 API to be present at runtime. We recommend the use of a JMS 2.0 compatible provider. + +If you happen to use an older message broker in your system, you may try upgrading to a +JMS 2.0 compatible driver for your existing broker generation. Alternatively, you may also +try to run against a JMS 1.1 based driver, simply putting the JMS 2.0 API jar on the +classpath but only using JMS 1.1 compatible API against your driver. Spring's JMS support +adheres to JMS 1.1 conventions by default, so with corresponding configuration it does +support such a scenario. However, please consider this for transition scenarios only. +==== + + + +[[jms-using]] +=== Using Spring JMS + +This section describes how to use Spring's JMS components. + + +[[jms-jmstemplate]] +==== Using `JmsTemplate` + +The `JmsTemplate` class is the central class in the JMS core package. It simplifies the +use of JMS, since it handles the creation and release of resources when sending or +synchronously receiving messages. + +Code that uses the `JmsTemplate` needs only to implement callback interfaces that give them +a clearly defined high-level contract. The `MessageCreator` callback interface creates a +message when given a `Session` provided by the calling code in `JmsTemplate`. To +allow for more complex usage of the JMS API, `SessionCallback` provides the +JMS session, and `ProducerCallback` exposes a `Session` and +`MessageProducer` pair. + +The JMS API exposes two types of send methods, one that takes delivery mode, priority, +and time-to-live as Quality of Service (QOS) parameters and one that takes no QOS +parameters and uses default values. Since `JmsTemplate` has many send methods, +setting the QOS parameters have been exposed as bean properties to +avoid duplication in the number of send methods. Similarly, the timeout value for +synchronous receive calls is set by using the `setReceiveTimeout` property. + +Some JMS providers allow the setting of default QOS values administratively through the +configuration of the `ConnectionFactory`. This has the effect that a call to a +`MessageProducer` instance's `send` method (`send(Destination destination, Message message)`) +uses different QOS default values than those specified in the JMS specification. In order +to provide consistent management of QOS values, the `JmsTemplate` must, therefore, be +specifically enabled to use its own QOS values by setting the boolean property +`isExplicitQosEnabled` to `true`. + +For convenience, `JmsTemplate` also exposes a basic request-reply operation that allows +for sending a message and waiting for a reply on a temporary queue that is created as part of +the operation. + +IMPORTANT: Instances of the `JmsTemplate` class are thread-safe, once configured. This is +important, because it means that you can configure a single instance of a `JmsTemplate` +and then safely inject this shared reference into multiple collaborators. To be +clear, the `JmsTemplate` is stateful, in that it maintains a reference to a +`ConnectionFactory`, but this state is not conversational state. + +As of Spring Framework 4.1, `JmsMessagingTemplate` is built on top of `JmsTemplate` +and provides an integration with the messaging abstraction -- that is, +`org.springframework.messaging.Message`. This lets you create the message to +send in a generic manner. + + +[[jms-connections]] +==== Connections + +The `JmsTemplate` requires a reference to a `ConnectionFactory`. The `ConnectionFactory` +is part of the JMS specification and serves as the entry point for working with JMS. It +is used by the client application as a factory to create connections with the JMS +provider and encapsulates various configuration parameters, many of which are +vendor-specific, such as SSL configuration options. + +When using JMS inside an EJB, the vendor provides implementations of the JMS interfaces +so that they can participate in declarative transaction management and perform pooling +of connections and sessions. In order to use this implementation, Jakarta EE containers +typically require that you declare a JMS connection factory as a `resource-ref` inside +the EJB or servlet deployment descriptors. To ensure the use of these features with the +`JmsTemplate` inside an EJB, the client application should ensure that it references the +managed implementation of the `ConnectionFactory`. + +[[jms-caching-resources]] +===== Caching Messaging Resources + +The standard API involves creating many intermediate objects. To send a message, the +following 'API' walk is performed: + +[literal] +[subs="verbatim,quotes"] +---- +ConnectionFactory->Connection->Session->MessageProducer->send +---- + +Between the `ConnectionFactory` and the `Send` operation, three intermediate +objects are created and destroyed. To optimize the resource usage and increase +performance, Spring provides two implementations of `ConnectionFactory`. + +[[jms-connection-factory]] +===== Using `SingleConnectionFactory` + +Spring provides an implementation of the `ConnectionFactory` interface, +`SingleConnectionFactory`, that returns the same `Connection` on all +`createConnection()` calls and ignores calls to `close()`. This is useful for testing and +standalone environments so that the same connection can be used for multiple +`JmsTemplate` calls that may span any number of transactions. `SingleConnectionFactory` +takes a reference to a standard `ConnectionFactory` that would typically come from JNDI. + +[[jdbc-connection-factory-caching]] +===== Using `CachingConnectionFactory` + +The `CachingConnectionFactory` extends the functionality of `SingleConnectionFactory` +and adds the caching of `Session`, `MessageProducer`, and `MessageConsumer` instances. +The initial cache size is set to `1`. You can use the `sessionCacheSize` property to +increase the number of cached sessions. Note that the number of actual cached sessions +is more than that number, as sessions are cached based on their acknowledgment mode, +so there can be up to four cached session instances (one for each acknowledgment mode) +when `sessionCacheSize` is set to one. `MessageProducer` and `MessageConsumer` instances +are cached within their owning session and also take into account the unique properties +of the producers and consumers when caching. MessageProducers are cached based on their +destination. MessageConsumers are cached based on a key composed of the destination, selector, +noLocal delivery flag, and the durable subscription name (if creating durable consumers). + +[NOTE] +==== +MessageProducers and MessageConsumers for temporary queues and topics +(TemporaryQueue/TemporaryTopic) will never be cached. Unfortunately, WebLogic JMS happens +to implement the temporary queue/topic interfaces on its regular destination implementation, +mis-indicating that none of its destinations can be cached. Please use a different connection +pool/cache on WebLogic, or customize `CachingConnectionFactory` for WebLogic purposes. +==== + + +[[jms-destinations]] +==== Destination Management + +Destinations, as `ConnectionFactory` instances, are JMS administered objects that you can store +and retrieve in JNDI. When configuring a Spring application context, you can use the +JNDI `JndiObjectFactoryBean` factory class or `` to perform dependency +injection on your object's references to JMS destinations. However, this strategy +is often cumbersome if there are a large number of destinations in the application or if there +are advanced destination management features unique to the JMS provider. Examples of +such advanced destination management include the creation of dynamic destinations or +support for a hierarchical namespace of destinations. The `JmsTemplate` delegates the +resolution of a destination name to a JMS destination object that implements the +`DestinationResolver` interface. `DynamicDestinationResolver` is the default +implementation used by `JmsTemplate` and accommodates resolving dynamic destinations. A +`JndiDestinationResolver` is also provided to act as a service locator for +destinations contained in JNDI and optionally falls back to the behavior contained in +`DynamicDestinationResolver`. + +Quite often, the destinations used in a JMS application are only known at runtime and, +therefore, cannot be administratively created when the application is deployed. This is +often because there is shared application logic between interacting system components +that create destinations at runtime according to a well-known naming convention. Even +though the creation of dynamic destinations is not part of the JMS specification, most +vendors have provided this functionality. Dynamic destinations are created with a user-defined name, +which differentiates them from temporary destinations, and are often +not registered in JNDI. The API used to create dynamic destinations varies from provider +to provider since the properties associated with the destination are vendor-specific. +However, a simple implementation choice that is sometimes made by vendors is to +disregard the warnings in the JMS specification and to use the method `TopicSession` +`createTopic(String topicName)` or the `QueueSession` `createQueue(String +queueName)` method to create a new destination with default destination properties. Depending +on the vendor implementation, `DynamicDestinationResolver` can then also create a +physical destination instead of only resolving one. + +The boolean property `pubSubDomain` is used to configure the `JmsTemplate` with +knowledge of what JMS domain is being used. By default, the value of this property is +false, indicating that the point-to-point domain, `Queues`, is to be used. This property +(used by `JmsTemplate`) determines the behavior of dynamic destination resolution through +implementations of the `DestinationResolver` interface. + +You can also configure the `JmsTemplate` with a default destination through the +property `defaultDestination`. The default destination is with send and receive +operations that do not refer to a specific destination. + + +[[jms-mdp]] +==== Message Listener Containers + +One of the most common uses of JMS messages in the EJB world is to drive message-driven +beans (MDBs). Spring offers a solution to create message-driven POJOs (MDPs) in a way +that does not tie a user to an EJB container. (See <> for detailed +coverage of Spring's MDP support.) Since Spring Framework 4.1, endpoint methods can be +annotated with `@JmsListener` -- see <> for more details. + +A message listener container is used to receive messages from a JMS message queue and +drive the `MessageListener` that is injected into it. The listener container is +responsible for all threading of message reception and dispatches into the listener for +processing. A message listener container is the intermediary between an MDP and a +messaging provider and takes care of registering to receive messages, participating in +transactions, resource acquisition and release, exception conversion, and so on. This +lets you write the (possibly complex) business logic +associated with receiving a message (and possibly respond to it), and delegates +boilerplate JMS infrastructure concerns to the framework. + +There are two standard JMS message listener containers packaged with Spring, each with +its specialized feature set. + +* <> +* <> + +[[jms-mdp-simple]] +===== Using `SimpleMessageListenerContainer` + +This message listener container is the simpler of the two standard flavors. It creates +a fixed number of JMS sessions and consumers at startup, registers the listener by using +the standard JMS `MessageConsumer.setMessageListener()` method, and leaves it up the JMS +provider to perform listener callbacks. This variant does not allow for dynamic adaption +to runtime demands or for participation in externally managed transactions. +Compatibility-wise, it stays very close to the spirit of the standalone JMS +specification, but is generally not compatible with Jakarta EE's JMS restrictions. + +NOTE: While `SimpleMessageListenerContainer` does not allow for participation in externally +managed transactions, it does support native JMS transactions. To enable this feature, +you can switch the `sessionTransacted` flag to `true` or, in the XML namespace, set the +`acknowledge` attribute to `transacted`. Exceptions thrown from your listener then lead +to a rollback, with the message getting redelivered. Alternatively, consider using +`CLIENT_ACKNOWLEDGE` mode, which provides redelivery in case of an exception as well but +does not use transacted `Session` instances and, therefore, does not include any other +`Session` operations (such as sending response messages) in the transaction protocol. + +IMPORTANT: The default `AUTO_ACKNOWLEDGE` mode does not provide proper reliability guarantees. +Messages can get lost when listener execution fails (since the provider automatically +acknowledges each message after listener invocation, with no exceptions to be propagated to +the provider) or when the listener container shuts down (you can configure this by setting +the `acceptMessagesWhileStopping` flag). Make sure to use transacted sessions in case of +reliability needs (for example, for reliable queue handling and durable topic subscriptions). + +[[jms-mdp-default]] +===== Using `DefaultMessageListenerContainer` + +This message listener container is used in most cases. In contrast to +`SimpleMessageListenerContainer`, this container variant allows for dynamic adaptation +to runtime demands and is able to participate in externally managed transactions. +Each received message is registered with an XA transaction when configured with a +`JtaTransactionManager`. As a result, processing may take advantage of XA transaction +semantics. This listener container strikes a good balance between low requirements on +the JMS provider, advanced functionality (such as participation in externally managed +transactions), and compatibility with Jakarta EE environments. + +You can customize the cache level of the container. Note that, when no caching is enabled, +a new connection and a new session is created for each message reception. Combining this +with a non-durable subscription with high loads may lead to message loss. Make sure to +use a proper cache level in such a case. + +This container also has recoverable capabilities when the broker goes down. By default, +a simple `BackOff` implementation retries every five seconds. You can specify +a custom `BackOff` implementation for more fine-grained recovery options. See +{api-spring-framework}/util/backoff/ExponentialBackOff.html[`ExponentialBackOff`] for an example. + +NOTE: Like its sibling (<>), +`DefaultMessageListenerContainer` supports native JMS transactions and allows for +customizing the acknowledgment mode. If feasible for your scenario, This is strongly +recommended over externally managed transactions -- that is, if you can live with +occasional duplicate messages in case of the JVM dying. Custom duplicate message +detection steps in your business logic can cover such situations -- for example, +in the form of a business entity existence check or a protocol table check. +Any such arrangements are significantly more efficient than the alternative: +wrapping your entire processing with an XA transaction (through configuring your +`DefaultMessageListenerContainer` with an `JtaTransactionManager`) to cover the +reception of the JMS message as well as the execution of the business logic in your +message listener (including database operations, etc.). + +IMPORTANT: The default `AUTO_ACKNOWLEDGE` mode does not provide proper reliability guarantees. +Messages can get lost when listener execution fails (since the provider automatically +acknowledges each message after listener invocation, with no exceptions to be propagated to +the provider) or when the listener container shuts down (you can configure this by setting +the `acceptMessagesWhileStopping` flag). Make sure to use transacted sessions in case of +reliability needs (for example, for reliable queue handling and durable topic subscriptions). + + +[[jms-tx]] +==== Transaction Management + +Spring provides a `JmsTransactionManager` that manages transactions for a single JMS +`ConnectionFactory`. This lets JMS applications leverage the managed-transaction +features of Spring, as described in +<>. +The `JmsTransactionManager` performs local resource transactions, binding a JMS +Connection/Session pair from the specified `ConnectionFactory` to the thread. +`JmsTemplate` automatically detects such transactional resources and operates +on them accordingly. + +In a Jakarta EE environment, the `ConnectionFactory` pools Connection and Session instances, +so those resources are efficiently reused across transactions. In a standalone environment, +using Spring's `SingleConnectionFactory` result in a shared JMS `Connection`, with +each transaction having its own independent `Session`. Alternatively, consider the use +of a provider-specific pooling adapter, such as ActiveMQ's `PooledConnectionFactory` +class. + +You can also use `JmsTemplate` with the `JtaTransactionManager` and an XA-capable JMS +`ConnectionFactory` to perform distributed transactions. Note that this requires the +use of a JTA transaction manager as well as a properly XA-configured ConnectionFactory. +(Check your Jakarta EE server's or JMS provider's documentation.) + +Reusing code across a managed and unmanaged transactional environment can be confusing +when using the JMS API to create a `Session` from a `Connection`. This is because the +JMS API has only one factory method to create a `Session`, and it requires values for the +transaction and acknowledgment modes. In a managed environment, setting these values is +the responsibility of the environment's transactional infrastructure, so these values +are ignored by the vendor's wrapper to the JMS Connection. When you use the `JmsTemplate` +in an unmanaged environment, you can specify these values through the use of the +properties `sessionTransacted` and `sessionAcknowledgeMode`. When you use a +`PlatformTransactionManager` with `JmsTemplate`, the template is always given a +transactional JMS `Session`. + + + +[[jms-sending]] +=== Sending a Message + +The `JmsTemplate` contains many convenience methods to send a message. Send +methods specify the destination by using a `jakarta.jms.Destination` object, and others +specify the destination by using a `String` in a JNDI lookup. The `send` method +that takes no destination argument uses the default destination. + +The following example uses the `MessageCreator` callback to create a text message from the +supplied `Session` object: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + import jakarta.jms.ConnectionFactory; + import jakarta.jms.JMSException; + import jakarta.jms.Message; + import jakarta.jms.Queue; + import jakarta.jms.Session; + + import org.springframework.jms.core.MessageCreator; + import org.springframework.jms.core.JmsTemplate; + + public class JmsQueueSender { + + private JmsTemplate jmsTemplate; + private Queue queue; + + public void setConnectionFactory(ConnectionFactory cf) { + this.jmsTemplate = new JmsTemplate(cf); + } + + public void setQueue(Queue queue) { + this.queue = queue; + } + + public void simpleSend() { + this.jmsTemplate.send(this.queue, new MessageCreator() { + public Message createMessage(Session session) throws JMSException { + return session.createTextMessage("hello queue world"); + } + }); + } + } +---- + +In the preceding example, the `JmsTemplate` is constructed by passing a reference to a +`ConnectionFactory`. As an alternative, a zero-argument constructor and +`connectionFactory` is provided and can be used for constructing the instance in +JavaBean style (using a `BeanFactory` or plain Java code). Alternatively, consider +deriving from Spring's `JmsGatewaySupport` convenience base class, which provides +pre-built bean properties for JMS configuration. + +The `send(String destinationName, MessageCreator creator)` method lets you send a +message by using the string name of the destination. If these names are registered in JNDI, +you should set the `destinationResolver` property of the template to an instance of +`JndiDestinationResolver`. + +If you created the `JmsTemplate` and specified a default destination, the +`send(MessageCreator c)` sends a message to that destination. + + +[[jms-msg-conversion]] +==== Using Message Converters + +To facilitate the sending of domain model objects, the `JmsTemplate` has +various send methods that take a Java object as an argument for a message's data +content. The overloaded methods `convertAndSend()` and `receiveAndConvert()` methods in +`JmsTemplate` delegate the conversion process to an instance of the `MessageConverter` +interface. This interface defines a simple contract to convert between Java objects and +JMS messages. The default implementation (`SimpleMessageConverter`) supports conversion +between `String` and `TextMessage`, `byte[]` and `BytesMessage`, and `java.util.Map` +and `MapMessage`. By using the converter, you and your application code can focus on the +business object that is being sent or received through JMS and not be concerned with the +details of how it is represented as a JMS message. + +The sandbox currently includes a `MapMessageConverter`, which uses reflection to convert +between a JavaBean and a `MapMessage`. Other popular implementation choices you might +implement yourself are converters that use an existing XML marshalling package (such as +JAXB or XStream) to create a `TextMessage` that represents the object. + +To accommodate the setting of a message's properties, headers, and body that can not be +generically encapsulated inside a converter class, the `MessagePostProcessor` interface +gives you access to the message after it has been converted but before it is sent. The +following example shows how to modify a message header and a property after a +`java.util.Map` is converted to a message: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + public void sendWithConversion() { + Map map = new HashMap(); + map.put("Name", "Mark"); + map.put("Age", new Integer(47)); + jmsTemplate.convertAndSend("testQueue", map, new MessagePostProcessor() { + public Message postProcessMessage(Message message) throws JMSException { + message.setIntProperty("AccountID", 1234); + message.setJMSCorrelationID("123-00001"); + return message; + } + }); + } +---- + +This results in a message of the following form: + +[literal] +[subs="verbatim,quotes"] +---- +MapMessage={ + Header={ + ... standard headers ... + CorrelationID={123-00001} + } + Properties={ + AccountID={Integer:1234} + } + Fields={ + Name={String:Mark} + Age={Integer:47} + } +} +---- + + +[[jms-callbacks]] +==== Using `SessionCallback` and `ProducerCallback` + +While the send operations cover many common usage scenarios, you might sometimes +want to perform multiple operations on a JMS `Session` or `MessageProducer`. The +`SessionCallback` and `ProducerCallback` expose the JMS `Session` and `Session` / +`MessageProducer` pair, respectively. The `execute()` methods on `JmsTemplate` run +these callback methods. + + + +[[jms-receiving]] +=== Receiving a Message + +This describes how to receive messages with JMS in Spring. + + +[[jms-receiving-sync]] +==== Synchronous Reception + +While JMS is typically associated with asynchronous processing, you can +consume messages synchronously. The overloaded `receive(..)` methods provide this +functionality. During a synchronous receive, the calling thread blocks until a message +becomes available. This can be a dangerous operation, since the calling thread can +potentially be blocked indefinitely. The `receiveTimeout` property specifies how long +the receiver should wait before giving up waiting for a message. + + +[[jms-receiving-async]] +==== Asynchronous reception: Message-Driven POJOs + +NOTE: Spring also supports annotated-listener endpoints through the use of the `@JmsListener` +annotation and provides an open infrastructure to register endpoints programmatically. +This is, by far, the most convenient way to setup an asynchronous receiver. +See <> for more details. + +In a fashion similar to a Message-Driven Bean (MDB) in the EJB world, the Message-Driven +POJO (MDP) acts as a receiver for JMS messages. The one restriction (but see +<>) on an MDP is that it must implement +the `jakarta.jms.MessageListener` interface. Note that, if your POJO receives messages +on multiple threads, it is important to ensure that your implementation is thread-safe. + +The following example shows a simple implementation of an MDP: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + import jakarta.jms.JMSException; + import jakarta.jms.Message; + import jakarta.jms.MessageListener; + import jakarta.jms.TextMessage; + + public class ExampleListener implements MessageListener { + + public void onMessage(Message message) { + if (message instanceof TextMessage textMessage) { + try { + System.out.println(textMessage.getText()); + } + catch (JMSException ex) { + throw new RuntimeException(ex); + } + } + else { + throw new IllegalArgumentException("Message must be of type TextMessage"); + } + } + } +---- + +Once you have implemented your `MessageListener`, it is time to create a message listener +container. + +The following example shows how to define and configure one of the message listener +containers that ships with Spring (in this case, `DefaultMessageListenerContainer`): + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + + + + + + +---- + +See the Spring javadoc of the various message listener containers (all of which implement +{api-spring-framework}/jms/listener/MessageListenerContainer.html[MessageListenerContainer]) +for a full description of the features supported by each implementation. + + +[[jms-receiving-async-session-aware-message-listener]] +==== Using the `SessionAwareMessageListener` Interface + +The `SessionAwareMessageListener` interface is a Spring-specific interface that provides +a similar contract to the JMS `MessageListener` interface but also gives the message-handling +method access to the JMS `Session` from which the `Message` was received. +The following listing shows the definition of the `SessionAwareMessageListener` interface: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + package org.springframework.jms.listener; + + public interface SessionAwareMessageListener { + + void onMessage(Message message, Session session) throws JMSException; + } +---- + +You can choose to have your MDPs implement this interface (in preference to the standard +JMS `MessageListener` interface) if you want your MDPs to be able to respond to any +received messages (by using the `Session` supplied in the `onMessage(Message, Session)` +method). All of the message listener container implementations that ship with Spring +have support for MDPs that implement either the `MessageListener` or +`SessionAwareMessageListener` interface. Classes that implement the +`SessionAwareMessageListener` come with the caveat that they are then tied to Spring +through the interface. The choice of whether or not to use it is left entirely up to you +as an application developer or architect. + +Note that the `onMessage(..)` method of the `SessionAwareMessageListener` +interface throws `JMSException`. In contrast to the standard JMS `MessageListener` +interface, when using the `SessionAwareMessageListener` interface, it is the +responsibility of the client code to handle any thrown exceptions. + + +[[jms-receiving-async-message-listener-adapter]] +==== Using `MessageListenerAdapter` + +The `MessageListenerAdapter` class is the final component in Spring's asynchronous +messaging support. In a nutshell, it lets you expose almost any class as an MDP +(though there are some constraints). + +Consider the following interface definition: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + public interface MessageDelegate { + + void handleMessage(String message); + + void handleMessage(Map message); + + void handleMessage(byte[] message); + + void handleMessage(Serializable message); + } +---- + +Notice that, although the interface extends neither the `MessageListener` nor the +`SessionAwareMessageListener` interface, you can still use it as an MDP by using the +`MessageListenerAdapter` class. Notice also how the various message handling methods are +strongly typed according to the contents of the various `Message` types that they can +receive and handle. + +Now consider the following implementation of the `MessageDelegate` interface: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + public class DefaultMessageDelegate implements MessageDelegate { + // implementation elided for clarity... + } +---- + +In particular, note how the preceding implementation of the `MessageDelegate` interface (the +`DefaultMessageDelegate` class) has no JMS dependencies at all. It truly is a +POJO that we can make into an MDP through the following configuration: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + + + + + + + + + + +---- + +The next example shows another MDP that can handle only receiving JMS +`TextMessage` messages. Notice how the message handling method is actually called +`receive` (the name of the message handling method in a `MessageListenerAdapter` +defaults to `handleMessage`), but it is configurable (as you can see later in this section). Notice +also how the `receive(..)` method is strongly typed to receive and respond only to JMS +`TextMessage` messages. +The following listing shows the definition of the `TextMessageDelegate` interface: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + public interface TextMessageDelegate { + + void receive(TextMessage message); + } +---- + +The following listing shows a class that implements the `TextMessageDelegate` interface: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + public class DefaultTextMessageDelegate implements TextMessageDelegate { + // implementation elided for clarity... + } +---- + +The configuration of the attendant `MessageListenerAdapter` would then be as follows: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + + + + + + + +---- + +Note that, if the `messageListener` receives a JMS `Message` of a type +other than `TextMessage`, an `IllegalStateException` is thrown (and subsequently +swallowed). Another of the capabilities of the `MessageListenerAdapter` class is the +ability to automatically send back a response `Message` if a handler method returns a +non-void value. Consider the following interface and class: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + public interface ResponsiveTextMessageDelegate { + + // notice the return type... + String receive(TextMessage message); + } +---- + +[source,java,indent=0,subs="verbatim,quotes"] +---- + public class DefaultResponsiveTextMessageDelegate implements ResponsiveTextMessageDelegate { + // implementation elided for clarity... + } +---- + +If you use the `DefaultResponsiveTextMessageDelegate` in conjunction with a +`MessageListenerAdapter`, any non-null value that is returned from the execution of +the `'receive(..)'` method is (in the default configuration) converted into a +`TextMessage`. The resulting `TextMessage` is then sent to the `Destination` (if +one exists) defined in the JMS `Reply-To` property of the original `Message` or the +default `Destination` set on the `MessageListenerAdapter` (if one has been configured). +If no `Destination` is found, an `InvalidDestinationException` is thrown +(note that this exception is not swallowed and propagates up the +call stack). + + +[[jms-tx-participation]] +==== Processing Messages Within Transactions + +Invoking a message listener within a transaction requires only reconfiguration of the +listener container. + +You can activate local resource transactions through the `sessionTransacted` flag +on the listener container definition. Each message listener invocation then operates +within an active JMS transaction, with message reception rolled back in case of listener +execution failure. Sending a response message (through `SessionAwareMessageListener`) is +part of the same local transaction, but any other resource operations (such as +database access) operate independently. This usually requires duplicate message +detection in the listener implementation, to cover the case where database processing +has committed but message processing failed to commit. + +Consider the following bean definition: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + + + +---- + +To participate in an externally managed transaction, you need to configure a +transaction manager and use a listener container that supports externally managed +transactions (typically, `DefaultMessageListenerContainer`). + +To configure a message listener container for XA transaction participation, you want +to configure a `JtaTransactionManager` (which, by default, delegates to the Jakarta EE +server's transaction subsystem). Note that the underlying JMS `ConnectionFactory` needs to +be XA-capable and properly registered with your JTA transaction coordinator. (Check your +Jakarta EE server's configuration of JNDI resources.) This lets message reception as well +as (for example) database access be part of the same transaction (with unified commit +semantics, at the expense of XA transaction log overhead). + +The following bean definition creates a transaction manager: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + +---- + +Then we need to add it to our earlier container configuration. The container +takes care of the rest. The following example shows how to do so: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + + <1> + +---- +<1> Our transaction manager. + + + +[[jms-jca-message-endpoint-manager]] +=== Support for JCA Message Endpoints + +Beginning with version 2.5, Spring also provides support for a JCA-based +`MessageListener` container. The `JmsMessageEndpointManager` tries to +automatically determine the `ActivationSpec` class name from the provider's +`ResourceAdapter` class name. Therefore, it is typically possible to provide +Spring's generic `JmsActivationSpecConfig`, as the following example shows: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + + + + + + +---- + +Alternatively, you can set up a `JmsMessageEndpointManager` with a given +`ActivationSpec` object. The `ActivationSpec` object may also come from a JNDI lookup +(using ``). The following example shows how to do so: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + + + + + + + +---- + +Using Spring's `ResourceAdapterFactoryBean`, you can configure the target `ResourceAdapter` +locally, as the following example shows: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + + + + + + + +---- + +The specified `WorkManager` can also point to an environment-specific thread pool -- +typically through a `SimpleTaskWorkManager` instance's `asyncTaskExecutor` property. Consider +defining a shared thread pool for all your `ResourceAdapter` instances if you happen to +use multiple adapters. + +In some environments (such as WebLogic 9 or above), you can instead obtain the entire `ResourceAdapter` object +from JNDI (by using ``). The Spring-based message +listeners can then interact with the server-hosted `ResourceAdapter`, which also use the +server's built-in `WorkManager`. + +See the javadoc for {api-spring-framework}/jms/listener/endpoint/JmsMessageEndpointManager.html[`JmsMessageEndpointManager`], +{api-spring-framework}/jms/listener/endpoint/JmsActivationSpecConfig.html[`JmsActivationSpecConfig`], +and {api-spring-framework}/jca/support/ResourceAdapterFactoryBean.html[`ResourceAdapterFactoryBean`] +for more details. + +Spring also provides a generic JCA message endpoint manager that is not tied to JMS: +`org.springframework.jca.endpoint.GenericMessageEndpointManager`. This component allows +for using any message listener type (such as a JMS `MessageListener`) and any +provider-specific `ActivationSpec` object. See your JCA provider's documentation to +find out about the actual capabilities of your connector, and see the +{api-spring-framework}/jca/endpoint/GenericMessageEndpointManager.html[`GenericMessageEndpointManager`] +javadoc for the Spring-specific configuration details. + +NOTE: JCA-based message endpoint management is very analogous to EJB 2.1 Message-Driven Beans. +It uses the same underlying resource provider contract. As with EJB 2.1 MDBs, you can use any +message listener interface supported by your JCA provider in the Spring context as well. +Spring nevertheless provides explicit "`convenience`" support for JMS, because JMS is the +most common endpoint API used with the JCA endpoint management contract. + + + +[[jms-annotated]] +=== Annotation-driven Listener Endpoints + +The easiest way to receive a message asynchronously is to use the annotated listener +endpoint infrastructure. In a nutshell, it lets you expose a method of a managed +bean as a JMS listener endpoint. The following example shows how to use it: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Component + public class MyService { + + @JmsListener(destination = "myDestination") + public void processOrder(String data) { ... } + } +---- + +The idea of the preceding example is that, whenever a message is available on the +`jakarta.jms.Destination` `myDestination`, the `processOrder` method is invoked +accordingly (in this case, with the content of the JMS message, similar to +what the <> +provides). + +The annotated endpoint infrastructure creates a message listener container +behind the scenes for each annotated method, by using a `JmsListenerContainerFactory`. +Such a container is not registered against the application context but can be easily +located for management purposes by using the `JmsListenerEndpointRegistry` bean. + +TIP: `@JmsListener` is a repeatable annotation on Java 8, so you can associate +several JMS destinations with the same method by adding additional `@JmsListener` +declarations to it. + + +[[jms-annotated-support]] +==== Enable Listener Endpoint Annotations + +To enable support for `@JmsListener` annotations, you can add `@EnableJms` to one of +your `@Configuration` classes, as the following example shows: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Configuration + @EnableJms + public class AppConfig { + + @Bean + public DefaultJmsListenerContainerFactory jmsListenerContainerFactory() { + DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory(); + factory.setConnectionFactory(connectionFactory()); + factory.setDestinationResolver(destinationResolver()); + factory.setSessionTransacted(true); + factory.setConcurrency("3-10"); + return factory; + } + } +---- + +By default, the infrastructure looks for a bean named `jmsListenerContainerFactory` +as the source for the factory to use to create message listener containers. In this +case (and ignoring the JMS infrastructure setup), you can invoke the `processOrder` +method with a core poll size of three threads and a maximum pool size of ten threads. + +You can customize the listener container factory to use for each annotation or you can +configure an explicit default by implementing the `JmsListenerConfigurer` interface. +The default is required only if at least one endpoint is registered without a specific +container factory. See the javadoc of classes that implement +{api-spring-framework}/jms/annotation/JmsListenerConfigurer.html[`JmsListenerConfigurer`] +for details and examples. + +If you prefer <>, you can use the `` +element, as the following example shows: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + + + + + +---- + + +[[jms-annotated-programmatic-registration]] +==== Programmatic Endpoint Registration + +`JmsListenerEndpoint` provides a model of a JMS endpoint and is responsible for configuring +the container for that model. The infrastructure lets you programmatically configure endpoints +in addition to the ones that are detected by the `JmsListener` annotation. +The following example shows how to do so: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Configuration + @EnableJms + public class AppConfig implements JmsListenerConfigurer { + + @Override + public void configureJmsListeners(JmsListenerEndpointRegistrar registrar) { + SimpleJmsListenerEndpoint endpoint = new SimpleJmsListenerEndpoint(); + endpoint.setId("myJmsEndpoint"); + endpoint.setDestination("anotherQueue"); + endpoint.setMessageListener(message -> { + // processing + }); + registrar.registerEndpoint(endpoint); + } + } +---- + +In the preceding example, we used `SimpleJmsListenerEndpoint`, which provides the actual +`MessageListener` to invoke. However, you could also build your own endpoint variant +to describe a custom invocation mechanism. + +Note that you could skip the use of `@JmsListener` altogether +and programmatically register only your endpoints through `JmsListenerConfigurer`. + + +[[jms-annotated-method-signature]] +==== Annotated Endpoint Method Signature + +So far, we have been injecting a simple `String` in our endpoint, but it can actually +have a very flexible method signature. In the following example, we rewrite it to inject the `Order` with +a custom header: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Component + public class MyService { + + @JmsListener(destination = "myDestination") + public void processOrder(Order order, @Header("order_type") String orderType) { + ... + } + } +---- + +The main elements you can inject in JMS listener endpoints are as follows: + +* The raw `jakarta.jms.Message` or any of its subclasses (provided that it + matches the incoming message type). +* The `jakarta.jms.Session` for optional access to the native JMS API (for example, for sending + a custom reply). +* The `org.springframework.messaging.Message` that represents the incoming JMS message. + Note that this message holds both the custom and the standard headers (as defined + by `JmsHeaders`). +* `@Header`-annotated method arguments to extract a specific header value, including + standard JMS headers. +* A `@Headers`-annotated argument that must also be assignable to `java.util.Map` for + getting access to all headers. +* A non-annotated element that is not one of the supported types (`Message` or + `Session`) is considered to be the payload. You can make that explicit by annotating + the parameter with `@Payload`. You can also turn on validation by adding an extra + `@Valid`. + +The ability to inject Spring's `Message` abstraction is particularly useful to benefit +from all the information stored in the transport-specific message without relying on +transport-specific API. The following example shows how to do so: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @JmsListener(destination = "myDestination") + public void processOrder(Message order) { ... } +---- + +Handling of method arguments is provided by `DefaultMessageHandlerMethodFactory`, which you can +further customize to support additional method arguments. You can customize the conversion and validation +support there as well. + +For instance, if we want to make sure our `Order` is valid before processing it, we can +annotate the payload with `@Valid` and configure the necessary validator, as the following example shows: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Configuration + @EnableJms + public class AppConfig implements JmsListenerConfigurer { + + @Override + public void configureJmsListeners(JmsListenerEndpointRegistrar registrar) { + registrar.setMessageHandlerMethodFactory(myJmsHandlerMethodFactory()); + } + + @Bean + public DefaultMessageHandlerMethodFactory myHandlerMethodFactory() { + DefaultMessageHandlerMethodFactory factory = new DefaultMessageHandlerMethodFactory(); + factory.setValidator(myValidator()); + return factory; + } + } +---- + + +[[jms-annotated-response]] +==== Response Management + +The existing support in <> +already lets your method have a non-`void` return type. When that is the case, the result of +the invocation is encapsulated in a `jakarta.jms.Message`, sent either in the destination specified +in the `JMSReplyTo` header of the original message or in the default destination configured on +the listener. You can now set that default destination by using the `@SendTo` annotation of the +messaging abstraction. + +Assuming that our `processOrder` method should now return an `OrderStatus`, we can write it +to automatically send a response, as the following example shows: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @JmsListener(destination = "myDestination") + @SendTo("status") + public OrderStatus processOrder(Order order) { + // order processing + return status; + } +---- + +TIP: If you have several `@JmsListener`-annotated methods, you can also place the `@SendTo` +annotation at the class level to share a default reply destination. + +If you need to set additional headers in a transport-independent manner, you can return a +`Message` instead, with a method similar to the following: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @JmsListener(destination = "myDestination") + @SendTo("status") + public Message processOrder(Order order) { + // order processing + return MessageBuilder + .withPayload(status) + .setHeader("code", 1234) + .build(); + } +---- + +If you need to compute the response destination at runtime, you can encapsulate your response +in a `JmsResponse` instance that also provides the destination to use at runtime. We can rewrite the previous +example as follows: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @JmsListener(destination = "myDestination") + public JmsResponse> processOrder(Order order) { + // order processing + Message response = MessageBuilder + .withPayload(status) + .setHeader("code", 1234) + .build(); + return JmsResponse.forQueue(response, "status"); + } +---- + +Finally, if you need to specify some QoS values for the response such as the priority or +the time to live, you can configure the `JmsListenerContainerFactory` accordingly, +as the following example shows: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Configuration + @EnableJms + public class AppConfig { + + @Bean + public DefaultJmsListenerContainerFactory jmsListenerContainerFactory() { + DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory(); + factory.setConnectionFactory(connectionFactory()); + QosSettings replyQosSettings = new QosSettings(); + replyQosSettings.setPriority(2); + replyQosSettings.setTimeToLive(10000); + factory.setReplyQosSettings(replyQosSettings); + return factory; + } + } +---- + + + +[[jms-namespace]] +=== JMS Namespace Support + +Spring provides an XML namespace for simplifying JMS configuration. To use the JMS +namespace elements, you need to reference the JMS schema, as the following example shows: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + xsi:schemaLocation=" + http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd + http://www.springframework.org/schema/jms https://www.springframework.org/schema/jms/spring-jms.xsd"> + + + + +---- +<1> Referencing the JMS schema. + + +The namespace consists of three top-level elements: ``, `` +and ``. `` enables the use of <>. `` and `` +define shared listener container configuration and can contain `` child elements. +The following example shows a basic configuration for two listeners: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + + + + +---- + +The preceding example is equivalent to creating two distinct listener container bean +definitions and two distinct `MessageListenerAdapter` bean definitions, as shown +in <>. In addition to the attributes shown +in the preceding example, the `listener` element can contain several optional ones. +The following table describes all of the available attributes: + +[[jms-namespace-listener-tbl]] +.Attributes of the JMS element +[cols="1,6"] +|=== +| Attribute | Description + +| `id` +| A bean name for the hosting listener container. If not specified, a bean name is + automatically generated. + +| `destination` (required) +| The destination name for this listener, resolved through the `DestinationResolver` + strategy. + +| `ref` (required) +| The bean name of the handler object. + +| `method` +| The name of the handler method to invoke. If the `ref` attribute points to a `MessageListener` + or Spring `SessionAwareMessageListener`, you can omit this attribute. + +| `response-destination` +| The name of the default response destination to which to send response messages. This is + applied in case of a request message that does not carry a `JMSReplyTo` field. The + type of this destination is determined by the listener-container's + `response-destination-type` attribute. Note that this applies only to a listener method with a + return value, for which each result object is converted into a response message. + +| `subscription` +| The name of the durable subscription, if any. + +| `selector` +| An optional message selector for this listener. + +| `concurrency` +| The number of concurrent sessions or consumers to start for this listener. This value can either be + a simple number indicating the maximum number (for example, `5`) or a range indicating the lower + as well as the upper limit (for example, `3-5`). Note that a specified minimum is only a hint + and might be ignored at runtime. The default is the value provided by the container. +|=== + +The `` element also accepts several optional attributes. This +allows for customization of the various strategies (for example, `taskExecutor` and +`destinationResolver`) as well as basic JMS settings and resource references. By using +these attributes, you can define highly-customized listener containers while +still benefiting from the convenience of the namespace. + +You can automatically expose such settings as a `JmsListenerContainerFactory` by +specifying the `id` of the bean to expose through the `factory-id` attribute, +as the following example shows: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + + + + +---- + +The following table describes all available attributes. See the class-level javadoc +of the {api-spring-framework}/jms/listener/AbstractMessageListenerContainer.html[`AbstractMessageListenerContainer`] +and its concrete subclasses for more details on the individual properties. The javadoc +also provides a discussion of transaction choices and message redelivery scenarios. + +[[jms-namespace-listener-container-tbl]] +.Attributes of the JMS element +[cols="1,6"] +|=== +| Attribute | Description + +| `container-type` +| The type of this listener container. The available options are `default`, `simple`, + `default102`, or `simple102` (the default option is `default`). + +| `container-class` +| A custom listener container implementation class as a fully qualified class name. + The default is Spring's standard `DefaultMessageListenerContainer` or + `SimpleMessageListenerContainer`, according to the `container-type` attribute. + +| `factory-id` +| Exposes the settings defined by this element as a `JmsListenerContainerFactory` + with the specified `id` so that they can be reused with other endpoints. + +| `connection-factory` +| A reference to the JMS `ConnectionFactory` bean (the default bean name is + `connectionFactory`). + +| `task-executor` +| A reference to the Spring `TaskExecutor` for the JMS listener invokers. + +| `destination-resolver` +| A reference to the `DestinationResolver` strategy for resolving JMS `Destination` instances. + +| `message-converter` +| A reference to the `MessageConverter` strategy for converting JMS Messages to listener + method arguments. The default is a `SimpleMessageConverter`. + +| `error-handler` +| A reference to an `ErrorHandler` strategy for handling any uncaught exceptions that + may occur during the execution of the `MessageListener`. + +| `destination-type` +| The JMS destination type for this listener: `queue`, `topic`, `durableTopic`, `sharedTopic`, + or `sharedDurableTopic`. This potentially enables the `pubSubDomain`, `subscriptionDurable` + and `subscriptionShared` properties of the container. The default is `queue` (which disables + those three properties). + +| `response-destination-type` +| The JMS destination type for responses: `queue` or `topic`. The default is the value of the + `destination-type` attribute. + +| `client-id` +| The JMS client ID for this listener container. You must specify it when you use + durable subscriptions. + +| `cache` +| The cache level for JMS resources: `none`, `connection`, `session`, `consumer`, or + `auto`. By default (`auto`), the cache level is effectively `consumer`, unless + an external transaction manager has been specified -- in which case, the effective + default will be `none` (assuming Jakarta EE-style transaction management, where the given + ConnectionFactory is an XA-aware pool). + +| `acknowledge` +| The native JMS acknowledge mode: `auto`, `client`, `dups-ok`, or `transacted`. A value + of `transacted` activates a locally transacted `Session`. As an alternative, you can specify + the `transaction-manager` attribute, described later in table. The default is `auto`. + +| `transaction-manager` +| A reference to an external `PlatformTransactionManager` (typically an XA-based + transaction coordinator, such as Spring's `JtaTransactionManager`). If not specified, + native acknowledging is used (see the `acknowledge` attribute). + +| `concurrency` +| The number of concurrent sessions or consumers to start for each listener. It can either be + a simple number indicating the maximum number (for example, `5`) or a range indicating the + lower as well as the upper limit (for example, `3-5`). Note that a specified minimum is just a + hint and might be ignored at runtime. The default is `1`. You should keep concurrency limited to `1` in + case of a topic listener or if queue ordering is important. Consider raising it for + general queues. + +| `prefetch` +| The maximum number of messages to load into a single session. Note that raising this + number might lead to starvation of concurrent consumers. + +| `receive-timeout` +| The timeout (in milliseconds) to use for receive calls. The default is `1000` (one + second). `-1` indicates no timeout. + +| `back-off` +| Specifies the `BackOff` instance to use to compute the interval between recovery + attempts. If the `BackOffExecution` implementation returns `BackOffExecution#STOP`, + the listener container does not further try to recover. The `recovery-interval` + value is ignored when this property is set. The default is a `FixedBackOff` with + an interval of 5000 milliseconds (that is, five seconds). + +| `recovery-interval` +| Specifies the interval between recovery attempts, in milliseconds. It offers a convenient + way to create a `FixedBackOff` with the specified interval. For more recovery + options, consider specifying a `BackOff` instance instead. The default is 5000 milliseconds + (that is, five seconds). + +| `phase` +| The lifecycle phase within which this container should start and stop. The lower the + value, the earlier this container starts and the later it stops. The default is + `Integer.MAX_VALUE`, meaning that the container starts as late as possible and stops as + soon as possible. +|=== + +Configuring a JCA-based listener container with the `jms` schema support is very similar, +as the following example shows: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + + +---- + +The following table describes the available configuration options for the JCA variant: + +[[jms-namespace-jca-listener-container-tbl]] +.Attributes of the JMS element +[cols="1,6"] +|=== +| Attribute | Description + +| `factory-id` +| Exposes the settings defined by this element as a `JmsListenerContainerFactory` + with the specified `id` so that they can be reused with other endpoints. + +| `resource-adapter` +| A reference to the JCA `ResourceAdapter` bean (the default bean name is + `resourceAdapter`). + +| `activation-spec-factory` +| A reference to the `JmsActivationSpecFactory`. The default is to autodetect the JMS + provider and its `ActivationSpec` class (see {api-spring-framework}/jms/listener/endpoint/DefaultJmsActivationSpecFactory.html[`DefaultJmsActivationSpecFactory`]). + +| `destination-resolver` +| A reference to the `DestinationResolver` strategy for resolving JMS `Destinations`. + +| `message-converter` +| A reference to the `MessageConverter` strategy for converting JMS Messages to listener + method arguments. The default is `SimpleMessageConverter`. + +| `destination-type` +| The JMS destination type for this listener: `queue`, `topic`, `durableTopic`, `sharedTopic`. + or `sharedDurableTopic`. This potentially enables the `pubSubDomain`, `subscriptionDurable`, + and `subscriptionShared` properties of the container. The default is `queue` (which disables + those three properties). + +| `response-destination-type` +| The JMS destination type for responses: `queue` or `topic`. The default is the value of the + `destination-type` attribute. + +| `client-id` +| The JMS client ID for this listener container. It needs to be specified when using + durable subscriptions. + +| `acknowledge` +| The native JMS acknowledge mode: `auto`, `client`, `dups-ok`, or `transacted`. A value + of `transacted` activates a locally transacted `Session`. As an alternative, you can specify + the `transaction-manager` attribute described later. The default is `auto`. + +| `transaction-manager` +| A reference to a Spring `JtaTransactionManager` or a + `jakarta.transaction.TransactionManager` for kicking off an XA transaction for each + incoming message. If not specified, native acknowledging is used (see the + `acknowledge` attribute). + +| `concurrency` +| The number of concurrent sessions or consumers to start for each listener. It can either be + a simple number indicating the maximum number (for example `5`) or a range indicating the + lower as well as the upper limit (for example, `3-5`). Note that a specified minimum is only a + hint and is typically ignored at runtime when you use a JCA listener container. + The default is 1. + +| `prefetch` +| The maximum number of messages to load into a single session. Note that raising this + number might lead to starvation of concurrent consumers. +|=== + + + + +[[jmx]] +== JMX + +The JMX (Java Management Extensions) support in Spring provides features that let you +easily and transparently integrate your Spring application into a JMX infrastructure. + +.JMX? +**** +This chapter is not an introduction to JMX. It does not try to explain why you might want +to use JMX. If you are new to JMX, see <> at the end of this chapter. +**** + +Specifically, Spring's JMX support provides four core features: + +* The automatic registration of any Spring bean as a JMX MBean. +* A flexible mechanism for controlling the management interface of your beans. +* The declarative exposure of MBeans over remote, JSR-160 connectors. +* The simple proxying of both local and remote MBean resources. + +These features are designed to work without coupling your application components to +either Spring or JMX interfaces and classes. Indeed, for the most part, your application +classes need not be aware of either Spring or JMX in order to take advantage of the +Spring JMX features. + + + +[[jmx-exporting]] +=== Exporting Your Beans to JMX + +The core class in Spring's JMX framework is the `MBeanExporter`. This class is +responsible for taking your Spring beans and registering them with a JMX `MBeanServer`. +For example, consider the following class: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + package org.springframework.jmx; + + public class JmxTestBean implements IJmxTestBean { + + private String name; + private int age; + private boolean isSuperman; + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public int add(int x, int y) { + return x + y; + } + + public void dontExposeMe() { + throw new RuntimeException(); + } + } +---- + +To expose the properties and methods of this bean as attributes and operations of an +MBean, you can configure an instance of the `MBeanExporter` class in your +configuration file and pass in the bean, as the following example shows: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + + + + + + + + + + + +---- + +The pertinent bean definition from the preceding configuration snippet is the `exporter` +bean. The `beans` property tells the `MBeanExporter` exactly which of your beans must be +exported to the JMX `MBeanServer`. In the default configuration, the key of each entry +in the `beans` `Map` is used as the `ObjectName` for the bean referenced by the +corresponding entry value. You can change this behavior, as described in <>. + +With this configuration, the `testBean` bean is exposed as an MBean under the +`ObjectName` `bean:name=testBean1`. By default, all `public` properties of the bean +are exposed as attributes and all `public` methods (except those inherited from the +`Object` class) are exposed as operations. + +NOTE: `MBeanExporter` is a `Lifecycle` bean (see <>). By default, MBeans are exported as late as possible during +the application lifecycle. You can configure the `phase` at which +the export happens or disable automatic registration by setting the `autoStartup` flag. + + +[[jmx-exporting-mbeanserver]] +==== Creating an MBeanServer + +The configuration shown in the <> assumes that the +application is running in an environment that has one (and only one) `MBeanServer` +already running. In this case, Spring tries to locate the running `MBeanServer` and +register your beans with that server (if any). This behavior is useful when your +application runs inside a container (such as Tomcat or IBM WebSphere) that has its +own `MBeanServer`. + +However, this approach is of no use in a standalone environment or when running inside +a container that does not provide an `MBeanServer`. To address this, you can create an +`MBeanServer` instance declaratively by adding an instance of the +`org.springframework.jmx.support.MBeanServerFactoryBean` class to your configuration. +You can also ensure that a specific `MBeanServer` is used by setting the value of the +`MBeanExporter` instance's `server` property to the `MBeanServer` value returned by an +`MBeanServerFactoryBean`, as the following example shows: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + + + + + + + + + + + + + + + + + +---- + +In the preceding example, an instance of `MBeanServer` is created by the `MBeanServerFactoryBean` and is +supplied to the `MBeanExporter` through the `server` property. When you supply your own +`MBeanServer` instance, the `MBeanExporter` does not try to locate a running +`MBeanServer` and uses the supplied `MBeanServer` instance. For this to work +correctly, you must have a JMX implementation on your classpath. + + +[[jmx-mbean-server]] +==== Reusing an Existing `MBeanServer` + +If no server is specified, the `MBeanExporter` tries to automatically detect a running +`MBeanServer`. This works in most environments, where only one `MBeanServer` instance is +used. However, when multiple instances exist, the exporter might pick the wrong server. +In such cases, you should use the `MBeanServer` `agentId` to indicate which instance to +be used, as the following example shows: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + + + + + + + ... + + +---- + +For platforms or cases where the existing `MBeanServer` has a dynamic (or unknown) +`agentId` that is retrieved through lookup methods, you should use +<>, +as the following example shows: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + + + + + + + + +---- + + +[[jmx-exporting-lazy]] +==== Lazily Initialized MBeans + +If you configure a bean with an `MBeanExporter` that is also configured for lazy +initialization, the `MBeanExporter` does not break this contract and avoids +instantiating the bean. Instead, it registers a proxy with the `MBeanServer` and +defers obtaining the bean from the container until the first invocation on the proxy +occurs. + + +[[jmx-exporting-auto]] +==== Automatic Registration of MBeans + +Any beans that are exported through the `MBeanExporter` and are already valid MBeans are +registered as-is with the `MBeanServer` without further intervention from Spring. You can cause MBeans +to be automatically detected by the `MBeanExporter` by setting the `autodetect` +property to `true`, as the following example shows: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + + +---- + +In the preceding example, the bean called `spring:mbean=true` is already a valid JMX MBean +and is automatically registered by Spring. By default, a bean that is autodetected for JMX +registration has its bean name used as the `ObjectName`. You can override this behavior, +as detailed in <>. + + +[[jmx-exporting-registration-behavior]] +==== Controlling the Registration Behavior + +Consider the scenario where a Spring `MBeanExporter` attempts to register an `MBean` +with an `MBeanServer` by using the `ObjectName` `bean:name=testBean1`. If an `MBean` +instance has already been registered under that same `ObjectName`, the default behavior +is to fail (and throw an `InstanceAlreadyExistsException`). + +You can control exactly what happens when an `MBean` is +registered with an `MBeanServer`. Spring's JMX support allows for three different +registration behaviors to control the registration behavior when the registration +process finds that an `MBean` has already been registered under the same `ObjectName`. +The following table summarizes these registration behaviors: + +[[jmx-registration-behaviors]] +.Registration Behaviors +[cols="1,4"] +|=== +| Registration behavior | Explanation + +| `FAIL_ON_EXISTING` +| This is the default registration behavior. If an `MBean` instance has already been + registered under the same `ObjectName`, the `MBean` that is being registered is not + registered, and an `InstanceAlreadyExistsException` is thrown. The existing + `MBean` is unaffected. + +| `IGNORE_EXISTING` +| If an `MBean` instance has already been registered under the same `ObjectName`, the + `MBean` that is being registered is not registered. The existing `MBean` is + unaffected, and no `Exception` is thrown. This is useful in settings where + multiple applications want to share a common `MBean` in a shared `MBeanServer`. + +| `REPLACE_EXISTING` +| If an `MBean` instance has already been registered under the same `ObjectName`, the + existing `MBean` that was previously registered is unregistered, and the new + `MBean` is registered in its place (the new `MBean` effectively replaces the + previous instance). +|=== + +The values in the preceding table are defined as enums on the `RegistrationPolicy` class. +If you want to change the default registration behavior, you need to set the value of the +`registrationPolicy` property on your `MBeanExporter` definition to one of those +values. + +The following example shows how to change from the default registration +behavior to the `REPLACE_EXISTING` behavior: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + + + + + + + + + + + + + + +---- + + + +[[jmx-interface]] +=== Controlling the Management Interface of Your Beans + +In the example in the <>, +you had little control over the management interface of your bean. All of the `public` +properties and methods of each exported bean were exposed as JMX attributes and +operations, respectively. To exercise finer-grained control over exactly which +properties and methods of your exported beans are actually exposed as JMX attributes +and operations, Spring JMX provides a comprehensive and extensible mechanism for +controlling the management interfaces of your beans. + + +[[jmx-interface-assembler]] +==== Using the `MBeanInfoAssembler` Interface + +Behind the scenes, the `MBeanExporter` delegates to an implementation of the +`org.springframework.jmx.export.assembler.MBeanInfoAssembler` interface, which is +responsible for defining the management interface of each bean that is exposed. +The default implementation, +`org.springframework.jmx.export.assembler.SimpleReflectiveMBeanInfoAssembler`, +defines a management interface that exposes all public properties and methods +(as you saw in the examples in the preceding sections). Spring provides two +additional implementations of the `MBeanInfoAssembler` interface that let you +control the generated management interface by using either source-level metadata +or any arbitrary interface. + + +[[jmx-interface-metadata]] +==== Using Source-level Metadata: Java Annotations + +By using the `MetadataMBeanInfoAssembler`, you can define the management interfaces +for your beans by using source-level metadata. The reading of metadata is encapsulated +by the `org.springframework.jmx.export.metadata.JmxAttributeSource` interface. +Spring JMX provides a default implementation that uses Java annotations, namely +`org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource`. +You must configure the `MetadataMBeanInfoAssembler` with an implementation instance of +the `JmxAttributeSource` interface for it to function correctly (there is no default). + +To mark a bean for export to JMX, you should annotate the bean class with the +`ManagedResource` annotation. You must mark each method you wish to expose as an operation +with the `ManagedOperation` annotation and mark each property you wish to expose +with the `ManagedAttribute` annotation. When marking properties, you can omit +either the annotation of the getter or the setter to create a write-only or read-only +attribute, respectively. + +NOTE: A `ManagedResource`-annotated bean must be public, as must the methods exposing +an operation or an attribute. + +The following example shows the annotated version of the `JmxTestBean` class that we +used in <>: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + package org.springframework.jmx; + + import org.springframework.jmx.export.annotation.ManagedResource; + import org.springframework.jmx.export.annotation.ManagedOperation; + import org.springframework.jmx.export.annotation.ManagedAttribute; + + @ManagedResource( + objectName="bean:name=testBean4", + description="My Managed Bean", + log=true, + logFile="jmx.log", + currencyTimeLimit=15, + persistPolicy="OnUpdate", + persistPeriod=200, + persistLocation="foo", + persistName="bar") + public class AnnotationTestBean implements IJmxTestBean { + + private String name; + private int age; + + @ManagedAttribute(description="The Age Attribute", currencyTimeLimit=15) + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + @ManagedAttribute(description="The Name Attribute", + currencyTimeLimit=20, + defaultValue="bar", + persistPolicy="OnUpdate") + public void setName(String name) { + this.name = name; + } + + @ManagedAttribute(defaultValue="foo", persistPeriod=300) + public String getName() { + return name; + } + + @ManagedOperation(description="Add two numbers") + @ManagedOperationParameters({ + @ManagedOperationParameter(name = "x", description = "The first number"), + @ManagedOperationParameter(name = "y", description = "The second number")}) + public int add(int x, int y) { + return x + y; + } + + public void dontExposeMe() { + throw new RuntimeException(); + } + + } +---- + +In the preceding example, you can see that the `JmxTestBean` class is marked with the +`ManagedResource` annotation and that this `ManagedResource` annotation is configured +with a set of properties. These properties can be used to configure various aspects +of the MBean that is generated by the `MBeanExporter` and are explained in greater +detail later in <>. + +Both the `age` and `name` properties are annotated with the `ManagedAttribute` +annotation, but, in the case of the `age` property, only the getter is marked. +This causes both of these properties to be included in the management interface +as attributes, but the `age` attribute is read-only. + +Finally, the `add(int, int)` method is marked with the `ManagedOperation` attribute, +whereas the `dontExposeMe()` method is not. This causes the management interface to +contain only one operation (`add(int, int)`) when you use the `MetadataMBeanInfoAssembler`. + +The following configuration shows how you can configure the `MBeanExporter` to use the +`MetadataMBeanInfoAssembler`: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + + + + + + + + + + + + + + + + + + + + + +---- + +In the preceding example, an `MetadataMBeanInfoAssembler` bean has been configured with an +instance of the `AnnotationJmxAttributeSource` class and passed to the `MBeanExporter` +through the assembler property. This is all that is required to take advantage of +metadata-driven management interfaces for your Spring-exposed MBeans. + + +[[jmx-interface-metadata-types]] +==== Source-level Metadata Types + +The following table describes the source-level metadata types that are available for use in Spring JMX: + +[[jmx-metadata-types]] +.Source-level metadata types +|=== +| Purpose| Annotation| Annotation Type + +| Mark all instances of a `Class` as JMX managed resources. +| `@ManagedResource` +| Class + +| Mark a method as a JMX operation. +| `@ManagedOperation` +| Method + +| Mark a getter or setter as one half of a JMX attribute. +| `@ManagedAttribute` +| Method (only getters and setters) + +| Define descriptions for operation parameters. +| `@ManagedOperationParameter` and `@ManagedOperationParameters` +| Method +|=== + +The following table describes the configuration parameters that are available for use on these source-level +metadata types: + +[[jmx-metadata-parameters]] +.Source-level metadata parameters +[cols="1,3,1"] +|=== +| Parameter | Description | Applies to + +| `ObjectName` +| Used by `MetadataNamingStrategy` to determine the `ObjectName` of a managed resource. +| `ManagedResource` + +| `description` +| Sets the friendly description of the resource, attribute or operation. +| `ManagedResource`, `ManagedAttribute`, `ManagedOperation`, or `ManagedOperationParameter` + +| `currencyTimeLimit` +| Sets the value of the `currencyTimeLimit` descriptor field. +| `ManagedResource` or `ManagedAttribute` + +| `defaultValue` +| Sets the value of the `defaultValue` descriptor field. +| `ManagedAttribute` + +| `log` +| Sets the value of the `log` descriptor field. +| `ManagedResource` + +| `logFile` +| Sets the value of the `logFile` descriptor field. +| `ManagedResource` + +| `persistPolicy` +| Sets the value of the `persistPolicy` descriptor field. +| `ManagedResource` + +| `persistPeriod` +| Sets the value of the `persistPeriod` descriptor field. +| `ManagedResource` + +| `persistLocation` +| Sets the value of the `persistLocation` descriptor field. +| `ManagedResource` + +| `persistName` +| Sets the value of the `persistName` descriptor field. +| `ManagedResource` + +| `name` +| Sets the display name of an operation parameter. +| `ManagedOperationParameter` + +| `index` +| Sets the index of an operation parameter. +| `ManagedOperationParameter` +|=== + + +[[jmx-interface-autodetect]] +==== Using the `AutodetectCapableMBeanInfoAssembler` Interface + +To simplify configuration even further, Spring includes the +`AutodetectCapableMBeanInfoAssembler` interface, which extends the `MBeanInfoAssembler` +interface to add support for autodetection of MBean resources. If you configure the +`MBeanExporter` with an instance of `AutodetectCapableMBeanInfoAssembler`, it is +allowed to "`vote`" on the inclusion of beans for exposure to JMX. + +The only implementation of the `AutodetectCapableMBeanInfo` interface is +the `MetadataMBeanInfoAssembler`, which votes to include any bean that is marked +with the `ManagedResource` attribute. The default approach in this case is to use the +bean name as the `ObjectName`, which results in a configuration similar to the following: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + + + + + + + + + + + + + + + + + +---- + +Notice that, in the preceding configuration, no beans are passed to the `MBeanExporter`. +However, the `JmxTestBean` is still registered, since it is marked with the `ManagedResource` +attribute and the `MetadataMBeanInfoAssembler` detects this and votes to include it. +The only problem with this approach is that the name of the `JmxTestBean` now has business +meaning. You can address this issue by changing the default behavior for `ObjectName` +creation as defined in <>. + + +[[jmx-interface-java]] +==== Defining Management Interfaces by Using Java Interfaces + +In addition to the `MetadataMBeanInfoAssembler`, Spring also includes the +`InterfaceBasedMBeanInfoAssembler`, which lets you constrain the methods and +properties that are exposed based on the set of methods defined in a collection of +interfaces. + +Although the standard mechanism for exposing MBeans is to use interfaces and a simple +naming scheme, `InterfaceBasedMBeanInfoAssembler` extends this functionality by +removing the need for naming conventions, letting you use more than one interface +and removing the need for your beans to implement the MBean interfaces. + +Consider the following interface, which is used to define a management interface for the +`JmxTestBean` class that we showed earlier: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + public interface IJmxTestBean { + + public int add(int x, int y); + + public long myOperation(); + + public int getAge(); + + public void setAge(int age); + + public void setName(String name); + + public String getName(); + + } +---- + +This interface defines the methods and properties that are exposed as operations and +attributes on the JMX MBean. The following code shows how to configure Spring JMX to use +this interface as the definition for the management interface: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + + + + + + + + + org.springframework.jmx.IJmxTestBean + + + + + + + + + + + +---- + +In the preceding example, the `InterfaceBasedMBeanInfoAssembler` is configured to use the +`IJmxTestBean` interface when constructing the management interface for any bean. It is +important to understand that beans processed by the `InterfaceBasedMBeanInfoAssembler` +are not required to implement the interface used to generate the JMX management +interface. + +In the preceding case, the `IJmxTestBean` interface is used to construct all management +interfaces for all beans. In many cases, this is not the desired behavior, and you may +want to use different interfaces for different beans. In this case, you can pass +`InterfaceBasedMBeanInfoAssembler` a `Properties` instance through the `interfaceMappings` +property, where the key of each entry is the bean name and the value of each entry is a +comma-separated list of interface names to use for that bean. + +If no management interface is specified through either the `managedInterfaces` or +`interfaceMappings` properties, the `InterfaceBasedMBeanInfoAssembler` reflects +on the bean and uses all of the interfaces implemented by that bean to create the +management interface. + + +[[jmx-interface-methodnames]] +==== Using `MethodNameBasedMBeanInfoAssembler` + +`MethodNameBasedMBeanInfoAssembler` lets you specify a list of method names +that are exposed to JMX as attributes and operations. The following code shows a sample +configuration: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + + + + + + + add,myOperation,getName,setName,getAge + + + + +---- + +In the preceding example, you can see that the `add` and `myOperation` methods are exposed as JMX +operations, and `getName()`, `setName(String)`, and `getAge()` are exposed as the +appropriate half of a JMX attribute. In the preceding code, the method mappings apply to +beans that are exposed to JMX. To control method exposure on a bean-by-bean basis, you can use +the `methodMappings` property of `MethodNameMBeanInfoAssembler` to map bean names to +lists of method names. + + + +[[jmx-naming]] +=== Controlling `ObjectName` Instances for Your Beans + +Behind the scenes, the `MBeanExporter` delegates to an implementation of the +`ObjectNamingStrategy` to obtain an `ObjectName` instance for each of the beans it registers. +By default, the default implementation, `KeyNamingStrategy` uses the key of the +`beans` `Map` as the `ObjectName`. In addition, the `KeyNamingStrategy` can map the key +of the `beans` `Map` to an entry in a `Properties` file (or files) to resolve the +`ObjectName`. In addition to the `KeyNamingStrategy`, Spring provides two additional +`ObjectNamingStrategy` implementations: the `IdentityNamingStrategy` (which builds an +`ObjectName` based on the JVM identity of the bean) and the `MetadataNamingStrategy` (which +uses source-level metadata to obtain the `ObjectName`). + + +[[jmx-naming-properties]] +==== Reading `ObjectName` Instances from Properties + +You can configure your own `KeyNamingStrategy` instance and configure it to read +`ObjectName` instances from a `Properties` instance rather than use a bean key. The +`KeyNamingStrategy` tries to locate an entry in the `Properties` with a key +that corresponds to the bean key. If no entry is found or if the `Properties` instance is +`null`, the bean key itself is used. + +The following code shows a sample configuration for the `KeyNamingStrategy`: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + + + + + + + + + + + + + + + + + bean:name=testBean1 + + + + names1.properties,names2.properties + + + + +---- + +The preceding example configures an instance of `KeyNamingStrategy` with a `Properties` instance that +is merged from the `Properties` instance defined by the mapping property and the +properties files located in the paths defined by the mappings property. In this +configuration, the `testBean` bean is given an `ObjectName` of `bean:name=testBean1`, +since this is the entry in the `Properties` instance that has a key corresponding to the +bean key. + +If no entry in the `Properties` instance can be found, the bean key name is used as +the `ObjectName`. + + +[[jmx-naming-metadata]] +==== Using `MetadataNamingStrategy` + +`MetadataNamingStrategy` uses the `objectName` property of the `ManagedResource` +attribute on each bean to create the `ObjectName`. The following code shows the +configuration for the `MetadataNamingStrategy`: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + + + + + + + + + + + + + + + + + + + + +---- + +If no `objectName` has been provided for the `ManagedResource` attribute, an +`ObjectName` is created with the following +format: _[fully-qualified-package-name]:type=[short-classname],name=[bean-name]_. For +example, the generated `ObjectName` for the following bean would be +`com.example:type=MyClass,name=myBean`: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + +---- + + +[[jmx-context-mbeanexport]] +==== Configuring Annotation-based MBean Export + +If you prefer to use <> to define +your management interfaces, a convenience subclass of `MBeanExporter` is available: +`AnnotationMBeanExporter`. When defining an instance of this subclass, you no longer need the +`namingStrategy`, `assembler`, and `attributeSource` configuration, +since it always uses standard Java annotation-based metadata (autodetection is +always enabled as well). In fact, rather than defining an `MBeanExporter` bean, an even +simpler syntax is supported by the `@EnableMBeanExport` `@Configuration` annotation, +as the following example shows: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Configuration + @EnableMBeanExport + public class AppConfig { + + } +---- + +If you prefer XML-based configuration, the `` element serves the +same purpose and is shown in the following listing: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + +---- + +If necessary, you can provide a reference to a particular MBean `server`, and the +`defaultDomain` attribute (a property of `AnnotationMBeanExporter`) accepts an alternate +value for the generated MBean `ObjectName` domains. This is used in place of the +fully qualified package name as described in the previous section on +<>, as the following example shows: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @EnableMBeanExport(server="myMBeanServer", defaultDomain="myDomain") + @Configuration + ContextConfiguration { + + } +---- + +The following example shows the XML equivalent of the preceding annotation-based example: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + +---- + +CAUTION: Do not use interface-based AOP proxies in combination with autodetection of JMX +annotations in your bean classes. Interface-based proxies "`hide`" the target class, which +also hides the JMX-managed resource annotations. Hence, you should use target-class proxies in that +case (through setting the 'proxy-target-class' flag on ``, +`` and so on). Otherwise, your JMX beans might be silently ignored at +startup. + + + +[[jmx-jsr160]] +=== Using JSR-160 Connectors + +For remote access, Spring JMX module offers two `FactoryBean` implementations inside the +`org.springframework.jmx.support` package for creating both server- and client-side +connectors. + + +[[jmx-jsr160-server]] +==== Server-side Connectors + +To have Spring JMX create, start, and expose a JSR-160 `JMXConnectorServer`, you can use the +following configuration: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + +---- + +By default, `ConnectorServerFactoryBean` creates a `JMXConnectorServer` bound to +`service:jmx:jmxmp://localhost:9875`. The `serverConnector` bean thus exposes the +local `MBeanServer` to clients through the JMXMP protocol on localhost, port 9875. Note +that the JMXMP protocol is marked as optional by the JSR 160 specification. Currently, +the main open-source JMX implementation, MX4J, and the one provided with the JDK +do not support JMXMP. + +To specify another URL and register the `JMXConnectorServer` itself with the +`MBeanServer`, you can use the `serviceUrl` and `ObjectName` properties, respectively, +as the following example shows: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + +---- + +If the `ObjectName` property is set, Spring automatically registers your connector +with the `MBeanServer` under that `ObjectName`. The following example shows the full set of +parameters that you can pass to the `ConnectorServerFactoryBean` when creating a +`JMXConnector`: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + + + + + + + + +---- + +Note that, when you use a RMI-based connector, you need the lookup service (`tnameserv` or +`rmiregistry`) to be started in order for the name registration to complete. + + +[[jmx-jsr160-client]] +==== Client-side Connectors + +To create an `MBeanServerConnection` to a remote JSR-160-enabled `MBeanServer`, you can use the +`MBeanServerConnectionFactoryBean`, as the following example shows: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + +---- + + +[[jmx-jsr160-protocols]] +==== JMX over Hessian or SOAP + +JSR-160 permits extensions to the way in which communication is done between the client +and the server. The examples shown in the preceding sections use the mandatory RMI-based implementation +required by the JSR-160 specification (IIOP and JRMP) and the (optional) JMXMP. By using +other providers or JMX implementations (such as http://mx4j.sourceforge.net[MX4J]) you +can take advantage of protocols such as SOAP or Hessian over simple HTTP or SSL and others, +as the following example shows: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + +---- + +In the preceding example, we used MX4J 3.0.0. See the official MX4J +documentation for more information. + + + +[[jmx-proxy]] +=== Accessing MBeans through Proxies + +Spring JMX lets you create proxies that re-route calls to MBeans that are registered in a +local or remote `MBeanServer`. These proxies provide you with a standard Java interface, +through which you can interact with your MBeans. The following code shows how to configure a +proxy for an MBean running in a local `MBeanServer`: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + +---- + +In the preceding example, you can see that a proxy is created for the MBean registered under the +`ObjectName` of `bean:name=testBean`. The set of interfaces that the proxy implements +is controlled by the `proxyInterfaces` property, and the rules for mapping methods and +properties on these interfaces to operations and attributes on the MBean are the same +rules used by the `InterfaceBasedMBeanInfoAssembler`. + +The `MBeanProxyFactoryBean` can create a proxy to any MBean that is accessible through an +`MBeanServerConnection`. By default, the local `MBeanServer` is located and used, but +you can override this and provide an `MBeanServerConnection` that points to a remote +`MBeanServer` to cater for proxies that point to remote MBeans: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + + + + + + +---- + +In the preceding example, we create an `MBeanServerConnection` that points to a remote machine +that uses the `MBeanServerConnectionFactoryBean`. This `MBeanServerConnection` is then +passed to the `MBeanProxyFactoryBean` through the `server` property. The proxy that is +created forwards all invocations to the `MBeanServer` through this +`MBeanServerConnection`. + + + +[[jmx-notifications]] +=== Notifications + +Spring's JMX offering includes comprehensive support for JMX notifications. + + +[[jmx-notifications-listeners]] +==== Registering Listeners for Notifications + +Spring's JMX support makes it easy to register any number of +`NotificationListeners` with any number of MBeans (this includes MBeans exported by +Spring's `MBeanExporter` and MBeans registered through some other mechanism). For +example, consider the scenario where one would like to be informed (through a +`Notification`) each and every time an attribute of a target MBean changes. The following +example writes notifications to the console: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + package com.example; + + import javax.management.AttributeChangeNotification; + import javax.management.Notification; + import javax.management.NotificationFilter; + import javax.management.NotificationListener; + + public class ConsoleLoggingNotificationListener + implements NotificationListener, NotificationFilter { + + public void handleNotification(Notification notification, Object handback) { + System.out.println(notification); + System.out.println(handback); + } + + public boolean isNotificationEnabled(Notification notification) { + return AttributeChangeNotification.class.isAssignableFrom(notification.getClass()); + } + + } +---- + +The following example adds `ConsoleLoggingNotificationListener` (defined in the preceding +example) to `notificationListenerMappings`: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + + + + + + + + + + + + + + + + + + + + +---- + +With the preceding configuration in place, every time a JMX `Notification` is broadcast from +the target MBean (`bean:name=testBean1`), the `ConsoleLoggingNotificationListener` bean +that was registered as a listener through the `notificationListenerMappings` property is +notified. The `ConsoleLoggingNotificationListener` bean can then take whatever action +it deems appropriate in response to the `Notification`. + +You can also use straight bean names as the link between exported beans and listeners, +as the following example shows: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + + + + + + + + + + + + + + + + + + + + +---- + +If you want to register a single `NotificationListener` instance for all of the beans +that the enclosing `MBeanExporter` exports, you can use the special wildcard (`{asterisk}`) +as the key for an entry in the `notificationListenerMappings` property +map, as the following example shows: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + + + + +---- + +If you need to do the inverse (that is, register a number of distinct listeners against +an MBean), you must instead use the `notificationListeners` list property (in +preference to the `notificationListenerMappings` property). This time, instead of +configuring a `NotificationListener` for a single MBean, we configure +`NotificationListenerBean` instances. A `NotificationListenerBean` encapsulates a +`NotificationListener` and the `ObjectName` (or `ObjectNames`) that it is to be +registered against in an `MBeanServer`. The `NotificationListenerBean` also encapsulates +a number of other properties, such as a `NotificationFilter` and an arbitrary handback +object that can be used in advanced JMX notification scenarios. + +The configuration when using `NotificationListenerBean` instances is not wildly +different to what was presented previously, as the following example shows: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + + + + + + + + + + + + + + bean:name=testBean1 + + + + + + + + + + + + + +---- + +The preceding example is equivalent to the first notification example. Assume, then, that +we want to be given a handback object every time a `Notification` is raised and that +we also want to filter out extraneous `Notifications` by supplying a +`NotificationFilter`. The following example accomplishes these goals: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + + + + + + + + + + + + + + bean:name=testBean1 + bean:name=testBean2 + + + + + + + + + + + + + + + + + + + + + + + + + + + +---- + +(For a full discussion of what a handback object is and, +indeed, what a `NotificationFilter` is, see the section of the JMX +specification (1.2) entitled 'The JMX Notification Model'.) + + +[[jmx-notifications-publishing]] +==== Publishing Notifications + +Spring provides support not only for registering to receive `Notifications` but also +for publishing `Notifications`. + +NOTE: This section is really only relevant to Spring-managed beans that have +been exposed as MBeans through an `MBeanExporter`. Any existing user-defined MBeans should +use the standard JMX APIs for notification publication. + +The key interface in Spring's JMX notification publication support is the +`NotificationPublisher` interface (defined in the +`org.springframework.jmx.export.notification` package). Any bean that is going to be +exported as an MBean through an `MBeanExporter` instance can implement the related +`NotificationPublisherAware` interface to gain access to a `NotificationPublisher` +instance. The `NotificationPublisherAware` interface supplies an instance of a +`NotificationPublisher` to the implementing bean through a simple setter method, +which the bean can then use to publish `Notifications`. + +As stated in the javadoc of the +{api-spring-framework}/jmx/export/notification/NotificationPublisher.html[`NotificationPublisher`] +interface, managed beans that publish events through the `NotificationPublisher` +mechanism are not responsible for the state management of notification listeners. +Spring's JMX support takes care of handling all the JMX infrastructure issues. +All you need to do, as an application developer, is implement the +`NotificationPublisherAware` interface and start publishing events by using the +supplied `NotificationPublisher` instance. Note that the `NotificationPublisher` +is set after the managed bean has been registered with an `MBeanServer`. + +Using a `NotificationPublisher` instance is quite straightforward. You create a JMX +`Notification` instance (or an instance of an appropriate `Notification` subclass), +populate the notification with the data pertinent to the event that is to be +published, and invoke the `sendNotification(Notification)` on the +`NotificationPublisher` instance, passing in the `Notification`. + +In the following example, exported instances of the `JmxTestBean` publish a +`NotificationEvent` every time the `add(int, int)` operation is invoked: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + package org.springframework.jmx; + + import org.springframework.jmx.export.notification.NotificationPublisherAware; + import org.springframework.jmx.export.notification.NotificationPublisher; + import javax.management.Notification; + + public class JmxTestBean implements IJmxTestBean, NotificationPublisherAware { + + private String name; + private int age; + private boolean isSuperman; + private NotificationPublisher publisher; + + // other getters and setters omitted for clarity + + public int add(int x, int y) { + int answer = x + y; + this.publisher.sendNotification(new Notification("add", this, 0)); + return answer; + } + + public void dontExposeMe() { + throw new RuntimeException(); + } + + public void setNotificationPublisher(NotificationPublisher notificationPublisher) { + this.publisher = notificationPublisher; + } + + } +---- + +The `NotificationPublisher` interface and the machinery to get it all working is one of +the nicer features of Spring's JMX support. It does, however, come with the price tag of +coupling your classes to both Spring and JMX. As always, the advice here is to be +pragmatic. If you need the functionality offered by the `NotificationPublisher` and +you can accept the coupling to both Spring and JMX, then do so. + + + +[[jmx-resources]] +=== Further Resources + +This section contains links to further resources about JMX: + +* The https://www.oracle.com/technetwork/java/javase/tech/javamanagement-140525.html[JMX +homepage] at Oracle. +* The https://jcp.org/aboutJava/communityprocess/final/jsr003/index3.html[JMX + specification] (JSR-000003). +* The https://jcp.org/aboutJava/communityprocess/final/jsr160/index.html[JMX Remote API + specification] (JSR-000160). +* The http://mx4j.sourceforge.net/[MX4J homepage]. (MX4J is an open-source implementation of + various JMX specs.) + + + + +[[mail]] +== Email + +This section describes how to send email with the Spring Framework. + +.Library dependencies +**** +The following JAR needs to be on the classpath of your application in order to use +the Spring Framework's email library: + +* The https://eclipse-ee4j.github.io/mail/[JavaMail / Jakarta Mail 1.6] library + +This library is freely available on the web -- for example, in Maven Central as +`com.sun.mail:jakarta.mail`. Please make sure to use the latest 1.6.x version +rather than Jakarta Mail 2.0 (which comes with a different package namespace). +**** + +The Spring Framework provides a helpful utility library for sending email that shields +you from the specifics of the underlying mailing system and is responsible for +low-level resource handling on behalf of the client. + +The `org.springframework.mail` package is the root level package for the Spring +Framework's email support. The central interface for sending emails is the `MailSender` +interface. A simple value object that encapsulates the properties of a simple mail such +as `from` and `to` (plus many others) is the `SimpleMailMessage` class. This package +also contains a hierarchy of checked exceptions that provide a higher level of +abstraction over the lower level mail system exceptions, with the root exception being +`MailException`. See the {api-spring-framework}/mail/MailException.html[javadoc] +for more information on the rich mail exception hierarchy. + +The `org.springframework.mail.javamail.JavaMailSender` interface adds specialized +JavaMail features, such as MIME message support to the `MailSender` interface +(from which it inherits). `JavaMailSender` also provides a callback interface called +`org.springframework.mail.javamail.MimeMessagePreparator` for preparing a `MimeMessage`. + + + +[[mail-usage]] +=== Usage + +Assume that we have a business interface called `OrderManager`, as the following example shows: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + public interface OrderManager { + + void placeOrder(Order order); + + } +---- + +Further assume that we have a requirement stating that an email message with an +order number needs to be generated and sent to a customer who placed the relevant order. + + +[[mail-usage-simple]] +==== Basic `MailSender` and `SimpleMailMessage` Usage + +The following example shows how to use `MailSender` and `SimpleMailMessage` to send an +email when someone places an order: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + import org.springframework.mail.MailException; + import org.springframework.mail.MailSender; + import org.springframework.mail.SimpleMailMessage; + + public class SimpleOrderManager implements OrderManager { + + private MailSender mailSender; + private SimpleMailMessage templateMessage; + + public void setMailSender(MailSender mailSender) { + this.mailSender = mailSender; + } + + public void setTemplateMessage(SimpleMailMessage templateMessage) { + this.templateMessage = templateMessage; + } + + public void placeOrder(Order order) { + + // Do the business calculations... + + // Call the collaborators to persist the order... + + // Create a thread safe "copy" of the template message and customize it + SimpleMailMessage msg = new SimpleMailMessage(this.templateMessage); + msg.setTo(order.getCustomer().getEmailAddress()); + msg.setText( + "Dear " + order.getCustomer().getFirstName() + + order.getCustomer().getLastName() + + ", thank you for placing order. Your order number is " + + order.getOrderNumber()); + try { + this.mailSender.send(msg); + } + catch (MailException ex) { + // simply log it and go on... + System.err.println(ex.getMessage()); + } + } + + } +---- + +The following example shows the bean definitions for the preceding code: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + + + + + + + + + + + +---- + + +[[mail-usage-mime]] +==== Using `JavaMailSender` and `MimeMessagePreparator` + +This section describes another implementation of `OrderManager` that uses the `MimeMessagePreparator` +callback interface. In the following example, the `mailSender` property is of type +`JavaMailSender` so that we are able to use the JavaMail `MimeMessage` class: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + import jakarta.mail.Message; + import jakarta.mail.MessagingException; + import jakarta.mail.internet.InternetAddress; + import jakarta.mail.internet.MimeMessage; + + import jakarta.mail.internet.MimeMessage; + import org.springframework.mail.MailException; + import org.springframework.mail.javamail.JavaMailSender; + import org.springframework.mail.javamail.MimeMessagePreparator; + + public class SimpleOrderManager implements OrderManager { + + private JavaMailSender mailSender; + + public void setMailSender(JavaMailSender mailSender) { + this.mailSender = mailSender; + } + + public void placeOrder(final Order order) { + // Do the business calculations... + // Call the collaborators to persist the order... + + MimeMessagePreparator preparator = new MimeMessagePreparator() { + public void prepare(MimeMessage mimeMessage) throws Exception { + mimeMessage.setRecipient(Message.RecipientType.TO, + new InternetAddress(order.getCustomer().getEmailAddress())); + mimeMessage.setFrom(new InternetAddress("mail@mycompany.example")); + mimeMessage.setText("Dear " + order.getCustomer().getFirstName() + " " + + order.getCustomer().getLastName() + ", thanks for your order. " + + "Your order number is " + order.getOrderNumber() + "."); + } + }; + + try { + this.mailSender.send(preparator); + } + catch (MailException ex) { + // simply log it and go on... + System.err.println(ex.getMessage()); + } + } + + } +---- + +NOTE: The mail code is a crosscutting concern and could well be a candidate for +refactoring into a <>, which could then +be run at appropriate joinpoints on the `OrderManager` target. + +The Spring Framework's mail support ships with the standard JavaMail implementation. +See the relevant javadoc for more information. + + + +[[mail-javamail-mime]] +=== Using the JavaMail `MimeMessageHelper` + +A class that comes in pretty handy when dealing with JavaMail messages is +`org.springframework.mail.javamail.MimeMessageHelper`, which shields you from +having to use the verbose JavaMail API. Using the `MimeMessageHelper`, it is +pretty easy to create a `MimeMessage`, as the following example shows: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + // of course you would use DI in any real-world cases + JavaMailSenderImpl sender = new JavaMailSenderImpl(); + sender.setHost("mail.host.com"); + + MimeMessage message = sender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(message); + helper.setTo("test@host.com"); + helper.setText("Thank you for ordering!"); + + sender.send(message); +---- + + +[[mail-javamail-mime-attachments]] +==== Sending Attachments and Inline Resources + +Multipart email messages allow for both attachments and inline resources. Examples of +inline resources include an image or a stylesheet that you want to use in your message but +that you do not want displayed as an attachment. + +[[mail-javamail-mime-attachments-attachment]] +===== Attachments + +The following example shows you how to use the `MimeMessageHelper` to send an email +with a single JPEG image attachment: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + JavaMailSenderImpl sender = new JavaMailSenderImpl(); + sender.setHost("mail.host.com"); + + MimeMessage message = sender.createMimeMessage(); + + // use the true flag to indicate you need a multipart message + MimeMessageHelper helper = new MimeMessageHelper(message, true); + helper.setTo("test@host.com"); + + helper.setText("Check out this image!"); + + // let's attach the infamous windows Sample file (this time copied to c:/) + FileSystemResource file = new FileSystemResource(new File("c:/Sample.jpg")); + helper.addAttachment("CoolImage.jpg", file); + + sender.send(message); +---- + +[[mail-javamail-mime-attachments-inline]] +===== Inline Resources + +The following example shows you how to use the `MimeMessageHelper` to send an email +with an inline image: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + JavaMailSenderImpl sender = new JavaMailSenderImpl(); + sender.setHost("mail.host.com"); + + MimeMessage message = sender.createMimeMessage(); + + // use the true flag to indicate you need a multipart message + MimeMessageHelper helper = new MimeMessageHelper(message, true); + helper.setTo("test@host.com"); + + // use the true flag to indicate the text included is HTML + helper.setText("", true); + + // let's include the infamous windows Sample file (this time copied to c:/) + FileSystemResource res = new FileSystemResource(new File("c:/Sample.jpg")); + helper.addInline("identifier1234", res); + + sender.send(message); +---- + +WARNING: Inline resources are added to the `MimeMessage` by using the specified `Content-ID` +(`identifier1234` in the above example). The order in which you add the text +and the resource are very important. Be sure to first add the text and then +the resources. If you are doing it the other way around, it does not work. + + +[[mail-templates]] +==== Creating Email Content by Using a Templating Library + +The code in the examples shown in the previous sections explicitly created the content of the email message, +by using methods calls such as `message.setText(..)`. This is fine for simple cases, and it +is okay in the context of the aforementioned examples, where the intent was to show you +the very basics of the API. + +In your typical enterprise application, though, developers often do not create the content +of email messages by using the previously shown approach for a number of reasons: + +* Creating HTML-based email content in Java code is tedious and error prone. +* There is no clear separation between display logic and business logic. +* Changing the display structure of the email content requires writing Java code, + recompiling, redeploying, and so on. + +Typically, the approach taken to address these issues is to use a template library (such +as FreeMarker) to define the display structure of email content. This leaves your code +tasked only with creating the data that is to be rendered in the email template and +sending the email. It is definitely a best practice when the content of your email messages +becomes even moderately complex, and, with the Spring Framework's support classes for +FreeMarker, it becomes quite easy to do. + + + + +[[scheduling]] +== Task Execution and Scheduling + +The Spring Framework provides abstractions for the asynchronous execution and scheduling of +tasks with the `TaskExecutor` and `TaskScheduler` interfaces, respectively. Spring also +features implementations of those interfaces that support thread pools or delegation to +CommonJ within an application server environment. Ultimately, the use of these +implementations behind the common interfaces abstracts away the differences between Java +SE 5, Java SE 6, and Jakarta EE environments. + +Spring also features integration classes to support scheduling with the `Timer` +(part of the JDK since 1.3) and the Quartz Scheduler ( https://www.quartz-scheduler.org/[]). +You can set up both of those schedulers by using a `FactoryBean` with optional references to +`Timer` or `Trigger` instances, respectively. Furthermore, a convenience class for both +the Quartz Scheduler and the `Timer` is available that lets you invoke a method of +an existing target object (analogous to the normal `MethodInvokingFactoryBean` +operation). + + + +[[scheduling-task-executor]] +=== The Spring `TaskExecutor` Abstraction + +Executors are the JDK name for the concept of thread pools. The "`executor`" naming is +due to the fact that there is no guarantee that the underlying implementation is +actually a pool. An executor may be single-threaded or even synchronous. Spring's +abstraction hides implementation details between the Java SE and Jakarta EE environments. + +Spring's `TaskExecutor` interface is identical to the `java.util.concurrent.Executor` +interface. In fact, originally, its primary reason for existence was to abstract away +the need for Java 5 when using thread pools. The interface has a single method +(`execute(Runnable task)`) that accepts a task for execution based on the semantics +and configuration of the thread pool. + +The `TaskExecutor` was originally created to give other Spring components an abstraction +for thread pooling where needed. Components such as the `ApplicationEventMulticaster`, +JMS's `AbstractMessageListenerContainer`, and Quartz integration all use the +`TaskExecutor` abstraction to pool threads. However, if your beans need thread pooling +behavior, you can also use this abstraction for your own needs. + + +[[scheduling-task-executor-types]] +==== `TaskExecutor` Types + +Spring includes a number of pre-built implementations of `TaskExecutor`. +In all likelihood, you should never need to implement your own. +The variants that Spring provides are as follows: + +* `SyncTaskExecutor`: + This implementation does not run invocations asynchronously. Instead, each + invocation takes place in the calling thread. It is primarily used in situations + where multi-threading is not necessary, such as in simple test cases. +* `SimpleAsyncTaskExecutor`: + This implementation does not reuse any threads. Rather, it starts up a new thread + for each invocation. However, it does support a concurrency limit that blocks + any invocations that are over the limit until a slot has been freed up. If you + are looking for true pooling, see `ThreadPoolTaskExecutor`, later in this list. +* `ConcurrentTaskExecutor`: + This implementation is an adapter for a `java.util.concurrent.Executor` instance. + There is an alternative (`ThreadPoolTaskExecutor`) that exposes the `Executor` + configuration parameters as bean properties. There is rarely a need to use + `ConcurrentTaskExecutor` directly. However, if the `ThreadPoolTaskExecutor` is not + flexible enough for your needs, `ConcurrentTaskExecutor` is an alternative. +* `ThreadPoolTaskExecutor`: + This implementation is most commonly used. It exposes bean properties for + configuring a `java.util.concurrent.ThreadPoolExecutor` and wraps it in a `TaskExecutor`. + If you need to adapt to a different kind of `java.util.concurrent.Executor`, we + recommend that you use a `ConcurrentTaskExecutor` instead. +* `DefaultManagedTaskExecutor`: + This implementation uses a JNDI-obtained `ManagedExecutorService` in a JSR-236 + compatible runtime environment (such as a Jakarta EE application server), + replacing a CommonJ WorkManager for that purpose. + + +[[scheduling-task-executor-usage]] +==== Using a `TaskExecutor` + +Spring's `TaskExecutor` implementations are used as simple JavaBeans. In the following example, +we define a bean that uses the `ThreadPoolTaskExecutor` to asynchronously print +out a set of messages: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + import org.springframework.core.task.TaskExecutor; + + public class TaskExecutorExample { + + private class MessagePrinterTask implements Runnable { + + private String message; + + public MessagePrinterTask(String message) { + this.message = message; + } + + public void run() { + System.out.println(message); + } + } + + private TaskExecutor taskExecutor; + + public TaskExecutorExample(TaskExecutor taskExecutor) { + this.taskExecutor = taskExecutor; + } + + public void printMessages() { + for(int i = 0; i < 25; i++) { + taskExecutor.execute(new MessagePrinterTask("Message" + i)); + } + } + } +---- + +As you can see, rather than retrieving a thread from the pool and executing it yourself, +you add your `Runnable` to the queue. Then the `TaskExecutor` uses its internal rules to +decide when the task gets run. + +To configure the rules that the `TaskExecutor` uses, we expose simple bean properties: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + + + + + + +---- + + + +[[scheduling-task-scheduler]] +=== The Spring `TaskScheduler` Abstraction + +In addition to the `TaskExecutor` abstraction, Spring 3.0 introduced a `TaskScheduler` +with a variety of methods for scheduling tasks to run at some point in the future. +The following listing shows the `TaskScheduler` interface definition: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + public interface TaskScheduler { + + ScheduledFuture schedule(Runnable task, Trigger trigger); + + ScheduledFuture schedule(Runnable task, Instant startTime); + + ScheduledFuture scheduleAtFixedRate(Runnable task, Instant startTime, Duration period); + + ScheduledFuture scheduleAtFixedRate(Runnable task, Duration period); + + ScheduledFuture scheduleWithFixedDelay(Runnable task, Instant startTime, Duration delay); + + ScheduledFuture scheduleWithFixedDelay(Runnable task, Duration delay); + +---- + +The simplest method is the one named `schedule` that takes only a `Runnable` and an `Instant`. +That causes the task to run once after the specified time. All of the other methods +are capable of scheduling tasks to run repeatedly. The fixed-rate and fixed-delay +methods are for simple, periodic execution, but the method that accepts a `Trigger` is +much more flexible. + + +[[scheduling-trigger-interface]] +==== `Trigger` Interface + +The `Trigger` interface is essentially inspired by JSR-236 which, as of Spring 3.0, +was not yet officially implemented. The basic idea of the `Trigger` is that execution +times may be determined based on past execution outcomes or even arbitrary conditions. +If these determinations do take into account the outcome of the preceding execution, +that information is available within a `TriggerContext`. The `Trigger` interface itself +is quite simple, as the following listing shows: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + public interface Trigger { + + Date nextExecutionTime(TriggerContext triggerContext); + } +---- + +The `TriggerContext` is the most important part. It encapsulates all of +the relevant data and is open for extension in the future, if necessary. The +`TriggerContext` is an interface (a `SimpleTriggerContext` implementation is used by +default). The following listing shows the available methods for `Trigger` implementations. + +[source,java,indent=0,subs="verbatim,quotes"] +---- + public interface TriggerContext { + + Date lastScheduledExecutionTime(); + + Date lastActualExecutionTime(); + + Date lastCompletionTime(); + } +---- + + +[[scheduling-trigger-implementations]] +==== `Trigger` Implementations + +Spring provides two implementations of the `Trigger` interface. The most interesting one +is the `CronTrigger`. It enables the scheduling of tasks based on +<>. +For example, the following task is scheduled to run 15 minutes past each hour but only +during the 9-to-5 "`business hours`" on weekdays: + +[source,java,indent=0] +[subs="verbatim"] +---- + scheduler.schedule(task, new CronTrigger("0 15 9-17 * * MON-FRI")); +---- + +The other implementation is a `PeriodicTrigger` that accepts a fixed +period, an optional initial delay value, and a boolean to indicate whether the period +should be interpreted as a fixed-rate or a fixed-delay. Since the `TaskScheduler` +interface already defines methods for scheduling tasks at a fixed rate or with a +fixed delay, those methods should be used directly whenever possible. The value of the +`PeriodicTrigger` implementation is that you can use it within components that rely on +the `Trigger` abstraction. For example, it may be convenient to allow periodic triggers, +cron-based triggers, and even custom trigger implementations to be used interchangeably. +Such a component could take advantage of dependency injection so that you can configure such `Triggers` +externally and, therefore, easily modify or extend them. + + +[[scheduling-task-scheduler-implementations]] +==== `TaskScheduler` implementations + +As with Spring's `TaskExecutor` abstraction, the primary benefit of the `TaskScheduler` +arrangement is that an application's scheduling needs are decoupled from the deployment +environment. This abstraction level is particularly relevant when deploying to an +application server environment where threads should not be created directly by the +application itself. For such scenarios, Spring provides a `TimerManagerTaskScheduler` +that delegates to a CommonJ `TimerManager` on WebLogic or WebSphere as well as a more recent +`DefaultManagedTaskScheduler` that delegates to a JSR-236 `ManagedScheduledExecutorService` +in a Jakarta EE environment. Both are typically configured with a JNDI lookup. + +Whenever external thread management is not a requirement, a simpler alternative is +a local `ScheduledExecutorService` setup within the application, which can be adapted +through Spring's `ConcurrentTaskScheduler`. As a convenience, Spring also provides a +`ThreadPoolTaskScheduler`, which internally delegates to a `ScheduledExecutorService` +to provide common bean-style configuration along the lines of `ThreadPoolTaskExecutor`. +These variants work perfectly fine for locally embedded thread pool setups in lenient +application server environments, as well -- in particular on Tomcat and Jetty. + + + +[[scheduling-annotation-support]] +=== Annotation Support for Scheduling and Asynchronous Execution + +Spring provides annotation support for both task scheduling and asynchronous method +execution. + + +[[scheduling-enable-annotation-support]] +==== Enable Scheduling Annotations + +To enable support for `@Scheduled` and `@Async` annotations, you can add `@EnableScheduling` and +`@EnableAsync` to one of your `@Configuration` classes, as the following example shows: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Configuration + @EnableAsync + @EnableScheduling + public class AppConfig { + } +---- + +You can pick and choose the relevant annotations for your application. For example, +if you need only support for `@Scheduled`, you can omit `@EnableAsync`. For more +fine-grained control, you can additionally implement the `SchedulingConfigurer` +interface, the `AsyncConfigurer` interface, or both. See the +{api-spring-framework}/scheduling/annotation/SchedulingConfigurer.html[`SchedulingConfigurer`] +and {api-spring-framework}/scheduling/annotation/AsyncConfigurer.html[`AsyncConfigurer`] +javadoc for full details. + +If you prefer XML configuration, you can use the `` element, +as the following example shows: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + +---- + +Note that, with the preceding XML, an executor reference is provided for handling those +tasks that correspond to methods with the `@Async` annotation, and the scheduler +reference is provided for managing those methods annotated with `@Scheduled`. + +NOTE: The default advice mode for processing `@Async` annotations is `proxy` which allows +for interception of calls through the proxy only. Local calls within the same class +cannot get intercepted that way. For a more advanced mode of interception, consider +switching to `aspectj` mode in combination with compile-time or load-time weaving. + + +[[scheduling-annotation-support-scheduled]] +==== The `@Scheduled` annotation + +You can add the `@Scheduled` annotation to a method, along with trigger metadata. For +example, the following method is invoked every five seconds (5000 milliseconds) with a +fixed delay, meaning that the period is measured from the completion time of each +preceding invocation. + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Scheduled(fixedDelay = 5000) + public void doSomething() { + // something that should run periodically + } +---- + +[NOTE] +==== +By default, milliseconds will be used as the time unit for fixed delay, fixed rate, and +initial delay values. If you would like to use a different time unit such as seconds or +minutes, you can configure this via the `timeUnit` attribute in `@Scheduled`. + +For example, the previous example can also be written as follows. + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Scheduled(fixedDelay = 5, timeUnit = TimeUnit.SECONDS) + public void doSomething() { + // something that should run periodically + } +---- +==== + +If you need a fixed-rate execution, you can use the `fixedRate` attribute within the +annotation. The following method is invoked every five seconds (measured between the +successive start times of each invocation). + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Scheduled(fixedRate = 5, timeUnit = TimeUnit.SECONDS) + public void doSomething() { + // something that should run periodically + } +---- + +For fixed-delay and fixed-rate tasks, you can specify an initial delay by indicating the +amount of time to wait before the first execution of the method, as the following +`fixedRate` example shows. + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Scheduled(initialDelay = 1000, fixedRate = 5000) + public void doSomething() { + // something that should run periodically + } +---- + +If simple periodic scheduling is not expressive enough, you can provide a +<>. +The following example runs only on weekdays: + +[source,java,indent=0] +[subs="verbatim"] +---- + @Scheduled(cron="*/5 * * * * MON-FRI") + public void doSomething() { + // something that should run on weekdays only + } +---- + +TIP: You can also use the `zone` attribute to specify the time zone in which the cron +expression is resolved. + +Notice that the methods to be scheduled must have void returns and must not accept any +arguments. If the method needs to interact with other objects from the application +context, those would typically have been provided through dependency injection. + +[NOTE] +==== +As of Spring Framework 4.3, `@Scheduled` methods are supported on beans of any scope. + +Make sure that you are not initializing multiple instances of the same `@Scheduled` +annotation class at runtime, unless you do want to schedule callbacks to each such +instance. Related to this, make sure that you do not use `@Configurable` on bean +classes that are annotated with `@Scheduled` and registered as regular Spring beans +with the container. Otherwise, you would get double initialization (once through the +container and once through the `@Configurable` aspect), with the consequence of each +`@Scheduled` method being invoked twice. +==== + + +[[scheduling-annotation-support-async]] +==== The `@Async` annotation + +You can provide the `@Async` annotation on a method so that invocation of that method +occurs asynchronously. In other words, the caller returns immediately upon +invocation, while the actual execution of the method occurs in a task that has been +submitted to a Spring `TaskExecutor`. In the simplest case, you can apply the annotation +to a method that returns `void`, as the following example shows: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Async + void doSomething() { + // this will be run asynchronously + } +---- + +Unlike the methods annotated with the `@Scheduled` annotation, these methods can expect +arguments, because they are invoked in the "`normal`" way by callers at runtime rather +than from a scheduled task being managed by the container. For example, the following code is +a legitimate application of the `@Async` annotation: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Async + void doSomething(String s) { + // this will be run asynchronously + } +---- + +Even methods that return a value can be invoked asynchronously. However, such methods +are required to have a `Future`-typed return value. This still provides the benefit of +asynchronous execution so that the caller can perform other tasks prior to calling +`get()` on that `Future`. The following example shows how to use `@Async` on a method +that returns a value: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Async + Future returnSomething(int i) { + // this will be run asynchronously + } +---- + +TIP: `@Async` methods may not only declare a regular `java.util.concurrent.Future` return type +but also Spring's `org.springframework.util.concurrent.ListenableFuture` or, as of Spring +4.2, JDK 8's `java.util.concurrent.CompletableFuture`, for richer interaction with the +asynchronous task and for immediate composition with further processing steps. + +You can not use `@Async` in conjunction with lifecycle callbacks such as +`@PostConstruct`. To asynchronously initialize Spring beans, you currently have to use +a separate initializing Spring bean that then invokes the `@Async` annotated method on the +target, as the following example shows: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + public class SampleBeanImpl implements SampleBean { + + @Async + void doSomething() { + // ... + } + + } + + public class SampleBeanInitializer { + + private final SampleBean bean; + + public SampleBeanInitializer(SampleBean bean) { + this.bean = bean; + } + + @PostConstruct + public void initialize() { + bean.doSomething(); + } + + } +---- + +NOTE: There is no direct XML equivalent for `@Async`, since such methods should be designed +for asynchronous execution in the first place, not externally re-declared to be asynchronous. +However, you can manually set up Spring's `AsyncExecutionInterceptor` with Spring AOP, +in combination with a custom pointcut. + + +[[scheduling-annotation-support-qualification]] +==== Executor Qualification with `@Async` + +By default, when specifying `@Async` on a method, the executor that is used is the +one <>, +i.e. the "`annotation-driven`" element if you are using XML or your `AsyncConfigurer` +implementation, if any. However, you can use the `value` attribute of the `@Async` +annotation when you need to indicate that an executor other than the default should be +used when executing a given method. The following example shows how to do so: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Async("otherExecutor") + void doSomething(String s) { + // this will be run asynchronously by "otherExecutor" + } +---- + +In this case, `"otherExecutor"` can be the name of any `Executor` bean in the Spring +container, or it may be the name of a qualifier associated with any `Executor` (for example, as +specified with the `` element or Spring's `@Qualifier` annotation). + + +[[scheduling-annotation-support-exception]] +==== Exception Management with `@Async` + +When an `@Async` method has a `Future`-typed return value, it is easy to manage +an exception that was thrown during the method execution, as this exception is +thrown when calling `get` on the `Future` result. With a `void` return type, +however, the exception is uncaught and cannot be transmitted. You can provide an +`AsyncUncaughtExceptionHandler` to handle such exceptions. The following example shows +how to do so: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + public class MyAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler { + + @Override + public void handleUncaughtException(Throwable ex, Method method, Object... params) { + // handle exception + } + } +---- + +By default, the exception is merely logged. You can define a custom `AsyncUncaughtExceptionHandler` +by using `AsyncConfigurer` or the `` XML element. + + + +[[scheduling-task-namespace]] +=== The `task` Namespace + +As of version 3.0, Spring includes an XML namespace for configuring `TaskExecutor` and +`TaskScheduler` instances. It also provides a convenient way to configure tasks to be +scheduled with a trigger. + + +[[scheduling-task-namespace-scheduler]] +==== The 'scheduler' Element + +The following element creates a `ThreadPoolTaskScheduler` instance with the +specified thread pool size: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + +---- + +The value provided for the `id` attribute is used as the prefix for thread names +within the pool. The `scheduler` element is relatively straightforward. If you do not +provide a `pool-size` attribute, the default thread pool has only a single thread. +There are no other configuration options for the scheduler. + + +[[scheduling-task-namespace-executor]] +==== The `executor` Element + +The following creates a `ThreadPoolTaskExecutor` instance: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + +---- + +As with the scheduler shown in the <>, +the value provided for the `id` attribute is used as the prefix for thread names within +the pool. As far as the pool size is concerned, the `executor` element supports more +configuration options than the `scheduler` element. For one thing, the thread pool for +a `ThreadPoolTaskExecutor` is itself more configurable. Rather than only a single size, +an executor's thread pool can have different values for the core and the max size. +If you provide a single value, the executor has a fixed-size thread pool (the core and +max sizes are the same). However, the `executor` element's `pool-size` attribute also +accepts a range in the form of `min-max`. The following example sets a minimum value of +`5` and a maximum value of `25`: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + +---- + +In the preceding configuration, a `queue-capacity` value has also been provided. +The configuration of the thread pool should also be considered in light of the +executor's queue capacity. For the full description of the relationship between pool +size and queue capacity, see the documentation for +https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ThreadPoolExecutor.html[`ThreadPoolExecutor`]. +The main idea is that, when a task is submitted, the executor first tries to use a +free thread if the number of active threads is currently less than the core size. +If the core size has been reached, the task is added to the queue, as long as its +capacity has not yet been reached. Only then, if the queue's capacity has been +reached, does the executor create a new thread beyond the core size. If the max size +has also been reached, then the executor rejects the task. + +By default, the queue is unbounded, but this is rarely the desired configuration, +because it can lead to `OutOfMemoryErrors` if enough tasks are added to that queue while +all pool threads are busy. Furthermore, if the queue is unbounded, the max size has +no effect at all. Since the executor always tries the queue before creating a new +thread beyond the core size, a queue must have a finite capacity for the thread pool to +grow beyond the core size (this is why a fixed-size pool is the only sensible case +when using an unbounded queue). + +Consider the case, as mentioned above, when a task is rejected. By default, when a +task is rejected, a thread pool executor throws a `TaskRejectedException`. However, +the rejection policy is actually configurable. The exception is thrown when using +the default rejection policy, which is the `AbortPolicy` implementation. +For applications where some tasks can be skipped under heavy load, you can instead +configure either `DiscardPolicy` or `DiscardOldestPolicy`. Another option that works +well for applications that need to throttle the submitted tasks under heavy load is +the `CallerRunsPolicy`. Instead of throwing an exception or discarding tasks, +that policy forces the thread that is calling the submit method to run the task itself. +The idea is that such a caller is busy while running that task and not able to submit +other tasks immediately. Therefore, it provides a simple way to throttle the incoming +load while maintaining the limits of the thread pool and queue. Typically, this allows +the executor to "`catch up`" on the tasks it is handling and thereby frees up some +capacity on the queue, in the pool, or both. You can choose any of these options from an +enumeration of values available for the `rejection-policy` attribute on the `executor` +element. + +The following example shows an `executor` element with a number of attributes to specify +various behaviors: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + +---- + +Finally, the `keep-alive` setting determines the time limit (in seconds) for which threads +may remain idle before being stopped. If there are more than the core number of threads +currently in the pool, after waiting this amount of time without processing a task, excess +threads get stopped. A time value of zero causes excess threads to stop +immediately after executing a task without remaining follow-up work in the task queue. +The following example sets the `keep-alive` value to two minutes: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + +---- + + +[[scheduling-task-namespace-scheduled-tasks]] +==== The 'scheduled-tasks' Element + +The most powerful feature of Spring's task namespace is the support for configuring +tasks to be scheduled within a Spring Application Context. This follows an approach +similar to other "`method-invokers`" in Spring, such as that provided by the JMS namespace +for configuring message-driven POJOs. Basically, a `ref` attribute can point to any +Spring-managed object, and the `method` attribute provides the name of a method to be +invoked on that object. The following listing shows a simple example: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + + +---- + +The scheduler is referenced by the outer element, and each individual +task includes the configuration of its trigger metadata. In the preceding example, that +metadata defines a periodic trigger with a fixed delay indicating the number of +milliseconds to wait after each task execution has completed. Another option is +`fixed-rate`, indicating how often the method should be run regardless of how long +any previous execution takes. Additionally, for both `fixed-delay` and `fixed-rate` tasks, you can specify an +'initial-delay' parameter, indicating the number of milliseconds to wait +before the first execution of the method. For more control, you can instead provide a `cron` attribute +to provide a <>. +The following example shows these other options: + +[source,xml,indent=0] +[subs="verbatim"] +---- + + + + + + + +---- + + + +[[scheduling-cron-expression]] +=== Cron Expressions + +All Spring cron expressions have to conform to the same format, whether you are using them in +<>, +<>, +or someplace else. +A well-formed cron expression, such as `* * * * * *`, consists of six space-separated time and date +fields, each with its own range of valid values: + + +.... + ┌───────────── second (0-59) + │ ┌───────────── minute (0 - 59) + │ │ ┌───────────── hour (0 - 23) + │ │ │ ┌───────────── day of the month (1 - 31) + │ │ │ │ ┌───────────── month (1 - 12) (or JAN-DEC) + │ │ │ │ │ ┌───────────── day of the week (0 - 7) + │ │ │ │ │ │ (0 or 7 is Sunday, or MON-SUN) + │ │ │ │ │ │ + * * * * * * +.... + +There are some rules that apply: + +* A field may be an asterisk (`*`), which always stands for "`first-last`". +For the day-of-the-month or day-of-the-week fields, a question mark (`?`) may be used instead of an +asterisk. +* Commas (`,`) are used to separate items of a list. +* Two numbers separated with a hyphen (`-`) express a range of numbers. +The specified range is inclusive. +* Following a range (or `*`) with `/` specifies the interval of the number's value through the range. +* English names can also be used for the month and day-of-week fields. +Use the first three letters of the particular day or month (case does not matter). +* The day-of-month and day-of-week fields can contain a `L` character, which has a different meaning +** In the day-of-month field, `L` stands for _the last day of the month_. +If followed by a negative offset (that is, `L-n`), it means _``n``th-to-last day of the month_. +** In the day-of-week field, `L` stands for _the last day of the week_. +If prefixed by a number or three-letter name (`dL` or `DDDL`), it means _the last day of week (`d` +or `DDD`) in the month_. +* The day-of-month field can be `nW`, which stands for _the nearest weekday to day of the month ``n``_. +If `n` falls on Saturday, this yields the Friday before it. +If `n` falls on Sunday, this yields the Monday after, which also happens if `n` is `1` and falls on +a Saturday (that is: `1W` stands for _the first weekday of the month_). +* If the day-of-month field is `LW`, it means _the last weekday of the month_. +* The day-of-week field can be `d#n` (or `DDD#n`), which stands for _the ``n``th day of week `d` +(or ``DDD``) in the month_. + +Here are some examples: + +|=== +| Cron Expression | Meaning + +|`0 0 * * * *` | top of every hour of every day +|`*/10 * * * * *` | every ten seconds +| `0 0 8-10 * * *` | 8, 9 and 10 o'clock of every day +| `0 0 6,19 * * *` | 6:00 AM and 7:00 PM every day +| `0 0/30 8-10 * * *` | 8:00, 8:30, 9:00, 9:30, 10:00 and 10:30 every day +| `0 0 9-17 * * MON-FRI`| on the hour nine-to-five weekdays +| `0 0 0 25 DEC ?` | every Christmas Day at midnight +| `0 0 0 L * *` | last day of the month at midnight +| `0 0 0 L-3 * *` | third-to-last day of the month at midnight +| `0 0 0 * * 5L` | last Friday of the month at midnight +| `0 0 0 * * THUL` | last Thursday of the month at midnight +| `0 0 0 1W * *` | first weekday of the month at midnight +| `0 0 0 LW * *` | last weekday of the month at midnight +| `0 0 0 ? * 5#2` | the second Friday in the month at midnight +| `0 0 0 ? * MON#1` | the first Monday in the month at midnight +|=== + +==== Macros + +Expressions such as `0 0 * * * *` are hard for humans to parse and are, therefore, hard to fix in case of bugs. +To improve readability, Spring supports the following macros, which represent commonly used sequences. +You can use these macros instead of the six-digit value, thus: `@Scheduled(cron = "@hourly")`. + +|=== +|Macro | Meaning + +| `@yearly` (or `@annually`) | once a year (`0 0 0 1 1 *`) +| `@monthly` | once a month (`0 0 0 1 * *`) +| `@weekly` | once a week (`0 0 0 * * 0`) +| `@daily` (or `@midnight`) | once a day (`0 0 0 * * *`), or +| `@hourly` | once an hour, (`0 0 * * * *`) +|=== + + + +[[scheduling-quartz]] +=== Using the Quartz Scheduler + +Quartz uses `Trigger`, `Job`, and `JobDetail` objects to realize scheduling of all kinds +of jobs. For the basic concepts behind Quartz, see +https://www.quartz-scheduler.org/[]. For convenience purposes, Spring offers a couple of +classes that simplify using Quartz within Spring-based applications. + + +[[scheduling-quartz-jobdetail]] +==== Using the `JobDetailFactoryBean` + +Quartz `JobDetail` objects contain all the information needed to run a job. Spring provides a +`JobDetailFactoryBean`, which provides bean-style properties for XML configuration purposes. +Consider the following example: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + + + + + +---- + +The job detail configuration has all the information it needs to run the job (`ExampleJob`). +The timeout is specified in the job data map. The job data map is available through the +`JobExecutionContext` (passed to you at execution time), but the `JobDetail` also gets +its properties from the job data mapped to properties of the job instance. So, in the following example, +the `ExampleJob` contains a bean property named `timeout`, and the `JobDetail` +has it applied automatically: + +[source,java,indent=0] +[subs="verbatim"] +---- + package example; + + public class ExampleJob extends QuartzJobBean { + + private int timeout; + + /** + * Setter called after the ExampleJob is instantiated + * with the value from the JobDetailFactoryBean (5) + */ + public void setTimeout(int timeout) { + this.timeout = timeout; + } + + protected void executeInternal(JobExecutionContext ctx) throws JobExecutionException { + // do the actual work + } + } +---- + +All additional properties from the job data map are available to you as well. + +NOTE: By using the `name` and `group` properties, you can modify the name and the group +of the job, respectively. By default, the name of the job matches the bean name +of the `JobDetailFactoryBean` (`exampleJob` in the preceding example above). + + +[[scheduling-quartz-method-invoking-job]] +==== Using the `MethodInvokingJobDetailFactoryBean` + +Often you merely need to invoke a method on a specific object. By using the +`MethodInvokingJobDetailFactoryBean`, you can do exactly this, as the following example shows: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + +---- + +The preceding example results in the `doIt` method being called on the +`exampleBusinessObject` method, as the following example shows: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + public class ExampleBusinessObject { + + // properties and collaborators + + public void doIt() { + // do the actual work + } + } +---- + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + +---- + +By using the `MethodInvokingJobDetailFactoryBean`, you need not create one-line jobs +that merely invoke a method. You need only create the actual business object and +wire up the detail object. + +By default, Quartz Jobs are stateless, resulting in the possibility of jobs interfering +with each other. If you specify two triggers for the same `JobDetail`, it is +possible that, before the first job has finished, the second one starts. If +`JobDetail` classes implement the `Stateful` interface, this does not happen. The second +job does not start before the first one has finished. To make jobs resulting from the +`MethodInvokingJobDetailFactoryBean` be non-concurrent, set the `concurrent` flag to +`false`, as the following example shows: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + + +---- + +NOTE: By default, jobs will run in a concurrent fashion. + + +[[scheduling-quartz-cron]] +==== Wiring up Jobs by Using Triggers and `SchedulerFactoryBean` + +We have created job details and jobs. We have also reviewed the convenience bean that lets +you invoke a method on a specific object. Of course, we still need to schedule the +jobs themselves. This is done by using triggers and a `SchedulerFactoryBean`. Several +triggers are available within Quartz, and Spring offers two Quartz `FactoryBean` +implementations with convenient defaults: `CronTriggerFactoryBean` and +`SimpleTriggerFactoryBean`. + +Triggers need to be scheduled. Spring offers a `SchedulerFactoryBean` that exposes +triggers to be set as properties. `SchedulerFactoryBean` schedules the actual jobs with +those triggers. + +The following listing uses both a `SimpleTriggerFactoryBean` and a `CronTriggerFactoryBean`: + +[source,xml,indent=0] +[subs="verbatim"] +---- + + + + + + + + + + + + + + +---- + +The preceding example sets up two triggers, one running every 50 seconds with a starting delay of 10 +seconds and one running every morning at 6 AM. To finalize everything, we need to set up the +`SchedulerFactoryBean`, as the following example shows: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + + + + + +---- + +More properties are available for the `SchedulerFactoryBean`, such as the calendars used by the +job details, properties to customize Quartz with, and a Spring-provided JDBC DataSource. See +the {api-spring-framework}/scheduling/quartz/SchedulerFactoryBean.html[`SchedulerFactoryBean`] +javadoc for more information. + +NOTE: `SchedulerFactoryBean` also recognizes a `quartz.properties` file in the classpath, +based on Quartz property keys, as with regular Quartz configuration. Please note that many +`SchedulerFactoryBean` settings interact with common Quartz settings in the properties file; +it is therefore not recommended to specify values at both levels. For example, do not set +an "org.quartz.jobStore.class" property if you mean to rely on a Spring-provided DataSource, +or specify an `org.springframework.scheduling.quartz.LocalDataSourceJobStore` variant which +is a full-fledged replacement for the standard `org.quartz.impl.jdbcjobstore.JobStoreTX`. + + + + +[[cache]] +== Cache Abstraction + +Since version 3.1, the Spring Framework provides support for transparently adding caching to +an existing Spring application. Similar to the <> +support, the caching abstraction allows consistent use of various caching solutions with +minimal impact on the code. + +In Spring Framework 4.1, the cache abstraction was significantly extended with support +for <> and more customization options. + + + +[[cache-strategies]] +=== Understanding the Cache Abstraction + +.Cache vs Buffer +**** + +The terms, "`buffer`" and "`cache,`" tend to be used interchangeably. Note, however, +that they represent different things. Traditionally, a buffer is used as an intermediate +temporary store for data between a fast and a slow entity. As one party would have to wait +for the other (which affects performance), the buffer alleviates this by allowing entire +blocks of data to move at once rather than in small chunks. The data is written and read +only once from the buffer. Furthermore, the buffers are visible to at least one party +that is aware of it. + +A cache, on the other hand, is, by definition, hidden, and neither party is aware that +caching occurs. It also improves performance but does so by letting the same data be +read multiple times in a fast fashion. + +You can find a further explanation of the differences between a buffer and a cache +https://en.wikipedia.org/wiki/Cache_(computing)#The_difference_between_buffer_and_cache[here]. +**** + +At its core, the cache abstraction applies caching to Java methods, thus reducing the +number of executions based on the information available in the cache. That is, each time +a targeted method is invoked, the abstraction applies a caching behavior that checks +whether the method has been already invoked for the given arguments. If it has been +invoked, the cached result is returned without having to invoke the actual method. +If the method has not been invoked, then it is invoked, and the result is cached and +returned to the user so that, the next time the method is invoked, the cached result is +returned. This way, expensive methods (whether CPU- or IO-bound) can be invoked only +once for a given set of parameters and the result reused without having to actually +invoke the method again. The caching logic is applied transparently without any +interference to the invoker. + +IMPORTANT: This approach works only for methods that are guaranteed to return the same +output (result) for a given input (or arguments) no matter how many times they are invoked. + +The caching abstraction provides other cache-related operations, such as the ability +to update the content of the cache or to remove one or all entries. These are useful if +the cache deals with data that can change during the course of the application. + +As with other services in the Spring Framework, the caching service is an abstraction +(not a cache implementation) and requires the use of actual storage to store the cache data -- +that is, the abstraction frees you from having to write the caching logic but does not +provide the actual data store. This abstraction is materialized by the +`org.springframework.cache.Cache` and `org.springframework.cache.CacheManager` interfaces. + +Spring provides <> of that abstraction: +JDK `java.util.concurrent.ConcurrentMap` based caches, Gemfire cache, +https://github.com/ben-manes/caffeine/wiki[Caffeine], and JSR-107 compliant caches (such +as Ehcache 3.x). See <> for more information on plugging in other cache +stores and providers. + +IMPORTANT: The caching abstraction has no special handling for multi-threaded and +multi-process environments, as such features are handled by the cache implementation. + +If you have a multi-process environment (that is, an application deployed on several nodes), +you need to configure your cache provider accordingly. Depending on your use cases, a copy +of the same data on several nodes can be enough. However, if you change the data during +the course of the application, you may need to enable other propagation mechanisms. + +Caching a particular item is a direct equivalent of the typical +get-if-not-found-then-proceed-and-put-eventually code blocks +found with programmatic cache interaction. +No locks are applied, and several threads may try to load the same item concurrently. +The same applies to eviction. If several threads are trying to update or evict data +concurrently, you may use stale data. Certain cache providers offer advanced features +in that area. See the documentation of your cache provider for more details. + +To use the cache abstraction, you need to take care of two aspects: + +* Caching declaration: Identify the methods that need to be cached and their policies. +* Cache configuration: The backing cache where the data is stored and from which it is read. + + + +[[cache-annotations]] +=== Declarative Annotation-based Caching + +For caching declaration, Spring's caching abstraction provides a set of Java annotations: + +* `@Cacheable`: Triggers cache population. +* `@CacheEvict`: Triggers cache eviction. +* `@CachePut`: Updates the cache without interfering with the method execution. +* `@Caching`: Regroups multiple cache operations to be applied on a method. +* `@CacheConfig`: Shares some common cache-related settings at class-level. + + +[[cache-annotations-cacheable]] +==== The `@Cacheable` Annotation + +As the name implies, you can use `@Cacheable` to demarcate methods that are cacheable -- +that is, methods for which the result is stored in the cache so that, on subsequent +invocations (with the same arguments), the value in the cache is returned without +having to actually invoke the method. In its simplest form, the annotation declaration +requires the name of the cache associated with the annotated method, as the following +example shows: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Cacheable("books") + public Book findBook(ISBN isbn) {...} +---- + +In the preceding snippet, the `findBook` method is associated with the cache named `books`. +Each time the method is called, the cache is checked to see whether the invocation has +already been run and does not have to be repeated. While in most cases, only one +cache is declared, the annotation lets multiple names be specified so that more than one +cache is being used. In this case, each of the caches is checked before invoking the +method -- if at least one cache is hit, the associated value is returned. + +NOTE: All the other caches that do not contain the value are also updated, even though +the cached method was not actually invoked. + +The following example uses `@Cacheable` on the `findBook` method with multiple caches: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Cacheable({"books", "isbns"}) + public Book findBook(ISBN isbn) {...} +---- + +[[cache-annotations-cacheable-default-key]] +===== Default Key Generation + +Since caches are essentially key-value stores, each invocation of a cached method +needs to be translated into a suitable key for cache access. The caching abstraction +uses a simple `KeyGenerator` based on the following algorithm: + +* If no params are given, return `SimpleKey.EMPTY`. +* If only one param is given, return that instance. +* If more than one param is given, return a `SimpleKey` that contains all parameters. + +This approach works well for most use-cases, as long as parameters have natural keys +and implement valid `hashCode()` and `equals()` methods. If that is not the case, +you need to change the strategy. + +To provide a different default key generator, you need to implement the +`org.springframework.cache.interceptor.KeyGenerator` interface. + +[NOTE] +==== +The default key generation strategy changed with the release of Spring 4.0. Earlier +versions of Spring used a key generation strategy that, for multiple key parameters, +considered only the `hashCode()` of parameters and not `equals()`. This could cause +unexpected key collisions (see https://jira.spring.io/browse/SPR-10237[SPR-10237] +for background). The new `SimpleKeyGenerator` uses a compound key for such scenarios. + +If you want to keep using the previous key strategy, you can configure the deprecated +`org.springframework.cache.interceptor.DefaultKeyGenerator` class or create a custom +hash-based `KeyGenerator` implementation. +==== + +[[cache-annotations-cacheable-key]] +===== Custom Key Generation Declaration + +Since caching is generic, the target methods are quite likely to have various signatures +that cannot be readily mapped on top of the cache structure. This tends to become obvious +when the target method has multiple arguments out of which only some are suitable for +caching (while the rest are used only by the method logic). Consider the following example: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Cacheable("books") + public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed) +---- + +At first glance, while the two `boolean` arguments influence the way the book is found, +they are no use for the cache. Furthermore, what if only one of the two is important +while the other is not? + +For such cases, the `@Cacheable` annotation lets you specify how the key is generated +through its `key` attribute. You can use <> to pick the +arguments of interest (or their nested properties), perform operations, or even +invoke arbitrary methods without having to write any code or implement any interface. +This is the recommended approach over the +<>, since methods tend to be +quite different in signatures as the code base grows. While the default strategy might +work for some methods, it rarely works for all methods. + +The following examples use various SpEL declarations (if you are not familiar with SpEL, +do yourself a favor and read <>): + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Cacheable(cacheNames="books", key="#isbn") + public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed) + + @Cacheable(cacheNames="books", key="#isbn.rawNumber") + public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed) + + @Cacheable(cacheNames="books", key="T(someType).hash(#isbn)") + public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed) +---- + +The preceding snippets show how easy it is to select a certain argument, one of its +properties, or even an arbitrary (static) method. + +If the algorithm responsible for generating the key is too specific or if it needs +to be shared, you can define a custom `keyGenerator` on the operation. To do so, +specify the name of the `KeyGenerator` bean implementation to use, as the following +example shows: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Cacheable(cacheNames="books", keyGenerator="myKeyGenerator") + public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed) +---- + +NOTE: The `key` and `keyGenerator` parameters are mutually exclusive and an operation +that specifies both results in an exception. + +[[cache-annotations-cacheable-default-cache-resolver]] +===== Default Cache Resolution + +The caching abstraction uses a simple `CacheResolver` that +retrieves the caches defined at the operation level by using the configured +`CacheManager`. + +To provide a different default cache resolver, you need to implement the +`org.springframework.cache.interceptor.CacheResolver` interface. + +[[cache-annotations-cacheable-cache-resolver]] +===== Custom Cache Resolution + +The default cache resolution fits well for applications that work with a +single `CacheManager` and have no complex cache resolution requirements. + +For applications that work with several cache managers, you can set the +`cacheManager` to use for each operation, as the following example shows: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Cacheable(cacheNames="books", cacheManager="anotherCacheManager") <1> + public Book findBook(ISBN isbn) {...} +---- +<1> Specifying `anotherCacheManager`. + + +You can also replace the `CacheResolver` entirely in a fashion similar to that of +replacing <>. The resolution is +requested for every cache operation, letting the implementation actually resolve +the caches to use based on runtime arguments. The following example shows how to +specify a `CacheResolver`: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Cacheable(cacheResolver="runtimeCacheResolver") <1> + public Book findBook(ISBN isbn) {...} +---- +<1> Specifying the `CacheResolver`. + + +[NOTE] +==== +Since Spring 4.1, the `value` attribute of the cache annotations are no longer +mandatory, since this particular information can be provided by the `CacheResolver` +regardless of the content of the annotation. + +Similarly to `key` and `keyGenerator`, the `cacheManager` and `cacheResolver` +parameters are mutually exclusive, and an operation specifying both +results in an exception, as a custom `CacheManager` is ignored by the +`CacheResolver` implementation. This is probably not what you expect. +==== + +[[cache-annotations-cacheable-synchronized]] +===== Synchronized Caching + +In a multi-threaded environment, certain operations might be concurrently invoked for +the same argument (typically on startup). By default, the cache abstraction does not +lock anything, and the same value may be computed several times, defeating the purpose +of caching. + +For those particular cases, you can use the `sync` attribute to instruct the underlying +cache provider to lock the cache entry while the value is being computed. As a result, +only one thread is busy computing the value, while the others are blocked until the entry +is updated in the cache. The following example shows how to use the `sync` attribute: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Cacheable(cacheNames="foos", sync=true) <1> + public Foo executeExpensiveOperation(String id) {...} +---- +<1> Using the `sync` attribute. + +NOTE: This is an optional feature, and your favorite cache library may not support it. +All `CacheManager` implementations provided by the core framework support it. See the +documentation of your cache provider for more details. + +[[cache-annotations-cacheable-condition]] +===== Conditional Caching + +Sometimes, a method might not be suitable for caching all the time (for example, it might +depend on the given arguments). The cache annotations support such use cases through the +`condition` parameter, which takes a `SpEL` expression that is evaluated to either `true` +or `false`. If `true`, the method is cached. If not, it behaves as if the method is not +cached (that is, the method is invoked every time no matter what values are in the cache +or what arguments are used). For example, the following method is cached only if the +argument `name` has a length shorter than 32: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Cacheable(cacheNames="book", condition="#name.length() < 32") <1> + public Book findBook(String name) +---- +<1> Setting a condition on `@Cacheable`. + + +In addition to the `condition` parameter, you can use the `unless` parameter to veto the +adding of a value to the cache. Unlike `condition`, `unless` expressions are evaluated +after the method has been invoked. To expand on the previous example, perhaps we only +want to cache paperback books, as the following example does: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result.hardback") <1> + public Book findBook(String name) +---- +<1> Using the `unless` attribute to block hardbacks. + + +The cache abstraction supports `java.util.Optional` return types. If an `Optional` value +is _present_, it will be stored in the associated cache. If an `Optional` value is not +present, `null` will be stored in the associated cache. `#result` always refers to the +business entity and never a supported wrapper, so the previous example can be rewritten +as follows: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result?.hardback") + public Optional findBook(String name) +---- + +Note that `#result` still refers to `Book` and not `Optional`. Since it might be +`null`, we use SpEL's <>. + +[[cache-spel-context]] +===== Available Caching SpEL Evaluation Context + +Each `SpEL` expression evaluates against a dedicated <>. +In addition to the built-in parameters, the framework provides dedicated caching-related +metadata, such as the argument names. The following table describes the items made +available to the context so that you can use them for key and conditional computations: + +[[cache-spel-context-tbl]] +.Cache SpEL available metadata +|=== +| Name| Location| Description| Example + +| `methodName` +| Root object +| The name of the method being invoked +| `#root.methodName` + +| `method` +| Root object +| The method being invoked +| `#root.method.name` + +| `target` +| Root object +| The target object being invoked +| `#root.target` + +| `targetClass` +| Root object +| The class of the target being invoked +| `#root.targetClass` + +| `args` +| Root object +| The arguments (as array) used for invoking the target +| `#root.args[0]` + +| `caches` +| Root object +| Collection of caches against which the current method is run +| `#root.caches[0].name` + +| Argument name +| Evaluation context +| Name of any of the method arguments. If the names are not available + (perhaps due to having no debug information), the argument names are also available under the `#a<#arg>` + where `#arg` stands for the argument index (starting from `0`). +| `#iban` or `#a0` (you can also use `#p0` or `#p<#arg>` notation as an alias). + +| `result` +| Evaluation context +| The result of the method call (the value to be cached). Only available in `unless` + expressions, `cache put` expressions (to compute the `key`), or `cache evict` + expressions (when `beforeInvocation` is `false`). For supported wrappers (such as + `Optional`), `#result` refers to the actual object, not the wrapper. +| `#result` +|=== + + +[[cache-annotations-put]] +==== The `@CachePut` Annotation + +When the cache needs to be updated without interfering with the method execution, +you can use the `@CachePut` annotation. That is, the method is always invoked and its +result is placed into the cache (according to the `@CachePut` options). It supports +the same options as `@Cacheable` and should be used for cache population rather than +method flow optimization. The following example uses the `@CachePut` annotation: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @CachePut(cacheNames="book", key="#isbn") + public Book updateBook(ISBN isbn, BookDescriptor descriptor) +---- + +IMPORTANT: Using `@CachePut` and `@Cacheable` annotations on the same method is generally +strongly discouraged because they have different behaviors. While the latter causes the +method invocation to be skipped by using the cache, the former forces the invocation in +order to run a cache update. This leads to unexpected behavior and, with the exception +of specific corner-cases (such as annotations having conditions that exclude them from each +other), such declarations should be avoided. Note also that such conditions should not rely +on the result object (that is, the `#result` variable), as these are validated up-front to +confirm the exclusion. + + +[[cache-annotations-evict]] +==== The `@CacheEvict` annotation + +The cache abstraction allows not just population of a cache store but also eviction. +This process is useful for removing stale or unused data from the cache. As opposed to +`@Cacheable`, `@CacheEvict` demarcates methods that perform cache +eviction (that is, methods that act as triggers for removing data from the cache). +Similarly to its sibling, `@CacheEvict` requires specifying one or more caches +that are affected by the action, allows a custom cache and key resolution or a +condition to be specified, and features an extra parameter +(`allEntries`) that indicates whether a cache-wide eviction needs to be performed +rather than just an entry eviction (based on the key). The following example evicts +all entries from the `books` cache: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @CacheEvict(cacheNames="books", allEntries=true) <1> + public void loadBooks(InputStream batch) +---- +<1> Using the `allEntries` attribute to evict all entries from the cache. + +This option comes in handy when an entire cache region needs to be cleared out. +Rather than evicting each entry (which would take a long time, since it is inefficient), +all the entries are removed in one operation, as the preceding example shows. +Note that the framework ignores any key specified in this scenario as it does not apply +(the entire cache is evicted, not only one entry). + +You can also indicate whether the eviction should occur after (the default) or before +the method is invoked by using the `beforeInvocation` attribute. The former provides the +same semantics as the rest of the annotations: Once the method completes successfully, +an action (in this case, eviction) on the cache is run. If the method does not +run (as it might be cached) or an exception is thrown, the eviction does not occur. +The latter (`beforeInvocation=true`) causes the eviction to always occur before the +method is invoked. This is useful in cases where the eviction does not need to be tied +to the method outcome. + +Note that `void` methods can be used with `@CacheEvict` - as the methods act as a +trigger, the return values are ignored (as they do not interact with the cache). This is +not the case with `@Cacheable` which adds data to the cache or updates data in the cache +and, thus, requires a result. + + +[[cache-annotations-caching]] +==== The `@Caching` Annotation + +Sometimes, multiple annotations of the same type (such as `@CacheEvict` or +`@CachePut`) need to be specified -- for example, because the condition or the key +expression is different between different caches. `@Caching` lets multiple nested +`@Cacheable`, `@CachePut`, and `@CacheEvict` annotations be used on the same method. +The following example uses two `@CacheEvict` annotations: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames="secondary", key="#p0") }) + public Book importBooks(String deposit, Date date) +---- + + +[[cache-annotations-config]] +==== The `@CacheConfig` annotation + +So far, we have seen that caching operations offer many customization options and that +you can set these options for each operation. However, some of the customization options +can be tedious to configure if they apply to all operations of the class. For +instance, specifying the name of the cache to use for every cache operation of the +class can be replaced by a single class-level definition. This is where `@CacheConfig` +comes into play. The following examples uses `@CacheConfig` to set the name of the cache: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @CacheConfig("books") <1> + public class BookRepositoryImpl implements BookRepository { + + @Cacheable + public Book findBook(ISBN isbn) {...} + } +---- +<1> Using `@CacheConfig` to set the name of the cache. + +`@CacheConfig` is a class-level annotation that allows sharing the cache names, +the custom `KeyGenerator`, the custom `CacheManager`, and the custom `CacheResolver`. +Placing this annotation on the class does not turn on any caching operation. + +An operation-level customization always overrides a customization set on `@CacheConfig`. +Therefore, this gives three levels of customizations for each cache operation: + +* Globally configured, available for `CacheManager`, `KeyGenerator`. +* At the class level, using `@CacheConfig`. +* At the operation level. + + +[[cache-annotation-enable]] +==== Enabling Caching Annotations + +It is important to note that even though declaring the cache annotations does not +automatically trigger their actions - like many things in Spring, the feature has to be +declaratively enabled (which means if you ever suspect caching is to blame, you can +disable it by removing only one configuration line rather than all the annotations in +your code). + +To enable caching annotations add the annotation `@EnableCaching` to one of your +`@Configuration` classes: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Configuration + @EnableCaching + public class AppConfig { + } +---- + +Alternatively, for XML configuration you can use the `cache:annotation-driven` element: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + +---- + +Both the `cache:annotation-driven` element and the `@EnableCaching` annotation let you +specify various options that influence the way the caching behavior is added to the +application through AOP. The configuration is intentionally similar with that of +<>. + +NOTE: The default advice mode for processing caching annotations is `proxy`, which allows +for interception of calls through the proxy only. Local calls within the same class +cannot get intercepted that way. For a more advanced mode of interception, consider +switching to `aspectj` mode in combination with compile-time or load-time weaving. + +NOTE: For more detail about advanced customizations (using Java configuration) that are +required to implement `CachingConfigurer`, see the +{api-spring-framework}/cache/annotation/CachingConfigurer.html[javadoc]. + +[[cache-annotation-driven-settings]] +.Cache annotation settings +[cols="1,1,1,3"] +|=== +| XML Attribute | Annotation Attribute | Default | Description + +| `cache-manager` +| N/A (see the {api-spring-framework}/cache/annotation/CachingConfigurer.html[`CachingConfigurer`] javadoc) +| `cacheManager` +| The name of the cache manager to use. A default `CacheResolver` is initialized behind + the scenes with this cache manager (or `cacheManager` if not set). For more + fine-grained management of the cache resolution, consider setting the 'cache-resolver' + attribute. + +| `cache-resolver` +| N/A (see the {api-spring-framework}/cache/annotation/CachingConfigurer.html[`CachingConfigurer`] javadoc) +| A `SimpleCacheResolver` using the configured `cacheManager`. +| The bean name of the CacheResolver that is to be used to resolve the backing caches. + This attribute is not required and needs to be specified only as an alternative to + the 'cache-manager' attribute. + +| `key-generator` +| N/A (see the {api-spring-framework}/cache/annotation/CachingConfigurer.html[`CachingConfigurer`] javadoc) +| `SimpleKeyGenerator` +| Name of the custom key generator to use. + +| `error-handler` +| N/A (see the {api-spring-framework}/cache/annotation/CachingConfigurer.html[`CachingConfigurer`] javadoc) +| `SimpleCacheErrorHandler` +| The name of the custom cache error handler to use. By default, any exception thrown during + a cache related operation is thrown back at the client. + +| `mode` +| `mode` +| `proxy` +| The default mode (`proxy`) processes annotated beans to be proxied by using Spring's AOP + framework (following proxy semantics, as discussed earlier, applying to method calls + coming in through the proxy only). The alternative mode (`aspectj`) instead weaves the + affected classes with Spring's AspectJ caching aspect, modifying the target class byte + code to apply to any kind of method call. AspectJ weaving requires `spring-aspects.jar` + in the classpath as well as load-time weaving (or compile-time weaving) enabled. (See + <> for details on how to set up + load-time weaving.) + +| `proxy-target-class` +| `proxyTargetClass` +| `false` +| Applies to proxy mode only. Controls what type of caching proxies are created for + classes annotated with the `@Cacheable` or `@CacheEvict` annotations. If the + `proxy-target-class` attribute is set to `true`, class-based proxies are created. + If `proxy-target-class` is `false` or if the attribute is omitted, standard JDK + interface-based proxies are created. (See <> + for a detailed examination of the different proxy types.) + +| `order` +| `order` +| Ordered.LOWEST_PRECEDENCE +| Defines the order of the cache advice that is applied to beans annotated with + `@Cacheable` or `@CacheEvict`. (For more information about the rules related to + ordering AOP advice, see <>.) + No specified ordering means that the AOP subsystem determines the order of the advice. +|=== + +NOTE: `` looks for `@Cacheable/@CachePut/@CacheEvict/@Caching` +only on beans in the same application context in which it is defined. This means that, +if you put `` in a `WebApplicationContext` for a +`DispatcherServlet`, it checks for beans only in your controllers, not your services. +See <> for more information. + +.Method visibility and cache annotations +**** +When you use proxies, you should apply the cache annotations only to methods with +public visibility. If you do annotate protected, private, or package-visible methods +with these annotations, no error is raised, but the annotated method does not exhibit +the configured caching settings. Consider using AspectJ (see the rest of this section) +if you need to annotate non-public methods, as it changes the bytecode itself. +**** + +TIP: Spring recommends that you only annotate concrete classes (and methods of concrete +classes) with the `@Cache{asterisk}` annotations, as opposed to annotating interfaces. +You certainly can place an `@Cache{asterisk}` annotation on an interface (or an interface +method), but this works only if you use the proxy mode (`mode="proxy"`). If you use the +weaving-based aspect (`mode="aspectj"`), the caching settings are not recognized on +interface-level declarations by the weaving infrastructure. + +NOTE: In proxy mode (the default), only external method calls coming in through the +proxy are intercepted. This means that self-invocation (in effect, a method within the +target object that calls another method of the target object) does not lead to actual +caching at runtime even if the invoked method is marked with `@Cacheable`. Consider +using the `aspectj` mode in this case. Also, the proxy must be fully initialized to +provide the expected behavior, so you should not rely on this feature in your +initialization code (that is, `@PostConstruct`). + + +[[cache-annotation-stereotype]] +==== Using Custom Annotations + +.Custom annotation and AspectJ +**** +This feature works only with the proxy-based approach but can be enabled +with a bit of extra effort by using AspectJ. + +The `spring-aspects` module defines an aspect for the standard annotations only. +If you have defined your own annotations, you also need to define an aspect for +those. Check `AnnotationCacheAspect` for an example. +**** + +The caching abstraction lets you use your own annotations to identify what method +triggers cache population or eviction. This is quite handy as a template mechanism, +as it eliminates the need to duplicate cache annotation declarations, which is +especially useful if the key or condition are specified or if the foreign imports +(`org.springframework`) are not allowed in your code base. Similarly to the rest +of the <> annotations, you can +use `@Cacheable`, `@CachePut`, `@CacheEvict`, and `@CacheConfig` as +<> (that is, annotations that +can annotate other annotations). In the following example, we replace a common +`@Cacheable` declaration with our own custom annotation: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.METHOD}) + @Cacheable(cacheNames="books", key="#isbn") + public @interface SlowService { + } +---- + +In the preceding example, we have defined our own `SlowService` annotation, +which itself is annotated with `@Cacheable`. Now we can replace the following code: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Cacheable(cacheNames="books", key="#isbn") + public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed) +---- + +The following example shows the custom annotation with which we can replace the +preceding code: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @SlowService + public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed) +---- + +Even though `@SlowService` is not a Spring annotation, the container automatically picks +up its declaration at runtime and understands its meaning. Note that, as mentioned +<>, annotation-driven behavior needs to be enabled. + + + +[[cache-jsr-107]] +=== JCache (JSR-107) Annotations + +Since version 4.1, Spring's caching abstraction fully supports the JCache standard +(JSR-107) annotations: `@CacheResult`, `@CachePut`, `@CacheRemove`, and `@CacheRemoveAll` +as well as the `@CacheDefaults`, `@CacheKey`, and `@CacheValue` companions. +You can use these annotations even without migrating your cache store to JSR-107. +The internal implementation uses Spring's caching abstraction and provides default +`CacheResolver` and `KeyGenerator` implementations that are compliant with the +specification. In other words, if you are already using Spring's caching abstraction, +you can switch to these standard annotations without changing your cache storage +(or configuration, for that matter). + + +[[cache-jsr-107-summary]] +==== Feature Summary + +For those who are familiar with Spring's caching annotations, the following table +describes the main differences between the Spring annotations and their JSR-107 +counterparts: + +.Spring vs. JSR-107 caching annotations +[cols="1,1,3"] +|=== +| Spring | JSR-107 | Remark + +| `@Cacheable` +| `@CacheResult` +| Fairly similar. `@CacheResult` can cache specific exceptions and force the + execution of the method regardless of the content of the cache. + +| `@CachePut` +| `@CachePut` +| While Spring updates the cache with the result of the method invocation, JCache + requires that it be passed it as an argument that is annotated with `@CacheValue`. + Due to this difference, JCache allows updating the cache before or after the + actual method invocation. + +| `@CacheEvict` +| `@CacheRemove` +| Fairly similar. `@CacheRemove` supports conditional eviction when the + method invocation results in an exception. + +| `@CacheEvict(allEntries=true)` +| `@CacheRemoveAll` +| See `@CacheRemove`. + +| `@CacheConfig` +| `@CacheDefaults` +| Lets you configure the same concepts, in a similar fashion. +|=== + +JCache has the notion of `javax.cache.annotation.CacheResolver`, which is identical +to the Spring's `CacheResolver` interface, except that JCache supports only a single +cache. By default, a simple implementation retrieves the cache to use based on the +name declared on the annotation. It should be noted that, if no cache name is +specified on the annotation, a default is automatically generated. See the javadoc +of `@CacheResult#cacheName()` for more information. + +`CacheResolver` instances are retrieved by a `CacheResolverFactory`. It is possible +to customize the factory for each cache operation, as the following example shows: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @CacheResult(cacheNames="books", cacheResolverFactory=MyCacheResolverFactory.class) <1> + public Book findBook(ISBN isbn) +---- +<1> Customizing the factory for this operation. + +NOTE: For all referenced classes, Spring tries to locate a bean with the given type. +If more than one match exists, a new instance is created and can use the regular +bean lifecycle callbacks, such as dependency injection. + +Keys are generated by a `javax.cache.annotation.CacheKeyGenerator` that serves the +same purpose as Spring's `KeyGenerator`. By default, all method arguments are taken +into account, unless at least one parameter is annotated with `@CacheKey`. This is +similar to Spring's <>. For instance, the following are identical operations, one using +Spring's abstraction and the other using JCache: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Cacheable(cacheNames="books", key="#isbn") + public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed) + + @CacheResult(cacheName="books") + public Book findBook(@CacheKey ISBN isbn, boolean checkWarehouse, boolean includeUsed) +---- + +You can also specify the `CacheKeyResolver` on the operation, similar to how you can +specify the `CacheResolverFactory`. + +JCache can manage exceptions thrown by annotated methods. This can prevent an update of +the cache, but it can also cache the exception as an indicator of the failure instead of +calling the method again. Assume that `InvalidIsbnNotFoundException` is thrown if the +structure of the ISBN is invalid. This is a permanent failure (no book could ever be +retrieved with such a parameter). The following caches the exception so that further +calls with the same, invalid, ISBN throw the cached exception directly instead of +invoking the method again: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @CacheResult(cacheName="books", exceptionCacheName="failures" + cachedExceptions = InvalidIsbnNotFoundException.class) + public Book findBook(ISBN isbn) +---- + + +==== Enabling JSR-107 Support + +You do not need to do anything specific to enable the JSR-107 support alongside Spring's +declarative annotation support. Both `@EnableCaching` and the `cache:annotation-driven` +XML element automatically enable the JCache support if both the JSR-107 API and the +`spring-context-support` module are present in the classpath. + +NOTE: Depending on your use case, the choice is basically yours. You can even mix and +match services by using the JSR-107 API on some and using Spring's own annotations on +others. However, if these services impact the same caches, you should use a consistent +and identical key generation implementation. + + + +[[cache-declarative-xml]] +=== Declarative XML-based Caching + +If annotations are not an option (perhaps due to having no access to the sources +or no external code), you can use XML for declarative caching. So, instead of +annotating the methods for caching, you can specify the target method and the +caching directives externally (similar to the declarative transaction management +<>). The example +from the previous section can be translated into the following example: + +[source,xml,indent=0] +[subs="verbatim"] +---- + + + + + + + + + + + + + + + + + +---- + +In the preceding configuration, the `bookService` is made cacheable. The caching semantics +to apply are encapsulated in the `cache:advice` definition, which causes the `findBooks` +method to be used for putting data into the cache and the `loadBooks` method for evicting +data. Both definitions work against the `books` cache. + +The `aop:config` definition applies the cache advice to the appropriate points in the +program by using the AspectJ pointcut expression (more information is available in +<>). In the preceding example, +all methods from the `BookService` are considered and the cache advice is applied to them. + +The declarative XML caching supports all of the annotation-based model, so moving between +the two should be fairly easy. Furthermore, both can be used inside the same application. +The XML-based approach does not touch the target code. However, it is inherently more +verbose. When dealing with classes that have overloaded methods that are targeted for +caching, identifying the proper methods does take an extra effort, since the `method` +argument is not a good discriminator. In these cases, you can use the AspectJ pointcut +to cherry pick the target methods and apply the appropriate caching functionality. +However, through XML, it is easier to apply package or group or interface-wide caching +(again, due to the AspectJ pointcut) and to create template-like definitions (as we did +in the preceding example by defining the target cache through the `cache:definitions` +`cache` attribute). + + + +[[cache-store-configuration]] +=== Configuring the Cache Storage + +The cache abstraction provides several storage integration options. To use them, you need +to declare an appropriate `CacheManager` (an entity that controls and manages `Cache` +instances and that can be used to retrieve these for storage). + + +[[cache-store-configuration-jdk]] +==== JDK `ConcurrentMap`-based Cache + +The JDK-based `Cache` implementation resides under +`org.springframework.cache.concurrent` package. It lets you use `ConcurrentHashMap` +as a backing `Cache` store. The following example shows how to configure two caches: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + + + + + + +---- + +The preceding snippet uses the `SimpleCacheManager` to create a `CacheManager` for the +two nested `ConcurrentMapCache` instances named `default` and `books`. Note that the +names are configured directly for each cache. + +As the cache is created by the application, it is bound to its lifecycle, making it +suitable for basic use cases, tests, or simple applications. The cache scales well +and is very fast, but it does not provide any management, persistence capabilities, +or eviction contracts. + + +[[cache-store-configuration-eviction]] +==== Ehcache-based Cache + +Ehcache 3.x is fully JSR-107 compliant and no dedicated support is required for it. See +<> for details. + + +[[cache-store-configuration-caffeine]] +==== Caffeine Cache + +Caffeine is a Java 8 rewrite of Guava's cache, and its implementation is located in the +`org.springframework.cache.caffeine` package and provides access to several features +of Caffeine. + +The following example configures a `CacheManager` that creates the cache on demand: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + +---- + +You can also provide the caches to use explicitly. In that case, only those +are made available by the manager. The following example shows how to do so: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + default + books + + + +---- + +The Caffeine `CacheManager` also supports custom `Caffeine` and `CacheLoader`. +See the https://github.com/ben-manes/caffeine/wiki[Caffeine documentation] +for more information about those. + + +[[cache-store-configuration-gemfire]] +==== GemFire-based Cache + +GemFire is a memory-oriented, disk-backed, elastically scalable, continuously available, +active (with built-in pattern-based subscription notifications), globally replicated +database and provides fully-featured edge caching. For further information on how to +use GemFire as a `CacheManager` (and more), see the +{doc-spring-gemfire}/html/[Spring Data GemFire reference documentation]. + + +[[cache-store-configuration-jsr107]] +==== JSR-107 Cache + +Spring's caching abstraction can also use JSR-107-compliant caches. The JCache +implementation is located in the `org.springframework.cache.jcache` package. + +Again, to use it, you need to declare the appropriate `CacheManager`. +The following example shows how to do so: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + +---- + + +[[cache-store-configuration-noop]] +==== Dealing with Caches without a Backing Store + +Sometimes, when switching environments or doing testing, you might have cache +declarations without having an actual backing cache configured. As this is an invalid +configuration, an exception is thrown at runtime, since the caching infrastructure +is unable to find a suitable store. In situations like this, rather than removing the +cache declarations (which can prove tedious), you can wire in a simple dummy cache that +performs no caching -- that is, it forces the cached methods to be invoked every time. +The following example shows how to do so: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + + + + + + +---- + +The `CompositeCacheManager` in the preceding chains multiple `CacheManager` instances and, +through the `fallbackToNoOpCache` flag, adds a no-op cache for all the definitions not +handled by the configured cache managers. That is, every cache definition not found in +either `jdkCache` or `gemfireCache` (configured earlier in the example) is handled by +the no-op cache, which does not store any information, causing the target method to be +invoked every time. + + + +[[cache-plug]] +=== Plugging-in Different Back-end Caches + +Clearly, there are plenty of caching products out there that you can use as a backing +store. For those that do not support JSR-107 you need to provide a `CacheManager` and a +`Cache` implementation. This may sound harder than it is, since, in practice, the classes +tend to be simple https://en.wikipedia.org/wiki/Adapter_pattern[adapters] that map the +caching abstraction framework on top of the storage API, as the _Caffeine_ classes do. +Most `CacheManager` classes can use the classes in the +`org.springframework.cache.support` package (such as `AbstractCacheManager` which takes +care of the boiler-plate code, leaving only the actual mapping to be completed). + + + +[[cache-specific-config]] +=== How can I Set the TTL/TTI/Eviction policy/XXX feature? + +Directly through your cache provider. The cache abstraction is an abstraction, +not a cache implementation. The solution you use might support various data +policies and different topologies that other solutions do not support (for example, +the JDK `ConcurrentHashMap` -- exposing that in the cache abstraction would be useless +because there would no backing support). Such functionality should be controlled +directly through the backing cache (when configuring it) or through its native API. + + + + +include::integration/integration-appendix.adoc[leveloffset=+1] diff --git a/src/docs/asciidoc/integration/integration-appendix.adoc b/framework-docs/src/docs/asciidoc/integration/integration-appendix.adoc similarity index 86% rename from src/docs/asciidoc/integration/integration-appendix.adoc rename to framework-docs/src/docs/asciidoc/integration/integration-appendix.adoc index 19e115d2e0ed..4b71d758733d 100644 --- a/src/docs/asciidoc/integration/integration-appendix.adoc +++ b/framework-docs/src/docs/asciidoc/integration/integration-appendix.adoc @@ -1,27 +1,27 @@ +[[integration.appendix]] = Appendix -[[xsd-schemas]] +[[integration.appendix.xsd-schemas]] == XML Schemas This part of the appendix lists XML schemas related to integration technologies. -[[xsd-schemas-jee]] +[[integration.appendix.xsd-schemas-jee]] === The `jee` Schema -The `jee` elements deal with issues related to Java EE (Java Enterprise Edition) configuration, +The `jee` elements deal with issues related to Jakarta EE (Enterprise Edition) configuration, such as looking up a JNDI object and defining EJB references. To use the elements in the `jee` schema, you need to have the following preamble at the top of your Spring XML configuration file. The text in the following snippet references the correct schema so that the elements in the `jee` namespace are available to you: -[source,xml,indent=0] -[subs="verbatim,quotes"] +[source,xml,indent=0,subs="verbatim,quotes"] ---- (simple) The following example shows how to use JNDI to look up a data source without the `jee` schema: -[source,xml,indent=0] -[subs="verbatim,quotes"] +[source,xml,indent=0,subs="verbatim,quotes"] ---- @@ -58,8 +57,7 @@ The following example shows how to use JNDI to look up a data source without the The following example shows how to use JNDI to look up a data source with the `jee` schema: -[source,xml,indent=0] -[subs="verbatim,quotes"] +[source,xml,indent=0,subs="verbatim,quotes"] ---- @@ -71,14 +69,13 @@ schema: -[[xsd-schemas-jee-jndi-lookup-environment-single]] +[[integration.appendix.xsd-schemas-jee-jndi-lookup-environment-single]] ==== `` (with Single JNDI Environment Setting) The following example shows how to use JNDI to look up an environment variable without `jee`: -[source,xml,indent=0] -[subs="verbatim,quotes"] +[source,xml,indent=0,subs="verbatim,quotes"] ---- @@ -92,8 +89,7 @@ The following example shows how to use JNDI to look up an environment variable w The following example shows how to use JNDI to look up an environment variable with `jee`: -[source,xml,indent=0] -[subs="verbatim,quotes"] +[source,xml,indent=0,subs="verbatim,quotes"] ---- ping=pong @@ -101,14 +97,13 @@ The following example shows how to use JNDI to look up an environment variable w ---- -[[xsd-schemas-jee-jndi-lookup-evironment-multiple]] +[[integration.appendix.xsd-schemas-jee-jndi-lookup-environment-multiple]] ==== `` (with Multiple JNDI Environment Settings) The following example shows how to use JNDI to look up multiple environment variables without `jee`: -[source,xml,indent=0] -[subs="verbatim,quotes"] +[source,xml,indent=0,subs="verbatim,quotes"] ---- @@ -124,8 +119,7 @@ without `jee`: The following example shows how to use JNDI to look up multiple environment variables with `jee`: -[source,xml,indent=0] -[subs="verbatim,quotes"] +[source,xml,indent=0,subs="verbatim,quotes"] ---- @@ -137,14 +131,13 @@ The following example shows how to use JNDI to look up multiple environment vari ---- -[[xsd-schemas-jee-jndi-lookup-complex]] +[[integration.appendix.xsd-schemas-jee-jndi-lookup-complex]] ==== `` (Complex) The following example shows how to use JNDI to look up a data source and a number of different properties without `jee`: -[source,xml,indent=0] -[subs="verbatim,quotes"] +[source,xml,indent=0,subs="verbatim,quotes"] ---- @@ -159,8 +152,7 @@ different properties without `jee`: The following example shows how to use JNDI to look up a data source and a number of different properties with `jee`: -[source,xml,indent=0] -[subs="verbatim,quotes"] +[source,xml,indent=0,subs="verbatim,quotes"] ---- ` (Simple) The `` element configures a reference to a local EJB Stateless Session Bean. @@ -181,8 +173,7 @@ The `` element configures a reference to a local EJB Stateless The following example shows how to configures a reference to a local EJB Stateless Session Bean without `jee`: -[source,xml,indent=0] -[subs="verbatim,quotes"] +[source,xml,indent=0,subs="verbatim,quotes"] ---- @@ -194,8 +185,7 @@ without `jee`: The following example shows how to configures a reference to a local EJB Stateless Session Bean with `jee`: -[source,xml,indent=0] -[subs="verbatim,quotes"] +[source,xml,indent=0,subs="verbatim,quotes"] ---- @@ -203,7 +193,7 @@ with `jee`: -[[xsd-schemas-jee-local-slsb-complex]] +[[integration.appendix.xsd-schemas-jee-local-slsb-complex]] ==== `` (Complex) The `` element configures a reference to a local EJB Stateless Session Bean. @@ -211,8 +201,7 @@ The `` element configures a reference to a local EJB Stateless The following example shows how to configures a reference to a local EJB Stateless Session Bean and a number of properties without `jee`: -[source,xml,indent=0] -[subs="verbatim,quotes"] +[source,xml,indent=0,subs="verbatim,quotes"] ---- @@ -227,8 +216,7 @@ and a number of properties without `jee`: The following example shows how to configures a reference to a local EJB Stateless Session Bean and a number of properties with `jee`: -[source,xml,indent=0] -[subs="verbatim,quotes"] +[source,xml,indent=0,subs="verbatim,quotes"] ---- The `` element configures a reference to a `remote` EJB Stateless Session Bean. @@ -247,8 +235,7 @@ The `` element configures a reference to a `remote` EJB Statel The following example shows how to configures a reference to a remote EJB Stateless Session Bean without `jee`: -[source,xml,indent=0] -[subs="verbatim,quotes"] +[source,xml,indent=0,subs="verbatim,quotes"] ---- @@ -265,8 +252,7 @@ without `jee`: The following example shows how to configures a reference to a remote EJB Stateless Session Bean with `jee`: -[source,xml,indent=0] -[subs="verbatim,quotes"] +[source,xml,indent=0,subs="verbatim,quotes"] ---- ` This element is detailed in @@ -320,7 +305,7 @@ This element is detailed in -[[xsd-schemas-cache]] +[[integration.appendix.xsd-schemas-cache]] === The `cache` Schema You can use the `cache` elements to enable support for Spring's `@CacheEvict`, `@CachePut`, @@ -332,8 +317,7 @@ To use the elements in the `cache` schema, you need to have the following preamb top of your Spring XML configuration file. The text in the following snippet references the correct schema so that the elements in the `cache` namespace are available to you: -[source,xml,indent=0] -[subs="verbatim,quotes"] +[source,xml,indent=0,subs="verbatim,quotes"] ---- >. ==== @@ -100,8 +97,7 @@ Finally, the following example shows the bean definitions that effect the inject Groovy-defined `Messenger` implementation into an instance of the `DefaultBookingService` class: -[source,xml,indent=0] -[subs="verbatim,quotes"] +[source,xml,indent=0,subs="verbatim,quotes"] ---- > involves defining dynamic-language-backed bean definitions, one for each bean that you want to configure (this is no different from normal JavaBean configuration). However, -instead of specifying the fully qualified classname of the class that is to be +instead of specifying the fully qualified class name of the class that is to be instantiated and configured by the container, you can use the `` element to define the dynamic language-backed bean. @@ -221,8 +217,7 @@ if we stick with <> from earlier this chapter, the following example shows what we would change in the Spring XML configuration to effect refreshable beans: -[source,xml,indent=0] -[subs="verbatim,quotes"] +[source,xml,indent=0,subs="verbatim,quotes"] ---- @@ -256,8 +251,7 @@ on the dynamic-language-backed bean when the program resumes execution. The following listing shows this sample application: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes"] ---- import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; @@ -282,8 +276,7 @@ surrounded by quotation marks. The following listing shows the changes that you (the developer) should make to the `Messenger.groovy` source file when the execution of the program is paused: -[source,groovy,indent=0] -[subs="verbatim,quotes"] +[source,groovy,indent=0,subs="verbatim,quotes"] ---- package org.springframework.scripting @@ -333,8 +326,7 @@ embedded directly in Spring bean definitions. More specifically, the inside a Spring configuration file. An example might clarify how the inline script feature works: -[source,xml,indent=0] -[subs="verbatim,quotes"] +[source,xml,indent=0,subs="verbatim,quotes"] ---- @@ -371,8 +363,7 @@ constructors and properties 100% clear, the following mixture of code and config does not work: .An approach that cannot work -[source,groovy,indent=0] -[subs="verbatim,quotes"] +[source,groovy,indent=0,subs="verbatim,quotes"] ---- // from the file 'Messenger.groovy' package org.springframework.scripting.groovy; @@ -394,8 +385,7 @@ does not work: } ---- -[source,xml,indent=0] -[subs="verbatim,quotes"] +[source,xml,indent=0,subs="verbatim,quotes"] ---- @@ -430,8 +420,7 @@ If you have read this chapter straight from the top, you have already <> of a Groovy-dynamic-language-backed bean. Now consider another example (again using an example from the Spring test suite): -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes"] ---- package org.springframework.scripting; @@ -443,8 +432,7 @@ bean. Now consider another example (again using an example from the Spring test The following example implements the `Calculator` interface in Groovy: -[source,groovy,indent=0] -[subs="verbatim,quotes"] +[source,groovy,indent=0,subs="verbatim,quotes"] ---- // from the file 'calculator.groovy' package org.springframework.scripting.groovy @@ -459,8 +447,7 @@ The following example implements the `Calculator` interface in Groovy: The following bean definition uses the calculator defined in Groovy: -[source,xml,indent=0] -[subs="verbatim,quotes"] +[source,xml,indent=0,subs="verbatim,quotes"] ---- @@ -470,8 +457,7 @@ The following bean definition uses the calculator defined in Groovy: Finally, the following small application exercises the preceding configuration: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes"] ---- package org.springframework.scripting; @@ -507,8 +493,7 @@ implementations of this interface could invoke any required initialization metho set some default property values, or specify a custom `MetaClass`. The following listing shows the `GroovyObjectCustomizer` interface definition: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public interface GroovyObjectCustomizer { @@ -522,8 +507,7 @@ has been defined). You can do whatever you like with the supplied `GroovyObject` reference. We expect that most people want to set a custom `MetaClass` with this callback, and the following example shows how to do so: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public final class SimpleMethodTracingCustomizer implements GroovyObjectCustomizer { @@ -548,8 +532,7 @@ search online. Plenty of articles address this topic. Actually, making use of a `GroovyObjectCustomizer` is easy if you use the Spring namespace support, as the following example shows: -[source,xml,indent=0] -[subs="verbatim,quotes"] +[source,xml,indent=0,subs="verbatim,quotes"] ---- @@ -563,8 +546,7 @@ following example shows: If you do not use the Spring namespace support, you can still use the `GroovyObjectCustomizer` functionality, as the following example shows: -[source,xml,indent=0] -[subs="verbatim,quotes"] +[source,xml,indent=0,subs="verbatim,quotes"] ---- @@ -614,8 +596,7 @@ Now we can show a fully working example of using a BeanShell-based bean that imp the `Messenger` interface that was defined earlier in this chapter. We again show the definition of the `Messenger` interface: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes"] ---- package org.springframework.scripting; @@ -628,8 +609,7 @@ definition of the `Messenger` interface: The following example shows the BeanShell "`implementation`" (we use the term loosely here) of the `Messenger` interface: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes"] ---- String message; @@ -645,8 +625,7 @@ of the `Messenger` interface: The following example shows the Spring XML that defines an "`instance`" of the above "`class`" (again, we use these terms very loosely here): -[source,xml,indent=0] -[subs="verbatim,quotes"] +[source,xml,indent=0,subs="verbatim,quotes"] ---- @@ -697,8 +676,7 @@ beans, you have to enable the "`refreshable beans`" functionality. See The following example shows an `org.springframework.web.servlet.mvc.Controller` implemented by using the Groovy dynamic language: -[source,groovy,indent=0] -[subs="verbatim,quotes"] +[source,groovy,indent=0,subs="verbatim,quotes"] ---- // from the file '/WEB-INF/groovy/FortuneController.groovy' package org.springframework.showcase.fortune.web @@ -708,8 +686,8 @@ by using the Groovy dynamic language: import org.springframework.web.servlet.ModelAndView import org.springframework.web.servlet.mvc.Controller - import javax.servlet.http.HttpServletRequest - import javax.servlet.http.HttpServletResponse + import jakarta.servlet.http.HttpServletRequest + import jakarta.servlet.http.HttpServletResponse class FortuneController implements Controller { @@ -722,8 +700,7 @@ by using the Groovy dynamic language: } ---- -[source,xml,indent=0] -[subs="verbatim,quotes"] +[source,xml,indent=0,subs="verbatim,quotes"] ---- > for a discussion of the `Validator` interface): -[source,groovy,indent=0] -[subs="verbatim,quotes"] +[source,groovy,indent=0,subs="verbatim,quotes"] ---- import org.springframework.validation.Validator import org.springframework.validation.Errors @@ -815,8 +791,7 @@ as it is with "`regular`" beans.) The following example uses the `scope` attribute to define a Groovy bean scoped as a <>: -[source,xml,indent=0] -[subs="verbatim,quotes"] +[source,xml,indent=0,subs="verbatim,quotes"] ---- >. See that chapter +<>. See that section for full details on this support and the `lang` elements. To use the elements in the `lang` schema, you need to have the following preamble at the top of your Spring XML configuration file. The text in the following snippet references the correct schema so that the tags in the `lang` namespace are available to you: -[source,xml,indent=0] -[subs="verbatim,quotes"] +[source,xml,indent=0,subs="verbatim,quotes"] ---- > DSL with {doc-root}/spring-framework/docs/{spring-version}/kdoc-api/spring-framework/org.springframework.web.reactive.function.server/router.html[router { }] -* WebFlux.fn <> DSL with {doc-root}/spring-framework/docs/{spring-version}/kdoc-api/spring-framework/org.springframework.web.reactive.function.server/co-router.html[coRouter { }] +* WebMvc.fn DSL with {docs-spring-framework}/kdoc-api/spring-webmvc/org.springframework.web.servlet.function/router.html[router { }] +* WebFlux.fn <> DSL with {docs-spring-framework}/kdoc-api/spring-webflux/org.springframework.web.reactive.function.server/router.html[router { }] +* WebFlux.fn <> DSL with {docs-spring-framework}/kdoc-api/spring-webflux/org.springframework.web.reactive.function.server/co-router.html[coRouter { }] These DSL let you write clean and idiomatic Kotlin code to build a `RouterFunction` instance as the following example shows: @@ -344,7 +345,7 @@ Spring Framework provides a https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/view/script/ScriptTemplateView.html[`ScriptTemplateView`] which supports https://www.jcp.org/en/jsr/detail?id=223[JSR-223] to render templates by using script engines. -By leveraging `kotlin-script-runtime` and `scripting-jsr223-embeddable` dependencies, it +By leveraging `scripting-jsr223` dependencies, it is possible to use such feature to render Kotlin-based templates with https://github.com/Kotlin/kotlinx.html[kotlinx.html] DSL or Kotlin multiline interpolated `String`. @@ -352,8 +353,7 @@ https://github.com/Kotlin/kotlinx.html[kotlinx.html] DSL or Kotlin multiline int [source,kotlin,indent=0] ---- dependencies { - compile("org.jetbrains.kotlin:kotlin-script-runtime:${kotlinVersion}") - runtime("org.jetbrains.kotlin:kotlin-scripting-jsr223-embeddable:${kotlinVersion}") + runtime("org.jetbrains.kotlin:kotlin-scripting-jsr223:${kotlinVersion}") } ---- @@ -389,17 +389,13 @@ project for more details. === Kotlin multiplatform serialization As of Spring Framework 5.3, https://github.com/Kotlin/kotlinx.serialization[Kotlin multiplatform serialization] is -supported in Spring MVC, Spring WebFlux and Spring Messaging. The builtin support currently only targets JSON format. - -To enable it, follow https://github.com/Kotlin/kotlinx.serialization#setup[those instructions] and make sure neither -Jackson, GSON or JSONB are in the classpath. - -NOTE: For a typical Spring Boot web application, that can be achieved by excluding `spring-boot-starter-json` dependency. - -In Spring MVC, if you need Jackson, GSON or JSONB for other purposes, you can keep them on the classpath and -<> to remove `MappingJackson2HttpMessageConverter` and add -`KotlinSerializationJsonHttpMessageConverter`. +supported in Spring MVC, Spring WebFlux and Spring Messaging (RSocket). The builtin support currently targets CBOR, JSON, and ProtoBuf formats. +To enable it, follow https://github.com/Kotlin/kotlinx.serialization#setup[those instructions] to add the related dependency and plugin. +With Spring MVC and WebFlux, both Kotlin serialization and Jackson will be configured by default if they are in the classpath since +Kotlin serialization is designed to serialize only Kotlin classes annotated with `@Serializable`. +With Spring Messaging (RSocket), make sure that neither Jackson, GSON or JSONB are in the classpath if you want automatic configuration, +if Jackson is needed configure `KotlinSerializationJsonMessageConverter` manually. == Coroutines @@ -414,10 +410,10 @@ Spring Framework provides support for Coroutines on the following scope: * https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/index.html[Deferred] and https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html[Flow] return values support in Spring MVC and WebFlux annotated `@Controller` * Suspending function support in Spring MVC and WebFlux annotated `@Controller` -* Extensions for WebFlux {doc-root}/spring-framework/docs/{spring-version}/kdoc-api/spring-framework/org.springframework.web.reactive.function.client/index.html[client] and {doc-root}/spring-framework/docs/{spring-version}/kdoc-api/spring-framework/org.springframework.web.reactive.function.server/index.html[server] functional API. -* WebFlux.fn {doc-root}/spring-framework/docs/{spring-version}/kdoc-api/spring-framework/org.springframework.web.reactive.function.server/co-router.html[coRouter { }] DSL +* Extensions for WebFlux {docs-spring-framework}/kdoc-api/spring-webflux/org.springframework.web.reactive.function.client/index.html[client] and {docs-spring-framework}/kdoc-api/spring-webflux/org.springframework.web.reactive.function.server/index.html[server] functional API. +* WebFlux.fn {docs-spring-framework}/kdoc-api/spring-webflux/org.springframework.web.reactive.function.server/co-router.html[coRouter { }] DSL * Suspending function and `Flow` support in RSocket `@MessageMapping` annotated methods -* Extensions for {doc-root}/spring-framework/docs/{spring-version}/kdoc-api/spring-framework/org.springframework.messaging.rsocket/index.html[`RSocketRequester`] +* Extensions for {docs-spring-framework}/kdoc-api/spring-messaging/org.springframework.messaging.rsocket/index.html[`RSocketRequester`] === Dependencies @@ -560,7 +556,7 @@ class CoroutinesViewController(banner: Banner) { === WebFlux.fn -Here is an example of Coroutines router defined via the {doc-root}/spring-framework/docs/{spring-version}/kdoc-api/spring-framework/org.springframework.web.reactive.function.server/co-router.html[coRouter { }] DSL and related handlers. +Here is an example of Coroutines router defined via the {docs-spring-framework}/kdoc-api/spring-webflux/org.springframework.web.reactive.function.server/co-router.html[coRouter { }] DSL and related handlers. [source,kotlin,indent=0] ---- @@ -872,7 +868,7 @@ https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-featu ==== Constructor injection -As described in the <>, +As described in the <>, JUnit 5 allows constructor injection of beans which is pretty useful with Kotlin in order to use `val` instead of `lateinit var`. You can use {api-spring-framework}/test/context/TestConstructor.html[`@TestConstructor(autowireMode = AutowireMode.ALL)`] diff --git a/src/docs/asciidoc/overview.adoc b/framework-docs/src/docs/asciidoc/overview.adoc similarity index 81% rename from src/docs/asciidoc/overview.adoc rename to framework-docs/src/docs/asciidoc/overview.adoc index 1b41c6f2e6ea..33afeb22909b 100644 --- a/src/docs/asciidoc/overview.adoc +++ b/framework-docs/src/docs/asciidoc/overview.adoc @@ -44,7 +44,7 @@ parallel, the Spring WebFlux reactive web framework. A note about modules: Spring's framework jars allow for deployment to JDK 9's module path ("Jigsaw"). For use in Jigsaw-enabled applications, the Spring Framework 5 jars come with "Automatic-Module-Name" manifest entries which define stable language-level module names -("spring.core", "spring.context" etc) independent from jar artifact names (the jars follow +("spring.core", "spring.context", etc.) independent from jar artifact names (the jars follow the same naming pattern with "-" instead of ".", e.g. "spring-core" and "spring-context"). Of course, Spring's framework jars keep working fine on the classpath on both JDK 8 and 9+. @@ -56,9 +56,10 @@ Of course, Spring's framework jars keep working fine on the classpath on both JD Spring came into being in 2003 as a response to the complexity of the early https://en.wikipedia.org/wiki/Java_Platform,_Enterprise_Edition[J2EE] specifications. -While some consider Java EE and Spring to be in competition, Spring is, in fact, complementary -to Java EE. The Spring programming model does not embrace the Java EE platform specification; -rather, it integrates with carefully selected individual specifications from the EE umbrella: +While some consider Java EE and its modern-day successor Jakarta EE to be in +competition with Spring, they are in fact complementary. The Spring programming +model does not embrace the Jakarta EE platform specification; rather, it integrates +with carefully selected individual specifications from the traditional EE umbrella: * Servlet API (https://jcp.org/en/jsr/detail?id=340[JSR 340]) * WebSocket API (https://www.jcp.org/en/jsr/detail?id=356[JSR 356]) @@ -71,19 +72,22 @@ rather, it integrates with carefully selected individual specifications from the The Spring Framework also supports the Dependency Injection (https://www.jcp.org/en/jsr/detail?id=330[JSR 330]) and Common Annotations -(https://jcp.org/en/jsr/detail?id=250[JSR 250]) specifications, which application developers -may choose to use instead of the Spring-specific mechanisms provided by the Spring Framework. - -As of Spring Framework 5.0, Spring requires the Java EE 7 level (e.g. Servlet 3.1+, JPA 2.1+) -as a minimum - while at the same time providing out-of-the-box integration with newer APIs -at the Java EE 8 level (e.g. Servlet 4.0, JSON Binding API) when encountered at runtime. -This keeps Spring fully compatible with e.g. Tomcat 8 and 9, WebSphere 9, and JBoss EAP 7. - -Over time, the role of Java EE in application development has evolved. In the early days of -Java EE and Spring, applications were created to be deployed to an application server. -Today, with the help of Spring Boot, applications are created in a devops- and -cloud-friendly way, with the Servlet container embedded and trivial to change. -As of Spring Framework 5, a WebFlux application does not even use the Servlet API directly +(https://jcp.org/en/jsr/detail?id=250[JSR 250]) specifications, which application +developers may choose to use instead of the Spring-specific mechanisms provided +by the Spring Framework. Originally, those were based on common `javax` packages. + +As of Spring Framework 6.0, Spring has been upgraded to the Jakarta EE 9 level +(e.g. Servlet 5.0+, JPA 3.0+), based on the `jakarta` namespace instead of the +traditional `javax` packages. With EE 9 as the minimum and EE 10 supported already, +Spring is prepared to provide out-of-the-box support for the further evolution of +the Jakarta EE APIs. Spring Framework 6.0 is fully compatible with Tomcat 10.1, +Jetty 11 and Undertow 2.3 as web servers, and also with Hibernate ORM 6.1. + +Over time, the role of Java/Jakarta EE in application development has evolved. In the +early days of J2EE and Spring, applications were created to be deployed to an application +server. Today, with the help of Spring Boot, applications are created in a devops- and +cloud-friendly way, with the Servlet container embedded and trivial to change. As of +Spring Framework 5, a WebFlux application does not even use the Servlet API directly and can run on servers (such as Netty) that are not Servlet containers. Spring continues to innovate and to evolve. Beyond the Spring Framework, there are other @@ -125,7 +129,7 @@ clean code structure with no circular dependencies between packages. == Feedback and Contributions For how-to questions or diagnosing or debugging issues, we suggest using Stack Overflow. Click -https://stackoverflow.com/questions/tagged/spring+or+spring-mvc+or+spring-aop+or+spring-jdbc+or+spring-r2dbc+or+spring-transactions+or+spring-annotations+or+spring-jms+or+spring-el+or+spring-test+or+spring+or+spring-remoting+or+spring-orm+or+spring-jmx+or+spring-cache+or+spring-webflux+or+spring-rsocket?tab=Newest[here] +https://stackoverflow.com/questions/tagged/spring+or+spring-mvc+or+spring-aop+or+spring-jdbc+or+spring-r2dbc+or+spring-transactions+or+spring-annotations+or+spring-jms+or+spring-el+or+spring-test+or+spring+or+spring-orm+or+spring-jmx+or+spring-cache+or+spring-webflux+or+spring-rsocket?tab=Newest[here] for a list of the suggested tags to use on Stack Overflow. If you're fairly certain that there is a problem in the Spring Framework or would like to suggest a feature, please use the https://github.com/spring-projects/spring-framework/issues[GitHub Issues]. @@ -135,8 +139,7 @@ https://github.com/spring-projects/spring-framework[Github]. However, please kee that, for all but the most trivial issues, we expect a ticket to be filed in the issue tracker, where discussions take place and leave a record for future reference. -For more details see the guidelines at the -https://github.com/spring-projects/spring-framework/blob/master/CONTRIBUTING.md[CONTRIBUTING], +For more details see the guidelines at the {spring-framework-main-code}/CONTRIBUTING.md[CONTRIBUTING], top-level project page. diff --git a/src/docs/asciidoc/rsocket.adoc b/framework-docs/src/docs/asciidoc/rsocket.adoc similarity index 90% rename from src/docs/asciidoc/rsocket.adoc rename to framework-docs/src/docs/asciidoc/rsocket.adoc index 97bf16736ccd..dbdd05c558db 100644 --- a/src/docs/asciidoc/rsocket.adoc +++ b/framework-docs/src/docs/asciidoc/rsocket.adoc @@ -2,7 +2,7 @@ = RSocket :gh-rsocket: https://github.com/rsocket :gh-rsocket-java: {gh-rsocket}/rsocket-java -:gh-rsocket-extentions: {gh-rsocket}/rsocket/blob/master/Extensions +:gh-rsocket-extensions: {gh-rsocket}/rsocket/blob/master/Extensions This section describes Spring Framework's support for the RSocket protocol. @@ -53,7 +53,7 @@ through RSocket across the network. === The Protocol One of the benefits of RSocket is that it has well defined behavior on the wire and an -easy to read https://rsocket.io/docs/Protocol[specification] along with some protocol +easy to read https://rsocket.io/about/protocol[specification] along with some protocol {gh-rsocket}/rsocket/tree/master/Extensions[extensions]. Therefore it is a good idea to read the spec, independent of language implementations and higher level framework APIs. This section provides a succinct overview to establish some context. @@ -99,9 +99,9 @@ and therefore only included in the first message on a request, i.e. with one of Protocol extensions define common metadata formats for use in applications: -* {gh-rsocket-extentions}/CompositeMetadata.md[Composite Metadata]-- multiple, +* {gh-rsocket-extensions}/CompositeMetadata.md[Composite Metadata]-- multiple, independently formatted metadata entries. -* {gh-rsocket-extentions}/Routing.md[Routing] -- the route for a request. +* {gh-rsocket-extensions}/Routing.md[Routing] -- the route for a request. @@ -218,7 +218,7 @@ established transparently and used. For data, the default mime type is derived from the first configured `Decoder`. For metadata, the default mime type is -{gh-rsocket-extentions}/CompositeMetadata.md[composite metadata] which allows multiple +{gh-rsocket-extensions}/CompositeMetadata.md[composite metadata] which allows multiple metadata value and mime type pairs per request. Typically both don't need to be changed. Data and metadata in the `SETUP` frame is optional. On the server side, @@ -291,7 +291,7 @@ infrastructure that's used on a server, but registered programmatically as follo ---- <1> Use `PathPatternRouteMatcher`, if `spring-web` is present, for efficient route matching. -<2> Create a responder from a class with `@MessageMaping` and/or `@ConnectMapping` methods. +<2> Create a responder from a class with `@MessageMapping` and/or `@ConnectMapping` methods. <3> Register the responder. [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] @@ -310,7 +310,7 @@ infrastructure that's used on a server, but registered programmatically as follo ---- <1> Use `PathPatternRouteMatcher`, if `spring-web` is present, for efficient route matching. -<2> Create a responder from a class with `@MessageMaping` and/or `@ConnectMapping` methods. +<2> Create a responder from a class with `@MessageMapping` and/or `@ConnectMapping` methods. <3> Register the responder. Note the above is only a shortcut designed for programmatic registration of client @@ -494,7 +494,7 @@ The `data(Object)` step is optional. Skip it for requests that don't send data: ---- Extra metadata values can be added if using -{gh-rsocket-extentions}/CompositeMetadata.md[composite metadata] (the default) and if the +{gh-rsocket-extensions}/CompositeMetadata.md[composite metadata] (the default) and if the values are supported by a registered `Encoder`. For example: [source,java,indent=0,subs="verbatim,quotes",role="primary"] @@ -605,8 +605,8 @@ Then start an RSocket server through the Java RSocket API and plug the ---- `RSocketMessageHandler` supports -{gh-rsocket-extentions}/CompositeMetadata.md[composite] and -{gh-rsocket-extentions}/Routing.md[routing] metadata by default. You can set its +{gh-rsocket-extensions}/CompositeMetadata.md[composite] and +{gh-rsocket-extensions}/Routing.md[routing] metadata by default. You can set its <> if you need to switch to a different mime type or register additional metadata mime types. @@ -821,7 +821,7 @@ requests to the `RSocketRequester` for the connection. See == MetadataExtractor Responders must interpret metadata. -{gh-rsocket-extentions}/CompositeMetadata.md[Composite metadata] allows independently +{gh-rsocket-extensions}/CompositeMetadata.md[Composite metadata] allows independently formatted metadata values (e.g. for routing, security, tracing) each with its own mime type. Applications need a way to configure metadata mime types to support, and a way to access extracted values. @@ -832,7 +832,7 @@ in annotated handler methods. `DefaultMetadataExtractor` can be given `Decoder` instances to decode metadata. Out of the box it has built-in support for -{gh-rsocket-extentions}/Routing.md["message/x.rsocket.routing.v0"] which it decodes to +{gh-rsocket-extensions}/Routing.md["message/x.rsocket.routing.v0"] which it decodes to `String` and saves under the "route" key. For any other mime type you'll need to provide a `Decoder` and register the mime type as follows: @@ -904,3 +904,79 @@ simply use a callback to customize registrations as follows: } .build() ---- + + + + +[[rsocket-interface]] +== RSocket Interface + +The Spring Framework lets you define an RSocket service as a Java interface with annotated +methods for RSocket exchanges. You can then generate a proxy that implements this interface +and performs the exchanges. This helps to simplify RSocket remote access by wrapping the +use of the underlying <>. + +One, declare an interface with `@RSocketExchange` methods: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + interface RadarService { + + @RSocketExchange("radars") + Flux getRadars(@Payload MapRequest request); + + // more RSocket exchange methods... + + } +---- + +Two, create a proxy that will perform the declared RSocket exchanges: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + RSocketRequester requester = ... ; + RSocketServiceProxyFactory factory = RSocketServiceProxyFactory.builder(requester).build(); + + RepositoryService service = factory.createClient(RadarService.class); +---- + + +[[rsocket-interface-method-parameters]] +=== Method Parameters + +Annotated, RSocket exchange methods support flexible method signatures with the following +method parameters: + +[cols="1,2", options="header"] +|=== +| Method argument | Description + +| `@DestinationVariable` +| Add a route variable to pass to `RSocketRequester` along with the route from the + `@RSocketExchange` annotation in order to expand template placeholders in the route. + This variable can be a String or any Object, which is then formatted via `toString()`. + +| `@Payload` +| Set the input payload(s) for the request. This can be a concrete value, or any producer + of values that can be adapted to a Reactive Streams `Publisher` via + `ReactiveAdapterRegistry` + +| `Object`, if followed by `MimeType` +| The value for a metadata entry in the input payload. This can be any `Object` as long + as the next argument is the metadata entry `MimeType`. The value can be a concrete + value or any producer of a single value that can be adapted to a Reactive Streams + `Publisher` via `ReactiveAdapterRegistry`. + +| `MimeType` +| The `MimeType` for a metadata entry. The preceding method argument is expected to be + the metadata value. + +|=== + + +[[rsocket-interface-return-values]] +=== Return Values + +Annotated, RSocket exchange methods support return values that are concrete value(s), or +any producer of value(s) that can be adapted to a Reactive Streams `Publisher` via +`ReactiveAdapterRegistry`. diff --git a/framework-docs/src/docs/asciidoc/spring-framework.adocbook b/framework-docs/src/docs/asciidoc/spring-framework.adocbook new file mode 100644 index 000000000000..a3f83c5f93c3 --- /dev/null +++ b/framework-docs/src/docs/asciidoc/spring-framework.adocbook @@ -0,0 +1,25 @@ +:noheader: += Spring Framework Documentation + +include::overview.adoc[leveloffset=+1] +include::core.adoc[leveloffset=+1] +include::testing.adoc[leveloffset=+1] +include::data-access.adoc[leveloffset=+1] +include::web.adoc[leveloffset=+1] +include::web-reactive.adoc[leveloffset=+1] +include::integration.adoc[leveloffset=+1] +include::languages.adoc[leveloffset=+1] +include::appendix.adoc[leveloffset=+1] + +Rod Johnson, Juergen Hoeller, Keith Donald, Colin Sampaleanu, Rob Harrop, Thomas Risberg, +Alef Arendsen, Darren Davison, Dmitriy Kopylenko, Mark Pollack, Thierry Templier, Erwin +Vervaet, Portia Tung, Ben Hale, Adrian Colyer, John Lewis, Costin Leau, Mark Fisher, Sam +Brannen, Ramnivas Laddad, Arjen Poutsma, Chris Beams, Tareq Abedrabbo, Andy Clement, Dave +Syer, Oliver Gierke, Rossen Stoyanchev, Phillip Webb, Rob Winch, Brian Clozel, Stephane +Nicoll, Sebastien Deleuze, Jay Bryant, Mark Paluch + +Copyright © 2002 - 2022 VMware, Inc. All Rights Reserved. + +Copies of this document may be made for your own use and for distribution to others, +provided that you do not charge any fee for such copies and further provided that each +copy contains this Copyright Notice, whether distributed in print or electronically. diff --git a/src/docs/asciidoc/testing.adoc b/framework-docs/src/docs/asciidoc/testing.adoc similarity index 92% rename from src/docs/asciidoc/testing.adoc rename to framework-docs/src/docs/asciidoc/testing.adoc index 9738ec87522b..e27f8432d52c 100644 --- a/src/docs/asciidoc/testing.adoc +++ b/framework-docs/src/docs/asciidoc/testing.adoc @@ -1,7 +1,5 @@ [[testing]] = Testing -:doc-root: https://docs.spring.io -:api-spring-framework: {doc-root}/spring-framework/docs/{spring-version}/javadoc-api/org/springframework :doc-spring-boot: {doc-root}/spring-boot/docs/current/reference :toc: left :toclevels: 4 @@ -34,8 +32,8 @@ manual.) == Unit Testing Dependency injection should make your code less dependent on the container than it would -be with traditional Java EE development. The POJOs that make up your application should -be testable in JUnit or TestNG tests, with objects instantiated by using the `new` +be with traditional J2EE / Java EE development. The POJOs that make up your application +should be testable in JUnit or TestNG tests, with objects instantiated by using the `new` operator, without Spring or any other container. You can use <> (in conjunction with other valuable testing techniques) to test your code in isolation. If you follow the architecture recommendations for Spring, the resulting clean layering @@ -80,7 +78,7 @@ out-of-container tests for code that depends on environment-specific properties. The `org.springframework.mock.jndi` package contains a partial implementation of the JNDI SPI, which you can use to set up a simple JNDI environment for test suites or stand-alone applications. If, for example, JDBC `DataSource` instances get bound to the same JNDI -names in test code as they do in a Java EE container, you can reuse both application code +names in test code as they do in a Jakarta EE container, you can reuse both application code and configuration in testing scenarios without modification. WARNING: The mock JNDI support in the `org.springframework.mock.jndi` package is @@ -94,11 +92,11 @@ parties such as https://github.com/h-thurow/Simple-JNDI[Simple-JNDI]. The `org.springframework.mock.web` package contains a comprehensive set of Servlet API mock objects that are useful for testing web contexts, controllers, and filters. These mock objects are targeted at usage with Spring's Web MVC framework and are generally more -convenient to use than dynamic mock objects (such as http://easymock.org/[EasyMock]) +convenient to use than dynamic mock objects (such as https://easymock.org/[EasyMock]) or alternative Servlet API mock objects (such as http://www.mockobjects.com[MockObjects]). -TIP: Since Spring Framework 5.0, the mock objects in `org.springframework.mock.web` are -based on the Servlet 4.0 API. +TIP: Since Spring Framework 6.0, the mock objects in `org.springframework.mock.web` are +based on the Servlet 6.0 API. The Spring MVC Test framework builds on the mock Servlet API objects to provide an integration testing framework for Spring MVC. See <>. @@ -144,11 +142,20 @@ categories: The `org.springframework.test.util` package contains several general purpose utilities for use in unit and integration testing. -`ReflectionTestUtils` is a collection of reflection-based utility methods. You can use -these methods in testing scenarios where you need to change the value of a constant, set -a non-`public` field, invoke a non-`public` setter method, or invoke a non-`public` -configuration or lifecycle callback method when testing application code for use cases -such as the following: +{api-spring-framework}/test/util/AopTestUtils.html[`AopTestUtils`] is a collection of +AOP-related utility methods. You can use these methods to obtain a reference to the +underlying target object hidden behind one or more Spring proxies. For example, if you +have configured a bean as a dynamic mock by using a library such as EasyMock or Mockito, +and the mock is wrapped in a Spring proxy, you may need direct access to the underlying +mock to configure expectations on it and perform verifications. For Spring's core AOP +utilities, see {api-spring-framework}/aop/support/AopUtils.html[`AopUtils`] and +{api-spring-framework}/aop/framework/AopProxyUtils.html[`AopProxyUtils`]. + +{api-spring-framework}/test/util/ReflectionTestUtils.html[`ReflectionTestUtils`] is a +collection of reflection-based utility methods. You can use these methods in testing +scenarios where you need to change the value of a constant, set a non-`public` field, +invoke a non-`public` setter method, or invoke a non-`public` configuration or lifecycle +callback method when testing application code for use cases such as the following: * ORM frameworks (such as JPA and Hibernate) that condone `private` or `protected` field access as opposed to `public` setter methods for properties in a domain entity. @@ -158,14 +165,20 @@ such as the following: * Use of annotations such as `@PostConstruct` and `@PreDestroy` for lifecycle callback methods. -{api-spring-framework}/test/util/AopTestUtils.html[`AopTestUtils`] is a collection of -AOP-related utility methods. You can use these methods to obtain a reference to the -underlying target object hidden behind one or more Spring proxies. For example, if you -have configured a bean as a dynamic mock by using a library such as EasyMock or Mockito, -and the mock is wrapped in a Spring proxy, you may need direct access to the underlying -mock to configure expectations on it and perform verifications. For Spring's core AOP -utilities, see {api-spring-framework}/aop/support/AopUtils.html[`AopUtils`] and -{api-spring-framework}/aop/framework/AopProxyUtils.html[`AopProxyUtils`]. +{api-spring-framework}/test/util/TestSocketUtils.html[`TestSocketUtils`] is a simple +utility for finding available TCP ports on `localhost` for use in integration testing +scenarios. + +[NOTE] +==== +`TestSocketUtils` can be used in integration tests which start an external server on an +available random port. However, these utilities make no guarantee about the subsequent +availability of a given port and are therefore unreliable. Instead of using +`TestSocketUtils` to find an available local port for a server, it is recommended that +you rely on a server's ability to start on a random ephemeral port that it selects or is +assigned by the operating system. To interact with that server, you should query the +server for the port it is currently using. +==== [[unit-testing-spring-mvc]] @@ -216,7 +229,7 @@ Doing so lets you test things such as: The Spring Framework provides first-class support for integration testing in the `spring-test` module. The name of the actual JAR file might include the release version and might also be in the long `org.springframework.test` form, depending on where you get -it from (see the <> +it from (see the <> for an explanation). This library includes the `org.springframework.test` package, which contains valuable classes for integration testing with a Spring container. This testing does not rely on an application server or other deployment environment. Such tests are @@ -414,6 +427,7 @@ Spring's testing annotations include the following: * <> * <> * <> +* <> * <> * <> * <> @@ -499,16 +513,17 @@ The following example shows such a case: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java ---- - @ContextConfiguration(initializers = CustomContextIntializer.class) // <1> + @ContextConfiguration(initializers = CustomContextInitializer.class) // <1> class ContextInitializerTests { // class body... } ---- +<1> Declaring an initializer class. [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] .Kotlin ---- - @ContextConfiguration(initializers = [CustomContextIntializer::class]) // <1> + @ContextConfiguration(initializers = [CustomContextInitializer::class]) // <1> class ContextInitializerTests { // class body... } @@ -1098,10 +1113,10 @@ javadoc. [[spring-testing-annotation-testexecutionlisteners]] ===== `@TestExecutionListeners` -`@TestExecutionListeners` defines class-level metadata for configuring the -`TestExecutionListener` implementations that should be registered with the -`TestContextManager`. Typically, `@TestExecutionListeners` is used in conjunction with -`@ContextConfiguration`. +`@TestExecutionListeners` is used to register listeners for a particular test class, its +subclasses, and its nested classes. If you wish to register a listener globally, you +should register it via the automatic discovery mechanism described in +<>. The following example shows how to register two `TestExecutionListener` implementations: @@ -1132,6 +1147,21 @@ By default, `@TestExecutionListeners` provides support for inheriting listeners superclasses or enclosing classes. See <> and the {api-spring-framework}/test/context/TestExecutionListeners.html[`@TestExecutionListeners` +javadoc] for an example and further details. If you discover that you need to switch +back to using the default `TestExecutionListener` implementations, see the note +in <>. + +[[spring-testing-annotation-recordapplicationevents]] +===== `@RecordApplicationEvents` + +`@RecordApplicationEvents` is a class-level annotation that is used to instruct the +_Spring TestContext Framework_ to record all application events that are published in the +`ApplicationContext` during the execution of a single test. + +The recorded events can be accessed via the `ApplicationEvents` API within tests. + +See <> and the +{api-spring-framework}/test/context/event/RecordApplicationEvents.html[`@RecordApplicationEvents` javadoc] for an example and further details. [[spring-testing-annotation-commit]] @@ -1463,13 +1493,12 @@ and can be used anywhere in the Spring Framework. * `@Autowired` * `@Qualifier` * `@Value` -* `@Resource` (javax.annotation) if JSR-250 is present -* `@ManagedBean` (javax.annotation) if JSR-250 is present -* `@Inject` (javax.inject) if JSR-330 is present -* `@Named` (javax.inject) if JSR-330 is present -* `@PersistenceContext` (javax.persistence) if JPA is present -* `@PersistenceUnit` (javax.persistence) if JPA is present -* `@Required` +* `@Resource` (jakarta.annotation) if JSR-250 is present +* `@ManagedBean` (jakarta.annotation) if JSR-250 is present +* `@Inject` (jakarta.inject) if JSR-330 is present +* `@Named` (jakarta.inject) if JSR-330 is present +* `@PersistenceContext` (jakarta.persistence) if JPA is present +* `@PersistenceUnit` (jakarta.persistence) if JPA is present * `@Transactional` (org.springframework.transaction.annotation) _with <>_ @@ -1645,8 +1674,10 @@ before failing. times that the test method is to be run is specified in the annotation. The scope of execution to be repeated includes execution of the test method itself as -well as any setting up or tearing down of the test fixture. The following example shows -how to use the `@Repeat` annotation: +well as any setting up or tearing down of the test fixture. When used with the +<>, the scope additionally includes +preparation of the test instance by `TestExecutionListener` implementations. The +following example shows how to use the `@Repeat` annotation: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1832,7 +1863,8 @@ constructor takes precedence over both `@TestConstructor` and the default mode. ===== The default _test constructor autowire mode_ can be changed by setting the `spring.test.constructor.autowire.mode` JVM system property to `all`. Alternatively, the -default mode may be set via the `SpringProperties` mechanism. +default mode may be set via the +<> mechanism. As of Spring Framework 5.3, the default mode may also be configured as a https://junit.org/junit5/docs/current/user-guide/#running-tests-config-params[JUnit Platform configuration parameter]. @@ -1855,7 +1887,7 @@ Spring test configuration annotations are processed within enclosing class hiera for inner test classes. If `@NestedTestConfiguration` is not present or meta-present on a test class, in its -super type hierarchy, or in its enclosing class hierarchy, the default _enclosing +supertype hierarchy, or in its enclosing class hierarchy, the default _enclosing configuration inheritance mode_ will be used. See the tip below for details on how to change the default mode. @@ -1864,8 +1896,8 @@ change the default mode. ===== The default _enclosing configuration inheritance mode_ is `INHERIT`, but it can be changed by setting the `spring.test.enclosing.configuration` JVM system property to -`OVERRIDE`. Alternatively, the default mode may be set via the `SpringProperties` -mechanism. +`OVERRIDE`. Alternatively, the default mode may be set via the +<> mechanism. ===== The <> honors `@NestedTestConfiguration` semantics for the @@ -1877,8 +1909,10 @@ following annotations. * <> * <> * <> +* <> * <> * <> +* <> * <> * <> * <> @@ -1942,6 +1976,19 @@ example, you can create a custom `@EnabledOnMac` annotation as follows: annotation class EnabledOnMac {} ---- +[NOTE] +==== +`@EnabledOnMac` is meant only as an example of what is possible. If you have that exact +use case, please use the built-in `@EnabledOnOs(MAC)` support in JUnit Jupiter. +==== + +[WARNING] +==== +Since JUnit 5.7, JUnit Jupiter also has a condition annotation named `@EnabledIf`. Thus, +if you wish to use Spring's `@EnabledIf` support make sure you import the annotation type +from the correct package. +==== + [[integration-testing-annotations-junit-jupiter-disabledif]] ===== `@DisabledIf` @@ -1990,6 +2037,19 @@ example, you can create a custom `@DisabledOnMac` annotation as follows: annotation class DisabledOnMac {} ---- +[NOTE] +==== +`@DisabledOnMac` is meant only as an example of what is possible. If you have that exact +use case, please use the built-in `@DisabledOnOs(MAC)` support in JUnit Jupiter. +==== + +[WARNING] +==== +Since JUnit 5.7, JUnit Jupiter also has a condition annotation named `@DisabledIf`. Thus, +if you wish to use Spring's `@DisabledIf` support make sure you import the annotation type +from the correct package. +==== + [[integration-testing-annotations-meta]] ==== Meta-Annotation Support for Testing @@ -2400,6 +2460,8 @@ by default, exactly in the following order: `WebApplicationContext`. * `DirtiesContextBeforeModesTestExecutionListener`: Handles the `@DirtiesContext` annotation for "`before`" modes. +* `ApplicationEventsTestExecutionListener`: Provides support for + <>. * `DependencyInjectionTestExecutionListener`: Provides dependency injection for the test instance. * `DirtiesContextTestExecutionListener`: Handles the `@DirtiesContext` annotation for @@ -2414,12 +2476,46 @@ by default, exactly in the following order: [[testcontext-tel-config-registering-tels]] ===== Registering `TestExecutionListener` Implementations -You can register `TestExecutionListener` implementations for a test class and its -subclasses by using the `@TestExecutionListeners` annotation. See +You can register `TestExecutionListener` implementations explicitly for a test class, its +subclasses, and its nested classes by using the `@TestExecutionListeners` annotation. See <> and the javadoc for {api-spring-framework}/test/context/TestExecutionListeners.html[`@TestExecutionListeners`] for details and examples. +.Switching to default `TestExecutionListener` implementations +[NOTE] +==== +If you extend a class that is annotated with `@TestExecutionListeners` and you need to +switch to using the default set of listeners, you can annotate your class with the +following. + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- + // Switch to default listeners + @TestExecutionListeners( + listeners = {}, + inheritListeners = false, + mergeMode = MERGE_WITH_DEFAULTS) + class MyTest extends BaseTest { + // class body... + } +---- + +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + // Switch to default listeners + @TestExecutionListeners( + listeners = [], + inheritListeners = false, + mergeMode = MERGE_WITH_DEFAULTS) + class MyTest : BaseTest { + // class body... + } +---- +==== + [[testcontext-tel-config-automatic-discovery]] ===== Automatic Discovery of Default `TestExecutionListener` Implementations @@ -2477,7 +2573,7 @@ listeners. The following listing demonstrates this style of configuration: } ---- -[source,java,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] .Kotlin ---- @ContextConfiguration @@ -2546,6 +2642,95 @@ be replaced with the following: } ---- +[[testcontext-application-events]] +==== Application Events + +Since Spring Framework 5.3.3, the TestContext framework provides support for recording +<> published in the +`ApplicationContext` so that assertions can be performed against those events within +tests. All events published during the execution of a single test are made available via +the `ApplicationEvents` API which allows you to process the events as a +`java.util.Stream`. + +To use `ApplicationEvents` in your tests, do the following. + +* Ensure that your test class is annotated or meta-annotated with + <>. +* Ensure that the `ApplicationEventsTestExecutionListener` is registered. Note, however, + that `ApplicationEventsTestExecutionListener` is registered by default and only needs + to be manually registered if you have custom configuration via + `@TestExecutionListeners` that does not include the default listeners. +* Annotate a field of type `ApplicationEvents` with `@Autowired` and use that instance of + `ApplicationEvents` in your test and lifecycle methods (such as `@BeforeEach` and + `@AfterEach` methods in JUnit Jupiter). +** When using the <>, you may declare a method + parameter of type `ApplicationEvents` in a test or lifecycle method as an alternative + to an `@Autowired` field in the test class. + +The following test class uses the `SpringExtension` for JUnit Jupiter and +https://assertj.github.io/doc/[AssertJ] to assert the types of application events +published while invoking a method in a Spring-managed component: + +// Don't use "quotes" in the "subs" section because of the asterisks in /* ... */ +[source,java,indent=0,subs="verbatim",role="primary"] +.Java +---- + @SpringJUnitConfig(/* ... */) + @RecordApplicationEvents // <1> + class OrderServiceTests { + + @Autowired + OrderService orderService; + + @Autowired + ApplicationEvents events; // <2> + + @Test + void submitOrder() { + // Invoke method in OrderService that publishes an event + orderService.submitOrder(new Order(/* ... */)); + // Verify that an OrderSubmitted event was published + long numEvents = events.stream(OrderSubmitted.class).count(); // <3> + assertThat(numEvents).isEqualTo(1); + } + } +---- +<1> Annotate the test class with `@RecordApplicationEvents`. +<2> Inject the `ApplicationEvents` instance for the current test. +<3> Use the `ApplicationEvents` API to count how many `OrderSubmitted` events were published. + +// Don't use "quotes" in the "subs" section because of the asterisks in /* ... */ +[source,kotlin,indent=0,subs="verbatim",role="secondary"] +.Kotlin +---- + @SpringJUnitConfig(/* ... */) + @RecordApplicationEvents // <1> + class OrderServiceTests { + + @Autowired + lateinit var orderService: OrderService + + @Autowired + lateinit var events: ApplicationEvents // <2> + + @Test + fun submitOrder() { + // Invoke method in OrderService that publishes an event + orderService.submitOrder(Order(/* ... */)) + // Verify that an OrderSubmitted event was published + val numEvents = events.stream(OrderSubmitted::class).count() // <3> + assertThat(numEvents).isEqualTo(1) + } + } +---- +<1> Annotate the test class with `@RecordApplicationEvents`. +<2> Inject the `ApplicationEvents` instance for the current test. +<3> Use the `ApplicationEvents` API to count how many `OrderSubmitted` events were published. + +See the +{api-spring-framework}/test/context/event/ApplicationEvents.html[`ApplicationEvents` +javadoc] for further details regarding the `ApplicationEvents` API. + [[testcontext-test-execution-events]] ==== Test Execution Events @@ -2563,8 +2748,6 @@ test's `ApplicationContext` can listen to the following events published by the * `AfterTestMethodEvent` * `AfterTestClassEvent` -NOTE: These events are only published if the `ApplicationContext` has already been loaded. - These events may be consumed for various reasons, such as resetting mock beans or tracing test execution. One advantage of consuming test execution events rather than implementing a custom `TestExecutionListener` is that test execution events may be consumed by any @@ -2572,6 +2755,32 @@ Spring bean registered in the test `ApplicationContext`, and such beans may bene directly from dependency injection and other features of the `ApplicationContext`. In contrast, a `TestExecutionListener` is not a bean in the `ApplicationContext`. +[NOTE] +==== +The `EventPublishingTestExecutionListener` is registered by default; however, it only +publishes events if the `ApplicationContext` has _already been loaded_. This prevents the +`ApplicationContext` from being loaded unnecessarily or too early. + +Consequently, a `BeforeTestClassEvent` will not be published until after the +`ApplicationContext` has been loaded by another `TestExecutionListener`. For example, with +the default set of `TestExecutionListener` implementations registered, a +`BeforeTestClassEvent` will not be published for the first test class that uses a +particular test `ApplicationContext`, but a `BeforeTestClassEvent` _will_ be published for +any subsequent test class in the same test suite that uses the same test +`ApplicationContext` since the context will already have been loaded when subsequent test +classes run (as long as the context has not been removed from the `ContextCache` via +`@DirtiesContext` or the max-size eviction policy). + +If you wish to ensure that a `BeforeTestClassEvent` is always published for every test +class, you need to register a `TestExecutionListener` that loads the `ApplicationContext` +in the `beforeTestClass` callback, and that `TestExecutionListener` must be registered +_before_ the `EventPublishingTestExecutionListener`. + +Similarly, if `@DirtiesContext` is used to remove the `ApplicationContext` from the +context cache after the last test method in a given test class, the `AfterTestClassEvent` +will not be published for that test class. +==== + In order to listen to test execution events, a Spring bean may choose to implement the `org.springframework.context.ApplicationListener` interface. Alternatively, listener methods can be annotated with `@EventListener` and configured to listen to one of the @@ -2979,7 +3188,7 @@ The term "`component class`" can refer to any of the following: * A class annotated with `@Configuration`. * A component (that is, a class annotated with `@Component`, `@Service`, `@Repository`, or other stereotype annotations). -* A JSR-330 compliant class that is annotated with `javax.inject` annotations. +* A JSR-330 compliant class that is annotated with `jakarta.inject` annotations. * Any class that contains `@Bean`-methods. * Any other class that is intended to be registered as a Spring component (i.e., a Spring bean in the `ApplicationContext`), potentially taking advantage of automatic autowiring @@ -4136,7 +4345,7 @@ properties. @DynamicPropertySource static void redisProperties(DynamicPropertyRegistry registry) { - registry.add("redis.host", redis::getContainerIpAddress); + registry.add("redis.host", redis::getHost); registry.add("redis.port", redis::getMappedPort); } @@ -4160,7 +4369,7 @@ properties. @DynamicPropertySource @JvmStatic fun redisProperties(registry: DynamicPropertyRegistry) { - registry.add("redis.host", redis::getContainerIpAddress) + registry.add("redis.host", redis::getHost) registry.add("redis.port", redis::getMappedPort) } } @@ -4320,8 +4529,9 @@ annotations by specifying a Spring resource prefix: Contrast the comments in this example with the previous example. -.[[testcontext-ctx-management-web-mocks]]Working with Web Mocks --- +[[testcontext-ctx-management-web-mocks]] +===== Working with Web Mocks + To provide comprehensive web testing support, the TestContext framework has a `ServletTestExecutionListener` that is enabled by default. When testing against a `WebApplicationContext`, this <> @@ -4395,7 +4605,6 @@ managed per test method by the `ServletTestExecutionListener`. //... } ---- --- [[testcontext-ctx-management-caching]] ===== Context Caching @@ -4458,8 +4667,8 @@ The size of the context cache is bounded with a default maximum size of 32. When maximum size is reached, a least recently used (LRU) eviction policy is used to evict and close stale contexts. You can configure the maximum size from the command line or a build script by setting a JVM system property named `spring.test.context.cache.maxSize`. As an -alternative, you can set the same property programmatically by using the -`SpringProperties` API. +alternative, you can set the same property via the +<> mechanism. Since having a large number of application contexts loaded within a given test suite can cause the suite to take an unnecessarily long time to run, it is often beneficial to @@ -4477,6 +4686,52 @@ context. Note that support for the `@DirtiesContext` annotation is provided by t `DirtiesContextBeforeModesTestExecutionListener` and the `DirtiesContextTestExecutionListener`, which are enabled by default. +.ApplicationContext lifecycle and console logging +[NOTE] +==== +When you need to debug a test executed with the Spring TestContext Framework, it can be +useful to analyze the console output (that is, output to the `SYSOUT` and `SYSERR` +streams). Some build tools and IDEs are able to associate console output with a given +test; however, some console output cannot be easily associated with a given test. + +With regard to console logging triggered by the Spring Framework itself or by components +registered in the `ApplicationContext`, it is important to understand the lifecycle of an +`ApplicationContext` that has been loaded by the Spring TestContext Framework within a +test suite. + +The `ApplicationContext` for a test is typically loaded when an instance of the test +class is being prepared -- for example, to perform dependency injection into `@Autowired` +fields of the test instance. This means that any console logging triggered during the +initialization of the `ApplicationContext` typically cannot be associated with an +individual test method. However, if the context is closed immediately before the +execution of a test method according to <> +semantics, a new instance of the context will be loaded just prior to execution of the +test method. In the latter scenario, an IDE or build tool may potentially associate +console logging with the individual test method. + +The `ApplicationContext` for a test can be closed via one of the following scenarios. + +* The context is closed according to `@DirtiesContext` semantics. +* The context is closed because it has been automatically evicted from the cache + according to the LRU eviction policy. +* The context is closed via a JVM shutdown hook when the JVM for the test suite + terminates. + +If the context is closed according to `@DirtiesContext` semantics after a particular test +method, an IDE or build tool may potentially associate console logging with the +individual test method. If the context is closed according to `@DirtiesContext` semantics +after a test class, any console logging triggered during the shutdown of the +`ApplicationContext` cannot be associated with an individual test method. Similarly, any +console logging triggered during the shutdown phase via a JVM shutdown hook cannot be +associated with an individual test method. + +When a Spring `ApplicationContext` is closed via a JVM shutdown hook, callbacks executed +during the shutdown phase are executed on a thread named `SpringContextShutdownHook`. So, +if you wish to disable console logging triggered when the `ApplicationContext` is closed +via a JVM shutdown hook, you may be able to register a custom filter with your logging +framework that allows you to ignore any logging initiated by that thread. +==== + [[testcontext-ctx-management-ctx-hierarchies]] ===== Context Hierarchies @@ -4504,7 +4759,7 @@ have different levels in a context hierarchy configured using different resource The remaining JUnit Jupiter based examples in this section show common configuration scenarios for integration tests that require the use of context hierarchies. -.Single test class with context hierarchy +**Single test class with context hierarchy** -- `ControllerIntegrationTests` represents a typical integration testing scenario for a Spring MVC web application by declaring a context hierarchy that consists of two levels, @@ -4550,8 +4805,7 @@ lowest context in the hierarchy). The following listing shows this configuration ---- -- - -.Class hierarchy with implicit parent context +**Class hierarchy with implicit parent context** -- The test classes in this example define a context hierarchy within a test class hierarchy. `AbstractWebTests` declares the configuration for a root @@ -4597,7 +4851,7 @@ configuration scenario: ---- -- -.Class hierarchy with merged context hierarchy configuration +**Class hierarchy with merged context hierarchy configuration** -- The classes in this example show the use of named hierarchy levels in order to merge the configuration for specific levels in a context hierarchy. `BaseTests` defines two levels @@ -4643,7 +4897,7 @@ The following listing shows this configuration scenario: ---- -- -.Class hierarchy with overridden context hierarchy configuration +**Class hierarchy with overridden context hierarchy configuration** -- In contrast to the previous example, this example demonstrates how to override the configuration for a given named level in a context hierarchy by setting the @@ -4847,8 +5101,7 @@ The preceding code listings use the same XML context file referenced by the `@ContextConfiguration` annotation (that is, `repository-config.xml`). The following shows this configuration: -[source,xml,indent=0] -[subs="verbatim,quotes"] +[source,xml,indent=0,subs="verbatim,quotes"] ---- @@ -5471,7 +5723,6 @@ following example shows the relevant annotations: [[testcontext-tx-false-positives]] .Avoid false positives when testing ORM code - [NOTE] ===== When you test application code that manipulates the state of a Hibernate session or JPA @@ -5593,6 +5844,86 @@ The following example shows matching methods for JPA: ---- ===== +[[testcontext-tx-orm-lifecycle-callbacks]] +.Testing ORM entity lifecycle callbacks +[NOTE] +===== +Similar to the note about avoiding <> +when testing ORM code, if your application makes use of entity lifecycle callbacks (also +known as entity listeners), make sure to flush the underlying unit of work within test +methods that run that code. Failing to _flush_ or _clear_ the underlying unit of work can +result in certain lifecycle callbacks not being invoked. + +For example, when using JPA, `@PostPersist`, `@PreUpdate`, and `@PostUpdate` callbacks +will not be called unless `entityManager.flush()` is invoked after an entity has been +saved or updated. Similarly, if an entity is already attached to the current unit of work +(associated with the current persistence context), an attempt to reload the entity will +not result in a `@PostLoad` callback unless `entityManager.clear()` is invoked before the +attempt to reload the entity. + +The following example shows how to flush the `EntityManager` to ensure that +`@PostPersist` callbacks are invoked when an entity is persisted. An entity listener with +a `@PostPersist` callback method has been registered for the `Person` entity used in the +example. + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- + // ... + + @Autowired + JpaPersonRepository repo; + + @PersistenceContext + EntityManager entityManager; + + @Transactional + @Test + void savePerson() { + // EntityManager#persist(...) results in @PrePersist but not @PostPersist + repo.save(new Person("Jane")); + + // Manual flush is required for @PostPersist callback to be invoked + entityManager.flush(); + + // Test code that relies on the @PostPersist callback + // having been invoked... + } + + // ... +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + // ... + + @Autowired + lateinit var repo: JpaPersonRepository + + @PersistenceContext + lateinit var entityManager: EntityManager + + @Transactional + @Test + fun savePerson() { + // EntityManager#persist(...) results in @PrePersist but not @PostPersist + repo.save(Person("Jane")) + + // Manual flush is required for @PostPersist callback to be invoked + entityManager.flush() + + // Test code that relies on the @PostPersist callback + // having been invoked... + } + + // ... +---- + +See +https://github.com/spring-projects/spring-framework/blob/main/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/JpaEntityListenerTests.java[JpaEntityListenerTests] +in the Spring Framework test suite for working examples using all JPA lifecycle callbacks. +===== + [[testcontext-executing-sql]] ==== Executing SQL Scripts @@ -6651,6 +6982,65 @@ classes by using `@ContextConfiguration`, `@TestExecutionListeners`, and so on a manually instrumenting your test class with a `TestContextManager`. See the source code of `AbstractTestNGSpringContextTests` for an example of how to instrument your test class. +[[testcontext-aot]] +==== Ahead of Time Support for Tests + +This chapter covers Spring's Ahead of Time (AOT) support for integration tests using the +Spring TestContext Framework. + +The testing support extends Spring's <> with the +following features. + +* Build-time detection of all integration tests in the current project that use the + TestContext framework to load an `ApplicationContext`. + - Provides explicit support for test classes based on JUnit Jupiter and JUnit 4 as well + as implicit support for TestNG and other testing frameworks that use Spring's core + testing annotations -- as long as the tests are run using a JUnit Platform + `TestEngine` that is registered for the current project. +* Build-time AOT processing: each unique test `ApplicationContext` in the current project + will be <>. +* Runtime AOT support: when executing in AOT runtime mode, a Spring integration test will + use an AOT-optimized `ApplicationContext` that participates transparently with the + <>. + +[WARNING] +==== +The `@ContextHierarchy` annotation is currently not supported in AOT mode. +==== + +To provide test-specific runtime hints for use within a GraalVM native image, you have +the following options. + +* Implement a custom + {api-spring-framework}/test/context/aot/TestRuntimeHintsRegistrar.html[`TestRuntimeHintsRegistrar`] + and register it globally via `META-INF/spring/aot.factories`. +* Implement a custom {api-spring-framework}/aot/hint/RuntimeHintsRegistrar.html[`RuntimeHintsRegistrar`] + and register it globally via `META-INF/spring/aot.factories` or locally on a test class + via {api-spring-framework}/context/annotation/ImportRuntimeHints.html[`@ImportRuntimeHints`]. +* Annotate a test class with {api-spring-framework}/aot/hint/annotation/Reflective.html[`@Reflective`] or + {api-spring-framework}/aot/hint/annotation/RegisterReflectionForBinding.html[`@RegisterReflectionForBinding`]. +* See <> for details on Spring's core runtime hints + and annotation support. + +[TIP] +==== +The `TestRuntimeHintsRegistrar` API serves as a companion to the core +`RuntimeHintsRegistrar` API. If you need to register global hints for testing support +that are not specific to particular test classes, favor implementing +`RuntimeHintsRegistrar` over the test-specific API. +==== + +If you implement a custom `ContextLoader`, it must implement +{api-spring-framework}/test/context/aot/AotContextLoader.html[`AotContextLoader`] in +order to provide AOT build-time processing and AOT runtime execution support. Note, +however, that all context loader implementations provided by the Spring Framework and +Spring Boot already implement `AotContextLoader`. + +If you implement a custom `TestExecutionListener`, it must implement +{api-spring-framework}/test/context/aot/AotTestExecutionListener.html[`AotTestExecutionListener`] +in order to participate in AOT processing. See the `SqlScriptsTestExecutionListener` in +the `spring-test` module for an example. + include::testing/testing-webtestclient.adoc[leveloffset=+2] @@ -6682,7 +7072,7 @@ do they involve any of the supporting `@InitBinder`, `@ModelAttribute`, or The Spring MVC Test framework, also known as `MockMvc`, aims to provide more complete testing for Spring MVC controllers without a running server. It does that by invoking -the `DispacherServlet` and passing +the `DispatcherServlet` and passing <> from the `spring-test` module which replicates the full Spring MVC request handling without a running server. @@ -6770,7 +7160,7 @@ To set up MockMvc through Spring configuration, use the following: @BeforeEach void setup(WebApplicationContext wac) { - this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); + this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); } // ... @@ -6940,6 +7330,8 @@ To perform requests that use any HTTP method, as the following example shows: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java ---- + // static import of MockMvcRequestBuilders.* + mockMvc.perform(post("/hotels/{id}", 42).accept(MediaType.APPLICATION_JSON)); ---- @@ -7065,12 +7457,15 @@ they must be specified on every request. [[spring-mvc-test-server-defining-expectations]] ===== Defining Expectations -You can define expectations by appending one or more `.andExpect(..)` calls after -performing a request, as the following example shows: +You can define expectations by appending one or more `andExpect(..)` calls after +performing a request, as the following example shows. As soon as one expectation fails, +no other expectations will be asserted. [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java ---- + // static import of MockMvcRequestBuilders.* and MockMvcResultMatchers.* + mockMvc.perform(get("/accounts/1")).andExpect(status().isOk()); ---- @@ -7080,10 +7475,25 @@ performing a request, as the following example shows: import org.springframework.test.web.servlet.get mockMvc.get("/accounts/1").andExpect { - status().isOk() + status { isOk() } } ---- +You can define multiple expectations by appending `andExpectAll(..)` after performing a +request, as the following example shows. In contrast to `andExpect(..)`, +`andExpectAll(..)` guarantees that all supplied expectations will be asserted and that +all failures will be tracked and reported. + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- + // static import of MockMvcRequestBuilders.* and MockMvcResultMatchers.* + + mockMvc.perform(get("/accounts/1")).andExpectAll( + status().isOk(), + content().contentType("application/json;charset=UTF-8")); +---- + `MockMvcResultMatchers.*` provides a number of expectations, some of which are further nested with more detailed expectations. @@ -7113,7 +7523,7 @@ The following test asserts that binding or validation failed: import org.springframework.test.web.servlet.post mockMvc.post("/persons").andExpect { - status().isOk() + status { isOk() } model { attributeHasErrors("person") } @@ -7141,7 +7551,7 @@ request. You can do so as follows, where `print()` is a static import from mockMvc.post("/persons").andDo { print() }.andExpect { - status().isOk() + status { isOk() } model { attributeHasErrors("person") } @@ -7171,7 +7581,7 @@ other expectations, as the following example shows: [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] .Kotlin ---- - var mvcResult = mockMvc.post("/persons").andExpect { status().isOk() }.andReturn() + var mvcResult = mockMvc.post("/persons").andExpect { status { isOk() } }.andReturn() // ... ---- @@ -7251,10 +7661,10 @@ If using MockMvc through the <>, there is nothing special to do t asynchronous requests work as the `WebTestClient` automatically does what is described in this section. -Servlet 3.0 asynchronous requests, -<>, work by exiting the Servlet container -thread and allowing the application to compute the response asynchronously, after which -an async dispatch is made to complete processing on a Servlet container thread. +Servlet asynchronous requests, <>, +work by exiting the Servlet container thread and allowing the application to compute +the response asynchronously, after which an async dispatch is made to complete +processing on a Servlet container thread. In Spring MVC Test, async requests can be tested by asserting the produced async value first, then manually performing the async dispatch, and finally verifying the response. @@ -7264,6 +7674,8 @@ or reactive type such as Reactor `Mono`: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java ---- + // static import of MockMvcRequestBuilders.* and MockMvcResultMatchers.* + @Test void test() throws Exception { MvcResult mvcResult = this.mockMvc.perform(get("/path")) @@ -7289,7 +7701,7 @@ or reactive type such as Reactor `Mono`: @Test fun test() { var mvcResult = mockMvc.get("/path").andExpect { - status().isOk() // <1> + status { isOk() } // <1> request { asyncStarted() } // <2> // TODO Remove unused generic parameter request { asyncResult("body") } // <3> @@ -7298,7 +7710,7 @@ or reactive type such as Reactor `Mono`: mockMvc.perform(asyncDispatch(mvcResult)) // <4> .andExpect { - status().isOk() // <5> + status { isOk() } // <5> content().string("body") } } @@ -7313,12 +7725,35 @@ or reactive type such as Reactor `Mono`: [[spring-mvc-test-vs-streaming-response]] ===== Streaming Responses -There are no options built into Spring MVC Test for container-less testing of streaming -responses. However you can test streaming requests through the <>. -This is also supported in Spring Boot where you can -{doc-spring-boot}/html/spring-boot-features.html#boot-features-testing-spring-boot-applications-testing-with-running-server[test a running server] -with `WebTestClient`. One extra advantage is the ability to use the `StepVerifier` from -project Reactor that allows declaring expectations on a stream of data. +The best way to test streaming responses such as Server-Sent Events is through the +<> which can be used as a test client to connect to a `MockMvc` instance +to perform tests on Spring MVC controllers without a running server. For example: + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- + WebTestClient client = MockMvcWebTestClient.bindToController(new SseController()).build(); + + FluxExchangeResult exchangeResult = client.get() + .uri("/persons") + .exchange() + .expectStatus().isOk() + .expectHeader().contentType("text/event-stream") + .returnResult(Person.class); + + // Use StepVerifier from Project Reactor to test the streaming response + + StepVerifier.create(exchangeResult.getResponseBody()) + .expectNext(new Person("N0"), new Person("N1"), new Person("N2")) + .expectNextCount(4) + .consumeNextWith(person -> assertThat(person.getName()).endsWith("7")) + .thenCancel() + .verify(); +---- + +`WebTestClient` can also connect to a live server and perform full end-to-end integration +tests. This is also supported in Spring Boot where you can +{doc-spring-boot}/html/spring-boot-features.html#boot-features-testing-spring-boot-applications-testing-with-running-server[test a running server]. [[spring-mvc-test-server-filters]] @@ -7392,9 +7827,9 @@ of testing even within the same project. ===== Further Examples The framework's own tests include -https://github.com/spring-projects/spring-framework/tree/master/spring-test/src/test/java/org/springframework/test/web/servlet/samples[ +{spring-framework-main-code}/spring-test/src/test/java/org/springframework/test/web/servlet/samples[ many sample tests] intended to show how to use MockMvc on its own or through the -https://github.com/spring-projects/spring-framework/tree/master/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client[ +{spring-framework-main-code}/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client[ WebTestClient]. Browse these examples for further ideas. @@ -7402,13 +7837,13 @@ WebTestClient]. Browse these examples for further ideas. ==== HtmlUnit Integration Spring provides integration between <> and -http://htmlunit.sourceforge.net/[HtmlUnit]. This simplifies performing end-to-end testing +https://htmlunit.sourceforge.io/[HtmlUnit]. This simplifies performing end-to-end testing when using HTML-based views. This integration lets you: * Easily test HTML pages by using tools such as - http://htmlunit.sourceforge.net/[HtmlUnit], + https://htmlunit.sourceforge.io/[HtmlUnit], https://www.seleniumhq.org[WebDriver], and - http://www.gebish.org/manual/current/#spock-junit-testng[Geb] without the need to + https://www.gebish.org/manual/current/#spock-junit-testng[Geb] without the need to deploy to a Servlet container. * Test JavaScript within pages. * Optionally, test using mock services to speed up testing. @@ -7704,7 +8139,7 @@ to create a message, as the following example shows: ---- Finally, we can verify that a new message was created successfully. The following -assertions use the https://joel-costigliola.github.io/assertj/[AssertJ] library: +assertions use the https://assertj.github.io/doc/[AssertJ] library: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -7735,11 +8170,11 @@ First, we no longer have to explicitly verify our form and then create a request looks like the form. Instead, we request the form, fill it out, and submit it, thereby significantly reducing the overhead. -Another important factor is that http://htmlunit.sourceforge.net/javascript.html[HtmlUnit +Another important factor is that https://htmlunit.sourceforge.io/javascript.html[HtmlUnit uses the Mozilla Rhino engine] to evaluate JavaScript. This means that we can also test the behavior of JavaScript within our pages. -See the http://htmlunit.sourceforge.net/gettingStarted.html[HtmlUnit documentation] for +See the https://htmlunit.sourceforge.io/gettingStarted.html[HtmlUnit documentation] for additional information about using HtmlUnit. [[spring-mvc-test-server-htmlunit-mah-advanced-builder]] @@ -8171,7 +8606,7 @@ annotation to look up our submit button with a `css` selector (*input[type=submi Finally, we can verify that a new message was created successfully. The following -assertions use the https://joel-costigliola.github.io/assertj/[AssertJ] assertion library: +assertions use the https://assertj.github.io/doc/[AssertJ] assertion library: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -8182,8 +8617,8 @@ assertions use the https://joel-costigliola.github.io/assertj/[AssertJ] assertio [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] .Kotlin ---- - assertThat(viewMessagePage.message.isEqualTo(expectedMessage) - assertThat(viewMessagePage.success.isEqualTo("Successfully created a new message") + assertThat(viewMessagePage.message).isEqualTo(expectedMessage) + assertThat(viewMessagePage.success).isEqualTo("Successfully created a new message") ---- We can see that our `ViewMessagePage` lets us interact with our custom domain model. For @@ -8346,7 +8781,7 @@ TIP: For additional information on creating a `MockMvc` instance, see ===== MockMvc and Geb In the previous section, we saw how to use MockMvc with WebDriver. In this section, we -use http://www.gebish.org/[Geb] to make our tests even Groovy-er. +use https://www.gebish.org/[Geb] to make our tests even Groovy-er. [[spring-mvc-test-server-htmlunit-geb-why]] ====== Why Geb and MockMvc? @@ -8449,7 +8884,7 @@ if we were at the wrong page. Next, we create a `content` closure that specifies all the areas of interest within the page. We can use a -http://www.gebish.org/manual/current/#the-jquery-ish-navigator-api[jQuery-ish Navigator +https://www.gebish.org/manual/current/#the-jquery-ish-navigator-api[jQuery-ish Navigator API] to select the content in which we are interested. Finally, we can verify that a new message was created successfully, as follows: @@ -8466,7 +8901,7 @@ message == expectedMessage ---- For further details on how to get the most out of Geb, see -http://www.gebish.org/manual/current/[The Book of Geb] user's manual. +https://www.gebish.org/manual/current/[The Book of Geb] user's manual. [[spring-mvc-test-client]] @@ -8603,7 +9038,7 @@ configuration. Check for the support for code completion on static members. ==== Further Examples of Client-side REST Tests Spring MVC Test's own tests include -https://github.com/spring-projects/spring-framework/tree/master/spring-test/src/test/java/org/springframework/test/web/client/samples[example +{spring-framework-main-code}/spring-test/src/test/java/org/springframework/test/web/client/samples[example tests] of client-side REST tests. @@ -8617,7 +9052,7 @@ See the following resources for more information about testing: * https://testng.org/[TestNG]: A testing framework inspired by JUnit with added support for test groups, data-driven testing, distributed testing, and other features. Supported in the <> -* https://joel-costigliola.github.io/assertj/[AssertJ]: "`Fluent assertions for Java`", +* https://assertj.github.io/doc/[AssertJ]: "`Fluent assertions for Java`", including support for Java 8 lambdas, streams, and other features. * https://en.wikipedia.org/wiki/Mock_Object[Mock Objects]: Article in Wikipedia. * http://www.mockobjects.com/[MockObjects.com]: Web site dedicated to mock objects, a diff --git a/src/docs/asciidoc/testing/testing-webtestclient.adoc b/framework-docs/src/docs/asciidoc/testing/testing-webtestclient.adoc similarity index 94% rename from src/docs/asciidoc/testing/testing-webtestclient.adoc rename to framework-docs/src/docs/asciidoc/testing/testing-webtestclient.adoc index 09542f2dc50c..4335760e1048 100644 --- a/src/docs/asciidoc/testing/testing-webtestclient.adoc +++ b/framework-docs/src/docs/asciidoc/testing/testing-webtestclient.adoc @@ -1,7 +1,5 @@ [[webtestclient]] = WebTestClient -:doc-root: https://docs.spring.io -:api-spring-framework: {doc-root}/spring-framework/docs/{spring-version}/javadoc-api/org/springframework `WebTestClient` is an HTTP client designed for testing server applications. It wraps Spring's <> and uses it to perform requests @@ -47,7 +45,7 @@ to handle requests: ---- For Spring MVC, use the following which delegates to the -{api-spring-framework}/https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/test/web/servlet/setup/StandaloneMockMvcBuilder.html[StandaloneMockMvcBuilder] +{api-spring-framework}/test/web/servlet/setup/StandaloneMockMvcBuilder.html[StandaloneMockMvcBuilder] to load infrastructure equivalent to the <>, registers the given controller(s), and creates an instance of <> to handle requests: @@ -264,19 +262,36 @@ To assert the response status and headers, use the following: .Java ---- client.get().uri("/persons/1") - .accept(MediaType.APPLICATION_JSON) - .exchange() - .expectStatus().isOk() - .expectHeader().contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().isOk() + .expectHeader().contentType(MediaType.APPLICATION_JSON); ---- [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] .Kotlin ---- client.get().uri("/persons/1") - .accept(MediaType.APPLICATION_JSON) - .exchange() - .expectStatus().isOk() - .expectHeader().contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().isOk() + .expectHeader().contentType(MediaType.APPLICATION_JSON) +---- + +If you would like for all expectations to be asserted even if one of them fails, you can +use `expectAll(..)` instead of multiple chained `expect*(..)` calls. This feature is +similar to the _soft assertions_ support in AssertJ and the `assertAll()` support in +JUnit Jupiter. + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- + client.get().uri("/persons/1") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectAll( + spec -> spec.expectStatus().isOk(), + spec -> spec.expectHeader().contentType(MediaType.APPLICATION_JSON) + ); ---- You can then choose to decode the response body through one of the following: diff --git a/src/docs/asciidoc/web-reactive.adoc b/framework-docs/src/docs/asciidoc/web-reactive.adoc similarity index 82% rename from src/docs/asciidoc/web-reactive.adoc rename to framework-docs/src/docs/asciidoc/web-reactive.adoc index f834d54abc5b..c199e90e08e3 100644 --- a/src/docs/asciidoc/web-reactive.adoc +++ b/framework-docs/src/docs/asciidoc/web-reactive.adoc @@ -1,7 +1,5 @@ [[spring-web-reactive]] = Web on Reactive Stack -:doc-root: https://docs.spring.io -:api-spring-framework: {doc-root}/spring-framework/docs/{spring-version}/javadoc-api/org/springframework :toc: left :toclevels: 4 :tabsize: 4 @@ -9,7 +7,7 @@ This part of the documentation covers support for reactive-stack web applications built on a https://www.reactive-streams.org/[Reactive Streams] API to run on non-blocking -servers, such as Netty, Undertow, and Servlet 3.1+ containers. Individual chapters cover +servers, such as Netty, Undertow, and Servlet containers. Individual chapters cover the <> framework, the reactive <>, support for <>, and <>. For Servlet-stack web applications, @@ -19,6 +17,18 @@ include::web/webflux.adoc[leveloffset=+1] include::web/webflux-webclient.adoc[leveloffset=+1] + +[[webflux-http-interface-client]] +== HTTP Interface Client + +The Spring Frameworks lets you define an HTTP service as a Java interface with HTTP +exchange methods. You can then generate a proxy that implements this interface and +performs the exchanges. This helps to simplify HTTP remote access and provides additional +flexibility for to choose an API style such as synchronous or reactive. + +See <> for details. + + include::web/webflux-websocket.adoc[leveloffset=+1] @@ -36,8 +46,6 @@ discussion of mock objects. response objects to provide support for testing WebFlux applications without an HTTP server. You can use the `WebTestClient` for end-to-end integration tests, too. - - include::rsocket.adoc[leveloffset=+1] @@ -57,14 +65,8 @@ For annotated controllers, WebFlux transparently adapts to the reactive library the application. This is done with the help of the {api-spring-framework}/core/ReactiveAdapterRegistry.html[`ReactiveAdapterRegistry`], which provides pluggable support for reactive library and other asynchronous types. The registry -has built-in support for RxJava 2/3, RxJava 1 (via RxJava Reactive Streams bridge), and -`CompletableFuture`, but you can register others, too. - -[NOTE] -==== -As of Spring Framework 5.3, support for RxJava 1 is deprecated. -==== - +has built-in support for RxJava 3, Kotlin coroutines and SmallRye Mutiny, but you can +register others, too. For functional APIs (such as <>, the `WebClient`, and others), the general rules for WebFlux APIs apply -- `Flux` and `Mono` as return values and a Reactive Streams diff --git a/src/docs/asciidoc/web.adoc b/framework-docs/src/docs/asciidoc/web.adoc similarity index 83% rename from src/docs/asciidoc/web.adoc rename to framework-docs/src/docs/asciidoc/web.adoc index aed854cfbb12..3aeb44518610 100644 --- a/src/docs/asciidoc/web.adoc +++ b/framework-docs/src/docs/asciidoc/web.adoc @@ -1,7 +1,5 @@ [[spring-web]] = Web on Servlet Stack -:doc-root: https://docs.spring.io -:api-spring-framework: {doc-root}/spring-framework/docs/{spring-version}/javadoc-api/org/springframework :toc: left :toclevels: 4 :tabsize: 4 diff --git a/framework-docs/src/docs/asciidoc/web/integration.adoc b/framework-docs/src/docs/asciidoc/web/integration.adoc new file mode 100644 index 000000000000..42c53776662a --- /dev/null +++ b/framework-docs/src/docs/asciidoc/web/integration.adoc @@ -0,0 +1,200 @@ +[[web-integration]] += Other Web Frameworks + +This chapter details Spring's integration with third-party web frameworks. + +One of the core value propositions of the Spring Framework is that of enabling +_choice_. In a general sense, Spring does not force you to use or buy into any +particular architecture, technology, or methodology (although it certainly recommends +some over others). This freedom to pick and choose the architecture, technology, or +methodology that is most relevant to a developer and their development team is +arguably most evident in the web area, where Spring provides its own web frameworks +(<> and <>) while, at the same time, +supporting integration with a number of popular third-party web frameworks. + + + + +[[web-integration-common]] +== Common Configuration + +Before diving into the integration specifics of each supported web framework, let us +first take a look at common Spring configuration that is not specific to any one web +framework. (This section is equally applicable to Spring's own web framework variants.) + +One of the concepts (for want of a better word) espoused by Spring's lightweight +application model is that of a layered architecture. Remember that in a "`classic`" +layered architecture, the web layer is but one of many layers. It serves as one of the +entry points into a server-side application, and it delegates to service objects +(facades) that are defined in a service layer to satisfy business-specific (and +presentation-technology agnostic) use cases. In Spring, these service objects, any other +business-specific objects, data-access objects, and others exist in a distinct "`business +context`", which contains no web or presentation layer objects (presentation objects, +such as Spring MVC controllers, are typically configured in a distinct "`presentation +context`"). This section details how you can configure a Spring container (a +`WebApplicationContext`) that contains all of the 'business beans' in your application. + +Moving on to specifics, all you need to do is declare a +{api-spring-framework}/web/context/ContextLoaderListener.html[`ContextLoaderListener`] +in the standard Jakarta EE servlet `web.xml` file of your web application and add a +`contextConfigLocation` section (in the same file) that defines which +set of Spring XML configuration files to load. + +Consider the following `` configuration: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + org.springframework.web.context.ContextLoaderListener + +---- + +Further consider the following `` configuration: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + contextConfigLocation + /WEB-INF/applicationContext*.xml + +---- + +If you do not specify the `contextConfigLocation` context parameter, the +`ContextLoaderListener` looks for a file called `/WEB-INF/applicationContext.xml` to +load. Once the context files are loaded, Spring creates a +{api-spring-framework}/web/context/WebApplicationContext.html[`WebApplicationContext`] +object based on the bean definitions and stores it in the `ServletContext` of the web +application. + +All Java web frameworks are built on top of the Servlet API, so you can use the +following code snippet to get access to this "`business context`" `ApplicationContext` +created by the `ContextLoaderListener`. + +The following example shows how to get the `WebApplicationContext`: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext); +---- + +The +{api-spring-framework}/web/context/support/WebApplicationContextUtils.html[`WebApplicationContextUtils`] +class is for convenience, so you need not remember the name of the `ServletContext` +attribute. Its `getWebApplicationContext()` method returns `null` if an object +does not exist under the `WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE` +key. Rather than risk getting `NullPointerExceptions` in your application, it is better +to use the `getRequiredWebApplicationContext()` method. This method throws an exception +when the `ApplicationContext` is missing. + +Once you have a reference to the `WebApplicationContext`, you can retrieve beans by their +name or type. Most developers retrieve beans by name and then cast them to one of their +implemented interfaces. + +Fortunately, most of the frameworks in this section have simpler ways of looking up beans. +Not only do they make it easy to get beans from a Spring container, but they also let you +use dependency injection on their controllers. Each web framework section has more detail +on its specific integration strategies. + + + + +[[jsf]] +== JSF + +JavaServer Faces (JSF) is the JCP's standard component-based, event-driven web +user interface framework. It is an official part of the Jakarta EE umbrella but also +individually usable, e.g. through embedding Mojarra or MyFaces within Tomcat. + +Please note that recent versions of JSF became closely tied to CDI infrastructure +in application servers, with some new JSF functionality only working in such an +environment. Spring's JSF support is not actively evolved anymore and primarily +exists for migration purposes when modernizing older JSF-based applications. + +The key element in Spring's JSF integration is the JSF `ELResolver` mechanism. + + + +[[jsf-springbeanfaceselresolver]] +=== Spring Bean Resolver + +`SpringBeanFacesELResolver` is a JSF compliant `ELResolver` implementation, +integrating with the standard Unified EL as used by JSF and JSP. It delegates to +Spring's "`business context`" `WebApplicationContext` first and then to the +default resolver of the underlying JSF implementation. + +Configuration-wise, you can define `SpringBeanFacesELResolver` in your JSF +`faces-context.xml` file, as the following example shows: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + org.springframework.web.jsf.el.SpringBeanFacesELResolver + ... + + +---- + + + +[[jsf-facescontextutils]] +=== Using `FacesContextUtils` + +A custom `ELResolver` works well when mapping your properties to beans in +`faces-config.xml`, but, at times, you may need to explicitly grab a bean. +The {api-spring-framework}/web/jsf/FacesContextUtils.html[`FacesContextUtils`] +class makes this easy. It is similar to `WebApplicationContextUtils`, except that +it takes a `FacesContext` parameter rather than a `ServletContext` parameter. + +The following example shows how to use `FacesContextUtils`: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + ApplicationContext ctx = FacesContextUtils.getWebApplicationContext(FacesContext.getCurrentInstance()); +---- + + + + +[[struts]] +== Apache Struts 2.x + +Invented by Craig McClanahan, https://struts.apache.org[Struts] is an open-source project +hosted by the Apache Software Foundation. At the time, it greatly simplified the +JSP/Servlet programming paradigm and won over many developers who were using proprietary +frameworks. It simplified the programming model, it was open source (and thus free as in +beer), and it had a large community, which let the project grow and become popular among +Java web developers. + +As a successor to the original Struts 1.x, check out Struts 2.x and the Struts-provided +https://struts.apache.org/release/2.3.x/docs/spring-plugin.html[Spring Plugin] for the +built-in Spring integration. + + + + +[[tapestry]] +== Apache Tapestry 5.x + +https://tapestry.apache.org/[Tapestry] is a ""Component oriented framework for creating +dynamic, robust, highly scalable web applications in Java."" + +While Spring has its own <>, there are a number of unique +advantages to building an enterprise Java application by using a combination of Tapestry +for the web user interface and the Spring container for the lower layers. + +For more information, see Tapestry's dedicated +https://tapestry.apache.org/integrating-with-spring-framework.html[integration module for Spring]. + + + + +[[web-integration-resources]] +== Further Resources + +The following links go to further resources about the various web frameworks described in +this chapter. + +* The https://www.oracle.com/technetwork/java/javaee/javaserverfaces-139869.html[JSF] homepage +* The https://struts.apache.org/[Struts] homepage +* The https://tapestry.apache.org/[Tapestry] homepage diff --git a/framework-docs/src/docs/asciidoc/web/web-data-binding-model-design.adoc b/framework-docs/src/docs/asciidoc/web/web-data-binding-model-design.adoc new file mode 100644 index 000000000000..352e63d3c6f3 --- /dev/null +++ b/framework-docs/src/docs/asciidoc/web/web-data-binding-model-design.adoc @@ -0,0 +1,95 @@ +In the context of web applications, _data binding_ involves the binding of HTTP request +parameters (that is, form data or query parameters) to properties in a model object and +its nested objects. + +Only `public` properties following the +https://www.oracle.com/java/technologies/javase/javabeans-spec.html[JavaBeans naming conventions] +are exposed for data binding — for example, `public String getFirstName()` and +`public void setFirstName(String)` methods for a `firstName` property. + +TIP: The model object, and its nested object graph, is also sometimes referred to as a +_command object_, _form-backing object_, or _POJO_ (Plain Old Java Object). + +By default, Spring permits binding to all public properties in the model object graph. +This means you need to carefully consider what public properties the model has, since a +client could target any public property path, even some that are not expected to be +targeted for a given use case. + +For example, given an HTTP form data endpoint, a malicious client could supply values for +properties that exist in the model object graph but are not part of the HTML form +presented in the browser. This could lead to data being set on the model object and any +of its nested objects, that is not expected to be updated. + +The recommended approach is to use a _dedicated model object_ that exposes only +properties that are relevant for the form submission. For example, on a form for changing +a user's email address, the model object should declare a minimum set of properties such +as in the following `ChangeEmailForm`. + +[source,java,indent=0,subs="verbatim,quotes"] +---- + public class ChangeEmailForm { + + private String oldEmailAddress; + private String newEmailAddress; + + public void setOldEmailAddress(String oldEmailAddress) { + this.oldEmailAddress = oldEmailAddress; + } + + public String getOldEmailAddress() { + return this.oldEmailAddress; + } + + public void setNewEmailAddress(String newEmailAddress) { + this.newEmailAddress = newEmailAddress; + } + + public String getNewEmailAddress() { + return this.newEmailAddress; + } + + } +---- + +If you cannot or do not want to use a _dedicated model object_ for each data +binding use case, you **must** limit the properties that are allowed for data binding. +Ideally, you can achieve this by registering _allowed field patterns_ via the +`setAllowedFields()` method on `WebDataBinder`. + +For example, to register allowed field patterns in your application, you can implement an +`@InitBinder` method in a `@Controller` or `@ControllerAdvice` component as shown below: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Controller + public class ChangeEmailController { + + @InitBinder + void initBinder(WebDataBinder binder) { + binder.setAllowedFields("oldEmailAddress", "newEmailAddress"); + } + + // @RequestMapping methods, etc. + + } +---- + +In addition to registering allowed patterns, it is also possible to register _disallowed +field patterns_ via the `setDisallowedFields()` method in `DataBinder` and its subclasses. +Please note, however, that an "allow list" is safer than a "deny list". Consequently, +`setAllowedFields()` should be favored over `setDisallowedFields()`. + +Note that matching against allowed field patterns is case-sensitive; whereas, matching +against disallowed field patterns is case-insensitive. In addition, a field matching a +disallowed pattern will not be accepted even if it also happens to match a pattern in the +allowed list. + +[WARNING] +==== +It is extremely important to properly configure allowed and disallowed field patterns +when exposing your domain model directly for data binding purposes. Otherwise, it is a +big security risk. + +Furthermore, it is strongly recommended that you do **not** use types from your domain +model such as JPA or Hibernate entities as the model object in data binding scenarios. +==== diff --git a/src/docs/asciidoc/web/web-uris.adoc b/framework-docs/src/docs/asciidoc/web/web-uris.adoc similarity index 91% rename from src/docs/asciidoc/web/web-uris.adoc rename to framework-docs/src/docs/asciidoc/web/web-uris.adoc index 29b3a91ef8c0..593fd97ffe07 100644 --- a/src/docs/asciidoc/web/web-uris.adoc +++ b/framework-docs/src/docs/asciidoc/web/web-uris.adoc @@ -1,4 +1,4 @@ -[[web-uricomponents]] +[id={chapter}.web-uricomponents] = UriComponents [.small]#Spring MVC and Spring WebFlux# @@ -82,7 +82,7 @@ as the following example shows: .build("Westin", "123") ---- -You shorter it further still with a full URI template, as the following example shows: +You can shorten it further still with a full URI template, as the following example shows: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -94,19 +94,18 @@ You shorter it further still with a full URI template, as the following example [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] .Kotlin ---- -val uri = UriComponentsBuilder - .fromUriString("https://example.com/hotels/%7Bhotel%7D?q={q}") - .build("Westin", "123") + val uri = UriComponentsBuilder + .fromUriString("https://example.com/hotels/%7Bhotel%7D?q={q}") + .build("Westin", "123") ---- - -[[web-uribuilder]] +[id={chapter}.web-uribuilder] = UriBuilder [.small]#Spring MVC and Spring WebFlux# -<> implements `UriBuilder`. You can create a +<<{chapter}.web-uricomponents, `UriComponentsBuilder`>> implements `UriBuilder`. You can create a `UriBuilder`, in turn, with a `UriBuilderFactory`. Together, `UriBuilderFactory` and `UriBuilder` provide a pluggable mechanism to build URIs from URI templates, based on shared configuration, such as a base URL, encoding preferences, and other details. @@ -194,8 +193,7 @@ that holds configuration and preferences, as the following example shows: ---- - -[[web-uri-encoding]] +[id={chapter}.web-uri-encoding] = URI Encoding [.small]#Spring MVC and Spring WebFlux# @@ -214,8 +212,10 @@ TIP: Consider ";", which is legal in a path but has reserved meaning. The first replaces ";", since it is a legal character in a path. For most cases, the first option is likely to give the expected result, because it treats URI -variables as opaque data to be fully encoded, while option 2 is useful only if -URI variables intentionally contain reserved characters. +variables as opaque data to be fully encoded, while the second option is useful if URI +variables do intentionally contain reserved characters. The second option is also useful +when not expanding URI variables at all since that will also encode anything that +incidentally looks like a URI variable. The following example uses the first option: @@ -250,7 +250,7 @@ as the following example shows: ---- URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}") .queryParam("q", "{q}") - .build("New York", "foo+bar") + .build("New York", "foo+bar"); ---- [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] .Kotlin @@ -265,18 +265,18 @@ You can shorten it further still with a full URI template, as the following exam [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java ---- - URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}?q={q}") - .build("New York", "foo+bar") + URI uri = UriComponentsBuilder.fromUriString("/hotel list/{city}?q={q}") + .build("New York", "foo+bar"); ---- [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] .Kotlin ---- - val uri = UriComponentsBuilder.fromPath("/hotel list/{city}?q={q}") + val uri = UriComponentsBuilder.fromUriString("/hotel list/{city}?q={q}") .build("New York", "foo+bar") ---- The `WebClient` and the `RestTemplate` expand and encode URI templates internally through -the `UriBuilderFactory` strategy. Both can be configured with a custom strategy. +the `UriBuilderFactory` strategy. Both can be configured with a custom strategy, as the following example shows: [source,java,indent=0,subs="verbatim,quotes",role="primary"] @@ -318,7 +318,7 @@ the approach to encoding, based on one of the below encoding modes: the first option in the earlier list, to pre-encode the URI template and strictly encode URI variables when expanded. * `VALUES_ONLY`: Does not encode the URI template and, instead, applies strict encoding -to URI variables through `UriUtils#encodeUriUriVariables` prior to expanding them into the +to URI variables through `UriUtils#encodeUriVariables` prior to expanding them into the template. * `URI_COMPONENT`: Uses `UriComponents#encode()`, corresponding to the second option in the earlier list, to encode URI component value _after_ URI variables are expanded. diff --git a/src/docs/asciidoc/web/webflux-cors.adoc b/framework-docs/src/docs/asciidoc/web/webflux-cors.adoc similarity index 98% rename from src/docs/asciidoc/web/webflux-cors.adoc rename to framework-docs/src/docs/asciidoc/web/webflux-cors.adoc index 601120cf42bf..91890ddb1f3b 100644 --- a/src/docs/asciidoc/web/webflux-cors.adoc +++ b/framework-docs/src/docs/asciidoc/web/webflux-cors.adoc @@ -1,5 +1,6 @@ [[webflux-cors]] = CORS +:doc-spring-security: {doc-root}/spring-security/reference [.small]#<># Spring WebFlux lets you handle CORS (Cross-Origin Resource Sharing). This section @@ -309,9 +310,8 @@ You can apply CORS support through the built-in good fit with <>. NOTE: If you try to use the `CorsFilter` with Spring Security, keep in mind that Spring -Security has -https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#cors[built-in support] -for CORS. +Security has {doc-spring-security}/servlet/integrations/cors.html[built-in support] for +CORS. To configure the filter, you can declare a `CorsWebFilter` bean and pass a `CorsConfigurationSource` to its constructor, as the following example shows: diff --git a/src/docs/asciidoc/web/webflux-functional.adoc b/framework-docs/src/docs/asciidoc/web/webflux-functional.adoc similarity index 90% rename from src/docs/asciidoc/web/webflux-functional.adoc rename to framework-docs/src/docs/asciidoc/web/webflux-functional.adoc index 9d504c0bc883..a7130097bab9 100644 --- a/src/docs/asciidoc/web/webflux-functional.adoc +++ b/framework-docs/src/docs/asciidoc/web/webflux-functional.adoc @@ -40,7 +40,7 @@ as the following example shows: PersonRepository repository = ... PersonHandler handler = new PersonHandler(repository); - RouterFunction route = route() + RouterFunction route = route() <1> .GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) .GET("/person", accept(APPLICATION_JSON), handler::listPeople) .POST("/person", handler::createPerson) @@ -64,6 +64,7 @@ as the following example shows: } } ---- +<1> Create router using `route()`. [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] .Kotlin @@ -145,7 +146,7 @@ val string = request.awaitBody() The following example extracts the body to a `Flux` (or a `Flow` in Kotlin), -where `Person` objects are decoded from someserialized form, such as JSON or XML: +where `Person` objects are decoded from some serialized form, such as JSON or XML: [source,java,role="primary"] .Java @@ -181,7 +182,7 @@ The following example shows how to access form data: [source,java,role="primary"] .Java ---- -Mono map = request.formData(); +Mono> map = request.formData(); ---- [source,kotlin,role="secondary"] .Kotlin @@ -194,7 +195,7 @@ The following example shows how to access multipart data as a map: [source,java,role="primary"] .Java ---- -Mono map = request.multipartData(); +Mono> map = request.multipartData(); ---- [source,kotlin,role="secondary"] .Kotlin @@ -202,20 +203,63 @@ Mono map = request.multipartData(); val map = request.awaitMultipartData() ---- -The following example shows how to access multiparts, one at a time, in streaming fashion: +The following example shows how to access multipart data, one at a time, in streaming fashion: -[source,java,role="primary"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java ---- -Flux parts = request.body(BodyExtractors.toParts()); +Flux allPartEvents = request.bodyToFlux(PartEvent.class); +allPartsEvents.windowUntil(PartEvent::isLast) + .concatMap(p -> p.switchOnFirst((signal, partEvents) -> { + if (signal.hasValue()) { + PartEvent event = signal.get(); + if (event instanceof FormPartEvent formEvent) { + String value = formEvent.value(); + // handle form field + } + else if (event instanceof FilePartEvent fileEvent) { + String filename = fileEvent.filename(); + Flux contents = partEvents.map(PartEvent::content); + // handle file upload + } + else { + return Mono.error(new RuntimeException("Unexpected event: " + event)); + } + } + else { + return partEvents; // either complete or error signal + } + })); ---- -[source,kotlin,role="secondary"] + +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] .Kotlin ---- -val parts = request.body(BodyExtractors.toParts()).asFlow() +val parts = request.bodyToFlux() +allPartsEvents.windowUntil(PartEvent::isLast) + .concatMap { + it.switchOnFirst { signal, partEvents -> + if (signal.hasValue()) { + val event = signal.get() + if (event is FormPartEvent) { + val value: String = event.value(); + // handle form field + } else if (event is FilePartEvent) { + val filename: String = event.filename(); + val contents: Flux = partEvents.map(PartEvent::content); + // handle file upload + } else { + return Mono.error(RuntimeException("Unexpected event: " + event)); + } + } else { + return partEvents; // either complete or error signal + } + } + } +} ---- - +Note that the body contents of the `PartEvent` objects must be completely consumed, relayed, or released to avoid memory leaks. [[webflux-fn-response]] === ServerResponse @@ -505,6 +549,8 @@ The example shown above also uses two request predicates, as the builder uses Router functions are evaluated in order: if the first route does not match, the second is evaluated, and so on. Therefore, it makes sense to declare more specific routes before general ones. +This is also important when registering router functions as Spring beans, as will +be described later. Note that this behavior is different from the annotation-based programming model, where the "most specific" controller method is picked automatically. @@ -588,10 +634,10 @@ RouterFunction route = route() .path("/person", builder -> builder // <1> .GET("/{id}", accept(APPLICATION_JSON), handler::getPerson) .GET(accept(APPLICATION_JSON), handler::listPeople) - .POST("/person", handler::createPerson)) + .POST(handler::createPerson)) .build(); ---- -<1> Note that second parameter of `path` is a consumer that takes the a router builder. +<1> Note that second parameter of `path` is a consumer that takes the router builder. [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] .Kotlin @@ -600,7 +646,7 @@ RouterFunction route = route() "/person".nest { GET("/{id}", accept(APPLICATION_JSON), handler::getPerson) GET(accept(APPLICATION_JSON), handler::listPeople) - POST("/person", handler::createPerson) + POST(handler::createPerson) } } ---- @@ -618,7 +664,7 @@ We can further improve by using the `nest` method together with `accept`: .nest(accept(APPLICATION_JSON), b2 -> b2 .GET("/{id}", handler::getPerson) .GET(handler::listPeople)) - .POST("/person", handler::createPerson)) + .POST(handler::createPerson)) .build(); ---- [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] @@ -629,7 +675,7 @@ We can further improve by using the `nest` method together with `accept`: accept(APPLICATION_JSON).nest { GET("/{id}", handler::getPerson) GET(handler::listPeople) - POST("/person", handler::createPerson) + POST(handler::createPerson) } } } @@ -656,8 +702,8 @@ components required to process requests. The WebFlux Java configuration declares infrastructure components to support functional endpoints: * `RouterFunctionMapping`: Detects one or more `RouterFunction` beans in the Spring -configuration, combines them through `RouterFunction.andOther`, and routes requests to the -resulting composed `RouterFunction`. +configuration, <>, combines them through +`RouterFunction.andOther`, and routes requests to the resulting composed `RouterFunction`. * `HandlerFunctionAdapter`: Simple adapter that lets `DispatcherHandler` invoke a `HandlerFunction` that was mapped to a request. * `ServerResponseResultHandler`: Handles the result from the invocation of a @@ -764,7 +810,7 @@ For instance, consider the following example: .before(request -> ServerRequest.from(request) // <1> .header("X-RequestHeader", "Value") .build())) - .POST("/person", handler::createPerson)) + .POST(handler::createPerson)) .after((request, response) -> logResponse(response)) // <2> .build(); ---- @@ -782,7 +828,7 @@ For instance, consider the following example: ServerRequest.from(it) .header("X-RequestHeader", "Value").build() } - POST("/person", handler::createPerson) + POST(handler::createPerson) after { _, response -> // <2> logResponse(response) } @@ -813,7 +859,7 @@ The following example shows how to do so: .nest(accept(APPLICATION_JSON), b2 -> b2 .GET("/{id}", handler::getPerson) .GET(handler::listPeople)) - .POST("/person", handler::createPerson)) + .POST(handler::createPerson)) .filter((request, next) -> { if (securityManager.allowAccessTo(request.path())) { return next.handle(request); @@ -833,7 +879,7 @@ The following example shows how to do so: ("/person" and accept(APPLICATION_JSON)).nest { GET("/{id}", handler::getPerson) GET("", handler::listPeople) - POST("/person", handler::createPerson) + POST(handler::createPerson) filter { request, next -> if (securityManager.allowAccessTo(request.path())) { next(request) diff --git a/src/docs/asciidoc/web/webflux-view.adoc b/framework-docs/src/docs/asciidoc/web/webflux-view.adoc similarity index 97% rename from src/docs/asciidoc/web/webflux-view.adoc rename to framework-docs/src/docs/asciidoc/web/webflux-view.adoc index 0321f1fd1dfe..04b9f8c1a0e1 100644 --- a/src/docs/asciidoc/web/webflux-view.adoc +++ b/framework-docs/src/docs/asciidoc/web/webflux-view.adoc @@ -26,7 +26,7 @@ configuration involves a few bean declarations, such as `SpringResourceTemplateResolver`, `SpringWebFluxTemplateEngine`, and `ThymeleafReactiveViewResolver`. For more details, see https://www.thymeleaf.org/documentation.html[Thymeleaf+Spring] and the WebFlux integration -http://forum.thymeleaf.org/Thymeleaf-3-0-8-JUST-PUBLISHED-td4030687.html[announcement]. +https://web.archive.org/web/20210623051330/http%3A//forum.thymeleaf.org/Thymeleaf-3-0-8-JUST-PUBLISHED-td4030687.html[announcement]. @@ -232,7 +232,7 @@ Java 8+. Using the latest update release available is highly recommended. line should be added for Kotlin script support. See https://github.com/sdeleuze/kotlin-script-templating[this example] for more detail. -You need to have the script templating library. One way to do that for Javascript is +You need to have the script templating library. One way to do that for JavaScript is through https://www.webjars.org/[WebJars]. @@ -380,8 +380,8 @@ The following example shows how compile a template: ---- Check out the Spring Framework unit tests, -https://github.com/spring-projects/spring-framework/tree/master/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/script[Java], and -https://github.com/spring-projects/spring-framework/tree/master/spring-webflux/src/test/resources/org/springframework/web/reactive/result/view/script[resources], +{spring-framework-main-code}/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/script[Java], and +{spring-framework-main-code}/spring-webflux/src/test/resources/org/springframework/web/reactive/result/view/script[resources], for more configuration examples. diff --git a/src/docs/asciidoc/web/webflux-webclient.adoc b/framework-docs/src/docs/asciidoc/web/webflux-webclient.adoc similarity index 83% rename from src/docs/asciidoc/web/webflux-webclient.adoc rename to framework-docs/src/docs/asciidoc/web/webflux-webclient.adoc index 124b21acfb24..e74d6f9d12e5 100644 --- a/src/docs/asciidoc/web/webflux-webclient.adoc +++ b/framework-docs/src/docs/asciidoc/web/webflux-webclient.adoc @@ -12,6 +12,7 @@ decode request and response content on the server side. support for the following: * https://github.com/reactor/reactor-netty[Reactor Netty] +* https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpClient.html[JDK HttpClient] * https://github.com/jetty-project/jetty-reactive-httpclient[Jetty Reactive HttpClient] * https://hc.apache.org/index.html[Apache HttpComponents] * Others can be plugged via `ClientHttpConnector`. @@ -89,7 +90,7 @@ modified copy as follows: === MaxInMemorySize Codecs have <> for buffering data in -memory to avoid application memory issues. By the default those are set to 256KB. +memory to avoid application memory issues. By default those are set to 256KB. If that's not enough you'll get the following error: ---- @@ -335,6 +336,40 @@ To configure a response timeout for a specific request: +[[webflux-client-builder-jdk-httpclient]] +=== JDK HttpClient + +The following example shows how to customize the JDK `HttpClient`: + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- + HttpClient httpClient = HttpClient.newBuilder() + .followRedirects(Redirect.NORMAL) + .connectTimeout(Duration.ofSeconds(20)) + .build(); + + ClientHttpConnector connector = + new JdkClientHttpConnector(httpClient, new DefaultDataBufferFactory()); + + WebClient webClient = WebClient.builder().clientConnector(connector).build(); +---- + +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val httpClient = HttpClient.newBuilder() + .followRedirects(Redirect.NORMAL) + .connectTimeout(Duration.ofSeconds(20)) + .build() + + val connector = JdkClientHttpConnector(httpClient, DefaultDataBufferFactory()) + + val webClient = WebClient.builder().clientConnector(connector).build() +---- + + + [[webflux-client-builder-jetty]] === Jetty @@ -382,7 +417,7 @@ shows: HttpClient httpClient = new HttpClient(); // Further customizations... - + ClientHttpConnector connector = new JettyClientHttpConnector(httpClient, resourceFactory()); <1> @@ -403,7 +438,7 @@ shows: val httpClient = HttpClient() // Further customizations... - + val connector = JettyClientHttpConnector(httpClient, resourceFactory()) // <1> return WebClient.builder().clientConnector(connector).build() // <2> @@ -425,6 +460,7 @@ The following example shows how to customize Apache HttpComponents `HttpClient` HttpAsyncClientBuilder clientBuilder = HttpAsyncClients.custom(); clientBuilder.setDefaultRequestConfig(...); CloseableHttpAsyncClient client = clientBuilder.build(); + ClientHttpConnector connector = new HttpComponentsClientHttpConnector(client); WebClient webClient = WebClient.builder().clientConnector(connector).build(); @@ -546,38 +582,33 @@ depending on the response status: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java ---- - Mono entityMono = client.get() - .uri("/persons/1") - .accept(MediaType.APPLICATION_JSON) - .exchangeToMono(response -> { - if (response.statusCode().equals(HttpStatus.OK)) { - return response.bodyToMono(Person.class); - } - else if (response.statusCode().is4xxClientError()) { - return response.bodyToMono(ErrorContainer.class); - } - else { - return Mono.error(response.createException()); - } - }); + Mono entityMono = client.get() + .uri("/persons/1") + .accept(MediaType.APPLICATION_JSON) + .exchangeToMono(response -> { + if (response.statusCode().equals(HttpStatus.OK)) { + return response.bodyToMono(Person.class); + } + else { + // Turn to error + return response.createError(); + } + }); ---- [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] .Kotlin ---- - val entity = client.get() - .uri("/persons/1") - .accept(MediaType.APPLICATION_JSON) - .awaitExchange { - if (response.statusCode() == HttpStatus.OK) { - return response.awaitBody(); - } - else if (response.statusCode().is4xxClientError) { - return response.awaitBody(); - } - else { - return response.createExceptionAndAwait(); - } - } +val entity = client.get() + .uri("/persons/1") + .accept(MediaType.APPLICATION_JSON) + .awaitExchange { + if (response.statusCode() == HttpStatus.OK) { + return response.awaitBody() + } + else { + throw response.createExceptionAndAwait() + } + } ---- When using the above, after the returned `Mono` or `Flux` completes, the response body @@ -759,9 +790,9 @@ multipart request. The following example shows how to create a `MultiValueMap() ---- +==== `PartEvent` + +To stream multipart data sequentially, you can provide multipart content through `PartEvent` +objects. + +- Form fields can be created via `FormPartEvent::create`. +- File uploads can be created via `FilePartEvent::create`. + +You can concatenate the streams returned from methods via `Flux::concat`, and create a request for +the `WebClient`. + +For instance, this sample will POST a multipart form containing a form field and a file. + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- +Resource resource = ... +Mono result = webClient + .post() + .uri("https://example.com/") + .body(Flux.concat( + FormPartEvent.create("field", "field value"), + FilePartEvent.create("file", resource) + ), PartEvent.class) + .retrieve() + .bodyToMono(String.class); +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- +var resource: Resource = ... +var result: Mono = webClient + .post() + .uri("https://example.com/") + .body( + Flux.concat( + FormPartEvent.create("field", "field value"), + FilePartEvent.create("file", resource) + ) + ) + .retrieve() + .bodyToMono() +---- + +On the server side, `PartEvent` objects that are received via `@RequestBody` or +`ServerRequest::bodyToFlux(PartEvent.class)` can be relayed to another service +via the `WebClient`. + [[webflux-client-filter]] @@ -887,9 +966,8 @@ a filter for basic authentication through a static factory method: .build() ---- -You can create a new `WebClient` instance by using another as a starting point. This allows -insert or removing filters without affecting the original `WebClient`. Below is an example -that inserts a basic authentication filter at index 0: +Filters can be added or removed by mutating an existing `WebClient` instance, resulting +in a new `WebClient` instance that does not affect the original one. For example: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -910,6 +988,54 @@ that inserts a basic authentication filter at index 0: .build() ---- +`WebClient` is a thin facade around the chain of filters followed by an +`ExchangeFunction`. It provides a workflow to make requests, to encode to and from higher +level objects, and it helps to ensure that response content is always consumed. +When filters handle the response in some way, extra care must be taken to always consume +its content or to otherwise propagate it downstream to the `WebClient` which will ensure +the same. Below is a filter that handles the `UNAUTHORIZED` status code but ensures that +any response content, whether expected or not, is released: + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- + public ExchangeFilterFunction renewTokenFilter() { + return (request, next) -> next.exchange(request).flatMap(response -> { + if (response.statusCode().value() == HttpStatus.UNAUTHORIZED.value()) { + return response.releaseBody() + .then(renewToken()) + .flatMap(token -> { + ClientRequest newRequest = ClientRequest.from(request).build(); + return next.exchange(newRequest); + }); + } else { + return Mono.just(response); + } + }); + } +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + fun renewTokenFilter(): ExchangeFilterFunction? { + return ExchangeFilterFunction { request: ClientRequest?, next: ExchangeFunction -> + next.exchange(request!!).flatMap { response: ClientResponse -> + if (response.statusCode().value() == HttpStatus.UNAUTHORIZED.value()) { + return@flatMap response.releaseBody() + .then(renewToken()) + .flatMap { token: String? -> + val newRequest = ClientRequest.from(request).build() + next.exchange(newRequest) + } + } else { + return@flatMap Mono.just(response) + } + } + } + } +---- + + [[webflux-client-attributes]] == Attributes @@ -951,6 +1077,11 @@ For example: .awaitBody() ---- +Note that you can configure a `defaultRequest` callback globally at the +`WebClient.Builder` level which lets you insert attributes into all requests, +which could be used for example in a Spring MVC application to populate +request attributes based on `ThreadLocal` data. + [[webflux-client-context]] == Context @@ -960,10 +1091,8 @@ chain but they only influence the current request. If you want to pass informati propagates to additional requests that are nested, e.g. via `flatMap`, or executed after, e.g. via `concatMap`, then you'll need to use the Reactor `Context`. -`WebClient` exposes a method to populate the Reactor `Context` for a given request. -This information is available to filters for the current request and it also propagates -to subsequent requests or other reactive clients participating in the downstream -processing chain. For example: +The Reactor `Context` needs to be populated at the end of a reactive chain in order to +apply to all operations. For example: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -977,18 +1106,14 @@ processing chain. For example: .build(); client.get().uri("https://example.org/") - .context(context -> context.put("foo", ...)) .retrieve() .bodyToMono(String.class) .flatMap(body -> { // perform nested request (context propagates automatically)... - }); + }) + .contextWrite(context -> context.put("foo", ...)); ---- -Note that you can also specify how to populate the context through the `defaultRequest` -method at the level of the `WebClient.Builder` and that applies to all requests. -This could be used for to example to pass information from `ThreadLocal` storage onto -a Reactor processing chain in a Spring MVC application. [[webflux-client-synchronous]] @@ -1015,7 +1140,7 @@ a Reactor processing chain in a Spring MVC application. client.get().uri("/person/{id}", i).retrieve() .awaitBody() } - + val persons = runBlocking { client.get().uri("/persons").retrieve() .bodyToFlow() @@ -1070,7 +1195,7 @@ inter-dependent, without ever blocking until the end. With `Flux` or `Mono`, you should never have to block in a Spring MVC or Spring WebFlux controller. Simply return the resulting reactive type from the controller method. The same principle apply to Kotlin Coroutines and Spring WebFlux, just use suspending function or return `Flow` in your -controller method . +controller method . ==== @@ -1082,7 +1207,7 @@ controller method . To test code that uses the `WebClient`, you can use a mock web server, such as the https://github.com/square/okhttp#mockwebserver[OkHttp MockWebServer]. To see an example of its use, check out -https://github.com/spring-projects/spring-framework/blob/master/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java[`WebClientIntegrationTests`] +{spring-framework-main-code}/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java[`WebClientIntegrationTests`] in the Spring Framework test suite or the https://github.com/square/okhttp/tree/master/samples/static-server[`static-server`] sample in the OkHttp repository. diff --git a/src/docs/asciidoc/web/webflux-websocket.adoc b/framework-docs/src/docs/asciidoc/web/webflux-websocket.adoc similarity index 99% rename from src/docs/asciidoc/web/webflux-websocket.adoc rename to framework-docs/src/docs/asciidoc/web/webflux-websocket.adoc index 0e08d972d502..a1fbd2a32efa 100644 --- a/src/docs/asciidoc/web/webflux-websocket.adoc +++ b/framework-docs/src/docs/asciidoc/web/webflux-websocket.adoc @@ -353,7 +353,7 @@ into the attributes of the `WebSocketSession`. [[webflux-websocket-server-config]] -=== Server Configation +=== Server Configuration [.small]#<># The `RequestUpgradeStrategy` for each server exposes configuration specific to the diff --git a/src/docs/asciidoc/web/webflux.adoc b/framework-docs/src/docs/asciidoc/web/webflux.adoc similarity index 88% rename from src/docs/asciidoc/web/webflux.adoc rename to framework-docs/src/docs/asciidoc/web/webflux.adoc index c3e0d2cf3462..4ff81e604bbf 100644 --- a/src/docs/asciidoc/web/webflux.adoc +++ b/framework-docs/src/docs/asciidoc/web/webflux.adoc @@ -1,19 +1,19 @@ [[webflux]] +:chapter: webflux = Spring WebFlux -:doc-spring-security: {doc-root}/spring-security/site/docs/current/reference +:doc-spring-security: {doc-root}/spring-security/reference The original web framework included in the Spring Framework, Spring Web MVC, was purpose-built for the Servlet API and Servlet containers. The reactive-stack web framework, Spring WebFlux, was added later in version 5.0. It is fully non-blocking, supports https://www.reactive-streams.org/[Reactive Streams] back pressure, and runs on such servers as -Netty, Undertow, and Servlet 3.1+ containers. +Netty, Undertow, and Servlet containers. Both web frameworks mirror the names of their source modules -(https://github.com/spring-projects/spring-framework/tree/master/spring-webmvc[spring-webmvc] and -https://github.com/spring-projects/spring-framework/tree/master/spring-webflux[spring-webflux]) -and co-exist side by side in the Spring Framework. Each module is optional. -Applications can use one or the other module or, in some cases, both -- -for example, Spring MVC controllers with the reactive `WebClient`. +({spring-framework-main-code}/spring-webmvc[spring-webmvc] and +{spring-framework-main-code}/spring-webflux[spring-webflux]) and co-exist side by side in the +Spring Framework. Each module is optional. Applications can use one or the other module or, +in some cases, both -- for example, Spring MVC controllers with the reactive `WebClient`. @@ -24,18 +24,18 @@ for example, Spring MVC controllers with the reactive `WebClient`. Why was Spring WebFlux created? Part of the answer is the need for a non-blocking web stack to handle concurrency with a -small number of threads and scale with fewer hardware resources. Servlet 3.1 did provide -an API for non-blocking I/O. However, using it leads away from the rest of the Servlet API, -where contracts are synchronous (`Filter`, `Servlet`) or blocking (`getParameter`, -`getPart`). This was the motivation for a new common API to serve as a foundation across -any non-blocking runtime. That is important because of servers (such as Netty) that are -well-established in the async, non-blocking space. +small number of threads and scale with fewer hardware resources. Servlet non-blocking I/O +leads away from the rest of the Servlet API, where contracts are synchronous +(`Filter`, `Servlet`) or blocking (`getParameter`, `getPart`). This was the motivation +for a new common API to serve as a foundation across any non-blocking runtime. That is +important because of servers (such as Netty) that are well-established in the async, +non-blocking space. The other part of the answer is functional programming. Much as the addition of annotations -in Java 5 created opportunities (such as annotated REST controllers or unit tests), the addition -of lambda expressions in Java 8 created opportunities for functional APIs in Java. +in Java 5 created opportunities (such as annotated REST controllers or unit tests), the +addition of lambda expressions in Java 8 created opportunities for functional APIs in Java. This is a boon for non-blocking applications and continuation-style APIs (as popularized -by `CompletableFuture` and http://reactivex.io/[ReactiveX]) that allow declarative +by `CompletableFuture` and https://reactivex.io/[ReactiveX]) that allow declarative composition of asynchronous logic. At the programming-model level, Java 8 enabled Spring WebFlux to offer functional web endpoints alongside annotated controllers. @@ -88,7 +88,7 @@ Spring WebFlux. It provides the https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Mono.html[`Mono`] and https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Flux.html[`Flux`] API types to work on data sequences of 0..1 (`Mono`) and 0..N (`Flux`) through a rich set of operators aligned with the -ReactiveX http://reactivex.io/documentation/operators.html[vocabulary of operators]. +ReactiveX https://reactivex.io/documentation/operators.html[vocabulary of operators]. Reactor is a Reactive Streams library and, therefore, all of its operators support non-blocking back pressure. Reactor has a strong focus on server-side Java. It is developed in close collaboration with Spring. @@ -150,7 +150,7 @@ You have maximum choice of libraries, since, historically, most are blocking. * If you are already shopping for a non-blocking web stack, Spring WebFlux offers the same execution model benefits as others in this space and also provides a choice of servers -(Netty, Tomcat, Jetty, Undertow, and Servlet 3.1+ containers), a choice of programming models +(Netty, Tomcat, Jetty, Undertow, and Servlet containers), a choice of programming models (annotated controllers and functional web endpoints), and a choice of reactive libraries (Reactor, RxJava, or other). @@ -188,7 +188,7 @@ unsure what benefits to look for, start by learning about how non-blocking I/O w [[webflux-server-choice]] === Servers -Spring WebFlux is supported on Tomcat, Jetty, Servlet 3.1+ containers, as well as on +Spring WebFlux is supported on Tomcat, Jetty, Servlet containers, as well as on non-Servlet runtimes such as Netty and Undertow. All servers are adapted to a low-level, <> so that higher-level <> can be supported across servers. @@ -206,7 +206,7 @@ used in the asynchronous, non-blocking space and lets a client and a server shar Tomcat and Jetty can be used with both Spring MVC and WebFlux. Keep in mind, however, that the way they are used is very different. Spring MVC relies on Servlet blocking I/O and lets applications use the Servlet API directly if they need to. Spring WebFlux -relies on Servlet 3.1 non-blocking I/O and uses the Servlet API behind a low-level +relies on Servlet non-blocking I/O and uses the Servlet API behind a low-level adapter. It is not exposed for direct use. For Undertow, Spring WebFlux uses Undertow APIs directly without the Servlet API. @@ -306,7 +306,7 @@ applications: * For server request processing there are two levels of support. ** <>: Basic contract for HTTP request handling with non-blocking I/O and Reactive Streams back pressure, along with adapters for Reactor Netty, -Undertow, Tomcat, Jetty, and any Servlet 3.1+ container. +Undertow, Tomcat, Jetty, and any Servlet container. ** <>: Slightly higher level, general-purpose web API for request handling, on top of which concrete programming models such as annotated controllers and functional endpoints are built. @@ -345,16 +345,16 @@ The following table describes the supported server APIs: | spring-web: Undertow to Reactive Streams bridge | Tomcat -| Servlet 3.1 non-blocking I/O; Tomcat API to read and write ByteBuffers vs byte[] -| spring-web: Servlet 3.1 non-blocking I/O to Reactive Streams bridge +| Servlet non-blocking I/O; Tomcat API to read and write ByteBuffers vs byte[] +| spring-web: Servlet non-blocking I/O to Reactive Streams bridge | Jetty -| Servlet 3.1 non-blocking I/O; Jetty API to write ByteBuffers vs byte[] -| spring-web: Servlet 3.1 non-blocking I/O to Reactive Streams bridge +| Servlet non-blocking I/O; Jetty API to write ByteBuffers vs byte[] +| spring-web: Servlet non-blocking I/O to Reactive Streams bridge -| Servlet 3.1 container -| Servlet 3.1 non-blocking I/O -| spring-web: Servlet 3.1 non-blocking I/O to Reactive Streams bridge +| Servlet container +| Servlet non-blocking I/O +| spring-web: Servlet non-blocking I/O to Reactive Streams bridge |=== The following table describes server dependencies (also see @@ -485,9 +485,9 @@ The code snippets below show using the `HttpHandler` adapters with each server A server.start() ---- -*Servlet 3.1+ Container* +*Servlet Container* -To deploy as a WAR to any Servlet 3.1+ container, you can extend and include +To deploy as a WAR to any Servlet container, you can extend and include {api-spring-framework}/web/server/adapter/AbstractReactiveWebInitializer.html[`AbstractReactiveWebInitializer`] in the WAR. That class wraps an `HttpHandler` with `ServletHttpHandlerAdapter` and registers that as a `Servlet`. @@ -620,11 +620,11 @@ https://github.com/synchronoss/nio-multipart[Synchronoss NIO Multipart] library. Both are configured through the `ServerCodecConfigurer` bean (see the <>). -To parse multipart data in streaming fashion, you can use the `Flux` returned from an -`HttpMessageReader` instead. For example, in an annotated controller, use of -`@RequestPart` implies `Map`-like access to individual parts by name and, hence, requires -parsing multipart data in full. By contrast, you can use `@RequestBody` to decode the -content to `Flux` without collecting to a `MultiValueMap`. +To parse multipart data in streaming fashion, you can use the `Flux` returned from the +`PartEventHttpMessageReader` instead of using `@RequestPart`, as that implies `Map`-like access +to individual parts by name and, hence, requires parsing multipart data in full. +By contrast, you can use `@RequestBody` to decode the content to `Flux` without +collecting to a `MultiValueMap`. [[webflux-forwarded-headers]] @@ -651,7 +651,7 @@ a proxy at the boundary of trust should be configured to remove untrusted forwar from the outside. You can also configure the `ForwardedHeaderTransformer` with `removeOnly=true`, in which case it removes but does not use the headers. -NOTE: In 5.1 `ForwardedHeaderFilter` was deprecated and superceded by +NOTE: In 5.1 `ForwardedHeaderFilter` was deprecated and superseded by `ForwardedHeaderTransformer` so forwarded headers can be processed earlier, before the exchange is created. If the filter is configured anyway, it is taken out of the list of filters, and `ForwardedHeaderTransformer` is used instead. @@ -677,8 +677,7 @@ Spring WebFlux provides fine-grained support for CORS configuration through anno controllers. However, when you use it with Spring Security, we advise relying on the built-in `CorsFilter`, which must be ordered ahead of Spring Security's chain of filters. -See the section on <> and the <> for more details. - +See the section on <> and the <> for more details. [[webflux-exception-handler]] @@ -1169,19 +1168,17 @@ as a `HandlerResult`, along with some additional context, and passed to the firs === Exceptions [.small]#<># -The `HandlerResult` returned from a `HandlerAdapter` can expose a function for error -handling based on some handler-specific mechanism. This error function is called if: - -* The handler (for example, `@Controller`) invocation fails. -* The handling of the handler return value through a `HandlerResultHandler` fails. +`HandlerAdapter` implementations can handle internally exceptions from invoking a request +handler, such as a controller method. However, an exception may be deferred if the request +handler returns an asynchronous value. -The error function can change the response (for example, to an error status), as long as an error -signal occurs before the reactive type returned from the handler produces any data items. +A `HandlerAdapter` may expose its exception handling mechanism as a +`DispatchExceptionHandler` set on the `HandlerResult` it returns. When that's set, +`DispatcherHandler` will also apply it to the handling of the result. -This is how `@ExceptionHandler` methods in `@Controller` classes are supported. -By contrast, support for the same in Spring MVC is built on a `HandlerExceptionResolver`. -This generally should not matter. However, keep in mind that, in WebFlux, you cannot use a -`@ControllerAdvice` to handle exceptions that occur before a handler is chosen. +A `HandlerAdapter` may also choose to implement `DispatchExceptionHandler`. Inn that case +`DispatcherHandler` will apply it to exceptions that arise before a handler is mapped, +e.g. during handler mapping, or earlier, e.g. in a `WebFilter`. See also <> in the "`Annotated Controller`" section or <> in the WebHandler API section. @@ -1201,7 +1198,7 @@ instance. The `View` is then used to render the response. [[webflux-viewresolution-handling]] ==== Handling -[.small]#<># +[.small]#<># The `HandlerResult` passed into `ViewResolutionResultHandler` contains the return value from the handler and the model that contains attributes added during request @@ -1349,6 +1346,29 @@ directly to the response body versus view resolution and rendering with an HTML +[[webflux-ann-requestmapping-proxying]] +==== AOP Proxies +[.small]#<># + +In some cases, you may need to decorate a controller with an AOP proxy at runtime. +One example is if you choose to have `@Transactional` annotations directly on the +controller. When this is the case, for controllers specifically, we recommend +using class-based proxying. This is automatically the case with such annotations +directly on the controller. + +If the controller implements an interface, and needs AOP proxying, you may need to +explicitly configure class-based proxying. For example, with `@EnableTransactionManagement` +you can change to `@EnableTransactionManagement(proxyTargetClass = true)`, and with +`` you can change to ``. + +NOTE: Keep in mind that as of 6.0, with interface proxying, Spring WebFlux no longer detects +controllers based solely on a type-level `@RequestMapping` annotation on the interface. +Please, enable class based proxying, or otherwise the interface must also have an +`@Controller` annotation. + + + + [[webflux-ann-requestmapping]] === Request Mapping [.small]#<># @@ -1449,7 +1469,7 @@ You can map requests by using glob patterns and wildcards: | `+{*path}+` | Matches zero or more path segments until the end of the path and captures it as a variable named "path" -| `+"/resources/{*file}"+` matches `+"/resources/images/file.png"+` and captures `+file=images/file.png+` +| `+"/resources/{*file}"+` matches `+"/resources/images/file.png"+` and captures `+file=/images/file.png+` |=== @@ -1518,10 +1538,10 @@ information or with the `-parameters` compiler flag on Java 8. The syntax `{*varName}` declares a URI variable that matches zero or more remaining path segments. For example `/resources/{*path}` matches all files under `/resources/`, and the -`"path"` variable captures the complete relative path. +`"path"` variable captures the complete path under `/resources`. The syntax `{varName:regex}` declares a URI variable with a regular expression that has the -syntax: `{varName:regex}`. For example, given a URL of `/spring-web-3.0.5 .jar`, the following method +syntax: `{varName:regex}`. For example, given a URL of `/spring-web-3.0.5.jar`, the following method extracts the name, version, and file extension: [source,java,indent=0,subs="verbatim,quotes",role="primary"] @@ -1542,9 +1562,9 @@ extracts the name, version, and file extension: ---- URI path patterns can also have embedded `${...}` placeholders that are resolved on startup -through `PropertyPlaceHolderConfigurer` against local, system, environment, and other property -sources. You ca use this to, for example, parameterize a base URL based on some external -configuration. +through `PropertySourcesPlaceholderConfigurer` against local, system, environment, and +other property sources. You can use this to, for example, parameterize a base URL based on +some external configuration. NOTE: Spring WebFlux uses `PathPattern` and the `PathPatternParser` for URI path matching support. Both classes are located in `spring-web` and are expressly designed for use with HTTP URL @@ -1671,7 +1691,7 @@ specific value (`myParam=myValue`). The following examples tests for a parameter ---- <1> Check that `myParam` equals `myValue`. -You can also use the same with request header conditions, as the follwing example shows: +You can also use the same with request header conditions, as the following example shows: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1853,8 +1873,8 @@ and others) and is equivalent to `required=false`. | For access to name-value pairs in URI path segments. See <>. | `@RequestParam` -| For access to Servlet request parameters. Parameter values are converted to the declared - method argument type. See <>. +| For access to query parameters. Parameter values are converted to the declared method argument + type. See <>. Note that use of `@RequestParam` is optional -- for example, to set its attributes. See "`Any other argument`" later in this table. @@ -1946,6 +1966,14 @@ generally supported for all return values. | `HttpHeaders` | For returning a response with headers and no body. +| `ErrorResponse` +| To render an RFC 7807 error response with details in the body, + see <> + +| `ProblemDetail` +| To render an RFC 7807 error response with details in the body, + see <> + | `String` | A view name to be resolved with `ViewResolver` instances and used together with the implicit model -- determined through command objects and `@ModelAttribute` methods. The handler @@ -1987,10 +2015,9 @@ generally supported for all return values. to be written (however, `text/event-stream` must be requested or declared in the mapping through the `produces` attribute). -| Any other return value -| If a return value is not matched to any of the above, it is, by default, treated as a view - name, if it is `String` or `void` (default view name selection applies), or as a model - attribute to be added to the model, unless it is a simple type, as determined by +| Other return values +| If a return value remains unresolved in any other way, it is treated as a model + attribute, unless it is a simple type as determined by {api-spring-framework}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty], in which case it remains unresolved. |=== @@ -2009,6 +2036,12 @@ By default, simple types (such as `int`, `long`, `Date`, and others) are support can be customized through a `WebDataBinder` (see <>) or by registering `Formatters` with the `FormattingConversionService` (see <>). +A practical issue in type conversion is the treatment of an empty String source value. +Such a value is treated as missing if it becomes `null` as a result of type conversion. +This can be the case for `Long`, `UUID`, and other target types. If you want to allow `null` +to be injected, either use the `required` flag on the argument annotation, or declare the +argument as `@Nullable`. + [[webflux-ann-matrix-variables]] ==== Matrix Variables @@ -2252,7 +2285,7 @@ The following example gets the value of the `Accept-Encoding` and `Keep-Alive` h //... } ---- -<1> Get the value of the `Accept-Encoging` header. +<1> Get the value of the `Accept-Encoding` header. <2> Get the value of the `Keep-Alive` header. [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] @@ -2265,7 +2298,7 @@ The following example gets the value of the `Accept-Encoding` and `Keep-Alive` h //... } ---- -<1> Get the value of the `Accept-Encoging` header. +<1> Get the value of the `Accept-Encoding` header. <2> Get the value of the `Keep-Alive` header. Type conversion is applied automatically if the target method parameter type is not @@ -2327,7 +2360,7 @@ Type conversion is applied automatically if the target method parameter type is [.small]#<># You can use the `@ModelAttribute` annotation on a method argument to access an attribute from the -model or have it instantiated if not present. The model attribute is also overlain with +model or have it instantiated if not present. The model attribute is also overlaid with the values of query parameters and form fields whose names match to field names. This is referred to as data binding, and it saves you from having to deal with parsing and converting individual query parameters and form fields. The following example binds an instance of `Pet`: @@ -2395,7 +2428,7 @@ immediately next to the `@ModelAttribute`, as the following example shows: <1> Adding a `BindingResult`. You can automatically apply validation after data binding by adding the -`javax.validation.Valid` annotation or Spring's `@Validated` annotation (see also +`jakarta.validation.Valid` annotation or Spring's `@Validated` annotation (see also <> and <>). The following example uses the `@Valid` annotation: @@ -2742,7 +2775,7 @@ you can declare a concrete target `Object`, instead of `Part`, as the following ---- <1> Using `@RequestPart` to get the metadata. -You can use `@RequestPart` in combination with `javax.validation.Valid` or Spring's +You can use `@RequestPart` in combination with `jakarta.validation.Valid` or Spring's `@Validated` annotation, which causes Standard Bean Validation to be applied. Validation errors lead to a `WebExchangeBindException` that results in a 400 (BAD_REQUEST) response. The exception contains a `BindingResult` with the error details and can also be handled @@ -2790,29 +2823,100 @@ as the following example shows: ---- <1> Using `@RequestBody`. +===== `PartEvent` + +To access multipart data sequentially, in a streaming fashion, you can use `@RequestBody` with +`Flux` (or `Flow` in Kotlin). +Each part in a multipart HTTP message will produce at +least one `PartEvent` containing both headers and a buffer with the contents of the part. + +- Form fields will produce a *single* `FormPartEvent`, containing the value of the field. +- File uploads will produce *one or more* `FilePartEvent` objects, containing the filename used +when uploading. If the file is large enough to be split across multiple buffers, the first +`FilePartEvent` will be followed by subsequent events. -To access multipart data sequentially, in streaming fashion, you can use `@RequestBody` with -`Flux` (or `Flow` in Kotlin) instead, as the following example shows: + +For example: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java ---- - @PostMapping("/") - public String handle(@RequestBody Flux parts) { <1> - // ... - } + @PostMapping("/") + public void handle(@RequestBody Flux allPartsEvents) { <1> + allPartsEvents.windowUntil(PartEvent::isLast) <2> + .concatMap(p -> p.switchOnFirst((signal, partEvents) -> { <3> + if (signal.hasValue()) { + PartEvent event = signal.get(); + if (event instanceof FormPartEvent formEvent) { <4> + String value = formEvent.value(); + // handle form field + } + else if (event instanceof FilePartEvent fileEvent) { <5> + String filename = fileEvent.filename(); + Flux contents = partEvents.map(PartEvent::content); <6> + // handle file upload + } + else { + return Mono.error(new RuntimeException("Unexpected event: " + event)); + } + } + else { + return partEvents; // either complete or error signal + } + })); + } ---- <1> Using `@RequestBody`. +<2> The final `PartEvent` for a particular part will have `isLast()` set to `true`, and can be +followed by additional events belonging to subsequent parts. +This makes the `isLast` property suitable as a predicate for the `Flux::windowUntil` operator, to +split events from all parts into windows that each belong to a single part. +<3> The `Flux::switchOnFirst` operator allows you to see whether you are handling a form field or +file upload. +<4> Handling the form field. +<5> Handling the file upload. +<6> The body contents must be completely consumed, relayed, or released to avoid memory leaks. [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] .Kotlin ---- @PostMapping("/") - fun handle(@RequestBody parts: Flow): String { // <1> - // ... - } + fun handle(@RequestBody allPartsEvents: Flux) = { // <1> + allPartsEvents.windowUntil(PartEvent::isLast) <2> + .concatMap { + it.switchOnFirst { signal, partEvents -> <3> + if (signal.hasValue()) { + val event = signal.get() + if (event is FormPartEvent) { <4> + val value: String = event.value(); + // handle form field + } else if (event is FilePartEvent) { <5> + val filename: String = event.filename(); + val contents: Flux = partEvents.map(PartEvent::content); <6> + // handle file upload + } else { + return Mono.error(RuntimeException("Unexpected event: " + event)); + } + } else { + return partEvents; // either complete or error signal + } + } + } +} ---- <1> Using `@RequestBody`. +<2> The final `PartEvent` for a particular part will have `isLast()` set to `true`, and can be +followed by additional events belonging to subsequent parts. +This makes the `isLast` property suitable as a predicate for the `Flux::windowUntil` operator, to +split events from all parts into windows that each belong to a single part. +<3> The `Flux::switchOnFirst` operator allows you to see whether you are handling a form field or +file upload. +<4> Handling the form field. +<5> Handling the file upload. +<6> The body contents must be completely consumed, relayed, or released to avoid memory leaks. + +Received part events can also be relayed to another service by using the `WebClient`. +See <>. [[webflux-ann-requestbody]] @@ -2864,7 +2968,7 @@ and fully non-blocking reading and (client-to-server) streaming. You can use the <> option of the <> to configure or customize message readers. -You can use `@RequestBody` in combination with `javax.validation.Valid` or Spring's +You can use `@RequestBody` in combination with `jakarta.validation.Valid` or Spring's `@Validated` annotation, which causes Standard Bean Validation to be applied. Validation errors cause a `WebExchangeBindException`, which results in a 400 (BAD_REQUEST) response. The exception contains a `BindingResult` with error details and can be handled in the @@ -2971,7 +3075,7 @@ configure or customize message writing. public ResponseEntity handle() { String body = ... ; String etag = ... ; - return ResponseEntity.ok().eTag(etag).build(body); + return ResponseEntity.ok().eTag(etag).body(body); } ---- [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] @@ -2987,7 +3091,17 @@ configure or customize message writing. WebFlux supports using a single value <> to produce the `ResponseEntity` asynchronously, and/or single and multi-value reactive types -for the body. +for the body. This allows a variety of async responses with `ResponseEntity` as follows: + +* `ResponseEntity>` or `ResponseEntity>` make the response status and + headers known immediately while the body is provided asynchronously at a later point. + Use `Mono` if the body consists of 0..1 values or `Flux` if it can produce multiple values. +* `Mono>` provides all three -- response status, headers, and body, + asynchronously at a later point. This allows the response status and headers to vary + depending on the outcome of asynchronous request handling. +* `Mono>>` or `Mono>>` are yet another + possible, albeit less common alternative. They provide the response status and headers + asynchronously first and then the response body, also asynchronously, second. [[webflux-ann-jackson]] @@ -3305,10 +3419,15 @@ controller-specific `Formatter` instances, as the following example shows: ---- <1> Adding a custom formatter (a `DateFormatter`, in this case). +[[webflux-ann-initbinder-model-design]] +==== Model Design +[.small]#<># + +include::web-data-binding-model-design.adoc[] [[webflux-ann-controller-exceptions]] -=== Managing Exceptions +=== Exceptions [.small]#<># `@Controller` and <> classes can have @@ -3368,21 +3487,22 @@ Support for `@ExceptionHandler` methods in Spring WebFlux is provided by the for more detail. -[[webflux-ann-rest-exceptions]] -==== REST API exceptions -[.small]#<># -A common requirement for REST services is to include error details in the body of the -response. The Spring Framework does not automatically do so, because the representation -of error details in the response body is application-specific. However, a -`@RestController` can use `@ExceptionHandler` methods with a `ResponseEntity` return -value to set the status and the body of the response. Such methods can also be declared -in `@ControllerAdvice` classes to apply them globally. +[[webflux-ann-exceptionhandler-args]] +==== Method Arguments +[.small]#<># + +`@ExceptionHandler` methods support the same <> +as `@RequestMapping` methods, except the request body might have been consumed already. + + -NOTE: Note that Spring WebFlux does not have an equivalent for the Spring MVC -`ResponseEntityExceptionHandler`, because WebFlux raises only `ResponseStatusException` -(or subclasses thereof), and those do not need to be translated to -an HTTP status code. +[[webflux-ann-exceptionhandler-return-values]] +==== Return Values +[.small]#<># + +`@ExceptionHandler` methods support the same <> +as `@RequestMapping` methods. @@ -3465,22 +3585,168 @@ include::web-uris.adoc[leveloffset=+2] include::webflux-cors.adoc[leveloffset=+1] +[[webflux-ann-rest-exceptions]] +== Error Responses +[.small]#<># + +A common requirement for REST services is to include details in the body of error +responses. The Spring Framework supports the "Problem Details for HTTP APIs" +specification, https://www.rfc-editor.org/rfc/rfc7807.html[RFC 7807]. + +The following are the main abstractions for this support: + +- `ProblemDetail` -- representation for an RFC 7807 problem detail; a simple container +for both standard fields defined in the spec, and for non-standard ones. +- `ErrorResponse` -- contract to expose HTTP error response details including HTTP +status, response headers, and a body in the format of RFC 7807; this allows exceptions to +encapsulate and expose the details of how they map to an HTTP response. All Spring WebFlux +exceptions implement this. +- `ErrorResponseException` -- basic `ErrorResponse` implementation that others +can use as a convenient base class. +- `ResponseEntityExceptionHandler` -- convenient base class for an +<> that handles all Spring WebFlux exceptions, +and any `ErrorResponseException`, and renders an error response with a body. + + + +[[webflux-ann-rest-exceptions-render]] +=== Render +[.small]#<># + +You can return `ProblemDetail` or `ErrorResponse` from any `@ExceptionHandler` or from +any `@RequestMapping` method to render an RFC 7807 response. This is processed as follows: + +- The `status` property of `ProblemDetail` determines the HTTP status. +- The `instance` property of `ProblemDetail` is set from the current URL path, if not +already set. +- For content negotiation, the Jackson `HttpMessageConverter` prefers +"application/problem+json" over "application/json" when rendering a `ProblemDetail`, +and also falls back on it if no compatible media type is found. + +To enable RFC 7807 responses for Spring WebFlux exceptions and for any +`ErrorResponseException`, extend `ResponseEntityExceptionHandler` and declare it as an +<> in Spring configuration. The handler +has an `@ExceptionHandler` method that handles any `ErrorResponse` exception, which +includes all built-in web exceptions. You can add more exception handling methods, and +use a protected method to map any exception to a `ProblemDetail`. + + + +[[webflux-ann-rest-exceptions-non-standard]] +=== Non-Standard Fields +[.small]#<># + +You can extend an RFC 7807 response with non-standard fields in one of two ways. + +One, insert into the "properties" `Map` of `ProblemDetail`. When using the Jackson +library, the Spring Framework registers `ProblemDetailJacksonMixin` that ensures this +"properties" `Map` is unwrapped and rendered as top level JSON properties in the +response, and likewise any unknown property during deserialization is inserted into +this `Map`. + +You can also extend `ProblemDetail` to add dedicated non-standard properties. +The copy constructor in `ProblemDetail` allows a subclass to make it easy to be created +from an existing `ProblemDetail`. This could be done centrally, e.g. from an +`@ControllerAdvice` such as `ResponseEntityExceptionHandler` that re-creates the +`ProblemDetail` of an exception into a subclass with the additional non-standard fields. + + + +[[webflux-ann-rest-exceptions-i18n]] +=== Internationalization +[.small]#<># + +It is a common requirement to internationalize error response details, and good practice +to customize the problem details for Spring WebFlux exceptions. This is supported as follows: + +- Each `ErrorResponse` exposes a message code and arguments to resolve the "detail" field +through a <>. +The actual message code value is parameterized with placeholders, e.g. +`"HTTP method {0} not supported"` to be expanded from the arguments. +- Each `ErrorResponse` also exposes a message code to resolve the "title" field. +- `ResponseEntityExceptionHandler` uses the message code and arguments to resolve the +"detail" and the "title" fields. + +By default, the message code for the "detail" field is "problemDetail." + the fully +qualified exception class name. Some exceptions may expose additional message codes in +which case a suffix is added to the default message code. The table below lists message +arguments and codes for Spring WebFlux exceptions: + +[[webflux-ann-rest-exceptions-codes]] +[cols="1,1,2", options="header"] +|=== +| Exception | Message Code | Message Code Arguments + +| `UnsupportedMediaTypeStatusException` +| (default) +| `{0}` the media type that is not supported, `{1}` list of supported media types + +| `UnsupportedMediaTypeStatusException` +| (default) + ".parseError" +| + +| `MissingRequestValueException` +| (default) +| `{0}` a label for the value (e.g. "request header", "cookie value", ...), `{1}` the value name + +| `UnsatisfiedRequestParameterException` +| (default) +| `{0}` the list of parameter conditions + +| `WebExchangeBindException` +| (default) +| `{0}` the list of global errors, `{1}` the list of field errors. +Message codes and arguments for each error within the `BindingResult` are also resolved +via `MessageSource`. + +| `NotAcceptableStatusException` +| (default) +| `{0}` list of supported media types + +| `NotAcceptableStatusException` +| (default) + ".parseError" +| + +| `ServerErrorException` +| (default) +| `{0}` the failure reason provided to the class constructor + +| `MethodNotAllowedException` +| (default) +| `{0}` the current HTTP method, `{1}` the list of supported HTTP methods + +|=== + +By default, the message code for the "title" field is "problemDetail.title." + the fully +qualified exception class name. + + + + +[[webflux-ann-rest-exceptions-client]] +=== Client Handling +[.small]#<># + +A client application can catch `WebClientResponseException`, when using the `WebClient`, +or `RestClientResponseException` when using the `RestTemplate`, and use their +`getResponseBodyAs` methods to decode the error response body to any target type such as +`ProblemDetail`, or a subclass of `ProblemDetail`. + + [[webflux-web-security]] == Web Security [.small]#<># -The https://projects.spring.io/spring-security/[Spring Security] project provides support +The https://spring.io/projects/spring-security[Spring Security] project provides support for protecting web applications from malicious exploits. See the Spring Security reference documentation, including: -* {doc-spring-security}/html5/#jc-webflux[WebFlux Security] -* {doc-spring-security}/html5/#test-webflux[WebFlux Testing Support] -* {doc-spring-security}/html5/#csrf[CSRF Protection] -* {doc-spring-security}/html5/#headers[Security Response Headers] - -include::webflux-view.adoc[leveloffset=+1] +* {doc-spring-security}/reactive/configuration/webflux.html[WebFlux Security] +* {doc-spring-security}/reactive/test/index.html[WebFlux Testing Support] +* {doc-spring-security}/features/exploits/csrf.html#csrf-protection[CSRF protection] +* {doc-spring-security}/features/exploits/headers.html[Security Response Headers] @@ -3654,6 +3920,7 @@ You should serve static resources with a `Cache-Control` and conditional respons for optimal performance. See the section on configuring <>. +include::webflux-view.adoc[leveloffset=+1] [[webflux-config]] @@ -3830,7 +4097,7 @@ as the following example shows: public class WebConfig implements WebFluxConfigurer { @Override - public Validator getValidator(); { + public Validator getValidator() { // ... } @@ -4155,8 +4422,8 @@ the example: @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/resources/**") - .addResourceLocations("/public", "classpath:/static/") - .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS)); + .addResourceLocations("/public", "classpath:/static/") + .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS)); } } @@ -4237,12 +4504,21 @@ Note that, when using both `EncodedResourceResolver` (for example, Gzip, Brotli `VersionedResourceResolver`, they must be registered in that order, to ensure content-based versions are always computed reliably based on the unencoded file. -https://www.webjars.org/documentation[WebJars] are also supported through the +For https://www.webjars.org/documentation[WebJars], versioned URLs like +`/webjars/jquery/1.2.0/jquery.min.js` are the recommended and most efficient way to use them. +The related resource location is configured out of the box with Spring Boot (or can be configured +manually via `ResourceHandlerRegistry`) and does not require to add the +`org.webjars:webjars-locator-core` dependency. + +Version-less URLs like `/webjars/jquery/jquery.min.js` are supported through the `WebJarsResourceResolver` which is automatically registered when the -`org.webjars:webjars-locator-core` library is present on the classpath. The resolver can -re-write URLs to include the version of the jar and can also match against incoming URLs -without versions -- for example, from `/jquery/jquery.min.js` to -`/jquery/1.2.0/jquery.min.js`. +`org.webjars:webjars-locator-core` library is present on the classpath, at the cost of a +classpath scanning that could slow down application startup. The resolver can re-write URLs to +include the version of the jar and can also match against incoming URLs without versions +-- for example, from `/webjars/jquery/jquery.min.js` to `/webjars/jquery/1.2.0/jquery.min.js`. + +TIP: The Java configuration based on `ResourceHandlerRegistry` provides further options +for fine-grained control, e.g. last-modified behavior and optimized resource resolution. @@ -4265,9 +4541,7 @@ The following example shows how to use `PathMatchConfigurer`: public void configurePathMatch(PathMatchConfigurer configurer) { configurer .setUseCaseSensitiveMatch(true) - .setUseTrailingSlashMatch(false) - .addPathPrefix("/api", - HandlerTypePredicate.forAnnotation(RestController.class)); + .addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController.class)); } } ---- @@ -4282,9 +4556,7 @@ The following example shows how to use `PathMatchConfigurer`: fun configurePathMatch(configurer: PathMatchConfigurer) { configurer .setUseCaseSensitiveMatch(true) - .setUseTrailingSlashMatch(false) - .addPathPrefix("/api", - HandlerTypePredicate.forAnnotation(RestController::class.java)) + .addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController::class.java)) } } ---- diff --git a/framework-docs/src/docs/asciidoc/web/webmvc-client.adoc b/framework-docs/src/docs/asciidoc/web/webmvc-client.adoc new file mode 100644 index 000000000000..0936a06ce0b6 --- /dev/null +++ b/framework-docs/src/docs/asciidoc/web/webmvc-client.adoc @@ -0,0 +1,55 @@ +[[webmvc-client]] += REST Clients + +This section describes options for client-side access to REST endpoints. + + + + +[[webmvc-resttemplate]] +== `RestTemplate` + +`RestTemplate` is a synchronous client to perform HTTP requests. It is the original +Spring REST client and exposes a simple, template-method API over underlying HTTP client +libraries. + +NOTE: As of 5.0 the `RestTemplate` is in maintenance mode, with only requests for minor +changes and bugs to be accepted. Please, consider using the +<> which offers a more modern API and +supports sync, async, and streaming scenarios. + +See <> for details. + + + + +[[webmvc-webclient]] +== `WebClient` + +`WebClient` is a non-blocking, reactive client to perform HTTP requests. It was +introduced in 5.0 and offers a modern alternative to the `RestTemplate`, with efficient +support for both synchronous and asynchronous, as well as streaming scenarios. + +In contrast to `RestTemplate`, `WebClient` supports the following: + +* Non-blocking I/O. +* Reactive Streams back pressure. +* High concurrency with fewer hardware resources. +* Functional-style, fluent API that takes advantage of Java 8 lambdas. +* Synchronous and asynchronous interactions. +* Streaming up to or streaming down from a server. + +See <> for more details. + + + + +[[webmvc-http-interface]] +== HTTP Interface + +The Spring Frameworks lets you define an HTTP service as a Java interface with HTTP +exchange methods. You can then generate a proxy that implements this interface and +performs the exchanges. This helps to simplify HTTP remote access and provides additional +flexibility for to choose an API style such as synchronous or reactive. + +See <> for details. diff --git a/src/docs/asciidoc/web/webmvc-cors.adoc b/framework-docs/src/docs/asciidoc/web/webmvc-cors.adoc similarity index 96% rename from src/docs/asciidoc/web/webmvc-cors.adoc rename to framework-docs/src/docs/asciidoc/web/webmvc-cors.adoc index f86e775c39c6..007c8dada078 100644 --- a/src/docs/asciidoc/web/webmvc-cors.adoc +++ b/framework-docs/src/docs/asciidoc/web/webmvc-cors.adoc @@ -1,5 +1,6 @@ [[mvc-cors]] = CORS +:doc-spring-security: {doc-root}/spring-security/reference [.small]#<># Spring MVC lets you handle CORS (Cross-Origin Resource Sharing). This section @@ -50,7 +51,7 @@ Each `HandlerMapping` can be {api-spring-framework}/web/servlet/handler/AbstractHandlerMapping.html#setCorsConfigurations-java.util.Map-[configured] individually with URL pattern-based `CorsConfiguration` mappings. In most cases, applications use the MVC Java configuration or the XML namespace to declare such mappings, which results -in a single global map being passed to all `HandlerMappping` instances. +in a single global map being passed to all `HandlerMapping` instances. You can combine global CORS configuration at the `HandlerMapping` level with more fine-grained, handler-level CORS configuration. For example, annotated controllers can use @@ -334,13 +335,12 @@ as the following example shows: You can apply CORS support through the built-in {api-spring-framework}/web/filter/CorsFilter.html[`CorsFilter`]. -NOTE: If you try to use the `CorsFilter` with Spring Security, keep in mind that -Spring Security has -https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#cors[built-in support] -for CORS. +NOTE: If you try to use the `CorsFilter` with Spring Security, keep in mind that Spring +Security has {doc-spring-security}/servlet/integrations/cors.html[built-in support] for +CORS. -To configure the filter, pass a -`CorsConfigurationSource` to its constructor, as the following example shows: +To configure the filter, pass a `CorsConfigurationSource` to its constructor, as the +following example shows: [source,java,indent=0,subs="verbatim",role="primary"] .Java diff --git a/src/docs/asciidoc/web/webmvc-functional.adoc b/framework-docs/src/docs/asciidoc/web/webmvc-functional.adoc similarity index 91% rename from src/docs/asciidoc/web/webmvc-functional.adoc rename to framework-docs/src/docs/asciidoc/web/webmvc-functional.adoc index ded811f373bb..11fb793d4f08 100644 --- a/src/docs/asciidoc/web/webmvc-functional.adoc +++ b/framework-docs/src/docs/asciidoc/web/webmvc-functional.adoc @@ -103,7 +103,7 @@ as the following example shows: If you register the `RouterFunction` as a bean, for instance by exposing it in a -@Configuration class, it will be auto-detected by the servlet, as explained in <>. +`@Configuration` class, it will be auto-detected by the servlet, as explained in <>. @@ -229,6 +229,60 @@ Mono asyncResponse = webClient.get().retrieve().bodyToMono(Perso ServerResponse.async(asyncResponse); ---- +https://www.w3.org/TR/eventsource/[Server-Sent Events] can be provided via the +static `sse` method on `ServerResponse`. The builder provided by that method +allows you to send Strings, or other objects as JSON. For example: + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- + public RouterFunction sse() { + return route(GET("/sse"), request -> ServerResponse.sse(sseBuilder -> { + // Save the sseBuilder object somewhere.. + })); + } + + // In some other thread, sending a String + sseBuilder.send("Hello world"); + + // Or an object, which will be transformed into JSON + Person person = ... + sseBuilder.send(person); + + // Customize the event by using the other methods + sseBuilder.id("42") + .event("sse event") + .data(person); + + // and done at some point + sseBuilder.complete(); +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + fun sse(): RouterFunction = router { + GET("/sse") { request -> ServerResponse.sse { sseBuilder -> + // Save the sseBuilder object somewhere.. + } + } + + // In some other thread, sending a String + sseBuilder.send("Hello world") + + // Or an object, which will be transformed into JSON + val person = ... + sseBuilder.send(person) + + // Customize the event by using the other methods + sseBuilder.id("42") + .event("sse event") + .data(person) + + // and done at some point + sseBuilder.complete() +---- + + [[webmvc-fn-handler-classes]] === Handler Classes @@ -421,7 +475,7 @@ For instance, the router function builder offers the method `GET(String, Handler Besides HTTP method-based mapping, the route builder offers a way to introduce additional predicates when mapping to requests. For each HTTP method there is an overloaded variant that takes a `RequestPredicate` as a -parameter, though which additional constraints can be expressed. +parameter, through which additional constraints can be expressed. [[webmvc-fn-predicates]] @@ -471,6 +525,8 @@ The example shown above also uses two request predicates, as the builder uses Router functions are evaluated in order: if the first route does not match, the second is evaluated, and so on. Therefore, it makes sense to declare more specific routes before general ones. +This is also important when registering router functions as Spring beans, as will +be described later. Note that this behavior is different from the annotation-based programming model, where the "most specific" controller method is picked automatically. @@ -556,7 +612,7 @@ RouterFunction route = route() .path("/person", builder -> builder // <1> .GET("/{id}", accept(APPLICATION_JSON), handler::getPerson) .GET(accept(APPLICATION_JSON), handler::listPeople) - .POST("/person", handler::createPerson)) + .POST(handler::createPerson)) .build(); ---- <1> Note that second parameter of `path` is a consumer that takes the router builder. @@ -570,7 +626,7 @@ RouterFunction route = route() "/person".nest { GET("/{id}", accept(APPLICATION_JSON), handler::getPerson) GET(accept(APPLICATION_JSON), handler::listPeople) - POST("/person", handler::createPerson) + POST(handler::createPerson) } } ---- @@ -588,7 +644,7 @@ We can further improve by using the `nest` method together with `accept`: .nest(accept(APPLICATION_JSON), b2 -> b2 .GET("/{id}", handler::getPerson) .GET(handler::listPeople)) - .POST("/person", handler::createPerson)) + .POST(handler::createPerson)) .build(); ---- [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] @@ -601,7 +657,7 @@ We can further improve by using the `nest` method together with `accept`: accept(APPLICATION_JSON).nest { GET("/{id}", handler::getPerson) GET("", handler::listPeople) - POST("/person", handler::createPerson) + POST(handler::createPerson) } } } @@ -618,8 +674,8 @@ components required to process requests. The MVC Java configuration declares the infrastructure components to support functional endpoints: * `RouterFunctionMapping`: Detects one or more `RouterFunction` beans in the Spring -configuration, combines them through `RouterFunction.andOther`, and routes requests to the -resulting composed `RouterFunction`. +configuration, <>, combines them through +`RouterFunction.andOther`, and routes requests to the resulting composed `RouterFunction`. * `HandlerFunctionAdapter`: Simple adapter that lets `DispatcherHandler` invoke a `HandlerFunction` that was mapped to a request. @@ -723,7 +779,7 @@ For instance, consider the following example: .before(request -> ServerRequest.from(request) // <1> .header("X-RequestHeader", "Value") .build())) - .POST("/person", handler::createPerson)) + .POST(handler::createPerson)) .after((request, response) -> logResponse(response)) // <2> .build(); ---- @@ -743,10 +799,10 @@ For instance, consider the following example: ServerRequest.from(it) .header("X-RequestHeader", "Value").build() } - POST("/person", handler::createPerson) - after { _, response -> // <2> - logResponse(response) - } + } + POST(handler::createPerson) + after { _, response -> // <2> + logResponse(response) } } ---- @@ -774,7 +830,7 @@ The following example shows how to do so: .nest(accept(APPLICATION_JSON), b2 -> b2 .GET("/{id}", handler::getPerson) .GET(handler::listPeople)) - .POST("/person", handler::createPerson)) + .POST(handler::createPerson)) .filter((request, next) -> { if (securityManager.allowAccessTo(request.path())) { return next.handle(request); @@ -796,7 +852,7 @@ The following example shows how to do so: ("/person" and accept(APPLICATION_JSON)).nest { GET("/{id}", handler::getPerson) GET("", handler::listPeople) - POST("/person", handler::createPerson) + POST(handler::createPerson) filter { request, next -> if (securityManager.allowAccessTo(request.path())) { next(request) diff --git a/src/docs/asciidoc/web/webmvc-test.adoc b/framework-docs/src/docs/asciidoc/web/webmvc-test.adoc similarity index 98% rename from src/docs/asciidoc/web/webmvc-test.adoc rename to framework-docs/src/docs/asciidoc/web/webmvc-test.adoc index 307fe10a6bf1..87b6a5387458 100644 --- a/src/docs/asciidoc/web/webmvc-test.adoc +++ b/framework-docs/src/docs/asciidoc/web/webmvc-test.adoc @@ -1,4 +1,4 @@ -[[testing]] +[[webmvc.test]] = Testing [.small]#<># diff --git a/src/docs/asciidoc/web/webmvc-view.adoc b/framework-docs/src/docs/asciidoc/web/webmvc-view.adoc similarity index 99% rename from src/docs/asciidoc/web/webmvc-view.adoc rename to framework-docs/src/docs/asciidoc/web/webmvc-view.adoc index 74972cb803ab..b0725de35d8a 100644 --- a/src/docs/asciidoc/web/webmvc-view.adoc +++ b/framework-docs/src/docs/asciidoc/web/webmvc-view.adoc @@ -479,7 +479,7 @@ In similar fashion, you can specify HTML escaping per field, as the following ex [[mvc-view-groovymarkup]] == Groovy Markup -The http://groovy-lang.org/templating.html#_the_markuptemplateengine[Groovy Markup Template Engine] +The https://groovy-lang.org/templating.html#_the_markuptemplateengine[Groovy Markup Template Engine] is primarily aimed at generating XML-like markup (XML, XHTML, HTML5, and others), but you can use it to generate any text-based content. The Spring Framework has a built-in integration for using Spring MVC with Groovy Markup. @@ -615,14 +615,14 @@ Java 8+. Using the latest update release available is highly recommended. line should be added for Kotlin script support. See https://github.com/sdeleuze/kotlin-script-templating[this example] for more details. -You need to have the script templating library. One way to do that for Javascript is +You need to have the script templating library. One way to do that for JavaScript is through https://www.webjars.org/[WebJars]. [[mvc-view-script-integrate]] === Script Templates -[.small]#<># +[.small]#<># You can declare a `ScriptTemplateConfigurer` bean to specify the script engine to use, the script files to load, what function to call to render templates, and so on. @@ -822,8 +822,8 @@ template engine configuration, for example). The following example shows how to ---- Check out the Spring Framework unit tests, -https://github.com/spring-projects/spring-framework/tree/master/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script[Java], and -https://github.com/spring-projects/spring-framework/tree/master/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script[resources], +{spring-framework-main-code}/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script[Java], and +{spring-framework-main-code}/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script[resources], for more configuration examples. @@ -905,8 +905,7 @@ called `spring-form.tld`. To use the tags from this library, add the following directive to the top of your JSP page: -[source,xml,indent=0] -[subs="verbatim,quotes"] +[source,xml,indent=0,subs="verbatim,quotes"] ---- <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> ---- @@ -1698,7 +1697,7 @@ located in the `WEB-INF/defs` directory. At initialization of the `WebApplicatio the files are loaded, and the definitions factory are initialized. After that has been done, the Tiles included in the definition files can be used as views within your Spring web application. To be able to use the views, you have to have a `ViewResolver` -as with any other view technology in Spring : typically a convenient `TilesViewResolver`. +as with any other view technology in Spring: typically a convenient `TilesViewResolver`. You can specify locale-specific Tiles definitions by adding an underscore and then the locale, as the following example shows: diff --git a/src/docs/asciidoc/web/webmvc.adoc b/framework-docs/src/docs/asciidoc/web/webmvc.adoc similarity index 87% rename from src/docs/asciidoc/web/webmvc.adoc rename to framework-docs/src/docs/asciidoc/web/webmvc.adoc index 16143be5917d..addc18fc9189 100644 --- a/src/docs/asciidoc/web/webmvc.adoc +++ b/framework-docs/src/docs/asciidoc/web/webmvc.adoc @@ -1,20 +1,21 @@ [[mvc]] +:chapter: mvc = Spring Web MVC -:doc-spring-security: {doc-root}/spring-security/site/docs/current/reference +:doc-spring-security: {doc-root}/spring-security/reference Spring Web MVC is the original web framework built on the Servlet API and has been included in the Spring Framework from the very beginning. The formal name, "`Spring Web MVC,`" comes from the name of its source module -(https://github.com/spring-projects/spring-framework/tree/master/spring-webmvc[`spring-webmvc`]), +({spring-framework-main-code}/spring-webmvc[`spring-webmvc`]), but it is more commonly known as "`Spring MVC`". Parallel to Spring Web MVC, Spring Framework 5.0 introduced a reactive-stack web framework whose name, "`Spring WebFlux,`" is also based on its source module -(https://github.com/spring-projects/spring-framework/tree/master/spring-webflux[`spring-webflux`]). +({spring-framework-main-code}/spring-webflux[`spring-webflux`]). This section covers Spring Web MVC. The <> covers Spring WebFlux. -For baseline information and compatibility with Servlet container and Java EE version +For baseline information and compatibility with Servlet container and Jakarta EE version ranges, see the Spring Framework https://github.com/spring-projects/spring-framework/wiki/Spring-Framework-Versions[Wiki]. @@ -84,6 +85,11 @@ NOTE: In addition to using the ServletContext API directly, you can also extend `AbstractAnnotationConfigDispatcherServletInitializer` and override specific methods (see the example under <>). +NOTE: For programmatic use cases, a `GenericWebApplicationContext` can be used as an +alternative to `AnnotationConfigWebApplicationContext`. See the +{api-spring-framework}/web/context/support/GenericWebApplicationContext.html[`GenericWebApplicationContext`] +javadoc for details. + The following example of `web.xml` configuration registers and initializes the `DispatcherServlet`: [source,xml,indent=0,subs="verbatim,quotes"] @@ -301,7 +307,7 @@ Applications can declare the infrastructure beans listed in <> is the best starting point. It declares the required beans in either Java or XML and provides a higher-level configuration callback API to @@ -315,9 +321,9 @@ provides many extra convenient options. [[mvc-container-config]] === Servlet Config -In a Servlet 3.0+ environment, you have the option of configuring the Servlet container -programmatically as an alternative or in combination with a `web.xml` file. The following -example registers a `DispatcherServlet`: +In a Servlet environment, you have the option of configuring the Servlet container +programmatically as an alternative or in combination with a `web.xml` file. +The following example registers a `DispatcherServlet`: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -527,13 +533,9 @@ The `HandlerExceptionResolver` beans declared in the `WebApplicationContext` are resolve exceptions thrown during request processing. Those exception resolvers allow customizing the logic to address exceptions. See <> for more details. -The Spring `DispatcherServlet` also supports the return of the -`last-modification-date`, as specified by the Servlet API. The process of determining -the last modification date for a specific request is straightforward: The -`DispatcherServlet` looks up an appropriate handler mapping and tests whether the -handler that is found implements the `LastModified` interface. If so, the value of the -`long getLastModified(request)` method of the `LastModified` interface is returned to -the client. +For HTTP caching support, handlers can use the `checkNotModified` methods of `WebRequest`, +along with further options for annotated controllers as described in +<>. You can customize individual `DispatcherServlet` instances by adding Servlet initialization parameters (`init-param` elements) to the Servlet declaration in the @@ -573,6 +575,58 @@ initialization parameters (`init-param` elements) to the Servlet declaration in +[[mvc-handlermapping-path]] +=== Path Matching + +The Servlet API exposes the full request path as `requestURI` and further sub-divides it +into `contextPath`, `servletPath`, and `pathInfo` whose values vary depending on how a +Servlet is mapped. From these inputs, Spring MVC needs to determine the lookup path to +use for mapping handlers, which should exclude the `contextPath` and any `servletMapping` +prefix, if applicable. + +The `servletPath` and `pathInfo` are decoded and that makes them impossible to compare +directly to the full `requestURI` in order to derive the lookupPath and that makes it +necessary to decode the `requestURI`. However this introduces its own issues because the +path may contain encoded reserved characters such as `"/"` or `";"` that can in turn +alter the structure of the path after they are decoded which can also lead to security +issues. In addition, Servlet containers may normalize the `servletPath` to varying +degrees which makes it further impossible to perform `startsWith` comparisons against +the `requestURI`. + +This is why it is best to avoid reliance on the `servletPath` which comes with the +prefix-based `servletPath` mapping type. If the `DispatcherServlet` is mapped as the +default Servlet with `"/"` or otherwise without a prefix with `"/*"` and the Servlet +container is 4.0+ then Spring MVC is able to detect the Servlet mapping type and avoid +use of the `servletPath` and `pathInfo` altogether. On a 3.1 Servlet container, +assuming the same Servlet mapping types, the equivalent can be achieved by providing +a `UrlPathHelper` with `alwaysUseFullPath=true` via <> in +the MVC config. + +Fortunately the default Servlet mapping `"/"` is a good choice. However, there is still +an issue in that the `requestURI` needs to be decoded to make it possible to compare to +controller mappings. This is again undesirable because of the potential to decode +reserved characters that alter the path structure. If such characters are not expected, +then you can reject them (like the Spring Security HTTP firewall), or you can configure +`UrlPathHelper` with `urlDecode=false` but controller mappings will need to match to the +encoded path which may not always work well. Furthermore, sometimes the +`DispatcherServlet` needs to share the URL space with another Servlet and may need to +be mapped by prefix. + +The above issues are addressed when using `PathPatternParser` and parsed patterns, as +an alternative to String path matching with `AntPathMatcher`. The `PathPatternParser` has +been available for use in Spring MVC from version 5.3, and is enabled by default from +version 6.0. Unlike `AntPathMatcher` which needs either the lookup path decoded or the +controller mapping encoded, a parsed `PathPattern` matches to a parsed representation +of the path called `RequestPath`, one path segment at a time. This allows decoding and +sanitizing path segment values individually without the risk of altering the structure +of the path. Parsed `PathPattern` also supports the use of `servletPath` prefix mapping +as long as a Servlet path mapping is used and the prefix is kept simple, i.e. it has no +encoded characters. For pattern syntax details and comparison, see +<>. + + + + [[mvc-handlermapping-interceptor]] === Interception @@ -597,7 +651,7 @@ See <> in the section on MVC configuration for examples configure interceptors. You can also register them directly by using setters on individual `HandlerMapping` implementations. -Note that `postHandle` is less useful with `@ResponseBody` and `ResponseEntity` methods for +`postHandle` method is less useful with `@ResponseBody` and `ResponseEntity` methods for which the response is written and committed within the `HandlerAdapter` and before `postHandle`. That means it is too late to make any changes to the response, such as adding an extra header. For such scenarios, you can implement `ResponseBodyAdvice` and either @@ -606,6 +660,7 @@ declare it as an <> bean or configure it directly on + [[mvc-exceptionhandlers]] === Exceptions [.small]#<># @@ -640,7 +695,7 @@ The following table lists the available `HandlerExceptionResolver` implementatio |=== -[[mvc-excetionhandlers-handling]] +[[mvc-exceptionhandlers-handling]] ==== Chain of Resolvers You can form an exception resolver chain by declaring multiple `HandlerExceptionResolver` @@ -690,8 +745,8 @@ or to render a JSON response, as the following example shows: @RequestMapping(path = "/error") public Map handle(HttpServletRequest request) { Map map = new HashMap(); - map.put("status", request.getAttribute("javax.servlet.error.status_code")); - map.put("reason", request.getAttribute("javax.servlet.error.message")); + map.put("status", request.getAttribute("jakarta.servlet.error.status_code")); + map.put("reason", request.getAttribute("jakarta.servlet.error.message")); return map; } } @@ -705,8 +760,8 @@ or to render a JSON response, as the following example shows: @RequestMapping(path = ["/error"]) fun handle(request: HttpServletRequest): Map { val map = HashMap() - map["status"] = request.getAttribute("javax.servlet.error.status_code") - map["reason"] = request.getAttribute("javax.servlet.error.message") + map["status"] = request.getAttribute("jakarta.servlet.error.status_code") + map["reason"] = request.getAttribute("jakarta.servlet.error.message") return map } } @@ -741,7 +796,7 @@ The following table provides more details on the `ViewResolver` hierarchy: you can use the `removeFromCache(String viewName, Locale loc)` method. | `UrlBasedViewResolver` -| Simple implementation of the `ViewResolver` interface that affects the direct +| Simple implementation of the `ViewResolver` interface that effects the direct resolution of logical view names to URLs without an explicit mapping definition. This is appropriate if your logical names match the names of your view resources in a straightforward manner, without the need for arbitrary mappings. @@ -998,6 +1053,9 @@ application, thereby enhancing user experience. A theme is a collection of stati resources, typically style sheets and images, that affect the visual style of the application. +WARNING: as of 6.0 support for themes has been deprecated theme in favor of using CSS, +and without any special support on the server side. + [[mvc-themeresolver-defining]] ==== Defining a theme @@ -1085,29 +1143,41 @@ request with a simple request parameter. `MultipartResolver` from the `org.springframework.web.multipart` package is a strategy for parsing multipart requests including file uploads. There is one implementation -based on https://jakarta.apache.org/commons/fileupload[Commons FileUpload] and another -based on Servlet 3.0 multipart request parsing. +based on https://commons.apache.org/proper/commons-fileupload[Commons FileUpload] and +another based on Servlet multipart request parsing. To enable multipart handling, you need to declare a `MultipartResolver` bean in your `DispatcherServlet` Spring configuration with a name of `multipartResolver`. -The `DispatcherServlet` detects it and applies it to the incoming request. When a POST with -content-type of `multipart/form-data` is received, the resolver parses the content and -wraps the current `HttpServletRequest` as `MultipartHttpServletRequest` to -provide access to resolved parts in addition to exposing them as request parameters. +The `DispatcherServlet` detects it and applies it to the incoming request. When a POST +with a content type of `multipart/form-data` is received, the resolver parses the +content wraps the current `HttpServletRequest` as a `MultipartHttpServletRequest` to +provide access to resolved files in addition to exposing parts as request parameters. [[mvc-multipart-resolver-commons]] ==== Apache Commons `FileUpload` To use Apache Commons `FileUpload`, you can configure a bean of type -`CommonsMultipartResolver` with a name of `multipartResolver`. You also need to -have `commons-fileupload` as a dependency on your classpath. +`CommonsMultipartResolver` with a name of `multipartResolver`. You also need to have +the `commons-fileupload` jar as a dependency on your classpath. + +This resolver variant delegates to a local library within the application, providing +maximum portability across Servlet containers. As an alternative, consider standard +Servlet multipart resolution through the container's own parser as discussed below. + +[NOTE] +==== +Commons FileUpload traditionally applies to POST requests only but accepts any +`multipart/` content type. See the +{api-spring-framework}/web/multipart/commons/CommonsMultipartResolver.html[`CommonsMultipartResolver`] +javadoc for details and configuration options. +==== [[mvc-multipart-resolver-standard]] -==== Servlet 3.0 +==== Servlet Multipart Parsing -Servlet 3.0 multipart parsing needs to be enabled through Servlet container configuration. +Servlet multipart parsing needs to be enabled through Servlet container configuration. To do so: * In Java, set a `MultipartConfigElement` on the Servlet registration. @@ -1147,9 +1217,19 @@ The following example shows how to set a `MultipartConfigElement` on the Servlet } ---- -Once the Servlet 3.0 configuration is in place, you can add a bean of type +Once the Servlet multipart configuration is in place, you can add a bean of type `StandardServletMultipartResolver` with a name of `multipartResolver`. +[NOTE] +==== +This resolver variant uses your Servlet container's multipart parser as-is, +potentially exposing the application to container implementation differences. +By default, it will try to parse any `multipart/` content type with any HTTP +method but this may not be supported across all Servlet containers. See the +{api-spring-framework}/web/multipart/support/StandardServletMultipartResolver.html[`StandardServletMultipartResolver`] +javadoc for details and configuration options. +==== + [[mvc-logging]] @@ -1444,17 +1524,23 @@ directly to the response body versus view resolution and rendering with an HTML [[mvc-ann-requestmapping-proxying]] ==== AOP Proxies +[.small]#<># In some cases, you may need to decorate a controller with an AOP proxy at runtime. One example is if you choose to have `@Transactional` annotations directly on the controller. When this is the case, for controllers specifically, we recommend -using class-based proxying. This is typically the default choice with controllers. -However, if a controller must implement an interface that is not a Spring Context -callback (such as `InitializingBean`, `*Aware`, and others), you may need to explicitly -configure class-based proxying. For example, with `` you can -change to ``, and with -`@EnableTransactionManagement` you can change to -`@EnableTransactionManagement(proxyTargetClass = true)`. +using class-based proxying. This is automatically the case with such annotations +directly on the controller. + +If the controller implements an interface, and needs AOP proxying, you may need to +explicitly configure class-based proxying. For example, with `@EnableTransactionManagement` +you can change to `@EnableTransactionManagement(proxyTargetClass = true)`, and with +`` you can change to ``. + +NOTE: Keep in mind that as of 6.0, with interface proxying, Spring MVC no longer detects +controllers based solely on a type-level `@RequestMapping` annotation on the interface. +Please, enable class based proxying, or otherwise the interface must also have an +`@Controller` annotation. @@ -1477,8 +1563,8 @@ There are also HTTP method specific shortcut variants of `@RequestMapping`: The shortcuts are <> that are provided because, arguably, most controller methods should be mapped to a specific HTTP method versus -using `@RequestMapping`, which, by default, matches to all HTTP methods. At the same, -a `@RequestMapping` is still needed at the class level to express shared mappings. +using `@RequestMapping`, which, by default, matches to all HTTP methods. +A `@RequestMapping` is still needed at the class level to express shared mappings. The following example has type and method level mappings: @@ -1538,11 +1624,11 @@ filesystem, and other locations. It is less efficient and the String path input challenge for dealing effectively with encoding and other issues with URLs. `PathPattern` is the recommended solution for web applications and it is the only choice in -Spring WebFlux. Prior to version 5.3, `AntPathMatcher` was the only choice in Spring MVC -and continues to be the default. However `PathPattern` can be enabled in the -<>. +Spring WebFlux. It was enabled for use in Spring MVC from version 5.3 and is enabled by +default from version 6.0. See <> for +customizations of path matching options. -`PathPattern` supports the same pattern syntax as `AntPathMatcher`. In addition it also +`PathPattern` supports the same pattern syntax as `AntPathMatcher`. In addition, it also supports the capturing pattern, e.g. `+{*spring}+`, for matching 0 or more path segments at the end of a path. `PathPattern` also restricts the use of `+**+` for matching multiple path segments such that it's only allowed at the end of a pattern. This eliminates many @@ -1618,7 +1704,7 @@ leave that detail out if the names are the same and your code is compiled with d information or with the `-parameters` compiler flag on Java 8. The syntax `{varName:regex}` declares a URI variable with a regular expression that has -syntax of `{varName:regex}`. For example, given URL `"/spring-web-3.0.5 .jar"`, the following method +syntax of `{varName:regex}`. For example, given URL `"/spring-web-3.0.5.jar"`, the following method extracts the name, version, and file extension: [source,java,indent=0,subs="verbatim,quotes",role="primary"] @@ -1639,9 +1725,9 @@ extracts the name, version, and file extension: ---- URI path patterns can also have embedded `${...}` placeholders that are resolved on startup -by using `PropertyPlaceHolderConfigurer` against local, system, environment, and other property -sources. You can use this, for example, to parameterize a base URL based on some external -configuration. +by using `PropertySourcesPlaceholderConfigurer` against local, system, environment, and +other property sources. You can use this, for example, to parameterize a base URL based on +some external configuration. @@ -1650,12 +1736,12 @@ configuration. [.small]#<># When multiple patterns match a URL, the best match must be selected. This is done with -one of the following depending on whether parsed `PathPattern`'s are enabled for use or not: +one of the following depending on whether use of parsed `PathPattern` is enabled for use or not: * {api-spring-framework}/web/util/pattern/PathPattern.html#SPECIFICITY_COMPARATOR[`PathPattern.SPECIFICITY_COMPARATOR`] * {api-spring-framework}/util/AntPathMatcher.html#getPatternComparator-java.lang.String-[`AntPathMatcher.getPatternComparator(String path)`] -Both help to sorts patterns with more specific ones on top. A pattern is less specific if +Both help to sort patterns with more specific ones on top. A pattern is less specific if it has a lower count of URI variables (counted as 1), single wildcards (counted as 1), and double wildcards (counted as 2). Given an equal score, the longer pattern is chosen. Given the same score and length, the pattern with more URI variables than wildcards is @@ -1684,7 +1770,7 @@ using the `Accept` header should be the preferred choice. Over time, the use of file name extensions has proven problematic in a variety of ways. It can cause ambiguity when overlain with the use of URI variables, path parameters, and URI encoding. Reasoning about URL-based authorization -and security (see next section for more details) also become more difficult. +and security (see next section for more details) also becomes more difficult. To completely disable the use of path extensions in versions prior to 5.3, set the following: @@ -1864,7 +1950,7 @@ instead. `@GetMapping` (and `@RequestMapping(method=HttpMethod.GET)`) support HTTP HEAD transparently for request mapping. Controller methods do not need to change. -A response wrapper, applied in `javax.servlet.http.HttpServlet`, ensures a `Content-Length` +A response wrapper, applied in `jakarta.servlet.http.HttpServlet`, ensures a `Content-Length` header is set to the number of bytes written (without actually writing to the response). `@GetMapping` (and `@RequestMapping(method=HttpMethod.GET)`) are implicitly mapped to @@ -1985,17 +2071,17 @@ and others) and is equivalent to `required=false`. | Generic access to request parameters and request and session attributes, without direct use of the Servlet API. -| `javax.servlet.ServletRequest`, `javax.servlet.ServletResponse` +| `jakarta.servlet.ServletRequest`, `jakarta.servlet.ServletResponse` | Choose any specific request or response type -- for example, `ServletRequest`, `HttpServletRequest`, or Spring's `MultipartRequest`, `MultipartHttpServletRequest`. -| `javax.servlet.http.HttpSession` +| `jakarta.servlet.http.HttpSession` | Enforces the presence of a session. As a consequence, such an argument is never `null`. Note that session access is not thread-safe. Consider setting the `RequestMappingHandlerAdapter` instance's `synchronizeOnSession` flag to `true` if multiple requests are allowed to concurrently access a session. -| `javax.servlet.http.PushBuilder` +| `jakarta.servlet.http.PushBuilder` | Servlet 4.0 push builder API for programmatic HTTP/2 resource pushes. Note that, per the Servlet specification, the injected `PushBuilder` instance can be null if the client does not support that HTTP/2 feature. @@ -2003,6 +2089,12 @@ and others) and is equivalent to `required=false`. | `java.security.Principal` | Currently authenticated user -- possibly a specific `Principal` implementation class if known. + Note that this argument is not resolved eagerly, if it is annotated in order to allow a custom resolver to resolve it + before falling back on default resolution via `HttpServletRequest#getUserPrincipal`. + For example, the Spring Security `Authentication` implements `Principal` and would be injected as such via + `HttpServletRequest#getUserPrincipal`, unless it is also annotated with `@AuthenticationPrincipal` in which case it + is resolved by a custom Spring Security resolver through `Authentication#getPrincipal`. + | `HttpMethod` | The HTTP method of the request. @@ -2096,8 +2188,8 @@ and others) and is equivalent to `required=false`. | Any other argument | If a method argument is not matched to any of the earlier values in this table and it is a simple type (as determined by - {api-spring-framework}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty], - it is a resolved as a `@RequestParam`. Otherwise, it is resolved as a `@ModelAttribute`. + {api-spring-framework}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]), + it is resolved as a `@RequestParam`. Otherwise, it is resolved as a `@ModelAttribute`. |=== @@ -2124,6 +2216,14 @@ supported for all return values. | `HttpHeaders` | For returning a response with headers and no body. +| `ErrorResponse` +| To render an RFC 7807 error response with details in the body, + see <> + +| `ProblemDetail` +| To render an RFC 7807 error response with details in the body, + see <> + | `String` | A view name to be resolved with `ViewResolver` implementations and used together with the implicit model -- determined through command objects and `@ModelAttribute` methods. The handler @@ -2182,23 +2282,18 @@ supported for all return values. | Write to the response `OutputStream` asynchronously. Also supported as the body of a `ResponseEntity`. See <> and <>. -| Reactive types -- Reactor, RxJava, or others through `ReactiveAdapterRegistry` -| Alternative to `DeferredResult` with multi-value streams (for example, `Flux`, `Observable`) - collected to a `List`. - - For streaming scenarios (for example, `text/event-stream`, `application/json+stream`), - `SseEmitter` and `ResponseBodyEmitter` are used instead, where `ServletOutputStream` - blocking I/O is performed on a Spring MVC-managed thread and back pressure is applied - against the completion of each write. - - See <> and <>. - -| Any other return value -| Any return value that does not match any of the earlier values in this table and that - is a `String` or `void` is treated as a view name (default view name selection through - `RequestToViewNameTranslator` applies), provided it is not a simple type, as determined by - {api-spring-framework}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]. - Values that are simple types remain unresolved. +| Reactor and other reactive types registered via `ReactiveAdapterRegistry` +| A single value type, e.g. `Mono`, is comparable to returning `DeferredResult`. + A multi-value type, e.g. `Flux`, may be treated as a stream depending on the requested + media type, e.g. "text/event-stream", "application/json+stream", or otherwise is + collected to a List and rendered as a single value. See <> and + <>. + +| Other return values +| If a return value remains unresolved in any other way, it is treated as a model + attribute, unless it is a simple type as determined by + {api-spring-framework}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty], + in which case it remains unresolved. |=== @@ -2216,6 +2311,24 @@ type conversion through a `WebDataBinder` (see <>) or by reg `Formatters` with the `FormattingConversionService`. See <>. +A practical issue in type conversion is the treatment of an empty String source value. +Such a value is treated as missing if it becomes `null` as a result of type conversion. +This can be the case for `Long`, `UUID`, and other target types. If you want to allow `null` +to be injected, either use the `required` flag on the argument annotation, or declare the +argument as `@Nullable`. + +[NOTE] +==== +As of 5.3, non-null arguments will be enforced even after type conversion. If your handler +method intends to accept a null value as well, either declare your argument as `@Nullable` +or mark it as `required=false` in the corresponding `@RequestParam`, etc. annotation. This is +a best practice and the recommended solution for regressions encountered in a 5.3 upgrade. + +Alternatively, you may specifically handle e.g. the resulting `MissingPathVariableException` +in the case of a required `@PathVariable`. A null value after conversion will be treated like +an empty original value, so the corresponding `Missing...Exception` variants will be thrown. +==== + [[mvc-ann-matrix-variables]] ==== Matrix Variables @@ -2548,33 +2661,41 @@ query parameters and form fields. The following example shows how to do so: .Java ---- @PostMapping("/owners/{ownerId}/pets/{petId}/edit") - public String processSubmit(@ModelAttribute Pet pet) { } <1> + public String processSubmit(@ModelAttribute Pet pet) { + // method logic... + } ---- -<1> Bind an instance of `Pet`. [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] .Kotlin ---- @PostMapping("/owners/{ownerId}/pets/{petId}/edit") -fun processSubmit(@ModelAttribute pet: Pet): String { } // <1> +fun processSubmit(@ModelAttribute pet: Pet): String { + // method logic... +} ---- -<1> Bind an instance of `Pet`. - -The `Pet` instance above is resolved as follows: -* From the model if already added by using <>. -* From the HTTP session by using <>. -* From a URI path variable passed through a `Converter` (see the next example). -* From the invocation of a default constructor. -* From the invocation of a "`primary constructor`" with arguments that match to Servlet -request parameters. Argument names are determined through JavaBeans -`@ConstructorProperties` or through runtime-retained parameter names in the bytecode. - -While it is common to use a <> to populate the model with -attributes, one other alternative is to rely on a `Converter` in combination -with a URI path variable convention. In the following example, the model attribute name, -`account`, matches the URI path variable, `account`, and the `Account` is loaded by passing -the `String` account number through a registered `Converter`: +The `Pet` instance above is sourced in one of the following ways: + +* Retrieved from the model where it may have been added by a + <>. +* Retrieved from the HTTP session if the model attribute was listed in + the class-level <> annotation. +* Obtained through a `Converter` where the model attribute name matches the name of a + request value such as a path variable or a request parameter (see next example). +* Instantiated using its default constructor. +* Instantiated through a "`primary constructor`" with arguments that match to Servlet + request parameters. Argument names are determined through JavaBeans + `@ConstructorProperties` or through runtime-retained parameter names in the bytecode. + +One alternative to using a <> to +supply it or relying on the framework to create the model attribute, is to have a +`Converter` to provide the instance. This is applied when the model attribute +name matches to the name of a request value such as a path variable or a request +parameter, and there is a `Converter` from `String` to the model attribute type. +In the following example, the model attribute name is `account` which matches the URI +path variable `account`, and there is a registered `Converter` which +could load the `Account` from a data store: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -2677,8 +2798,8 @@ alternatively, set `@ModelAttribute(binding=false)`, as the following example sh <1> Setting `@ModelAttribute(binding=false)`. You can automatically apply validation after data binding by adding the -`javax.validation.Valid` annotation or Spring's `@Validated` annotation ( -<> and +`jakarta.validation.Valid` annotation or Spring's `@Validated` annotation +(<> and <>). The following example shows how to do so: [source,java,indent=0,subs="verbatim,quotes",role="primary"] @@ -2741,7 +2862,7 @@ The following example uses the `@SessionAttributes` annotation: ---- @Controller @SessionAttributes("pet") // <1> - public class EditPetForm { + class EditPetForm { // ... } ---- @@ -2766,9 +2887,8 @@ storage, as the following example shows: if (errors.hasErrors) { // ... } - status.setComplete(); // <2> - // ... - } + status.setComplete(); // <2> + // ... } } ---- @@ -2828,7 +2948,7 @@ as the following example shows: For use cases that require adding or removing session attributes, consider injecting `org.springframework.web.context.request.WebRequest` or -`javax.servlet.http.HttpSession` into the controller method. +`jakarta.servlet.http.HttpSession` into the controller method. For temporary storage of model attributes in the session as part of a controller workflow, consider using `@SessionAttributes` as described in @@ -3017,7 +3137,7 @@ When the `@RequestParam` annotation is declared as a `Map `MultiValueMap`, without a parameter name specified in the annotation, then the map is populated with the multipart files for each given parameter name. -NOTE: With Servlet 3.0 multipart parsing, you may also declare `javax.servlet.http.Part` +NOTE: With Servlet multipart parsing, you may also declare `jakarta.servlet.http.Part` instead of Spring's `MultipartFile`, as a method argument or collection value type. You can also use multipart content as part of data binding to a @@ -3119,7 +3239,7 @@ probably want it deserialized from JSON (similar to `@RequestBody`). Use the } ---- -You can use `@RequestPart` in combination with `javax.validation.Valid` or use Spring's +You can use `@RequestPart` in combination with `jakarta.validation.Valid` or use Spring's `@Validated` annotation, both of which cause Standard Bean Validation to be applied. By default, validation errors cause a `MethodArgumentNotValidException`, which is turned into a 400 (BAD_REQUEST) response. Alternatively, you can handle validation errors locally @@ -3176,7 +3296,7 @@ The following example uses a `@RequestBody` argument: You can use the <> option of the <> to configure or customize message conversion. -You can use `@RequestBody` in combination with `javax.validation.Valid` or Spring's +You can use `@RequestBody` in combination with `jakarta.validation.Valid` or Spring's `@Validated` annotation, both of which cause Standard Bean Validation to be applied. By default, validation errors cause a `MethodArgumentNotValidException`, which is turned into a 400 (BAD_REQUEST) response. Alternatively, you can handle validation errors locally @@ -3282,7 +3402,7 @@ See <> for details. public ResponseEntity handle() { String body = ... ; String etag = ... ; - return ResponseEntity.ok().eTag(etag).build(body); + return ResponseEntity.ok().eTag(etag).body(body); } ---- [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] @@ -3298,7 +3418,14 @@ See <> for details. Spring MVC supports using a single value <> to produce the `ResponseEntity` asynchronously, and/or single and multi-value reactive -types for the body. +types for the body. This allows the following types of async responses: + +* `ResponseEntity>` or `ResponseEntity>` make the response status and + headers known immediately while the body is provided asynchronously at a later point. + Use `Mono` if the body consists of 0..1 values or `Flux` if it can produce multiple values. +* `Mono>` provides all three -- response status, headers, and body, + asynchronously at a later point. This allows the response status and headers to vary + depending on the outcome of asynchronous request handling. [[mvc-ann-jackson]] @@ -3640,6 +3767,13 @@ controller-specific `Formatter` implementations, as the following example shows: ---- <1> Defining an `@InitBinder` method on a custom formatter. +[[mvc-ann-initbinder-model-design]] +==== Model Design +[.small]#<># + +include::web-data-binding-model-design.adoc[] + + [[mvc-ann-exceptionhandler]] === Exceptions [.small]#<># @@ -3676,14 +3810,15 @@ controller-specific `Formatter` implementations, as the following example shows: } ---- -The exception may match against a top-level exception being propagated (that is, a direct -`IOException` being thrown) or against the immediate cause within a top-level wrapper exception -(for example, an `IOException` wrapped inside an `IllegalStateException`). +The exception may match against a top-level exception being propagated (e.g. a direct +`IOException` being thrown) or against a nested cause within a wrapper exception (e.g. +an `IOException` wrapped inside an `IllegalStateException`). As of 5.3, this can match +at arbitrary cause levels, whereas previously only an immediate cause was considered. For matching exception types, preferably declare the target exception as a method argument, -as the preceding example shows. When multiple exception methods match, a root exception match is generally -preferred to a cause exception match. More specifically, the `ExceptionDepthComparator` is -used to sort exceptions based on their depth from the thrown exception type. +as the preceding example shows. When multiple exception methods match, a root exception match is +generally preferred to a cause exception match. More specifically, the `ExceptionDepthComparator` +is used to sort exceptions based on their depth from the thrown exception type. Alternatively, the annotation declaration may narrow the exception types to match, as the following example shows: @@ -3767,6 +3902,7 @@ level, <> mechanism. [[mvc-ann-exceptionhandler-args]] ==== Method Arguments +[.small]#<># `@ExceptionHandler` methods support the following arguments: @@ -3784,11 +3920,11 @@ level, <> mechanism. | Generic access to request parameters and request and session attributes without direct use of the Servlet API. -| `javax.servlet.ServletRequest`, `javax.servlet.ServletResponse` +| `jakarta.servlet.ServletRequest`, `jakarta.servlet.ServletResponse` | Choose any specific request or response type (for example, `ServletRequest` or `HttpServletRequest` or Spring's `MultipartRequest` or `MultipartHttpServletRequest`). -| `javax.servlet.http.HttpSession` +| `jakarta.servlet.http.HttpSession` | Enforces the presence of a session. As a consequence, such an argument is never `null`. + Note that session access is not thread-safe. Consider setting the `RequestMappingHandlerAdapter` instance's `synchronizeOnSession` flag to `true` if multiple @@ -3831,6 +3967,7 @@ level, <> mechanism. [[mvc-ann-exceptionhandler-return-values]] ==== Return Values +[.small]#<># `@ExceptionHandler` methods support the following return values: @@ -3847,6 +3984,14 @@ level, <> mechanism. be converted through `HttpMessageConverter` instances and written to the response. See <>. +| `ErrorResponse` +| To render an RFC 7807 error response with details in the body, +see <> + +| `ProblemDetail` +| To render an RFC 7807 error response with details in the body, +see <> + | `String` | A view name to be resolved with `ViewResolver` implementations and used together with the implicit model -- determined through command objects and `@ModelAttribute` methods. @@ -3889,52 +4034,30 @@ level, <> mechanism. |=== -[[mvc-ann-rest-exceptions]] -==== REST API exceptions -[.small]#<># - -A common requirement for REST services is to include error details in the body of the -response. The Spring Framework does not automatically do this because the representation -of error details in the response body is application-specific. However, a -`@RestController` may use `@ExceptionHandler` methods with a `ResponseEntity` return -value to set the status and the body of the response. Such methods can also be declared -in `@ControllerAdvice` classes to apply them globally. - -Applications that implement global exception handling with error details in the response -body should consider extending -{api-spring-framework}/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.html[`ResponseEntityExceptionHandler`], -which provides handling for exceptions that Spring MVC raises and provides hooks to -customize the response body. To make use of this, create a subclass of -`ResponseEntityExceptionHandler`, annotate it with `@ControllerAdvice`, override the -necessary methods, and declare it as a Spring bean. - - [[mvc-ann-controller-advice]] === Controller Advice [.small]#<># -Typically `@ExceptionHandler`, `@InitBinder`, and `@ModelAttribute` methods apply within -the `@Controller` class (or class hierarchy) in which they are declared. If you want such -methods to apply more globally (across controllers), you can declare them in a class -annotated with `@ControllerAdvice` or `@RestControllerAdvice`. +`@ExceptionHandler`, `@InitBinder`, and `@ModelAttribute` methods apply only to the +`@Controller` class, or class hierarchy, in which they are declared. If, instead, they +are declared in an `@ControllerAdvice` or `@RestControllerAdvice` class, then they apply +to any controller. Moreover, as of 5.3, `@ExceptionHandler` methods in `@ControllerAdvice` +can be used to handle exceptions from any `@Controller` or any other handler. -`@ControllerAdvice` is annotated with `@Component`, which means such classes can be -registered as Spring beans through <>. `@RestControllerAdvice` is a composed annotation that is annotated -with both `@ControllerAdvice` and `@ResponseBody`, which essentially means -`@ExceptionHandler` methods are rendered to the response body through message conversion -(versus view resolution or template rendering). +`@ControllerAdvice` is meta-annotated with `@Component` and therefore can be registered as +a Spring bean through <>. `@RestControllerAdvice` is meta-annotated with `@ControllerAdvice` +and `@ResponseBody`, and that means `@ExceptionHandler` methods will have their return +value rendered via response body message conversion, rather than via HTML views. -On startup, the infrastructure classes for `@RequestMapping` and `@ExceptionHandler` -methods detect Spring beans annotated with `@ControllerAdvice` and then apply their -methods at runtime. Global `@ExceptionHandler` methods (from a `@ControllerAdvice`) are -applied _after_ local ones (from the `@Controller`). By contrast, global `@ModelAttribute` -and `@InitBinder` methods are applied _before_ local ones. +On startup, `RequestMappingHandlerMapping` and `ExceptionHandlerExceptionResolver` detect +controller advice beans and apply them at runtime. Global `@ExceptionHandler` methods, +from an `@ControllerAdvice`, are applied _after_ local ones, from the `@Controller`. +By contrast, global `@ModelAttribute` and `@InitBinder` methods are applied _before_ local ones. -By default, `@ControllerAdvice` methods apply to every request (that is, all controllers), -but you can narrow that down to a subset of controllers by using attributes on the -annotation, as the following example shows: +The `@ControllerAdvice` annotation has attributes that let you narrow the set of controllers +and handlers that they apply to. For example: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -3997,24 +4120,22 @@ as the following example shows: ---- HttpServletRequest request = ... - // Re-uses host, scheme, port, path and query string... + // Re-uses scheme, host, port, path, and query string... - ServletUriComponentsBuilder ucb = ServletUriComponentsBuilder.fromRequest(request) - .replaceQueryParam("accountId", "{id}").build() - .expand("123") - .encode(); + URI uri = ServletUriComponentsBuilder.fromRequest(request) + .replaceQueryParam("accountId", "{id}") + .build("123"); ---- [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] .Kotlin ---- val request: HttpServletRequest = ... - // Re-uses host, scheme, port, path and query string... + // Re-uses scheme, host, port, path, and query string... - val ucb = ServletUriComponentsBuilder.fromRequest(request) - .replaceQueryParam("accountId", "{id}").build() - .expand("123") - .encode() + val uri = ServletUriComponentsBuilder.fromRequest(request) + .replaceQueryParam("accountId", "{id}") + .build("123") ---- You can create URIs relative to the context path, as the following example shows: @@ -4022,18 +4143,26 @@ You can create URIs relative to the context path, as the following example shows [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java ---- - // Re-uses host, port and context path... + HttpServletRequest request = ... + + // Re-uses scheme, host, port, and context path... - ServletUriComponentsBuilder ucb = ServletUriComponentsBuilder.fromContextPath(request) - .path("/accounts").build() + URI uri = ServletUriComponentsBuilder.fromContextPath(request) + .path("/accounts") + .build() + .toUri(); ---- [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] .Kotlin ---- - // Re-uses host, port and context path... + val request: HttpServletRequest = ... - val ucb = ServletUriComponentsBuilder.fromContextPath(request) - .path("/accounts").build() + // Re-uses scheme, host, port, and context path... + + val uri = ServletUriComponentsBuilder.fromContextPath(request) + .path("/accounts") + .build() + .toUri() ---- You can create URIs relative to a Servlet (for example, `/main/{asterisk}`), @@ -4042,18 +4171,26 @@ as the following example shows: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java ---- - // Re-uses host, port, context path, and Servlet prefix... + HttpServletRequest request = ... - ServletUriComponentsBuilder ucb = ServletUriComponentsBuilder.fromServletMapping(request) - .path("/accounts").build() + // Re-uses scheme, host, port, context path, and Servlet mapping prefix... + + URI uri = ServletUriComponentsBuilder.fromServletMapping(request) + .path("/accounts") + .build() + .toUri(); ---- [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] .Kotlin ---- - // Re-uses host, port, context path, and Servlet prefix... + val request: HttpServletRequest = ... - val ucb = ServletUriComponentsBuilder.fromServletMapping(request) - .path("/accounts").build() + // Re-uses scheme, host, port, context path, and Servlet mapping prefix... + + val uri = ServletUriComponentsBuilder.fromServletMapping(request) + .path("/accounts") + .build() + .toUri() ---- NOTE: As of 5.1, `ServletUriComponentsBuilder` ignores information from the `Forwarded` and @@ -4245,11 +4382,11 @@ capital letters of the class and the method name (for example, the `getThing` me == Asynchronous Requests [.small]#<># -Spring MVC has an extensive integration with Servlet 3.0 asynchronous request +Spring MVC has an extensive integration with Servlet asynchronous request <>: * <> and <> -return values in controller methods and provide basic support for a single asynchronous +return values in controller methods provide basic support for a single asynchronous return value. * Controllers can <> multiple values, including <> and <>. @@ -4419,10 +4556,10 @@ methods for timeout and completion callbacks. ==== Compared to WebFlux The Servlet API was originally built for making a single pass through the Filter-Servlet -chain. Asynchronous request processing, added in Servlet 3.0, lets applications exit -the Filter-Servlet chain but leave the response open for further processing. The Spring MVC -asynchronous support is built around that mechanism. When a controller returns a `DeferredResult`, -the Filter-Servlet chain is exited, and the Servlet container thread is released. Later, when +chain. Asynchronous request processing lets applications exit the Filter-Servlet chain +but leave the response open for further processing. The Spring MVC asynchronous support +is built around that mechanism. When a controller returns a `DeferredResult`, the +Filter-Servlet chain is exited, and the Servlet container thread is released. Later, when the `DeferredResult` is set, an `ASYNC` dispatch (to the same URL) is made, during which the controller is mapped again but, rather than invoking it, the `DeferredResult` value is used (as if the controller returned it) to resume processing. @@ -4633,6 +4770,46 @@ suitable under load. If you plan to stream with a reactive type, you should use +[[mvc-ann-async-context-propagation]] +=== Context Propagation + +It is common to propagate context via `java.lang.ThreadLocal`. This works transparently +for handling on the same thread, but requires additional work for asynchronous handling +across multiple threads. The Micrometer +https://github.com/micrometer-metrics/context-propagation#context-propagation-library[Context Propagation] +library simplifies context propagation across threads, and across context mechanisms such +as `ThreadLocal` values, +Reactor https://projectreactor.io/docs/core/release/reference/#context[context], +GraphQL Java https://www.graphql-java.com/documentation/concerns/#context-objects[context], +and others. + +If Micrometer Context Propagation is present on the classpath, when a controller method +returns a <> such as `Flux` or `Mono`, all +`ThreadLocal` values, for which there is a registered `io.micrometer.ThreadLocalAccessor`, +are written to the Reactor `Context` as key-value pairs, using the key assigned by the +`ThreadLocalAccessor`. + +For other asynchronous handling scenarios, you can use the Context Propagation library +directly. For example: + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- + // Capture ThreadLocal values from the main thread ... + ContextSnapshot snapshot = ContextSnapshot.captureAll(); + + // On a different thread: restore ThreadLocal values + try (ContextSnapshot.Scope scoped = snapshot.setThreadLocals()) { + // ... + } +---- + +For more details, see the +https://micrometer.io/docs/contextPropagation[documentation] of the Micrometer Context +Propagation library. + + + [[mvc-ann-async-disconnects]] === Disconnects [.small]#<># @@ -4663,7 +4840,7 @@ The MVC configuration also exposes several options for asynchronous requests. Filter and Servlet declarations have an `asyncSupported` flag that needs to be set to `true` to enable asynchronous request processing. In addition, Filter mappings should be -declared to handle the `ASYNC` `javax.servlet.DispatchType`. +declared to handle the `ASYNC` `jakarta.servlet.DispatchType`. In Java configuration, when you use `AbstractAnnotationConfigDispatcherServletInitializer` to initialize the Servlet container, this is done automatically. @@ -4696,23 +4873,210 @@ Note that you can also set the default timeout value on a `DeferredResult`, a `ResponseBodyEmitter`, and an `SseEmitter`. For a `Callable`, you can use `WebAsyncTask` to provide a timeout value. + include::webmvc-cors.adoc[leveloffset=+1] +[[mvc-ann-rest-exceptions]] +== Error Responses +[.small]#<># + +A common requirement for REST services is to include details in the body of error +responses. The Spring Framework supports the "Problem Details for HTTP APIs" +specification, https://www.rfc-editor.org/rfc/rfc7807.html[RFC 7807]. + +The following are the main abstractions for this support: + +- `ProblemDetail` -- representation for an RFC 7807 problem detail; a simple container +for both standard fields defined in the spec, and for non-standard ones. +- `ErrorResponse` -- contract to expose HTTP error response details including HTTP +status, response headers, and a body in the format of RFC 7807; this allows exceptions to +encapsulate and expose the details of how they map to an HTTP response. All Spring MVC +exceptions implement this. +- `ErrorResponseException` -- basic `ErrorResponse` implementation that others +can use as a convenient base class. +- `ResponseEntityExceptionHandler` -- convenient base class for an +<> that handles all Spring MVC exceptions, +and any `ErrorResponseException`, and renders an error response with a body. + + + +[[mvc-ann-rest-exceptions-render]] +=== Render +[.small]#<># + +You can return `ProblemDetail` or `ErrorResponse` from any `@ExceptionHandler` or from +any `@RequestMapping` method to render an RFC 7807 response. This is processed as follows: + +- The `status` property of `ProblemDetail` determines the HTTP status. +- The `instance` property of `ProblemDetail` is set from the current URL path, if not +already set. +- For content negotiation, the Jackson `HttpMessageConverter` prefers +"application/problem+json" over "application/json" when rendering a `ProblemDetail`, +and also falls back on it if no compatible media type is found. + +To enable RFC 7807 responses for Spring WebFlux exceptions and for any +`ErrorResponseException`, extend `ResponseEntityExceptionHandler` and declare it as an +<> in Spring configuration. The handler +has an `@ExceptionHandler` method that handles any `ErrorResponse` exception, which +includes all built-in web exceptions. You can add more exception handling methods, and +use a protected method to map any exception to a `ProblemDetail`. + + + +[[mvc-ann-rest-exceptions-non-standard]] +=== Non-Standard Fields +[.small]#<># + +You can extend an RFC 7807 response with non-standard fields in one of two ways. + +One, insert into the "properties" `Map` of `ProblemDetail`. When using the Jackson +library, the Spring Framework registers `ProblemDetailJacksonMixin` that ensures this +"properties" `Map` is unwrapped and rendered as top level JSON properties in the +response, and likewise any unknown property during deserialization is inserted into +this `Map`. + +You can also extend `ProblemDetail` to add dedicated non-standard properties. +The copy constructor in `ProblemDetail` allows a subclass to make it easy to be created +from an existing `ProblemDetail`. This could be done centrally, e.g. from an +`@ControllerAdvice` such as `ResponseEntityExceptionHandler` that re-creates the +`ProblemDetail` of an exception into a subclass with the additional non-standard fields. + + + +[[mvc-ann-rest-exceptions-i18n]] +=== Internationalization +[.small]#<># + +It is a common requirement to internationalize error response details, and good practice +to customize the problem details for Spring MVC exceptions. This is supported as follows: + +- Each `ErrorResponse` exposes a message code and arguments to resolve the "detail" field +through a <>. +The actual message code value is parameterized with placeholders, e.g. +`"HTTP method {0} not supported"` to be expanded from the arguments. +- Each `ErrorResponse` also exposes a message code to resolve the "title" field. +- `ResponseEntityExceptionHandler` uses the message code and arguments to resolve the +"detail" and the "title" fields. + +By default, the message code for the "detail" field is "problemDetail." + the fully +qualified exception class name. Some exceptions may expose additional message codes in +which case a suffix is added to the default message code. The table below lists message +arguments and codes for Spring MVC exceptions: + +[[mvc-ann-rest-exceptions-codes]] +[cols="1,1,2", options="header"] +|=== +| Exception | Message Code | Message Code Arguments + +| `AsyncRequestTimeoutException` +| (default) +| + +| `ConversionNotSupportedException` +| (default) +| `{0}` property name, `{1}` property value + +| `HttpMediaTypeNotAcceptableException` +| (default) +| `{0}` list of supported media types + +| `HttpMediaTypeNotAcceptableException` +| (default) + ".parseError" +| + +| `HttpMediaTypeNotSupportedException` +| (default) +| `{0}` the media type that is not supported, `{1}` list of supported media types + +| `HttpMediaTypeNotSupportedException` +| (default) + ".parseError" +| + +| `HttpMessageNotReadableException` +| (default) +| + +| `HttpMessageNotWritableException` +| (default) +| + +| `HttpRequestMethodNotSupportedException` +| (default) +| `{0}` the current HTTP method, `{1}` the list of supported HTTP methods + +| `MethodArgumentNotValidException` +| (default) +| `{0}` the list of global errors, `{1}` the list of field errors. + Message codes and arguments for each error within the `BindingResult` are also resolved + via `MessageSource`. + +| `MissingRequestHeaderException` +| (default) +| `{0}` the header name + +| `MissingServletRequestParameterException` +| (default) +| `{0}` the request parameter name + +| `MissingMatrixVariableException` +| (default) +| `{0}` the matrix variable name + +| `MissingPathVariableException` +| (default) +| `{0}` the path variable name + +| `MissingRequestCookieException` +| (default) +| `{0}` the cookie name + +| `MissingServletRequestPartException` +| (default) +| `{0}` the part name + +| `NoHandlerFoundException` +| (default) +| + +| `TypeMismatchException` +| (default) +| `{0}` property name, `{1}` property value + +| `UnsatisfiedServletRequestParameterException` +| (default) +| `{0}` the list of parameter conditions + +|=== + +By default, the message code for the "title" field is "problemDetail.title." + the fully +qualified exception class name. + + + +[[mvc-ann-rest-exceptions-client]] +=== Client Handling +[.small]#<># + +A client application can catch `WebClientResponseException`, when using the `WebClient`, +or `RestClientResponseException` when using the `RestTemplate`, and use their +`getResponseBodyAs` methods to decode the error response body to any target type such as +`ProblemDetail`, or a subclass of `ProblemDetail`. + [[mvc-web-security]] == Web Security [.small]#<># -The https://projects.spring.io/spring-security/[Spring Security] project provides support +The https://spring.io/projects/spring-security[Spring Security] project provides support for protecting web applications from malicious exploits. See the Spring Security reference documentation, including: -* {doc-spring-security}/html5/#mvc[Spring MVC Security] -* {doc-spring-security}/html5/#test-mockmvc[Spring MVC Test Support] -* {doc-spring-security}/html5/#csrf[CSRF protection] -* {doc-spring-security}/html5/#headers[Security Response Headers] +* {doc-spring-security}/servlet/integrations/mvc.html[Spring MVC Security] +* {doc-spring-security}/servlet/test/mockmvc/setup.html[Spring MVC Test Support] +* {doc-spring-security}/features/exploits/csrf.html#csrf-protection[CSRF protection] +* {doc-spring-security}/features/exploits/headers.html[Security Response Headers] https://hdiv.org/[HDIV] is another web security framework that integrates with Spring MVC. @@ -4781,7 +5145,7 @@ use case-oriented approach that focuses on the common scenarios: val ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS).noTransform().cachePublic() ---- -`WebContentGenerator` also accept a simpler `cachePeriod` property (defined in seconds) that +`WebContentGenerator` also accepts a simpler `cachePeriod` property (defined in seconds) that works as follows: * A `-1` value does not generate a `Cache-Control` response header. @@ -4833,7 +5197,7 @@ settings to a `ResponseEntity`, as the following example shows: } ---- -The preceding example sends an 304 (NOT_MODIFIED) response with an empty body if the comparison +The preceding example sends a 304 (NOT_MODIFIED) response with an empty body if the comparison to the conditional request headers indicates that the content has not changed. Otherwise, the `ETag` and `Cache-Control` headers are added to the response. @@ -4996,6 +5360,7 @@ following example shows: @Configuration @EnableWebMvc class WebConfig : WebMvcConfigurer { + // Implement configuration methods... } ---- @@ -5230,7 +5595,6 @@ the following example shows: public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LocaleChangeInterceptor()); registry.addInterceptor(new ThemeChangeInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**"); - registry.addInterceptor(new SecurityInterceptor()).addPathPatterns("/secure/*"); } } ---- @@ -5244,7 +5608,6 @@ the following example shows: override fun addInterceptors(registry: InterceptorRegistry) { registry.addInterceptor(LocaleChangeInterceptor()) registry.addInterceptor(ThemeChangeInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**") - registry.addInterceptor(SecurityInterceptor()).addPathPatterns("/secure/*") } } ---- @@ -5260,13 +5623,19 @@ The following example shows how to achieve the same configuration in XML: - - - - ---- +NOTE: Mapped interceptors are not ideally suited as a security layer due to the potential +for a mismatch with annotated controller path matching, which can also match trailing +slashes and path extensions transparently, along with other path matching options. Many +of these options have been deprecated but the potential for a mismatch remains. +Generally, we recommend using Spring Security which includes a dedicated +https://docs.spring.io/spring-security/reference/servlet/integrations/mvc.html#mvc-requestmatcher[MvcRequestMatcher] +to align with Spring MVC path matching and also has a security firewall that blocks many +unwanted characters in URL paths. + + [[mvc-config-content-negotiation]] @@ -5276,12 +5645,10 @@ The following example shows how to achieve the same configuration in XML: You can configure how Spring MVC determines the requested media types from the request (for example, `Accept` header, URL path extension, query parameter, and others). -By default, the URL path extension is checked first -- with `json`, `xml`, `rss`, and `atom` -registered as known extensions (depending on classpath dependencies). The `Accept` header -is checked second. +By default, only the `Accept` header is checked. -Consider changing those defaults to `Accept` header only, and, if you must use URL-based -content type resolution, consider using the query parameter strategy over path extensions. See +If you must use URL-based content type resolution, consider using the query parameter +strategy over path extensions. See <> and <> for more details. @@ -5634,8 +6001,8 @@ The following listing shows how to do so with Java configuration: @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/resources/**") - .addResourceLocations("/public", "classpath:/static/") - .setCacheControl(CacheControl.maxAge(Duration.ofDays(365))); + .addResourceLocations("/public", "classpath:/static/") + .setCacheControl(CacheControl.maxAge(Duration.ofDays(365))); } } ---- @@ -5735,12 +6102,21 @@ Note that, when using both `EncodedResourceResolver` (for example, for serving g brotli-encoded resources) and `VersionResourceResolver`, you must register them in this order. That ensures content-based versions are always computed reliably, based on the unencoded file. -https://www.webjars.org/documentation[WebJars] are also supported through the +For https://www.webjars.org/documentation[WebJars], versioned URLs like +`/webjars/jquery/1.2.0/jquery.min.js` are the recommended and most efficient way to use them. +The related resource location is configured out of the box with Spring Boot (or can be configured +manually via `ResourceHandlerRegistry`) and does not require to add the +`org.webjars:webjars-locator-core` dependency. + +Version-less URLs like `/webjars/jquery/jquery.min.js` are supported through the `WebJarsResourceResolver` which is automatically registered when the -`org.webjars:webjars-locator-core` library is present on the classpath. The resolver can -re-write URLs to include the version of the jar and can also match against incoming URLs -without versions -- for example, from `/jquery/jquery.min.js` to -`/jquery/1.2.0/jquery.min.js`. +`org.webjars:webjars-locator-core` library is present on the classpath, at the cost of a +classpath scanning that could slow down application startup. The resolver can re-write URLs to +include the version of the jar and can also match against incoming URLs without versions +-- for example, from `/webjars/jquery/jquery.min.js` to `/webjars/jquery/1.2.0/jquery.min.js`. + +TIP: The Java configuration based on `ResourceHandlerRegistry` provides further options +for fine-grained control, e.g. last-modified behavior and optimized resource resolution. @@ -5858,9 +6234,7 @@ The following example shows how to customize path matching in Java configuration @Override public void configurePathMatch(PathMatchConfigurer configurer) { - configurer - .setPatternParser(new PathPatternParser()) - .addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController.class)); + configurer.addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController.class)); } private PathPatternParser patternParser() { @@ -5876,9 +6250,7 @@ The following example shows how to customize path matching in Java configuration class WebConfig : WebMvcConfigurer { override fun configurePathMatch(configurer: PathMatchConfigurer) { - configurer - .setPatternParser(patternParser) - .addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController::class.java)) + configurer.addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController::class.java)) } fun patternParser(): PathPatternParser { @@ -5887,13 +6259,12 @@ The following example shows how to customize path matching in Java configuration } ---- -The following example shows how to achieve the same configuration in XML: +The following example shows how to customize path matching in XML configuration: [source,xml,indent=0,subs="verbatim,quotes"] ---- @@ -5990,5 +6361,5 @@ For more details, see the https://github.com/spring-projects/spring-framework/wiki/HTTP-2-support[HTTP/2 wiki page]. The Servlet API does expose one construct related to HTTP/2. You can use the -`javax.servlet.http.PushBuilder` to proactively push resources to clients, and it +`jakarta.servlet.http.PushBuilder` to proactively push resources to clients, and it is supported as a <> to `@RequestMapping` methods. diff --git a/src/docs/asciidoc/web/websocket-intro.adoc b/framework-docs/src/docs/asciidoc/web/websocket-intro.adoc similarity index 95% rename from src/docs/asciidoc/web/websocket-intro.adoc rename to framework-docs/src/docs/asciidoc/web/websocket-intro.adoc index 8b9feeab5197..913ac8b0bef5 100644 --- a/src/docs/asciidoc/web/websocket-intro.adoc +++ b/framework-docs/src/docs/asciidoc/web/websocket-intro.adoc @@ -1,4 +1,4 @@ -[[websocket-intro]] +[[{chapter}.websocket-intro]] = Introduction to WebSocket The WebSocket protocol, https://tools.ietf.org/html/rfc6455[RFC 6455], provides a standardized @@ -10,8 +10,7 @@ A WebSocket interaction begins with an HTTP request that uses the HTTP `Upgrade` to upgrade or, in this case, to switch to the WebSocket protocol. The following example shows such an interaction: -[source,yaml,indent=0] -[subs="verbatim,quotes"] +[source,yaml,indent=0,subs="verbatim,quotes"] ---- GET /spring-websocket-portfolio/portfolio HTTP/1.1 Host: localhost:8080 @@ -29,8 +28,7 @@ shows such an interaction: Instead of the usual 200 status code, a server with WebSocket support returns output similar to the following: -[source,yaml,indent=0] -[subs="verbatim,quotes"] +[source,yaml,indent=0,subs="verbatim,quotes"] ---- HTTP/1.1 101 Switching Protocols <1> Upgrade: websocket @@ -56,7 +54,7 @@ instructions of the cloud provider related to WebSocket support. -[[websocket-intro-architecture]] +[[{chapter}.websocket-intro-architecture]] == HTTP Versus WebSocket Even though WebSocket is designed to be HTTP-compatible and starts with an HTTP request, @@ -82,7 +80,7 @@ In the absence of that, they need to come up with their own conventions. -[[websocket-intro-when-to-use]] +[[{chapter}.websocket-intro-when-to-use]] == When to Use WebSockets WebSockets can make a web page be dynamic and interactive. However, in many cases, diff --git a/src/docs/asciidoc/web/websocket.adoc b/framework-docs/src/docs/asciidoc/web/websocket.adoc similarity index 94% rename from src/docs/asciidoc/web/websocket.adoc rename to framework-docs/src/docs/asciidoc/web/websocket.adoc index e517a52107ff..af5f28d34f6e 100644 --- a/src/docs/asciidoc/web/websocket.adoc +++ b/framework-docs/src/docs/asciidoc/web/websocket.adoc @@ -1,6 +1,6 @@ [[websocket]] = WebSockets -:doc-spring-security: {doc-root}/spring-security/site/docs/current/reference +:doc-spring-security: {doc-root}/spring-security/reference [.small]#<># This part of the reference documentation covers support for Servlet stack, WebSocket @@ -29,8 +29,7 @@ Creating a WebSocket server is as simple as implementing `WebSocketHandler` or, likely, extending either `TextWebSocketHandler` or `BinaryWebSocketHandler`. The following example uses `TextWebSocketHandler`: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes"] ---- import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.WebSocketSession; @@ -49,8 +48,7 @@ example uses `TextWebSocketHandler`: There is dedicated WebSocket Java configuration and XML namespace support for mapping the preceding WebSocket handler to a specific URL, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes"] ---- import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.config.annotation.WebSocketConfigurer; @@ -75,8 +73,7 @@ WebSocket handler to a specific URL, as the following example shows: The following example shows the XML configuration equivalent of the preceding example: -[source,xml,indent=0] -[subs="verbatim,quotes,attributes"] +[source,xml,indent=0,subs="verbatim,quotes,attributes"] ---- ` element in `web.xml`, as the following example shows: -[source,xml,indent=0] -[subs="verbatim,quotes,attributes"] +[source,xml,indent=0,subs="verbatim,quotes,attributes"] ---- - + https://jakarta.ee/xml/ns/jakartaee + https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd" + version="5.0"> @@ -237,15 +231,14 @@ You can then selectively enable web fragments by name, such as Spring's own `SpringServletContainerInitializer` that provides support for the Servlet 3 Java initialization API. The following example shows how to do so: -[source,xml,indent=0] -[subs="verbatim,quotes,attributes"] +[source,xml,indent=0,subs="verbatim,quotes,attributes"] ---- - + https://jakarta.ee/xml/ns/jakartaee + https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd" + version="5.0"> spring_web @@ -267,8 +260,7 @@ and others. For Tomcat, WildFly, and GlassFish, you can add a `ServletServerContainerFactoryBean` to your WebSocket Java config, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @EnableWebSocket @@ -287,8 +279,7 @@ WebSocket Java config, as the following example shows: The following example shows the XML configuration equivalent of the preceding example: -[source,xml,indent=0] -[subs="verbatim,quotes,attributes"] +[source,xml,indent=0,subs="verbatim,quotes,attributes"] ---- transports = new ArrayList<>(2); transports.add(new WebSocketTransport(new StandardWebSocketClient())); @@ -806,8 +790,7 @@ To use `SockJsClient` to simulate a large number of concurrent users, you need to configure the underlying HTTP client (for XHR transports) to allow a sufficient number of connections and threads. The following example shows how to do so with Jetty: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes"] ---- HttpClient jettyHttpClient = new HttpClient(); jettyHttpClient.setMaxConnectionsPerDestination(1000); @@ -817,8 +800,7 @@ jettyHttpClient.setExecutor(new QueuedThreadPool(1000)); The following example shows the server-side SockJS-related properties (see javadoc for details) that you should also consider customizing: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration public class WebSocketConfig extends WebSocketMessageBrokerConfigurationSupport { @@ -942,7 +924,7 @@ destination:/topic/price.stock.MMM A server cannot send unsolicited messages. All messages from a server must be in response to a specific client subscription, and the -`subscription-id` header of the server message must match the `id` header of the +`subscription` header of the server message must match the `id` header of the client subscription. The preceding overview is intended to provide the most basic understanding of the @@ -978,8 +960,7 @@ STOMP over WebSocket support is available in the `spring-messaging` and `spring-websocket` modules. Once you have those dependencies, you can expose a STOMP endpoints, over WebSocket with <>, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes"] ---- import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; @@ -1011,8 +992,7 @@ route messages whose destination header begins with `/topic `or `/queue` to the The following example shows the XML configuration equivalent of the preceding example: -[source,xml,indent=0] -[subs="verbatim,quotes,attributes"] +[source,xml,indent=0,subs="verbatim,quotes,attributes"] ---- > applies. For Jetty, however you need to set the `HandshakeHandler` and `WebSocketPolicy` through the `StompEndpointRegistry`: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @EnableWebSocketMessageBroker @@ -1142,7 +1119,7 @@ Contract for sending a message that enables loose coupling between producers and `SubscribableChannel` that uses an `Executor` for delivering messages. Both the Java configuration (that is, `@EnableWebSocketMessageBroker`) and the XML namespace configuration -(that is,``) use the preceding components to assemble a message +(that is, ``) use the preceding components to assemble a message workflow. The following diagram shows the components used when the simple built-in message broker is enabled: @@ -1180,8 +1157,7 @@ to broadcast to subscribed clients. We can trace the flow through a simple example. Consider the following example, which sets up a server: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @EnableWebSocketMessageBroker @@ -1202,7 +1178,7 @@ We can trace the flow through a simple example. Consider the following example, @Controller public class GreetingController { - @MessageMapping("/greeting") { + @MessageMapping("/greeting") public String handle(String greeting) { return "[" + getTimestamp() + ": " + greeting; } @@ -1216,7 +1192,7 @@ is established, STOMP frames begin to flow on it. . The client sends a SUBSCRIBE frame with a destination header of `/topic/greeting`. Once received and decoded, the message is sent to the `clientInboundChannel` and is then routed to the message broker, which stores the client subscription. -. The client sends a aSEND frame to `/app/greeting`. The `/app` prefix helps to route it to +. The client sends a SEND frame to `/app/greeting`. The `/app` prefix helps to route it to annotated controllers. After the `/app` prefix is stripped, the remaining `/greeting` part of the destination is mapped to the `@MessageMapping` method in `GreetingController`. . The value returned from `GreetingController` is turned into a Spring `Message` with @@ -1283,7 +1259,7 @@ The following table describes the method arguments: The presence of this annotation is not required since it is, by default, assumed if no other argument is matched. -You can annotate payload arguments with `@javax.validation.Valid` or Spring's `@Validated`, +You can annotate payload arguments with `@jakarta.validation.Valid` or Spring's `@Validated`, to have the payload arguments be automatically validated. | `@Header` @@ -1358,8 +1334,7 @@ when a subscription is stored and ready for broadcasts, a client should ask for receipt if the server supports it (simple broker does not). For example, with the Java <>, you could do the following to add a receipt: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Autowired private TaskScheduler messageBrokerTaskScheduler; @@ -1372,7 +1347,7 @@ receipt if the server supports it (simple broker does not). For example, with th headers.setDestination("/topic/..."); headers.setReceipt("r1"); FrameHandler handler = ...; - stompSession.subscribe(headers, handler).addReceiptTask(() -> { + stompSession.subscribe(headers, handler).addReceiptTask(receiptHeaders -> { // Subscription ready... }); ---- @@ -1390,8 +1365,7 @@ An application can use `@MessageExceptionHandler` methods to handle exceptions f itself or through a method argument if you want to get access to the exception instance. The following example declares an exception through a method argument: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Controller public class MyController { @@ -1427,8 +1401,7 @@ The easiest way to do so is to inject a `SimpMessagingTemplate` and use it to send messages. Typically, you would inject it by type, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Controller public class GreetingController { @@ -1467,11 +1440,13 @@ See <>. If configured with a task scheduler, the simple broker supports https://stomp.github.io/stomp-specification-1.2.html#Heart-beating[STOMP heartbeats]. -For that, you can declare your own scheduler or use the one that is automatically -declared and used internally. The following example shows how to declare your own scheduler: +To configure a scheduler, you can declare your own `TaskScheduler` bean and set it through +the `MessageBrokerRegistry`. Alternatively, you can use the one that is automatically +declared in the built-in WebSocket configuration, however, you'll' need `@Lazy` to avoid +a cycle between the built-in WebSocket configuration and your +`WebSocketMessageBrokerConfigurer`. For example: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @EnableWebSocketMessageBroker @@ -1480,13 +1455,12 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { private TaskScheduler messageBrokerTaskScheduler; @Autowired - public void setMessageBrokerTaskScheduler(TaskScheduler taskScheduler) { + public void setMessageBrokerTaskScheduler(@Lazy TaskScheduler taskScheduler) { this.messageBrokerTaskScheduler = taskScheduler; } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { - registry.enableSimpleBroker("/queue/", "/topic/") .setHeartbeatValue(new long[] {10000, 20000}) .setTaskScheduler(this.messageBrokerTaskScheduler); @@ -1515,8 +1489,7 @@ and run it with STOMP support enabled. Then you can enable the STOMP broker rela The following example configuration enables a full-featured broker: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @EnableWebSocketMessageBroker @@ -1538,8 +1511,7 @@ The following example configuration enables a full-featured broker: The following example shows the XML configuration equivalent of the preceding example: -[source,xml,indent=0] -[subs="verbatim,quotes,attributes"] +[source,xml,indent=0,subs="verbatim,quotes,attributes"] ---- >, or otherwise the +broker would handle "/user" prefixed messages that should only be handled by +`UserDestinationMessageHandler`. On the sending side, messages can be sent to a destination such as pass:q[`/user/{username}/queue/position-updates`], which in turn is translated @@ -1873,8 +1848,7 @@ A message-handling method can send messages to the user associated with the message being handled through the `@SendToUser` annotation (also supported on the class-level to share a common destination), as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Controller public class PortfolioController { @@ -1893,8 +1867,7 @@ to the given destination are targeted. However, sometimes, it may be necessary t target only the session that sent the message being handled. You can do so by setting the `broadcast` attribute to false, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Controller public class MyController { @@ -1924,8 +1897,7 @@ component by, for example, injecting the `SimpMessagingTemplate` created by the the XML namespace. (The bean name is `brokerMessagingTemplate` if required for qualification with `@Qualifier`.) The following example shows how to do so: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Service public class TradeServiceImpl implements TradeService { @@ -1974,8 +1946,7 @@ not match the exact order of publication. If this is an issue, enable the `setPreservePublishOrder` flag, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @EnableWebSocketMessageBroker @@ -1992,8 +1963,7 @@ If this is an issue, enable the `setPreservePublishOrder` flag, as the following The following example shows the XML configuration equivalent of the preceding example: -[source,xml,indent=0] -[subs="verbatim,quotes,attributes"] +[source,xml,indent=0,subs="verbatim,quotes,attributes"] ---- > provide notifications for the lifecycle +<> provide notifications for the lifecycle of a STOMP connection but not for every client message. Applications can also register a `ChannelInterceptor` to intercept any message and in any part of the processing chain. The following example shows how to intercept inbound messages from clients: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @EnableWebSocketMessageBroker @@ -2082,8 +2051,7 @@ The following example shows how to intercept inbound messages from clients: A custom `ChannelInterceptor` can use `StompHeaderAccessor` or `SimpMessageHeaderAccessor` to access information about the message, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class MyChannelInterceptor implements ChannelInterceptor { @@ -2118,8 +2086,7 @@ Spring provides a STOMP over WebSocket client and a STOMP over TCP client. To begin, you can create and configure `WebSocketStompClient`, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes"] ---- WebSocketClient webSocketClient = new StandardWebSocketClient(); WebSocketStompClient stompClient = new WebSocketStompClient(webSocketClient); @@ -2135,8 +2102,7 @@ use WebSocket or HTTP-based transport as a fallback. For more details, see Next, you can establish a connection and provide a handler for the STOMP session, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes"] ---- String url = "ws://127.0.0.1:8080/endpoint"; StompSessionHandler sessionHandler = new MyStompSessionHandler(); @@ -2145,8 +2111,7 @@ as the following example shows: When the session is ready for use, the handler is notified, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class MyStompSessionHandler extends StompSessionHandlerAdapter { @@ -2160,8 +2125,7 @@ public class MyStompSessionHandler extends StompSessionHandlerAdapter { Once the session is established, any payload can be sent and is serialized with the configured `MessageConverter`, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes"] ---- session.send("/topic/something", "payload"); ---- @@ -2171,8 +2135,7 @@ for messages on the subscription and returns a `Subscription` handle that you ca use to unsubscribe. For each received message, the handler can specify the target `Object` type to which the payload should be deserialized, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes"] ---- session.subscribe("/topic/something", new StompFrameHandler() { @@ -2231,8 +2194,7 @@ transport-level errors including `ConnectionLostException`. Each WebSocket session has a map of attributes. The map is attached as a header to inbound client messages and may be accessed from a controller method, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Controller public class MyController { @@ -2251,8 +2213,7 @@ registered on the `clientInboundChannel`. Those are typically singletons and liv longer than any individual WebSocket session. Therefore, you need to use a scope proxy mode for WebSocket-scoped beans, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Component @Scope(scopeName = "websocket", proxyMode = ScopedProxyMode.TARGET_CLASS) @@ -2360,8 +2321,7 @@ documentation of the XML schema for important additional details. The following example shows a possible configuration: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @EnableWebSocketMessageBroker @@ -2379,8 +2339,7 @@ The following example shows a possible configuration: The following example shows the XML configuration equivalent of the preceding example: -[source,xml,indent=0] -[subs="verbatim,quotes,attributes"] +[source,xml,indent=0,subs="verbatim,quotes,attributes"] ---- `, key -infrastructure components automatically gather statisticss and counters that provide +infrastructure components automatically gather statistics and counters that provide important insight into the internal state of the application. The configuration also declares a bean of type `WebSocketMessageBrokerStats` that gathers all available information in one place and by default logs it at the `INFO` level once diff --git a/src/docs/dist/license.txt b/framework-docs/src/docs/dist/license.txt similarity index 96% rename from src/docs/dist/license.txt rename to framework-docs/src/docs/dist/license.txt index 97bee37bea99..89cf3d232fa7 100644 --- a/src/docs/dist/license.txt +++ b/framework-docs/src/docs/dist/license.txt @@ -212,7 +212,7 @@ code for these subcomponents is subject to the terms and conditions of the following licenses. ->>> ASM 7.1 (org.ow2.asm:asm:7.1, org.ow2.asm:asm-commons:7.1): +>>> ASM 9.1 (org.ow2.asm:asm:9.1, org.ow2.asm:asm-commons:9.1): Copyright (c) 2000-2011 INRIA, France Telecom All rights reserved. @@ -255,10 +255,18 @@ CGLIB 3.3 is licensed under the Apache License, version 2.0, the text of which is included above. ->>> Objenesis 3.1 (org.objenesis:objenesis:3.1): +>>> JavaPoet 1.13.0 (com.squareup:javapoet:1.13.0): + +Per the LICENSE file in the JavaPoet JAR distribution downloaded from +https://github.com/square/javapoet/archive/refs/tags/javapoet-1.13.0.zip, +JavaPoet 1.13.0 is licensed under the Apache License, version 2.0, the text of +which is included above. + + +>>> Objenesis 3.2 (org.objenesis:objenesis:3.2): Per the LICENSE file in the Objenesis ZIP distribution downloaded from -http://objenesis.org/download.html, Objenesis 3.1 is licensed under the +http://objenesis.org/download.html, Objenesis 3.2 is licensed under the Apache License, version 2.0, the text of which is included above. Per the NOTICE file in the Objenesis ZIP distribution downloaded from diff --git a/src/docs/dist/notice.txt b/framework-docs/src/docs/dist/notice.txt similarity index 100% rename from src/docs/dist/notice.txt rename to framework-docs/src/docs/dist/notice.txt diff --git a/src/docs/dist/readme.txt b/framework-docs/src/docs/dist/readme.txt similarity index 100% rename from src/docs/dist/readme.txt rename to framework-docs/src/docs/dist/readme.txt diff --git a/src/docs/spring-framework.png b/framework-docs/src/docs/spring-framework.png similarity index 100% rename from src/docs/spring-framework.png rename to framework-docs/src/docs/spring-framework.png diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle new file mode 100644 index 000000000000..f54aa462b7f4 --- /dev/null +++ b/framework-platform/framework-platform.gradle @@ -0,0 +1,148 @@ +plugins { + id 'java-platform' +} + +javaPlatform { + allowDependencies() +} + +dependencies { + api(platform("com.fasterxml.jackson:jackson-bom:2.14.0")) + api(platform("io.micrometer:micrometer-bom:1.10.0")) + api(platform("io.netty:netty-bom:4.1.85.Final")) + api(platform("io.netty:netty5-bom:5.0.0.Alpha5")) + api(platform("io.projectreactor:reactor-bom:2022.0.0")) + api(platform("io.rsocket:rsocket-bom:1.1.3")) + api(platform("org.apache.groovy:groovy-bom:4.0.5")) + api(platform("org.apache.logging.log4j:log4j-bom:2.19.0")) + api(platform("org.eclipse.jetty:jetty-bom:11.0.12")) + api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.4")) + api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.4.0")) + api(platform("org.junit:junit-bom:5.9.1")) + api(platform("org.mockito:mockito-bom:4.8.1")) + + constraints { + api("com.fasterxml.woodstox:woodstox-core:6.4.0") + api("com.fasterxml:aalto-xml:1.3.1") + api("com.github.ben-manes.caffeine:caffeine:3.1.1") + api("com.github.librepdf:openpdf:1.3.30") + api("com.google.code.findbugs:findbugs:3.0.1") + api("com.google.code.findbugs:jsr305:3.0.2") + api("com.google.code.gson:gson:2.9.1") + api("com.google.protobuf:protobuf-java-util:3.21.5") + api("com.googlecode.protobuf-java-format:protobuf-java-format:1.4") + api("com.h2database:h2:2.1.214") + api("com.jayway.jsonpath:json-path:2.7.0") + api("com.rometools:rome:1.18.0") + api("com.squareup.okhttp3:mockwebserver:3.14.9") + api("com.squareup.okhttp3:okhttp:3.14.9") + api("com.sun.activation:jakarta.activation:2.0.1") + api("com.sun.mail:jakarta.mail:2.0.1") + api("com.sun.xml.bind:jaxb-core:3.0.2") + api("com.sun.xml.bind:jaxb-impl:3.0.2") + api("com.sun.xml.bind:jaxb-xjc:3.0.2") + api("com.thoughtworks.qdox:qdox:2.0.2") + api("com.thoughtworks.xstream:xstream:1.4.19") + api("commons-io:commons-io:2.11.0") + api("de.bechte.junit:junit-hierarchicalcontextrunner:4.12.1") + api("info.picocli:picocli:4.6.3") + api("io.micrometer:context-propagation:1.0.0") + api("io.mockk:mockk:1.12.1") + api("io.projectreactor.netty:reactor-netty5-http:2.0.0-M3") + api("io.projectreactor.tools:blockhound:1.0.6.RELEASE") + api("io.r2dbc:r2dbc-h2:1.0.0.RELEASE") + api("io.r2dbc:r2dbc-spi-test:1.0.0.RELEASE") + api("io.r2dbc:r2dbc-spi:1.0.0.RELEASE") + api("io.reactivex.rxjava3:rxjava:3.1.5") + api("io.smallrye.reactive:mutiny:1.8.0") + api("io.undertow:undertow-core:2.3.0.Final") + api("io.undertow:undertow-servlet:2.3.0.Final") + api("io.undertow:undertow-websockets-jsr:2.3.0.Final") + api("io.vavr:vavr:0.10.4") + api("jakarta.activation:jakarta.activation-api:2.0.1") + api("jakarta.annotation:jakarta.annotation-api:2.0.0") + api("jakarta.ejb:jakarta.ejb-api:4.0.0") + api("jakarta.el:jakarta.el-api:4.0.0") + api("jakarta.enterprise.concurrent:jakarta.enterprise.concurrent-api:2.0.0") + api("jakarta.faces:jakarta.faces-api:3.0.0") + api("jakarta.inject:jakarta.inject-api:2.0.0") + api("jakarta.inject:jakarta.inject-tck:2.0.1") + api("jakarta.interceptor:jakarta.interceptor-api:2.0.0") + api("jakarta.jms:jakarta.jms-api:3.0.0") + api("jakarta.json.bind:jakarta.json.bind-api:2.0.0") + api("jakarta.json:jakarta.json-api:2.0.1") + api("jakarta.mail:jakarta.mail-api:2.0.1") + api("jakarta.persistence:jakarta.persistence-api:3.0.0") + api("jakarta.resource:jakarta.resource-api:2.0.0") + api("jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api:3.0.0") + api("jakarta.servlet.jsp:jakarta.servlet.jsp-api:3.1.0") + api("jakarta.servlet:jakarta.servlet-api:6.0.0") + api("jakarta.transaction:jakarta.transaction-api:2.0.1") + api("jakarta.validation:jakarta.validation-api:3.0.2") + api("jakarta.websocket:jakarta.websocket-api:2.1.0") + api("jakarta.websocket:jakarta.websocket-client-api:2.1.0") + api("jakarta.xml.bind:jakarta.xml.bind-api:3.0.1") + api("javax.cache:cache-api:1.1.1") + api("javax.money:money-api:1.1") + api("jaxen:jaxen:1.2.0") + api("junit:junit:4.13.2") + api("net.sf.jopt-simple:jopt-simple:5.0.4") + api("net.sourceforge.htmlunit:htmlunit:2.66.0") + api("org.apache-extras.beanshell:bsh:2.0b6") + api("org.apache.activemq:activemq-broker:5.16.2") + api("org.apache.activemq:activemq-kahadb-store:5.16.2") + api("org.apache.activemq:activemq-stomp:5.16.2") + api("org.apache.commons:commons-pool2:2.9.0") + api("org.apache.derby:derby:10.16.1.1") + api("org.apache.derby:derbyclient:10.16.1.1") + api("org.apache.derby:derbytools:10.16.1.1") + api("org.apache.httpcomponents.client5:httpclient5:5.1.3") + api("org.apache.httpcomponents.core5:httpcore5-reactive:5.1.3") + api("org.apache.poi:poi-ooxml:5.2.2") + api("org.apache.tomcat.embed:tomcat-embed-core:10.1.1") + api("org.apache.tomcat.embed:tomcat-embed-websocket:10.1.1") + api("org.apache.tomcat:tomcat-util:10.1.1") + api("org.apache.tomcat:tomcat-websocket:10.1.1") + api("org.aspectj:aspectjrt:1.9.9.1") + api("org.aspectj:aspectjtools:1.9.9.1") + api("org.aspectj:aspectjweaver:1.9.9.1") + api("org.assertj:assertj-core:3.23.1") + api("org.awaitility:awaitility:3.1.6") + api("org.bouncycastle:bcpkix-jdk18on:1.71") + api("org.codehaus.jettison:jettison:1.3.8") + api("org.dom4j:dom4j:2.1.3") + api("org.eclipse.jetty:jetty-reactive-httpclient:3.0.7") + api("org.eclipse.persistence:org.eclipse.persistence.jpa:3.0.3") + api("org.eclipse:yasson:2.0.4") + api("org.ehcache:ehcache:3.4.0") + api("org.ehcache:jcache:1.0.1") + api("org.freemarker:freemarker:2.3.31") + // Substitute for "javax.management:jmxremote_optional:1.0.1_04" which + // is not available on Maven Central + api("org.glassfish.external:opendmk_jmxremote_optional_jar:1.0-b01-ea") + api("org.glassfish.tyrus:tyrus-container-servlet:2.0.1") + api("org.glassfish:jakarta.el:4.0.2") + api("org.graalvm.sdk:graal-sdk:22.3.0") + api("org.hamcrest:hamcrest:2.2") + api("org.hibernate:hibernate-core-jakarta:5.6.12.Final") + api("org.hibernate:hibernate-validator:7.0.5.Final") + api("org.hsqldb:hsqldb:2.7.0") + api("org.javamoney:moneta:1.4.2") + api("org.jruby:jruby:9.3.8.0") + api("org.junit.support:testng-engine:1.0.4") + api("org.mozilla:rhino:1.7.11") + api("org.ogce:xpp3:1.1.6") + api("org.python:jython-standalone:2.7.1") + api("org.quartz-scheduler:quartz:2.3.2") + api("org.seleniumhq.selenium:htmlunit-driver:2.66.0") + api("org.seleniumhq.selenium:selenium-java:3.141.59") + api("org.skyscreamer:jsonassert:1.5.0") + api("org.slf4j:slf4j-api:2.0.3") + api("org.testng:testng:7.6.1") + api("org.webjars:underscorejs:1.8.3") + api("org.webjars:webjars-locator-core:0.48") + api("org.xmlunit:xmlunit-assertj:2.9.0") + api("org.xmlunit:xmlunit-matchers:2.9.0") + api("org.yaml:snakeyaml:1.30") + } +} diff --git a/gradle.properties b/gradle.properties index 61d8962566ab..1690d0129637 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,9 @@ -version=5.3.1-SNAPSHOT -org.gradle.jvmargs=-Xmx1536M +version=6.0.0 + org.gradle.caching=true +org.gradle.jvmargs=-Xmx2048m org.gradle.parallel=true -kotlin.stdlib.default.dependency=false \ No newline at end of file + +kotlinVersion=1.7.20 + +kotlin.stdlib.default.dependency=false diff --git a/gradle/custom-java-home.gradle b/gradle/custom-java-home.gradle deleted file mode 100644 index 54d1de1eb8f9..000000000000 --- a/gradle/custom-java-home.gradle +++ /dev/null @@ -1,80 +0,0 @@ -// ----------------------------------------------------------------------------- -// -// This script adds support for the following two JVM system properties -// that control the build for alternative JDKs (i.e., a JDK other than -// the one used to launch the Gradle process). -// -// - customJavaHome: absolute path to the alternate JDK installation to -// use to compile Java code and execute tests. This system property -// is also used in spring-oxm.gradle to determine whether JiBX is -// supported. -// -// - customJavaSourceVersion: Java version supplied to the `--release` -// command line flag to control the Java source and target -// compatibility version. Supported versions include 9 or higher. -// Do not set this system property if Java 8 should be used. -// -// Examples: -// -// ./gradlew -DcustomJavaHome=/Library/Java/JavaVirtualMachines/jdk-14.jdk/Contents/Home test -// -// ./gradlew --no-build-cache -DcustomJavaHome=/Library/Java/JavaVirtualMachines/jdk-14.jdk/Contents/Home test -// -// ./gradlew -DcustomJavaHome=/Library/Java/JavaVirtualMachines/jdk-14.jdk/Contents/Home -DcustomJavaSourceVersion=14 test -// -// -// Credits: inspired by work from Marc Philipp and Stephane Nicoll -// -// ----------------------------------------------------------------------------- - -import org.gradle.internal.os.OperatingSystem -// import org.jetbrains.kotlin.gradle.dsl.KotlinJvmCompile - -def customJavaHome = System.getProperty("customJavaHome") - -if (customJavaHome) { - def customJavaHomeDir = new File(customJavaHome) - def customJavaSourceVersion = System.getProperty("customJavaSourceVersion") - - tasks.withType(JavaCompile) { - logger.info("Java home for " + it.name + " task in " + project.name + ": " + customJavaHomeDir) - options.forkOptions.javaHome = customJavaHomeDir - inputs.property("customJavaHome", customJavaHome) - if (customJavaSourceVersion) { - options.compilerArgs += [ "--release", customJavaSourceVersion] - inputs.property("customJavaSourceVersion", customJavaSourceVersion) - } - } - - tasks.withType(GroovyCompile) { - logger.info("Java home for " + it.name + " task in " + project.name + ": " + customJavaHomeDir) - options.forkOptions.javaHome = customJavaHomeDir - inputs.property("customJavaHome", customJavaHome) - if (customJavaSourceVersion) { - options.compilerArgs += [ "--release", customJavaSourceVersion] - inputs.property("customJavaSourceVersion", customJavaSourceVersion) - } - } - - /* - tasks.withType(KotlinJvmCompile) { - logger.info("Java home for " + it.name + " task in " + project.name + ": " + customJavaHome) - kotlinOptions.jdkHome = customJavaHomeDir - inputs.property("customJavaHome", customJavaHome) - } - */ - - tasks.withType(Test) { - def javaExecutable = customJavaHome + "/bin/java" - if (OperatingSystem.current().isWindows()) { - javaExecutable += ".exe" - } - logger.info("Java executable for " + it.name + " task in " + project.name + ": " + javaExecutable) - executable = javaExecutable - inputs.property("customJavaHome", customJavaHome) - if (customJavaSourceVersion) { - inputs.property("customJavaSourceVersion", customJavaSourceVersion) - } - } - -} diff --git a/gradle/docs-dokka.gradle b/gradle/docs-dokka.gradle new file mode 100644 index 000000000000..147c39497f2a --- /dev/null +++ b/gradle/docs-dokka.gradle @@ -0,0 +1,30 @@ +tasks.findByName("dokkaHtmlPartial")?.configure { + outputDirectory.set(new File(buildDir, "docs/kdoc")) + dokkaSourceSets { + configureEach { + sourceRoots.setFrom(file("src/main/kotlin")) + classpath.from(sourceSets["main"].runtimeClasspath) + externalDocumentationLink { + url.set(new URL("https://docs.spring.io/spring-framework/docs/current/javadoc-api/")) + } + externalDocumentationLink { + url.set(new URL("https://projectreactor.io/docs/core/release/api/")) + } + externalDocumentationLink { + url.set(new URL("https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/")) + } + externalDocumentationLink { + url.set(new URL("https://kotlin.github.io/kotlinx.coroutines/")) + } + externalDocumentationLink { + url.set(new URL("https://javadoc.io/doc/org.hamcrest/hamcrest/2.1/")) + } + externalDocumentationLink { + url.set(new URL("https://javadoc.io/doc/jakarta.servlet/jakarta.servlet-api/latest/")) + } + externalDocumentationLink { + url.set(new URL("https://javadoc.io/static/io.rsocket/rsocket-core/1.1.1/")) + } + } + } +} diff --git a/gradle/docs.gradle b/gradle/docs.gradle deleted file mode 100644 index 76ca834b4522..000000000000 --- a/gradle/docs.gradle +++ /dev/null @@ -1,284 +0,0 @@ -configurations { - asciidoctorExt -} - -dependencies { - asciidoctorExt("io.spring.asciidoctor:spring-asciidoctor-extensions-block-switch:0.4.3.RELEASE") -} - -repositories { - maven { - url "https://repo.spring.io/release" - mavenContent { - includeGroup "io.spring.asciidoctor" - } - } -} - -/** - * Produce Javadoc for all Spring Framework modules in "build/docs/javadoc" - */ -task api(type: Javadoc) { - group = "Documentation" - description = "Generates aggregated Javadoc API documentation." - title = "${rootProject.description} ${version} API" - - dependsOn { - moduleProjects.collect { - it.tasks.getByName("jar") - } - } - doFirst { - classpath = files( - // ensure the javadoc process can resolve types compiled from .aj sources - project(":spring-aspects").sourceSets.main.output - ) - classpath += files(moduleProjects.collect { it.sourceSets.main.compileClasspath }) - } - - options { - encoding = "UTF-8" - memberLevel = JavadocMemberLevel.PROTECTED - author = true - header = rootProject.description - use = true - overview = "src/docs/api/overview.html" - stylesheetFile = file("src/docs/api/stylesheet.css") - splitIndex = true - links(project.ext.javadocLinks) - addStringOption('Xdoclint:none', '-quiet') - if(JavaVersion.current().isJava9Compatible()) { - addBooleanOption('html5', true) - } - } - source moduleProjects.collect { project -> - project.sourceSets.main.allJava - } - maxMemory = "1024m" - destinationDir = file("$buildDir/docs/javadoc") -} - -/** - * Produce KDoc for all Spring Framework modules in "build/docs/kdoc" - */ -dokka { - dependsOn { - tasks.getByName("api") - } - - doFirst { - configuration { - classpath = moduleProjects.collect { project -> project.jar.outputs.files.getFiles() }.flatten() - classpath += files(moduleProjects.collect { it.sourceSets.main.compileClasspath }) - - moduleProjects.findAll { - it.pluginManager.hasPlugin("kotlin") - }.each { project -> - def kotlinDirs = project.sourceSets.main.kotlin.srcDirs.collect() - kotlinDirs -= project.sourceSets.main.java.srcDirs - kotlinDirs.each { dir -> - if (dir.exists()) { - sourceRoot { - path = dir.path - } - } - } - } - } - } - - outputFormat = "html" - outputDirectory = "$buildDir/docs/kdoc" - - configuration { - moduleName = "spring-framework" - - externalDocumentationLink { - url = new URL("https://docs.spring.io/spring-framework/docs/$version/javadoc-api/") - packageListUrl = new File(buildDir, "docs/javadoc/package-list").toURI().toURL() - } - externalDocumentationLink { - url = new URL("https://projectreactor.io/docs/core/release/api/") - } - externalDocumentationLink { - url = new URL("https://www.reactive-streams.org/reactive-streams-1.0.1-javadoc/") - } - externalDocumentationLink { - url = new URL("https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/") - } - externalDocumentationLink { - url = new URL("https://r2dbc.io/spec/0.8.3.RELEASE/api/") - } - } -} - -task downloadResources(type: Download) { - def version = "0.2.2.RELEASE" - src "https://repo.spring.io/release/io/spring/docresources/" + - "spring-doc-resources/$version/spring-doc-resources-${version}.zip" - dest project.file("$buildDir/docs/spring-doc-resources.zip") - onlyIfModified true - useETag "all" -} - -task extractDocResources(type: Copy, dependsOn: downloadResources) { - from project.zipTree(downloadResources.dest); - into "$buildDir/docs/spring-docs-resources/" -} - -asciidoctorj { - version = '2.4.1' - fatalWarnings ".*" - options doctype: 'book', eruby: 'erubis' - attributes([ - icons: 'font', - idprefix: '', - idseparator: '-', - docinfo: 'shared', - revnumber: project.version, - sectanchors: '', - sectnums: '', - 'source-highlighter': 'highlight.js', - highlightjsdir: 'js/highlight', - 'highlightjs-theme': 'googlecode', - stylesdir: 'css/', - stylesheet: 'stylesheet.css', - 'spring-version': project.version - ]) -} - -/** - * Generate the Spring Framework Reference documentation from "src/docs/asciidoc" - * in "build/docs/ref-docs/html5". - */ -asciidoctor { - baseDirFollowsSourceDir() - configurations 'asciidoctorExt' - sources { - include '*.adoc' - } - outputDir "$buildDir/docs/ref-docs/html5" - logDocuments = true - resources { - from(sourceDir) { - include 'images/*.png', 'css/**', 'js/**' - } - from extractDocResources - } -} - -/** - * Generate the Spring Framework Reference documentation from "src/docs/asciidoc" - * in "build/docs/ref-docs/pdf". - */ -asciidoctorPdf { - baseDirFollowsSourceDir() - configurations 'asciidoctorExt' - sources { - include '*.adoc' - } - outputDir "$buildDir/docs/ref-docs/pdf" - logDocuments = true -} - -/** - * Zip all docs (API and reference) into a single archive - */ -task docsZip(type: Zip, dependsOn: ['api', 'asciidoctor', 'asciidoctorPdf', 'dokka']) { - group = "Distribution" - description = "Builds -${archiveClassifier} archive containing api and reference " + - "for deployment at https://docs.spring.io/spring-framework/docs." - - archiveBaseName.set("spring-framework") - archiveClassifier.set("docs") - from("src/dist") { - include "changelog.txt" - } - from (api) { - into "javadoc-api" - } - from ("$asciidoctor.outputDir") { - into "reference/html" - } - from ("$asciidoctorPdf.outputDir") { - into "reference/pdf" - } - from (dokka) { - into "kdoc-api" - } -} - -/** - * Zip all Spring Framework schemas into a single archive - */ -task schemaZip(type: Zip) { - group = "Distribution" - archiveBaseName.set("spring-framework") - archiveClassifier.set("schema") - description = "Builds -${archiveClassifier} archive containing all " + - "XSDs for deployment at https://springframework.org/schema." - duplicatesStrategy DuplicatesStrategy.EXCLUDE - moduleProjects.each { module -> - def Properties schemas = new Properties(); - - module.sourceSets.main.resources.find { - (it.path.endsWith("META-INF/spring.schemas") || it.path.endsWith("META-INF\\spring.schemas")) - }?.withInputStream { schemas.load(it) } - - for (def key : schemas.keySet()) { - def shortName = key.replaceAll(/http.*schema.(.*).spring-.*/, '$1') - assert shortName != key - File xsdFile = module.sourceSets.main.resources.find { - (it.path.endsWith(schemas.get(key)) || it.path.endsWith(schemas.get(key).replaceAll('\\/','\\\\'))) - } - assert xsdFile != null - into (shortName) { - from xsdFile.path - } - } - } -} - -/** - * Create a distribution zip with everything: - * docs, schemas, jars, source jars, javadoc jars - */ -task distZip(type: Zip, dependsOn: [docsZip, schemaZip]) { - group = "Distribution" - archiveBaseName.set("spring-framework") - archiveClassifier.set("dist") - description = "Builds -${archiveClassifier} archive, containing all jars and docs, " + - "suitable for community download page." - - ext.baseDir = "spring-framework-${project.version}"; - - from("src/docs/dist") { - include "readme.txt" - include "license.txt" - include "notice.txt" - into "${baseDir}" - expand(copyright: new Date().format("yyyy"), version: project.version) - } - - from(zipTree(docsZip.archivePath)) { - into "${baseDir}/docs" - } - - from(zipTree(schemaZip.archivePath)) { - into "${baseDir}/schema" - } - - moduleProjects.each { module -> - into ("${baseDir}/libs") { - from module.jar - if (module.tasks.findByPath("sourcesJar")) { - from module.sourcesJar - } - if (module.tasks.findByPath("javadocJar")) { - from module.javadocJar - } - } - } -} - -distZip.mustRunAfter moduleProjects.check diff --git a/gradle/ide.gradle b/gradle/ide.gradle index 554d4b3c5432..09235f2c61e5 100644 --- a/gradle/ide.gradle +++ b/gradle/ide.gradle @@ -1,11 +1,11 @@ import org.gradle.plugins.ide.eclipse.model.ProjectDependency import org.gradle.plugins.ide.eclipse.model.SourceFolder -apply plugin: "eclipse" +apply plugin: 'eclipse' eclipse.jdt { - sourceCompatibility = 1.8 - targetCompatibility = 1.8 + sourceCompatibility = 17 + targetCompatibility = 17 } // Replace classpath entries with project dependencies (GRADLE-1116) @@ -18,103 +18,73 @@ eclipse.classpath.file.whenMerged { classpath -> if (matcher) { def projectName = matcher[0][1] def path = "/${projectName}" - if(!classpath.entries.find { e -> e instanceof ProjectDependency && e.path == path }) { - def dependency = new ProjectDependency(path) - dependency.exported = true - classpath.entries.add(dependency) + if (!classpath.entries.find { e -> e instanceof ProjectDependency && e.path == path }) { + def recursiveDependency = entry.path.matches('.+/' + projectName + '/build/([^/]+/)+(?:main|test)') + // Avoid recursive dependency on current project. + if (!recursiveDependency) { + classpath.entries.add(new ProjectDependency(path)) + } } classpath.entries.remove(entry) } } + + // Remove any remaining direct depencencies on JARs in the build/libs folder + // except the repack JARs. classpath.entries.removeAll { entry -> (entry.path =~ /(?!.*?repack.*\.jar).*?\/([^\/]+)\/build\/libs\/[^\/]+\.jar/) } } - // Use separate main/test outputs (prevents WTP from packaging test classes) -eclipse.classpath.defaultOutputDir = file(project.name+"/bin/eclipse") +eclipse.classpath.defaultOutputDir = file(project.name + '/bin/eclipse') eclipse.classpath.file.beforeMerged { classpath -> classpath.entries.findAll{ it instanceof SourceFolder }.each { - if(it.output.startsWith("bin/")) { + if (it.output.startsWith('bin/')) { it.output = null } } } -eclipse.classpath.file.whenMerged { classpath -> - classpath.entries.findAll{ it instanceof SourceFolder }.each { - it.output = "bin/" + it.path.split("/")[1] +eclipse.classpath.file.whenMerged { + entries.findAll{ it instanceof SourceFolder }.each { + it.output = 'bin/' + it.path.split('/')[1] } } // Ensure project dependencies come after 3rd-party libs (SPR-11836) // https://jira.spring.io/browse/SPR-11836 -eclipse.classpath.file.whenMerged { classpath -> - classpath.entries.findAll { it instanceof ProjectDependency }.each { +eclipse.classpath.file.whenMerged { + entries.findAll { it instanceof ProjectDependency }.each { // delete from original position - classpath.entries.remove(it) + entries.remove(it) // append to end of classpath - classpath.entries.add(it) + entries.add(it) } } -// Allow projects to be used as WTP modules -eclipse.project.natures "org.eclipse.wst.common.project.facet.core.nature" +// Ensure that JMH sources and resources are treated as test classpath entries +// so that they can see test fixtures. +// https://github.com/melix/jmh-gradle-plugin/issues/157 +eclipse.classpath.file.whenMerged { + entries.findAll { it.path =~ /src\/jmh\/(java|kotlin|resources)/ }.each { + it.entryAttributes['test'] = 'true' + } +} // Include project specific settings task eclipseSettings(type: Copy) { from rootProject.files( - "src/eclipse/org.eclipse.jdt.ui.prefs", - "src/eclipse/org.eclipse.wst.common.project.facet.core.xml") + 'src/eclipse/org.eclipse.core.resources.prefs', + 'src/eclipse/org.eclipse.jdt.core.prefs', + 'src/eclipse/org.eclipse.jdt.ui.prefs') into project.file('.settings/') outputs.upToDateWhen { false } } -task eclipseWstComponent(type: Copy) { - from rootProject.files( - "src/eclipse/org.eclipse.wst.common.component") - into project.file('.settings/') - expand(deployname: project.name) - outputs.upToDateWhen { false } -} - -task eclipseJdtPrepare(type: Copy) { - from rootProject.file("src/eclipse/org.eclipse.jdt.core.prefs") - into project.file(".settings/") - outputs.upToDateWhen { false } +task cleanEclipseSettings(type: Delete) { + delete project.file('.settings/org.eclipse.core.resources.prefs') + delete project.file('.settings/org.eclipse.jdt.core.prefs') + delete project.file('.settings/org.eclipse.jdt.ui.prefs') } -task cleanEclipseJdtUi(type: Delete) { - delete project.file(".settings/org.eclipse.jdt.core.prefs") - delete project.file(".settings/org.eclipse.jdt.ui.prefs") - delete project.file(".settings/org.eclipse.wst.common.component") - delete project.file(".settings/org.eclipse.wst.common.project.facet.core.xml") -} - -task eclipseBuildship(type: Copy) { - from rootProject.files( - "src/eclipse/org.eclipse.jdt.ui.prefs", - "src/eclipse/org.eclipse.jdt.core.prefs") - into project.file('.settings/') - outputs.upToDateWhen { false } -} - -tasks["eclipseJdt"].dependsOn(eclipseJdtPrepare) -tasks["cleanEclipse"].dependsOn(cleanEclipseJdtUi) -tasks["eclipse"].dependsOn(eclipseSettings, eclipseWstComponent) - - -// Filter 'build' folder -eclipse.project.file.withXml { - def node = it.asNode() - - def filteredResources = node.get("filteredResources") - if(filteredResources) { - node.remove(filteredResources) - } - def filterNode = node.appendNode("filteredResources").appendNode("filter") - filterNode.appendNode("id", "1359048889071") - filterNode.appendNode("name", "") - filterNode.appendNode("type", "30") - def matcherNode = filterNode.appendNode("matcher") - matcherNode.appendNode("id", "org.eclipse.ui.ide.multiFilter") - matcherNode.appendNode("arguments", "1.0-projectRelativePath-matches-false-false-build") -} +tasks['eclipse'].dependsOn(eclipseSettings) +tasks['eclipseJdt'].dependsOn(eclipseSettings) +tasks['cleanEclipse'].dependsOn(cleanEclipseSettings) diff --git a/gradle/spring-module.gradle b/gradle/spring-module.gradle index ebf196ff1fa4..99ae6762e206 100644 --- a/gradle/spring-module.gradle +++ b/gradle/spring-module.gradle @@ -1,17 +1,47 @@ -apply plugin: 'org.springframework.build.compile' +apply plugin: 'java-library' +apply plugin: 'org.springframework.build.conventions' apply plugin: 'org.springframework.build.optional-dependencies' -apply plugin: 'me.champeau.gradle.jmh' +// Uncomment the following for Shadow support in the jmhJar block. +// Currently commented out due to ZipException: archive is not a ZIP archive +// apply plugin: 'com.github.johnrengelman.shadow' +apply plugin: 'me.champeau.jmh' apply from: "$rootDir/gradle/publications.gradle" dependencies { - jmh 'org.openjdk.jmh:jmh-core:1.23' - jmh 'org.openjdk.jmh:jmh-generator-annprocess:1.23' - jmh 'net.sf.jopt-simple:jopt-simple:4.6' + jmh 'org.openjdk.jmh:jmh-core:1.32' + jmh 'org.openjdk.jmh:jmh-generator-annprocess:1.32' + jmh 'net.sf.jopt-simple:jopt-simple' } + +pluginManager.withPlugin("kotlin") { + apply plugin: "org.jetbrains.dokka" + apply from: "${rootDir}/gradle/docs-dokka.gradle" +} + jmh { duplicateClassesStrategy = DuplicatesStrategy.EXCLUDE } +tasks.findByName("processJmhResources").configure { + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} + +jmhJar { + // Uncomment the following for Shadow's Transformer support. + // mergeServiceFiles() + // append('META-INF/spring.handlers') + // append('META-INF/spring.schemas') + // append('META-INF/spring.tooling') + exclude 'LICENSE' + exclude 'THIRD-PARTY' + exclude 'META-INF/license.txt' + exclude 'META-INF/notice.txt' + exclude 'META-INF/DEPENDENCIES' + exclude 'META-INF/LICENSE*' + exclude 'META-INF/NOTICE' + exclude 'META-INF/THIRD-PARTY' +} + jar { manifest.attributes["Implementation-Title"] = project.name manifest.attributes["Implementation-Version"] = project.version @@ -19,7 +49,7 @@ jar { manifest.attributes["Created-By"] = "${System.getProperty("java.version")} (${System.getProperty("java.specification.vendor")})" - from("${rootDir}/src/docs/dist") { + from("${rootDir}/framework-docs/src/docs/dist") { include "license.txt" include "notice.txt" into "META-INF" diff --git a/gradle/toolchains.gradle b/gradle/toolchains.gradle new file mode 100644 index 000000000000..f7eaeb8c2f4d --- /dev/null +++ b/gradle/toolchains.gradle @@ -0,0 +1,143 @@ +/** + * Apply the JVM Toolchain conventions + * See https://docs.gradle.org/current/userguide/toolchains.html + * + * One can choose the toolchain to use for compiling the MAIN sources and/or compiling + * and running the TEST sources. These options apply to Java, Kotlin and Groovy sources + * when available. + * {@code "./gradlew check -PmainToolchain=17 -PtestToolchain=18"} will use: + *
    + *
  • a JDK17 toolchain for compiling the main SourceSet + *
  • a JDK18 toolchain for compiling and running the test SourceSet + *
+ * + * By default, the build will fall back to using the current JDK and 17 language level for all sourceSets. + * + * Gradle will automatically detect JDK distributions in well-known locations. + * The following command will list the detected JDKs on the host. + * {@code + * $ ./gradlew -q javaToolchains + * } + * + * We can also configure ENV variables and let Gradle know about them: + * {@code + * $ echo JDK17 + * /opt/openjdk/java17 + * $ echo JDK18 + * /opt/openjdk/java18 + * $ ./gradlew -Porg.gradle.java.installations.fromEnv=JDK17,JDK18 check + * } + * + * @author Brian Clozel + * @author Sam Brannen + */ + +def mainToolchainConfigured() { + return project.hasProperty('mainToolchain') && project.mainToolchain +} + +def testToolchainConfigured() { + return project.hasProperty('testToolchain') && project.testToolchain +} + +def mainToolchainLanguageVersion() { + if (mainToolchainConfigured()) { + return JavaLanguageVersion.of(project.mainToolchain.toString()) + } + return JavaLanguageVersion.of(17) +} + +def testToolchainLanguageVersion() { + if (testToolchainConfigured()) { + return JavaLanguageVersion.of(project.testToolchain.toString()) + } + return mainToolchainLanguageVersion() +} + +plugins.withType(JavaPlugin) { + // Configure the Java Toolchain if the 'mainToolchain' is configured + if (mainToolchainConfigured()) { + java { + toolchain { + languageVersion = mainToolchainLanguageVersion() + } + } + } + else { + // Fallback to JDK17 + java { + sourceCompatibility = JavaVersion.VERSION_17 + } + } + // Configure a specific Java Toolchain for compiling and running tests if the 'testToolchain' property is defined + if (testToolchainConfigured()) { + def testLanguageVersion = testToolchainLanguageVersion() + tasks.withType(JavaCompile).matching { it.name.contains("Test") }.configureEach { + javaCompiler = javaToolchains.compilerFor { + languageVersion = testLanguageVersion + } + } + tasks.withType(Test).configureEach{ + javaLauncher = javaToolchains.launcherFor { + languageVersion = testLanguageVersion + } + } + } +} + +plugins.withType(GroovyPlugin) { + // Fallback to JDK17 + if (!mainToolchainConfigured()) { + compileGroovy { + sourceCompatibility = JavaVersion.VERSION_17 + } + } +} + +pluginManager.withPlugin("kotlin") { + // Fallback to JDK17 + compileKotlin { + kotlinOptions { + jvmTarget = '17' + } + } + compileTestKotlin { + kotlinOptions { + jvmTarget = '17' + } + } +} + +// Configure the JMH plugin to use the toolchain for generating and running JMH bytecode +pluginManager.withPlugin("me.champeau.jmh") { + if (mainToolchainConfigured() || testToolchainConfigured()) { + tasks.matching { it.name.contains('jmh') && it.hasProperty('javaLauncher') }.configureEach { + javaLauncher.set(javaToolchains.launcherFor { + languageVersion.set(testToolchainLanguageVersion()) + }) + } + tasks.withType(JavaCompile).matching { it.name.contains("Jmh") }.configureEach { + javaCompiler = javaToolchains.compilerFor { + languageVersion = testToolchainLanguageVersion() + } + } + } +} + +// Store resolved Toolchain JVM information as custom values in the build scan. +rootProject.ext { + resolvedMainToolchain = false + resolvedTestToolchain = false +} +gradle.taskGraph.afterTask { Task task, TaskState state -> + if (mainToolchainConfigured() && !resolvedMainToolchain && task instanceof JavaCompile && task.javaCompiler.isPresent()) { + def metadata = task.javaCompiler.get().metadata + task.project.buildScan.value('Main toolchain', "$metadata.vendor $metadata.languageVersion ($metadata.installationPath)") + resolvedMainToolchain = true + } + if (testToolchainConfigured() && !resolvedTestToolchain && task instanceof Test && task.javaLauncher.isPresent()) { + def metadata = task.javaLauncher.get().metadata + task.project.buildScan.value('Test toolchain', "$metadata.vendor $metadata.languageVersion ($metadata.installationPath)") + resolvedTestToolchain = true + } +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e708b1c023ec..41d9927a4d4f 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index be52383ef49c..ae04661ee733 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 4f906e0c811f..1b6c787337ff 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,67 +17,101 @@ # ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +APP_BASE_NAME=${0##*/} # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -106,80 +140,95 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/import-into-eclipse.md b/import-into-eclipse.md index bb665d788a13..9258de40dba5 100644 --- a/import-into-eclipse.md +++ b/import-into-eclipse.md @@ -3,41 +3,46 @@ This document will guide you through the process of importing the Spring Framework projects into Eclipse or the Spring Tool Suite (_STS_). It is recommended that you have a recent version of Eclipse. As a bare minimum you will need Eclipse with full Java -8 support, Eclipse Buildship, the Kotlin plugin, and the Groovy plugin. +17 support and Eclipse Buildship. -The following instructions have been tested against [STS](https://spring.io/tools) 4.3.2 -([download](https://github.com/spring-projects/sts4/wiki/Previous-Versions#spring-tools-432-changelog)) -(based on Eclipse 4.12) with [Eclipse Buildship](https://projects.eclipse.org/projects/tools.buildship). +The following instructions have been tested against [STS](https://spring.io/tools) 4.12.0 +([download](https://github.com/spring-projects/sts4/wiki/Previous-Versions#spring-tools-4120-changelog)) +(based on Eclipse 4.21) with [Eclipse Buildship](https://projects.eclipse.org/projects/tools.buildship). The instructions should work with the latest Eclipse distribution as long as you install [Buildship](https://marketplace.eclipse.org/content/buildship-gradle-integration). Note that STS 4 comes with Buildship preinstalled. +If you are using Eclipse 4.21, you will need to install +[Java 17 Support for Eclipse 2021-09 (4.21)](https://marketplace.eclipse.org/content/java-17-support-eclipse-2021-09-421) +from the Eclipse Marketplace. + ## Steps _When instructed to execute `./gradlew` from the command line, be sure to execute it within your locally cloned `spring-framework` working directory._ -1. Ensure that Eclipse launches with JDK 8. - - For example, on Mac OS this can be configured in the `Info.plist` file located in the `Contents` folder of the installed Eclipse or STS application (e.g., the `Eclipse.app` file). -1. Install the [Kotlin Plugin for Eclipse](https://marketplace.eclipse.org/content/kotlin-plugin-eclipse) in Eclipse. -1. Install the [Eclipse Groovy Development Tools](https://github.com/groovy/groovy-eclipse/wiki) in Eclipse. -1. Switch to Groovy 2.5 (Preferences -> Groovy -> Compiler -> Switch to 2.5...) in Eclipse. -1. Change the _Forbidden reference (access rule)_ in Eclipse from Error to Warning -(Preferences -> Java -> Compiler -> Errors/Warnings -> Deprecated and restricted API -> Forbidden reference (access rule)). -1. Optionally install the [AspectJ Development Tools](https://marketplace.eclipse.org/content/aspectj-development-tools) (_AJDT_) if you need to work with the `spring-aspects` project. The AspectJ Development Tools available in the Eclipse Marketplace have been tested with these instructions using STS 4.5 (Eclipse 4.14). -1. Optionally install the [TestNG plugin](https://testng.org/doc/eclipse.html) in Eclipse if you need to execute TestNG tests in the `spring-test` module. +1. Ensure that the _Forbidden reference (access rule)_ in Eclipse is set to `Info` +(Preferences → Java → Compiler → Errors/Warnings → Deprecated and restricted API → Forbidden reference (access rule)). +1. Optionally install the [Kotlin Plugin for Eclipse](https://marketplace.eclipse.org/content/kotlin-plugin-eclipse) if you need to execute Kotlin-based tests or develop Kotlin extensions. + - **NOTE**: As of September 21, 2021, it appears that the Kotlin Plugin for Eclipse does not yet work with Eclipse 4.21. +1. Optionally install the [AspectJ Development Tools](https://marketplace.eclipse.org/content/aspectj-development-tools) (_AJDT_) if you need to work with the `spring-aspects` project. + - **NOTE**: As of September 21, 2021, it appears that the AspectJ Development Tools do not yet work with Eclipse 4.21. +1. Optionally install the [TestNG plugin](https://testng.org/doc/eclipse.html) in Eclipse if you need to execute individual TestNG test classes or tests in the `spring-test` module. + - As an alternative to installing the TestNG plugin, you can execute the `org.springframework.test.context.testng.TestNGTestSuite` class as a "JUnit 5" test class in Eclipse. 1. Build `spring-oxm` from the command line with `./gradlew :spring-oxm:check`. -1. To apply project specific settings, run `./gradlew eclipseBuildship` from the command line. -1. Import into Eclipse (File -> Import -> Gradle -> Existing Gradle Project -> Navigate to the locally cloned `spring-framework` directory -> Select Finish). +1. To apply Spring Framework specific settings, run `./gradlew cleanEclipse eclipse` from the command line. +1. Import all projects into Eclipse (File → Import → Gradle → Existing Gradle Project → Navigate to the locally cloned `spring-framework` directory → Select Finish). - If you have not installed AJDT, exclude the `spring-aspects` project from the import, if prompted, or close it after the import. - - If you run into errors during the import, you may need to set the _Java home_ for Gradle Buildship to the location of your JDK 8 installation in Eclipse (Preferences -> Gradle -> Java home). -1. If you need to execute JAXB-related tests in the `spring-oxm` project and wish to have the generated sources available, add the `build/generated-sources/jaxb` folder to the build path (right click on the `jaxb` folder and select `Build Path -> Use as Source Folder`). - - If you do not see the `build` folder in the `spring-oxm` project, ensure that the "Gradle build folder" is not filtered out from the view. This setting is available under "Filters" in the configuration of the Package Explorer (available by clicking on the small downward facing arrow in the upper right corner of the Package Explorer). + - If you run into errors during the import, you may need to set the _Java home_ for Gradle Buildship to the location of your JDK 8 installation in Eclipse (Preferences → Gradle → Java home). +1. If you need to execute JAXB-related tests in the `spring-oxm` project and wish to have the generated sources available, add the `build/generated-sources/jaxb` folder to the build path (right click on the `jaxb` folder and select "Build Path → Use as Source Folder"). + - If you do not see the `build` folder in the `spring-oxm` project, ensure that the "Gradle build folder" is not filtered out from the view. This setting is available under "Filters" in the configuration of the Package Explorer (available by clicking on the _three vertical dots_ in the upper right corner of the Package Explorer). 1. Code away! ## Known Issues -1. `spring-core` and `spring-oxm` should be pre-compiled due to repackaged dependencies. - - See `*RepackJar` tasks in the build. +1. `spring-core` should be pre-compiled due to repackaged dependencies. + - See `*RepackJar` tasks in the `spring-core.gradle` build file. +1. `spring-oxm` should be pre-compiled due to JAXB types generated for tests. + - Note that executing `./gradlew :spring-oxm:check` as explained in the _Steps_ above will compile `spring-core` and generate JAXB types for `spring-oxm`. 1. `spring-aspects` does not compile due to references to aspect types unknown to Eclipse. - If you installed _AJDT_ into Eclipse it should work. 1. While JUnit tests pass from the command line with Gradle, some may fail when run from diff --git a/import-into-idea.md b/import-into-idea.md index 1d555305c024..bc6336f748ef 100644 --- a/import-into-idea.md +++ b/import-into-idea.md @@ -31,6 +31,6 @@ You'll notice these files are already intentionally in .gitignore. The same poli ## FAQ -Q. What about IntelliJ IDEA's own [Gradle support](https://confluence.jetbrains.net/display/IDEADEV/Gradle+integration)? +Q. What about IntelliJ IDEA's own [Gradle support](https://www.jetbrains.com/help/idea/gradle.html)? A. Keep an eye on https://youtrack.jetbrains.com/issue/IDEA-53476 diff --git a/integration-tests/integration-tests.gradle b/integration-tests/integration-tests.gradle index 71e23b906049..88455c9391b7 100644 --- a/integration-tests/integration-tests.gradle +++ b/integration-tests/integration-tests.gradle @@ -1,26 +1,31 @@ +plugins { + id 'org.springframework.build.runtimehints-agent' +} + description = "Spring Integration Tests" dependencies { - testCompile(project(":spring-aop")) - testCompile(project(":spring-beans")) - testCompile(project(":spring-context")) - testCompile(project(":spring-core")) - testCompile(testFixtures(project(":spring-aop"))) - testCompile(testFixtures(project(":spring-beans"))) - testCompile(testFixtures(project(":spring-core"))) - testCompile(testFixtures(project(":spring-tx"))) - testCompile(project(":spring-expression")) - testCompile(project(":spring-jdbc")) - testCompile(project(":spring-orm")) - testCompile(project(":spring-test")) - testCompile(project(":spring-tx")) - testCompile(project(":spring-web")) - testCompile("javax.inject:javax.inject") - testCompile("javax.resource:javax.resource-api") - testCompile("javax.servlet:javax.servlet-api") - testCompile("org.aspectj:aspectjweaver") - testCompile("org.hsqldb:hsqldb") - testCompile("org.hibernate:hibernate-core") + testImplementation(project(":spring-aop")) + testImplementation(project(":spring-beans")) + testImplementation(project(":spring-context")) + testImplementation(project(":spring-core")) + testImplementation(project(":spring-core-test")) + testImplementation(testFixtures(project(":spring-aop"))) + testImplementation(testFixtures(project(":spring-beans"))) + testImplementation(testFixtures(project(":spring-core"))) + testImplementation(testFixtures(project(":spring-tx"))) + testImplementation(project(":spring-expression")) + testImplementation(project(":spring-jdbc")) + testImplementation(project(":spring-orm")) + testImplementation(project(":spring-test")) + testImplementation(project(":spring-tx")) + testImplementation(project(":spring-web")) + testImplementation("jakarta.inject:jakarta.inject-api") + testImplementation("jakarta.resource:jakarta.resource-api") + testImplementation("jakarta.servlet:jakarta.servlet-api") + testImplementation("org.aspectj:aspectjweaver") + testImplementation("org.hsqldb:hsqldb") + testImplementation("org.hibernate:hibernate-core-jakarta") } normalization { diff --git a/integration-tests/src/test/java/org/springframework/aop/config/AopNamespaceHandlerAdviceOrderIntegrationTests.java b/integration-tests/src/test/java/org/springframework/aop/config/AopNamespaceHandlerAdviceOrderIntegrationTests.java index da32ff120181..8cc9a4c05407 100644 --- a/integration-tests/src/test/java/org/springframework/aop/config/AopNamespaceHandlerAdviceOrderIntegrationTests.java +++ b/integration-tests/src/test/java/org/springframework/aop/config/AopNamespaceHandlerAdviceOrderIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -28,7 +28,7 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatException; /** * Integration tests for advice invocation order for advice configured via the @@ -52,7 +52,7 @@ void afterAdviceIsInvokedFirst(@Autowired Echo echo, @Autowired InvocationTracki assertThat(aspect.invocations).containsExactly("around - start", "before", "around - end", "after", "after returning"); aspect.invocations.clear(); - assertThatExceptionOfType(Exception.class).isThrownBy(() -> echo.echo(new Exception())); + assertThatException().isThrownBy(() -> echo.echo(new Exception())); assertThat(aspect.invocations).containsExactly("around - start", "before", "around - end", "after", "after throwing"); } } @@ -69,7 +69,7 @@ void afterAdviceIsInvokedLast(@Autowired Echo echo, @Autowired InvocationTrackin assertThat(aspect.invocations).containsExactly("around - start", "before", "around - end", "after returning", "after"); aspect.invocations.clear(); - assertThatExceptionOfType(Exception.class).isThrownBy(() -> echo.echo(new Exception())); + assertThatException().isThrownBy(() -> echo.echo(new Exception())); assertThat(aspect.invocations).containsExactly("around - start", "before", "around - end", "after throwing", "after"); } } diff --git a/integration-tests/src/test/java/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorIntegrationTests.java b/integration-tests/src/test/java/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorIntegrationTests.java index cf067e01415c..48142f4c9374 100644 --- a/integration-tests/src/test/java/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorIntegrationTests.java +++ b/integration-tests/src/test/java/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorIntegrationTests.java @@ -20,8 +20,7 @@ import java.lang.reflect.Method; import java.util.List; -import javax.servlet.ServletException; - +import jakarta.servlet.ServletException; import org.junit.jupiter.api.Test; import org.springframework.aop.support.AopUtils; diff --git a/integration-tests/src/test/java/org/springframework/aop/framework/autoproxy/AspectJAutoProxyAdviceOrderIntegrationTests.java b/integration-tests/src/test/java/org/springframework/aop/framework/autoproxy/AspectJAutoProxyAdviceOrderIntegrationTests.java index 78d45922be0c..d09a4cf78117 100644 --- a/integration-tests/src/test/java/org/springframework/aop/framework/autoproxy/AspectJAutoProxyAdviceOrderIntegrationTests.java +++ b/integration-tests/src/test/java/org/springframework/aop/framework/autoproxy/AspectJAutoProxyAdviceOrderIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -38,7 +38,7 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatException; /** * Integration tests for advice invocation order for advice configured via @@ -65,8 +65,7 @@ void afterAdviceIsInvokedLast(@Autowired Echo echo, @Autowired AfterAdviceFirstA assertThat(aspect.invocations).containsExactly("around - start", "before", "after returning", "after", "around - end"); aspect.invocations.clear(); - assertThatExceptionOfType(Exception.class).isThrownBy( - () -> echo.echo(new Exception())); + assertThatException().isThrownBy(() -> echo.echo(new Exception())); assertThat(aspect.invocations).containsExactly("around - start", "before", "after throwing", "after", "around - end"); } } @@ -95,8 +94,7 @@ void afterAdviceIsInvokedLast(@Autowired Echo echo, @Autowired AfterAdviceLastAs assertThat(aspect.invocations).containsExactly("around - start", "before", "after returning", "after", "around - end"); aspect.invocations.clear(); - assertThatExceptionOfType(Exception.class).isThrownBy( - () -> echo.echo(new Exception())); + assertThatException().isThrownBy(() -> echo.echo(new Exception())); assertThat(aspect.invocations).containsExactly("around - start", "before", "after throwing", "after", "around - end"); } } diff --git a/integration-tests/src/test/java/org/springframework/aot/RuntimeHintsAgentTests.java b/integration-tests/src/test/java/org/springframework/aot/RuntimeHintsAgentTests.java new file mode 100644 index 000000000000..2309cbc01d6e --- /dev/null +++ b/integration-tests/src/test/java/org/springframework/aot/RuntimeHintsAgentTests.java @@ -0,0 +1,297 @@ +/* + * Copyright 2002-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.aot; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.ResourceBundle; +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import org.springframework.aot.agent.HintType; +import org.springframework.aot.agent.MethodReference; +import org.springframework.aot.agent.RecordedInvocation; +import org.springframework.aot.agent.RecordedInvocationsListener; +import org.springframework.aot.agent.RecordedInvocationsPublisher; +import org.springframework.aot.agent.RuntimeHintsAgent; +import org.springframework.aot.test.agent.EnabledIfRuntimeHintsAgent; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link RuntimeHintsAgent}. + * + * @author Brian Clozel + */ +@EnabledIfRuntimeHintsAgent +public class RuntimeHintsAgentTests { + + private static final ClassLoader classLoader = ClassLoader.getSystemClassLoader(); + + private static Constructor defaultConstructor; + + private static Method toStringMethod; + + private static Method privateGreetMethod; + + + @BeforeAll + public static void classSetup() throws NoSuchMethodException { + defaultConstructor = String.class.getConstructor(); + toStringMethod = String.class.getMethod("toString"); + privateGreetMethod = PrivateClass.class.getDeclaredMethod("greet"); + } + + + @ParameterizedTest + @MethodSource("instrumentedReflectionMethods") + void shouldInstrumentReflectionMethods(Runnable runnable, MethodReference methodReference) { + RecordingSession session = RecordingSession.record(runnable); + assertThat(session.recordedInvocations()).hasSize(1); + RecordedInvocation invocation = session.recordedInvocations().findFirst().get(); + assertThat(invocation.getMethodReference()).isEqualTo(methodReference); + assertThat(invocation.getStackFrames()).first().matches(frame -> frame.getClassName().equals(RuntimeHintsAgentTests.class.getName())); + } + + private static Stream instrumentedReflectionMethods() { + return Stream.of( + Arguments.of((Runnable) () -> { + try { + Class.forName("java.lang.String"); + } + catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + }, MethodReference.of(Class.class, "forName")), + Arguments.of((Runnable) () -> String.class.getClasses(), MethodReference.of(Class.class, "getClasses")), + Arguments.of((Runnable) () -> { + try { + String.class.getConstructor(); + } + catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + }, MethodReference.of(Class.class, "getConstructor")), + Arguments.of((Runnable) () -> String.class.getConstructors(), MethodReference.of(Class.class, "getConstructors")), + Arguments.of((Runnable) () -> String.class.getDeclaredClasses(), MethodReference.of(Class.class, "getDeclaredClasses")), + Arguments.of((Runnable) () -> { + try { + String.class.getDeclaredConstructor(); + } + catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + }, MethodReference.of(Class.class, "getDeclaredConstructor")), + Arguments.of((Runnable) () -> String.class.getDeclaredConstructors(), MethodReference.of(Class.class, "getDeclaredConstructors")), + Arguments.of((Runnable) () -> { + try { + String.class.getDeclaredField("value"); + } + catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } + }, MethodReference.of(Class.class, "getDeclaredField")), + Arguments.of((Runnable) () -> String.class.getDeclaredFields(), MethodReference.of(Class.class, "getDeclaredFields")), + Arguments.of((Runnable) () -> { + try { + String.class.getDeclaredMethod("toString"); + } + catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + }, MethodReference.of(Class.class, "getDeclaredMethod")), + Arguments.of((Runnable) () -> String.class.getDeclaredMethods(), MethodReference.of(Class.class, "getDeclaredMethods")), + Arguments.of((Runnable) () -> { + try { + String.class.getField("value"); + } + catch (NoSuchFieldException e) { + } + }, MethodReference.of(Class.class, "getField")), + Arguments.of((Runnable) () -> String.class.getFields(), MethodReference.of(Class.class, "getFields")), + Arguments.of((Runnable) () -> { + try { + String.class.getMethod("toString"); + } + catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + }, MethodReference.of(Class.class, "getMethod")), + Arguments.of((Runnable) () -> String.class.getMethods(), MethodReference.of(Class.class, "getMethods")), + Arguments.of((Runnable) () -> { + try { + classLoader.loadClass("java.lang.String"); + } + catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + }, MethodReference.of(ClassLoader.class, "loadClass")), + Arguments.of((Runnable) () -> { + try { + defaultConstructor.newInstance(); + } + catch (Exception e) { + throw new RuntimeException(e); + } + }, MethodReference.of(Constructor.class, "newInstance")), + Arguments.of((Runnable) () -> { + try { + toStringMethod.invoke(""); + } + catch (Exception e) { + throw new RuntimeException(e); + } + }, MethodReference.of(Method.class, "invoke")), + Arguments.of((Runnable) () -> { + try { + privateGreetMethod.invoke(new PrivateClass()); + } + catch (Exception e) { + throw new RuntimeException(e); + } + }, MethodReference.of(Method.class, "invoke")) + ); + } + + @ParameterizedTest + @MethodSource("instrumentedResourceBundleMethods") + void shouldInstrumentResourceBundleMethods(Runnable runnable, MethodReference methodReference) { + RecordingSession session = RecordingSession.record(runnable); + assertThat(session.recordedInvocations(HintType.RESOURCE_BUNDLE)).hasSize(1); + + RecordedInvocation resolution = session.recordedInvocations(HintType.RESOURCE_BUNDLE).findFirst().get(); + assertThat(resolution.getMethodReference()).isEqualTo(methodReference); + assertThat(resolution.getStackFrames()).first().matches(frame -> frame.getClassName().equals(RuntimeHintsAgentTests.class.getName())); + } + + + private static Stream instrumentedResourceBundleMethods() { + return Stream.of( + Arguments.of((Runnable) () -> { + try { + ResourceBundle.getBundle("testBundle"); + } + catch (Throwable exc) { + } + }, + MethodReference.of(ResourceBundle.class, "getBundle")) + ); + } + + @ParameterizedTest + @MethodSource("instrumentedResourcePatternMethods") + void shouldInstrumentResourcePatternMethods(Runnable runnable, MethodReference methodReference) { + RecordingSession session = RecordingSession.record(runnable); + assertThat(session.recordedInvocations(HintType.RESOURCE_PATTERN)).hasSize(1); + + RecordedInvocation resolution = session.recordedInvocations(HintType.RESOURCE_PATTERN).findFirst().get(); + assertThat(resolution.getMethodReference()).isEqualTo(methodReference); + assertThat(resolution.getStackFrames()).first().matches(frame -> frame.getClassName().equals(RuntimeHintsAgentTests.class.getName())); + } + + + private static Stream instrumentedResourcePatternMethods() { + return Stream.of( + Arguments.of((Runnable) () -> RuntimeHintsAgentTests.class.getResource("sample.txt"), + MethodReference.of(Class.class, "getResource")), + Arguments.of((Runnable) () -> RuntimeHintsAgentTests.class.getResourceAsStream("sample.txt"), + MethodReference.of(Class.class, "getResourceAsStream")), + Arguments.of((Runnable) () -> classLoader.getResource("sample.txt"), + MethodReference.of(ClassLoader.class, "getResource")), + Arguments.of((Runnable) () -> classLoader.getResourceAsStream("sample.txt"), + MethodReference.of(ClassLoader.class, "getResourceAsStream")), + Arguments.of((Runnable) () -> { + try { + classLoader.getResources("sample.txt"); + } + catch (IOException e) { + } + }, + MethodReference.of(ClassLoader.class, "getResources")), + Arguments.of((Runnable) () -> { + try { + RuntimeHintsAgentTests.class.getModule().getResourceAsStream("sample.txt"); + } + catch (IOException e) { + } + }, + MethodReference.of(Module.class, "getResourceAsStream")), + Arguments.of((Runnable) () -> classLoader.resources("sample.txt"), + MethodReference.of(ClassLoader.class, "resources")) + ); + } + + @Test + void shouldInstrumentStaticMethodHandle() { + RecordingSession session = RecordingSession.record(ClassLoader.class::getClasses); + assertThat(session.recordedInvocations(HintType.REFLECTION)).hasSize(1); + + RecordedInvocation resolution = session.recordedInvocations(HintType.REFLECTION).findFirst().get(); + assertThat(resolution.getMethodReference()).isEqualTo(MethodReference.of(Class.class, "getClasses")); + assertThat(resolution.getStackFrames()).first().extracting(StackWalker.StackFrame::getClassName) + .isEqualTo(RuntimeHintsAgentTests.class.getName() + "$RecordingSession"); + } + + static class RecordingSession implements RecordedInvocationsListener { + + final Deque recordedInvocations = new ArrayDeque<>(); + + static RecordingSession record(Runnable action) { + RecordingSession session = new RecordingSession(); + RecordedInvocationsPublisher.addListener(session); + try { + action.run(); + } + finally { + RecordedInvocationsPublisher.removeListener(session); + } + return session; + } + + @Override + public void onInvocation(RecordedInvocation invocation) { + this.recordedInvocations.addLast(invocation); + } + + Stream recordedInvocations() { + return this.recordedInvocations.stream(); + } + + Stream recordedInvocations(HintType hintType) { + return recordedInvocations().filter(invocation -> invocation.getHintType() == hintType); + } + + } + + private static class PrivateClass { + + @SuppressWarnings("unused") + private String greet() { + return "hello"; + } + + } + +} diff --git a/integration-tests/src/test/java/org/springframework/aot/test/ReflectionInvocationsTests.java b/integration-tests/src/test/java/org/springframework/aot/test/ReflectionInvocationsTests.java new file mode 100644 index 000000000000..541025c19c17 --- /dev/null +++ b/integration-tests/src/test/java/org/springframework/aot/test/ReflectionInvocationsTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-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.aot.test; + +import org.junit.jupiter.api.Test; + +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.test.agent.EnabledIfRuntimeHintsAgent; +import org.springframework.aot.test.agent.RuntimeHintsInvocations; +import org.springframework.aot.test.agent.RuntimeHintsRecorder; + +import static org.assertj.core.api.Assertions.assertThat; + + +@EnabledIfRuntimeHintsAgent +class ReflectionInvocationsTests { + + @Test + void sampleTest() { + RuntimeHints hints = new RuntimeHints(); + hints.reflection().registerType(String.class, MemberCategory.INTROSPECT_PUBLIC_METHODS); + + RuntimeHintsInvocations invocations = RuntimeHintsRecorder.record(() -> { + SampleReflection sample = new SampleReflection(); + sample.sample(); // does Method[] methods = String.class.getMethods(); + }); + assertThat(invocations).match(hints); + } + + @Test + void multipleCallsTest() { + RuntimeHints hints = new RuntimeHints(); + hints.reflection().registerType(String.class, MemberCategory.INTROSPECT_PUBLIC_METHODS); + hints.reflection().registerType(Integer.class,MemberCategory.INTROSPECT_PUBLIC_METHODS); + RuntimeHintsInvocations invocations = RuntimeHintsRecorder.record(() -> { + SampleReflection sample = new SampleReflection(); + sample.multipleCalls(); // does Method[] methods = String.class.getMethods(); methods = Integer.class.getMethods(); + }); + assertThat(invocations).match(hints); + } + +} diff --git a/integration-tests/src/test/java/org/springframework/aot/test/SampleReflection.java b/integration-tests/src/test/java/org/springframework/aot/test/SampleReflection.java new file mode 100644 index 000000000000..3f8cbadffc61 --- /dev/null +++ b/integration-tests/src/test/java/org/springframework/aot/test/SampleReflection.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-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.aot.test; + +import java.lang.reflect.Method; + +/** + * @author Brian Clozel + * @since 6.0 + */ +public class SampleReflection { + + @SuppressWarnings("unused") + public void sample() { + String value = "Sample"; + Method[] methods = String.class.getMethods(); + } + + @SuppressWarnings("unused") + public void multipleCalls() { + String value = "Sample"; + Method[] methods = String.class.getMethods(); + methods = Integer.class.getMethods(); + } + +} diff --git a/integration-tests/src/test/java/org/springframework/cache/annotation/EnableCachingIntegrationTests.java b/integration-tests/src/test/java/org/springframework/cache/annotation/EnableCachingIntegrationTests.java index 645482958a94..63534df9635c 100644 --- a/integration-tests/src/test/java/org/springframework/cache/annotation/EnableCachingIntegrationTests.java +++ b/integration-tests/src/test/java/org/springframework/cache/annotation/EnableCachingIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -34,7 +34,7 @@ import org.springframework.stereotype.Repository; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatException; /** * Integration tests for the @EnableCaching annotation. @@ -62,8 +62,7 @@ void repositoryUsesAspectJAdviceMode() { // this test is a bit fragile, but gets the job done, proving that an // attempt was made to look up the AJ aspect. It's due to classpath issues // in .integration-tests that it's not found. - assertThatExceptionOfType(Exception.class).isThrownBy( - ctx::refresh) + assertThatException().isThrownBy(ctx::refresh) .withMessageContaining("AspectJCachingConfiguration"); } diff --git a/integration-tests/src/test/java/org/springframework/context/annotation/jsr330/ClassPathBeanDefinitionScannerJsr330ScopeIntegrationTests.java b/integration-tests/src/test/java/org/springframework/context/annotation/jsr330/ClassPathBeanDefinitionScannerJsr330ScopeIntegrationTests.java index bd86cc9041d1..c059826c8255 100644 --- a/integration-tests/src/test/java/org/springframework/context/annotation/jsr330/ClassPathBeanDefinitionScannerJsr330ScopeIntegrationTests.java +++ b/integration-tests/src/test/java/org/springframework/context/annotation/jsr330/ClassPathBeanDefinitionScannerJsr330ScopeIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -21,9 +21,8 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import javax.inject.Named; -import javax.inject.Singleton; - +import jakarta.inject.Named; +import jakarta.inject.Singleton; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -34,7 +33,6 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.ClassPathBeanDefinitionScanner; import org.springframework.context.annotation.ScopeMetadata; -import org.springframework.context.annotation.ScopeMetadataResolver; import org.springframework.context.annotation.ScopedProxyMode; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpSession; @@ -307,29 +305,25 @@ private ApplicationContext createContext(final ScopedProxyMode scopedProxyMode) GenericWebApplicationContext context = new GenericWebApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); scanner.setIncludeAnnotationConfig(false); - scanner.setScopeMetadataResolver(new ScopeMetadataResolver() { - @Override - public ScopeMetadata resolveScopeMetadata(BeanDefinition definition) { - ScopeMetadata metadata = new ScopeMetadata(); - if (definition instanceof AnnotatedBeanDefinition) { - AnnotatedBeanDefinition annDef = (AnnotatedBeanDefinition) definition; - for (String type : annDef.getMetadata().getAnnotationTypes()) { - if (type.equals(javax.inject.Singleton.class.getName())) { - metadata.setScopeName(BeanDefinition.SCOPE_SINGLETON); - break; - } - else if (annDef.getMetadata().getMetaAnnotationTypes(type).contains(javax.inject.Scope.class.getName())) { - metadata.setScopeName(type.substring(type.length() - 13, type.length() - 6).toLowerCase()); - metadata.setScopedProxyMode(scopedProxyMode); - break; - } - else if (type.startsWith("javax.inject")) { - metadata.setScopeName(BeanDefinition.SCOPE_PROTOTYPE); - } + scanner.setScopeMetadataResolver(definition -> { + ScopeMetadata metadata = new ScopeMetadata(); + if (definition instanceof AnnotatedBeanDefinition annDef) { + for (String type : annDef.getMetadata().getAnnotationTypes()) { + if (type.equals(jakarta.inject.Singleton.class.getName())) { + metadata.setScopeName(BeanDefinition.SCOPE_SINGLETON); + break; + } + else if (annDef.getMetadata().getMetaAnnotationTypes(type).contains(jakarta.inject.Scope.class.getName())) { + metadata.setScopeName(type.substring(type.length() - 13, type.length() - 6).toLowerCase()); + metadata.setScopedProxyMode(scopedProxyMode); + break; + } + else if (type.startsWith("jakarta.inject")) { + metadata.setScopeName(BeanDefinition.SCOPE_PROTOTYPE); } } - return metadata; } + return metadata; }); // Scan twice in order to find errors in the bean definition compatibility check. @@ -391,14 +385,14 @@ public static class SessionScopedTestBean extends ScopedTestBean implements Anot @Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) - @javax.inject.Scope + @jakarta.inject.Scope public @interface RequestScoped { } @Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) - @javax.inject.Scope + @jakarta.inject.Scope public @interface SessionScoped { } diff --git a/integration-tests/src/test/java/org/springframework/core/env/EnvironmentSystemIntegrationTests.java b/integration-tests/src/test/java/org/springframework/core/env/EnvironmentSystemIntegrationTests.java index 20e23ecca310..2bbc001f28eb 100644 --- a/integration-tests/src/test/java/org/springframework/core/env/EnvironmentSystemIntegrationTests.java +++ b/integration-tests/src/test/java/org/springframework/core/env/EnvironmentSystemIntegrationTests.java @@ -41,9 +41,6 @@ import org.springframework.context.support.GenericXmlApplicationContext; import org.springframework.context.support.StaticApplicationContext; import org.springframework.core.io.ClassPathResource; -import org.springframework.jca.context.ResourceAdapterApplicationContext; -import org.springframework.jca.support.SimpleBootstrapContext; -import org.springframework.jca.work.SimpleTaskWorkManager; import org.springframework.mock.env.MockEnvironment; import org.springframework.mock.env.MockPropertySource; import org.springframework.mock.web.MockServletConfig; @@ -535,22 +532,6 @@ void registerServletParamPropertySources_StaticWebApplicationContext() { assertThat(environment.getProperty("pSysProps1")).isEqualTo("pSysProps1Value"); } - @Test - void resourceAdapterApplicationContext() { - ResourceAdapterApplicationContext ctx = new ResourceAdapterApplicationContext(new SimpleBootstrapContext(new SimpleTaskWorkManager())); - - assertHasStandardEnvironment(ctx); - - registerEnvironmentBeanDefinition(ctx); - - ctx.setEnvironment(prodEnv); - ctx.refresh(); - - assertHasEnvironment(ctx, prodEnv); - assertEnvironmentBeanRegistered(ctx); - assertEnvironmentAwareInvoked(ctx, prodEnv); - } - @Test void abstractApplicationContextValidatesRequiredPropertiesOnRefresh() { { diff --git a/integration-tests/src/test/java/org/springframework/scheduling/annotation/ScheduledAndTransactionalAnnotationIntegrationTests.java b/integration-tests/src/test/java/org/springframework/scheduling/annotation/ScheduledAndTransactionalAnnotationIntegrationTests.java index 16db4fc393a1..9062d6e3f615 100644 --- a/integration-tests/src/test/java/org/springframework/scheduling/annotation/ScheduledAndTransactionalAnnotationIntegrationTests.java +++ b/integration-tests/src/test/java/org/springframework/scheduling/annotation/ScheduledAndTransactionalAnnotationIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -74,7 +74,7 @@ void succeedsWhenSubclassProxyAndScheduledMethodNotPresentOnInterface() throws I MyRepository repository = ctx.getBean(MyRepository.class); CallCountingTransactionManager txManager = ctx.getBean(CallCountingTransactionManager.class); - assertThat(AopUtils.isCglibProxy(repository)).isEqualTo(true); + assertThat(AopUtils.isCglibProxy(repository)).isTrue(); assertThat(repository.getInvocationCount()).isGreaterThan(0); assertThat(txManager.commits).isGreaterThan(0); } diff --git a/integration-tests/src/test/java/org/springframework/transaction/annotation/EnableTransactionManagementIntegrationTests.java b/integration-tests/src/test/java/org/springframework/transaction/annotation/EnableTransactionManagementIntegrationTests.java index 8ba437386b04..bf52ef5fde18 100644 --- a/integration-tests/src/test/java/org/springframework/transaction/annotation/EnableTransactionManagementIntegrationTests.java +++ b/integration-tests/src/test/java/org/springframework/transaction/annotation/EnableTransactionManagementIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -45,7 +45,7 @@ import org.springframework.transaction.testfixture.CallCountingTransactionManager; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatException; /** * Integration tests for the @EnableTransactionManagement annotation. @@ -98,7 +98,7 @@ void repositoryUsesAspectJAdviceMode() { // this test is a bit fragile, but gets the job done, proving that an // attempt was made to look up the AJ aspect. It's due to classpath issues // in .integration-tests that it's not found. - assertThatExceptionOfType(Exception.class) + assertThatException() .isThrownBy(ctx::refresh) .withMessageContaining("AspectJJtaTransactionManagementConfiguration"); } diff --git a/integration-tests/src/test/resources/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorIntegrationTests-context.xml b/integration-tests/src/test/resources/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorIntegrationTests-context.xml index 90ea7dc1f663..92abf56386a1 100644 --- a/integration-tests/src/test/resources/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorIntegrationTests-context.xml +++ b/integration-tests/src/test/resources/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorIntegrationTests-context.xml @@ -41,7 +41,7 @@ PROPAGATION_REQUIRED PROPAGATION_REQUIRED - PROPAGATION_REQUIRED,+javax.servlet.ServletException,-java.lang.Exception + PROPAGATION_REQUIRED,+jakarta.servlet.ServletException,-java.lang.Exception
diff --git a/settings.gradle b/settings.gradle index aacebba92d84..c2bb70b3472a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,13 +1,14 @@ pluginManagement { repositories { + mavenCentral() gradlePluginPortal() - maven { url 'https://repo.spring.io/plugins-release' } + maven { url "https://repo.spring.io/release" } } } plugins { - id "com.gradle.enterprise" version "3.2" - id "io.spring.gradle-enterprise-conventions" version "0.0.2" + id "com.gradle.enterprise" version "3.11.1" + id "io.spring.ge.conventions" version "0.0.11" } include "spring-aop" @@ -17,8 +18,7 @@ include "spring-context" include "spring-context-indexer" include "spring-context-support" include "spring-core" -include "kotlin-coroutines" -project(':kotlin-coroutines').projectDir = file('spring-core/kotlin-coroutines') +include "spring-core-test" include "spring-expression" include "spring-instrument" include "spring-jcl" @@ -35,6 +35,8 @@ include "spring-webflux" include "spring-webmvc" include "spring-websocket" include "framework-bom" +include "framework-docs" +include "framework-platform" include "integration-tests" rootProject.name = "spring" @@ -45,19 +47,13 @@ rootProject.children.each {project -> settings.gradle.projectsLoaded { gradleEnterprise { buildScan { - if (settings.gradle.rootProject.hasProperty('customJavaHome')) { - value("Custom JAVA_HOME", settings.gradle.rootProject.getProperty('customJavaHome')) - } - if (settings.gradle.rootProject.hasProperty('customJavaSourceVersion')) { - value("Custom Java Source Version", settings.gradle.rootProject.getProperty('customJavaSourceVersion')) - } File buildDir = settings.gradle.rootProject.getBuildDir() buildDir.mkdirs() new File(buildDir, "build-scan-uri.txt").text = "(build scan not generated)" buildScanPublished { scan -> if (buildDir.exists()) { new File(buildDir, "build-scan-uri.txt").text = "${scan.buildScanUri}\n" - } + } } } } diff --git a/spring-aop/spring-aop.gradle b/spring-aop/spring-aop.gradle index 73bb378f39a7..87a999850222 100644 --- a/spring-aop/spring-aop.gradle +++ b/spring-aop/spring-aop.gradle @@ -1,12 +1,13 @@ description = "Spring AOP" dependencies { - compile(project(":spring-beans")) - compile(project(":spring-core")) + api(project(":spring-beans")) + api(project(":spring-core")) optional("org.aspectj:aspectjweaver") optional("org.apache.commons:commons-pool2") - optional("com.jamonapi:jamon") - testCompile(testFixtures(project(":spring-beans"))) - testCompile(testFixtures(project(":spring-core"))) + testImplementation(project(":spring-core-test")) + testImplementation(testFixtures(project(":spring-beans"))) + testImplementation(testFixtures(project(":spring-core"))) + testFixturesImplementation(testFixtures(project(":spring-beans"))) testFixturesImplementation(testFixtures(project(":spring-core"))) } diff --git a/spring-aop/src/main/java/org/aopalliance/intercept/Joinpoint.java b/spring-aop/src/main/java/org/aopalliance/intercept/Joinpoint.java index 780275e9782c..b9755389409b 100644 --- a/spring-aop/src/main/java/org/aopalliance/intercept/Joinpoint.java +++ b/spring-aop/src/main/java/org/aopalliance/intercept/Joinpoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-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. @@ -26,7 +26,7 @@ * terminology). * *

A runtime joinpoint is an event that occurs on a static - * joinpoint (i.e. a location in a the program). For instance, an + * joinpoint (i.e. a location in a program). For instance, an * invocation is the runtime joinpoint on a method (static joinpoint). * The static part of a given joinpoint can be generically retrieved * using the {@link #getStaticPart()} method. @@ -63,7 +63,7 @@ public interface Joinpoint { /** * Return the static part of this joinpoint. *

The static part is an accessible object on which a chain of - * interceptors are installed. + * interceptors is installed. */ @Nonnull AccessibleObject getStaticPart(); diff --git a/spring-aop/src/main/java/org/springframework/aop/DynamicIntroductionAdvice.java b/spring-aop/src/main/java/org/springframework/aop/DynamicIntroductionAdvice.java index 08c704857f75..2f46775b9459 100644 --- a/spring-aop/src/main/java/org/springframework/aop/DynamicIntroductionAdvice.java +++ b/spring-aop/src/main/java/org/springframework/aop/DynamicIntroductionAdvice.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-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. @@ -26,7 +26,7 @@ *

Introductions are often mixins, enabling the building of composite * objects that can achieve many of the goals of multiple inheritance in Java. * - *

Compared to {qlink IntroductionInfo}, this interface allows an advice to + *

Compared to {@link IntroductionInfo}, this interface allows an advice to * implement a range of interfaces that is not necessarily known in advance. * Thus an {@link IntroductionAdvisor} can be used to specify which interfaces * will be exposed in an advised object. diff --git a/spring-aop/src/main/java/org/springframework/aop/IntroductionAwareMethodMatcher.java b/spring-aop/src/main/java/org/springframework/aop/IntroductionAwareMethodMatcher.java index 4185c92fa2cc..181237b2a8fc 100644 --- a/spring-aop/src/main/java/org/springframework/aop/IntroductionAwareMethodMatcher.java +++ b/spring-aop/src/main/java/org/springframework/aop/IntroductionAwareMethodMatcher.java @@ -36,7 +36,7 @@ public interface IntroductionAwareMethodMatcher extends MethodMatcher { * @param targetClass the target class * @param hasIntroductions {@code true} if the object on whose behalf we are * asking is the subject on one or more introductions; {@code false} otherwise - * @return whether or not this method matches statically + * @return whether this method matches statically */ boolean matches(Method method, Class targetClass, boolean hasIntroductions); diff --git a/spring-aop/src/main/java/org/springframework/aop/MethodMatcher.java b/spring-aop/src/main/java/org/springframework/aop/MethodMatcher.java index 29127c73d426..d5f8c2a89a56 100644 --- a/spring-aop/src/main/java/org/springframework/aop/MethodMatcher.java +++ b/spring-aop/src/main/java/org/springframework/aop/MethodMatcher.java @@ -60,7 +60,7 @@ public interface MethodMatcher { * will be made. * @param method the candidate method * @param targetClass the target class - * @return whether or not this method matches statically + * @return whether this method matches statically */ boolean matches(Method method, Class targetClass); @@ -70,7 +70,7 @@ public interface MethodMatcher { * runtime even if the 2-arg matches method returns {@code true}? *

Can be invoked when an AOP proxy is created, and need not be invoked * again before each method invocation, - * @return whether or not a runtime match via the 3-arg + * @return whether a runtime match via the 3-arg * {@link #matches(java.lang.reflect.Method, Class, Object[])} method * is required if static matching passed */ diff --git a/spring-aop/src/main/java/org/springframework/aop/SpringProxy.java b/spring-aop/src/main/java/org/springframework/aop/SpringProxy.java index 42e44ceb0282..95057e0d7dba 100644 --- a/spring-aop/src/main/java/org/springframework/aop/SpringProxy.java +++ b/spring-aop/src/main/java/org/springframework/aop/SpringProxy.java @@ -18,7 +18,7 @@ /** * Marker interface implemented by all AOP proxies. Used to detect - * whether or not objects are Spring-generated proxies. + * whether objects are Spring-generated proxies. * * @author Rob Harrop * @since 2.0.1 diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AbstractAspectJAdvice.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AbstractAspectJAdvice.java index 32b691f4410b..1d7c74b811e6 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AbstractAspectJAdvice.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AbstractAspectJAdvice.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -78,10 +78,9 @@ public abstract class AbstractAspectJAdvice implements Advice, AspectJPrecedence */ public static JoinPoint currentJoinPoint() { MethodInvocation mi = ExposeInvocationInterceptor.currentInvocation(); - if (!(mi instanceof ProxyMethodInvocation)) { + if (!(mi instanceof ProxyMethodInvocation pmi)) { throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi); } - ProxyMethodInvocation pmi = (ProxyMethodInvocation) mi; JoinPoint jp = (JoinPoint) pmi.getUserAttribute(JOIN_POINT_KEY); if (jp == null) { jp = new MethodInvocationProceedingJoinPoint(pmi); @@ -262,7 +261,7 @@ public void setArgumentNames(String argNames) { public void setArgumentNamesFromStringArray(String... args) { this.argumentNames = new String[args.length]; for (int i = 0; i < args.length; i++) { - this.argumentNames[i] = StringUtils.trimWhitespace(args[i]); + this.argumentNames[i] = args[i].strip(); if (!isVariableName(this.argumentNames[i])) { throw new IllegalArgumentException( "'argumentNames' property of AbstractAspectJAdvice contains an argument name '" + @@ -350,17 +349,8 @@ protected Class getDiscoveredThrowingType() { return this.discoveredThrowingType; } - private boolean isVariableName(String name) { - char[] chars = name.toCharArray(); - if (!Character.isJavaIdentifierStart(chars[0])) { - return false; - } - for (int i = 1; i < chars.length; i++) { - if (!Character.isJavaIdentifierPart(chars[i])) { - return false; - } - } - return true; + private static boolean isVariableName(String name) { + return AspectJProxyUtils.isVariableName(name); } @@ -377,7 +367,7 @@ private boolean isVariableName(String name) { * to which argument name. There are multiple strategies for determining * this binding, which are arranged in a ChainOfResponsibility. */ - public final synchronized void calculateArgumentBindings() { + public final void calculateArgumentBindings() { // The simple case... nothing to bind. if (this.argumentsIntrospected || this.parameterTypes.length == 0) { return; @@ -640,7 +630,6 @@ protected Object invokeAdviceMethodWithGivenArgs(Object[] args) throws Throwable } try { ReflectionUtils.makeAccessible(this.aspectJAdviceMethod); - // TODO AopUtils.invokeJoinpointUsingReflection return this.aspectJAdviceMethod.invoke(this.aspectInstanceFactory.getAspectInstance(), actualArgs); } catch (IllegalArgumentException ex) { @@ -724,10 +713,9 @@ public boolean equals(@Nullable Object other) { if (this == other) { return true; } - if (!(other instanceof AdviceExcludingMethodMatcher)) { + if (!(other instanceof AdviceExcludingMethodMatcher otherMm)) { return false; } - AdviceExcludingMethodMatcher otherMm = (AdviceExcludingMethodMatcher) other; return this.adviceMethod.equals(otherMm.adviceMethod); } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscoverer.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscoverer.java index ec58afb29652..13d21faab205 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscoverer.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscoverer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -158,7 +158,7 @@ public class AspectJAdviceParameterNameDiscoverer implements ParameterNameDiscov /** The pointcut expression associated with the advice, as a simple String. */ @Nullable - private String pointcutExpression; + private final String pointcutExpression; private boolean raiseExceptions; @@ -470,22 +470,10 @@ else if (numAnnotationSlots == 1) { */ @Nullable private String maybeExtractVariableName(@Nullable String candidateToken) { - if (!StringUtils.hasLength(candidateToken)) { - return null; - } - if (Character.isJavaIdentifierStart(candidateToken.charAt(0)) && - Character.isLowerCase(candidateToken.charAt(0))) { - char[] tokenChars = candidateToken.toCharArray(); - for (char tokenChar : tokenChars) { - if (!Character.isJavaIdentifierPart(tokenChar)) { - return null; - } - } + if (AspectJProxyUtils.isVariableName(candidateToken)) { return candidateToken; } - else { - return null; - } + return null; } /** @@ -498,7 +486,7 @@ private void maybeExtractVariableNamesFromArgs(@Nullable String argsSpec, List= 0 && bodyStart != (currentToken.length() - 1)) { sb.append(currentToken.substring(bodyStart + 1)); - sb.append(" "); + sb.append(' '); } numTokensConsumed++; int currentIndex = startIndex + numTokensConsumed; @@ -657,7 +645,7 @@ private PointcutBody getPointcutBody(String[] tokens, int startIndex) { toAppend = toAppend.substring(1); } sb.append(toAppend); - sb.append(" "); + sb.append(' '); currentIndex++; numTokensConsumed++; } @@ -771,10 +759,10 @@ private void findAndBind(Class argumentType, String varName) { */ private static class PointcutBody { - private int numTokensConsumed; + private final int numTokensConsumed; @Nullable - private String text; + private final String text; public PointcutBody(int tokens, @Nullable String text) { this.numTokensConsumed = tokens; diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAroundAdvice.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAroundAdvice.java index 7a7029323d26..d1584c54af8a 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAroundAdvice.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAroundAdvice.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -63,10 +63,9 @@ protected boolean supportsProceedingJoinPoint() { @Override @Nullable public Object invoke(MethodInvocation mi) throws Throwable { - if (!(mi instanceof ProxyMethodInvocation)) { + if (!(mi instanceof ProxyMethodInvocation pmi)) { throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi); } - ProxyMethodInvocation pmi = (ProxyMethodInvocation) mi; ProceedingJoinPoint pjp = lazyGetProceedingJoinPoint(pmi); JoinPointMatch jpm = getJoinPointMatch(pmi); return invokeAdviceMethod(pjp, jpm, null, null); diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java index 1cdef3141da1..b5c8b87ed84a 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -378,7 +378,7 @@ public boolean matches(Method method, Class targetClass, Object... args) { } catch (Throwable ex) { if (logger.isDebugEnabled()) { - logger.debug("Failed to evaluate join point for arguments " + Arrays.asList(args) + + logger.debug("Failed to evaluate join point for arguments " + Arrays.toString(args) + " - falling back to non-match", ex); } return false; @@ -523,10 +523,9 @@ public boolean equals(@Nullable Object other) { if (this == other) { return true; } - if (!(other instanceof AspectJExpressionPointcut)) { + if (!(other instanceof AspectJExpressionPointcut otherPc)) { return false; } - AspectJExpressionPointcut otherPc = (AspectJExpressionPointcut) other; return ObjectUtils.nullSafeEquals(this.getExpression(), otherPc.getExpression()) && ObjectUtils.nullSafeEquals(this.pointcutDeclarationScope, otherPc.pointcutDeclarationScope) && ObjectUtils.nullSafeEquals(this.pointcutParameterNames, otherPc.pointcutParameterNames) && @@ -547,7 +546,7 @@ public String toString() { StringBuilder sb = new StringBuilder("AspectJExpressionPointcut: ("); for (int i = 0; i < this.pointcutParameterTypes.length; i++) { sb.append(this.pointcutParameterTypes[i].getName()); - sb.append(" "); + sb.append(' '); sb.append(this.pointcutParameterNames[i]); if ((i+1) < this.pointcutParameterTypes.length) { sb.append(", "); diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJPointcutAdvisor.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJPointcutAdvisor.java index b1a12175212b..442df0808f26 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJPointcutAdvisor.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJPointcutAdvisor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -97,10 +97,9 @@ public boolean equals(@Nullable Object other) { if (this == other) { return true; } - if (!(other instanceof AspectJPointcutAdvisor)) { + if (!(other instanceof AspectJPointcutAdvisor otherAdvisor)) { return false; } - AspectJPointcutAdvisor otherAdvisor = (AspectJPointcutAdvisor) other; return this.advice.equals(otherAdvisor.advice); } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJProxyUtils.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJProxyUtils.java index 833a109f131e..e161007abe98 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJProxyUtils.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJProxyUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -21,6 +21,8 @@ import org.springframework.aop.Advisor; import org.springframework.aop.PointcutAdvisor; import org.springframework.aop.interceptor.ExposeInvocationInterceptor; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; /** * Utility methods for working with AspectJ proxies. @@ -73,4 +75,19 @@ private static boolean isAspectJAdvice(Advisor advisor) { ((PointcutAdvisor) advisor).getPointcut() instanceof AspectJExpressionPointcut)); } + static boolean isVariableName(@Nullable String name) { + if (!StringUtils.hasLength(name)) { + return false; + } + if (!Character.isJavaIdentifierStart(name.charAt(0))) { + return false; + } + for (int i = 1; i < name.length(); i++) { + if (!Character.isJavaIdentifierPart(name.charAt(i))) { + return false; + } + } + return true; + } + } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/MethodInvocationProceedingJoinPoint.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/MethodInvocationProceedingJoinPoint.java index d25ec2eb6bc4..ef9016a15728 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/MethodInvocationProceedingJoinPoint.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/MethodInvocationProceedingJoinPoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -255,19 +255,19 @@ private String toString(boolean includeModifier, boolean includeReturnTypeAndArg StringBuilder sb = new StringBuilder(); if (includeModifier) { sb.append(Modifier.toString(getModifiers())); - sb.append(" "); + sb.append(' '); } if (includeReturnTypeAndArgs) { appendType(sb, getReturnType(), useLongReturnAndArgumentTypeName); - sb.append(" "); + sb.append(' '); } appendType(sb, getDeclaringType(), useLongTypeName); - sb.append("."); + sb.append('.'); sb.append(getMethod().getName()); - sb.append("("); + sb.append('('); Class[] parametersTypes = getParameterTypes(); appendTypes(sb, parametersTypes, includeReturnTypeAndArgs, useLongReturnAndArgumentTypeName); - sb.append(")"); + sb.append(')'); return sb.toString(); } @@ -278,7 +278,7 @@ private void appendTypes(StringBuilder sb, Class[] types, boolean includeArgs for (int size = types.length, i = 0; i < size; i++) { appendType(sb, types[i], useLongReturnAndArgumentTypeName); if (i < size - 1) { - sb.append(","); + sb.append(','); } } } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactory.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactory.java index e76156bf8265..54110bcd0d81 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactory.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -103,10 +103,11 @@ private boolean compiledByAjc(Class clazz) { @Override public void validate(Class aspectClass) throws AopConfigException { // If the parent has the annotation and isn't abstract it's an error - if (aspectClass.getSuperclass().getAnnotation(Aspect.class) != null && - !Modifier.isAbstract(aspectClass.getSuperclass().getModifiers())) { + Class superclass = aspectClass.getSuperclass(); + if (superclass.getAnnotation(Aspect.class) != null && + !Modifier.isAbstract(superclass.getModifiers())) { throw new AopConfigException("[" + aspectClass.getName() + "] cannot extend concrete aspect [" + - aspectClass.getSuperclass().getName() + "]"); + superclass.getName() + "]"); } AjType ajType = AjTypeSystem.getAjType(aspectClass); @@ -213,8 +214,7 @@ private AspectJAnnotationType determineAnnotationType(A annotation) { private String resolveExpression(A annotation) { for (String attributeName : EXPRESSION_ATTRIBUTES) { Object val = AnnotationUtils.getValue(annotation, attributeName); - if (val instanceof String) { - String str = (String) val; + if (val instanceof String str) { if (!str.isEmpty()) { return str; } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJAdvisorFactory.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJAdvisorFactory.java index ddba1f0c1f10..c3bf1685297e 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJAdvisorFactory.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJAdvisorFactory.java @@ -39,7 +39,7 @@ public interface AspectJAdvisorFactory { /** - * Determine whether or not the given class is an aspect, as reported + * Determine whether the given class is an aspect, as reported * by AspectJ's {@link org.aspectj.lang.reflect.AjTypeSystem}. *

Will simply return {@code false} if the supposed aspect is * invalid (such as an extension of a concrete aspect class). @@ -47,7 +47,7 @@ public interface AspectJAdvisorFactory { * such as those with unsupported instantiation models. * Use the {@link #validate} method to handle these cases if necessary. * @param clazz the supposed annotation-style AspectJ class - * @return whether or not this class is recognized by AspectJ as an aspect class + * @return whether this class is recognized by AspectJ as an aspect class */ boolean isAspect(Class clazz); diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJProxyFactory.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJProxyFactory.java index ae9e41d6ad96..c3437b5d3dc1 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJProxyFactory.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJProxyFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -34,7 +34,7 @@ /** * AspectJ-based proxy factory, allowing for programmatic building * of proxies which include AspectJ aspects (code style as well - * Java 5 annotation style). + * annotation style). * * @author Rob Harrop * @author Juergen Hoeller diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectMetadata.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectMetadata.java index 048cc603ccc1..969ec866eebb 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectMetadata.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectMetadata.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -60,14 +60,14 @@ public class AspectMetadata implements Serializable { private final Class aspectClass; /** - * AspectJ reflection information (AspectJ 5 / Java 5 specific). - * Re-resolved on deserialization since it isn't serializable itself. + * AspectJ reflection information. + *

Re-resolved on deserialization since it isn't serializable itself. */ private transient AjType ajType; /** * Spring AOP pointcut corresponding to the per clause of the - * aspect. Will be the Pointcut.TRUE canonical instance in the + * aspect. Will be the {@code Pointcut.TRUE} canonical instance in the * case of a singleton, otherwise an AspectJExpressionPointcut. */ private final Pointcut perClausePointcut; @@ -101,24 +101,22 @@ public AspectMetadata(Class aspectClass, String aspectName) { this.ajType = ajType; switch (this.ajType.getPerClause().getKind()) { - case SINGLETON: + case SINGLETON -> { this.perClausePointcut = Pointcut.TRUE; - return; - case PERTARGET: - case PERTHIS: + } + case PERTARGET, PERTHIS -> { AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); ajexp.setLocation(aspectClass.getName()); ajexp.setExpression(findPerClause(aspectClass)); ajexp.setPointcutDeclarationScope(aspectClass); this.perClausePointcut = ajexp; - return; - case PERTYPEWITHIN: + } + case PERTYPEWITHIN -> { // Works with a type pattern this.perClausePointcut = new ComposablePointcut(new TypePatternClassFilter(findPerClause(aspectClass))); - return; - default: - throw new AopConfigException( - "PerClause " + ajType.getPerClause().getKind() + " not supported by Spring AOP for " + aspectClass); + } + default -> throw new AopConfigException( + "PerClause " + ajType.getPerClause().getKind() + " not supported by Spring AOP for " + aspectClass); } } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/InstantiationModelAwarePointcutAdvisorImpl.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/InstantiationModelAwarePointcutAdvisorImpl.java index 7d89175c8c2d..33af3adfc309 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/InstantiationModelAwarePointcutAdvisorImpl.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/InstantiationModelAwarePointcutAdvisorImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -220,21 +220,18 @@ private void determineAdviceType() { } else { switch (aspectJAnnotation.getAnnotationType()) { - case AtPointcut: - case AtAround: + case AtPointcut, AtAround -> { this.isBeforeAdvice = false; this.isAfterAdvice = false; - break; - case AtBefore: + } + case AtBefore -> { this.isBeforeAdvice = true; this.isAfterAdvice = false; - break; - case AtAfter: - case AtAfterReturning: - case AtAfterThrowing: + } + case AtAfter, AtAfterReturning, AtAfterThrowing -> { this.isBeforeAdvice = false; this.isAfterAdvice = true; - break; + } } } } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactory.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactory.java index 5355b2bbb379..00e7ad567ef6 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactory.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -51,6 +51,7 @@ import org.springframework.core.convert.converter.ConvertingComparator; import org.springframework.lang.Nullable; import org.springframework.util.ReflectionUtils; +import org.springframework.util.ReflectionUtils.MethodFilter; import org.springframework.util.StringUtils; import org.springframework.util.comparator.InstanceComparator; @@ -70,7 +71,11 @@ @SuppressWarnings("serial") public class ReflectiveAspectJAdvisorFactory extends AbstractAspectJAdvisorFactory implements Serializable { - private static final Comparator METHOD_COMPARATOR; + // Exclude @Pointcut methods + private static final MethodFilter adviceMethodFilter = ReflectionUtils.USER_DECLARED_METHODS + .and(method -> (AnnotationUtils.getAnnotation(method, Pointcut.class) == null)); + + private static final Comparator adviceMethodComparator; static { // Note: although @After is ordered before @AfterReturning and @AfterThrowing, @@ -86,7 +91,7 @@ public class ReflectiveAspectJAdvisorFactory extends AbstractAspectJAdvisorFacto return (ann != null ? ann.getAnnotation() : null); }); Comparator methodNameComparator = new ConvertingComparator<>(Method::getName); - METHOD_COMPARATOR = adviceKindComparator.thenComparing(methodNameComparator); + adviceMethodComparator = adviceKindComparator.thenComparing(methodNameComparator); } @@ -160,15 +165,10 @@ public List getAdvisors(MetadataAwareAspectInstanceFactory aspectInstan } private List getAdvisorMethods(Class aspectClass) { - final List methods = new ArrayList<>(); - ReflectionUtils.doWithMethods(aspectClass, method -> { - // Exclude pointcuts - if (AnnotationUtils.getAnnotation(method, Pointcut.class) == null) { - methods.add(method); - } - }, ReflectionUtils.USER_DECLARED_METHODS); + List methods = new ArrayList<>(); + ReflectionUtils.doWithMethods(aspectClass, methods::add, adviceMethodFilter); if (methods.size() > 1) { - methods.sort(METHOD_COMPARATOR); + methods.sort(adviceMethodComparator); } return methods; } @@ -261,42 +261,36 @@ public Advice getAdvice(Method candidateAdviceMethod, AspectJExpressionPointcut AbstractAspectJAdvice springAdvice; switch (aspectJAnnotation.getAnnotationType()) { - case AtPointcut: + case AtPointcut -> { if (logger.isDebugEnabled()) { logger.debug("Processing pointcut '" + candidateAdviceMethod.getName() + "'"); } return null; - case AtAround: - springAdvice = new AspectJAroundAdvice( - candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); - break; - case AtBefore: - springAdvice = new AspectJMethodBeforeAdvice( - candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); - break; - case AtAfter: - springAdvice = new AspectJAfterAdvice( - candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); - break; - case AtAfterReturning: + } + case AtAround -> springAdvice = new AspectJAroundAdvice( + candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); + case AtBefore -> springAdvice = new AspectJMethodBeforeAdvice( + candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); + case AtAfter -> springAdvice = new AspectJAfterAdvice( + candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); + case AtAfterReturning -> { springAdvice = new AspectJAfterReturningAdvice( candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); AfterReturning afterReturningAnnotation = (AfterReturning) aspectJAnnotation.getAnnotation(); if (StringUtils.hasText(afterReturningAnnotation.returning())) { springAdvice.setReturningName(afterReturningAnnotation.returning()); } - break; - case AtAfterThrowing: + } + case AtAfterThrowing -> { springAdvice = new AspectJAfterThrowingAdvice( candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); AfterThrowing afterThrowingAnnotation = (AfterThrowing) aspectJAnnotation.getAnnotation(); if (StringUtils.hasText(afterThrowingAnnotation.throwing())) { springAdvice.setThrowingName(afterThrowingAnnotation.throwing()); } - break; - default: - throw new UnsupportedOperationException( - "Unsupported advice type on method: " + candidateAdviceMethod); + } + default -> throw new UnsupportedOperationException( + "Unsupported advice type on method: " + candidateAdviceMethod); } // Now to configure the advice... diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/autoproxy/AspectJAwareAdvisorAutoProxyCreator.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/autoproxy/AspectJAwareAdvisorAutoProxyCreator.java index 2d2aabd07fae..3c56c8722632 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/autoproxy/AspectJAwareAdvisorAutoProxyCreator.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/autoproxy/AspectJAwareAdvisorAutoProxyCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -60,8 +60,8 @@ public class AspectJAwareAdvisorAutoProxyCreator extends AbstractAdvisorAutoProx *

  • Otherwise the advice declared first gets highest precedence (i.e., runs * first).
  • * - *

    Important: Advisors are sorted in precedence order, from highest - * precedence to lowest. "On the way in" to a join point, the highest precedence + *

    Important: Advisors are sorted in precedence order, from the highest + * precedence to the lowest. "On the way in" to a join point, the highest precedence * advisor should run first. "On the way out" of a join point, the highest * precedence advisor should run last. */ @@ -100,8 +100,8 @@ protected boolean shouldSkip(Class beanClass, String beanName) { // TODO: Consider optimization by caching the list of the aspect names List candidateAdvisors = findCandidateAdvisors(); for (Advisor advisor : candidateAdvisors) { - if (advisor instanceof AspectJPointcutAdvisor && - ((AspectJPointcutAdvisor) advisor).getAspectName().equals(beanName)) { + if (advisor instanceof AspectJPointcutAdvisor pointcutAdvisor && + pointcutAdvisor.getAspectName().equals(beanName)) { return true; } } @@ -143,13 +143,12 @@ public String toString() { Advice advice = this.advisor.getAdvice(); StringBuilder sb = new StringBuilder(ClassUtils.getShortName(advice.getClass())); boolean appended = false; - if (this.advisor instanceof Ordered) { - sb.append(": order = ").append(((Ordered) this.advisor).getOrder()); + if (this.advisor instanceof Ordered ordered) { + sb.append(": order = ").append(ordered.getOrder()); appended = true; } - if (advice instanceof AbstractAspectJAdvice) { + if (advice instanceof AbstractAspectJAdvice ajAdvice) { sb.append(!appended ? ": " : ", "); - AbstractAspectJAdvice ajAdvice = (AbstractAspectJAdvice) advice; sb.append("aspect name = "); sb.append(ajAdvice.getAspectName()); sb.append(", declaration order = "); diff --git a/spring-aop/src/main/java/org/springframework/aop/config/AspectJAutoProxyBeanDefinitionParser.java b/spring-aop/src/main/java/org/springframework/aop/config/AspectJAutoProxyBeanDefinitionParser.java index 4271bec65d5e..70b9762006b0 100644 --- a/spring-aop/src/main/java/org/springframework/aop/config/AspectJAutoProxyBeanDefinitionParser.java +++ b/spring-aop/src/main/java/org/springframework/aop/config/AspectJAutoProxyBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-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. @@ -59,8 +59,7 @@ private void addIncludePatterns(Element element, ParserContext parserContext, Be NodeList childNodes = element.getChildNodes(); for (int i = 0; i < childNodes.getLength(); i++) { Node node = childNodes.item(i); - if (node instanceof Element) { - Element includeElement = (Element) node; + if (node instanceof Element includeElement) { TypedStringValue valueHolder = new TypedStringValue(includeElement.getAttribute("name")); valueHolder.setSource(parserContext.extractSource(includeElement)); includePatterns.add(valueHolder); diff --git a/spring-aop/src/main/java/org/springframework/aop/config/ConfigBeanDefinitionParser.java b/spring-aop/src/main/java/org/springframework/aop/config/ConfigBeanDefinitionParser.java index de31818c70a6..fff18c0a4e42 100644 --- a/spring-aop/src/main/java/org/springframework/aop/config/ConfigBeanDefinitionParser.java +++ b/spring-aop/src/main/java/org/springframework/aop/config/ConfigBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -93,7 +93,7 @@ class ConfigBeanDefinitionParser implements BeanDefinitionParser { private static final int POINTCUT_INDEX = 1; private static final int ASPECT_INSTANCE_FACTORY_INDEX = 2; - private ParseState parseState = new ParseState(); + private final ParseState parseState = new ParseState(); @Override diff --git a/spring-aop/src/main/java/org/springframework/aop/config/ScopedProxyBeanDefinitionDecorator.java b/spring-aop/src/main/java/org/springframework/aop/config/ScopedProxyBeanDefinitionDecorator.java index ca940ec518f1..e116ec85947a 100644 --- a/spring-aop/src/main/java/org/springframework/aop/config/ScopedProxyBeanDefinitionDecorator.java +++ b/spring-aop/src/main/java/org/springframework/aop/config/ScopedProxyBeanDefinitionDecorator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-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. @@ -42,8 +42,7 @@ class ScopedProxyBeanDefinitionDecorator implements BeanDefinitionDecorator { @Override public BeanDefinitionHolder decorate(Node node, BeanDefinitionHolder definition, ParserContext parserContext) { boolean proxyTargetClass = true; - if (node instanceof Element) { - Element ele = (Element) node; + if (node instanceof Element ele) { if (ele.hasAttribute(PROXY_TARGET_CLASS)) { proxyTargetClass = Boolean.parseBoolean(ele.getAttribute(PROXY_TARGET_CLASS)); } diff --git a/spring-aop/src/main/java/org/springframework/aop/config/SimpleBeanFactoryAwareAspectInstanceFactory.java b/spring-aop/src/main/java/org/springframework/aop/config/SimpleBeanFactoryAwareAspectInstanceFactory.java index 3d89993caddd..34a180a2c845 100644 --- a/spring-aop/src/main/java/org/springframework/aop/config/SimpleBeanFactoryAwareAspectInstanceFactory.java +++ b/spring-aop/src/main/java/org/springframework/aop/config/SimpleBeanFactoryAwareAspectInstanceFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-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. @@ -58,7 +58,7 @@ public void setBeanFactory(BeanFactory beanFactory) { /** - * Look up the aspect bean from the {@link BeanFactory} and returns it. + * Look up the aspect bean from the {@link BeanFactory} and return it. * @see #setAspectBeanName */ @Override diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/AbstractAdvisingBeanPostProcessor.java b/spring-aop/src/main/java/org/springframework/aop/framework/AbstractAdvisingBeanPostProcessor.java index a3a87f2117f8..1c333af6944b 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/AbstractAdvisingBeanPostProcessor.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/AbstractAdvisingBeanPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -22,6 +22,8 @@ import org.springframework.aop.Advisor; import org.springframework.aop.support.AopUtils; import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor; +import org.springframework.core.SmartClassLoader; import org.springframework.lang.Nullable; /** @@ -32,7 +34,8 @@ * @since 3.2 */ @SuppressWarnings("serial") -public abstract class AbstractAdvisingBeanPostProcessor extends ProxyProcessorSupport implements BeanPostProcessor { +public abstract class AbstractAdvisingBeanPostProcessor extends ProxyProcessorSupport + implements SmartInstantiationAwareBeanPostProcessor { @Nullable protected Advisor advisor; @@ -57,8 +60,27 @@ public void setBeforeExistingAdvisors(boolean beforeExistingAdvisors) { @Override - public Object postProcessBeforeInitialization(Object bean, String beanName) { - return bean; + public Class determineBeanType(Class beanClass, String beanName) { + if (this.advisor != null && isEligible(beanClass)) { + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.copyFrom(this); + proxyFactory.setTargetClass(beanClass); + + if (!proxyFactory.isProxyTargetClass()) { + evaluateProxyInterfaces(beanClass, proxyFactory); + } + proxyFactory.addAdvisor(this.advisor); + customizeProxyFactory(proxyFactory); + + // Use original ClassLoader if bean class not locally loaded in overriding class loader + ClassLoader classLoader = getProxyClassLoader(); + if (classLoader instanceof SmartClassLoader && classLoader != beanClass.getClassLoader()) { + classLoader = ((SmartClassLoader) classLoader).getOriginalClassLoader(); + } + return proxyFactory.getProxyClass(classLoader); + } + + return beanClass; } @Override @@ -68,8 +90,7 @@ public Object postProcessAfterInitialization(Object bean, String beanName) { return bean; } - if (bean instanceof Advised) { - Advised advised = (Advised) bean; + if (bean instanceof Advised advised) { if (!advised.isFrozen() && isEligible(AopUtils.getTargetClass(bean))) { // Add our local Advisor to the existing proxy's Advisor chain... if (this.beforeExistingAdvisors) { @@ -89,7 +110,13 @@ public Object postProcessAfterInitialization(Object bean, String beanName) { } proxyFactory.addAdvisor(this.advisor); customizeProxyFactory(proxyFactory); - return proxyFactory.getProxy(getProxyClassLoader()); + + // Use original ClassLoader if bean class not locally loaded in overriding class loader + ClassLoader classLoader = getProxyClassLoader(); + if (classLoader instanceof SmartClassLoader && classLoader != bean.getClass().getClassLoader()) { + classLoader = ((SmartClassLoader) classLoader).getOriginalClassLoader(); + } + return proxyFactory.getProxy(classLoader); } // No proxy needed. diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/AdvisedSupport.java b/spring-aop/src/main/java/org/springframework/aop/framework/AdvisedSupport.java index 5664cf7ffd55..b6e192e7d352 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/AdvisedSupport.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/AdvisedSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -72,12 +72,14 @@ public class AdvisedSupport extends ProxyConfig implements Advised { /** Package-protected to allow direct access for efficiency. */ + @SuppressWarnings("serial") TargetSource targetSource = EMPTY_TARGET_SOURCE; /** Whether the Advisors are already filtered for the specific target class. */ private boolean preFiltered = false; /** The AdvisorChainFactory to use. */ + @SuppressWarnings("serial") AdvisorChainFactory advisorChainFactory = new DefaultAdvisorChainFactory(); /** Cache with Method as key and advisor chain List as value. */ @@ -87,12 +89,14 @@ public class AdvisedSupport extends ProxyConfig implements Advised { * Interfaces to be implemented by the proxy. Held in List to keep the order * of registration, to create JDK proxy with specified order of interfaces. */ + @SuppressWarnings("serial") private List> interfaces = new ArrayList<>(); /** * List of Advisors. If an Advice is added, it will be wrapped * in an Advisor before being added to this List. */ + @SuppressWarnings("serial") private List advisors = new ArrayList<>(); @@ -283,8 +287,7 @@ public void removeAdvisor(int index) throws AopConfigException { } Advisor advisor = this.advisors.remove(index); - if (advisor instanceof IntroductionAdvisor) { - IntroductionAdvisor ia = (IntroductionAdvisor) advisor; + if (advisor instanceof IntroductionAdvisor ia) { // We need to remove introduction interfaces. for (Class ifc : ia.getInterfaces()) { removeInterface(ifc); @@ -314,7 +317,7 @@ public boolean replaceAdvisor(Advisor a, Advisor b) throws AopConfigException { } /** - * Add all of the given advisors to this proxy configuration. + * Add all the given advisors to this proxy configuration. * @param advisors the advisors to register */ public void addAdvisors(Advisor... advisors) { @@ -322,7 +325,7 @@ public void addAdvisors(Advisor... advisors) { } /** - * Add all of the given advisors to this proxy configuration. + * Add all the given advisors to this proxy configuration. * @param advisors the advisors to register */ public void addAdvisors(Collection advisors) { @@ -331,8 +334,8 @@ public void addAdvisors(Collection advisors) { } if (!CollectionUtils.isEmpty(advisors)) { for (Advisor advisor : advisors) { - if (advisor instanceof IntroductionAdvisor) { - validateIntroductionAdvisor((IntroductionAdvisor) advisor); + if (advisor instanceof IntroductionAdvisor introductionAdvisor) { + validateIntroductionAdvisor(introductionAdvisor); } Assert.notNull(advisor, "Advisor must not be null"); this.advisors.add(advisor); @@ -521,8 +524,8 @@ AdvisedSupport getConfigurationOnlyCopy() { copy.copyFrom(this); copy.targetSource = EmptyTargetSource.forClass(getTargetClass(), getTargetSource().isStatic()); copy.advisorChainFactory = this.advisorChainFactory; - copy.interfaces = this.interfaces; - copy.advisors = this.advisors; + copy.interfaces = new ArrayList<>(this.interfaces); + copy.advisors = new ArrayList<>(this.advisors); return copy; } diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/AopProxy.java b/spring-aop/src/main/java/org/springframework/aop/framework/AopProxy.java index cc7a3fd099d7..f103477504a1 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/AopProxy.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/AopProxy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-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. @@ -52,4 +52,13 @@ public interface AopProxy { */ Object getProxy(@Nullable ClassLoader classLoader); + /** + * Determine the proxy class. + * @param classLoader the class loader to create the proxy class with + * (or {@code null} for the low-level proxy facility's default) + * @return the proxy class + * @since 6.0 + */ + Class getProxyClass(@Nullable ClassLoader classLoader); + } diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/AopProxyUtils.java b/spring-aop/src/main/java/org/springframework/aop/framework/AopProxyUtils.java index 5dd18747ef3a..d133c5d9a7b6 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/AopProxyUtils.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/AopProxyUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -19,7 +19,9 @@ import java.lang.reflect.Array; import java.lang.reflect.Method; import java.lang.reflect.Proxy; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import org.springframework.aop.SpringProxy; import org.springframework.aop.TargetClassAware; @@ -29,17 +31,20 @@ import org.springframework.core.DecoratingProxy; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; /** * Utility methods for AOP proxy factories. - * Mainly for internal use within the AOP framework. + * + *

    Mainly for internal use within the AOP framework. * *

    See {@link org.springframework.aop.support.AopUtils} for a collection of * generic AOP utility methods which do not depend on AOP framework internals. * * @author Rod Johnson * @author Juergen Hoeller + * @author Sam Brannen * @see org.springframework.aop.support.AopUtils */ public abstract class AopProxyUtils { @@ -55,10 +60,10 @@ public abstract class AopProxyUtils { */ @Nullable public static Object getSingletonTarget(Object candidate) { - if (candidate instanceof Advised) { - TargetSource targetSource = ((Advised) candidate).getTargetSource(); - if (targetSource instanceof SingletonTargetSource) { - return ((SingletonTargetSource) targetSource).getTarget(); + if (candidate instanceof Advised advised) { + TargetSource targetSource = advised.getTargetSource(); + if (targetSource instanceof SingletonTargetSource singleTargetSource) { + return singleTargetSource.getTarget(); } } return null; @@ -78,8 +83,8 @@ public static Class ultimateTargetClass(Object candidate) { Assert.notNull(candidate, "Candidate object must not be null"); Object current = candidate; Class result = null; - while (current instanceof TargetClassAware) { - result = ((TargetClassAware) current).getTargetClass(); + while (current instanceof TargetClassAware targetClassAware) { + result = targetClassAware.getTargetClass(); current = getSingletonTarget(current); } if (result == null) { @@ -88,6 +93,46 @@ public static Class ultimateTargetClass(Object candidate) { return result; } + /** + * Complete the set of interfaces that are typically required in a JDK dynamic + * proxy generated by Spring AOP. + *

    Specifically, {@link SpringProxy}, {@link Advised}, and {@link DecoratingProxy} + * will be appended to the set of user-specified interfaces. + *

    This method can be useful when registering + * {@linkplain org.springframework.aot.hint.ProxyHints proxy hints} for Spring's + * AOT support, as demonstrated in the following example which uses this method + * via a {@code static} import. + *

    +	 * RuntimeHints hints = ...
    +	 * hints.proxies().registerJdkProxy(completeJdkProxyInterfaces(MyInterface.class));
    +	 * 
    + * @param userInterfaces the set of user-specified interfaces implemented by + * the component to be proxied + * @return the complete set of interfaces that the proxy should implement + * @throws IllegalArgumentException if a supplied {@code Class} is {@code null}, + * is not an {@linkplain Class#isInterface() interface}, or is a + * {@linkplain Class#isSealed() sealed} interface + * @since 6.0 + * @see SpringProxy + * @see Advised + * @see DecoratingProxy + * @see org.springframework.aot.hint.RuntimeHints#proxies() + * @see org.springframework.aot.hint.ProxyHints#registerJdkProxy(Class...) + */ + public static Class[] completeJdkProxyInterfaces(Class... userInterfaces) { + List> completedInterfaces = new ArrayList<>(userInterfaces.length + 3); + for (Class ifc : userInterfaces) { + Assert.notNull(ifc, "'userInterfaces' must not contain null values"); + Assert.isTrue(ifc.isInterface() && !ifc.isSealed(), + () -> ifc.getName() + " must be a non-sealed interface"); + completedInterfaces.add(ifc); + } + completedInterfaces.add(SpringProxy.class); + completedInterfaces.add(Advised.class); + completedInterfaces.add(DecoratingProxy.class); + return completedInterfaces.toArray(Class[]::new); + } + /** * Determine the complete set of interfaces to proxy for the given AOP configuration. *

    This will always add the {@link Advised} interface unless the AdvisedSupport's @@ -124,40 +169,29 @@ static Class[] completeProxiedInterfaces(AdvisedSupport advised, boolean deco if (targetClass.isInterface()) { advised.setInterfaces(targetClass); } - else if (Proxy.isProxyClass(targetClass)) { + else if (Proxy.isProxyClass(targetClass) || ClassUtils.isLambdaClass(targetClass)) { advised.setInterfaces(targetClass.getInterfaces()); } specifiedInterfaces = advised.getProxiedInterfaces(); } } - boolean addSpringProxy = !advised.isInterfaceProxied(SpringProxy.class); - boolean addAdvised = !advised.isOpaque() && !advised.isInterfaceProxied(Advised.class); - boolean addDecoratingProxy = (decoratingProxy && !advised.isInterfaceProxied(DecoratingProxy.class)); - int nonUserIfcCount = 0; - if (addSpringProxy) { - nonUserIfcCount++; - } - if (addAdvised) { - nonUserIfcCount++; - } - if (addDecoratingProxy) { - nonUserIfcCount++; + List> proxiedInterfaces = new ArrayList<>(specifiedInterfaces.length + 3); + for (Class ifc : specifiedInterfaces) { + // Only non-sealed interfaces are actually eligible for JDK proxying (on JDK 17) + if (!ifc.isSealed()) { + proxiedInterfaces.add(ifc); + } } - Class[] proxiedInterfaces = new Class[specifiedInterfaces.length + nonUserIfcCount]; - System.arraycopy(specifiedInterfaces, 0, proxiedInterfaces, 0, specifiedInterfaces.length); - int index = specifiedInterfaces.length; - if (addSpringProxy) { - proxiedInterfaces[index] = SpringProxy.class; - index++; + if (!advised.isInterfaceProxied(SpringProxy.class)) { + proxiedInterfaces.add(SpringProxy.class); } - if (addAdvised) { - proxiedInterfaces[index] = Advised.class; - index++; + if (!advised.isOpaque() && !advised.isInterfaceProxied(Advised.class)) { + proxiedInterfaces.add(Advised.class); } - if (addDecoratingProxy) { - proxiedInterfaces[index] = DecoratingProxy.class; + if (decoratingProxy && !advised.isInterfaceProxied(DecoratingProxy.class)) { + proxiedInterfaces.add(DecoratingProxy.class); } - return proxiedInterfaces; + return ClassUtils.toClassArray(proxiedInterfaces); } /** diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java b/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java index 69fe9a048773..4beff09bc343 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -48,6 +48,7 @@ import org.springframework.cglib.proxy.MethodInterceptor; import org.springframework.cglib.proxy.MethodProxy; import org.springframework.cglib.proxy.NoOp; +import org.springframework.core.KotlinDetector; import org.springframework.core.SmartClassLoader; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -125,9 +126,6 @@ class CglibAopProxy implements AopProxy, Serializable { */ public CglibAopProxy(AdvisedSupport config) throws AopConfigException { Assert.notNull(config, "AdvisedSupport must not be null"); - if (config.getAdvisorCount() == 0 && config.getTargetSource() == AdvisedSupport.EMPTY_TARGET_SOURCE) { - throw new AopConfigException("No advisors and no TargetSource specified"); - } this.advised = config; this.advisedDispatcher = new AdvisedDispatcher(this.advised); } @@ -152,11 +150,20 @@ public void setConstructorArguments(@Nullable Object[] constructorArgs, @Nullabl @Override public Object getProxy() { - return getProxy(null); + return buildProxy(null, false); } @Override public Object getProxy(@Nullable ClassLoader classLoader) { + return buildProxy(classLoader, false); + } + + @Override + public Class getProxyClass(@Nullable ClassLoader classLoader) { + return (Class) buildProxy(classLoader, true); + } + + private Object buildProxy(@Nullable ClassLoader classLoader, boolean classOnly) { if (logger.isTraceEnabled()) { logger.trace("Creating CGLIB proxy: " + this.advised.getTargetSource()); } @@ -189,6 +196,7 @@ public Object getProxy(@Nullable ClassLoader classLoader) { enhancer.setSuperclass(proxySuperClass); enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised)); enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE); + enhancer.setAttemptLoad(true); enhancer.setStrategy(new ClassLoaderAwareGeneratorStrategy(classLoader)); Callback[] callbacks = getCallbacks(rootClass); @@ -202,7 +210,7 @@ public Object getProxy(@Nullable ClassLoader classLoader) { enhancer.setCallbackTypes(types); // Generate the proxy class and create a proxy instance. - return createProxyClassAndInstance(enhancer, callbacks); + return (classOnly ? createProxyClass(enhancer) : createProxyClassAndInstance(enhancer, callbacks)); } catch (CodeGenerationException | IllegalArgumentException ex) { throw new AopConfigException("Could not generate CGLIB subclass of " + this.advised.getTargetClass() + @@ -215,6 +223,11 @@ public Object getProxy(@Nullable ClassLoader classLoader) { } } + protected Class createProxyClass(Enhancer enhancer) { + enhancer.setInterceptDuringConstruction(false); + return enhancer.createClass(); + } + protected Object createProxyClassAndInstance(Enhancer enhancer, Callback[] callbacks) { enhancer.setInterceptDuringConstruction(false); enhancer.setCallbacks(callbacks); @@ -236,7 +249,7 @@ protected Enhancer createEnhancer() { * validates it if not. */ private void validateClassIfNecessary(Class proxySuperClass, @Nullable ClassLoader proxyClassLoader) { - if (logger.isWarnEnabled()) { + if (!this.advised.isOptimize() && logger.isInfoEnabled()) { synchronized (validatedClasses) { if (!validatedClasses.containsKey(proxySuperClass)) { doValidateClass(proxySuperClass, proxyClassLoader, @@ -407,10 +420,9 @@ public static class SerializableNoOp implements NoOp, Serializable { /** - * Method interceptor used for static targets with no advice chain. The call - * is passed directly back to the target. Used when the proxy needs to be - * exposed and it can't be determined that the method won't return - * {@code this}. + * Method interceptor used for static targets with no advice chain. The call is + * passed directly back to the target. Used when the proxy needs to be exposed + * and it can't be determined that the method won't return {@code this}. */ private static class StaticUnadvisedInterceptor implements MethodInterceptor, Serializable { @@ -424,7 +436,7 @@ public StaticUnadvisedInterceptor(@Nullable Object target) { @Override @Nullable public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { - Object retVal = methodProxy.invoke(this.target, args); + Object retVal = AopUtils.invokeJoinpointUsingReflection(this.target, method, args); return processReturnType(proxy, this.target, method, retVal); } } @@ -449,7 +461,7 @@ public Object intercept(Object proxy, Method method, Object[] args, MethodProxy Object oldProxy = null; try { oldProxy = AopContext.setCurrentProxy(proxy); - Object retVal = methodProxy.invoke(this.target, args); + Object retVal = AopUtils.invokeJoinpointUsingReflection(this.target, method, args); return processReturnType(proxy, this.target, method, retVal); } finally { @@ -477,7 +489,7 @@ public DynamicUnadvisedInterceptor(TargetSource targetSource) { public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { Object target = this.targetSource.getTarget(); try { - Object retVal = methodProxy.invoke(target, args); + Object retVal = AopUtils.invokeJoinpointUsingReflection(target, method, args); return processReturnType(proxy, target, method, retVal); } finally { @@ -507,7 +519,7 @@ public Object intercept(Object proxy, Method method, Object[] args, MethodProxy Object target = this.targetSource.getTarget(); try { oldProxy = AopContext.setCurrentProxy(proxy); - Object retVal = methodProxy.invoke(target, args); + Object retVal = AopUtils.invokeJoinpointUsingReflection(target, method, args); return processReturnType(proxy, target, method, retVal); } finally { @@ -678,13 +690,13 @@ public Object intercept(Object proxy, Method method, Object[] args, MethodProxy Object retVal; // Check whether we only have one InvokerInterceptor: that is, // no real advice, but just reflective invocation of the target. - if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) { + if (chain.isEmpty()) { // We can skip creating a MethodInvocation: just invoke the target directly. // Note that the final invoker must be an InvokerInterceptor, so we know // it does nothing but a reflective operation on the target, and no hot // swapping or fancy proxying. Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args); - retVal = methodProxy.invoke(target, argsToUse); + retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse); } else { // We need to create a method invocation... @@ -726,20 +738,11 @@ public int hashCode() { */ private static class CglibMethodInvocation extends ReflectiveMethodInvocation { - @Nullable - private final MethodProxy methodProxy; - public CglibMethodInvocation(Object proxy, @Nullable Object target, Method method, Object[] arguments, @Nullable Class targetClass, List interceptorsAndDynamicMethodMatchers, MethodProxy methodProxy) { super(proxy, target, method, arguments, targetClass, interceptorsAndDynamicMethodMatchers); - - // Only use method proxy for public methods not derived from java.lang.Object - this.methodProxy = (Modifier.isPublic(method.getModifiers()) && - method.getDeclaringClass() != Object.class && !AopUtils.isEqualsMethod(method) && - !AopUtils.isHashCodeMethod(method) && !AopUtils.isToStringMethod(method) ? - methodProxy : null); } @Override @@ -752,28 +755,21 @@ public Object proceed() throws Throwable { throw ex; } catch (Exception ex) { - if (ReflectionUtils.declaresException(getMethod(), ex.getClass())) { + if (ReflectionUtils.declaresException(getMethod(), ex.getClass()) || + KotlinDetector.isKotlinType(getMethod().getDeclaringClass())) { + // Propagate original exception if declared on the target method + // (with callers expecting it). Always propagate it for Kotlin code + // since checked exceptions do not have to be explicitly declared there. throw ex; } else { + // Checked exception thrown in the interceptor but not declared on the + // target method signature -> apply an UndeclaredThrowableException, + // aligned with standard JDK dynamic proxy behavior. throw new UndeclaredThrowableException(ex); } } } - - /** - * Gives a marginal performance improvement versus using reflection to - * invoke the target when invoking public methods. - */ - @Override - protected Object invokeJoinpoint() throws Throwable { - if (this.methodProxy != null) { - return this.methodProxy.invoke(this.target, this.arguments); - } - else { - return super.invokeJoinpoint(); - } - } } @@ -923,10 +919,9 @@ public boolean equals(@Nullable Object other) { if (this == other) { return true; } - if (!(other instanceof ProxyCallbackFilter)) { + if (!(other instanceof ProxyCallbackFilter otherCallbackFilter)) { return false; } - ProxyCallbackFilter otherCallbackFilter = (ProxyCallbackFilter) other; AdvisedSupport otherAdvised = otherCallbackFilter.advised; if (this.advised.isFrozen() != otherAdvised.isFrozen()) { return false; diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/DefaultAdvisorChainFactory.java b/spring-aop/src/main/java/org/springframework/aop/framework/DefaultAdvisorChainFactory.java index a8f458781d65..67e435a88357 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/DefaultAdvisorChainFactory.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/DefaultAdvisorChainFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -60,9 +60,8 @@ public List getInterceptorsAndDynamicInterceptionAdvice( Boolean hasIntroductions = null; for (Advisor advisor : advisors) { - if (advisor instanceof PointcutAdvisor) { + if (advisor instanceof PointcutAdvisor pointcutAdvisor) { // Add it conditionally. - PointcutAdvisor pointcutAdvisor = (PointcutAdvisor) advisor; if (config.isPreFiltered() || pointcutAdvisor.getPointcut().getClassFilter().matches(actualClass)) { MethodMatcher mm = pointcutAdvisor.getPointcut().getMethodMatcher(); boolean match; @@ -90,8 +89,7 @@ public List getInterceptorsAndDynamicInterceptionAdvice( } } } - else if (advisor instanceof IntroductionAdvisor) { - IntroductionAdvisor ia = (IntroductionAdvisor) advisor; + else if (advisor instanceof IntroductionAdvisor ia) { if (config.isPreFiltered() || ia.getClassFilter().matches(actualClass)) { Interceptor[] interceptors = registry.getInterceptors(advisor); interceptorList.addAll(Arrays.asList(interceptors)); @@ -111,8 +109,7 @@ else if (advisor instanceof IntroductionAdvisor) { */ private static boolean hasMatchingIntroductions(Advisor[] advisors, Class actualClass) { for (Advisor advisor : advisors) { - if (advisor instanceof IntroductionAdvisor) { - IntroductionAdvisor ia = (IntroductionAdvisor) advisor; + if (advisor instanceof IntroductionAdvisor ia) { if (ia.getClassFilter().matches(actualClass)) { return true; } diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/DefaultAopProxyFactory.java b/spring-aop/src/main/java/org/springframework/aop/framework/DefaultAopProxyFactory.java index d84df2f4c9e8..5710f8e03bc3 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/DefaultAopProxyFactory.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/DefaultAopProxyFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-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. @@ -20,6 +20,7 @@ import java.lang.reflect.Proxy; import org.springframework.aop.SpringProxy; +import org.springframework.util.ClassUtils; /** * Default {@link AopProxyFactory} implementation, creating either a CGLIB proxy @@ -39,32 +40,26 @@ * @author Rod Johnson * @author Juergen Hoeller * @author Sebastien Deleuze + * @author Sam Brannen * @since 12.03.2004 * @see AdvisedSupport#setOptimize * @see AdvisedSupport#setProxyTargetClass * @see AdvisedSupport#setInterfaces */ -@SuppressWarnings("serial") public class DefaultAopProxyFactory implements AopProxyFactory, Serializable { - /** - * Whether this environment lives within a native image. - * Exposed as a private static field rather than in a {@code NativeImageDetector.inNativeImage()} static method due to https://github.com/oracle/graal/issues/2594. - * @see ImageInfo.java - */ - private static final boolean IN_NATIVE_IMAGE = (System.getProperty("org.graalvm.nativeimage.imagecode") != null); + private static final long serialVersionUID = 7930414337282325166L; @Override public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException { - if (!IN_NATIVE_IMAGE && - (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config))) { + if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) { Class targetClass = config.getTargetClass(); if (targetClass == null) { throw new AopConfigException("TargetSource cannot determine target class: " + "Either an interface or a target is required for proxy creation."); } - if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) { + if (targetClass.isInterface() || Proxy.isProxyClass(targetClass) || ClassUtils.isLambdaClass(targetClass)) { return new JdkDynamicAopProxy(config); } return new ObjenesisCglibAopProxy(config); diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/JdkDynamicAopProxy.java b/spring-aop/src/main/java/org/springframework/aop/framework/JdkDynamicAopProxy.java index fcfaf93dd832..81b48229ef39 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/JdkDynamicAopProxy.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/JdkDynamicAopProxy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -72,8 +72,8 @@ final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializa * NOTE: We could avoid the code duplication between this class and the CGLIB * proxies by refactoring "invoke" into a template method. However, this approach * adds at least 10% performance overhead versus a copy-paste solution, so we sacrifice - * elegance for performance. (We have a good test suite to ensure that the different - * proxies behave the same :-) + * elegance for performance (we have a good test suite to ensure that the different + * proxies behave the same :-)). * This way, we can also more easily take advantage of minor optimizations in each class. */ @@ -104,9 +104,6 @@ final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializa */ public JdkDynamicAopProxy(AdvisedSupport config) throws AopConfigException { Assert.notNull(config, "AdvisedSupport must not be null"); - if (config.getAdvisorCount() == 0 && config.getTargetSource() == AdvisedSupport.EMPTY_TARGET_SOURCE) { - throw new AopConfigException("No advisors and no TargetSource specified"); - } this.advised = config; this.proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true); findDefinedEqualsAndHashCodeMethods(this.proxiedInterfaces); @@ -126,6 +123,12 @@ public Object getProxy(@Nullable ClassLoader classLoader) { return Proxy.newProxyInstance(classLoader, this.proxiedInterfaces, this); } + @SuppressWarnings("deprecation") + @Override + public Class getProxyClass(@Nullable ClassLoader classLoader) { + return Proxy.getProxyClass(classLoader, this.proxiedInterfaces); + } + /** * Finds any {@link #equals} or {@link #hashCode} method that may be defined * on the supplied set of interfaces. @@ -198,7 +201,7 @@ else if (!this.advised.opaque && method.getDeclaringClass().isInterface() && // Get the interception chain for this method. List chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); - // Check whether we have any advice. If we don't, we can fallback on direct + // Check whether we have any advice. If we don't, we can fall back on direct // reflective invocation of the target, and avoid creating a MethodInvocation. if (chain.isEmpty()) { // We can skip creating a MethodInvocation: just invoke the target directly diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/ObjenesisCglibAopProxy.java b/spring-aop/src/main/java/org/springframework/aop/framework/ObjenesisCglibAopProxy.java index ba31e39fe263..df24ece09578 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/ObjenesisCglibAopProxy.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/ObjenesisCglibAopProxy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -52,6 +52,11 @@ public ObjenesisCglibAopProxy(AdvisedSupport config) { } + @Override + protected Class createProxyClass(Enhancer enhancer) { + return enhancer.createClass(); + } + @Override protected Object createProxyClassAndInstance(Enhancer enhancer, Callback[] callbacks) { Class proxyClass = enhancer.createClass(); diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/ProxyConfig.java b/spring-aop/src/main/java/org/springframework/aop/framework/ProxyConfig.java index 38665cafbc61..3b6010f8f58c 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/ProxyConfig.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/ProxyConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -73,11 +73,9 @@ public boolean isProxyTargetClass() { * The exact meaning of "aggressive optimizations" will differ * between proxies, but there is usually some tradeoff. * Default is "false". - *

    For example, optimization will usually mean that advice changes won't - * take effect after a proxy has been created. For this reason, optimization - * is disabled by default. An optimize value of "true" may be ignored - * if other settings preclude optimization: for example, if "exposeProxy" - * is set to "true" and that's not compatible with the optimization. + *

    With Spring's current proxy options, this flag effectively + * enforces CGLIB proxies (similar to {@link #setProxyTargetClass}) + * but without any class validation checks (for final methods etc). */ public void setOptimize(boolean optimize) { this.optimize = optimize; diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/ProxyCreatorSupport.java b/spring-aop/src/main/java/org/springframework/aop/framework/ProxyCreatorSupport.java index 9fa04a33e92f..096294321f80 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/ProxyCreatorSupport.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/ProxyCreatorSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -86,7 +86,7 @@ public void addListener(AdvisedSupportListener listener) { /** * Remove the given AdvisedSupportListener from this proxy configuration. - * @param listener the listener to deregister + * @param listener the listener to remove */ public void removeListener(AdvisedSupportListener listener) { Assert.notNull(listener, "AdvisedSupportListener must not be null"); diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/ProxyFactory.java b/spring-aop/src/main/java/org/springframework/aop/framework/ProxyFactory.java index f4b6208e44fe..56330a6395a3 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/ProxyFactory.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/ProxyFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-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. @@ -110,6 +110,17 @@ public Object getProxy(@Nullable ClassLoader classLoader) { return createAopProxy().getProxy(classLoader); } + /** + * Determine the proxy class according to the settings in this factory. + * @param classLoader the class loader to create the proxy class with + * (or {@code null} for the low-level proxy facility's default) + * @return the proxy class + * @since 6.0 + */ + public Class getProxyClass(@Nullable ClassLoader classLoader) { + return createAopProxy().getProxyClass(classLoader); + } + /** * Create a new proxy for the given interface and interceptor. diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/ProxyFactoryBean.java b/spring-aop/src/main/java/org/springframework/aop/framework/ProxyFactoryBean.java index 6c9efc49f0d7..77b8858171f7 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/ProxyFactoryBean.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/ProxyFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -50,7 +50,7 @@ /** * {@link org.springframework.beans.factory.FactoryBean} implementation that builds an - * AOP proxy based on beans in Spring {@link org.springframework.beans.factory.BeanFactory}. + * AOP proxy based on beans in a Spring {@link org.springframework.beans.factory.BeanFactory}. * *

    {@link org.aopalliance.intercept.MethodInterceptor MethodInterceptors} and * {@link org.springframework.aop.Advisor Advisors} are identified by a list of bean @@ -61,10 +61,11 @@ * *

    Global interceptors and advisors can be added at the factory level. The specified * ones are expanded in an interceptor list where an "xxx*" entry is included in the - * list, matching the given prefix with the bean names (e.g. "global*" would match - * both "globalBean1" and "globalBean2", "*" all defined interceptors). The matching - * interceptors get applied according to their returned order value, if they implement - * the {@link org.springframework.core.Ordered} interface. + * list, matching the given prefix with the bean names — for example, "global*" + * would match both "globalBean1" and "globalBean2"; whereas, "*" would match all + * defined interceptors. The matching interceptors get applied according to their + * returned order value, if they implement the {@link org.springframework.core.Ordered} + * interface. * *

    Creates a JDK proxy when proxy interfaces are given, and a CGLIB proxy for the * actual target class if not. Note that the latter will only work if the target class @@ -75,7 +76,7 @@ * This won't work for existing prototype references, which are independent. However, * it will work for prototypes subsequently obtained from the factory. Changes to * interception will work immediately on singletons (including existing references). - * However, to change interfaces or target it's necessary to obtain a new instance + * However, to change interfaces or a target it's necessary to obtain a new instance * from the factory. This means that singleton instances obtained from the factory * do not have the same object identity. However, they do have the same interceptors * and target, and changing any reference will change all objects. @@ -273,19 +274,9 @@ public Class getObjectType() { return this.singletonInstance.getClass(); } } - Class[] ifcs = getProxiedInterfaces(); - if (ifcs.length == 1) { - return ifcs[0]; - } - else if (ifcs.length > 1) { - return createCompositeInterface(ifcs); - } - else if (this.targetName != null && this.beanFactory != null) { - return this.beanFactory.getType(this.targetName); - } - else { - return getTargetClass(); - } + // This might be incomplete since it potentially misses introduced interfaces + // from Advisors that will be lazily retrieved via setInterceptorNames. + return createAopProxy().getProxyClass(this.proxyClassLoader); } @Override @@ -294,19 +285,6 @@ public boolean isSingleton() { } - /** - * Create a composite interface Class for the given interfaces, - * implementing the given interfaces in one single Class. - *

    The default implementation builds a JDK proxy class for the - * given interfaces. - * @param interfaces the interfaces to merge - * @return the merged interface as Class - * @see java.lang.reflect.Proxy#getProxyClass - */ - protected Class createCompositeInterface(Class[] interfaces) { - return ClassUtils.createCompositeInterface(interfaces, this.proxyClassLoader); - } - /** * Return the singleton instance of this class's proxy object, * lazily creating it if it hasn't been created already. @@ -406,7 +384,7 @@ private boolean isNamedBeanAnAdvisorOrAdvice(String beanName) { if (namedBeanClass != null) { return (Advisor.class.isAssignableFrom(namedBeanClass) || Advice.class.isAssignableFrom(namedBeanClass)); } - // Treat it as an target bean if we can't tell. + // Treat it as a target bean if we can't tell. if (logger.isDebugEnabled()) { logger.debug("Could not determine type of bean with name '" + beanName + "' - assuming it is neither an Advisor nor an Advice"); @@ -421,14 +399,10 @@ private boolean isNamedBeanAnAdvisorOrAdvice(String beanName) { * are unaffected by such changes. */ private synchronized void initializeAdvisorChain() throws AopConfigException, BeansException { - if (this.advisorChainInitialized) { - return; - } - - if (!ObjectUtils.isEmpty(this.interceptorNames)) { + if (!this.advisorChainInitialized && !ObjectUtils.isEmpty(this.interceptorNames)) { if (this.beanFactory == null) { throw new IllegalStateException("No BeanFactory available anymore (probably due to serialization) " + - "- cannot resolve interceptor names " + Arrays.asList(this.interceptorNames)); + "- cannot resolve interceptor names " + Arrays.toString(this.interceptorNames)); } // Globals can't be last unless we specified a targetSource using the property... @@ -440,12 +414,11 @@ private synchronized void initializeAdvisorChain() throws AopConfigException, Be // Materialize interceptor chain from bean names. for (String name : this.interceptorNames) { if (name.endsWith(GLOBAL_SUFFIX)) { - if (!(this.beanFactory instanceof ListableBeanFactory)) { + if (!(this.beanFactory instanceof ListableBeanFactory lbf)) { throw new AopConfigException( "Can only use global advisors or interceptors with a ListableBeanFactory"); } - addGlobalAdvisors((ListableBeanFactory) this.beanFactory, - name.substring(0, name.length() - GLOBAL_SUFFIX.length())); + addGlobalAdvisors(lbf, name.substring(0, name.length() - GLOBAL_SUFFIX.length())); } else { @@ -464,9 +437,9 @@ private synchronized void initializeAdvisorChain() throws AopConfigException, Be addAdvisorOnChainCreation(advice); } } - } - this.advisorChainInitialized = true; + this.advisorChainInitialized = true; + } } @@ -479,17 +452,16 @@ private List freshAdvisorChain() { Advisor[] advisors = getAdvisors(); List freshAdvisors = new ArrayList<>(advisors.length); for (Advisor advisor : advisors) { - if (advisor instanceof PrototypePlaceholderAdvisor) { - PrototypePlaceholderAdvisor pa = (PrototypePlaceholderAdvisor) advisor; + if (advisor instanceof PrototypePlaceholderAdvisor ppa) { if (logger.isDebugEnabled()) { - logger.debug("Refreshing bean named '" + pa.getBeanName() + "'"); + logger.debug("Refreshing bean named '" + ppa.getBeanName() + "'"); } // Replace the placeholder with a fresh prototype instance resulting from a getBean lookup if (this.beanFactory == null) { throw new IllegalStateException("No BeanFactory available anymore (probably due to " + - "serialization) - cannot resolve prototype advisor '" + pa.getBeanName() + "'"); + "serialization) - cannot resolve prototype advisor '" + ppa.getBeanName() + "'"); } - Object bean = this.beanFactory.getBean(pa.getBeanName()); + Object bean = this.beanFactory.getBean(ppa.getBeanName()); Advisor refreshedAdvisor = namedBeanToAdvisor(bean); freshAdvisors.add(refreshedAdvisor); } @@ -561,7 +533,7 @@ private TargetSource freshTargetSource() { logger.debug("Refreshing target with name '" + this.targetName + "'"); } Object target = this.beanFactory.getBean(this.targetName); - return (target instanceof TargetSource ? (TargetSource) target : new SingletonTargetSource(target)); + return (target instanceof TargetSource targetSource ? targetSource : new SingletonTargetSource(target)); } } diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/ReflectiveMethodInvocation.java b/spring-aop/src/main/java/org/springframework/aop/framework/ReflectiveMethodInvocation.java index 1db34223cb41..6b57eb9fa49a 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/ReflectiveMethodInvocation.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/ReflectiveMethodInvocation.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -165,11 +165,9 @@ public Object proceed() throws Throwable { Object interceptorOrInterceptionAdvice = this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex); - if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) { + if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher dm) { // Evaluate dynamic method matcher here: static part will already have // been evaluated and found to match. - InterceptorAndDynamicMethodMatcher dm = - (InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice; Class targetClass = (this.targetClass != null ? this.targetClass : this.method.getDeclaringClass()); if (dm.methodMatcher.matches(this.method, targetClass, this.arguments)) { return dm.interceptor.invoke(this); diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/adapter/DefaultAdvisorAdapterRegistry.java b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/DefaultAdvisorAdapterRegistry.java index 7f2c66144b66..9335a1f5a012 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/adapter/DefaultAdvisorAdapterRegistry.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/DefaultAdvisorAdapterRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -58,10 +58,9 @@ public Advisor wrap(Object adviceObject) throws UnknownAdviceTypeException { if (adviceObject instanceof Advisor) { return (Advisor) adviceObject; } - if (!(adviceObject instanceof Advice)) { + if (!(adviceObject instanceof Advice advice)) { throw new UnknownAdviceTypeException(adviceObject); } - Advice advice = (Advice) adviceObject; if (advice instanceof MethodInterceptor) { // So well-known it doesn't even need an adapter. return new DefaultPointcutAdvisor(advice); diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/adapter/ThrowsAdviceAdapter.java b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/ThrowsAdviceAdapter.java index dd557215c563..a6b09c63cbbe 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/adapter/ThrowsAdviceAdapter.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/ThrowsAdviceAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-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. @@ -25,8 +25,8 @@ import org.springframework.aop.ThrowsAdvice; /** - * Adapter to enable {@link org.springframework.aop.MethodBeforeAdvice} - * to be used in the Spring AOP framework. + * Adapter to enable {@link org.springframework.aop.ThrowsAdvice} to be used + * in the Spring AOP framework. * * @author Rod Johnson * @author Juergen Hoeller diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractAutoProxyCreator.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractAutoProxyCreator.java index d4ffb04faf15..f6363db6a430 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractAutoProxyCreator.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractAutoProxyCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -17,6 +17,7 @@ package org.springframework.aop.framework.autoproxy; import java.lang.reflect.Constructor; +import java.lang.reflect.Proxy; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -37,6 +38,7 @@ import org.springframework.aop.framework.ProxyProcessorSupport; import org.springframework.aop.framework.adapter.AdvisorAdapterRegistry; import org.springframework.aop.framework.adapter.GlobalAdvisorAdapterRegistry; +import org.springframework.aop.target.EmptyTargetSource; import org.springframework.aop.target.SingletonTargetSource; import org.springframework.beans.BeansException; import org.springframework.beans.PropertyValues; @@ -46,8 +48,10 @@ import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor; +import org.springframework.core.SmartClassLoader; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; /** @@ -83,6 +87,7 @@ * @author Juergen Hoeller * @author Rod Johnson * @author Rob Harrop + * @author Sam Brannen * @since 13.10.2003 * @see #setInterceptorNames * @see #getAdvicesAndAdvisorsForBean @@ -115,7 +120,7 @@ public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport private AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); /** - * Indicates whether or not the proxy should be frozen. Overridden from super + * Indicates whether the proxy should be frozen. Overridden from super * to prevent the configuration from becoming frozen too early. */ private boolean freezeProxy = false; @@ -141,9 +146,9 @@ public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport /** - * Set whether or not the proxy should be frozen, preventing advice + * Set whether the proxy should be frozen, preventing advice * from being added to it once it is created. - *

    Overridden from the super class to prevent the proxy configuration + *

    Overridden from the superclass to prevent the proxy configuration * from being frozen before the proxy is created. */ @Override @@ -227,6 +232,30 @@ public Class predictBeanType(Class beanClass, String beanName) { return this.proxyTypes.get(cacheKey); } + @Override + public Class determineBeanType(Class beanClass, String beanName) { + Object cacheKey = getCacheKey(beanClass, beanName); + Class proxyType = this.proxyTypes.get(cacheKey); + if (proxyType == null) { + TargetSource targetSource = getCustomTargetSource(beanClass, beanName); + if (targetSource != null) { + if (StringUtils.hasLength(beanName)) { + this.targetSourcedBeans.add(beanName); + } + } + else { + targetSource = EmptyTargetSource.forClass(beanClass); + } + Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(beanClass, beanName, targetSource); + if (specificInterceptors != DO_NOT_PROXY) { + this.advisedBeans.put(cacheKey, Boolean.TRUE); + proxyType = createProxyClass(beanClass, beanName, specificInterceptors, targetSource); + this.proxyTypes.put(cacheKey, proxyType); + } + } + return (proxyType != null ? proxyType : beanClass); + } + @Override @Nullable public Constructor[] determineCandidateConstructors(Class beanClass, String beanName) { @@ -432,6 +461,18 @@ protected TargetSource getCustomTargetSource(Class beanClass, String beanName protected Object createProxy(Class beanClass, @Nullable String beanName, @Nullable Object[] specificInterceptors, TargetSource targetSource) { + return buildProxy(beanClass, beanName, specificInterceptors, targetSource, false); + } + + private Class createProxyClass(Class beanClass, @Nullable String beanName, + @Nullable Object[] specificInterceptors, TargetSource targetSource) { + + return (Class) buildProxy(beanClass, beanName, specificInterceptors, targetSource, true); + } + + private Object buildProxy(Class beanClass, @Nullable String beanName, + @Nullable Object[] specificInterceptors, TargetSource targetSource, boolean classOnly) { + if (this.beanFactory instanceof ConfigurableListableBeanFactory) { AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass); } @@ -439,7 +480,17 @@ protected Object createProxy(Class beanClass, @Nullable String beanName, ProxyFactory proxyFactory = new ProxyFactory(); proxyFactory.copyFrom(this); - if (!proxyFactory.isProxyTargetClass()) { + if (proxyFactory.isProxyTargetClass()) { + // Explicit handling of JDK proxy targets and lambdas (for introduction advice scenarios) + if (Proxy.isProxyClass(beanClass) || ClassUtils.isLambdaClass(beanClass)) { + // Must allow for introductions; can't just set interfaces to the proxy's interfaces only. + for (Class ifc : beanClass.getInterfaces()) { + proxyFactory.addInterface(ifc); + } + } + } + else { + // No proxyTargetClass flag enforced, let's apply our default checks... if (shouldProxyTargetClass(beanClass, beanName)) { proxyFactory.setProxyTargetClass(true); } @@ -458,7 +509,12 @@ protected Object createProxy(Class beanClass, @Nullable String beanName, proxyFactory.setPreFiltered(true); } - return proxyFactory.getProxy(getProxyClassLoader()); + // Use original ClassLoader if bean class not locally loaded in overriding class loader + ClassLoader classLoader = getProxyClassLoader(); + if (classLoader instanceof SmartClassLoader && classLoader != beanClass.getClassLoader()) { + classLoader = ((SmartClassLoader) classLoader).getOriginalClassLoader(); + } + return (classOnly ? proxyFactory.getProxyClass(classLoader) : proxyFactory.getProxy(classLoader)); } /** @@ -503,7 +559,10 @@ protected Advisor[] buildAdvisors(@Nullable String beanName, @Nullable Object[] List allInterceptors = new ArrayList<>(); if (specificInterceptors != null) { - allInterceptors.addAll(Arrays.asList(specificInterceptors)); + if (specificInterceptors.length > 0) { + // specificInterceptors may equal PROXY_WITHOUT_ADDITIONAL_INTERCEPTORS + allInterceptors.addAll(Arrays.asList(specificInterceptors)); + } if (commonInterceptors.length > 0) { if (this.applyCommonInterceptorsFirst) { allInterceptors.addAll(0, Arrays.asList(commonInterceptors)); diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreator.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreator.java index 8c49d4ff7d28..823961064347 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreator.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreator.java @@ -25,7 +25,6 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.PatternMatchUtils; -import org.springframework.util.StringUtils; /** * Auto proxy creator that identifies beans to proxy via a list of names. @@ -61,7 +60,7 @@ public class BeanNameAutoProxyCreator extends AbstractAutoProxyCreator { * FactoryBean will get proxied. This default behavior applies as of Spring 2.0. * If you intend to proxy a FactoryBean instance itself (a rare use case, but * Spring 1.2's default behavior), specify the bean name of the FactoryBean - * including the factory-bean prefix "&": e.g. "&myFactoryBean". + * including the factory-bean prefix "&": e.g. "&myFactoryBean". * @see org.springframework.beans.factory.FactoryBean * @see org.springframework.beans.factory.BeanFactory#FACTORY_BEAN_PREFIX */ @@ -69,7 +68,7 @@ public void setBeanNames(String... beanNames) { Assert.notEmpty(beanNames, "'beanNames' must not be empty"); this.beanNames = new ArrayList<>(beanNames.length); for (String mappedName : beanNames) { - this.beanNames.add(StringUtils.trimWhitespace(mappedName)); + this.beanNames.add(mappedName.strip()); } } diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/target/AbstractBeanFactoryBasedTargetSourceCreator.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/target/AbstractBeanFactoryBasedTargetSourceCreator.java index 4fd173f2893e..fcef74b56ef4 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/target/AbstractBeanFactoryBasedTargetSourceCreator.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/target/AbstractBeanFactoryBasedTargetSourceCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -125,12 +125,8 @@ public final TargetSource getTargetSource(Class beanClass, String beanName) { */ protected DefaultListableBeanFactory getInternalBeanFactoryForBean(String beanName) { synchronized (this.internalBeanFactories) { - DefaultListableBeanFactory internalBeanFactory = this.internalBeanFactories.get(beanName); - if (internalBeanFactory == null) { - internalBeanFactory = buildInternalBeanFactory(this.beanFactory); - this.internalBeanFactories.put(beanName, internalBeanFactory); - } - return internalBeanFactory; + return this.internalBeanFactories.computeIfAbsent(beanName, + name -> buildInternalBeanFactory(this.beanFactory)); } } diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/target/QuickTargetSourceCreator.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/target/QuickTargetSourceCreator.java index a6a741e0f879..f7df6c30249b 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/target/QuickTargetSourceCreator.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/target/QuickTargetSourceCreator.java @@ -25,9 +25,11 @@ /** * Convenient TargetSourceCreator using bean name prefixes to create one of three * well-known TargetSource types: - *
  • : CommonsPool2TargetSource - *
  • % ThreadLocalTargetSource - *
  • ! PrototypeTargetSource + *
      + *
    • : CommonsPool2TargetSource
    • + *
    • % ThreadLocalTargetSource
    • + *
    • ! PrototypeTargetSource
    • + *
    * * @author Rod Johnson * @author Stephane Nicoll diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/AbstractTraceInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/AbstractTraceInterceptor.java index 892cf5cacc05..2d67708c351a 100644 --- a/spring-aop/src/main/java/org/springframework/aop/interceptor/AbstractTraceInterceptor.java +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/AbstractTraceInterceptor.java @@ -56,7 +56,7 @@ public abstract class AbstractTraceInterceptor implements MethodInterceptor, Ser protected transient Log defaultLogger = LogFactory.getLog(getClass()); /** - * Indicates whether or not proxy class names should be hidden when using dynamic loggers. + * Indicates whether proxy class names should be hidden when using dynamic loggers. * @see #setUseDynamicLogger */ private boolean hideProxyClassNames = false; @@ -119,7 +119,7 @@ public void setLogExceptionStackTrace(boolean logExceptionStackTrace) { /** - * Determines whether or not logging is enabled for the particular {@code MethodInvocation}. + * Determines whether logging is enabled for the particular {@code MethodInvocation}. * If not, the method invocation proceeds as normal, otherwise the method invocation is passed * to the {@code invokeUnderTrace} method for handling. * @see #invokeUnderTrace(org.aopalliance.intercept.MethodInvocation, org.apache.commons.logging.Log) diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionAspectSupport.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionAspectSupport.java index 8908cab491d2..5cef18836f8e 100644 --- a/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionAspectSupport.java +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionAspectSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -20,7 +20,6 @@ import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.concurrent.Future; @@ -34,14 +33,15 @@ import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils; -import org.springframework.core.task.AsyncListenableTaskExecutor; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.config.EmbeddedValueResolver; import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.core.task.TaskExecutor; import org.springframework.core.task.support.TaskExecutorAdapter; import org.springframework.lang.Nullable; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; -import org.springframework.util.concurrent.ListenableFuture; +import org.springframework.util.StringValueResolver; import org.springframework.util.function.SingletonSupplier; /** @@ -57,6 +57,7 @@ * @author Chris Beams * @author Juergen Hoeller * @author Stephane Nicoll + * @author He Bo * @since 3.1.2 */ public abstract class AsyncExecutionAspectSupport implements BeanFactoryAware { @@ -81,6 +82,8 @@ public abstract class AsyncExecutionAspectSupport implements BeanFactoryAware { @Nullable private BeanFactory beanFactory; + @Nullable + private StringValueResolver embeddedValueResolver; /** * Create a new instance with a default {@link AsyncUncaughtExceptionHandler}. @@ -151,12 +154,14 @@ public void setExceptionHandler(AsyncUncaughtExceptionHandler exceptionHandler) @Override public void setBeanFactory(BeanFactory beanFactory) { this.beanFactory = beanFactory; + if (beanFactory instanceof ConfigurableBeanFactory configurableBeanFactory) { + this.embeddedValueResolver = new EmbeddedValueResolver(configurableBeanFactory); + } } /** * Determine the specific executor to use when executing the given method. - * Should preferably return an {@link AsyncListenableTaskExecutor} implementation. * @return the executor to use (or {@code null}, but just if no default executor is available) */ @Nullable @@ -165,6 +170,9 @@ protected AsyncTaskExecutor determineAsyncExecutor(Method method) { if (executor == null) { Executor targetExecutor; String qualifier = getExecutorQualifier(method); + if (this.embeddedValueResolver != null && StringUtils.hasLength(qualifier)) { + qualifier = this.embeddedValueResolver.resolveStringValue(qualifier); + } if (StringUtils.hasLength(qualifier)) { targetExecutor = findQualifiedExecutor(this.beanFactory, qualifier); } @@ -174,8 +182,8 @@ protected AsyncTaskExecutor determineAsyncExecutor(Method method) { if (targetExecutor == null) { return null; } - executor = (targetExecutor instanceof AsyncListenableTaskExecutor ? - (AsyncListenableTaskExecutor) targetExecutor : new TaskExecutorAdapter(targetExecutor)); + executor = (targetExecutor instanceof AsyncTaskExecutor ? + (AsyncTaskExecutor) targetExecutor : new TaskExecutorAdapter(targetExecutor)); this.executors.put(method, executor); } return executor; @@ -184,7 +192,7 @@ protected AsyncTaskExecutor determineAsyncExecutor(Method method) { /** * Return the qualifier or bean name of the executor to be used when executing the * given async method, typically specified in the form of an annotation attribute. - * Returning an empty string or {@code null} indicates that no specific executor has + *

    Returning an empty string or {@code null} indicates that no specific executor has * been specified and that the {@linkplain #setExecutor(Executor) default executor} * should be used. * @param method the method to inspect for executor qualifier metadata @@ -213,7 +221,7 @@ protected Executor findQualifiedExecutor(@Nullable BeanFactory beanFactory, Stri /** * Retrieve or build a default executor for this advice instance. - * An executor returned from here will be cached for further use. + *

    An executor returned from here will be cached for further use. *

    The default implementation searches for a unique {@link TaskExecutor} bean * in the context, or for an {@link Executor} bean named "taskExecutor" otherwise. * If neither of the two is resolvable, this implementation will return {@code null}. @@ -233,7 +241,8 @@ protected Executor getDefaultExecutor(@Nullable BeanFactory beanFactory) { return beanFactory.getBean(TaskExecutor.class); } catch (NoUniqueBeanDefinitionException ex) { - logger.debug("Could not find unique TaskExecutor bean", ex); + logger.debug("Could not find unique TaskExecutor bean. " + + "Continuing search for an Executor bean named 'taskExecutor'", ex); try { return beanFactory.getBean(DEFAULT_TASK_EXECUTOR_BEAN_NAME, Executor.class); } @@ -246,7 +255,8 @@ protected Executor getDefaultExecutor(@Nullable BeanFactory beanFactory) { } } catch (NoSuchBeanDefinitionException ex) { - logger.debug("Could not find default TaskExecutor bean", ex); + logger.debug("Could not find default TaskExecutor bean. " + + "Continuing search for an Executor bean named 'taskExecutor'", ex); try { return beanFactory.getBean(DEFAULT_TASK_EXECUTOR_BEAN_NAME, Executor.class); } @@ -269,27 +279,25 @@ protected Executor getDefaultExecutor(@Nullable BeanFactory beanFactory) { * @return the execution result (potentially a corresponding {@link Future} handle) */ @Nullable + @SuppressWarnings("deprecation") protected Object doSubmit(Callable task, AsyncTaskExecutor executor, Class returnType) { if (CompletableFuture.class.isAssignableFrom(returnType)) { - return CompletableFuture.supplyAsync(() -> { - try { - return task.call(); - } - catch (Throwable ex) { - throw new CompletionException(ex); - } - }, executor); + return executor.submitCompletable(task); } - else if (ListenableFuture.class.isAssignableFrom(returnType)) { - return ((AsyncListenableTaskExecutor) executor).submitListenable(task); + else if (org.springframework.util.concurrent.ListenableFuture.class.isAssignableFrom(returnType)) { + return ((org.springframework.core.task.AsyncListenableTaskExecutor) executor).submitListenable(task); } else if (Future.class.isAssignableFrom(returnType)) { return executor.submit(task); } - else { + else if (void.class == returnType) { executor.submit(task); return null; } + else { + throw new IllegalArgumentException( + "Invalid return type for async method (only Future and void supported): " + returnType); + } } /** diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionInterceptor.java index 8d97ad311c70..a466164bcc7b 100644 --- a/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionInterceptor.java +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -47,7 +47,7 @@ * target method needs to implement the same signature, it will have to return * a temporary Future handle that just passes the return value through * (like Spring's {@link org.springframework.scheduling.annotation.AsyncResult} - * or EJB 3.1's {@code javax.ejb.AsyncResult}). + * or EJB's {@code jakarta.ejb.AsyncResult}). * *

    When the return type is {@code java.util.concurrent.Future}, any exception thrown * during the execution can be accessed and managed by the caller. With {@code void} diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncUncaughtExceptionHandler.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncUncaughtExceptionHandler.java index a3d7d850eb63..868e4898f5e0 100644 --- a/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncUncaughtExceptionHandler.java +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncUncaughtExceptionHandler.java @@ -36,7 +36,7 @@ public interface AsyncUncaughtExceptionHandler { * Handle the given uncaught exception thrown from an asynchronous method. * @param ex the exception thrown from the asynchronous method * @param method the asynchronous method - * @param params the parameters used to invoked the method + * @param params the parameters used to invoke the method */ void handleUncaughtException(Throwable ex, Method method, Object... params); diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/CustomizableTraceInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/CustomizableTraceInterceptor.java index a46c85d0d13b..bc675836229f 100644 --- a/spring-aop/src/main/java/org/springframework/aop/interceptor/CustomizableTraceInterceptor.java +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/CustomizableTraceInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -299,7 +299,7 @@ protected String replacePlaceholders(String message, MethodInvocation methodInvo Object target = methodInvocation.getThis(); Assert.state(target != null, "Target must not be null"); - StringBuffer output = new StringBuffer(); + StringBuilder output = new StringBuilder(); while (matcher.find()) { String match = matcher.group(); if (PLACEHOLDER_METHOD_NAME.equals(match)) { @@ -341,15 +341,15 @@ else if (PLACEHOLDER_INVOCATION_TIME.equals(match)) { /** * Adds the {@code String} representation of the method return value - * to the supplied {@code StringBuffer}. Correctly handles + * to the supplied {@code StringBuilder}. Correctly handles * {@code null} and {@code void} results. * @param methodInvocation the {@code MethodInvocation} that returned the value * @param matcher the {@code Matcher} containing the matched placeholder - * @param output the {@code StringBuffer} to write output to + * @param output the {@code StringBuilder} to write output to * @param returnValue the value returned by the method invocation. */ private void appendReturnValue( - MethodInvocation methodInvocation, Matcher matcher, StringBuffer output, @Nullable Object returnValue) { + MethodInvocation methodInvocation, Matcher matcher, StringBuilder output, @Nullable Object returnValue) { if (methodInvocation.getMethod().getReturnType() == void.class) { matcher.appendReplacement(output, "void"); @@ -370,9 +370,9 @@ else if (returnValue == null) { * @param methodInvocation the {@code MethodInvocation} being logged. * Arguments will be retrieved from the corresponding {@code Method}. * @param matcher the {@code Matcher} containing the state of the output - * @param output the {@code StringBuffer} containing the output + * @param output the {@code StringBuilder} containing the output */ - private void appendArgumentTypes(MethodInvocation methodInvocation, Matcher matcher, StringBuffer output) { + private void appendArgumentTypes(MethodInvocation methodInvocation, Matcher matcher, StringBuilder output) { Class[] argumentTypes = methodInvocation.getMethod().getParameterTypes(); String[] argumentTypeShortNames = new String[argumentTypes.length]; for (int i = 0; i < argumentTypeShortNames.length; i++) { diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/ExposeBeanNameAdvisors.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/ExposeBeanNameAdvisors.java index 7dfb777dee39..1f095ed89e9e 100644 --- a/spring-aop/src/main/java/org/springframework/aop/interceptor/ExposeBeanNameAdvisors.java +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/ExposeBeanNameAdvisors.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -68,10 +68,9 @@ public static String getBeanName() throws IllegalStateException { * @throws IllegalStateException if the bean name has not been exposed */ public static String getBeanName(MethodInvocation mi) throws IllegalStateException { - if (!(mi instanceof ProxyMethodInvocation)) { + if (!(mi instanceof ProxyMethodInvocation pmi)) { throw new IllegalArgumentException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi); } - ProxyMethodInvocation pmi = (ProxyMethodInvocation) mi; String beanName = (String) pmi.getUserAttribute(BEAN_NAME_ATTRIBUTE); if (beanName == null) { throw new IllegalStateException("Cannot get bean name; not set on MethodInvocation: " + mi); @@ -113,10 +112,9 @@ public ExposeBeanNameInterceptor(String beanName) { @Override @Nullable public Object invoke(MethodInvocation mi) throws Throwable { - if (!(mi instanceof ProxyMethodInvocation)) { + if (!(mi instanceof ProxyMethodInvocation pmi)) { throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi); } - ProxyMethodInvocation pmi = (ProxyMethodInvocation) mi; pmi.setUserAttribute(BEAN_NAME_ATTRIBUTE, this.beanName); return mi.proceed(); } @@ -138,10 +136,9 @@ public ExposeBeanNameIntroduction(String beanName) { @Override @Nullable public Object invoke(MethodInvocation mi) throws Throwable { - if (!(mi instanceof ProxyMethodInvocation)) { + if (!(mi instanceof ProxyMethodInvocation pmi)) { throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi); } - ProxyMethodInvocation pmi = (ProxyMethodInvocation) mi; pmi.setUserAttribute(BEAN_NAME_ATTRIBUTE, this.beanName); return super.invoke(mi); } diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/JamonPerformanceMonitorInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/JamonPerformanceMonitorInterceptor.java deleted file mode 100644 index 0476a4f3c7a1..000000000000 --- a/spring-aop/src/main/java/org/springframework/aop/interceptor/JamonPerformanceMonitorInterceptor.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright 2002-2017 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.aop.interceptor; - -import com.jamonapi.MonKey; -import com.jamonapi.MonKeyImp; -import com.jamonapi.Monitor; -import com.jamonapi.MonitorFactory; -import com.jamonapi.utils.Misc; -import org.aopalliance.intercept.MethodInvocation; -import org.apache.commons.logging.Log; - -/** - * Performance monitor interceptor that uses JAMon library to perform the - * performance measurement on the intercepted method and output the stats. - * In addition, it tracks/counts exceptions thrown by the intercepted method. - * The stack traces can be viewed in the JAMon web application. - * - *

    This code is inspired by Thierry Templier's blog. - * - * @author Dmitriy Kopylenko - * @author Juergen Hoeller - * @author Rob Harrop - * @author Steve Souza - * @since 1.1.3 - * @see com.jamonapi.MonitorFactory - * @see PerformanceMonitorInterceptor - */ -@SuppressWarnings("serial") -public class JamonPerformanceMonitorInterceptor extends AbstractMonitoringInterceptor { - - private boolean trackAllInvocations = false; - - - /** - * Create a new JamonPerformanceMonitorInterceptor with a static logger. - */ - public JamonPerformanceMonitorInterceptor() { - } - - /** - * Create a new JamonPerformanceMonitorInterceptor with a dynamic or static logger, - * according to the given flag. - * @param useDynamicLogger whether to use a dynamic logger or a static logger - * @see #setUseDynamicLogger - */ - public JamonPerformanceMonitorInterceptor(boolean useDynamicLogger) { - setUseDynamicLogger(useDynamicLogger); - } - - /** - * Create a new JamonPerformanceMonitorInterceptor with a dynamic or static logger, - * according to the given flag. - * @param useDynamicLogger whether to use a dynamic logger or a static logger - * @param trackAllInvocations whether to track all invocations that go through - * this interceptor, or just invocations with trace logging enabled - * @see #setUseDynamicLogger - */ - public JamonPerformanceMonitorInterceptor(boolean useDynamicLogger, boolean trackAllInvocations) { - setUseDynamicLogger(useDynamicLogger); - setTrackAllInvocations(trackAllInvocations); - } - - - /** - * Set whether to track all invocations that go through this interceptor, - * or just invocations with trace logging enabled. - *

    Default is "false": Only invocations with trace logging enabled will - * be monitored. Specify "true" to let JAMon track all invocations, - * gathering statistics even when trace logging is disabled. - */ - public void setTrackAllInvocations(boolean trackAllInvocations) { - this.trackAllInvocations = trackAllInvocations; - } - - - /** - * Always applies the interceptor if the "trackAllInvocations" flag has been set; - * else just kicks in if the log is enabled. - * @see #setTrackAllInvocations - * @see #isLogEnabled - */ - @Override - protected boolean isInterceptorEnabled(MethodInvocation invocation, Log logger) { - return (this.trackAllInvocations || isLogEnabled(logger)); - } - - /** - * Wraps the invocation with a JAMon Monitor and writes the current - * performance statistics to the log (if enabled). - * @see com.jamonapi.MonitorFactory#start - * @see com.jamonapi.Monitor#stop - */ - @Override - protected Object invokeUnderTrace(MethodInvocation invocation, Log logger) throws Throwable { - String name = createInvocationTraceName(invocation); - MonKey key = new MonKeyImp(name, name, "ms."); - - Monitor monitor = MonitorFactory.start(key); - try { - return invocation.proceed(); - } - catch (Throwable ex) { - trackException(key, ex); - throw ex; - } - finally { - monitor.stop(); - if (!this.trackAllInvocations || isLogEnabled(logger)) { - writeToLog(logger, "JAMon performance statistics for method [" + name + "]:\n" + monitor); - } - } - } - - /** - * Count the thrown exception and put the stack trace in the details portion of the key. - * This will allow the stack trace to be viewed in the JAMon web application. - */ - protected void trackException(MonKey key, Throwable ex) { - String stackTrace = "stackTrace=" + Misc.getExceptionTrace(ex); - key.setDetails(stackTrace); - - // Specific exception counter. Example: java.lang.RuntimeException - MonitorFactory.add(new MonKeyImp(ex.getClass().getName(), stackTrace, "Exception"), 1); - - // General exception counter which is a total for all exceptions thrown - MonitorFactory.add(new MonKeyImp(MonitorFactory.EXCEPTIONS_LABEL, stackTrace, "Exception"), 1); - } - -} diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/PerformanceMonitorInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/PerformanceMonitorInterceptor.java index 6a0e5315feb2..610f950cff77 100644 --- a/spring-aop/src/main/java/org/springframework/aop/interceptor/PerformanceMonitorInterceptor.java +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/PerformanceMonitorInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-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. @@ -31,7 +31,6 @@ * @author Dmitriy Kopylenko * @author Rob Harrop * @see org.springframework.util.StopWatch - * @see JamonPerformanceMonitorInterceptor */ @SuppressWarnings("serial") public class PerformanceMonitorInterceptor extends AbstractMonitoringInterceptor { diff --git a/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyBeanRegistrationAotProcessor.java b/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyBeanRegistrationAotProcessor.java new file mode 100644 index 000000000000..7f71d3a3fc47 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyBeanRegistrationAotProcessor.java @@ -0,0 +1,171 @@ +/* + * Copyright 2002-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.aop.scope; + +import java.lang.reflect.Executable; +import java.util.function.Predicate; + +import javax.lang.model.element.Modifier; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.aot.generate.GeneratedMethod; +import org.springframework.aot.generate.GenerationContext; +import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; +import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; +import org.springframework.beans.factory.aot.BeanRegistrationCode; +import org.springframework.beans.factory.aot.BeanRegistrationCodeFragments; +import org.springframework.beans.factory.aot.BeanRegistrationCodeFragmentsDecorator; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.support.InstanceSupplier; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.core.ResolvableType; +import org.springframework.javapoet.ClassName; +import org.springframework.javapoet.CodeBlock; +import org.springframework.lang.Nullable; + +/** + * {@link BeanRegistrationAotProcessor} for {@link ScopedProxyFactoryBean}. + * + * @author Stephane Nicoll + * @author Phillip Webb + * @since 6.0 + */ +class ScopedProxyBeanRegistrationAotProcessor implements BeanRegistrationAotProcessor { + + private static final Log logger = LogFactory.getLog(ScopedProxyBeanRegistrationAotProcessor.class); + + + @Override + public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + Class beanType = registeredBean.getBeanType().toClass(); + if (beanType.equals(ScopedProxyFactoryBean.class)) { + String targetBeanName = getTargetBeanName(registeredBean.getMergedBeanDefinition()); + BeanDefinition targetBeanDefinition = + getTargetBeanDefinition(registeredBean.getBeanFactory(), targetBeanName); + if (targetBeanDefinition == null) { + logger.warn("Could not handle " + ScopedProxyFactoryBean.class.getSimpleName() + + ": no target bean definition found with name " + targetBeanName); + return null; + } + return BeanRegistrationAotContribution.withCustomCodeFragments(codeFragments -> + new ScopedProxyBeanRegistrationCodeFragments(codeFragments, registeredBean, + targetBeanName, targetBeanDefinition)); + } + return null; + } + + @Nullable + private String getTargetBeanName(BeanDefinition beanDefinition) { + Object value = beanDefinition.getPropertyValues().get("targetBeanName"); + return (value instanceof String ? (String) value : null); + } + + @Nullable + private BeanDefinition getTargetBeanDefinition(ConfigurableBeanFactory beanFactory, + @Nullable String targetBeanName) { + + if (targetBeanName != null && beanFactory.containsBean(targetBeanName)) { + return beanFactory.getMergedBeanDefinition(targetBeanName); + } + return null; + } + + + private static class ScopedProxyBeanRegistrationCodeFragments extends BeanRegistrationCodeFragmentsDecorator { + + private static final String REGISTERED_BEAN_PARAMETER_NAME = "registeredBean"; + + private final RegisteredBean registeredBean; + + private final String targetBeanName; + + private final BeanDefinition targetBeanDefinition; + + ScopedProxyBeanRegistrationCodeFragments(BeanRegistrationCodeFragments delegate, + RegisteredBean registeredBean, String targetBeanName, BeanDefinition targetBeanDefinition) { + + super(delegate); + this.registeredBean = registeredBean; + this.targetBeanName = targetBeanName; + this.targetBeanDefinition = targetBeanDefinition; + } + + @Override + public ClassName getTarget(RegisteredBean registeredBean, Executable constructorOrFactoryMethod) { + return ClassName.get(this.targetBeanDefinition.getResolvableType().toClass()); + } + + @Override + public CodeBlock generateNewBeanDefinitionCode(GenerationContext generationContext, + ResolvableType beanType, BeanRegistrationCode beanRegistrationCode) { + + return super.generateNewBeanDefinitionCode(generationContext, + this.targetBeanDefinition.getResolvableType(), beanRegistrationCode); + } + + @Override + public CodeBlock generateSetBeanDefinitionPropertiesCode( + GenerationContext generationContext, + BeanRegistrationCode beanRegistrationCode, + RootBeanDefinition beanDefinition, Predicate attributeFilter) { + + RootBeanDefinition processedBeanDefinition = new RootBeanDefinition( + beanDefinition); + processedBeanDefinition + .setTargetType(this.targetBeanDefinition.getResolvableType()); + processedBeanDefinition.getPropertyValues() + .removePropertyValue("targetBeanName"); + return super.generateSetBeanDefinitionPropertiesCode(generationContext, + beanRegistrationCode, processedBeanDefinition, attributeFilter); + } + + @Override + public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext, + BeanRegistrationCode beanRegistrationCode, + Executable constructorOrFactoryMethod, + boolean allowDirectSupplierShortcut) { + + GeneratedMethod generatedMethod = beanRegistrationCode.getMethods() + .add("getScopedProxyInstance", method -> { + method.addJavadoc( + "Create the scoped proxy bean instance for '$L'.", + this.registeredBean.getBeanName()); + method.addModifiers(Modifier.PRIVATE, Modifier.STATIC); + method.returns(ScopedProxyFactoryBean.class); + method.addParameter(RegisteredBean.class, + REGISTERED_BEAN_PARAMETER_NAME); + method.addStatement("$T factory = new $T()", + ScopedProxyFactoryBean.class, + ScopedProxyFactoryBean.class); + method.addStatement("factory.setTargetBeanName($S)", + this.targetBeanName); + method.addStatement( + "factory.setBeanFactory($L.getBeanFactory())", + REGISTERED_BEAN_PARAMETER_NAME); + method.addStatement("return factory"); + }); + return CodeBlock.of("$T.of($L)", InstanceSupplier.class, + generatedMethod.toMethodReference().toCodeBlock()); + } + + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyFactoryBean.java b/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyFactoryBean.java index 281b975a4c8c..fe7934b0ca0c 100644 --- a/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyFactoryBean.java +++ b/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -85,11 +85,9 @@ public void setTargetBeanName(String targetBeanName) { @Override public void setBeanFactory(BeanFactory beanFactory) { - if (!(beanFactory instanceof ConfigurableBeanFactory)) { + if (!(beanFactory instanceof ConfigurableBeanFactory cbf)) { throw new IllegalStateException("Not running in a ConfigurableBeanFactory: " + beanFactory); } - ConfigurableBeanFactory cbf = (ConfigurableBeanFactory) beanFactory; - this.scopedTargetSource.setBeanFactory(beanFactory); ProxyFactory pf = new ProxyFactory(); diff --git a/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyUtils.java b/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyUtils.java index e1899cf8e5bc..968cea81833d 100644 --- a/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyUtils.java +++ b/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -80,8 +80,8 @@ public static BeanDefinitionHolder createScopedProxy(BeanDefinitionHolder defini // Copy autowire settings from original bean definition. proxyDefinition.setAutowireCandidate(targetDefinition.isAutowireCandidate()); proxyDefinition.setPrimary(targetDefinition.isPrimary()); - if (targetDefinition instanceof AbstractBeanDefinition) { - proxyDefinition.copyQualifiersFrom((AbstractBeanDefinition) targetDefinition); + if (targetDefinition instanceof AbstractBeanDefinition abd) { + proxyDefinition.copyQualifiersFrom(abd); } // The target bean should be ignored in favor of the scoped proxy. diff --git a/spring-aop/src/main/java/org/springframework/aop/support/AbstractBeanFactoryPointcutAdvisor.java b/spring-aop/src/main/java/org/springframework/aop/support/AbstractBeanFactoryPointcutAdvisor.java index ac69e3962b06..ccb008ebd013 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/AbstractBeanFactoryPointcutAdvisor.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/AbstractBeanFactoryPointcutAdvisor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-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. @@ -137,7 +137,7 @@ public String toString() { StringBuilder sb = new StringBuilder(getClass().getName()); sb.append(": advice "); if (this.adviceBeanName != null) { - sb.append("bean '").append(this.adviceBeanName).append("'"); + sb.append("bean '").append(this.adviceBeanName).append('\''); } else { sb.append(this.advice); diff --git a/spring-aop/src/main/java/org/springframework/aop/support/AbstractPointcutAdvisor.java b/spring-aop/src/main/java/org/springframework/aop/support/AbstractPointcutAdvisor.java index 7b7c95a8ac5e..6937e31ce273 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/AbstractPointcutAdvisor.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/AbstractPointcutAdvisor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-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. @@ -52,8 +52,8 @@ public int getOrder() { return this.order; } Advice advice = getAdvice(); - if (advice instanceof Ordered) { - return ((Ordered) advice).getOrder(); + if (advice instanceof Ordered ordered) { + return ordered.getOrder(); } return Ordered.LOWEST_PRECEDENCE; } @@ -69,10 +69,9 @@ public boolean equals(@Nullable Object other) { if (this == other) { return true; } - if (!(other instanceof PointcutAdvisor)) { + if (!(other instanceof PointcutAdvisor otherAdvisor)) { return false; } - PointcutAdvisor otherAdvisor = (PointcutAdvisor) other; return (ObjectUtils.nullSafeEquals(getAdvice(), otherAdvisor.getAdvice()) && ObjectUtils.nullSafeEquals(getPointcut(), otherAdvisor.getPointcut())); } diff --git a/spring-aop/src/main/java/org/springframework/aop/support/AbstractRegexpMethodPointcut.java b/spring-aop/src/main/java/org/springframework/aop/support/AbstractRegexpMethodPointcut.java index a6ca47c15c37..44f57669012f 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/AbstractRegexpMethodPointcut.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/AbstractRegexpMethodPointcut.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -24,7 +24,6 @@ import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; -import org.springframework.util.StringUtils; /** * Abstract base regular expression pointcut bean. JavaBean properties are: @@ -81,7 +80,7 @@ public void setPatterns(String... patterns) { Assert.notEmpty(patterns, "'patterns' must not be empty"); this.patterns = new String[patterns.length]; for (int i = 0; i < patterns.length; i++) { - this.patterns[i] = StringUtils.trimWhitespace(patterns[i]); + this.patterns[i] = patterns[i].strip(); } initPatternRepresentation(this.patterns); } @@ -111,7 +110,7 @@ public void setExcludedPatterns(String... excludedPatterns) { Assert.notEmpty(excludedPatterns, "'excludedPatterns' must not be empty"); this.excludedPatterns = new String[excludedPatterns.length]; for (int i = 0; i < excludedPatterns.length; i++) { - this.excludedPatterns[i] = StringUtils.trimWhitespace(excludedPatterns[i]); + this.excludedPatterns[i] = excludedPatterns[i].strip(); } initExcludedPatternRepresentation(this.excludedPatterns); } @@ -200,10 +199,9 @@ public boolean equals(@Nullable Object other) { if (this == other) { return true; } - if (!(other instanceof AbstractRegexpMethodPointcut)) { + if (!(other instanceof AbstractRegexpMethodPointcut otherPointcut)) { return false; } - AbstractRegexpMethodPointcut otherPointcut = (AbstractRegexpMethodPointcut) other; return (Arrays.equals(this.patterns, otherPointcut.patterns) && Arrays.equals(this.excludedPatterns, otherPointcut.excludedPatterns)); } diff --git a/spring-aop/src/main/java/org/springframework/aop/support/AopUtils.java b/spring-aop/src/main/java/org/springframework/aop/support/AopUtils.java index 8055ec98cb60..0b821ce76dc5 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/AopUtils.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/AopUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -183,8 +183,8 @@ public static boolean isFinalizeMethod(@Nullable Method method) { * may be {@code DefaultFoo}. In this case, the method may be * {@code DefaultFoo.bar()}. This enables attributes on that method to be found. *

    NOTE: In contrast to {@link org.springframework.util.ClassUtils#getMostSpecificMethod}, - * this method resolves Java 5 bridge methods in order to retrieve attributes - * from the original method definition. + * this method resolves bridge methods in order to retrieve attributes from + * the original method definition. * @param method the method to be invoked, which may come from an interface * @param targetClass the target class for the current invocation. * May be {@code null} or may not even implement the method. @@ -217,7 +217,7 @@ public static boolean canApply(Pointcut pc, Class targetClass) { * out a pointcut for a class. * @param pc the static or dynamic pointcut to check * @param targetClass the class to test - * @param hasIntroductions whether or not the advisor chain + * @param hasIntroductions whether the advisor chain * for this bean includes any introductions * @return whether the pointcut can apply on any method */ @@ -261,7 +261,7 @@ public static boolean canApply(Pointcut pc, Class targetClass, boolean hasInt /** * Can the given advisor apply at all on the given class? * This is an important test as it can be used to optimize - * out a advisor for a class. + * out an advisor for a class. * @param advisor the advisor to check * @param targetClass class we're testing * @return whether the pointcut can apply on any method @@ -272,11 +272,11 @@ public static boolean canApply(Advisor advisor, Class targetClass) { /** * Can the given advisor apply at all on the given class? - *

    This is an important test as it can be used to optimize out a advisor for a class. + *

    This is an important test as it can be used to optimize out an advisor for a class. * This version also takes into account introductions (for IntroductionAwareMethodMatchers). * @param advisor the advisor to check * @param targetClass class we're testing - * @param hasIntroductions whether or not the advisor chain for this bean includes + * @param hasIntroductions whether the advisor chain for this bean includes * any introductions * @return whether the pointcut can apply on any method */ @@ -284,8 +284,7 @@ public static boolean canApply(Advisor advisor, Class targetClass, boolean ha if (advisor instanceof IntroductionAdvisor) { return ((IntroductionAdvisor) advisor).getClassFilter().matches(targetClass); } - else if (advisor instanceof PointcutAdvisor) { - PointcutAdvisor pca = (PointcutAdvisor) advisor; + else if (advisor instanceof PointcutAdvisor pca) { return canApply(pca.getPointcut(), targetClass, hasIntroductions); } else { diff --git a/spring-aop/src/main/java/org/springframework/aop/support/ComposablePointcut.java b/spring-aop/src/main/java/org/springframework/aop/support/ComposablePointcut.java index 4bbcaf333411..61b9e9fcbb91 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/ComposablePointcut.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/ComposablePointcut.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -46,8 +46,10 @@ public class ComposablePointcut implements Pointcut, Serializable { /** use serialVersionUID from Spring 1.2 for interoperability. */ private static final long serialVersionUID = -2743223737633663832L; + @SuppressWarnings("serial") private ClassFilter classFilter; + @SuppressWarnings("serial") private MethodMatcher methodMatcher; @@ -188,10 +190,9 @@ public boolean equals(@Nullable Object other) { if (this == other) { return true; } - if (!(other instanceof ComposablePointcut)) { + if (!(other instanceof ComposablePointcut otherPointcut)) { return false; } - ComposablePointcut otherPointcut = (ComposablePointcut) other; return (this.classFilter.equals(otherPointcut.classFilter) && this.methodMatcher.equals(otherPointcut.methodMatcher)); } diff --git a/spring-aop/src/main/java/org/springframework/aop/support/ControlFlowPointcut.java b/spring-aop/src/main/java/org/springframework/aop/support/ControlFlowPointcut.java index 6adeea047758..788eae69a5fe 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/ControlFlowPointcut.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/ControlFlowPointcut.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -128,10 +128,9 @@ public boolean equals(@Nullable Object other) { if (this == other) { return true; } - if (!(other instanceof ControlFlowPointcut)) { + if (!(other instanceof ControlFlowPointcut that)) { return false; } - ControlFlowPointcut that = (ControlFlowPointcut) other; return (this.clazz.equals(that.clazz)) && ObjectUtils.nullSafeEquals(this.methodName, that.methodName); } @@ -146,7 +145,7 @@ public int hashCode() { @Override public String toString() { - return getClass().getName() + ": class = " + this.clazz.getName() + "; methodName = " + methodName; + return getClass().getName() + ": class = " + this.clazz.getName() + "; methodName = " + this.methodName; } } diff --git a/spring-aop/src/main/java/org/springframework/aop/support/DefaultIntroductionAdvisor.java b/spring-aop/src/main/java/org/springframework/aop/support/DefaultIntroductionAdvisor.java index d76cdd725374..5fefc059f36a 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/DefaultIntroductionAdvisor.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/DefaultIntroductionAdvisor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -71,7 +71,8 @@ public DefaultIntroductionAdvisor(Advice advice, @Nullable IntroductionInfo intr if (introductionInfo != null) { Class[] introducedInterfaces = introductionInfo.getInterfaces(); if (introducedInterfaces.length == 0) { - throw new IllegalArgumentException("IntroductionAdviceSupport implements no interfaces"); + throw new IllegalArgumentException( + "IntroductionInfo defines no interfaces to introduce: " + introductionInfo); } for (Class ifc : introducedInterfaces) { addInterface(ifc); @@ -154,10 +155,9 @@ public boolean equals(@Nullable Object other) { if (this == other) { return true; } - if (!(other instanceof DefaultIntroductionAdvisor)) { + if (!(other instanceof DefaultIntroductionAdvisor otherAdvisor)) { return false; } - DefaultIntroductionAdvisor otherAdvisor = (DefaultIntroductionAdvisor) other; return (this.advice.equals(otherAdvisor.advice) && this.interfaces.equals(otherAdvisor.interfaces)); } diff --git a/spring-aop/src/main/java/org/springframework/aop/support/DefaultPointcutAdvisor.java b/spring-aop/src/main/java/org/springframework/aop/support/DefaultPointcutAdvisor.java index 0f3ded0e6b89..45c2e17254b9 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/DefaultPointcutAdvisor.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/DefaultPointcutAdvisor.java @@ -43,7 +43,7 @@ public class DefaultPointcutAdvisor extends AbstractGenericPointcutAdvisor imple /** * Create an empty DefaultPointcutAdvisor. - *

    Advice must be set before use using setter methods. + *

    Advice must be set before using setter methods. * Pointcut will normally be set also, but defaults to {@code Pointcut.TRUE}. */ public DefaultPointcutAdvisor() { diff --git a/spring-aop/src/main/java/org/springframework/aop/support/DelegatePerTargetObjectIntroductionInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/support/DelegatePerTargetObjectIntroductionInterceptor.java index e55cf0b6d3bd..299a474a64e0 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/DelegatePerTargetObjectIntroductionInterceptor.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/DelegatePerTargetObjectIntroductionInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-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. @@ -61,9 +61,9 @@ public class DelegatePerTargetObjectIntroductionInterceptor extends Introduction */ private final Map delegateMap = new WeakHashMap<>(); - private Class defaultImplType; + private final Class defaultImplType; - private Class interfaceType; + private final Class interfaceType; public DelegatePerTargetObjectIntroductionInterceptor(Class defaultImplType, Class interfaceType) { diff --git a/spring-aop/src/main/java/org/springframework/aop/support/MethodMatchers.java b/spring-aop/src/main/java/org/springframework/aop/support/MethodMatchers.java index 23b4d9b78de5..19bf5adbb076 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/MethodMatchers.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/MethodMatchers.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -90,12 +90,12 @@ public static MethodMatcher intersection(MethodMatcher mm1, MethodMatcher mm2) { * @param targetClass the target class * @param hasIntroductions {@code true} if the object on whose behalf we are * asking is the subject on one or more introductions; {@code false} otherwise - * @return whether or not this method matches statically + * @return whether this method matches statically */ public static boolean matches(MethodMatcher mm, Method method, Class targetClass, boolean hasIntroductions) { Assert.notNull(mm, "MethodMatcher must not be null"); - return (mm instanceof IntroductionAwareMethodMatcher ? - ((IntroductionAwareMethodMatcher) mm).matches(method, targetClass, hasIntroductions) : + return (mm instanceof IntroductionAwareMethodMatcher iamm ? + iamm.matches(method, targetClass, hasIntroductions) : mm.matches(method, targetClass)); } @@ -146,10 +146,9 @@ public boolean equals(@Nullable Object other) { if (this == other) { return true; } - if (!(other instanceof UnionMethodMatcher)) { + if (!(other instanceof UnionMethodMatcher that)) { return false; } - UnionMethodMatcher that = (UnionMethodMatcher) other; return (this.mm1.equals(that.mm1) && this.mm2.equals(that.mm2)); } @@ -223,8 +222,7 @@ public boolean equals(@Nullable Object other) { } ClassFilter otherCf1 = ClassFilter.TRUE; ClassFilter otherCf2 = ClassFilter.TRUE; - if (other instanceof ClassFilterAwareUnionMethodMatcher) { - ClassFilterAwareUnionMethodMatcher cfa = (ClassFilterAwareUnionMethodMatcher) other; + if (other instanceof ClassFilterAwareUnionMethodMatcher cfa) { otherCf1 = cfa.cf1; otherCf2 = cfa.cf2; } @@ -312,10 +310,9 @@ public boolean equals(@Nullable Object other) { if (this == other) { return true; } - if (!(other instanceof IntersectionMethodMatcher)) { + if (!(other instanceof IntersectionMethodMatcher that)) { return false; } - IntersectionMethodMatcher that = (IntersectionMethodMatcher) other; return (this.mm1.equals(that.mm1) && this.mm2.equals(that.mm2)); } diff --git a/spring-aop/src/main/java/org/springframework/aop/support/annotation/AnnotationClassFilter.java b/spring-aop/src/main/java/org/springframework/aop/support/annotation/AnnotationClassFilter.java index 847f1bb86220..997bcb9509e6 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/annotation/AnnotationClassFilter.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/annotation/AnnotationClassFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -24,8 +24,7 @@ import org.springframework.util.Assert; /** - * Simple ClassFilter that looks for a specific Java 5 annotation - * being present on a class. + * Simple ClassFilter that looks for a specific annotation being present on a class. * * @author Juergen Hoeller * @since 2.0 @@ -72,10 +71,9 @@ public boolean equals(@Nullable Object other) { if (this == other) { return true; } - if (!(other instanceof AnnotationClassFilter)) { + if (!(other instanceof AnnotationClassFilter otherCf)) { return false; } - AnnotationClassFilter otherCf = (AnnotationClassFilter) other; return (this.annotationType.equals(otherCf.annotationType) && this.checkInherited == otherCf.checkInherited); } diff --git a/spring-aop/src/main/java/org/springframework/aop/support/annotation/AnnotationMatchingPointcut.java b/spring-aop/src/main/java/org/springframework/aop/support/annotation/AnnotationMatchingPointcut.java index d734fbc0f147..35d7d2e88eeb 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/annotation/AnnotationMatchingPointcut.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/annotation/AnnotationMatchingPointcut.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -26,9 +26,8 @@ import org.springframework.util.Assert; /** - * Simple Pointcut that looks for a specific Java 5 annotation - * being present on a {@link #forClassAnnotation class} or - * {@link #forMethodAnnotation method}. + * Simple {@link Pointcut} that looks for a specific annotation being present on a + * {@linkplain #forClassAnnotation class} or {@linkplain #forMethodAnnotation method}. * * @author Juergen Hoeller * @author Sam Brannen @@ -125,10 +124,9 @@ public boolean equals(@Nullable Object other) { if (this == other) { return true; } - if (!(other instanceof AnnotationMatchingPointcut)) { + if (!(other instanceof AnnotationMatchingPointcut otherPointcut)) { return false; } - AnnotationMatchingPointcut otherPointcut = (AnnotationMatchingPointcut) other; return (this.classFilter.equals(otherPointcut.classFilter) && this.methodMatcher.equals(otherPointcut.methodMatcher)); } @@ -189,10 +187,9 @@ public boolean equals(Object obj) { if (this == obj) { return true; } - if (!(obj instanceof AnnotationCandidateClassFilter)) { + if (!(obj instanceof AnnotationCandidateClassFilter that)) { return false; } - AnnotationCandidateClassFilter that = (AnnotationCandidateClassFilter) obj; return this.annotationType.equals(that.annotationType); } diff --git a/spring-aop/src/main/java/org/springframework/aop/support/annotation/AnnotationMethodMatcher.java b/spring-aop/src/main/java/org/springframework/aop/support/annotation/AnnotationMethodMatcher.java index 720c7b889bbb..af5dfa4cdcf2 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/annotation/AnnotationMethodMatcher.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/annotation/AnnotationMethodMatcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -27,9 +27,10 @@ import org.springframework.util.Assert; /** - * Simple MethodMatcher that looks for a specific Java 5 annotation - * being present on a method (checking both the method on the invoked - * interface, if any, and the corresponding method on the target class). + * Simple {@link org.springframework.aop.MethodMatcher MethodMatcher} that looks + * for a specific annotation being present on a method (checking both the method + * on the invoked interface, if any, and the corresponding method on the target + * class). * * @author Juergen Hoeller * @author Sam Brannen @@ -92,10 +93,9 @@ public boolean equals(@Nullable Object other) { if (this == other) { return true; } - if (!(other instanceof AnnotationMethodMatcher)) { + if (!(other instanceof AnnotationMethodMatcher otherMm)) { return false; } - AnnotationMethodMatcher otherMm = (AnnotationMethodMatcher) other; return (this.annotationType.equals(otherMm.annotationType) && this.checkInherited == otherMm.checkInherited); } diff --git a/spring-aop/src/main/java/org/springframework/aop/target/AbstractBeanFactoryBasedTargetSource.java b/spring-aop/src/main/java/org/springframework/aop/target/AbstractBeanFactoryBasedTargetSource.java index cf32c396d847..743e4c7bc1b2 100644 --- a/spring-aop/src/main/java/org/springframework/aop/target/AbstractBeanFactoryBasedTargetSource.java +++ b/spring-aop/src/main/java/org/springframework/aop/target/AbstractBeanFactoryBasedTargetSource.java @@ -54,7 +54,7 @@ public abstract class AbstractBeanFactoryBasedTargetSource implements TargetSour /** Logger available to subclasses. */ - protected final Log logger = LogFactory.getLog(getClass()); + protected final transient Log logger = LogFactory.getLog(getClass()); /** Name of the target bean we will create on each invocation. */ private String targetBeanName; @@ -66,6 +66,7 @@ public abstract class AbstractBeanFactoryBasedTargetSource implements TargetSour * BeanFactory that owns this TargetSource. We need to hold onto this * reference so that we can create new prototype instances as necessary. */ + @SuppressWarnings("serial") private BeanFactory beanFactory; @@ -191,9 +192,9 @@ public int hashCode() { @Override public String toString() { StringBuilder sb = new StringBuilder(getClass().getSimpleName()); - sb.append(" for target bean '").append(this.targetBeanName).append("'"); + sb.append(" for target bean '").append(this.targetBeanName).append('\''); if (this.targetClass != null) { - sb.append(" of type [").append(this.targetClass.getName()).append("]"); + sb.append(" of type [").append(this.targetClass.getName()).append(']'); } return sb.toString(); } diff --git a/spring-aop/src/main/java/org/springframework/aop/target/AbstractPoolingTargetSource.java b/spring-aop/src/main/java/org/springframework/aop/target/AbstractPoolingTargetSource.java index 59017bfabeba..30e9c1e2ef23 100644 --- a/spring-aop/src/main/java/org/springframework/aop/target/AbstractPoolingTargetSource.java +++ b/spring-aop/src/main/java/org/springframework/aop/target/AbstractPoolingTargetSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -37,7 +37,7 @@ * {@link AbstractPrototypeBasedTargetSource} can be used to create objects * in order to put them into the pool. * - *

    Subclasses must also implement some of the monitoring methods from the + *

    Subclasses must also implement some monitoring methods from the * {@link PoolingConfig} interface. The {@link #getPoolingConfigMixin()} method * makes these stats available on proxied objects through an IntroductionAdvisor. * @@ -116,7 +116,7 @@ public final void setBeanFactory(BeanFactory beanFactory) throws BeansException /** - * Return an IntroductionAdvisor that providing a mixin + * Return an IntroductionAdvisor that provides a mixin * exposing statistics about the pool maintained by this object. */ public DefaultIntroductionAdvisor getPoolingConfigMixin() { diff --git a/spring-aop/src/main/java/org/springframework/aop/target/EmptyTargetSource.java b/spring-aop/src/main/java/org/springframework/aop/target/EmptyTargetSource.java index 2ebcc216dce7..da63c2bb2f8c 100644 --- a/spring-aop/src/main/java/org/springframework/aop/target/EmptyTargetSource.java +++ b/spring-aop/src/main/java/org/springframework/aop/target/EmptyTargetSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -135,10 +135,9 @@ public boolean equals(Object other) { if (this == other) { return true; } - if (!(other instanceof EmptyTargetSource)) { + if (!(other instanceof EmptyTargetSource otherTs)) { return false; } - EmptyTargetSource otherTs = (EmptyTargetSource) other; return (ObjectUtils.nullSafeEquals(this.targetClass, otherTs.targetClass) && this.isStatic == otherTs.isStatic); } diff --git a/spring-aop/src/main/java/org/springframework/aop/target/HotSwappableTargetSource.java b/spring-aop/src/main/java/org/springframework/aop/target/HotSwappableTargetSource.java index 5d85f5043863..5bfda76d8ece 100644 --- a/spring-aop/src/main/java/org/springframework/aop/target/HotSwappableTargetSource.java +++ b/spring-aop/src/main/java/org/springframework/aop/target/HotSwappableTargetSource.java @@ -42,6 +42,7 @@ public class HotSwappableTargetSource implements TargetSource, Serializable { /** The current target object. */ + @SuppressWarnings("serial") private Object target; diff --git a/spring-aop/src/main/java/org/springframework/aop/target/SingletonTargetSource.java b/spring-aop/src/main/java/org/springframework/aop/target/SingletonTargetSource.java index 24ae9865a6db..5e1cafc1b605 100644 --- a/spring-aop/src/main/java/org/springframework/aop/target/SingletonTargetSource.java +++ b/spring-aop/src/main/java/org/springframework/aop/target/SingletonTargetSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -42,6 +42,7 @@ public class SingletonTargetSource implements TargetSource, Serializable { /** Target cached and invoked using reflection. */ + @SuppressWarnings("serial") private final Object target; @@ -85,10 +86,9 @@ public boolean equals(Object other) { if (this == other) { return true; } - if (!(other instanceof SingletonTargetSource)) { + if (!(other instanceof SingletonTargetSource otherTargetSource)) { return false; } - SingletonTargetSource otherTargetSource = (SingletonTargetSource) other; return this.target.equals(otherTargetSource.target); } diff --git a/spring-aop/src/main/java/org/springframework/aop/target/dynamic/BeanFactoryRefreshableTargetSource.java b/spring-aop/src/main/java/org/springframework/aop/target/dynamic/BeanFactoryRefreshableTargetSource.java index 66f84a1c9864..0d5988f6b52b 100644 --- a/spring-aop/src/main/java/org/springframework/aop/target/dynamic/BeanFactoryRefreshableTargetSource.java +++ b/spring-aop/src/main/java/org/springframework/aop/target/dynamic/BeanFactoryRefreshableTargetSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-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. @@ -63,7 +63,7 @@ public BeanFactoryRefreshableTargetSource(BeanFactory beanFactory, String beanNa */ @Override protected final Object freshTarget() { - return this.obtainFreshBean(this.beanFactory, this.beanName); + return obtainFreshBean(this.beanFactory, this.beanName); } /** diff --git a/spring-aop/src/main/resources/META-INF/spring.factories b/spring-aop/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000000..72fff390d168 --- /dev/null +++ b/spring-aop/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.beans.factory.generator.BeanRegistrationContributionProvider= \ +org.springframework.aop.scope.ScopedProxyBeanRegistrationContributionProvider \ No newline at end of file diff --git a/spring-aop/src/main/resources/META-INF/spring/aot.factories b/spring-aop/src/main/resources/META-INF/spring/aot.factories new file mode 100644 index 000000000000..7264434e5cb6 --- /dev/null +++ b/spring-aop/src/main/resources/META-INF/spring/aot.factories @@ -0,0 +1,2 @@ +org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=\ +org.springframework.aop.scope.ScopedProxyBeanRegistrationAotProcessor diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscoverAnnotationTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscoverAnnotationTests.java deleted file mode 100644 index fb64933db171..000000000000 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscoverAnnotationTests.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2002-2018 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.aop.aspectj; - -import org.aspectj.lang.ProceedingJoinPoint; -import org.junit.jupiter.api.Test; - -/** - * Additional parameter name discover tests that need Java 5. - * Yes this will re-run the tests from the superclass, but that - * doesn't matter in the grand scheme of things... - * - * @author Adrian Colyer - * @author Chris Beams - */ -public class AspectJAdviceParameterNameDiscoverAnnotationTests extends AspectJAdviceParameterNameDiscovererTests { - - @Test - public void testAnnotationBinding() { - assertParameterNames(getMethod("pjpAndAnAnnotation"), - "execution(* *(..)) && @annotation(ann)", - new String[] {"thisJoinPoint","ann"}); - } - - - public void pjpAndAnAnnotation(ProceedingJoinPoint pjp, MyAnnotation ann) {} - - @interface MyAnnotation {} - -} diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscovererTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscovererTests.java index bd37f5d3770c..beb986e6f82d 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscovererTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscovererTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -17,8 +17,11 @@ package org.springframework.aop.aspectj; import java.lang.reflect.Method; +import java.util.Arrays; import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.ProceedingJoinPoint; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.aop.aspectj.AspectJAdviceParameterNameDiscoverer.AmbiguousBindingException; @@ -27,200 +30,265 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** - * Unit tests for the {@link AspectJAdviceParameterNameDiscoverer} class. - * - *

    See also {@link TigerAspectJAdviceParameterNameDiscovererTests} for tests relating to annotations. + * Unit tests for {@link AspectJAdviceParameterNameDiscoverer}. * * @author Adrian Colyer * @author Chris Beams + * @author Sam Brannen */ -public class AspectJAdviceParameterNameDiscovererTests { +class AspectJAdviceParameterNameDiscovererTests { - @Test - public void testNoArgs() { - assertParameterNames(getMethod("noArgs"), "execution(* *(..))", new String[0]); - } + @Nested + class StandardTests { - @Test - public void testJoinPointOnly() { - assertParameterNames(getMethod("tjp"), "execution(* *(..))", new String[] {"thisJoinPoint"}); - } + @Test + void noArgs() { + assertParameterNames(getMethod("noArgs"), "execution(* *(..))", new String[0]); + } - @Test - public void testJoinPointStaticPartOnly() { - assertParameterNames(getMethod("tjpsp"), "execution(* *(..))", new String[] {"thisJoinPointStaticPart"}); - } + @Test + void joinPointOnly() { + assertParameterNames(getMethod("tjp"), "execution(* *(..))", new String[] {"thisJoinPoint"}); + } - @Test - public void testTwoJoinPoints() { - assertException(getMethod("twoJoinPoints"), "foo()", IllegalStateException.class, - "Failed to bind all argument names: 1 argument(s) could not be bound"); - } + @Test + void joinPointStaticPartOnly() { + assertParameterNames(getMethod("tjpsp"), "execution(* *(..))", new String[] {"thisJoinPointStaticPart"}); + } - @Test - public void testOneThrowable() { - assertParameterNames(getMethod("oneThrowable"), "foo()", null, "ex", new String[] {"ex"}); - } + @Test + void twoJoinPoints() { + assertException(getMethod("twoJoinPoints"), "foo()", IllegalStateException.class, + "Failed to bind all argument names: 1 argument(s) could not be bound"); + } - @Test - public void testOneJPAndOneThrowable() { - assertParameterNames(getMethod("jpAndOneThrowable"), "foo()", null, "ex", new String[] {"thisJoinPoint", "ex"}); - } + @Test + void oneThrowable() { + assertParameterNames(getMethod("oneThrowable"), "foo()", null, "ex", new String[] {"ex"}); + } - @Test - public void testOneJPAndTwoThrowables() { - assertException(getMethod("jpAndTwoThrowables"), "foo()", null, "ex", AmbiguousBindingException.class, - "Binding of throwing parameter 'ex' is ambiguous: could be bound to argument 1 or argument 2"); - } + @Test + void oneJPAndOneThrowable() { + assertParameterNames(getMethod("jpAndOneThrowable"), "foo()", null, "ex", new String[] {"thisJoinPoint", "ex"}); + } - @Test - public void testThrowableNoCandidates() { - assertException(getMethod("noArgs"), "foo()", null, "ex", IllegalStateException.class, - "Not enough arguments in method to satisfy binding of returning and throwing variables"); - } + @Test + void oneJPAndTwoThrowables() { + assertException(getMethod("jpAndTwoThrowables"), "foo()", null, "ex", AmbiguousBindingException.class, + "Binding of throwing parameter 'ex' is ambiguous: could be bound to argument 1 or argument 2"); + } - @Test - public void testReturning() { - assertParameterNames(getMethod("oneObject"), "foo()", "obj", null, new String[] {"obj"}); - } + @Test + void throwableNoCandidates() { + assertException(getMethod("noArgs"), "foo()", null, "ex", IllegalStateException.class, + "Not enough arguments in method to satisfy binding of returning and throwing variables"); + } - @Test - public void testAmbiguousReturning() { - assertException(getMethod("twoObjects"), "foo()", "obj", null, AmbiguousBindingException.class, - "Binding of returning parameter 'obj' is ambiguous, there are 2 candidates."); - } + @Test + void returning() { + assertParameterNames(getMethod("oneObject"), "foo()", "obj", null, new String[] {"obj"}); + } - @Test - public void testReturningNoCandidates() { - assertException(getMethod("noArgs"), "foo()", "obj", null, IllegalStateException.class, - "Not enough arguments in method to satisfy binding of returning and throwing variables"); - } + @Test + void ambiguousReturning() { + assertException(getMethod("twoObjects"), "foo()", "obj", null, AmbiguousBindingException.class, + "Binding of returning parameter 'obj' is ambiguous, there are 2 candidates."); + } - @Test - public void testThisBindingOneCandidate() { - assertParameterNames(getMethod("oneObject"), "this(x)", new String[] {"x"}); - } + @Test + void returningNoCandidates() { + assertException(getMethod("noArgs"), "foo()", "obj", null, IllegalStateException.class, + "Not enough arguments in method to satisfy binding of returning and throwing variables"); + } - @Test - public void testThisBindingWithAlternateTokenizations() { - assertParameterNames(getMethod("oneObject"), "this( x )", new String[] {"x"}); - assertParameterNames(getMethod("oneObject"), "this( x)", new String[] {"x"}); - assertParameterNames(getMethod("oneObject"), "this (x )", new String[] {"x"}); - assertParameterNames(getMethod("oneObject"), "this(x )", new String[] {"x"}); - assertParameterNames(getMethod("oneObject"), "foo() && this(x)", new String[] {"x"}); - } + @Test + void thisBindingOneCandidate() { + assertParameterNames(getMethod("oneObject"), "this(x)", new String[] {"x"}); + } - @Test - public void testThisBindingTwoCandidates() { - assertException(getMethod("oneObject"), "this(x) || this(y)", AmbiguousBindingException.class, - "Found 2 candidate this(), target() or args() variables but only one unbound argument slot"); - } + @Test + void thisBindingWithAlternateTokenizations() { + assertParameterNames(getMethod("oneObject"), "this( x )", new String[] {"x"}); + assertParameterNames(getMethod("oneObject"), "this( x)", new String[] {"x"}); + assertParameterNames(getMethod("oneObject"), "this (x )", new String[] {"x"}); + assertParameterNames(getMethod("oneObject"), "this(x )", new String[] {"x"}); + assertParameterNames(getMethod("oneObject"), "foo() && this(x)", new String[] {"x"}); + } - @Test - public void testThisBindingWithBadPointcutExpressions() { - assertException(getMethod("oneObject"), "this(", IllegalStateException.class, - "Failed to bind all argument names: 1 argument(s) could not be bound"); - assertException(getMethod("oneObject"), "this(x && foo()", IllegalStateException.class, - "Failed to bind all argument names: 1 argument(s) could not be bound"); - } + @Test + void thisBindingTwoCandidates() { + assertException(getMethod("oneObject"), "this(x) || this(y)", AmbiguousBindingException.class, + "Found 2 candidate this(), target() or args() variables but only one unbound argument slot"); + } - @Test - public void testTargetBindingOneCandidate() { - assertParameterNames(getMethod("oneObject"), "target(x)", new String[] {"x"}); - } + @Test + void thisBindingWithBadPointcutExpressions() { + assertException(getMethod("oneObject"), "this(", IllegalStateException.class, + "Failed to bind all argument names: 1 argument(s) could not be bound"); + assertException(getMethod("oneObject"), "this(x && foo()", IllegalStateException.class, + "Failed to bind all argument names: 1 argument(s) could not be bound"); + } - @Test - public void testTargetBindingWithAlternateTokenizations() { - assertParameterNames(getMethod("oneObject"), "target( x )", new String[] {"x"}); - assertParameterNames(getMethod("oneObject"), "target( x)", new String[] {"x"}); - assertParameterNames(getMethod("oneObject"), "target (x )", new String[] {"x"}); - assertParameterNames(getMethod("oneObject"), "target(x )", new String[] {"x"}); - assertParameterNames(getMethod("oneObject"), "foo() && target(x)", new String[] {"x"}); - } + @Test + void targetBindingOneCandidate() { + assertParameterNames(getMethod("oneObject"), "target(x)", new String[] {"x"}); + } - @Test - public void testTargetBindingTwoCandidates() { - assertException(getMethod("oneObject"), "target(x) || target(y)", AmbiguousBindingException.class, - "Found 2 candidate this(), target() or args() variables but only one unbound argument slot"); - } + @Test + void targetBindingWithAlternateTokenizations() { + assertParameterNames(getMethod("oneObject"), "target( x )", new String[] {"x"}); + assertParameterNames(getMethod("oneObject"), "target( x)", new String[] {"x"}); + assertParameterNames(getMethod("oneObject"), "target (x )", new String[] {"x"}); + assertParameterNames(getMethod("oneObject"), "target(x )", new String[] {"x"}); + assertParameterNames(getMethod("oneObject"), "foo() && target(x)", new String[] {"x"}); + } - @Test - public void testTargetBindingWithBadPointcutExpressions() { - assertException(getMethod("oneObject"), "target(", IllegalStateException.class, - "Failed to bind all argument names: 1 argument(s) could not be bound"); - assertException(getMethod("oneObject"), "target(x && foo()", IllegalStateException.class, - "Failed to bind all argument names: 1 argument(s) could not be bound"); - } + @Test + void targetBindingTwoCandidates() { + assertException(getMethod("oneObject"), "target(x) || target(y)", AmbiguousBindingException.class, + "Found 2 candidate this(), target() or args() variables but only one unbound argument slot"); + } - @Test - public void testArgsBindingOneObject() { - assertParameterNames(getMethod("oneObject"), "args(x)", new String[] {"x"}); - } + @Test + void targetBindingWithBadPointcutExpressions() { + assertException(getMethod("oneObject"), "target(", IllegalStateException.class, + "Failed to bind all argument names: 1 argument(s) could not be bound"); + assertException(getMethod("oneObject"), "target(x && foo()", IllegalStateException.class, + "Failed to bind all argument names: 1 argument(s) could not be bound"); + } - @Test - public void testArgsBindingOneObjectTwoCandidates() { - assertException(getMethod("oneObject"), "args(x,y)", AmbiguousBindingException.class, - "Found 2 candidate this(), target() or args() variables but only one unbound argument slot"); - } + @Test + void argsBindingOneObject() { + assertParameterNames(getMethod("oneObject"), "args(x)", new String[] {"x"}); + } - @Test - public void testAmbiguousArgsBinding() { - assertException(getMethod("twoObjects"), "args(x,y)", AmbiguousBindingException.class, - "Still 2 unbound args at this(),target(),args() binding stage, with no way to determine between them"); - } + @Test + void argsBindingOneObjectTwoCandidates() { + assertException(getMethod("oneObject"), "args(x,y)", AmbiguousBindingException.class, + "Found 2 candidate this(), target() or args() variables but only one unbound argument slot"); + } - @Test - public void testArgsOnePrimitive() { - assertParameterNames(getMethod("onePrimitive"), "args(count)", new String[] {"count"}); - } + @Test + void ambiguousArgsBinding() { + assertException(getMethod("twoObjects"), "args(x,y)", AmbiguousBindingException.class, + "Still 2 unbound args at this(),target(),args() binding stage, with no way to determine between them"); + } - @Test - public void testArgsOnePrimitiveOneObject() { - assertException(getMethod("oneObjectOnePrimitive"), "args(count,obj)", AmbiguousBindingException.class, - "Found 2 candidate variable names but only one candidate binding slot when matching primitive args"); - } + @Test + void argsOnePrimitive() { + assertParameterNames(getMethod("onePrimitive"), "args(count)", new String[] {"count"}); + } - @Test - public void testThisAndPrimitive() { - assertParameterNames(getMethod("oneObjectOnePrimitive"), "args(count) && this(obj)", - new String[] {"obj", "count"}); - } + @Test + void argsOnePrimitiveOneObject() { + assertException(getMethod("oneObjectOnePrimitive"), "args(count,obj)", AmbiguousBindingException.class, + "Found 2 candidate variable names but only one candidate binding slot when matching primitive args"); + } - @Test - public void testTargetAndPrimitive() { - assertParameterNames(getMethod("oneObjectOnePrimitive"), "args(count) && target(obj)", - new String[] {"obj", "count"}); - } + @Test + void thisAndPrimitive() { + assertParameterNames(getMethod("oneObjectOnePrimitive"), "args(count) && this(obj)", + new String[] {"obj", "count"}); + } - @Test - public void testThrowingAndPrimitive() { - assertParameterNames(getMethod("oneThrowableOnePrimitive"), "args(count)", null, "ex", - new String[] {"ex", "count"}); - } + @Test + void targetAndPrimitive() { + assertParameterNames(getMethod("oneObjectOnePrimitive"), "args(count) && target(obj)", + new String[] {"obj", "count"}); + } - @Test - public void testAllTogetherNow() { - assertParameterNames(getMethod("theBigOne"), "this(foo) && args(x)", null, "ex", - new String[] {"thisJoinPoint", "ex", "x", "foo"}); - } + @Test + void throwingAndPrimitive() { + assertParameterNames(getMethod("oneThrowableOnePrimitive"), "args(count)", null, "ex", + new String[] {"ex", "count"}); + } + + @Test + void allTogetherNow() { + assertParameterNames(getMethod("theBigOne"), "this(foo) && args(x)", null, "ex", + new String[] {"thisJoinPoint", "ex", "x", "foo"}); + } - @Test - public void testReferenceBinding() { - assertParameterNames(getMethod("onePrimitive"),"somepc(foo)", new String[] {"foo"}); + @Test + void referenceBinding() { + assertParameterNames(getMethod("onePrimitive"),"somepc(foo)", new String[] {"foo"}); + } + + @Test + void referenceBindingWithAlternateTokenizations() { + assertParameterNames(getMethod("onePrimitive"),"call(bar *) && somepc(foo)", new String[] {"foo"}); + assertParameterNames(getMethod("onePrimitive"),"somepc ( foo )", new String[] {"foo"}); + assertParameterNames(getMethod("onePrimitive"),"somepc( foo)", new String[] {"foo"}); + } } - @Test - public void testReferenceBindingWithAlternateTokenizations() { - assertParameterNames(getMethod("onePrimitive"),"call(bar *) && somepc(foo)", new String[] {"foo"}); - assertParameterNames(getMethod("onePrimitive"),"somepc ( foo )", new String[] {"foo"}); - assertParameterNames(getMethod("onePrimitive"),"somepc( foo)", new String[] {"foo"}); + /** + * Tests just the annotation binding part of {@link AspectJAdviceParameterNameDiscoverer}. + */ + @Nested + class AnnotationTests { + + @Test + void atThis() { + assertParameterNames(getMethod("oneAnnotation"),"@this(a)", new String[] {"a"}); + } + + @Test + void atTarget() { + assertParameterNames(getMethod("oneAnnotation"),"@target(a)", new String[] {"a"}); + } + + @Test + void atArgs() { + assertParameterNames(getMethod("oneAnnotation"),"@args(a)", new String[] {"a"}); + } + + @Test + void atWithin() { + assertParameterNames(getMethod("oneAnnotation"),"@within(a)", new String[] {"a"}); + } + + @Test + void atWithincode() { + assertParameterNames(getMethod("oneAnnotation"),"@withincode(a)", new String[] {"a"}); + } + + @Test + void atAnnotation() { + assertParameterNames(getMethod("oneAnnotation"),"@annotation(a)", new String[] {"a"}); + } + + @Test + void ambiguousAnnotationTwoVars() { + assertException(getMethod("twoAnnotations"),"@annotation(a) && @this(x)", AmbiguousBindingException.class, + "Found 2 potential annotation variable(s), and 2 potential argument slots"); + } + + @Test + void ambiguousAnnotationOneVar() { + assertException(getMethod("oneAnnotation"),"@annotation(a) && @this(x)",IllegalArgumentException.class, + "Found 2 candidate annotation binding variables but only one potential argument binding slot"); + } + + @Test + void annotationMedley() { + assertParameterNames(getMethod("annotationMedley"),"@annotation(a) && args(count) && this(foo)", + null, "ex", new String[] {"ex", "foo", "count", "a"}); + } + + @Test + void annotationBinding() { + assertParameterNames(getMethod("pjpAndAnAnnotation"), + "execution(* *(..)) && @annotation(ann)", + new String[] {"thisJoinPoint","ann"}); + } + } - protected Method getMethod(String name) { + private Method getMethod(String name) { // Assumes no overloading of test methods... - Method[] candidates = getClass().getMethods(); - for (Method candidate : candidates) { + for (Method candidate : getClass().getMethods()) { if (candidate.getName().equals(name)) { return candidate; } @@ -228,11 +296,11 @@ protected Method getMethod(String name) { throw new AssertionError("Bad test specification, no method '" + name + "' found in test class"); } - protected void assertParameterNames(Method method, String pointcut, String[] parameterNames) { + private void assertParameterNames(Method method, String pointcut, String[] parameterNames) { assertParameterNames(method, pointcut, null, null, parameterNames); } - protected void assertParameterNames( + private void assertParameterNames( Method method, String pointcut, String returning, String throwing, String[] parameterNames) { assertThat(parameterNames.length).as("bad test specification, must have same number of parameter names as method arguments").isEqualTo(method.getParameterCount()); @@ -243,8 +311,8 @@ protected void assertParameterNames( discoverer.setThrowingName(throwing); String[] discoveredNames = discoverer.getParameterNames(method); - String formattedExpectedNames = format(parameterNames); - String formattedActualNames = format(discoveredNames); + String formattedExpectedNames = Arrays.toString(parameterNames); + String formattedActualNames = Arrays.toString(discoveredNames); assertThat(discoveredNames.length).as("Expecting " + parameterNames.length + " parameter names in return set '" + formattedExpectedNames + "', but found " + discoveredNames.length + @@ -257,37 +325,23 @@ protected void assertParameterNames( } } - protected void assertException(Method method, String pointcut, Class exceptionType, String message) { + private void assertException(Method method, String pointcut, Class exceptionType, String message) { assertException(method, pointcut, null, null, exceptionType, message); } - protected void assertException(Method method, String pointcut, String returning, + private void assertException(Method method, String pointcut, String returning, String throwing, Class exceptionType, String message) { AspectJAdviceParameterNameDiscoverer discoverer = new AspectJAdviceParameterNameDiscoverer(pointcut); discoverer.setRaiseExceptions(true); discoverer.setReturningName(returning); discoverer.setThrowingName(throwing); - assertThatExceptionOfType(exceptionType).isThrownBy(() -> - discoverer.getParameterNames(method)) + assertThatExceptionOfType(exceptionType) + .isThrownBy(() -> discoverer.getParameterNames(method)) .withMessageContaining(message); } - private static String format(String[] names) { - StringBuilder sb = new StringBuilder(); - sb.append("("); - for (int i = 0; i < names.length; i++) { - sb.append(names[i]); - if ((i + 1) < names.length) { - sb.append(","); - } - } - sb.append(")"); - return sb.toString(); - } - - // Methods to discover parameter names for public void noArgs() { @@ -329,4 +383,14 @@ public void oneThrowableOnePrimitive(Throwable x, int y) { public void theBigOne(JoinPoint jp, Throwable x, int y, Object foo) { } + public void oneAnnotation(MyAnnotation ann) {} + + public void twoAnnotations(MyAnnotation ann, MyAnnotation anotherAnn) {} + + public void annotationMedley(Throwable t, Object foo, int x, MyAnnotation ma) {} + + public void pjpAndAnAnnotation(ProceedingJoinPoint pjp, MyAnnotation ann) {} + + @interface MyAnnotation {} + } diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutTests.java index 20f3106c6247..e1b2a93b9589 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -17,6 +17,9 @@ package org.springframework.aop.aspectj; import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; @@ -25,6 +28,8 @@ import org.aspectj.weaver.tools.UnsupportedPointcutPrimitiveException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import test.annotation.EmptySpringAnnotation; +import test.annotation.transaction.Tx; import org.springframework.aop.ClassFilter; import org.springframework.aop.MethodMatcher; @@ -56,12 +61,19 @@ public class AspectJExpressionPointcutTests { private Method setSomeNumber; + private final Map methodsOnHasGeneric = new HashMap<>(); + @BeforeEach public void setUp() throws NoSuchMethodException { getAge = TestBean.class.getMethod("getAge"); setAge = TestBean.class.getMethod("setAge", int.class); setSomeNumber = TestBean.class.getMethod("setSomeNumber", Number.class); + + // Assumes no overloading + for (Method method : HasGeneric.class.getMethods()) { + methodsOnHasGeneric.put(method.getName(), method); + } } @@ -299,6 +311,279 @@ public void absquatulate() { } } + @Test + public void testMatchGenericArgument() { + String expression = "execution(* set*(java.util.List) )"; + AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); + ajexp.setExpression(expression); + + // TODO this will currently map, would be nice for optimization + //assertTrue(ajexp.matches(HasGeneric.class)); + //assertFalse(ajexp.matches(TestBean.class)); + + Method takesGenericList = methodsOnHasGeneric.get("setFriends"); + assertThat(ajexp.matches(takesGenericList, HasGeneric.class)).isTrue(); + assertThat(ajexp.matches(methodsOnHasGeneric.get("setEnemies"), HasGeneric.class)).isTrue(); + assertThat(ajexp.matches(methodsOnHasGeneric.get("setPartners"), HasGeneric.class)).isFalse(); + assertThat(ajexp.matches(methodsOnHasGeneric.get("setPhoneNumbers"), HasGeneric.class)).isFalse(); + + assertThat(ajexp.matches(getAge, TestBean.class)).isFalse(); + } + + @Test + public void testMatchVarargs() throws Exception { + + @SuppressWarnings("unused") + class MyTemplate { + public int queryForInt(String sql, Object... params) { + return 0; + } + } + + String expression = "execution(int *.*(String, Object...))"; + AspectJExpressionPointcut jdbcVarArgs = new AspectJExpressionPointcut(); + jdbcVarArgs.setExpression(expression); + + assertThat(jdbcVarArgs.matches( + MyTemplate.class.getMethod("queryForInt", String.class, Object[].class), + MyTemplate.class)).isTrue(); + + Method takesGenericList = methodsOnHasGeneric.get("setFriends"); + assertThat(jdbcVarArgs.matches(takesGenericList, HasGeneric.class)).isFalse(); + assertThat(jdbcVarArgs.matches(methodsOnHasGeneric.get("setEnemies"), HasGeneric.class)).isFalse(); + assertThat(jdbcVarArgs.matches(methodsOnHasGeneric.get("setPartners"), HasGeneric.class)).isFalse(); + assertThat(jdbcVarArgs.matches(methodsOnHasGeneric.get("setPhoneNumbers"), HasGeneric.class)).isFalse(); + assertThat(jdbcVarArgs.matches(getAge, TestBean.class)).isFalse(); + } + + @Test + public void testMatchAnnotationOnClassWithAtWithin() throws Exception { + String expression = "@within(test.annotation.transaction.Tx)"; + testMatchAnnotationOnClass(expression); + } + + @Test + public void testMatchAnnotationOnClassWithoutBinding() throws Exception { + String expression = "within(@test.annotation.transaction.Tx *)"; + testMatchAnnotationOnClass(expression); + } + + @Test + public void testMatchAnnotationOnClassWithSubpackageWildcard() throws Exception { + String expression = "within(@(test.annotation..*) *)"; + AspectJExpressionPointcut springAnnotatedPc = testMatchAnnotationOnClass(expression); + assertThat(springAnnotatedPc.matches(TestBean.class.getMethod("setName", String.class), TestBean.class)).isFalse(); + assertThat(springAnnotatedPc.matches(SpringAnnotated.class.getMethod("foo"), SpringAnnotated.class)).isTrue(); + + expression = "within(@(test.annotation.transaction..*) *)"; + AspectJExpressionPointcut springTxAnnotatedPc = testMatchAnnotationOnClass(expression); + assertThat(springTxAnnotatedPc.matches(SpringAnnotated.class.getMethod("foo"), SpringAnnotated.class)).isFalse(); + } + + @Test + public void testMatchAnnotationOnClassWithExactPackageWildcard() throws Exception { + String expression = "within(@(test.annotation.transaction.*) *)"; + testMatchAnnotationOnClass(expression); + } + + private AspectJExpressionPointcut testMatchAnnotationOnClass(String expression) throws Exception { + AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); + ajexp.setExpression(expression); + + assertThat(ajexp.matches(getAge, TestBean.class)).isFalse(); + assertThat(ajexp.matches(HasTransactionalAnnotation.class.getMethod("foo"), HasTransactionalAnnotation.class)).isTrue(); + assertThat(ajexp.matches(HasTransactionalAnnotation.class.getMethod("bar", String.class), HasTransactionalAnnotation.class)).isTrue(); + assertThat(ajexp.matches(BeanB.class.getMethod("setName", String.class), BeanB.class)).isTrue(); + assertThat(ajexp.matches(BeanA.class.getMethod("setName", String.class), BeanA.class)).isFalse(); + return ajexp; + } + + @Test + public void testAnnotationOnMethodWithFQN() throws Exception { + String expression = "@annotation(test.annotation.transaction.Tx)"; + AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); + ajexp.setExpression(expression); + + assertThat(ajexp.matches(getAge, TestBean.class)).isFalse(); + assertThat(ajexp.matches(HasTransactionalAnnotation.class.getMethod("foo"), HasTransactionalAnnotation.class)).isFalse(); + assertThat(ajexp.matches(HasTransactionalAnnotation.class.getMethod("bar", String.class), HasTransactionalAnnotation.class)).isFalse(); + assertThat(ajexp.matches(BeanA.class.getMethod("setName", String.class), BeanA.class)).isFalse(); + assertThat(ajexp.matches(BeanA.class.getMethod("getAge"), BeanA.class)).isTrue(); + assertThat(ajexp.matches(BeanA.class.getMethod("setName", String.class), BeanA.class)).isFalse(); + } + + @Test + public void testAnnotationOnCglibProxyMethod() throws Exception { + String expression = "@annotation(test.annotation.transaction.Tx)"; + AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); + ajexp.setExpression(expression); + + ProxyFactory factory = new ProxyFactory(new BeanA()); + factory.setProxyTargetClass(true); + BeanA proxy = (BeanA) factory.getProxy(); + assertThat(ajexp.matches(BeanA.class.getMethod("getAge"), proxy.getClass())).isTrue(); + } + + @Test + public void testAnnotationOnDynamicProxyMethod() throws Exception { + String expression = "@annotation(test.annotation.transaction.Tx)"; + AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); + ajexp.setExpression(expression); + + ProxyFactory factory = new ProxyFactory(new BeanA()); + factory.setProxyTargetClass(false); + IBeanA proxy = (IBeanA) factory.getProxy(); + assertThat(ajexp.matches(IBeanA.class.getMethod("getAge"), proxy.getClass())).isTrue(); + } + + @Test + public void testAnnotationOnMethodWithWildcard() throws Exception { + String expression = "execution(@(test.annotation..*) * *(..))"; + AspectJExpressionPointcut anySpringMethodAnnotation = new AspectJExpressionPointcut(); + anySpringMethodAnnotation.setExpression(expression); + + assertThat(anySpringMethodAnnotation.matches(getAge, TestBean.class)).isFalse(); + assertThat(anySpringMethodAnnotation.matches( + HasTransactionalAnnotation.class.getMethod("foo"), HasTransactionalAnnotation.class)).isFalse(); + assertThat(anySpringMethodAnnotation.matches( + HasTransactionalAnnotation.class.getMethod("bar", String.class), HasTransactionalAnnotation.class)).isFalse(); + assertThat(anySpringMethodAnnotation.matches(BeanA.class.getMethod("setName", String.class), BeanA.class)).isFalse(); + assertThat(anySpringMethodAnnotation.matches(BeanA.class.getMethod("getAge"), BeanA.class)).isTrue(); + assertThat(anySpringMethodAnnotation.matches(BeanA.class.getMethod("setName", String.class), BeanA.class)).isFalse(); + } + + @Test + public void testAnnotationOnMethodArgumentsWithFQN() throws Exception { + String expression = "@args(*, test.annotation.EmptySpringAnnotation))"; + AspectJExpressionPointcut takesSpringAnnotatedArgument2 = new AspectJExpressionPointcut(); + takesSpringAnnotatedArgument2.setExpression(expression); + + assertThat(takesSpringAnnotatedArgument2.matches(getAge, TestBean.class)).isFalse(); + assertThat(takesSpringAnnotatedArgument2.matches( + HasTransactionalAnnotation.class.getMethod("foo"), HasTransactionalAnnotation.class)).isFalse(); + assertThat(takesSpringAnnotatedArgument2.matches( + HasTransactionalAnnotation.class.getMethod("bar", String.class), HasTransactionalAnnotation.class)).isFalse(); + assertThat(takesSpringAnnotatedArgument2.matches(BeanA.class.getMethod("setName", String.class), BeanA.class)).isFalse(); + assertThat(takesSpringAnnotatedArgument2.matches(BeanA.class.getMethod("getAge"), BeanA.class)).isFalse(); + assertThat(takesSpringAnnotatedArgument2.matches(BeanA.class.getMethod("setName", String.class), BeanA.class)).isFalse(); + + assertThat(takesSpringAnnotatedArgument2.matches( + ProcessesSpringAnnotatedParameters.class.getMethod("takesAnnotatedParameters", TestBean.class, SpringAnnotated.class), + ProcessesSpringAnnotatedParameters.class)).isTrue(); + + // True because it maybeMatches with potential argument subtypes + assertThat(takesSpringAnnotatedArgument2.matches( + ProcessesSpringAnnotatedParameters.class.getMethod("takesNoAnnotatedParameters", TestBean.class, BeanA.class), + ProcessesSpringAnnotatedParameters.class)).isTrue(); + + assertThat(takesSpringAnnotatedArgument2.matches( + ProcessesSpringAnnotatedParameters.class.getMethod("takesNoAnnotatedParameters", TestBean.class, BeanA.class), + ProcessesSpringAnnotatedParameters.class, new TestBean(), new BeanA())).isFalse(); + } + + @Test + public void testAnnotationOnMethodArgumentsWithWildcards() throws Exception { + String expression = "execution(* *(*, @(test..*) *))"; + AspectJExpressionPointcut takesSpringAnnotatedArgument2 = new AspectJExpressionPointcut(); + takesSpringAnnotatedArgument2.setExpression(expression); + + assertThat(takesSpringAnnotatedArgument2.matches(getAge, TestBean.class)).isFalse(); + assertThat(takesSpringAnnotatedArgument2.matches( + HasTransactionalAnnotation.class.getMethod("foo"), HasTransactionalAnnotation.class)).isFalse(); + assertThat(takesSpringAnnotatedArgument2.matches( + HasTransactionalAnnotation.class.getMethod("bar", String.class), HasTransactionalAnnotation.class)).isFalse(); + assertThat(takesSpringAnnotatedArgument2.matches(BeanA.class.getMethod("setName", String.class), BeanA.class)).isFalse(); + assertThat(takesSpringAnnotatedArgument2.matches(BeanA.class.getMethod("getAge"), BeanA.class)).isFalse(); + assertThat(takesSpringAnnotatedArgument2.matches(BeanA.class.getMethod("setName", String.class), BeanA.class)).isFalse(); + + assertThat(takesSpringAnnotatedArgument2.matches( + ProcessesSpringAnnotatedParameters.class.getMethod("takesAnnotatedParameters", TestBean.class, SpringAnnotated.class), + ProcessesSpringAnnotatedParameters.class)).isTrue(); + assertThat(takesSpringAnnotatedArgument2.matches( + ProcessesSpringAnnotatedParameters.class.getMethod("takesNoAnnotatedParameters", TestBean.class, BeanA.class), + ProcessesSpringAnnotatedParameters.class)).isFalse(); + } + + + public static class HasGeneric { + + public void setFriends(List friends) { + } + public void setEnemies(List enemies) { + } + public void setPartners(List partners) { + } + public void setPhoneNumbers(List numbers) { + } + } + + + public static class ProcessesSpringAnnotatedParameters { + + public void takesAnnotatedParameters(TestBean tb, SpringAnnotated sa) { + } + + public void takesNoAnnotatedParameters(TestBean tb, BeanA tb3) { + } + } + + + @Tx + public static class HasTransactionalAnnotation { + + public void foo() { + } + public Object bar(String foo) { + throw new UnsupportedOperationException(); + } + } + + + @EmptySpringAnnotation + public static class SpringAnnotated { + + public void foo() { + } + } + + + interface IBeanA { + + @Tx + int getAge(); + } + + + static class BeanA implements IBeanA { + + @SuppressWarnings("unused") + private String name; + + private int age; + + public void setName(String name) { + this.name = name; + } + + @Tx + @Override + public int getAge() { + return age; + } + } + + + @Tx + static class BeanB { + + @SuppressWarnings("unused") + private String name; + + public void setName(String name) { + this.name = name; + } + } + } diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/TigerAspectJAdviceParameterNameDiscovererTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/TigerAspectJAdviceParameterNameDiscovererTests.java deleted file mode 100644 index 0cc3947260a5..000000000000 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/TigerAspectJAdviceParameterNameDiscovererTests.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2002-2017 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.aop.aspectj; - -import org.junit.jupiter.api.Test; - -import org.springframework.aop.aspectj.AspectJAdviceParameterNameDiscoverer.AmbiguousBindingException; - -/** - * Tests just the annotation binding part of {@link AspectJAdviceParameterNameDiscoverer}; - * see supertype for remaining tests. - * - * @author Adrian Colyer - * @author Chris Beams - */ -public class TigerAspectJAdviceParameterNameDiscovererTests extends AspectJAdviceParameterNameDiscovererTests { - - @Test - public void testAtThis() { - assertParameterNames(getMethod("oneAnnotation"),"@this(a)", new String[] {"a"}); - } - - @Test - public void testAtTarget() { - assertParameterNames(getMethod("oneAnnotation"),"@target(a)", new String[] {"a"}); - } - - @Test - public void testAtArgs() { - assertParameterNames(getMethod("oneAnnotation"),"@args(a)", new String[] {"a"}); - } - - @Test - public void testAtWithin() { - assertParameterNames(getMethod("oneAnnotation"),"@within(a)", new String[] {"a"}); - } - - @Test - public void testAtWithincode() { - assertParameterNames(getMethod("oneAnnotation"),"@withincode(a)", new String[] {"a"}); - } - - @Test - public void testAtAnnotation() { - assertParameterNames(getMethod("oneAnnotation"),"@annotation(a)", new String[] {"a"}); - } - - @Test - public void testAmbiguousAnnotationTwoVars() { - assertException(getMethod("twoAnnotations"),"@annotation(a) && @this(x)", AmbiguousBindingException.class, - "Found 2 potential annotation variable(s), and 2 potential argument slots"); - } - - @Test - public void testAmbiguousAnnotationOneVar() { - assertException(getMethod("oneAnnotation"),"@annotation(a) && @this(x)",IllegalArgumentException.class, - "Found 2 candidate annotation binding variables but only one potential argument binding slot"); - } - - @Test - public void testAnnotationMedley() { - assertParameterNames(getMethod("annotationMedley"),"@annotation(a) && args(count) && this(foo)", - null, "ex", new String[] {"ex", "foo", "count", "a"}); - } - - - public void oneAnnotation(MyAnnotation ann) {} - - public void twoAnnotations(MyAnnotation ann, MyAnnotation anotherAnn) {} - - public void annotationMedley(Throwable t, Object foo, int x, MyAnnotation ma) {} - - @interface MyAnnotation {} - -} diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/TigerAspectJExpressionPointcutTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/TigerAspectJExpressionPointcutTests.java deleted file mode 100644 index 23d63fb1bea0..000000000000 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/TigerAspectJExpressionPointcutTests.java +++ /dev/null @@ -1,330 +0,0 @@ -/* - * Copyright 2002-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.aop.aspectj; - -import java.lang.reflect.Method; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import test.annotation.EmptySpringAnnotation; -import test.annotation.transaction.Tx; - -import org.springframework.aop.framework.ProxyFactory; -import org.springframework.beans.testfixture.beans.TestBean; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Java 5 specific {@link AspectJExpressionPointcutTests}. - * - * @author Rod Johnson - * @author Chris Beams - */ -public class TigerAspectJExpressionPointcutTests { - - private Method getAge; - - private final Map methodsOnHasGeneric = new HashMap<>(); - - - @BeforeEach - public void setup() throws NoSuchMethodException { - getAge = TestBean.class.getMethod("getAge"); - // Assumes no overloading - for (Method method : HasGeneric.class.getMethods()) { - methodsOnHasGeneric.put(method.getName(), method); - } - } - - - @Test - public void testMatchGenericArgument() { - String expression = "execution(* set*(java.util.List) )"; - AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); - ajexp.setExpression(expression); - - // TODO this will currently map, would be nice for optimization - //assertTrue(ajexp.matches(HasGeneric.class)); - //assertFalse(ajexp.matches(TestBean.class)); - - Method takesGenericList = methodsOnHasGeneric.get("setFriends"); - assertThat(ajexp.matches(takesGenericList, HasGeneric.class)).isTrue(); - assertThat(ajexp.matches(methodsOnHasGeneric.get("setEnemies"), HasGeneric.class)).isTrue(); - assertThat(ajexp.matches(methodsOnHasGeneric.get("setPartners"), HasGeneric.class)).isFalse(); - assertThat(ajexp.matches(methodsOnHasGeneric.get("setPhoneNumbers"), HasGeneric.class)).isFalse(); - - assertThat(ajexp.matches(getAge, TestBean.class)).isFalse(); - } - - @Test - public void testMatchVarargs() throws Exception { - - @SuppressWarnings("unused") - class MyTemplate { - public int queryForInt(String sql, Object... params) { - return 0; - } - } - - String expression = "execution(int *.*(String, Object...))"; - AspectJExpressionPointcut jdbcVarArgs = new AspectJExpressionPointcut(); - jdbcVarArgs.setExpression(expression); - - assertThat(jdbcVarArgs.matches( - MyTemplate.class.getMethod("queryForInt", String.class, Object[].class), - MyTemplate.class)).isTrue(); - - Method takesGenericList = methodsOnHasGeneric.get("setFriends"); - assertThat(jdbcVarArgs.matches(takesGenericList, HasGeneric.class)).isFalse(); - assertThat(jdbcVarArgs.matches(methodsOnHasGeneric.get("setEnemies"), HasGeneric.class)).isFalse(); - assertThat(jdbcVarArgs.matches(methodsOnHasGeneric.get("setPartners"), HasGeneric.class)).isFalse(); - assertThat(jdbcVarArgs.matches(methodsOnHasGeneric.get("setPhoneNumbers"), HasGeneric.class)).isFalse(); - assertThat(jdbcVarArgs.matches(getAge, TestBean.class)).isFalse(); - } - - @Test - public void testMatchAnnotationOnClassWithAtWithin() throws Exception { - String expression = "@within(test.annotation.transaction.Tx)"; - testMatchAnnotationOnClass(expression); - } - - @Test - public void testMatchAnnotationOnClassWithoutBinding() throws Exception { - String expression = "within(@test.annotation.transaction.Tx *)"; - testMatchAnnotationOnClass(expression); - } - - @Test - public void testMatchAnnotationOnClassWithSubpackageWildcard() throws Exception { - String expression = "within(@(test.annotation..*) *)"; - AspectJExpressionPointcut springAnnotatedPc = testMatchAnnotationOnClass(expression); - assertThat(springAnnotatedPc.matches(TestBean.class.getMethod("setName", String.class), TestBean.class)).isFalse(); - assertThat(springAnnotatedPc.matches(SpringAnnotated.class.getMethod("foo"), SpringAnnotated.class)).isTrue(); - - expression = "within(@(test.annotation.transaction..*) *)"; - AspectJExpressionPointcut springTxAnnotatedPc = testMatchAnnotationOnClass(expression); - assertThat(springTxAnnotatedPc.matches(SpringAnnotated.class.getMethod("foo"), SpringAnnotated.class)).isFalse(); - } - - @Test - public void testMatchAnnotationOnClassWithExactPackageWildcard() throws Exception { - String expression = "within(@(test.annotation.transaction.*) *)"; - testMatchAnnotationOnClass(expression); - } - - private AspectJExpressionPointcut testMatchAnnotationOnClass(String expression) throws Exception { - AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); - ajexp.setExpression(expression); - - assertThat(ajexp.matches(getAge, TestBean.class)).isFalse(); - assertThat(ajexp.matches(HasTransactionalAnnotation.class.getMethod("foo"), HasTransactionalAnnotation.class)).isTrue(); - assertThat(ajexp.matches(HasTransactionalAnnotation.class.getMethod("bar", String.class), HasTransactionalAnnotation.class)).isTrue(); - assertThat(ajexp.matches(BeanB.class.getMethod("setName", String.class), BeanB.class)).isTrue(); - assertThat(ajexp.matches(BeanA.class.getMethod("setName", String.class), BeanA.class)).isFalse(); - return ajexp; - } - - @Test - public void testAnnotationOnMethodWithFQN() throws Exception { - String expression = "@annotation(test.annotation.transaction.Tx)"; - AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); - ajexp.setExpression(expression); - - assertThat(ajexp.matches(getAge, TestBean.class)).isFalse(); - assertThat(ajexp.matches(HasTransactionalAnnotation.class.getMethod("foo"), HasTransactionalAnnotation.class)).isFalse(); - assertThat(ajexp.matches(HasTransactionalAnnotation.class.getMethod("bar", String.class), HasTransactionalAnnotation.class)).isFalse(); - assertThat(ajexp.matches(BeanA.class.getMethod("setName", String.class), BeanA.class)).isFalse(); - assertThat(ajexp.matches(BeanA.class.getMethod("getAge"), BeanA.class)).isTrue(); - assertThat(ajexp.matches(BeanA.class.getMethod("setName", String.class), BeanA.class)).isFalse(); - } - - @Test - public void testAnnotationOnCglibProxyMethod() throws Exception { - String expression = "@annotation(test.annotation.transaction.Tx)"; - AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); - ajexp.setExpression(expression); - - ProxyFactory factory = new ProxyFactory(new BeanA()); - factory.setProxyTargetClass(true); - BeanA proxy = (BeanA) factory.getProxy(); - assertThat(ajexp.matches(BeanA.class.getMethod("getAge"), proxy.getClass())).isTrue(); - } - - @Test - public void testAnnotationOnDynamicProxyMethod() throws Exception { - String expression = "@annotation(test.annotation.transaction.Tx)"; - AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); - ajexp.setExpression(expression); - - ProxyFactory factory = new ProxyFactory(new BeanA()); - factory.setProxyTargetClass(false); - IBeanA proxy = (IBeanA) factory.getProxy(); - assertThat(ajexp.matches(IBeanA.class.getMethod("getAge"), proxy.getClass())).isTrue(); - } - - @Test - public void testAnnotationOnMethodWithWildcard() throws Exception { - String expression = "execution(@(test.annotation..*) * *(..))"; - AspectJExpressionPointcut anySpringMethodAnnotation = new AspectJExpressionPointcut(); - anySpringMethodAnnotation.setExpression(expression); - - assertThat(anySpringMethodAnnotation.matches(getAge, TestBean.class)).isFalse(); - assertThat(anySpringMethodAnnotation.matches( - HasTransactionalAnnotation.class.getMethod("foo"), HasTransactionalAnnotation.class)).isFalse(); - assertThat(anySpringMethodAnnotation.matches( - HasTransactionalAnnotation.class.getMethod("bar", String.class), HasTransactionalAnnotation.class)).isFalse(); - assertThat(anySpringMethodAnnotation.matches(BeanA.class.getMethod("setName", String.class), BeanA.class)).isFalse(); - assertThat(anySpringMethodAnnotation.matches(BeanA.class.getMethod("getAge"), BeanA.class)).isTrue(); - assertThat(anySpringMethodAnnotation.matches(BeanA.class.getMethod("setName", String.class), BeanA.class)).isFalse(); - } - - @Test - public void testAnnotationOnMethodArgumentsWithFQN() throws Exception { - String expression = "@args(*, test.annotation.EmptySpringAnnotation))"; - AspectJExpressionPointcut takesSpringAnnotatedArgument2 = new AspectJExpressionPointcut(); - takesSpringAnnotatedArgument2.setExpression(expression); - - assertThat(takesSpringAnnotatedArgument2.matches(getAge, TestBean.class)).isFalse(); - assertThat(takesSpringAnnotatedArgument2.matches( - HasTransactionalAnnotation.class.getMethod("foo"), HasTransactionalAnnotation.class)).isFalse(); - assertThat(takesSpringAnnotatedArgument2.matches( - HasTransactionalAnnotation.class.getMethod("bar", String.class), HasTransactionalAnnotation.class)).isFalse(); - assertThat(takesSpringAnnotatedArgument2.matches(BeanA.class.getMethod("setName", String.class), BeanA.class)).isFalse(); - assertThat(takesSpringAnnotatedArgument2.matches(BeanA.class.getMethod("getAge"), BeanA.class)).isFalse(); - assertThat(takesSpringAnnotatedArgument2.matches(BeanA.class.getMethod("setName", String.class), BeanA.class)).isFalse(); - - assertThat(takesSpringAnnotatedArgument2.matches( - ProcessesSpringAnnotatedParameters.class.getMethod("takesAnnotatedParameters", TestBean.class, SpringAnnotated.class), - ProcessesSpringAnnotatedParameters.class)).isTrue(); - - // True because it maybeMatches with potential argument subtypes - assertThat(takesSpringAnnotatedArgument2.matches( - ProcessesSpringAnnotatedParameters.class.getMethod("takesNoAnnotatedParameters", TestBean.class, BeanA.class), - ProcessesSpringAnnotatedParameters.class)).isTrue(); - - assertThat(takesSpringAnnotatedArgument2.matches( - ProcessesSpringAnnotatedParameters.class.getMethod("takesNoAnnotatedParameters", TestBean.class, BeanA.class), - ProcessesSpringAnnotatedParameters.class, new TestBean(), new BeanA())).isFalse(); - } - - @Test - public void testAnnotationOnMethodArgumentsWithWildcards() throws Exception { - String expression = "execution(* *(*, @(test..*) *))"; - AspectJExpressionPointcut takesSpringAnnotatedArgument2 = new AspectJExpressionPointcut(); - takesSpringAnnotatedArgument2.setExpression(expression); - - assertThat(takesSpringAnnotatedArgument2.matches(getAge, TestBean.class)).isFalse(); - assertThat(takesSpringAnnotatedArgument2.matches( - HasTransactionalAnnotation.class.getMethod("foo"), HasTransactionalAnnotation.class)).isFalse(); - assertThat(takesSpringAnnotatedArgument2.matches( - HasTransactionalAnnotation.class.getMethod("bar", String.class), HasTransactionalAnnotation.class)).isFalse(); - assertThat(takesSpringAnnotatedArgument2.matches(BeanA.class.getMethod("setName", String.class), BeanA.class)).isFalse(); - assertThat(takesSpringAnnotatedArgument2.matches(BeanA.class.getMethod("getAge"), BeanA.class)).isFalse(); - assertThat(takesSpringAnnotatedArgument2.matches(BeanA.class.getMethod("setName", String.class), BeanA.class)).isFalse(); - - assertThat(takesSpringAnnotatedArgument2.matches( - ProcessesSpringAnnotatedParameters.class.getMethod("takesAnnotatedParameters", TestBean.class, SpringAnnotated.class), - ProcessesSpringAnnotatedParameters.class)).isTrue(); - assertThat(takesSpringAnnotatedArgument2.matches( - ProcessesSpringAnnotatedParameters.class.getMethod("takesNoAnnotatedParameters", TestBean.class, BeanA.class), - ProcessesSpringAnnotatedParameters.class)).isFalse(); - } - - - public static class HasGeneric { - - public void setFriends(List friends) { - } - public void setEnemies(List enemies) { - } - public void setPartners(List partners) { - } - public void setPhoneNumbers(List numbers) { - } - } - - - public static class ProcessesSpringAnnotatedParameters { - - public void takesAnnotatedParameters(TestBean tb, SpringAnnotated sa) { - } - - public void takesNoAnnotatedParameters(TestBean tb, BeanA tb3) { - } - } - - - @Tx - public static class HasTransactionalAnnotation { - - public void foo() { - } - public Object bar(String foo) { - throw new UnsupportedOperationException(); - } - } - - - @EmptySpringAnnotation - public static class SpringAnnotated { - - public void foo() { - } - } - - - interface IBeanA { - - @Tx - int getAge(); - } - - - static class BeanA implements IBeanA { - - @SuppressWarnings("unused") - private String name; - - private int age; - - public void setName(String name) { - this.name = name; - } - - @Tx - @Override - public int getAge() { - return age; - } - } - - - @Tx - static class BeanB { - - @SuppressWarnings("unused") - private String name; - - public void setName(String name) { - this.name = name; - } - } - -} diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/TrickyAspectJPointcutExpressionTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/TrickyAspectJPointcutExpressionTests.java index 7a6c7e93c7b1..9ce2fdc69134 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/TrickyAspectJPointcutExpressionTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/TrickyAspectJPointcutExpressionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -142,11 +142,11 @@ public TestException(String string) { @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited - public static @interface Log { + @interface Log { } - public static interface TestService { + public interface TestService { public String sayHello(); } diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactoryTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactoryTests.java index 612aa61af66a..847ef4f38044 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactoryTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -98,7 +98,7 @@ void rejectsPerCflowBelowAspect() { } @Test - void perTargetAspect() throws SecurityException, NoSuchMethodException { + void perTargetAspect() throws Exception { TestBean target = new TestBean(); int realAge = 65; target.setAge(realAge); @@ -130,7 +130,7 @@ void perTargetAspect() throws SecurityException, NoSuchMethodException { } @Test - void multiplePerTargetAspects() throws SecurityException, NoSuchMethodException { + void multiplePerTargetAspects() throws Exception { TestBean target = new TestBean(); int realAge = 65; target.setAge(realAge); @@ -158,7 +158,7 @@ void multiplePerTargetAspects() throws SecurityException, NoSuchMethodException } @Test - void multiplePerTargetAspectsWithOrderAnnotation() throws SecurityException, NoSuchMethodException { + void multiplePerTargetAspectsWithOrderAnnotation() throws Exception { TestBean target = new TestBean(); int realAge = 65; target.setAge(realAge); @@ -184,7 +184,7 @@ void multiplePerTargetAspectsWithOrderAnnotation() throws SecurityException, NoS } @Test - void perThisAspect() throws SecurityException, NoSuchMethodException { + void perThisAspect() throws Exception { TestBean target = new TestBean(); int realAge = 65; target.setAge(realAge); @@ -220,7 +220,7 @@ void perThisAspect() throws SecurityException, NoSuchMethodException { } @Test - void perTypeWithinAspect() throws SecurityException, NoSuchMethodException { + void perTypeWithinAspect() throws Exception { TestBean target = new TestBean(); int realAge = 65; target.setAge(realAge); @@ -322,7 +322,7 @@ void bindingWithMultipleArgsDifferentlyOrdered() { int b = 12; int c = 25; String d = "d"; - StringBuffer e = new StringBuffer("stringbuf"); + StringBuilder e = new StringBuilder("stringbuf"); String expectedResult = a + b+ c + d + e; assertThat(mva.mungeArgs(a, b, c, d, e)).isEqualTo(expectedResult); } @@ -728,12 +728,12 @@ void changeReturnType(ProceedingJoinPoint pjp, int age) throws Throwable { @Aspect static class ManyValuedArgs { - String mungeArgs(String a, int b, int c, String d, StringBuffer e) { + String mungeArgs(String a, int b, int c, String d, StringBuilder e) { return a + b + c + d + e; } @Around(value="execution(String mungeArgs(..)) && args(a, b, c, d, e)", argNames="b,c,d,e,a") - String reverseAdvice(ProceedingJoinPoint pjp, int b, int c, String d, StringBuffer e, String a) throws Throwable { + String reverseAdvice(ProceedingJoinPoint pjp, int b, int c, String d, StringBuilder e, String a) throws Throwable { assertThat(pjp.proceed()).isEqualTo(a + b+ c+ d+ e); return a + b + c + d + e; } @@ -931,7 +931,7 @@ private Method getGetterFromSetter(Method setter) { return setter.getDeclaringClass().getMethod(getterName); } catch (NoSuchMethodException ex) { - // must be write only + // must be write-only return null; } } diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectMetadataTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectMetadataTests.java index 637baa2450a8..2554895430fb 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectMetadataTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectMetadataTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -28,7 +28,6 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** - * @since 2.0 * @author Rod Johnson * @author Chris Beams * @author Sam Brannen @@ -56,7 +55,7 @@ void perTargetAspect() { assertThat(am.getAjType().getPerClause().getKind()).isEqualTo(PerClauseKind.PERTARGET); assertThat(am.getPerClausePointcut()).isInstanceOf(AspectJExpressionPointcut.class); assertThat(((AspectJExpressionPointcut) am.getPerClausePointcut()).getExpression()) - .isEqualTo("execution(* *.getSpouse())"); + .isEqualTo("execution(* *.getSpouse())"); } @Test @@ -67,7 +66,7 @@ void perThisAspect() { assertThat(am.getAjType().getPerClause().getKind()).isEqualTo(PerClauseKind.PERTHIS); assertThat(am.getPerClausePointcut()).isInstanceOf(AspectJExpressionPointcut.class); assertThat(((AspectJExpressionPointcut) am.getPerClausePointcut()).getExpression()) - .isEqualTo("execution(* *.getSpouse())"); + .isEqualTo("execution(* *.getSpouse())"); } } diff --git a/spring-aop/src/test/java/org/springframework/aop/config/AopNamespaceHandlerEventTests.java b/spring-aop/src/test/java/org/springframework/aop/config/AopNamespaceHandlerEventTests.java index d4c774424d04..ffb270cdd9b4 100644 --- a/spring-aop/src/test/java/org/springframework/aop/config/AopNamespaceHandlerEventTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/config/AopNamespaceHandlerEventTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -16,6 +16,7 @@ package org.springframework.aop.config; +import java.util.Arrays; import java.util.HashSet; import java.util.Set; @@ -40,7 +41,7 @@ * @author Juergen Hoeller * @author Chris Beams */ -public class AopNamespaceHandlerEventTests { +class AopNamespaceHandlerEventTests { private static final Class CLASS = AopNamespaceHandlerEventTests.class; @@ -57,25 +58,24 @@ public class AopNamespaceHandlerEventTests { @BeforeEach - public void setup() { + void setup() { this.reader = new XmlBeanDefinitionReader(this.beanFactory); this.reader.setEventListener(this.eventListener); } @Test - public void testPointcutEvents() { + void pointcutEvents() { this.reader.loadBeanDefinitions(POINTCUT_EVENTS_CONTEXT); ComponentDefinition[] componentDefinitions = this.eventListener.getComponentDefinitions(); - assertThat(componentDefinitions.length).as("Incorrect number of events fired").isEqualTo(1); - boolean condition = componentDefinitions[0] instanceof CompositeComponentDefinition; - assertThat(condition).as("No holder with nested components").isTrue(); + assertThat(componentDefinitions).as("Incorrect number of events fired").hasSize(1); + assertThat(componentDefinitions[0]).as("No holder with nested components").isInstanceOf(CompositeComponentDefinition.class); CompositeComponentDefinition compositeDef = (CompositeComponentDefinition) componentDefinitions[0]; assertThat(compositeDef.getName()).isEqualTo("aop:config"); ComponentDefinition[] nestedComponentDefs = compositeDef.getNestedComponents(); - assertThat(nestedComponentDefs.length).as("Incorrect number of inner components").isEqualTo(2); + assertThat(nestedComponentDefs).as("Incorrect number of inner components").hasSize(2); PointcutComponentDefinition pcd = null; for (ComponentDefinition componentDefinition : nestedComponentDefs) { if (componentDefinition instanceof PointcutComponentDefinition) { @@ -84,84 +84,77 @@ public void testPointcutEvents() { } } assertThat(pcd).as("PointcutComponentDefinition not found").isNotNull(); - assertThat(pcd.getBeanDefinitions().length).as("Incorrect number of BeanDefinitions").isEqualTo(1); + assertThat(pcd.getBeanDefinitions()).as("Incorrect number of BeanDefinitions").hasSize(1); } @Test - public void testAdvisorEventsWithPointcutRef() { + void advisorEventsWithPointcutRef() { this.reader.loadBeanDefinitions(POINTCUT_REF_CONTEXT); ComponentDefinition[] componentDefinitions = this.eventListener.getComponentDefinitions(); - assertThat(componentDefinitions.length).as("Incorrect number of events fired").isEqualTo(2); + assertThat(componentDefinitions).as("Incorrect number of events fired").hasSize(2); - boolean condition1 = componentDefinitions[0] instanceof CompositeComponentDefinition; - assertThat(condition1).as("No holder with nested components").isTrue(); + assertThat(componentDefinitions[0]).as("No holder with nested components").isInstanceOf(CompositeComponentDefinition.class); CompositeComponentDefinition compositeDef = (CompositeComponentDefinition) componentDefinitions[0]; assertThat(compositeDef.getName()).isEqualTo("aop:config"); ComponentDefinition[] nestedComponentDefs = compositeDef.getNestedComponents(); - assertThat(nestedComponentDefs.length).as("Incorrect number of inner components").isEqualTo(3); + assertThat(nestedComponentDefs).as("Incorrect number of inner components").hasSize(3); AdvisorComponentDefinition acd = null; - for (int i = 0; i < nestedComponentDefs.length; i++) { - ComponentDefinition componentDefinition = nestedComponentDefs[i]; + for (ComponentDefinition componentDefinition : nestedComponentDefs) { if (componentDefinition instanceof AdvisorComponentDefinition) { acd = (AdvisorComponentDefinition) componentDefinition; break; } } assertThat(acd).as("AdvisorComponentDefinition not found").isNotNull(); - assertThat(acd.getBeanDefinitions().length).isEqualTo(1); - assertThat(acd.getBeanReferences().length).isEqualTo(2); + assertThat(acd.getBeanDefinitions()).hasSize(1); + assertThat(acd.getBeanReferences()).hasSize(2); - boolean condition = componentDefinitions[1] instanceof BeanComponentDefinition; - assertThat(condition).as("No advice bean found").isTrue(); + assertThat(componentDefinitions[1]).as("No advice bean found").isInstanceOf(BeanComponentDefinition.class); BeanComponentDefinition adviceDef = (BeanComponentDefinition) componentDefinitions[1]; assertThat(adviceDef.getBeanName()).isEqualTo("countingAdvice"); } @Test - public void testAdvisorEventsWithDirectPointcut() { + void advisorEventsWithDirectPointcut() { this.reader.loadBeanDefinitions(DIRECT_POINTCUT_EVENTS_CONTEXT); ComponentDefinition[] componentDefinitions = this.eventListener.getComponentDefinitions(); - assertThat(componentDefinitions.length).as("Incorrect number of events fired").isEqualTo(2); + assertThat(componentDefinitions).as("Incorrect number of events fired").hasSize(2); - boolean condition1 = componentDefinitions[0] instanceof CompositeComponentDefinition; - assertThat(condition1).as("No holder with nested components").isTrue(); + assertThat(componentDefinitions[0]).as("No holder with nested components").isInstanceOf(CompositeComponentDefinition.class); CompositeComponentDefinition compositeDef = (CompositeComponentDefinition) componentDefinitions[0]; assertThat(compositeDef.getName()).isEqualTo("aop:config"); ComponentDefinition[] nestedComponentDefs = compositeDef.getNestedComponents(); - assertThat(nestedComponentDefs.length).as("Incorrect number of inner components").isEqualTo(2); + assertThat(nestedComponentDefs).as("Incorrect number of inner components").hasSize(2); AdvisorComponentDefinition acd = null; - for (int i = 0; i < nestedComponentDefs.length; i++) { - ComponentDefinition componentDefinition = nestedComponentDefs[i]; + for (ComponentDefinition componentDefinition : nestedComponentDefs) { if (componentDefinition instanceof AdvisorComponentDefinition) { acd = (AdvisorComponentDefinition) componentDefinition; break; } } assertThat(acd).as("AdvisorComponentDefinition not found").isNotNull(); - assertThat(acd.getBeanDefinitions().length).isEqualTo(2); - assertThat(acd.getBeanReferences().length).isEqualTo(1); + assertThat(acd.getBeanDefinitions()).hasSize(2); + assertThat(acd.getBeanReferences()).hasSize(1); - boolean condition = componentDefinitions[1] instanceof BeanComponentDefinition; - assertThat(condition).as("No advice bean found").isTrue(); + assertThat(componentDefinitions[1]).as("No advice bean found").isInstanceOf(BeanComponentDefinition.class); BeanComponentDefinition adviceDef = (BeanComponentDefinition) componentDefinitions[1]; assertThat(adviceDef.getBeanName()).isEqualTo("countingAdvice"); } @Test - public void testAspectEvent() { + void aspectEvent() { this.reader.loadBeanDefinitions(CONTEXT); ComponentDefinition[] componentDefinitions = this.eventListener.getComponentDefinitions(); - assertThat(componentDefinitions.length).as("Incorrect number of events fired").isEqualTo(5); + assertThat(componentDefinitions).as("Incorrect number of events fired").hasSize(2); - boolean condition = componentDefinitions[0] instanceof CompositeComponentDefinition; - assertThat(condition).as("No holder with nested components").isTrue(); + assertThat(componentDefinitions[0]).as("No holder with nested components").isInstanceOf(CompositeComponentDefinition.class); CompositeComponentDefinition compositeDef = (CompositeComponentDefinition) componentDefinitions[0]; assertThat(compositeDef.getName()).isEqualTo("aop:config"); ComponentDefinition[] nestedComponentDefs = compositeDef.getNestedComponents(); - assertThat(nestedComponentDefs.length).as("Incorrect number of inner components").isEqualTo(2); + assertThat(nestedComponentDefs).as("Incorrect number of inner components").hasSize(2); AspectComponentDefinition acd = null; for (ComponentDefinition componentDefinition : nestedComponentDefs) { if (componentDefinition instanceof AspectComponentDefinition) { @@ -172,9 +165,9 @@ public void testAspectEvent() { assertThat(acd).as("AspectComponentDefinition not found").isNotNull(); BeanDefinition[] beanDefinitions = acd.getBeanDefinitions(); - assertThat(beanDefinitions.length).isEqualTo(5); + assertThat(beanDefinitions).hasSize(5); BeanReference[] beanReferences = acd.getBeanReferences(); - assertThat(beanReferences.length).isEqualTo(6); + assertThat(beanReferences).hasSize(6); Set expectedReferences = new HashSet<>(); expectedReferences.add("pc"); @@ -182,19 +175,16 @@ public void testAspectEvent() { for (BeanReference beanReference : beanReferences) { expectedReferences.remove(beanReference.getBeanName()); } - assertThat(expectedReferences.size()).as("Incorrect references found").isEqualTo(0); + assertThat(expectedReferences).as("Incorrect references found").isEmpty(); - for (int i = 1; i < componentDefinitions.length; i++) { - boolean condition1 = componentDefinitions[i] instanceof BeanComponentDefinition; - assertThat(condition1).isTrue(); - } + Arrays.stream(componentDefinitions).skip(1).forEach(definition -> + assertThat(definition).isInstanceOf(BeanComponentDefinition.class)); ComponentDefinition[] nestedComponentDefs2 = acd.getNestedComponents(); - assertThat(nestedComponentDefs2.length).as("Inner PointcutComponentDefinition not found").isEqualTo(1); - boolean condition1 = nestedComponentDefs2[0] instanceof PointcutComponentDefinition; - assertThat(condition1).isTrue(); + assertThat(nestedComponentDefs2).as("Inner PointcutComponentDefinition not found").hasSize(1); + assertThat(nestedComponentDefs2[0]).isInstanceOf(PointcutComponentDefinition.class); PointcutComponentDefinition pcd = (PointcutComponentDefinition) nestedComponentDefs2[0]; - assertThat(pcd.getBeanDefinitions().length).as("Incorrect number of BeanDefinitions").isEqualTo(1); + assertThat(pcd.getBeanDefinitions()).as("Incorrect number of BeanDefinitions").hasSize(1); } } diff --git a/spring-aop/src/test/java/org/springframework/aop/config/AopNamespaceHandlerPointcutErrorTests.java b/spring-aop/src/test/java/org/springframework/aop/config/AopNamespaceHandlerPointcutErrorTests.java index 6a01b03ad7b2..db5fdd5fdc51 100644 --- a/spring-aop/src/test/java/org/springframework/aop/config/AopNamespaceHandlerPointcutErrorTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/config/AopNamespaceHandlerPointcutErrorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -30,10 +30,10 @@ * @author Mark Fisher * @author Chris Beams */ -public class AopNamespaceHandlerPointcutErrorTests { +class AopNamespaceHandlerPointcutErrorTests { @Test - public void testDuplicatePointcutConfig() { + void duplicatePointcutConfig() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(() -> new XmlBeanDefinitionReader(bf).loadBeanDefinitions( @@ -42,7 +42,7 @@ public void testDuplicatePointcutConfig() { } @Test - public void testMissingPointcutConfig() { + void missingPointcutConfig() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(() -> new XmlBeanDefinitionReader(bf).loadBeanDefinitions( diff --git a/spring-aop/src/test/java/org/springframework/aop/config/TopLevelAopTagTests.java b/spring-aop/src/test/java/org/springframework/aop/config/TopLevelAopTagTests.java index f1ab7f273db9..7e34bc23ef32 100644 --- a/spring-aop/src/test/java/org/springframework/aop/config/TopLevelAopTagTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/config/TopLevelAopTagTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -30,10 +30,10 @@ * @author Rob Harrop * @author Chris Beams */ -public class TopLevelAopTagTests { +class TopLevelAopTagTests { @Test - public void testParse() { + void parse() { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(beanFactory).loadBeanDefinitions( qualifiedResource(TopLevelAopTagTests.class, "context.xml")); diff --git a/spring-aop/src/test/java/org/springframework/aop/framework/AopProxyUtilsTests.java b/spring-aop/src/test/java/org/springframework/aop/framework/AopProxyUtilsTests.java index c8475794f337..9199b005712d 100644 --- a/spring-aop/src/test/java/org/springframework/aop/framework/AopProxyUtilsTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/framework/AopProxyUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -17,119 +17,139 @@ package org.springframework.aop.framework; import java.lang.reflect.Proxy; -import java.util.Arrays; -import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.aop.SpringProxy; import org.springframework.beans.testfixture.beans.ITestBean; import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.DecoratingProxy; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** + * Tests for {@link AopProxyUtils}. + * * @author Rod Johnson * @author Chris Beams + * @author Sam Brannen */ -public class AopProxyUtilsTests { +class AopProxyUtilsTests { @Test - public void testCompleteProxiedInterfacesWorksWithNull() { + void completeProxiedInterfacesWorksWithNull() { AdvisedSupport as = new AdvisedSupport(); Class[] completedInterfaces = AopProxyUtils.completeProxiedInterfaces(as); - assertThat(completedInterfaces.length).isEqualTo(2); - List ifaces = Arrays.asList(completedInterfaces); - assertThat(ifaces.contains(Advised.class)).isTrue(); - assertThat(ifaces.contains(SpringProxy.class)).isTrue(); + assertThat(completedInterfaces).containsExactly(SpringProxy.class, Advised.class); } @Test - public void testCompleteProxiedInterfacesWorksWithNullOpaque() { + void completeProxiedInterfacesWorksWithNullOpaque() { AdvisedSupport as = new AdvisedSupport(); as.setOpaque(true); Class[] completedInterfaces = AopProxyUtils.completeProxiedInterfaces(as); - assertThat(completedInterfaces.length).isEqualTo(1); + assertThat(completedInterfaces).containsExactly(SpringProxy.class); } @Test - public void testCompleteProxiedInterfacesAdvisedNotIncluded() { + void completeProxiedInterfacesAdvisedNotIncluded() { AdvisedSupport as = new AdvisedSupport(); as.addInterface(ITestBean.class); as.addInterface(Comparable.class); Class[] completedInterfaces = AopProxyUtils.completeProxiedInterfaces(as); - assertThat(completedInterfaces.length).isEqualTo(4); - - // Can't assume ordering for others, so use a list - List l = Arrays.asList(completedInterfaces); - assertThat(l.contains(Advised.class)).isTrue(); - assertThat(l.contains(ITestBean.class)).isTrue(); - assertThat(l.contains(Comparable.class)).isTrue(); + assertThat(completedInterfaces).containsExactly( + ITestBean.class, Comparable.class, SpringProxy.class, Advised.class); } @Test - public void testCompleteProxiedInterfacesAdvisedIncluded() { + void completeProxiedInterfacesAdvisedIncluded() { AdvisedSupport as = new AdvisedSupport(); + as.addInterface(Advised.class); as.addInterface(ITestBean.class); as.addInterface(Comparable.class); - as.addInterface(Advised.class); Class[] completedInterfaces = AopProxyUtils.completeProxiedInterfaces(as); - assertThat(completedInterfaces.length).isEqualTo(4); - - // Can't assume ordering for others, so use a list - List l = Arrays.asList(completedInterfaces); - assertThat(l.contains(Advised.class)).isTrue(); - assertThat(l.contains(ITestBean.class)).isTrue(); - assertThat(l.contains(Comparable.class)).isTrue(); + assertThat(completedInterfaces).containsExactly( + Advised.class, ITestBean.class, Comparable.class, SpringProxy.class); } @Test - public void testCompleteProxiedInterfacesAdvisedNotIncludedOpaque() { + void completeProxiedInterfacesAdvisedNotIncludedOpaque() { AdvisedSupport as = new AdvisedSupport(); as.setOpaque(true); as.addInterface(ITestBean.class); as.addInterface(Comparable.class); Class[] completedInterfaces = AopProxyUtils.completeProxiedInterfaces(as); - assertThat(completedInterfaces.length).isEqualTo(3); - - // Can't assume ordering for others, so use a list - List l = Arrays.asList(completedInterfaces); - assertThat(l.contains(Advised.class)).isFalse(); - assertThat(l.contains(ITestBean.class)).isTrue(); - assertThat(l.contains(Comparable.class)).isTrue(); + assertThat(completedInterfaces).containsExactly(ITestBean.class, Comparable.class, SpringProxy.class); } @Test - public void testProxiedUserInterfacesWithSingleInterface() { + void proxiedUserInterfacesWithSingleInterface() { ProxyFactory pf = new ProxyFactory(); pf.setTarget(new TestBean()); pf.addInterface(ITestBean.class); - Object proxy = pf.getProxy(); - Class[] userInterfaces = AopProxyUtils.proxiedUserInterfaces(proxy); - assertThat(userInterfaces.length).isEqualTo(1); - assertThat(userInterfaces[0]).isEqualTo(ITestBean.class); + Class[] userInterfaces = AopProxyUtils.proxiedUserInterfaces(pf.getProxy()); + assertThat(userInterfaces).containsExactly(ITestBean.class); } @Test - public void testProxiedUserInterfacesWithMultipleInterfaces() { + void proxiedUserInterfacesWithMultipleInterfaces() { ProxyFactory pf = new ProxyFactory(); pf.setTarget(new TestBean()); pf.addInterface(ITestBean.class); pf.addInterface(Comparable.class); - Object proxy = pf.getProxy(); - Class[] userInterfaces = AopProxyUtils.proxiedUserInterfaces(proxy); - assertThat(userInterfaces.length).isEqualTo(2); - assertThat(userInterfaces[0]).isEqualTo(ITestBean.class); - assertThat(userInterfaces[1]).isEqualTo(Comparable.class); + Class[] userInterfaces = AopProxyUtils.proxiedUserInterfaces(pf.getProxy()); + assertThat(userInterfaces).containsExactly(ITestBean.class, Comparable.class); } @Test - public void testProxiedUserInterfacesWithNoInterface() { + void proxiedUserInterfacesWithNoInterface() { Object proxy = Proxy.newProxyInstance(getClass().getClassLoader(), new Class[0], (proxy1, method, args) -> null); - assertThatIllegalArgumentException().isThrownBy(() -> - AopProxyUtils.proxiedUserInterfaces(proxy)); + assertThatIllegalArgumentException().isThrownBy(() -> AopProxyUtils.proxiedUserInterfaces(proxy)); + } + + @Test + void completeJdkProxyInterfacesFromNullInterface() { + assertThatIllegalArgumentException() + .isThrownBy(() -> AopProxyUtils.completeJdkProxyInterfaces(ITestBean.class, null, Comparable.class)) + .withMessage("'userInterfaces' must not contain null values"); + } + + @Test + void completeJdkProxyInterfacesFromClassThatIsNotAnInterface() { + assertThatIllegalArgumentException() + .isThrownBy(() -> AopProxyUtils.completeJdkProxyInterfaces(TestBean.class)) + .withMessage(TestBean.class.getName() + " must be a non-sealed interface"); + } + + @Test + void completeJdkProxyInterfacesFromSealedInterface() { + assertThatIllegalArgumentException() + .isThrownBy(() -> AopProxyUtils.completeJdkProxyInterfaces(SealedInterface.class)) + .withMessage(SealedInterface.class.getName() + " must be a non-sealed interface"); + } + + @Test + void completeJdkProxyInterfacesFromSingleClass() { + Class[] jdkProxyInterfaces = AopProxyUtils.completeJdkProxyInterfaces(ITestBean.class); + assertThat(jdkProxyInterfaces).containsExactly( + ITestBean.class, SpringProxy.class, Advised.class, DecoratingProxy.class); + } + + @Test + void completeJdkProxyInterfacesFromMultipleClasses() { + Class[] jdkProxyInterfaces = AopProxyUtils.completeJdkProxyInterfaces(ITestBean.class, Comparable.class); + assertThat(jdkProxyInterfaces).containsExactly( + ITestBean.class, Comparable.class, SpringProxy.class, Advised.class, DecoratingProxy.class); + } + + + sealed interface SealedInterface { + } + + @SuppressWarnings("unused") + static final class SealedClass implements SealedInterface { } } diff --git a/spring-aop/src/test/java/org/springframework/aop/framework/IntroductionBenchmarkTests.java b/spring-aop/src/test/java/org/springframework/aop/framework/IntroductionBenchmarkTests.java index d3defccfb429..aa9b1706f71a 100644 --- a/spring-aop/src/test/java/org/springframework/aop/framework/IntroductionBenchmarkTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/framework/IntroductionBenchmarkTests.java @@ -48,7 +48,7 @@ public int getCount() { } } - public static interface Counter { + public interface Counter { int getCount(); } diff --git a/spring-aop/src/test/java/org/springframework/aop/framework/ProxyFactoryTests.java b/spring-aop/src/test/java/org/springframework/aop/framework/ProxyFactoryTests.java index 2ae1d635116d..88da7bf92738 100644 --- a/spring-aop/src/test/java/org/springframework/aop/framework/ProxyFactoryTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/framework/ProxyFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -44,7 +44,7 @@ import org.springframework.core.testfixture.TimeStamped; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatRuntimeException; /** * Also tests AdvisedSupport and ProxyCreatorSupport superclasses. @@ -183,7 +183,7 @@ public void testAddRepeatedInterface() { } @Test - public void testGetsAllInterfaces() throws Exception { + public void testGetsAllInterfaces() { // Extend to get new interface class TestBeanSubclass extends TestBean implements Comparable { @Override @@ -240,6 +240,16 @@ public Object invoke(MethodInvocation invocation) throws Throwable { assertThat(factory.countAdvicesOfType(NopInterceptor.class) == 2).isTrue(); } + @Test + public void testSealedInterfaceExclusion() { + // String implements ConstantDesc on JDK 12+, sealed as of JDK 17 + ProxyFactory factory = new ProxyFactory(new String()); + NopInterceptor di = new NopInterceptor(); + factory.addAdvice(0, di); + Object proxy = factory.getProxy(); + assertThat(proxy).isInstanceOf(CharSequence.class); + } + /** * Should see effect immediately on behavior. */ @@ -267,7 +277,7 @@ public void testCanAddAndRemoveAspectInterfacesOnSingleton() { assertThat(config.getAdvisors().length == oldCount).isTrue(); - assertThatExceptionOfType(RuntimeException.class) + assertThatRuntimeException() .as("Existing object won't implement this interface any more") .isThrownBy(ts::getTimeStamp); // Existing reference will fail diff --git a/spring-aop/src/test/java/org/springframework/aop/framework/adapter/ThrowsAdviceInterceptorTests.java b/spring-aop/src/test/java/org/springframework/aop/framework/adapter/ThrowsAdviceInterceptorTests.java index 2c1060274e74..40b038b457ef 100644 --- a/spring-aop/src/test/java/org/springframework/aop/framework/adapter/ThrowsAdviceInterceptorTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/framework/adapter/ThrowsAdviceInterceptorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -26,6 +26,7 @@ import org.springframework.aop.testfixture.advice.MyThrowsHandler; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatException; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.BDDMockito.given; @@ -63,9 +64,7 @@ public void testNoHandlerMethodForThrowable() throws Throwable { Exception ex = new Exception(); MethodInvocation mi = mock(MethodInvocation.class); given(mi.proceed()).willThrow(ex); - assertThatExceptionOfType(Exception.class).isThrownBy(() -> - ti.invoke(mi)) - .isSameAs(ex); + assertThatException().isThrownBy(() -> ti.invoke(mi)).isSameAs(ex); assertThat(th.getCalls()).isEqualTo(0); } diff --git a/spring-aop/src/test/java/org/springframework/aop/interceptor/ConcurrencyThrottleInterceptorTests.java b/spring-aop/src/test/java/org/springframework/aop/interceptor/ConcurrencyThrottleInterceptorTests.java index 9936573300d4..d135689decca 100644 --- a/spring-aop/src/test/java/org/springframework/aop/interceptor/ConcurrencyThrottleInterceptorTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/interceptor/ConcurrencyThrottleInterceptorTests.java @@ -125,16 +125,7 @@ public void run() { try { this.proxy.exceptional(this.ex); } - catch (RuntimeException ex) { - if (ex == this.ex) { - logger.debug("Expected exception thrown", ex); - } - else { - // should never happen - ex.printStackTrace(); - } - } - catch (Error err) { + catch (RuntimeException | Error err) { if (err == this.ex) { logger.debug("Expected exception thrown", err); } diff --git a/spring-aop/src/test/java/org/springframework/aop/interceptor/JamonPerformanceMonitorInterceptorTests.java b/spring-aop/src/test/java/org/springframework/aop/interceptor/JamonPerformanceMonitorInterceptorTests.java deleted file mode 100644 index 18556d6df509..000000000000 --- a/spring-aop/src/test/java/org/springframework/aop/interceptor/JamonPerformanceMonitorInterceptorTests.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2002-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.aop.interceptor; - -import com.jamonapi.MonitorFactory; -import org.aopalliance.intercept.MethodInvocation; -import org.apache.commons.logging.Log; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; - -/** - * @author Steve Souza - * @since 4.1 - */ -public class JamonPerformanceMonitorInterceptorTests { - - private final JamonPerformanceMonitorInterceptor interceptor = new JamonPerformanceMonitorInterceptor(); - - private final MethodInvocation mi = mock(MethodInvocation.class); - - private final Log log = mock(Log.class); - - - @BeforeEach - public void setUp() { - MonitorFactory.reset(); - } - - @AfterEach - public void tearDown() { - MonitorFactory.reset(); - } - - - @Test - public void testInvokeUnderTraceWithNormalProcessing() throws Throwable { - given(mi.getMethod()).willReturn(String.class.getMethod("toString")); - - interceptor.invokeUnderTrace(mi, log); - - assertThat(MonitorFactory.getNumRows() > 0).as("jamon must track the method being invoked").isTrue(); - assertThat(MonitorFactory.getReport().contains("toString")).as("The jamon report must contain the toString method that was invoked").isTrue(); - } - - @Test - public void testInvokeUnderTraceWithExceptionTracking() throws Throwable { - given(mi.getMethod()).willReturn(String.class.getMethod("toString")); - given(mi.proceed()).willThrow(new IllegalArgumentException()); - - assertThatIllegalArgumentException().isThrownBy(() -> - interceptor.invokeUnderTrace(mi, log)); - - assertThat(MonitorFactory.getNumRows()).as("Monitors must exist for the method invocation and 2 exceptions").isEqualTo(3); - assertThat(MonitorFactory.getReport().contains("toString")).as("The jamon report must contain the toString method that was invoked").isTrue(); - assertThat(MonitorFactory.getReport().contains(MonitorFactory.EXCEPTIONS_LABEL)).as("The jamon report must contain the generic exception: " + MonitorFactory.EXCEPTIONS_LABEL).isTrue(); - assertThat(MonitorFactory.getReport().contains("IllegalArgumentException")).as("The jamon report must contain the specific exception: IllegalArgumentException'").isTrue(); - } - -} diff --git a/spring-aop/src/test/java/org/springframework/aop/scope/ScopedProxyBeanRegistrationAotProcessorTests.java b/spring-aop/src/test/java/org/springframework/aop/scope/ScopedProxyBeanRegistrationAotProcessorTests.java new file mode 100644 index 000000000000..6760391892f4 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/scope/ScopedProxyBeanRegistrationAotProcessorTests.java @@ -0,0 +1,162 @@ +/* + * Copyright 2002-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.aop.scope; + +import java.util.Properties; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import javax.lang.model.element.Modifier; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.framework.AopInfrastructureBean; +import org.springframework.aot.generate.MethodReference; +import org.springframework.aot.generate.MethodReference.ArgumentCodeGenerator; +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.aot.AotServices; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; +import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; +import org.springframework.beans.factory.aot.TestBeanRegistrationsAotProcessor; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.PropertiesFactoryBean; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.factory.aot.MockBeanFactoryInitializationCode; +import org.springframework.beans.testfixture.beans.factory.generator.factory.NumberHolder; +import org.springframework.core.ResolvableType; +import org.springframework.core.test.tools.Compiled; +import org.springframework.core.test.tools.TestCompiler; +import org.springframework.javapoet.CodeBlock; +import org.springframework.javapoet.MethodSpec; +import org.springframework.javapoet.ParameterizedTypeName; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link ScopedProxyBeanRegistrationAotProcessor}. + * + * @author Stephane Nicoll + * @author Phillip Webb + * @since 6.0 + */ +class ScopedProxyBeanRegistrationAotProcessorTests { + + private final DefaultListableBeanFactory beanFactory; + + private final TestBeanRegistrationsAotProcessor processor; + + private final TestGenerationContext generationContext; + + private final MockBeanFactoryInitializationCode beanFactoryInitializationCode; + + + ScopedProxyBeanRegistrationAotProcessorTests() { + this.beanFactory = new DefaultListableBeanFactory(); + this.processor = new TestBeanRegistrationsAotProcessor(); + this.generationContext = new TestGenerationContext(); + this.beanFactoryInitializationCode = new MockBeanFactoryInitializationCode(this.generationContext); + } + + + @Test + void scopedProxyBeanRegistrationAotProcessorIsRegistered() { + assertThat(AotServices.factoriesAndBeans(this.beanFactory).load(BeanRegistrationAotProcessor.class)) + .anyMatch(ScopedProxyBeanRegistrationAotProcessor.class::isInstance); + } + + @Test + void getBeanRegistrationCodeGeneratorWhenNotScopedProxy() { + BeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(PropertiesFactoryBean.class).getBeanDefinition(); + this.beanFactory.registerBeanDefinition("test", beanDefinition); + compile((freshBeanFactory, compiled) -> { + Object bean = freshBeanFactory.getBean("test"); + assertThat(bean).isInstanceOf(Properties.class); + }); + } + + @Test + void getBeanRegistrationCodeGeneratorWhenScopedProxyWithoutTargetBeanName() { + BeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(ScopedProxyFactoryBean.class).getBeanDefinition(); + this.beanFactory.registerBeanDefinition("test", beanDefinition); + compile((freshBeanFactory, compiled) -> + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> + freshBeanFactory.getBean("test")).withMessageContaining("'targetBeanName' is required")); + } + + @Test + void getBeanRegistrationCodeGeneratorWhenScopedProxyWithInvalidTargetBeanName() { + BeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(ScopedProxyFactoryBean.class) + .addPropertyValue("targetBeanName", "testDoesNotExist") + .getBeanDefinition(); + this.beanFactory.registerBeanDefinition("test", beanDefinition); + compile((freshBeanFactory, compiled) -> + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> + freshBeanFactory.getBean("test")).withMessageContaining("No bean named 'testDoesNotExist'")); + } + + @Test + void getBeanRegistrationCodeGeneratorWhenScopedProxyWithTargetBeanName() { + RootBeanDefinition targetBean = new RootBeanDefinition(); + targetBean.setTargetType( + ResolvableType.forClassWithGenerics(NumberHolder.class, Integer.class)); + targetBean.setScope("custom"); + this.beanFactory.registerBeanDefinition("numberHolder", targetBean); + BeanDefinition scopedBean = BeanDefinitionBuilder + .rootBeanDefinition(ScopedProxyFactoryBean.class) + .addPropertyValue("targetBeanName", "numberHolder").getBeanDefinition(); + this.beanFactory.registerBeanDefinition("test", scopedBean); + compile((freshBeanFactory, compiled) -> { + Object bean = freshBeanFactory.getBean("test"); + assertThat(bean).isNotNull().isInstanceOf(NumberHolder.class).isInstanceOf(AopInfrastructureBean.class); + }); + } + + @SuppressWarnings("unchecked") + private void compile(BiConsumer result) { + BeanFactoryInitializationAotContribution contribution = this.processor.processAheadOfTime(this.beanFactory); + assertThat(contribution).isNotNull(); + contribution.applyTo(this.generationContext, this.beanFactoryInitializationCode); + MethodReference methodReference = this.beanFactoryInitializationCode + .getInitializers().get(0); + this.beanFactoryInitializationCode.getTypeBuilder().set(type -> { + CodeBlock methodInvocation = methodReference.toInvokeCodeBlock( + ArgumentCodeGenerator.of(DefaultListableBeanFactory.class, "beanFactory"), + this.beanFactoryInitializationCode.getClassName()); + type.addModifiers(Modifier.PUBLIC); + type.addSuperinterface(ParameterizedTypeName.get(Consumer.class, DefaultListableBeanFactory.class)); + type.addMethod(MethodSpec.methodBuilder("accept").addModifiers(Modifier.PUBLIC) + .addParameter(DefaultListableBeanFactory.class, "beanFactory") + .addStatement(methodInvocation) + .build()); + }); + this.generationContext.writeGeneratedContent(); + TestCompiler.forSystem().with(this.generationContext).compile(compiled -> { + DefaultListableBeanFactory freshBeanFactory = new DefaultListableBeanFactory(); + freshBeanFactory.setBeanClassLoader(compiled.getClassLoader()); + compiled.getInstance(Consumer.class).accept(freshBeanFactory); + result.accept(freshBeanFactory, compiled); + }); + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/support/ClassUtilsTests.java b/spring-aop/src/test/java/org/springframework/aop/support/ClassUtilsTests.java index 6eac64eb77b8..f1fffbdf9cb4 100644 --- a/spring-aop/src/test/java/org/springframework/aop/support/ClassUtilsTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/support/ClassUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.aop.support; import org.junit.jupiter.api.Test; @@ -29,10 +30,10 @@ * @author Rob Harrop * @author Rick Evans */ -public class ClassUtilsTests { +class ClassUtilsTests { @Test - public void getShortNameForCglibClass() { + void getShortNameForCglibClass() { TestBean tb = new TestBean(); ProxyFactory pf = new ProxyFactory(); pf.setTarget(tb); @@ -41,4 +42,5 @@ public void getShortNameForCglibClass() { String className = ClassUtils.getShortName(proxy.getClass()); assertThat(className).as("Class name did not match").isEqualTo("TestBean"); } + } diff --git a/spring-aop/src/test/java/org/springframework/aop/support/DelegatingIntroductionInterceptorTests.java b/spring-aop/src/test/java/org/springframework/aop/support/DelegatingIntroductionInterceptorTests.java index 3e6e08afc1ca..b5100f719ff0 100644 --- a/spring-aop/src/test/java/org/springframework/aop/support/DelegatingIntroductionInterceptorTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/support/DelegatingIntroductionInterceptorTests.java @@ -307,7 +307,7 @@ public interface ITester { } - private static interface SubTimeStamped extends TimeStamped { + private interface SubTimeStamped extends TimeStamped { } } diff --git a/spring-aop/src/test/java/org/springframework/aop/target/PrototypeBasedTargetSourceTests.java b/spring-aop/src/test/java/org/springframework/aop/target/PrototypeBasedTargetSourceTests.java index 24af32140c75..d2dcf75ee9ca 100644 --- a/spring-aop/src/test/java/org/springframework/aop/target/PrototypeBasedTargetSourceTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/target/PrototypeBasedTargetSourceTests.java @@ -71,7 +71,7 @@ private static class TestTargetSource extends AbstractPrototypeBasedTargetSource * Nonserializable test field to check that subclass * state can't prevent serialization from working */ - @SuppressWarnings("unused") + @SuppressWarnings({"unused", "serial"}) private TestBean thisFieldIsNotSerializable = new TestBean(); @Override diff --git a/spring-aop/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerEventTests-context.xml b/spring-aop/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerEventTests-context.xml index 64f222cdd372..1c2ba7c237b5 100644 --- a/spring-aop/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerEventTests-context.xml +++ b/spring-aop/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerEventTests-context.xml @@ -16,12 +16,6 @@ - - - - - - diff --git a/spring-aop/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerEventTests-directPointcutEvents.xml b/spring-aop/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerEventTests-directPointcutEvents.xml index 852f7479377e..2f2024ada6cd 100644 --- a/spring-aop/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerEventTests-directPointcutEvents.xml +++ b/spring-aop/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerEventTests-directPointcutEvents.xml @@ -9,6 +9,6 @@ - + diff --git a/spring-aop/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerEventTests-pointcutRefEvents.xml b/spring-aop/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerEventTests-pointcutRefEvents.xml index 8300b280512d..0b12eb945e5d 100644 --- a/spring-aop/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerEventTests-pointcutRefEvents.xml +++ b/spring-aop/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerEventTests-pointcutRefEvents.xml @@ -10,6 +10,6 @@ - + diff --git a/spring-aop/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerPointcutErrorTests-pointcutDuplication.xml b/spring-aop/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerPointcutErrorTests-pointcutDuplication.xml index 33693b7c1c40..75ca4e9d364b 100644 --- a/spring-aop/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerPointcutErrorTests-pointcutDuplication.xml +++ b/spring-aop/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerPointcutErrorTests-pointcutDuplication.xml @@ -12,10 +12,6 @@ - - - - diff --git a/spring-aop/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerPointcutErrorTests-pointcutMissing.xml b/spring-aop/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerPointcutErrorTests-pointcutMissing.xml index 3d2037805e41..0524b1071c61 100644 --- a/spring-aop/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerPointcutErrorTests-pointcutMissing.xml +++ b/spring-aop/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerPointcutErrorTests-pointcutMissing.xml @@ -12,10 +12,6 @@ - - - - diff --git a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/advice/MethodCounter.java b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/advice/MethodCounter.java index 384973bd9aa7..3cdba737e89f 100644 --- a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/advice/MethodCounter.java +++ b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/advice/MethodCounter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -19,6 +19,7 @@ import java.io.Serializable; import java.lang.reflect.Method; import java.util.HashMap; +import java.util.Map; /** * Abstract superclass for counting advices etc. @@ -31,7 +32,7 @@ public class MethodCounter implements Serializable { /** Method name --> count, does not understand overloading */ - private HashMap map = new HashMap<>(); + private Map map = new HashMap<>(); private int allCount; diff --git a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/scope/SimpleTarget.java b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/scope/SimpleTarget.java new file mode 100644 index 000000000000..ee0370d73520 --- /dev/null +++ b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/scope/SimpleTarget.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-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.aop.testfixture.scope; + +public class SimpleTarget { +} diff --git a/spring-aspects/spring-aspects.gradle b/spring-aspects/spring-aspects.gradle index 12adbfb7e5f4..bd4e49ff638b 100644 --- a/spring-aspects/spring-aspects.gradle +++ b/spring-aspects/spring-aspects.gradle @@ -8,10 +8,17 @@ sourceSets.main.java.srcDirs = files() sourceSets.test.aspectj.srcDir "src/test/java" sourceSets.test.java.srcDirs = files() -aspectj.version = dependencyManagement.managedVersions['org.aspectj:aspectjweaver'] +compileAspectj { + sourceCompatibility "17" + targetCompatibility "17" +} +compileTestAspectj { + sourceCompatibility "17" + targetCompatibility "17" +} dependencies { - compile("org.aspectj:aspectjweaver") + api("org.aspectj:aspectjweaver") compileOnly("org.aspectj:aspectjrt") optional(project(":spring-aop")) // for @Async support optional(project(":spring-beans")) // for @Configurable support @@ -20,14 +27,14 @@ dependencies { optional(project(":spring-orm")) // for JPA exception translation support optional(project(":spring-tx")) // for JPA, @Transactional support optional("javax.cache:cache-api") // for JCache aspect - optional("javax.transaction:javax.transaction-api") // for @javax.transaction.Transactional support - testCompile(project(":spring-core")) // for CodeStyleAspect - testCompile(project(":spring-test")) - testCompile(testFixtures(project(":spring-context"))) - testCompile(testFixtures(project(":spring-context-support"))) - testCompile(testFixtures(project(":spring-core"))) - testCompile(testFixtures(project(":spring-tx"))) - testCompile("javax.mail:javax.mail-api") + optional("jakarta.transaction:jakarta.transaction-api") // for @jakarta.transaction.Transactional support + testImplementation(project(":spring-core")) // for CodeStyleAspect + testImplementation(project(":spring-test")) + testImplementation(testFixtures(project(":spring-context"))) + testImplementation(testFixtures(project(":spring-context-support"))) + testImplementation(testFixtures(project(":spring-core"))) + testImplementation(testFixtures(project(":spring-tx"))) + testImplementation("jakarta.mail:jakarta.mail-api") testCompileOnly("org.aspectj:aspectjrt") } diff --git a/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/AbstractDependencyInjectionAspect.aj b/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/AbstractDependencyInjectionAspect.aj index 94f9f8c6ded5..19ba5e5b0530 100644 --- a/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/AbstractDependencyInjectionAspect.aj +++ b/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/AbstractDependencyInjectionAspect.aj @@ -35,7 +35,7 @@ public abstract aspect AbstractDependencyInjectionAspect { mostSpecificSubTypeConstruction() && !preConstructionConfiguration(); /** - * Select least specific super type that is marked for DI + * Select least specific supertype that is marked for DI * (so that injection occurs only once with pre-construction injection). */ public abstract pointcut leastSpecificSuperTypeConstruction(); diff --git a/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/ConfigurableObject.java b/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/ConfigurableObject.java index c670dfc06927..02e0d6391fb4 100644 --- a/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/ConfigurableObject.java +++ b/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/ConfigurableObject.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-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. @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.beans.factory.aspectj; /** - * Marker interface for domain object that need DI through aspects. + * Marker interface for domain objects that need DI through aspects. * * @author Ramnivas Laddad * @since 2.5 */ public interface ConfigurableObject { - } diff --git a/spring-aspects/src/main/java/org/springframework/context/annotation/aspectj/EnableSpringConfigured.java b/spring-aspects/src/main/java/org/springframework/context/annotation/aspectj/EnableSpringConfigured.java index e26a94f71b55..0f362d91e8f0 100644 --- a/spring-aspects/src/main/java/org/springframework/context/annotation/aspectj/EnableSpringConfigured.java +++ b/spring-aspects/src/main/java/org/springframework/context/annotation/aspectj/EnableSpringConfigured.java @@ -26,7 +26,7 @@ /** * Signals the current application context to apply dependency injection to - * non-managed classes that are instantiated outside of the Spring bean factory + * non-managed classes that are instantiated outside the Spring bean factory * (typically classes annotated with the * {@link org.springframework.beans.factory.annotation.Configurable @Configurable} * annotation). diff --git a/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AbstractTransactionAspect.aj b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AbstractTransactionAspect.aj index aed8e4ab65a8..782ca35e0777 100644 --- a/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AbstractTransactionAspect.aj +++ b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AbstractTransactionAspect.aj @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-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. @@ -59,7 +59,8 @@ public abstract aspect AbstractTransactionAspect extends TransactionAspectSuppor @Override public void destroy() { - clearTransactionManagerCache(); // An aspect is basically a singleton + // An aspect is basically a singleton -> cleanup on destruction + clearTransactionManagerCache(); } @SuppressAjWarnings("adviceDidNotMatch") diff --git a/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJJtaTransactionManagementConfiguration.java b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJJtaTransactionManagementConfiguration.java index ec733d3cf2b9..0ed7ffb69eb8 100644 --- a/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJJtaTransactionManagementConfiguration.java +++ b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJJtaTransactionManagementConfiguration.java @@ -27,7 +27,7 @@ /** * {@code @Configuration} class that registers the Spring infrastructure beans necessary * to enable AspectJ-based annotation-driven transaction management for the JTA 1.2 - * {@link javax.transaction.Transactional} annotation in addition to Spring's own + * {@link jakarta.transaction.Transactional} annotation in addition to Spring's own * {@link org.springframework.transaction.annotation.Transactional} annotation. * * @author Juergen Hoeller diff --git a/spring-aspects/src/main/java/org/springframework/transaction/aspectj/JtaAnnotationTransactionAspect.aj b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/JtaAnnotationTransactionAspect.aj index 1644ce50651f..8b374ea0d86e 100644 --- a/spring-aspects/src/main/java/org/springframework/transaction/aspectj/JtaAnnotationTransactionAspect.aj +++ b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/JtaAnnotationTransactionAspect.aj @@ -16,7 +16,7 @@ package org.springframework.transaction.aspectj; -import javax.transaction.Transactional; +import jakarta.transaction.Transactional; import org.aspectj.lang.annotation.RequiredTypes; @@ -24,7 +24,7 @@ import org.springframework.transaction.annotation.AnnotationTransactionAttribute /** * Concrete AspectJ transaction aspect using the JTA 1.2 - * {@link javax.transaction.Transactional} annotation. + * {@link jakarta.transaction.Transactional} annotation. * *

    When using this aspect, you must annotate the implementation class * (and/or methods within that class), not the interface (if any) that @@ -42,10 +42,10 @@ import org.springframework.transaction.annotation.AnnotationTransactionAttribute * * @author Stephane Nicoll * @since 4.2 - * @see javax.transaction.Transactional + * @see jakarta.transaction.Transactional * @see AnnotationTransactionAspect */ -@RequiredTypes("javax.transaction.Transactional") +@RequiredTypes("jakarta.transaction.Transactional") public aspect JtaAnnotationTransactionAspect extends AbstractTransactionAspect { public JtaAnnotationTransactionAspect() { diff --git a/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJEnableCachingIsolatedTests.java b/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJEnableCachingIsolatedTests.java index 7ca1037efbc4..6c16c26364a6 100644 --- a/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJEnableCachingIsolatedTests.java +++ b/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJEnableCachingIsolatedTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -20,9 +20,8 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.BeanCreationException; import org.springframework.cache.CacheManager; -import org.springframework.cache.annotation.CachingConfigurerSupport; +import org.springframework.cache.annotation.CachingConfigurer; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.interceptor.CacheErrorHandler; import org.springframework.cache.interceptor.CacheResolver; @@ -107,10 +106,7 @@ public void multipleCachingConfigurers() { try { load(MultiCacheManagerConfigurer.class, EnableCachingConfig.class); } - catch (BeanCreationException ex) { - Throwable root = ex.getRootCause(); - boolean condition = root instanceof IllegalStateException; - assertThat(condition).isTrue(); + catch (IllegalStateException ex) { assertThat(ex.getMessage().contains("implementations of CachingConfigurer")).isTrue(); } } @@ -147,7 +143,7 @@ public void bothSetOnlyResolverIsUsed() { @Configuration @EnableCaching(mode = AdviceMode.ASPECTJ) - static class EnableCachingConfig extends CachingConfigurerSupport { + static class EnableCachingConfig implements CachingConfigurer { @Override @Bean @@ -224,7 +220,7 @@ public CacheManager cm2() { @Configuration @EnableCaching(mode = AdviceMode.ASPECTJ) - static class MultiCacheManagerConfigurer extends CachingConfigurerSupport { + static class MultiCacheManagerConfigurer implements CachingConfigurer { @Bean public CacheManager cm1() { @@ -250,7 +246,7 @@ public KeyGenerator keyGenerator() { @Configuration @EnableCaching(mode = AdviceMode.ASPECTJ) - static class EmptyConfigSupportConfig extends CachingConfigurerSupport { + static class EmptyConfigSupportConfig implements CachingConfigurer { @Bean public CacheManager cm() { @@ -262,7 +258,7 @@ public CacheManager cm() { @Configuration @EnableCaching(mode = AdviceMode.ASPECTJ) - static class FullCachingConfig extends CachingConfigurerSupport { + static class FullCachingConfig implements CachingConfigurer { @Override @Bean diff --git a/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJEnableCachingTests.java b/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJEnableCachingTests.java index 7e693d6ea13a..4c5f7b414ab5 100644 --- a/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJEnableCachingTests.java +++ b/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJEnableCachingTests.java @@ -17,7 +17,7 @@ package org.springframework.cache.aspectj; import org.springframework.cache.CacheManager; -import org.springframework.cache.annotation.CachingConfigurerSupport; +import org.springframework.cache.annotation.CachingConfigurer; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.config.AnnotatedClassCacheableService; import org.springframework.cache.config.CacheableService; @@ -47,7 +47,7 @@ protected ConfigurableApplicationContext getApplicationContext() { @Configuration @EnableCaching(mode = AdviceMode.ASPECTJ) - static class EnableCachingConfig extends CachingConfigurerSupport { + static class EnableCachingConfig implements CachingConfigurer { @Override @Bean diff --git a/spring-aspects/src/test/java/org/springframework/cache/aspectj/JCacheAspectJNamespaceConfigTests.java b/spring-aspects/src/test/java/org/springframework/cache/aspectj/JCacheAspectJNamespaceConfigTests.java index 81ba6d0faff9..c755d8c3f4aa 100644 --- a/spring-aspects/src/test/java/org/springframework/cache/aspectj/JCacheAspectJNamespaceConfigTests.java +++ b/spring-aspects/src/test/java/org/springframework/cache/aspectj/JCacheAspectJNamespaceConfigTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-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. @@ -22,13 +22,18 @@ /** * @author Stephane Nicoll + * @author Sam Brannen */ public class JCacheAspectJNamespaceConfigTests extends AbstractJCacheAnnotationTests { @Override protected ApplicationContext getApplicationContext() { - return new GenericXmlApplicationContext( - "/org/springframework/cache/config/annotation-jcache-aspectj.xml"); + GenericXmlApplicationContext context = new GenericXmlApplicationContext(); + // Disallow bean definition overriding to test https://github.com/spring-projects/spring-framework/pull/27499 + context.setAllowBeanDefinitionOverriding(false); + context.load("/org/springframework/cache/config/annotation-jcache-aspectj.xml"); + context.refresh(); + return context; } } diff --git a/spring-aspects/src/test/java/org/springframework/transaction/aspectj/ClassWithPrivateAnnotatedMember.java b/spring-aspects/src/test/java/org/springframework/transaction/aspectj/ClassWithPrivateAnnotatedMember.java index 7d5b2e6d8376..d0d824c1db73 100644 --- a/spring-aspects/src/test/java/org/springframework/transaction/aspectj/ClassWithPrivateAnnotatedMember.java +++ b/spring-aspects/src/test/java/org/springframework/transaction/aspectj/ClassWithPrivateAnnotatedMember.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.transaction.aspectj; import org.springframework.transaction.annotation.Transactional; @@ -29,4 +30,5 @@ public void doSomething() { @Transactional private void doInTransaction() {} + } diff --git a/spring-aspects/src/test/java/org/springframework/transaction/aspectj/ClassWithProtectedAnnotatedMember.java b/spring-aspects/src/test/java/org/springframework/transaction/aspectj/ClassWithProtectedAnnotatedMember.java index 359eab233cb0..12c5db965d87 100644 --- a/spring-aspects/src/test/java/org/springframework/transaction/aspectj/ClassWithProtectedAnnotatedMember.java +++ b/spring-aspects/src/test/java/org/springframework/transaction/aspectj/ClassWithProtectedAnnotatedMember.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.transaction.aspectj; import org.springframework.transaction.annotation.Transactional; @@ -29,4 +30,5 @@ public void doSomething() { @Transactional protected void doInTransaction() {} + } diff --git a/spring-aspects/src/test/java/org/springframework/transaction/aspectj/JtaTransactionAspectsTests.java b/spring-aspects/src/test/java/org/springframework/transaction/aspectj/JtaTransactionAspectsTests.java index e0bd918a72ea..b6ef121a7962 100644 --- a/spring-aspects/src/test/java/org/springframework/transaction/aspectj/JtaTransactionAspectsTests.java +++ b/spring-aspects/src/test/java/org/springframework/transaction/aspectj/JtaTransactionAspectsTests.java @@ -18,8 +18,7 @@ import java.io.IOException; -import javax.transaction.Transactional; - +import jakarta.transaction.Transactional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/spring-aspects/src/test/java/org/springframework/transaction/aspectj/TransactionAspectTests.java b/spring-aspects/src/test/java/org/springframework/transaction/aspectj/TransactionAspectTests.java index 722c357d20e2..2d92b8a59b79 100644 --- a/spring-aspects/src/test/java/org/springframework/transaction/aspectj/TransactionAspectTests.java +++ b/spring-aspects/src/test/java/org/springframework/transaction/aspectj/TransactionAspectTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -22,7 +22,9 @@ import org.springframework.transaction.testfixture.CallCountingTransactionManager; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatException; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatRuntimeException; /** * @author Rod Johnson @@ -104,44 +106,44 @@ public void notTransactional() throws Throwable { @Test public void defaultCommitOnAnnotatedClass() throws Throwable { Exception ex = new Exception(); - assertThatExceptionOfType(Exception.class).isThrownBy(() -> - testRollback(() -> annotationOnlyOnClassWithNoInterface.echo(ex), false)) + assertThatException() + .isThrownBy(() -> testRollback(() -> annotationOnlyOnClassWithNoInterface.echo(ex), false)) .isSameAs(ex); } @Test public void defaultRollbackOnAnnotatedClass() throws Throwable { RuntimeException ex = new RuntimeException(); - assertThatExceptionOfType(RuntimeException.class).isThrownBy(() -> - testRollback(() -> annotationOnlyOnClassWithNoInterface.echo(ex), true)) + assertThatRuntimeException() + .isThrownBy(() -> testRollback(() -> annotationOnlyOnClassWithNoInterface.echo(ex), true)) .isSameAs(ex); } @Test public void defaultCommitOnSubclassOfAnnotatedClass() throws Throwable { Exception ex = new Exception(); - assertThatExceptionOfType(Exception.class).isThrownBy(() -> - testRollback(() -> new SubclassOfClassWithTransactionalAnnotation().echo(ex), false)) + assertThatException() + .isThrownBy(() -> testRollback(() -> new SubclassOfClassWithTransactionalAnnotation().echo(ex), false)) .isSameAs(ex); } @Test public void defaultCommitOnSubclassOfClassWithTransactionalMethodAnnotated() throws Throwable { Exception ex = new Exception(); - assertThatExceptionOfType(Exception.class).isThrownBy(() -> - testRollback(() -> new SubclassOfClassWithTransactionalMethodAnnotation().echo(ex), false)) + assertThatException() + .isThrownBy(() -> testRollback(() -> new SubclassOfClassWithTransactionalMethodAnnotation().echo(ex), false)) .isSameAs(ex); } @Test public void noCommitOnImplementationOfAnnotatedInterface() throws Throwable { - final Exception ex = new Exception(); + Exception ex = new Exception(); testNotTransactional(() -> new ImplementsAnnotatedInterface().echo(ex), ex); } @Test public void noRollbackOnImplementationOfAnnotatedInterface() throws Throwable { - final Exception rollbackProvokingException = new RuntimeException(); + Exception rollbackProvokingException = new RuntimeException(); testNotTransactional(() -> new ImplementsAnnotatedInterface().echo(rollbackProvokingException), rollbackProvokingException); } @@ -165,8 +167,9 @@ protected void testRollback(TransactionOperationCallback toc, boolean rollback) protected void testNotTransactional(TransactionOperationCallback toc, Throwable expected) throws Throwable { txManager.clear(); assertThat(txManager.begun).isEqualTo(0); - assertThatExceptionOfType(Throwable.class).isThrownBy( - toc::performTransactionalOperation).isSameAs(expected); + assertThatExceptionOfType(Throwable.class) + .isThrownBy(toc::performTransactionalOperation) + .isSameAs(expected); assertThat(txManager.begun).isEqualTo(0); } diff --git a/spring-beans/spring-beans.gradle b/spring-beans/spring-beans.gradle index 73e9942f8ef6..877d28852dc0 100644 --- a/spring-beans/spring-beans.gradle +++ b/spring-beans/spring-beans.gradle @@ -1,40 +1,17 @@ description = "Spring Beans" -apply plugin: "groovy" apply plugin: "kotlin" dependencies { - compile(project(":spring-core")) - optional("javax.inject:javax.inject") + api(project(":spring-core")) + optional("jakarta.inject:jakarta.inject-api") optional("org.yaml:snakeyaml") - optional("org.codehaus.groovy:groovy-xml") + optional("org.apache.groovy:groovy-xml") optional("org.jetbrains.kotlin:kotlin-reflect") optional("org.jetbrains.kotlin:kotlin-stdlib") - testCompile(testFixtures(project(":spring-core"))) - testCompile("javax.annotation:javax.annotation-api") + testImplementation(testFixtures(project(":spring-core"))) + testImplementation(project(":spring-core-test")) + testImplementation("jakarta.annotation:jakarta.annotation-api") testFixturesApi("org.junit.jupiter:junit-jupiter-api") testFixturesImplementation("org.assertj:assertj-core") -} - -// This module does joint compilation for Java and Groovy code with the compileGroovy task. -sourceSets { - main.groovy.srcDirs += "src/main/java" - main.java.srcDirs = [] -} - -compileGroovy { - sourceCompatibility = 1.8 - targetCompatibility = 1.8 - options.compilerArgs += "-Werror" -} - -// This module also builds Kotlin code and the compileKotlin task naturally depends on -// compileJava. We need to redefine dependencies to break task cycles. -tasks.named('compileGroovy') { - // Groovy only needs the declared dependencies (and not the result of Java compilation) - classpath = sourceSets.main.compileClasspath -} -tasks.named('compileKotlin') { - // Kotlin also depends on the result of Groovy compilation - classpath += files(sourceSets.main.groovy.classesDirectory) -} +} \ No newline at end of file diff --git a/spring-beans/src/jmh/java/org/springframework/beans/AbstractPropertyAccessorBenchmark.java b/spring-beans/src/jmh/java/org/springframework/beans/AbstractPropertyAccessorBenchmark.java index d407318f103c..6cdd116b4cb0 100644 --- a/spring-beans/src/jmh/java/org/springframework/beans/AbstractPropertyAccessorBenchmark.java +++ b/spring-beans/src/jmh/java/org/springframework/beans/AbstractPropertyAccessorBenchmark.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -61,20 +61,15 @@ public void setup() { this.propertyAccessor = new BeanWrapperImpl(this.target); } switch (this.customEditor) { - case "stringTrimmer": + case "stringTrimmer" -> this.propertyAccessor.registerCustomEditor(String.class, new StringTrimmerEditor(false)); - break; - case "numberOnPath": + case "numberOnPath" -> this.propertyAccessor.registerCustomEditor(int.class, "array.somePath", new CustomNumberEditor(Integer.class, false)); - break; - case "numberOnNestedPath": + case "numberOnNestedPath" -> this.propertyAccessor.registerCustomEditor(int.class, "array[0].somePath", new CustomNumberEditor(Integer.class, false)); - break; - case "numberOnType": + case "numberOnType" -> this.propertyAccessor.registerCustomEditor(int.class, new CustomNumberEditor(Integer.class, false)); - break; } - } } @@ -98,4 +93,5 @@ public void setArray(int[] array) { this.array = array; } } + } diff --git a/spring-beans/src/jmh/java/org/springframework/beans/BeanUtilsBenchmark.java b/spring-beans/src/jmh/java/org/springframework/beans/BeanUtilsBenchmark.java new file mode 100644 index 000000000000..b656648d8754 --- /dev/null +++ b/spring-beans/src/jmh/java/org/springframework/beans/BeanUtilsBenchmark.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-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.beans; + +import java.lang.reflect.Constructor; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; + +@State(Scope.Thread) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +public class BeanUtilsBenchmark { + + private Constructor noArgConstructor; + private Constructor constructor; + + @Setup + public void setUp() throws NoSuchMethodException { + this.noArgConstructor = TestClass1.class.getDeclaredConstructor(); + this.constructor = TestClass2.class.getDeclaredConstructor(int.class, String.class); + } + + @Benchmark + public Object emptyConstructor() { + return BeanUtils.instantiateClass(this.noArgConstructor); + } + + @Benchmark + public Object nonEmptyConstructor() { + return BeanUtils.instantiateClass(this.constructor, 1, "str"); + } + + static class TestClass1 { + } + + @SuppressWarnings("unused") + static class TestClass2 { + private final int value1; + private final String value2; + + TestClass2(int value1, String value2) { + this.value1 = value1; + this.value2 = value2; + } + } + +} diff --git a/spring-beans/src/jmh/kotlin/org/springframework/beans/KotlinBeanUtilsBenchmark.kt b/spring-beans/src/jmh/kotlin/org/springframework/beans/KotlinBeanUtilsBenchmark.kt new file mode 100644 index 000000000000..9bbdd9711df0 --- /dev/null +++ b/spring-beans/src/jmh/kotlin/org/springframework/beans/KotlinBeanUtilsBenchmark.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2002-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.beans + +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.Benchmark +import org.openjdk.jmh.annotations.BenchmarkMode +import org.openjdk.jmh.annotations.Mode +import org.openjdk.jmh.annotations.OutputTimeUnit +import org.openjdk.jmh.annotations.Scope +import org.openjdk.jmh.annotations.State + +@State(Scope.Thread) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +open class KotlinBeanUtilsBenchmark { + + private val noArgConstructor = TestClass1::class.java.getDeclaredConstructor() + private val constructor = TestClass2::class.java.getDeclaredConstructor(Int::class.java, String::class.java) + + @Benchmark + fun emptyConstructor(): Any { + return BeanUtils.instantiateClass(noArgConstructor) + } + + @Benchmark + fun nonEmptyConstructor(): Any { + return BeanUtils.instantiateClass(constructor, 1, "str") + } + + class TestClass1() + + @Suppress("UNUSED_PARAMETER") + class TestClass2(int: Int, string: String) +} + diff --git a/spring-beans/src/jmh/resources/org/springframework/beans/factory/ConcurrentBeanFactoryBenchmark-context.xml b/spring-beans/src/jmh/resources/org/springframework/beans/factory/ConcurrentBeanFactoryBenchmark-context.xml index 8dceadf07278..d9336a02b9ee 100644 --- a/spring-beans/src/jmh/resources/org/springframework/beans/factory/ConcurrentBeanFactoryBenchmark-context.xml +++ b/spring-beans/src/jmh/resources/org/springframework/beans/factory/ConcurrentBeanFactoryBenchmark-context.xml @@ -1,14 +1,13 @@ - + - - - - diff --git a/spring-beans/src/main/groovy/org/springframework/beans/factory/groovy/GroovyDynamicElementReader.groovy b/spring-beans/src/main/groovy/org/springframework/beans/factory/groovy/GroovyDynamicElementReader.groovy deleted file mode 100644 index d0171216948b..000000000000 --- a/spring-beans/src/main/groovy/org/springframework/beans/factory/groovy/GroovyDynamicElementReader.groovy +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright 2002-2013 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.beans.factory.groovy - -import groovy.xml.StreamingMarkupBuilder -import org.springframework.beans.factory.config.BeanDefinitionHolder -import org.springframework.beans.factory.xml.BeanDefinitionParserDelegate -import org.w3c.dom.Element - -/** - * Used by GroovyBeanDefinitionReader to read a Spring XML namespace expression - * in the Groovy DSL. - * - * @author Jeff Brown - * @author Juergen Hoeller - * @since 4.0 - */ -@groovy.transform.PackageScope -class GroovyDynamicElementReader extends GroovyObjectSupport { - - private final String rootNamespace - - private final Map xmlNamespaces - - private final BeanDefinitionParserDelegate delegate - - private final GroovyBeanDefinitionWrapper beanDefinition - - protected final boolean decorating; - - private boolean callAfterInvocation = true - - - public GroovyDynamicElementReader(String namespace, Map namespaceMap, - BeanDefinitionParserDelegate delegate, GroovyBeanDefinitionWrapper beanDefinition, boolean decorating) { - super(); - this.rootNamespace = namespace - this.xmlNamespaces = namespaceMap - this.delegate = delegate - this.beanDefinition = beanDefinition; - this.decorating = decorating; - } - - - @Override - public Object invokeMethod(String name, Object args) { - if (name.equals("doCall")) { - def callable = args[0] - callable.resolveStrategy = Closure.DELEGATE_FIRST - callable.delegate = this - def result = callable.call() - - if (this.callAfterInvocation) { - afterInvocation() - this.callAfterInvocation = false - } - return result - } - - else { - StreamingMarkupBuilder builder = new StreamingMarkupBuilder(); - def myNamespace = this.rootNamespace - def myNamespaces = this.xmlNamespaces - - def callable = { - for (namespace in myNamespaces) { - mkp.declareNamespace([(namespace.key):namespace.value]) - } - if (args && (args[-1] instanceof Closure)) { - args[-1].resolveStrategy = Closure.DELEGATE_FIRST - args[-1].delegate = builder - } - delegate."$myNamespace"."$name"(*args) - } - - callable.resolveStrategy = Closure.DELEGATE_FIRST - callable.delegate = builder - def writable = builder.bind(callable) - def sw = new StringWriter() - writable.writeTo(sw) - - Element element = this.delegate.readerContext.readDocumentFromString(sw.toString()).documentElement - this.delegate.initDefaults(element) - if (this.decorating) { - BeanDefinitionHolder holder = this.beanDefinition.beanDefinitionHolder; - holder = this.delegate.decorateIfRequired(element, holder, null) - this.beanDefinition.setBeanDefinitionHolder(holder) - } - else { - def beanDefinition = this.delegate.parseCustomElement(element) - if (beanDefinition) { - this.beanDefinition.setBeanDefinition(beanDefinition) - } - } - if (this.callAfterInvocation) { - afterInvocation() - this.callAfterInvocation = false - } - return element - } - } - - /** - * Hook that subclass or anonymous classes can overwrite to implement custom behavior - * after invocation completes. - */ - protected void afterInvocation() { - // NOOP - } - -} diff --git a/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java b/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java index 16ab258a14e9..5310bd4688eb 100644 --- a/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -60,6 +60,7 @@ * @author Stephane Nicoll * @author Rod Johnson * @author Rob Harrop + * @author Sam Brannen * @since 4.2 * @see #registerCustomEditor * @see #setPropertyValues @@ -279,7 +280,7 @@ protected void setPropertyValue(PropertyTokenHolder tokens, PropertyValue pv) th } } - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "rawtypes"}) private void processKeyedProperty(PropertyTokenHolder tokens, PropertyValue pv) { Object propValue = getPropertyHoldingValue(tokens); PropertyHandler ph = getLocalPropertyHandler(tokens.actualName); @@ -305,8 +306,10 @@ private void processKeyedProperty(PropertyTokenHolder tokens, PropertyValue pv) Class componentType = propValue.getClass().getComponentType(); Object newArray = Array.newInstance(componentType, arrayIndex + 1); System.arraycopy(propValue, 0, newArray, 0, length); - setPropertyValue(tokens.actualName, newArray); - propValue = getPropertyValue(tokens.actualName); + int lastKeyIndex = tokens.canonicalName.lastIndexOf('['); + String propName = tokens.canonicalName.substring(0, lastKeyIndex); + setPropertyValue(propName, newArray); + propValue = getPropertyValue(propName); } Array.set(propValue, arrayIndex, convertedValue); } @@ -316,9 +319,8 @@ private void processKeyedProperty(PropertyTokenHolder tokens, PropertyValue pv) } } - else if (propValue instanceof List) { + else if (propValue instanceof List list) { Class requiredType = ph.getCollectionType(tokens.keys.length); - List list = (List) propValue; int index = Integer.parseInt(lastKey); Object oldValue = null; if (isExtractOldValueForEditor() && index < list.size()) { @@ -352,10 +354,9 @@ else if (propValue instanceof List) { } } - else if (propValue instanceof Map) { + else if (propValue instanceof Map map) { Class mapKeyType = ph.getMapKeyType(tokens.keys.length); Class mapValueType = ph.getMapValueType(tokens.keys.length); - Map map = (Map) propValue; // IMPORTANT: Do not pass full property name in here - property editors // must not kick in for map keys but rather only for map values. TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(mapKeyType); @@ -444,8 +445,8 @@ private void processLocalProperty(PropertyTokenHolder tokens, PropertyValue pv) oldValue = ph.getValue(); } catch (Exception ex) { - if (ex instanceof PrivilegedActionException) { - ex = ((PrivilegedActionException) ex).getException(); + if (ex instanceof PrivilegedActionException pae) { + ex = pae.getException(); } if (logger.isDebugEnabled()) { logger.debug("Could not read previous value of property '" + @@ -615,7 +616,7 @@ public Object getPropertyValue(String propertyName) throws BeansException { return nestedPa.getPropertyValue(tokens); } - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "rawtypes"}) @Nullable protected Object getPropertyValue(PropertyTokenHolder tokens) throws BeansException { String propertyName = tokens.canonicalName; @@ -651,15 +652,13 @@ else if (value.getClass().isArray()) { value = growArrayIfNecessary(value, index, indexedPropertyName.toString()); value = Array.get(value, index); } - else if (value instanceof List) { + else if (value instanceof List list) { int index = Integer.parseInt(key); - List list = (List) value; growCollectionIfNecessary(list, index, indexedPropertyName.toString(), ph, i + 1); value = list.get(index); } - else if (value instanceof Set) { + else if (value instanceof Set set) { // Apply index to Iterator in case of a Set. - Set set = (Set) value; int index = Integer.parseInt(key); if (index < 0 || index >= set.size()) { throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, @@ -675,8 +674,7 @@ else if (value instanceof Set) { } } } - else if (value instanceof Map) { - Map map = (Map) value; + else if (value instanceof Map map) { Class mapKeyType = ph.getResolvableType().getNested(i + 1).asMap().resolveGeneric(0); // IMPORTANT: Do not pass full property name in here - property editors // must not kick in for map keys but rather only for map values. @@ -839,7 +837,7 @@ private AbstractNestablePropertyAccessor getNestedPropertyAccessor(String nested PropertyTokenHolder tokens = getPropertyNameTokens(nestedProperty); String canonicalName = tokens.canonicalName; Object value = getPropertyValue(tokens); - if (value == null || (value instanceof Optional && !((Optional) value).isPresent())) { + if (value == null || (value instanceof Optional optional && optional.isEmpty())) { if (isAutoGrowNestedPaths()) { value = setDefaultValue(tokens); } diff --git a/spring-beans/src/main/java/org/springframework/beans/AbstractPropertyAccessor.java b/spring-beans/src/main/java/org/springframework/beans/AbstractPropertyAccessor.java index 1d6b5f48eab9..01e67dbdf14d 100644 --- a/spring-beans/src/main/java/org/springframework/beans/AbstractPropertyAccessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/AbstractPropertyAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -89,8 +89,8 @@ public void setPropertyValues(PropertyValues pvs, boolean ignoreUnknown, boolean throws BeansException { List propertyAccessExceptions = null; - List propertyValues = (pvs instanceof MutablePropertyValues ? - ((MutablePropertyValues) pvs).getPropertyValueList() : Arrays.asList(pvs.getPropertyValues())); + List propertyValues = (pvs instanceof MutablePropertyValues mpvs ? + mpvs.getPropertyValueList() : Arrays.asList(pvs.getPropertyValues())); if (ignoreUnknown) { this.suppressNotWritablePropertyException = true; diff --git a/spring-beans/src/main/java/org/springframework/beans/BeanMetadataAttribute.java b/spring-beans/src/main/java/org/springframework/beans/BeanMetadataAttribute.java index db6435d5c0e4..57662a952b1a 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeanMetadataAttribute.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeanMetadataAttribute.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-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. @@ -85,10 +85,9 @@ public boolean equals(@Nullable Object other) { if (this == other) { return true; } - if (!(other instanceof BeanMetadataAttribute)) { + if (!(other instanceof BeanMetadataAttribute otherMa)) { return false; } - BeanMetadataAttribute otherMa = (BeanMetadataAttribute) other; return (this.name.equals(otherMa.name) && ObjectUtils.nullSafeEquals(this.value, otherMa.value) && ObjectUtils.nullSafeEquals(this.source, otherMa.source)); diff --git a/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java b/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java index 3980a24dd719..e71fc723b62b 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -29,7 +29,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.Date; -import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -41,8 +40,6 @@ import kotlin.reflect.full.KClasses; import kotlin.reflect.jvm.KCallablesJvm; import kotlin.reflect.jvm.ReflectJvmMapping; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.KotlinDetector; @@ -75,25 +72,21 @@ */ public abstract class BeanUtils { - private static final Log logger = LogFactory.getLog(BeanUtils.class); - private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); private static final Set> unknownEditorTypes = Collections.newSetFromMap(new ConcurrentReferenceHashMap<>(64)); - private static final Map, Object> DEFAULT_TYPE_VALUES; - - static { - Map, Object> values = new HashMap<>(); - values.put(boolean.class, false); - values.put(byte.class, (byte) 0); - values.put(short.class, (short) 0); - values.put(int.class, 0); - values.put(long.class, (long) 0); - DEFAULT_TYPE_VALUES = Collections.unmodifiableMap(values); - } + private static final Map, Object> DEFAULT_TYPE_VALUES = Map.of( + boolean.class, false, + byte.class, (byte) 0, + short.class, (short) 0, + int.class, 0, + long.class, 0L, + float.class, 0F, + double.class, 0D, + char.class, '\0'); /** @@ -101,9 +94,9 @@ public abstract class BeanUtils { * @param clazz class to instantiate * @return the new instance * @throws BeanInstantiationException if the bean cannot be instantiated + * @see Class#newInstance() * @deprecated as of Spring 5.0, following the deprecation of * {@link Class#newInstance()} in JDK 9 - * @see Class#newInstance() */ @Deprecated public static T instantiate(Class clazz) throws BeanInstantiationException { @@ -143,19 +136,20 @@ public static T instantiateClass(Class clazz) throws BeanInstantiationExc if (clazz.isInterface()) { throw new BeanInstantiationException(clazz, "Specified class is an interface"); } + Constructor ctor; try { - return instantiateClass(clazz.getDeclaredConstructor()); + ctor = clazz.getDeclaredConstructor(); } catch (NoSuchMethodException ex) { - Constructor ctor = findPrimaryConstructor(clazz); - if (ctor != null) { - return instantiateClass(ctor); + ctor = findPrimaryConstructor(clazz); + if (ctor == null) { + throw new BeanInstantiationException(clazz, "No default constructor found", ex); } - throw new BeanInstantiationException(clazz, "No default constructor found", ex); } catch (LinkageError err) { throw new BeanInstantiationException(clazz, "Unresolvable class definition", err); } + return instantiateClass(ctor); } /** @@ -197,8 +191,12 @@ public static T instantiateClass(Constructor ctor, Object... args) throws return KotlinDelegate.instantiateClass(ctor, args); } else { + int parameterCount = ctor.getParameterCount(); + Assert.isTrue(args.length <= parameterCount, "Can't specify more arguments than constructor parameters"); + if (parameterCount == 0) { + return ctor.newInstance(); + } Class[] parameterTypes = ctor.getParameterTypes(); - Assert.isTrue(args.length <= parameterTypes.length, "Can't specify more arguments than constructor parameters"); Object[] argsWithDefaultValues = new Object[args.length]; for (int i = 0 ; i < args.length; i++) { if (args[i] == null) { @@ -227,32 +225,45 @@ public static T instantiateClass(Constructor ctor, Object... args) throws } /** - * Return a resolvable constructor for the provided class, either a primary constructor - * or single public constructor or simply a default constructor. Callers have to be - * prepared to resolve arguments for the returned constructor's parameters, if any. + * Return a resolvable constructor for the provided class, either a primary or single + * public constructor with arguments, or a single non-public constructor with arguments, + * or simply a default constructor. Callers have to be prepared to resolve arguments + * for the returned constructor's parameters, if any. * @param clazz the class to check + * @throws IllegalStateException in case of no unique constructor found at all * @since 5.3 * @see #findPrimaryConstructor */ @SuppressWarnings("unchecked") public static Constructor getResolvableConstructor(Class clazz) { Constructor ctor = findPrimaryConstructor(clazz); - if (ctor == null) { - Constructor[] ctors = clazz.getConstructors(); + if (ctor != null) { + return ctor; + } + + Constructor[] ctors = clazz.getConstructors(); + if (ctors.length == 1) { + // A single public constructor + return (Constructor) ctors[0]; + } + else if (ctors.length == 0){ + ctors = clazz.getDeclaredConstructors(); if (ctors.length == 1) { - ctor = (Constructor) ctors[0]; - } - else { - try { - ctor = clazz.getDeclaredConstructor(); - } - catch (NoSuchMethodException ex) { - throw new IllegalStateException("No primary or single public constructor found for " + - clazz + " - and no default constructor found either"); - } + // A single non-public constructor, e.g. from a non-public record type + return (Constructor) ctors[0]; } } - return ctor; + + // Several constructors -> let's try to take the default constructor + try { + return clazz.getDeclaredConstructor(); + } + catch (NoSuchMethodException ex) { + // Giving up... + } + + // No unique constructor at all + throw new IllegalStateException("No primary or single unique constructor found for " + clazz); } /** @@ -268,10 +279,7 @@ public static Constructor getResolvableConstructor(Class clazz) { public static Constructor findPrimaryConstructor(Class clazz) { Assert.notNull(clazz, "Class must not be null"); if (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(clazz)) { - Constructor kotlinPrimaryConstructor = KotlinDelegate.findPrimaryConstructor(clazz); - if (kotlinPrimaryConstructor != null) { - return kotlinPrimaryConstructor; - } + return KotlinDelegate.findPrimaryConstructor(clazz); } return null; } @@ -531,7 +539,7 @@ public static PropertyDescriptor findPropertyForMethod(Method method, Class c /** * Find a JavaBeans PropertyEditor following the 'Editor' suffix convention - * (e.g. "mypackage.MyDomainClass" -> "mypackage.MyDomainClassEditor"). + * (e.g. "mypackage.MyDomainClass" → "mypackage.MyDomainClassEditor"). *

    Compatible to the standard JavaBeans convention as implemented by * {@link java.beans.PropertyEditorManager} but isolated from the latter's * registered default editors for primitive types. @@ -543,6 +551,7 @@ public static PropertyEditor findEditorByConvention(@Nullable Class targetTyp if (targetType == null || targetType.isArray() || unknownEditorTypes.contains(targetType)) { return null; } + ClassLoader cl = targetType.getClassLoader(); if (cl == null) { try { @@ -553,34 +562,29 @@ public static PropertyEditor findEditorByConvention(@Nullable Class targetTyp } catch (Throwable ex) { // e.g. AccessControlException on Google App Engine - if (logger.isDebugEnabled()) { - logger.debug("Could not access system ClassLoader: " + ex); - } return null; } } + String targetTypeName = targetType.getName(); String editorName = targetTypeName + "Editor"; try { Class editorClass = cl.loadClass(editorName); - if (!PropertyEditor.class.isAssignableFrom(editorClass)) { - if (logger.isInfoEnabled()) { - logger.info("Editor class [" + editorName + - "] does not implement [java.beans.PropertyEditor] interface"); + if (editorClass != null) { + if (!PropertyEditor.class.isAssignableFrom(editorClass)) { + unknownEditorTypes.add(targetType); + return null; } - unknownEditorTypes.add(targetType); - return null; + return (PropertyEditor) instantiateClass(editorClass); } - return (PropertyEditor) instantiateClass(editorClass); + // Misbehaving ClassLoader returned null instead of ClassNotFoundException + // - fall back to unknown editor type registration below } catch (ClassNotFoundException ex) { - if (logger.isTraceEnabled()) { - logger.trace("No property editor [" + editorName + "] found for type " + - targetTypeName + " according to 'Editor' suffix convention"); - } - unknownEditorTypes.add(targetType); - return null; + // Ignore - fall back to unknown editor type registration below } + unknownEditorTypes.add(targetType); + return null; } /** @@ -609,8 +613,8 @@ public static Class findPropertyType(String propertyName, @Nullable Class. * @return a corresponding MethodParameter object */ public static MethodParameter getWriteMethodParameter(PropertyDescriptor pd) { - if (pd instanceof GenericTypeAwarePropertyDescriptor) { - return new MethodParameter(((GenericTypeAwarePropertyDescriptor) pd).getWriteMethodParameter()); + if (pd instanceof GenericTypeAwarePropertyDescriptor typeAwarePd) { + return new MethodParameter(typeAwarePd.getWriteMethodParameter()); } else { Method writeMethod = pd.getWriteMethod(); @@ -686,7 +690,25 @@ public static boolean isSimpleValueType(Class type) { * from each other, as long as the properties match. Any bean properties that the * source bean exposes but the target bean does not will silently be ignored. *

    This is just a convenience method. For more complex transfer needs, - * consider using a full BeanWrapper. + * consider using a full {@link BeanWrapper}. + *

    As of Spring Framework 5.3, this method honors generic type information + * when matching properties in the source and target objects. + *

    The following table provides a non-exhaustive set of examples of source + * and target property types that can be copied as well as source and target + * property types that cannot be copied. + * + * + * + * + * + * + * + * + * + * + * + * + *
    source property typetarget property typecopy supported
    {@code Integer}{@code Integer}yes
    {@code Integer}{@code Number}yes
    {@code List}{@code List}yes
    {@code List}{@code List}yes
    {@code List}{@code List}yes
    {@code List}{@code List}yes
    {@code String}{@code Integer}no
    {@code Number}{@code Integer}no
    {@code List}{@code List}no
    {@code List}{@code List}no
    * @param source the source bean * @param target the target bean * @throws BeansException if the copying failed @@ -703,7 +725,10 @@ public static void copyProperties(Object source, Object target) throws BeansExce * from each other, as long as the properties match. Any bean properties that the * source bean exposes but the target bean does not will silently be ignored. *

    This is just a convenience method. For more complex transfer needs, - * consider using a full BeanWrapper. + * consider using a full {@link BeanWrapper}. + *

    As of Spring Framework 5.3, this method honors generic type information + * when matching properties in the source and target objects. See the + * documentation for {@link #copyProperties(Object, Object)} for details. * @param source the source bean * @param target the target bean * @param editable the class (or interface) to restrict property setting to @@ -721,7 +746,10 @@ public static void copyProperties(Object source, Object target, Class editabl * from each other, as long as the properties match. Any bean properties that the * source bean exposes but the target bean does not will silently be ignored. *

    This is just a convenience method. For more complex transfer needs, - * consider using a full BeanWrapper. + * consider using a full {@link BeanWrapper}. + *

    As of Spring Framework 5.3, this method honors generic type information + * when matching properties in the source and target objects. See the + * documentation for {@link #copyProperties(Object, Object)} for details. * @param source the source bean * @param target the target bean * @param ignoreProperties array of property names to ignore @@ -738,7 +766,8 @@ public static void copyProperties(Object source, Object target, String... ignore * from each other, as long as the properties match. Any bean properties that the * source bean exposes but the target bean does not will silently be ignored. *

    As of Spring Framework 5.3, this method honors generic type information - * when matching properties in the source and target objects. + * when matching properties in the source and target objects. See the + * documentation for {@link #copyProperties(Object, Object)} for details. * @param source the source bean * @param target the target bean * @param editable the class (or interface) to restrict property setting to @@ -772,7 +801,14 @@ private static void copyProperties(Object source, Object target, @Nullable Class if (readMethod != null) { ResolvableType sourceResolvableType = ResolvableType.forMethodReturnType(readMethod); ResolvableType targetResolvableType = ResolvableType.forMethodParameter(writeMethod, 0); - if (targetResolvableType.isAssignableFrom(sourceResolvableType)) { + + // Ignore generic types in assignable check if either ResolvableType has unresolvable generics. + boolean isAssignable = + (sourceResolvableType.hasUnresolvableGenerics() || targetResolvableType.hasUnresolvableGenerics() ? + ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType()) : + targetResolvableType.isAssignableFrom(sourceResolvableType)); + + if (isAssignable) { try { if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) { readMethod.setAccessible(true); @@ -844,9 +880,13 @@ public static T instantiateClass(Constructor ctor, Object... args) } List parameters = kotlinConstructor.getParameters(); - Map argParameters = CollectionUtils.newHashMap(parameters.size()); + Assert.isTrue(args.length <= parameters.size(), - "Number of provided arguments should be less of equals than number of constructor parameters"); + "Number of provided arguments must be less than or equal to the number of constructor parameters"); + if (parameters.isEmpty()) { + return kotlinConstructor.call(); + } + Map argParameters = CollectionUtils.newHashMap(parameters.size()); for (int i = 0 ; i < args.length ; i++) { if (!(parameters.get(i).isOptional() && args[i] == null)) { argParameters.put(parameters.get(i), args[i]); diff --git a/spring-beans/src/main/java/org/springframework/beans/BeanWrapper.java b/spring-beans/src/main/java/org/springframework/beans/BeanWrapper.java index 90ee4f129cba..798191cc55d7 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeanWrapper.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeanWrapper.java @@ -82,7 +82,7 @@ public interface BeanWrapper extends ConfigurablePropertyAccessor { * Obtain the property descriptor for a specific property * of the wrapped object. * @param propertyName the property to obtain the descriptor for - * (may be a nested path, but no indexed/mapped property) + * (may be a nested path, but not an indexed/mapped property) * @return the property descriptor for the specified property * @throws InvalidPropertyException if there is no such property */ diff --git a/spring-beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java b/spring-beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java index c2fb1aca6f71..4f610998ae03 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -18,11 +18,6 @@ import java.beans.PropertyDescriptor; import java.lang.reflect.Method; -import java.security.AccessControlContext; -import java.security.AccessController; -import java.security.PrivilegedAction; -import java.security.PrivilegedActionException; -import java.security.PrivilegedExceptionAction; import org.springframework.core.ResolvableType; import org.springframework.core.convert.Property; @@ -69,12 +64,6 @@ public class BeanWrapperImpl extends AbstractNestablePropertyAccessor implements @Nullable private CachedIntrospectionResults cachedIntrospectionResults; - /** - * The security context used for invoking the property methods. - */ - @Nullable - private AccessControlContext acc; - /** * Create a new empty BeanWrapperImpl. Wrapped instance needs to be set afterwards. @@ -131,7 +120,6 @@ public BeanWrapperImpl(Object object, String nestedPath, Object rootObject) { */ private BeanWrapperImpl(Object object, String nestedPath, BeanWrapperImpl parent) { super(object, nestedPath, parent); - setSecurityContext(parent.acc); } @@ -176,23 +164,6 @@ private CachedIntrospectionResults getCachedIntrospectionResults() { return this.cachedIntrospectionResults; } - /** - * Set the security context used during the invocation of the wrapped instance methods. - * Can be null. - */ - public void setSecurityContext(@Nullable AccessControlContext acc) { - this.acc = acc; - } - - /** - * Return the security context used during the invocation of the wrapped instance methods. - * Can be null. - */ - @Nullable - public AccessControlContext getSecurityContext() { - return this.acc; - } - /** * Convert the given value for the specified property to the latter's type. @@ -290,47 +261,16 @@ public TypeDescriptor nested(int level) { @Nullable public Object getValue() throws Exception { Method readMethod = this.pd.getReadMethod(); - if (System.getSecurityManager() != null) { - AccessController.doPrivileged((PrivilegedAction) () -> { - ReflectionUtils.makeAccessible(readMethod); - return null; - }); - try { - return AccessController.doPrivileged((PrivilegedExceptionAction) - () -> readMethod.invoke(getWrappedInstance(), (Object[]) null), acc); - } - catch (PrivilegedActionException pae) { - throw pae.getException(); - } - } - else { - ReflectionUtils.makeAccessible(readMethod); - return readMethod.invoke(getWrappedInstance(), (Object[]) null); - } + ReflectionUtils.makeAccessible(readMethod); + return readMethod.invoke(getWrappedInstance(), (Object[]) null); } @Override public void setValue(@Nullable Object value) throws Exception { - Method writeMethod = (this.pd instanceof GenericTypeAwarePropertyDescriptor ? - ((GenericTypeAwarePropertyDescriptor) this.pd).getWriteMethodForActualAccess() : - this.pd.getWriteMethod()); - if (System.getSecurityManager() != null) { - AccessController.doPrivileged((PrivilegedAction) () -> { - ReflectionUtils.makeAccessible(writeMethod); - return null; - }); - try { - AccessController.doPrivileged((PrivilegedExceptionAction) - () -> writeMethod.invoke(getWrappedInstance(), value), acc); - } - catch (PrivilegedActionException ex) { - throw ex.getException(); - } - } - else { - ReflectionUtils.makeAccessible(writeMethod); - writeMethod.invoke(getWrappedInstance(), value); - } + Method writeMethod = (this.pd instanceof GenericTypeAwarePropertyDescriptor typeAwarePd ? + typeAwarePd.getWriteMethodForActualAccess() : this.pd.getWriteMethod()); + ReflectionUtils.makeAccessible(writeMethod); + writeMethod.invoke(getWrappedInstance(), value); } } diff --git a/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java b/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java index 7b7a67d91ccf..e7b8d426e9f1 100644 --- a/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java +++ b/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -18,10 +18,11 @@ import java.beans.BeanInfo; import java.beans.IntrospectionException; -import java.beans.Introspector; import java.beans.PropertyDescriptor; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.net.URL; +import java.security.ProtectionDomain; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; @@ -34,7 +35,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.core.SpringProperties; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.lang.Nullable; @@ -58,14 +58,15 @@ *

    Note that for caching to work effectively, some preconditions need to be met: * Prefer an arrangement where the Spring jars live in the same ClassLoader as the * application classes, which allows for clean caching along with the application's - * lifecycle in any case. For a web application, consider declaring a local - * {@link org.springframework.web.util.IntrospectorCleanupListener} in {@code web.xml} - * in case of a multi-ClassLoader layout, which will allow for effective caching as well. + * lifecycle in any case. * - *

    In case of a non-clean ClassLoader arrangement without a cleanup listener having - * been set up, this class will fall back to a weak-reference-based caching model that - * recreates much-requested entries every time the garbage collector removed them. In - * such a scenario, consider the {@link #IGNORE_BEANINFO_PROPERTY_NAME} system property. + *

    As of 6.0, Spring's default introspection discovers basic JavaBeans properties + * through an efficient method reflection pass. For full JavaBeans introspection + * including indexed properties and all JDK-supported customizers, configure a + * {@code META-INF/spring.factories} file with the following content: + * {@code org.springframework.beans.BeanInfoFactory=org.springframework.beans.StandardBeanInfoFactory} + * For Spring 5.3 compatible extended introspection including non-void setter methods: + * {@code org.springframework.beans.BeanInfoFactory=org.springframework.beans.ExtendedBeanInfoFactory} * * @author Rod Johnson * @author Juergen Hoeller @@ -76,35 +77,11 @@ */ public final class CachedIntrospectionResults { - /** - * System property that instructs Spring to use the {@link Introspector#IGNORE_ALL_BEANINFO} - * mode when calling the JavaBeans {@link Introspector}: "spring.beaninfo.ignore", with a - * value of "true" skipping the search for {@code BeanInfo} classes (typically for scenarios - * where no such classes are being defined for beans in the application in the first place). - *

    The default is "false", considering all {@code BeanInfo} metadata classes, like for - * standard {@link Introspector#getBeanInfo(Class)} calls. Consider switching this flag to - * "true" if you experience repeated ClassLoader access for non-existing {@code BeanInfo} - * classes, in case such access is expensive on startup or on lazy loading. - *

    Note that such an effect may also indicate a scenario where caching doesn't work - * effectively: Prefer an arrangement where the Spring jars live in the same ClassLoader - * as the application classes, which allows for clean caching along with the application's - * lifecycle in any case. For a web application, consider declaring a local - * {@link org.springframework.web.util.IntrospectorCleanupListener} in {@code web.xml} - * in case of a multi-ClassLoader layout, which will allow for effective caching as well. - * @see Introspector#getBeanInfo(Class, int) - */ - public static final String IGNORE_BEANINFO_PROPERTY_NAME = "spring.beaninfo.ignore"; - - private static final PropertyDescriptor[] EMPTY_PROPERTY_DESCRIPTOR_ARRAY = {}; - - - private static final boolean shouldIntrospectorIgnoreBeaninfoClasses = - SpringProperties.getFlag(IGNORE_BEANINFO_PROPERTY_NAME); - - /** Stores the BeanInfoFactory instances. */ private static final List beanInfoFactories = SpringFactoriesLoader.loadFactories( BeanInfoFactory.class, CachedIntrospectionResults.class.getClassLoader()); + private static final SimpleBeanInfoFactory simpleBeanInfoFactory = new SimpleBeanInfoFactory(); + private static final Log logger = LogFactory.getLog(CachedIntrospectionResults.class); /** @@ -239,7 +216,7 @@ private static boolean isUnderneathClassLoader(@Nullable ClassLoader candidate, * Retrieve a {@link BeanInfo} descriptor for the given target class. * @param beanClass the target class to introspect * @return the resulting {@code BeanInfo} descriptor (never {@code null}) - * @throws IntrospectionException from the underlying {@link Introspector} + * @throws IntrospectionException from introspecting the given bean class */ private static BeanInfo getBeanInfo(Class beanClass) throws IntrospectionException { for (BeanInfoFactory beanInfoFactory : beanInfoFactories) { @@ -248,9 +225,7 @@ private static BeanInfo getBeanInfo(Class beanClass) throws IntrospectionExce return beanInfo; } } - return (shouldIntrospectorIgnoreBeaninfoClasses ? - Introspector.getBeanInfo(beanClass, Introspector.IGNORE_ALL_BEANINFO) : - Introspector.getBeanInfo(beanClass)); + return simpleBeanInfoFactory.getBeanInfo(beanClass); } @@ -286,9 +261,17 @@ private CachedIntrospectionResults(Class beanClass) throws BeansException { // This call is slow so we do it once. PropertyDescriptor[] pds = this.beanInfo.getPropertyDescriptors(); for (PropertyDescriptor pd : pds) { - if (Class.class == beanClass && - ("classLoader".equals(pd.getName()) || "protectionDomain".equals(pd.getName()))) { - // Ignore Class.getClassLoader() and getProtectionDomain() methods - nobody needs to bind to those + if (Class.class == beanClass && !("name".equals(pd.getName()) || + (pd.getName().endsWith("Name") && String.class == pd.getPropertyType()))) { + // Only allow all name variants of Class properties + continue; + } + if (URL.class == beanClass && "content".equals(pd.getName())) { + // Only allow URL attribute introspection, not content resolution + continue; + } + if (pd.getWriteMethod() == null && isInvalidReadOnlyPropertyType(pd.getPropertyType(), beanClass)) { + // Ignore read-only properties such as ClassLoader - no need to bind to those continue; } if (logger.isTraceEnabled()) { @@ -337,6 +320,11 @@ private void introspectInterfaces(Class beanClass, Class currClass, Set beanClass, Set readMethod for (Method method : beanClass.getMethods()) { if (!this.propertyDescriptors.containsKey(method.getName()) && - !readMethodNames.contains((method.getName())) && isPlainAccessor(method)) { + !readMethodNames.contains(method.getName()) && isPlainAccessor(method)) { this.propertyDescriptors.put(method.getName(), new GenericTypeAwarePropertyDescriptor(beanClass, method.getName(), method, null, null)); readMethodNames.add(method.getName()); @@ -363,8 +351,10 @@ private void introspectPlainAccessors(Class beanClass, Set readMethod } private boolean isPlainAccessor(Method method) { - if (method.getParameterCount() > 0 || method.getReturnType() == void.class || - method.getDeclaringClass() == Object.class || Modifier.isStatic(method.getModifiers())) { + if (Modifier.isStatic(method.getModifiers()) || + method.getDeclaringClass() == Object.class || method.getDeclaringClass() == Class.class || + method.getParameterCount() > 0 || method.getReturnType() == void.class || + isInvalidReadOnlyPropertyType(method.getReturnType(), method.getDeclaringClass())) { return false; } try { @@ -377,6 +367,13 @@ private boolean isPlainAccessor(Method method) { } } + private boolean isInvalidReadOnlyPropertyType(@Nullable Class returnType, Class beanClass) { + return (returnType != null && (ClassLoader.class.isAssignableFrom(returnType) || + ProtectionDomain.class.isAssignableFrom(returnType) || + (AutoCloseable.class.isAssignableFrom(returnType) && + !AutoCloseable.class.isAssignableFrom(beanClass)))); + } + BeanInfo getBeanInfo() { return this.beanInfo; @@ -400,7 +397,7 @@ PropertyDescriptor getPropertyDescriptor(String name) { } PropertyDescriptor[] getPropertyDescriptors() { - return this.propertyDescriptors.values().toArray(EMPTY_PROPERTY_DESCRIPTOR_ARRAY); + return this.propertyDescriptors.values().toArray(PropertyDescriptorUtils.EMPTY_PROPERTY_DESCRIPTOR_ARRAY); } private PropertyDescriptor buildGenericTypeAwarePropertyDescriptor(Class beanClass, PropertyDescriptor pd) { diff --git a/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfo.java b/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfo.java index 21ce57cf6add..d0ef5af79fe7 100644 --- a/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfo.java +++ b/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfo.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -137,9 +137,10 @@ private List findCandidateWriteMethods(MethodDescriptor[] methodDescript } } // Sort non-void returning write methods to guard against the ill effects of - // non-deterministic sorting of methods returned from Class#getDeclaredMethods - // under JDK 7. See https://bugs.java.com/view_bug.do?bug_id=7023180 - matches.sort((m1, m2) -> m2.toString().compareTo(m1.toString())); + // non-deterministic sorting of methods returned from Class#getMethods. + // For historical reasons, the natural sort order is reversed. + // See https://github.com/spring-projects/spring-framework/issues/14744. + matches.sort(Comparator.comparing(Method::toString).reversed()); return matches; } @@ -188,8 +189,7 @@ private PropertyDescriptor findExistingPropertyDescriptor(String propertyName, C for (PropertyDescriptor pd : this.propertyDescriptors) { final Class candidateType; final String candidateName = pd.getName(); - if (pd instanceof IndexedPropertyDescriptor) { - IndexedPropertyDescriptor ipd = (IndexedPropertyDescriptor) pd; + if (pd instanceof IndexedPropertyDescriptor ipd) { candidateType = ipd.getIndexedPropertyType(); if (candidateName.equals(propertyName) && (candidateType.equals(propertyType) || candidateType.equals(propertyType.getComponentType()))) { @@ -494,10 +494,9 @@ public boolean equals(@Nullable Object other) { if (this == other) { return true; } - if (!(other instanceof IndexedPropertyDescriptor)) { + if (!(other instanceof IndexedPropertyDescriptor otherPd)) { return false; } - IndexedPropertyDescriptor otherPd = (IndexedPropertyDescriptor) other; return (ObjectUtils.nullSafeEquals(getIndexedReadMethod(), otherPd.getIndexedReadMethod()) && ObjectUtils.nullSafeEquals(getIndexedWriteMethod(), otherPd.getIndexedWriteMethod()) && ObjectUtils.nullSafeEquals(getIndexedPropertyType(), otherPd.getIndexedPropertyType()) && diff --git a/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfoFactory.java b/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfoFactory.java index d7d2b2ecc9e1..5f41742632d9 100644 --- a/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfoFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfoFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-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. @@ -18,34 +18,35 @@ import java.beans.BeanInfo; import java.beans.IntrospectionException; -import java.beans.Introspector; import java.lang.reflect.Method; import org.springframework.core.Ordered; -import org.springframework.lang.Nullable; +import org.springframework.lang.NonNull; /** - * {@link BeanInfoFactory} implementation that evaluates whether bean classes have - * "non-standard" JavaBeans setter methods and are thus candidates for introspection - * by Spring's (package-visible) {@code ExtendedBeanInfo} implementation. + * Extension of {@link StandardBeanInfoFactory} that supports "non-standard" + * JavaBeans setter methods through introspection by Spring's + * (package-visible) {@code ExtendedBeanInfo} implementation. + * + *

    To be configured via a {@code META-INF/spring.factories} file with the following content: + * {@code org.springframework.beans.BeanInfoFactory=org.springframework.beans.ExtendedBeanInfoFactory} * *

    Ordered at {@link Ordered#LOWEST_PRECEDENCE} to allow other user-defined * {@link BeanInfoFactory} types to take precedence. * * @author Chris Beams + * @author Juergen Hoeller * @since 3.2 - * @see BeanInfoFactory + * @see StandardBeanInfoFactory * @see CachedIntrospectionResults */ -public class ExtendedBeanInfoFactory implements BeanInfoFactory, Ordered { +public class ExtendedBeanInfoFactory extends StandardBeanInfoFactory { - /** - * Return an {@link ExtendedBeanInfo} for the given bean class, if applicable. - */ @Override - @Nullable + @NonNull public BeanInfo getBeanInfo(Class beanClass) throws IntrospectionException { - return (supports(beanClass) ? new ExtendedBeanInfo(Introspector.getBeanInfo(beanClass)) : null); + BeanInfo beanInfo = super.getBeanInfo(beanClass); + return (supports(beanClass) ? new ExtendedBeanInfo(beanInfo) : beanInfo); } /** @@ -61,9 +62,4 @@ private boolean supports(Class beanClass) { return false; } - @Override - public int getOrder() { - return Ordered.LOWEST_PRECEDENCE; - } - } diff --git a/spring-beans/src/main/java/org/springframework/beans/GenericTypeAwarePropertyDescriptor.java b/spring-beans/src/main/java/org/springframework/beans/GenericTypeAwarePropertyDescriptor.java index 603f5aae150e..fb5d4d15ae1d 100644 --- a/spring-beans/src/main/java/org/springframework/beans/GenericTypeAwarePropertyDescriptor.java +++ b/spring-beans/src/main/java/org/springframework/beans/GenericTypeAwarePropertyDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -138,7 +138,7 @@ public Method getWriteMethodForActualAccess() { Set ambiguousCandidates = this.ambiguousWriteMethods; if (ambiguousCandidates != null) { this.ambiguousWriteMethods = null; - LogFactory.getLog(GenericTypeAwarePropertyDescriptor.class).warn("Invalid JavaBean property '" + + LogFactory.getLog(GenericTypeAwarePropertyDescriptor.class).debug("Non-unique JavaBean property '" + getName() + "' being accessed! Ambiguous write methods found next to actually used [" + this.writeMethod + "]: " + ambiguousCandidates); } @@ -168,10 +168,9 @@ public boolean equals(@Nullable Object other) { if (this == other) { return true; } - if (!(other instanceof GenericTypeAwarePropertyDescriptor)) { + if (!(other instanceof GenericTypeAwarePropertyDescriptor otherPd)) { return false; } - GenericTypeAwarePropertyDescriptor otherPd = (GenericTypeAwarePropertyDescriptor) other; return (getBeanClass().equals(otherPd.getBeanClass()) && PropertyDescriptorUtils.equals(this, otherPd)); } diff --git a/spring-beans/src/main/java/org/springframework/beans/MutablePropertyValues.java b/spring-beans/src/main/java/org/springframework/beans/MutablePropertyValues.java index 97c0a0ab05b9..de1d13ead614 100644 --- a/spring-beans/src/main/java/org/springframework/beans/MutablePropertyValues.java +++ b/spring-beans/src/main/java/org/springframework/beans/MutablePropertyValues.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -221,8 +221,7 @@ public void setPropertyValueAt(PropertyValue pv, int i) { */ private PropertyValue mergeIfRequired(PropertyValue newPv, PropertyValue currentPv) { Object value = newPv.getValue(); - if (value instanceof Mergeable) { - Mergeable mergeable = (Mergeable) value; + if (value instanceof Mergeable mergeable) { if (mergeable.isMergeEnabled()) { Object merged = mergeable.merge(currentPv.getValue()); return new PropertyValue(newPv.getName(), merged); @@ -327,7 +326,7 @@ public boolean isEmpty() { /** * Register the specified property as "processed" in the sense * of some processor calling the corresponding setter method - * outside of the PropertyValue(s) mechanism. + * outside the PropertyValue(s) mechanism. *

    This will lead to {@code true} being returned from * a {@link #contains} call for the specified property. * @param propertyName the name of the property. diff --git a/spring-beans/src/main/java/org/springframework/beans/PropertyAccessor.java b/spring-beans/src/main/java/org/springframework/beans/PropertyAccessor.java index 3a417aadcd37..03201a89d0d7 100644 --- a/spring-beans/src/main/java/org/springframework/beans/PropertyAccessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -23,8 +23,9 @@ /** * Common interface for classes that can access named properties - * (such as bean properties of an object or fields in an object) - * Serves as base interface for {@link BeanWrapper}. + * (such as bean properties of an object or fields in an object). + * + *

    Serves as base interface for {@link BeanWrapper}. * * @author Juergen Hoeller * @since 1.1 diff --git a/spring-beans/src/main/java/org/springframework/beans/PropertyAccessorUtils.java b/spring-beans/src/main/java/org/springframework/beans/PropertyAccessorUtils.java index 55f50e5a275c..564194e2f0aa 100644 --- a/spring-beans/src/main/java/org/springframework/beans/PropertyAccessorUtils.java +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyAccessorUtils.java @@ -134,8 +134,8 @@ public static boolean matchesProperty(String registeredPath, String propertyPath /** * Determine the canonical name for the given property path. * Removes surrounding quotes from map keys:
    - * {@code map['key']} -> {@code map[key]}
    - * {@code map["key"]} -> {@code map[key]} + * {@code map['key']} → {@code map[key]}
    + * {@code map["key"]} → {@code map[key]} * @param propertyName the bean property path * @return the canonical representation of the property path */ diff --git a/spring-beans/src/main/java/org/springframework/beans/PropertyDescriptorUtils.java b/spring-beans/src/main/java/org/springframework/beans/PropertyDescriptorUtils.java index aa9909822d18..330231556633 100644 --- a/spring-beans/src/main/java/org/springframework/beans/PropertyDescriptorUtils.java +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyDescriptorUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -19,10 +19,16 @@ import java.beans.IntrospectionException; import java.beans.PropertyDescriptor; import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; import java.util.Enumeration; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; /** * Common delegate methods for Spring's internal {@link PropertyDescriptor} implementations. @@ -32,6 +38,80 @@ */ abstract class PropertyDescriptorUtils { + public static final PropertyDescriptor[] EMPTY_PROPERTY_DESCRIPTOR_ARRAY = {}; + + + /** + * Simple introspection algorithm for basic set/get/is accessor methods, + * building corresponding JavaBeans property descriptors for them. + *

    This just supports the basic JavaBeans conventions, without indexed + * properties or any customizers, and without other BeanInfo metadata. + * For standard JavaBeans introspection, use the JavaBeans Introspector. + * @param beanClass the target class to introspect + * @return a collection of property descriptors + * @throws IntrospectionException from introspecting the given bean class + * @since 5.3.24 + * @see SimpleBeanInfoFactory + * @see java.beans.Introspector#getBeanInfo(Class) + */ + public static Collection determineBasicProperties(Class beanClass) + throws IntrospectionException { + + Map pdMap = new TreeMap<>(); + + for (Method method : beanClass.getMethods()) { + String methodName = method.getName(); + + boolean setter; + int nameIndex; + if (methodName.startsWith("set") && method.getParameterCount() == 1) { + setter = true; + nameIndex = 3; + } + else if (methodName.startsWith("get") && method.getParameterCount() == 0 && method.getReturnType() != Void.TYPE) { + setter = false; + nameIndex = 3; + } + else if (methodName.startsWith("is") && method.getParameterCount() == 0 && method.getReturnType() == boolean.class) { + setter = false; + nameIndex = 2; + } + else { + continue; + } + + String propertyName = StringUtils.uncapitalizeAsProperty(methodName.substring(nameIndex)); + if (propertyName.isEmpty()) { + continue; + } + + BasicPropertyDescriptor pd = pdMap.get(propertyName); + if (pd != null) { + if (setter) { + if (pd.getWriteMethod() == null || + pd.getWriteMethod().getParameterTypes()[0].isAssignableFrom(method.getParameterTypes()[0])) { + pd.setWriteMethod(method); + } + else { + pd.addWriteMethod(method); + } + } + else { + if (pd.getReadMethod() == null || + (pd.getReadMethod().getReturnType() == method.getReturnType() && method.getName().startsWith("is"))) { + pd.setReadMethod(method); + } + } + } + else { + pd = new BasicPropertyDescriptor(propertyName, (!setter ? method : null), (setter ? method : null)); + pdMap.put(propertyName, pd); + } + } + + return pdMap.values(); + } + /** * See {@link java.beans.FeatureDescriptor}. */ @@ -173,4 +253,72 @@ public static boolean equals(PropertyDescriptor pd, PropertyDescriptor otherPd) pd.isBound() == otherPd.isBound() && pd.isConstrained() == otherPd.isConstrained()); } + + /** + * PropertyDescriptor for {@link #determineBasicProperties(Class)}, + * not performing any early type determination for + * {@link #setReadMethod}/{@link #setWriteMethod}. + * @since 5.3.24 + */ + private static class BasicPropertyDescriptor extends PropertyDescriptor { + + @Nullable + private Method readMethod; + + @Nullable + private Method writeMethod; + + private final List alternativeWriteMethods = new ArrayList<>(); + + public BasicPropertyDescriptor(String propertyName, @Nullable Method readMethod, @Nullable Method writeMethod) + throws IntrospectionException { + + super(propertyName, readMethod, writeMethod); + } + + @Override + public void setReadMethod(@Nullable Method readMethod) { + this.readMethod = readMethod; + } + + @Override + @Nullable + public Method getReadMethod() { + return this.readMethod; + } + + @Override + public void setWriteMethod(@Nullable Method writeMethod) { + this.writeMethod = writeMethod; + } + + public void addWriteMethod(Method writeMethod) { + if (this.writeMethod != null) { + this.alternativeWriteMethods.add(this.writeMethod); + this.writeMethod = null; + } + this.alternativeWriteMethods.add(writeMethod); + } + + @Override + @Nullable + public Method getWriteMethod() { + if (this.writeMethod == null && !this.alternativeWriteMethods.isEmpty()) { + if (this.readMethod == null) { + return this.alternativeWriteMethods.get(0); + } + else { + for (Method method : this.alternativeWriteMethods) { + if (this.readMethod.getReturnType().isAssignableFrom(method.getParameterTypes()[0])) { + this.writeMethod = method; + break; + } + } + } + } + return this.writeMethod; + } + } + + } diff --git a/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistrar.java b/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistrar.java index 1d5974cfd155..69e2a68b3e3c 100644 --- a/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistrar.java +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistrar.java @@ -22,7 +22,7 @@ * {@link org.springframework.beans.PropertyEditorRegistry property editor registry}. * *

    This is particularly useful when you need to use the same set of - * property editors in several different situations: write a corresponding + * property editors in several situations: write a corresponding * registrar and reuse that in each case. * * @author Juergen Hoeller diff --git a/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistrySupport.java b/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistrySupport.java index d1354e1d89b0..2d39e2ef096c 100644 --- a/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistrySupport.java +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistrySupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -71,7 +71,6 @@ import org.springframework.beans.propertyeditors.URLEditor; import org.springframework.beans.propertyeditors.UUIDEditor; import org.springframework.beans.propertyeditors.ZoneIdEditor; -import org.springframework.core.SpringProperties; import org.springframework.core.convert.ConversionService; import org.springframework.core.io.Resource; import org.springframework.core.io.support.ResourceArrayPropertyEditor; @@ -93,14 +92,6 @@ */ public class PropertyEditorRegistrySupport implements PropertyEditorRegistry { - /** - * Boolean flag controlled by a {@code spring.xml.ignore} system property that instructs Spring to - * ignore XML, i.e. to not initialize the XML-related infrastructure. - *

    The default is "false". - */ - private static final boolean shouldIgnoreXml = SpringProperties.getFlag("spring.xml.ignore"); - - @Nullable private ConversionService conversionService; @@ -218,9 +209,7 @@ private void createDefaultEditors() { this.defaultEditors.put(Currency.class, new CurrencyEditor()); this.defaultEditors.put(File.class, new FileEditor()); this.defaultEditors.put(InputStream.class, new InputStreamEditor()); - if (!shouldIgnoreXml) { - this.defaultEditors.put(InputSource.class, new InputSourceEditor()); - } + this.defaultEditors.put(InputSource.class, new InputSourceEditor()); this.defaultEditors.put(Locale.class, new LocaleEditor()); this.defaultEditors.put(Path.class, new PathEditor()); this.defaultEditors.put(Pattern.class, new PatternEditor()); @@ -422,16 +411,19 @@ private PropertyEditor getCustomEditor(@Nullable Class requiredType) { } if (editor == null) { // Find editor for superclass or interface. - for (Iterator> it = this.customEditors.keySet().iterator(); it.hasNext() && editor == null;) { - Class key = it.next(); + for (Map.Entry, PropertyEditor> entry : this.customEditors.entrySet()) { + Class key = entry.getKey(); if (key.isAssignableFrom(requiredType)) { - editor = this.customEditors.get(key); + editor = entry.getValue(); // Cache editor for search type, to avoid the overhead // of repeated assignable-from checks. if (this.customEditorCache == null) { this.customEditorCache = new HashMap<>(); } this.customEditorCache.put(requiredType, editor); + if (editor != null) { + break; + } } } } diff --git a/spring-beans/src/main/java/org/springframework/beans/PropertyValue.java b/spring-beans/src/main/java/org/springframework/beans/PropertyValue.java index 16c6bae560a4..93b97042b010 100644 --- a/spring-beans/src/main/java/org/springframework/beans/PropertyValue.java +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyValue.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -192,10 +192,9 @@ public boolean equals(@Nullable Object other) { if (this == other) { return true; } - if (!(other instanceof PropertyValue)) { + if (!(other instanceof PropertyValue otherPv)) { return false; } - PropertyValue otherPv = (PropertyValue) other; return (this.name.equals(otherPv.name) && ObjectUtils.nullSafeEquals(this.value, otherPv.value) && ObjectUtils.nullSafeEquals(getSource(), otherPv.getSource())); diff --git a/spring-beans/src/main/java/org/springframework/beans/SimpleBeanInfoFactory.java b/spring-beans/src/main/java/org/springframework/beans/SimpleBeanInfoFactory.java new file mode 100644 index 000000000000..6719b156a510 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/SimpleBeanInfoFactory.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-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.beans; + +import java.beans.BeanInfo; +import java.beans.IntrospectionException; +import java.beans.PropertyDescriptor; +import java.beans.SimpleBeanInfo; +import java.util.Collection; + +import org.springframework.core.Ordered; +import org.springframework.lang.NonNull; + +/** + * {@link BeanInfoFactory} implementation that bypasses the standard {@link java.beans.Introspector} + * for faster introspection, reduced to basic property determination (as commonly needed in Spring). + * + *

    Used by default in 6.0 through direct invocation from {@link CachedIntrospectionResults}. + * Potentially configured via a {@code META-INF/spring.factories} file with the following content, + * overriding other custom {@code org.springframework.beans.BeanInfoFactory} declarations: + * {@code org.springframework.beans.BeanInfoFactory=org.springframework.beans.SimpleBeanInfoFactory} + * + *

    Ordered at {@code Ordered.LOWEST_PRECEDENCE - 1} to override {@link ExtendedBeanInfoFactory} + * (registered by default in 5.3) if necessary while still allowing other user-defined + * {@link BeanInfoFactory} types to take precedence. + * + * @author Juergen Hoeller + * @since 5.3.24 + * @see ExtendedBeanInfoFactory + * @see CachedIntrospectionResults + */ +class SimpleBeanInfoFactory implements BeanInfoFactory, Ordered { + + @Override + @NonNull + public BeanInfo getBeanInfo(Class beanClass) throws IntrospectionException { + Collection pds = + PropertyDescriptorUtils.determineBasicProperties(beanClass); + + return new SimpleBeanInfo() { + @Override + public PropertyDescriptor[] getPropertyDescriptors() { + return pds.toArray(PropertyDescriptorUtils.EMPTY_PROPERTY_DESCRIPTOR_ARRAY); + } + }; + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE - 1; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/StandardBeanInfoFactory.java b/spring-beans/src/main/java/org/springframework/beans/StandardBeanInfoFactory.java new file mode 100644 index 000000000000..7abdade7c47f --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/StandardBeanInfoFactory.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-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.beans; + +import java.beans.BeanInfo; +import java.beans.IntrospectionException; +import java.beans.Introspector; + +import org.springframework.core.Ordered; +import org.springframework.core.SpringProperties; +import org.springframework.lang.NonNull; + +/** + * {@link BeanInfoFactory} implementation that performs standard + * {@link java.beans.Introspector} inspection. + * + *

    To be configured via a {@code META-INF/spring.factories} file with the following content: + * {@code org.springframework.beans.BeanInfoFactory=org.springframework.beans.StandardBeanInfoFactory} + * + *

    Ordered at {@link Ordered#LOWEST_PRECEDENCE} to allow other user-defined + * {@link BeanInfoFactory} types to take precedence. + * + * @author Juergen Hoeller + * @since 6.0 + * @see ExtendedBeanInfoFactory + * @see CachedIntrospectionResults + * @see Introspector#getBeanInfo(Class) + */ +public class StandardBeanInfoFactory implements BeanInfoFactory, Ordered { + + /** + * System property that instructs Spring to use the {@link Introspector#IGNORE_ALL_BEANINFO} + * mode when calling the JavaBeans {@link Introspector}: "spring.beaninfo.ignore", with a + * value of "true" skipping the search for {@code BeanInfo} classes (typically for scenarios + * where no such classes are being defined for beans in the application in the first place). + *

    The default is "false", considering all {@code BeanInfo} metadata classes, like for + * standard {@link Introspector#getBeanInfo(Class)} calls. Consider switching this flag to + * "true" if you experience repeated ClassLoader access for non-existing {@code BeanInfo} + * classes, in case such access is expensive on startup or on lazy loading. + *

    Note that such an effect may also indicate a scenario where caching doesn't work + * effectively: Prefer an arrangement where the Spring jars live in the same ClassLoader + * as the application classes, which allows for clean caching along with the application's + * lifecycle in any case. For a web application, consider declaring a local + * {@link org.springframework.web.util.IntrospectorCleanupListener} in {@code web.xml} + * in case of a multi-ClassLoader layout, which will allow for effective caching as well. + * @see Introspector#getBeanInfo(Class, int) + */ + public static final String IGNORE_BEANINFO_PROPERTY_NAME = "spring.beaninfo.ignore"; + + private static final boolean shouldIntrospectorIgnoreBeaninfoClasses = + SpringProperties.getFlag(IGNORE_BEANINFO_PROPERTY_NAME); + + + @Override + @NonNull + public BeanInfo getBeanInfo(Class beanClass) throws IntrospectionException { + return (shouldIntrospectorIgnoreBeaninfoClasses ? + Introspector.getBeanInfo(beanClass, Introspector.IGNORE_ALL_BEANINFO) : + Introspector.getBeanInfo(beanClass)); + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/TypeConverterDelegate.java b/spring-beans/src/main/java/org/springframework/beans/TypeConverterDelegate.java index 38a59ba3d687..2c419b1c109b 100644 --- a/spring-beans/src/main/java/org/springframework/beans/TypeConverterDelegate.java +++ b/spring-beans/src/main/java/org/springframework/beans/TypeConverterDelegate.java @@ -247,14 +247,14 @@ else if (conversionService != null && typeDescriptor != null) { // Definitely doesn't match: throw IllegalArgumentException/IllegalStateException StringBuilder msg = new StringBuilder(); msg.append("Cannot convert value of type '").append(ClassUtils.getDescriptiveType(newValue)); - msg.append("' to required type '").append(ClassUtils.getQualifiedName(requiredType)).append("'"); + msg.append("' to required type '").append(ClassUtils.getQualifiedName(requiredType)).append('\''); if (propertyName != null) { - msg.append(" for property '").append(propertyName).append("'"); + msg.append(" for property '").append(propertyName).append('\''); } if (editor != null) { msg.append(": PropertyEditor [").append(editor.getClass().getName()).append( "] returned inappropriate value of type '").append( - ClassUtils.getDescriptiveType(convertedValue)).append("'"); + ClassUtils.getDescriptiveType(convertedValue)).append('\''); throw new IllegalArgumentException(msg.toString()); } else { diff --git a/spring-beans/src/main/java/org/springframework/beans/TypeMismatchException.java b/spring-beans/src/main/java/org/springframework/beans/TypeMismatchException.java index 3177c4b20ed2..ccfa6a003050 100644 --- a/spring-beans/src/main/java/org/springframework/beans/TypeMismatchException.java +++ b/spring-beans/src/main/java/org/springframework/beans/TypeMismatchException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -41,10 +41,10 @@ public class TypeMismatchException extends PropertyAccessException { private String propertyName; @Nullable - private transient Object value; + private final transient Object value; @Nullable - private Class requiredType; + private final Class requiredType; /** @@ -71,7 +71,8 @@ public TypeMismatchException(PropertyChangeEvent propertyChangeEvent, @Nullable (requiredType != null ? " to required type '" + ClassUtils.getQualifiedName(requiredType) + "'" : "") + (propertyChangeEvent.getPropertyName() != null ? - " for property '" + propertyChangeEvent.getPropertyName() + "'" : ""), + " for property '" + propertyChangeEvent.getPropertyName() + "'" : "") + + (cause != null ? "; " + cause.getMessage() : ""), cause); this.propertyName = propertyChangeEvent.getPropertyName(); this.value = propertyChangeEvent.getNewValue(); @@ -97,7 +98,8 @@ public TypeMismatchException(@Nullable Object value, @Nullable Class required */ public TypeMismatchException(@Nullable Object value, @Nullable Class requiredType, @Nullable Throwable cause) { super("Failed to convert value of type '" + ClassUtils.getDescriptiveType(value) + "'" + - (requiredType != null ? " to required type '" + ClassUtils.getQualifiedName(requiredType) + "'" : ""), + (requiredType != null ? " to required type '" + ClassUtils.getQualifiedName(requiredType) + "'" : "") + + (cause != null ? "; " + cause.getMessage() : ""), cause); this.value = value; this.requiredType = requiredType; diff --git a/spring-beans/src/main/java/org/springframework/beans/annotation/AnnotationBeanUtils.java b/spring-beans/src/main/java/org/springframework/beans/annotation/AnnotationBeanUtils.java deleted file mode 100644 index c0b22ad47d37..000000000000 --- a/spring-beans/src/main/java/org/springframework/beans/annotation/AnnotationBeanUtils.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2002-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.beans.annotation; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Method; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -import org.springframework.beans.BeanWrapper; -import org.springframework.beans.PropertyAccessorFactory; -import org.springframework.lang.Nullable; -import org.springframework.util.ReflectionUtils; -import org.springframework.util.StringValueResolver; - -/** - * General utility methods for working with annotations in JavaBeans style. - * - * @author Rob Harrop - * @author Juergen Hoeller - * @since 2.0 - * @deprecated as of 5.2, in favor of custom annotation attribute processing - */ -@Deprecated -public abstract class AnnotationBeanUtils { - - /** - * Copy the properties of the supplied {@link Annotation} to the supplied target bean. - * Any properties defined in {@code excludedProperties} will not be copied. - * @param ann the annotation to copy from - * @param bean the bean instance to copy to - * @param excludedProperties the names of excluded properties, if any - * @see org.springframework.beans.BeanWrapper - */ - public static void copyPropertiesToBean(Annotation ann, Object bean, String... excludedProperties) { - copyPropertiesToBean(ann, bean, null, excludedProperties); - } - - /** - * Copy the properties of the supplied {@link Annotation} to the supplied target bean. - * Any properties defined in {@code excludedProperties} will not be copied. - *

    A specified value resolver may resolve placeholders in property values, for example. - * @param ann the annotation to copy from - * @param bean the bean instance to copy to - * @param valueResolver a resolve to post-process String property values (may be {@code null}) - * @param excludedProperties the names of excluded properties, if any - * @see org.springframework.beans.BeanWrapper - */ - public static void copyPropertiesToBean(Annotation ann, Object bean, @Nullable StringValueResolver valueResolver, - String... excludedProperties) { - - Set excluded = (excludedProperties.length == 0 ? Collections.emptySet() : - new HashSet<>(Arrays.asList(excludedProperties))); - Method[] annotationProperties = ann.annotationType().getDeclaredMethods(); - BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(bean); - for (Method annotationProperty : annotationProperties) { - String propertyName = annotationProperty.getName(); - if (!excluded.contains(propertyName) && bw.isWritableProperty(propertyName)) { - Object value = ReflectionUtils.invokeMethod(annotationProperty, ann); - if (valueResolver != null && value instanceof String) { - value = valueResolver.resolveStringValue((String) value); - } - bw.setPropertyValue(propertyName, value); - } - } - } - -} diff --git a/spring-beans/src/main/java/org/springframework/beans/annotation/package-info.java b/spring-beans/src/main/java/org/springframework/beans/annotation/package-info.java deleted file mode 100644 index d2667da96fd4..000000000000 --- a/spring-beans/src/main/java/org/springframework/beans/annotation/package-info.java +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Support package for beans-style handling of Java 5 annotations. - */ -@NonNullApi -@NonNullFields -package org.springframework.beans.annotation; - -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactory.java index f31c29d85170..93c909e047b6 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -84,7 +84,7 @@ * (only applicable when running in a web application context) *

  • {@code postProcessBeforeInitialization} methods of BeanPostProcessors *
  • InitializingBean's {@code afterPropertiesSet} - *
  • a custom init-method definition + *
  • a custom {@code init-method} definition *
  • {@code postProcessAfterInitialization} methods of BeanPostProcessors * * @@ -92,7 +92,7 @@ *
      *
    1. {@code postProcessBeforeDestruction} methods of DestructionAwareBeanPostProcessors *
    2. DisposableBean's {@code destroy} - *
    3. a custom destroy-method definition + *
    4. a custom {@code destroy-method} definition *
    * * @author Rod Johnson @@ -102,6 +102,8 @@ * @see BeanNameAware#setBeanName * @see BeanClassLoaderAware#setBeanClassLoader * @see BeanFactoryAware#setBeanFactory + * @see org.springframework.context.EnvironmentAware#setEnvironment + * @see org.springframework.context.EmbeddedValueResolverAware#setEmbeddedValueResolver * @see org.springframework.context.ResourceLoaderAware#setResourceLoader * @see org.springframework.context.ApplicationEventPublisherAware#setApplicationEventPublisher * @see org.springframework.context.MessageSourceAware#setMessageSource @@ -111,6 +113,7 @@ * @see InitializingBean#afterPropertiesSet * @see org.springframework.beans.factory.support.RootBeanDefinition#getInitMethodName * @see org.springframework.beans.factory.config.BeanPostProcessor#postProcessAfterInitialization + * @see org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor#postProcessBeforeDestruction * @see DisposableBean#destroy * @see org.springframework.beans.factory.support.RootBeanDefinition#getDestroyMethodName */ @@ -211,6 +214,7 @@ public interface BeanFactory { /** * Return a provider for the specified bean, allowing for lazy on-demand retrieval * of instances, including availability and uniqueness options. + *

    For matching a generic type, consider {@link #getBeanProvider(ResolvableType)}. * @param requiredType type the bean must match; can be an interface or superclass * @return a corresponding provider handle * @since 5.1 @@ -220,13 +224,20 @@ public interface BeanFactory { /** * Return a provider for the specified bean, allowing for lazy on-demand retrieval - * of instances, including availability and uniqueness options. - * @param requiredType type the bean must match; can be a generic type declaration. - * Note that collection types are not supported here, in contrast to reflective + * of instances, including availability and uniqueness options. This variant allows + * for specifying a generic type to match, similar to reflective injection points + * with generic type declarations in method/constructor parameters. + *

    Note that collections of beans are not supported here, in contrast to reflective * injection points. For programmatically retrieving a list of beans matching a * specific type, specify the actual bean type as an argument here and subsequently * use {@link ObjectProvider#orderedStream()} or its lazy streaming/iteration options. + *

    Also, generics matching is strict here, as per the Java assignment rules. + * For lenient fallback matching with unchecked semantics (similar to the ´unchecked´ + * Java compiler warning), consider calling {@link #getBeanProvider(Class)} with the + * raw type as a second step if no full generic match is + * {@link ObjectProvider#getIfAvailable() available} with this variant. * @return a corresponding provider handle + * @param requiredType type the bean must match; can be a generic type declaration * @since 5.1 * @see ObjectProvider#iterator() * @see ObjectProvider#stream() diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactoryUtils.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactoryUtils.java index 17a5d70460de..079760177033 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactoryUtils.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactoryUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -161,11 +161,9 @@ public static String[] beanNamesIncludingAncestors(ListableBeanFactory lbf) { public static String[] beanNamesForTypeIncludingAncestors(ListableBeanFactory lbf, ResolvableType type) { Assert.notNull(lbf, "ListableBeanFactory must not be null"); String[] result = lbf.getBeanNamesForType(type); - if (lbf instanceof HierarchicalBeanFactory) { - HierarchicalBeanFactory hbf = (HierarchicalBeanFactory) lbf; - if (hbf.getParentBeanFactory() instanceof ListableBeanFactory) { - String[] parentResult = beanNamesForTypeIncludingAncestors( - (ListableBeanFactory) hbf.getParentBeanFactory(), type); + if (lbf instanceof HierarchicalBeanFactory hbf) { + if (hbf.getParentBeanFactory() instanceof ListableBeanFactory pbf) { + String[] parentResult = beanNamesForTypeIncludingAncestors(pbf, type); result = mergeNamesWithParent(result, parentResult, hbf); } } @@ -199,11 +197,10 @@ public static String[] beanNamesForTypeIncludingAncestors( Assert.notNull(lbf, "ListableBeanFactory must not be null"); String[] result = lbf.getBeanNamesForType(type, includeNonSingletons, allowEagerInit); - if (lbf instanceof HierarchicalBeanFactory) { - HierarchicalBeanFactory hbf = (HierarchicalBeanFactory) lbf; - if (hbf.getParentBeanFactory() instanceof ListableBeanFactory) { + if (lbf instanceof HierarchicalBeanFactory hbf) { + if (hbf.getParentBeanFactory() instanceof ListableBeanFactory pbf) { String[] parentResult = beanNamesForTypeIncludingAncestors( - (ListableBeanFactory) hbf.getParentBeanFactory(), type, includeNonSingletons, allowEagerInit); + pbf, type, includeNonSingletons, allowEagerInit); result = mergeNamesWithParent(result, parentResult, hbf); } } @@ -226,11 +223,9 @@ public static String[] beanNamesForTypeIncludingAncestors( public static String[] beanNamesForTypeIncludingAncestors(ListableBeanFactory lbf, Class type) { Assert.notNull(lbf, "ListableBeanFactory must not be null"); String[] result = lbf.getBeanNamesForType(type); - if (lbf instanceof HierarchicalBeanFactory) { - HierarchicalBeanFactory hbf = (HierarchicalBeanFactory) lbf; - if (hbf.getParentBeanFactory() instanceof ListableBeanFactory) { - String[] parentResult = beanNamesForTypeIncludingAncestors( - (ListableBeanFactory) hbf.getParentBeanFactory(), type); + if (lbf instanceof HierarchicalBeanFactory hbf) { + if (hbf.getParentBeanFactory() instanceof ListableBeanFactory pbf) { + String[] parentResult = beanNamesForTypeIncludingAncestors(pbf, type); result = mergeNamesWithParent(result, parentResult, hbf); } } @@ -263,11 +258,10 @@ public static String[] beanNamesForTypeIncludingAncestors( Assert.notNull(lbf, "ListableBeanFactory must not be null"); String[] result = lbf.getBeanNamesForType(type, includeNonSingletons, allowEagerInit); - if (lbf instanceof HierarchicalBeanFactory) { - HierarchicalBeanFactory hbf = (HierarchicalBeanFactory) lbf; - if (hbf.getParentBeanFactory() instanceof ListableBeanFactory) { + if (lbf instanceof HierarchicalBeanFactory hbf) { + if (hbf.getParentBeanFactory() instanceof ListableBeanFactory pbf) { String[] parentResult = beanNamesForTypeIncludingAncestors( - (ListableBeanFactory) hbf.getParentBeanFactory(), type, includeNonSingletons, allowEagerInit); + pbf, type, includeNonSingletons, allowEagerInit); result = mergeNamesWithParent(result, parentResult, hbf); } } @@ -289,11 +283,9 @@ public static String[] beanNamesForAnnotationIncludingAncestors( Assert.notNull(lbf, "ListableBeanFactory must not be null"); String[] result = lbf.getBeanNamesForAnnotation(annotationType); - if (lbf instanceof HierarchicalBeanFactory) { - HierarchicalBeanFactory hbf = (HierarchicalBeanFactory) lbf; - if (hbf.getParentBeanFactory() instanceof ListableBeanFactory) { - String[] parentResult = beanNamesForAnnotationIncludingAncestors( - (ListableBeanFactory) hbf.getParentBeanFactory(), annotationType); + if (lbf instanceof HierarchicalBeanFactory hbf) { + if (hbf.getParentBeanFactory() instanceof ListableBeanFactory pbf) { + String[] parentResult = beanNamesForAnnotationIncludingAncestors(pbf, annotationType); result = mergeNamesWithParent(result, parentResult, hbf); } } @@ -327,11 +319,9 @@ public static Map beansOfTypeIncludingAncestors(ListableBeanFacto Assert.notNull(lbf, "ListableBeanFactory must not be null"); Map result = new LinkedHashMap<>(4); result.putAll(lbf.getBeansOfType(type)); - if (lbf instanceof HierarchicalBeanFactory) { - HierarchicalBeanFactory hbf = (HierarchicalBeanFactory) lbf; - if (hbf.getParentBeanFactory() instanceof ListableBeanFactory) { - Map parentResult = beansOfTypeIncludingAncestors( - (ListableBeanFactory) hbf.getParentBeanFactory(), type); + if (lbf instanceof HierarchicalBeanFactory hbf) { + if (hbf.getParentBeanFactory() instanceof ListableBeanFactory pbf) { + Map parentResult = beansOfTypeIncludingAncestors(pbf, type); parentResult.forEach((beanName, beanInstance) -> { if (!result.containsKey(beanName) && !hbf.containsLocalBean(beanName)) { result.put(beanName, beanInstance); @@ -376,11 +366,9 @@ public static Map beansOfTypeIncludingAncestors( Assert.notNull(lbf, "ListableBeanFactory must not be null"); Map result = new LinkedHashMap<>(4); result.putAll(lbf.getBeansOfType(type, includeNonSingletons, allowEagerInit)); - if (lbf instanceof HierarchicalBeanFactory) { - HierarchicalBeanFactory hbf = (HierarchicalBeanFactory) lbf; - if (hbf.getParentBeanFactory() instanceof ListableBeanFactory) { - Map parentResult = beansOfTypeIncludingAncestors( - (ListableBeanFactory) hbf.getParentBeanFactory(), type, includeNonSingletons, allowEagerInit); + if (lbf instanceof HierarchicalBeanFactory hbf) { + if (hbf.getParentBeanFactory() instanceof ListableBeanFactory pbf) { + Map parentResult = beansOfTypeIncludingAncestors(pbf, type, includeNonSingletons, allowEagerInit); parentResult.forEach((beanName, beanInstance) -> { if (!result.containsKey(beanName) && !hbf.containsLocalBean(beanName)) { result.put(beanName, beanInstance); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/FactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/FactoryBean.java index 224563cc7749..97362ce1f7c9 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/FactoryBean.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/FactoryBean.java @@ -45,7 +45,7 @@ * *

    The container is only responsible for managing the lifecycle of the FactoryBean * instance, not the lifecycle of the objects created by the FactoryBean. Therefore, - * a destroy method on an exposed bean object (such as {@link java.io.Closeable#close()} + * a destroy method on an exposed bean object (such as {@link java.io.Closeable#close()}) * will not be called automatically. Instead, a FactoryBean should implement * {@link DisposableBean} and delegate any such close call to the underlying object. * @@ -108,7 +108,7 @@ public interface FactoryBean { * been fully initialized. It must not rely on state created during * initialization; of course, it can still use such state if available. *

    NOTE: Autowiring will simply ignore FactoryBeans that return - * {@code null} here. Therefore it is highly recommended to implement + * {@code null} here. Therefore, it is highly recommended to implement * this method properly, using the current state of the FactoryBean. * @return the type of object that this FactoryBean creates, * or {@code null} if not known at the time of the call diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/InjectionPoint.java b/spring-beans/src/main/java/org/springframework/beans/factory/InjectionPoint.java index 13e765893dc4..eed896e1b4f0 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/InjectionPoint.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/InjectionPoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -112,7 +112,7 @@ public Field getField() { * @since 5.0 */ protected final MethodParameter obtainMethodParameter() { - Assert.state(this.methodParameter != null, "Neither Field nor MethodParameter"); + Assert.state(this.methodParameter != null, "MethodParameter is not available"); return this.methodParameter; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/ListableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/ListableBeanFactory.java index 389f19c9e488..edb0381dbd00 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/ListableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/ListableBeanFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -18,6 +18,7 @@ import java.lang.annotation.Annotation; import java.util.Map; +import java.util.Set; import org.springframework.beans.BeansException; import org.springframework.core.ResolvableType; @@ -322,7 +323,8 @@ Map getBeansOfType(@Nullable Class type, boolean includeNonSin * (at class, interface or factory method level of the specified bean) * @return the names of all matching beans * @since 4.0 - * @see #findAnnotationOnBean + * @see #getBeansWithAnnotation(Class) + * @see #findAnnotationOnBean(String, Class) */ String[] getBeanNamesForAnnotation(Class annotationType); @@ -337,13 +339,15 @@ Map getBeansOfType(@Nullable Class type, boolean includeNonSin * keys and the corresponding bean instances as values * @throws BeansException if a bean could not be created * @since 3.0 - * @see #findAnnotationOnBean + * @see #findAnnotationOnBean(String, Class) + * @see #findAnnotationOnBean(String, Class, boolean) + * @see #findAllAnnotationsOnBean(String, Class, boolean) */ Map getBeansWithAnnotation(Class annotationType) throws BeansException; /** * Find an {@link Annotation} of {@code annotationType} on the specified bean, - * traversing its interfaces and super classes if no annotation can be found on + * traversing its interfaces and superclasses if no annotation can be found on * the given class itself, as well as checking the bean's factory method (if any). * @param beanName the name of the bean to look for annotations on * @param annotationType the type of annotation to look for @@ -351,11 +355,57 @@ Map getBeansOfType(@Nullable Class type, boolean includeNonSin * @return the annotation of the given type if found, or {@code null} otherwise * @throws NoSuchBeanDefinitionException if there is no bean with the given name * @since 3.0 - * @see #getBeanNamesForAnnotation - * @see #getBeansWithAnnotation + * @see #findAnnotationOnBean(String, Class, boolean) + * @see #findAllAnnotationsOnBean(String, Class, boolean) + * @see #getBeanNamesForAnnotation(Class) + * @see #getBeansWithAnnotation(Class) + * @see #getType(String) */ @Nullable A findAnnotationOnBean(String beanName, Class annotationType) throws NoSuchBeanDefinitionException; + /** + * Find an {@link Annotation} of {@code annotationType} on the specified bean, + * traversing its interfaces and superclasses if no annotation can be found on + * the given class itself, as well as checking the bean's factory method (if any). + * @param beanName the name of the bean to look for annotations on + * @param annotationType the type of annotation to look for + * (at class, interface or factory method level of the specified bean) + * @param allowFactoryBeanInit whether a {@code FactoryBean} may get initialized + * just for the purpose of determining its object type + * @return the annotation of the given type if found, or {@code null} otherwise + * @throws NoSuchBeanDefinitionException if there is no bean with the given name + * @since 5.3.14 + * @see #findAnnotationOnBean(String, Class) + * @see #findAllAnnotationsOnBean(String, Class, boolean) + * @see #getBeanNamesForAnnotation(Class) + * @see #getBeansWithAnnotation(Class) + * @see #getType(String, boolean) + */ + @Nullable + A findAnnotationOnBean( + String beanName, Class annotationType, boolean allowFactoryBeanInit) + throws NoSuchBeanDefinitionException; + + /** + * Find all {@link Annotation} instances of {@code annotationType} on the specified + * bean, traversing its interfaces and superclasses if no annotation can be found on + * the given class itself, as well as checking the bean's factory method (if any). + * @param beanName the name of the bean to look for annotations on + * @param annotationType the type of annotation to look for + * (at class, interface or factory method level of the specified bean) + * @param allowFactoryBeanInit whether a {@code FactoryBean} may get initialized + * just for the purpose of determining its object type + * @return the set of annotations of the given type found (potentially empty) + * @throws NoSuchBeanDefinitionException if there is no bean with the given name + * @since 6.0 + * @see #getBeanNamesForAnnotation(Class) + * @see #findAnnotationOnBean(String, Class, boolean) + * @see #getType(String, boolean) + */ + Set findAllAnnotationsOnBean( + String beanName, Class annotationType, boolean allowFactoryBeanInit) + throws NoSuchBeanDefinitionException; + } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/UnsatisfiedDependencyException.java b/spring-beans/src/main/java/org/springframework/beans/factory/UnsatisfiedDependencyException.java index 8ae820d9ac81..29c4b47349f8 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/UnsatisfiedDependencyException.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/UnsatisfiedDependencyException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -62,7 +62,7 @@ public UnsatisfiedDependencyException( public UnsatisfiedDependencyException( @Nullable String resourceDescription, @Nullable String beanName, String propertyName, BeansException ex) { - this(resourceDescription, beanName, propertyName, ""); + this(resourceDescription, beanName, propertyName, ex.getMessage()); initCause(ex); } @@ -94,7 +94,7 @@ public UnsatisfiedDependencyException( public UnsatisfiedDependencyException( @Nullable String resourceDescription, @Nullable String beanName, @Nullable InjectionPoint injectionPoint, BeansException ex) { - this(resourceDescription, beanName, injectionPoint, ""); + this(resourceDescription, beanName, injectionPoint, ex.getMessage()); initCause(ex); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AnnotatedGenericBeanDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AnnotatedGenericBeanDefinition.java index c0cf85a172f1..b8cb9070636c 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AnnotatedGenericBeanDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AnnotatedGenericBeanDefinition.java @@ -70,8 +70,8 @@ public AnnotatedGenericBeanDefinition(Class beanClass) { */ public AnnotatedGenericBeanDefinition(AnnotationMetadata metadata) { Assert.notNull(metadata, "AnnotationMetadata must not be null"); - if (metadata instanceof StandardAnnotationMetadata) { - setBeanClass(((StandardAnnotationMetadata) metadata).getIntrospectedClass()); + if (metadata instanceof StandardAnnotationMetadata sam) { + setBeanClass(sam.getIntrospectedClass()); } else { setBeanClassName(metadata.getClassName()); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Autowired.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Autowired.java index 242fddbad8e1..fad36ac5bf08 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Autowired.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Autowired.java @@ -25,7 +25,7 @@ /** * Marks a constructor, field, setter method, or config method as to be autowired by * Spring's dependency injection facilities. This is an alternative to the JSR-330 - * {@link javax.inject.Inject} annotation, adding required-vs-optional semantics. + * {@link jakarta.inject.Inject} annotation, adding required-vs-optional semantics. * *

    Autowired Constructors

    *

    Only one constructor of any given bean class may declare this annotation with the diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java index bd42f89d3010..4c71d9ad83c3 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -22,10 +22,13 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Member; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.lang.reflect.Proxy; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashSet; @@ -37,6 +40,12 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.aot.generate.AccessControl; +import org.springframework.aot.generate.GeneratedClass; +import org.springframework.aot.generate.GeneratedMethod; +import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.hint.ExecutableMode; +import org.springframework.aot.hint.RuntimeHints; import org.springframework.beans.BeanUtils; import org.springframework.beans.BeansException; import org.springframework.beans.PropertyValues; @@ -44,27 +53,38 @@ import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; -import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.InjectionPoint; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.UnsatisfiedDependencyException; +import org.springframework.beans.factory.aot.AutowiredArgumentsCodeGenerator; +import org.springframework.beans.factory.aot.AutowiredFieldValueResolver; +import org.springframework.beans.factory.aot.AutowiredMethodArgumentsResolver; +import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; +import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; +import org.springframework.beans.factory.aot.BeanRegistrationCode; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.DependencyDescriptor; import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor; +import org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory; +import org.springframework.beans.factory.support.AutowireCandidateResolver; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.LookupOverride; import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor; +import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.core.BridgeMethodResolver; import org.springframework.core.MethodParameter; import org.springframework.core.Ordered; import org.springframework.core.PriorityOrdered; -import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.javapoet.ClassName; +import org.springframework.javapoet.CodeBlock; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; @@ -75,8 +95,10 @@ * by default, Spring's {@link Autowired @Autowired} and {@link Value @Value} * annotations. * - *

    Also supports JSR-330's {@link javax.inject.Inject @Inject} annotation, + *

    Also supports the common {@link jakarta.inject.Inject @Inject} annotation, * if available, as a direct alternative to Spring's own {@code @Autowired}. + * Additionally, it retains support for the {@code javax.inject.Inject} variant + * dating back to the original JSR-330 specification (as known from Java EE 6-8). * *

    Autowired Constructors

    *

    Only one constructor of any given bean class may declare this annotation with @@ -123,13 +145,14 @@ * @author Stephane Nicoll * @author Sebastien Deleuze * @author Sam Brannen + * @author Phillip Webb * @since 2.5 * @see #setAutowiredAnnotationType * @see Autowired * @see Value */ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationAwareBeanPostProcessor, - MergedBeanDefinitionPostProcessor, PriorityOrdered, BeanFactoryAware { + MergedBeanDefinitionPostProcessor, BeanRegistrationAotProcessor, PriorityOrdered, BeanFactoryAware { protected final Log logger = LogFactory.getLog(getClass()); @@ -154,20 +177,30 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA /** * Create a new {@code AutowiredAnnotationBeanPostProcessor} for Spring's * standard {@link Autowired @Autowired} and {@link Value @Value} annotations. - *

    Also supports JSR-330's {@link javax.inject.Inject @Inject} annotation, - * if available. + *

    Also supports the common {@link jakarta.inject.Inject @Inject} annotation, + * if available, as well as the original {@code javax.inject.Inject} variant. */ @SuppressWarnings("unchecked") public AutowiredAnnotationBeanPostProcessor() { this.autowiredAnnotationTypes.add(Autowired.class); this.autowiredAnnotationTypes.add(Value.class); + + try { + this.autowiredAnnotationTypes.add((Class) + ClassUtils.forName("jakarta.inject.Inject", AutowiredAnnotationBeanPostProcessor.class.getClassLoader())); + logger.trace("'jakarta.inject.Inject' annotation found and supported for autowiring"); + } + catch (ClassNotFoundException ex) { + // jakarta.inject API not available - simply skip. + } + try { this.autowiredAnnotationTypes.add((Class) ClassUtils.forName("javax.inject.Inject", AutowiredAnnotationBeanPostProcessor.class.getClassLoader())); - logger.trace("JSR-330 'javax.inject.Inject' annotation found and supported for autowiring"); + logger.trace("'javax.inject.Inject' annotation found and supported for autowiring"); } catch (ClassNotFoundException ex) { - // JSR-330 API not available - simply skip. + // javax.inject API not available - simply skip. } } @@ -177,7 +210,7 @@ public AutowiredAnnotationBeanPostProcessor() { * setter methods, and arbitrary config methods. *

    The default autowired annotation types are the Spring-provided * {@link Autowired @Autowired} and {@link Value @Value} annotations as well - * as JSR-330's {@link javax.inject.Inject @Inject} annotation, if available. + * as the common {@code @Inject} annotation, if available. *

    This setter property exists so that developers can provide their own * (non-Spring-specific) annotation type to indicate that a member is supposed * to be autowired. @@ -193,7 +226,7 @@ public void setAutowiredAnnotationType(Class autowiredAnno * setter methods, and arbitrary config methods. *

    The default autowired annotation types are the Spring-provided * {@link Autowired @Autowired} and {@link Value @Value} annotations as well - * as JSR-330's {@link javax.inject.Inject @Inject} annotation, if available. + * as the common {@code @Inject} annotation, if available. *

    This setter property exists so that developers can provide their own * (non-Spring-specific) annotation types to indicate that a member is supposed * to be autowired. @@ -243,8 +276,40 @@ public void setBeanFactory(BeanFactory beanFactory) { @Override public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class beanType, String beanName) { + findInjectionMetadata(beanName, beanType, beanDefinition); + } + + @Override + public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + Class beanClass = registeredBean.getBeanClass(); + String beanName = registeredBean.getBeanName(); + RootBeanDefinition beanDefinition = registeredBean.getMergedBeanDefinition(); + InjectionMetadata metadata = findInjectionMetadata(beanName, beanClass, beanDefinition); + Collection autowiredElements = getAutowiredElements(metadata); + if (!ObjectUtils.isEmpty(autowiredElements)) { + return new AotContribution(beanClass, autowiredElements, getAutowireCandidateResolver()); + } + return null; + } + + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private Collection getAutowiredElements(InjectionMetadata metadata) { + return (Collection) metadata.getInjectedElements(); + } + + @Nullable + private AutowireCandidateResolver getAutowireCandidateResolver() { + if (this.beanFactory instanceof DefaultListableBeanFactory lbf) { + return lbf.getAutowireCandidateResolver(); + } + return null; + } + + private InjectionMetadata findInjectionMetadata(String beanName, Class beanType, RootBeanDefinition beanDefinition) { InjectionMetadata metadata = findAutowiringMetadata(beanName, beanType, null); metadata.checkConfigMembers(beanDefinition); + return metadata; } @Override @@ -253,44 +318,26 @@ public void resetBeanDefinition(String beanName) { this.injectionMetadataCache.remove(beanName); } + @Override + public Class determineBeanType(Class beanClass, String beanName) throws BeanCreationException { + checkLookupMethods(beanClass, beanName); + + // Pick up subclass with fresh lookup method override from above + if (this.beanFactory instanceof AbstractAutowireCapableBeanFactory aacbf) { + RootBeanDefinition mbd = (RootBeanDefinition) this.beanFactory.getMergedBeanDefinition(beanName); + if (mbd.getFactoryMethodName() == null && mbd.hasBeanClass()) { + return aacbf.getInstantiationStrategy().getActualBeanClass(mbd, beanName, this.beanFactory); + } + } + return beanClass; + } + @Override @Nullable public Constructor[] determineCandidateConstructors(Class beanClass, final String beanName) throws BeanCreationException { - // Let's check for lookup methods here... - if (!this.lookupMethodsChecked.contains(beanName)) { - if (AnnotationUtils.isCandidateClass(beanClass, Lookup.class)) { - try { - Class targetClass = beanClass; - do { - ReflectionUtils.doWithLocalMethods(targetClass, method -> { - Lookup lookup = method.getAnnotation(Lookup.class); - if (lookup != null) { - Assert.state(this.beanFactory != null, "No BeanFactory available"); - LookupOverride override = new LookupOverride(method, lookup.value()); - try { - RootBeanDefinition mbd = (RootBeanDefinition) - this.beanFactory.getMergedBeanDefinition(beanName); - mbd.getMethodOverrides().addOverride(override); - } - catch (NoSuchBeanDefinitionException ex) { - throw new BeanCreationException(beanName, - "Cannot apply @Lookup to beans without corresponding bean definition"); - } - } - }); - targetClass = targetClass.getSuperclass(); - } - while (targetClass != null && targetClass != Object.class); - - } - catch (IllegalStateException ex) { - throw new BeanCreationException(beanName, "Lookup method resolution failed", ex); - } - } - this.lookupMethodsChecked.add(beanName); - } + checkLookupMethods(beanClass, beanName); // Quick check on the concurrent map first, with minimal locking. Constructor[] candidateConstructors = this.candidateConstructorsCache.get(beanClass); @@ -392,6 +439,41 @@ else if (nonSyntheticConstructors == 1 && primaryConstructor != null) { return (candidateConstructors.length > 0 ? candidateConstructors : null); } + private void checkLookupMethods(Class beanClass, final String beanName) throws BeanCreationException { + if (!this.lookupMethodsChecked.contains(beanName)) { + if (AnnotationUtils.isCandidateClass(beanClass, Lookup.class)) { + try { + Class targetClass = beanClass; + do { + ReflectionUtils.doWithLocalMethods(targetClass, method -> { + Lookup lookup = method.getAnnotation(Lookup.class); + if (lookup != null) { + Assert.state(this.beanFactory != null, "No BeanFactory available"); + LookupOverride override = new LookupOverride(method, lookup.value()); + try { + RootBeanDefinition mbd = (RootBeanDefinition) + this.beanFactory.getMergedBeanDefinition(beanName); + mbd.getMethodOverrides().addOverride(override); + } + catch (NoSuchBeanDefinitionException ex) { + throw new BeanCreationException(beanName, + "Cannot apply @Lookup to beans without corresponding bean definition"); + } + } + }); + targetClass = targetClass.getSuperclass(); + } + while (targetClass != null && targetClass != Object.class); + + } + catch (IllegalStateException ex) { + throw new BeanCreationException(beanName, "Lookup method resolution failed", ex); + } + } + this.lookupMethodsChecked.add(beanName); + } + } + @Override public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) { InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs); @@ -407,14 +489,6 @@ public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, Str return pvs; } - @Deprecated - @Override - public PropertyValues postProcessPropertyValues( - PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) { - - return postProcessProperties(pvs, bean, beanName); - } - /** * 'Native' processing method for direct calls with an arbitrary target instance, * resolving all of its fields and methods which are annotated with one of the @@ -438,7 +512,6 @@ public void processInjection(Object bean) throws BeanCreationException { } } - private InjectionMetadata findAutowiringMetadata(String beanName, Class clazz, @Nullable PropertyValues pvs) { // Fall back to class name as cache key, for backwards compatibility with custom callers. String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName()); @@ -459,7 +532,7 @@ private InjectionMetadata findAutowiringMetadata(String beanName, Class clazz return metadata; } - private InjectionMetadata buildAutowiringMetadata(final Class clazz) { + private InjectionMetadata buildAutowiringMetadata(Class clazz) { if (!AnnotationUtils.isCandidateClass(clazz, this.autowiredAnnotationTypes)) { return InjectionMetadata.EMPTY; } @@ -537,42 +610,11 @@ private MergedAnnotation findAutowiredAnnotation(AccessibleObject ao) { * @param ann the Autowired annotation * @return whether the annotation indicates that a dependency is required */ - @SuppressWarnings({"deprecation", "cast"}) protected boolean determineRequiredStatus(MergedAnnotation ann) { - // The following (AnnotationAttributes) cast is required on JDK 9+. - return determineRequiredStatus((AnnotationAttributes) - ann.asMap(mergedAnnotation -> new AnnotationAttributes(mergedAnnotation.getType()))); - } - - /** - * Determine if the annotated field or method requires its dependency. - *

    A 'required' dependency means that autowiring should fail when no beans - * are found. Otherwise, the autowiring process will simply bypass the field - * or method when no beans are found. - * @param ann the Autowired annotation - * @return whether the annotation indicates that a dependency is required - * @deprecated since 5.2, in favor of {@link #determineRequiredStatus(MergedAnnotation)} - */ - @Deprecated - protected boolean determineRequiredStatus(AnnotationAttributes ann) { - return (!ann.containsKey(this.requiredParameterName) || + return (ann.getValue(this.requiredParameterName).isEmpty() || this.requiredParameterValue == ann.getBoolean(this.requiredParameterName)); } - /** - * Obtain all beans of the given type as autowire candidates. - * @param type the type of the bean - * @return the target beans, or an empty Collection if no bean of this type is found - * @throws BeansException if bean retrieval failed - */ - protected Map findAutowireCandidates(Class type) throws BeansException { - if (this.beanFactory == null) { - throw new IllegalStateException("No BeanFactory configured - " + - "override the getBeanOfType method or specify the 'beanFactory' property"); - } - return BeanFactoryUtils.beansOfTypeIncludingAncestors(this.beanFactory, type); - } - /** * Register the specified bean as dependent on the autowired beans. */ @@ -595,8 +637,7 @@ private void registerDependentBeans(@Nullable String beanName, Set autow */ @Nullable private Object resolvedCachedArgument(@Nullable String beanName, @Nullable Object cachedArgument) { - if (cachedArgument instanceof DependencyDescriptor) { - DependencyDescriptor descriptor = (DependencyDescriptor) cachedArgument; + if (cachedArgument instanceof DependencyDescriptor descriptor) { Assert.state(this.beanFactory != null, "No BeanFactory available"); return this.beanFactory.resolveDependency(descriptor, beanName, null, null); } @@ -607,11 +648,23 @@ private Object resolvedCachedArgument(@Nullable String beanName, @Nullable Objec /** - * Class representing injection information about an annotated field. + * Base class representing injection information. */ - private class AutowiredFieldElement extends InjectionMetadata.InjectedElement { + private abstract static class AutowiredElement extends InjectionMetadata.InjectedElement { - private final boolean required; + protected final boolean required; + + protected AutowiredElement(Member member, @Nullable PropertyDescriptor pd, boolean required) { + super(member, pd); + this.required = required; + } + } + + + /** + * Class representing injection information about an annotated field. + */ + private class AutowiredFieldElement extends AutowiredElement { private volatile boolean cached; @@ -619,8 +672,7 @@ private class AutowiredFieldElement extends InjectionMetadata.InjectedElement { private volatile Object cachedFieldValue; public AutowiredFieldElement(Field field, boolean required) { - super(field, null); - this.required = required; + super(field, null, required); } @Override @@ -628,54 +680,65 @@ protected void inject(Object bean, @Nullable String beanName, @Nullable Property Field field = (Field) this.member; Object value; if (this.cached) { - value = resolvedCachedArgument(beanName, this.cachedFieldValue); - } - else { - DependencyDescriptor desc = new DependencyDescriptor(field, this.required); - desc.setContainingClass(bean.getClass()); - Set autowiredBeanNames = new LinkedHashSet<>(1); - Assert.state(beanFactory != null, "No BeanFactory available"); - TypeConverter typeConverter = beanFactory.getTypeConverter(); try { - value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter); - } - catch (BeansException ex) { - throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(field), ex); + value = resolvedCachedArgument(beanName, this.cachedFieldValue); } - synchronized (this) { - if (!this.cached) { - Object cachedFieldValue = null; - if (value != null || this.required) { - cachedFieldValue = desc; - registerDependentBeans(beanName, autowiredBeanNames); - if (autowiredBeanNames.size() == 1) { - String autowiredBeanName = autowiredBeanNames.iterator().next(); - if (beanFactory.containsBean(autowiredBeanName) && - beanFactory.isTypeMatch(autowiredBeanName, field.getType())) { - cachedFieldValue = new ShortcutDependencyDescriptor( - desc, autowiredBeanName, field.getType()); - } - } - } - this.cachedFieldValue = cachedFieldValue; - this.cached = true; - } + catch (NoSuchBeanDefinitionException ex) { + // Unexpected removal of target bean for cached argument -> re-resolve + value = resolveFieldValue(field, bean, beanName); } } + else { + value = resolveFieldValue(field, bean, beanName); + } if (value != null) { ReflectionUtils.makeAccessible(field); field.set(bean, value); } } + + @Nullable + private Object resolveFieldValue(Field field, Object bean, @Nullable String beanName) { + DependencyDescriptor desc = new DependencyDescriptor(field, this.required); + desc.setContainingClass(bean.getClass()); + Set autowiredBeanNames = new LinkedHashSet<>(1); + Assert.state(beanFactory != null, "No BeanFactory available"); + TypeConverter typeConverter = beanFactory.getTypeConverter(); + Object value; + try { + value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter); + } + catch (BeansException ex) { + throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(field), ex); + } + synchronized (this) { + if (!this.cached) { + Object cachedFieldValue = null; + if (value != null || this.required) { + cachedFieldValue = desc; + registerDependentBeans(beanName, autowiredBeanNames); + if (autowiredBeanNames.size() == 1) { + String autowiredBeanName = autowiredBeanNames.iterator().next(); + if (beanFactory.containsBean(autowiredBeanName) && + beanFactory.isTypeMatch(autowiredBeanName, field.getType())) { + cachedFieldValue = new ShortcutDependencyDescriptor( + desc, autowiredBeanName, field.getType()); + } + } + } + this.cachedFieldValue = cachedFieldValue; + this.cached = true; + } + } + return value; + } } /** * Class representing injection information about an annotated method. */ - private class AutowiredMethodElement extends InjectionMetadata.InjectedElement { - - private final boolean required; + private class AutowiredMethodElement extends AutowiredElement { private volatile boolean cached; @@ -683,8 +746,7 @@ private class AutowiredMethodElement extends InjectionMetadata.InjectedElement { private volatile Object[] cachedMethodArguments; public AutowiredMethodElement(Method method, boolean required, @Nullable PropertyDescriptor pd) { - super(method, pd); - this.required = required; + super(method, pd, required); } @Override @@ -695,59 +757,17 @@ protected void inject(Object bean, @Nullable String beanName, @Nullable Property Method method = (Method) this.member; Object[] arguments; if (this.cached) { - // Shortcut for avoiding synchronization... - arguments = resolveCachedArguments(beanName); - } - else { - int argumentCount = method.getParameterCount(); - arguments = new Object[argumentCount]; - DependencyDescriptor[] descriptors = new DependencyDescriptor[argumentCount]; - Set autowiredBeans = new LinkedHashSet<>(argumentCount); - Assert.state(beanFactory != null, "No BeanFactory available"); - TypeConverter typeConverter = beanFactory.getTypeConverter(); - for (int i = 0; i < arguments.length; i++) { - MethodParameter methodParam = new MethodParameter(method, i); - DependencyDescriptor currDesc = new DependencyDescriptor(methodParam, this.required); - currDesc.setContainingClass(bean.getClass()); - descriptors[i] = currDesc; - try { - Object arg = beanFactory.resolveDependency(currDesc, beanName, autowiredBeans, typeConverter); - if (arg == null && !this.required) { - arguments = null; - break; - } - arguments[i] = arg; - } - catch (BeansException ex) { - throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(methodParam), ex); - } + try { + arguments = resolveCachedArguments(beanName); } - synchronized (this) { - if (!this.cached) { - if (arguments != null) { - DependencyDescriptor[] cachedMethodArguments = Arrays.copyOf(descriptors, arguments.length); - registerDependentBeans(beanName, autowiredBeans); - if (autowiredBeans.size() == argumentCount) { - Iterator it = autowiredBeans.iterator(); - Class[] paramTypes = method.getParameterTypes(); - for (int i = 0; i < paramTypes.length; i++) { - String autowiredBeanName = it.next(); - if (beanFactory.containsBean(autowiredBeanName) && - beanFactory.isTypeMatch(autowiredBeanName, paramTypes[i])) { - cachedMethodArguments[i] = new ShortcutDependencyDescriptor( - descriptors[i], autowiredBeanName, paramTypes[i]); - } - } - } - this.cachedMethodArguments = cachedMethodArguments; - } - else { - this.cachedMethodArguments = null; - } - this.cached = true; - } + catch (NoSuchBeanDefinitionException ex) { + // Unexpected removal of target bean for cached argument -> re-resolve + arguments = resolveMethodArguments(method, bean, beanName); } } + else { + arguments = resolveMethodArguments(method, bean, beanName); + } if (arguments != null) { try { ReflectionUtils.makeAccessible(method); @@ -771,6 +791,59 @@ private Object[] resolveCachedArguments(@Nullable String beanName) { } return arguments; } + + @Nullable + private Object[] resolveMethodArguments(Method method, Object bean, @Nullable String beanName) { + int argumentCount = method.getParameterCount(); + Object[] arguments = new Object[argumentCount]; + DependencyDescriptor[] descriptors = new DependencyDescriptor[argumentCount]; + Set autowiredBeans = new LinkedHashSet<>(argumentCount); + Assert.state(beanFactory != null, "No BeanFactory available"); + TypeConverter typeConverter = beanFactory.getTypeConverter(); + for (int i = 0; i < arguments.length; i++) { + MethodParameter methodParam = new MethodParameter(method, i); + DependencyDescriptor currDesc = new DependencyDescriptor(methodParam, this.required); + currDesc.setContainingClass(bean.getClass()); + descriptors[i] = currDesc; + try { + Object arg = beanFactory.resolveDependency(currDesc, beanName, autowiredBeans, typeConverter); + if (arg == null && !this.required) { + arguments = null; + break; + } + arguments[i] = arg; + } + catch (BeansException ex) { + throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(methodParam), ex); + } + } + synchronized (this) { + if (!this.cached) { + if (arguments != null) { + DependencyDescriptor[] cachedMethodArguments = Arrays.copyOf(descriptors, arguments.length); + registerDependentBeans(beanName, autowiredBeans); + if (autowiredBeans.size() == argumentCount) { + Iterator it = autowiredBeans.iterator(); + Class[] paramTypes = method.getParameterTypes(); + for (int i = 0; i < paramTypes.length; i++) { + String autowiredBeanName = it.next(); + if (beanFactory.containsBean(autowiredBeanName) && + beanFactory.isTypeMatch(autowiredBeanName, paramTypes[i])) { + cachedMethodArguments[i] = new ShortcutDependencyDescriptor( + descriptors[i], autowiredBeanName, paramTypes[i]); + } + } + } + this.cachedMethodArguments = cachedMethodArguments; + } + else { + this.cachedMethodArguments = null; + } + this.cached = true; + } + } + return arguments; + } } @@ -796,4 +869,163 @@ public Object resolveShortcut(BeanFactory beanFactory) { } } + + /** + * {@link BeanRegistrationAotContribution} to autowire fields and methods. + */ + private static class AotContribution implements BeanRegistrationAotContribution { + + private static final String REGISTERED_BEAN_PARAMETER = "registeredBean"; + + private static final String INSTANCE_PARAMETER = "instance"; + + private final Class target; + + private final Collection autowiredElements; + + @Nullable + private final AutowireCandidateResolver candidateResolver; + + AotContribution(Class target, Collection autowiredElements, + @Nullable AutowireCandidateResolver candidateResolver) { + + this.target = target; + this.autowiredElements = autowiredElements; + this.candidateResolver = candidateResolver; + } + + @Override + public void applyTo(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode) { + GeneratedClass generatedClass = generationContext.getGeneratedClasses() + .addForFeatureComponent("Autowiring", this.target, type -> { + type.addJavadoc("Autowiring for {@link $T}.", this.target); + type.addModifiers(javax.lang.model.element.Modifier.PUBLIC); + }); + GeneratedMethod generateMethod = generatedClass.getMethods().add("apply", method -> { + method.addJavadoc("Apply the autowiring."); + method.addModifiers(javax.lang.model.element.Modifier.PUBLIC, + javax.lang.model.element.Modifier.STATIC); + method.addParameter(RegisteredBean.class, REGISTERED_BEAN_PARAMETER); + method.addParameter(this.target, INSTANCE_PARAMETER); + method.returns(this.target); + method.addCode(generateMethodCode(generatedClass.getName(), + generationContext.getRuntimeHints())); + }); + beanRegistrationCode.addInstancePostProcessor(generateMethod.toMethodReference()); + + if (this.candidateResolver != null) { + registerHints(generationContext.getRuntimeHints()); + } + } + + private CodeBlock generateMethodCode(ClassName targetClassName, RuntimeHints hints) { + CodeBlock.Builder code = CodeBlock.builder(); + for (AutowiredElement autowiredElement : this.autowiredElements) { + code.addStatement(generateMethodStatementForElement( + targetClassName, autowiredElement, hints)); + } + code.addStatement("return $L", INSTANCE_PARAMETER); + return code.build(); + } + + private CodeBlock generateMethodStatementForElement(ClassName targetClassName, + AutowiredElement autowiredElement, RuntimeHints hints) { + + Member member = autowiredElement.getMember(); + boolean required = autowiredElement.required; + if (member instanceof Field field) { + return generateMethodStatementForField( + targetClassName, field, required, hints); + } + if (member instanceof Method method) { + return generateMethodStatementForMethod( + targetClassName, method, required, hints); + } + throw new IllegalStateException( + "Unsupported member type " + member.getClass().getName()); + } + + private CodeBlock generateMethodStatementForField(ClassName targetClassName, + Field field, boolean required, RuntimeHints hints) { + + hints.reflection().registerField(field); + CodeBlock resolver = CodeBlock.of("$T.$L($S)", + AutowiredFieldValueResolver.class, + (!required) ? "forField" : "forRequiredField", field.getName()); + AccessControl accessControl = AccessControl.forMember(field); + if (!accessControl.isAccessibleFrom(targetClassName)) { + return CodeBlock.of("$L.resolveAndSet($L, $L)", resolver, + REGISTERED_BEAN_PARAMETER, INSTANCE_PARAMETER); + } + return CodeBlock.of("$L.$L = $L.resolve($L)", INSTANCE_PARAMETER, + field.getName(), resolver, REGISTERED_BEAN_PARAMETER); + } + + private CodeBlock generateMethodStatementForMethod(ClassName targetClassName, + Method method, boolean required, RuntimeHints hints) { + + CodeBlock.Builder code = CodeBlock.builder(); + code.add("$T.$L", AutowiredMethodArgumentsResolver.class, + (!required) ? "forMethod" : "forRequiredMethod"); + code.add("($S", method.getName()); + if (method.getParameterCount() > 0) { + code.add(", $L", generateParameterTypesCode(method.getParameterTypes())); + } + code.add(")"); + AccessControl accessControl = AccessControl.forMember(method); + if (!accessControl.isAccessibleFrom(targetClassName)) { + hints.reflection().registerMethod(method, ExecutableMode.INVOKE); + code.add(".resolveAndInvoke($L, $L)", REGISTERED_BEAN_PARAMETER, INSTANCE_PARAMETER); + } + else { + hints.reflection().registerMethod(method, ExecutableMode.INTROSPECT); + CodeBlock arguments = new AutowiredArgumentsCodeGenerator(this.target, + method).generateCode(method.getParameterTypes()); + CodeBlock injectionCode = CodeBlock.of("args -> $L.$L($L)", + INSTANCE_PARAMETER, method.getName(), arguments); + code.add(".resolve($L, $L)", REGISTERED_BEAN_PARAMETER, injectionCode); + } + return code.build(); + } + + private CodeBlock generateParameterTypesCode(Class[] parameterTypes) { + CodeBlock.Builder code = CodeBlock.builder(); + for (int i = 0; i < parameterTypes.length; i++) { + code.add(i != 0 ? ", " : ""); + code.add("$T.class", parameterTypes[i]); + } + return code.build(); + } + + private void registerHints(RuntimeHints runtimeHints) { + this.autowiredElements.forEach(autowiredElement -> { + boolean required = autowiredElement.required; + Member member = autowiredElement.getMember(); + if (member instanceof Field field) { + DependencyDescriptor dependencyDescriptor = new DependencyDescriptor(field, required); + registerProxyIfNecessary(runtimeHints, dependencyDescriptor); + } + if (member instanceof Method method) { + Class[] parameterTypes = method.getParameterTypes(); + for (int i = 0; i < parameterTypes.length; i++) { + MethodParameter methodParam = new MethodParameter(method, i); + DependencyDescriptor dependencyDescriptor = new DependencyDescriptor(methodParam, required); + registerProxyIfNecessary(runtimeHints, dependencyDescriptor); + } + } + }); + } + + private void registerProxyIfNecessary(RuntimeHints runtimeHints, DependencyDescriptor dependencyDescriptor) { + if (this.candidateResolver != null) { + Class proxyType = + this.candidateResolver.getLazyResolutionProxyClass(dependencyDescriptor, null); + if (proxyType != null && Proxy.isProxyClass(proxyType)) { + runtimeHints.proxies().registerJdkProxy(proxyType.getInterfaces()); + } + } + } + + } + } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/BeanFactoryAnnotationUtils.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/BeanFactoryAnnotationUtils.java index 58e04c4c032d..fd41a5cfe0a1 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/BeanFactoryAnnotationUtils.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/BeanFactoryAnnotationUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -48,7 +48,7 @@ public abstract class BeanFactoryAnnotationUtils { /** - * Retrieve all bean of type {@code T} from the given {@code BeanFactory} declaring a + * Retrieve all beans of type {@code T} from the given {@code BeanFactory} declaring a * qualifier (e.g. via {@code } or {@code @Qualifier}) matching the given * qualifier, or having a bean name matching the given qualifier. * @param beanFactory the factory to get the target beans from (also searching ancestors) @@ -90,9 +90,9 @@ public static T qualifiedBeanOfType(BeanFactory beanFactory, Class beanTy Assert.notNull(beanFactory, "BeanFactory must not be null"); - if (beanFactory instanceof ListableBeanFactory) { + if (beanFactory instanceof ListableBeanFactory lbf) { // Full qualifier matching supported. - return qualifiedBeanOfType((ListableBeanFactory) beanFactory, beanType, qualifier); + return qualifiedBeanOfType(lbf, beanType, qualifier); } else if (beanFactory.containsBean(qualifier)) { // Fallback: target bean at least found by bean name. @@ -163,11 +163,10 @@ public static boolean isQualifierMatch( } try { Class beanType = beanFactory.getType(beanName); - if (beanFactory instanceof ConfigurableBeanFactory) { - BeanDefinition bd = ((ConfigurableBeanFactory) beanFactory).getMergedBeanDefinition(beanName); + if (beanFactory instanceof ConfigurableBeanFactory cbf) { + BeanDefinition bd = cbf.getMergedBeanDefinition(beanName); // Explicit qualifier metadata on bean definition? (typically in XML definition) - if (bd instanceof AbstractBeanDefinition) { - AbstractBeanDefinition abd = (AbstractBeanDefinition) bd; + if (bd instanceof AbstractBeanDefinition abd) { AutowireCandidateQualifier candidate = abd.getQualifier(Qualifier.class.getName()); if (candidate != null) { Object value = candidate.getAttribute(AutowireCandidateQualifier.VALUE_KEY); @@ -177,8 +176,8 @@ public static boolean isQualifierMatch( } } // Corresponding qualifier on factory method? (typically in configuration class) - if (bd instanceof RootBeanDefinition) { - Method factoryMethod = ((RootBeanDefinition) bd).getResolvedFactoryMethod(); + if (bd instanceof RootBeanDefinition rbd) { + Method factoryMethod = rbd.getResolvedFactoryMethod(); if (factoryMethod != null) { Qualifier targetAnnotation = AnnotationUtils.getAnnotation(factoryMethod, Qualifier.class); if (targetAnnotation != null) { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/CustomAutowireConfigurer.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/CustomAutowireConfigurer.java index d43329a0f626..86fe4482b2ee 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/CustomAutowireConfigurer.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/CustomAutowireConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-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. @@ -91,11 +91,10 @@ public void setCustomQualifierTypes(Set customQualifierTypes) { @SuppressWarnings("unchecked") public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { if (this.customQualifierTypes != null) { - if (!(beanFactory instanceof DefaultListableBeanFactory)) { + if (!(beanFactory instanceof DefaultListableBeanFactory dlbf)) { throw new IllegalStateException( "CustomAutowireConfigurer needs to operate on a DefaultListableBeanFactory"); } - DefaultListableBeanFactory dlbf = (DefaultListableBeanFactory) beanFactory; if (!(dlbf.getAutowireCandidateResolver() instanceof QualifierAnnotationAutowireCandidateResolver)) { dlbf.setAutowireCandidateResolver(new QualifierAnnotationAutowireCandidateResolver()); } @@ -106,8 +105,7 @@ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) if (value instanceof Class) { customType = (Class) value; } - else if (value instanceof String) { - String className = (String) value; + else if (value instanceof String className) { customType = (Class) ClassUtils.resolveClassName(className, this.beanClassLoader); } else { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InitDestroyAnnotationBeanPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InitDestroyAnnotationBeanPostProcessor.java index f76a03b8dc9d..28281b6dfc6a 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InitDestroyAnnotationBeanPostProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InitDestroyAnnotationBeanPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -32,20 +32,25 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Stream; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; +import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor; import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor; +import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.core.Ordered; import org.springframework.core.PriorityOrdered; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; import org.springframework.util.ReflectionUtils; /** @@ -66,19 +71,21 @@ * init method and destroy method, respectively. * *

    Spring's {@link org.springframework.context.annotation.CommonAnnotationBeanPostProcessor} - * supports the JSR-250 {@link javax.annotation.PostConstruct} and {@link javax.annotation.PreDestroy} + * supports the {@link jakarta.annotation.PostConstruct} and {@link jakarta.annotation.PreDestroy} * annotations out of the box, as init annotation and destroy annotation, respectively. - * Furthermore, it also supports the {@link javax.annotation.Resource} annotation + * Furthermore, it also supports the {@link jakarta.annotation.Resource} annotation * for annotation-driven injection of named beans. * * @author Juergen Hoeller + * @author Stephane Nicoll + * @author Phillip Webb * @since 2.5 * @see #setInitAnnotationType * @see #setDestroyAnnotationType */ @SuppressWarnings("serial") -public class InitDestroyAnnotationBeanPostProcessor - implements DestructionAwareBeanPostProcessor, MergedBeanDefinitionPostProcessor, PriorityOrdered, Serializable { +public class InitDestroyAnnotationBeanPostProcessor implements DestructionAwareBeanPostProcessor, + MergedBeanDefinitionPostProcessor, BeanRegistrationAotProcessor, PriorityOrdered, Serializable { private final transient LifecycleMetadata emptyLifecycleMetadata = new LifecycleMetadata(Object.class, Collections.emptyList(), Collections.emptyList()) { @@ -117,7 +124,7 @@ public boolean hasDestroyMethods() { * methods to call after configuration of a bean. *

    Any custom annotation can be used, since there are no required * annotation attributes. There is no default, although a typical choice - * is the JSR-250 {@link javax.annotation.PostConstruct} annotation. + * is the {@link jakarta.annotation.PostConstruct} annotation. */ public void setInitAnnotationType(Class initAnnotationType) { this.initAnnotationType = initAnnotationType; @@ -128,7 +135,7 @@ public void setInitAnnotationType(Class initAnnotationType * methods to call when the context is shutting down. *

    Any custom annotation can be used, since there are no required * annotation attributes. There is no default, although a typical choice - * is the JSR-250 {@link javax.annotation.PreDestroy} annotation. + * is the {@link jakarta.annotation.PreDestroy} annotation. */ public void setDestroyAnnotationType(Class destroyAnnotationType) { this.destroyAnnotationType = destroyAnnotationType; @@ -146,8 +153,36 @@ public int getOrder() { @Override public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class beanType, String beanName) { + findInjectionMetadata(beanDefinition, beanType); + } + + @Override + public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + RootBeanDefinition beanDefinition = registeredBean.getMergedBeanDefinition(); + beanDefinition.resolveDestroyMethodIfNecessary(); + LifecycleMetadata metadata = findInjectionMetadata(beanDefinition, registeredBean.getBeanClass()); + if (!CollectionUtils.isEmpty(metadata.initMethods)) { + String[] initMethodNames = safeMerge(beanDefinition.getInitMethodNames(), metadata.initMethods); + beanDefinition.setInitMethodNames(initMethodNames); + } + if (!CollectionUtils.isEmpty(metadata.destroyMethods)) { + String[] destroyMethodNames = safeMerge(beanDefinition.getDestroyMethodNames(), metadata.destroyMethods); + beanDefinition.setDestroyMethodNames(destroyMethodNames); + } + return null; + } + + private LifecycleMetadata findInjectionMetadata(RootBeanDefinition beanDefinition, Class beanType) { LifecycleMetadata metadata = findLifecycleMetadata(beanType); metadata.checkConfigMembers(beanDefinition); + return metadata; + } + + private String[] safeMerge(@Nullable String[] existingNames, Collection detectedElements) { + Stream detectedNames = detectedElements.stream().map(LifecycleElement::getIdentifier); + Stream mergedNames = (existingNames != null ? + Stream.concat(Stream.of(existingNames), detectedNames) : detectedNames); + return mergedNames.distinct().toArray(String[]::new); } @Override @@ -302,7 +337,7 @@ public void checkConfigMembers(RootBeanDefinition beanDefinition) { beanDefinition.registerExternallyManagedInitMethod(methodIdentifier); checkedInitMethods.add(element); if (logger.isTraceEnabled()) { - logger.trace("Registered init method on class [" + this.targetClass.getName() + "]: " + element); + logger.trace("Registered init method on class [" + this.targetClass.getName() + "]: " + methodIdentifier); } } } @@ -313,7 +348,7 @@ public void checkConfigMembers(RootBeanDefinition beanDefinition) { beanDefinition.registerExternallyManagedDestroyMethod(methodIdentifier); checkedDestroyMethods.add(element); if (logger.isTraceEnabled()) { - logger.trace("Registered destroy method on class [" + this.targetClass.getName() + "]: " + element); + logger.trace("Registered destroy method on class [" + this.targetClass.getName() + "]: " + methodIdentifier); } } } @@ -394,10 +429,9 @@ public boolean equals(@Nullable Object other) { if (this == other) { return true; } - if (!(other instanceof LifecycleElement)) { + if (!(other instanceof LifecycleElement otherElement)) { return false; } - LifecycleElement otherElement = (LifecycleElement) other; return (this.identifier.equals(otherElement.identifier)); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InjectionMetadata.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InjectionMetadata.java index f5cc0f9d5280..8898dea97a40 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InjectionMetadata.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InjectionMetadata.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -88,6 +88,14 @@ public InjectionMetadata(Class targetClass, Collection eleme } + /** + * Return the {@link InjectedElement elements} to inject. + * @return the elements to inject + */ + public Collection getInjectedElements() { + return Collections.unmodifiableCollection(this.injectedElements); + } + /** * Determine whether this metadata instance needs to be refreshed. * @param clazz the current target class @@ -141,8 +149,7 @@ public void clear(@Nullable PropertyValues pvs) { * Return an {@code InjectionMetadata} instance, possibly for empty elements. * @param elements the elements to inject (possibly empty) * @param clazz the target class - * @return a new {@link #InjectionMetadata(Class, Collection)} instance, - * or {@link #EMPTY} in case of no elements + * @return a new {@link #InjectionMetadata(Class, Collection)} instance * @since 5.2 */ public static InjectionMetadata forElements(Collection elements, Class clazz) { @@ -305,10 +312,9 @@ public boolean equals(@Nullable Object other) { if (this == other) { return true; } - if (!(other instanceof InjectedElement)) { + if (!(other instanceof InjectedElement otherElement)) { return false; } - InjectedElement otherElement = (InjectedElement) other; return this.member.equals(otherElement.member); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/JakartaAnnotationsRuntimeHints.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/JakartaAnnotationsRuntimeHints.java new file mode 100644 index 000000000000..0239d42d7df8 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/JakartaAnnotationsRuntimeHints.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-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.beans.factory.annotation; + +import java.util.stream.Stream; + +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.util.ClassUtils; + +/** + * {@link RuntimeHintsRegistrar} for Jakarta annotations. + *

    Hints are only registered if Jakarta inject is on the classpath. + * + * @author Brian Clozel + */ +class JakartaAnnotationsRuntimeHints implements RuntimeHintsRegistrar { + + @Override + public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + if (ClassUtils.isPresent("jakarta.inject.Inject", classLoader)) { + Stream.of("jakarta.inject.Inject", "jakarta.inject.Qualifier").forEach(annotationType -> + hints.reflection().registerType(ClassUtils.resolveClassName(annotationType, classLoader))); + } + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/ParameterResolutionDelegate.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/ParameterResolutionDelegate.java index fc212da6c6b3..f8f7b0dce6f4 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/ParameterResolutionDelegate.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/ParameterResolutionDelegate.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -140,6 +140,8 @@ public static Object resolveDependency( * Due to a bug in {@code javac} on JDK versions prior to JDK 9, looking up * annotations directly on a {@link Parameter} will fail for inner class * constructors. + *

    Note: Since Spring 6 may still encounter user code compiled with + * {@code javac 8}, this workaround is kept in place for the time being. *

    Bug in javac in JDK < 9

    *

    The parameter annotations array in the compiled byte code excludes an entry * for the implicit enclosing instance parameter for an inner class diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java index e4e104b4c8e4..c2f34a45889e 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java @@ -46,7 +46,7 @@ * against {@link Qualifier qualifier annotations} on the field or parameter to be autowired. * Also supports suggested expression values through a {@link Value value} annotation. * - *

    Also supports JSR-330's {@link javax.inject.Qualifier} annotation, if available. + *

    Also supports JSR-330's {@link jakarta.inject.Qualifier} annotation, if available. * * @author Mark Fisher * @author Juergen Hoeller @@ -66,13 +66,13 @@ public class QualifierAnnotationAutowireCandidateResolver extends GenericTypeAwa /** * Create a new QualifierAnnotationAutowireCandidateResolver * for Spring's standard {@link Qualifier} annotation. - *

    Also supports JSR-330's {@link javax.inject.Qualifier} annotation, if available. + *

    Also supports JSR-330's {@link jakarta.inject.Qualifier} annotation, if available. */ @SuppressWarnings("unchecked") public QualifierAnnotationAutowireCandidateResolver() { this.qualifierTypes.add(Qualifier.class); try { - this.qualifierTypes.add((Class) ClassUtils.forName("javax.inject.Qualifier", + this.qualifierTypes.add((Class) ClassUtils.forName("jakarta.inject.Qualifier", QualifierAnnotationAutowireCandidateResolver.class.getClassLoader())); } catch (ClassNotFoundException ex) { @@ -137,7 +137,7 @@ public void setValueAnnotationType(Class valueAnnotationTy * as a qualifier, the bean must 'match' against the annotation as * well as any attributes it may contain. The bean definition must contain * the same qualifier or match by meta attributes. A "value" attribute will - * fallback to match against the bean name or an alias if a qualifier or + * fall back to match against the bean name or an alias if a qualifier or * attribute does not match. * @see Qualifier */ @@ -186,7 +186,7 @@ protected boolean checkQualifiers(BeanDefinitionHolder bdHolder, Annotation[] an if (isQualifier(metaType)) { foundMeta = true; // Only accept fallback match if @Qualifier annotation has a value... - // Otherwise it is just a marker for a custom qualifier annotation. + // Otherwise, it is just a marker for a custom qualifier annotation. if ((fallbackToMeta && ObjectUtils.isEmpty(AnnotationUtils.getValue(metaAnn))) || !checkQualifier(bdHolder, metaAnn, typeConverter)) { return false; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Required.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Required.java deleted file mode 100644 index 4993cf25d4b4..000000000000 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Required.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2002-2018 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.beans.factory.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Marks a method (typically a JavaBean setter method) as being 'required': that is, - * the setter method must be configured to be dependency-injected with a value. - * - *

    Please do consult the javadoc for the {@link RequiredAnnotationBeanPostProcessor} - * class (which, by default, checks for the presence of this annotation). - * - * @author Rob Harrop - * @since 2.0 - * @see RequiredAnnotationBeanPostProcessor - * @deprecated as of 5.1, in favor of using constructor injection for required settings - * (or a custom {@link org.springframework.beans.factory.InitializingBean} implementation) - */ -@Deprecated -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.METHOD) -public @interface Required { - -} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/RequiredAnnotationBeanPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/RequiredAnnotationBeanPostProcessor.java deleted file mode 100644 index 9978f979b506..000000000000 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/RequiredAnnotationBeanPostProcessor.java +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Copyright 2002-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.beans.factory.annotation; - -import java.beans.PropertyDescriptor; -import java.lang.annotation.Annotation; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -import org.springframework.beans.PropertyValues; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.BeanFactoryAware; -import org.springframework.beans.factory.BeanInitializationException; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor; -import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor; -import org.springframework.beans.factory.support.RootBeanDefinition; -import org.springframework.core.Conventions; -import org.springframework.core.Ordered; -import org.springframework.core.PriorityOrdered; -import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; - -/** - * {@link org.springframework.beans.factory.config.BeanPostProcessor} implementation - * that enforces required JavaBean properties to have been configured. - * Required bean properties are detected through a Java 5 annotation: - * by default, Spring's {@link Required} annotation. - * - *

    The motivation for the existence of this BeanPostProcessor is to allow - * developers to annotate the setter properties of their own classes with an - * arbitrary JDK 1.5 annotation to indicate that the container must check - * for the configuration of a dependency injected value. This neatly pushes - * responsibility for such checking onto the container (where it arguably belongs), - * and obviates the need (in part) for a developer to code a method that - * simply checks that all required properties have actually been set. - * - *

    Please note that an 'init' method may still need to be implemented (and may - * still be desirable), because all that this class does is enforcing that a - * 'required' property has actually been configured with a value. It does - * not check anything else... In particular, it does not check that a - * configured value is not {@code null}. - * - *

    Note: A default RequiredAnnotationBeanPostProcessor will be registered - * by the "context:annotation-config" and "context:component-scan" XML tags. - * Remove or turn off the default annotation configuration there if you intend - * to specify a custom RequiredAnnotationBeanPostProcessor bean definition. - * - * @author Rob Harrop - * @author Juergen Hoeller - * @since 2.0 - * @see #setRequiredAnnotationType - * @see Required - * @deprecated as of 5.1, in favor of using constructor injection for required settings - * (or a custom {@link org.springframework.beans.factory.InitializingBean} implementation) - */ -@Deprecated -public class RequiredAnnotationBeanPostProcessor implements SmartInstantiationAwareBeanPostProcessor, - MergedBeanDefinitionPostProcessor, PriorityOrdered, BeanFactoryAware { - - /** - * Bean definition attribute that may indicate whether a given bean is supposed - * to be skipped when performing this post-processor's required property check. - * @see #shouldSkip - */ - public static final String SKIP_REQUIRED_CHECK_ATTRIBUTE = - Conventions.getQualifiedAttributeName(RequiredAnnotationBeanPostProcessor.class, "skipRequiredCheck"); - - - private Class requiredAnnotationType = Required.class; - - private int order = Ordered.LOWEST_PRECEDENCE - 1; - - @Nullable - private ConfigurableListableBeanFactory beanFactory; - - /** - * Cache for validated bean names, skipping re-validation for the same bean. - */ - private final Set validatedBeanNames = Collections.newSetFromMap(new ConcurrentHashMap<>(64)); - - - /** - * Set the 'required' annotation type, to be used on bean property - * setter methods. - *

    The default required annotation type is the Spring-provided - * {@link Required} annotation. - *

    This setter property exists so that developers can provide their own - * (non-Spring-specific) annotation type to indicate that a property value - * is required. - */ - public void setRequiredAnnotationType(Class requiredAnnotationType) { - Assert.notNull(requiredAnnotationType, "'requiredAnnotationType' must not be null"); - this.requiredAnnotationType = requiredAnnotationType; - } - - /** - * Return the 'required' annotation type. - */ - protected Class getRequiredAnnotationType() { - return this.requiredAnnotationType; - } - - @Override - public void setBeanFactory(BeanFactory beanFactory) { - if (beanFactory instanceof ConfigurableListableBeanFactory) { - this.beanFactory = (ConfigurableListableBeanFactory) beanFactory; - } - } - - public void setOrder(int order) { - this.order = order; - } - - @Override - public int getOrder() { - return this.order; - } - - - @Override - public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class beanType, String beanName) { - } - - @Override - public PropertyValues postProcessPropertyValues( - PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) { - - if (!this.validatedBeanNames.contains(beanName)) { - if (!shouldSkip(this.beanFactory, beanName)) { - List invalidProperties = new ArrayList<>(); - for (PropertyDescriptor pd : pds) { - if (isRequiredProperty(pd) && !pvs.contains(pd.getName())) { - invalidProperties.add(pd.getName()); - } - } - if (!invalidProperties.isEmpty()) { - throw new BeanInitializationException(buildExceptionMessage(invalidProperties, beanName)); - } - } - this.validatedBeanNames.add(beanName); - } - return pvs; - } - - /** - * Check whether the given bean definition is not subject to the annotation-based - * required property check as performed by this post-processor. - *

    The default implementations check for the presence of the - * {@link #SKIP_REQUIRED_CHECK_ATTRIBUTE} attribute in the bean definition, if any. - * It also suggests skipping in case of a bean definition with a "factory-bean" - * reference set, assuming that instance-based factories pre-populate the bean. - * @param beanFactory the BeanFactory to check against - * @param beanName the name of the bean to check against - * @return {@code true} to skip the bean; {@code false} to process it - */ - protected boolean shouldSkip(@Nullable ConfigurableListableBeanFactory beanFactory, String beanName) { - if (beanFactory == null || !beanFactory.containsBeanDefinition(beanName)) { - return false; - } - BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName); - if (beanDefinition.getFactoryBeanName() != null) { - return true; - } - Object value = beanDefinition.getAttribute(SKIP_REQUIRED_CHECK_ATTRIBUTE); - return (value != null && (Boolean.TRUE.equals(value) || Boolean.parseBoolean(value.toString()))); - } - - /** - * Is the supplied property required to have a value (that is, to be dependency-injected)? - *

    This implementation looks for the existence of a - * {@link #setRequiredAnnotationType "required" annotation} - * on the supplied {@link PropertyDescriptor property}. - * @param propertyDescriptor the target PropertyDescriptor (never {@code null}) - * @return {@code true} if the supplied property has been marked as being required; - * {@code false} if not, or if the supplied property does not have a setter method - */ - protected boolean isRequiredProperty(PropertyDescriptor propertyDescriptor) { - Method setter = propertyDescriptor.getWriteMethod(); - return (setter != null && AnnotationUtils.getAnnotation(setter, getRequiredAnnotationType()) != null); - } - - /** - * Build an exception message for the given list of invalid properties. - * @param invalidProperties the list of names of invalid properties - * @param beanName the name of the bean - * @return the exception message - */ - private String buildExceptionMessage(List invalidProperties, String beanName) { - int size = invalidProperties.size(); - StringBuilder sb = new StringBuilder(); - sb.append(size == 1 ? "Property" : "Properties"); - for (int i = 0; i < size; i++) { - String propertyName = invalidProperties.get(i); - if (i > 0) { - if (i == (size - 1)) { - sb.append(" and"); - } - else { - sb.append(","); - } - } - sb.append(" '").append(propertyName).append("'"); - } - sb.append(size == 1 ? " is" : " are"); - sb.append(" required for bean '").append(beanName).append("'"); - return sb.toString(); - } - -} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/AotServices.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AotServices.java new file mode 100644 index 000000000000..c26a9c1ac845 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AotServices.java @@ -0,0 +1,239 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.io.support.SpringFactoriesLoader; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * A collection of AOT services that can be {@link Loader loaded} from + * a {@link SpringFactoriesLoader} or obtained from a {@link ListableBeanFactory}. + * + * @author Phillip Webb + * @since 6.0 + * @param the service type + */ +public final class AotServices implements Iterable { + + /** + * The location to look for AOT factories. + */ + public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring/aot.factories"; + + private final List services; + + private final Map beans; + + private final Map sources; + + + private AotServices(List loaded, Map beans) { + this.services = collectServices(loaded, beans); + this.sources = collectSources(loaded, beans.values()); + this.beans = beans; + } + + private List collectServices(List loaded, Map beans) { + List services = new ArrayList<>(); + services.addAll(beans.values()); + services.addAll(loaded); + AnnotationAwareOrderComparator.sort(services); + return Collections.unmodifiableList(services); + } + + private Map collectSources(Collection loaded, Collection beans) { + Map sources = new IdentityHashMap<>(); + loaded.forEach(service -> sources.put(service, Source.SPRING_FACTORIES_LOADER)); + beans.forEach(service -> sources.put(service, Source.BEAN_FACTORY)); + return Collections.unmodifiableMap(sources); + } + + /** + * Create a new {@link Loader} that will obtain AOT services from + * {@value #FACTORIES_RESOURCE_LOCATION}. + * @return a new {@link Loader} instance + */ + public static Loader factories() { + return factories((ClassLoader) null); + } + + /** + * Create a new {@link Loader} that will obtain AOT services from + * {@value #FACTORIES_RESOURCE_LOCATION}. + * @param classLoader the class loader used to load the factories resource + * @return a new {@link Loader} instance + */ + public static Loader factories(@Nullable ClassLoader classLoader) { + return factories(getSpringFactoriesLoader(classLoader)); + } + + /** + * Create a new {@link Loader} that will obtain AOT services from the given + * {@link SpringFactoriesLoader}. + * @param springFactoriesLoader the spring factories loader + * @return a new {@link Loader} instance + */ + public static Loader factories(SpringFactoriesLoader springFactoriesLoader) { + Assert.notNull(springFactoriesLoader, "'springFactoriesLoader' must not be null"); + return new Loader(springFactoriesLoader, null); + } + + /** + * Create a new {@link Loader} that will obtain AOT services from + * {@value #FACTORIES_RESOURCE_LOCATION} as well as the given + * {@link ListableBeanFactory}. + * @param beanFactory the bean factory + * @return a new {@link Loader} instance + */ + public static Loader factoriesAndBeans(ListableBeanFactory beanFactory) { + ClassLoader classLoader = (beanFactory instanceof ConfigurableBeanFactory configurableBeanFactory ? + configurableBeanFactory.getBeanClassLoader() : null); + return factoriesAndBeans(getSpringFactoriesLoader(classLoader), beanFactory); + } + + /** + * Create a new {@link Loader} that will obtain AOT services from the given + * {@link SpringFactoriesLoader} and {@link ListableBeanFactory}. + * @param springFactoriesLoader the spring factories loader + * @param beanFactory the bean factory + * @return a new {@link Loader} instance + */ + public static Loader factoriesAndBeans(SpringFactoriesLoader springFactoriesLoader, ListableBeanFactory beanFactory) { + Assert.notNull(beanFactory, "'beanFactory' must not be null"); + Assert.notNull(springFactoriesLoader, "'springFactoriesLoader' must not be null"); + return new Loader(springFactoriesLoader, beanFactory); + } + + private static SpringFactoriesLoader getSpringFactoriesLoader( + @Nullable ClassLoader classLoader) { + return SpringFactoriesLoader.forResourceLocation(FACTORIES_RESOURCE_LOCATION, + classLoader); + } + + @Override + public Iterator iterator() { + return this.services.iterator(); + } + + /** + * Return a {@link Stream} of the AOT services. + * @return a stream of the services + */ + public Stream stream() { + return this.services.stream(); + } + + /** + * Return the AOT services as a {@link List}. + * @return a list of the services + */ + public List asList() { + return this.services; + } + + /** + * Find the AOT service that was loaded for the given bean name. + * @param beanName the bean name + * @return the AOT service or {@code null} + */ + @Nullable + public T findByBeanName(String beanName) { + return this.beans.get(beanName); + } + + /** + * Get the source of the given service. + * @param service the service instance + * @return the source of the service + */ + public Source getSource(T service) { + Source source = this.sources.get(service); + Assert.state(source != null, + () -> "Unable to find service " + ObjectUtils.identityToString(source)); + return source; + } + + + /** + * Loader class used to actually load the services. + */ + public static class Loader { + + private final SpringFactoriesLoader springFactoriesLoader; + + @Nullable + private final ListableBeanFactory beanFactory; + + + Loader(SpringFactoriesLoader springFactoriesLoader, @Nullable ListableBeanFactory beanFactory) { + this.springFactoriesLoader = springFactoriesLoader; + this.beanFactory = beanFactory; + } + + + /** + * Load all AOT services of the given type. + * @param the service type + * @param type the service type + * @return a new {@link AotServices} instance + */ + public AotServices load(Class type) { + return new AotServices<>(this.springFactoriesLoader.load(type), loadBeans(type)); + } + + private Map loadBeans(Class type) { + return (this.beanFactory != null) ? BeanFactoryUtils + .beansOfTypeIncludingAncestors(this.beanFactory, type, true, false) + : Collections.emptyMap(); + } + + } + + /** + * Sources from which services were obtained. + */ + public enum Source { + + /** + * An AOT service loaded from {@link SpringFactoriesLoader}. + */ + SPRING_FACTORIES_LOADER, + + /** + * An AOT service loaded from a {@link BeanFactory}. + */ + BEAN_FACTORY + + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredArguments.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredArguments.java new file mode 100644 index 000000000000..f4c090647919 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredArguments.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Resolved arguments to be autowired. + * + * @author Phillip Webb + * @author Stephane Nicoll + * @since 6.0 + * @see BeanInstanceSupplier + * @see AutowiredMethodArgumentsResolver + */ +@FunctionalInterface +public interface AutowiredArguments { + + /** + * Return the resolved argument at the specified index. + * @param the type of the argument + * @param index the argument index + * @param requiredType the required argument type + * @return the argument + */ + @SuppressWarnings("unchecked") + @Nullable + default T get(int index, Class requiredType) { + Object value = getObject(index); + if (!ClassUtils.isAssignableValue(requiredType, value)) { + throw new IllegalArgumentException("Argument type mismatch: expected '" + + ClassUtils.getQualifiedName(requiredType) + "' for value [" + value + "]"); + } + return (T) value; + } + + /** + * Return the resolved argument at the specified index. + * @param the type of the argument + * @param index the argument index + * @return the argument + */ + @SuppressWarnings("unchecked") + @Nullable + default T get(int index) { + return (T) getObject(index); + } + + /** + * Return the resolved argument at the specified index. + * @param index the argument index + * @return the argument + */ + @Nullable + default Object getObject(int index) { + return toArray()[index]; + } + + /** + * Return the arguments as an object array. + * @return the arguments as an object array + */ + Object[] toArray(); + + /** + * Factory method to create a new {@link AutowiredArguments} instance from + * the given object array. + * @param arguments the arguments + * @return a new {@link AutowiredArguments} instance + */ + static AutowiredArguments of(Object[] arguments) { + Assert.notNull(arguments, "'arguments' must not be null"); + return () -> arguments; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredArgumentsCodeGenerator.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredArgumentsCodeGenerator.java new file mode 100644 index 000000000000..8a8f152cd7ca --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredArgumentsCodeGenerator.java @@ -0,0 +1,102 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.function.Predicate; + +import org.springframework.javapoet.CodeBlock; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; + +/** + * Code generator to apply {@link AutowiredArguments}. + * + *

    Generates code in the form: {@code args.get(0), args.get(1)} or + * {@code args.get(0, String.class), args.get(1, Integer.class)} + * + *

    The simpler form is only used if the target method or constructor is + * unambiguous. + * + * @author Phillip Webb + * @author Stephane Nicoll + * @since 6.0 + */ +public class AutowiredArgumentsCodeGenerator { + + private final Class target; + + private final Executable executable; + + + public AutowiredArgumentsCodeGenerator(Class target, Executable executable) { + this.target = target; + this.executable = executable; + } + + + public CodeBlock generateCode(Class[] parameterTypes) { + return generateCode(parameterTypes, 0, "args"); + } + + public CodeBlock generateCode(Class[] parameterTypes, int startIndex) { + return generateCode(parameterTypes, startIndex, "args"); + } + + public CodeBlock generateCode(Class[] parameterTypes, int startIndex, + String variableName) { + + Assert.notNull(parameterTypes, "'parameterTypes' must not be null"); + Assert.notNull(variableName, "'variableName' must not be null"); + boolean ambiguous = isAmbiguous(); + CodeBlock.Builder code = CodeBlock.builder(); + for (int i = startIndex; i < parameterTypes.length; i++) { + code.add((i != startIndex) ? ", " : ""); + if (!ambiguous) { + code.add("$L.get($L)", variableName, i - startIndex); + } + else { + code.add("$L.get($L, $T.class)", variableName, i - startIndex, + parameterTypes[i]); + } + } + return code.build(); + } + + private boolean isAmbiguous() { + if (this.executable instanceof Constructor constructor) { + return Arrays.stream(this.target.getDeclaredConstructors()) + .filter(Predicate.not(constructor::equals)) + .anyMatch(this::hasSameParameterCount); + } + if (this.executable instanceof Method method) { + return Arrays.stream(ReflectionUtils.getAllDeclaredMethods(this.target)) + .filter(Predicate.not(method::equals)) + .filter(candidate -> candidate.getName().equals(method.getName())) + .anyMatch(this::hasSameParameterCount); + } + return true; + } + + private boolean hasSameParameterCount(Executable executable) { + return this.executable.getParameterCount() == executable.getParameterCount(); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredElementResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredElementResolver.java new file mode 100644 index 000000000000..1cd165908374 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredElementResolver.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +import java.util.Set; + +import javax.lang.model.element.Element; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.core.log.LogMessage; + +/** + * Base class for resolvers that support autowiring related to an + * {@link Element}. + * + * @author Phillip Webb + * @author Stephane Nicoll + * @since 6.0 + */ +abstract class AutowiredElementResolver { + + private final Log logger = LogFactory.getLog(getClass()); + + protected final void registerDependentBeans(ConfigurableBeanFactory beanFactory, + String beanName, Set autowiredBeanNames) { + + for (String autowiredBeanName : autowiredBeanNames) { + if (beanFactory.containsBean(autowiredBeanName)) { + beanFactory.registerDependentBean(autowiredBeanName, beanName); + } + logger.trace(LogMessage.format( + "Autowiring by type from bean name %s' to bean named '%s'", beanName, + autowiredBeanName)); + } + } + + + /** + * {@link DependencyDescriptor} that supports shortcut bean resolution. + */ + @SuppressWarnings("serial") + static class ShortcutDependencyDescriptor extends DependencyDescriptor { + + private final String shortcut; + + private final Class requiredType; + + + public ShortcutDependencyDescriptor(DependencyDescriptor original, + String shortcut, Class requiredType) { + super(original); + this.shortcut = shortcut; + this.requiredType = requiredType; + } + + + @Override + public Object resolveShortcut(BeanFactory beanFactory) { + return beanFactory.getBean(this.shortcut, this.requiredType); + } + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredFieldValueResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredFieldValueResolver.java new file mode 100644 index 000000000000..8237ccab4ed0 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredFieldValueResolver.java @@ -0,0 +1,208 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +import java.lang.reflect.Field; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.aot.hint.ExecutableMode; +import org.springframework.beans.BeansException; +import org.springframework.beans.TypeConverter; +import org.springframework.beans.factory.InjectionPoint; +import org.springframework.beans.factory.UnsatisfiedDependencyException; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.function.ThrowingConsumer; + +/** + * Resolver used to support the autowiring of fields. Typically used in + * AOT-processed applications as a targeted alternative to the + * {@link org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor + * AutowiredAnnotationBeanPostProcessor}. + * + *

    When resolving arguments in a native image, the {@link Field} being used must + * be marked with an {@link ExecutableMode#INTROSPECT introspection} hint so + * that field annotations can be read. Full {@link ExecutableMode#INVOKE + * invocation} hints are only required if the + * {@link #resolveAndSet(RegisteredBean, Object)} method of this class is being + * used (typically to support private fields). + * + * @author Phillip Webb + * @author Stephane Nicoll + * @since 6.0 + */ +public final class AutowiredFieldValueResolver extends AutowiredElementResolver { + + private final String fieldName; + + private final boolean required; + + @Nullable + private final String shortcut; + + + private AutowiredFieldValueResolver(String fieldName, boolean required, + @Nullable String shortcut) { + + Assert.hasText(fieldName, "'fieldName' must not be empty"); + this.fieldName = fieldName; + this.required = required; + this.shortcut = shortcut; + } + + + /** + * Create a new {@link AutowiredFieldValueResolver} for the specified field + * where injection is optional. + * @param fieldName the field name + * @return a new {@link AutowiredFieldValueResolver} instance + */ + public static AutowiredFieldValueResolver forField(String fieldName) { + return new AutowiredFieldValueResolver(fieldName, false, null); + } + + /** + * Create a new {@link AutowiredFieldValueResolver} for the specified field + * where injection is required. + * @param fieldName the field name + * @return a new {@link AutowiredFieldValueResolver} instance + */ + public static AutowiredFieldValueResolver forRequiredField(String fieldName) { + return new AutowiredFieldValueResolver(fieldName, true, null); + } + + + /** + * Return a new {@link AutowiredFieldValueResolver} instance that uses a + * direct bean name injection shortcut. + * @param beanName the bean name to use as a shortcut + * @return a new {@link AutowiredFieldValueResolver} instance that uses the + * shortcuts + */ + public AutowiredFieldValueResolver withShortcut(String beanName) { + return new AutowiredFieldValueResolver(this.fieldName, this.required, beanName); + } + + /** + * Resolve the field for the specified registered bean and provide it to the + * given action. + * @param registeredBean the registered bean + * @param action the action to execute with the resolved field value + */ + public void resolve(RegisteredBean registeredBean, ThrowingConsumer action) { + Assert.notNull(registeredBean, "'registeredBean' must not be null"); + Assert.notNull(action, "'action' must not be null"); + T resolved = resolve(registeredBean); + if (resolved != null) { + action.accept(resolved); + } + } + + /** + * Resolve the field value for the specified registered bean. + * @param registeredBean the registered bean + * @param requiredType the required type + * @return the resolved field value + */ + @Nullable + @SuppressWarnings("unchecked") + public T resolve(RegisteredBean registeredBean, Class requiredType) { + Object value = resolveObject(registeredBean); + Assert.isInstanceOf(requiredType, value); + return (T) value; + } + + /** + * Resolve the field value for the specified registered bean. + * @param registeredBean the registered bean + * @return the resolved field value + */ + @Nullable + @SuppressWarnings("unchecked") + public T resolve(RegisteredBean registeredBean) { + return (T) resolveObject(registeredBean); + } + + /** + * Resolve the field value for the specified registered bean. + * @param registeredBean the registered bean + * @return the resolved field value + */ + @Nullable + public Object resolveObject(RegisteredBean registeredBean) { + Assert.notNull(registeredBean, "'registeredBean' must not be null"); + return resolveValue(registeredBean, getField(registeredBean)); + } + + /** + * Resolve the field value for the specified registered bean and set it + * using reflection. + * @param registeredBean the registered bean + * @param instance the bean instance + */ + public void resolveAndSet(RegisteredBean registeredBean, Object instance) { + Assert.notNull(registeredBean, "'registeredBean' must not be null"); + Assert.notNull(instance, "'instance' must not be null"); + Field field = getField(registeredBean); + Object resolved = resolveValue(registeredBean, field); + if (resolved != null) { + ReflectionUtils.makeAccessible(field); + ReflectionUtils.setField(field, instance, resolved); + } + } + + @Nullable + private Object resolveValue(RegisteredBean registeredBean, Field field) { + String beanName = registeredBean.getBeanName(); + Class beanClass = registeredBean.getBeanClass(); + ConfigurableBeanFactory beanFactory = registeredBean.getBeanFactory(); + DependencyDescriptor descriptor = new DependencyDescriptor(field, this.required); + descriptor.setContainingClass(beanClass); + if (this.shortcut != null) { + descriptor = new ShortcutDependencyDescriptor(descriptor, this.shortcut, + field.getType()); + } + Set autowiredBeanNames = new LinkedHashSet<>(1); + TypeConverter typeConverter = beanFactory.getTypeConverter(); + try { + Assert.isInstanceOf(AutowireCapableBeanFactory.class, beanFactory); + Object value = ((AutowireCapableBeanFactory) beanFactory).resolveDependency( + descriptor, beanName, autowiredBeanNames, typeConverter); + registerDependentBeans(beanFactory, beanName, autowiredBeanNames); + return value; + } + catch (BeansException ex) { + throw new UnsatisfiedDependencyException(null, beanName, + new InjectionPoint(field), ex); + } + } + + private Field getField(RegisteredBean registeredBean) { + Field field = ReflectionUtils.findField(registeredBean.getBeanClass(), + this.fieldName); + Assert.notNull(field, () -> "No field '" + this.fieldName + "' found on " + + registeredBean.getBeanClass().getName()); + return field; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredMethodArgumentsResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredMethodArgumentsResolver.java new file mode 100644 index 000000000000..7b878e6fc8f4 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredMethodArgumentsResolver.java @@ -0,0 +1,220 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.aot.hint.ExecutableMode; +import org.springframework.beans.BeansException; +import org.springframework.beans.TypeConverter; +import org.springframework.beans.factory.InjectionPoint; +import org.springframework.beans.factory.UnsatisfiedDependencyException; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.core.MethodParameter; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.function.ThrowingConsumer; + +/** + * Resolver used to support the autowiring of methods. Typically used in + * AOT-processed applications as a targeted alternative to the + * {@link org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor + * AutowiredAnnotationBeanPostProcessor}. + * + *

    When resolving arguments in a native image, the {@link Method} being used + * must be marked with an {@link ExecutableMode#INTROSPECT introspection} hint + * so that field annotations can be read. Full {@link ExecutableMode#INVOKE + * invocation} hints are only required if the + * {@link #resolveAndInvoke(RegisteredBean, Object)} method of this class is + * being used (typically to support private methods). + * + * @author Phillip Webb + * @author Stephane Nicoll + * @since 6.0 + */ +public final class AutowiredMethodArgumentsResolver extends AutowiredElementResolver { + + private final String methodName; + + private final Class[] parameterTypes; + + private final boolean required; + + @Nullable + private final String[] shortcuts; + + + private AutowiredMethodArgumentsResolver(String methodName, Class[] parameterTypes, + boolean required, @Nullable String[] shortcuts) { + + Assert.hasText(methodName, "'methodName' must not be empty"); + this.methodName = methodName; + this.parameterTypes = parameterTypes; + this.required = required; + this.shortcuts = shortcuts; + } + + /** + * Create a new {@link AutowiredMethodArgumentsResolver} for the specified + * method where injection is optional. + * @param methodName the method name + * @param parameterTypes the factory method parameter types + * @return a new {@link AutowiredFieldValueResolver} instance + */ + public static AutowiredMethodArgumentsResolver forMethod(String methodName, + Class... parameterTypes) { + + return new AutowiredMethodArgumentsResolver(methodName, parameterTypes, false, + null); + } + + /** + * Create a new {@link AutowiredMethodArgumentsResolver} for the specified + * method where injection is required. + * @param methodName the method name + * @param parameterTypes the factory method parameter types + * @return a new {@link AutowiredFieldValueResolver} instance + */ + public static AutowiredMethodArgumentsResolver forRequiredMethod(String methodName, + Class... parameterTypes) { + + return new AutowiredMethodArgumentsResolver(methodName, parameterTypes, true, + null); + } + + /** + * Return a new {@link AutowiredMethodArgumentsResolver} instance + * that uses direct bean name injection shortcuts for specific parameters. + * @param beanNames the bean names to use as shortcuts (aligned with the + * method parameters) + * @return a new {@link AutowiredMethodArgumentsResolver} instance that uses + * the shortcuts + */ + public AutowiredMethodArgumentsResolver withShortcut(String... beanNames) { + return new AutowiredMethodArgumentsResolver(this.methodName, this.parameterTypes, + this.required, beanNames); + } + + /** + * Resolve the method arguments for the specified registered bean and + * provide it to the given action. + * @param registeredBean the registered bean + * @param action the action to execute with the resolved method arguments + */ + public void resolve(RegisteredBean registeredBean, + ThrowingConsumer action) { + + Assert.notNull(registeredBean, "'registeredBean' must not be null"); + Assert.notNull(action, "'action' must not be null"); + AutowiredArguments resolved = resolve(registeredBean); + if (resolved != null) { + action.accept(resolved); + } + } + + /** + * Resolve the method arguments for the specified registered bean. + * @param registeredBean the registered bean + * @return the resolved method arguments + */ + @Nullable + public AutowiredArguments resolve(RegisteredBean registeredBean) { + Assert.notNull(registeredBean, "'registeredBean' must not be null"); + return resolveArguments(registeredBean, getMethod(registeredBean)); + } + + /** + * Resolve the method arguments for the specified registered bean and invoke + * the method using reflection. + * @param registeredBean the registered bean + * @param instance the bean instance + */ + public void resolveAndInvoke(RegisteredBean registeredBean, Object instance) { + Assert.notNull(registeredBean, "'registeredBean' must not be null"); + Assert.notNull(instance, "'instance' must not be null"); + Method method = getMethod(registeredBean); + AutowiredArguments resolved = resolveArguments(registeredBean, method); + if (resolved != null) { + ReflectionUtils.makeAccessible(method); + ReflectionUtils.invokeMethod(method, instance, resolved.toArray()); + } + } + + @Nullable + private AutowiredArguments resolveArguments(RegisteredBean registeredBean, + Method method) { + + String beanName = registeredBean.getBeanName(); + Class beanClass = registeredBean.getBeanClass(); + ConfigurableBeanFactory beanFactory = registeredBean.getBeanFactory(); + Assert.isInstanceOf(AutowireCapableBeanFactory.class, beanFactory); + AutowireCapableBeanFactory autowireCapableBeanFactory = (AutowireCapableBeanFactory) beanFactory; + int argumentCount = method.getParameterCount(); + Object[] arguments = new Object[argumentCount]; + Set autowiredBeanNames = new LinkedHashSet<>(argumentCount); + TypeConverter typeConverter = beanFactory.getTypeConverter(); + for (int i = 0; i < argumentCount; i++) { + MethodParameter parameter = new MethodParameter(method, i); + DependencyDescriptor descriptor = new DependencyDescriptor(parameter, + this.required); + descriptor.setContainingClass(beanClass); + String shortcut = (this.shortcuts != null) ? this.shortcuts[i] : null; + if (shortcut != null) { + descriptor = new ShortcutDependencyDescriptor(descriptor, shortcut, + parameter.getParameterType()); + } + try { + Object argument = autowireCapableBeanFactory.resolveDependency(descriptor, + beanName, autowiredBeanNames, typeConverter); + if (argument == null && !this.required) { + return null; + } + arguments[i] = argument; + } + catch (BeansException ex) { + throw new UnsatisfiedDependencyException(null, beanName, + new InjectionPoint(parameter), ex); + } + } + registerDependentBeans(beanFactory, beanName, autowiredBeanNames); + return AutowiredArguments.of(arguments); + } + + private Method getMethod(RegisteredBean registeredBean) { + Method method = ReflectionUtils.findMethod(registeredBean.getBeanClass(), + this.methodName, this.parameterTypes); + Assert.notNull(method, () -> + "Method '%s' with parameter types [%s] declared on %s could not be found.".formatted( + this.methodName, toCommaSeparatedNames(this.parameterTypes), + registeredBean.getBeanClass().getName())); + return method; + } + + private String toCommaSeparatedNames(Class... parameterTypes) { + return Arrays.stream(parameterTypes).map(Class::getName) + .collect(Collectors.joining(", ")); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGenerator.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGenerator.java new file mode 100644 index 000000000000..4ef2debf2d6e --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGenerator.java @@ -0,0 +1,263 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.List; + +import javax.lang.model.element.Modifier; + +import org.springframework.aot.generate.GeneratedClass; +import org.springframework.aot.generate.GeneratedMethod; +import org.springframework.aot.generate.GeneratedMethods; +import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.generate.MethodReference; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.beans.factory.support.AutowireCandidateResolver; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.core.MethodParameter; +import org.springframework.javapoet.ClassName; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * Generates a method that returns a {@link BeanDefinition} to be registered. + * + * @author Phillip Webb + * @author Stephane Nicoll + * @since 6.0 + * @see BeanDefinitionMethodGeneratorFactory + */ +class BeanDefinitionMethodGenerator { + + private final BeanDefinitionMethodGeneratorFactory methodGeneratorFactory; + + private final RegisteredBean registeredBean; + + private final Executable constructorOrFactoryMethod; + + @Nullable + private final String currentPropertyName; + + private final List aotContributions; + + + /** + * Create a new {@link BeanDefinitionMethodGenerator} instance. + * @param methodGeneratorFactory the method generator factory + * @param registeredBean the registered bean + * @param currentPropertyName the current property name + * @param aotContributions the AOT contributions + */ + BeanDefinitionMethodGenerator( + BeanDefinitionMethodGeneratorFactory methodGeneratorFactory, + RegisteredBean registeredBean, @Nullable String currentPropertyName, + List aotContributions) { + + this.methodGeneratorFactory = methodGeneratorFactory; + this.registeredBean = registeredBean; + this.constructorOrFactoryMethod = registeredBean.resolveConstructorOrFactoryMethod(); + this.currentPropertyName = currentPropertyName; + this.aotContributions = aotContributions; + } + + + /** + * Generate the method that returns the {@link BeanDefinition} to be + * registered. + * @param generationContext the generation context + * @param beanRegistrationsCode the bean registrations code + * @return a reference to the generated method. + */ + MethodReference generateBeanDefinitionMethod(GenerationContext generationContext, + BeanRegistrationsCode beanRegistrationsCode) { + + registerRuntimeHintsIfNecessary(generationContext.getRuntimeHints()); + BeanRegistrationCodeFragments codeFragments = getCodeFragments(generationContext, + beanRegistrationsCode); + ClassName target = codeFragments.getTarget(this.registeredBean, this.constructorOrFactoryMethod); + if (isWritablePackageName(target)) { + GeneratedClass generatedClass = lookupGeneratedClass(generationContext, target); + GeneratedMethods generatedMethods = generatedClass.getMethods().withPrefix(getName()); + GeneratedMethod generatedMethod = generateBeanDefinitionMethod(generationContext, + generatedClass.getName(), generatedMethods, codeFragments, Modifier.PUBLIC); + return generatedMethod.toMethodReference(); + } + GeneratedMethods generatedMethods = beanRegistrationsCode.getMethods().withPrefix(getName()); + GeneratedMethod generatedMethod = generateBeanDefinitionMethod(generationContext, + beanRegistrationsCode.getClassName(), generatedMethods, codeFragments, Modifier.PRIVATE); + return generatedMethod.toMethodReference(); + } + + /** + * Specify if the {@link ClassName} belongs to a writable package. + * @param target the target to check + * @return {@code true} if generated code in that package is allowed + */ + private boolean isWritablePackageName(ClassName target) { + String packageName = target.packageName(); + return (!packageName.startsWith("java.") && !packageName.startsWith("javax.")); + } + + /** + * Return the {@link GeneratedClass} to use for the specified {@code target}. + *

    If the target class is an inner class, a corresponding inner class in + * the original structure is created. + * @param generationContext the generation context to use + * @param target the chosen target class name for the bean definition + * @return the generated class to use + */ + private static GeneratedClass lookupGeneratedClass(GenerationContext generationContext, ClassName target) { + ClassName topLevelClassName = target.topLevelClassName(); + GeneratedClass generatedClass = generationContext.getGeneratedClasses() + .getOrAddForFeatureComponent("BeanDefinitions", topLevelClassName, type -> { + type.addJavadoc("Bean definitions for {@link $T}", topLevelClassName); + type.addModifiers(Modifier.PUBLIC); + }); + List names = target.simpleNames(); + if (names.size() == 1) { + return generatedClass; + } + List namesToProcess = names.subList(1, names.size()); + ClassName currentTargetClassName = topLevelClassName; + GeneratedClass tmp = generatedClass; + for (String nameToProcess : namesToProcess) { + currentTargetClassName = currentTargetClassName.nestedClass(nameToProcess); + tmp = createInnerClass(tmp, nameToProcess + "__BeanDefinitions", currentTargetClassName); + } + return tmp; + } + + private static GeneratedClass createInnerClass(GeneratedClass generatedClass, + String name, ClassName target) { + return generatedClass.getOrAdd(name, type -> { + type.addJavadoc("Bean definitions for {@link $T}", target); + type.addModifiers(Modifier.PUBLIC, Modifier.STATIC); + }); + } + + private BeanRegistrationCodeFragments getCodeFragments(GenerationContext generationContext, + BeanRegistrationsCode beanRegistrationsCode) { + + BeanRegistrationCodeFragments codeFragments = new DefaultBeanRegistrationCodeFragments( + beanRegistrationsCode, this.registeredBean, this.methodGeneratorFactory); + for (BeanRegistrationAotContribution aotContribution : this.aotContributions) { + codeFragments = aotContribution.customizeBeanRegistrationCodeFragments(generationContext, codeFragments); + } + return codeFragments; + } + + private GeneratedMethod generateBeanDefinitionMethod( + GenerationContext generationContext, ClassName className, + GeneratedMethods generatedMethods, BeanRegistrationCodeFragments codeFragments, + Modifier modifier) { + + BeanRegistrationCodeGenerator codeGenerator = new BeanRegistrationCodeGenerator( + className, generatedMethods, this.registeredBean, + this.constructorOrFactoryMethod, codeFragments); + this.aotContributions.forEach(aotContribution -> aotContribution + .applyTo(generationContext, codeGenerator)); + return generatedMethods.add("getBeanDefinition", method -> { + method.addJavadoc("Get the $L definition for '$L'", + (!this.registeredBean.isInnerBean()) ? "bean" : "inner-bean", + getName()); + method.addModifiers(modifier, Modifier.STATIC); + method.returns(BeanDefinition.class); + method.addCode(codeGenerator.generateCode(generationContext)); + }); + } + + private String getName() { + if (this.currentPropertyName != null) { + return this.currentPropertyName; + } + if (!this.registeredBean.isGeneratedBeanName()) { + return getSimpleBeanName(this.registeredBean.getBeanName()); + } + RegisteredBean nonGeneratedParent = this.registeredBean; + while (nonGeneratedParent != null && nonGeneratedParent.isGeneratedBeanName()) { + nonGeneratedParent = nonGeneratedParent.getParent(); + } + if (nonGeneratedParent != null) { + return getSimpleBeanName(nonGeneratedParent.getBeanName()) + "InnerBean"; + } + return "innerBean"; + } + + private String getSimpleBeanName(String beanName) { + int lastDot = beanName.lastIndexOf('.'); + beanName = (lastDot != -1) ? beanName.substring(lastDot + 1) : beanName; + int lastDollar = beanName.lastIndexOf('$'); + beanName = (lastDollar != -1) ? beanName.substring(lastDollar + 1) : beanName; + return StringUtils.uncapitalize(beanName); + } + + private void registerRuntimeHintsIfNecessary(RuntimeHints runtimeHints) { + if (this.registeredBean.getBeanFactory() instanceof DefaultListableBeanFactory dlbf) { + ProxyRuntimeHintsRegistrar registrar = new ProxyRuntimeHintsRegistrar(dlbf.getAutowireCandidateResolver()); + if (this.constructorOrFactoryMethod instanceof Method method) { + registrar.registerRuntimeHints(runtimeHints, method); + } + else if (this.constructorOrFactoryMethod instanceof Constructor constructor) { + registrar.registerRuntimeHints(runtimeHints, constructor); + } + } + } + + + private static class ProxyRuntimeHintsRegistrar { + + private final AutowireCandidateResolver candidateResolver; + + public ProxyRuntimeHintsRegistrar(AutowireCandidateResolver candidateResolver) { + this.candidateResolver = candidateResolver; + } + + public void registerRuntimeHints(RuntimeHints runtimeHints, Method method) { + Class[] parameterTypes = method.getParameterTypes(); + for (int i = 0; i < parameterTypes.length; i++) { + MethodParameter methodParam = new MethodParameter(method, i); + DependencyDescriptor dependencyDescriptor = new DependencyDescriptor(methodParam, true); + registerProxyIfNecessary(runtimeHints, dependencyDescriptor); + } + } + + public void registerRuntimeHints(RuntimeHints runtimeHints, Constructor constructor) { + Class[] parameterTypes = constructor.getParameterTypes(); + for (int i = 0; i < parameterTypes.length; i++) { + MethodParameter methodParam = new MethodParameter(constructor, i); + DependencyDescriptor dependencyDescriptor = new DependencyDescriptor( + methodParam, true); + registerProxyIfNecessary(runtimeHints, dependencyDescriptor); + } + } + + private void registerProxyIfNecessary(RuntimeHints runtimeHints, DependencyDescriptor dependencyDescriptor) { + Class proxyType = this.candidateResolver.getLazyResolutionProxyClass(dependencyDescriptor, null); + if (proxyType != null && Proxy.isProxyClass(proxyType)) { + runtimeHints.proxies().registerJdkProxy(proxyType.getInterfaces()); + } + } + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGeneratorFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGeneratorFactory.java new file mode 100644 index 000000000000..73f34e71abe7 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGeneratorFactory.java @@ -0,0 +1,169 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.aot.AotServices.Source; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.core.log.LogMessage; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Factory used to create a {@link BeanDefinitionMethodGenerator} instance for a + * {@link RegisteredBean}. + * + * @author Phillip Webb + * @author Stephane Nicoll + * @since 6.0 + * @see BeanDefinitionMethodGenerator + * @see #getBeanDefinitionMethodGenerator(RegisteredBean) + */ +class BeanDefinitionMethodGeneratorFactory { + + private static final Log logger = LogFactory.getLog(BeanDefinitionMethodGeneratorFactory.class); + + + private final AotServices aotProcessors; + + private final AotServices excludeFilters; + + + /** + * Create a new {@link BeanDefinitionMethodGeneratorFactory} backed by the + * given {@link ConfigurableListableBeanFactory}. + * @param beanFactory the bean factory use + */ + BeanDefinitionMethodGeneratorFactory(ConfigurableListableBeanFactory beanFactory) { + this(AotServices.factoriesAndBeans(beanFactory)); + } + + /** + * Create a new {@link BeanDefinitionMethodGeneratorFactory} backed by the + * given {@link AotServices.Loader}. + * @param loader the AOT services loader to use + */ + BeanDefinitionMethodGeneratorFactory(AotServices.Loader loader) { + this.aotProcessors = loader.load(BeanRegistrationAotProcessor.class); + this.excludeFilters = loader.load(BeanRegistrationExcludeFilter.class); + for (BeanRegistrationExcludeFilter excludeFilter : this.excludeFilters) { + if (this.excludeFilters.getSource(excludeFilter) == Source.BEAN_FACTORY) { + Assert.state(excludeFilter instanceof BeanRegistrationAotProcessor + || excludeFilter instanceof BeanFactoryInitializationAotProcessor, + () -> "BeanRegistrationExcludeFilter bean of type %s must also implement an AOT processor interface" + .formatted(excludeFilter.getClass().getName())); + } + } + } + + + /** + * Return a {@link BeanDefinitionMethodGenerator} for the given + * {@link RegisteredBean} defined with the specified property name, or + * {@code null} if the registered bean is excluded by a + * {@link BeanRegistrationExcludeFilter}. The resulting + * {@link BeanDefinitionMethodGenerator} will include all + * {@link BeanRegistrationAotProcessor} provided contributions. + * @param registeredBean the registered bean + * @param currentPropertyName the property name that this bean belongs to + * @return a new {@link BeanDefinitionMethodGenerator} instance or + * {@code null} + */ + @Nullable + BeanDefinitionMethodGenerator getBeanDefinitionMethodGenerator( + RegisteredBean registeredBean, @Nullable String currentPropertyName) { + + if (isExcluded(registeredBean)) { + return null; + } + List contributions = getAotContributions( + registeredBean); + return new BeanDefinitionMethodGenerator(this, registeredBean, + currentPropertyName, contributions); + } + + /** + * Return a {@link BeanDefinitionMethodGenerator} for the given + * {@link RegisteredBean} or {@code null} if the registered bean is excluded + * by a {@link BeanRegistrationExcludeFilter}. The resulting + * {@link BeanDefinitionMethodGenerator} will include all + * {@link BeanRegistrationAotProcessor} provided contributions. + * @param registeredBean the registered bean + * @return a new {@link BeanDefinitionMethodGenerator} instance or + * {@code null} + */ + @Nullable + BeanDefinitionMethodGenerator getBeanDefinitionMethodGenerator(RegisteredBean registeredBean) { + return getBeanDefinitionMethodGenerator(registeredBean, null); + } + + private boolean isExcluded(RegisteredBean registeredBean) { + if (isImplicitlyExcluded(registeredBean)) { + return true; + } + for (BeanRegistrationExcludeFilter excludeFilter : this.excludeFilters) { + if (excludeFilter.isExcludedFromAotProcessing(registeredBean)) { + logger.trace(LogMessage.format( + "Excluding registered bean '%s' from bean factory %s due to %s", + registeredBean.getBeanName(), + ObjectUtils.identityToString(registeredBean.getBeanFactory()), + excludeFilter.getClass().getName())); + return true; + } + } + return false; + } + + private boolean isImplicitlyExcluded(RegisteredBean registeredBean) { + Class beanClass = registeredBean.getBeanClass(); + if (BeanFactoryInitializationAotProcessor.class.isAssignableFrom(beanClass)) { + return true; + } + if (BeanRegistrationAotProcessor.class.isAssignableFrom(beanClass)) { + BeanRegistrationAotProcessor processor = this.aotProcessors.findByBeanName(registeredBean.getBeanName()); + return (processor == null) || processor.isBeanExcludedFromAotProcessing(); + } + return false; + } + + private List getAotContributions( + RegisteredBean registeredBean) { + + String beanName = registeredBean.getBeanName(); + List contributions = new ArrayList<>(); + for (BeanRegistrationAotProcessor aotProcessor : this.aotProcessors) { + BeanRegistrationAotContribution contribution = aotProcessor + .processAheadOfTime(registeredBean); + if (contribution != null) { + logger.trace(LogMessage.format( + "Adding bean registration AOT contribution %S from %S to '%S'", + contribution.getClass().getName(), + aotProcessor.getClass().getName(), beanName)); + contributions.add(contribution); + } + } + return contributions; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGenerator.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGenerator.java new file mode 100644 index 000000000000..f4dc48e2ed81 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGenerator.java @@ -0,0 +1,305 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +import java.beans.PropertyDescriptor; +import java.lang.reflect.Method; +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.BiFunction; +import java.util.function.BiPredicate; +import java.util.function.Function; +import java.util.function.Predicate; + +import org.springframework.aot.generate.GeneratedMethods; +import org.springframework.aot.hint.ExecutableMode; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.PropertyValue; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueHolder; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.InstanceSupplier; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.javapoet.CodeBlock; +import org.springframework.javapoet.CodeBlock.Builder; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * Internal code generator to set {@link RootBeanDefinition} properties. + *

    + * Generates code in the following form:

    + * beanDefinition.setPrimary(true);
    + * beanDefinition.setScope(BeanDefinition.SCOPE_PROTOTYPE);
    + * ...
    + * 
    + *

    + * The generated code expects the following variables to be available: + *

    + *

      + *
    • {@code beanDefinition} - The {@link RootBeanDefinition} to + * configure.
    • + *
    + *

    + * Note that this generator does not set the {@link InstanceSupplier}. + * + * @author Phillip Webb + * @author Stephane Nicoll + * @since 6.0 + */ +class BeanDefinitionPropertiesCodeGenerator { + + private static final RootBeanDefinition DEFAULT_BEAN_DEFINITION = new RootBeanDefinition(); + + private static final String BEAN_DEFINITION_VARIABLE = BeanRegistrationCodeFragments.BEAN_DEFINITION_VARIABLE; + + private final RuntimeHints hints; + + private final Predicate attributeFilter; + + private final BeanDefinitionPropertyValueCodeGenerator valueCodeGenerator; + + + BeanDefinitionPropertiesCodeGenerator(RuntimeHints hints, + Predicate attributeFilter, GeneratedMethods generatedMethods, + BiFunction customValueCodeGenerator) { + + this.hints = hints; + this.attributeFilter = attributeFilter; + this.valueCodeGenerator = new BeanDefinitionPropertyValueCodeGenerator(generatedMethods, + (object, type) -> customValueCodeGenerator.apply(PropertyNamesStack.peek(), object)); + } + + + CodeBlock generateCode(RootBeanDefinition beanDefinition) { + CodeBlock.Builder code = CodeBlock.builder(); + addStatementForValue(code, beanDefinition, BeanDefinition::isPrimary, + "$L.setPrimary($L)"); + addStatementForValue(code, beanDefinition, BeanDefinition::getScope, + this::hasScope, "$L.setScope($S)"); + addStatementForValue(code, beanDefinition, BeanDefinition::getDependsOn, + this::hasDependsOn, "$L.setDependsOn($L)", this::toStringVarArgs); + addStatementForValue(code, beanDefinition, BeanDefinition::isAutowireCandidate, + "$L.setAutowireCandidate($L)"); + addStatementForValue(code, beanDefinition, BeanDefinition::getRole, + this::hasRole, "$L.setRole($L)", this::toRole); + addStatementForValue(code, beanDefinition, AbstractBeanDefinition::getLazyInit, + "$L.setLazyInit($L)"); + addStatementForValue(code, beanDefinition, AbstractBeanDefinition::isSynthetic, + "$L.setSynthetic($L)"); + addInitDestroyMethods(code, beanDefinition, beanDefinition.getInitMethodNames(), + "$L.setInitMethodNames($L)"); + addInitDestroyMethods(code, beanDefinition, beanDefinition.getDestroyMethodNames(), + "$L.setDestroyMethodNames($L)"); + addConstructorArgumentValues(code, beanDefinition); + addPropertyValues(code, beanDefinition); + addAttributes(code, beanDefinition); + return code.build(); + } + + private void addInitDestroyMethods(Builder code, + AbstractBeanDefinition beanDefinition, @Nullable String[] methodNames, String format) { + if (!ObjectUtils.isEmpty(methodNames)) { + Class beanType = ClassUtils.getUserClass(beanDefinition.getResolvableType().toClass()); + Arrays.stream(methodNames).forEach(methodName -> addInitDestroyHint(beanType, methodName)); + CodeBlock arguments = Arrays.stream(methodNames) + .map(name -> CodeBlock.of("$S", name)) + .collect(CodeBlock.joining(", ")); + code.addStatement(format, BEAN_DEFINITION_VARIABLE, arguments); + } + } + + private void addInitDestroyHint(Class beanUserClass, String methodName) { + Method method = ReflectionUtils.findMethod(beanUserClass, methodName); + if (method != null) { + this.hints.reflection().registerMethod(method, ExecutableMode.INVOKE); + } + } + + private void addConstructorArgumentValues(CodeBlock.Builder code, + BeanDefinition beanDefinition) { + + Map argumentValues = beanDefinition + .getConstructorArgumentValues().getIndexedArgumentValues(); + if (!argumentValues.isEmpty()) { + argumentValues.forEach((index, valueHolder) -> { + CodeBlock valueCode = generateValue(valueHolder.getName(), valueHolder.getValue()); + code.addStatement( + "$L.getConstructorArgumentValues().addIndexedArgumentValue($L, $L)", + BEAN_DEFINITION_VARIABLE, index, valueCode); + }); + } + } + + private void addPropertyValues(CodeBlock.Builder code, + RootBeanDefinition beanDefinition) { + + MutablePropertyValues propertyValues = beanDefinition.getPropertyValues(); + if (!propertyValues.isEmpty()) { + for (PropertyValue propertyValue : propertyValues) { + String name = propertyValue.getName(); + CodeBlock valueCode = generateValue(name, propertyValue.getValue()); + code.addStatement("$L.getPropertyValues().addPropertyValue($S, $L)", + BEAN_DEFINITION_VARIABLE, propertyValue.getName(), valueCode); + } + Class infrastructureType = getInfrastructureType(beanDefinition); + if (infrastructureType != Object.class) { + Map writeMethods = getWriteMethods(infrastructureType); + for (PropertyValue propertyValue : propertyValues) { + Method writeMethod = writeMethods.get(propertyValue.getName()); + if (writeMethod != null) { + this.hints.reflection().registerMethod(writeMethod, ExecutableMode.INVOKE); + } + } + } + } + } + + private CodeBlock generateValue(@Nullable String name, @Nullable Object value) { + try { + PropertyNamesStack.push(name); + return this.valueCodeGenerator.generateCode(value); + } + finally { + PropertyNamesStack.pop(); + } + } + + private Class getInfrastructureType(RootBeanDefinition beanDefinition) { + if (beanDefinition.hasBeanClass()) { + Class beanClass = beanDefinition.getBeanClass(); + if (FactoryBean.class.isAssignableFrom(beanClass)) { + return beanClass; + } + } + return ClassUtils.getUserClass(beanDefinition.getResolvableType().toClass()); + } + + private Map getWriteMethods(Class clazz) { + Map writeMethods = new HashMap<>(); + for (PropertyDescriptor propertyDescriptor : BeanUtils.getPropertyDescriptors(clazz)) { + writeMethods.put(propertyDescriptor.getName(), propertyDescriptor.getWriteMethod()); + } + return Collections.unmodifiableMap(writeMethods); + } + + private void addAttributes(CodeBlock.Builder code, BeanDefinition beanDefinition) { + String[] attributeNames = beanDefinition.attributeNames(); + if (!ObjectUtils.isEmpty(attributeNames)) { + for (String attributeName : attributeNames) { + if (this.attributeFilter.test(attributeName)) { + CodeBlock value = this.valueCodeGenerator + .generateCode(beanDefinition.getAttribute(attributeName)); + code.addStatement("$L.setAttribute($S, $L)", + BEAN_DEFINITION_VARIABLE, attributeName, value); + } + } + } + } + + private boolean hasScope(String defaultValue, String actualValue) { + return StringUtils.hasText(actualValue) + && !ConfigurableBeanFactory.SCOPE_SINGLETON.equals(actualValue); + } + + private boolean hasDependsOn(String[] defaultValue, String[] actualValue) { + return !ObjectUtils.isEmpty(actualValue); + } + + private boolean hasRole(int defaultValue, int actualValue) { + return actualValue != BeanDefinition.ROLE_APPLICATION; + } + + private CodeBlock toStringVarArgs(String[] strings) { + return Arrays.stream(strings).map(string -> CodeBlock.of("$S", string)) + .collect(CodeBlock.joining(",")); + } + + private Object toRole(int value) { + return switch (value) { + case BeanDefinition.ROLE_INFRASTRUCTURE -> CodeBlock.builder() + .add("$T.ROLE_INFRASTRUCTURE", BeanDefinition.class).build(); + case BeanDefinition.ROLE_SUPPORT -> CodeBlock.builder() + .add("$T.ROLE_SUPPORT", BeanDefinition.class).build(); + default -> value; + }; + } + + private void addStatementForValue( + CodeBlock.Builder code, BeanDefinition beanDefinition, + Function getter, String format) { + + addStatementForValue(code, beanDefinition, getter, + (defaultValue, actualValue) -> !Objects.equals(defaultValue, actualValue), + format); + } + + private void addStatementForValue( + CodeBlock.Builder code, BeanDefinition beanDefinition, + Function getter, BiPredicate filter, String format) { + + addStatementForValue(code, beanDefinition, getter, filter, format, + actualValue -> actualValue); + } + + @SuppressWarnings("unchecked") + private void addStatementForValue( + CodeBlock.Builder code, BeanDefinition beanDefinition, + Function getter, BiPredicate filter, String format, + Function formatter) { + + T defaultValue = getter.apply((B) DEFAULT_BEAN_DEFINITION); + T actualValue = getter.apply((B) beanDefinition); + if (filter.test(defaultValue, actualValue)) { + code.addStatement(format, BEAN_DEFINITION_VARIABLE, + formatter.apply(actualValue)); + } + } + + static class PropertyNamesStack { + + private static final ThreadLocal> threadLocal = ThreadLocal.withInitial(ArrayDeque::new); + + static void push(@Nullable String name) { + String valueToSet = (name != null) ? name : ""; + threadLocal.get().push(valueToSet); + } + + static void pop() { + threadLocal.get().pop(); + } + + @Nullable + static String peek() { + String value = threadLocal.get().peek(); + return ("".equals(value) ? null : value); + } + + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGenerator.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGenerator.java new file mode 100644 index 000000000000..f291ffd187ed --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGenerator.java @@ -0,0 +1,560 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.function.BiFunction; +import java.util.stream.Stream; + +import org.springframework.aot.generate.GeneratedMethod; +import org.springframework.aot.generate.GeneratedMethods; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanReference; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.support.ManagedList; +import org.springframework.beans.factory.support.ManagedMap; +import org.springframework.beans.factory.support.ManagedSet; +import org.springframework.core.ResolvableType; +import org.springframework.javapoet.AnnotationSpec; +import org.springframework.javapoet.CodeBlock; +import org.springframework.javapoet.CodeBlock.Builder; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; + +/** + * Internal code generator used to generate code for a single value contained in + * a {@link BeanDefinition} property. + * + * @author Stephane Nicoll + * @author Phillip Webb + * @since 6.0 + */ +class BeanDefinitionPropertyValueCodeGenerator { + + static final CodeBlock NULL_VALUE_CODE_BLOCK = CodeBlock.of("null"); + + private final GeneratedMethods generatedMethods; + + private final List delegates; + + + BeanDefinitionPropertyValueCodeGenerator(GeneratedMethods generatedMethods, + @Nullable BiFunction customValueGenerator) { + this.generatedMethods = generatedMethods; + this.delegates = new ArrayList<>(); + if (customValueGenerator != null) { + this.delegates.add(customValueGenerator::apply); + } + this.delegates.addAll(List.of( + new PrimitiveDelegate(), + new StringDelegate(), + new CharsetDelegate(), + new EnumDelegate(), + new ClassDelegate(), + new ResolvableTypeDelegate(), + new ArrayDelegate(), + new ManagedListDelegate(), + new ManagedSetDelegate(), + new ManagedMapDelegate(), + new ListDelegate(), + new SetDelegate(), + new MapDelegate(), + new BeanReferenceDelegate() + )); + } + + + CodeBlock generateCode(@Nullable Object value) { + ResolvableType type = ResolvableType.forInstance(value); + try { + return generateCode(value, type); + } + catch (Exception ex) { + throw new IllegalArgumentException(buildErrorMessage(value, type), ex); + } + } + + private CodeBlock generateCodeForElement(@Nullable Object value, ResolvableType type) { + try { + return generateCode(value, type); + } + catch (Exception ex) { + throw new IllegalArgumentException(buildErrorMessage(value, type), ex); + } + } + + private static String buildErrorMessage(@Nullable Object value, ResolvableType type) { + StringBuilder message = new StringBuilder("Failed to generate code for '"); + message.append(value).append("'"); + if (type != ResolvableType.NONE) { + message.append(" with type ").append(type); + } + return message.toString(); + } + + private CodeBlock generateCode(@Nullable Object value, ResolvableType type) { + if (value == null) { + return NULL_VALUE_CODE_BLOCK; + } + for (Delegate delegate : this.delegates) { + CodeBlock code = delegate.generateCode(value, type); + if (code != null) { + return code; + } + } + throw new IllegalArgumentException("Code generation does not support " + type); + } + + + /** + * Internal delegate used to support generation for a specific type. + */ + @FunctionalInterface + private interface Delegate { + + @Nullable + CodeBlock generateCode(Object value, ResolvableType type); + + } + + + /** + * {@link Delegate} for {@code primitive} types. + */ + private static class PrimitiveDelegate implements Delegate { + + private static final Map CHAR_ESCAPES = Map.of( + '\b', "\\b", + '\t', "\\t", + '\n', "\\n", + '\f', "\\f", + '\r', "\\r", + '\"', "\"", + '\'', "\\'", + '\\', "\\\\" + ); + + + @Override + @Nullable + public CodeBlock generateCode(Object value, ResolvableType type) { + if (value instanceof Boolean || value instanceof Integer) { + return CodeBlock.of("$L", value); + } + if (value instanceof Byte) { + return CodeBlock.of("(byte) $L", value); + } + if (value instanceof Short) { + return CodeBlock.of("(short) $L", value); + } + if (value instanceof Long) { + return CodeBlock.of("$LL", value); + } + if (value instanceof Float) { + return CodeBlock.of("$LF", value); + } + if (value instanceof Double) { + return CodeBlock.of("(double) $L", value); + } + if (value instanceof Character character) { + return CodeBlock.of("'$L'", escape(character)); + } + return null; + } + + private String escape(char ch) { + String escaped = CHAR_ESCAPES.get(ch); + if (escaped != null) { + return escaped; + } + return (!Character.isISOControl(ch)) ? Character.toString(ch) + : String.format("\\u%04x", (int) ch); + } + } + + + /** + * {@link Delegate} for {@link String} types. + */ + private static class StringDelegate implements Delegate { + + @Override + @Nullable + public CodeBlock generateCode(Object value, ResolvableType type) { + if (value instanceof String) { + return CodeBlock.of("$S", value); + } + return null; + } + } + + + /** + * {@link Delegate} for {@link Charset} types. + */ + private static class CharsetDelegate implements Delegate { + + @Override + @Nullable + public CodeBlock generateCode(Object value, ResolvableType type) { + if (value instanceof Charset charset) { + return CodeBlock.of("$T.forName($S)", Charset.class, charset.name()); + } + return null; + } + + } + + + /** + * {@link Delegate} for {@link Enum} types. + */ + private static class EnumDelegate implements Delegate { + + @Override + @Nullable + public CodeBlock generateCode(Object value, ResolvableType type) { + if (value instanceof Enum enumValue) { + return CodeBlock.of("$T.$L", enumValue.getDeclaringClass(), + enumValue.name()); + } + return null; + } + } + + + /** + * {@link Delegate} for {@link Class} types. + */ + private static class ClassDelegate implements Delegate { + + @Override + @Nullable + public CodeBlock generateCode(Object value, ResolvableType type) { + if (value instanceof Class clazz) { + return CodeBlock.of("$T.class", ClassUtils.getUserClass(clazz)); + } + return null; + } + } + + + /** + * {@link Delegate} for {@link ResolvableType} types. + */ + private static class ResolvableTypeDelegate implements Delegate { + + @Override + @Nullable + public CodeBlock generateCode(Object value, ResolvableType type) { + if (value instanceof ResolvableType resolvableType) { + return ResolvableTypeCodeGenerator.generateCode(resolvableType); + } + return null; + } + } + + + /** + * {@link Delegate} for {@code array} types. + */ + private class ArrayDelegate implements Delegate { + + @Override + @Nullable + public CodeBlock generateCode(@Nullable Object value, ResolvableType type) { + if (type.isArray()) { + ResolvableType componentType = type.getComponentType(); + Stream elements = Arrays.stream(ObjectUtils.toObjectArray(value)).map(component -> + BeanDefinitionPropertyValueCodeGenerator.this.generateCode(component, componentType)); + CodeBlock.Builder code = CodeBlock.builder(); + code.add("new $T {", type.toClass()); + code.add(elements.collect(CodeBlock.joining(", "))); + code.add("}"); + return code.build(); + } + return null; + } + } + + + /** + * Abstract {@link Delegate} for {@code Collection} types. + */ + private abstract class CollectionDelegate> implements Delegate { + + private final Class collectionType; + + private final CodeBlock emptyResult; + + public CollectionDelegate(Class collectionType, CodeBlock emptyResult) { + this.collectionType = collectionType; + this.emptyResult = emptyResult; + } + + @Override + @SuppressWarnings("unchecked") + @Nullable + public CodeBlock generateCode(Object value, ResolvableType type) { + if (this.collectionType.isInstance(value)) { + T collection = (T) value; + if (collection.isEmpty()) { + return this.emptyResult; + } + ResolvableType elementType = type.as(this.collectionType).getGeneric(); + return generateCollectionCode(elementType, collection); + } + return null; + } + + protected CodeBlock generateCollectionCode(ResolvableType elementType, T collection) { + return generateCollectionOf(collection, this.collectionType, elementType); + } + + protected final CodeBlock generateCollectionOf(Collection collection, + Class collectionType, ResolvableType elementType) { + Builder code = CodeBlock.builder(); + code.add("$T.of(", collectionType); + Iterator iterator = collection.iterator(); + while (iterator.hasNext()) { + Object element = iterator.next(); + code.add("$L", BeanDefinitionPropertyValueCodeGenerator.this + .generateCodeForElement(element, elementType)); + if (iterator.hasNext()) { + code.add(", "); + } + } + code.add(")"); + return code.build(); + } + } + + + /** + * {@link Delegate} for {@link ManagedList} types. + */ + private class ManagedListDelegate extends CollectionDelegate> { + + public ManagedListDelegate() { + super(ManagedList.class, CodeBlock.of("new $T()", ManagedList.class)); + } + } + + + /** + * {@link Delegate} for {@link ManagedSet} types. + */ + private class ManagedSetDelegate extends CollectionDelegate> { + + public ManagedSetDelegate() { + super(ManagedSet.class, CodeBlock.of("new $T()", ManagedSet.class)); + } + } + + + /** + * {@link Delegate} for {@link ManagedMap} types. + */ + private class ManagedMapDelegate implements Delegate { + + private static final CodeBlock EMPTY_RESULT = CodeBlock.of("$T.ofEntries()", ManagedMap.class); + + @Override + @Nullable + public CodeBlock generateCode(Object value, ResolvableType type) { + if (value instanceof ManagedMap managedMap) { + return generateManagedMapCode(type, managedMap); + } + return null; + } + + private CodeBlock generateManagedMapCode(ResolvableType type, ManagedMap managedMap) { + if (managedMap.isEmpty()) { + return EMPTY_RESULT; + } + ResolvableType keyType = type.as(Map.class).getGeneric(0); + ResolvableType valueType = type.as(Map.class).getGeneric(1); + CodeBlock.Builder code = CodeBlock.builder(); + code.add("$T.ofEntries(", ManagedMap.class); + Iterator> iterator = managedMap.entrySet().iterator(); + while (iterator.hasNext()) { + Entry entry = iterator.next(); + code.add("$T.entry($L,$L)", Map.class, + BeanDefinitionPropertyValueCodeGenerator.this + .generateCodeForElement(entry.getKey(), keyType), + BeanDefinitionPropertyValueCodeGenerator.this + .generateCodeForElement(entry.getValue(), valueType)); + if (iterator.hasNext()) { + code.add(", "); + } + } + code.add(")"); + return code.build(); + } + } + + + /** + * {@link Delegate} for {@link List} types. + */ + private class ListDelegate extends CollectionDelegate> { + + ListDelegate() { + super(List.class, CodeBlock.of("$T.emptyList()", Collections.class)); + } + } + + + /** + * {@link Delegate} for {@link Set} types. + */ + private class SetDelegate extends CollectionDelegate> { + + SetDelegate() { + super(Set.class, CodeBlock.of("$T.emptySet()", Collections.class)); + } + + @Override + protected CodeBlock generateCollectionCode(ResolvableType elementType, Set set) { + if (set instanceof LinkedHashSet) { + return CodeBlock.of("new $T($L)", LinkedHashSet.class, + generateCollectionOf(set, List.class, elementType)); + } + set = orderForCodeConsistency(set); + return super.generateCollectionCode(elementType, set); + } + + private Set orderForCodeConsistency(Set set) { + return new TreeSet(set); + } + } + + + /** + * {@link Delegate} for {@link Map} types. + */ + private class MapDelegate implements Delegate { + + private static final CodeBlock EMPTY_RESULT = CodeBlock.of("$T.emptyMap()", Collections.class); + + @Override + @Nullable + public CodeBlock generateCode(Object value, ResolvableType type) { + if (value instanceof Map map) { + return generateMapCode(type, map); + } + return null; + } + + private CodeBlock generateMapCode(ResolvableType type, Map map) { + if (map.isEmpty()) { + return EMPTY_RESULT; + } + ResolvableType keyType = type.as(Map.class).getGeneric(0); + ResolvableType valueType = type.as(Map.class).getGeneric(1); + if (map instanceof LinkedHashMap) { + return generateLinkedHashMapCode(map, keyType, valueType); + } + map = orderForCodeConsistency(map); + boolean useOfEntries = map.size() > 10; + CodeBlock.Builder code = CodeBlock.builder(); + code.add("$T" + ((!useOfEntries) ? ".of(" : ".ofEntries("), Map.class); + Iterator> iterator = map.entrySet().iterator(); + while (iterator.hasNext()) { + Entry entry = iterator.next(); + CodeBlock keyCode = BeanDefinitionPropertyValueCodeGenerator.this + .generateCodeForElement(entry.getKey(), keyType); + CodeBlock valueCode = BeanDefinitionPropertyValueCodeGenerator.this + .generateCodeForElement(entry.getValue(), valueType); + if (!useOfEntries) { + code.add("$L, $L", keyCode, valueCode); + } + else { + code.add("$T.entry($L,$L)", Map.class, keyCode, valueCode); + } + if (iterator.hasNext()) { + code.add(", "); + } + } + code.add(")"); + return code.build(); + } + + private Map orderForCodeConsistency(Map map) { + return new TreeMap<>(map); + } + + private CodeBlock generateLinkedHashMapCode(Map map, + ResolvableType keyType, ResolvableType valueType) { + + GeneratedMethods generatedMethods = BeanDefinitionPropertyValueCodeGenerator.this.generatedMethods; + GeneratedMethod generatedMethod = generatedMethods.add("getMap", method -> { + method.addAnnotation(AnnotationSpec + .builder(SuppressWarnings.class) + .addMember("value", "{\"rawtypes\", \"unchecked\"}") + .build()); + method.returns(Map.class); + method.addStatement("$T map = new $T($L)", Map.class, + LinkedHashMap.class, map.size()); + map.forEach((key, value) -> method.addStatement("map.put($L, $L)", + BeanDefinitionPropertyValueCodeGenerator.this + .generateCodeForElement(key, keyType), + BeanDefinitionPropertyValueCodeGenerator.this + .generateCodeForElement(value, valueType))); + method.addStatement("return map"); + }); + return CodeBlock.of("$L()", generatedMethod.getName()); + } + } + + + /** + * {@link Delegate} for {@link BeanReference} types. + */ + private static class BeanReferenceDelegate implements Delegate { + + @Override + @Nullable + public CodeBlock generateCode(Object value, ResolvableType type) { + if (value instanceof RuntimeBeanReference runtimeBeanReference && + runtimeBeanReference.getBeanType() != null) { + return CodeBlock.of("new $T($T.class)", RuntimeBeanReference.class, + runtimeBeanReference.getBeanType()); + } + else if (value instanceof BeanReference beanReference) { + return CodeBlock.of("new $T($S)", RuntimeBeanReference.class, + beanReference.getBeanName()); + } + return null; + } + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanFactoryInitializationAotContribution.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanFactoryInitializationAotContribution.java new file mode 100644 index 000000000000..41e96bd8cf93 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanFactoryInitializationAotContribution.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +import org.springframework.aot.generate.GenerationContext; + +/** + * AOT contribution from a {@link BeanFactoryInitializationAotProcessor} used to + * initialize a bean factory. + * + *

    Note: Beans implementing this interface will not have registration methods + * generated during AOT processing unless they also implement + * {@link org.springframework.beans.factory.aot.BeanRegistrationExcludeFilter}. + * + * @author Phillip Webb + * @since 6.0 + * @see BeanFactoryInitializationAotProcessor + */ +@FunctionalInterface +public interface BeanFactoryInitializationAotContribution { + + /** + * Apply this contribution to the given {@link BeanFactoryInitializationCode}. + * @param generationContext the active generation context + * @param beanFactoryInitializationCode the bean factory initialization code + */ + void applyTo(GenerationContext generationContext, + BeanFactoryInitializationCode beanFactoryInitializationCode); + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanFactoryInitializationAotProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanFactoryInitializationAotProcessor.java new file mode 100644 index 000000000000..cfa48d8fc5ae --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanFactoryInitializationAotProcessor.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.lang.Nullable; + +/** + * AOT processor that makes bean factory initialization contributions by + * processing {@link ConfigurableListableBeanFactory} instances. + * + *

    {@code BeanFactoryInitializationAotProcessor} implementations may be + * registered in a {@value AotServices#FACTORIES_RESOURCE_LOCATION} resource or + * as a bean. + * + *

    Using this interface on a registered bean will cause the bean and + * all of its dependencies to be initialized during AOT processing. We generally + * recommend that this interface is only used with infrastructure beans such as + * {@link BeanFactoryPostProcessor} which have limited dependencies and are + * already initialized early in the bean factory lifecycle. If such a bean is + * registered using a factory method, make sure to make it {@code static} so + * that its enclosing class does not have to be initialized. + * + *

    A component that implements this interface is not contributed. + * + * @author Phillip Webb + * @author Stephane Nicoll + * @since 6.0 + * @see BeanFactoryInitializationAotContribution + */ +@FunctionalInterface +public interface BeanFactoryInitializationAotProcessor { + + /** + * Process the given {@link ConfigurableListableBeanFactory} instance + * ahead-of-time and return a contribution or {@code null}. + *

    Processors are free to use any techniques they like to analyze the given + * bean factory. Most typically use reflection to find fields or methods to + * use in the contribution. Contributions typically generate source code or + * resource files that can be used when the AOT optimized application runs. + *

    If the given bean factory does not contain anything that is relevant to + * the processor, this method should return a {@code null} contribution. + * @param beanFactory the bean factory to process + * @return a {@link BeanFactoryInitializationAotContribution} or {@code null} + */ + @Nullable + BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory); + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanFactoryInitializationCode.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanFactoryInitializationCode.java new file mode 100644 index 000000000000..e7da3299e81a --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanFactoryInitializationCode.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +import org.springframework.aot.generate.GeneratedMethods; +import org.springframework.aot.generate.MethodReference; + +/** + * Interface that can be used to configure the code that will be generated to + * perform bean factory initialization. + * + * @author Phillip Webb + * @author Stephane Nicoll + * @since 6.0 + * @see BeanFactoryInitializationAotContribution + */ +public interface BeanFactoryInitializationCode { + + /** + * The recommended variable name to use to refer to the bean factory. + */ + String BEAN_FACTORY_VARIABLE = "beanFactory"; + + /** + * Get the {@link GeneratedMethods} used by the initializing code. + * @return the generated methods + */ + GeneratedMethods getMethods(); + + /** + * Add an initializer method call. An initializer can use a flexible signature, + * using any of the following: + *

      + *
    • {@code DefaultListableBeanFactory}, or {@code ConfigurableListableBeanFactory} + * to use the bean factory.
    • + *
    • {@code ConfigurableEnvironment} or {@code Environment} to access the + * environment.
    • + *
    • {@code ResourceLoader} to load resources.
    • + *
    + * @param methodReference a reference to the initialize method to call. + */ + void addInitializer(MethodReference methodReference); + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanInstanceSupplier.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanInstanceSupplier.java new file mode 100644 index 000000000000..939f753bae75 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanInstanceSupplier.java @@ -0,0 +1,507 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.aot.hint.ExecutableMode; +import org.springframework.beans.BeansException; +import org.springframework.beans.TypeConverter; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.InjectionPoint; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.UnsatisfiedDependencyException; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.config.ConstructorArgumentValues; +import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueHolder; +import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionValueResolver; +import org.springframework.beans.factory.support.InstanceSupplier; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.support.SimpleInstantiationStrategy; +import org.springframework.core.CollectionFactory; +import org.springframework.core.MethodParameter; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.function.ThrowingBiFunction; +import org.springframework.util.function.ThrowingFunction; +import org.springframework.util.function.ThrowingSupplier; + +/** + * Specialized {@link InstanceSupplier} that provides the factory {@link Method} + * used to instantiate the underlying bean instance, if any. Transparently + * handles resolution of {@link AutowiredArguments} if necessary. Typically used + * in AOT-processed applications as a targeted alternative to the reflection + * based injection. + * + *

    If no {@code generator} is provided, reflection is used to instantiate the + * bean instance, and full {@link ExecutableMode#INVOKE invocation} hints are + * contributed. Multiple generator callback styles are supported: + *

      + *
    • A function with the {@code registeredBean} and resolved {@code arguments} + * for executables that require arguments resolution. An + * {@link ExecutableMode#INTROSPECT introspection} hint is added so that + * parameter annotations can be read
    • + *
    • A function with only the {@code registeredBean} for simpler cases that + * do not require resolution of arguments
    • + *
    • A supplier when a method reference can be used
    • + *
    + * Generator callbacks handle checked exceptions so that the caller does not + * have to deal with them. + * + * @author Phillip Webb + * @author Stephane Nicoll + * @since 6.0 + * @param the type of instance supplied by this supplier + * @see AutowiredArguments + */ +public final class BeanInstanceSupplier extends AutowiredElementResolver implements InstanceSupplier { + + private final ExecutableLookup lookup; + + @Nullable + private final ThrowingBiFunction generator; + + @Nullable + private final String[] shortcuts; + + + private BeanInstanceSupplier(ExecutableLookup lookup, + @Nullable ThrowingBiFunction generator, + @Nullable String[] shortcuts) { + this.lookup = lookup; + this.generator = generator; + this.shortcuts = shortcuts; + } + + /** + * Create a {@link BeanInstanceSupplier} that resolves + * arguments for the specified bean constructor. + * @param the type of instance supplied + * @param parameterTypes the constructor parameter types + * @return a new {@link BeanInstanceSupplier} instance + */ + public static BeanInstanceSupplier forConstructor( + Class... parameterTypes) { + + Assert.notNull(parameterTypes, "'parameterTypes' must not be null"); + Assert.noNullElements(parameterTypes, "'parameterTypes' must not contain null elements"); + return new BeanInstanceSupplier<>(new ConstructorLookup(parameterTypes), null, null); + } + + /** + * Create a new {@link BeanInstanceSupplier} that + * resolves arguments for the specified factory method. + * @param the type of instance supplied + * @param declaringClass the class that declares the factory method + * @param methodName the factory method name + * @param parameterTypes the factory method parameter types + * @return a new {@link BeanInstanceSupplier} instance + */ + public static BeanInstanceSupplier forFactoryMethod( + Class declaringClass, String methodName, Class... parameterTypes) { + + Assert.notNull(declaringClass, "'declaringClass' must not be null"); + Assert.hasText(methodName, "'methodName' must not be empty"); + Assert.notNull(parameterTypes, "'parameterTypes' must not be null"); + Assert.noNullElements(parameterTypes, "'parameterTypes' must not contain null elements"); + return new BeanInstanceSupplier<>( + new FactoryMethodLookup(declaringClass, methodName, parameterTypes), + null, null); + } + + + ExecutableLookup getLookup() { + return this.lookup; + } + + /** + * Return a new {@link BeanInstanceSupplier} instance that uses the specified + * {@code generator} bi-function to instantiate the underlying bean. + * @param generator a {@link ThrowingBiFunction} that uses the + * {@link RegisteredBean} and resolved {@link AutowiredArguments} to + * instantiate the underlying bean + * @return a new {@link BeanInstanceSupplier} instance with the specified + * generator + */ + public BeanInstanceSupplier withGenerator( + ThrowingBiFunction generator) { + Assert.notNull(generator, "'generator' must not be null"); + return new BeanInstanceSupplier(this.lookup, generator, this.shortcuts); + } + + /** + * Return a new {@link BeanInstanceSupplier} instance that uses the specified + * {@code generator} function to instantiate the underlying bean. + * @param generator a {@link ThrowingFunction} that uses the + * {@link RegisteredBean} to instantiate the underlying bean + * @return a new {@link BeanInstanceSupplier} instance with the specified + * generator + */ + public BeanInstanceSupplier withGenerator( + ThrowingFunction generator) { + Assert.notNull(generator, "'generator' must not be null"); + return new BeanInstanceSupplier<>(this.lookup, + (registeredBean, args) -> generator.apply(registeredBean), this.shortcuts); + } + + /** + * Return a new {@link BeanInstanceSupplier} instance that uses the specified + * {@code generator} supplier to instantiate the underlying bean. + * @param generator a {@link ThrowingSupplier} to instantiate the underlying + * bean + * @return a new {@link BeanInstanceSupplier} instance with the specified + * generator + */ + public BeanInstanceSupplier withGenerator(ThrowingSupplier generator) { + Assert.notNull(generator, "'generator' must not be null"); + return new BeanInstanceSupplier<>(this.lookup, + (registeredBean, args) -> generator.get(), this.shortcuts); + } + + /** + * Return a new {@link BeanInstanceSupplier} instance + * that uses direct bean name injection shortcuts for specific parameters. + * @param beanNames the bean names to use as shortcuts (aligned with the + * constructor or factory method parameters) + * @return a new {@link BeanInstanceSupplier} instance + * that uses the shortcuts + */ + public BeanInstanceSupplier withShortcuts(String... beanNames) { + return new BeanInstanceSupplier(this.lookup, this.generator, beanNames); + } + + @Override + public T get(RegisteredBean registeredBean) throws Exception { + Assert.notNull(registeredBean, "'registeredBean' must not be null"); + Executable executable = this.lookup.get(registeredBean); + AutowiredArguments arguments = resolveArguments(registeredBean, executable); + if (this.generator != null) { + return invokeBeanSupplier(executable, () -> this.generator.apply(registeredBean, arguments)); + } + return invokeBeanSupplier(executable, + () -> instantiate(registeredBean.getBeanFactory(), executable, arguments.toArray())); + } + + private T invokeBeanSupplier(Executable executable, ThrowingSupplier beanSupplier) { + if (!(executable instanceof Method)) { + return beanSupplier.get(); + } + try { + SimpleInstantiationStrategy.setCurrentlyInvokedFactoryMethod((Method) executable); + return beanSupplier.get(); + } + finally { + SimpleInstantiationStrategy.setCurrentlyInvokedFactoryMethod(null); + } + } + + @Nullable + @Override + public Method getFactoryMethod() { + if (this.lookup instanceof FactoryMethodLookup factoryMethodLookup) { + return factoryMethodLookup.get(); + } + return null; + } + + /** + * Resolve arguments for the specified registered bean. + * @param registeredBean the registered bean + * @return the resolved constructor or factory method arguments + */ + AutowiredArguments resolveArguments(RegisteredBean registeredBean) { + Assert.notNull(registeredBean, "'registeredBean' must not be null"); + return resolveArguments(registeredBean, this.lookup.get(registeredBean)); + } + + private AutowiredArguments resolveArguments(RegisteredBean registeredBean,Executable executable) { + Assert.isInstanceOf(AbstractAutowireCapableBeanFactory.class, registeredBean.getBeanFactory()); + String beanName = registeredBean.getBeanName(); + Class beanClass = registeredBean.getBeanClass(); + AbstractAutowireCapableBeanFactory beanFactory = + (AbstractAutowireCapableBeanFactory) registeredBean.getBeanFactory(); + RootBeanDefinition mergedBeanDefinition = registeredBean.getMergedBeanDefinition(); + int startIndex = (executable instanceof Constructor constructor && + ClassUtils.isInnerClass(constructor.getDeclaringClass())) ? 1 : 0; + int parameterCount = executable.getParameterCount(); + Object[] resolved = new Object[parameterCount - startIndex]; + Assert.isTrue(this.shortcuts == null || this.shortcuts.length == resolved.length, + () -> "'shortcuts' must contain " + resolved.length + " elements"); + Set autowiredBeans = new LinkedHashSet<>(resolved.length); + ConstructorArgumentValues argumentValues = resolveArgumentValues(beanFactory, + beanName, mergedBeanDefinition); + for (int i = startIndex; i < parameterCount; i++) { + MethodParameter parameter = getMethodParameter(executable, i); + DependencyDescriptor dependencyDescriptor = new DependencyDescriptor(parameter, true); + String shortcut = (this.shortcuts != null) ? this.shortcuts[i - startIndex] : null; + if (shortcut != null) { + dependencyDescriptor = new ShortcutDependencyDescriptor( + dependencyDescriptor, shortcut, beanClass); + } + ValueHolder argumentValue = argumentValues.getIndexedArgumentValue(i, null); + resolved[i - startIndex] = resolveArgument(beanFactory, beanName, + autowiredBeans, parameter, dependencyDescriptor, argumentValue); + } + registerDependentBeans(beanFactory, beanName, autowiredBeans); + return AutowiredArguments.of(resolved); + } + + private MethodParameter getMethodParameter(Executable executable, int index) { + if (executable instanceof Constructor constructor) { + return new MethodParameter(constructor, index); + } + if (executable instanceof Method method) { + return new MethodParameter(method, index); + } + throw new IllegalStateException( + "Unsupported executable " + executable.getClass().getName()); + } + + private ConstructorArgumentValues resolveArgumentValues( + AbstractAutowireCapableBeanFactory beanFactory, String beanName, + RootBeanDefinition mergedBeanDefinition) { + + ConstructorArgumentValues resolved = new ConstructorArgumentValues(); + if (mergedBeanDefinition.hasConstructorArgumentValues()) { + BeanDefinitionValueResolver valueResolver = new BeanDefinitionValueResolver( + beanFactory, beanName, mergedBeanDefinition, beanFactory.getTypeConverter()); + ConstructorArgumentValues values = mergedBeanDefinition.getConstructorArgumentValues(); + values.getIndexedArgumentValues().forEach((index, valueHolder) -> { + ValueHolder resolvedValue = resolveArgumentValue(valueResolver, valueHolder); + resolved.addIndexedArgumentValue(index, resolvedValue); + }); + } + return resolved; + } + + private ValueHolder resolveArgumentValue(BeanDefinitionValueResolver resolver, + ValueHolder valueHolder) { + + if (valueHolder.isConverted()) { + return valueHolder; + } + Object resolvedValue = resolver.resolveValueIfNecessary("constructor argument", + valueHolder.getValue()); + ValueHolder resolvedValueHolder = new ValueHolder(resolvedValue, + valueHolder.getType(), valueHolder.getName()); + resolvedValueHolder.setSource(valueHolder); + return resolvedValueHolder; + } + + @Nullable + private Object resolveArgument(AbstractAutowireCapableBeanFactory beanFactory, + String beanName, Set autowiredBeans, MethodParameter parameter, + DependencyDescriptor dependencyDescriptor, @Nullable ValueHolder argumentValue) { + + TypeConverter typeConverter = beanFactory.getTypeConverter(); + Class parameterType = parameter.getParameterType(); + if (argumentValue != null) { + return (!argumentValue.isConverted()) ? + typeConverter.convertIfNecessary(argumentValue.getValue(), parameterType) : + argumentValue.getConvertedValue(); + } + try { + try { + return beanFactory.resolveDependency(dependencyDescriptor, beanName, + autowiredBeans, typeConverter); + } + catch (NoSuchBeanDefinitionException ex) { + if (parameterType.isArray()) { + return Array.newInstance(parameterType.getComponentType(), 0); + } + if (CollectionFactory.isApproximableCollectionType(parameterType)) { + return CollectionFactory.createCollection(parameterType, 0); + } + if (CollectionFactory.isApproximableMapType(parameterType)) { + return CollectionFactory.createMap(parameterType, 0); + } + throw ex; + } + } + catch (BeansException ex) { + throw new UnsatisfiedDependencyException(null, beanName, + new InjectionPoint(parameter), ex); + } + } + + @SuppressWarnings("unchecked") + private T instantiate(ConfigurableBeanFactory beanFactory, Executable executable, + Object[] arguments) { + + try { + if (executable instanceof Constructor constructor) { + return (T) instantiate(constructor, arguments); + } + if (executable instanceof Method method) { + return (T) instantiate(beanFactory, method, arguments); + } + } + catch (Exception ex) { + throw new BeanCreationException( + "Unable to instantiate bean using " + executable, ex); + } + throw new IllegalStateException( + "Unsupported executable " + executable.getClass().getName()); + } + + private Object instantiate(Constructor constructor, Object[] arguments) throws Exception { + Class declaringClass = constructor.getDeclaringClass(); + if (ClassUtils.isInnerClass(declaringClass)) { + Object enclosingInstance = createInstance(declaringClass.getEnclosingClass()); + arguments = ObjectUtils.addObjectToArray(arguments, enclosingInstance, 0); + } + ReflectionUtils.makeAccessible(constructor); + return constructor.newInstance(arguments); + } + + private Object instantiate(ConfigurableBeanFactory beanFactory, Method method, + Object[] arguments) { + + ReflectionUtils.makeAccessible(method); + Object target = getFactoryMethodTarget(beanFactory, method); + return ReflectionUtils.invokeMethod(method, target, arguments); + } + + @Nullable + private Object getFactoryMethodTarget(BeanFactory beanFactory, Method method) { + if (Modifier.isStatic(method.getModifiers())) { + return null; + } + Class declaringClass = method.getDeclaringClass(); + return beanFactory.getBean(declaringClass); + } + + private Object createInstance(Class clazz) throws Exception { + if (!ClassUtils.isInnerClass(clazz)) { + Constructor constructor = clazz.getDeclaredConstructor(); + ReflectionUtils.makeAccessible(constructor); + return constructor.newInstance(); + } + Class enclosingClass = clazz.getEnclosingClass(); + Constructor constructor = clazz.getDeclaredConstructor(enclosingClass); + return constructor.newInstance(createInstance(enclosingClass)); + } + + + private static String toCommaSeparatedNames(Class... parameterTypes) { + return Arrays.stream(parameterTypes).map(Class::getName).collect(Collectors.joining(", ")); + } + + /** + * Performs lookup of the {@link Executable}. + */ + static abstract class ExecutableLookup { + + abstract Executable get(RegisteredBean registeredBean); + + } + + + /** + * Performs lookup of the {@link Constructor}. + */ + private static class ConstructorLookup extends ExecutableLookup { + + private final Class[] parameterTypes; + + + ConstructorLookup(Class[] parameterTypes) { + this.parameterTypes = parameterTypes; + } + + + @Override + public Executable get(RegisteredBean registeredBean) { + Class beanClass = registeredBean.getBeanClass(); + try { + Class[] actualParameterTypes = (!ClassUtils.isInnerClass(beanClass)) ? + this.parameterTypes : ObjectUtils.addObjectToArray( + this.parameterTypes, beanClass.getEnclosingClass(), 0); + return beanClass.getDeclaredConstructor(actualParameterTypes); + } + catch (NoSuchMethodException ex) { + throw new IllegalArgumentException( + "%s cannot be found on %s".formatted(this, beanClass.getName()), ex); + } + } + + @Override + public String toString() { + return "Constructor with parameter types [%s]".formatted( + toCommaSeparatedNames(this.parameterTypes)); + } + + } + + + /** + * Performs lookup of the factory {@link Method}. + */ + private static class FactoryMethodLookup extends ExecutableLookup { + + private final Class declaringClass; + + private final String methodName; + + private final Class[] parameterTypes; + + + FactoryMethodLookup(Class declaringClass, String methodName, + Class[] parameterTypes) { + this.declaringClass = declaringClass; + this.methodName = methodName; + this.parameterTypes = parameterTypes; + } + + + @Override + public Executable get(RegisteredBean registeredBean) { + return get(); + } + + Method get() { + Method method = ReflectionUtils.findMethod(this.declaringClass, + this.methodName, this.parameterTypes); + Assert.notNull(method, () -> "%s cannot be found".formatted(this)); + return method; + } + + @Override + public String toString() { + return "Factory method '%s' with parameter types [%s] declared on %s".formatted( + this.methodName, toCommaSeparatedNames(this.parameterTypes), + this.declaringClass); + } + + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationAotContribution.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationAotContribution.java new file mode 100644 index 000000000000..4febbdd8bcb1 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationAotContribution.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +import java.util.function.UnaryOperator; + +import org.springframework.aot.generate.GenerationContext; +import org.springframework.util.Assert; + +/** + * AOT contribution from a {@link BeanRegistrationAotProcessor} used to register + * a single bean definition. + * + * @author Phillip Webb + * @author Stephane Nicoll + * @since 6.0 + * @see BeanRegistrationAotProcessor + */ +@FunctionalInterface +public interface BeanRegistrationAotContribution { + + /** + * Customize the {@link BeanRegistrationCodeFragments} that will be used to + * generate the bean registration code. Custom code fragments can be used if + * default code generation isn't suitable. + * @param generationContext the generation context + * @param codeFragments the existing code fragments + * @return the code fragments to use, may be the original instance or a + * wrapper + */ + default BeanRegistrationCodeFragments customizeBeanRegistrationCodeFragments( + GenerationContext generationContext, BeanRegistrationCodeFragments codeFragments) { + + return codeFragments; + } + + /** + * Apply this contribution to the given {@link BeanRegistrationCode}. + * @param generationContext the generation context + * @param beanRegistrationCode the generated registration + */ + void applyTo(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode); + + /** + * Create a {@link BeanRegistrationAotContribution} that customizes + * the {@link BeanRegistrationCodeFragments}. Typically used in + * conjunction with an extension of {@link BeanRegistrationCodeFragmentsDecorator} + * that overrides a specific callback. + * @param defaultCodeFragments the default code fragments + * @return a new {@link BeanRegistrationAotContribution} instance + * @see BeanRegistrationCodeFragmentsDecorator + */ + static BeanRegistrationAotContribution withCustomCodeFragments( + UnaryOperator defaultCodeFragments) { + + Assert.notNull(defaultCodeFragments, "'defaultCodeFragments' must not be null"); + + return new BeanRegistrationAotContribution() { + @Override + public BeanRegistrationCodeFragments customizeBeanRegistrationCodeFragments( + GenerationContext generationContext, BeanRegistrationCodeFragments codeFragments) { + return defaultCodeFragments.apply(codeFragments); + } + @Override + public void applyTo(GenerationContext generationContext, + BeanRegistrationCode beanRegistrationCode) { + } + }; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationAotProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationAotProcessor.java new file mode 100644 index 000000000000..5e2c17169610 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationAotProcessor.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.lang.Nullable; + +/** + * AOT processor that makes bean registration contributions by processing + * {@link RegisteredBean} instances. + * + *

    {@code BeanRegistrationAotProcessor} implementations may be registered in + * a {@value AotServices#FACTORIES_RESOURCE_LOCATION} resource or as a bean. + * + *

    Using this interface on a registered bean will cause the bean and + * all of its dependencies to be initialized during AOT processing. We generally + * recommend that this interface is only used with infrastructure beans such as + * {@link BeanPostProcessor} which have limited dependencies and are already + * initialized early in the bean factory lifecycle. If such a bean is registered + * using a factory method, make sure to make it {@code static} so that its + * enclosing class does not have to be initialized. + * + *

    An AOT processor replaces its usual runtime behavior by an optimized + * arrangement, usually in generated code. For that reason, a component that + * implements this interface is not contributed by default. If a component that + * implements this interface still needs to be invoked at runtime, + * {@link #isBeanExcludedFromAotProcessing} can be overridden. + * + * @author Phillip Webb + * @author Stephane Nicoll + * @since 6.0 + * @see BeanRegistrationAotContribution + */ +@FunctionalInterface +public interface BeanRegistrationAotProcessor { + + /** + * Process the given {@link RegisteredBean} instance ahead-of-time and + * return a contribution or {@code null}. + *

    + * Processors are free to use any techniques they like to analyze the given + * instance. Most typically use reflection to find fields or methods to use + * in the contribution. Contributions typically generate source code or + * resource files that can be used when the AOT optimized application runs. + *

    + * If the given instance isn't relevant to the processor, it should return a + * {@code null} contribution. + * @param registeredBean the registered bean to process + * @return a {@link BeanRegistrationAotContribution} or {@code null} + */ + @Nullable + BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean); + + /** + * Return if the bean instance associated with this processor should be + * excluded from AOT processing itself. By default, this method returns + * {@code true} to automatically exclude the bean, if the definition should + * be written then this method may be overridden to return {@code true}. + * @return if the bean should be excluded from AOT processing + * @see BeanRegistrationExcludeFilter + */ + default boolean isBeanExcludedFromAotProcessing() { + return true; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationCode.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationCode.java new file mode 100644 index 000000000000..a55a6efa4245 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationCode.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +import org.springframework.aot.generate.GeneratedMethods; +import org.springframework.aot.generate.MethodReference; +import org.springframework.beans.factory.support.InstanceSupplier; +import org.springframework.javapoet.ClassName; +import org.springframework.util.function.ThrowingBiFunction; + +/** + * Interface that can be used to configure the code that will be generated to + * perform registration of a single bean. + * + * @author Phillip Webb + * @since 6.0 + * @see BeanRegistrationCodeFragments + */ +public interface BeanRegistrationCode { + + /** + * Return the name of the class being used for registrations. + * @return the name of the class + */ + ClassName getClassName(); + + /** + * Return a {@link GeneratedMethods} being used by the registrations code. + * @return the generated methods + */ + GeneratedMethods getMethods(); + + /** + * Add an instance post processor method call to the registration code. + * @param methodReference a reference to the post-process method to call. + * The referenced method must have a functional signature compatible with + * {@link InstanceSupplier#andThen}. + * @see InstanceSupplier#andThen(ThrowingBiFunction) + */ + void addInstancePostProcessor(MethodReference methodReference); + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationCodeFragments.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationCodeFragments.java new file mode 100644 index 000000000000..d7f02a43e5ec --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationCodeFragments.java @@ -0,0 +1,118 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +import java.lang.reflect.Executable; +import java.util.List; +import java.util.function.Predicate; + +import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.generate.MethodReference; +import org.springframework.beans.factory.support.InstanceSupplier; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.core.ResolvableType; +import org.springframework.javapoet.ClassName; +import org.springframework.javapoet.CodeBlock; + +/** + * Generate the various fragments of code needed to register a bean. + * + * @author Phillip Webb + * @since 6.0 + */ +public interface BeanRegistrationCodeFragments { + + /** + * The variable name to used when creating the bean definition. + */ + String BEAN_DEFINITION_VARIABLE = "beanDefinition"; + + /** + * The variable name to used when creating the bean definition. + */ + String INSTANCE_SUPPLIER_VARIABLE = "instanceSupplier"; + + + /** + * Return the target for the registration. Used to determine where to write + * the code. + * @param registeredBean the registered bean + * @param constructorOrFactoryMethod the constructor or factory method + * @return the target {@link ClassName} + */ + ClassName getTarget(RegisteredBean registeredBean, + Executable constructorOrFactoryMethod); + + /** + * Generate the code that defines the new bean definition instance. + * @param generationContext the generation context + * @param beanType the bean type + * @param beanRegistrationCode the bean registration code + * @return the generated code + */ + CodeBlock generateNewBeanDefinitionCode(GenerationContext generationContext, + ResolvableType beanType, BeanRegistrationCode beanRegistrationCode); + + /** + * Generate the code that sets the properties of the bean definition. + * @param generationContext the generation context + * @param beanRegistrationCode the bean registration code + * @param attributeFilter any attribute filtering that should be applied + * @return the generated code + */ + CodeBlock generateSetBeanDefinitionPropertiesCode( + GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode, + RootBeanDefinition beanDefinition, Predicate attributeFilter); + + /** + * Generate the code that sets the instance supplier on the bean definition. + * @param generationContext the generation context + * @param beanRegistrationCode the bean registration code + * @param instanceSupplierCode the instance supplier code supplier code + * @param postProcessors any instance post processors that should be applied + * @return the generated code + * @see #generateInstanceSupplierCode + */ + CodeBlock generateSetBeanInstanceSupplierCode( + GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode, + CodeBlock instanceSupplierCode, List postProcessors); + + /** + * Generate the instance supplier code. + * @param generationContext the generation context + * @param beanRegistrationCode the bean registration code + * @param constructorOrFactoryMethod the constructor or factory method for + * the bean + * @param allowDirectSupplierShortcut if direct suppliers may be used rather + * than always needing an {@link InstanceSupplier} + * @return the generated code + */ + CodeBlock generateInstanceSupplierCode( + GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode, + Executable constructorOrFactoryMethod, boolean allowDirectSupplierShortcut); + + /** + * Generate the return statement. + * @param generationContext the generation context + * @param beanRegistrationCode the bean registration code + * @return the generated code + */ + CodeBlock generateReturnCode( + GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode); + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationCodeFragmentsDecorator.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationCodeFragmentsDecorator.java new file mode 100644 index 000000000000..69cae124b8fa --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationCodeFragmentsDecorator.java @@ -0,0 +1,102 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +import java.lang.reflect.Executable; +import java.util.List; +import java.util.function.Predicate; +import java.util.function.UnaryOperator; + +import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.generate.MethodReference; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.core.ResolvableType; +import org.springframework.javapoet.ClassName; +import org.springframework.javapoet.CodeBlock; +import org.springframework.util.Assert; + +/** + * A {@link BeanRegistrationCodeFragments} decorator implementation. Typically + * used when part of the default code fragments have to customized, by extending + * this class and using it as part of + * {@link BeanRegistrationAotContribution#withCustomCodeFragments(UnaryOperator)}. + * + * @author Phillip Webb + * @author Stephane Nicoll + * @since 6.0 + */ +public class BeanRegistrationCodeFragmentsDecorator implements BeanRegistrationCodeFragments { + + private final BeanRegistrationCodeFragments delegate; + + + protected BeanRegistrationCodeFragmentsDecorator(BeanRegistrationCodeFragments delegate) { + Assert.notNull(delegate, "Delegate must not be null"); + this.delegate = delegate; + } + + @Override + public ClassName getTarget(RegisteredBean registeredBean, Executable constructorOrFactoryMethod) { + return this.delegate.getTarget(registeredBean, constructorOrFactoryMethod); + } + + @Override + public CodeBlock generateNewBeanDefinitionCode(GenerationContext generationContext, + ResolvableType beanType, BeanRegistrationCode beanRegistrationCode) { + + return this.delegate.generateNewBeanDefinitionCode(generationContext, + beanType, beanRegistrationCode); + } + + @Override + public CodeBlock generateSetBeanDefinitionPropertiesCode( + GenerationContext generationContext, + BeanRegistrationCode beanRegistrationCode, RootBeanDefinition beanDefinition, + Predicate attributeFilter) { + + return this.delegate.generateSetBeanDefinitionPropertiesCode( + generationContext, beanRegistrationCode, beanDefinition, attributeFilter); + } + + @Override + public CodeBlock generateSetBeanInstanceSupplierCode( + GenerationContext generationContext, + BeanRegistrationCode beanRegistrationCode, CodeBlock instanceSupplierCode, + List postProcessors) { + + return this.delegate.generateSetBeanInstanceSupplierCode(generationContext, + beanRegistrationCode, instanceSupplierCode, postProcessors); + } + + @Override + public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext, + BeanRegistrationCode beanRegistrationCode, + Executable constructorOrFactoryMethod, boolean allowDirectSupplierShortcut) { + + return this.delegate.generateInstanceSupplierCode(generationContext, + beanRegistrationCode, constructorOrFactoryMethod, allowDirectSupplierShortcut); + } + + @Override + public CodeBlock generateReturnCode(GenerationContext generationContext, + BeanRegistrationCode beanRegistrationCode) { + + return this.delegate.generateReturnCode(generationContext, beanRegistrationCode); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationCodeGenerator.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationCodeGenerator.java new file mode 100644 index 000000000000..3547378b0673 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationCodeGenerator.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +import java.lang.reflect.Executable; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +import org.springframework.aot.generate.GeneratedMethods; +import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.generate.MethodReference; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.javapoet.ClassName; +import org.springframework.javapoet.CodeBlock; +import org.springframework.util.Assert; + +/** + * {@link BeanRegistrationCode} implementation with code generation support. + * + * @author Phillip Webb + * @since 6.0 + */ +class BeanRegistrationCodeGenerator implements BeanRegistrationCode { + + private static final Predicate REJECT_ALL_ATTRIBUTES_FILTER = attribute -> false; + + private final ClassName className; + + private final GeneratedMethods generatedMethods; + + private final List instancePostProcessors = new ArrayList<>(); + + private final RegisteredBean registeredBean; + + private final Executable constructorOrFactoryMethod; + + private final BeanRegistrationCodeFragments codeFragments; + + + BeanRegistrationCodeGenerator(ClassName className, GeneratedMethods generatedMethods, + RegisteredBean registeredBean, Executable constructorOrFactoryMethod, + BeanRegistrationCodeFragments codeFragments) { + + this.className = className; + this.generatedMethods = generatedMethods; + this.registeredBean = registeredBean; + this.constructorOrFactoryMethod = constructorOrFactoryMethod; + this.codeFragments = codeFragments; + } + + @Override + public ClassName getClassName() { + return this.className; + } + + @Override + public GeneratedMethods getMethods() { + return this.generatedMethods; + } + + @Override + public void addInstancePostProcessor(MethodReference methodReference) { + Assert.notNull(methodReference, "'methodReference' must not be null"); + this.instancePostProcessors.add(methodReference); + } + + CodeBlock generateCode(GenerationContext generationContext) { + CodeBlock.Builder code = CodeBlock.builder(); + code.add(this.codeFragments.generateNewBeanDefinitionCode(generationContext, + this.registeredBean.getBeanType(), this)); + code.add(this.codeFragments.generateSetBeanDefinitionPropertiesCode( + generationContext, this, this.registeredBean.getMergedBeanDefinition(), + REJECT_ALL_ATTRIBUTES_FILTER)); + CodeBlock instanceSupplierCode = this.codeFragments.generateInstanceSupplierCode( + generationContext, this, this.constructorOrFactoryMethod, + this.instancePostProcessors.isEmpty()); + code.add(this.codeFragments.generateSetBeanInstanceSupplierCode(generationContext, + this, instanceSupplierCode, this.instancePostProcessors)); + code.add(this.codeFragments.generateReturnCode(generationContext, this)); + return code.build(); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationExcludeFilter.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationExcludeFilter.java new file mode 100644 index 000000000000..10812417bc9d --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationExcludeFilter.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +import org.springframework.beans.factory.support.RegisteredBean; + +/** + * Filter that can be used to exclude AOT processing of a + * {@link RegisteredBean}. + * + * @author Phillip Webb + * @author Stephane Nicoll + * @since 6.0 + */ +@FunctionalInterface +public interface BeanRegistrationExcludeFilter { + + /** + * Return if the registered bean should be excluded from AOT processing and + * registration. + * @param registeredBean the registered bean + * @return if the registered bean should be excluded + */ + boolean isExcludedFromAotProcessing(RegisteredBean registeredBean); + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationsAotContribution.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationsAotContribution.java new file mode 100644 index 000000000000..bda1d4713519 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationsAotContribution.java @@ -0,0 +1,143 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +import java.util.Map; + +import javax.lang.model.element.Modifier; + +import org.springframework.aot.generate.GeneratedClass; +import org.springframework.aot.generate.GeneratedMethod; +import org.springframework.aot.generate.GeneratedMethods; +import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.generate.MethodReference; +import org.springframework.aot.generate.MethodReference.ArgumentCodeGenerator; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.javapoet.ClassName; +import org.springframework.javapoet.CodeBlock; +import org.springframework.javapoet.MethodSpec; + +/** + * AOT contribution from a {@link BeanRegistrationsAotProcessor} used to + * register bean definitions and aliases. + * + * @author Phillip Webb + * @author Sebastien Deleuze + * @author Stephane Nicoll + * @since 6.0 + * @see BeanRegistrationsAotProcessor + */ +class BeanRegistrationsAotContribution + implements BeanFactoryInitializationAotContribution { + + private static final String BEAN_FACTORY_PARAMETER_NAME = "beanFactory"; + + private final Map registrations; + + BeanRegistrationsAotContribution(Map registrations) { + this.registrations = registrations; + } + + + @Override + public void applyTo(GenerationContext generationContext, + BeanFactoryInitializationCode beanFactoryInitializationCode) { + + GeneratedClass generatedClass = generationContext.getGeneratedClasses() + .addForFeature("BeanFactoryRegistrations", type -> { + type.addJavadoc("Register bean definitions for the bean factory."); + type.addModifiers(Modifier.PUBLIC); + }); + BeanRegistrationsCodeGenerator codeGenerator = new BeanRegistrationsCodeGenerator(generatedClass); + GeneratedMethod generatedBeanDefinitionsMethod = codeGenerator.getMethods().add("registerBeanDefinitions", method -> + generateRegisterBeanDefinitionsMethod(method, generationContext, codeGenerator)); + beanFactoryInitializationCode.addInitializer(generatedBeanDefinitionsMethod.toMethodReference()); + GeneratedMethod generatedAliasesMethod = codeGenerator.getMethods().add("registerAliases", + this::generateRegisterAliasesMethod); + beanFactoryInitializationCode.addInitializer(generatedAliasesMethod.toMethodReference()); + } + + private void generateRegisterBeanDefinitionsMethod(MethodSpec.Builder method, + GenerationContext generationContext, + BeanRegistrationsCode beanRegistrationsCode) { + + method.addJavadoc("Register the bean definitions."); + method.addModifiers(Modifier.PUBLIC); + method.addParameter(DefaultListableBeanFactory.class, + BEAN_FACTORY_PARAMETER_NAME); + CodeBlock.Builder code = CodeBlock.builder(); + this.registrations.forEach((beanName, registration) -> { + MethodReference beanDefinitionMethod = registration.methodGenerator + .generateBeanDefinitionMethod(generationContext, + beanRegistrationsCode); + CodeBlock methodInvocation = beanDefinitionMethod.toInvokeCodeBlock( + ArgumentCodeGenerator.none(), beanRegistrationsCode.getClassName()); + code.addStatement("$L.registerBeanDefinition($S, $L)", + BEAN_FACTORY_PARAMETER_NAME, beanName, + methodInvocation); + }); + method.addCode(code.build()); + } + + private void generateRegisterAliasesMethod(MethodSpec.Builder method) { + method.addJavadoc("Register the aliases."); + method.addModifiers(Modifier.PUBLIC); + method.addParameter(DefaultListableBeanFactory.class, + BEAN_FACTORY_PARAMETER_NAME); + CodeBlock.Builder code = CodeBlock.builder(); + this.registrations.forEach((beanName, registration) -> { + for (String alias : registration.aliases) { + code.addStatement("$L.registerAlias($S, $S)", + BEAN_FACTORY_PARAMETER_NAME, beanName, alias); + } + }); + method.addCode(code.build()); + } + + /** + * Gather the necessary information to register a particular bean. + * @param methodGenerator the {@link BeanDefinitionMethodGenerator} to use + * @param aliases the bean aliases, if any + */ + record Registration(BeanDefinitionMethodGenerator methodGenerator, String[] aliases) {} + + + /** + * {@link BeanRegistrationsCode} with generation support. + */ + static class BeanRegistrationsCodeGenerator implements BeanRegistrationsCode { + + private final GeneratedClass generatedClass; + + public BeanRegistrationsCodeGenerator(GeneratedClass generatedClass) { + this.generatedClass = generatedClass; + } + + + @Override + public ClassName getClassName() { + return this.generatedClass.getName(); + } + + @Override + public GeneratedMethods getMethods() { + return this.generatedClass.getMethods(); + } + + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationsAotProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationsAotProcessor.java new file mode 100644 index 000000000000..cbbdcf0680ed --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationsAotProcessor.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.beans.factory.aot.BeanRegistrationsAotContribution.Registration; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.RegisteredBean; + +/** + * {@link BeanFactoryInitializationAotProcessor} that contributes code to + * register beans. + * + * @author Phillip Webb + * @author Sebastien Deleuze + * @author Stephane Nicoll + * @since 6.0 + */ +class BeanRegistrationsAotProcessor implements BeanFactoryInitializationAotProcessor { + + @Override + public BeanRegistrationsAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { + BeanDefinitionMethodGeneratorFactory beanDefinitionMethodGeneratorFactory = + new BeanDefinitionMethodGeneratorFactory(beanFactory); + Map registrations = new LinkedHashMap<>(); + for (String beanName : beanFactory.getBeanDefinitionNames()) { + RegisteredBean registeredBean = RegisteredBean.of(beanFactory, beanName); + BeanDefinitionMethodGenerator beanDefinitionMethodGenerator = beanDefinitionMethodGeneratorFactory + .getBeanDefinitionMethodGenerator(registeredBean); + if (beanDefinitionMethodGenerator != null) { + registrations.put(beanName, new Registration(beanDefinitionMethodGenerator, + beanFactory.getAliases(beanName))); + } + } + if (registrations.isEmpty()) { + return null; + } + return new BeanRegistrationsAotContribution(registrations); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationsCode.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationsCode.java new file mode 100644 index 000000000000..f325d7da0c67 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationsCode.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +import org.springframework.aot.generate.GeneratedMethods; +import org.springframework.javapoet.ClassName; + +/** + * Interface that can be used to configure the code that will be generated to + * register beans. + * + * @author Phillip Webb + * @since 6.0 + */ +public interface BeanRegistrationsCode { + + /** + * Return the name of the class being used for registrations. + * @return the generated class name. + */ + ClassName getClassName(); + + /** + * Return a {@link GeneratedMethods} being used by the registrations code. + * @return the method generator + */ + GeneratedMethods getMethods(); + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/DefaultBeanRegistrationCodeFragments.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/DefaultBeanRegistrationCodeFragments.java new file mode 100644 index 000000000000..38c0a23072ad --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/DefaultBeanRegistrationCodeFragments.java @@ -0,0 +1,218 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.util.List; +import java.util.function.Predicate; + +import org.springframework.aot.generate.AccessControl; +import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.generate.MethodReference; +import org.springframework.aot.generate.MethodReference.ArgumentCodeGenerator; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.support.InstanceSupplier; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.core.ResolvableType; +import org.springframework.javapoet.ClassName; +import org.springframework.javapoet.CodeBlock; +import org.springframework.javapoet.ParameterizedTypeName; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Internal {@link BeanRegistrationCodeFragments} implementation used by + * default. + * + * @author Phillip Webb + */ +class DefaultBeanRegistrationCodeFragments implements BeanRegistrationCodeFragments { + + /** + * The variable name used to hold the bean type. + */ + private static final String BEAN_TYPE_VARIABLE = "beanType"; + + + private final BeanRegistrationsCode beanRegistrationsCode; + + private final RegisteredBean registeredBean; + + private final BeanDefinitionMethodGeneratorFactory beanDefinitionMethodGeneratorFactory; + + + DefaultBeanRegistrationCodeFragments(BeanRegistrationsCode beanRegistrationsCode, + RegisteredBean registeredBean, + BeanDefinitionMethodGeneratorFactory beanDefinitionMethodGeneratorFactory) { + + this.beanRegistrationsCode = beanRegistrationsCode; + this.registeredBean = registeredBean; + this.beanDefinitionMethodGeneratorFactory = beanDefinitionMethodGeneratorFactory; + } + + + @Override + public ClassName getTarget(RegisteredBean registeredBean, + Executable constructorOrFactoryMethod) { + + Class target = extractDeclaringClass(registeredBean.getBeanType(), constructorOrFactoryMethod); + while (target.getName().startsWith("java.") && registeredBean.isInnerBean()) { + RegisteredBean parent = registeredBean.getParent(); + Assert.state(parent != null, "No parent available for inner bean"); + target = parent.getBeanClass(); + } + return ClassName.get(target); + } + + private Class extractDeclaringClass(ResolvableType beanType, Executable executable) { + Class declaringClass = ClassUtils.getUserClass(executable.getDeclaringClass()); + if (executable instanceof Constructor + && AccessControl.forMember(executable).isPublic() + && FactoryBean.class.isAssignableFrom(declaringClass)) { + return extractTargetClassFromFactoryBean(declaringClass, beanType); + } + return executable.getDeclaringClass(); + } + + /** + * Extract the target class of a public {@link FactoryBean} based on its + * constructor. If the implementation does not resolve the target class + * because it itself uses a generic, attempt to extract it from the + * bean type. + * @param factoryBeanType the factory bean type + * @param beanType the bean type + * @return the target class to use + */ + private Class extractTargetClassFromFactoryBean(Class factoryBeanType, ResolvableType beanType) { + ResolvableType target = ResolvableType.forType(factoryBeanType).as(FactoryBean.class).getGeneric(0); + if (target.getType().equals(Class.class)) { + return target.toClass(); + } + else if (factoryBeanType.isAssignableFrom(beanType.toClass())) { + return beanType.as(FactoryBean.class).getGeneric(0).toClass(); + } + return beanType.toClass(); + } + + @Override + public CodeBlock generateNewBeanDefinitionCode(GenerationContext generationContext, + ResolvableType beanType, BeanRegistrationCode beanRegistrationCode) { + + CodeBlock.Builder code = CodeBlock.builder(); + code.addStatement(generateBeanTypeCode(beanType)); + code.addStatement("$T $L = new $T($L)", RootBeanDefinition.class, + BEAN_DEFINITION_VARIABLE, RootBeanDefinition.class, BEAN_TYPE_VARIABLE); + return code.build(); + } + + private CodeBlock generateBeanTypeCode(ResolvableType beanType) { + if (!beanType.hasGenerics()) { + return CodeBlock.of("$T $L = $T.class", Class.class, BEAN_TYPE_VARIABLE, + ClassUtils.getUserClass(beanType.toClass())); + } + return CodeBlock.of("$T $L = $L", ResolvableType.class, BEAN_TYPE_VARIABLE, + ResolvableTypeCodeGenerator.generateCode(beanType)); + } + + @Override + public CodeBlock generateSetBeanDefinitionPropertiesCode( + GenerationContext generationContext, + BeanRegistrationCode beanRegistrationCode, RootBeanDefinition beanDefinition, + Predicate attributeFilter) { + + return new BeanDefinitionPropertiesCodeGenerator( + generationContext.getRuntimeHints(), attributeFilter, + beanRegistrationCode.getMethods(), + (name, value) -> generateValueCode(generationContext, name, value)) + .generateCode(beanDefinition); + } + + @Nullable + protected CodeBlock generateValueCode(GenerationContext generationContext, + String name, Object value) { + + RegisteredBean innerRegisteredBean = getInnerRegisteredBean(value); + if (innerRegisteredBean != null) { + BeanDefinitionMethodGenerator methodGenerator = this.beanDefinitionMethodGeneratorFactory + .getBeanDefinitionMethodGenerator(innerRegisteredBean, name); + Assert.state(methodGenerator != null, "Unexpected filtering of inner-bean"); + MethodReference generatedMethod = methodGenerator + .generateBeanDefinitionMethod(generationContext, this.beanRegistrationsCode); + return generatedMethod.toInvokeCodeBlock(ArgumentCodeGenerator.none()); + } + return null; + } + + @Nullable + private RegisteredBean getInnerRegisteredBean(Object value) { + if (value instanceof BeanDefinitionHolder beanDefinitionHolder) { + return RegisteredBean.ofInnerBean(this.registeredBean, beanDefinitionHolder); + } + if (value instanceof BeanDefinition beanDefinition) { + return RegisteredBean.ofInnerBean(this.registeredBean, beanDefinition); + } + return null; + } + + @Override + public CodeBlock generateSetBeanInstanceSupplierCode( + GenerationContext generationContext, + BeanRegistrationCode beanRegistrationCode, CodeBlock instanceSupplierCode, + List postProcessors) { + + CodeBlock.Builder code = CodeBlock.builder(); + if (postProcessors.isEmpty()) { + code.addStatement("$L.setInstanceSupplier($L)", BEAN_DEFINITION_VARIABLE, instanceSupplierCode); + return code.build(); + } + code.addStatement("$T $L = $L", + ParameterizedTypeName.get(InstanceSupplier.class, this.registeredBean.getBeanClass()), + INSTANCE_SUPPLIER_VARIABLE, instanceSupplierCode); + for (MethodReference postProcessor : postProcessors) { + code.addStatement("$L = $L.andThen($L)", INSTANCE_SUPPLIER_VARIABLE, + INSTANCE_SUPPLIER_VARIABLE, postProcessor.toCodeBlock()); + } + code.addStatement("$L.setInstanceSupplier($L)", BEAN_DEFINITION_VARIABLE, + INSTANCE_SUPPLIER_VARIABLE); + return code.build(); + } + + @Override + public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext, + BeanRegistrationCode beanRegistrationCode, + Executable constructorOrFactoryMethod, boolean allowDirectSupplierShortcut) { + + return new InstanceSupplierCodeGenerator(generationContext, + beanRegistrationCode.getClassName(), beanRegistrationCode.getMethods(), allowDirectSupplierShortcut) + .generateCode(this.registeredBean, constructorOrFactoryMethod); + } + + @Override + public CodeBlock generateReturnCode(GenerationContext generationContext, + BeanRegistrationCode beanRegistrationCode) { + + CodeBlock.Builder code = CodeBlock.builder(); + code.addStatement("return $L", BEAN_DEFINITION_VARIABLE); + return code.build(); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/InstanceSupplierCodeGenerator.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/InstanceSupplierCodeGenerator.java new file mode 100644 index 000000000000..9ae4f0db3a24 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/InstanceSupplierCodeGenerator.java @@ -0,0 +1,327 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.function.Consumer; + +import org.springframework.aot.generate.AccessControl; +import org.springframework.aot.generate.AccessControl.Visibility; +import org.springframework.aot.generate.GeneratedMethod; +import org.springframework.aot.generate.GeneratedMethods; +import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.generate.MethodReference.ArgumentCodeGenerator; +import org.springframework.aot.hint.ExecutableMode; +import org.springframework.beans.factory.support.InstanceSupplier; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.core.ResolvableType; +import org.springframework.javapoet.ClassName; +import org.springframework.javapoet.CodeBlock; +import org.springframework.javapoet.CodeBlock.Builder; +import org.springframework.javapoet.MethodSpec; +import org.springframework.javapoet.ParameterizedTypeName; +import org.springframework.util.ClassUtils; +import org.springframework.util.function.ThrowingSupplier; + +/** + * Internal code generator to create an {@link InstanceSupplier}, usually in + * the form of a {@link BeanInstanceSupplier} that retains the executable + * that is used to instantiate the bean. + *

    + * Generated code is usually a method reference that generate the + * {@link BeanInstanceSupplier}, but some shortcut can be used as well such + * as: + *

    + * {@code InstanceSupplier.of(TheGeneratedClass::getMyBeanInstance);}
    + * 
    + * + * @author Phillip Webb + * @author Stephane Nicoll + * @since 6.0 + */ +class InstanceSupplierCodeGenerator { + + private static final String REGISTERED_BEAN_PARAMETER_NAME = "registeredBean"; + + private static final String ARGS_PARAMETER_NAME = "args"; + + private static final javax.lang.model.element.Modifier[] PRIVATE_STATIC = { + javax.lang.model.element.Modifier.PRIVATE, + javax.lang.model.element.Modifier.STATIC }; + + private static final CodeBlock NO_ARGS = CodeBlock.of(""); + + + private final GenerationContext generationContext; + + private final ClassName className; + + private final GeneratedMethods generatedMethods; + + private final boolean allowDirectSupplierShortcut; + + + InstanceSupplierCodeGenerator(GenerationContext generationContext, + ClassName className, GeneratedMethods generatedMethods, boolean allowDirectSupplierShortcut) { + + this.generationContext = generationContext; + this.className = className; + this.generatedMethods = generatedMethods; + this.allowDirectSupplierShortcut = allowDirectSupplierShortcut; + } + + + CodeBlock generateCode(RegisteredBean registeredBean, + Executable constructorOrFactoryMethod) { + + if (constructorOrFactoryMethod instanceof Constructor constructor) { + return generateCodeForConstructor(registeredBean, constructor); + } + if (constructorOrFactoryMethod instanceof Method method) { + return generateCodeForFactoryMethod(registeredBean, method); + } + throw new IllegalStateException( + "No suitable executor found for " + registeredBean.getBeanName()); + } + + private CodeBlock generateCodeForConstructor(RegisteredBean registeredBean, Constructor constructor) { + String beanName = registeredBean.getBeanName(); + Class beanClass = registeredBean.getBeanClass(); + Class declaringClass = constructor.getDeclaringClass(); + boolean dependsOnBean = ClassUtils.isInnerClass(declaringClass); + Visibility accessVisibility = getAccessVisibility(registeredBean, constructor); + if (accessVisibility != Visibility.PRIVATE) { + return generateCodeForAccessibleConstructor(beanName, beanClass, constructor, + dependsOnBean, declaringClass); + } + return generateCodeForInaccessibleConstructor(beanName, beanClass, constructor, dependsOnBean); + } + + private CodeBlock generateCodeForAccessibleConstructor(String beanName, Class beanClass, + Constructor constructor, boolean dependsOnBean, Class declaringClass) { + + this.generationContext.getRuntimeHints().reflection().registerConstructor( + constructor, ExecutableMode.INTROSPECT); + if (!dependsOnBean && constructor.getParameterCount() == 0) { + if (!this.allowDirectSupplierShortcut) { + return CodeBlock.of("$T.using($T::new)", InstanceSupplier.class, declaringClass); + } + if (!isThrowingCheckedException(constructor)) { + return CodeBlock.of("$T::new", declaringClass); + } + return CodeBlock.of("$T.of($T::new)", ThrowingSupplier.class, declaringClass); + } + GeneratedMethod generatedMethod = generateGetInstanceSupplierMethod(method -> + buildGetInstanceMethodForConstructor(method, beanName, beanClass, constructor, + declaringClass, dependsOnBean, PRIVATE_STATIC)); + return generateReturnStatement(generatedMethod); + } + + private CodeBlock generateCodeForInaccessibleConstructor(String beanName, + Class beanClass, Constructor constructor, boolean dependsOnBean) { + + this.generationContext.getRuntimeHints().reflection() + .registerConstructor(constructor, ExecutableMode.INVOKE); + GeneratedMethod generatedMethod = generateGetInstanceSupplierMethod(method -> { + method.addJavadoc("Get the bean instance supplier for '$L'.", beanName); + method.addModifiers(PRIVATE_STATIC); + method.returns(ParameterizedTypeName.get(BeanInstanceSupplier.class, beanClass)); + int parameterOffset = (!dependsOnBean) ? 0 : 1; + method.addStatement(generateResolverForConstructor(beanClass, constructor, parameterOffset)); + }); + return generateReturnStatement(generatedMethod); + } + + private void buildGetInstanceMethodForConstructor(MethodSpec.Builder method, + String beanName, Class beanClass, Constructor constructor, Class declaringClass, + boolean dependsOnBean, javax.lang.model.element.Modifier... modifiers) { + + method.addJavadoc("Get the bean instance supplier for '$L'.", beanName); + method.addModifiers(modifiers); + method.returns(ParameterizedTypeName.get(BeanInstanceSupplier.class, beanClass)); + int parameterOffset = (!dependsOnBean) ? 0 : 1; + CodeBlock.Builder code = CodeBlock.builder(); + code.add(generateResolverForConstructor(beanClass, constructor, parameterOffset)); + boolean hasArguments = constructor.getParameterCount() > 0; + CodeBlock arguments = hasArguments ? + new AutowiredArgumentsCodeGenerator(declaringClass, constructor) + .generateCode(constructor.getParameterTypes(), parameterOffset) + : NO_ARGS; + CodeBlock newInstance = generateNewInstanceCodeForConstructor(dependsOnBean, declaringClass, arguments); + code.add(generateWithGeneratorCode(hasArguments, newInstance)); + method.addStatement(code.build()); + } + + private CodeBlock generateResolverForConstructor(Class beanClass, + Constructor constructor, int parameterOffset) { + + CodeBlock parameterTypes = generateParameterTypesCode(constructor.getParameterTypes(), parameterOffset); + return CodeBlock.of("return $T.<$T>forConstructor($L)", BeanInstanceSupplier.class, beanClass, parameterTypes); + } + + private CodeBlock generateNewInstanceCodeForConstructor(boolean dependsOnBean, + Class declaringClass, CodeBlock args) { + + if (!dependsOnBean) { + return CodeBlock.of("new $T($L)", declaringClass, args); + } + return CodeBlock.of("$L.getBeanFactory().getBean($T.class).new $L($L)", + REGISTERED_BEAN_PARAMETER_NAME, declaringClass.getEnclosingClass(), + declaringClass.getSimpleName(), args); + } + + private CodeBlock generateCodeForFactoryMethod(RegisteredBean registeredBean, + Method factoryMethod) { + + String beanName = registeredBean.getBeanName(); + Class beanClass = registeredBean.getBeanClass(); + Class declaringClass = ClassUtils + .getUserClass(factoryMethod.getDeclaringClass()); + boolean dependsOnBean = !Modifier.isStatic(factoryMethod.getModifiers()); + Visibility accessVisibility = getAccessVisibility(registeredBean, factoryMethod); + if (accessVisibility != Visibility.PRIVATE) { + return generateCodeForAccessibleFactoryMethod( + beanName, beanClass, factoryMethod, declaringClass, dependsOnBean); + } + return generateCodeForInaccessibleFactoryMethod(beanName, beanClass, factoryMethod, declaringClass); + } + + private CodeBlock generateCodeForAccessibleFactoryMethod(String beanName, + Class beanClass, Method factoryMethod, Class declaringClass, boolean dependsOnBean) { + + this.generationContext.getRuntimeHints().reflection().registerMethod( + factoryMethod, ExecutableMode.INTROSPECT); + if (!dependsOnBean && factoryMethod.getParameterCount() == 0) { + CodeBlock.Builder code = CodeBlock.builder(); + code.add("$T.<$T>forFactoryMethod($T.class, $S)", BeanInstanceSupplier.class, + beanClass, declaringClass, factoryMethod.getName()); + code.add(".withGenerator($T::$L)", declaringClass, factoryMethod.getName()); + return code.build(); + } + GeneratedMethod getInstanceMethod = generateGetInstanceSupplierMethod(method -> + buildGetInstanceMethodForFactoryMethod(method, beanName, beanClass, factoryMethod, + declaringClass, dependsOnBean, PRIVATE_STATIC)); + return generateReturnStatement(getInstanceMethod); + } + + private CodeBlock generateCodeForInaccessibleFactoryMethod(String beanName, Class beanClass, + Method factoryMethod, Class declaringClass) { + + this.generationContext.getRuntimeHints().reflection().registerMethod(factoryMethod, ExecutableMode.INVOKE); + GeneratedMethod getInstanceMethod = generateGetInstanceSupplierMethod(method -> { + method.addJavadoc("Get the bean instance supplier for '$L'.", beanName); + method.addModifiers(PRIVATE_STATIC); + method.returns(ParameterizedTypeName.get(BeanInstanceSupplier.class, beanClass)); + method.addStatement(generateInstanceSupplierForFactoryMethod( + beanClass, factoryMethod, declaringClass, factoryMethod.getName())); + }); + return generateReturnStatement(getInstanceMethod); + } + + private void buildGetInstanceMethodForFactoryMethod(MethodSpec.Builder method, + String beanName, Class beanClass, Method factoryMethod, Class declaringClass, + boolean dependsOnBean, javax.lang.model.element.Modifier... modifiers) { + + String factoryMethodName = factoryMethod.getName(); + method.addJavadoc("Get the bean instance supplier for '$L'.", beanName); + method.addModifiers(modifiers); + method.returns(ParameterizedTypeName.get(BeanInstanceSupplier.class, beanClass)); + CodeBlock.Builder code = CodeBlock.builder(); + code.add(generateInstanceSupplierForFactoryMethod( + beanClass, factoryMethod, declaringClass, factoryMethodName)); + boolean hasArguments = factoryMethod.getParameterCount() > 0; + CodeBlock arguments = hasArguments ? + new AutowiredArgumentsCodeGenerator(declaringClass, factoryMethod) + .generateCode(factoryMethod.getParameterTypes()) + : NO_ARGS; + CodeBlock newInstance = generateNewInstanceCodeForMethod( + dependsOnBean, declaringClass, factoryMethodName, arguments); + code.add(generateWithGeneratorCode(hasArguments, newInstance)); + method.addStatement(code.build()); + } + + private CodeBlock generateInstanceSupplierForFactoryMethod(Class beanClass, + Method factoryMethod, Class declaringClass, String factoryMethodName) { + + if (factoryMethod.getParameterCount() == 0) { + return CodeBlock.of("return $T.<$T>forFactoryMethod($T.class, $S)", + BeanInstanceSupplier.class, beanClass, declaringClass, + factoryMethodName); + } + CodeBlock parameterTypes = generateParameterTypesCode(factoryMethod.getParameterTypes(), 0); + return CodeBlock.of("return $T.<$T>forFactoryMethod($T.class, $S, $L)", + BeanInstanceSupplier.class, beanClass, declaringClass, factoryMethodName, parameterTypes); + } + + private CodeBlock generateNewInstanceCodeForMethod(boolean dependsOnBean, + Class declaringClass, String factoryMethodName, CodeBlock args) { + + if (!dependsOnBean) { + return CodeBlock.of("$T.$L($L)", declaringClass, factoryMethodName, args); + } + return CodeBlock.of("$L.getBeanFactory().getBean($T.class).$L($L)", + REGISTERED_BEAN_PARAMETER_NAME, declaringClass, factoryMethodName, args); + } + + private CodeBlock generateReturnStatement(GeneratedMethod generatedMethod) { + return generatedMethod.toMethodReference().toInvokeCodeBlock( + ArgumentCodeGenerator.none(), this.className); + } + + private CodeBlock generateWithGeneratorCode(boolean hasArguments, CodeBlock newInstance) { + CodeBlock lambdaArguments = (hasArguments + ? CodeBlock.of("($L, $L)", REGISTERED_BEAN_PARAMETER_NAME, ARGS_PARAMETER_NAME) + : CodeBlock.of("($L)", REGISTERED_BEAN_PARAMETER_NAME)); + Builder code = CodeBlock.builder(); + code.add("\n"); + code.indent().indent(); + code.add(".withGenerator($L -> $L)", lambdaArguments, newInstance); + code.unindent().unindent(); + return code.build(); + } + + private Visibility getAccessVisibility(RegisteredBean registeredBean, Member member) { + AccessControl beanTypeAccessControl = AccessControl.forResolvableType(registeredBean.getBeanType()); + AccessControl memberAccessControl = AccessControl.forMember(member); + return AccessControl.lowest(beanTypeAccessControl, memberAccessControl).getVisibility(); + } + + private CodeBlock generateParameterTypesCode(Class[] parameterTypes, int offset) { + CodeBlock.Builder code = CodeBlock.builder(); + for (int i = offset; i < parameterTypes.length; i++) { + code.add(i != offset ? ", " : ""); + code.add("$T.class", parameterTypes[i]); + } + return code.build(); + } + + private GeneratedMethod generateGetInstanceSupplierMethod(Consumer method) { + return this.generatedMethods.add("getInstanceSupplier", method); + } + + private boolean isThrowingCheckedException(Executable executable) { + return Arrays.stream(executable.getGenericExceptionTypes()) + .map(ResolvableType::forType).map(ResolvableType::toClass) + .anyMatch(Exception.class::isAssignableFrom); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/ResolvableTypeCodeGenerator.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/ResolvableTypeCodeGenerator.java new file mode 100644 index 000000000000..e7b715dd006c --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/ResolvableTypeCodeGenerator.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +import java.util.Arrays; + +import org.springframework.core.ResolvableType; +import org.springframework.javapoet.CodeBlock; +import org.springframework.util.ClassUtils; + +/** + * Internal code generator used to support {@link ResolvableType}. + * + * @author Stephane Nicoll + * @author Phillip Webb + * @since 6.0 + */ +final class ResolvableTypeCodeGenerator { + + + private ResolvableTypeCodeGenerator() { + } + + + public static CodeBlock generateCode(ResolvableType resolvableType) { + return generateCode(resolvableType, false); + } + + private static CodeBlock generateCode(ResolvableType resolvableType, boolean allowClassResult) { + if (ResolvableType.NONE.equals(resolvableType)) { + return CodeBlock.of("$T.NONE", ResolvableType.class); + } + Class type = ClassUtils.getUserClass(resolvableType.toClass()); + if (resolvableType.hasGenerics() && !resolvableType.hasUnresolvableGenerics()) { + return generateCodeWithGenerics(resolvableType, type); + } + if (allowClassResult) { + return CodeBlock.of("$T.class", type); + } + return CodeBlock.of("$T.forClass($T.class)", ResolvableType.class, type); + } + + private static CodeBlock generateCodeWithGenerics(ResolvableType target, Class type) { + ResolvableType[] generics = target.getGenerics(); + boolean hasNoNestedGenerics = Arrays.stream(generics).noneMatch(ResolvableType::hasGenerics); + CodeBlock.Builder code = CodeBlock.builder(); + code.add("$T.forClassWithGenerics($T.class", ResolvableType.class, type); + for (ResolvableType generic : generics) { + code.add(", $L", generateCode(generic, hasNoNestedGenerics)); + } + code.add(")"); + return code.build(); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/package-info.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/package-info.java new file mode 100644 index 000000000000..bf7c97a915d0 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/package-info.java @@ -0,0 +1,9 @@ +/** + * AOT support for bean factories. + */ +@NonNullApi +@NonNullFields +package org.springframework.beans.factory.aot; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinitionHolder.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinitionHolder.java index d6767f7dcf7e..0b25ad6144a1 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinitionHolder.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinitionHolder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-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. @@ -168,10 +168,9 @@ public boolean equals(@Nullable Object other) { if (this == other) { return true; } - if (!(other instanceof BeanDefinitionHolder)) { + if (!(other instanceof BeanDefinitionHolder otherHolder)) { return false; } - BeanDefinitionHolder otherHolder = (BeanDefinitionHolder) other; return this.beanDefinition.equals(otherHolder.beanDefinition) && this.beanName.equals(otherHolder.beanName) && ObjectUtils.nullSafeEquals(this.aliases, otherHolder.aliases); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinitionVisitor.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinitionVisitor.java index 7b826c50d7cd..d3d83c71ca93 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinitionVisitor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinitionVisitor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -178,8 +178,7 @@ protected Object resolveValue(@Nullable Object value) { else if (value instanceof BeanDefinitionHolder) { visitBeanDefinition(((BeanDefinitionHolder) value).getBeanDefinition()); } - else if (value instanceof RuntimeBeanReference) { - RuntimeBeanReference ref = (RuntimeBeanReference) value; + else if (value instanceof RuntimeBeanReference ref) { String newBeanName = resolveStringValue(ref.getBeanName()); if (newBeanName == null) { return null; @@ -188,8 +187,7 @@ else if (value instanceof RuntimeBeanReference) { return new RuntimeBeanReference(newBeanName); } } - else if (value instanceof RuntimeBeanNameReference) { - RuntimeBeanNameReference ref = (RuntimeBeanNameReference) value; + else if (value instanceof RuntimeBeanNameReference ref) { String newBeanName = resolveStringValue(ref.getBeanName()); if (newBeanName == null) { return null; @@ -210,8 +208,7 @@ else if (value instanceof Set) { else if (value instanceof Map) { visitMap((Map) value); } - else if (value instanceof TypedStringValue) { - TypedStringValue typedStringValue = (TypedStringValue) value; + else if (value instanceof TypedStringValue typedStringValue) { String stringValue = typedStringValue.getValue(); if (stringValue != null) { String visitedString = resolveStringValue(stringValue); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanExpressionContext.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanExpressionContext.java index e6e383935e78..e50e9346587e 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanExpressionContext.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanExpressionContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -73,10 +73,9 @@ public boolean equals(@Nullable Object other) { if (this == other) { return true; } - if (!(other instanceof BeanExpressionContext)) { + if (!(other instanceof BeanExpressionContext otherContext)) { return false; } - BeanExpressionContext otherContext = (BeanExpressionContext) other; return (this.beanFactory == otherContext.beanFactory && this.scope == otherContext.scope); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanExpressionResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanExpressionResolver.java index fc55304f0fdc..2975de790c4c 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanExpressionResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanExpressionResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-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. @@ -20,8 +20,8 @@ import org.springframework.lang.Nullable; /** - * Strategy interface for resolving a value through evaluating it - * as an expression, if applicable. + * Strategy interface for resolving a value by evaluating it as an expression, + * if applicable. * *

    A raw {@link org.springframework.beans.factory.BeanFactory} does not * contain a default implementation of this strategy. However, @@ -36,12 +36,13 @@ public interface BeanExpressionResolver { /** * Evaluate the given value as an expression, if applicable; * return the value as-is otherwise. - * @param value the value to check - * @param evalContext the evaluation context + * @param value the value to evaluate as an expression + * @param beanExpressionContext the bean expression context to use when + * evaluating the expression * @return the resolved value (potentially the given value as-is) * @throws BeansException if evaluation failed */ @Nullable - Object evaluate(@Nullable String value, BeanExpressionContext evalContext) throws BeansException; + Object evaluate(@Nullable String value, BeanExpressionContext beanExpressionContext) throws BeansException; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanFactoryPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanFactoryPostProcessor.java index 5db855fe79e7..68c286cffb09 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanFactoryPostProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanFactoryPostProcessor.java @@ -30,7 +30,7 @@ * *

    A {@code BeanFactoryPostProcessor} may interact with and modify bean * definitions, but never bean instances. Doing so may cause premature bean - * instantiation, violating the container and causing unintended side-effects. + * instantiation, violating the container and causing unintended side effects. * If bean instance interaction is required, consider implementing * {@link BeanPostProcessor} instead. * diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/ConfigurableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/ConfigurableBeanFactory.java index 81f4d8760154..09460499e2be 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/ConfigurableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/ConfigurableBeanFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -17,7 +17,6 @@ package org.springframework.beans.factory.config; import java.beans.PropertyEditor; -import java.security.AccessControlContext; import org.springframework.beans.PropertyEditorRegistrar; import org.springframework.beans.PropertyEditorRegistry; @@ -291,13 +290,6 @@ public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, Single */ ApplicationStartup getApplicationStartup(); - /** - * Provides a security access control context relevant to this factory. - * @return the applicable AccessControlContext (never {@code null}) - * @since 3.0 - */ - AccessControlContext getAccessControlContext(); - /** * Copy all relevant configuration from the given other factory. *

    Should include all standard configuration settings as well as diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/ConfigurableListableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/ConfigurableListableBeanFactory.java index 3b4c8f595e2a..249d6bc31d1b 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/ConfigurableListableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/ConfigurableListableBeanFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-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. @@ -137,7 +137,10 @@ boolean isAutowireCandidate(String beanName, DependencyDescriptor descriptor) /** * Freeze all bean definitions, signalling that the registered bean definitions * will not be modified or post-processed any further. - *

    This allows the factory to aggressively cache bean definition metadata. + *

    This allows the factory to aggressively cache bean definition metadata + * going forward, after clearing the initial temporary metadata cache. + * @see #clearMetadataCache() + * @see #isConfigurationFrozen() */ void freezeConfiguration(); @@ -145,6 +148,7 @@ boolean isAutowireCandidate(String beanName, DependencyDescriptor descriptor) * Return whether this factory's bean definitions are frozen, * i.e. are not supposed to be modified or post-processed any further. * @return {@code true} if the factory's configuration is considered frozen + * @see #freezeConfiguration() */ boolean isConfigurationFrozen(); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/ConstructorArgumentValues.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/ConstructorArgumentValues.java index 4fbfb435875d..836b868c74d6 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/ConstructorArgumentValues.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/ConstructorArgumentValues.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -65,7 +65,7 @@ public ConstructorArgumentValues(ConstructorArgumentValues original) { /** * Copy all given argument values into this object, using separate holder - * instances to keep the values independent from the original object. + * instances to keep the values independent of the original object. *

    Note: Identical ValueHolder instances will only be registered once, * to allow for merging and re-merging of argument value definitions. Distinct * ValueHolder instances carrying the same content are of course allowed. @@ -121,8 +121,7 @@ public void addIndexedArgumentValue(int index, ValueHolder newValue) { */ private void addOrMergeIndexedArgumentValue(Integer key, ValueHolder newValue) { ValueHolder currentValue = this.indexedArgumentValues.get(key); - if (currentValue != null && newValue.getValue() instanceof Mergeable) { - Mergeable mergeable = (Mergeable) newValue.getValue(); + if (currentValue != null && newValue.getValue() instanceof Mergeable mergeable) { if (mergeable.isMergeEnabled()) { newValue.setValue(mergeable.merge(currentValue.getValue())); } @@ -230,8 +229,7 @@ private void addOrMergeGenericArgumentValue(ValueHolder newValue) { for (Iterator it = this.genericArgumentValues.iterator(); it.hasNext();) { ValueHolder currentValue = it.next(); if (newValue.getName().equals(currentValue.getName())) { - if (newValue.getValue() instanceof Mergeable) { - Mergeable mergeable = (Mergeable) newValue.getValue(); + if (newValue.getValue() instanceof Mergeable mergeable) { if (mergeable.isMergeEnabled()) { newValue.setValue(mergeable.merge(currentValue.getValue())); } @@ -351,7 +349,9 @@ public ValueHolder getArgumentValue(int index, Class requiredType, String req * @return the ValueHolder for the argument, or {@code null} if none set */ @Nullable - public ValueHolder getArgumentValue(int index, @Nullable Class requiredType, @Nullable String requiredName, @Nullable Set usedValueHolders) { + public ValueHolder getArgumentValue(int index, @Nullable Class requiredType, + @Nullable String requiredName, @Nullable Set usedValueHolders) { + Assert.isTrue(index >= 0, "Index must not be negative"); ValueHolder valueHolder = getIndexedArgumentValue(index, requiredType, requiredName); if (valueHolder == null) { @@ -390,10 +390,9 @@ public boolean equals(@Nullable Object other) { if (this == other) { return true; } - if (!(other instanceof ConstructorArgumentValues)) { + if (!(other instanceof ConstructorArgumentValues that)) { return false; } - ConstructorArgumentValues that = (ConstructorArgumentValues) other; if (this.genericArgumentValues.size() != that.genericArgumentValues.size() || this.indexedArgumentValues.size() != that.indexedArgumentValues.size()) { return false; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/CustomScopeConfigurer.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/CustomScopeConfigurer.java index f292cc9852f4..92702225d42d 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/CustomScopeConfigurer.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/CustomScopeConfigurer.java @@ -101,8 +101,7 @@ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) if (value instanceof Scope) { beanFactory.registerScope(scopeKey, (Scope) value); } - else if (value instanceof Class) { - Class scopeClass = (Class) value; + else if (value instanceof Class scopeClass) { Assert.isAssignable(Scope.class, scopeClass, "Invalid scope class"); beanFactory.registerScope(scopeKey, (Scope) BeanUtils.instantiateClass(scopeClass)); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/DependencyDescriptor.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/DependencyDescriptor.java index d5fd082162b6..4825563735ff 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/DependencyDescriptor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/DependencyDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -21,8 +21,6 @@ import java.io.Serializable; import java.lang.annotation.Annotation; import java.lang.reflect.Field; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; import java.util.Map; import java.util.Optional; @@ -182,7 +180,7 @@ public boolean isRequired() { /** * Check whether the underlying field is annotated with any variant of a - * {@code Nullable} annotation, e.g. {@code javax.annotation.Nullable} or + * {@code Nullable} annotation, e.g. {@code jakarta.annotation.Nullable} or * {@code edu.umd.cs.findbugs.annotations.Nullable}. */ private boolean hasNullableAnnotation() { @@ -220,26 +218,6 @@ public Object resolveNotUnique(ResolvableType type, Map matching throw new NoUniqueBeanDefinitionException(type, matchingBeans.keySet()); } - /** - * Resolve the specified not-unique scenario: by default, - * throwing a {@link NoUniqueBeanDefinitionException}. - *

    Subclasses may override this to select one of the instances or - * to opt out with no result at all through returning {@code null}. - * @param type the requested bean type - * @param matchingBeans a map of bean names and corresponding bean - * instances which have been pre-selected for the given type - * (qualifiers etc already applied) - * @return a bean instance to proceed with, or {@code null} for none - * @throws BeansException in case of the not-unique scenario being fatal - * @since 4.3 - * @deprecated as of 5.1, in favor of {@link #resolveNotUnique(ResolvableType, Map)} - */ - @Deprecated - @Nullable - public Object resolveNotUnique(Class type, Map matchingBeans) throws BeansException { - throw new NoUniqueBeanDefinitionException(type, matchingBeans.keySet()); - } - /** * Resolve a shortcut for this dependency against the given factory, for example * taking some pre-resolved information into account. @@ -385,23 +363,8 @@ public String getDependencyName() { public Class getDependencyType() { if (this.field != null) { if (this.nestingLevel > 1) { - Type type = this.field.getGenericType(); - for (int i = 2; i <= this.nestingLevel; i++) { - if (type instanceof ParameterizedType) { - Type[] args = ((ParameterizedType) type).getActualTypeArguments(); - type = args[args.length - 1]; - } - } - if (type instanceof Class) { - return (Class) type; - } - else if (type instanceof ParameterizedType) { - Type arg = ((ParameterizedType) type).getRawType(); - if (arg instanceof Class) { - return (Class) arg; - } - } - return Object.class; + Class clazz = getResolvableType().getRawClass(); + return (clazz != null ? clazz : Object.class); } else { return this.field.getType(); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/InstantiationAwareBeanPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/InstantiationAwareBeanPostProcessor.java index c4e21122ed81..e842353cb55d 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/InstantiationAwareBeanPostProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/InstantiationAwareBeanPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -16,8 +16,6 @@ package org.springframework.beans.factory.config; -import java.beans.PropertyDescriptor; - import org.springframework.beans.BeansException; import org.springframework.beans.PropertyValues; import org.springframework.lang.Nullable; @@ -34,9 +32,7 @@ * *

    NOTE: This interface is a special purpose interface, mainly for * internal use within the framework. It is recommended to implement the plain - * {@link BeanPostProcessor} interface as far as possible, or to derive from - * {@link InstantiationAwareBeanPostProcessorAdapter} in order to be shielded - * from extensions to this interface. + * {@link BeanPostProcessor} interface as far as possible. * * @author Juergen Hoeller * @author Rod Johnson @@ -96,54 +92,20 @@ default boolean postProcessAfterInstantiation(Object bean, String beanName) thro /** * Post-process the given property values before the factory applies them - * to the given bean, without any need for property descriptors. - *

    Implementations should return {@code null} (the default) if they provide a custom - * {@link #postProcessPropertyValues} implementation, and {@code pvs} otherwise. - * In a future version of this interface (with {@link #postProcessPropertyValues} removed), - * the default implementation will return the given {@code pvs} as-is directly. + * to the given bean. + *

    The default implementation returns the given {@code pvs} as-is. * @param pvs the property values that the factory is about to apply (never {@code null}) * @param bean the bean instance created, but whose properties have not yet been set * @param beanName the name of the bean * @return the actual property values to apply to the given bean (can be the passed-in - * PropertyValues instance), or {@code null} which proceeds with the existing properties - * but specifically continues with a call to {@link #postProcessPropertyValues} - * (requiring initialized {@code PropertyDescriptor}s for the current bean class) + * PropertyValues instance), or {@code null} to skip property population * @throws org.springframework.beans.BeansException in case of errors * @since 5.1 - * @see #postProcessPropertyValues */ @Nullable default PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) throws BeansException { - return null; - } - - /** - * Post-process the given property values before the factory applies them - * to the given bean. Allows for checking whether all dependencies have been - * satisfied, for example based on a "Required" annotation on bean property setters. - *

    Also allows for replacing the property values to apply, typically through - * creating a new MutablePropertyValues instance based on the original PropertyValues, - * adding or removing specific values. - *

    The default implementation returns the given {@code pvs} as-is. - * @param pvs the property values that the factory is about to apply (never {@code null}) - * @param pds the relevant property descriptors for the target bean (with ignored - * dependency types - which the factory handles specifically - already filtered out) - * @param bean the bean instance created, but whose properties have not yet been set - * @param beanName the name of the bean - * @return the actual property values to apply to the given bean (can be the passed-in - * PropertyValues instance), or {@code null} to skip property population - * @throws org.springframework.beans.BeansException in case of errors - * @see #postProcessProperties - * @see org.springframework.beans.MutablePropertyValues - * @deprecated as of 5.1, in favor of {@link #postProcessProperties(PropertyValues, Object, String)} - */ - @Deprecated - @Nullable - default PropertyValues postProcessPropertyValues( - PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeansException { - return pvs; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/InstantiationAwareBeanPostProcessorAdapter.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/InstantiationAwareBeanPostProcessorAdapter.java deleted file mode 100644 index b3c112c8be46..000000000000 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/InstantiationAwareBeanPostProcessorAdapter.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2002-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.beans.factory.config; - -/** - * Adapter that implements all methods on {@link SmartInstantiationAwareBeanPostProcessor} - * as no-ops, which will not change normal processing of each bean instantiated - * by the container. Subclasses may override merely those methods that they are - * actually interested in. - * - *

    Note that this base class is only recommendable if you actually require - * {@link InstantiationAwareBeanPostProcessor} functionality. If all you need - * is plain {@link BeanPostProcessor} functionality, prefer a straight - * implementation of that (simpler) interface. - * - * @author Rod Johnson - * @author Juergen Hoeller - * @since 2.0 - * @deprecated as of 5.3 in favor of implementing {@link InstantiationAwareBeanPostProcessor} - * or {@link SmartInstantiationAwareBeanPostProcessor} directly. - */ -@Deprecated -public abstract class InstantiationAwareBeanPostProcessorAdapter implements SmartInstantiationAwareBeanPostProcessor { - -} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/MethodInvokingBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/MethodInvokingBean.java index 1374c4105e98..57fa7ab222c0 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/MethodInvokingBean.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/MethodInvokingBean.java @@ -47,17 +47,17 @@ * which uses this class to call a static initialization method: * *

    - * <bean id="myObject" class="org.springframework.beans.factory.config.MethodInvokingBean">
    - *   <property name="staticMethod" value="com.whatever.MyClass.init"/>
    - * </bean>
    + * <bean id="myObject" class="org.springframework.beans.factory.config.MethodInvokingBean"> + * <property name="staticMethod" value="com.whatever.MyClass.init"/> + * </bean> * *

    An example of calling an instance method to start some server bean: * *

    - * <bean id="myStarter" class="org.springframework.beans.factory.config.MethodInvokingBean">
    - *   <property name="targetObject" ref="myServer"/>
    - *   <property name="targetMethod" value="start"/>
    - * </bean>
    + * <bean id="myStarter" class="org.springframework.beans.factory.config.MethodInvokingBean"> + * <property name="targetObject" ref="myServer"/> + * <property name="targetMethod" value="start"/> + * </bean> * * @author Juergen Hoeller * @since 4.0.3 diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/MethodInvokingFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/MethodInvokingFactoryBean.java index 86a6be985645..3ff39d81e5e3 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/MethodInvokingFactoryBean.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/MethodInvokingFactoryBean.java @@ -56,24 +56,24 @@ * which uses this class to call a static factory method: * *
    - * <bean id="myObject" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
    - *   <property name="staticMethod" value="com.whatever.MyClassFactory.getInstance"/>
    - * </bean>
    + * <bean id="myObject" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean"> + * <property name="staticMethod" value="com.whatever.MyClassFactory.getInstance"/> + * </bean> * *

    An example of calling a static method then an instance method to get at a * Java system property. Somewhat verbose, but it works. * *

    - * <bean id="sysProps" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
    - *   <property name="targetClass" value="java.lang.System"/>
    - *   <property name="targetMethod" value="getProperties"/>
    - * </bean>
    + * <bean id="sysProps" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
    + *   <property name="targetClass" value="java.lang.System"/>
    + *   <property name="targetMethod" value="getProperties"/>
    + * </bean>
      *
    - * <bean id="javaVersion" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
    - *   <property name="targetObject" ref="sysProps"/>
    - *   <property name="targetMethod" value="getProperty"/>
    - *   <property name="arguments" value="java.version"/>
    - * </bean>
    + * <bean id="javaVersion" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean"> + * <property name="targetObject" ref="sysProps"/> + * <property name="targetMethod" value="getProperty"/> + * <property name="arguments" value="java.version"/> + * </bean> * * @author Colin Sampaleanu * @author Juergen Hoeller diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/PlaceholderConfigurerSupport.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/PlaceholderConfigurerSupport.java index fc29a9eda995..9a11f7af3ff2 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/PlaceholderConfigurerSupport.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/PlaceholderConfigurerSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -36,15 +36,16 @@ * Example XML bean definition: * *
    - * <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"/>
    - *   <property name="driverClassName" value="${driver}"/>
    - *   <property name="url" value="jdbc:${dbname}"/>
    + * <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    + *   <property name="driverClassName" value="${driver}" />
    + *   <property name="url" value="jdbc:${dbname}" />
      * </bean>
      * 
    * * Example properties file: * - *
    driver=com.mysql.jdbc.Driver
    + * 
    + * driver=com.mysql.jdbc.Driver
      * dbname=mysql:mydb
    * * Annotated bean definitions may take advantage of property replacement using @@ -56,7 +57,8 @@ * in bean references. Furthermore, placeholder values can also cross-reference * other placeholders, like: * - *
    rootPath=myrootdir
    + * 
    + * rootPath=myrootdir
      * subPath=${rootPath}/subdir
    * * In contrast to {@link PropertyOverrideConfigurer}, subclasses of this type allow @@ -71,13 +73,13 @@ * *

    Default property values can be defined globally for each configurer instance * via the {@link #setProperties properties} property, or on a property-by-property basis - * using the default value separator which is {@code ":"} by default and - * customizable via {@link #setValueSeparator(String)}. + * using the value separator which is {@code ":"} by default and customizable via + * {@link #setValueSeparator(String)}. * *

    Example XML property with default value: * *

    - *   
    + *   <property name="url" value="jdbc:${dbname:defaultdb}" />
      * 
    * * @author Chris Beams diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyResourceConfigurer.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyResourceConfigurer.java index ad2422687772..d6de515fa3f5 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyResourceConfigurer.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyResourceConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-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. @@ -86,7 +86,7 @@ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) processProperties(beanFactory, mergedProps); } catch (IOException ex) { - throw new BeanInitializationException("Could not load properties", ex); + throw new BeanInitializationException("Could not load properties: " + ex.getMessage(), ex); } } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/ProviderCreatingFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/ProviderCreatingFactoryBean.java index 11933292d418..96d7bf3a6035 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/ProviderCreatingFactoryBean.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/ProviderCreatingFactoryBean.java @@ -18,7 +18,7 @@ import java.io.Serializable; -import javax.inject.Provider; +import jakarta.inject.Provider; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; @@ -27,18 +27,18 @@ /** * A {@link org.springframework.beans.factory.FactoryBean} implementation that - * returns a value which is a JSR-330 {@link javax.inject.Provider} that in turn + * returns a value which is a JSR-330 {@link jakarta.inject.Provider} that in turn * returns a bean sourced from a {@link org.springframework.beans.factory.BeanFactory}. * *

    This is basically a JSR-330 compliant variant of Spring's good old * {@link ObjectFactoryCreatingFactoryBean}. It can be used for traditional * external dependency injection configuration that targets a property or - * constructor argument of type {@code javax.inject.Provider}, as an + * constructor argument of type {@code jakarta.inject.Provider}, as an * alternative to JSR-330's {@code @Inject} annotation-driven approach. * * @author Juergen Hoeller * @since 3.0.2 - * @see javax.inject.Provider + * @see jakarta.inject.Provider * @see ObjectFactoryCreatingFactoryBean */ public class ProviderCreatingFactoryBean extends AbstractFactoryBean> { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/RuntimeBeanNameReference.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/RuntimeBeanNameReference.java index 805f6bbd61bc..90a59c2ac7f5 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/RuntimeBeanNameReference.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/RuntimeBeanNameReference.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-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. @@ -71,10 +71,9 @@ public boolean equals(@Nullable Object other) { if (this == other) { return true; } - if (!(other instanceof RuntimeBeanNameReference)) { + if (!(other instanceof RuntimeBeanNameReference that)) { return false; } - RuntimeBeanNameReference that = (RuntimeBeanNameReference) other; return this.beanName.equals(that.beanName); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/RuntimeBeanReference.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/RuntimeBeanReference.java index 11b4f1ac79b6..fbb748ad3c14 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/RuntimeBeanReference.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/RuntimeBeanReference.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -82,7 +82,7 @@ public RuntimeBeanReference(Class beanType) { * @since 5.2 */ public RuntimeBeanReference(Class beanType, boolean toParent) { - Assert.notNull(beanType, "'beanType' must not be empty"); + Assert.notNull(beanType, "'beanType' must not be null"); this.beanName = beanType.getName(); this.beanType = beanType; this.toParent = toParent; @@ -135,10 +135,9 @@ public boolean equals(@Nullable Object other) { if (this == other) { return true; } - if (!(other instanceof RuntimeBeanReference)) { + if (!(other instanceof RuntimeBeanReference that)) { return false; } - RuntimeBeanReference that = (RuntimeBeanReference) other; return (this.beanName.equals(that.beanName) && this.beanType == that.beanType && this.toParent == that.toParent); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/Scope.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/Scope.java index d2054ae059a2..9606eb87264b 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/Scope.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/Scope.java @@ -138,7 +138,7 @@ public interface Scope { *

    The exact meaning of the conversation ID depends on the underlying * storage mechanism. In the case of session-scoped objects, the * conversation ID would typically be equal to (or derived from) the - * {@link javax.servlet.http.HttpSession#getId() session ID}; in the + * {@link jakarta.servlet.http.HttpSession#getId() session ID}; in the * case of a custom conversation that sits within the overall session, * the specific ID for the current conversation would be appropriate. *

    Note: This is an optional operation. It is perfectly valid to diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/ServiceLocatorFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/ServiceLocatorFactoryBean.java index e0ee01e6e3d7..272e97f5b74f 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/ServiceLocatorFactoryBean.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/ServiceLocatorFactoryBean.java @@ -83,22 +83,22 @@ *

    A sample config in an XML-based * {@link org.springframework.beans.factory.BeanFactory} might look as follows: * - *

    <beans>
    + * 
    <beans>
      *
    - *   <!-- Prototype bean since we have state -->
    - *   <bean id="myService" class="a.b.c.MyService" singleton="false"/>
    + *   <!-- Prototype bean since we have state -->
    + *   <bean id="myService" class="a.b.c.MyService" singleton="false"/>
      *
    - *   <!-- will lookup the above 'myService' bean by *TYPE* -->
    + *   <!-- will lookup the above 'myService' bean by *TYPE* -->
      *   <bean id="myServiceFactory"
    - *            class="org.springframework.beans.factory.config.ServiceLocatorFactoryBean">
    - *     <property name="serviceLocatorInterface" value="a.b.c.ServiceFactory"/>
    - *   </bean>
    + *            class="org.springframework.beans.factory.config.ServiceLocatorFactoryBean">
    + *     <property name="serviceLocatorInterface" value="a.b.c.ServiceFactory"/>
    + *   </bean>
      *
    - *   <bean id="clientBean" class="a.b.c.MyClientBean">
    - *     <property name="myServiceFactory" ref="myServiceFactory"/>
    - *   </bean>
    + *   <bean id="clientBean" class="a.b.c.MyClientBean">
    + *     <property name="myServiceFactory" ref="myServiceFactory"/>
    + *   </bean>
      *
    - *</beans>
    + *</beans>
    * *

    The attendant {@code MyClientBean} class implementation might then * look something like this: @@ -135,22 +135,22 @@ *

    A sample config in an XML-based * {@link org.springframework.beans.factory.BeanFactory} might look as follows: * - *

    <beans>
    + * 
    <beans>
      *
    - *   <!-- Prototype beans since we have state (both extend MyService) -->
    - *   <bean id="specialService" class="a.b.c.SpecialService" singleton="false"/>
    - *   <bean id="anotherService" class="a.b.c.AnotherService" singleton="false"/>
    + *   <!-- Prototype beans since we have state (both extend MyService) -->
    + *   <bean id="specialService" class="a.b.c.SpecialService" singleton="false"/>
    + *   <bean id="anotherService" class="a.b.c.AnotherService" singleton="false"/>
      *
      *   <bean id="myServiceFactory"
    - *            class="org.springframework.beans.factory.config.ServiceLocatorFactoryBean">
    - *     <property name="serviceLocatorInterface" value="a.b.c.ServiceFactory"/>
    - *   </bean>
    + *            class="org.springframework.beans.factory.config.ServiceLocatorFactoryBean">
    + *     <property name="serviceLocatorInterface" value="a.b.c.ServiceFactory"/>
    + *   </bean>
      *
    - *   <bean id="clientBean" class="a.b.c.MyClientBean">
    - *     <property name="myServiceFactory" ref="myServiceFactory"/>
    - *   </bean>
    + *   <bean id="clientBean" class="a.b.c.MyClientBean">
    + *     <property name="myServiceFactory" ref="myServiceFactory"/>
    + *   </bean>
      *
    - *</beans>
    + *</beans>
    * *

    The attendant {@code MyClientBean} class implementation might then * look something like this: diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/SmartInstantiationAwareBeanPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/SmartInstantiationAwareBeanPostProcessor.java index d4a6ce7f7156..cd54203aea54 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/SmartInstantiationAwareBeanPostProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/SmartInstantiationAwareBeanPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-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. @@ -28,12 +28,10 @@ *

    NOTE: This interface is a special purpose interface, mainly for * internal use within the framework. In general, application-provided * post-processors should simply implement the plain {@link BeanPostProcessor} - * interface or derive from the {@link InstantiationAwareBeanPostProcessorAdapter} - * class. New methods might be added to this interface even in point releases. + * interface. * * @author Juergen Hoeller * @since 2.0.3 - * @see InstantiationAwareBeanPostProcessorAdapter */ public interface SmartInstantiationAwareBeanPostProcessor extends InstantiationAwareBeanPostProcessor { @@ -41,6 +39,8 @@ public interface SmartInstantiationAwareBeanPostProcessor extends InstantiationA * Predict the type of the bean to be eventually returned from this * processor's {@link #postProcessBeforeInstantiation} callback. *

    The default implementation returns {@code null}. + * Specific implementations should try to predict the bean type as + * far as known/cached already, without extra processing steps. * @param beanClass the raw class of the bean * @param beanName the name of the bean * @return the type of the bean, or {@code null} if not predictable @@ -51,6 +51,22 @@ default Class predictBeanType(Class beanClass, String beanName) throws Bea return null; } + /** + * Determine the type of the bean to be eventually returned from this + * processor's {@link #postProcessBeforeInstantiation} callback. + *

    The default implementation returns the given bean class as-is. + * Specific implementations should fully evaluate their processing steps + * in order to create/initialize a potential proxy class upfront. + * @param beanClass the raw class of the bean + * @param beanName the name of the bean + * @return the type of the bean (never {@code null}) + * @throws org.springframework.beans.BeansException in case of errors + * @since 6.0 + */ + default Class determineBeanType(Class beanClass, String beanName) throws BeansException { + return beanClass; + } + /** * Determine the candidate constructors to use for the given bean. *

    The default implementation returns {@code null}. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/TypedStringValue.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/TypedStringValue.java index 70d4ab2ff09c..b80727269f75 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/TypedStringValue.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/TypedStringValue.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -115,10 +115,10 @@ public void setTargetType(Class targetType) { */ public Class getTargetType() { Object targetTypeValue = this.targetType; - if (!(targetTypeValue instanceof Class)) { + if (!(targetTypeValue instanceof Class clazz)) { throw new IllegalStateException("Typed String value does not carry a resolved target type"); } - return (Class) targetTypeValue; + return clazz; } /** @@ -134,8 +134,8 @@ public void setTargetTypeName(@Nullable String targetTypeName) { @Nullable public String getTargetTypeName() { Object targetTypeValue = this.targetType; - if (targetTypeValue instanceof Class) { - return ((Class) targetTypeValue).getName(); + if (targetTypeValue instanceof Class clazz) { + return clazz.getName(); } else { return (String) targetTypeValue; @@ -219,10 +219,9 @@ public boolean equals(@Nullable Object other) { if (this == other) { return true; } - if (!(other instanceof TypedStringValue)) { + if (!(other instanceof TypedStringValue otherValue)) { return false; } - TypedStringValue otherValue = (TypedStringValue) other; return (ObjectUtils.nullSafeEquals(this.value, otherValue.value) && ObjectUtils.nullSafeEquals(this.targetType, otherValue.targetType)); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlProcessor.java index a7ea8c8d1fe7..536b597e6dd8 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -52,6 +52,7 @@ * @author Dave Syer * @author Juergen Hoeller * @author Sam Brannen + * @author Brian Clozel * @since 4.1 */ public abstract class YamlProcessor { @@ -85,7 +86,7 @@ public abstract class YamlProcessor { *

    * when mapped with *
    -	 * setDocumentMatchers(properties ->
    +	 * setDocumentMatchers(properties ->
     	 *     ("prod".equals(properties.getProperty("environment")) ? MatchStatus.FOUND : MatchStatus.NOT_FOUND));
     	 * 
    * would end up as @@ -96,7 +97,7 @@ public abstract class YamlProcessor { *
    */ public void setDocumentMatchers(DocumentMatcher... matchers) { - this.documentMatchers = Arrays.asList(matchers); + this.documentMatchers = List.of(matchers); } /** @@ -128,10 +129,11 @@ public void setResources(Resource... resources) { /** * Set the supported types that can be loaded from YAML documents. - *

    If no supported types are configured, all types encountered in YAML - * documents will be supported. If an unsupported type is encountered, an - * {@link IllegalStateException} will be thrown when the corresponding YAML - * node is processed. + *

    If no supported types are configured, only Java standard classes + * (as defined in {@link org.yaml.snakeyaml.constructor.SafeConstructor}) + * encountered in YAML documents will be supported. + * If an unsupported type is encountered, an {@link IllegalStateException} + * will be thrown when the corresponding YAML node is processed. * @param supportedTypes the supported types, or an empty array to clear the * supported types * @since 5.1.16 @@ -144,7 +146,7 @@ public void setSupportedTypes(Class... supportedTypes) { else { Assert.noNullElements(supportedTypes, "'supportedTypes' must not contain null elements"); this.supportedTypes = Arrays.stream(supportedTypes).map(Class::getName) - .collect(Collectors.collectingAndThen(Collectors.toSet(), Collections::unmodifiableSet)); + .collect(Collectors.toUnmodifiableSet()); } } @@ -153,7 +155,7 @@ public void setSupportedTypes(Class... supportedTypes) { * resources. Each resource is parsed in turn and the documents inside checked against * the {@link #setDocumentMatchers(DocumentMatcher...) matchers}. If a document * matches it is passed into the callback, along with its representation as Properties. - * Depending on the {@link #setResolutionMethod(ResolutionMethod)} not all of the + * Depending on the {@link #setResolutionMethod(ResolutionMethod)} not all the * documents will be parsed. * @param callback a callback to delegate to once matching documents are found * @see #createYaml() @@ -182,12 +184,8 @@ protected void process(MatchCallback callback) { protected Yaml createYaml() { LoaderOptions loaderOptions = new LoaderOptions(); loaderOptions.setAllowDuplicateKeys(false); - - if (!this.supportedTypes.isEmpty()) { - return new Yaml(new FilteringConstructor(loaderOptions), new Representer(), - new DumperOptions(), loaderOptions); - } - return new Yaml(loaderOptions); + return new Yaml(new FilteringConstructor(loaderOptions), new Representer(), + new DumperOptions(), loaderOptions); } private boolean process(MatchCallback callback, Yaml yaml, Resource resource) { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionReader.java b/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionReader.java index cf13a4081730..383e3c6d712e 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionReader.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -75,18 +75,18 @@ * * def reader = new GroovyBeanDefinitionReader(myApplicationContext) * reader.beans { - * dataSource(BasicDataSource) { // <--- invokeMethod + * dataSource(BasicDataSource) { // <--- invokeMethod * driverClassName = "org.hsqldb.jdbcDriver" * url = "jdbc:hsqldb:mem:grailsDB" - * username = "sa" // <-- setProperty + * username = "sa" // <-- setProperty * password = "" * settings = [mynew:"setting"] * } * sessionFactory(SessionFactory) { - * dataSource = dataSource // <-- getProperty for retrieving references + * dataSource = dataSource // <-- getProperty for retrieving references * } * myService(MyService) { - * nestedBean = { AnotherBean bean -> // <-- setProperty with closure for nested bean + * nestedBean = { AnotherBean bean -> // <-- setProperty with closure for nested bean * dataSource = dataSource * } * } @@ -113,7 +113,7 @@ * dataSource = dataSource * } * myService(MyService) { - * nestedBean = { AnotherBean bean -> + * nestedBean = { AnotherBean bean -> * dataSource = dataSource * } * } @@ -244,7 +244,7 @@ public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefin } @SuppressWarnings("serial") - Closure beans = new Closure(this) { + Closure beans = new Closure<>(this) { @Override public Object call(Object... args) { invokeBeanDefiningClosure((Closure) args[0]); @@ -377,23 +377,23 @@ public void importBeans(String resourcePattern) throws IOException { @Override public Object invokeMethod(String name, Object arg) { Object[] args = (Object[])arg; - if ("beans".equals(name) && args.length == 1 && args[0] instanceof Closure) { - return beans((Closure) args[0]); + if ("beans".equals(name) && args.length == 1 && args[0] instanceof Closure closure) { + return beans(closure); } else if ("ref".equals(name)) { String refName; if (args[0] == null) { throw new IllegalArgumentException("Argument to ref() is not a valid bean or was not found"); } - if (args[0] instanceof RuntimeBeanReference) { - refName = ((RuntimeBeanReference) args[0]).getBeanName(); + if (args[0] instanceof RuntimeBeanReference runtimeBeanReference) { + refName = runtimeBeanReference.getBeanName(); } else { refName = args[0].toString(); } boolean parentRef = false; - if (args.length > 1 && args[1] instanceof Boolean) { - parentRef = (Boolean) args[1]; + if (args.length > 1 && args[1] instanceof Boolean bool) { + parentRef = bool; } return new RuntimeBeanReference(refName, parentRef); } @@ -430,11 +430,11 @@ private boolean addDeferredProperty(String property, Object newValue) { private void finalizeDeferredProperties() { for (DeferredProperty dp : this.deferredProperties.values()) { - if (dp.value instanceof List) { - dp.value = manageListIfNecessary((List) dp.value); + if (dp.value instanceof List list) { + dp.value = manageListIfNecessary(list); } - else if (dp.value instanceof Map) { - dp.value = manageMapIfNecessary((Map) dp.value); + else if (dp.value instanceof Map map) { + dp.value = manageMapIfNecessary(map); } dp.apply(); } @@ -462,8 +462,7 @@ protected GroovyBeanDefinitionReader invokeBeanDefiningClosure(Closure callab */ private GroovyBeanDefinitionWrapper invokeBeanDefiningMethod(String beanName, Object[] args) { boolean hasClosureArgument = (args[args.length - 1] instanceof Closure); - if (args[0] instanceof Class) { - Class beanClass = (Class) args[0]; + if (args[0] instanceof Class beanClass) { if (hasClosureArgument) { if (args.length - 1 != 1) { this.currentBeanDefinition = new GroovyBeanDefinitionWrapper( @@ -478,27 +477,26 @@ private GroovyBeanDefinitionWrapper invokeBeanDefiningMethod(String beanName, Ob beanName, beanClass, resolveConstructorArguments(args, 1, args.length)); } } - else if (args[0] instanceof RuntimeBeanReference) { + else if (args[0] instanceof RuntimeBeanReference runtimeBeanReference) { this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(beanName); - this.currentBeanDefinition.getBeanDefinition().setFactoryBeanName(((RuntimeBeanReference) args[0]).getBeanName()); + this.currentBeanDefinition.getBeanDefinition().setFactoryBeanName(runtimeBeanReference.getBeanName()); } - else if (args[0] instanceof Map) { + else if (args[0] instanceof Map namedArgs) { // named constructor arguments - if (args.length > 1 && args[1] instanceof Class) { + if (args.length > 1 && args[1] instanceof Class clazz) { List constructorArgs = resolveConstructorArguments(args, 2, hasClosureArgument ? args.length - 1 : args.length); - this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(beanName, (Class) args[1], constructorArgs); - Map namedArgs = (Map) args[0]; - for (Object o : namedArgs.keySet()) { - String propName = (String) o; - setProperty(propName, namedArgs.get(propName)); + this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(beanName, clazz, constructorArgs); + for (Map.Entry entity : namedArgs.entrySet()) { + String propName = (String) entity.getKey(); + setProperty(propName, entity.getValue()); } } // factory method syntax else { this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(beanName); // First arg is the map containing factoryBean : factoryMethod - Map.Entry factoryBeanEntry = ((Map) args[0]).entrySet().iterator().next(); + Map.Entry factoryBeanEntry = namedArgs.entrySet().iterator().next(); // If we have a closure body, that will be the last argument. // In between are the constructor args int constructorArgsTest = (hasClosureArgument ? 2 : 1); @@ -547,14 +545,14 @@ protected List resolveConstructorArguments(Object[] args, int start, int if (constructorArgs[i] instanceof GString) { constructorArgs[i] = constructorArgs[i].toString(); } - else if (constructorArgs[i] instanceof List) { - constructorArgs[i] = manageListIfNecessary((List) constructorArgs[i]); + else if (constructorArgs[i] instanceof List list) { + constructorArgs[i] = manageListIfNecessary(list); } - else if (constructorArgs[i] instanceof Map){ - constructorArgs[i] = manageMapIfNecessary((Map) constructorArgs[i]); + else if (constructorArgs[i] instanceof Map map){ + constructorArgs[i] = manageMapIfNecessary(map); } } - return Arrays.asList(constructorArgs); + return List.of(constructorArgs); } /** @@ -619,10 +617,9 @@ protected void applyPropertyToBeanDefinition(String name, Object value) { if (addDeferredProperty(name, value)) { return; } - else if (value instanceof Closure) { + else if (value instanceof Closure callable) { GroovyBeanDefinitionWrapper current = this.currentBeanDefinition; try { - Closure callable = (Closure) value; Class parameterType = callable.getParameterTypes()[0]; if (Object.class == parameterType) { this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(""); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionWrapper.java b/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionWrapper.java index d1aeff7d0bcc..322c41429f29 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionWrapper.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionWrapper.java @@ -194,9 +194,8 @@ else if (Boolean.TRUE.equals(newValue)) { } } // constructorArgs - else if (CONSTRUCTOR_ARGS.equals(property) && newValue instanceof List) { + else if (CONSTRUCTOR_ARGS.equals(property) && newValue instanceof List args) { ConstructorArgumentValues cav = new ConstructorArgumentValues(); - List args = (List) newValue; for (Object arg : args) { cav.addGenericArgumentValue(arg); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyDynamicElementReader.java b/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyDynamicElementReader.java new file mode 100644 index 000000000000..bcab348fece4 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyDynamicElementReader.java @@ -0,0 +1,144 @@ +/* + * Copyright 2002-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.beans.factory.groovy; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.Map; + +import groovy.lang.Closure; +import groovy.lang.GroovyObject; +import groovy.lang.GroovyObjectSupport; +import groovy.lang.Writable; +import groovy.xml.StreamingMarkupBuilder; +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.xml.BeanDefinitionParserDelegate; + +/** + * Used by GroovyBeanDefinitionReader to read a Spring XML namespace expression + * in the Groovy DSL. + * + * @author Jeff Brown + * @author Juergen Hoeller + * @author Dave Syer + * @since 4.0 + */ +class GroovyDynamicElementReader extends GroovyObjectSupport { + + private final String rootNamespace; + + private final Map xmlNamespaces; + + private final BeanDefinitionParserDelegate delegate; + + private final GroovyBeanDefinitionWrapper beanDefinition; + + protected final boolean decorating; + + private boolean callAfterInvocation = true; + + + public GroovyDynamicElementReader(String namespace, Map namespaceMap, + BeanDefinitionParserDelegate delegate, GroovyBeanDefinitionWrapper beanDefinition, boolean decorating) { + + this.rootNamespace = namespace; + this.xmlNamespaces = namespaceMap; + this.delegate = delegate; + this.beanDefinition = beanDefinition; + this.decorating = decorating; + } + + + @Override + public Object invokeMethod(String name, Object obj) { + Object[] args = (Object[]) obj; + if (name.equals("doCall")) { + @SuppressWarnings("unchecked") + Closure callable = (Closure) args[0]; + callable.setResolveStrategy(Closure.DELEGATE_FIRST); + callable.setDelegate(this); + Object result = callable.call(); + + if (this.callAfterInvocation) { + afterInvocation(); + this.callAfterInvocation = false; + } + return result; + } + else { + StreamingMarkupBuilder builder = new StreamingMarkupBuilder(); + String myNamespace = this.rootNamespace; + Map myNamespaces = this.xmlNamespaces; + + Closure callable = new Closure<>(this) { + @Override + public Object call(Object... arguments) { + ((GroovyObject) getProperty("mkp")).invokeMethod("declareNamespace", new Object[] {myNamespaces}); + int len = args.length; + if (len > 0 && args[len-1] instanceof Closure callable) { + callable.setResolveStrategy(Closure.DELEGATE_FIRST); + callable.setDelegate(builder); + } + return ((GroovyObject) ((GroovyObject) getDelegate()).getProperty(myNamespace)).invokeMethod(name, args); + } + }; + + callable.setResolveStrategy(Closure.DELEGATE_FIRST); + callable.setDelegate(builder); + Writable writable = (Writable) builder.bind(callable); + StringWriter sw = new StringWriter(); + try { + writable.writeTo(sw); + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + + Element element = this.delegate.getReaderContext().readDocumentFromString(sw.toString()).getDocumentElement(); + this.delegate.initDefaults(element); + if (this.decorating) { + BeanDefinitionHolder holder = this.beanDefinition.getBeanDefinitionHolder(); + holder = this.delegate.decorateIfRequired(element, holder, null); + this.beanDefinition.setBeanDefinitionHolder(holder); + } + else { + BeanDefinition beanDefinition = this.delegate.parseCustomElement(element); + if (beanDefinition != null) { + this.beanDefinition.setBeanDefinition((AbstractBeanDefinition) beanDefinition); + } + } + if (this.callAfterInvocation) { + afterInvocation(); + this.callAfterInvocation = false; + } + return element; + } + } + + /** + * Hook that subclasses or anonymous classes can override to implement custom behavior + * after invocation completes. + */ + protected void afterInvocation() { + // NOOP + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/BeanComponentDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/BeanComponentDefinition.java index 32305abbacc3..b21c12a9a28e 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/BeanComponentDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/BeanComponentDefinition.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -36,9 +36,9 @@ */ public class BeanComponentDefinition extends BeanDefinitionHolder implements ComponentDefinition { - private BeanDefinition[] innerBeanDefinitions; + private final BeanDefinition[] innerBeanDefinitions; - private BeanReference[] beanReferences; + private final BeanReference[] beanReferences; /** @@ -124,7 +124,7 @@ public String toString() { } /** - * This implementations expects the other object to be of type BeanComponentDefinition + * This implementation expects the other object to be of type BeanComponentDefinition * as well, in addition to the superclass's equality requirements. */ @Override diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/ComponentDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/ComponentDefinition.java index a89902539c73..33ec279f9c24 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/ComponentDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/ComponentDefinition.java @@ -50,13 +50,13 @@ * {@link #getBeanReferences}, tools may wish to inspect all {@link BeanDefinition BeanDefinitions} to gather * the full set of {@link BeanReference BeanReferences}. Implementations are required to provide * all {@link BeanReference BeanReferences} that are required to validate the configuration of the - * overall logical entity as well as those required to provide full user visualisation of the configuration. + * overall logical entity as well as those required to provide full user visualization of the configuration. * It is expected that certain {@link BeanReference BeanReferences} will not be important to * validation or to the user view of the configuration and as such these may be omitted. A tool may wish to * display any additional {@link BeanReference BeanReferences} sourced through the supplied * {@link BeanDefinition BeanDefinitions} but this is not considered to be a typical case. * - *

    Tools can determine the important of contained {@link BeanDefinition BeanDefinitions} by checking the + *

    Tools can determine the importance of contained {@link BeanDefinition BeanDefinitions} by checking the * {@link BeanDefinition#getRole role identifier}. The role is essentially a hint to the tool as to how * important the configuration provider believes a {@link BeanDefinition} is to the end user. It is expected * that tools will not display all {@link BeanDefinition BeanDefinitions} for a given diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/AbstractServiceLoaderBasedFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/AbstractServiceLoaderBasedFactoryBean.java index 974940a951c8..3ee514663c68 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/AbstractServiceLoaderBasedFactoryBean.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/AbstractServiceLoaderBasedFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-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. @@ -69,7 +69,7 @@ public void setBeanClassLoader(@Nullable ClassLoader beanClassLoader) { */ @Override protected Object createInstance() { - Assert.notNull(getServiceType(), "Property 'serviceType' is required"); + Assert.state(getServiceType() != null, "Property 'serviceType' is required"); return getObjectToExpose(ServiceLoader.load(getServiceType(), this.beanClassLoader)); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/package-info.java b/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/package-info.java index 4e66e56347e4..b6a97c2c7ef6 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/package-info.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/package-info.java @@ -1,5 +1,5 @@ /** - * Support package for the Java 6 ServiceLoader facility. + * Support package for the Java {@link java.util.ServiceLoader} facility. */ @NonNullApi @NonNullFields diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java index 4defe323d461..816619a66b8c 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -21,10 +21,6 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.security.AccessController; -import java.security.PrivilegedAction; -import java.security.PrivilegedActionException; -import java.security.PrivilegedExceptionAction; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -65,6 +61,7 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.ConstructorArgumentValues; import org.springframework.beans.factory.config.DependencyDescriptor; import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor; @@ -77,12 +74,12 @@ import org.springframework.core.PriorityOrdered; import org.springframework.core.ResolvableType; import org.springframework.lang.Nullable; -import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils.MethodCallback; import org.springframework.util.StringUtils; +import org.springframework.util.function.ThrowingSupplier; /** * Abstract bean factory superclass that implements default bean creation, @@ -123,14 +120,6 @@ public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory implements AutowireCapableBeanFactory { - /** - * Whether this environment lives within a native image. - * Exposed as a private static field rather than in a {@code NativeImageDetector.inNativeImage()} static method due to https://github.com/oracle/graal/issues/2594. - * @see ImageInfo.java - */ - private static final boolean IN_NATIVE_IMAGE = (System.getProperty("org.graalvm.nativeimage.imagecode") != null); - - /** Strategy for creating bean instances. */ private InstantiationStrategy instantiationStrategy; @@ -184,12 +173,7 @@ public AbstractAutowireCapableBeanFactory() { ignoreDependencyInterface(BeanNameAware.class); ignoreDependencyInterface(BeanFactoryAware.class); ignoreDependencyInterface(BeanClassLoaderAware.class); - if (IN_NATIVE_IMAGE) { - this.instantiationStrategy = new SimpleInstantiationStrategy(); - } - else { - this.instantiationStrategy = new CglibSubclassingInstantiationStrategy(); - } + this.instantiationStrategy = new CglibSubclassingInstantiationStrategy(); } /** @@ -214,7 +198,7 @@ public void setInstantiationStrategy(InstantiationStrategy instantiationStrategy /** * Return the instantiation strategy to use for creating bean instances. */ - protected InstantiationStrategy getInstantiationStrategy() { + public InstantiationStrategy getInstantiationStrategy() { return this.instantiationStrategy; } @@ -232,7 +216,7 @@ public void setParameterNameDiscoverer(@Nullable ParameterNameDiscoverer paramet * names if needed. */ @Nullable - protected ParameterNameDiscoverer getParameterNameDiscoverer() { + public ParameterNameDiscoverer getParameterNameDiscoverer() { return this.parameterNameDiscoverer; } @@ -253,6 +237,15 @@ public void setAllowCircularReferences(boolean allowCircularReferences) { this.allowCircularReferences = allowCircularReferences; } + /** + * Return whether to allow circular references between beans. + * @since 5.3.10 + * @see #setAllowCircularReferences + */ + public boolean isAllowCircularReferences() { + return this.allowCircularReferences; + } + /** * Set whether to allow the raw injection of a bean instance into some other * bean's property, despite the injected bean eventually getting wrapped @@ -271,6 +264,15 @@ public void setAllowRawInjectionDespiteWrapping(boolean allowRawInjectionDespite this.allowRawInjectionDespiteWrapping = allowRawInjectionDespiteWrapping; } + /** + * Return whether to allow the raw injection of a bean instance. + * @since 5.3.10 + * @see #setAllowRawInjectionDespiteWrapping + */ + public boolean isAllowRawInjectionDespiteWrapping() { + return this.allowRawInjectionDespiteWrapping; + } + /** * Ignore the given dependency type for autowiring: * for example, String. Default is none. @@ -296,9 +298,7 @@ public void ignoreDependencyInterface(Class ifc) { @Override public void copyConfigurationFrom(ConfigurableBeanFactory otherFactory) { super.copyConfigurationFrom(otherFactory); - if (otherFactory instanceof AbstractAutowireCapableBeanFactory) { - AbstractAutowireCapableBeanFactory otherAutowireFactory = - (AbstractAutowireCapableBeanFactory) otherFactory; + if (otherFactory instanceof AbstractAutowireCapableBeanFactory otherAutowireFactory) { this.instantiationStrategy = otherAutowireFactory.instantiationStrategy; this.allowCircularReferences = otherAutowireFactory.allowCircularReferences; this.ignoredDependencyTypes.addAll(otherAutowireFactory.ignoredDependencyTypes); @@ -337,8 +337,7 @@ public Object configureBean(Object existingBean, String beanName) throws BeansEx markBeanAsCreated(beanName); BeanDefinition mbd = getMergedBeanDefinition(beanName); RootBeanDefinition bd = null; - if (mbd instanceof RootBeanDefinition) { - RootBeanDefinition rbd = (RootBeanDefinition) mbd; + if (mbd instanceof RootBeanDefinition rbd) { bd = (rbd.isPrototype() ? rbd : rbd.cloneBeanDefinition()); } if (bd == null) { @@ -376,15 +375,7 @@ public Object autowire(Class beanClass, int autowireMode, boolean dependencyC return autowireConstructor(beanClass.getName(), bd, null, null).getWrappedInstance(); } else { - Object bean; - if (System.getSecurityManager() != null) { - bean = AccessController.doPrivileged( - (PrivilegedAction) () -> getInstantiationStrategy().instantiate(bd, null, this), - getAccessControlContext()); - } - else { - bean = getInstantiationStrategy().instantiate(bd, null, this); - } + Object bean = getInstantiationStrategy().instantiate(bd, null, this); populateBean(beanClass.getName(), bd, new BeanWrapperImpl(bean)); return bean; } @@ -452,8 +443,7 @@ public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, St @Override public void destroyBean(Object existingBean) { - new DisposableBeanAdapter( - existingBean, getBeanPostProcessorCache().destructionAware, getAccessControlContext()).destroy(); + new DisposableBeanAdapter(existingBean, getBeanPostProcessorCache().destructionAware).destroy(); } @@ -586,7 +576,7 @@ protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Post-processing of merged bean definition failed", ex); } - mbd.postProcessed = true; + mbd.markAsPostProcessed(); } } @@ -613,8 +603,7 @@ protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable throw (BeanCreationException) ex; } else { - throw new BeanCreationException( - mbd.getResourceDescription(), beanName, "Initialization of bean failed", ex); + throw new BeanCreationException(mbd.getResourceDescription(), beanName, ex.getMessage(), ex); } } @@ -688,9 +677,15 @@ protected Class predictBeanType(String beanName, RootBeanDefinition mbd, Clas protected Class determineTargetType(String beanName, RootBeanDefinition mbd, Class... typesToMatch) { Class targetType = mbd.getTargetType(); if (targetType == null) { - targetType = (mbd.getFactoryMethodName() != null ? - getTypeForFactoryMethod(beanName, mbd, typesToMatch) : - resolveBeanClass(mbd, beanName, typesToMatch)); + if (mbd.getFactoryMethodName() != null) { + targetType = getTypeForFactoryMethod(beanName, mbd, typesToMatch); + } + else { + targetType = resolveBeanClass(mbd, beanName, typesToMatch); + if (mbd.hasBeanClass()) { + targetType = getInstantiationStrategy().getActualBeanClass(mbd, beanName, this); + } + } if (ObjectUtils.isEmpty(typesToMatch) || getTempClassLoader() == null) { mbd.resolvedTargetType = targetType; } @@ -941,24 +936,6 @@ private ResolvableType getTypeForFactoryBeanFromMethod(Class beanClass, Strin return finder.getResult(); } - /** - * This implementation attempts to query the FactoryBean's generic parameter metadata - * if present to determine the object type. If not present, i.e. the FactoryBean is - * declared as a raw type, checks the FactoryBean's {@code getObjectType} method - * on a plain instance of the FactoryBean, without bean properties applied yet. - * If this doesn't return a type yet, a full creation of the FactoryBean is - * used as fallback (through delegation to the superclass's implementation). - *

    The shortcut check for a FactoryBean is only applied in case of a singleton - * FactoryBean. If the FactoryBean instance itself is not kept as singleton, - * it will be fully created to check the type of its exposed object. - */ - @Override - @Deprecated - @Nullable - protected Class getTypeForFactoryBean(String beanName, RootBeanDefinition mbd) { - return getTypeForFactoryBean(beanName, mbd, true).resolve(); - } - /** * Obtain a reference for early access to the specified bean, * typically for the purpose of resolving a circular reference. @@ -1022,6 +999,11 @@ private FactoryBean getSingletonFactoryBeanForTypeCheck(String beanName, Root throw ex; } catch (BeanCreationException ex) { + // Don't swallow a linkage error since it contains a full stacktrace on + // first occurrence... and just a plain NoClassDefFoundError afterwards. + if (ex.contains(LinkageError.class)) { + throw ex; + } // Instantiation failure, maybe too early... if (logger.isDebugEnabled()) { logger.debug("Bean creation exception on singleton FactoryBean type check: " + ex); @@ -1218,19 +1200,41 @@ protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd /** * Obtain a bean instance from the given supplier. - * @param instanceSupplier the configured supplier + * @param supplier the configured supplier * @param beanName the corresponding bean name * @return a BeanWrapper for the new instance * @since 5.0 * @see #getObjectForBeanInstance */ - protected BeanWrapper obtainFromSupplier(Supplier instanceSupplier, String beanName) { - Object instance; + protected BeanWrapper obtainFromSupplier(Supplier supplier, String beanName) { + Object instance = obtainInstanceFromSupplier(supplier, beanName); + if (instance == null) { + instance = new NullBean(); + } + BeanWrapper bw = new BeanWrapperImpl(instance); + initBeanWrapper(bw); + return bw; + } + @Nullable + private Object obtainInstanceFromSupplier(Supplier supplier, String beanName) { String outerBean = this.currentlyCreatedBean.get(); this.currentlyCreatedBean.set(beanName); try { - instance = instanceSupplier.get(); + if (supplier instanceof InstanceSupplier instanceSupplier) { + return instanceSupplier.get(RegisteredBean.of((ConfigurableListableBeanFactory) this, beanName)); + } + if (supplier instanceof ThrowingSupplier throwableSupplier) { + return throwableSupplier.getWithException(); + } + return supplier.get(); + } + catch (Throwable ex) { + if (ex instanceof BeansException beansException) { + throw beansException; + } + throw new BeanCreationException(beanName, + "Instantiation of supplied bean failed", ex); } finally { if (outerBean != null) { @@ -1240,13 +1244,6 @@ protected BeanWrapper obtainFromSupplier(Supplier instanceSupplier, String be this.currentlyCreatedBean.remove(); } } - - if (instance == null) { - instance = new NullBean(); - } - BeanWrapper bw = new BeanWrapperImpl(instance); - initBeanWrapper(bw); - return bw; } /** @@ -1300,22 +1297,13 @@ protected Constructor[] determineConstructorsFromBeanPostProcessors(@Nullable */ protected BeanWrapper instantiateBean(String beanName, RootBeanDefinition mbd) { try { - Object beanInstance; - if (System.getSecurityManager() != null) { - beanInstance = AccessController.doPrivileged( - (PrivilegedAction) () -> getInstantiationStrategy().instantiate(mbd, beanName, this), - getAccessControlContext()); - } - else { - beanInstance = getInstantiationStrategy().instantiate(mbd, beanName, this); - } + Object beanInstance = getInstantiationStrategy().instantiate(mbd, beanName, this); BeanWrapper bw = new BeanWrapperImpl(beanInstance); initBeanWrapper(bw); return bw; } catch (Throwable ex) { - throw new BeanCreationException( - mbd.getResourceDescription(), beanName, "Instantiation of bean failed", ex); + throw new BeanCreationException(mbd.getResourceDescription(), beanName, ex.getMessage(), ex); } } @@ -1326,7 +1314,7 @@ protected BeanWrapper instantiateBean(String beanName, RootBeanDefinition mbd) { * @param beanName the name of the bean * @param mbd the bean definition for the bean * @param explicitArgs argument values passed in programmatically via the getBean method, - * or {@code null} if none (-> use constructor argument values from bean definition) + * or {@code null} if none (implying the use of constructor argument values from bean definition) * @return a BeanWrapper for the new instance * @see #getBean(String, Object[]) */ @@ -1347,7 +1335,7 @@ protected BeanWrapper instantiateUsingFactoryMethod( * @param mbd the bean definition for the bean * @param ctors the chosen candidate constructors * @param explicitArgs argument values passed in programmatically via the getBean method, - * or {@code null} if none (-> use constructor argument values from bean definition) + * or {@code null} if none (implying the use of constructor argument values from bean definition) * @return a BeanWrapper for the new instance */ protected BeanWrapper autowireConstructor( @@ -1363,7 +1351,6 @@ protected BeanWrapper autowireConstructor( * @param mbd the bean definition for the bean * @param bw the BeanWrapper with bean instance */ - @SuppressWarnings("deprecation") // for postProcessPropertyValues protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) { if (bw == null) { if (mbd.hasPropertyValues()) { @@ -1406,7 +1393,6 @@ protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable B boolean hasInstAwareBpps = hasInstantiationAwareBeanPostProcessors(); boolean needsDepCheck = (mbd.getDependencyCheck() != AbstractBeanDefinition.DEPENDENCY_CHECK_NONE); - PropertyDescriptor[] filteredPds = null; if (hasInstAwareBpps) { if (pvs == null) { pvs = mbd.getPropertyValues(); @@ -1414,21 +1400,13 @@ protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable B for (InstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().instantiationAware) { PropertyValues pvsToUse = bp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName); if (pvsToUse == null) { - if (filteredPds == null) { - filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching); - } - pvsToUse = bp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName); - if (pvsToUse == null) { - return; - } + return; } pvs = pvsToUse; } } if (needsDepCheck) { - if (filteredPds == null) { - filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching); - } + PropertyDescriptor[] filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching); checkDependencies(beanName, mbd, filteredPds, pvs); } @@ -1494,7 +1472,7 @@ protected void autowireByType( try { PropertyDescriptor pd = bw.getPropertyDescriptor(propertyName); // Don't try autowiring by type for type Object: never makes sense, - // even if it technically is a unsatisfied, non-simple property. + // even if it technically is an unsatisfied, non-simple property. if (Object.class != pd.getPropertyType()) { MethodParameter methodParam = BeanUtils.getWriteMethodParameter(pd); // Do not allow eager init for type matching in case of a prioritized post-processor. @@ -1639,10 +1617,6 @@ protected void applyPropertyValues(String beanName, BeanDefinition mbd, BeanWrap return; } - if (System.getSecurityManager() != null && bw instanceof BeanWrapperImpl) { - ((BeanWrapperImpl) bw).setSecurityContext(getAccessControlContext()); - } - MutablePropertyValues mpvs = null; List original; @@ -1724,8 +1698,7 @@ else if (convertible && originalValue instanceof TypedStringValue && bw.setPropertyValues(new MutablePropertyValues(deepCopy)); } catch (BeansException ex) { - throw new BeanCreationException( - mbd.getResourceDescription(), beanName, "Error setting property values", ex); + throw new BeanCreationException(mbd.getResourceDescription(), beanName, ex.getMessage(), ex); } } @@ -1765,15 +1738,7 @@ private Object convertForProperty( * @see #applyBeanPostProcessorsAfterInitialization */ protected Object initializeBean(String beanName, Object bean, @Nullable RootBeanDefinition mbd) { - if (System.getSecurityManager() != null) { - AccessController.doPrivileged((PrivilegedAction) () -> { - invokeAwareMethods(beanName, bean); - return null; - }, getAccessControlContext()); - } - else { - invokeAwareMethods(beanName, bean); - } + invokeAwareMethods(beanName, bean); Object wrappedBean = bean; if (mbd == null || !mbd.isSynthetic()) { @@ -1785,8 +1750,7 @@ protected Object initializeBean(String beanName, Object bean, @Nullable RootBean } catch (Throwable ex) { throw new BeanCreationException( - (mbd != null ? mbd.getResourceDescription() : null), - beanName, "Invocation of init method failed", ex); + (mbd != null ? mbd.getResourceDescription() : null), beanName, ex.getMessage(), ex); } if (mbd == null || !mbd.isSynthetic()) { wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName); @@ -1828,32 +1792,23 @@ protected void invokeInitMethods(String beanName, Object bean, @Nullable RootBea throws Throwable { boolean isInitializingBean = (bean instanceof InitializingBean); - if (isInitializingBean && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) { + if (isInitializingBean && (mbd == null || !mbd.hasAnyExternallyManagedInitMethod("afterPropertiesSet"))) { if (logger.isTraceEnabled()) { logger.trace("Invoking afterPropertiesSet() on bean with name '" + beanName + "'"); } - if (System.getSecurityManager() != null) { - try { - AccessController.doPrivileged((PrivilegedExceptionAction) () -> { - ((InitializingBean) bean).afterPropertiesSet(); - return null; - }, getAccessControlContext()); - } - catch (PrivilegedActionException pae) { - throw pae.getException(); - } - } - else { - ((InitializingBean) bean).afterPropertiesSet(); - } + ((InitializingBean) bean).afterPropertiesSet(); } if (mbd != null && bean.getClass() != NullBean.class) { - String initMethodName = mbd.getInitMethodName(); - if (StringUtils.hasLength(initMethodName) && - !(isInitializingBean && "afterPropertiesSet".equals(initMethodName)) && - !mbd.isExternallyManagedInitMethod(initMethodName)) { - invokeCustomInitMethod(beanName, bean, mbd); + String[] initMethodNames = mbd.getInitMethodNames(); + if (initMethodNames != null) { + for (String initMethodName : initMethodNames) { + if (StringUtils.hasLength(initMethodName) && + !(isInitializingBean && "afterPropertiesSet".equals(initMethodName)) && + !mbd.hasAnyExternallyManagedInitMethod(initMethodName)) { + invokeCustomInitMethod(beanName, bean, mbd, initMethodName); + } + } } } } @@ -1865,11 +1820,9 @@ protected void invokeInitMethods(String beanName, Object bean, @Nullable RootBea * methods with arguments. * @see #invokeInitMethods */ - protected void invokeCustomInitMethod(String beanName, Object bean, RootBeanDefinition mbd) + protected void invokeCustomInitMethod(String beanName, Object bean, RootBeanDefinition mbd, String initMethodName) throws Throwable { - String initMethodName = mbd.getInitMethodName(); - Assert.state(initMethodName != null, "No init method set"); Method initMethod = (mbd.isNonPublicAccessAllowed() ? BeanUtils.findMethod(bean.getClass(), initMethodName) : ClassUtils.getMethodIfAvailable(bean.getClass(), initMethodName)); @@ -1892,30 +1845,14 @@ protected void invokeCustomInitMethod(String beanName, Object bean, RootBeanDefi if (logger.isTraceEnabled()) { logger.trace("Invoking init method '" + initMethodName + "' on bean with name '" + beanName + "'"); } - Method methodToInvoke = ClassUtils.getInterfaceMethodIfPossible(initMethod); + Method methodToInvoke = ClassUtils.getInterfaceMethodIfPossible(initMethod, bean.getClass()); - if (System.getSecurityManager() != null) { - AccessController.doPrivileged((PrivilegedAction) () -> { - ReflectionUtils.makeAccessible(methodToInvoke); - return null; - }); - try { - AccessController.doPrivileged((PrivilegedExceptionAction) - () -> methodToInvoke.invoke(bean), getAccessControlContext()); - } - catch (PrivilegedActionException pae) { - InvocationTargetException ex = (InvocationTargetException) pae.getException(); - throw ex.getTargetException(); - } + try { + ReflectionUtils.makeAccessible(methodToInvoke); + methodToInvoke.invoke(bean); } - else { - try { - ReflectionUtils.makeAccessible(methodToInvoke); - methodToInvoke.invoke(bean); - } - catch (InvocationTargetException ex) { - throw ex.getTargetException(); - } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); } } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java index c30bc3271a12..201e3d27187b 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -184,10 +184,10 @@ public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccess private MethodOverrides methodOverrides = new MethodOverrides(); @Nullable - private String initMethodName; + private String[] initMethodNames; @Nullable - private String destroyMethodName; + private String[] destroyMethodNames; private boolean enforceInitMethod = true; @@ -236,8 +236,7 @@ protected AbstractBeanDefinition(BeanDefinition original) { setSource(original.getSource()); copyAttributesFrom(original); - if (original instanceof AbstractBeanDefinition) { - AbstractBeanDefinition originalAbd = (AbstractBeanDefinition) original; + if (original instanceof AbstractBeanDefinition originalAbd) { if (originalAbd.hasBeanClass()) { setBeanClass(originalAbd.getBeanClass()); } @@ -263,9 +262,9 @@ protected AbstractBeanDefinition(BeanDefinition original) { setInstanceSupplier(originalAbd.getInstanceSupplier()); setNonPublicAccessAllowed(originalAbd.isNonPublicAccessAllowed()); setLenientConstructorResolution(originalAbd.isLenientConstructorResolution()); - setInitMethodName(originalAbd.getInitMethodName()); + setInitMethodNames(originalAbd.getInitMethodNames()); setEnforceInitMethod(originalAbd.isEnforceInitMethod()); - setDestroyMethodName(originalAbd.getDestroyMethodName()); + setDestroyMethodNames(originalAbd.getDestroyMethodNames()); setEnforceDestroyMethod(originalAbd.isEnforceDestroyMethod()); setSynthetic(originalAbd.isSynthetic()); setResource(originalAbd.getResource()); @@ -313,8 +312,7 @@ public void overrideFrom(BeanDefinition other) { setSource(other.getSource()); copyAttributesFrom(other); - if (other instanceof AbstractBeanDefinition) { - AbstractBeanDefinition otherAbd = (AbstractBeanDefinition) other; + if (other instanceof AbstractBeanDefinition otherAbd) { if (otherAbd.hasBeanClass()) { setBeanClass(otherAbd.getBeanClass()); } @@ -340,12 +338,12 @@ public void overrideFrom(BeanDefinition other) { setInstanceSupplier(otherAbd.getInstanceSupplier()); setNonPublicAccessAllowed(otherAbd.isNonPublicAccessAllowed()); setLenientConstructorResolution(otherAbd.isLenientConstructorResolution()); - if (otherAbd.getInitMethodName() != null) { - setInitMethodName(otherAbd.getInitMethodName()); + if (otherAbd.getInitMethodNames() != null) { + setInitMethodNames(otherAbd.getInitMethodNames()); setEnforceInitMethod(otherAbd.isEnforceInitMethod()); } - if (otherAbd.getDestroyMethodName() != null) { - setDestroyMethodName(otherAbd.getDestroyMethodName()); + if (otherAbd.getDestroyMethodNames() != null) { + setDestroyMethodNames(otherAbd.getDestroyMethodNames()); setEnforceDestroyMethod(otherAbd.isEnforceDestroyMethod()); } setSynthetic(otherAbd.isSynthetic()); @@ -392,13 +390,7 @@ public void setBeanClassName(@Nullable String beanClassName) { @Override @Nullable public String getBeanClassName() { - Object beanClassObject = this.beanClass; - if (beanClassObject instanceof Class) { - return ((Class) beanClassObject).getName(); - } - else { - return (String) beanClassObject; - } + return (this.beanClass instanceof Class clazz ? clazz.getName() : (String) this.beanClass); } /** @@ -435,11 +427,11 @@ public Class getBeanClass() throws IllegalStateException { if (beanClassObject == null) { throw new IllegalStateException("No bean class specified on bean definition"); } - if (!(beanClassObject instanceof Class)) { + if (!(beanClassObject instanceof Class clazz)) { throw new IllegalStateException( "Bean class name [" + beanClassObject + "] has not been resolved into an actual Class"); } - return (Class) beanClassObject; + return clazz; } /** @@ -894,7 +886,7 @@ public MutablePropertyValues getPropertyValues() { } /** - * Return if there are property values values defined for this bean. + * Return if there are property values defined for this bean. * @since 5.0.2 */ @Override @@ -926,26 +918,46 @@ public boolean hasMethodOverrides() { return !this.methodOverrides.isEmpty(); } + /** + * Specify the names of multiple initializer methods. + *

    The default is {@code null} in which case there are no initializer methods. + * @since 6.0 + * @see #setInitMethodName + */ + public void setInitMethodNames(@Nullable String... initMethodNames) { + this.initMethodNames = initMethodNames; + } + + /** + * Return the names of the initializer methods. + * @since 6.0 + */ + @Nullable + public String[] getInitMethodNames() { + return this.initMethodNames; + } + /** * Set the name of the initializer method. *

    The default is {@code null} in which case there is no initializer method. + * @see #setInitMethodNames */ @Override public void setInitMethodName(@Nullable String initMethodName) { - this.initMethodName = initMethodName; + this.initMethodNames = (initMethodName != null ? new String[] {initMethodName} : null); } /** - * Return the name of the initializer method. + * Return the name of the initializer method (the first one in case of multiple methods). */ @Override @Nullable public String getInitMethodName() { - return this.initMethodName; + return (!ObjectUtils.isEmpty(this.initMethodNames) ? this.initMethodNames[0] : null); } /** - * Specify whether or not the configured initializer method is the default. + * Specify whether the configured initializer method is the default. *

    The default value is {@code true} for a locally specified init method * but switched to {@code false} for a shared setting in a defaults section * (e.g. {@code bean init-method} versus {@code beans default-init-method} @@ -965,26 +977,46 @@ public boolean isEnforceInitMethod() { return this.enforceInitMethod; } + /** + * Specify the names of multiple destroy methods. + *

    The default is {@code null} in which case there are no destroy methods. + * @since 6.0 + * @see #setDestroyMethodName + */ + public void setDestroyMethodNames(@Nullable String... destroyMethodNames) { + this.destroyMethodNames = destroyMethodNames; + } + + /** + * Return the names of the destroy methods. + * @since 6.0 + */ + @Nullable + public String[] getDestroyMethodNames() { + return this.destroyMethodNames; + } + /** * Set the name of the destroy method. *

    The default is {@code null} in which case there is no destroy method. + * @see #setDestroyMethodNames */ @Override public void setDestroyMethodName(@Nullable String destroyMethodName) { - this.destroyMethodName = destroyMethodName; + this.destroyMethodNames = (destroyMethodName != null ? new String[] {destroyMethodName} : null); } /** - * Return the name of the destroy method. + * Return the name of the destroy method (the first one in case of multiple methods). */ @Override @Nullable public String getDestroyMethodName() { - return this.destroyMethodName; + return (!ObjectUtils.isEmpty(this.destroyMethodNames) ? this.destroyMethodNames[0] : null); } /** - * Specify whether or not the configured destroy method is the default. + * Specify whether the configured destroy method is the default. *

    The default value is {@code true} for a locally specified destroy method * but switched to {@code false} for a shared setting in a defaults section * (e.g. {@code bean destroy-method} versus {@code beans default-destroy-method} @@ -1104,8 +1136,7 @@ public void setOriginatingBeanDefinition(BeanDefinition originatingBd) { @Override @Nullable public BeanDefinition getOriginatingBeanDefinition() { - return (this.resource instanceof BeanDefinitionResource ? - ((BeanDefinitionResource) this.resource).getBeanDefinition() : null); + return (this.resource instanceof BeanDefinitionResource bdr ? bdr.getBeanDefinition() : null); } /** @@ -1178,10 +1209,9 @@ public boolean equals(@Nullable Object other) { if (this == other) { return true; } - if (!(other instanceof AbstractBeanDefinition)) { + if (!(other instanceof AbstractBeanDefinition that)) { return false; } - AbstractBeanDefinition that = (AbstractBeanDefinition) other; return (ObjectUtils.nullSafeEquals(getBeanClassName(), that.getBeanClassName()) && ObjectUtils.nullSafeEquals(this.scope, that.scope) && this.abstractFlag == that.abstractFlag && @@ -1194,26 +1224,44 @@ public boolean equals(@Nullable Object other) { this.primary == that.primary && this.nonPublicAccessAllowed == that.nonPublicAccessAllowed && this.lenientConstructorResolution == that.lenientConstructorResolution && - ObjectUtils.nullSafeEquals(this.constructorArgumentValues, that.constructorArgumentValues) && - ObjectUtils.nullSafeEquals(this.propertyValues, that.propertyValues) && + equalsConstructorArgumentValues(that) && + equalsPropertyValues(that) && ObjectUtils.nullSafeEquals(this.methodOverrides, that.methodOverrides) && ObjectUtils.nullSafeEquals(this.factoryBeanName, that.factoryBeanName) && ObjectUtils.nullSafeEquals(this.factoryMethodName, that.factoryMethodName) && - ObjectUtils.nullSafeEquals(this.initMethodName, that.initMethodName) && + ObjectUtils.nullSafeEquals(this.initMethodNames, that.initMethodNames) && this.enforceInitMethod == that.enforceInitMethod && - ObjectUtils.nullSafeEquals(this.destroyMethodName, that.destroyMethodName) && + ObjectUtils.nullSafeEquals(this.destroyMethodNames, that.destroyMethodNames) && this.enforceDestroyMethod == that.enforceDestroyMethod && this.synthetic == that.synthetic && this.role == that.role && super.equals(other)); } + private boolean equalsConstructorArgumentValues(AbstractBeanDefinition other) { + if (!hasConstructorArgumentValues()) { + return !other.hasConstructorArgumentValues(); + } + return ObjectUtils.nullSafeEquals(this.constructorArgumentValues, other.constructorArgumentValues); + } + + private boolean equalsPropertyValues(AbstractBeanDefinition other) { + if (!hasPropertyValues()) { + return !other.hasPropertyValues(); + } + return ObjectUtils.nullSafeEquals(this.propertyValues, other.propertyValues); + } + @Override public int hashCode() { int hashCode = ObjectUtils.nullSafeHashCode(getBeanClassName()); hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.scope); - hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.constructorArgumentValues); - hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.propertyValues); + if (hasConstructorArgumentValues()) { + hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.constructorArgumentValues); + } + if (hasPropertyValues()) { + hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.propertyValues); + } hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.factoryBeanName); hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.factoryMethodName); hashCode = 29 * hashCode + super.hashCode(); @@ -1223,7 +1271,7 @@ public int hashCode() { @Override public String toString() { StringBuilder sb = new StringBuilder("class ["); - sb.append(getBeanClassName()).append("]"); + sb.append(getBeanClassName()).append(']'); sb.append("; scope=").append(this.scope); sb.append("; abstract=").append(this.abstractFlag); sb.append("; lazyInit=").append(this.lazyInit); @@ -1233,8 +1281,8 @@ public String toString() { sb.append("; primary=").append(this.primary); sb.append("; factoryBeanName=").append(this.factoryBeanName); sb.append("; factoryMethodName=").append(this.factoryMethodName); - sb.append("; initMethodName=").append(this.initMethodName); - sb.append("; destroyMethodName=").append(this.destroyMethodName); + sb.append("; initMethodNames=").append(Arrays.toString(this.initMethodNames)); + sb.append("; destroyMethodNames=").append(Arrays.toString(this.destroyMethodNames)); if (this.resource != null) { sb.append("; defined in ").append(this.resource.getDescription()); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinitionReader.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinitionReader.java index a9a556caea4e..3c23aecf46ef 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinitionReader.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinitionReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -103,10 +103,6 @@ protected AbstractBeanDefinitionReader(BeanDefinitionRegistry registry) { } - public final BeanDefinitionRegistry getBeanFactory() { - return this.registry; - } - @Override public final BeanDefinitionRegistry getRegistry() { return this.registry; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java index 443b7ee586dd..a8bcb4a09dba 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -17,11 +17,6 @@ package org.springframework.beans.factory.support; import java.beans.PropertyEditor; -import java.security.AccessControlContext; -import java.security.AccessController; -import java.security.PrivilegedAction; -import java.security.PrivilegedActionException; -import java.security.PrivilegedExceptionAction; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -112,6 +107,7 @@ * @author Costin Leau * @author Chris Beams * @author Phillip Webb + * @author Sam Brannen * @since 15 April 2001 * @see #getBeanDefinition * @see #createBean @@ -161,15 +157,11 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp /** Cache of pre-filtered post-processors. */ @Nullable - private volatile BeanPostProcessorCache beanPostProcessorCache; + private BeanPostProcessorCache beanPostProcessorCache; /** Map from scope identifier String to corresponding Scope. */ private final Map scopes = new LinkedHashMap<>(8); - /** Security context used when running with a SecurityManager. */ - @Nullable - private SecurityContextProvider securityContextProvider; - /** Map from bean name to merged RootBeanDefinition. */ private final Map mergedBeanDefinitions = new ConcurrentHashMap<>(256); @@ -250,7 +242,7 @@ protected T doGetBean( throws BeansException { String beanName = transformedBeanName(name); - Object bean; + Object beanInstance; // Eagerly check singleton cache for manually registered singletons. Object sharedInstance = getSingleton(beanName); @@ -264,7 +256,7 @@ protected T doGetBean( logger.trace("Returning cached instance of singleton bean '" + beanName + "'"); } } - bean = getObjectForBeanInstance(sharedInstance, name, beanName, null); + beanInstance = getObjectForBeanInstance(sharedInstance, name, beanName, null); } else { @@ -279,9 +271,8 @@ protected T doGetBean( if (parentBeanFactory != null && !containsBeanDefinition(beanName)) { // Not found -> check parent. String nameToLookup = originalBeanName(name); - if (parentBeanFactory instanceof AbstractBeanFactory) { - return ((AbstractBeanFactory) parentBeanFactory).doGetBean( - nameToLookup, requiredType, args, typeCheckOnly); + if (parentBeanFactory instanceof AbstractBeanFactory abf) { + return abf.doGetBean(nameToLookup, requiredType, args, typeCheckOnly); } else if (args != null) { // Delegation to parent with explicit args. @@ -342,7 +333,7 @@ else if (requiredType != null) { throw ex; } }); - bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); + beanInstance = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); } else if (mbd.isPrototype()) { @@ -355,13 +346,13 @@ else if (mbd.isPrototype()) { finally { afterPrototypeCreation(beanName); } - bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd); + beanInstance = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd); } else { String scopeName = mbd.getScope(); if (!StringUtils.hasLength(scopeName)) { - throw new IllegalStateException("No scope name defined for bean ´" + beanName + "'"); + throw new IllegalStateException("No scope name defined for bean '" + beanName + "'"); } Scope scope = this.scopes.get(scopeName); if (scope == null) { @@ -377,7 +368,7 @@ else if (mbd.isPrototype()) { afterPrototypeCreation(beanName); } }); - bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd); + beanInstance = getObjectForBeanInstance(scopedInstance, name, beanName, mbd); } catch (IllegalStateException ex) { throw new ScopeNotActiveException(beanName, scopeName, ex); @@ -395,14 +386,19 @@ else if (mbd.isPrototype()) { } } + return adaptBeanInstance(name, beanInstance, requiredType); + } + + @SuppressWarnings("unchecked") + T adaptBeanInstance(String name, Object bean, @Nullable Class requiredType) { // Check if required type matches the type of the actual bean instance. if (requiredType != null && !requiredType.isInstance(bean)) { try { - T convertedBean = getTypeConverter().convertIfNecessary(bean, requiredType); + Object convertedBean = getTypeConverter().convertIfNecessary(bean, requiredType); if (convertedBean == null) { throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass()); } - return convertedBean; + return (T) convertedBean; } catch (TypeMismatchException ex) { if (logger.isTraceEnabled()) { @@ -432,8 +428,8 @@ public boolean isSingleton(String name) throws NoSuchBeanDefinitionException { Object beanInstance = getSingleton(beanName, false); if (beanInstance != null) { - if (beanInstance instanceof FactoryBean) { - return (BeanFactoryUtils.isFactoryDereference(name) || ((FactoryBean) beanInstance).isSingleton()); + if (beanInstance instanceof FactoryBean factoryBean) { + return (BeanFactoryUtils.isFactoryDereference(name) || factoryBean.isSingleton()); } else { return !BeanFactoryUtils.isFactoryDereference(name); @@ -490,17 +486,8 @@ public boolean isPrototype(String name) throws NoSuchBeanDefinitionException { } if (isFactoryBean(beanName, mbd)) { FactoryBean fb = (FactoryBean) getBean(FACTORY_BEAN_PREFIX + beanName); - if (System.getSecurityManager() != null) { - return AccessController.doPrivileged( - (PrivilegedAction) () -> - ((fb instanceof SmartFactoryBean && ((SmartFactoryBean) fb).isPrototype()) || - !fb.isSingleton()), - getAccessControlContext()); - } - else { - return ((fb instanceof SmartFactoryBean && ((SmartFactoryBean) fb).isPrototype()) || - !fb.isSingleton()); - } + return ((fb instanceof SmartFactoryBean smartFactoryBean && smartFactoryBean.isPrototype()) || + !fb.isSingleton()); } else { return false; @@ -535,9 +522,9 @@ protected boolean isTypeMatch(String name, ResolvableType typeToMatch, boolean a // Check manually registered singletons. Object beanInstance = getSingleton(beanName, false); if (beanInstance != null && beanInstance.getClass() != NullBean.class) { - if (beanInstance instanceof FactoryBean) { + if (beanInstance instanceof FactoryBean factoryBean) { if (!isFactoryDereference) { - Class type = getTypeForFactoryBean((FactoryBean) beanInstance); + Class type = getTypeForFactoryBean(factoryBean); return (type != null && typeToMatch.isAssignableFrom(type)); } else { @@ -588,7 +575,7 @@ else if (containsSingleton(beanName) && !containsBeanDefinition(beanName)) { RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName); BeanDefinitionHolder dbd = mbd.getDecoratedDefinition(); - // Setup the types that we want to match against + // Set up the types that we want to match against Class classToMatch = typeToMatch.resolve(); if (classToMatch == null) { classToMatch = FactoryBean.class; @@ -600,7 +587,7 @@ else if (containsSingleton(beanName) && !containsBeanDefinition(beanName)) { // Attempt to predict the bean type Class predictedType = null; - // We're looking for a regular reference but we're a factory bean that has + // We're looking for a regular reference, but we're a factory bean that has // a decorated bean definition. The target bean should be the same type // as FactoryBean would ultimately return. if (!isFactoryDereference && dbd != null && isFactoryBean(beanName, mbd)) { @@ -638,7 +625,7 @@ else if (containsSingleton(beanName) && !containsBeanDefinition(beanName)) { } else if (isFactoryDereference) { // Special case: A SmartInstantiationAwareBeanPostProcessor returned a non-FactoryBean - // type but we nevertheless are being asked to dereference a FactoryBean... + // type, but we nevertheless are being asked to dereference a FactoryBean... // Let's check the original bean class and proceed with it if it is a FactoryBean. predictedType = predictBeanType(beanName, mbd, FactoryBean.class); if (predictedType == null || !FactoryBean.class.isAssignableFrom(predictedType)) { @@ -686,8 +673,8 @@ public Class getType(String name, boolean allowFactoryBeanInit) throws NoSuch // Check manually registered singletons. Object beanInstance = getSingleton(beanName, false); if (beanInstance != null && beanInstance.getClass() != NullBean.class) { - if (beanInstance instanceof FactoryBean && !BeanFactoryUtils.isFactoryDereference(name)) { - return getTypeForFactoryBean((FactoryBean) beanInstance); + if (beanInstance instanceof FactoryBean factoryBean && !BeanFactoryUtils.isFactoryDereference(name)) { + return getTypeForFactoryBean(factoryBean); } else { return beanInstance.getClass(); @@ -702,33 +689,35 @@ public Class getType(String name, boolean allowFactoryBeanInit) throws NoSuch } RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName); - - // Check decorated bean definition, if any: We assume it'll be easier - // to determine the decorated bean's type than the proxy's type. - BeanDefinitionHolder dbd = mbd.getDecoratedDefinition(); - if (dbd != null && !BeanFactoryUtils.isFactoryDereference(name)) { - RootBeanDefinition tbd = getMergedBeanDefinition(dbd.getBeanName(), dbd.getBeanDefinition(), mbd); - Class targetClass = predictBeanType(dbd.getBeanName(), tbd); - if (targetClass != null && !FactoryBean.class.isAssignableFrom(targetClass)) { - return targetClass; - } - } - Class beanClass = predictBeanType(beanName, mbd); - // Check bean class whether we're dealing with a FactoryBean. - if (beanClass != null && FactoryBean.class.isAssignableFrom(beanClass)) { - if (!BeanFactoryUtils.isFactoryDereference(name)) { - // If it's a FactoryBean, we want to look at what it creates, not at the factory class. - return getTypeForFactoryBean(beanName, mbd, allowFactoryBeanInit).resolve(); + if (beanClass != null) { + // Check bean class whether we're dealing with a FactoryBean. + if (FactoryBean.class.isAssignableFrom(beanClass)) { + if (!BeanFactoryUtils.isFactoryDereference(name)) { + // If it's a FactoryBean, we want to look at what it creates, not at the factory class. + beanClass = getTypeForFactoryBean(beanName, mbd, allowFactoryBeanInit).resolve(); + } } - else { - return beanClass; + else if (BeanFactoryUtils.isFactoryDereference(name)) { + return null; } } - else { - return (!BeanFactoryUtils.isFactoryDereference(name) ? beanClass : null); + + if (beanClass == null) { + // Check decorated bean definition, if any: We assume it'll be easier + // to determine the decorated bean's type than the proxy's type. + BeanDefinitionHolder dbd = mbd.getDecoratedDefinition(); + if (dbd != null && !BeanFactoryUtils.isFactoryDereference(name)) { + RootBeanDefinition tbd = getMergedBeanDefinition(dbd.getBeanName(), dbd.getBeanDefinition(), mbd); + Class targetClass = predictBeanType(dbd.getBeanName(), tbd); + if (targetClass != null && !FactoryBean.class.isAssignableFrom(targetClass)) { + return targetClass; + } + } } + + return beanClass; } @Override @@ -939,10 +928,12 @@ public String resolveEmbeddedValue(@Nullable String value) { @Override public void addBeanPostProcessor(BeanPostProcessor beanPostProcessor) { Assert.notNull(beanPostProcessor, "BeanPostProcessor must not be null"); - // Remove from old position, if any - this.beanPostProcessors.remove(beanPostProcessor); - // Add to end of list - this.beanPostProcessors.add(beanPostProcessor); + synchronized (this.beanPostProcessors) { + // Remove from old position, if any + this.beanPostProcessors.remove(beanPostProcessor); + // Add to end of list + this.beanPostProcessors.add(beanPostProcessor); + } } /** @@ -952,8 +943,12 @@ public void addBeanPostProcessor(BeanPostProcessor beanPostProcessor) { * @see #addBeanPostProcessor */ public void addBeanPostProcessors(Collection beanPostProcessors) { - this.beanPostProcessors.removeAll(beanPostProcessors); - this.beanPostProcessors.addAll(beanPostProcessors); + synchronized (this.beanPostProcessors) { + // Remove from old position, if any + this.beanPostProcessors.removeAll(beanPostProcessors); + // Add to end of list + this.beanPostProcessors.addAll(beanPostProcessors); + } } @Override @@ -975,26 +970,34 @@ public List getBeanPostProcessors() { * @since 5.3 */ BeanPostProcessorCache getBeanPostProcessorCache() { - BeanPostProcessorCache bpCache = this.beanPostProcessorCache; - if (bpCache == null) { - bpCache = new BeanPostProcessorCache(); - for (BeanPostProcessor bp : this.beanPostProcessors) { - if (bp instanceof InstantiationAwareBeanPostProcessor) { - bpCache.instantiationAware.add((InstantiationAwareBeanPostProcessor) bp); - if (bp instanceof SmartInstantiationAwareBeanPostProcessor) { - bpCache.smartInstantiationAware.add((SmartInstantiationAwareBeanPostProcessor) bp); + synchronized (this.beanPostProcessors) { + BeanPostProcessorCache bppCache = this.beanPostProcessorCache; + if (bppCache == null) { + bppCache = new BeanPostProcessorCache(); + for (BeanPostProcessor bpp : this.beanPostProcessors) { + if (bpp instanceof InstantiationAwareBeanPostProcessor instantiationAwareBpp) { + bppCache.instantiationAware.add(instantiationAwareBpp); + if (bpp instanceof SmartInstantiationAwareBeanPostProcessor smartInstantiationAwareBpp) { + bppCache.smartInstantiationAware.add(smartInstantiationAwareBpp); + } + } + if (bpp instanceof DestructionAwareBeanPostProcessor destructionAwareBpp) { + bppCache.destructionAware.add(destructionAwareBpp); + } + if (bpp instanceof MergedBeanDefinitionPostProcessor mergedBeanDefBpp) { + bppCache.mergedDefinition.add(mergedBeanDefBpp); } } - if (bp instanceof DestructionAwareBeanPostProcessor) { - bpCache.destructionAware.add((DestructionAwareBeanPostProcessor) bp); - } - if (bp instanceof MergedBeanDefinitionPostProcessor) { - bpCache.mergedDefinition.add((MergedBeanDefinitionPostProcessor) bp); - } + this.beanPostProcessorCache = bppCache; } - this.beanPostProcessorCache = bpCache; + return bppCache; + } + } + + private void resetBeanPostProcessorCache() { + synchronized (this.beanPostProcessors) { + this.beanPostProcessorCache = null; } - return bpCache; } /** @@ -1049,18 +1052,9 @@ public Scope getRegisteredScope(String scopeName) { return this.scopes.get(scopeName); } - /** - * Set the security context provider for this bean factory. If a security manager - * is set, interaction with the user code will be executed using the privileged - * of the provided security context. - */ - public void setSecurityContextProvider(SecurityContextProvider securityProvider) { - this.securityContextProvider = securityProvider; - } - @Override public void setApplicationStartup(ApplicationStartup applicationStartup) { - Assert.notNull(applicationStartup, "applicationStartup should not be null"); + Assert.notNull(applicationStartup, "applicationStartup must not be null"); this.applicationStartup = applicationStartup; } @@ -1069,17 +1063,6 @@ public ApplicationStartup getApplicationStartup() { return this.applicationStartup; } - /** - * Delegate the creation of the access control context to the - * {@link #setSecurityContextProvider SecurityContextProvider}. - */ - @Override - public AccessControlContext getAccessControlContext() { - return (this.securityContextProvider != null ? - this.securityContextProvider.getAccessControlContext() : - AccessController.getContext()); - } - @Override public void copyConfigurationFrom(ConfigurableBeanFactory otherFactory) { Assert.notNull(otherFactory, "BeanFactory must not be null"); @@ -1087,14 +1070,12 @@ public void copyConfigurationFrom(ConfigurableBeanFactory otherFactory) { setCacheBeanMetadata(otherFactory.isCacheBeanMetadata()); setBeanExpressionResolver(otherFactory.getBeanExpressionResolver()); setConversionService(otherFactory.getConversionService()); - if (otherFactory instanceof AbstractBeanFactory) { - AbstractBeanFactory otherAbstractFactory = (AbstractBeanFactory) otherFactory; + if (otherFactory instanceof AbstractBeanFactory otherAbstractFactory) { this.propertyEditorRegistrars.addAll(otherAbstractFactory.propertyEditorRegistrars); this.customEditors.putAll(otherAbstractFactory.customEditors); this.typeConverter = otherAbstractFactory.typeConverter; this.beanPostProcessors.addAll(otherAbstractFactory.beanPostProcessors); this.scopes.putAll(otherAbstractFactory.scopes); - this.securityContextProvider = otherAbstractFactory.securityContextProvider; } else { setTypeConverter(otherFactory.getTypeConverter()); @@ -1120,8 +1101,8 @@ public void copyConfigurationFrom(ConfigurableBeanFactory otherFactory) { public BeanDefinition getMergedBeanDefinition(String name) throws BeansException { String beanName = transformedBeanName(name); // Efficiently check whether bean definition exists in this factory. - if (!containsBeanDefinition(beanName) && getParentBeanFactory() instanceof ConfigurableBeanFactory) { - return ((ConfigurableBeanFactory) getParentBeanFactory()).getMergedBeanDefinition(beanName); + if (!containsBeanDefinition(beanName) && getParentBeanFactory() instanceof ConfigurableBeanFactory parent) { + return parent.getMergedBeanDefinition(beanName); } // Resolve merged bean definition locally. return getMergedLocalBeanDefinition(beanName); @@ -1135,9 +1116,9 @@ public boolean isFactoryBean(String name) throws NoSuchBeanDefinitionException { return (beanInstance instanceof FactoryBean); } // No singleton instance found -> check bean definition. - if (!containsBeanDefinition(beanName) && getParentBeanFactory() instanceof ConfigurableBeanFactory) { + if (!containsBeanDefinition(beanName) && getParentBeanFactory() instanceof ConfigurableBeanFactory cbf) { // No bean definition found in this factory -> delegate to parent. - return ((ConfigurableBeanFactory) getParentBeanFactory()).isFactoryBean(name); + return cbf.isFactoryBean(name); } return isFactoryBean(beanName, getMergedLocalBeanDefinition(beanName)); } @@ -1155,12 +1136,12 @@ public boolean isActuallyInCreation(String beanName) { protected boolean isPrototypeCurrentlyInCreation(String beanName) { Object curVal = this.prototypesCurrentlyInCreation.get(); return (curVal != null && - (curVal.equals(beanName) || (curVal instanceof Set && ((Set) curVal).contains(beanName)))); + (curVal.equals(beanName) || (curVal instanceof Set set && set.contains(beanName)))); } /** * Callback before prototype creation. - *

    The default implementation register the prototype as currently in creation. + *

    The default implementation registers the prototype as currently in creation. * @param beanName the name of the prototype about to be created * @see #isPrototypeCurrentlyInCreation */ @@ -1170,9 +1151,9 @@ protected void beforePrototypeCreation(String beanName) { if (curVal == null) { this.prototypesCurrentlyInCreation.set(beanName); } - else if (curVal instanceof String) { + else if (curVal instanceof String strValue) { Set beanNameSet = new HashSet<>(2); - beanNameSet.add((String) curVal); + beanNameSet.add(strValue); beanNameSet.add(beanName); this.prototypesCurrentlyInCreation.set(beanNameSet); } @@ -1194,8 +1175,7 @@ protected void afterPrototypeCreation(String beanName) { if (curVal instanceof String) { this.prototypesCurrentlyInCreation.remove(); } - else if (curVal instanceof Set) { - Set beanNameSet = (Set) curVal; + else if (curVal instanceof Set beanNameSet) { beanNameSet.remove(beanName); if (beanNameSet.isEmpty()) { this.prototypesCurrentlyInCreation.remove(); @@ -1217,7 +1197,7 @@ public void destroyBean(String beanName, Object beanInstance) { */ protected void destroyBean(String beanName, Object bean, RootBeanDefinition mbd) { new DisposableBeanAdapter( - bean, beanName, mbd, getBeanPostProcessorCache().destructionAware, getAccessControlContext()).destroy(); + bean, beanName, mbd, getBeanPostProcessorCache().destructionAware).destroy(); } @Override @@ -1288,8 +1268,8 @@ protected void initBeanWrapper(BeanWrapper bw) { * @param registry the PropertyEditorRegistry to initialize */ protected void registerCustomEditors(PropertyEditorRegistry registry) { - if (registry instanceof PropertyEditorRegistrySupport) { - ((PropertyEditorRegistrySupport) registry).useConfigValueEditors(); + if (registry instanceof PropertyEditorRegistrySupport registrySupport) { + registrySupport.useConfigValueEditors(); } if (!this.propertyEditorRegistrars.isEmpty()) { for (PropertyEditorRegistrar registrar : this.propertyEditorRegistrars) { @@ -1298,8 +1278,7 @@ protected void registerCustomEditors(PropertyEditorRegistry registry) { } catch (BeanCreationException ex) { Throwable rootCause = ex.getMostSpecificCause(); - if (rootCause instanceof BeanCurrentlyInCreationException) { - BeanCreationException bce = (BeanCreationException) rootCause; + if (rootCause instanceof BeanCurrentlyInCreationException bce) { String bceBeanName = bce.getBeanName(); if (bceBeanName != null && isCurrentlyInCreation(bceBeanName)) { if (logger.isDebugEnabled()) { @@ -1380,8 +1359,8 @@ protected RootBeanDefinition getMergedBeanDefinition( previous = mbd; if (bd.getParentName() == null) { // Use copy of given root bean definition. - if (bd instanceof RootBeanDefinition) { - mbd = ((RootBeanDefinition) bd).cloneBeanDefinition(); + if (bd instanceof RootBeanDefinition rootBeanDef) { + mbd = rootBeanDef.cloneBeanDefinition(); } else { mbd = new RootBeanDefinition(bd); @@ -1396,9 +1375,8 @@ protected RootBeanDefinition getMergedBeanDefinition( pbd = getMergedBeanDefinition(parentBeanName); } else { - BeanFactory parent = getParentBeanFactory(); - if (parent instanceof ConfigurableBeanFactory) { - pbd = ((ConfigurableBeanFactory) parent).getMergedBeanDefinition(parentBeanName); + if (getParentBeanFactory() instanceof ConfigurableBeanFactory parent) { + pbd = parent.getMergedBeanDefinition(parentBeanName); } else { throw new NoSuchBeanDefinitionException(parentBeanName, @@ -1521,17 +1499,7 @@ protected Class resolveBeanClass(RootBeanDefinition mbd, String beanName, Cla if (mbd.hasBeanClass()) { return mbd.getBeanClass(); } - if (System.getSecurityManager() != null) { - return AccessController.doPrivileged((PrivilegedExceptionAction>) - () -> doResolveBeanClass(mbd, typesToMatch), getAccessControlContext()); - } - else { - return doResolveBeanClass(mbd, typesToMatch); - } - } - catch (PrivilegedActionException pae) { - ClassNotFoundException ex = (ClassNotFoundException) pae.getException(); - throw new CannotLoadBeanClassException(mbd.getResourceDescription(), beanName, mbd.getBeanClassName(), ex); + return doResolveBeanClass(mbd, typesToMatch); } catch (ClassNotFoundException ex) { throw new CannotLoadBeanClassException(mbd.getResourceDescription(), beanName, mbd.getBeanClassName(), ex); @@ -1556,8 +1524,7 @@ private Class doResolveBeanClass(RootBeanDefinition mbd, Class... typesToM if (tempClassLoader != null) { dynamicLoader = tempClassLoader; freshResolve = true; - if (tempClassLoader instanceof DecoratingClassLoader) { - DecoratingClassLoader dcl = (DecoratingClassLoader) tempClassLoader; + if (tempClassLoader instanceof DecoratingClassLoader dcl) { for (Class typeToMatch : typesToMatch) { dcl.excludeClass(typeToMatch.getName()); } @@ -1570,11 +1537,11 @@ private Class doResolveBeanClass(RootBeanDefinition mbd, Class... typesToM Object evaluated = evaluateBeanDefinitionString(className, mbd); if (!className.equals(evaluated)) { // A dynamically resolved expression, supported as of 4.2... - if (evaluated instanceof Class) { - return (Class) evaluated; + if (evaluated instanceof Class clazz) { + return clazz; } - else if (evaluated instanceof String) { - className = (String) evaluated; + else if (evaluated instanceof String str) { + className = str; freshResolve = true; } else { @@ -1731,37 +1698,15 @@ else if (mbd.isLazyInit()) { */ ResolvableType getTypeForFactoryBeanFromAttributes(AttributeAccessor attributes) { Object attribute = attributes.getAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE); - if (attribute instanceof ResolvableType) { - return (ResolvableType) attribute; + if (attribute instanceof ResolvableType resolvableType) { + return resolvableType; } - if (attribute instanceof Class) { - return ResolvableType.forClass((Class) attribute); + if (attribute instanceof Class clazz) { + return ResolvableType.forClass(clazz); } return ResolvableType.NONE; } - /** - * Determine the bean type for the given FactoryBean definition, as far as possible. - * Only called if there is no singleton instance registered for the target bean already. - *

    The default implementation creates the FactoryBean via {@code getBean} - * to call its {@code getObjectType} method. Subclasses are encouraged to optimize - * this, typically by just instantiating the FactoryBean but not populating it yet, - * trying whether its {@code getObjectType} method already returns a type. - * If no type found, a full FactoryBean creation as performed by this implementation - * should be used as fallback. - * @param beanName the name of the bean - * @param mbd the merged bean definition for the bean - * @return the type for the bean if determinable, or {@code null} otherwise - * @see org.springframework.beans.factory.FactoryBean#getObjectType() - * @see #getBean(String) - * @deprecated since 5.2 in favor of {@link #getTypeForFactoryBean(String, RootBeanDefinition, boolean)} - */ - @Nullable - @Deprecated - protected Class getTypeForFactoryBean(String beanName, RootBeanDefinition mbd) { - return getTypeForFactoryBean(beanName, mbd, true).resolve(); - } - /** * Mark the specified bean as already created (or about to be created). *

    This allows the bean factory to optimize its caching for repeated @@ -1771,12 +1716,12 @@ protected Class getTypeForFactoryBean(String beanName, RootBeanDefinition mbd protected void markBeanAsCreated(String beanName) { if (!this.alreadyCreated.contains(beanName)) { synchronized (this.mergedBeanDefinitions) { - if (!this.alreadyCreated.contains(beanName)) { + if (!isBeanEligibleForMetadataCaching(beanName)) { // Let the bean definition get re-merged now that we're actually creating // the bean... just in case some of its metadata changed in the meantime. clearMergedBeanDefinition(beanName); - this.alreadyCreated.add(beanName); } + this.alreadyCreated.add(beanName); } } } @@ -1857,7 +1802,7 @@ protected Object getObjectForBeanInstance( // Now we have the bean instance, which may be a normal bean or a FactoryBean. // If it's a FactoryBean, we use it to create a bean instance, unless the // caller actually wants a reference to the factory. - if (!(beanInstance instanceof FactoryBean)) { + if (!(beanInstance instanceof FactoryBean factoryBean)) { return beanInstance; } @@ -1870,13 +1815,12 @@ protected Object getObjectForBeanInstance( } if (object == null) { // Return bean instance from factory. - FactoryBean factory = (FactoryBean) beanInstance; // Caches object obtained from FactoryBean if it is a singleton. if (mbd == null && containsBeanDefinition(beanName)) { mbd = getMergedLocalBeanDefinition(beanName); } boolean synthetic = (mbd != null && mbd.isSynthetic()); - object = getObjectFromFactoryBean(factory, beanName, !synthetic); + object = getObjectFromFactoryBean(factoryBean, beanName, !synthetic); } return object; } @@ -1920,14 +1864,13 @@ protected boolean requiresDestruction(Object bean, RootBeanDefinition mbd) { * @see #registerDependentBean */ protected void registerDisposableBeanIfNecessary(String beanName, Object bean, RootBeanDefinition mbd) { - AccessControlContext acc = (System.getSecurityManager() != null ? getAccessControlContext() : null); if (!mbd.isPrototype() && requiresDestruction(bean, mbd)) { if (mbd.isSingleton()) { // Register a DisposableBean implementation that performs all destruction // work for the given bean: DestructionAwareBeanPostProcessors, // DisposableBean interface, custom destroy method. registerDisposableBean(beanName, new DisposableBeanAdapter( - bean, beanName, mbd, getBeanPostProcessorCache().destructionAware, acc)); + bean, beanName, mbd, getBeanPostProcessorCache().destructionAware)); } else { // A bean with a custom scope... @@ -1936,7 +1879,7 @@ protected void registerDisposableBeanIfNecessary(String beanName, Object bean, R throw new IllegalStateException("No Scope registered for scope name '" + mbd.getScope() + "'"); } scope.registerDestructionCallback(beanName, new DisposableBeanAdapter( - bean, beanName, mbd, getBeanPostProcessorCache().destructionAware, acc)); + bean, beanName, mbd, getBeanPostProcessorCache().destructionAware)); } } } @@ -2004,32 +1947,33 @@ protected abstract Object createBean(String beanName, RootBeanDefinition mbd, @N * * @since 5.3 */ + @SuppressWarnings("serial") private class BeanPostProcessorCacheAwareList extends CopyOnWriteArrayList { @Override public BeanPostProcessor set(int index, BeanPostProcessor element) { BeanPostProcessor result = super.set(index, element); - beanPostProcessorCache = null; + resetBeanPostProcessorCache(); return result; } @Override public boolean add(BeanPostProcessor o) { boolean success = super.add(o); - beanPostProcessorCache = null; + resetBeanPostProcessorCache(); return success; } @Override public void add(int index, BeanPostProcessor element) { super.add(index, element); - beanPostProcessorCache = null; + resetBeanPostProcessorCache(); } @Override public BeanPostProcessor remove(int index) { BeanPostProcessor result = super.remove(index); - beanPostProcessorCache = null; + resetBeanPostProcessorCache(); return result; } @@ -2037,7 +1981,7 @@ public BeanPostProcessor remove(int index) { public boolean remove(Object o) { boolean success = super.remove(o); if (success) { - beanPostProcessorCache = null; + resetBeanPostProcessorCache(); } return success; } @@ -2046,7 +1990,7 @@ public boolean remove(Object o) { public boolean removeAll(Collection c) { boolean success = super.removeAll(c); if (success) { - beanPostProcessorCache = null; + resetBeanPostProcessorCache(); } return success; } @@ -2055,7 +1999,7 @@ public boolean removeAll(Collection c) { public boolean retainAll(Collection c) { boolean success = super.retainAll(c); if (success) { - beanPostProcessorCache = null; + resetBeanPostProcessorCache(); } return success; } @@ -2064,7 +2008,7 @@ public boolean retainAll(Collection c) { public boolean addAll(Collection c) { boolean success = super.addAll(c); if (success) { - beanPostProcessorCache = null; + resetBeanPostProcessorCache(); } return success; } @@ -2073,7 +2017,7 @@ public boolean addAll(Collection c) { public boolean addAll(int index, Collection c) { boolean success = super.addAll(index, c); if (success) { - beanPostProcessorCache = null; + resetBeanPostProcessorCache(); } return success; } @@ -2082,7 +2026,7 @@ public boolean addAll(int index, Collection c) { public boolean removeIf(Predicate filter) { boolean success = super.removeIf(filter); if (success) { - beanPostProcessorCache = null; + resetBeanPostProcessorCache(); } return success; } @@ -2090,7 +2034,7 @@ public boolean removeIf(Predicate filter) { @Override public void replaceAll(UnaryOperator operator) { super.replaceAll(operator); - beanPostProcessorCache = null; + resetBeanPostProcessorCache(); } } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AutowireCandidateResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AutowireCandidateResolver.java index 0a1f0a7d6910..e7eafe4158c8 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AutowireCandidateResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AutowireCandidateResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -100,6 +100,20 @@ default Object getLazyResolutionProxyIfNecessary(DependencyDescriptor descriptor return null; } + /** + * Determine the proxy class for lazy resolution of the dependency target, + * if demanded by the injection point. + *

    The default implementation simply returns {@code null}. + * @param descriptor the descriptor for the target method parameter or field + * @param beanName the name of the bean that contains the injection point + * @return the lazy resolution proxy class for the dependency target, if any + * @since 6.0 + */ + @Nullable + default Class getLazyResolutionProxyClass(DependencyDescriptor descriptor, @Nullable String beanName) { + return null; + } + /** * Return a clone of this resolver instance if necessary, retaining its local * configuration and allowing for the cloned instance to get associated with diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AutowireUtils.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AutowireUtils.java index d807cc4dc2fe..c91508cb1810 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AutowireUtils.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AutowireUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -198,8 +198,7 @@ public static Class resolveReturnTypeForFactoryMethod( Type methodParameterType = methodParameterTypes[i]; Object arg = args[i]; if (methodParameterType.equals(genericReturnType)) { - if (arg instanceof TypedStringValue) { - TypedStringValue typedValue = ((TypedStringValue) arg); + if (arg instanceof TypedStringValue typedValue) { if (typedValue.hasTargetType()) { return typedValue.getTargetType(); } @@ -220,8 +219,7 @@ else if (arg != null && !(arg instanceof BeanMetadataElement)) { } return method.getReturnType(); } - else if (methodParameterType instanceof ParameterizedType) { - ParameterizedType parameterizedType = (ParameterizedType) methodParameterType; + else if (methodParameterType instanceof ParameterizedType parameterizedType) { Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); for (Type typeArg : actualTypeArguments) { if (typeArg.equals(genericReturnType)) { @@ -233,8 +231,7 @@ else if (methodParameterType instanceof ParameterizedType) { if (arg instanceof String) { className = (String) arg; } - else if (arg instanceof TypedStringValue) { - TypedStringValue typedValue = ((TypedStringValue) arg); + else if (arg instanceof TypedStringValue typedValue) { String targetTypeName = typedValue.getTargetTypeName(); if (targetTypeName == null || Class.class.getName().equals(targetTypeName)) { className = typedValue.getValue(); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionBuilder.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionBuilder.java index dab55e957daa..7e936b9e65b2 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionBuilder.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -21,6 +21,7 @@ import org.springframework.beans.factory.config.AutowiredPropertyMarker; import org.springframework.beans.factory.config.BeanDefinitionCustomizer; import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.core.ResolvableType; import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; @@ -102,7 +103,7 @@ public static BeanDefinitionBuilder rootBeanDefinition(String beanClassName, @Nu * @param beanClass the {@code Class} of the bean that the definition is being created for */ public static BeanDefinitionBuilder rootBeanDefinition(Class beanClass) { - return rootBeanDefinition(beanClass, null); + return rootBeanDefinition(beanClass, (String) null); } /** @@ -117,6 +118,29 @@ public static BeanDefinitionBuilder rootBeanDefinition(Class beanClass, @Null return builder; } + /** + * Create a new {@code BeanDefinitionBuilder} used to construct a {@link RootBeanDefinition}. + * @param beanType the {@link ResolvableType type} of the bean that the definition is being created for + * @param instanceSupplier a callback for creating an instance of the bean + * @since 5.3.9 + */ + public static BeanDefinitionBuilder rootBeanDefinition(ResolvableType beanType, Supplier instanceSupplier) { + RootBeanDefinition beanDefinition = new RootBeanDefinition(beanType); + beanDefinition.setInstanceSupplier(instanceSupplier); + return new BeanDefinitionBuilder(beanDefinition); + } + + /** + * Create a new {@code BeanDefinitionBuilder} used to construct a {@link RootBeanDefinition}. + * @param beanClass the {@code Class} of the bean that the definition is being created for + * @param instanceSupplier a callback for creating an instance of the bean + * @since 5.3.9 + * @see #rootBeanDefinition(ResolvableType, Supplier) + */ + public static BeanDefinitionBuilder rootBeanDefinition(Class beanClass, Supplier instanceSupplier) { + return rootBeanDefinition(ResolvableType.forClass(beanClass), instanceSupplier); + } + /** * Create a new {@code BeanDefinitionBuilder} used to construct a {@link ChildBeanDefinition}. * @param parentName the name of the parent bean @@ -268,7 +292,7 @@ public BeanDefinitionBuilder setScope(@Nullable String scope) { } /** - * Set whether or not this definition is abstract. + * Set whether this definition is abstract. */ public BeanDefinitionBuilder setAbstract(boolean flag) { this.beanDefinition.setAbstract(flag); @@ -331,6 +355,16 @@ public BeanDefinitionBuilder setRole(int role) { return this; } + /** + * Set whether this bean is 'synthetic', that is, not defined by + * the application itself. + * @since 5.3.9 + */ + public BeanDefinitionBuilder setSynthetic(boolean synthetic) { + this.beanDefinition.setSynthetic(synthetic); + return this; + } + /** * Apply the given customizers to the underlying bean definition. * @since 5.0 diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionOverrideException.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionOverrideException.java index c2c6281b7f94..f894298b151a 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionOverrideException.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionOverrideException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -49,7 +49,7 @@ public BeanDefinitionOverrideException( super(beanDefinition.getResourceDescription(), beanName, "Cannot register bean definition [" + beanDefinition + "] for bean '" + beanName + - "': There is already [" + existingDefinition + "] bound."); + "' since there is already [" + existingDefinition + "] bound."); this.beanDefinition = beanDefinition; this.existingDefinition = existingDefinition; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionReader.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionReader.java index a6fedff03795..8441abbc7664 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionReader.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-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. @@ -22,15 +22,15 @@ import org.springframework.lang.Nullable; /** - * Simple interface for bean definition readers. - * Specifies load methods with Resource and String location parameters. + * Simple interface for bean definition readers that specifies load methods with + * {@link Resource} and {@link String} location parameters. * *

    Concrete bean definition readers can of course add additional * load and register methods for bean definitions, specific to * their bean definition format. * *

    Note that a bean definition reader does not have to implement - * this interface. It only serves as suggestion for bean definition + * this interface. It only serves as a suggestion for bean definition * readers that want to follow standard naming conventions. * * @author Juergen Hoeller @@ -41,14 +41,14 @@ public interface BeanDefinitionReader { /** * Return the bean factory to register the bean definitions with. - *

    The factory is exposed through the BeanDefinitionRegistry interface, + *

    The factory is exposed through the {@link BeanDefinitionRegistry} interface, * encapsulating the methods that are relevant for bean definition handling. */ BeanDefinitionRegistry getRegistry(); /** - * Return the resource loader to use for resource locations. - * Can be checked for the ResourcePatternResolver interface and cast + * Return the {@link ResourceLoader} to use for resource locations. + *

    Can be checked for the {@code ResourcePatternResolver} interface and cast * accordingly, for loading multiple resources for a given resource pattern. *

    A {@code null} return value suggests that absolute resource loading * is not available for this bean definition reader. @@ -56,10 +56,10 @@ public interface BeanDefinitionReader { * from within a bean definition resource, for example via the "import" * tag in XML bean definitions. It is recommended, however, to apply * such imports relative to the defining resource; only explicit full - * resource locations will trigger absolute resource loading. + * resource locations will trigger absolute path based resource loading. *

    There is also a {@code loadBeanDefinitions(String)} method available, * for loading bean definitions from a resource location (or location pattern). - * This is a convenience to avoid explicit ResourceLoader handling. + * This is a convenience to avoid explicit {@code ResourceLoader} handling. * @see #loadBeanDefinitions(String) * @see org.springframework.core.io.support.ResourcePatternResolver */ @@ -70,13 +70,13 @@ public interface BeanDefinitionReader { * Return the class loader to use for bean classes. *

    {@code null} suggests to not load bean classes eagerly * but rather to just register bean definitions with class names, - * with the corresponding Classes to be resolved later (or never). + * with the corresponding classes to be resolved later (or never). */ @Nullable ClassLoader getBeanClassLoader(); /** - * Return the BeanNameGenerator to use for anonymous beans + * Return the {@link BeanNameGenerator} to use for anonymous beans * (without explicit bean name specified). */ BeanNameGenerator getBeanNameGenerator(); @@ -101,9 +101,10 @@ public interface BeanDefinitionReader { /** * Load bean definitions from the specified resource location. *

    The location can also be a location pattern, provided that the - * ResourceLoader of this bean definition reader is a ResourcePatternResolver. - * @param location the resource location, to be loaded with the ResourceLoader - * (or ResourcePatternResolver) of this bean definition reader + * {@link ResourceLoader} of this bean definition reader is a + * {@code ResourcePatternResolver}. + * @param location the resource location, to be loaded with the {@code ResourceLoader} + * (or {@code ResourcePatternResolver}) of this bean definition reader * @return the number of bean definitions found * @throws BeanDefinitionStoreException in case of loading or parsing errors * @see #getResourceLoader() @@ -114,8 +115,8 @@ public interface BeanDefinitionReader { /** * Load bean definitions from the specified resource locations. - * @param locations the resource locations, to be loaded with the ResourceLoader - * (or ResourcePatternResolver) of this bean definition reader + * @param locations the resource locations, to be loaded with the {@code ResourceLoader} + * (or {@code ResourcePatternResolver}) of this bean definition reader * @return the number of bean definitions found * @throws BeanDefinitionStoreException in case of loading or parsing errors */ diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionValueResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionValueResolver.java index be9667b19a38..62a660a873b4 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionValueResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionValueResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -23,8 +23,10 @@ import java.util.Map; import java.util.Properties; import java.util.Set; +import java.util.function.BiFunction; import org.springframework.beans.BeanWrapper; +import org.springframework.beans.BeanWrapperImpl; import org.springframework.beans.BeansException; import org.springframework.beans.TypeConverter; import org.springframework.beans.factory.BeanCreationException; @@ -55,10 +57,12 @@ * Used by {@link AbstractAutowireCapableBeanFactory}. * * @author Juergen Hoeller + * @author Sam Brannen + * @author Stephane Nicoll * @since 1.2 * @see AbstractAutowireCapableBeanFactory */ -class BeanDefinitionValueResolver { +public class BeanDefinitionValueResolver { private final AbstractAutowireCapableBeanFactory beanFactory; @@ -70,7 +74,8 @@ class BeanDefinitionValueResolver { /** - * Create a BeanDefinitionValueResolver for the given BeanFactory and BeanDefinition. + * Create a BeanDefinitionValueResolver for the given BeanFactory and BeanDefinition, + * using the given {@link TypeConverter}. * @param beanFactory the BeanFactory to resolve against * @param beanName the name of the bean that we work on * @param beanDefinition the BeanDefinition of the bean that we work on @@ -85,6 +90,24 @@ public BeanDefinitionValueResolver(AbstractAutowireCapableBeanFactory beanFactor this.typeConverter = typeConverter; } + /** + * Create a BeanDefinitionValueResolver for the given BeanFactory and BeanDefinition + * using a default {@link TypeConverter}. + * @param beanFactory the BeanFactory to resolve against + * @param beanName the name of the bean that we work on + * @param beanDefinition the BeanDefinition of the bean that we work on + */ + public BeanDefinitionValueResolver(AbstractAutowireCapableBeanFactory beanFactory, String beanName, + BeanDefinition beanDefinition) { + + this.beanFactory = beanFactory; + this.beanName = beanName; + this.beanDefinition = beanDefinition; + BeanWrapper beanWrapper = new BeanWrapperImpl(); + beanFactory.initBeanWrapper(beanWrapper); + this.typeConverter = beanWrapper; + } + /** * Given a PropertyValue, return a value, resolving any references to other @@ -108,12 +131,11 @@ public BeanDefinitionValueResolver(AbstractAutowireCapableBeanFactory beanFactor public Object resolveValueIfNecessary(Object argName, @Nullable Object value) { // We must check each value to see whether it requires a runtime reference // to another bean to be resolved. - if (value instanceof RuntimeBeanReference) { - RuntimeBeanReference ref = (RuntimeBeanReference) value; + if (value instanceof RuntimeBeanReference ref) { return resolveReference(argName, ref); } - else if (value instanceof RuntimeBeanNameReference) { - String refName = ((RuntimeBeanNameReference) value).getBeanName(); + else if (value instanceof RuntimeBeanNameReference ref) { + String refName = ref.getBeanName(); refName = String.valueOf(doEvaluate(refName)); if (!this.beanFactory.containsBean(refName)) { throw new BeanDefinitionStoreException( @@ -121,22 +143,19 @@ else if (value instanceof RuntimeBeanNameReference) { } return refName; } - else if (value instanceof BeanDefinitionHolder) { + else if (value instanceof BeanDefinitionHolder bdHolder) { // Resolve BeanDefinitionHolder: contains BeanDefinition with name and aliases. - BeanDefinitionHolder bdHolder = (BeanDefinitionHolder) value; - return resolveInnerBean(argName, bdHolder.getBeanName(), bdHolder.getBeanDefinition()); + return resolveInnerBean(bdHolder.getBeanName(), bdHolder.getBeanDefinition(), + (name, mbd) -> resolveInnerBeanValue(argName, name, mbd)); } - else if (value instanceof BeanDefinition) { - // Resolve plain BeanDefinition, without contained name: use dummy name. - BeanDefinition bd = (BeanDefinition) value; - String innerBeanName = "(inner bean)" + BeanFactoryUtils.GENERATED_BEAN_NAME_SEPARATOR + - ObjectUtils.getIdentityHexString(bd); - return resolveInnerBean(argName, innerBeanName, bd); + else if (value instanceof BeanDefinition bd) { + return resolveInnerBean(null, bd, + (name, mbd) -> resolveInnerBeanValue(argName, name, mbd)); } - else if (value instanceof DependencyDescriptor) { + else if (value instanceof DependencyDescriptor dependencyDescriptor) { Set autowiredBeanNames = new LinkedHashSet<>(4); Object result = this.beanFactory.resolveDependency( - (DependencyDescriptor) value, this.beanName, autowiredBeanNames, this.typeConverter); + dependencyDescriptor, this.beanName, autowiredBeanNames, this.typeConverter); for (String autowiredBeanName : autowiredBeanNames) { if (this.beanFactory.containsBean(autowiredBeanName)) { this.beanFactory.registerDependentBean(autowiredBeanName, this.beanName); @@ -144,16 +163,15 @@ else if (value instanceof DependencyDescriptor) { } return result; } - else if (value instanceof ManagedArray) { + else if (value instanceof ManagedArray managedArray) { // May need to resolve contained runtime references. - ManagedArray array = (ManagedArray) value; - Class elementType = array.resolvedElementType; + Class elementType = managedArray.resolvedElementType; if (elementType == null) { - String elementTypeName = array.getElementTypeName(); + String elementTypeName = managedArray.getElementTypeName(); if (StringUtils.hasText(elementTypeName)) { try { elementType = ClassUtils.forName(elementTypeName, this.beanFactory.getBeanClassLoader()); - array.resolvedElementType = elementType; + managedArray.resolvedElementType = elementType; } catch (Throwable ex) { // Improve the message by showing the context. @@ -168,27 +186,27 @@ else if (value instanceof ManagedArray) { } return resolveManagedArray(argName, (List) value, elementType); } - else if (value instanceof ManagedList) { + else if (value instanceof ManagedList managedList) { // May need to resolve contained runtime references. - return resolveManagedList(argName, (List) value); + return resolveManagedList(argName, managedList); } - else if (value instanceof ManagedSet) { + else if (value instanceof ManagedSet managedSet) { // May need to resolve contained runtime references. - return resolveManagedSet(argName, (Set) value); + return resolveManagedSet(argName, managedSet); } - else if (value instanceof ManagedMap) { + else if (value instanceof ManagedMap managedMap) { // May need to resolve contained runtime references. - return resolveManagedMap(argName, (Map) value); + return resolveManagedMap(argName, managedMap); } - else if (value instanceof ManagedProperties) { - Properties original = (Properties) value; + else if (value instanceof ManagedProperties original) { + // Properties original = managedProperties; Properties copy = new Properties(); original.forEach((propKey, propValue) -> { - if (propKey instanceof TypedStringValue) { - propKey = evaluate((TypedStringValue) propKey); + if (propKey instanceof TypedStringValue typedStringValue) { + propKey = evaluate(typedStringValue); } - if (propValue instanceof TypedStringValue) { - propValue = evaluate((TypedStringValue) propValue); + if (propValue instanceof TypedStringValue typedStringValue) { + propValue = evaluate(typedStringValue); } if (propKey == null || propValue == null) { throw new BeanCreationException( @@ -199,9 +217,8 @@ else if (value instanceof ManagedProperties) { }); return copy; } - else if (value instanceof TypedStringValue) { + else if (value instanceof TypedStringValue typedStringValue) { // Convert value to target type here. - TypedStringValue typedStringValue = (TypedStringValue) value; Object valueObject = evaluate(typedStringValue); try { Class resolvedTargetType = resolveTargetType(typedStringValue); @@ -227,6 +244,25 @@ else if (value instanceof NullBean) { } } + /** + * Resolve an inner bean definition and invoke the specified {@code resolver} + * on its merged bean definition. + * @param innerBeanName the inner bean name (or {@code null} to assign one) + * @param innerBd the inner raw bean definition + * @param resolver the function to invoke to resolve + * @param the type of the resolution + * @return a resolved inner bean, as a result of applying the {@code resolver} + * @since 6.0 + */ + public T resolveInnerBean(@Nullable String innerBeanName, BeanDefinition innerBd, + BiFunction resolver) { + + String nameToUse = (innerBeanName != null ? innerBeanName : "(inner bean)" + + BeanFactoryUtils.GENERATED_BEAN_NAME_SEPARATOR + ObjectUtils.getIdentityHexString(innerBd)); + return resolver.apply(nameToUse, + this.beanFactory.getMergedBeanDefinition(nameToUse, innerBd, this.beanDefinition)); + } + /** * Evaluate the given value as an expression, if necessary. * @param value the candidate value (may be an expression) @@ -248,11 +284,10 @@ protected Object evaluate(TypedStringValue value) { */ @Nullable protected Object evaluate(@Nullable Object value) { - if (value instanceof String) { - return doEvaluate((String) value); + if (value instanceof String str) { + return doEvaluate(str); } - else if (value instanceof String[]) { - String[] values = (String[]) value; + else if (value instanceof String[] values) { boolean actuallyResolved = false; Object[] resolvedValues = new Object[values.length]; for (int i = 0; i < values.length; i++) { @@ -347,14 +382,12 @@ private Object resolveReference(Object argName, RuntimeBeanReference ref) { * Resolve an inner bean definition. * @param argName the name of the argument that the inner bean is defined for * @param innerBeanName the name of the inner bean - * @param innerBd the bean definition for the inner bean + * @param mbd the merged bean definition for the inner bean * @return the resolved inner bean instance */ @Nullable - private Object resolveInnerBean(Object argName, String innerBeanName, BeanDefinition innerBd) { - RootBeanDefinition mbd = null; + private Object resolveInnerBeanValue(Object argName, String innerBeanName, RootBeanDefinition mbd) { try { - mbd = this.beanFactory.getMergedBeanDefinition(innerBeanName, innerBd, this.beanDefinition); // Check given bean name whether it is unique. If not already unique, // add counter - increasing the counter until the name is unique. String actualInnerBeanName = innerBeanName; @@ -372,10 +405,9 @@ private Object resolveInnerBean(Object argName, String innerBeanName, BeanDefini } // Actually create the inner bean instance now... Object innerBean = this.beanFactory.createBean(actualInnerBeanName, mbd, null); - if (innerBean instanceof FactoryBean) { + if (innerBean instanceof FactoryBean factoryBean) { boolean synthetic = mbd.isSynthetic(); - innerBean = this.beanFactory.getObjectFromFactoryBean( - (FactoryBean) innerBean, actualInnerBeanName, !synthetic); + innerBean = this.beanFactory.getObjectFromFactoryBean(factoryBean, actualInnerBeanName, !synthetic); } if (innerBean instanceof NullBean) { innerBean = null; @@ -386,7 +418,7 @@ private Object resolveInnerBean(Object argName, String innerBeanName, BeanDefini throw new BeanCreationException( this.beanDefinition.getResourceDescription(), this.beanName, "Cannot create inner bean '" + innerBeanName + "' " + - (mbd != null && mbd.getBeanClassName() != null ? "of type [" + mbd.getBeanClassName() + "] " : "") + + (mbd.getBeanClassName() != null ? "of type [" + mbd.getBeanClassName() + "] " : "") + "while setting " + argName, ex); } } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/CglibSubclassingInstantiationStrategy.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/CglibSubclassingInstantiationStrategy.java index 698a04d2498a..39d7c180c5a6 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/CglibSubclassingInstantiationStrategy.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/CglibSubclassingInstantiationStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -35,6 +35,7 @@ import org.springframework.cglib.proxy.MethodInterceptor; import org.springframework.cglib.proxy.MethodProxy; import org.springframework.cglib.proxy.NoOp; +import org.springframework.core.ResolvableType; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -80,10 +81,17 @@ protected Object instantiateWithMethodInjection(RootBeanDefinition bd, @Nullable protected Object instantiateWithMethodInjection(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner, @Nullable Constructor ctor, Object... args) { - // Must generate CGLIB subclass... return new CglibSubclassCreator(bd, owner).instantiate(ctor, args); } + @Override + public Class getActualBeanClass(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner) { + if (!bd.hasMethodOverrides()) { + return super.getActualBeanClass(bd, beanName, owner); + } + return new CglibSubclassCreator(bd, owner).createEnhancedSubclass(bd); + } + /** * An inner class created for historical reasons to avoid external CGLIB dependency @@ -141,10 +149,11 @@ public Object instantiate(@Nullable Constructor ctor, Object... args) { * Create an enhanced subclass of the bean class for the provided bean * definition, using CGLIB. */ - private Class createEnhancedSubclass(RootBeanDefinition beanDefinition) { + public Class createEnhancedSubclass(RootBeanDefinition beanDefinition) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(beanDefinition.getBeanClass()); enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE); + enhancer.setAttemptLoad(true); if (this.owner instanceof ConfigurableBeanFactory) { ClassLoader cl = ((ConfigurableBeanFactory) this.owner).getBeanClassLoader(); enhancer.setStrategy(new ClassLoaderAwareGeneratorStrategy(cl)); @@ -244,8 +253,10 @@ public Object intercept(Object obj, Method method, Object[] args, MethodProxy mp return (bean.equals(null) ? null : bean); } else { - return (argsToUse != null ? this.owner.getBean(method.getReturnType(), argsToUse) : - this.owner.getBean(method.getReturnType())); + // Find target bean matching the (potentially generic) method return type + ResolvableType genericReturnType = ResolvableType.forMethodReturnType(method); + return (argsToUse != null ? this.owner.getBeanProvider(genericReturnType).getObject(argsToUse) : + this.owner.getBeanProvider(genericReturnType).getObject()); } } } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/ChildBeanDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/ChildBeanDefinition.java index 51b981f6193c..bab5f2118faa 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/ChildBeanDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/ChildBeanDefinition.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-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. @@ -160,10 +160,9 @@ public boolean equals(@Nullable Object other) { if (this == other) { return true; } - if (!(other instanceof ChildBeanDefinition)) { + if (!(other instanceof ChildBeanDefinition that)) { return false; } - ChildBeanDefinition that = (ChildBeanDefinition) other; return (ObjectUtils.nullSafeEquals(this.parentName, that.parentName) && super.equals(other)); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java index f49690677a91..4985d0bc5be0 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -22,11 +22,10 @@ import java.lang.reflect.Executable; import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.security.AccessController; -import java.security.PrivilegedAction; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.Deque; import java.util.HashSet; @@ -34,10 +33,14 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; import org.apache.commons.logging.Log; import org.springframework.beans.BeanMetadataElement; +import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanWrapper; import org.springframework.beans.BeanWrapperImpl; import org.springframework.beans.BeansException; @@ -45,18 +48,23 @@ import org.springframework.beans.TypeMismatchException; import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InjectionPoint; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.beans.factory.UnsatisfiedDependencyException; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanReference; import org.springframework.beans.factory.config.ConstructorArgumentValues; import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueHolder; import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.core.CollectionFactory; import org.springframework.core.MethodParameter; import org.springframework.core.NamedThreadLocal; import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.core.ResolvableType; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -67,6 +75,7 @@ /** * Delegate for resolving constructors and factory methods. + * *

    Performs constructor resolution through argument matching. * * @author Juergen Hoeller @@ -75,9 +84,12 @@ * @author Costin Leau * @author Sebastien Deleuze * @author Sam Brannen + * @author Stephane Nicoll + * @author Phil Webb * @since 2.0 * @see #autowireConstructor * @see #instantiateUsingFactoryMethod + * @see #resolveConstructorOrFactoryMethod * @see AbstractAutowireCapableBeanFactory */ class ConstructorResolver { @@ -85,7 +97,7 @@ class ConstructorResolver { private static final Object[] EMPTY_ARGS = new Object[0]; /** - * Marker for autowired arguments in a cached argument array, to be later replaced + * Marker for autowired arguments in a cached argument array, to be replaced * by a {@linkplain #resolveAutowiredArgument resolved autowired argument}. */ private static final Object autowiredArgumentMarker = new Object(); @@ -109,6 +121,8 @@ public ConstructorResolver(AbstractAutowireCapableBeanFactory beanFactory) { } + // BeanWrapper-based construction + /** * "autowire constructor" (with constructor arguments by type) behavior. * Also applied if explicit constructor argument values are specified, @@ -149,7 +163,7 @@ public BeanWrapper autowireConstructor(String beanName, RootBeanDefinition mbd, } } if (argsToResolve != null) { - argsToUse = resolvePreparedArguments(beanName, mbd, bw, constructorToUse, argsToResolve, true); + argsToUse = resolvePreparedArguments(beanName, mbd, bw, constructorToUse, argsToResolve); } } @@ -276,12 +290,12 @@ else if (constructorToUse != null && typeDiffWeight == minTypeDiffWeight) { throw ex; } throw new BeanCreationException(mbd.getResourceDescription(), beanName, - "Could not resolve matching constructor " + + "Could not resolve matching constructor on bean class [" + mbd.getBeanClassName() + "] " + "(hint: specify index/type/name arguments for simple parameters to avoid type ambiguities)"); } else if (ambiguousConstructors != null && !mbd.isLenientConstructorResolution()) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, - "Ambiguous constructor matches found in bean '" + beanName + "' " + + "Ambiguous constructor matches found on bean class [" + mbd.getBeanClassName() + "] " + "(hint: specify index/type/name arguments for simple parameters to avoid type ambiguities): " + ambiguousConstructors); } @@ -301,18 +315,10 @@ private Object instantiate( try { InstantiationStrategy strategy = this.beanFactory.getInstantiationStrategy(); - if (System.getSecurityManager() != null) { - return AccessController.doPrivileged((PrivilegedAction) () -> - strategy.instantiate(mbd, beanName, this.beanFactory, constructorToUse, argsToUse), - this.beanFactory.getAccessControlContext()); - } - else { - return strategy.instantiate(mbd, beanName, this.beanFactory, constructorToUse, argsToUse); - } + return strategy.instantiate(mbd, beanName, this.beanFactory, constructorToUse, argsToUse); } catch (Throwable ex) { - throw new BeanCreationException(mbd.getResourceDescription(), beanName, - "Bean instantiation via constructor failed", ex); + throw new BeanCreationException(mbd.getResourceDescription(), beanName, ex.getMessage(), ex); } } @@ -338,7 +344,7 @@ public void resolveFactoryMethodIfPossible(RootBeanDefinition mbd) { Method[] candidates = getCandidateMethods(factoryClass, mbd); Method uniqueCandidate = null; for (Method candidate : candidates) { - if (Modifier.isStatic(candidate.getModifiers()) == isStatic && mbd.isFactoryMethod(candidate)) { + if ((!isStatic || isStaticCandidate(candidate, factoryClass)) && mbd.isFactoryMethod(candidate)) { if (uniqueCandidate == null) { uniqueCandidate = candidate; } @@ -364,15 +370,12 @@ private boolean isParamMismatch(Method uniqueCandidate, Method candidate) { * Called as the starting point for factory method determination. */ private Method[] getCandidateMethods(Class factoryClass, RootBeanDefinition mbd) { - if (System.getSecurityManager() != null) { - return AccessController.doPrivileged((PrivilegedAction) () -> - (mbd.isNonPublicAccessAllowed() ? - ReflectionUtils.getAllDeclaredMethods(factoryClass) : factoryClass.getMethods())); - } - else { - return (mbd.isNonPublicAccessAllowed() ? - ReflectionUtils.getAllDeclaredMethods(factoryClass) : factoryClass.getMethods()); - } + return (mbd.isNonPublicAccessAllowed() ? + ReflectionUtils.getUniqueDeclaredMethods(factoryClass) : factoryClass.getMethods()); + } + + private boolean isStaticCandidate(Method method, Class factoryClass) { + return (Modifier.isStatic(method.getModifiers()) && method.getDeclaringClass() == factoryClass); } /** @@ -410,6 +413,7 @@ public BeanWrapper instantiateUsingFactoryMethod( if (mbd.isSingleton() && this.beanFactory.containsSingleton(beanName)) { throw new ImplicitlyAppearedSingletonException(); } + this.beanFactory.registerDependentBean(factoryBeanName, beanName); factoryClass = factoryBean.getClass(); isStatic = false; } @@ -444,7 +448,7 @@ public BeanWrapper instantiateUsingFactoryMethod( } } if (argsToResolve != null) { - argsToUse = resolvePreparedArguments(beanName, mbd, bw, factoryMethodToUse, argsToResolve, true); + argsToUse = resolvePreparedArguments(beanName, mbd, bw, factoryMethodToUse, argsToResolve); } } @@ -466,7 +470,7 @@ public BeanWrapper instantiateUsingFactoryMethod( candidates = new ArrayList<>(); Method[] rawCandidates = getCandidateMethods(factoryClass, mbd); for (Method candidate : rawCandidates) { - if (Modifier.isStatic(candidate.getModifiers()) == isStatic && mbd.isFactoryMethod(candidate)) { + if ((!isStatic || isStaticCandidate(candidate, factoryClass)) && mbd.isFactoryMethod(candidate)) { candidates.add(candidate); } } @@ -606,9 +610,9 @@ else if (resolvedValues != null) { } String argDesc = StringUtils.collectionToCommaDelimitedString(argTypes); throw new BeanCreationException(mbd.getResourceDescription(), beanName, - "No matching factory method found: " + + "No matching factory method found on class [" + factoryClass.getName() + "]: " + (mbd.getFactoryBeanName() != null ? - "factory bean '" + mbd.getFactoryBeanName() + "'; " : "") + + "factory bean '" + mbd.getFactoryBeanName() + "'; " : "") + "factory method '" + mbd.getFactoryMethodName() + "(" + argDesc + ")'. " + "Check that a method with the specified name " + (minNrOfArgs > 0 ? "and arguments " : "") + @@ -617,12 +621,12 @@ else if (resolvedValues != null) { } else if (void.class == factoryMethodToUse.getReturnType()) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, - "Invalid factory method '" + mbd.getFactoryMethodName() + - "': needs to have a non-void return type!"); + "Invalid factory method '" + mbd.getFactoryMethodName() + "' on class [" + + factoryClass.getName() + "]: needs to have a non-void return type!"); } else if (ambiguousFactoryMethods != null) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, - "Ambiguous factory method matches found in bean '" + beanName + "' " + + "Ambiguous factory method matches found on class [" + factoryClass.getName() + "] " + "(hint: specify index/type/name arguments for simple parameters to avoid type ambiguities): " + ambiguousFactoryMethods); } @@ -641,20 +645,11 @@ private Object instantiate(String beanName, RootBeanDefinition mbd, @Nullable Object factoryBean, Method factoryMethod, Object[] args) { try { - if (System.getSecurityManager() != null) { - return AccessController.doPrivileged((PrivilegedAction) () -> - this.beanFactory.getInstantiationStrategy().instantiate( - mbd, beanName, this.beanFactory, factoryBean, factoryMethod, args), - this.beanFactory.getAccessControlContext()); - } - else { - return this.beanFactory.getInstantiationStrategy().instantiate( - mbd, beanName, this.beanFactory, factoryBean, factoryMethod, args); - } + return this.beanFactory.getInstantiationStrategy().instantiate( + mbd, beanName, this.beanFactory, factoryBean, factoryMethod, args); } catch (Throwable ex) { - throw new BeanCreationException(mbd.getResourceDescription(), beanName, - "Bean instantiation via factory method failed", ex); + throw new BeanCreationException(mbd.getResourceDescription(), beanName, ex.getMessage(), ex); } } @@ -816,7 +811,7 @@ private ArgumentsHolder createArgumentArray( * Resolve the prepared arguments stored in the given bean definition. */ private Object[] resolvePreparedArguments(String beanName, RootBeanDefinition mbd, BeanWrapper bw, - Executable executable, Object[] argsToResolve, boolean fallback) { + Executable executable, Object[] argsToResolve) { TypeConverter customConverter = this.beanFactory.getCustomTypeConverter(); TypeConverter converter = (customConverter != null ? customConverter : bw); @@ -829,7 +824,7 @@ private Object[] resolvePreparedArguments(String beanName, RootBeanDefinition mb Object argValue = argsToResolve[argIndex]; MethodParameter methodParam = MethodParameter.forExecutable(executable, argIndex); if (argValue == autowiredArgumentMarker) { - argValue = resolveAutowiredArgument(methodParam, beanName, null, converter, fallback); + argValue = resolveAutowiredArgument(methodParam, beanName, null, converter, true); } else if (argValue instanceof BeanMetadataElement) { argValue = valueResolver.resolveValueIfNecessary("constructor argument", argValue); @@ -906,6 +901,311 @@ else if (CollectionFactory.isApproximableMapType(paramType)) { } } + + // AOT-oriented pre-resolution + + public Executable resolveConstructorOrFactoryMethod(String beanName, RootBeanDefinition mbd) { + Supplier beanType = () -> getBeanType(beanName, mbd); + List valueTypes = (mbd.hasConstructorArgumentValues() ? + determineParameterValueTypes(mbd) : Collections.emptyList()); + Method resolvedFactoryMethod = resolveFactoryMethod(beanName, mbd, valueTypes); + if (resolvedFactoryMethod != null) { + return resolvedFactoryMethod; + } + + Class factoryBeanClass = getFactoryBeanClass(beanName, mbd); + if (factoryBeanClass != null && !factoryBeanClass.equals(mbd.getResolvableType().toClass())) { + ResolvableType resolvableType = mbd.getResolvableType(); + boolean isCompatible = ResolvableType.forClass(factoryBeanClass) + .as(FactoryBean.class).getGeneric(0).isAssignableFrom(resolvableType); + Assert.state(isCompatible, () -> String.format( + "Incompatible target type '%s' for factory bean '%s'", + resolvableType.toClass().getName(), factoryBeanClass.getName())); + Executable executable = resolveConstructor(beanName, mbd, + () -> ResolvableType.forClass(factoryBeanClass), valueTypes); + if (executable != null) { + return executable; + } + throw new IllegalStateException("No suitable FactoryBean constructor found for " + + mbd + " and argument types " + valueTypes); + + } + + Executable resolvedConstructor = resolveConstructor(beanName, mbd, beanType, valueTypes); + if (resolvedConstructor != null) { + return resolvedConstructor; + } + + throw new IllegalStateException("No constructor or factory method candidate found for " + + mbd + " and argument types " + valueTypes); + } + + private List determineParameterValueTypes(RootBeanDefinition mbd) { + List parameterTypes = new ArrayList<>(); + for (ValueHolder valueHolder : mbd.getConstructorArgumentValues().getIndexedArgumentValues().values()) { + parameterTypes.add(determineParameterValueType(mbd, valueHolder)); + } + return parameterTypes; + } + + private ResolvableType determineParameterValueType(RootBeanDefinition mbd, ValueHolder valueHolder) { + if (valueHolder.getType() != null) { + return ResolvableType.forClass( + ClassUtils.resolveClassName(valueHolder.getType(), this.beanFactory.getBeanClassLoader())); + } + Object value = valueHolder.getValue(); + if (value instanceof BeanReference br) { + if (value instanceof RuntimeBeanReference rbr) { + if (rbr.getBeanType() != null) { + return ResolvableType.forClass(rbr.getBeanType()); + } + } + return ResolvableType.forClass(this.beanFactory.getType(br.getBeanName(), false)); + } + if (value instanceof BeanDefinition innerBd) { + String nameToUse = "(inner bean)"; + ResolvableType type = getBeanType(nameToUse, + this.beanFactory.getMergedBeanDefinition(nameToUse, innerBd, mbd)); + return (FactoryBean.class.isAssignableFrom(type.toClass()) ? + type.as(FactoryBean.class).getGeneric(0) : type); + } + if (value instanceof Class clazz) { + return ResolvableType.forClassWithGenerics(Class.class, clazz); + } + return ResolvableType.forInstance(value); + } + + @Nullable + private Executable resolveConstructor(String beanName, RootBeanDefinition mbd, + Supplier beanType, List valueTypes) { + + Class type = ClassUtils.getUserClass(beanType.get().toClass()); + Constructor[] ctors = this.beanFactory.determineConstructorsFromBeanPostProcessors(type, beanName); + if (ctors == null) { + if (!mbd.hasConstructorArgumentValues()) { + ctors = mbd.getPreferredConstructors(); + } + if (ctors == null) { + ctors = (mbd.isNonPublicAccessAllowed() ? type.getDeclaredConstructors() : type.getConstructors()); + } + } + if (ctors.length == 1) { + return ctors[0]; + } + + Function, List> parameterTypesFactory = executable -> { + List types = new ArrayList<>(); + for (int i = 0; i < executable.getParameterCount(); i++) { + types.add(ResolvableType.forConstructorParameter(executable, i)); + } + return types; + }; + List matches = Arrays.stream(ctors) + .filter(executable -> match(parameterTypesFactory.apply(executable), + valueTypes, FallbackMode.NONE)) + .toList(); + if (matches.size() == 1) { + return matches.get(0); + } + List assignableElementFallbackMatches = Arrays + .stream(ctors) + .filter(executable -> match(parameterTypesFactory.apply(executable), + valueTypes, FallbackMode.ASSIGNABLE_ELEMENT)) + .toList(); + if (assignableElementFallbackMatches.size() == 1) { + return assignableElementFallbackMatches.get(0); + } + List typeConversionFallbackMatches = Arrays + .stream(ctors) + .filter(executable -> match(parameterTypesFactory.apply(executable), + valueTypes, FallbackMode.TYPE_CONVERSION)) + .toList(); + return (typeConversionFallbackMatches.size() == 1 ? typeConversionFallbackMatches.get(0) : null); + } + + @Nullable + private Method resolveFactoryMethod(String beanName, RootBeanDefinition mbd, List valueTypes) { + if (mbd.isFactoryMethodUnique) { + Method resolvedFactoryMethod = mbd.getResolvedFactoryMethod(); + if (resolvedFactoryMethod != null) { + return resolvedFactoryMethod; + } + } + + String factoryMethodName = mbd.getFactoryMethodName(); + if (factoryMethodName != null) { + String factoryBeanName = mbd.getFactoryBeanName(); + Class factoryClass; + boolean isStatic; + if (factoryBeanName != null) { + factoryClass = this.beanFactory.getType(factoryBeanName); + isStatic = false; + } + else { + factoryClass = this.beanFactory.resolveBeanClass(mbd, beanName); + isStatic = true; + } + + Assert.state(factoryClass != null, () -> "Failed to determine bean class of " + mbd); + Method[] rawCandidates = getCandidateMethods(factoryClass, mbd); + List candidates = new ArrayList<>(); + for (Method candidate : rawCandidates) { + if ((!isStatic || isStaticCandidate(candidate, factoryClass)) && mbd.isFactoryMethod(candidate)) { + candidates.add(candidate); + } + } + + Method result = null; + if (candidates.size() == 1) { + result = candidates.get(0); + } + else if (candidates.size() > 1) { + Function> parameterTypesFactory = method -> { + List types = new ArrayList<>(); + for (int i = 0; i < method.getParameterCount(); i++) { + types.add(ResolvableType.forMethodParameter(method, i)); + } + return types; + }; + result = (Method) resolveFactoryMethod(candidates, parameterTypesFactory, valueTypes); + } + + if (result == null) { + throw new BeanCreationException(mbd.getResourceDescription(), beanName, + "No matching factory method found on class [" + factoryClass.getName() + "]: " + + (mbd.getFactoryBeanName() != null ? + "factory bean '" + mbd.getFactoryBeanName() + "'; " : "") + + "factory method '" + mbd.getFactoryMethodName() + "'. "); + } + return result; + } + + return null; + } + + @Nullable + private Executable resolveFactoryMethod(List executables, + Function> parameterTypesFactory, + List valueTypes) { + + List matches = executables.stream() + .filter(executable -> match(parameterTypesFactory.apply(executable), valueTypes, FallbackMode.NONE)) + .toList(); + if (matches.size() == 1) { + return matches.get(0); + } + List assignableElementFallbackMatches = executables.stream() + .filter(executable -> match(parameterTypesFactory.apply(executable), + valueTypes, FallbackMode.ASSIGNABLE_ELEMENT)) + .toList(); + if (assignableElementFallbackMatches.size() == 1) { + return assignableElementFallbackMatches.get(0); + } + List typeConversionFallbackMatches = executables.stream() + .filter(executable -> match(parameterTypesFactory.apply(executable), + valueTypes, FallbackMode.TYPE_CONVERSION)) + .toList(); + Assert.state(typeConversionFallbackMatches.size() <= 1, + () -> "Multiple matches with parameters '" + valueTypes + "': " + typeConversionFallbackMatches); + return (typeConversionFallbackMatches.size() == 1 ? typeConversionFallbackMatches.get(0) : null); + } + + private boolean match( + List parameterTypes, List valueTypes, FallbackMode fallbackMode) { + + if (parameterTypes.size() != valueTypes.size()) { + return false; + } + for (int i = 0; i < parameterTypes.size(); i++) { + if (!isMatch(parameterTypes.get(i), valueTypes.get(i), fallbackMode)) { + return false; + } + } + return true; + } + + private boolean isMatch(ResolvableType parameterType, ResolvableType valueType, FallbackMode fallbackMode) { + if (isAssignable(valueType).test(parameterType)) { + return true; + } + return switch (fallbackMode) { + case ASSIGNABLE_ELEMENT -> isAssignable(valueType).test(extractElementType(parameterType)); + case TYPE_CONVERSION -> typeConversionFallback(valueType).test(parameterType); + default -> false; + }; + } + + private Predicate isAssignable(ResolvableType valueType) { + return parameterType -> parameterType.isAssignableFrom(valueType); + } + + private ResolvableType extractElementType(ResolvableType parameterType) { + if (parameterType.isArray()) { + return parameterType.getComponentType(); + } + if (Collection.class.isAssignableFrom(parameterType.toClass())) { + return parameterType.as(Collection.class).getGeneric(0); + } + return ResolvableType.NONE; + } + + private Predicate typeConversionFallback(ResolvableType valueType) { + return parameterType -> { + if (valueOrCollection(valueType, this::isStringForClassFallback).test(parameterType)) { + return true; + } + return valueOrCollection(valueType, this::isSimpleValueType).test(parameterType); + }; + } + + private Predicate valueOrCollection(ResolvableType valueType, + Function> predicateProvider) { + + return parameterType -> { + if (predicateProvider.apply(valueType).test(parameterType)) { + return true; + } + if (predicateProvider.apply(extractElementType(valueType)).test(extractElementType(parameterType))) { + return true; + } + return (predicateProvider.apply(valueType).test(extractElementType(parameterType))); + }; + } + + /** + * Return a {@link Predicate} for a parameter type that checks if its target + * value is a {@link Class} and the value type is a {@link String}. This is + * a regular use cases where a {@link Class} is defined in the bean + * definition as an FQN. + * @param valueType the type of the value + * @return a predicate to indicate a fallback match for a String to Class + * parameter + */ + private Predicate isStringForClassFallback(ResolvableType valueType) { + return parameterType -> (valueType.isAssignableFrom(String.class) && + parameterType.isAssignableFrom(Class.class)); + } + + private Predicate isSimpleValueType(ResolvableType valueType) { + return parameterType -> (BeanUtils.isSimpleValueType(parameterType.toClass()) && + BeanUtils.isSimpleValueType(valueType.toClass())); + } + + @Nullable + private Class getFactoryBeanClass(String beanName, RootBeanDefinition mbd) { + Class beanClass = this.beanFactory.resolveBeanClass(mbd, beanName); + return (beanClass != null && FactoryBean.class.isAssignableFrom(beanClass) ? beanClass : null); + } + + private ResolvableType getBeanType(String beanName, RootBeanDefinition mbd) { + ResolvableType resolvableType = mbd.getResolvableType(); + if (resolvableType != ResolvableType.NONE) { + return resolvableType; + } + return ResolvableType.forClass(this.beanFactory.resolveBeanClass(mbd, beanName)); + } + + static InjectionPoint setCurrentInjectionPoint(@Nullable InjectionPoint injectionPoint) { InjectionPoint old = currentInjectionPoint.get(); if (injectionPoint != null) { @@ -983,7 +1283,7 @@ public void storeCache(RootBeanDefinition mbd, Executable constructorOrFactoryMe /** - * Delegate for checking Java 6's {@link ConstructorProperties} annotation. + * Delegate for checking Java's {@link ConstructorProperties} annotation. */ private static class ConstructorPropertiesChecker { @@ -1004,4 +1304,14 @@ public static String[] evaluate(Constructor candidate, int paramCount) { } } + + private enum FallbackMode { + + NONE, + + ASSIGNABLE_ELEMENT, + + TYPE_CONVERSION + } + } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java index dc79d3c6ad75..bf7a54ae06e4 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -20,13 +20,12 @@ import java.io.NotSerializableException; import java.io.ObjectInputStream; import java.io.ObjectStreamException; +import java.io.Serial; import java.io.Serializable; import java.lang.annotation.Annotation; import java.lang.ref.Reference; import java.lang.ref.WeakReference; import java.lang.reflect.Method; -import java.security.AccessController; -import java.security.PrivilegedAction; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -43,7 +42,7 @@ import java.util.function.Predicate; import java.util.stream.Stream; -import javax.inject.Provider; +import jakarta.inject.Provider; import org.springframework.beans.BeansException; import org.springframework.beans.TypeConverter; @@ -55,7 +54,6 @@ import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.BeanNotOfRequiredTypeException; import org.springframework.beans.factory.CannotLoadBeanClassException; -import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InjectionPoint; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.NoUniqueBeanDefinitionException; @@ -127,7 +125,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto static { try { javaxInjectProviderClass = - ClassUtils.forName("javax.inject.Provider", DefaultListableBeanFactory.class.getClassLoader()); + ClassUtils.forName("jakarta.inject.Provider", DefaultListableBeanFactory.class.getClassLoader()); } catch (ClassNotFoundException ex) { // JSR-330 API not available - Provider interface simply not supported then. @@ -280,7 +278,7 @@ public void setDependencyComparator(@Nullable Comparator dependencyCompa } /** - * Return the dependency comparator for this BeanFactory (may be {@code null}. + * Return the dependency comparator for this BeanFactory (may be {@code null}). * @since 4.0 */ @Nullable @@ -295,16 +293,8 @@ public Comparator getDependencyComparator() { */ public void setAutowireCandidateResolver(AutowireCandidateResolver autowireCandidateResolver) { Assert.notNull(autowireCandidateResolver, "AutowireCandidateResolver must not be null"); - if (autowireCandidateResolver instanceof BeanFactoryAware) { - if (System.getSecurityManager() != null) { - AccessController.doPrivileged((PrivilegedAction) () -> { - ((BeanFactoryAware) autowireCandidateResolver).setBeanFactory(this); - return null; - }, getAccessControlContext()); - } - else { - ((BeanFactoryAware) autowireCandidateResolver).setBeanFactory(this); - } + if (autowireCandidateResolver instanceof BeanFactoryAware beanFactoryAware) { + beanFactoryAware.setBeanFactory(this); } this.autowireCandidateResolver = autowireCandidateResolver; } @@ -320,8 +310,7 @@ public AutowireCandidateResolver getAutowireCandidateResolver() { @Override public void copyConfigurationFrom(ConfigurableBeanFactory otherFactory) { super.copyConfigurationFrom(otherFactory); - if (otherFactory instanceof DefaultListableBeanFactory) { - DefaultListableBeanFactory otherListableFactory = (DefaultListableBeanFactory) otherFactory; + if (otherFactory instanceof DefaultListableBeanFactory otherListableFactory) { this.allowBeanDefinitionOverriding = otherListableFactory.allowBeanDefinitionOverriding; this.allowEagerClassLoading = otherListableFactory.allowEagerClassLoading; this.dependencyComparator = otherListableFactory.dependencyComparator; @@ -399,7 +388,7 @@ public ObjectProvider getBeanProvider(Class requiredType, boolean allo @Override public ObjectProvider getBeanProvider(ResolvableType requiredType, boolean allowEagerInit) { - return new BeanObjectProvider() { + return new BeanObjectProvider<>() { @Override public T getObject() throws BeansException { T resolved = resolveBean(requiredType, null, false); @@ -496,8 +485,8 @@ private T resolveBean(ResolvableType requiredType, @Nullable Object[] args, return namedBean.getBeanInstance(); } BeanFactory parent = getParentBeanFactory(); - if (parent instanceof DefaultListableBeanFactory) { - return ((DefaultListableBeanFactory) parent).resolveBean(requiredType, args, nonUniqueAsNull); + if (parent instanceof DefaultListableBeanFactory dlfb) { + return dlfb.resolveBean(requiredType, args, nonUniqueAsNull); } else if (parent != null) { ObjectProvider parentProvider = parent.getBeanProvider(requiredType); @@ -585,7 +574,9 @@ private String[] doGetBeanNamesForType(ResolvableType type, boolean includeNonSi if (!matchFound) { // In case of FactoryBean, try to match FactoryBean instance itself next. beanName = FACTORY_BEAN_PREFIX + beanName; - matchFound = isTypeMatch(beanName, type, allowFactoryBeanInit); + if (includeNonSingletons || isSingleton(beanName, mbd, dbd)) { + matchFound = isTypeMatch(beanName, type, allowFactoryBeanInit); + } } } if (matchFound) { @@ -675,8 +666,7 @@ public Map getBeansOfType( } catch (BeanCreationException ex) { Throwable rootCause = ex.getMostSpecificCause(); - if (rootCause instanceof BeanCurrentlyInCreationException) { - BeanCreationException bce = (BeanCreationException) rootCause; + if (rootCause instanceof BeanCurrentlyInCreationException bce) { String exBeanName = bce.getBeanName(); if (exBeanName != null && isCurrentlyInCreation(exBeanName)) { if (logger.isTraceEnabled()) { @@ -730,31 +720,33 @@ public Map getBeansWithAnnotation(Class an public A findAnnotationOnBean(String beanName, Class annotationType) throws NoSuchBeanDefinitionException { - return findMergedAnnotationOnBean(beanName, annotationType) - .synthesize(MergedAnnotation::isPresent).orElse(null); + return findAnnotationOnBean(beanName, annotationType, true); } - private MergedAnnotation findMergedAnnotationOnBean( - String beanName, Class annotationType) { + @Override + @Nullable + public A findAnnotationOnBean( + String beanName, Class annotationType, boolean allowFactoryBeanInit) + throws NoSuchBeanDefinitionException { - Class beanType = getType(beanName); + Class beanType = getType(beanName, allowFactoryBeanInit); if (beanType != null) { MergedAnnotation annotation = MergedAnnotations.from(beanType, SearchStrategy.TYPE_HIERARCHY).get(annotationType); if (annotation.isPresent()) { - return annotation; + return annotation.synthesize(); } } if (containsBeanDefinition(beanName)) { RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName); // Check raw bean class, e.g. in case of a proxy. - if (bd.hasBeanClass()) { + if (bd.hasBeanClass() && bd.getFactoryMethodName() == null) { Class beanClass = bd.getBeanClass(); if (beanClass != beanType) { MergedAnnotation annotation = MergedAnnotations.from(beanClass, SearchStrategy.TYPE_HIERARCHY).get(annotationType); if (annotation.isPresent()) { - return annotation; + return annotation.synthesize(); } } } @@ -764,11 +756,48 @@ private MergedAnnotation findMergedAnnotationOnBean( MergedAnnotation annotation = MergedAnnotations.from(factoryMethod, SearchStrategy.TYPE_HIERARCHY).get(annotationType); if (annotation.isPresent()) { - return annotation; + return annotation.synthesize(); } } } - return MergedAnnotation.missing(); + return null; + } + + @Override + public Set findAllAnnotationsOnBean( + String beanName, Class annotationType, boolean allowFactoryBeanInit) + throws NoSuchBeanDefinitionException { + + Set annotations = new LinkedHashSet<>(); + Class beanType = getType(beanName, allowFactoryBeanInit); + if (beanType != null) { + MergedAnnotations.from(beanType, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY) + .stream(annotationType) + .filter(MergedAnnotation::isPresent) + .forEach(mergedAnnotation -> annotations.add(mergedAnnotation.synthesize())); + } + if (containsBeanDefinition(beanName)) { + RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName); + // Check raw bean class, e.g. in case of a proxy. + if (bd.hasBeanClass() && bd.getFactoryMethodName() == null) { + Class beanClass = bd.getBeanClass(); + if (beanClass != beanType) { + MergedAnnotations.from(beanClass, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY) + .stream(annotationType) + .filter(MergedAnnotation::isPresent) + .forEach(mergedAnnotation -> annotations.add(mergedAnnotation.synthesize())); + } + } + // Check annotations declared on factory method, if any. + Method factoryMethod = bd.getResolvedFactoryMethod(); + if (factoryMethod != null) { + MergedAnnotations.from(factoryMethod, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY) + .stream(annotationType) + .filter(MergedAnnotation::isPresent) + .forEach(mergedAnnotation -> annotations.add(mergedAnnotation.synthesize())); + } + } + return annotations; } @@ -816,13 +845,13 @@ else if (containsSingleton(beanName)) { } BeanFactory parent = getParentBeanFactory(); - if (parent instanceof DefaultListableBeanFactory) { + if (parent instanceof DefaultListableBeanFactory dlbf) { // No bean definition found in this factory -> delegate to parent. - return ((DefaultListableBeanFactory) parent).isAutowireCandidate(beanName, descriptor, resolver); + return dlbf.isAutowireCandidate(beanName, descriptor, resolver); } - else if (parent instanceof ConfigurableListableBeanFactory) { + else if (parent instanceof ConfigurableListableBeanFactory clbf) { // If no DefaultListableBeanFactory, can't pass the resolver along. - return ((ConfigurableListableBeanFactory) parent).isAutowireCandidate(beanName, descriptor); + return clbf.isAutowireCandidate(beanName, descriptor); } else { return true; @@ -888,6 +917,7 @@ public void clearMetadataCache() { @Override public void freezeConfiguration() { + clearMetadataCache(); this.configurationFrozen = true; this.frozenBeanDefinitionNames = StringUtils.toStringArray(this.beanDefinitionNames); } @@ -923,21 +953,8 @@ public void preInstantiateSingletons() throws BeansException { if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) { if (isFactoryBean(beanName)) { Object bean = getBean(FACTORY_BEAN_PREFIX + beanName); - if (bean instanceof FactoryBean) { - FactoryBean factory = (FactoryBean) bean; - boolean isEagerInit; - if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) { - isEagerInit = AccessController.doPrivileged( - (PrivilegedAction) ((SmartFactoryBean) factory)::isEagerInit, - getAccessControlContext()); - } - else { - isEagerInit = (factory instanceof SmartFactoryBean && - ((SmartFactoryBean) factory).isEagerInit()); - } - if (isEagerInit) { - getBean(beanName); - } + if (bean instanceof SmartFactoryBean smartFactoryBean && smartFactoryBean.isEagerInit()) { + getBean(beanName); } } else { @@ -949,19 +966,10 @@ public void preInstantiateSingletons() throws BeansException { // Trigger post-initialization callback for all applicable beans... for (String beanName : beanNames) { Object singletonInstance = getSingleton(beanName); - if (singletonInstance instanceof SmartInitializingSingleton) { + if (singletonInstance instanceof SmartInitializingSingleton smartSingleton) { StartupStep smartInitialize = this.getApplicationStartup().start("spring.beans.smart-initialize") .tag("beanName", beanName); - SmartInitializingSingleton smartSingleton = (SmartInitializingSingleton) singletonInstance; - if (System.getSecurityManager() != null) { - AccessController.doPrivileged((PrivilegedAction) () -> { - smartSingleton.afterSingletonsInstantiated(); - return null; - }, getAccessControlContext()); - } - else { - smartSingleton.afterSingletonsInstantiated(); - } + smartSingleton.afterSingletonsInstantiated(); smartInitialize.end(); } } @@ -979,9 +987,9 @@ public void registerBeanDefinition(String beanName, BeanDefinition beanDefinitio Assert.hasText(beanName, "Bean name must not be empty"); Assert.notNull(beanDefinition, "BeanDefinition must not be null"); - if (beanDefinition instanceof AbstractBeanDefinition) { + if (beanDefinition instanceof AbstractBeanDefinition abd) { try { - ((AbstractBeanDefinition) beanDefinition).validate(); + abd.validate(); } catch (BeanDefinitionValidationException ex) { throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName, @@ -1019,6 +1027,23 @@ else if (!beanDefinition.equals(existingDefinition)) { this.beanDefinitionMap.put(beanName, beanDefinition); } else { + if (isAlias(beanName)) { + if (!isAllowBeanDefinitionOverriding()) { + String aliasedName = canonicalName(beanName); + if (containsBeanDefinition(aliasedName)) { // alias for existing bean definition + throw new BeanDefinitionOverrideException( + beanName, beanDefinition, getBeanDefinition(aliasedName)); + } + else { // alias pointing to non-existing bean definition + throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName, + "Cannot register bean definition for bean '" + beanName + + "' since there is already an alias for bean '" + aliasedName + "' bound."); + } + } + else { + removeAlias(beanName); + } + } if (hasBeanCreationStarted()) { // Cannot modify startup-time collection elements anymore (for stable iteration) synchronized (this.beanDefinitionMap) { @@ -1204,8 +1229,8 @@ public NamedBeanHolder resolveNamedBean(Class requiredType) throws Bea return namedBean; } BeanFactory parent = getParentBeanFactory(); - if (parent instanceof AutowireCapableBeanFactory) { - return ((AutowireCapableBeanFactory) parent).resolveNamedBean(requiredType); + if (parent instanceof AutowireCapableBeanFactory acbf) { + return acbf.resolveNamedBean(requiredType); } throw new NoSuchBeanDefinitionException(requiredType); } @@ -1231,8 +1256,7 @@ private NamedBeanHolder resolveNamedBean( } if (candidateNames.length == 1) { - String beanName = candidateNames[0]; - return new NamedBeanHolder<>(beanName, (T) getBean(beanName, requiredType.toClass(), args)); + return resolveNamedBean(candidateNames[0], requiredType, args); } else if (candidateNames.length > 1) { Map candidates = CollectionUtils.newLinkedHashMap(candidateNames.length); @@ -1251,8 +1275,11 @@ else if (candidateNames.length > 1) { } if (candidateName != null) { Object beanInstance = candidates.get(candidateName); - if (beanInstance == null || beanInstance instanceof Class) { - beanInstance = getBean(candidateName, requiredType.toClass(), args); + if (beanInstance == null) { + return null; + } + if (beanInstance instanceof Class) { + return resolveNamedBean(candidateName, requiredType, args); } return new NamedBeanHolder<>(candidateName, (T) beanInstance); } @@ -1264,6 +1291,17 @@ else if (candidateNames.length > 1) { return null; } + @Nullable + private NamedBeanHolder resolveNamedBean( + String beanName, ResolvableType requiredType, @Nullable Object[] args) throws BeansException { + + Object bean = getBean(beanName, null, args); + if (bean instanceof NullBean) { + return null; + } + return new NamedBeanHolder<>(beanName, adaptBeanInstance(beanName, bean, requiredType.toClass())); + } + @Override @Nullable public Object resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName, @@ -1389,7 +1427,7 @@ private Object resolveMultipleBeans(DependencyDescriptor descriptor, @Nullable S Class type = descriptor.getDependencyType(); - if (descriptor instanceof StreamDependencyDescriptor) { + if (descriptor instanceof StreamDependencyDescriptor streamDependencyDescriptor) { Map matchingBeans = findAutowireCandidates(beanName, type, descriptor); if (autowiredBeanNames != null) { autowiredBeanNames.addAll(matchingBeans.keySet()); @@ -1397,7 +1435,7 @@ private Object resolveMultipleBeans(DependencyDescriptor descriptor, @Nullable S Stream stream = matchingBeans.keySet().stream() .map(name -> descriptor.resolveCandidate(name, type, this)) .filter(bean -> !(bean instanceof NullBean)); - if (((StreamDependencyDescriptor) descriptor).isOrdered()) { + if (streamDependencyDescriptor.isOrdered()) { stream = stream.sorted(adaptOrderComparator(matchingBeans)); } return stream; @@ -1422,10 +1460,10 @@ else if (type.isArray()) { } TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter()); Object result = converter.convertIfNecessary(matchingBeans.values(), resolvedArrayType); - if (result instanceof Object[]) { + if (result instanceof Object[] array) { Comparator comparator = adaptDependencyComparator(matchingBeans); if (comparator != null) { - Arrays.sort((Object[]) result, comparator); + Arrays.sort(array, comparator); } } return result; @@ -1445,12 +1483,10 @@ else if (Collection.class.isAssignableFrom(type) && type.isInterface()) { } TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter()); Object result = converter.convertIfNecessary(matchingBeans.values(), type); - if (result instanceof List) { - if (((List) result).size() > 1) { - Comparator comparator = adaptDependencyComparator(matchingBeans); - if (comparator != null) { - ((List) result).sort(comparator); - } + if (result instanceof List list && list.size() > 1) { + Comparator comparator = adaptDependencyComparator(matchingBeans); + if (comparator != null) { + list.sort(comparator); } } return result; @@ -1492,8 +1528,8 @@ private boolean indicatesMultipleBeans(Class type) { @Nullable private Comparator adaptDependencyComparator(Map matchingBeans) { Comparator comparator = getDependencyComparator(); - if (comparator instanceof OrderComparator) { - return ((OrderComparator) comparator).withSourceProvider( + if (comparator instanceof OrderComparator orderComparator) { + return orderComparator.withSourceProvider( createFactoryAwareOrderSourceProvider(matchingBeans)); } else { @@ -1503,8 +1539,8 @@ private Comparator adaptDependencyComparator(Map matchingBean private Comparator adaptOrderComparator(Map matchingBeans) { Comparator dependencyComparator = getDependencyComparator(); - OrderComparator comparator = (dependencyComparator instanceof OrderComparator ? - (OrderComparator) dependencyComparator : OrderComparator.INSTANCE); + OrderComparator comparator = (dependencyComparator instanceof OrderComparator orderComparator ? + orderComparator : OrderComparator.INSTANCE); return comparator.withSourceProvider(createFactoryAwareOrderSourceProvider(matchingBeans)); } @@ -1587,8 +1623,8 @@ private void addCandidateEntry(Map candidates, String candidateN candidates.put(candidateName, beanInstance); } } - else if (containsSingleton(candidateName) || (descriptor instanceof StreamDependencyDescriptor && - ((StreamDependencyDescriptor) descriptor).isOrdered())) { + else if (containsSingleton(candidateName) || (descriptor instanceof StreamDependencyDescriptor streamDescriptor && + streamDescriptor.isOrdered())) { Object beanInstance = descriptor.resolveCandidate(candidateName, requiredType, this); candidates.put(candidateName, (beanInstance instanceof NullBean ? null : beanInstance)); } @@ -1664,7 +1700,7 @@ else if (candidateLocal) { /** * Determine the candidate with the highest priority in the given set of beans. - *

    Based on {@code @javax.annotation.Priority}. As defined by the related + *

    Based on {@code @jakarta.annotation.Priority}. As defined by the related * {@link org.springframework.core.Ordered} interface, the lowest value has * the highest priority. * @param candidates a Map of candidate names and candidate instances @@ -1717,14 +1753,13 @@ protected boolean isPrimary(String beanName, Object beanInstance) { if (containsBeanDefinition(transformedBeanName)) { return getMergedLocalBeanDefinition(transformedBeanName).isPrimary(); } - BeanFactory parent = getParentBeanFactory(); - return (parent instanceof DefaultListableBeanFactory && - ((DefaultListableBeanFactory) parent).isPrimary(transformedBeanName, beanInstance)); + return (getParentBeanFactory() instanceof DefaultListableBeanFactory parent && + parent.isPrimary(transformedBeanName, beanInstance)); } /** * Return the priority assigned for the given bean instance by - * the {@code javax.annotation.Priority} annotation. + * the {@code jakarta.annotation.Priority} annotation. *

    The default implementation delegates to the specified * {@link #setDependencyComparator dependency comparator}, checking its * {@link OrderComparator#getPriority method} if it is an extension of @@ -1737,8 +1772,8 @@ protected boolean isPrimary(String beanName, Object beanInstance) { @Nullable protected Integer getPriority(Object beanInstance) { Comparator comparator = getDependencyComparator(); - if (comparator instanceof OrderComparator) { - return ((OrderComparator) comparator).getPriority(beanInstance); + if (comparator instanceof OrderComparator orderComparator) { + return orderComparator.getPriority(beanInstance); } return null; } @@ -1802,9 +1837,8 @@ private void checkBeanNotOfRequiredType(Class type, DependencyDescriptor desc } } - BeanFactory parent = getParentBeanFactory(); - if (parent instanceof DefaultListableBeanFactory) { - ((DefaultListableBeanFactory) parent).checkBeanNotOfRequiredType(type, descriptor); + if (getParentBeanFactory() instanceof DefaultListableBeanFactory parent) { + parent.checkBeanNotOfRequiredType(type, descriptor); } } @@ -1826,7 +1860,7 @@ public Object resolveCandidate(String beanName, Class requiredType, BeanFacto } }; Object result = doResolveDependency(descriptorToUse, beanName, null, null); - return (result instanceof Optional ? (Optional) result : Optional.ofNullable(result)); + return (result instanceof Optional optional ? optional : Optional.ofNullable(result)); } @@ -1851,11 +1885,13 @@ public String toString() { // Serialization support //--------------------------------------------------------------------- + @Serial private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { throw new NotSerializableException("DefaultListableBeanFactory itself is not deserializable - " + "just a SerializedBeanFactoryReference is"); } + @Serial protected Object writeReplace() throws ObjectStreamException { if (this.serializationId != null) { return new SerializedBeanFactoryReference(this.serializationId); @@ -2099,8 +2135,8 @@ private Stream resolveStream(boolean ordered) { /** - * Separate inner class for avoiding a hard dependency on the {@code javax.inject} API. - * Actual {@code javax.inject.Provider} implementation is nested here in order to make it + * Separate inner class for avoiding a hard dependency on the {@code jakarta.inject} API. + * Actual {@code jakarta.inject.Provider} implementation is nested here in order to make it * invisible for Graal's introspection of DefaultListableBeanFactory's nested classes. */ private class Jsr330Factory implements Serializable { @@ -2143,20 +2179,25 @@ public FactoryAwareOrderSourceProvider(Map instancesToBeanNames) @Nullable public Object getOrderSource(Object obj) { String beanName = this.instancesToBeanNames.get(obj); - if (beanName == null || !containsBeanDefinition(beanName)) { + if (beanName == null) { return null; } - RootBeanDefinition beanDefinition = getMergedLocalBeanDefinition(beanName); - List sources = new ArrayList<>(2); - Method factoryMethod = beanDefinition.getResolvedFactoryMethod(); - if (factoryMethod != null) { - sources.add(factoryMethod); + try { + RootBeanDefinition beanDefinition = (RootBeanDefinition) getMergedBeanDefinition(beanName); + List sources = new ArrayList<>(2); + Method factoryMethod = beanDefinition.getResolvedFactoryMethod(); + if (factoryMethod != null) { + sources.add(factoryMethod); + } + Class targetType = beanDefinition.getTargetType(); + if (targetType != null && targetType != obj.getClass()) { + sources.add(targetType); + } + return sources.toArray(); } - Class targetType = beanDefinition.getTargetType(); - if (targetType != null && targetType != obj.getClass()) { - sources.add(targetType); + catch (NoSuchBeanDefinitionException ex) { + return null; } - return sources.toArray(); } } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java index fb277b62b8a6..374e65aa0d54 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -19,11 +19,6 @@ import java.io.Serializable; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.security.AccessControlContext; -import java.security.AccessController; -import java.security.PrivilegedAction; -import java.security.PrivilegedActionException; -import java.security.PrivilegedExceptionAction; import java.util.ArrayList; import java.util.List; @@ -37,6 +32,7 @@ import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; @@ -52,15 +48,18 @@ * @author Juergen Hoeller * @author Costin Leau * @author Stephane Nicoll + * @author Sam Brannen * @since 2.0 * @see AbstractBeanFactory * @see org.springframework.beans.factory.DisposableBean * @see org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor - * @see AbstractBeanDefinition#getDestroyMethodName() + * @see AbstractBeanDefinition#getDestroyMethodNames() */ @SuppressWarnings("serial") class DisposableBeanAdapter implements DisposableBean, Runnable, Serializable { + private static final String DESTROY_METHOD_NAME = "destroy"; + private static final String CLOSE_METHOD_NAME = "close"; private static final String SHUTDOWN_METHOD_NAME = "shutdown"; @@ -72,18 +71,17 @@ class DisposableBeanAdapter implements DisposableBean, Runnable, Serializable { private final String beanName; - private final boolean invokeDisposableBean; - private final boolean nonPublicAccessAllowed; - @Nullable - private final AccessControlContext acc; + private final boolean invokeDisposableBean; + + private boolean invokeAutoCloseable; @Nullable - private String destroyMethodName; + private String[] destroyMethodNames; @Nullable - private transient Method destroyMethod; + private transient Method[] destroyMethods; @Nullable private final List beanPostProcessors; @@ -98,40 +96,54 @@ class DisposableBeanAdapter implements DisposableBean, Runnable, Serializable { * (potentially DestructionAwareBeanPostProcessor), if any */ public DisposableBeanAdapter(Object bean, String beanName, RootBeanDefinition beanDefinition, - List postProcessors, @Nullable AccessControlContext acc) { + List postProcessors) { Assert.notNull(bean, "Disposable bean must not be null"); this.bean = bean; this.beanName = beanName; - this.invokeDisposableBean = - (this.bean instanceof DisposableBean && !beanDefinition.isExternallyManagedDestroyMethod("destroy")); this.nonPublicAccessAllowed = beanDefinition.isNonPublicAccessAllowed(); - this.acc = acc; - String destroyMethodName = inferDestroyMethodIfNecessary(bean, beanDefinition); - if (destroyMethodName != null && !(this.invokeDisposableBean && "destroy".equals(destroyMethodName)) && - !beanDefinition.isExternallyManagedDestroyMethod(destroyMethodName)) { - this.destroyMethodName = destroyMethodName; - Method destroyMethod = determineDestroyMethod(destroyMethodName); - if (destroyMethod == null) { - if (beanDefinition.isEnforceDestroyMethod()) { - throw new BeanDefinitionValidationException("Could not find a destroy method named '" + - destroyMethodName + "' on bean with name '" + beanName + "'"); - } - } - else { - Class[] paramTypes = destroyMethod.getParameterTypes(); - if (paramTypes.length > 1) { - throw new BeanDefinitionValidationException("Method '" + destroyMethodName + "' of bean '" + - beanName + "' has more than one parameter - not supported as destroy method"); - } - else if (paramTypes.length == 1 && boolean.class != paramTypes[0]) { - throw new BeanDefinitionValidationException("Method '" + destroyMethodName + "' of bean '" + - beanName + "' has a non-boolean parameter - not supported as destroy method"); + this.invokeDisposableBean = (bean instanceof DisposableBean && + !beanDefinition.hasAnyExternallyManagedDestroyMethod(DESTROY_METHOD_NAME)); + + String[] destroyMethodNames = inferDestroyMethodsIfNecessary(bean.getClass(), beanDefinition); + if (!ObjectUtils.isEmpty(destroyMethodNames) && + !(this.invokeDisposableBean && DESTROY_METHOD_NAME.equals(destroyMethodNames[0])) && + !beanDefinition.hasAnyExternallyManagedDestroyMethod(destroyMethodNames[0])) { + + this.invokeAutoCloseable = + (bean instanceof AutoCloseable && CLOSE_METHOD_NAME.equals(destroyMethodNames[0])); + if (!this.invokeAutoCloseable) { + this.destroyMethodNames = destroyMethodNames; + Method[] destroyMethods = new Method[destroyMethodNames.length]; + for (int i = 0; i < destroyMethodNames.length; i++) { + String destroyMethodName = destroyMethodNames[i]; + Method destroyMethod = determineDestroyMethod(destroyMethodName); + if (destroyMethod == null) { + if (beanDefinition.isEnforceDestroyMethod()) { + throw new BeanDefinitionValidationException("Could not find a destroy method named '" + + destroyMethodName + "' on bean with name '" + beanName + "'"); + } + } + else { + if (destroyMethod.getParameterCount() > 0) { + Class[] paramTypes = destroyMethod.getParameterTypes(); + if (paramTypes.length > 1) { + throw new BeanDefinitionValidationException("Method '" + destroyMethodName + "' of bean '" + + beanName + "' has more than one parameter - not supported as destroy method"); + } + else if (paramTypes.length == 1 && boolean.class != paramTypes[0]) { + throw new BeanDefinitionValidationException("Method '" + destroyMethodName + "' of bean '" + + beanName + "' has a non-boolean parameter - not supported as destroy method"); + } + } + destroyMethod = ClassUtils.getInterfaceMethodIfPossible(destroyMethod, bean.getClass()); + } + destroyMethods[i] = destroyMethod; } - destroyMethod = ClassUtils.getInterfaceMethodIfPossible(destroyMethod); + this.destroyMethods = destroyMethods; } - this.destroyMethod = destroyMethod; } + this.beanPostProcessors = filterPostProcessors(postProcessors, bean); } @@ -141,95 +153,32 @@ else if (paramTypes.length == 1 && boolean.class != paramTypes[0]) { * @param postProcessors the List of BeanPostProcessors * (potentially DestructionAwareBeanPostProcessor), if any */ - public DisposableBeanAdapter( - Object bean, List postProcessors, AccessControlContext acc) { - + public DisposableBeanAdapter(Object bean, List postProcessors) { Assert.notNull(bean, "Disposable bean must not be null"); this.bean = bean; this.beanName = bean.getClass().getName(); - this.invokeDisposableBean = (this.bean instanceof DisposableBean); this.nonPublicAccessAllowed = true; - this.acc = acc; + this.invokeDisposableBean = (this.bean instanceof DisposableBean); this.beanPostProcessors = filterPostProcessors(postProcessors, bean); } /** * Create a new DisposableBeanAdapter for the given bean. */ - private DisposableBeanAdapter(Object bean, String beanName, boolean invokeDisposableBean, - boolean nonPublicAccessAllowed, @Nullable String destroyMethodName, + private DisposableBeanAdapter(Object bean, String beanName, boolean nonPublicAccessAllowed, + boolean invokeDisposableBean, boolean invokeAutoCloseable, @Nullable String[] destroyMethodNames, @Nullable List postProcessors) { this.bean = bean; this.beanName = beanName; - this.invokeDisposableBean = invokeDisposableBean; this.nonPublicAccessAllowed = nonPublicAccessAllowed; - this.acc = null; - this.destroyMethodName = destroyMethodName; + this.invokeDisposableBean = invokeDisposableBean; + this.invokeAutoCloseable = invokeAutoCloseable; + this.destroyMethodNames = destroyMethodNames; this.beanPostProcessors = postProcessors; } - /** - * If the current value of the given beanDefinition's "destroyMethodName" property is - * {@link AbstractBeanDefinition#INFER_METHOD}, then attempt to infer a destroy method. - * Candidate methods are currently limited to public, no-arg methods named "close" or - * "shutdown" (whether declared locally or inherited). The given BeanDefinition's - * "destroyMethodName" is updated to be null if no such method is found, otherwise set - * to the name of the inferred method. This constant serves as the default for the - * {@code @Bean#destroyMethod} attribute and the value of the constant may also be - * used in XML within the {@code } or {@code - * } attributes. - *

    Also processes the {@link java.io.Closeable} and {@link java.lang.AutoCloseable} - * interfaces, reflectively calling the "close" method on implementing beans as well. - */ - @Nullable - private String inferDestroyMethodIfNecessary(Object bean, RootBeanDefinition beanDefinition) { - String destroyMethodName = beanDefinition.getDestroyMethodName(); - if (AbstractBeanDefinition.INFER_METHOD.equals(destroyMethodName) || - (destroyMethodName == null && bean instanceof AutoCloseable)) { - // Only perform destroy method inference or Closeable detection - // in case of the bean not explicitly implementing DisposableBean - if (!(bean instanceof DisposableBean)) { - try { - return bean.getClass().getMethod(CLOSE_METHOD_NAME).getName(); - } - catch (NoSuchMethodException ex) { - try { - return bean.getClass().getMethod(SHUTDOWN_METHOD_NAME).getName(); - } - catch (NoSuchMethodException ex2) { - // no candidate destroy method found - } - } - } - return null; - } - return (StringUtils.hasLength(destroyMethodName) ? destroyMethodName : null); - } - - /** - * Search for all DestructionAwareBeanPostProcessors in the List. - * @param processors the List to search - * @return the filtered List of DestructionAwareBeanPostProcessors - */ - @Nullable - private List filterPostProcessors( - List processors, Object bean) { - - List filteredPostProcessors = null; - if (!CollectionUtils.isEmpty(processors)) { - filteredPostProcessors = new ArrayList<>(processors.size()); - for (DestructionAwareBeanPostProcessor processor : processors) { - if (processor.requiresDestruction(bean)) { - filteredPostProcessors.add(processor); - } - } - } - return filteredPostProcessors; - } - - @Override public void run() { destroy(); @@ -248,18 +197,28 @@ public void destroy() { logger.trace("Invoking destroy() on bean with name '" + this.beanName + "'"); } try { - if (System.getSecurityManager() != null) { - AccessController.doPrivileged((PrivilegedExceptionAction) () -> { - ((DisposableBean) this.bean).destroy(); - return null; - }, this.acc); + ((DisposableBean) this.bean).destroy(); + } + catch (Throwable ex) { + String msg = "Invocation of destroy method failed on bean with name '" + this.beanName + "'"; + if (logger.isDebugEnabled()) { + logger.warn(msg, ex); } else { - ((DisposableBean) this.bean).destroy(); + logger.warn(msg + ": " + ex); } } + } + + if (this.invokeAutoCloseable) { + if (logger.isTraceEnabled()) { + logger.trace("Invoking close() on bean with name '" + this.beanName + "'"); + } + try { + ((AutoCloseable) this.bean).close(); + } catch (Throwable ex) { - String msg = "Invocation of destroy method failed on bean with name '" + this.beanName + "'"; + String msg = "Invocation of close method failed on bean with name '" + this.beanName + "'"; if (logger.isDebugEnabled()) { logger.warn(msg, ex); } @@ -268,14 +227,18 @@ public void destroy() { } } } - - if (this.destroyMethod != null) { - invokeCustomDestroyMethod(this.destroyMethod); + else if (this.destroyMethods != null) { + for (Method destroyMethod : this.destroyMethods) { + invokeCustomDestroyMethod(destroyMethod); + } } - else if (this.destroyMethodName != null) { - Method methodToInvoke = determineDestroyMethod(this.destroyMethodName); - if (methodToInvoke != null) { - invokeCustomDestroyMethod(ClassUtils.getInterfaceMethodIfPossible(methodToInvoke)); + else if (this.destroyMethodNames != null) { + for (String destroyMethodName: this.destroyMethodNames) { + Method destroyMethod = determineDestroyMethod(destroyMethodName); + if (destroyMethod != null) { + invokeCustomDestroyMethod( + ClassUtils.getInterfaceMethodIfPossible(destroyMethod, this.bean.getClass())); + } } } } @@ -284,12 +247,7 @@ else if (this.destroyMethodName != null) { @Nullable private Method determineDestroyMethod(String name) { try { - if (System.getSecurityManager() != null) { - return AccessController.doPrivileged((PrivilegedAction) () -> findDestroyMethod(name)); - } - else { - return findDestroyMethod(name); - } + return findDestroyMethod(name); } catch (IllegalArgumentException ex) { throw new BeanDefinitionValidationException("Could not find unique destroy method on bean with name '" + @@ -310,37 +268,22 @@ private Method findDestroyMethod(String name) { * for a method with a single boolean argument (passing in "true", * assuming a "force" parameter), else logging an error. */ - private void invokeCustomDestroyMethod(final Method destroyMethod) { + private void invokeCustomDestroyMethod(Method destroyMethod) { int paramCount = destroyMethod.getParameterCount(); final Object[] args = new Object[paramCount]; if (paramCount == 1) { args[0] = Boolean.TRUE; } if (logger.isTraceEnabled()) { - logger.trace("Invoking destroy method '" + this.destroyMethodName + + logger.trace("Invoking custom destroy method '" + destroyMethod.getName() + "' on bean with name '" + this.beanName + "'"); } try { - if (System.getSecurityManager() != null) { - AccessController.doPrivileged((PrivilegedAction) () -> { - ReflectionUtils.makeAccessible(destroyMethod); - return null; - }); - try { - AccessController.doPrivileged((PrivilegedExceptionAction) () -> - destroyMethod.invoke(this.bean, args), this.acc); - } - catch (PrivilegedActionException pax) { - throw (InvocationTargetException) pax.getException(); - } - } - else { - ReflectionUtils.makeAccessible(destroyMethod); - destroyMethod.invoke(this.bean, args); - } + ReflectionUtils.makeAccessible(destroyMethod); + destroyMethod.invoke(this.bean, args); } catch (InvocationTargetException ex) { - String msg = "Destroy method '" + this.destroyMethodName + "' on bean with name '" + + String msg = "Custom destroy method '" + destroyMethod.getName() + "' on bean with name '" + this.beanName + "' threw an exception"; if (logger.isDebugEnabled()) { logger.warn(msg, ex.getTargetException()); @@ -350,7 +293,7 @@ private void invokeCustomDestroyMethod(final Method destroyMethod) { } } catch (Throwable ex) { - logger.warn("Failed to invoke destroy method '" + this.destroyMethodName + + logger.warn("Failed to invoke custom destroy method '" + destroyMethod.getName() + "' on bean with name '" + this.beanName + "'", ex); } } @@ -370,8 +313,9 @@ protected Object writeReplace() { } } } - return new DisposableBeanAdapter(this.bean, this.beanName, this.invokeDisposableBean, - this.nonPublicAccessAllowed, this.destroyMethodName, serializablePostProcessors); + return new DisposableBeanAdapter( + this.bean, this.beanName, this.nonPublicAccessAllowed, this.invokeDisposableBean, + this.invokeAutoCloseable, this.destroyMethodNames, serializablePostProcessors); } @@ -381,15 +325,62 @@ protected Object writeReplace() { * @param beanDefinition the corresponding bean definition */ public static boolean hasDestroyMethod(Object bean, RootBeanDefinition beanDefinition) { - if (bean instanceof DisposableBean || bean instanceof AutoCloseable) { - return true; + return (bean instanceof DisposableBean + || inferDestroyMethodsIfNecessary(bean.getClass(), beanDefinition) != null); + } + + + /** + * If the current value of the given beanDefinition's "destroyMethodName" property is + * {@link AbstractBeanDefinition#INFER_METHOD}, then attempt to infer a destroy method. + * Candidate methods are currently limited to public, no-arg methods named "close" or + * "shutdown" (whether declared locally or inherited). The given BeanDefinition's + * "destroyMethodName" is updated to be null if no such method is found, otherwise set + * to the name of the inferred method. This constant serves as the default for the + * {@code @Bean#destroyMethod} attribute and the value of the constant may also be + * used in XML within the {@code } or {@code + * } attributes. + *

    Also processes the {@link java.io.Closeable} and {@link java.lang.AutoCloseable} + * interfaces, reflectively calling the "close" method on implementing beans as well. + */ + @Nullable + static String[] inferDestroyMethodsIfNecessary(Class target, RootBeanDefinition beanDefinition) { + String[] destroyMethodNames = beanDefinition.getDestroyMethodNames(); + if (destroyMethodNames != null && destroyMethodNames.length > 1) { + return destroyMethodNames; } - String destroyMethodName = beanDefinition.getDestroyMethodName(); - if (AbstractBeanDefinition.INFER_METHOD.equals(destroyMethodName)) { - return (ClassUtils.hasMethod(bean.getClass(), CLOSE_METHOD_NAME) || - ClassUtils.hasMethod(bean.getClass(), SHUTDOWN_METHOD_NAME)); + + String destroyMethodName = beanDefinition.resolvedDestroyMethodName; + if (destroyMethodName == null) { + destroyMethodName = beanDefinition.getDestroyMethodName(); + boolean autoCloseable = (AutoCloseable.class.isAssignableFrom(target)); + if (AbstractBeanDefinition.INFER_METHOD.equals(destroyMethodName) || + (destroyMethodName == null && autoCloseable)) { + // Only perform destroy method inference in case of the bean + // not explicitly implementing the DisposableBean interface + destroyMethodName = null; + if (!(DisposableBean.class.isAssignableFrom(target))) { + if (autoCloseable) { + destroyMethodName = CLOSE_METHOD_NAME; + } + else { + try { + destroyMethodName = target.getMethod(CLOSE_METHOD_NAME).getName(); + } + catch (NoSuchMethodException ex) { + try { + destroyMethodName = target.getMethod(SHUTDOWN_METHOD_NAME).getName(); + } + catch (NoSuchMethodException ex2) { + // no candidate destroy method found + } + } + } + } + } + beanDefinition.resolvedDestroyMethodName = (destroyMethodName != null ? destroyMethodName : ""); } - return StringUtils.hasLength(destroyMethodName); + return (StringUtils.hasLength(destroyMethodName) ? new String[] {destroyMethodName} : null); } /** @@ -408,4 +399,25 @@ public static boolean hasApplicableProcessors(Object bean, List filterPostProcessors( + List processors, Object bean) { + + List filteredPostProcessors = null; + if (!CollectionUtils.isEmpty(processors)) { + filteredPostProcessors = new ArrayList<>(processors.size()); + for (DestructionAwareBeanPostProcessor processor : processors) { + if (processor.requiresDestruction(bean)) { + filteredPostProcessors.add(processor); + } + } + } + return filteredPostProcessors; + } + } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/FactoryBeanRegistrySupport.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/FactoryBeanRegistrySupport.java index fc689a7aec32..6750414847c7 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/FactoryBeanRegistrySupport.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/FactoryBeanRegistrySupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -16,11 +16,6 @@ package org.springframework.beans.factory.support; -import java.security.AccessControlContext; -import java.security.AccessController; -import java.security.PrivilegedAction; -import java.security.PrivilegedActionException; -import java.security.PrivilegedExceptionAction; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -56,13 +51,7 @@ public abstract class FactoryBeanRegistrySupport extends DefaultSingletonBeanReg @Nullable protected Class getTypeForFactoryBean(FactoryBean factoryBean) { try { - if (System.getSecurityManager() != null) { - return AccessController.doPrivileged( - (PrivilegedAction>) factoryBean::getObjectType, getAccessControlContext()); - } - else { - return factoryBean.getObjectType(); - } + return factoryBean.getObjectType(); } catch (Throwable ex) { // Thrown from the FactoryBean's getObjectType implementation. @@ -156,18 +145,7 @@ protected Object getObjectFromFactoryBean(FactoryBean factory, String beanNam private Object doGetObjectFromFactoryBean(FactoryBean factory, String beanName) throws BeanCreationException { Object object; try { - if (System.getSecurityManager() != null) { - AccessControlContext acc = getAccessControlContext(); - try { - object = AccessController.doPrivileged((PrivilegedExceptionAction) factory::getObject, acc); - } - catch (PrivilegedActionException pae) { - throw pae.getException(); - } - } - else { - object = factory.getObject(); - } + object = factory.getObject(); } catch (FactoryBeanNotInitializedException ex) { throw new BeanCurrentlyInCreationException(beanName, ex.toString()); @@ -239,14 +217,4 @@ protected void clearSingletonCache() { } } - /** - * Return the security context for this bean factory. If a security manager - * is set, interaction with the user code will be executed using the privileged - * of the security context returned by this method. - * @see AccessController#getContext() - */ - protected AccessControlContext getAccessControlContext() { - return AccessController.getContext(); - } - } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/GenericBeanDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/GenericBeanDefinition.java index ca779d1e66e9..b0a40b332ae3 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/GenericBeanDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/GenericBeanDefinition.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -88,10 +88,9 @@ public boolean equals(@Nullable Object other) { if (this == other) { return true; } - if (!(other instanceof GenericBeanDefinition)) { + if (!(other instanceof GenericBeanDefinition that)) { return false; } - GenericBeanDefinition that = (GenericBeanDefinition) other; return (ObjectUtils.nullSafeEquals(this.parentName, that.parentName) && super.equals(other)); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/GenericTypeAwareAutowireCandidateResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/GenericTypeAwareAutowireCandidateResolver.java index 01b81450903d..960f86b7fdb1 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/GenericTypeAwareAutowireCandidateResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/GenericTypeAwareAutowireCandidateResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -33,7 +33,7 @@ /** * Basic {@link AutowireCandidateResolver} that performs a full generic type * match with the candidate's type if the dependency is declared as a generic type - * (e.g. Repository<Customer>). + * (e.g. {@code Repository}). * *

    This is the base class for * {@link org.springframework.beans.factory.annotation.QualifierAnnotationAutowireCandidateResolver}, @@ -102,6 +102,22 @@ protected boolean checkGenericTypeMatch(BeanDefinitionHolder bdHolder, Dependenc } } } + else { + // Pre-existing target type: In case of a generic FactoryBean type, + // unwrap nested generic type when matching a non-FactoryBean type. + Class resolvedClass = targetType.resolve(); + if (resolvedClass != null && FactoryBean.class.isAssignableFrom(resolvedClass)) { + Class typeToBeMatched = dependencyType.resolve(); + if (typeToBeMatched != null && !FactoryBean.class.isAssignableFrom(typeToBeMatched)) { + targetType = targetType.getGeneric(); + if (descriptor.fallbackMatchAllowed()) { + // Matching the Class-based type determination for FactoryBean + // objects in the lazy-determination getType code path below. + targetType = ResolvableType.forClass(targetType.resolve()); + } + } + } + } } if (targetType == null) { @@ -142,8 +158,7 @@ protected boolean checkGenericTypeMatch(BeanDefinitionHolder bdHolder, Dependenc @Nullable protected RootBeanDefinition getResolvedDecoratedDefinition(RootBeanDefinition rbd) { BeanDefinitionHolder decDef = rbd.getDecoratedDefinition(); - if (decDef != null && this.beanFactory instanceof ConfigurableListableBeanFactory) { - ConfigurableListableBeanFactory clbf = (ConfigurableListableBeanFactory) this.beanFactory; + if (decDef != null && this.beanFactory instanceof ConfigurableListableBeanFactory clbf) { if (clbf.containsBeanDefinition(decDef.getBeanName())) { BeanDefinition dbd = clbf.getMergedBeanDefinition(decDef.getBeanName()); if (dbd instanceof RootBeanDefinition) { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/InstanceSupplier.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/InstanceSupplier.java new file mode 100644 index 000000000000..4938c0e41555 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/InstanceSupplier.java @@ -0,0 +1,150 @@ +/* + * Copyright 2002-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.beans.factory.support; + +import java.lang.reflect.Method; +import java.util.function.Supplier; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.function.ThrowingBiFunction; +import org.springframework.util.function.ThrowingSupplier; + +/** + * Specialized {@link Supplier} that can be set on a + * {@link AbstractBeanDefinition#setInstanceSupplier(Supplier) BeanDefinition} + * when details about the {@link RegisteredBean registered bean} are needed to + * supply the instance. + * + * @author Phillip Webb + * @author Stephane Nicoll + * @since 6.0 + * @param the type of instance supplied by this supplier + * @see RegisteredBean + */ +@FunctionalInterface +public interface InstanceSupplier extends ThrowingSupplier { + + @Override + default T getWithException() { + throw new IllegalStateException("No RegisteredBean parameter provided"); + } + + /** + * Get the supplied instance. + * @param registeredBean the registered bean requesting the instance + * @return the supplied instance + * @throws Exception on error + */ + T get(RegisteredBean registeredBean) throws Exception; + + /** + * Return the factory method that this supplier uses to create the + * instance, or {@code null} if it is not known or this supplier uses + * another means. + * @return the factory method used to create the instance, or {@code null} + */ + @Nullable + default Method getFactoryMethod() { + return null; + } + + /** + * Return a composed instance supplier that first obtains the instance from + * this supplier and then applies the {@code after} function to obtain the + * result. + * @param the type of output of the {@code after} function, and of the + * composed function + * @param after the function to apply after the instance is obtained + * @return a composed instance supplier + */ + default InstanceSupplier andThen( + ThrowingBiFunction after) { + Assert.notNull(after, "'after' function must not be null"); + return new InstanceSupplier() { + + @Override + public V get(RegisteredBean registeredBean) throws Exception { + return after.applyWithException(registeredBean, InstanceSupplier.this.get(registeredBean)); + } + + @Override + public Method getFactoryMethod() { + return InstanceSupplier.this.getFactoryMethod(); + } + + }; + } + + /** + * Factory method to create an {@link InstanceSupplier} from a + * {@link ThrowingSupplier}. + * @param the type of instance supplied by this supplier + * @param supplier the source supplier + * @return a new {@link InstanceSupplier} + */ + static InstanceSupplier using(ThrowingSupplier supplier) { + Assert.notNull(supplier, "Supplier must not be null"); + if (supplier instanceof InstanceSupplier instanceSupplier) { + return instanceSupplier; + } + return registeredBean -> supplier.getWithException(); + } + + /** + * Factory method to create an {@link InstanceSupplier} from a + * {@link ThrowingSupplier}. + * @param the type of instance supplied by this supplier + * @param factoryMethod the factory method being used + * @param supplier the source supplier + * @return a new {@link InstanceSupplier} + */ + static InstanceSupplier using(@Nullable Method factoryMethod, ThrowingSupplier supplier) { + Assert.notNull(supplier, "Supplier must not be null"); + if (supplier instanceof InstanceSupplier instanceSupplier + && instanceSupplier.getFactoryMethod() == factoryMethod) { + return instanceSupplier; + } + return new InstanceSupplier() { + + @Override + public T get(RegisteredBean registeredBean) throws Exception { + return supplier.getWithException(); + } + + @Override + public Method getFactoryMethod() { + return factoryMethod; + } + + }; + } + + /** + * Lambda friendly method that can be used to create an + * {@link InstanceSupplier} and add post processors in a single call. For + * example: {@code InstanceSupplier.of(registeredBean -> ...).andThen(...)}. + * @param the type of instance supplied by this supplier + * @param instanceSupplier the source instance supplier + * @return a new {@link InstanceSupplier} + */ + static InstanceSupplier of(InstanceSupplier instanceSupplier) { + Assert.notNull(instanceSupplier, "InstanceSupplier must not be null"); + return instanceSupplier; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/InstantiationStrategy.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/InstantiationStrategy.java index e9179585fc88..450d85aa9ec3 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/InstantiationStrategy.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/InstantiationStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -83,4 +83,12 @@ Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory @Nullable Object factoryBean, Method factoryMethod, Object... args) throws BeansException; + /** + * Determine the actual class for the given bean definition, as instantiated at runtime. + * @since 6.0 + */ + default Class getActualBeanClass(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner) { + return bd.getBeanClass(); + } + } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/LookupOverride.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/LookupOverride.java index b424c38bb087..faa7478a8198 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/LookupOverride.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/LookupOverride.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-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. @@ -19,17 +19,25 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import org.springframework.core.ResolvableType; import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; /** - * Represents an override of a method that looks up an object in the same IoC context. + * Represents an override of a method that looks up an object in the same IoC context, + * either by bean name or by bean type (based on the declared method return type). * - *

    Methods eligible for lookup override must not have arguments. + *

    Methods eligible for lookup override may declare arguments in which case the + * given arguments are passed to the bean retrieval operation. * * @author Rod Johnson * @author Juergen Hoeller * @since 1.1 + * @see org.springframework.beans.factory.BeanFactory#getBean(String) + * @see org.springframework.beans.factory.BeanFactory#getBean(Class) + * @see org.springframework.beans.factory.BeanFactory#getBean(String, Object...) + * @see org.springframework.beans.factory.BeanFactory#getBean(Class, Object...) + * @see org.springframework.beans.factory.BeanFactory#getBeanProvider(ResolvableType) */ public class LookupOverride extends MethodOverride { @@ -43,8 +51,8 @@ public class LookupOverride extends MethodOverride { /** * Construct a new LookupOverride. * @param methodName the name of the method to override - * @param beanName the name of the bean in the current {@code BeanFactory} - * that the overridden method should return (may be {@code null}) + * @param beanName the name of the bean in the current {@code BeanFactory} that the + * overridden method should return (may be {@code null} for type-based bean retrieval) */ public LookupOverride(String methodName, @Nullable String beanName) { super(methodName); @@ -53,9 +61,9 @@ public LookupOverride(String methodName, @Nullable String beanName) { /** * Construct a new LookupOverride. - * @param method the method to override - * @param beanName the name of the bean in the current {@code BeanFactory} - * that the overridden method should return (may be {@code null}) + * @param method the method declaration to override + * @param beanName the name of the bean in the current {@code BeanFactory} that the + * overridden method should return (may be {@code null} for type-based bean retrieval) */ public LookupOverride(Method method, @Nullable String beanName) { super(method.getName()); @@ -94,10 +102,9 @@ public boolean matches(Method method) { @Override public boolean equals(@Nullable Object other) { - if (!(other instanceof LookupOverride) || !super.equals(other)) { + if (!(other instanceof LookupOverride that) || !super.equals(other)) { return false; } - LookupOverride that = (LookupOverride) other; return (ObjectUtils.nullSafeEquals(this.method, that.method) && ObjectUtils.nullSafeEquals(this.beanName, that.beanName)); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/ManagedList.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/ManagedList.java index 7a282ed7fce6..2b0a25a91396 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/ManagedList.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/ManagedList.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -17,6 +17,7 @@ package org.springframework.beans.factory.support; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import org.springframework.beans.BeanMetadataElement; @@ -30,6 +31,8 @@ * @author Rod Johnson * @author Rob Harrop * @author Juergen Hoeller + * @author Stephane Nicoll + * @author Sam Brannen * @since 27.05.2003 * @param the element type */ @@ -53,6 +56,21 @@ public ManagedList(int initialCapacity) { } + /** + * Create a new instance containing an arbitrary number of elements. + * @param elements the elements to be contained in the list + * @param the {@code List}'s element type + * @return a {@code ManagedList} containing the specified elements + * @since 5.3.16 + */ + @SafeVarargs + @SuppressWarnings("varargs") + public static ManagedList of(E... elements) { + ManagedList list = new ManagedList<>(); + Collections.addAll(list, elements); + return list; + } + /** * Set the configuration source {@code Object} for this metadata element. *

    The exact type of the object will depend on the configuration mechanism used. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/ManagedMap.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/ManagedMap.java index 55b76f0317f0..b0eef75e3efa 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/ManagedMap.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/ManagedMap.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -18,6 +18,7 @@ import java.util.LinkedHashMap; import java.util.Map; +import java.util.Map.Entry; import org.springframework.beans.BeanMetadataElement; import org.springframework.beans.Mergeable; @@ -56,6 +57,26 @@ public ManagedMap(int initialCapacity) { } + /** + * Return a new instance containing keys and values extracted from the + * given entries. The entries themselves are not stored in the map. + * @param entries {@code Map.Entry}s containing the keys and values + * from which the map is populated + * @param the {@code Map}'s key type + * @param the {@code Map}'s value type + * @return a {@code Map} containing the specified mappings + * @since 5.3.16 + */ + @SafeVarargs + @SuppressWarnings("unchecked") + public static ManagedMap ofEntries(Entry... entries) { + ManagedMap map = new ManagedMap<>(); + for (Entry entry : entries) { + map.put(entry.getKey(), entry.getValue()); + } + return map; + } + /** * Set the configuration source {@code Object} for this metadata element. *

    The exact type of the object will depend on the configuration mechanism used. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/ManagedSet.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/ManagedSet.java index 7f84ec7f4456..1381dde65152 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/ManagedSet.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/ManagedSet.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -16,6 +16,7 @@ package org.springframework.beans.factory.support; +import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; @@ -29,6 +30,8 @@ * * @author Juergen Hoeller * @author Rob Harrop + * @author Stephane Nicoll + * @author Sam Brannen * @since 21.01.2004 * @param the element type */ @@ -52,6 +55,21 @@ public ManagedSet(int initialCapacity) { } + /** + * Create a new instance containing an arbitrary number of elements. + * @param elements the elements to be contained in the set + * @param the {@code Set}'s element type + * @return a {@code ManagedSet} containing the specified elements + * @since 5.3.16 + */ + @SafeVarargs + @SuppressWarnings("varargs") + public static ManagedSet of(E... elements) { + ManagedSet set = new ManagedSet<>(); + Collections.addAll(set, elements); + return set; + } + /** * Set the configuration source {@code Object} for this metadata element. *

    The exact type of the object will depend on the configuration mechanism used. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodOverride.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodOverride.java index 24a2056cf195..0107e104c213 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodOverride.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodOverride.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-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. @@ -109,10 +109,9 @@ public boolean equals(@Nullable Object other) { if (this == other) { return true; } - if (!(other instanceof MethodOverride)) { + if (!(other instanceof MethodOverride that)) { return false; } - MethodOverride that = (MethodOverride) other; return (ObjectUtils.nullSafeEquals(this.methodName, that.methodName) && ObjectUtils.nullSafeEquals(this.source, that.source)); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodOverrides.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodOverrides.java index a84a15f83020..07dc3a6b0db9 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodOverrides.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodOverrides.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -107,10 +107,9 @@ public boolean equals(@Nullable Object other) { if (this == other) { return true; } - if (!(other instanceof MethodOverrides)) { + if (!(other instanceof MethodOverrides that)) { return false; } - MethodOverrides that = (MethodOverrides) other; return this.overrides.equals(that.overrides); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodReplacer.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodReplacer.java index 5677d944ff3f..e4e5df879e5b 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodReplacer.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodReplacer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-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. @@ -19,9 +19,8 @@ import java.lang.reflect.Method; /** - * Interface to be implemented by classes that can reimplement any method - * on an IoC-managed object: the Method Injection form of - * Dependency Injection. + * Interface to be implemented by classes that can reimplement any method on an + * IoC-managed object: the Method Injection form of Dependency Injection. * *

    Such methods may be (but need not be) abstract, in which case the * container will create a concrete subclass to instantiate. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/PropertiesBeanDefinitionReader.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/PropertiesBeanDefinitionReader.java index 617a13be564b..e85e080a3d3e 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/PropertiesBeanDefinitionReader.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/PropertiesBeanDefinitionReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -35,8 +35,8 @@ import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.core.io.Resource; import org.springframework.core.io.support.EncodedResource; -import org.springframework.core.io.support.ResourcePropertiesPersister; import org.springframework.lang.Nullable; +import org.springframework.util.DefaultPropertiesPersister; import org.springframework.util.PropertiesPersister; import org.springframework.util.StringUtils; @@ -148,7 +148,7 @@ public class PropertiesBeanDefinitionReader extends AbstractBeanDefinitionReader @Nullable private String defaultParentBean; - private PropertiesPersister propertiesPersister = ResourcePropertiesPersister.INSTANCE; + private PropertiesPersister propertiesPersister = DefaultPropertiesPersister.INSTANCE; /** @@ -187,12 +187,12 @@ public String getDefaultParentBean() { /** * Set the PropertiesPersister to use for parsing properties files. - * The default is ResourcePropertiesPersister. - * @see ResourcePropertiesPersister#INSTANCE + * The default is {@code DefaultPropertiesPersister}. + * @see DefaultPropertiesPersister#INSTANCE */ public void setPropertiesPersister(@Nullable PropertiesPersister propertiesPersister) { this.propertiesPersister = - (propertiesPersister != null ? propertiesPersister : ResourcePropertiesPersister.INSTANCE); + (propertiesPersister != null ? propertiesPersister : DefaultPropertiesPersister.INSTANCE); } /** @@ -363,10 +363,9 @@ public int registerBeanDefinitions(Map map, @Nullable String prefix, Strin int beanCount = 0; for (Object key : map.keySet()) { - if (!(key instanceof String)) { + if (!(key instanceof String keyString)) { throw new IllegalArgumentException("Illegal key [" + key + "]: only Strings allowed"); } - String keyString = (String) key; if (keyString.startsWith(prefix)) { // Key is of form: prefix.property String nameAndProperty = keyString.substring(prefix.length()); @@ -429,40 +428,40 @@ protected void registerBeanDefinition(String beanName, Map map, String pre int beginIndex = prefixWithSep.length(); for (Map.Entry entry : map.entrySet()) { - String key = StringUtils.trimWhitespace((String) entry.getKey()); + String key = ((String) entry.getKey()).strip(); if (key.startsWith(prefixWithSep)) { String property = key.substring(beginIndex); if (CLASS_KEY.equals(property)) { - className = StringUtils.trimWhitespace((String) entry.getValue()); + className = ((String) entry.getValue()).strip(); } else if (PARENT_KEY.equals(property)) { - parent = StringUtils.trimWhitespace((String) entry.getValue()); + parent = ((String) entry.getValue()).strip(); } else if (ABSTRACT_KEY.equals(property)) { - String val = StringUtils.trimWhitespace((String) entry.getValue()); + String val = ((String) entry.getValue()).strip(); isAbstract = TRUE_VALUE.equals(val); } else if (SCOPE_KEY.equals(property)) { // Spring 2.0 style - scope = StringUtils.trimWhitespace((String) entry.getValue()); + scope = ((String) entry.getValue()).strip(); } else if (SINGLETON_KEY.equals(property)) { // Spring 1.2 style - String val = StringUtils.trimWhitespace((String) entry.getValue()); + String val = ((String) entry.getValue()).strip(); scope = (!StringUtils.hasLength(val) || TRUE_VALUE.equals(val) ? BeanDefinition.SCOPE_SINGLETON : BeanDefinition.SCOPE_PROTOTYPE); } else if (LAZY_INIT_KEY.equals(property)) { - String val = StringUtils.trimWhitespace((String) entry.getValue()); + String val = ((String) entry.getValue()).strip(); lazyInit = TRUE_VALUE.equals(val); } else if (property.startsWith(CONSTRUCTOR_ARG_PREFIX)) { if (property.endsWith(REF_SUFFIX)) { - int index = Integer.parseInt(property.substring(1, property.length() - REF_SUFFIX.length())); + int index = Integer.parseInt(property, 1, property.length() - REF_SUFFIX.length(), 10); cas.addIndexedArgumentValue(index, new RuntimeBeanReference(entry.getValue().toString())); } else { - int index = Integer.parseInt(property.substring(1)); + int index = Integer.parseInt(property, 1, property.length(), 10); cas.addIndexedArgumentValue(index, readValue(entry)); } } @@ -470,7 +469,7 @@ else if (property.endsWith(REF_SUFFIX)) { // This isn't a real property, but a reference to another prototype // Extract property name: property is of form dog(ref) property = property.substring(0, property.length() - REF_SUFFIX.length()); - String ref = StringUtils.trimWhitespace((String) entry.getValue()); + String ref = ((String) entry.getValue()).strip(); // It doesn't matter if the referenced bean hasn't yet been registered: // this will ensure that the reference is resolved at runtime. @@ -519,8 +518,7 @@ else if (property.endsWith(REF_SUFFIX)) { */ private Object readValue(Map.Entry entry) { Object val = entry.getValue(); - if (val instanceof String) { - String strVal = (String) val; + if (val instanceof String strVal) { // If it starts with a reference prefix... if (strVal.startsWith(REF_PREFIX)) { // Expand the reference. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/RegisteredBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/RegisteredBean.java new file mode 100644 index 000000000000..06a17bd563c3 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/RegisteredBean.java @@ -0,0 +1,264 @@ +/* + * Copyright 2002-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.beans.factory.support; + +import java.lang.reflect.Executable; +import java.util.function.BiFunction; +import java.util.function.Supplier; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.core.ResolvableType; +import org.springframework.core.style.ToStringCreator; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * A {@code RegisteredBean} represents a bean that has been registered with a + * {@link BeanFactory}, but has not necessarily been instantiated. It provides + * access to the bean factory that contains the bean as well as the bean name. + * In the case of inner-beans, the bean name may have been generated. + * + * @author Phillip Webb + * @since 6.0 + */ +public final class RegisteredBean { + + private final ConfigurableListableBeanFactory beanFactory; + + private final Supplier beanName; + + private final boolean generatedBeanName; + + private final Supplier mergedBeanDefinition; + + @Nullable + private final RegisteredBean parent; + + + private RegisteredBean(ConfigurableListableBeanFactory beanFactory, Supplier beanName, + boolean generatedBeanName, Supplier mergedBeanDefinition, + @Nullable RegisteredBean parent) { + + this.beanFactory = beanFactory; + this.beanName = beanName; + this.generatedBeanName = generatedBeanName; + this.mergedBeanDefinition = mergedBeanDefinition; + this.parent = parent; + } + + + /** + * Create a new {@link RegisteredBean} instance for a regular bean. + * @param beanFactory the source bean factory + * @param beanName the bean name + * @return a new {@link RegisteredBean} instance + */ + public static RegisteredBean of(ConfigurableListableBeanFactory beanFactory, String beanName) { + Assert.notNull(beanFactory, "'beanFactory' must not be null"); + Assert.hasLength(beanName, "'beanName' must not be empty"); + return new RegisteredBean(beanFactory, () -> beanName, false, + () -> (RootBeanDefinition) beanFactory.getMergedBeanDefinition(beanName), + null); + } + + /** + * Create a new {@link RegisteredBean} instance for an inner-bean. + * @param parent the parent of the inner-bean + * @param innerBean a {@link BeanDefinitionHolder} for the inner bean + * @return a new {@link RegisteredBean} instance + */ + public static RegisteredBean ofInnerBean(RegisteredBean parent, BeanDefinitionHolder innerBean) { + Assert.notNull(innerBean, "'innerBean' must not be null"); + return ofInnerBean(parent, innerBean.getBeanName(), innerBean.getBeanDefinition()); + } + + /** + * Create a new {@link RegisteredBean} instance for an inner-bean. + * @param parent the parent of the inner-bean + * @param innerBeanDefinition the inner-bean definition + * @return a new {@link RegisteredBean} instance + */ + public static RegisteredBean ofInnerBean(RegisteredBean parent, BeanDefinition innerBeanDefinition) { + return ofInnerBean(parent, null, innerBeanDefinition); + } + + /** + * Create a new {@link RegisteredBean} instance for an inner-bean. + * @param parent the parent of the inner-bean + * @param innerBeanName the name of the inner bean or {@code null} to + * generate a name + * @param innerBeanDefinition the inner-bean definition + * @return a new {@link RegisteredBean} instance + */ + public static RegisteredBean ofInnerBean(RegisteredBean parent, + @Nullable String innerBeanName, BeanDefinition innerBeanDefinition) { + + Assert.notNull(parent, "'parent' must not be null"); + Assert.notNull(innerBeanDefinition, "'innerBeanDefinition' must not be null"); + InnerBeanResolver resolver = new InnerBeanResolver(parent, innerBeanName, innerBeanDefinition); + Supplier beanName = (StringUtils.hasLength(innerBeanName) ? + () -> innerBeanName : resolver::resolveBeanName); + return new RegisteredBean(parent.getBeanFactory(), beanName, + innerBeanName == null, resolver::resolveMergedBeanDefinition, parent); + } + + + /** + * Return the name of the bean. + * @return the beanName the bean name + */ + public String getBeanName() { + return this.beanName.get(); + } + + /** + * Return if the bean name is generated. + * @return {@code true} if the name was generated + */ + public boolean isGeneratedBeanName() { + return this.generatedBeanName; + } + + /** + * Return the bean factory containing the bean. + * @return the bean factory + */ + public ConfigurableListableBeanFactory getBeanFactory() { + return this.beanFactory; + } + + /** + * Return the user-defined class of the bean. + * @return the bean class + */ + public Class getBeanClass() { + return ClassUtils.getUserClass(getBeanType().toClass()); + } + + /** + * Return the {@link ResolvableType} of the bean. + * @return the bean type + */ + public ResolvableType getBeanType() { + return getMergedBeanDefinition().getResolvableType(); + } + + /** + * Return the merged bean definition of the bean. + * @return the merged bean definition + * @see ConfigurableListableBeanFactory#getMergedBeanDefinition(String) + */ + public RootBeanDefinition getMergedBeanDefinition() { + return this.mergedBeanDefinition.get(); + } + + /** + * Return if this instance is for an inner-bean. + * @return if an inner-bean + */ + public boolean isInnerBean() { + return this.parent != null; + } + + /** + * Return the parent of this instance or {@code null} if not an inner-bean. + * @return the parent + */ + @Nullable + public RegisteredBean getParent() { + return this.parent; + } + + /** + * Resolve the constructor or factory method to use for this bean. + * @return the {@link java.lang.reflect.Constructor} or {@link java.lang.reflect.Method} + */ + public Executable resolveConstructorOrFactoryMethod() { + return new ConstructorResolver((AbstractAutowireCapableBeanFactory) getBeanFactory()) + .resolveConstructorOrFactoryMethod(getBeanName(), getMergedBeanDefinition()); + } + + + @Override + public String toString() { + return new ToStringCreator(this).append("beanName", getBeanName()) + .append("mergedBeanDefinition", getMergedBeanDefinition()).toString(); + } + + + /** + * Resolver used to obtain inner-bean details. + */ + private static class InnerBeanResolver { + + private final RegisteredBean parent; + + @Nullable + private final String innerBeanName; + + private final BeanDefinition innerBeanDefinition; + + @Nullable + private volatile String resolvedBeanName; + + + InnerBeanResolver(RegisteredBean parent, @Nullable String innerBeanName, + BeanDefinition innerBeanDefinition) { + + Assert.isInstanceOf(AbstractAutowireCapableBeanFactory.class, + parent.getBeanFactory()); + this.parent = parent; + this.innerBeanName = innerBeanName; + this.innerBeanDefinition = innerBeanDefinition; + } + + + String resolveBeanName() { + String resolvedBeanName = this.resolvedBeanName; + if (resolvedBeanName != null) { + return resolvedBeanName; + } + resolvedBeanName = resolveInnerBean( + (beanName, mergedBeanDefinition) -> beanName); + this.resolvedBeanName = resolvedBeanName; + return resolvedBeanName; + } + + RootBeanDefinition resolveMergedBeanDefinition() { + return resolveInnerBean( + (beanName, mergedBeanDefinition) -> mergedBeanDefinition); + } + + private T resolveInnerBean( + BiFunction resolver) { + + // Always use a fresh BeanDefinitionValueResolver in case the parent merged bean definition has changed. + BeanDefinitionValueResolver beanDefinitionValueResolver = new BeanDefinitionValueResolver( + (AbstractAutowireCapableBeanFactory) this.parent.getBeanFactory(), + this.parent.getBeanName(), this.parent.getMergedBeanDefinition()); + return beanDefinitionValueResolver.resolveInnerBean(this.innerBeanName, + this.innerBeanDefinition, resolver); + } + + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/ReplaceOverride.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/ReplaceOverride.java index 1b51bf706553..a702b7d7af1a 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/ReplaceOverride.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/ReplaceOverride.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -25,7 +25,7 @@ import org.springframework.util.ObjectUtils; /** - * Extension of MethodOverride that represents an arbitrary + * Extension of {@link MethodOverride} that represents an arbitrary * override of a method by the IoC container. * *

    Any non-final method can be overridden, irrespective of its @@ -45,7 +45,7 @@ public class ReplaceOverride extends MethodOverride { /** * Construct a new ReplaceOverride. * @param methodName the name of the method to override - * @param methodReplacerBeanName the bean name of the MethodReplacer + * @param methodReplacerBeanName the bean name of the {@link MethodReplacer} */ public ReplaceOverride(String methodName, String methodReplacerBeanName) { super(methodName); @@ -97,10 +97,9 @@ public boolean matches(Method method) { @Override public boolean equals(@Nullable Object other) { - if (!(other instanceof ReplaceOverride) || !super.equals(other)) { + if (!(other instanceof ReplaceOverride that) || !super.equals(other)) { return false; } - ReplaceOverride that = (ReplaceOverride) other; return (ObjectUtils.nullSafeEquals(this.methodReplacerBeanName, that.methodReplacerBeanName) && ObjectUtils.nullSafeEquals(this.typeIdentifiers, that.typeIdentifiers)); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/RootBeanDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/RootBeanDefinition.java index d70c8040bde4..8dd6a0efbe3a 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/RootBeanDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/RootBeanDefinition.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -21,7 +21,8 @@ import java.lang.reflect.Executable; import java.lang.reflect.Member; import java.lang.reflect.Method; -import java.util.HashSet; +import java.util.Collections; +import java.util.LinkedHashSet; import java.util.Set; import java.util.function.Supplier; @@ -48,6 +49,7 @@ * * @author Rod Johnson * @author Juergen Hoeller + * @author Sam Brannen * @see GenericBeanDefinition * @see ChildBeanDefinition */ @@ -86,6 +88,10 @@ public class RootBeanDefinition extends AbstractBeanDefinition { @Nullable volatile Method factoryMethodToIntrospect; + /** Package-visible field for caching a resolved destroy method name (also for inferred). */ + @Nullable + volatile String resolvedDestroyMethodName; + /** Common lock for the four constructor fields below. */ final Object constructorArgumentLock = new Object(); @@ -133,7 +139,6 @@ public class RootBeanDefinition extends AbstractBeanDefinition { * @see #setPropertyValues */ public RootBeanDefinition() { - super(); } /** @@ -142,10 +147,19 @@ public RootBeanDefinition() { * @see #setBeanClass */ public RootBeanDefinition(@Nullable Class beanClass) { - super(); setBeanClass(beanClass); } + /** + * Create a new RootBeanDefinition for a singleton. + * @param beanType the type of bean to instantiate + * @since 6.0 + * @see #setTargetType(ResolvableType) + */ + public RootBeanDefinition(@Nullable ResolvableType beanType) { + setTargetType(beanType); + } + /** * Create a new RootBeanDefinition for a singleton bean, constructing each instance * through calling the given supplier (possibly a lambda or method reference). @@ -156,7 +170,6 @@ public RootBeanDefinition(@Nullable Class beanClass) { * @see #setInstanceSupplier */ public RootBeanDefinition(@Nullable Class beanClass, @Nullable Supplier instanceSupplier) { - super(); setBeanClass(beanClass); setInstanceSupplier(instanceSupplier); } @@ -172,7 +185,6 @@ public RootBeanDefinition(@Nullable Class beanClass, @Nullable Supplier RootBeanDefinition(@Nullable Class beanClass, String scope, @Nullable Supplier instanceSupplier) { - super(); setBeanClass(beanClass); setScope(scope); setInstanceSupplier(instanceSupplier); @@ -187,7 +199,6 @@ public RootBeanDefinition(@Nullable Class beanClass, String scope, @Nulla * (not applicable to autowiring a constructor, thus ignored there) */ public RootBeanDefinition(@Nullable Class beanClass, int autowireMode, boolean dependencyCheck) { - super(); setBeanClass(beanClass); setAutowireMode(autowireMode); if (dependencyCheck && getResolvedAutowireMode() != AUTOWIRE_CONSTRUCTOR) { @@ -309,7 +320,7 @@ public AnnotatedElement getQualifiedElement() { * Specify a generics-containing target type of this bean definition, if known in advance. * @since 4.3.3 */ - public void setTargetType(ResolvableType targetType) { + public void setTargetType(@Nullable ResolvableType targetType) { this.targetType = targetType; } @@ -407,6 +418,9 @@ public boolean isFactoryMethod(Method candidate) { */ public void setResolvedFactoryMethod(@Nullable Method method) { this.factoryMethodToIntrospect = method; + if (method != null) { + setUniqueFactoryMethodName(method.getName()); + } } /** @@ -418,15 +432,42 @@ public Method getResolvedFactoryMethod() { return this.factoryMethodToIntrospect; } + @Override + public void setInstanceSupplier(@Nullable Supplier instanceSupplier) { + super.setInstanceSupplier(instanceSupplier); + Method factoryMethod = (instanceSupplier instanceof InstanceSupplier ? + ((InstanceSupplier) instanceSupplier).getFactoryMethod() : null); + if (factoryMethod != null) { + setResolvedFactoryMethod(factoryMethod); + } + } + + /** + * Mark this bean definition as post-processed, + * i.e. processed by {@link MergedBeanDefinitionPostProcessor}. + * @since 6.0 + */ + public void markAsPostProcessed() { + synchronized (this.postProcessingLock) { + this.postProcessed = true; + } + } + + /** + * Register an externally managed configuration method or field. + */ public void registerExternallyManagedConfigMember(Member configMember) { synchronized (this.postProcessingLock) { if (this.externallyManagedConfigMembers == null) { - this.externallyManagedConfigMembers = new HashSet<>(1); + this.externallyManagedConfigMembers = new LinkedHashSet<>(1); } this.externallyManagedConfigMembers.add(configMember); } } + /** + * Determine if the given method or field is an externally managed configuration member. + */ public boolean isExternallyManagedConfigMember(Member configMember) { synchronized (this.postProcessingLock) { return (this.externallyManagedConfigMembers != null && @@ -434,15 +475,44 @@ public boolean isExternallyManagedConfigMember(Member configMember) { } } + /** + * Get all externally managed configuration methods and fields (as an immutable Set). + * @since 5.3.11 + */ + public Set getExternallyManagedConfigMembers() { + synchronized (this.postProcessingLock) { + return (this.externallyManagedConfigMembers != null ? + Collections.unmodifiableSet(new LinkedHashSet<>(this.externallyManagedConfigMembers)) : + Collections.emptySet()); + } + } + + /** + * Register an externally managed configuration initialization method — + * for example, a method annotated with JSR-250's + * {@link jakarta.annotation.PostConstruct} annotation. + *

    The supplied {@code initMethod} may be the + * {@linkplain Method#getName() simple method name} for non-private methods or the + * {@linkplain org.springframework.util.ClassUtils#getQualifiedMethodName(Method) + * qualified method name} for {@code private} methods. A qualified name is + * necessary for {@code private} methods in order to disambiguate between + * multiple private methods with the same name within a class hierarchy. + */ public void registerExternallyManagedInitMethod(String initMethod) { synchronized (this.postProcessingLock) { if (this.externallyManagedInitMethods == null) { - this.externallyManagedInitMethods = new HashSet<>(1); + this.externallyManagedInitMethods = new LinkedHashSet<>(1); } this.externallyManagedInitMethods.add(initMethod); } } + /** + * Determine if the given method name indicates an externally managed + * initialization method. + *

    See {@link #registerExternallyManagedInitMethod} for details + * regarding the format for the supplied {@code initMethod}. + */ public boolean isExternallyManagedInitMethod(String initMethod) { synchronized (this.postProcessingLock) { return (this.externallyManagedInitMethods != null && @@ -450,15 +520,85 @@ public boolean isExternallyManagedInitMethod(String initMethod) { } } + /** + * Determine if the given method name indicates an externally managed + * initialization method, regardless of method visibility. + *

    In contrast to {@link #isExternallyManagedInitMethod(String)}, this + * method also returns {@code true} if there is a {@code private} externally + * managed initialization method that has been + * {@linkplain #registerExternallyManagedInitMethod(String) registered} + * using a qualified method name instead of a simple method name. + * @since 5.3.17 + */ + boolean hasAnyExternallyManagedInitMethod(String initMethod) { + synchronized (this.postProcessingLock) { + if (isExternallyManagedInitMethod(initMethod)) { + return true; + } + if (this.externallyManagedInitMethods != null) { + for (String candidate : this.externallyManagedInitMethods) { + int indexOfDot = candidate.lastIndexOf('.'); + if (indexOfDot >= 0) { + String methodName = candidate.substring(indexOfDot + 1); + if (methodName.equals(initMethod)) { + return true; + } + } + } + } + return false; + } + } + + /** + * Return all externally managed initialization methods (as an immutable Set). + *

    See {@link #registerExternallyManagedInitMethod} for details + * regarding the format for the initialization methods in the returned set. + * @since 5.3.11 + */ + public Set getExternallyManagedInitMethods() { + synchronized (this.postProcessingLock) { + return (this.externallyManagedInitMethods != null ? + Collections.unmodifiableSet(new LinkedHashSet<>(this.externallyManagedInitMethods)) : + Collections.emptySet()); + } + } + + /** + * Resolve the inferred destroy method if necessary. + * @since 6.0 + */ + public void resolveDestroyMethodIfNecessary() { + setDestroyMethodNames(DisposableBeanAdapter + .inferDestroyMethodsIfNecessary(getResolvableType().toClass(), this)); + } + + /** + * Register an externally managed configuration destruction method — + * for example, a method annotated with JSR-250's + * {@link jakarta.annotation.PreDestroy} annotation. + *

    The supplied {@code destroyMethod} may be the + * {@linkplain Method#getName() simple method name} for non-private methods or the + * {@linkplain org.springframework.util.ClassUtils#getQualifiedMethodName(Method) + * qualified method name} for {@code private} methods. A qualified name is + * necessary for {@code private} methods in order to disambiguate between + * multiple private methods with the same name within a class hierarchy. + */ public void registerExternallyManagedDestroyMethod(String destroyMethod) { synchronized (this.postProcessingLock) { if (this.externallyManagedDestroyMethods == null) { - this.externallyManagedDestroyMethods = new HashSet<>(1); + this.externallyManagedDestroyMethods = new LinkedHashSet<>(1); } this.externallyManagedDestroyMethods.add(destroyMethod); } } + /** + * Determine if the given method name indicates an externally managed + * destruction method. + *

    See {@link #registerExternallyManagedDestroyMethod} for details + * regarding the format for the supplied {@code destroyMethod}. + */ public boolean isExternallyManagedDestroyMethod(String destroyMethod) { synchronized (this.postProcessingLock) { return (this.externallyManagedDestroyMethods != null && @@ -466,6 +606,50 @@ public boolean isExternallyManagedDestroyMethod(String destroyMethod) { } } + /** + * Determine if the given method name indicates an externally managed + * destruction method, regardless of method visibility. + *

    In contrast to {@link #isExternallyManagedDestroyMethod(String)}, this + * method also returns {@code true} if there is a {@code private} externally + * managed destruction method that has been + * {@linkplain #registerExternallyManagedDestroyMethod(String) registered} + * using a qualified method name instead of a simple method name. + * @since 5.3.17 + */ + boolean hasAnyExternallyManagedDestroyMethod(String destroyMethod) { + synchronized (this.postProcessingLock) { + if (isExternallyManagedDestroyMethod(destroyMethod)) { + return true; + } + if (this.externallyManagedDestroyMethods != null) { + for (String candidate : this.externallyManagedDestroyMethods) { + int indexOfDot = candidate.lastIndexOf('.'); + if (indexOfDot >= 0) { + String methodName = candidate.substring(indexOfDot + 1); + if (methodName.equals(destroyMethod)) { + return true; + } + } + } + } + return false; + } + } + + /** + * Get all externally managed destruction methods (as an immutable Set). + *

    See {@link #registerExternallyManagedDestroyMethod} for details + * regarding the format for the destruction methods in the returned set. + * @since 5.3.11 + */ + public Set getExternallyManagedDestroyMethods() { + synchronized (this.postProcessingLock) { + return (this.externallyManagedDestroyMethods != null ? + Collections.unmodifiableSet(new LinkedHashSet<>(this.externallyManagedDestroyMethods)) : + Collections.emptySet()); + } + } + @Override public RootBeanDefinition cloneBeanDefinition() { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/SecurityContextProvider.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/SecurityContextProvider.java deleted file mode 100644 index d2f70c46ebf9..000000000000 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/SecurityContextProvider.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2002-2012 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.beans.factory.support; - -import java.security.AccessControlContext; - -/** - * Provider of the security context of the code running inside the bean factory. - * - * @author Costin Leau - * @since 3.0 - */ -public interface SecurityContextProvider { - - /** - * Provides a security access control context relevant to a bean factory. - * @return bean factory security control context - */ - AccessControlContext getAccessControlContext(); - -} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleAutowireCandidateResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleAutowireCandidateResolver.java index f33eeecf16aa..2afdf73924a4 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleAutowireCandidateResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleAutowireCandidateResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -64,6 +64,12 @@ public Object getLazyResolutionProxyIfNecessary(DependencyDescriptor descriptor, return null; } + @Override + @Nullable + public Class getLazyResolutionProxyClass(DependencyDescriptor descriptor, @Nullable String beanName) { + return null; + } + /** * This implementation returns {@code this} as-is. * @see #INSTANCE diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleInstantiationStrategy.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleInstantiationStrategy.java index 0b05cf50cc7e..c897aa4c7487 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleInstantiationStrategy.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleInstantiationStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -19,9 +19,6 @@ import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.security.AccessController; -import java.security.PrivilegedAction; -import java.security.PrivilegedExceptionAction; import org.springframework.beans.BeanInstantiationException; import org.springframework.beans.BeanUtils; @@ -56,6 +53,15 @@ public static Method getCurrentlyInvokedFactoryMethod() { return currentlyInvokedFactoryMethod.get(); } + /** + * Set the factory method currently being invoked or {@code null} to reset. + * @param method the factory method currently being invoked or {@code null} + * @since 6.0 + */ + public static void setCurrentlyInvokedFactoryMethod(@Nullable Method method) { + currentlyInvokedFactoryMethod.set(method); + } + @Override public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner) { @@ -70,13 +76,7 @@ public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, Bean throw new BeanInstantiationException(clazz, "Specified class is an interface"); } try { - if (System.getSecurityManager() != null) { - constructorToUse = AccessController.doPrivileged( - (PrivilegedExceptionAction>) clazz::getDeclaredConstructor); - } - else { - constructorToUse = clazz.getDeclaredConstructor(); - } + constructorToUse = clazz.getDeclaredConstructor(); bd.resolvedConstructorOrFactoryMethod = constructorToUse; } catch (Throwable ex) { @@ -107,13 +107,6 @@ public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, Bean final Constructor ctor, Object... args) { if (!bd.hasMethodOverrides()) { - if (System.getSecurityManager() != null) { - // use own privileged to change accessibility (when security is on) - AccessController.doPrivileged((PrivilegedAction) () -> { - ReflectionUtils.makeAccessible(ctor); - return null; - }); - } return BeanUtils.instantiateClass(ctor, args); } else { @@ -138,15 +131,7 @@ public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, Bean @Nullable Object factoryBean, final Method factoryMethod, Object... args) { try { - if (System.getSecurityManager() != null) { - AccessController.doPrivileged((PrivilegedAction) () -> { - ReflectionUtils.makeAccessible(factoryMethod); - return null; - }); - } - else { - ReflectionUtils.makeAccessible(factoryMethod); - } + ReflectionUtils.makeAccessible(factoryMethod); Method priorInvokedFactoryMethod = currentlyInvokedFactoryMethod.get(); try { @@ -176,7 +161,8 @@ public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, Bean "Cannot access factory method '" + factoryMethod.getName() + "'; is it public?", ex); } catch (InvocationTargetException ex) { - String msg = "Factory method '" + factoryMethod.getName() + "' threw exception"; + String msg = "Factory method '" + factoryMethod.getName() + "' threw exception with message: " + + ex.getTargetException().getMessage(); if (bd.getFactoryBeanName() != null && owner instanceof ConfigurableBeanFactory && ((ConfigurableBeanFactory) owner).isCurrentlyInCreation(bd.getFactoryBeanName())) { msg = "Circular reference involving containing bean '" + bd.getFactoryBeanName() + "' - consider " + diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleSecurityContextProvider.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleSecurityContextProvider.java deleted file mode 100644 index b28fbffa3702..000000000000 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleSecurityContextProvider.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2002-2018 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.beans.factory.support; - -import java.security.AccessControlContext; -import java.security.AccessController; - -import org.springframework.lang.Nullable; - -/** - * Simple {@link SecurityContextProvider} implementation. - * - * @author Costin Leau - * @since 3.0 - */ -public class SimpleSecurityContextProvider implements SecurityContextProvider { - - @Nullable - private final AccessControlContext acc; - - - /** - * Construct a new {@code SimpleSecurityContextProvider} instance. - *

    The security context will be retrieved on each call from the current - * thread. - */ - public SimpleSecurityContextProvider() { - this(null); - } - - /** - * Construct a new {@code SimpleSecurityContextProvider} instance. - *

    If the given control context is null, the security context will be - * retrieved on each call from the current thread. - * @param acc access control context (can be {@code null}) - * @see AccessController#getContext() - */ - public SimpleSecurityContextProvider(@Nullable AccessControlContext acc) { - this.acc = acc; - } - - - @Override - public AccessControlContext getAccessControlContext() { - return (this.acc != null ? this.acc : AccessController.getContext()); - } - -} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/StaticListableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/StaticListableBeanFactory.java index a5430120dfdb..f3d43d107ed6 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/StaticListableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/StaticListableBeanFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -19,9 +19,11 @@ import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Stream; import org.springframework.beans.BeansException; @@ -284,7 +286,7 @@ public ObjectProvider getBeanProvider(Class requiredType, boolean allo @SuppressWarnings("unchecked") @Override public ObjectProvider getBeanProvider(ResolvableType requiredType, boolean allowEagerInit) { - return new ObjectProvider() { + return new ObjectProvider<>() { @Override public T getObject() throws BeansException { String[] beanNames = getBeanNamesForType(requiredType); @@ -459,8 +461,26 @@ public Map getBeansWithAnnotation(Class an public A findAnnotationOnBean(String beanName, Class annotationType) throws NoSuchBeanDefinitionException { - Class beanType = getType(beanName); + return findAnnotationOnBean(beanName, annotationType, true); + } + + @Override + @Nullable + public A findAnnotationOnBean( + String beanName, Class annotationType, boolean allowFactoryBeanInit) + throws NoSuchBeanDefinitionException { + + Class beanType = getType(beanName, allowFactoryBeanInit); return (beanType != null ? AnnotatedElementUtils.findMergedAnnotation(beanType, annotationType) : null); } + @Override + public Set findAllAnnotationsOnBean( + String beanName, Class annotationType, boolean allowFactoryBeanInit) throws NoSuchBeanDefinitionException { + + Class beanType = getType(beanName, allowFactoryBeanInit); + return (beanType != null ? + AnnotatedElementUtils.findAllMergedAnnotations(beanType, annotationType) : Collections.emptySet()); + } + } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/AbstractBeanDefinitionParser.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/AbstractBeanDefinitionParser.java index 288ad403f4b4..018c85123f9b 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/AbstractBeanDefinitionParser.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/AbstractBeanDefinitionParser.java @@ -124,7 +124,7 @@ protected String resolveId(Element element, AbstractBeanDefinition definition, P /** * Register the supplied {@link BeanDefinitionHolder bean} with the supplied * {@link BeanDefinitionRegistry registry}. - *

    Subclasses can override this method to control whether or not the supplied + *

    Subclasses can override this method to control whether the supplied * {@link BeanDefinitionHolder bean} is actually even registered, or to * register even more beans. *

    The default implementation registers the supplied {@link BeanDefinitionHolder bean} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionParserDelegate.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionParserDelegate.java index 6be311eb1efe..57d0037139d3 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionParserDelegate.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionParserDelegate.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -1186,8 +1186,7 @@ public Map parseMapElement(Element mapEle, @Nullable BeanDefinit Element valueEle = null; for (int j = 0; j < entrySubNodes.getLength(); j++) { Node node = entrySubNodes.item(j); - if (node instanceof Element) { - Element candidateEle = (Element) node; + if (node instanceof Element candidateEle) { if (nodeNameEquals(candidateEle, KEY_ELEMENT)) { if (keyEle != null) { error(" element is only allowed to contain one sub-element", entryEle); @@ -1362,7 +1361,7 @@ public boolean parseMergeAttribute(Element collectionElement) { } /** - * Parse a custom element (outside of the default namespace). + * Parse a custom element (outside the default namespace). * @param ele the element to parse * @return the resulting bean definition */ @@ -1372,7 +1371,7 @@ public BeanDefinition parseCustomElement(Element ele) { } /** - * Parse a custom element (outside of the default namespace). + * Parse a custom element (outside the default namespace). * @param ele the element to parse * @param containingBd the containing bean definition (if any) * @return the resulting bean definition diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultBeanDefinitionDocumentReader.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultBeanDefinitionDocumentReader.java index af5026dce346..b8e2935a9c46 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultBeanDefinitionDocumentReader.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultBeanDefinitionDocumentReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -170,8 +170,7 @@ protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate d NodeList nl = root.getChildNodes(); for (int i = 0; i < nl.getLength(); i++) { Node node = nl.item(i); - if (node instanceof Element) { - Element ele = (Element) node; + if (node instanceof Element ele) { if (delegate.isDefaultNamespace(ele)) { parseDefaultElement(ele, delegate); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultDocumentLoader.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultDocumentLoader.java index a443e7b1d669..08b1d16f2778 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultDocumentLoader.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultDocumentLoader.java @@ -39,7 +39,7 @@ * when starting your JVM. For example, to use the Oracle {@link DocumentBuilder}, * you might start your application like as follows: * - *

    java -Djavax.xml.parsers.DocumentBuilderFactory=oracle.xml.jaxp.JXDocumentBuilderFactory MyMainClass
    + *
    java -Djavax.xml.parsers.DocumentBuilderFactory=oracle.xml.jaxp.JXDocumentBuilderFactory MyMainClass
    * * @author Rob Harrop * @author Juergen Hoeller diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultNamespaceHandlerResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultNamespaceHandlerResolver.java index 6dfda38e6bd4..815e0f59b5ff 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultNamespaceHandlerResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultNamespaceHandlerResolver.java @@ -96,7 +96,7 @@ public DefaultNamespaceHandlerResolver(@Nullable ClassLoader classLoader) { * Create a new {@code DefaultNamespaceHandlerResolver} using the * supplied mapping file location. * @param classLoader the {@link ClassLoader} instance used to load mapping resources - * may be {@code null}, in which case the thread context ClassLoader will be used) + * may be {@code null}, in which case the thread context ClassLoader will be used * @param handlerMappingsLocation the mapping file location */ public DefaultNamespaceHandlerResolver(@Nullable ClassLoader classLoader, String handlerMappingsLocation) { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/DelegatingEntityResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/DelegatingEntityResolver.java index 1335d0401787..fe8f6f61a37f 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/DelegatingEntityResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/DelegatingEntityResolver.java @@ -56,7 +56,7 @@ public class DelegatingEntityResolver implements EntityResolver { *

    Configures the {@link PluggableSchemaResolver} with the supplied * {@link ClassLoader}. * @param classLoader the ClassLoader to use for loading - * (can be {@code null}) to use the default ClassLoader) + * (can be {@code null} to use the default ClassLoader) */ public DelegatingEntityResolver(@Nullable ClassLoader classLoader) { this.dtdResolver = new BeansDtdResolver(); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/ParserContext.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/ParserContext.java index 2223f789ae1c..4bd6ef58e966 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/ParserContext.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/ParserContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -64,24 +64,24 @@ public ParserContext(XmlReaderContext readerContext, BeanDefinitionParserDelegat } - public final XmlReaderContext getReaderContext() { + public XmlReaderContext getReaderContext() { return this.readerContext; } - public final BeanDefinitionRegistry getRegistry() { + public BeanDefinitionRegistry getRegistry() { return this.readerContext.getRegistry(); } - public final BeanDefinitionParserDelegate getDelegate() { + public BeanDefinitionParserDelegate getDelegate() { return this.delegate; } @Nullable - public final BeanDefinition getContainingBeanDefinition() { + public BeanDefinition getContainingBeanDefinition() { return this.containingBeanDefinition; } - public final boolean isNested() { + public boolean isNested() { return (this.containingBeanDefinition != null); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/PluggableSchemaResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/PluggableSchemaResolver.java index 3b1bff1febc3..659b21b40b97 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/PluggableSchemaResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/PluggableSchemaResolver.java @@ -71,16 +71,16 @@ public class PluggableSchemaResolver implements EntityResolver { private final String schemaMappingsLocation; - /** Stores the mapping of schema URL -> local schema path. */ + /** Stores the mapping of schema URL → local schema path. */ @Nullable private volatile Map schemaMappings; /** - * Loads the schema URL -> schema file location mappings using the default + * Loads the schema URL → schema file location mappings using the default * mapping file pattern "META-INF/spring.schemas". * @param classLoader the ClassLoader to use for loading - * (can be {@code null}) to use the default ClassLoader) + * (can be {@code null} to use the default ClassLoader) * @see PropertiesLoaderUtils#loadAllProperties(String, ClassLoader) */ public PluggableSchemaResolver(@Nullable ClassLoader classLoader) { @@ -89,10 +89,10 @@ public PluggableSchemaResolver(@Nullable ClassLoader classLoader) { } /** - * Loads the schema URL -> schema file location mappings using the given + * Loads the schema URL → schema file location mappings using the given * mapping file pattern. * @param classLoader the ClassLoader to use for loading - * (can be {@code null}) to use the default ClassLoader) + * (can be {@code null} to use the default ClassLoader) * @param schemaMappingsLocation the location of the file that defines schema mappings * (must not be empty) * @see PropertiesLoaderUtils#loadAllProperties(String, ClassLoader) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/ResourceEntityResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/ResourceEntityResolver.java index b74e0133263a..dec51c9b79ec 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/ResourceEntityResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/ResourceEntityResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -18,8 +18,8 @@ import java.io.File; import java.io.IOException; -import java.net.URL; import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -29,6 +29,7 @@ import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.lang.Nullable; +import org.springframework.util.ResourceUtils; /** * {@code EntityResolver} implementation that tries to resolve entity references @@ -80,8 +81,8 @@ public InputSource resolveEntity(@Nullable String publicId, @Nullable String sys if (source == null && systemId != null) { String resourcePath = null; try { - String decodedSystemId = URLDecoder.decode(systemId, "UTF-8"); - String givenUrl = new URL(decodedSystemId).toString(); + String decodedSystemId = URLDecoder.decode(systemId, StandardCharsets.UTF_8); + String givenUrl = ResourceUtils.toURL(decodedSystemId).toString(); String systemRootUrl = new File("").toURI().toURL().toString(); // Try relative to resource base if currently in system root. if (givenUrl.startsWith(systemRootUrl)) { @@ -115,7 +116,7 @@ else if (systemId.endsWith(DTD_SUFFIX) || systemId.endsWith(XSD_SUFFIX)) { url = "https:" + url.substring(5); } try { - source = new InputSource(new URL(url).openStream()); + source = new InputSource(ResourceUtils.toURL(url).openStream()); source.setPublicId(publicId); source.setSystemId(systemId); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/SimpleConstructorNamespaceHandler.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/SimpleConstructorNamespaceHandler.java index ddffb17d5175..7cf160d848f0 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/SimpleConstructorNamespaceHandler.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/SimpleConstructorNamespaceHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -50,7 +50,7 @@ * the bean that will be considered as a parameter. * * Note: This implementation supports only named parameters - there is no - * support for indexes or types. Further more, the names are used as hints by + * support for indexes or types. Furthermore, the names are used as hints by * the container which, by default, does type introspection. * * @author Costin Leau @@ -78,10 +78,9 @@ public BeanDefinition parse(Element element, ParserContext parserContext) { @Override public BeanDefinitionHolder decorate(Node node, BeanDefinitionHolder definition, ParserContext parserContext) { - if (node instanceof Attr) { - Attr attr = (Attr) node; - String argName = StringUtils.trimWhitespace(parserContext.getDelegate().getLocalName(attr)); - String argValue = StringUtils.trimWhitespace(attr.getValue()); + if (node instanceof Attr attr) { + String argName = parserContext.getDelegate().getLocalName(attr).strip(); + String argValue = attr.getValue().strip(); ConstructorArgumentValues cvs = definition.getBeanDefinition().getConstructorArgumentValues(); boolean ref = false; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/SimplePropertyNamespaceHandler.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/SimplePropertyNamespaceHandler.java index 9cecef4eed47..ec3c1512d8a8 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/SimplePropertyNamespaceHandler.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/SimplePropertyNamespaceHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-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. @@ -67,8 +67,7 @@ public BeanDefinition parse(Element element, ParserContext parserContext) { @Override public BeanDefinitionHolder decorate(Node node, BeanDefinitionHolder definition, ParserContext parserContext) { - if (node instanceof Attr) { - Attr attr = (Attr) node; + if (node instanceof Attr attr) { String propertyName = parserContext.getDelegate().getLocalName(attr); String propertyValue = attr.getValue(); MutablePropertyValues pvs = definition.getBeanDefinition().getPropertyValues(); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/XmlBeanDefinitionReader.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/XmlBeanDefinitionReader.java index 589208a4d3af..7e7c07926674 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/XmlBeanDefinitionReader.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/XmlBeanDefinitionReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -128,7 +128,7 @@ public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader { private final XmlValidationModeDetector validationModeDetector = new XmlValidationModeDetector(); private final ThreadLocal> resourcesCurrentlyBeingLoaded = - new NamedThreadLocal>("XML bean definition resources currently being loaded"){ + new NamedThreadLocal<>("XML bean definition resources currently being loaded"){ @Override protected Set initialValue() { return new HashSet<>(4); @@ -184,7 +184,7 @@ public int getValidationMode() { } /** - * Set whether or not the XML parser should be XML namespace aware. + * Set whether the XML parser should be XML namespace aware. * Default is "false". *

    This is typically not needed when schema validation is active. * However, without validation, this has to be switched to "true" @@ -195,7 +195,7 @@ public void setNamespaceAware(boolean namespaceAware) { } /** - * Return whether or not the XML parser should be XML namespace aware. + * Return whether the XML parser should be XML namespace aware. */ public boolean isNamespaceAware() { return this.namespaceAware; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/XmlBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/XmlBeanFactory.java deleted file mode 100644 index b762a4181002..000000000000 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/XmlBeanFactory.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2002-2018 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.beans.factory.xml; - -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.support.DefaultListableBeanFactory; -import org.springframework.core.io.Resource; - -/** - * Convenience extension of {@link DefaultListableBeanFactory} that reads bean definitions - * from an XML document. Delegates to {@link XmlBeanDefinitionReader} underneath; effectively - * equivalent to using an XmlBeanDefinitionReader with a DefaultListableBeanFactory. - * - *

    The structure, element and attribute names of the required XML document - * are hard-coded in this class. (Of course a transform could be run if necessary - * to produce this format). "beans" doesn't need to be the root element of the XML - * document: This class will parse all bean definition elements in the XML file. - * - *

    This class registers each bean definition with the {@link DefaultListableBeanFactory} - * superclass, and relies on the latter's implementation of the {@link BeanFactory} interface. - * It supports singletons, prototypes, and references to either of these kinds of bean. - * See {@code "spring-beans-3.x.xsd"} (or historically, {@code "spring-beans-2.0.dtd"}) for - * details on options and configuration style. - * - *

    For advanced needs, consider using a {@link DefaultListableBeanFactory} with - * an {@link XmlBeanDefinitionReader}. The latter allows for reading from multiple XML - * resources and is highly configurable in its actual XML parsing behavior. - * - * @author Rod Johnson - * @author Juergen Hoeller - * @author Chris Beams - * @since 15 April 2001 - * @see org.springframework.beans.factory.support.DefaultListableBeanFactory - * @see XmlBeanDefinitionReader - * @deprecated as of Spring 3.1 in favor of {@link DefaultListableBeanFactory} and - * {@link XmlBeanDefinitionReader} - */ -@Deprecated -@SuppressWarnings({"serial", "all"}) -public class XmlBeanFactory extends DefaultListableBeanFactory { - - private final XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(this); - - - /** - * Create a new XmlBeanFactory with the given resource, - * which must be parsable using DOM. - * @param resource the XML resource to load bean definitions from - * @throws BeansException in case of loading or parsing errors - */ - public XmlBeanFactory(Resource resource) throws BeansException { - this(resource, null); - } - - /** - * Create a new XmlBeanFactory with the given input stream, - * which must be parsable using DOM. - * @param resource the XML resource to load bean definitions from - * @param parentBeanFactory parent bean factory - * @throws BeansException in case of loading or parsing errors - */ - public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException { - super(parentBeanFactory); - this.reader.loadBeanDefinitions(resource); - } - -} diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CharsetEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CharsetEditor.java index adcb806e684e..ef772db749cb 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CharsetEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CharsetEditor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-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. @@ -29,6 +29,7 @@ * e.g. {@code UTF-8}, {@code ISO-8859-16}, etc. * * @author Arjen Poutsma + * @author Sam Brannen * @since 2.5.4 * @see Charset */ @@ -37,7 +38,7 @@ public class CharsetEditor extends PropertyEditorSupport { @Override public void setAsText(String text) throws IllegalArgumentException { if (StringUtils.hasText(text)) { - setValue(Charset.forName(text)); + setValue(Charset.forName(text.trim())); } else { setValue(null); diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CurrencyEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CurrencyEditor.java index 0d044ffe11aa..b6b9d318afe3 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CurrencyEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CurrencyEditor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-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. @@ -19,11 +19,14 @@ import java.beans.PropertyEditorSupport; import java.util.Currency; +import org.springframework.util.StringUtils; + /** * Editor for {@code java.util.Currency}, translating currency codes into Currency * objects. Exposes the currency code as text representation of a Currency object. * * @author Juergen Hoeller + * @author Sam Brannen * @since 3.0 * @see java.util.Currency */ @@ -31,6 +34,9 @@ public class CurrencyEditor extends PropertyEditorSupport { @Override public void setAsText(String text) throws IllegalArgumentException { + if (StringUtils.hasText(text)) { + text = text.trim(); + } setValue(Currency.getInstance(text)); } diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomCollectionEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomCollectionEditor.java index bcf39977df22..898adb52ecca 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomCollectionEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomCollectionEditor.java @@ -117,9 +117,8 @@ else if (value == null || (this.collectionType.isInstance(value) && !alwaysCreat // Use the source value as-is, as it matches the target type. super.setValue(value); } - else if (value instanceof Collection) { + else if (value instanceof Collection source) { // Convert Collection elements. - Collection source = (Collection) value; Collection target = createCollection(this.collectionType, source.size()); for (Object elem : source) { target.add(convertElement(elem)); diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomMapEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomMapEditor.java index a1e5ebd119b6..d421a8e25c00 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomMapEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomMapEditor.java @@ -107,9 +107,8 @@ else if (value == null || (this.mapType.isInstance(value) && !alwaysCreateNewMap // Use the source value as-is, as it matches the target type. super.setValue(value); } - else if (value instanceof Map) { + else if (value instanceof Map source) { // Convert Map elements. - Map source = (Map) value; Map target = createMap(this.mapType, source.size()); source.forEach((key, val) -> target.put(convertKey(key), convertValue(val))); super.setValue(target); diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/LocaleEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/LocaleEditor.java index 29bf43af3856..5a327f710e0c 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/LocaleEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/LocaleEditor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-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. @@ -23,9 +23,9 @@ /** * Editor for {@code java.util.Locale}, to directly populate a Locale property. * - *

    Expects the same syntax as Locale's {@code toString}, i.e. language + + *

    Expects the same syntax as Locale's {@code toString()}, i.e. language + * optionally country + optionally variant, separated by "_" (e.g. "en", "en_US"). - * Also accepts spaces as separators, as alternative to underscores. + * Also accepts spaces as separators, as an alternative to underscores. * * @author Juergen Hoeller * @since 26.05.2003 diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PathEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PathEditor.java index f1edae00c793..13226e6ca0db 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PathEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PathEditor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -26,8 +26,8 @@ import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceEditor; -import org.springframework.core.io.ResourceLoader; import org.springframework.util.Assert; +import org.springframework.util.ResourceUtils; /** * Editor for {@code java.nio.file.Path}, to directly populate a Path @@ -74,10 +74,10 @@ public PathEditor(ResourceEditor resourceEditor) { @Override public void setAsText(String text) throws IllegalArgumentException { - boolean nioPathCandidate = !text.startsWith(ResourceLoader.CLASSPATH_URL_PREFIX); + boolean nioPathCandidate = !text.startsWith(ResourceUtils.CLASSPATH_URL_PREFIX); if (nioPathCandidate && !text.startsWith("/")) { try { - URI uri = new URI(text); + URI uri = ResourceUtils.toURI(text); if (uri.getScheme() != null) { nioPathCandidate = false; // Let's try NIO file system providers via Paths.get(URI) @@ -85,9 +85,13 @@ public void setAsText(String text) throws IllegalArgumentException { return; } } - catch (URISyntaxException | FileSystemNotFoundException ex) { - // Not a valid URI (let's try as Spring resource location), - // or a URI scheme not registered for NIO (let's try URL + catch (URISyntaxException ex) { + // Not a valid URI; potentially a Windows-style path after + // a file prefix (let's try as Spring resource location) + nioPathCandidate = !text.startsWith(ResourceUtils.FILE_URL_PREFIX); + } + catch (FileSystemNotFoundException ex) { + // URI scheme not registered for NIO (let's try URL // protocol handlers via Spring's resource mechanism). } } @@ -97,7 +101,7 @@ public void setAsText(String text) throws IllegalArgumentException { if (resource == null) { setValue(null); } - else if (!resource.exists() && nioPathCandidate) { + else if (nioPathCandidate && !resource.exists()) { setValue(Paths.get(text).normalize()); } else { diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/StringArrayPropertyEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/StringArrayPropertyEditor.java index 1a7a8ccc24f8..e278c8721b65 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/StringArrayPropertyEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/StringArrayPropertyEditor.java @@ -26,7 +26,7 @@ * Custom {@link java.beans.PropertyEditor} for String arrays. * *

    Strings must be in CSV format, with a customizable separator. - * By default values in the result are trimmed of whitespace. + * By default, values in the result are trimmed of whitespace. * * @author Rod Johnson * @author Juergen Hoeller @@ -86,7 +86,7 @@ public StringArrayPropertyEditor(String separator, boolean emptyArrayAsNull) { * @param emptyArrayAsNull {@code true} if an empty String array * is to be transformed into {@code null} * @param trimValues {@code true} if the values in the parsed arrays - * are to be trimmed of whitespace (default is true) + * are to be trimmed of whitespace (default is {@code true}) */ public StringArrayPropertyEditor(String separator, boolean emptyArrayAsNull, boolean trimValues) { this(separator, null, emptyArrayAsNull, trimValues); @@ -114,7 +114,7 @@ public StringArrayPropertyEditor(String separator, @Nullable String charsToDelet * @param emptyArrayAsNull {@code true} if an empty String array * is to be transformed into {@code null} * @param trimValues {@code true} if the values in the parsed arrays - * are to be trimmed of whitespace (default is true) + * are to be trimmed of whitespace (default is {@code true}) */ public StringArrayPropertyEditor( String separator, @Nullable String charsToDelete, boolean emptyArrayAsNull, boolean trimValues) { diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/TimeZoneEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/TimeZoneEditor.java index 6b809169b969..3833c0268123 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/TimeZoneEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/TimeZoneEditor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-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. @@ -28,6 +28,7 @@ * * @author Juergen Hoeller * @author Nicholas Williams + * @author Sam Brannen * @since 3.0 * @see java.util.TimeZone * @see ZoneIdEditor @@ -36,6 +37,9 @@ public class TimeZoneEditor extends PropertyEditorSupport { @Override public void setAsText(String text) throws IllegalArgumentException { + if (StringUtils.hasText(text)) { + text = text.trim(); + } setValue(StringUtils.parseTimeZoneString(text)); } diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/URIEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/URIEditor.java index 344fb5d439f9..e94e65f5a94f 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/URIEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/URIEditor.java @@ -130,7 +130,7 @@ public void setAsText(String text) throws IllegalArgumentException { /** * Create a URI instance for the given user-specified String value. - *

    The default implementation encodes the value into a RFC-2396 compliant URI. + *

    The default implementation encodes the value into an RFC-2396 compliant URI. * @param value the value to convert into a URI instance * @return the URI instance * @throws java.net.URISyntaxException if URI conversion failed diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ZoneIdEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ZoneIdEditor.java index 912bdd5fb74b..4992e33aebd0 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ZoneIdEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ZoneIdEditor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-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. @@ -19,11 +19,14 @@ import java.beans.PropertyEditorSupport; import java.time.ZoneId; +import org.springframework.util.StringUtils; + /** * Editor for {@code java.time.ZoneId}, translating zone ID Strings into {@code ZoneId} * objects. Exposes the {@code TimeZone} ID as a text representation. * * @author Nicholas Williams + * @author Sam Brannen * @since 4.0 * @see java.time.ZoneId * @see TimeZoneEditor @@ -32,6 +35,9 @@ public class ZoneIdEditor extends PropertyEditorSupport { @Override public void setAsText(String text) throws IllegalArgumentException { + if (StringUtils.hasText(text)) { + text = text.trim(); + } setValue(ZoneId.of(text)); } diff --git a/spring-beans/src/main/java/org/springframework/beans/support/MutableSortDefinition.java b/spring-beans/src/main/java/org/springframework/beans/support/MutableSortDefinition.java index 2a428fc9d077..096b0d6a2cde 100644 --- a/spring-beans/src/main/java/org/springframework/beans/support/MutableSortDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/support/MutableSortDefinition.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-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. @@ -159,10 +159,9 @@ public boolean equals(@Nullable Object other) { if (this == other) { return true; } - if (!(other instanceof SortDefinition)) { + if (!(other instanceof SortDefinition otherSd)) { return false; } - SortDefinition otherSd = (SortDefinition) other; return (getProperty().equals(otherSd.getProperty()) && isAscending() == otherSd.isAscending() && isIgnoreCase() == otherSd.isIgnoreCase()); diff --git a/spring-beans/src/main/java/org/springframework/beans/support/PropertyComparator.java b/spring-beans/src/main/java/org/springframework/beans/support/PropertyComparator.java index 43e927f28306..519ead579879 100644 --- a/spring-beans/src/main/java/org/springframework/beans/support/PropertyComparator.java +++ b/spring-beans/src/main/java/org/springframework/beans/support/PropertyComparator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -44,8 +44,6 @@ public class PropertyComparator implements Comparator { private final SortDefinition sortDefinition; - private final BeanWrapperImpl beanWrapper = new BeanWrapperImpl(false); - /** * Create a new PropertyComparator for the given SortDefinition. @@ -115,8 +113,9 @@ private Object getPropertyValue(Object obj) { // (similar to JSTL EL). If the property doesn't exist in the // first place, let the exception through. try { - this.beanWrapper.setWrappedInstance(obj); - return this.beanWrapper.getPropertyValue(this.sortDefinition.getProperty()); + BeanWrapperImpl beanWrapper = new BeanWrapperImpl(false); + beanWrapper.setWrappedInstance(obj); + return beanWrapper.getPropertyValue(this.sortDefinition.getProperty()); } catch (BeansException ex) { logger.debug("PropertyComparator could not access property - treating as null for sorting", ex); diff --git a/spring-beans/src/main/java/org/springframework/beans/support/ResourceEditorRegistrar.java b/spring-beans/src/main/java/org/springframework/beans/support/ResourceEditorRegistrar.java index 2865bea12e95..784cdf1b717c 100644 --- a/spring-beans/src/main/java/org/springframework/beans/support/ResourceEditorRegistrar.java +++ b/spring-beans/src/main/java/org/springframework/beans/support/ResourceEditorRegistrar.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-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. @@ -43,6 +43,7 @@ import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceEditor; import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.WritableResource; import org.springframework.core.io.support.ResourceArrayPropertyEditor; import org.springframework.core.io.support.ResourcePatternResolver; @@ -102,6 +103,7 @@ public void registerCustomEditors(PropertyEditorRegistry registry) { ResourceEditor baseEditor = new ResourceEditor(this.resourceLoader, this.propertyResolver); doRegisterEditor(registry, Resource.class, baseEditor); doRegisterEditor(registry, ContextResource.class, baseEditor); + doRegisterEditor(registry, WritableResource.class, baseEditor); doRegisterEditor(registry, InputStream.class, new InputStreamEditor(baseEditor)); doRegisterEditor(registry, InputSource.class, new InputSourceEditor(baseEditor)); doRegisterEditor(registry, File.class, new FileEditor(baseEditor)); diff --git a/spring-beans/src/main/kotlin/org/springframework/beans/factory/ListableBeanFactoryExtensions.kt b/spring-beans/src/main/kotlin/org/springframework/beans/factory/ListableBeanFactoryExtensions.kt index 174507edfa85..00ed65c25277 100644 --- a/spring-beans/src/main/kotlin/org/springframework/beans/factory/ListableBeanFactoryExtensions.kt +++ b/spring-beans/src/main/kotlin/org/springframework/beans/factory/ListableBeanFactoryExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-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. @@ -39,7 +39,7 @@ inline fun ListableBeanFactory.getBeansOfType(includeNonSingle /** * Extension for [ListableBeanFactory.getBeanNamesForAnnotation] providing a - * `getBeansOfType()` variant. + * `getBeanNamesForAnnotation()` variant. * * @author Sebastien Deleuze * @since 5.0 @@ -64,6 +64,6 @@ inline fun ListableBeanFactory.getBeansWithAnnotation() * @author Sebastien Deleuze * @since 5.0 */ -inline fun ListableBeanFactory.findAnnotationOnBean(beanName:String): Annotation? = +inline fun ListableBeanFactory.findAnnotationOnBean(beanName:String): T? = findAnnotationOnBean(beanName, T::class.java) diff --git a/spring-beans/src/main/resources/META-INF/spring.factories b/spring-beans/src/main/resources/META-INF/spring.factories deleted file mode 100644 index b623047ec2ba..000000000000 --- a/spring-beans/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1 +0,0 @@ -org.springframework.beans.BeanInfoFactory=org.springframework.beans.ExtendedBeanInfoFactory \ No newline at end of file diff --git a/spring-beans/src/main/resources/META-INF/spring/aot.factories b/spring-beans/src/main/resources/META-INF/spring/aot.factories new file mode 100644 index 000000000000..fd9edf06871c --- /dev/null +++ b/spring-beans/src/main/resources/META-INF/spring/aot.factories @@ -0,0 +1,5 @@ +org.springframework.aot.hint.RuntimeHintsRegistrar=\ +org.springframework.beans.factory.annotation.JakartaAnnotationsRuntimeHints + +org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor=\ +org.springframework.beans.factory.aot.BeanRegistrationsAotProcessor diff --git a/spring-beans/src/test/java/org/springframework/beans/AbstractPropertyAccessorTests.java b/spring-beans/src/test/java/org/springframework/beans/AbstractPropertyAccessorTests.java index 0bb5febde05f..cae248880f5e 100644 --- a/spring-beans/src/test/java/org/springframework/beans/AbstractPropertyAccessorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/AbstractPropertyAccessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -68,25 +68,25 @@ * @author Dave Syer * @author Stephane Nicoll */ -public abstract class AbstractPropertyAccessorTests { +abstract class AbstractPropertyAccessorTests { protected abstract AbstractPropertyAccessor createAccessor(Object target); @Test - public void createWithNullTarget() { + void createWithNullTarget() { assertThatIllegalArgumentException().isThrownBy(() -> createAccessor(null)); } @Test - public void isReadableProperty() { + void isReadableProperty() { AbstractPropertyAccessor accessor = createAccessor(new Simple("John", 2)); assertThat(accessor.isReadableProperty("name")).isTrue(); } @Test - public void isReadablePropertyNotReadable() { + void isReadablePropertyNotReadable() { AbstractPropertyAccessor accessor = createAccessor(new NoRead()); assertThat(accessor.isReadableProperty("age")).isFalse(); @@ -96,42 +96,42 @@ public void isReadablePropertyNotReadable() { * Shouldn't throw an exception: should just return false */ @Test - public void isReadablePropertyNoSuchProperty() { + void isReadablePropertyNoSuchProperty() { AbstractPropertyAccessor accessor = createAccessor(new NoRead()); assertThat(accessor.isReadableProperty("xxxxx")).isFalse(); } @Test - public void isReadablePropertyNull() { + void isReadablePropertyNull() { AbstractPropertyAccessor accessor = createAccessor(new NoRead()); assertThatIllegalArgumentException().isThrownBy(() -> accessor.isReadableProperty(null)); } @Test - public void isWritableProperty() { + void isWritableProperty() { AbstractPropertyAccessor accessor = createAccessor(new Simple("John", 2)); assertThat(accessor.isWritableProperty("name")).isTrue(); } @Test - public void isWritablePropertyNull() { + void isWritablePropertyNull() { AbstractPropertyAccessor accessor = createAccessor(new NoRead()); assertThatIllegalArgumentException().isThrownBy(() -> accessor.isWritableProperty(null)); } @Test - public void isWritablePropertyNoSuchProperty() { + void isWritablePropertyNoSuchProperty() { AbstractPropertyAccessor accessor = createAccessor(new NoRead()); assertThat(accessor.isWritableProperty("xxxxx")).isFalse(); } @Test - public void isReadableWritableForIndexedProperties() { + void isReadableWritableForIndexedProperties() { IndexedTestBean target = new IndexedTestBean(); AbstractPropertyAccessor accessor = createAccessor(target); @@ -177,28 +177,28 @@ public void isReadableWritableForIndexedProperties() { } @Test - public void getSimpleProperty() { + void getSimpleProperty() { Simple target = new Simple("John", 2); AbstractPropertyAccessor accessor = createAccessor(target); assertThat(accessor.getPropertyValue("name")).isEqualTo("John"); } @Test - public void getNestedProperty() { + void getNestedProperty() { Person target = createPerson("John", "London", "UK"); AbstractPropertyAccessor accessor = createAccessor(target); assertThat(accessor.getPropertyValue("address.city")).isEqualTo("London"); } @Test - public void getNestedDeepProperty() { + void getNestedDeepProperty() { Person target = createPerson("John", "London", "UK"); AbstractPropertyAccessor accessor = createAccessor(target); assertThat(accessor.getPropertyValue("address.country.name")).isEqualTo("UK"); } @Test - public void getAnotherNestedDeepProperty() { + void getAnotherNestedDeepProperty() { ITestBean target = new TestBean("rod", 31); ITestBean kerry = new TestBean("kerry", 35); target.setSpouse(kerry); @@ -213,7 +213,7 @@ public void getAnotherNestedDeepProperty() { } @Test - public void getPropertyIntermediatePropertyIsNull() { + void getPropertyIntermediatePropertyIsNull() { Person target = createPerson("John", "London", "UK"); target.address = null; AbstractPropertyAccessor accessor = createAccessor(target); @@ -226,7 +226,7 @@ public void getPropertyIntermediatePropertyIsNull() { } @Test - public void getPropertyIntermediatePropertyIsNullWithAutoGrow() { + void getPropertyIntermediatePropertyIsNullWithAutoGrow() { Person target = createPerson("John", "London", "UK"); target.address = null; AbstractPropertyAccessor accessor = createAccessor(target); @@ -236,7 +236,7 @@ public void getPropertyIntermediatePropertyIsNullWithAutoGrow() { } @Test - public void getPropertyIntermediateMapEntryIsNullWithAutoGrow() { + void getPropertyIntermediateMapEntryIsNullWithAutoGrow() { Foo target = new Foo(); AbstractPropertyAccessor accessor = createAccessor(target); accessor.setConversionService(new DefaultConversionService()); @@ -246,7 +246,7 @@ public void getPropertyIntermediateMapEntryIsNullWithAutoGrow() { } @Test - public void getUnknownProperty() { + void getUnknownProperty() { Simple target = new Simple("John", 2); AbstractPropertyAccessor accessor = createAccessor(target); assertThatExceptionOfType(NotReadablePropertyException.class).isThrownBy(() -> @@ -258,7 +258,7 @@ public void getUnknownProperty() { } @Test - public void getUnknownNestedProperty() { + void getUnknownNestedProperty() { Person target = createPerson("John", "London", "UK"); AbstractPropertyAccessor accessor = createAccessor(target); @@ -267,7 +267,7 @@ public void getUnknownNestedProperty() { } @Test - public void setSimpleProperty() { + void setSimpleProperty() { Simple target = new Simple("John", 2); AbstractPropertyAccessor accessor = createAccessor(target); @@ -278,7 +278,7 @@ public void setSimpleProperty() { } @Test - public void setNestedProperty() { + void setNestedProperty() { Person target = createPerson("John", "Paris", "FR"); AbstractPropertyAccessor accessor = createAccessor(target); @@ -287,7 +287,7 @@ public void setNestedProperty() { } @Test - public void setNestedPropertyPolymorphic() throws Exception { + void setNestedPropertyPolymorphic() throws Exception { ITestBean target = new TestBean("rod", 31); ITestBean kerry = new Employee(); @@ -296,10 +296,10 @@ public void setNestedPropertyPolymorphic() throws Exception { accessor.setPropertyValue("spouse.age", 35); accessor.setPropertyValue("spouse.name", "Kerry"); accessor.setPropertyValue("spouse.company", "Lewisham"); - assertThat(kerry.getName().equals("Kerry")).as("kerry name is Kerry").isTrue(); + assertThat(kerry.getName()).as("kerry name is Kerry").isEqualTo("Kerry"); assertThat(target.getSpouse() == kerry).as("nested set worked").isTrue(); - assertThat(kerry.getSpouse() == null).as("no back relation").isTrue(); + assertThat(kerry.getSpouse()).as("no back relation").isNull(); accessor.setPropertyValue(new PropertyValue("spouse.spouse", target)); assertThat(kerry.getSpouse() == target).as("nested set worked").isTrue(); @@ -308,7 +308,7 @@ public void setNestedPropertyPolymorphic() throws Exception { } @Test - public void setAnotherNestedProperty() throws Exception { + void setAnotherNestedProperty() throws Exception { ITestBean target = new TestBean("rod", 31); ITestBean kerry = new TestBean("kerry", 0); @@ -316,7 +316,7 @@ public void setAnotherNestedProperty() throws Exception { accessor.setPropertyValue("spouse", kerry); assertThat(target.getSpouse() == kerry).as("nested set worked").isTrue(); - assertThat(kerry.getSpouse() == null).as("no back relation").isTrue(); + assertThat(kerry.getSpouse()).as("no back relation").isNull(); accessor.setPropertyValue(new PropertyValue("spouse.spouse", target)); assertThat(kerry.getSpouse() == target).as("nested set worked").isTrue(); assertThat(kerry.getAge() == 0).as("kerry age not set").isTrue(); @@ -328,7 +328,7 @@ public void setAnotherNestedProperty() throws Exception { } @Test - public void setYetAnotherNestedProperties() { + void setYetAnotherNestedProperties() { String doctorCompany = ""; String lawyerCompany = "Dr. Sueem"; TestBean target = new TestBean(); @@ -340,7 +340,7 @@ public void setYetAnotherNestedProperties() { } @Test - public void setNestedDeepProperty() { + void setNestedDeepProperty() { Person target = createPerson("John", "Paris", "FR"); AbstractPropertyAccessor accessor = createAccessor(target); @@ -349,7 +349,7 @@ public void setNestedDeepProperty() { } @Test - public void testErrorMessageOfNestedProperty() { + void testErrorMessageOfNestedProperty() { ITestBean target = new TestBean(); ITestBean child = new DifferentTestBean(); child.setName("test"); @@ -364,7 +364,7 @@ public void testErrorMessageOfNestedProperty() { } @Test - public void setPropertyIntermediatePropertyIsNull() { + void setPropertyIntermediatePropertyIsNull() { Person target = createPerson("John", "Paris", "FR"); target.address.country = null; AbstractPropertyAccessor accessor = createAccessor(target); @@ -378,7 +378,7 @@ public void setPropertyIntermediatePropertyIsNull() { } @Test - public void setAnotherPropertyIntermediatePropertyIsNull() throws Exception { + void setAnotherPropertyIntermediatePropertyIsNull() throws Exception { ITestBean target = new TestBean("rod", 31); AbstractPropertyAccessor accessor = createAccessor(target); assertThatExceptionOfType(NullValueInNestedPathException.class).isThrownBy(() -> @@ -387,7 +387,7 @@ public void setAnotherPropertyIntermediatePropertyIsNull() throws Exception { } @Test - public void setPropertyIntermediatePropertyIsNullWithAutoGrow() { + void setPropertyIntermediatePropertyIsNullWithAutoGrow() { Person target = createPerson("John", "Paris", "FR"); target.address.country = null; AbstractPropertyAccessor accessor = createAccessor(target); @@ -398,7 +398,7 @@ public void setPropertyIntermediatePropertyIsNullWithAutoGrow() { } @Test - public void setPropertyIntermediateListIsNullWithAutoGrow() { + void setPropertyIntermediateListIsNullWithAutoGrow() { Foo target = new Foo(); AbstractPropertyAccessor accessor = createAccessor(target); accessor.setConversionService(new DefaultConversionService()); @@ -410,7 +410,7 @@ public void setPropertyIntermediateListIsNullWithAutoGrow() { } @Test - public void setPropertyIntermediateListIsNullWithNoConversionService() { + void setPropertyIntermediateListIsNullWithNoConversionService() { Foo target = new Foo(); AbstractPropertyAccessor accessor = createAccessor(target); accessor.setAutoGrowNestedPaths(true); @@ -419,7 +419,7 @@ public void setPropertyIntermediateListIsNullWithNoConversionService() { } @Test - public void setPropertyIntermediateListIsNullWithBadConversionService() { + void setPropertyIntermediateListIsNullWithBadConversionService() { Foo target = new Foo(); AbstractPropertyAccessor accessor = createAccessor(target); accessor.setConversionService(new GenericConversionService() { @@ -435,7 +435,7 @@ public Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceTy @Test - public void setEmptyPropertyValues() { + void setEmptyPropertyValues() { TestBean target = new TestBean(); int age = 50; String name = "Tony"; @@ -452,7 +452,7 @@ public void setEmptyPropertyValues() { @Test - public void setValidPropertyValues() { + void setValidPropertyValues() { TestBean target = new TestBean(); String newName = "tony"; int newAge = 65; @@ -469,7 +469,7 @@ public void setValidPropertyValues() { } @Test - public void setIndividualValidPropertyValues() { + void setIndividualValidPropertyValues() { TestBean target = new TestBean(); String newName = "tony"; int newAge = 65; @@ -478,39 +478,39 @@ public void setIndividualValidPropertyValues() { accessor.setPropertyValue("age", newAge); accessor.setPropertyValue(new PropertyValue("name", newName)); accessor.setPropertyValue(new PropertyValue("touchy", newTouchy)); - assertThat(target.getName().equals(newName)).as("Name property should have changed").isTrue(); - assertThat(target.getTouchy().equals(newTouchy)).as("Touchy property should have changed").isTrue(); + assertThat(target.getName()).as("Name property should have changed").isEqualTo(newName); + assertThat(target.getTouchy()).as("Touchy property should have changed").isEqualTo(newTouchy); assertThat(target.getAge() == newAge).as("Age property should have changed").isTrue(); } @Test - public void setPropertyIsReflectedImmediately() { + void setPropertyIsReflectedImmediately() { TestBean target = new TestBean(); int newAge = 33; AbstractPropertyAccessor accessor = createAccessor(target); target.setAge(newAge); Object bwAge = accessor.getPropertyValue("age"); - assertThat(bwAge instanceof Integer).as("Age is an integer").isTrue(); + assertThat(bwAge).as("Age is an integer").isInstanceOf(Integer.class); assertThat(bwAge).as("Bean wrapper must pick up changes").isEqualTo(newAge); } @Test - public void setPropertyToNull() { + void setPropertyToNull() { TestBean target = new TestBean(); target.setName("Frank"); // we need to change it back target.setSpouse(target); AbstractPropertyAccessor accessor = createAccessor(target); - assertThat(target.getName() != null).as("name is not null to start off").isTrue(); + assertThat(target.getName()).as("name is not null to start off").isNotNull(); accessor.setPropertyValue("name", null); - assertThat(target.getName() == null).as("name is now null").isTrue(); + assertThat(target.getName()).as("name is now null").isNull(); // now test with non-string - assertThat(target.getSpouse() != null).as("spouse is not null to start off").isTrue(); + assertThat(target.getSpouse()).as("spouse is not null to start off").isNotNull(); accessor.setPropertyValue("spouse", null); - assertThat(target.getSpouse() == null).as("spouse is now null").isTrue(); + assertThat(target.getSpouse()).as("spouse is now null").isNull(); } @Test - public void setIndexedPropertyIgnored() { + void setIndexedPropertyIgnored() { MutablePropertyValues values = new MutablePropertyValues(); values.add("toBeIgnored[0]", 42); AbstractPropertyAccessor accessor = createAccessor(new Object()); @@ -518,7 +518,7 @@ public void setIndexedPropertyIgnored() { } @Test - public void setPropertyWithPrimitiveConversion() { + void setPropertyWithPrimitiveConversion() { MutablePropertyValues values = new MutablePropertyValues(); values.add("name", 42); TestBean target = new TestBean(); @@ -528,7 +528,7 @@ public void setPropertyWithPrimitiveConversion() { } @Test - public void setPropertyWithCustomEditor() { + void setPropertyWithCustomEditor() { MutablePropertyValues values = new MutablePropertyValues(); values.add("name", Integer.class); TestBean target = new TestBean(); @@ -544,7 +544,7 @@ public void setValue(Object value) { } @Test - public void setStringPropertyWithCustomEditor() throws Exception { + void setStringPropertyWithCustomEditor() throws Exception { TestBean target = new TestBean(); AbstractPropertyAccessor accessor = createAccessor(target); accessor.registerCustomEditor(String.class, "name", new PropertyEditorSupport() { @@ -567,7 +567,7 @@ public void setValue(Object value) { } @Test - public void setBooleanProperty() { + void setBooleanProperty() { BooleanTestBean target = new BooleanTestBean(); AbstractPropertyAccessor accessor = createAccessor(target); @@ -581,7 +581,7 @@ public void setBooleanProperty() { } @Test - public void setNumberProperties() { + void setNumberProperties() { NumberTestBean target = new NumberTestBean(); AbstractPropertyAccessor accessor = createAccessor(target); accessor.setPropertyValue("short2", "2"); @@ -591,24 +591,24 @@ public void setNumberProperties() { accessor.setPropertyValue("float2", "8.1"); accessor.setPropertyValue("double2", "6.1"); accessor.setPropertyValue("bigDecimal", "4.0"); - assertThat(new Short("2").equals(accessor.getPropertyValue("short2"))).as("Correct short2 value").isTrue(); - assertThat(new Short("2").equals(target.getShort2())).as("Correct short2 value").isTrue(); - assertThat(new Integer("8").equals(accessor.getPropertyValue("int2"))).as("Correct int2 value").isTrue(); - assertThat(new Integer("8").equals(target.getInt2())).as("Correct int2 value").isTrue(); - assertThat(new Long("6").equals(accessor.getPropertyValue("long2"))).as("Correct long2 value").isTrue(); - assertThat(new Long("6").equals(target.getLong2())).as("Correct long2 value").isTrue(); - assertThat(new BigInteger("3").equals(accessor.getPropertyValue("bigInteger"))).as("Correct bigInteger value").isTrue(); - assertThat(new BigInteger("3").equals(target.getBigInteger())).as("Correct bigInteger value").isTrue(); - assertThat(new Float("8.1").equals(accessor.getPropertyValue("float2"))).as("Correct float2 value").isTrue(); - assertThat(new Float("8.1").equals(target.getFloat2())).as("Correct float2 value").isTrue(); - assertThat(new Double("6.1").equals(accessor.getPropertyValue("double2"))).as("Correct double2 value").isTrue(); - assertThat(new Double("6.1").equals(target.getDouble2())).as("Correct double2 value").isTrue(); - assertThat(new BigDecimal("4.0").equals(accessor.getPropertyValue("bigDecimal"))).as("Correct bigDecimal value").isTrue(); - assertThat(new BigDecimal("4.0").equals(target.getBigDecimal())).as("Correct bigDecimal value").isTrue(); - } - - @Test - public void setNumberPropertiesWithCoercion() { + assertThat(Short.valueOf("2")).as("Correct short2 value").isEqualTo(accessor.getPropertyValue("short2")); + assertThat(Short.valueOf("2")).as("Correct short2 value").isEqualTo(target.getShort2()); + assertThat(Integer.valueOf("8")).as("Correct int2 value").isEqualTo(accessor.getPropertyValue("int2")); + assertThat(Integer.valueOf("8")).as("Correct int2 value").isEqualTo(target.getInt2()); + assertThat(Long.valueOf("6")).as("Correct long2 value").isEqualTo(accessor.getPropertyValue("long2")); + assertThat(Long.valueOf("6")).as("Correct long2 value").isEqualTo(target.getLong2()); + assertThat(new BigInteger("3")).as("Correct bigInteger value").isEqualTo(accessor.getPropertyValue("bigInteger")); + assertThat(new BigInteger("3")).as("Correct bigInteger value").isEqualTo(target.getBigInteger()); + assertThat(Float.valueOf("8.1")).as("Correct float2 value").isEqualTo(accessor.getPropertyValue("float2")); + assertThat(Float.valueOf("8.1")).as("Correct float2 value").isEqualTo(target.getFloat2()); + assertThat(Double.valueOf("6.1").equals(accessor.getPropertyValue("double2"))).as("Correct double2 value").isTrue(); + assertThat(Double.valueOf("6.1")).as("Correct double2 value").isEqualTo(target.getDouble2()); + assertThat(new BigDecimal("4.0")).as("Correct bigDecimal value").isEqualTo(accessor.getPropertyValue("bigDecimal")); + assertThat(new BigDecimal("4.0")).as("Correct bigDecimal value").isEqualTo(target.getBigDecimal()); + } + + @Test + void setNumberPropertiesWithCoercion() { NumberTestBean target = new NumberTestBean(); AbstractPropertyAccessor accessor = createAccessor(target); accessor.setPropertyValue("short2", 2); @@ -616,26 +616,26 @@ public void setNumberPropertiesWithCoercion() { accessor.setPropertyValue("long2", new BigInteger("6")); accessor.setPropertyValue("bigInteger", 3L); accessor.setPropertyValue("float2", 8.1D); - accessor.setPropertyValue("double2", new BigDecimal(6.1)); + accessor.setPropertyValue("double2", new BigDecimal("6.1")); accessor.setPropertyValue("bigDecimal", 4.0F); - assertThat(new Short("2").equals(accessor.getPropertyValue("short2"))).as("Correct short2 value").isTrue(); - assertThat(new Short("2").equals(target.getShort2())).as("Correct short2 value").isTrue(); - assertThat(new Integer("8").equals(accessor.getPropertyValue("int2"))).as("Correct int2 value").isTrue(); - assertThat(new Integer("8").equals(target.getInt2())).as("Correct int2 value").isTrue(); - assertThat(new Long("6").equals(accessor.getPropertyValue("long2"))).as("Correct long2 value").isTrue(); - assertThat(new Long("6").equals(target.getLong2())).as("Correct long2 value").isTrue(); + assertThat(Short.valueOf("2")).as("Correct short2 value").isEqualTo(accessor.getPropertyValue("short2")); + assertThat(Short.valueOf("2")).as("Correct short2 value").isEqualTo(target.getShort2()); + assertThat(Integer.valueOf("8")).as("Correct int2 value").isEqualTo(accessor.getPropertyValue("int2")); + assertThat(Integer.valueOf("8")).as("Correct int2 value").isEqualTo(target.getInt2()); + assertThat(Long.valueOf("6")).as("Correct long2 value").isEqualTo(accessor.getPropertyValue("long2")); + assertThat(Long.valueOf("6")).as("Correct long2 value").isEqualTo(target.getLong2()); assertThat(new BigInteger("3").equals(accessor.getPropertyValue("bigInteger"))).as("Correct bigInteger value").isTrue(); - assertThat(new BigInteger("3").equals(target.getBigInteger())).as("Correct bigInteger value").isTrue(); - assertThat(new Float("8.1").equals(accessor.getPropertyValue("float2"))).as("Correct float2 value").isTrue(); - assertThat(new Float("8.1").equals(target.getFloat2())).as("Correct float2 value").isTrue(); - assertThat(new Double("6.1").equals(accessor.getPropertyValue("double2"))).as("Correct double2 value").isTrue(); - assertThat(new Double("6.1").equals(target.getDouble2())).as("Correct double2 value").isTrue(); - assertThat(new BigDecimal("4.0").equals(accessor.getPropertyValue("bigDecimal"))).as("Correct bigDecimal value").isTrue(); - assertThat(new BigDecimal("4.0").equals(target.getBigDecimal())).as("Correct bigDecimal value").isTrue(); + assertThat(new BigInteger("3")).as("Correct bigInteger value").isEqualTo(target.getBigInteger()); + assertThat(Float.valueOf("8.1")).as("Correct float2 value").isEqualTo(accessor.getPropertyValue("float2")); + assertThat(Float.valueOf("8.1")).as("Correct float2 value").isEqualTo(target.getFloat2()); + assertThat(Double.valueOf("6.1")).as("Correct double2 value").isEqualTo(accessor.getPropertyValue("double2")); + assertThat(Double.valueOf("6.1")).as("Correct double2 value").isEqualTo(target.getDouble2()); + assertThat(new BigDecimal("4.0")).as("Correct bigDecimal value").isEqualTo(accessor.getPropertyValue("bigDecimal")); + assertThat(new BigDecimal("4.0")).as("Correct bigDecimal value").isEqualTo(target.getBigDecimal()); } @Test - public void setPrimitiveProperties() { + void setPrimitiveProperties() { NumberPropertyBean target = new NumberPropertyBean(); AbstractPropertyAccessor accessor = createAccessor(target); @@ -677,14 +677,14 @@ public void setPrimitiveProperties() { assertThat(target.getMyLong().longValue()).isEqualTo(Long.MAX_VALUE); assertThat((double) target.getMyPrimitiveFloat()).isCloseTo(Float.MAX_VALUE, within(0.001)); - assertThat((double) target.getMyFloat().floatValue()).isCloseTo(Float.MAX_VALUE, within(0.001)); + assertThat((double) target.getMyFloat()).isCloseTo(Float.MAX_VALUE, within(0.001)); assertThat(target.getMyPrimitiveDouble()).isCloseTo(Double.MAX_VALUE, within(0.001)); assertThat(target.getMyDouble().doubleValue()).isCloseTo(Double.MAX_VALUE, within(0.001)); } @Test - public void setEnumProperty() { + void setEnumProperty() { EnumTester target = new EnumTester(); AbstractPropertyAccessor accessor = createAccessor(target); @@ -699,7 +699,7 @@ public void setEnumProperty() { } @Test - public void setGenericEnumProperty() { + void setGenericEnumProperty() { EnumConsumer target = new EnumConsumer(); AbstractPropertyAccessor accessor = createAccessor(target); accessor.setPropertyValue("enumValue", TestEnum.class.getName() + ".TEST_VALUE"); @@ -707,7 +707,7 @@ public void setGenericEnumProperty() { } @Test - public void setWildcardEnumProperty() { + void setWildcardEnumProperty() { WildcardEnumConsumer target = new WildcardEnumConsumer(); AbstractPropertyAccessor accessor = createAccessor(target); accessor.setPropertyValue("enumValue", TestEnum.class.getName() + ".TEST_VALUE"); @@ -715,7 +715,7 @@ public void setWildcardEnumProperty() { } @Test - public void setPropertiesProperty() throws Exception { + void setPropertiesProperty() throws Exception { PropsTester target = new PropsTester(); AbstractPropertyAccessor accessor = createAccessor(target); accessor.setPropertyValue("name", "ptest"); @@ -724,53 +724,41 @@ public void setPropertiesProperty() throws Exception { String ps = "peace=war\nfreedom=slavery"; accessor.setPropertyValue("properties", ps); - assertThat(target.name.equals("ptest")).as("name was set").isTrue(); - assertThat(target.properties != null).as("properties non null").isTrue(); + assertThat(target.name).as("name was set").isEqualTo("ptest"); + assertThat(target.properties).as("properties non null").isNotNull(); String freedomVal = target.properties.getProperty("freedom"); String peaceVal = target.properties.getProperty("peace"); - assertThat(peaceVal.equals("war")).as("peace==war").isTrue(); + assertThat(peaceVal).as("peace==war").isEqualTo("war"); assertThat(freedomVal.equals("slavery")).as("Freedom==slavery").isTrue(); } @Test - public void setStringArrayProperty() throws Exception { + void setStringArrayProperty() throws Exception { PropsTester target = new PropsTester(); AbstractPropertyAccessor accessor = createAccessor(target); - accessor.setPropertyValue("stringArray", new String[] {"foo", "fi", "fi", "fum"}); - assertThat(target.stringArray.length == 4).as("stringArray length = 4").isTrue(); - assertThat(target.stringArray[0].equals("foo") && target.stringArray[1].equals("fi") && - target.stringArray[2].equals("fi") && target.stringArray[3].equals("fum")).as("correct values").isTrue(); + accessor.setPropertyValue("stringArray", new String[]{"foo", "fi", "fi", "fum"}); + assertThat(target.stringArray).containsExactly("foo", "fi", "fi", "fum"); - List list = new ArrayList<>(); - list.add("foo"); - list.add("fi"); - list.add("fi"); - list.add("fum"); - accessor.setPropertyValue("stringArray", list); - assertThat(target.stringArray.length == 4).as("stringArray length = 4").isTrue(); - assertThat(target.stringArray[0].equals("foo") && target.stringArray[1].equals("fi") && - target.stringArray[2].equals("fi") && target.stringArray[3].equals("fum")).as("correct values").isTrue(); + accessor.setPropertyValue("stringArray", Arrays.asList("foo", "fi", "fi", "fum")); + assertThat(target.stringArray).containsExactly("foo", "fi", "fi", "fum"); Set set = new HashSet<>(); set.add("foo"); set.add("fi"); set.add("fum"); accessor.setPropertyValue("stringArray", set); - assertThat(target.stringArray.length == 3).as("stringArray length = 3").isTrue(); - List result = Arrays.asList(target.stringArray); - assertThat(result.contains("foo") && result.contains("fi") && result.contains("fum")).as("correct values").isTrue(); + assertThat(target.stringArray).containsExactlyInAnyOrder("foo", "fi", "fum"); accessor.setPropertyValue("stringArray", "one"); - assertThat(target.stringArray.length == 1).as("stringArray length = 1").isTrue(); - assertThat(target.stringArray[0].equals("one")).as("stringArray elt is ok").isTrue(); + assertThat(target.stringArray).containsExactly("one"); accessor.setPropertyValue("stringArray", null); - assertThat(target.stringArray == null).as("stringArray is null").isTrue(); + assertThat(target.stringArray).as("stringArray is null").isNull(); } @Test - public void setStringArrayPropertyWithCustomStringEditor() throws Exception { + void setStringArrayPropertyWithCustomStringEditor() throws Exception { PropsTester target = new PropsTester(); AbstractPropertyAccessor accessor = createAccessor(target); accessor.registerCustomEditor(String.class, "stringArray", new PropertyEditorSupport() { @@ -781,126 +769,90 @@ public void setAsText(String text) { }); accessor.setPropertyValue("stringArray", new String[] {"4foo", "7fi", "6fi", "5fum"}); - assertThat(target.stringArray.length == 4).as("stringArray length = 4").isTrue(); - assertThat(target.stringArray[0].equals("foo") && target.stringArray[1].equals("fi") && - target.stringArray[2].equals("fi") && target.stringArray[3].equals("fum")).as("correct values").isTrue(); + assertThat(target.stringArray).containsExactly("foo", "fi", "fi", "fum"); - List list = new ArrayList<>(); - list.add("4foo"); - list.add("7fi"); - list.add("6fi"); - list.add("5fum"); + List list = Arrays.asList("4foo", "7fi", "6fi", "5fum"); accessor.setPropertyValue("stringArray", list); - assertThat(target.stringArray.length == 4).as("stringArray length = 4").isTrue(); - assertThat(target.stringArray[0].equals("foo") && target.stringArray[1].equals("fi") && - target.stringArray[2].equals("fi") && target.stringArray[3].equals("fum")).as("correct values").isTrue(); + assertThat(target.stringArray).containsExactly("foo", "fi", "fi", "fum"); Set set = new HashSet<>(); set.add("4foo"); set.add("7fi"); set.add("6fum"); accessor.setPropertyValue("stringArray", set); - assertThat(target.stringArray.length == 3).as("stringArray length = 3").isTrue(); - List result = Arrays.asList(target.stringArray); - assertThat(result.contains("foo") && result.contains("fi") && result.contains("fum")).as("correct values").isTrue(); + assertThat(target.stringArray).containsExactlyInAnyOrder("foo", "fi", "fum"); accessor.setPropertyValue("stringArray", "8one"); - assertThat(target.stringArray.length == 1).as("stringArray length = 1").isTrue(); - assertThat(target.stringArray[0].equals("one")).as("correct values").isTrue(); + assertThat(target.stringArray).containsExactly("one"); } @Test - public void setStringArrayPropertyWithStringSplitting() throws Exception { + void setStringArrayPropertyWithStringSplitting() throws Exception { PropsTester target = new PropsTester(); AbstractPropertyAccessor accessor = createAccessor(target); accessor.useConfigValueEditors(); accessor.setPropertyValue("stringArray", "a1,b2"); - assertThat(target.stringArray.length == 2).as("stringArray length = 2").isTrue(); - assertThat(target.stringArray[0].equals("a1") && target.stringArray[1].equals("b2")).as("correct values").isTrue(); + assertThat(target.stringArray).containsExactly("a1", "b2"); } @Test - public void setStringArrayPropertyWithCustomStringDelimiter() throws Exception { + void setStringArrayPropertyWithCustomStringDelimiter() throws Exception { PropsTester target = new PropsTester(); AbstractPropertyAccessor accessor = createAccessor(target); accessor.registerCustomEditor(String[].class, "stringArray", new StringArrayPropertyEditor("-")); accessor.setPropertyValue("stringArray", "a1-b2"); - assertThat(target.stringArray.length == 2).as("stringArray length = 2").isTrue(); - assertThat(target.stringArray[0].equals("a1") && target.stringArray[1].equals("b2")).as("correct values").isTrue(); + assertThat(target.stringArray).containsExactly("a1", "b2"); } @Test - public void setStringArrayWithAutoGrow() throws Exception { + void setStringArrayWithAutoGrow() throws Exception { StringArrayBean target = new StringArrayBean(); AbstractPropertyAccessor accessor = createAccessor(target); accessor.setAutoGrowNestedPaths(true); accessor.setPropertyValue("array[0]", "Test0"); - assertThat(target.getArray().length).isEqualTo(1); + assertThat(target.getArray()).containsExactly("Test0"); accessor.setPropertyValue("array[2]", "Test2"); - assertThat(target.getArray().length).isEqualTo(3); - assertThat(target.getArray()[0].equals("Test0") && target.getArray()[1] == null && - target.getArray()[2].equals("Test2")).as("correct values").isTrue(); + assertThat(target.getArray()).containsExactly("Test0", null, "Test2"); } @Test - public void setIntArrayProperty() { + void setIntArrayProperty() { PropsTester target = new PropsTester(); AbstractPropertyAccessor accessor = createAccessor(target); accessor.setPropertyValue("intArray", new int[] {4, 5, 2, 3}); - assertThat(target.intArray.length == 4).as("intArray length = 4").isTrue(); - assertThat(target.intArray[0] == 4 && target.intArray[1] == 5 && - target.intArray[2] == 2 && target.intArray[3] == 3).as("correct values").isTrue(); + assertThat(target.intArray).containsExactly(4, 5, 2, 3); accessor.setPropertyValue("intArray", new String[] {"4", "5", "2", "3"}); - assertThat(target.intArray.length == 4).as("intArray length = 4").isTrue(); - assertThat(target.intArray[0] == 4 && target.intArray[1] == 5 && - target.intArray[2] == 2 && target.intArray[3] == 3).as("correct values").isTrue(); - - List list = new ArrayList<>(); - list.add(4); - list.add("5"); - list.add(2); - list.add("3"); - accessor.setPropertyValue("intArray", list); - assertThat(target.intArray.length == 4).as("intArray length = 4").isTrue(); - assertThat(target.intArray[0] == 4 && target.intArray[1] == 5 && - target.intArray[2] == 2 && target.intArray[3] == 3).as("correct values").isTrue(); + assertThat(target.intArray).containsExactly(4, 5, 2, 3); + + accessor.setPropertyValue("intArray", Arrays.asList(4, "5", 2, "3")); + assertThat(target.intArray).containsExactly(4, 5, 2, 3); Set set = new HashSet<>(); set.add("4"); set.add(5); set.add("3"); accessor.setPropertyValue("intArray", set); - assertThat(target.intArray.length == 3).as("intArray length = 3").isTrue(); - List result = new ArrayList<>(); - result.add(target.intArray[0]); - result.add(target.intArray[1]); - result.add(target.intArray[2]); - assertThat(result.contains(4) && result.contains(5) && - result.contains(3)).as("correct values").isTrue(); + assertThat(target.intArray).containsExactlyInAnyOrder(4, 5, 3); accessor.setPropertyValue("intArray", new Integer[] {1}); - assertThat(target.intArray.length == 1).as("intArray length = 4").isTrue(); - assertThat(target.intArray[0] == 1).as("correct values").isTrue(); + assertThat(target.intArray).containsExactly(1); accessor.setPropertyValue("intArray", 1); - assertThat(target.intArray.length == 1).as("intArray length = 4").isTrue(); - assertThat(target.intArray[0] == 1).as("correct values").isTrue(); + assertThat(target.intArray).containsExactly(1); accessor.setPropertyValue("intArray", new String[] {"1"}); - assertThat(target.intArray.length == 1).as("intArray length = 4").isTrue(); - assertThat(target.intArray[0] == 1).as("correct values").isTrue(); + assertThat(target.intArray).containsExactly(1); accessor.setPropertyValue("intArray", "1"); - assertThat(target.intArray.length == 1).as("intArray length = 4").isTrue(); - assertThat(target.intArray[0] == 1).as("correct values").isTrue(); + assertThat(target.intArray).containsExactly(1); } @Test - public void setIntArrayPropertyWithCustomEditor() { + void setIntArrayPropertyWithCustomEditor() { PropsTester target = new PropsTester(); AbstractPropertyAccessor accessor = createAccessor(target); accessor.registerCustomEditor(int.class, new PropertyEditorSupport() { @@ -911,50 +863,40 @@ public void setAsText(String text) { }); accessor.setPropertyValue("intArray", new int[] {4, 5, 2, 3}); - assertThat(target.intArray.length == 4).as("intArray length = 4").isTrue(); - assertThat(target.intArray[0] == 4 && target.intArray[1] == 5 && - target.intArray[2] == 2 && target.intArray[3] == 3).as("correct values").isTrue(); + assertThat(target.intArray).containsExactly(4, 5, 2, 3); accessor.setPropertyValue("intArray", new String[] {"3", "4", "1", "2"}); - assertThat(target.intArray.length == 4).as("intArray length = 4").isTrue(); - assertThat(target.intArray[0] == 4 && target.intArray[1] == 5 && - target.intArray[2] == 2 && target.intArray[3] == 3).as("correct values").isTrue(); + assertThat(target.intArray).containsExactly(4, 5, 2, 3); accessor.setPropertyValue("intArray", 1); - assertThat(target.intArray.length == 1).as("intArray length = 4").isTrue(); - assertThat(target.intArray[0] == 1).as("correct values").isTrue(); + assertThat(target.intArray).containsExactly(1); - accessor.setPropertyValue("intArray", new String[] {"0"}); - assertThat(target.intArray.length == 1).as("intArray length = 4").isTrue(); - assertThat(target.intArray[0] == 1).as("correct values").isTrue(); + accessor.setPropertyValue("intArray", new String[]{"0"}); + assertThat(target.intArray).containsExactly(1); accessor.setPropertyValue("intArray", "0"); - assertThat(target.intArray.length == 1).as("intArray length = 4").isTrue(); - assertThat(target.intArray[0] == 1).as("correct values").isTrue(); + assertThat(target.intArray).containsExactly(1); } @Test - public void setIntArrayPropertyWithStringSplitting() throws Exception { + void setIntArrayPropertyWithStringSplitting() throws Exception { PropsTester target = new PropsTester(); AbstractPropertyAccessor accessor = createAccessor(target); accessor.useConfigValueEditors(); accessor.setPropertyValue("intArray", "4,5"); - assertThat(target.intArray.length == 2).as("intArray length = 2").isTrue(); - assertThat(target.intArray[0] == 4 && target.intArray[1] == 5).as("correct values").isTrue(); + assertThat(target.intArray).containsExactly(4, 5); } @Test - public void setPrimitiveArrayProperty() { + void setPrimitiveArrayProperty() { PrimitiveArrayBean target = new PrimitiveArrayBean(); AbstractPropertyAccessor accessor = createAccessor(target); - accessor.setPropertyValue("array", new String[] {"1", "2"}); - assertThat(target.getArray().length).isEqualTo(2); - assertThat(target.getArray()[0]).isEqualTo(1); - assertThat(target.getArray()[1]).isEqualTo(2); + accessor.setPropertyValue("array", new String[]{"1", "2"}); + assertThat(target.getArray()).containsExactly(1, 2); } @Test - public void setPrimitiveArrayPropertyLargeMatchingWithSpecificEditor() { + void setPrimitiveArrayPropertyLargeMatchingWithSpecificEditor() { PrimitiveArrayBean target = new PrimitiveArrayBean(); AbstractPropertyAccessor accessor = createAccessor(target); accessor.registerCustomEditor(int.class, "array", new PropertyEditorSupport() { @@ -965,15 +907,14 @@ public void setValue(Object value) { } } }); - int[] input = new int[1024]; + int[] input = new int[10]; accessor.setPropertyValue("array", input); - assertThat(target.getArray().length).isEqualTo(1024); - assertThat(target.getArray()[0]).isEqualTo(1); - assertThat(target.getArray()[1]).isEqualTo(1); + assertThat(target.getArray()).hasSize(10); + assertThat(Arrays.stream(target.getArray())).allMatch(n -> n == 1); } @Test - public void setPrimitiveArrayPropertyLargeMatchingWithIndexSpecificEditor() { + void setPrimitiveArrayPropertyLargeMatchingWithIndexSpecificEditor() { PrimitiveArrayBean target = new PrimitiveArrayBean(); AbstractPropertyAccessor accessor = createAccessor(target); accessor.registerCustomEditor(int.class, "array[1]", new PropertyEditorSupport() { @@ -984,49 +925,38 @@ public void setValue(Object value) { } } }); - int[] input = new int[1024]; + int[] input = new int[10]; accessor.setPropertyValue("array", input); - assertThat(target.getArray().length).isEqualTo(1024); - assertThat(target.getArray()[0]).isEqualTo(0); + assertThat(target.getArray()).hasSize(10); + assertThat(target.getArray()[0]).isZero(); assertThat(target.getArray()[1]).isEqualTo(1); + assertThat(Arrays.stream(target.getArray()).skip(2)).allMatch(n -> n == 0); } @Test - public void setPrimitiveArrayPropertyWithAutoGrow() throws Exception { + void setPrimitiveArrayPropertyWithAutoGrow() throws Exception { PrimitiveArrayBean target = new PrimitiveArrayBean(); AbstractPropertyAccessor accessor = createAccessor(target); accessor.setAutoGrowNestedPaths(true); accessor.setPropertyValue("array[0]", 1); - assertThat(target.getArray().length).isEqualTo(1); + assertThat(target.getArray()).containsExactly(1); accessor.setPropertyValue("array[2]", 3); - assertThat(target.getArray().length).isEqualTo(3); - assertThat(target.getArray()[0] == 1 && target.getArray()[1] == 0 && - target.getArray()[2] == 3).as("correct values").isTrue(); + assertThat(target.getArray()).containsExactly(1, 0, 3); } @Test - @SuppressWarnings("rawtypes") - public void setGenericArrayProperty() { + void setGenericArrayProperty() { + @SuppressWarnings("rawtypes") SkipReaderStub target = new SkipReaderStub(); AbstractPropertyAccessor accessor = createAccessor(target); - List values = new ArrayList<>(); - values.add("1"); - values.add("2"); - values.add("3"); - values.add("4"); - accessor.setPropertyValue("items", values); - Object[] result = target.items; - assertThat(result.length).isEqualTo(4); - assertThat(result[0]).isEqualTo("1"); - assertThat(result[1]).isEqualTo("2"); - assertThat(result[2]).isEqualTo("3"); - assertThat(result[3]).isEqualTo("4"); + accessor.setPropertyValue("items", Arrays.asList("1", "2", "3", "4")); + assertThat(target.items).containsExactly("1", "2", "3", "4"); } @Test - public void setArrayPropertyToObject() { + void setArrayPropertyToObject() { ArrayToObject target = new ArrayToObject(); AbstractPropertyAccessor accessor = createAccessor(target); @@ -1040,7 +970,7 @@ public void setArrayPropertyToObject() { } @Test - public void setCollectionProperty() { + void setCollectionProperty() { IndexedTestBean target = new IndexedTestBean(); AbstractPropertyAccessor accessor = createAccessor(target); Collection coll = new HashSet<>(); @@ -1061,9 +991,9 @@ public void setCollectionProperty() { assertThat((List) target.getList()).isSameAs(list); } - @SuppressWarnings("unchecked") // list cannot be properly parameterized as it breaks other tests @Test - public void setCollectionPropertyNonMatchingType() { + @SuppressWarnings("unchecked") // list cannot be properly parameterized as it breaks other tests + void setCollectionPropertyNonMatchingType() { IndexedTestBean target = new IndexedTestBean(); AbstractPropertyAccessor accessor = createAccessor(target); Collection coll = new ArrayList<>(); @@ -1078,7 +1008,7 @@ public void setCollectionPropertyNonMatchingType() { Set list = new HashSet<>(); list.add("list1"); accessor.setPropertyValue("list", list); - assertThat(target.getCollection().size()).isEqualTo(1); + assertThat(target.getCollection()).hasSize(1); assertThat(target.getCollection().containsAll(coll)).isTrue(); assertThat(target.getSet().size()).isEqualTo(1); assertThat(target.getSet().containsAll(set)).isTrue(); @@ -1088,9 +1018,9 @@ public void setCollectionPropertyNonMatchingType() { assertThat(target.getList().containsAll(list)).isTrue(); } - @SuppressWarnings("unchecked") // list cannot be properly parameterized as it breaks other tests @Test - public void setCollectionPropertyWithArrayValue() { + @SuppressWarnings("unchecked") // list cannot be properly parameterized as it breaks other tests + void setCollectionPropertyWithArrayValue() { IndexedTestBean target = new IndexedTestBean(); AbstractPropertyAccessor accessor = createAccessor(target); Collection coll = new HashSet<>(); @@ -1105,7 +1035,7 @@ public void setCollectionPropertyWithArrayValue() { Set list = new HashSet<>(); list.add("list1"); accessor.setPropertyValue("list", list.toArray()); - assertThat(target.getCollection().size()).isEqualTo(1); + assertThat(target.getCollection()).hasSize(1); assertThat(target.getCollection().containsAll(coll)).isTrue(); assertThat(target.getSet().size()).isEqualTo(1); assertThat(target.getSet().containsAll(set)).isTrue(); @@ -1115,24 +1045,24 @@ public void setCollectionPropertyWithArrayValue() { assertThat(target.getList().containsAll(list)).isTrue(); } - @SuppressWarnings("unchecked") // list cannot be properly parameterized as it breaks other tests @Test - public void setCollectionPropertyWithIntArrayValue() { + @SuppressWarnings("unchecked") // list cannot be properly parameterized as it breaks other tests + void setCollectionPropertyWithIntArrayValue() { IndexedTestBean target = new IndexedTestBean(); AbstractPropertyAccessor accessor = createAccessor(target); Collection coll = new HashSet<>(); coll.add(0); - accessor.setPropertyValue("collection", new int[] {0}); + accessor.setPropertyValue("collection", new int[]{0}); List set = new ArrayList<>(); set.add(1); - accessor.setPropertyValue("set", new int[] {1}); + accessor.setPropertyValue("set", new int[]{1}); List sortedSet = new ArrayList<>(); sortedSet.add(2); - accessor.setPropertyValue("sortedSet", new int[] {2}); + accessor.setPropertyValue("sortedSet", new int[]{2}); Set list = new HashSet<>(); list.add(3); - accessor.setPropertyValue("list", new int[] {3}); - assertThat(target.getCollection().size()).isEqualTo(1); + accessor.setPropertyValue("list", new int[]{3}); + assertThat(target.getCollection()).hasSize(1); assertThat(target.getCollection().containsAll(coll)).isTrue(); assertThat(target.getSet().size()).isEqualTo(1); assertThat(target.getSet().containsAll(set)).isTrue(); @@ -1142,9 +1072,9 @@ public void setCollectionPropertyWithIntArrayValue() { assertThat(target.getList().containsAll(list)).isTrue(); } - @SuppressWarnings("unchecked") // list cannot be properly parameterized as it breaks other tests @Test - public void setCollectionPropertyWithIntegerValue() { + @SuppressWarnings("unchecked") // list cannot be properly parameterized as it breaks other tests + void setCollectionPropertyWithIntegerValue() { IndexedTestBean target = new IndexedTestBean(); AbstractPropertyAccessor accessor = createAccessor(target); Collection coll = new HashSet<>(); @@ -1159,7 +1089,7 @@ public void setCollectionPropertyWithIntegerValue() { Set list = new HashSet<>(); list.add(3); accessor.setPropertyValue("list", 3); - assertThat(target.getCollection().size()).isEqualTo(1); + assertThat(target.getCollection()).hasSize(1); assertThat(target.getCollection().containsAll(coll)).isTrue(); assertThat(target.getSet().size()).isEqualTo(1); assertThat(target.getSet().containsAll(set)).isTrue(); @@ -1169,9 +1099,9 @@ public void setCollectionPropertyWithIntegerValue() { assertThat(target.getList().containsAll(list)).isTrue(); } - @SuppressWarnings("unchecked") // list cannot be properly parameterized as it breaks other tests @Test - public void setCollectionPropertyWithStringValue() { + @SuppressWarnings("unchecked") // list cannot be properly parameterized as it breaks other tests + void setCollectionPropertyWithStringValue() { IndexedTestBean target = new IndexedTestBean(); AbstractPropertyAccessor accessor = createAccessor(target); List set = new ArrayList<>(); @@ -1192,7 +1122,7 @@ public void setCollectionPropertyWithStringValue() { } @Test - public void setCollectionPropertyWithStringValueAndCustomEditor() { + void setCollectionPropertyWithStringValueAndCustomEditor() { IndexedTestBean target = new IndexedTestBean(); AbstractPropertyAccessor accessor = createAccessor(target); accessor.registerCustomEditor(String.class, "set", new StringTrimmerEditor(false)); @@ -1213,7 +1143,7 @@ public void setCollectionPropertyWithStringValueAndCustomEditor() { } @Test - public void setMapProperty() { + void setMapProperty() { IndexedTestBean target = new IndexedTestBean(); AbstractPropertyAccessor accessor = createAccessor(target); Map map = new HashMap<>(); @@ -1227,7 +1157,8 @@ public void setMapProperty() { } @Test - public void setMapPropertyNonMatchingType() { + @SuppressWarnings("unchecked") + void setMapPropertyNonMatchingType() { IndexedTestBean target = new IndexedTestBean(); AbstractPropertyAccessor accessor = createAccessor(target); Map map = new TreeMap<>(); @@ -1236,14 +1167,14 @@ public void setMapPropertyNonMatchingType() { Map sortedMap = new TreeMap<>(); sortedMap.put("sortedKey", "sortedValue"); accessor.setPropertyValue("sortedMap", sortedMap); - assertThat(target.getMap().size()).isEqualTo(1); + assertThat(target.getMap()).hasSize(1); assertThat(target.getMap().get("key")).isEqualTo("value"); assertThat(target.getSortedMap().size()).isEqualTo(1); assertThat(target.getSortedMap().get("sortedKey")).isEqualTo("sortedValue"); } @Test - public void setMapPropertyWithTypeConversion() { + void setMapPropertyWithTypeConversion() { IndexedTestBean target = new IndexedTestBean(); AbstractPropertyAccessor accessor = createAccessor(target); accessor.registerCustomEditor(TestBean.class, new PropertyEditorSupport() { @@ -1272,7 +1203,7 @@ public void setAsText(String text) throws IllegalArgumentException { } @Test - public void setMapPropertyWithUnmodifiableMap() { + void setMapPropertyWithUnmodifiableMap() { IndexedTestBean target = new IndexedTestBean(); AbstractPropertyAccessor accessor = createAccessor(target); accessor.registerCustomEditor(TestBean.class, "map", new PropertyEditorSupport() { @@ -1296,7 +1227,7 @@ public void setAsText(String text) throws IllegalArgumentException { } @Test - public void setMapPropertyWithCustomUnmodifiableMap() { + void setMapPropertyWithCustomUnmodifiableMap() { IndexedTestBean target = new IndexedTestBean(); AbstractPropertyAccessor accessor = createAccessor(target); accessor.registerCustomEditor(TestBean.class, "map", new PropertyEditorSupport() { @@ -1319,9 +1250,9 @@ public void setAsText(String text) throws IllegalArgumentException { assertThat(((TestBean) target.getMap().get(2)).getName()).isEqualTo("rob"); } - @SuppressWarnings({ "unchecked", "rawtypes" }) // must work with raw map in this test @Test - public void setRawMapPropertyWithNoEditorRegistered() { + @SuppressWarnings({ "unchecked", "rawtypes" }) // must work with raw map in this test + void setRawMapPropertyWithNoEditorRegistered() { IndexedTestBean target = new IndexedTestBean(); AbstractPropertyAccessor accessor = createAccessor(target); Map inputMap = new HashMap(); @@ -1336,7 +1267,7 @@ public void setRawMapPropertyWithNoEditorRegistered() { } @Test - public void setUnknownProperty() { + void setUnknownProperty() { Simple target = new Simple("John", 2); AbstractPropertyAccessor accessor = createAccessor(target); assertThatExceptionOfType(NotWritablePropertyException.class).isThrownBy(() -> @@ -1349,7 +1280,7 @@ public void setUnknownProperty() { } @Test - public void setUnknownPropertyWithPossibleMatches() { + void setUnknownPropertyWithPossibleMatches() { Simple target = new Simple("John", 2); AbstractPropertyAccessor accessor = createAccessor(target); assertThatExceptionOfType(NotWritablePropertyException.class).isThrownBy(() -> @@ -1361,7 +1292,7 @@ public void setUnknownPropertyWithPossibleMatches() { } @Test - public void setUnknownOptionalProperty() { + void setUnknownOptionalProperty() { Simple target = new Simple("John", 2); AbstractPropertyAccessor accessor = createAccessor(target); PropertyValue value = new PropertyValue("foo", "value"); @@ -1370,7 +1301,7 @@ public void setUnknownOptionalProperty() { } @Test - public void setPropertyInProtectedBaseBean() { + void setPropertyInProtectedBaseBean() { DerivedFromProtectedBaseBean target = new DerivedFromProtectedBaseBean(); AbstractPropertyAccessor accessor = createAccessor(target); accessor.setPropertyValue("someProperty", "someValue"); @@ -1379,7 +1310,7 @@ public void setPropertyInProtectedBaseBean() { } @Test - public void setPropertyTypeMismatch() { + void setPropertyTypeMismatch() { TestBean target = new TestBean(); AbstractPropertyAccessor accessor = createAccessor(target); assertThatExceptionOfType(TypeMismatchException.class).isThrownBy(() -> @@ -1387,7 +1318,7 @@ public void setPropertyTypeMismatch() { } @Test - public void setEmptyValueForPrimitiveProperty() { + void setEmptyValueForPrimitiveProperty() { TestBean target = new TestBean(); AbstractPropertyAccessor accessor = createAccessor(target); assertThatExceptionOfType(TypeMismatchException.class).isThrownBy(() -> @@ -1395,7 +1326,7 @@ public void setEmptyValueForPrimitiveProperty() { } @Test - public void setUnknownNestedProperty() { + void setUnknownNestedProperty() { Person target = createPerson("John", "Paris", "FR"); AbstractPropertyAccessor accessor = createAccessor(target); @@ -1404,7 +1335,7 @@ public void setUnknownNestedProperty() { } @Test - public void setPropertyValuesIgnoresInvalidNestedOnRequest() { + void setPropertyValuesIgnoresInvalidNestedOnRequest() { ITestBean target = new TestBean(); MutablePropertyValues pvs = new MutablePropertyValues(); pvs.addPropertyValue(new PropertyValue("name", "rod")); @@ -1418,7 +1349,7 @@ public void setPropertyValuesIgnoresInvalidNestedOnRequest() { } @Test - public void getAndSetIndexedProperties() { + void getAndSetIndexedProperties() { IndexedTestBean target = new IndexedTestBean(); AbstractPropertyAccessor accessor = createAccessor(target); TestBean tb0 = target.getArray()[0]; @@ -1488,7 +1419,7 @@ public void getAndSetIndexedProperties() { } @Test - public void getAndSetIndexedPropertiesWithDirectAccess() { + void getAndSetIndexedPropertiesWithDirectAccess() { IndexedTestBean target = new IndexedTestBean(); AbstractPropertyAccessor accessor = createAccessor(target); TestBean tb0 = target.getArray()[0]; @@ -1527,7 +1458,7 @@ public void getAndSetIndexedPropertiesWithDirectAccess() { assertThat((target.getList().get(0))).isEqualTo(tb3); assertThat((target.getList().get(1))).isEqualTo(tb2); assertThat((target.getList().get(2))).isEqualTo(tb0); - assertThat((target.getList().get(3))).isEqualTo(null); + assertThat((target.getList().get(3))).isNull(); assertThat((target.getList().get(4))).isEqualTo(tb1); assertThat((target.getMap().get("key1"))).isEqualTo(tb1); assertThat((target.getMap().get("key2"))).isEqualTo(tb0); @@ -1538,7 +1469,7 @@ public void getAndSetIndexedPropertiesWithDirectAccess() { assertThat(accessor.getPropertyValue("list[0]")).isEqualTo(tb3); assertThat(accessor.getPropertyValue("list[1]")).isEqualTo(tb2); assertThat(accessor.getPropertyValue("list[2]")).isEqualTo(tb0); - assertThat(accessor.getPropertyValue("list[3]")).isEqualTo(null); + assertThat(accessor.getPropertyValue("list[3]")).isNull(); assertThat(accessor.getPropertyValue("list[4]")).isEqualTo(tb1); assertThat(accessor.getPropertyValue("map[\"key1\"]")).isEqualTo(tb1); assertThat(accessor.getPropertyValue("map['key2']")).isEqualTo(tb0); @@ -1547,7 +1478,7 @@ public void getAndSetIndexedPropertiesWithDirectAccess() { } @Test - public void propertyType() { + void propertyType() { Person target = createPerson("John", "Paris", "FR"); AbstractPropertyAccessor accessor = createAccessor(target); @@ -1555,7 +1486,7 @@ public void propertyType() { } @Test - public void propertyTypeUnknownProperty() { + void propertyTypeUnknownProperty() { Simple target = new Simple("John", 2); AbstractPropertyAccessor accessor = createAccessor(target); @@ -1563,7 +1494,7 @@ public void propertyTypeUnknownProperty() { } @Test - public void propertyTypeDescriptor() { + void propertyTypeDescriptor() { Person target = createPerson("John", "Paris", "FR"); AbstractPropertyAccessor accessor = createAccessor(target); @@ -1571,7 +1502,7 @@ public void propertyTypeDescriptor() { } @Test - public void propertyTypeDescriptorUnknownProperty() { + void propertyTypeDescriptorUnknownProperty() { Simple target = new Simple("John", 2); AbstractPropertyAccessor accessor = createAccessor(target); @@ -1579,10 +1510,10 @@ public void propertyTypeDescriptorUnknownProperty() { } @Test - public void propertyTypeIndexedProperty() { + void propertyTypeIndexedProperty() { IndexedTestBean target = new IndexedTestBean(); AbstractPropertyAccessor accessor = createAccessor(target); - assertThat(accessor.getPropertyType("map[key0]")).isEqualTo(null); + assertThat(accessor.getPropertyType("map[key0]")).isNull(); accessor = createAccessor(target); accessor.setPropertyValue("map[key0]", "my String"); @@ -1594,7 +1525,7 @@ public void propertyTypeIndexedProperty() { } @Test - public void cornerSpr10115() { + void cornerSpr10115() { Spr10115Bean target = new Spr10115Bean(); AbstractPropertyAccessor accessor = createAccessor(target); accessor.setPropertyValue("prop1", "val1"); @@ -1602,7 +1533,7 @@ public void cornerSpr10115() { } @Test - public void cornerSpr13837() { + void cornerSpr13837() { Spr13837Bean target = new Spr13837Bean(); AbstractPropertyAccessor accessor = createAccessor(target); accessor.setPropertyValue("something", 42); diff --git a/spring-beans/src/test/java/org/springframework/beans/AbstractPropertyValuesTests.java b/spring-beans/src/test/java/org/springframework/beans/AbstractPropertyValuesTests.java index 9f3bd08ec9f3..7569be3d881b 100644 --- a/spring-beans/src/test/java/org/springframework/beans/AbstractPropertyValuesTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/AbstractPropertyValuesTests.java @@ -43,13 +43,13 @@ protected void doTestTony(PropertyValues pvs) { m.put("forname", "Tony"); m.put("surname", "Blair"); m.put("age", "50"); - for (int i = 0; i < ps.length; i++) { - Object val = m.get(ps[i].getName()); + for (PropertyValue element : ps) { + Object val = m.get(element.getName()); assertThat(val != null).as("Can't have unexpected value").isTrue(); boolean condition = val instanceof String; assertThat(condition).as("Val i string").isTrue(); - assertThat(val.equals(ps[i].getValue())).as("val matches expected").isTrue(); - m.remove(ps[i].getName()); + assertThat(val.equals(element.getValue())).as("val matches expected").isTrue(); + m.remove(element.getName()); } assertThat(m.size() == 0).as("Map size is 0").isTrue(); } diff --git a/spring-beans/src/test/java/org/springframework/beans/BeanUtilsTests.java b/spring-beans/src/test/java/org/springframework/beans/BeanUtilsTests.java index ef1bbf4616bf..6be56ed7ecf7 100644 --- a/spring-beans/src/test/java/org/springframework/beans/BeanUtilsTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/BeanUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -19,7 +19,10 @@ import java.beans.Introspector; import java.beans.PropertyDescriptor; import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.lang.reflect.Proxy; import java.net.URI; import java.net.URL; import java.time.DayOfWeek; @@ -45,6 +48,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.SoftAssertions.assertSoftly; /** * Unit tests for {@link BeanUtils}. @@ -81,19 +85,43 @@ void instantiateClassWithOptionalNullableType() throws NoSuchMethodException { } @Test // gh-22531 - void instantiateClassWithOptionalPrimitiveType() throws NoSuchMethodException { - Constructor ctor = BeanWithPrimitiveTypes.class.getDeclaredConstructor(int.class, boolean.class, String.class); - BeanWithPrimitiveTypes bean = BeanUtils.instantiateClass(ctor, null, null, "foo"); - assertThat(bean.getCounter()).isEqualTo(0); - assertThat(bean.isFlag()).isEqualTo(false); - assertThat(bean.getValue()).isEqualTo("foo"); + void instantiateClassWithFewerArgsThanParameters() throws NoSuchMethodException { + Constructor constructor = getBeanWithPrimitiveTypesConstructor(); + + assertThatExceptionOfType(BeanInstantiationException.class).isThrownBy(() -> + BeanUtils.instantiateClass(constructor, null, null, "foo")); } @Test // gh-22531 void instantiateClassWithMoreArgsThanParameters() throws NoSuchMethodException { - Constructor ctor = BeanWithPrimitiveTypes.class.getDeclaredConstructor(int.class, boolean.class, String.class); + Constructor constructor = getBeanWithPrimitiveTypesConstructor(); + assertThatExceptionOfType(BeanInstantiationException.class).isThrownBy(() -> - BeanUtils.instantiateClass(ctor, null, null, "foo", null)); + BeanUtils.instantiateClass(constructor, null, null, null, null, null, null, null, null, "foo", null)); + } + + @Test // gh-22531, gh-27390 + void instantiateClassWithOptionalPrimitiveTypes() throws NoSuchMethodException { + Constructor constructor = getBeanWithPrimitiveTypesConstructor(); + + BeanWithPrimitiveTypes bean = BeanUtils.instantiateClass(constructor, null, null, null, null, null, null, null, null, "foo"); + + assertSoftly(softly -> { + softly.assertThat(bean.isFlag()).isFalse(); + softly.assertThat(bean.getByteCount()).isEqualTo((byte) 0); + softly.assertThat(bean.getShortCount()).isEqualTo((short) 0); + softly.assertThat(bean.getIntCount()).isEqualTo(0); + softly.assertThat(bean.getLongCount()).isEqualTo(0L); + softly.assertThat(bean.getFloatCount()).isEqualTo(0F); + softly.assertThat(bean.getDoubleCount()).isEqualTo(0D); + softly.assertThat(bean.getCharacter()).isEqualTo('\0'); + softly.assertThat(bean.getText()).isEqualTo("foo"); + }); + } + + private Constructor getBeanWithPrimitiveTypesConstructor() throws NoSuchMethodException { + return BeanWithPrimitiveTypes.class.getConstructor(boolean.class, byte.class, short.class, int.class, + long.class, float.class, double.class, char.class, String.class); } @Test @@ -174,17 +202,95 @@ void copyPropertiesWithDifferentTypes2() throws Exception { assertThat(tb2.getTouchy().equals(tb.getTouchy())).as("Touchy copied").isTrue(); } + /** + * {@code Integer} can be copied to {@code Number}. + */ @Test - void copyPropertiesHonorsGenericTypeMatches() { + void copyPropertiesFromSubTypeToSuperType() { + IntegerHolder integerHolder = new IntegerHolder(); + integerHolder.setNumber(42); + NumberHolder numberHolder = new NumberHolder(); + + BeanUtils.copyProperties(integerHolder, numberHolder); + assertThat(integerHolder.getNumber()).isEqualTo(42); + assertThat(numberHolder.getNumber()).isEqualTo(42); + } + + /** + * {@code List} can be copied to {@code List}. + */ + @Test + void copyPropertiesHonorsGenericTypeMatchesFromIntegerToInteger() { IntegerListHolder1 integerListHolder1 = new IntegerListHolder1(); integerListHolder1.getList().add(42); IntegerListHolder2 integerListHolder2 = new IntegerListHolder2(); BeanUtils.copyProperties(integerListHolder1, integerListHolder2); - assertThat(integerListHolder1.getList()).containsOnly(42); - assertThat(integerListHolder2.getList()).containsOnly(42); + assertThat(integerListHolder1.getList()).containsExactly(42); + assertThat(integerListHolder2.getList()).containsExactly(42); + } + + /** + * {@code List} can be copied to {@code List}. + */ + @Test + void copyPropertiesHonorsGenericTypeMatchesFromWildcardToWildcard() { + List list = List.of("foo", 42); + WildcardListHolder1 wildcardListHolder1 = new WildcardListHolder1(); + wildcardListHolder1.setList(list); + WildcardListHolder2 wildcardListHolder2 = new WildcardListHolder2(); + assertThat(wildcardListHolder2.getList()).isEmpty(); + + BeanUtils.copyProperties(wildcardListHolder1, wildcardListHolder2); + assertThat(wildcardListHolder1.getList()).isEqualTo(list); + assertThat(wildcardListHolder2.getList()).isEqualTo(list); + } + + /** + * {@code List} can be copied to {@code List}. + */ + @Test + void copyPropertiesHonorsGenericTypeMatchesFromIntegerToWildcard() { + IntegerListHolder1 integerListHolder1 = new IntegerListHolder1(); + integerListHolder1.getList().add(42); + WildcardListHolder2 wildcardListHolder2 = new WildcardListHolder2(); + + BeanUtils.copyProperties(integerListHolder1, wildcardListHolder2); + assertThat(integerListHolder1.getList()).containsExactly(42); + assertThat(wildcardListHolder2.getList()).isEqualTo(List.of(42)); } + /** + * {@code List} can be copied to {@code List}. + */ + @Test + void copyPropertiesHonorsGenericTypeMatchesForUpperBoundedWildcard() { + IntegerListHolder1 integerListHolder1 = new IntegerListHolder1(); + integerListHolder1.getList().add(42); + NumberUpperBoundedWildcardListHolder numberListHolder = new NumberUpperBoundedWildcardListHolder(); + + BeanUtils.copyProperties(integerListHolder1, numberListHolder); + assertThat(integerListHolder1.getList()).containsExactly(42); + assertThat(numberListHolder.getList()).isEqualTo(List.of(42)); + } + + /** + * {@code Number} can NOT be copied to {@code Integer}. + */ + @Test + void copyPropertiesDoesNotCopyFromSuperTypeToSubType() { + NumberHolder numberHolder = new NumberHolder(); + numberHolder.setNumber(42); + IntegerHolder integerHolder = new IntegerHolder(); + + BeanUtils.copyProperties(numberHolder, integerHolder); + assertThat(numberHolder.getNumber()).isEqualTo(42); + assertThat(integerHolder.getNumber()).isNull(); + } + + /** + * {@code List} can NOT be copied to {@code List}. + */ @Test void copyPropertiesDoesNotHonorGenericTypeMismatches() { IntegerListHolder1 integerListHolder = new IntegerListHolder1(); @@ -192,10 +298,47 @@ void copyPropertiesDoesNotHonorGenericTypeMismatches() { LongListHolder longListHolder = new LongListHolder(); BeanUtils.copyProperties(integerListHolder, longListHolder); - assertThat(integerListHolder.getList()).containsOnly(42); + assertThat(integerListHolder.getList()).containsExactly(42); assertThat(longListHolder.getList()).isEmpty(); } + /** + * {@code List} can NOT be copied to {@code List}. + */ + @Test + void copyPropertiesDoesNotHonorGenericTypeMismatchesFromSubTypeToSuperType() { + IntegerListHolder1 integerListHolder = new IntegerListHolder1(); + integerListHolder.getList().add(42); + NumberListHolder numberListHolder = new NumberListHolder(); + + BeanUtils.copyProperties(integerListHolder, numberListHolder); + assertThat(integerListHolder.getList()).containsExactly(42); + assertThat(numberListHolder.getList()).isEmpty(); + } + + @Test // gh-26531 + void copyPropertiesIgnoresGenericsIfSourceOrTargetHasUnresolvableGenerics() throws Exception { + Order original = new Order("test", List.of("foo", "bar")); + + // Create a Proxy that loses the generic type information for the getLineItems() method. + OrderSummary proxy = proxyOrder(original); + assertThat(OrderSummary.class.getDeclaredMethod("getLineItems").toGenericString()) + .contains("java.util.List"); + assertThat(proxy.getClass().getDeclaredMethod("getLineItems").toGenericString()) + .contains("java.util.List") + .doesNotContain(""); + + // Ensure that our custom Proxy works as expected. + assertThat(proxy.getId()).isEqualTo("test"); + assertThat(proxy.getLineItems()).containsExactly("foo", "bar"); + + // Copy from proxy to target. + Order target = new Order(); + BeanUtils.copyProperties(proxy, target); + assertThat(target.getId()).isEqualTo("test"); + assertThat(target.getLineItems()).containsExactly("foo", "bar"); + } + @Test void copyPropertiesWithEditable() throws Exception { TestBean tb = new TestBean(); @@ -361,6 +504,90 @@ private void assertSignatureEquals(Method desiredMethod, String signature) { } + @SuppressWarnings("unused") + private static class NumberHolder { + + private Number number; + + public Number getNumber() { + return number; + } + + public void setNumber(Number number) { + this.number = number; + } + } + + @SuppressWarnings("unused") + private static class IntegerHolder { + + private Integer number; + + public Integer getNumber() { + return number; + } + + public void setNumber(Integer number) { + this.number = number; + } + } + + @SuppressWarnings("unused") + private static class WildcardListHolder1 { + + private List list = new ArrayList<>(); + + public List getList() { + return list; + } + + public void setList(List list) { + this.list = list; + } + } + + @SuppressWarnings("unused") + private static class WildcardListHolder2 { + + private List list = new ArrayList<>(); + + public List getList() { + return list; + } + + public void setList(List list) { + this.list = list; + } + } + + @SuppressWarnings("unused") + private static class NumberUpperBoundedWildcardListHolder { + + private List list = new ArrayList<>(); + + public List getList() { + return list; + } + + public void setList(List list) { + this.list = list; + } + } + + @SuppressWarnings("unused") + private static class NumberListHolder { + + private List list = new ArrayList<>(); + + public List getList() { + return list; + } + + public void setList(List list) { + this.list = list; + } + } + @SuppressWarnings("unused") private static class IntegerListHolder1 { @@ -601,30 +828,68 @@ public String getValue() { private static class BeanWithPrimitiveTypes { - private int counter; - private boolean flag; + private byte byteCount; + private short shortCount; + private int intCount; + private long longCount; + private float floatCount; + private double doubleCount; + private char character; + private String text; - private String value; @SuppressWarnings("unused") - public BeanWithPrimitiveTypes(int counter, boolean flag, String value) { - this.counter = counter; - this.flag = flag; - this.value = value; - } + public BeanWithPrimitiveTypes(boolean flag, byte byteCount, short shortCount, int intCount, long longCount, + float floatCount, double doubleCount, char character, String text) { - public int getCounter() { - return counter; + this.flag = flag; + this.byteCount = byteCount; + this.shortCount = shortCount; + this.intCount = intCount; + this.longCount = longCount; + this.floatCount = floatCount; + this.doubleCount = doubleCount; + this.character = character; + this.text = text; } public boolean isFlag() { return flag; } - public String getValue() { - return value; + public byte getByteCount() { + return byteCount; + } + + public short getShortCount() { + return shortCount; + } + + public int getIntCount() { + return intCount; + } + + public long getLongCount() { + return longCount; + } + + public float getFloatCount() { + return floatCount; + } + + public double getDoubleCount() { + return doubleCount; + } + + public char getCharacter() { + return character; + } + + public String getText() { + return text; } + } private static class PrivateBeanWithPrivateConstructor { @@ -633,4 +898,77 @@ private PrivateBeanWithPrivateConstructor() { } } + @SuppressWarnings("unused") + private static class Order { + + private String id; + private List lineItems; + + + Order() { + } + + Order(String id, List lineItems) { + this.id = id; + this.lineItems = lineItems; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public List getLineItems() { + return this.lineItems; + } + + public void setLineItems(List lineItems) { + this.lineItems = lineItems; + } + + @Override + public String toString() { + return "Order [id=" + this.id + ", lineItems=" + this.lineItems + "]"; + } + } + + private interface OrderSummary { + + String getId(); + + List getLineItems(); + } + + + private OrderSummary proxyOrder(Order order) { + return (OrderSummary) Proxy.newProxyInstance(getClass().getClassLoader(), + new Class[] { OrderSummary.class }, new OrderInvocationHandler(order)); + } + + + private static class OrderInvocationHandler implements InvocationHandler { + + private final Order order; + + + OrderInvocationHandler(Order order) { + this.order = order; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + try { + // Ignore args since OrderSummary doesn't declare any methods with arguments, + // and we're not supporting equals(Object), etc. + return Order.class.getDeclaredMethod(method.getName()).invoke(this.order); + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + } + } + } diff --git a/spring-beans/src/test/java/org/springframework/beans/BeanWrapperAutoGrowingTests.java b/spring-beans/src/test/java/org/springframework/beans/BeanWrapperAutoGrowingTests.java index c17e2c7359b0..78524e632b0c 100644 --- a/spring-beans/src/test/java/org/springframework/beans/BeanWrapperAutoGrowingTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/BeanWrapperAutoGrowingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -28,6 +28,7 @@ /** * @author Keith Donald * @author Juergen Hoeller + * @author Sam Brannen */ public class BeanWrapperAutoGrowingTests { @@ -37,7 +38,7 @@ public class BeanWrapperAutoGrowingTests { @BeforeEach - public void setUp() { + public void setup() { wrapper.setAutoGrowNestedPaths(true); } @@ -66,11 +67,6 @@ public void getPropertyValueAutoGrowArray() { assertThat(bean.getArray()[0]).isInstanceOf(Bean.class); } - private void assertNotNull(Object propertyValue) { - assertThat(propertyValue).isNotNull(); - } - - @Test public void setPropertyValueAutoGrowArray() { wrapper.setPropertyValue("array[0].prop", "test"); @@ -93,12 +89,39 @@ public void getPropertyValueAutoGrowArrayBySeveralElements() { } @Test - public void getPropertyValueAutoGrowMultiDimensionalArray() { + public void getPropertyValueAutoGrow2dArray() { assertNotNull(wrapper.getPropertyValue("multiArray[0][0]")); assertThat(bean.getMultiArray()[0].length).isEqualTo(1); assertThat(bean.getMultiArray()[0][0]).isInstanceOf(Bean.class); } + @Test + public void getPropertyValueAutoGrow3dArray() { + assertNotNull(wrapper.getPropertyValue("threeDimensionalArray[1][2][3]")); + assertThat(bean.getThreeDimensionalArray()[1].length).isEqualTo(3); + assertThat(bean.getThreeDimensionalArray()[1][2][3]).isInstanceOf(Bean.class); + } + + @Test + public void setPropertyValueAutoGrow2dArray() { + Bean newBean = new Bean(); + newBean.setProp("enigma"); + wrapper.setPropertyValue("multiArray[2][3]", newBean); + assertThat(bean.getMultiArray()[2][3]) + .isInstanceOf(Bean.class) + .extracting(Bean::getProp).isEqualTo("enigma"); + } + + @Test + public void setPropertyValueAutoGrow3dArray() { + Bean newBean = new Bean(); + newBean.setProp("enigma"); + wrapper.setPropertyValue("threeDimensionalArray[2][3][4]", newBean); + assertThat(bean.getThreeDimensionalArray()[2][3][4]) + .isInstanceOf(Bean.class) + .extracting(Bean::getProp).isEqualTo("enigma"); + } + @Test public void getPropertyValueAutoGrowList() { assertNotNull(wrapper.getPropertyValue("list[0]")); @@ -131,7 +154,7 @@ public void getPropertyValueAutoGrowListBySeveralElements() { public void getPropertyValueAutoGrowListFailsAgainstLimit() { wrapper.setAutoGrowCollectionLimit(2); assertThatExceptionOfType(InvalidPropertyException.class).isThrownBy(() -> - assertNotNull(wrapper.getPropertyValue("list[4]"))) + wrapper.getPropertyValue("list[4]")) .withRootCauseInstanceOf(IndexOutOfBoundsException.class); } @@ -161,6 +184,11 @@ public void setNestedPropertyValueAutoGrowMap() { } + private static void assertNotNull(Object propertyValue) { + assertThat(propertyValue).isNotNull(); + } + + @SuppressWarnings("rawtypes") public static class Bean { @@ -174,6 +202,8 @@ public static class Bean { private Bean[][] multiArray; + private Bean[][][] threeDimensionalArray; + private List list; private List> multiList; @@ -214,6 +244,14 @@ public void setMultiArray(Bean[][] multiArray) { this.multiArray = multiArray; } + public Bean[][][] getThreeDimensionalArray() { + return threeDimensionalArray; + } + + public void setThreeDimensionalArray(Bean[][][] threeDimensionalArray) { + this.threeDimensionalArray = threeDimensionalArray; + } + public List getList() { return list; } diff --git a/spring-beans/src/test/java/org/springframework/beans/BeanWrapperEnumTests.java b/spring-beans/src/test/java/org/springframework/beans/BeanWrapperEnumTests.java index 3a5ac3717127..95bd30cfb846 100644 --- a/spring-beans/src/test/java/org/springframework/beans/BeanWrapperEnumTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/BeanWrapperEnumTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -46,7 +46,7 @@ public void testCustomEnumWithNull() { GenericBean gb = new GenericBean<>(); BeanWrapper bw = new BeanWrapperImpl(gb); bw.setPropertyValue("customEnum", null); - assertThat(gb.getCustomEnum()).isEqualTo(null); + assertThat(gb.getCustomEnum()).isNull(); } @Test @@ -54,7 +54,7 @@ public void testCustomEnumWithEmptyString() { GenericBean gb = new GenericBean<>(); BeanWrapper bw = new BeanWrapperImpl(gb); bw.setPropertyValue("customEnum", ""); - assertThat(gb.getCustomEnum()).isEqualTo(null); + assertThat(gb.getCustomEnum()).isNull(); } @Test diff --git a/spring-beans/src/test/java/org/springframework/beans/BeanWrapperGenericsTests.java b/spring-beans/src/test/java/org/springframework/beans/BeanWrapperGenericsTests.java index be0c2cc5953a..16bc4de0e2a0 100644 --- a/spring-beans/src/test/java/org/springframework/beans/BeanWrapperGenericsTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/BeanWrapperGenericsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -16,7 +16,6 @@ package org.springframework.beans; -import java.net.MalformedURLException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -46,10 +45,10 @@ * @author Chris Beams * @since 18.01.2006 */ -public class BeanWrapperGenericsTests { +class BeanWrapperGenericsTests { @Test - public void testGenericSet() { + void testGenericSet() { GenericBean gb = new GenericBean<>(); BeanWrapper bw = new BeanWrapperImpl(gb); Set input = new HashSet<>(); @@ -61,7 +60,7 @@ public void testGenericSet() { } @Test - public void testGenericLowerBoundedSet() { + void testGenericLowerBoundedSet() { GenericBean gb = new GenericBean<>(); BeanWrapper bw = new BeanWrapperImpl(gb); bw.registerCustomEditor(Number.class, new CustomNumberEditor(Integer.class, true)); @@ -74,7 +73,7 @@ public void testGenericLowerBoundedSet() { } @Test - public void testGenericSetWithConversionFailure() { + void testGenericSetWithConversionFailure() { GenericBean gb = new GenericBean<>(); BeanWrapper bw = new BeanWrapperImpl(gb); Set input = new HashSet<>(); @@ -85,7 +84,7 @@ public void testGenericSetWithConversionFailure() { } @Test - public void testGenericList() throws MalformedURLException { + void testGenericList() throws Exception { GenericBean gb = new GenericBean<>(); BeanWrapper bw = new BeanWrapperImpl(gb); List input = new ArrayList<>(); @@ -97,7 +96,7 @@ public void testGenericList() throws MalformedURLException { } @Test - public void testGenericListElement() throws MalformedURLException { + void testGenericListElement() throws Exception { GenericBean gb = new GenericBean<>(); gb.setResourceList(new ArrayList<>()); BeanWrapper bw = new BeanWrapperImpl(gb); @@ -106,29 +105,29 @@ public void testGenericListElement() throws MalformedURLException { } @Test - public void testGenericMap() { + void testGenericMap() { GenericBean gb = new GenericBean<>(); BeanWrapper bw = new BeanWrapperImpl(gb); Map input = new HashMap<>(); input.put("4", "5"); input.put("6", "7"); bw.setPropertyValue("shortMap", input); - assertThat(gb.getShortMap().get(new Short("4"))).isEqualTo(5); - assertThat(gb.getShortMap().get(new Short("6"))).isEqualTo(7); + assertThat(gb.getShortMap().get(Short.valueOf("4"))).isEqualTo(5); + assertThat(gb.getShortMap().get(Short.valueOf("6"))).isEqualTo(7); } @Test - public void testGenericMapElement() { + void testGenericMapElement() { GenericBean gb = new GenericBean<>(); gb.setShortMap(new HashMap<>()); BeanWrapper bw = new BeanWrapperImpl(gb); bw.setPropertyValue("shortMap[4]", "5"); assertThat(bw.getPropertyValue("shortMap[4]")).isEqualTo(5); - assertThat(gb.getShortMap().get(new Short("4"))).isEqualTo(5); + assertThat(gb.getShortMap().get(Short.valueOf("4"))).isEqualTo(5); } @Test - public void testGenericMapWithKeyType() { + void testGenericMapWithKeyType() { GenericBean gb = new GenericBean<>(); BeanWrapper bw = new BeanWrapperImpl(gb); Map input = new HashMap<>(); @@ -140,17 +139,17 @@ public void testGenericMapWithKeyType() { } @Test - public void testGenericMapElementWithKeyType() { + void testGenericMapElementWithKeyType() { GenericBean gb = new GenericBean<>(); gb.setLongMap(new HashMap()); BeanWrapper bw = new BeanWrapperImpl(gb); bw.setPropertyValue("longMap[4]", "5"); - assertThat(gb.getLongMap().get(new Long("4"))).isEqualTo("5"); + assertThat(gb.getLongMap().get(Long.valueOf("4"))).isEqualTo("5"); assertThat(bw.getPropertyValue("longMap[4]")).isEqualTo("5"); } @Test - public void testGenericMapWithCollectionValue() { + void testGenericMapWithCollectionValue() { GenericBean gb = new GenericBean<>(); BeanWrapper bw = new BeanWrapperImpl(gb); bw.registerCustomEditor(Number.class, new CustomNumberEditor(Integer.class, false)); @@ -162,14 +161,12 @@ public void testGenericMapWithCollectionValue() { value2.add(Boolean.TRUE); input.put("2", value2); bw.setPropertyValue("collectionMap", input); - boolean condition1 = gb.getCollectionMap().get(1) instanceof HashSet; - assertThat(condition1).isTrue(); - boolean condition = gb.getCollectionMap().get(2) instanceof ArrayList; - assertThat(condition).isTrue(); + assertThat(gb.getCollectionMap().get(1) instanceof HashSet).isTrue(); + assertThat(gb.getCollectionMap().get(2) instanceof ArrayList).isTrue(); } @Test - public void testGenericMapElementWithCollectionValue() { + void testGenericMapElementWithCollectionValue() { GenericBean gb = new GenericBean<>(); gb.setCollectionMap(new HashMap<>()); BeanWrapper bw = new BeanWrapperImpl(gb); @@ -177,24 +174,23 @@ public void testGenericMapElementWithCollectionValue() { HashSet value1 = new HashSet<>(); value1.add(1); bw.setPropertyValue("collectionMap[1]", value1); - boolean condition = gb.getCollectionMap().get(1) instanceof HashSet; - assertThat(condition).isTrue(); + assertThat(gb.getCollectionMap().get(1) instanceof HashSet).isTrue(); } @Test - public void testGenericMapFromProperties() { + void testGenericMapFromProperties() { GenericBean gb = new GenericBean<>(); BeanWrapper bw = new BeanWrapperImpl(gb); Properties input = new Properties(); input.setProperty("4", "5"); input.setProperty("6", "7"); bw.setPropertyValue("shortMap", input); - assertThat(gb.getShortMap().get(new Short("4"))).isEqualTo(5); - assertThat(gb.getShortMap().get(new Short("6"))).isEqualTo(7); + assertThat(gb.getShortMap().get(Short.valueOf("4"))).isEqualTo(5); + assertThat(gb.getShortMap().get(Short.valueOf("6"))).isEqualTo(7); } @Test - public void testGenericListOfLists() throws MalformedURLException { + void testGenericListOfLists() { GenericBean gb = new GenericBean<>(); List> list = new ArrayList<>(); list.add(new ArrayList<>()); @@ -206,7 +202,7 @@ public void testGenericListOfLists() throws MalformedURLException { } @Test - public void testGenericListOfListsWithElementConversion() throws MalformedURLException { + void testGenericListOfListsWithElementConversion() { GenericBean gb = new GenericBean<>(); List> list = new ArrayList<>(); list.add(new ArrayList<>()); @@ -218,7 +214,7 @@ public void testGenericListOfListsWithElementConversion() throws MalformedURLExc } @Test - public void testGenericListOfArrays() throws MalformedURLException { + void testGenericListOfArrays() { GenericBean gb = new GenericBean<>(); ArrayList list = new ArrayList<>(); list.add(new String[] {"str1", "str2"}); @@ -230,7 +226,7 @@ public void testGenericListOfArrays() throws MalformedURLException { } @Test - public void testGenericListOfArraysWithElementConversion() throws MalformedURLException { + void testGenericListOfArraysWithElementConversion() { GenericBean gb = new GenericBean<>(); ArrayList list = new ArrayList<>(); list.add(new String[] {"str1", "str2"}); @@ -243,55 +239,55 @@ public void testGenericListOfArraysWithElementConversion() throws MalformedURLEx } @Test - public void testGenericListOfMaps() throws MalformedURLException { + void testGenericListOfMaps() { GenericBean gb = new GenericBean<>(); List> list = new ArrayList<>(); list.add(new HashMap<>()); gb.setListOfMaps(list); BeanWrapper bw = new BeanWrapperImpl(gb); - bw.setPropertyValue("listOfMaps[0][10]", new Long(5)); - assertThat(bw.getPropertyValue("listOfMaps[0][10]")).isEqualTo(new Long(5)); - assertThat(gb.getListOfMaps().get(0).get(10)).isEqualTo(new Long(5)); + bw.setPropertyValue("listOfMaps[0][10]", 5L); + assertThat(bw.getPropertyValue("listOfMaps[0][10]")).isEqualTo(5L); + assertThat(gb.getListOfMaps().get(0).get(10)).isEqualTo(Long.valueOf(5)); } @Test - public void testGenericListOfMapsWithElementConversion() throws MalformedURLException { + void testGenericListOfMapsWithElementConversion() { GenericBean gb = new GenericBean<>(); List> list = new ArrayList<>(); list.add(new HashMap<>()); gb.setListOfMaps(list); BeanWrapper bw = new BeanWrapperImpl(gb); bw.setPropertyValue("listOfMaps[0][10]", "5"); - assertThat(bw.getPropertyValue("listOfMaps[0][10]")).isEqualTo(new Long(5)); - assertThat(gb.getListOfMaps().get(0).get(10)).isEqualTo(new Long(5)); + assertThat(bw.getPropertyValue("listOfMaps[0][10]")).isEqualTo(5L); + assertThat(gb.getListOfMaps().get(0).get(10)).isEqualTo(Long.valueOf(5)); } @Test - public void testGenericMapOfMaps() throws MalformedURLException { + void testGenericMapOfMaps() { GenericBean gb = new GenericBean<>(); Map> map = new HashMap<>(); map.put("mykey", new HashMap<>()); gb.setMapOfMaps(map); BeanWrapper bw = new BeanWrapperImpl(gb); - bw.setPropertyValue("mapOfMaps[mykey][10]", new Long(5)); - assertThat(bw.getPropertyValue("mapOfMaps[mykey][10]")).isEqualTo(new Long(5)); - assertThat(gb.getMapOfMaps().get("mykey").get(10)).isEqualTo(new Long(5)); + bw.setPropertyValue("mapOfMaps[mykey][10]", 5L); + assertThat(bw.getPropertyValue("mapOfMaps[mykey][10]")).isEqualTo(5L); + assertThat(gb.getMapOfMaps().get("mykey").get(10)).isEqualTo(Long.valueOf(5)); } @Test - public void testGenericMapOfMapsWithElementConversion() throws MalformedURLException { + void testGenericMapOfMapsWithElementConversion() { GenericBean gb = new GenericBean<>(); Map> map = new HashMap<>(); map.put("mykey", new HashMap<>()); gb.setMapOfMaps(map); BeanWrapper bw = new BeanWrapperImpl(gb); bw.setPropertyValue("mapOfMaps[mykey][10]", "5"); - assertThat(bw.getPropertyValue("mapOfMaps[mykey][10]")).isEqualTo(new Long(5)); - assertThat(gb.getMapOfMaps().get("mykey").get(10)).isEqualTo(new Long(5)); + assertThat(bw.getPropertyValue("mapOfMaps[mykey][10]")).isEqualTo(5L); + assertThat(gb.getMapOfMaps().get("mykey").get(10)).isEqualTo(Long.valueOf(5)); } @Test - public void testGenericMapOfLists() throws MalformedURLException { + void testGenericMapOfLists() { GenericBean gb = new GenericBean<>(); Map> map = new HashMap<>(); map.put(1, new ArrayList<>()); @@ -303,7 +299,7 @@ public void testGenericMapOfLists() throws MalformedURLException { } @Test - public void testGenericMapOfListsWithElementConversion() throws MalformedURLException { + void testGenericMapOfListsWithElementConversion() { GenericBean gb = new GenericBean<>(); Map> map = new HashMap<>(); map.put(1, new ArrayList<>()); @@ -315,7 +311,7 @@ public void testGenericMapOfListsWithElementConversion() throws MalformedURLExce } @Test - public void testGenericTypeNestingMapOfInteger() throws Exception { + void testGenericTypeNestingMapOfInteger() { Map map = new HashMap<>(); map.put("testKey", "100"); @@ -324,14 +320,13 @@ public void testGenericTypeNestingMapOfInteger() throws Exception { bw.setPropertyValue("mapOfInteger", map); Object obj = gb.getMapOfInteger().get("testKey"); - boolean condition = obj instanceof Integer; - assertThat(condition).isTrue(); + assertThat(obj instanceof Integer).isTrue(); } @Test - public void testGenericTypeNestingMapOfListOfInteger() throws Exception { + void testGenericTypeNestingMapOfListOfInteger() { Map> map = new HashMap<>(); - List list = Arrays.asList(new String[] {"1", "2", "3"}); + List list = Arrays.asList("1", "2", "3"); map.put("testKey", list); NestedGenericCollectionBean gb = new NestedGenericCollectionBean(); @@ -339,13 +334,12 @@ public void testGenericTypeNestingMapOfListOfInteger() throws Exception { bw.setPropertyValue("mapOfListOfInteger", map); Object obj = gb.getMapOfListOfInteger().get("testKey").get(0); - boolean condition = obj instanceof Integer; - assertThat(condition).isTrue(); + assertThat(obj instanceof Integer).isTrue(); assertThat(((Integer) obj).intValue()).isEqualTo(1); } @Test - public void testGenericTypeNestingListOfMapOfInteger() throws Exception { + void testGenericTypeNestingListOfMapOfInteger() { List> list = new ArrayList<>(); Map map = new HashMap<>(); map.put("testKey", "5"); @@ -356,15 +350,14 @@ public void testGenericTypeNestingListOfMapOfInteger() throws Exception { bw.setPropertyValue("listOfMapOfInteger", list); Object obj = gb.getListOfMapOfInteger().get(0).get("testKey"); - boolean condition = obj instanceof Integer; - assertThat(condition).isTrue(); + assertThat(obj instanceof Integer).isTrue(); assertThat(((Integer) obj).intValue()).isEqualTo(5); } @Test - public void testGenericTypeNestingMapOfListOfListOfInteger() throws Exception { + void testGenericTypeNestingMapOfListOfListOfInteger() { Map>> map = new HashMap<>(); - List list = Arrays.asList(new String[] {"1", "2", "3"}); + List list = Arrays.asList("1", "2", "3"); map.put("testKey", Collections.singletonList(list)); NestedGenericCollectionBean gb = new NestedGenericCollectionBean(); @@ -372,13 +365,12 @@ public void testGenericTypeNestingMapOfListOfListOfInteger() throws Exception { bw.setPropertyValue("mapOfListOfListOfInteger", map); Object obj = gb.getMapOfListOfListOfInteger().get("testKey").get(0).get(0); - boolean condition = obj instanceof Integer; - assertThat(condition).isTrue(); + assertThat(obj instanceof Integer).isTrue(); assertThat(((Integer) obj).intValue()).isEqualTo(1); } @Test - public void testComplexGenericMap() { + void testComplexGenericMap() { Map, List> inputMap = new HashMap<>(); List inputKey = new ArrayList<>(); inputKey.add("1"); @@ -391,11 +383,11 @@ public void testComplexGenericMap() { bw.setPropertyValue("genericMap", inputMap); assertThat(holder.getGenericMap().keySet().iterator().next().get(0)).isEqualTo(1); - assertThat(holder.getGenericMap().values().iterator().next().get(0)).isEqualTo(new Long(10)); + assertThat(holder.getGenericMap().values().iterator().next().get(0)).isEqualTo(Long.valueOf(10)); } @Test - public void testComplexGenericMapWithCollectionConversion() { + void testComplexGenericMapWithCollectionConversion() { Map, Set> inputMap = new HashMap<>(); Set inputKey = new HashSet<>(); inputKey.add("1"); @@ -408,11 +400,11 @@ public void testComplexGenericMapWithCollectionConversion() { bw.setPropertyValue("genericMap", inputMap); assertThat(holder.getGenericMap().keySet().iterator().next().get(0)).isEqualTo(1); - assertThat(holder.getGenericMap().values().iterator().next().get(0)).isEqualTo(new Long(10)); + assertThat(holder.getGenericMap().values().iterator().next().get(0)).isEqualTo(Long.valueOf(10)); } @Test - public void testComplexGenericIndexedMapEntry() { + void testComplexGenericIndexedMapEntry() { List inputValue = new ArrayList<>(); inputValue.add("10"); @@ -421,11 +413,11 @@ public void testComplexGenericIndexedMapEntry() { bw.setPropertyValue("genericIndexedMap[1]", inputValue); assertThat(holder.getGenericIndexedMap().keySet().iterator().next()).isEqualTo(1); - assertThat(holder.getGenericIndexedMap().values().iterator().next().get(0)).isEqualTo(new Long(10)); + assertThat(holder.getGenericIndexedMap().values().iterator().next().get(0)).isEqualTo(Long.valueOf(10)); } @Test - public void testComplexGenericIndexedMapEntryWithCollectionConversion() { + void testComplexGenericIndexedMapEntryWithCollectionConversion() { Set inputValue = new HashSet<>(); inputValue.add("10"); @@ -434,11 +426,11 @@ public void testComplexGenericIndexedMapEntryWithCollectionConversion() { bw.setPropertyValue("genericIndexedMap[1]", inputValue); assertThat(holder.getGenericIndexedMap().keySet().iterator().next()).isEqualTo(1); - assertThat(holder.getGenericIndexedMap().values().iterator().next().get(0)).isEqualTo(new Long(10)); + assertThat(holder.getGenericIndexedMap().values().iterator().next().get(0)).isEqualTo(Long.valueOf(10)); } @Test - public void testComplexDerivedIndexedMapEntry() { + void testComplexDerivedIndexedMapEntry() { List inputValue = new ArrayList<>(); inputValue.add("10"); @@ -447,11 +439,11 @@ public void testComplexDerivedIndexedMapEntry() { bw.setPropertyValue("derivedIndexedMap[1]", inputValue); assertThat(holder.getDerivedIndexedMap().keySet().iterator().next()).isEqualTo(1); - assertThat(holder.getDerivedIndexedMap().values().iterator().next().get(0)).isEqualTo(new Long(10)); + assertThat(holder.getDerivedIndexedMap().values().iterator().next().get(0)).isEqualTo(Long.valueOf(10)); } @Test - public void testComplexDerivedIndexedMapEntryWithCollectionConversion() { + void testComplexDerivedIndexedMapEntryWithCollectionConversion() { Set inputValue = new HashSet<>(); inputValue.add("10"); @@ -460,11 +452,11 @@ public void testComplexDerivedIndexedMapEntryWithCollectionConversion() { bw.setPropertyValue("derivedIndexedMap[1]", inputValue); assertThat(holder.getDerivedIndexedMap().keySet().iterator().next()).isEqualTo(1); - assertThat(holder.getDerivedIndexedMap().values().iterator().next().get(0)).isEqualTo(new Long(10)); + assertThat(holder.getDerivedIndexedMap().values().iterator().next().get(0)).isEqualTo(Long.valueOf(10)); } @Test - public void testGenericallyTypedIntegerBean() throws Exception { + void testGenericallyTypedIntegerBean() { GenericIntegerBean gb = new GenericIntegerBean(); BeanWrapper bw = new BeanWrapperImpl(gb); bw.setPropertyValue("genericProperty", "10"); @@ -475,7 +467,7 @@ public void testGenericallyTypedIntegerBean() throws Exception { } @Test - public void testGenericallyTypedSetOfIntegerBean() throws Exception { + void testGenericallyTypedSetOfIntegerBean() { GenericSetOfIntegerBean gb = new GenericSetOfIntegerBean(); BeanWrapper bw = new BeanWrapperImpl(gb); bw.setPropertyValue("genericProperty", "10"); @@ -486,23 +478,23 @@ public void testGenericallyTypedSetOfIntegerBean() throws Exception { } @Test - public void testSettingGenericPropertyWithReadOnlyInterface() { + void testSettingGenericPropertyWithReadOnlyInterface() { Bar bar = new Bar(); BeanWrapper bw = new BeanWrapperImpl(bar); bw.setPropertyValue("version", "10"); - assertThat(bar.getVersion()).isEqualTo(new Double(10.0)); + assertThat(bar.getVersion()).isEqualTo(Double.valueOf(10.0)); } @Test - public void testSettingLongPropertyWithGenericInterface() { + void testSettingLongPropertyWithGenericInterface() { Promotion bean = new Promotion(); BeanWrapper bw = new BeanWrapperImpl(bean); bw.setPropertyValue("id", "10"); - assertThat(bean.getId()).isEqualTo(new Long(10)); + assertThat(bean.getId()).isEqualTo(Long.valueOf(10)); } @Test - public void testUntypedPropertyWithMapAtRuntime() { + void testUntypedPropertyWithMapAtRuntime() { class Holder { private final D data; public Holder(D data) { diff --git a/spring-beans/src/test/java/org/springframework/beans/BeanWrapperTests.java b/spring-beans/src/test/java/org/springframework/beans/BeanWrapperTests.java index 0711189b5f59..17a775a7f123 100644 --- a/spring-beans/src/test/java/org/springframework/beans/BeanWrapperTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/BeanWrapperTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -23,9 +23,13 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.OverridingClassLoader; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.UrlResource; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatNoException; /** * Specific {@link BeanWrapperImpl} tests. @@ -37,7 +41,7 @@ * @author Chris Beams * @author Dave Syer */ -public class BeanWrapperTests extends AbstractPropertyAccessorTests { +class BeanWrapperTests extends AbstractPropertyAccessorTests { @Override protected BeanWrapperImpl createAccessor(Object target) { @@ -46,7 +50,7 @@ protected BeanWrapperImpl createAccessor(Object target) { @Test - public void setterDoesNotCallGetter() { + void setterDoesNotCallGetter() { GetterBean target = new GetterBean(); BeanWrapper accessor = createAccessor(target); accessor.setPropertyValue("name", "tom"); @@ -55,7 +59,7 @@ public void setterDoesNotCallGetter() { } @Test - public void getterSilentlyFailWithOldValueExtraction() { + void getterSilentlyFailWithOldValueExtraction() { GetterBean target = new GetterBean(); BeanWrapper accessor = createAccessor(target); accessor.setExtractOldValueForEditor(true); // This will call the getter @@ -65,7 +69,7 @@ public void getterSilentlyFailWithOldValueExtraction() { } @Test - public void aliasedSetterThroughDefaultMethod() { + void aliasedSetterThroughDefaultMethod() { GetterBean target = new GetterBean(); BeanWrapper accessor = createAccessor(target); accessor.setPropertyValue("aliasedName", "tom"); @@ -74,7 +78,7 @@ public void aliasedSetterThroughDefaultMethod() { } @Test - public void setValidAndInvalidPropertyValuesShouldContainExceptionDetails() { + void setValidAndInvalidPropertyValuesShouldContainExceptionDetails() { TestBean target = new TestBean(); String newName = "tony"; String invalidTouchy = ".valid"; @@ -91,12 +95,12 @@ public void setValidAndInvalidPropertyValuesShouldContainExceptionDetails() { .getNewValue()).isEqualTo(invalidTouchy); }); // Test validly set property matches - assertThat(target.getName().equals(newName)).as("Valid set property must stick").isTrue(); - assertThat(target.getAge() == 0).as("Invalid set property must retain old value").isTrue(); + assertThat(target.getName()).as("Valid set property must stick").isEqualTo(newName); + assertThat(target.getAge()).as("Invalid set property must retain old value").isEqualTo(0); } @Test - public void checkNotWritablePropertyHoldPossibleMatches() { + void checkNotWritablePropertyHoldPossibleMatches() { TestBean target = new TestBean(); BeanWrapper accessor = createAccessor(target); assertThatExceptionOfType(NotWritablePropertyException.class).isThrownBy(() -> @@ -104,16 +108,16 @@ public void checkNotWritablePropertyHoldPossibleMatches() { .satisfies(ex -> assertThat(ex.getPossibleMatches()).containsExactly("age")); } - @Test // Can't be shared; there is no such thing as a read-only field - public void setReadOnlyMapProperty() { + @Test // Can't be shared; there is no such thing as a read-only field + void setReadOnlyMapProperty() { TypedReadOnlyMap map = new TypedReadOnlyMap(Collections.singletonMap("key", new TestBean())); TypedReadOnlyMapClient target = new TypedReadOnlyMapClient(); BeanWrapper accessor = createAccessor(target); - accessor.setPropertyValue("map", map); + assertThatNoException().isThrownBy(() -> accessor.setPropertyValue("map", map)); } @Test - public void notWritablePropertyExceptionContainsAlternativeMatch() { + void notWritablePropertyExceptionContainsAlternativeMatch() { IntelliBean target = new IntelliBean(); BeanWrapper bw = createAccessor(target); try { @@ -121,12 +125,12 @@ public void notWritablePropertyExceptionContainsAlternativeMatch() { } catch (NotWritablePropertyException ex) { assertThat(ex.getPossibleMatches()).as("Possible matches not determined").isNotNull(); - assertThat(ex.getPossibleMatches().length).as("Invalid amount of alternatives").isEqualTo(1); + assertThat(ex.getPossibleMatches()).as("Invalid amount of alternatives").hasSize(1); } } @Test - public void notWritablePropertyExceptionContainsAlternativeMatches() { + void notWritablePropertyExceptionContainsAlternativeMatches() { IntelliBean target = new IntelliBean(); BeanWrapper bw = createAccessor(target); try { @@ -134,39 +138,93 @@ public void notWritablePropertyExceptionContainsAlternativeMatches() { } catch (NotWritablePropertyException ex) { assertThat(ex.getPossibleMatches()).as("Possible matches not determined").isNotNull(); - assertThat(ex.getPossibleMatches().length).as("Invalid amount of alternatives").isEqualTo(3); + assertThat(ex.getPossibleMatches()).as("Invalid amount of alternatives").hasSize(3); } } @Override @Test // Can't be shared: no type mismatch with a field - public void setPropertyTypeMismatch() { + void setPropertyTypeMismatch() { PropertyTypeMismatch target = new PropertyTypeMismatch(); BeanWrapper accessor = createAccessor(target); accessor.setPropertyValue("object", "a String"); assertThat(target.value).isEqualTo("a String"); - assertThat(target.getObject() == 8).isTrue(); + assertThat(target.getObject()).isEqualTo(8); assertThat(accessor.getPropertyValue("object")).isEqualTo(8); } @Test - public void propertyDescriptors() { + void setterOverload() { + SetterOverload target = new SetterOverload(); + BeanWrapper accessor = createAccessor(target); + accessor.setPropertyValue("object", "a String"); + assertThat(target.value).isEqualTo("a String"); + assertThat(target.getObject()).isEqualTo("a String"); + assertThat(accessor.getPropertyValue("object")).isEqualTo("a String"); + } + + @Test + void propertyDescriptors() throws Exception { TestBean target = new TestBean(); target.setSpouse(new TestBean()); BeanWrapper accessor = createAccessor(target); accessor.setPropertyValue("name", "a"); accessor.setPropertyValue("spouse.name", "b"); + assertThat(target.getName()).isEqualTo("a"); assertThat(target.getSpouse().getName()).isEqualTo("b"); assertThat(accessor.getPropertyValue("name")).isEqualTo("a"); assertThat(accessor.getPropertyValue("spouse.name")).isEqualTo("b"); assertThat(accessor.getPropertyDescriptor("name").getPropertyType()).isEqualTo(String.class); assertThat(accessor.getPropertyDescriptor("spouse.name").getPropertyType()).isEqualTo(String.class); + + assertThat(accessor.isReadableProperty("class.package")).isFalse(); + assertThat(accessor.isReadableProperty("class.module")).isFalse(); + assertThat(accessor.isReadableProperty("class.classLoader")).isFalse(); + assertThat(accessor.isReadableProperty("class.name")).isTrue(); + assertThat(accessor.isReadableProperty("class.simpleName")).isTrue(); + assertThat(accessor.getPropertyValue("class.name")).isEqualTo(TestBean.class.getName()); + assertThat(accessor.getPropertyValue("class.simpleName")).isEqualTo(TestBean.class.getSimpleName()); + assertThat(accessor.getPropertyDescriptor("class.name").getPropertyType()).isEqualTo(String.class); + assertThat(accessor.getPropertyDescriptor("class.simpleName").getPropertyType()).isEqualTo(String.class); + + accessor = createAccessor(new DefaultResourceLoader()); + + assertThat(accessor.isReadableProperty("class.package")).isFalse(); + assertThat(accessor.isReadableProperty("class.module")).isFalse(); + assertThat(accessor.isReadableProperty("class.classLoader")).isFalse(); + assertThat(accessor.isReadableProperty("class.name")).isTrue(); + assertThat(accessor.isReadableProperty("class.simpleName")).isTrue(); + assertThat(accessor.isReadableProperty("classLoader")).isTrue(); + assertThat(accessor.isWritableProperty("classLoader")).isTrue(); + OverridingClassLoader ocl = new OverridingClassLoader(getClass().getClassLoader()); + accessor.setPropertyValue("classLoader", ocl); + assertThat(accessor.getPropertyValue("classLoader")).isSameAs(ocl); + + accessor = createAccessor(new UrlResource("https://spring.io/")); + + assertThat(accessor.isReadableProperty("class.package")).isFalse(); + assertThat(accessor.isReadableProperty("class.module")).isFalse(); + assertThat(accessor.isReadableProperty("class.classLoader")).isFalse(); + assertThat(accessor.isReadableProperty("class.name")).isTrue(); + assertThat(accessor.isReadableProperty("class.simpleName")).isTrue(); + assertThat(accessor.isReadableProperty("URL.protocol")).isTrue(); + assertThat(accessor.isReadableProperty("URL.host")).isTrue(); + assertThat(accessor.isReadableProperty("URL.port")).isTrue(); + assertThat(accessor.isReadableProperty("URL.file")).isTrue(); + assertThat(accessor.isReadableProperty("URL.content")).isFalse(); + assertThat(accessor.isReadableProperty("inputStream")).isFalse(); + assertThat(accessor.isReadableProperty("filename")).isTrue(); + assertThat(accessor.isReadableProperty("description")).isTrue(); + + accessor = createAccessor(new ActiveResource()); + + assertThat(accessor.isReadableProperty("resource")).isTrue(); } @Test @SuppressWarnings("unchecked") - public void getPropertyWithOptional() { + void getPropertyWithOptional() { GetterWithOptional target = new GetterWithOptional(); TestBean tb = new TestBean("x"); BeanWrapper accessor = createAccessor(target); @@ -189,7 +247,7 @@ public void getPropertyWithOptional() { } @Test - public void getPropertyWithOptionalAndAutoGrow() { + void getPropertyWithOptionalAndAutoGrow() { GetterWithOptional target = new GetterWithOptional(); BeanWrapper accessor = createAccessor(target); accessor.setAutoGrowNestedPaths(true); @@ -201,7 +259,7 @@ public void getPropertyWithOptionalAndAutoGrow() { } @Test - public void incompletelyQuotedKeyLeadsToPropertyException() { + void incompletelyQuotedKeyLeadsToPropertyException() { TestBean target = new TestBean(); BeanWrapper accessor = createAccessor(target); assertThatExceptionOfType(NotWritablePropertyException.class).isThrownBy(() -> @@ -304,6 +362,36 @@ public Integer getObject() { } + public static class SetterOverload { + + public String value; + + public void setObject(Integer length) { + this.value = length.toString(); + } + + public void setObject(String object) { + this.value = object; + } + + public String getObject() { + return this.value; + } + } + + + public static class ActiveResource implements AutoCloseable { + + public ActiveResource getResource() { + return this; + } + + @Override + public void close() throws Exception { + } + } + + public static class GetterWithOptional { public TestBean value; diff --git a/spring-beans/src/test/java/org/springframework/beans/ConcurrentBeanWrapperTests.java b/spring-beans/src/test/java/org/springframework/beans/ConcurrentBeanWrapperTests.java index f1d4912b16e9..d0d3e233270e 100644 --- a/spring-beans/src/test/java/org/springframework/beans/ConcurrentBeanWrapperTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/ConcurrentBeanWrapperTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -18,14 +18,14 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.util.Collections; -import java.util.HashSet; import java.util.Iterator; import java.util.Properties; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -36,23 +36,21 @@ * @author Chris Beams * @since 08.03.2004 */ -public class ConcurrentBeanWrapperTests { +class ConcurrentBeanWrapperTests { private final Log logger = LogFactory.getLog(getClass()); - private Set set = Collections.synchronizedSet(new HashSet()); + private final Set set = ConcurrentHashMap.newKeySet(); private Throwable ex = null; - @Test - public void testSingleThread() { - for (int i = 0; i < 100; i++) { - performSet(); - } + @RepeatedTest(100) + void testSingleThread() { + performSet(); } @Test - public void testConcurrent() { + void testConcurrent() { for (int i = 0; i < 10; i++) { TestRun run = new TestRun(this); set.add(run); @@ -82,7 +80,7 @@ private static void performSet() { Properties p = (Properties) System.getProperties().clone(); - assertThat(p.size() != 0).as("The System properties must not be empty").isTrue(); + assertThat(p).as("The System properties must not be empty").isNotEmpty(); for (Iterator i = p.entrySet().iterator(); i.hasNext();) { i.next(); @@ -111,7 +109,7 @@ private static class TestRun implements Runnable { private ConcurrentBeanWrapperTests test; - public TestRun(ConcurrentBeanWrapperTests test) { + TestRun(ConcurrentBeanWrapperTests test) { this.test = test; } diff --git a/spring-beans/src/test/java/org/springframework/beans/DirectFieldAccessorTests.java b/spring-beans/src/test/java/org/springframework/beans/DirectFieldAccessorTests.java index 1ca8ddc60298..2cbe8f2ff41c 100644 --- a/spring-beans/src/test/java/org/springframework/beans/DirectFieldAccessorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/DirectFieldAccessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -29,7 +29,7 @@ * @author Chris Beams * @author Stephane Nicoll */ -public class DirectFieldAccessorTests extends AbstractPropertyAccessorTests { +class DirectFieldAccessorTests extends AbstractPropertyAccessorTests { @Override protected DirectFieldAccessor createAccessor(Object target) { @@ -38,7 +38,7 @@ protected DirectFieldAccessor createAccessor(Object target) { @Test - public void withShadowedField() { + void withShadowedField() { final StringBuilder sb = new StringBuilder(); TestBean target = new TestBean() { diff --git a/spring-beans/src/test/java/org/springframework/beans/ExtendedBeanInfoFactoryTests.java b/spring-beans/src/test/java/org/springframework/beans/ExtendedBeanInfoFactoryTests.java deleted file mode 100644 index c4449bcb5b95..000000000000 --- a/spring-beans/src/test/java/org/springframework/beans/ExtendedBeanInfoFactoryTests.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2002-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.beans; - -import java.beans.IntrospectionException; - -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - - -/** - * Unit tests for {@link ExtendedBeanInfoTests}. - * - * @author Chris Beams - */ -public class ExtendedBeanInfoFactoryTests { - - private ExtendedBeanInfoFactory factory = new ExtendedBeanInfoFactory(); - - @Test - public void shouldNotSupportClassHavingOnlyVoidReturningSetter() throws IntrospectionException { - @SuppressWarnings("unused") - class C { - public void setFoo(String s) { } - } - assertThat(factory.getBeanInfo(C.class)).isNull(); - } - - @Test - public void shouldSupportClassHavingNonVoidReturningSetter() throws IntrospectionException { - @SuppressWarnings("unused") - class C { - public C setFoo(String s) { return this; } - } - assertThat(factory.getBeanInfo(C.class)).isNotNull(); - } - - @Test - public void shouldSupportClassHavingNonVoidReturningIndexedSetter() throws IntrospectionException { - @SuppressWarnings("unused") - class C { - public C setFoo(int i, String s) { return this; } - } - assertThat(factory.getBeanInfo(C.class)).isNotNull(); - } - - @Test - public void shouldNotSupportClassHavingNonPublicNonVoidReturningIndexedSetter() throws IntrospectionException { - @SuppressWarnings("unused") - class C { - void setBar(String s) { } - } - assertThat(factory.getBeanInfo(C.class)).isNull(); - } - - @Test - public void shouldNotSupportClassHavingNonVoidReturningParameterlessSetter() throws IntrospectionException { - @SuppressWarnings("unused") - class C { - C setBar() { return this; } - } - assertThat(factory.getBeanInfo(C.class)).isNull(); - } - - @Test - public void shouldNotSupportClassHavingNonVoidReturningMethodNamedSet() throws IntrospectionException { - @SuppressWarnings("unused") - class C { - C set(String s) { return this; } - } - assertThat(factory.getBeanInfo(C.class)).isNull(); - } - -} diff --git a/spring-beans/src/test/java/org/springframework/beans/ExtendedBeanInfoTests.java b/spring-beans/src/test/java/org/springframework/beans/ExtendedBeanInfoTests.java index d9876729023f..f3a1b5f1a506 100644 --- a/spring-beans/src/test/java/org/springframework/beans/ExtendedBeanInfoTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/ExtendedBeanInfoTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -18,7 +18,6 @@ import java.beans.BeanInfo; import java.beans.IndexedPropertyDescriptor; -import java.beans.IntrospectionException; import java.beans.Introspector; import java.beans.PropertyDescriptor; import java.math.BigDecimal; @@ -28,6 +27,7 @@ import org.springframework.beans.testfixture.beans.TestBean; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; /** * @author Chris Beams @@ -35,10 +35,10 @@ * @author Sam Brannen * @since 3.1 */ -public class ExtendedBeanInfoTests { +class ExtendedBeanInfoTests { @Test - public void standardReadMethodOnly() throws IntrospectionException { + void standardReadMethodOnly() throws Exception { @SuppressWarnings("unused") class C { public String getFoo() { return null; } } @@ -54,7 +54,7 @@ public void standardReadMethodOnly() throws IntrospectionException { } @Test - public void standardWriteMethodOnly() throws IntrospectionException { + void standardWriteMethodOnly() throws Exception { @SuppressWarnings("unused") class C { public void setFoo(String f) { } } @@ -70,7 +70,7 @@ public void setFoo(String f) { } } @Test - public void standardReadAndWriteMethods() throws IntrospectionException { + void standardReadAndWriteMethods() throws Exception { @SuppressWarnings("unused") class C { public void setFoo(String f) { } public String getFoo() { return null; } @@ -87,7 +87,7 @@ public void setFoo(String f) { } } @Test - public void nonStandardWriteMethodOnly() throws IntrospectionException { + void nonStandardWriteMethodOnly() throws Exception { @SuppressWarnings("unused") class C { public C setFoo(String foo) { return this; } } @@ -103,7 +103,7 @@ public void nonStandardWriteMethodOnly() throws IntrospectionException { } @Test - public void standardReadAndNonStandardWriteMethods() throws IntrospectionException { + void standardReadAndNonStandardWriteMethods() throws Exception { @SuppressWarnings("unused") class C { public String getFoo() { return null; } public C setFoo(String foo) { return this; } @@ -116,15 +116,12 @@ public void standardReadAndNonStandardWriteMethods() throws IntrospectionExcepti ExtendedBeanInfo ebi = new ExtendedBeanInfo(bi); - assertThat(hasReadMethodForProperty(bi, "foo")).isTrue(); - assertThat(hasWriteMethodForProperty(bi, "foo")).isFalse(); - assertThat(hasReadMethodForProperty(ebi, "foo")).isTrue(); assertThat(hasWriteMethodForProperty(ebi, "foo")).isTrue(); } @Test - public void standardReadAndNonStandardIndexedWriteMethod() throws IntrospectionException { + void standardReadAndNonStandardIndexedWriteMethod() throws Exception { @SuppressWarnings("unused") class C { public String[] getFoo() { return null; } public C setFoo(int i, String foo) { return this; } @@ -144,7 +141,7 @@ public void standardReadAndNonStandardIndexedWriteMethod() throws IntrospectionE } @Test - public void standardReadMethodsAndOverloadedNonStandardWriteMethods() throws Exception { + void standardReadMethodsAndOverloadedNonStandardWriteMethods() throws Exception { @SuppressWarnings("unused") class C { public String getFoo() { return null; } public C setFoo(String foo) { return this; } @@ -158,9 +155,6 @@ public void standardReadMethodsAndOverloadedNonStandardWriteMethods() throws Exc ExtendedBeanInfo ebi = new ExtendedBeanInfo(bi); - assertThat(hasReadMethodForProperty(bi, "foo")).isTrue(); - assertThat(hasWriteMethodForProperty(bi, "foo")).isFalse(); - assertThat(hasReadMethodForProperty(ebi, "foo")).isTrue(); assertThat(hasWriteMethodForProperty(ebi, "foo")).isTrue(); @@ -174,7 +168,7 @@ public void standardReadMethodsAndOverloadedNonStandardWriteMethods() throws Exc } @Test - public void cornerSpr9414() throws IntrospectionException { + void cornerSpr9414() throws Exception { @SuppressWarnings("unused") class Parent { public Number getProperty1() { return 1; @@ -197,8 +191,8 @@ public Integer getProperty1() { } @Test - public void cornerSpr9453() throws IntrospectionException { - final class Bean implements Spr9453> { + void cornerSpr9453() throws Exception { + class Bean implements Spr9453> { @Override public Class getProp() { return null; @@ -215,7 +209,7 @@ public Class getProp() { } @Test - public void standardReadMethodInSuperclassAndNonStandardWriteMethodInSubclass() throws Exception { + void standardReadMethodInSuperclassAndNonStandardWriteMethodInSubclass() throws Exception { @SuppressWarnings("unused") class B { public String getFoo() { return null; } } @@ -230,15 +224,12 @@ public void standardReadMethodInSuperclassAndNonStandardWriteMethodInSubclass() ExtendedBeanInfo ebi = new ExtendedBeanInfo(bi); - assertThat(hasReadMethodForProperty(bi, "foo")).isTrue(); - assertThat(hasWriteMethodForProperty(bi, "foo")).isFalse(); - assertThat(hasReadMethodForProperty(ebi, "foo")).isTrue(); assertThat(hasWriteMethodForProperty(ebi, "foo")).isTrue(); } @Test - public void standardReadMethodInSuperAndSubclassesAndGenericBuilderStyleNonStandardWriteMethodInSuperAndSubclasses() throws Exception { + void standardReadMethodInSuperAndSubclassesAndGenericBuilderStyleNonStandardWriteMethodInSuperAndSubclasses() throws Exception { abstract class B> { @SuppressWarnings("unchecked") protected final This instance = (This) this; @@ -276,12 +267,6 @@ public C setBar(int bar) { BeanInfo ebi = new ExtendedBeanInfo(bi); - assertThat(hasReadMethodForProperty(bi, "foo")).isTrue(); - assertThat(hasWriteMethodForProperty(bi, "foo")).isFalse(); - - assertThat(hasReadMethodForProperty(bi, "bar")).isTrue(); - assertThat(hasWriteMethodForProperty(bi, "bar")).isFalse(); - assertThat(hasReadMethodForProperty(ebi, "foo")).isTrue(); assertThat(hasWriteMethodForProperty(ebi, "foo")).isTrue(); @@ -290,7 +275,7 @@ public C setBar(int bar) { } @Test - public void nonPublicStandardReadAndWriteMethods() throws Exception { + void nonPublicStandardReadAndWriteMethods() throws Exception { @SuppressWarnings("unused") class C { String getFoo() { return null; } C setFoo(String foo) { return this; } @@ -311,7 +296,7 @@ public void nonPublicStandardReadAndWriteMethods() throws Exception { * in strange edge cases. */ @Test - public void readMethodReturnsSupertypeOfWriteMethodParameter() throws IntrospectionException { + void readMethodReturnsSupertypeOfWriteMethodParameter() throws Exception { @SuppressWarnings("unused") class C { public Number getFoo() { return null; } public void setFoo(Integer foo) { } @@ -326,7 +311,7 @@ public void setFoo(Integer foo) { } } @Test - public void indexedReadMethodReturnsSupertypeOfIndexedWriteMethodParameter() throws IntrospectionException { + void indexedReadMethodReturnsSupertypeOfIndexedWriteMethodParameter() throws Exception { @SuppressWarnings("unused") class C { public Number getFoos(int index) { return null; } public void setFoos(int index, Integer foo) { } @@ -345,7 +330,7 @@ public void setFoos(int index, Integer foo) { } * in strange edge cases. */ @Test - public void readMethodReturnsSubtypeOfWriteMethodParameter() throws IntrospectionException { + void readMethodReturnsSubtypeOfWriteMethodParameter() throws Exception { @SuppressWarnings("unused") class C { public Integer getFoo() { return null; } public void setFoo(Number foo) { } @@ -362,7 +347,7 @@ public void setFoo(Number foo) { } } @Test - public void indexedReadMethodReturnsSubtypeOfIndexedWriteMethodParameter() throws IntrospectionException { + void indexedReadMethodReturnsSubtypeOfIndexedWriteMethodParameter() throws Exception { @SuppressWarnings("unused") class C { public Integer getFoos(int index) { return null; } public void setFoo(int index, Number foo) { } @@ -379,7 +364,7 @@ public void setFoo(int index, Number foo) { } } @Test - public void indexedReadMethodOnly() throws IntrospectionException { + void indexedReadMethodOnly() throws Exception { @SuppressWarnings("unused") class C { // indexed read method @@ -397,7 +382,7 @@ class C { } @Test - public void indexedWriteMethodOnly() throws IntrospectionException { + void indexedWriteMethodOnly() throws Exception { @SuppressWarnings("unused") class C { // indexed write method @@ -415,7 +400,7 @@ public void setFoos(int i, String foo) { } } @Test - public void indexedReadAndIndexedWriteMethods() throws IntrospectionException { + void indexedReadAndIndexedWriteMethods() throws Exception { @SuppressWarnings("unused") class C { // indexed read method @@ -439,7 +424,7 @@ public void setFoos(int i, String foo) { } } @Test - public void readAndWriteAndIndexedReadAndIndexedWriteMethods() throws IntrospectionException { + void readAndWriteAndIndexedReadAndIndexedWriteMethods() throws Exception { @SuppressWarnings("unused") class C { // read method @@ -467,7 +452,7 @@ public void setFoos(int i, String foo) { } } @Test - public void indexedReadAndNonStandardIndexedWrite() throws IntrospectionException { + void indexedReadAndNonStandardIndexedWrite() throws Exception { @SuppressWarnings("unused") class C { // indexed read method @@ -489,7 +474,7 @@ class C { } @Test - public void indexedReadAndNonStandardWriteAndNonStandardIndexedWrite() throws IntrospectionException { + void indexedReadAndNonStandardWriteAndNonStandardIndexedWrite() throws Exception { @SuppressWarnings("unused") class C { // non-standard write method @@ -519,7 +504,7 @@ class C { } @Test - public void cornerSpr9702() throws IntrospectionException { + void cornerSpr9702() throws Exception { { // baseline with standard write method @SuppressWarnings("unused") class C { @@ -569,16 +554,16 @@ class C { * IntrospectionException regarding a "type mismatch between indexed and non-indexed * methods" intermittently (approximately one out of every four times) under JDK 7 * due to non-deterministic results from {@link Class#getDeclaredMethods()}. - * See https://bugs.java.com/view_bug.do?bug_id=7023180 + * See https://bugs.java.com/bugdatabase/view_bug.do?bug_id=7023180 * @see #cornerSpr9702() */ @Test - public void cornerSpr10111() throws Exception { - new ExtendedBeanInfo(Introspector.getBeanInfo(BigDecimal.class)); + void cornerSpr10111() throws Exception { + assertThatNoException().isThrownBy(() -> new ExtendedBeanInfo(Introspector.getBeanInfo(BigDecimal.class))); } @Test - public void subclassWriteMethodWithCovariantReturnType() throws IntrospectionException { + void subclassWriteMethodWithCovariantReturnType() throws Exception { @SuppressWarnings("unused") class B { public String getFoo() { return null; } public Number setFoo(String foo) { return null; } @@ -597,9 +582,6 @@ class C extends B { BeanInfo ebi = new ExtendedBeanInfo(bi); - assertThat(hasReadMethodForProperty(bi, "foo")).isTrue(); - assertThat(hasWriteMethodForProperty(bi, "foo")).isFalse(); - assertThat(hasReadMethodForProperty(ebi, "foo")).isTrue(); assertThat(hasWriteMethodForProperty(ebi, "foo")).isTrue(); @@ -607,7 +589,7 @@ class C extends B { } @Test - public void nonStandardReadMethodAndStandardWriteMethod() throws IntrospectionException { + void nonStandardReadMethodAndStandardWriteMethod() throws Exception { @SuppressWarnings("unused") class C { public void getFoo() { } public void setFoo(String foo) { } @@ -628,7 +610,7 @@ public void setFoo(String foo) { } * could occur when handling ArrayList.set(int,Object) */ @Test - public void emptyPropertiesIgnored() throws IntrospectionException { + void emptyPropertiesIgnored() throws Exception { @SuppressWarnings("unused") class C { public Object set(Object o) { return null; } public Object set(int i, Object o) { return null; } @@ -641,7 +623,7 @@ public void emptyPropertiesIgnored() throws IntrospectionException { } @Test - public void overloadedNonStandardWriteMethodsOnly_orderA() throws IntrospectionException, SecurityException, NoSuchMethodException { + void overloadedNonStandardWriteMethodsOnly_orderA() throws Exception { @SuppressWarnings("unused") class C { public Object setFoo(String p) { return new Object(); } public Object setFoo(int p) { return new Object(); } @@ -653,9 +635,6 @@ public void overloadedNonStandardWriteMethodsOnly_orderA() throws IntrospectionE BeanInfo ebi = new ExtendedBeanInfo(bi); - assertThat(hasReadMethodForProperty(bi, "foo")).isFalse(); - assertThat(hasWriteMethodForProperty(bi, "foo")).isFalse(); - assertThat(hasReadMethodForProperty(ebi, "foo")).isFalse(); assertThat(hasWriteMethodForProperty(ebi, "foo")).isTrue(); @@ -669,7 +648,7 @@ public void overloadedNonStandardWriteMethodsOnly_orderA() throws IntrospectionE } @Test - public void overloadedNonStandardWriteMethodsOnly_orderB() throws IntrospectionException, SecurityException, NoSuchMethodException { + void overloadedNonStandardWriteMethodsOnly_orderB() throws Exception { @SuppressWarnings("unused") class C { public Object setFoo(int p) { return new Object(); } public Object setFoo(String p) { return new Object(); } @@ -681,9 +660,6 @@ public void overloadedNonStandardWriteMethodsOnly_orderB() throws IntrospectionE BeanInfo ebi = new ExtendedBeanInfo(bi); - assertThat(hasReadMethodForProperty(bi, "foo")).isFalse(); - assertThat(hasWriteMethodForProperty(bi, "foo")).isFalse(); - assertThat(hasReadMethodForProperty(ebi, "foo")).isFalse(); assertThat(hasWriteMethodForProperty(ebi, "foo")).isTrue(); @@ -704,7 +680,7 @@ public void overloadedNonStandardWriteMethodsOnly_orderB() throws IntrospectionE * not actually intended to serve as an indexed write method; it just appears that way. */ @Test - public void reproSpr8522() throws IntrospectionException { + void reproSpr8522() throws Exception { @SuppressWarnings("unused") class C { public Object setDateFormat(String pattern) { return new Object(); } public Object setDateFormat(int style) { return new Object(); } @@ -731,7 +707,7 @@ public void reproSpr8522() throws IntrospectionException { } @Test - public void propertyCountsMatch() throws IntrospectionException { + void propertyCountsMatch() throws Exception { BeanInfo bi = Introspector.getBeanInfo(TestBean.class); BeanInfo ebi = new ExtendedBeanInfo(bi); @@ -739,7 +715,7 @@ public void propertyCountsMatch() throws IntrospectionException { } @Test - public void propertyCountsWithNonStandardWriteMethod() throws IntrospectionException { + void propertyCountsWithNonStandardWriteMethod() throws Exception { class ExtendedTestBean extends TestBean { @SuppressWarnings("unused") public ExtendedTestBean setFoo(String s) { return this; } @@ -763,7 +739,7 @@ class ExtendedTestBean extends TestBean { * Test that {@link ExtendedBeanInfo#getPropertyDescriptors()} does the same. */ @Test - public void propertyDescriptorOrderIsEqual() throws IntrospectionException { + void propertyDescriptorOrderIsEqual() throws Exception { BeanInfo bi = Introspector.getBeanInfo(TestBean.class); BeanInfo ebi = new ExtendedBeanInfo(bi); @@ -773,7 +749,7 @@ public void propertyDescriptorOrderIsEqual() throws IntrospectionException { } @Test - public void propertyDescriptorComparator() throws IntrospectionException { + void propertyDescriptorComparator() throws Exception { ExtendedBeanInfo.PropertyDescriptorComparator c = new ExtendedBeanInfo.PropertyDescriptorComparator(); assertThat(c.compare(new PropertyDescriptor("a", null, null), new PropertyDescriptor("a", null, null))).isEqualTo(0); @@ -792,7 +768,7 @@ public void propertyDescriptorComparator() throws IntrospectionException { } @Test - public void reproSpr8806() throws IntrospectionException { + void reproSpr8806() throws Exception { // does not throw Introspector.getBeanInfo(LawLibrary.class); @@ -801,7 +777,7 @@ public void reproSpr8806() throws IntrospectionException { } @Test - public void cornerSpr8949() throws IntrospectionException { + void cornerSpr8949() throws Exception { class A { @SuppressWarnings("unused") public boolean isTargetMethod() { @@ -832,7 +808,7 @@ public boolean isTargetMethod() { } @Test - public void cornerSpr8937AndSpr12582() throws IntrospectionException { + void cornerSpr8937AndSpr12582() throws Exception { @SuppressWarnings("unused") class A { public void setAddress(String addr){ } public void setAddress(int index, String addr) { } @@ -855,7 +831,7 @@ public void setAddress(int index, String addr) { } } @Test - public void shouldSupportStaticWriteMethod() throws IntrospectionException { + void shouldSupportStaticWriteMethod() throws Exception { { BeanInfo bi = Introspector.getBeanInfo(WithStaticWriteMethod.class); assertThat(hasReadMethodForProperty(bi, "prop1")).isFalse(); @@ -873,7 +849,7 @@ public void shouldSupportStaticWriteMethod() throws IntrospectionException { } @Test // SPR-12434 - public void shouldDetectValidPropertiesAndIgnoreInvalidProperties() throws IntrospectionException { + void shouldDetectValidPropertiesAndIgnoreInvalidProperties() throws Exception { BeanInfo bi = new ExtendedBeanInfo(Introspector.getBeanInfo(java.awt.Window.class)); assertThat(hasReadMethodForProperty(bi, "locationByPlatform")).isTrue(); assertThat(hasWriteMethodForProperty(bi, "locationByPlatform")).isTrue(); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/BeanFactoryUtilsTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/BeanFactoryUtilsTests.java index d70a7bbd1bd7..b3230e53e0ee 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/BeanFactoryUtilsTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/BeanFactoryUtilsTests.java @@ -63,9 +63,8 @@ public class BeanFactoryUtilsTests { @BeforeEach - public void setUp() { + public void setup() { // Interesting hierarchical factory to test counts. - // Slow to read so we cache it. DefaultListableBeanFactory grandParent = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(grandParent).loadBeanDefinitions(ROOT_CONTEXT); @@ -93,7 +92,7 @@ public void testHierarchicalCountBeansWithNonHierarchicalFactory() { * Check that override doesn't count as two separate beans. */ @Test - public void testHierarchicalCountBeansWithOverride() throws Exception { + public void testHierarchicalCountBeansWithOverride() { // Leaf count assertThat(this.listableBeanFactory.getBeanDefinitionCount() == 1).isTrue(); // Count minus duplicate @@ -101,14 +100,14 @@ public void testHierarchicalCountBeansWithOverride() throws Exception { } @Test - public void testHierarchicalNamesWithNoMatch() throws Exception { + public void testHierarchicalNamesWithNoMatch() { List names = Arrays.asList( BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.listableBeanFactory, NoOp.class)); assertThat(names.size()).isEqualTo(0); } @Test - public void testHierarchicalNamesWithMatchOnlyInRoot() throws Exception { + public void testHierarchicalNamesWithMatchOnlyInRoot() { List names = Arrays.asList( BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.listableBeanFactory, IndexedTestBean.class)); assertThat(names.size()).isEqualTo(1); @@ -118,7 +117,7 @@ public void testHierarchicalNamesWithMatchOnlyInRoot() throws Exception { } @Test - public void testGetBeanNamesForTypeWithOverride() throws Exception { + public void testGetBeanNamesForTypeWithOverride() { List names = Arrays.asList( BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.listableBeanFactory, ITestBean.class)); // includes 2 TestBeans from FactoryBeans (DummyFactory definitions) @@ -236,7 +235,7 @@ public void testFindsBeansOfTypeWithDefaultFactory() { } @Test - public void testHierarchicalResolutionWithOverride() throws Exception { + public void testHierarchicalResolutionWithOverride() { Object test3 = this.listableBeanFactory.getBean("test3"); Object test = this.listableBeanFactory.getBean("test"); @@ -276,14 +275,14 @@ public void testHierarchicalResolutionWithOverride() throws Exception { } @Test - public void testHierarchicalNamesForAnnotationWithNoMatch() throws Exception { + public void testHierarchicalNamesForAnnotationWithNoMatch() { List names = Arrays.asList( BeanFactoryUtils.beanNamesForAnnotationIncludingAncestors(this.listableBeanFactory, Override.class)); assertThat(names.size()).isEqualTo(0); } @Test - public void testHierarchicalNamesForAnnotationWithMatchOnlyInRoot() throws Exception { + public void testHierarchicalNamesForAnnotationWithMatchOnlyInRoot() { List names = Arrays.asList( BeanFactoryUtils.beanNamesForAnnotationIncludingAncestors(this.listableBeanFactory, TestAnnotation.class)); assertThat(names.size()).isEqualTo(1); @@ -293,7 +292,7 @@ public void testHierarchicalNamesForAnnotationWithMatchOnlyInRoot() throws Excep } @Test - public void testGetBeanNamesForAnnotationWithOverride() throws Exception { + public void testGetBeanNamesForAnnotationWithOverride() { AnnotatedBean annotatedBean = new AnnotatedBean(); this.listableBeanFactory.registerSingleton("anotherAnnotatedBean", annotatedBean); List names = Arrays.asList( @@ -433,6 +432,7 @@ public void isSingletonAndIsPrototypeWithStaticFactory() { String basePackage() default ""; } + @Retention(RetentionPolicy.RUNTIME) @ControllerAdvice @interface RestControllerAdvice { @@ -444,18 +444,23 @@ public void isSingletonAndIsPrototypeWithStaticFactory() { String basePackage() default ""; } + @ControllerAdvice("com.example") static class ControllerAdviceClass { } + @RestControllerAdvice("com.example") static class RestControllerAdviceClass { } + static class TestBeanSmartFactoryBean implements SmartFactoryBean { private final TestBean testBean = new TestBean("enigma", 42); + private final boolean singleton; + private final boolean prototype; TestBeanSmartFactoryBean(boolean singleton, boolean prototype) { @@ -478,7 +483,8 @@ public Class getObjectType() { return TestBean.class; } - public TestBean getObject() throws Exception { + @Override + public TestBean getObject() { // We don't really care if the actual instance is a singleton or prototype // for the tests that use this factory. return this.testBean; diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java index dd4ad2b8f1e1..cbf2ff49dacd 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -20,13 +20,9 @@ import java.io.Serializable; import java.lang.reflect.Field; import java.net.MalformedURLException; -import java.security.AccessControlContext; -import java.security.AccessController; -import java.security.Principal; -import java.security.PrivilegedAction; import java.text.NumberFormat; import java.text.ParseException; -import java.util.Arrays; +import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.List; @@ -39,17 +35,14 @@ import java.util.concurrent.Callable; import java.util.stream.Collectors; import java.util.stream.IntStream; +import java.util.stream.Stream; -import javax.annotation.Priority; -import javax.security.auth.Subject; - +import jakarta.annotation.Priority; import org.junit.jupiter.api.Test; import org.springframework.beans.BeansException; import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.NotWritablePropertyException; -import org.springframework.beans.PropertyEditorRegistrar; -import org.springframework.beans.PropertyEditorRegistry; import org.springframework.beans.PropertyValue; import org.springframework.beans.TypeConverter; import org.springframework.beans.TypeMismatchException; @@ -84,13 +77,11 @@ import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotationAwareOrderComparator; -import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.io.Resource; import org.springframework.core.io.UrlResource; import org.springframework.core.testfixture.io.SerializationTestUtils; -import org.springframework.core.testfixture.security.TestPrincipal; import org.springframework.lang.Nullable; import org.springframework.util.StringValueResolver; @@ -98,6 +89,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatNoException; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; @@ -181,10 +173,8 @@ void prototypeFactoryBeanIgnoredByNonEagerTypeMatching() { registerBeanDefinitions(p); assertThat(!DummyFactory.wasPrototypeCreated()).as("prototype not instantiated").isTrue(); - String[] beanNames = lbf.getBeanNamesForType(TestBean.class, true, false); - assertThat(beanNames.length).isEqualTo(0); - beanNames = lbf.getBeanNamesForAnnotation(SuppressWarnings.class); - assertThat(beanNames.length).isEqualTo(0); + assertBeanNamesForType(TestBean.class, false, false); + assertThat(lbf.getBeanNamesForAnnotation(SuppressWarnings.class)).isEmpty(); assertThat(lbf.containsSingleton("x1")).isFalse(); assertThat(lbf.containsBean("x1")).isTrue(); @@ -215,10 +205,8 @@ void singletonFactoryBeanIgnoredByNonEagerTypeMatching() { registerBeanDefinitions(p); assertThat(!DummyFactory.wasPrototypeCreated()).as("prototype not instantiated").isTrue(); - String[] beanNames = lbf.getBeanNamesForType(TestBean.class, true, false); - assertThat(beanNames.length).isEqualTo(0); - beanNames = lbf.getBeanNamesForAnnotation(SuppressWarnings.class); - assertThat(beanNames.length).isEqualTo(0); + assertBeanNamesForType(TestBean.class, false, false); + assertThat(lbf.getBeanNamesForAnnotation(SuppressWarnings.class)).isEmpty(); assertThat(lbf.containsSingleton("x1")).isFalse(); assertThat(lbf.containsBean("x1")).isTrue(); @@ -248,10 +236,8 @@ void nonInitializedFactoryBeanIgnoredByNonEagerTypeMatching() { registerBeanDefinitions(p); assertThat(!DummyFactory.wasPrototypeCreated()).as("prototype not instantiated").isTrue(); - String[] beanNames = lbf.getBeanNamesForType(TestBean.class, true, false); - assertThat(beanNames.length).isEqualTo(0); - beanNames = lbf.getBeanNamesForAnnotation(SuppressWarnings.class); - assertThat(beanNames.length).isEqualTo(0); + assertBeanNamesForType(TestBean.class, false, false); + assertThat(lbf.getBeanNamesForAnnotation(SuppressWarnings.class)).isEmpty(); assertThat(lbf.containsSingleton("x1")).isFalse(); assertThat(lbf.containsBean("x1")).isTrue(); @@ -281,10 +267,8 @@ void initializedFactoryBeanFoundByNonEagerTypeMatching() { registerBeanDefinitions(p); lbf.preInstantiateSingletons(); - assertThat(!DummyFactory.wasPrototypeCreated()).as("prototype not instantiated").isTrue(); - String[] beanNames = lbf.getBeanNamesForType(TestBean.class, true, false); - assertThat(beanNames.length).isEqualTo(1); - assertThat(beanNames[0]).isEqualTo("x1"); + assertThat(DummyFactory.wasPrototypeCreated()).as("prototype not instantiated").isFalse(); + assertBeanNamesForType(TestBean.class, true, false, "x1"); assertThat(lbf.containsSingleton("x1")).isTrue(); assertThat(lbf.containsBean("x1")).isTrue(); assertThat(lbf.containsBean("&x1")).isTrue(); @@ -319,14 +303,10 @@ void initializedFactoryBeanFoundByNonEagerTypeMatching() { assertThat(lbf.isTypeMatch("&x2", Object.class)).isTrue(); assertThat(lbf.getType("x2")).isEqualTo(TestBean.class); assertThat(lbf.getType("&x2")).isEqualTo(DummyFactory.class); - assertThat(lbf.getAliases("x1").length).isEqualTo(1); - assertThat(lbf.getAliases("x1")[0]).isEqualTo("x2"); - assertThat(lbf.getAliases("&x1").length).isEqualTo(1); - assertThat(lbf.getAliases("&x1")[0]).isEqualTo("&x2"); - assertThat(lbf.getAliases("x2").length).isEqualTo(1); - assertThat(lbf.getAliases("x2")[0]).isEqualTo("x1"); - assertThat(lbf.getAliases("&x2").length).isEqualTo(1); - assertThat(lbf.getAliases("&x2")[0]).isEqualTo("&x1"); + assertThat(lbf.getAliases("x1")).containsExactly("x2"); + assertThat(lbf.getAliases("&x1")).containsExactly("&x2"); + assertThat(lbf.getAliases("x2")).containsExactly("x1"); + assertThat(lbf.getAliases("&x2")).containsExactly("&x1"); } @Test @@ -336,9 +316,7 @@ void staticFactoryMethodFoundByNonEagerTypeMatching() { lbf.registerBeanDefinition("x1", rbd); TestBeanFactory.initialized = false; - String[] beanNames = lbf.getBeanNamesForType(TestBean.class, true, false); - assertThat(beanNames.length).isEqualTo(1); - assertThat(beanNames[0]).isEqualTo("x1"); + assertBeanNamesForType(TestBean.class, true, false, "x1"); assertThat(lbf.containsSingleton("x1")).isFalse(); assertThat(lbf.containsBean("x1")).isTrue(); assertThat(lbf.containsBean("&x1")).isFalse(); @@ -349,7 +327,7 @@ void staticFactoryMethodFoundByNonEagerTypeMatching() { assertThat(lbf.isTypeMatch("x1", TestBean.class)).isTrue(); assertThat(lbf.isTypeMatch("&x1", TestBean.class)).isFalse(); assertThat(lbf.getType("x1")).isEqualTo(TestBean.class); - assertThat(lbf.getType("&x1")).isEqualTo(null); + assertThat(lbf.getType("&x1")).isNull(); assertThat(TestBeanFactory.initialized).isFalse(); } @@ -361,9 +339,7 @@ void staticPrototypeFactoryMethodFoundByNonEagerTypeMatching() { lbf.registerBeanDefinition("x1", rbd); TestBeanFactory.initialized = false; - String[] beanNames = lbf.getBeanNamesForType(TestBean.class, true, false); - assertThat(beanNames.length).isEqualTo(1); - assertThat(beanNames[0]).isEqualTo("x1"); + assertBeanNamesForType(TestBean.class, true, false, "x1"); assertThat(lbf.containsSingleton("x1")).isFalse(); assertThat(lbf.containsBean("x1")).isTrue(); assertThat(lbf.containsBean("&x1")).isFalse(); @@ -374,7 +350,7 @@ void staticPrototypeFactoryMethodFoundByNonEagerTypeMatching() { assertThat(lbf.isTypeMatch("x1", TestBean.class)).isTrue(); assertThat(lbf.isTypeMatch("&x1", TestBean.class)).isFalse(); assertThat(lbf.getType("x1")).isEqualTo(TestBean.class); - assertThat(lbf.getType("&x1")).isEqualTo(null); + assertThat(lbf.getType("&x1")).isNull(); assertThat(TestBeanFactory.initialized).isFalse(); } @@ -388,9 +364,7 @@ void nonStaticFactoryMethodFoundByNonEagerTypeMatching() { lbf.registerBeanDefinition("x1", rbd); TestBeanFactory.initialized = false; - String[] beanNames = lbf.getBeanNamesForType(TestBean.class, true, false); - assertThat(beanNames.length).isEqualTo(1); - assertThat(beanNames[0]).isEqualTo("x1"); + assertBeanNamesForType(TestBean.class, true, false, "x1"); assertThat(lbf.containsSingleton("x1")).isFalse(); assertThat(lbf.containsBean("x1")).isTrue(); assertThat(lbf.containsBean("&x1")).isFalse(); @@ -401,7 +375,7 @@ void nonStaticFactoryMethodFoundByNonEagerTypeMatching() { assertThat(lbf.isTypeMatch("x1", TestBean.class)).isTrue(); assertThat(lbf.isTypeMatch("&x1", TestBean.class)).isFalse(); assertThat(lbf.getType("x1")).isEqualTo(TestBean.class); - assertThat(lbf.getType("&x1")).isEqualTo(null); + assertThat(lbf.getType("&x1")).isNull(); assertThat(TestBeanFactory.initialized).isFalse(); } @@ -416,9 +390,7 @@ void nonStaticPrototypeFactoryMethodFoundByNonEagerTypeMatching() { lbf.registerBeanDefinition("x1", rbd); TestBeanFactory.initialized = false; - String[] beanNames = lbf.getBeanNamesForType(TestBean.class, true, false); - assertThat(beanNames.length).isEqualTo(1); - assertThat(beanNames[0]).isEqualTo("x1"); + assertBeanNamesForType(TestBean.class, true, false, "x1"); assertThat(lbf.containsSingleton("x1")).isFalse(); assertThat(lbf.containsBean("x1")).isTrue(); assertThat(lbf.containsBean("&x1")).isFalse(); @@ -433,7 +405,7 @@ void nonStaticPrototypeFactoryMethodFoundByNonEagerTypeMatching() { assertThat(lbf.isTypeMatch("x1", Object.class)).isTrue(); assertThat(lbf.isTypeMatch("&x1", Object.class)).isFalse(); assertThat(lbf.getType("x1")).isEqualTo(TestBean.class); - assertThat(lbf.getType("&x1")).isEqualTo(null); + assertThat(lbf.getType("&x1")).isNull(); assertThat(TestBeanFactory.initialized).isFalse(); lbf.registerAlias("x1", "x2"); @@ -450,15 +422,11 @@ void nonStaticPrototypeFactoryMethodFoundByNonEagerTypeMatching() { assertThat(lbf.isTypeMatch("x2", Object.class)).isTrue(); assertThat(lbf.isTypeMatch("&x2", Object.class)).isFalse(); assertThat(lbf.getType("x2")).isEqualTo(TestBean.class); - assertThat(lbf.getType("&x2")).isEqualTo(null); - assertThat(lbf.getAliases("x1").length).isEqualTo(1); - assertThat(lbf.getAliases("x1")[0]).isEqualTo("x2"); - assertThat(lbf.getAliases("&x1").length).isEqualTo(1); - assertThat(lbf.getAliases("&x1")[0]).isEqualTo("&x2"); - assertThat(lbf.getAliases("x2").length).isEqualTo(1); - assertThat(lbf.getAliases("x2")[0]).isEqualTo("x1"); - assertThat(lbf.getAliases("&x2").length).isEqualTo(1); - assertThat(lbf.getAliases("&x2")[0]).isEqualTo("&x1"); + assertThat(lbf.getType("&x2")).isNull(); + assertThat(lbf.getAliases("x1")).containsExactly("x2"); + assertThat(lbf.getAliases("&x1")).containsExactly("&x2"); + assertThat(lbf.getAliases("x2")).containsExactly("x1"); + assertThat(lbf.getAliases("&x2")).containsExactly("&x1"); } @Test @@ -622,8 +590,7 @@ void arrayReferenceByName() { lbf.registerSingleton("string", "A"); TestBean self = (TestBean) lbf.getBean("self"); - assertThat(self.getStringArray()).hasSize(1); - assertThat(self.getStringArray()).contains("A"); + assertThat(self.getStringArray()).containsExactly("A"); } @Test @@ -636,8 +603,7 @@ void arrayReferenceByType() { lbf.registerSingleton("string", "A"); TestBean self = (TestBean) lbf.getBean("self"); - assertThat(self.getStringArray()).hasSize(1); - assertThat(self.getStringArray()).contains("A"); + assertThat(self.getStringArray()).containsExactly("A"); } @Test @@ -669,8 +635,7 @@ void possibleMatches() { .withCauseInstanceOf(NotWritablePropertyException.class) .satisfies(ex -> { NotWritablePropertyException cause = (NotWritablePropertyException) ex.getCause(); - assertThat(cause.getPossibleMatches()).hasSize(1); - assertThat(cause.getPossibleMatches()[0]).isEqualTo("age"); + assertThat(cause.getPossibleMatches()).containsExactly("age"); }); } @@ -688,7 +653,7 @@ void prototype() { lbf = new DefaultListableBeanFactory(); p = new Properties(); p.setProperty("kerry.(class)", TestBean.class.getName()); - p.setProperty("kerry.(scope)", "prototype"); + p.setProperty("kerry.(scope)", BeanDefinition.SCOPE_PROTOTYPE); p.setProperty("kerry.age", "35"); registerBeanDefinitions(p); kerry1 = (TestBean) lbf.getBean("kerry"); @@ -785,12 +750,13 @@ void canReferenceParentBeanFromChildViaAlias() { factory.registerBeanDefinition("child", childDefinition); factory.registerAlias("parent", "alias"); - TestBean child = (TestBean) factory.getBean("child"); + TestBean child = factory.getBean("child", TestBean.class); assertThat(child.getName()).isEqualTo(EXPECTED_NAME); assertThat(child.getAge()).isEqualTo(EXPECTED_AGE); - Object mergedBeanDefinition2 = factory.getMergedBeanDefinition("child"); + BeanDefinition mergedBeanDefinition1 = factory.getMergedBeanDefinition("child"); + BeanDefinition mergedBeanDefinition2 = factory.getMergedBeanDefinition("child"); - assertThat(mergedBeanDefinition2).as("Use cached merged bean definition").isEqualTo(mergedBeanDefinition2); + assertThat(mergedBeanDefinition1).as("Use cached merged bean definition").isSameAs(mergedBeanDefinition2); } @Test @@ -807,6 +773,29 @@ void getTypeWorksAfterParentChildMerging() { assertThat(factory.getType("child")).isEqualTo(DerivedTestBean.class); } + @Test + void mergedBeanDefinitionChangesRetainedAfterFreezeConfiguration() { + RootBeanDefinition parentDefinition = new RootBeanDefinition(Object.class); + ChildBeanDefinition childDefinition = new ChildBeanDefinition("parent"); + + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + factory.registerBeanDefinition("parent", parentDefinition); + factory.registerBeanDefinition("child", childDefinition); + + assertThat(factory.getType("parent")).isEqualTo(Object.class); + assertThat(factory.getType("child")).isEqualTo(Object.class); + ((RootBeanDefinition) factory.getBeanDefinition("parent")).setBeanClass(TestBean.class); + + factory.freezeConfiguration(); + + assertThat(factory.getType("parent")).isEqualTo(TestBean.class); + assertThat(factory.getType("child")).isEqualTo(TestBean.class); + ((RootBeanDefinition) factory.getMergedBeanDefinition("child")).setBeanClass(DerivedTestBean.class); + + assertThat(factory.getBean("parent")).isInstanceOf(TestBean.class); + assertThat(factory.getBean("child")).isInstanceOf(DerivedTestBean.class); + } + @Test void nameAlreadyBound() { Properties p = new Properties(); @@ -863,8 +852,11 @@ void beanDefinitionOverriding() { lbf.registerBeanDefinition("test", new RootBeanDefinition(NestedTestBean.class)); lbf.registerAlias("otherTest", "test2"); lbf.registerAlias("test", "test2"); + lbf.registerAlias("test", "testX"); + lbf.registerBeanDefinition("testX", new RootBeanDefinition(TestBean.class)); assertThat(lbf.getBean("test")).isInstanceOf(NestedTestBean.class); assertThat(lbf.getBean("test2")).isInstanceOf(NestedTestBean.class); + assertThat(lbf.getBean("testX")).isInstanceOf(TestBean.class); } @Test @@ -873,6 +865,7 @@ void beanDefinitionOverridingNotAllowed() { BeanDefinition oldDef = new RootBeanDefinition(TestBean.class); BeanDefinition newDef = new RootBeanDefinition(NestedTestBean.class); lbf.registerBeanDefinition("test", oldDef); + lbf.registerAlias("test", "testX"); assertThatExceptionOfType(BeanDefinitionOverrideException.class).isThrownBy(() -> lbf.registerBeanDefinition("test", newDef)) .satisfies(ex -> { @@ -880,6 +873,13 @@ void beanDefinitionOverridingNotAllowed() { assertThat(ex.getBeanDefinition()).isEqualTo(newDef); assertThat(ex.getExistingDefinition()).isEqualTo(oldDef); }); + assertThatExceptionOfType(BeanDefinitionOverrideException.class).isThrownBy(() -> + lbf.registerBeanDefinition("testX", newDef)) + .satisfies(ex -> { + assertThat(ex.getBeanName()).isEqualTo("testX"); + assertThat(ex.getBeanDefinition()).isEqualTo(newDef); + assertThat(ex.getExistingDefinition()).isEqualTo(oldDef); + }); } @Test @@ -983,22 +983,19 @@ void customEditor() { bd.setPropertyValues(pvs); lbf.registerBeanDefinition("testBean", bd); TestBean testBean = (TestBean) lbf.getBean("testBean"); - assertThat(testBean.getMyFloat().floatValue() == 1.1f).isTrue(); + assertThat(testBean.getMyFloat() == 1.1f).isTrue(); } @Test void customConverter() { GenericConversionService conversionService = new DefaultConversionService(); - conversionService.addConverter(new Converter() { - @Override - public Float convert(String source) { - try { - NumberFormat nf = NumberFormat.getInstance(Locale.GERMAN); - return nf.parse(source).floatValue(); - } - catch (ParseException ex) { - throw new IllegalArgumentException(ex); - } + conversionService.addConverter(String.class, Float.class, source -> { + try { + NumberFormat nf = NumberFormat.getInstance(Locale.GERMAN); + return nf.parse(source).floatValue(); + } + catch (ParseException ex) { + throw new IllegalArgumentException(ex); } }); lbf.setConversionService(conversionService); @@ -1008,17 +1005,14 @@ public Float convert(String source) { bd.setPropertyValues(pvs); lbf.registerBeanDefinition("testBean", bd); TestBean testBean = (TestBean) lbf.getBean("testBean"); - assertThat(testBean.getMyFloat().floatValue() == 1.1f).isTrue(); + assertThat(testBean.getMyFloat() == 1.1f).isTrue(); } @Test void customEditorWithBeanReference() { - lbf.addPropertyEditorRegistrar(new PropertyEditorRegistrar() { - @Override - public void registerCustomEditors(PropertyEditorRegistry registry) { - NumberFormat nf = NumberFormat.getInstance(Locale.GERMAN); - registry.registerCustomEditor(Float.class, new CustomNumberEditor(Float.class, nf, true)); - } + lbf.addPropertyEditorRegistrar(registry -> { + NumberFormat nf = NumberFormat.getInstance(Locale.GERMAN); + registry.registerCustomEditor(Float.class, new CustomNumberEditor(Float.class, nf, true)); }); MutablePropertyValues pvs = new MutablePropertyValues(); pvs.add("myFloat", new RuntimeBeanReference("myFloat")); @@ -1027,7 +1021,7 @@ public void registerCustomEditors(PropertyEditorRegistry registry) { lbf.registerBeanDefinition("testBean", bd); lbf.registerSingleton("myFloat", "1,1"); TestBean testBean = (TestBean) lbf.getBean("testBean"); - assertThat(testBean.getMyFloat().floatValue() == 1.1f).isTrue(); + assertThat(testBean.getMyFloat() == 1.1f).isTrue(); } @Test @@ -1043,7 +1037,7 @@ void customTypeConverter() { TestBean testBean = (TestBean) lbf.getBean("testBean"); assertThat(testBean.getName()).isEqualTo("myName"); assertThat(testBean.getAge()).isEqualTo(5); - assertThat(testBean.getMyFloat().floatValue() == 1.1f).isTrue(); + assertThat(testBean.getMyFloat() == 1.1f).isTrue(); } @Test @@ -1060,7 +1054,7 @@ void customTypeConverterWithBeanReference() { TestBean testBean = (TestBean) lbf.getBean("testBean"); assertThat(testBean.getName()).isEqualTo("myName"); assertThat(testBean.getAge()).isEqualTo(5); - assertThat(testBean.getMyFloat().floatValue() == 1.1f).isTrue(); + assertThat(testBean.getMyFloat() == 1.1f).isTrue(); } @Test @@ -1153,7 +1147,7 @@ void registerExistingSingletonWithAutowire() { assertThat(lbf.containsBean("singletonObject")).isTrue(); assertThat(lbf.isSingleton("singletonObject")).isTrue(); assertThat(lbf.getType("singletonObject")).isEqualTo(TestBean.class); - assertThat(lbf.getAliases("singletonObject").length).isEqualTo(0); + assertThat(lbf.getAliases("singletonObject")).isEmpty(); DependenciesBean test = (DependenciesBean) lbf.getBean("test"); assertThat(lbf.getBean("singletonObject")).isEqualTo(singletonObject); assertThat(test.getSpouse()).isEqualTo(singletonObject); @@ -1799,12 +1793,12 @@ void getBeanWithArgsNotCreatedForFactoryBeanChecking() { assertThat(bean.beanName).isEqualTo("bd1"); assertThat(bean.spouseAge).isEqualTo(42); - assertThat(lbf.getBeanNamesForType(ConstructorDependency.class).length).isEqualTo(1); - assertThat(lbf.getBeanNamesForType(ConstructorDependencyFactoryBean.class).length).isEqualTo(1); - assertThat(lbf.getBeanNamesForType(ResolvableType.forClassWithGenerics(FactoryBean.class, Object.class)).length).isEqualTo(1); - assertThat(lbf.getBeanNamesForType(ResolvableType.forClassWithGenerics(FactoryBean.class, String.class)).length).isEqualTo(0); - assertThat(lbf.getBeanNamesForType(ResolvableType.forClassWithGenerics(FactoryBean.class, Object.class), true, true).length).isEqualTo(1); - assertThat(lbf.getBeanNamesForType(ResolvableType.forClassWithGenerics(FactoryBean.class, String.class), true, true).length).isEqualTo(0); + assertThat(lbf.getBeanNamesForType(ConstructorDependency.class)).hasSize(1); + assertThat(lbf.getBeanNamesForType(ConstructorDependencyFactoryBean.class)).hasSize(1); + assertThat(lbf.getBeanNamesForType(ResolvableType.forClassWithGenerics(FactoryBean.class, Object.class))).hasSize(1); + assertThat(lbf.getBeanNamesForType(ResolvableType.forClassWithGenerics(FactoryBean.class, String.class))).isEmpty(); + assertThat(lbf.getBeanNamesForType(ResolvableType.forClassWithGenerics(FactoryBean.class, Object.class), true, true)).hasSize(1); + assertThat(lbf.getBeanNamesForType(ResolvableType.forClassWithGenerics(FactoryBean.class, String.class), true, true)).isEmpty(); } private RootBeanDefinition createConstructorDependencyBeanDefinition(int age) { @@ -1838,8 +1832,7 @@ void autowireBeanWithFactoryBeanByType() { assertThat(factoryBean).as("The FactoryBean should have been registered.").isNotNull(); FactoryBeanDependentBean bean = (FactoryBeanDependentBean) lbf.autowire(FactoryBeanDependentBean.class, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, true); - Object mergedBeanDefinition2 = bean.getFactoryBean(); - assertThat(mergedBeanDefinition2).as("The FactoryBeanDependentBean should have been autowired 'by type' with the LazyInitFactory.").isEqualTo(mergedBeanDefinition2); + assertThat(bean.getFactoryBean()).as("The FactoryBeanDependentBean should have been autowired 'by type' with the LazyInitFactory.").isEqualTo(factoryBean); } @Test @@ -1870,46 +1863,45 @@ void getTypeForAbstractFactoryBean() { @Test void getBeanNamesForTypeBeforeFactoryBeanCreation() { + FactoryBeanThatShouldntBeCalled.instantiated = false; lbf.registerBeanDefinition("factoryBean", new RootBeanDefinition(FactoryBeanThatShouldntBeCalled.class)); assertThat(lbf.containsSingleton("factoryBean")).isFalse(); + assertThat(FactoryBeanThatShouldntBeCalled.instantiated).isFalse(); - String[] beanNames = lbf.getBeanNamesForType(Runnable.class, false, false); - assertThat(beanNames.length).isEqualTo(1); - assertThat(beanNames[0]).isEqualTo("&factoryBean"); - - beanNames = lbf.getBeanNamesForType(Callable.class, false, false); - assertThat(beanNames.length).isEqualTo(1); - assertThat(beanNames[0]).isEqualTo("&factoryBean"); - - beanNames = lbf.getBeanNamesForType(RepositoryFactoryInformation.class, false, false); - assertThat(beanNames.length).isEqualTo(1); - assertThat(beanNames[0]).isEqualTo("&factoryBean"); - - beanNames = lbf.getBeanNamesForType(FactoryBean.class, false, false); - assertThat(beanNames.length).isEqualTo(1); - assertThat(beanNames[0]).isEqualTo("&factoryBean"); + assertBeanNamesForType(Runnable.class, false, false, "&factoryBean"); + assertBeanNamesForType(Callable.class, false, false, "&factoryBean"); + assertBeanNamesForType(RepositoryFactoryInformation.class, false, false, "&factoryBean"); + assertBeanNamesForType(FactoryBean.class, false, false, "&factoryBean"); } @Test void getBeanNamesForTypeAfterFactoryBeanCreation() { + FactoryBeanThatShouldntBeCalled.instantiated = false; lbf.registerBeanDefinition("factoryBean", new RootBeanDefinition(FactoryBeanThatShouldntBeCalled.class)); lbf.getBean("&factoryBean"); + assertThat(FactoryBeanThatShouldntBeCalled.instantiated).isTrue(); + assertThat(lbf.containsSingleton("factoryBean")).isTrue(); + + assertBeanNamesForType(Runnable.class, false, false, "&factoryBean"); + assertBeanNamesForType(Callable.class, false, false, "&factoryBean"); + assertBeanNamesForType(RepositoryFactoryInformation.class, false, false, "&factoryBean"); + assertBeanNamesForType(FactoryBean.class, false, false, "&factoryBean"); + } + + @Test // gh-28616 + void getBeanNamesForTypeWithPrototypeScopedFactoryBean() { + FactoryBeanThatShouldntBeCalled.instantiated = false; + RootBeanDefinition beanDefinition = new RootBeanDefinition(FactoryBeanThatShouldntBeCalled.class); + beanDefinition.setScope(BeanDefinition.SCOPE_PROTOTYPE); + lbf.registerBeanDefinition("factoryBean", beanDefinition); + assertThat(FactoryBeanThatShouldntBeCalled.instantiated).isFalse(); + assertThat(lbf.containsSingleton("factoryBean")).isFalse(); - String[] beanNames = lbf.getBeanNamesForType(Runnable.class, false, false); - assertThat(beanNames.length).isEqualTo(1); - assertThat(beanNames[0]).isEqualTo("&factoryBean"); - - beanNames = lbf.getBeanNamesForType(Callable.class, false, false); - assertThat(beanNames.length).isEqualTo(1); - assertThat(beanNames[0]).isEqualTo("&factoryBean"); - - beanNames = lbf.getBeanNamesForType(RepositoryFactoryInformation.class, false, false); - assertThat(beanNames.length).isEqualTo(1); - assertThat(beanNames[0]).isEqualTo("&factoryBean"); - - beanNames = lbf.getBeanNamesForType(FactoryBean.class, false, false); - assertThat(beanNames.length).isEqualTo(1); - assertThat(beanNames[0]).isEqualTo("&factoryBean"); + // We should not find any beans of the following types if the FactoryBean itself is prototype-scoped. + assertBeanNamesForType(Runnable.class, false, false); + assertBeanNamesForType(Callable.class, false, false); + assertBeanNamesForType(RepositoryFactoryInformation.class, false, false); + assertBeanNamesForType(FactoryBean.class, false, false); } /** @@ -2019,6 +2011,19 @@ void autowireBeanByTypePrimaryTakesPrecedenceOverPriority() { assertThat(bean.getSpouse()).isEqualTo(lbf.getBean("spouse")); } + @Test + void beanProviderWithParentBeanFactoryReuseOrder() { + DefaultListableBeanFactory parentBf = new DefaultListableBeanFactory(); + parentBf.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE); + parentBf.registerBeanDefinition("regular", new RootBeanDefinition(TestBean.class)); + parentBf.registerBeanDefinition("test", new RootBeanDefinition(HighPriorityTestBean.class)); + lbf.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE); + lbf.setParentBeanFactory(parentBf); + lbf.registerBeanDefinition("low", new RootBeanDefinition(LowPriorityTestBean.class)); + Stream> orderedTypes = lbf.getBeanProvider(TestBean.class).orderedStream().map(Object::getClass); + assertThat(orderedTypes).containsExactly(HighPriorityTestBean.class, LowPriorityTestBean.class, TestBean.class); + } + @Test void autowireExistingBeanByName() { RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); @@ -2174,8 +2179,7 @@ void circularReferenceThroughAutowiring() { RootBeanDefinition bd = new RootBeanDefinition(ConstructorDependencyBean.class); bd.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR); lbf.registerBeanDefinition("test", bd); - assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy( - lbf::preInstantiateSingletons); + assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(lbf::preInstantiateSingletons); } @Test @@ -2183,8 +2187,7 @@ void circularReferenceThroughFactoryBeanAutowiring() { RootBeanDefinition bd = new RootBeanDefinition(ConstructorDependencyFactoryBean.class); bd.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR); lbf.registerBeanDefinition("test", bd); - assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy( - lbf::preInstantiateSingletons); + assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(lbf::preInstantiateSingletons); } @Test @@ -2192,8 +2195,7 @@ void circularReferenceThroughFactoryBeanTypeCheck() { RootBeanDefinition bd = new RootBeanDefinition(ConstructorDependencyFactoryBean.class); bd.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR); lbf.registerBeanDefinition("test", bd); - assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(() -> - lbf.getBeansOfType(String.class)); + assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(() -> lbf.getBeansOfType(String.class)); } @Test @@ -2220,8 +2222,7 @@ void constructorDependencyWithUnresolvableClass() { RootBeanDefinition bd = new RootBeanDefinition(ConstructorDependencyWithClassResolution.class); bd.getConstructorArgumentValues().addGenericArgumentValue("java.lang.Strin"); lbf.registerBeanDefinition("test", bd); - assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy( - lbf::preInstantiateSingletons); + assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(lbf::preInstantiateSingletons); } @Test @@ -2245,7 +2246,13 @@ void beanDefinitionWithAbstractClass() { @Test void prototypeFactoryBeanNotEagerlyCalled() { lbf.registerBeanDefinition("test", new RootBeanDefinition(FactoryBeanThatShouldntBeCalled.class)); - lbf.preInstantiateSingletons(); + assertThatNoException().isThrownBy(lbf::preInstantiateSingletons); + } + + @Test + void prototypeFactoryBeanNotEagerlyCalledInCaseOfBeanClassName() { + lbf.registerBeanDefinition("test", new RootBeanDefinition(FactoryBeanThatShouldntBeCalled.class.getName(), null, null)); + assertThatNoException().isThrownBy(lbf::preInstantiateSingletons); } @Test @@ -2285,13 +2292,6 @@ void smartInitFactory() { assertThat(factory.initialized).isTrue(); } - @Test - void prototypeFactoryBeanNotEagerlyCalledInCaseOfBeanClassName() { - lbf.registerBeanDefinition("test", - new RootBeanDefinition(FactoryBeanThatShouldntBeCalled.class.getName(), null, null)); - lbf.preInstantiateSingletons(); - } - @Test void prototypeStringCreatedRepeatedly() { RootBeanDefinition stringDef = new RootBeanDefinition(String.class); @@ -2307,9 +2307,7 @@ void prototypeStringCreatedRepeatedly() { @Test void prototypeWithArrayConversionForConstructor() { - List list = new ManagedList<>(); - list.add("myName"); - list.add("myBeanName"); + List list = ManagedList.of("myName", "myBeanName"); RootBeanDefinition bd = new RootBeanDefinition(DerivedTestBean.class); bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); bd.getConstructorArgumentValues().addGenericArgumentValue(list); @@ -2325,9 +2323,7 @@ void prototypeWithArrayConversionForConstructor() { @Test void prototypeWithArrayConversionForFactoryMethod() { - List list = new ManagedList<>(); - list.add("myName"); - list.add("myBeanName"); + List list = ManagedList.of("myName", "myBeanName"); RootBeanDefinition bd = new RootBeanDefinition(DerivedTestBean.class); bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); bd.setFactoryMethodName("create"); @@ -2342,6 +2338,19 @@ void prototypeWithArrayConversionForFactoryMethod() { assertThat(tb2.getBeanName()).isEqualTo("myBeanName"); } + @Test + void multipleInitAndDestroyMethods() { + RootBeanDefinition bd = new RootBeanDefinition(BeanWithInitAndDestroyMethods.class); + bd.setInitMethodNames("init1", "init2"); + bd.setDestroyMethodNames("destroy2", "destroy1"); + lbf.registerBeanDefinition("test", bd); + BeanWithInitAndDestroyMethods bean = lbf.getBean("test", BeanWithInitAndDestroyMethods.class); + assertThat(bean.initMethods).containsExactly("init", "init1", "init2"); + assertThat(bean.destroyMethods).isEmpty(); + lbf.destroySingletons(); + assertThat(bean.destroyMethods).containsExactly("destroy", "destroy2", "destroy1"); + } + @Test void beanPostProcessorWithWrappedObjectAndDisposableBean() { RootBeanDefinition bd = new RootBeanDefinition(BeanWithDisposableBean.class); @@ -2388,8 +2397,7 @@ public Object postProcessBeforeInitialization(Object bean, String beanName) { BeanWithDestroyMethod.closeCount = 0; lbf.preInstantiateSingletons(); lbf.destroySingletons(); - Object mergedBeanDefinition2 = BeanWithDestroyMethod.closeCount; - assertThat(mergedBeanDefinition2).as("Destroy methods invoked").isEqualTo(mergedBeanDefinition2); + assertThat(BeanWithDestroyMethod.closeCount).as("Destroy methods invoked").isEqualTo(1); } @Test @@ -2403,8 +2411,7 @@ void destroyMethodOnInnerBean() { BeanWithDestroyMethod.closeCount = 0; lbf.preInstantiateSingletons(); lbf.destroySingletons(); - Object mergedBeanDefinition2 = BeanWithDestroyMethod.closeCount; - assertThat(mergedBeanDefinition2).as("Destroy methods invoked").isEqualTo(mergedBeanDefinition2); + assertThat(BeanWithDestroyMethod.closeCount).as("Destroy methods invoked").isEqualTo(2); } @Test @@ -2419,8 +2426,7 @@ void destroyMethodOnInnerBeanAsPrototype() { BeanWithDestroyMethod.closeCount = 0; lbf.preInstantiateSingletons(); lbf.destroySingletons(); - Object mergedBeanDefinition2 = BeanWithDestroyMethod.closeCount; - assertThat(mergedBeanDefinition2).as("Destroy methods invoked").isEqualTo(mergedBeanDefinition2); + assertThat(BeanWithDestroyMethod.closeCount).as("Destroy methods invoked").isEqualTo(1); } @Test @@ -2478,10 +2484,7 @@ private void findTypeOfPrototypeFactoryMethodOnBeanInstance(boolean singleton) { lbf.registerBeanDefinition("fmWithArgs", factoryMethodDefinitionWithArgs); assertThat(lbf.getBeanDefinitionCount()).isEqualTo(4); - List tbNames = Arrays.asList(lbf.getBeanNamesForType(TestBean.class)); - assertThat(tbNames.contains("fmWithProperties")).isTrue(); - assertThat(tbNames.contains("fmWithArgs")).isTrue(); - assertThat(tbNames.size()).isEqualTo(2); + assertBeanNamesForType(TestBean.class, true, true, "fmWithProperties", "fmWithArgs"); TestBean tb = (TestBean) lbf.getBean("fmWithProperties"); TestBean second = (TestBean) lbf.getBean("fmWithProperties"); @@ -2542,14 +2545,15 @@ void explicitScopeInheritanceForChildBeanDefinitions() { factory.registerBeanDefinition("child", child); AbstractBeanDefinition def = (AbstractBeanDefinition) factory.getBeanDefinition("child"); - Object mergedBeanDefinition2 = def.getScope(); - assertThat(mergedBeanDefinition2).as("Child 'scope' not overriding parent scope (it must).").isEqualTo(mergedBeanDefinition2); + assertThat(def.getScope()).as("Child 'scope' not overriding parent scope (it must).").isEqualTo(theChildScope); } @Test void scopeInheritanceForChildBeanDefinitions() { + String theParentScope = "bonanza!"; + RootBeanDefinition parent = new RootBeanDefinition(); - parent.setScope("bonanza!"); + parent.setScope(theParentScope); AbstractBeanDefinition child = new ChildBeanDefinition("parent"); child.setBeanClass(TestBean.class); @@ -2559,8 +2563,7 @@ void scopeInheritanceForChildBeanDefinitions() { factory.registerBeanDefinition("child", child); BeanDefinition def = factory.getMergedBeanDefinition("child"); - Object mergedBeanDefinition2 = def.getScope(); - assertThat(mergedBeanDefinition2).as("Child 'scope' not inherited").isEqualTo(mergedBeanDefinition2); + assertThat(def.getScope()).as("Child 'scope' not inherited").isEqualTo(theParentScope); } @Test @@ -2596,40 +2599,21 @@ public boolean postProcessAfterInstantiation(Object bean, String beanName) throw }); lbf.preInstantiateSingletons(); TestBean tb = (TestBean) lbf.getBean("test"); - Object mergedBeanDefinition2 = tb.getName(); - assertThat(mergedBeanDefinition2).as("Name was set on field by IAPP").isEqualTo(mergedBeanDefinition2); + assertThat(tb.getName()).as("Name was set on field by IAPP").isEqualTo(nameSetOnField); if (!skipPropertyPopulation) { - Object mergedBeanDefinition21 = tb.getAge(); - assertThat(mergedBeanDefinition21).as("Property value still set").isEqualTo(mergedBeanDefinition21); + assertThat(tb.getAge()).as("Property value still set").isEqualTo(ageSetByPropertyValue); } else { - Object mergedBeanDefinition21 = tb.getAge(); - assertThat(mergedBeanDefinition21).as("Property value was NOT set and still has default value").isEqualTo(mergedBeanDefinition21); + assertThat(tb.getAge()).as("Property value was NOT set and still has default value").isEqualTo(0); } } - @Test - @SuppressWarnings({ "unchecked", "rawtypes" }) - void initSecurityAwarePrototypeBean() { - RootBeanDefinition bd = new RootBeanDefinition(TestSecuredBean.class); - bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); - bd.setInitMethodName("init"); - lbf.registerBeanDefinition("test", bd); - final Subject subject = new Subject(); - subject.getPrincipals().add(new TestPrincipal("user1")); - - TestSecuredBean bean = (TestSecuredBean) Subject.doAsPrivileged(subject, - (PrivilegedAction) () -> lbf.getBean("test"), null); - assertThat(bean).isNotNull(); - assertThat(bean.getUserName()).isEqualTo("user1"); - } - @Test void containsBeanReturnsTrueEvenForAbstractBeanDefinition() { lbf.registerBeanDefinition("abs", BeanDefinitionBuilder .rootBeanDefinition(TestBean.class).setAbstract(true).getBeanDefinition()); - assertThat(lbf.containsBean("abs")).isEqualTo(true); - assertThat(lbf.containsBean("bogus")).isEqualTo(false); + assertThat(lbf.containsBean("abs")).isTrue(); + assertThat(lbf.containsBean("bogus")).isFalse(); } @Test @@ -2688,6 +2672,19 @@ private int registerBeanDefinitions(Properties p, String prefix) { return (new org.springframework.beans.factory.support.PropertiesBeanDefinitionReader(lbf)).registerBeanDefinitions(p, prefix); } + private void assertBeanNamesForType(Class type, boolean includeNonSingletons, boolean allowEagerInit, String... names) { + if (names.length == 0) { + assertThat(lbf.getBeanNamesForType(type, includeNonSingletons, allowEagerInit)) + .as("bean names for type " + type.getName()) + .isEmpty(); + } + else { + assertThat(lbf.getBeanNamesForType(type, includeNonSingletons, allowEagerInit)) + .as("bean names for type " + type.getName()) + .containsExactly(names); + } + } + public static class NoDependencies { @@ -2724,8 +2721,12 @@ public void setBeanName(String name) { @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } ConstructorDependency that = (ConstructorDependency) o; return spouseAge == that.spouseAge && Objects.equals(spouse, that.spouse) && @@ -2785,9 +2786,42 @@ public ConstructorDependencyWithClassResolution() { } + static class BeanWithInitAndDestroyMethods implements InitializingBean, DisposableBean { + + final List initMethods = new ArrayList<>(); + final List destroyMethods = new ArrayList<>(); + + @Override + public void afterPropertiesSet() { + initMethods.add("init"); + } + + void init1() { + initMethods.add("init1"); + } + + void init2() { + initMethods.add("init2"); + } + + @Override + public void destroy() { + destroyMethods.add("destroy"); + } + + void destroy1() { + destroyMethods.add("destroy1"); + } + + void destroy2() { + destroyMethods.add("destroy2"); + } + } + + public static class BeanWithDisposableBean implements DisposableBean { - private static boolean closed; + static boolean closed; @Override public void destroy() { @@ -2798,7 +2832,7 @@ public void destroy() { public static class BeanWithCloseable implements Closeable { - private static boolean closed; + static boolean closed; @Override public void close() { @@ -2815,7 +2849,7 @@ public static abstract class BaseClassWithDestroyMethod { public static class BeanWithDestroyMethod extends BaseClassWithDestroyMethod { - private static int closeCount = 0; + static int closeCount = 0; @SuppressWarnings("unused") private BeanWithDestroyMethod inner; @@ -2874,6 +2908,12 @@ public static abstract class RepositoryFactoryBeanSupport, S, ID extends Serializable> extends RepositoryFactoryBeanSupport implements Runnable, Callable { + static boolean instantiated = false; + + { + instantiated = true; + } + @Override public T getObject() { throw new IllegalStateException(); @@ -3036,7 +3076,7 @@ public CustomTypeConverter(NumberFormat numberFormat) { public Object convertIfNecessary(Object value, @Nullable Class requiredType) { if (value instanceof String && Float.class.isAssignableFrom(requiredType)) { try { - return new Float(this.numberFormat.parse((String) value).floatValue()); + return this.numberFormat.parse((String) value).floatValue(); } catch (ParseException ex) { throw new TypeMismatchException(value, requiredType, ex); @@ -3064,37 +3104,6 @@ public Object convertIfNecessary(Object value, @Nullable Class requiredType, @Nu } - @SuppressWarnings("unused") - private static class TestSecuredBean { - - private String userName; - - void init() { - AccessControlContext acc = AccessController.getContext(); - Subject subject = Subject.getSubject(acc); - if (subject == null) { - return; - } - setNameFromPrincipal(subject.getPrincipals()); - } - - private void setNameFromPrincipal(Set principals) { - if (principals == null) { - return; - } - for (Iterator it = principals.iterator(); it.hasNext();) { - Principal p = it.next(); - this.userName = p.getName(); - return; - } - } - - public String getUserName() { - return this.userName; - } - } - - @SuppressWarnings("unused") private static class KnowsIfInstantiated { @@ -3111,7 +3120,6 @@ public static boolean wasInstantiated() { public KnowsIfInstantiated() { instantiated = true; } - } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/Spr5475Tests.java b/spring-beans/src/test/java/org/springframework/beans/factory/Spr5475Tests.java index 5b82ef031ffe..846f6f6696c7 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/Spr5475Tests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/Spr5475Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -31,6 +31,7 @@ * invoking a factory method is not instructive to the user and rather misleading. * * @author Chris Beams + * @author Juergen Hoeller */ public class Spr5475Tests { @@ -40,7 +41,8 @@ public void noArgFactoryMethodInvokedWithOneArg() { rootBeanDefinition(Foo.class) .setFactoryMethod("noArgFactory") .addConstructorArgValue("bogusArg").getBeanDefinition(), - "Error creating bean with name 'foo': No matching factory method found: factory method 'noArgFactory(String)'. " + + "Error creating bean with name 'foo': No matching factory method found on class " + + "[org.springframework.beans.factory.Spr5475Tests$Foo]: factory method 'noArgFactory(String)'. " + "Check that a method with the specified name and arguments exists and that it is static."); } @@ -51,7 +53,8 @@ public void noArgFactoryMethodInvokedWithTwoArgs() { .setFactoryMethod("noArgFactory") .addConstructorArgValue("bogusArg1") .addConstructorArgValue("bogusArg2".getBytes()).getBeanDefinition(), - "Error creating bean with name 'foo': No matching factory method found: factory method 'noArgFactory(String,byte[])'. " + + "Error creating bean with name 'foo': No matching factory method found on class " + + "[org.springframework.beans.factory.Spr5475Tests$Foo]: factory method 'noArgFactory(String,byte[])'. " + "Check that a method with the specified name and arguments exists and that it is static."); } @@ -65,7 +68,8 @@ public void noArgFactoryMethodInvokedWithTwoArgsAndTypesSpecified() { def.setConstructorArgumentValues(cav); assertExceptionMessageForMisconfiguredFactoryMethod(def, - "Error creating bean with name 'foo': No matching factory method found: factory method 'noArgFactory(CharSequence,byte[])'. " + + "Error creating bean with name 'foo': No matching factory method found on class " + + "[org.springframework.beans.factory.Spr5475Tests$Foo]: factory method 'noArgFactory(CharSequence,byte[])'. " + "Check that a method with the specified name and arguments exists and that it is static."); } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java index 0493c7f54fc7..e9b72d71a5b9 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -22,7 +22,6 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.InvocationHandler; -import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.ArrayList; import java.util.Arrays; @@ -38,7 +37,6 @@ import java.util.concurrent.Callable; import java.util.function.Consumer; import java.util.function.Function; -import java.util.stream.Collectors; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -251,29 +249,36 @@ public void testExtendedResourceInjectionWithDefaultMethod() { } @Test - @SuppressWarnings("deprecation") - public void testExtendedResourceInjectionWithAtRequired() { - bf.addBeanPostProcessor(new RequiredAnnotationBeanPostProcessor()); - RootBeanDefinition bd = new RootBeanDefinition(TypedExtendedResourceInjectionBean.class); - bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); - bf.registerBeanDefinition("annotatedBean", bd); + public void testOptionalResourceInjection() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(OptionalResourceInjectionBean.class)); TestBean tb = new TestBean(); bf.registerSingleton("testBean", tb); - NestedTestBean ntb = new NestedTestBean(); - bf.registerSingleton("nestedTestBean", ntb); + IndexedTestBean itb = new IndexedTestBean(); + bf.registerSingleton("indexedTestBean", itb); + NestedTestBean ntb1 = new NestedTestBean(); + bf.registerSingleton("nestedTestBean1", ntb1); + NestedTestBean ntb2 = new NestedTestBean(); + bf.registerSingleton("nestedTestBean2", ntb2); - TypedExtendedResourceInjectionBean bean = (TypedExtendedResourceInjectionBean) bf.getBean("annotatedBean"); + OptionalResourceInjectionBean bean = (OptionalResourceInjectionBean) bf.getBean("annotatedBean"); assertThat(bean.getTestBean()).isSameAs(tb); assertThat(bean.getTestBean2()).isSameAs(tb); assertThat(bean.getTestBean3()).isSameAs(tb); assertThat(bean.getTestBean4()).isSameAs(tb); - assertThat(bean.getNestedTestBean()).isSameAs(ntb); - assertThat(bean.getBeanFactory()).isSameAs(bf); + assertThat(bean.getIndexedTestBean()).isSameAs(itb); + assertThat(bean.getNestedTestBeans().length).isEqualTo(2); + assertThat(bean.getNestedTestBeans()[0]).isSameAs(ntb1); + assertThat(bean.getNestedTestBeans()[1]).isSameAs(ntb2); + assertThat(bean.nestedTestBeansField.length).isEqualTo(2); + assertThat(bean.nestedTestBeansField[0]).isSameAs(ntb1); + assertThat(bean.nestedTestBeansField[1]).isSameAs(ntb2); } @Test - public void testOptionalResourceInjection() { - bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(OptionalResourceInjectionBean.class)); + public void testOptionalResourceInjectionWithSingletonRemoval() { + RootBeanDefinition rbd = new RootBeanDefinition(OptionalResourceInjectionBean.class); + rbd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", rbd); TestBean tb = new TestBean(); bf.registerSingleton("testBean", tb); IndexedTestBean itb = new IndexedTestBean(); @@ -295,6 +300,93 @@ public void testOptionalResourceInjection() { assertThat(bean.nestedTestBeansField.length).isEqualTo(2); assertThat(bean.nestedTestBeansField[0]).isSameAs(ntb1); assertThat(bean.nestedTestBeansField[1]).isSameAs(ntb2); + + bf.destroySingleton("testBean"); + + bean = (OptionalResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isNull(); + assertThat(bean.getTestBean2()).isNull(); + assertThat(bean.getTestBean3()).isNull(); + assertThat(bean.getTestBean4()).isNull(); + assertThat(bean.getIndexedTestBean()).isSameAs(itb); + assertThat(bean.getNestedTestBeans().length).isEqualTo(2); + assertThat(bean.getNestedTestBeans()[0]).isSameAs(ntb1); + assertThat(bean.getNestedTestBeans()[1]).isSameAs(ntb2); + assertThat(bean.nestedTestBeansField.length).isEqualTo(2); + assertThat(bean.nestedTestBeansField[0]).isSameAs(ntb1); + assertThat(bean.nestedTestBeansField[1]).isSameAs(ntb2); + + bf.registerSingleton("testBean", tb); + + bean = (OptionalResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb); + assertThat(bean.getTestBean3()).isSameAs(tb); + assertThat(bean.getTestBean4()).isSameAs(tb); + assertThat(bean.getIndexedTestBean()).isSameAs(itb); + assertThat(bean.getNestedTestBeans().length).isEqualTo(2); + assertThat(bean.getNestedTestBeans()[0]).isSameAs(ntb1); + assertThat(bean.getNestedTestBeans()[1]).isSameAs(ntb2); + assertThat(bean.nestedTestBeansField.length).isEqualTo(2); + assertThat(bean.nestedTestBeansField[0]).isSameAs(ntb1); + assertThat(bean.nestedTestBeansField[1]).isSameAs(ntb2); + } + + @Test + public void testOptionalResourceInjectionWithBeanDefinitionRemoval() { + RootBeanDefinition rbd = new RootBeanDefinition(OptionalResourceInjectionBean.class); + rbd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", rbd); + bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); + IndexedTestBean itb = new IndexedTestBean(); + bf.registerSingleton("indexedTestBean", itb); + NestedTestBean ntb1 = new NestedTestBean(); + bf.registerSingleton("nestedTestBean1", ntb1); + NestedTestBean ntb2 = new NestedTestBean(); + bf.registerSingleton("nestedTestBean2", ntb2); + + OptionalResourceInjectionBean bean = (OptionalResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean")); + assertThat(bean.getTestBean2()).isSameAs(bf.getBean("testBean")); + assertThat(bean.getTestBean3()).isSameAs(bf.getBean("testBean")); + assertThat(bean.getTestBean4()).isSameAs(bf.getBean("testBean")); + assertThat(bean.getIndexedTestBean()).isSameAs(itb); + assertThat(bean.getNestedTestBeans().length).isEqualTo(2); + assertThat(bean.getNestedTestBeans()[0]).isSameAs(ntb1); + assertThat(bean.getNestedTestBeans()[1]).isSameAs(ntb2); + assertThat(bean.nestedTestBeansField.length).isEqualTo(2); + assertThat(bean.nestedTestBeansField[0]).isSameAs(ntb1); + assertThat(bean.nestedTestBeansField[1]).isSameAs(ntb2); + + bf.removeBeanDefinition("testBean"); + + bean = (OptionalResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isNull(); + assertThat(bean.getTestBean2()).isNull(); + assertThat(bean.getTestBean3()).isNull(); + assertThat(bean.getTestBean4()).isNull(); + assertThat(bean.getIndexedTestBean()).isSameAs(itb); + assertThat(bean.getNestedTestBeans().length).isEqualTo(2); + assertThat(bean.getNestedTestBeans()[0]).isSameAs(ntb1); + assertThat(bean.getNestedTestBeans()[1]).isSameAs(ntb2); + assertThat(bean.nestedTestBeansField.length).isEqualTo(2); + assertThat(bean.nestedTestBeansField[0]).isSameAs(ntb1); + assertThat(bean.nestedTestBeansField[1]).isSameAs(ntb2); + + bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); + + bean = (OptionalResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean")); + assertThat(bean.getTestBean2()).isSameAs(bf.getBean("testBean")); + assertThat(bean.getTestBean3()).isSameAs(bf.getBean("testBean")); + assertThat(bean.getTestBean4()).isSameAs(bf.getBean("testBean")); + assertThat(bean.getIndexedTestBean()).isSameAs(itb); + assertThat(bean.getNestedTestBeans().length).isEqualTo(2); + assertThat(bean.getNestedTestBeans()[0]).isSameAs(ntb1); + assertThat(bean.getNestedTestBeans()[1]).isSameAs(ntb2); + assertThat(bean.nestedTestBeansField.length).isEqualTo(2); + assertThat(bean.nestedTestBeansField[0]).isSameAs(ntb1); + assertThat(bean.nestedTestBeansField[1]).isSameAs(ntb2); } @Test @@ -533,6 +625,83 @@ public void testConstructorResourceInjection() { assertThat(bean.getBeanFactory()).isSameAs(bf); } + @Test + public void testConstructorResourceInjectionWithSingletonRemoval() { + RootBeanDefinition bd = new RootBeanDefinition(ConstructorResourceInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + NestedTestBean ntb = new NestedTestBean(); + bf.registerSingleton("nestedTestBean", ntb); + + ConstructorResourceInjectionBean bean = (ConstructorResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb); + assertThat(bean.getTestBean3()).isSameAs(tb); + assertThat(bean.getTestBean4()).isSameAs(tb); + assertThat(bean.getNestedTestBean()).isSameAs(ntb); + assertThat(bean.getBeanFactory()).isSameAs(bf); + + bf.destroySingleton("nestedTestBean"); + + bean = (ConstructorResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb); + assertThat(bean.getTestBean3()).isSameAs(tb); + assertThat(bean.getTestBean4()).isSameAs(tb); + assertThat(bean.getNestedTestBean()).isNull(); + assertThat(bean.getBeanFactory()).isSameAs(bf); + + bf.registerSingleton("nestedTestBean", ntb); + + bean = (ConstructorResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb); + assertThat(bean.getTestBean3()).isSameAs(tb); + assertThat(bean.getTestBean4()).isSameAs(tb); + assertThat(bean.getNestedTestBean()).isSameAs(ntb); + assertThat(bean.getBeanFactory()).isSameAs(bf); + } + + @Test + public void testConstructorResourceInjectionWithBeanDefinitionRemoval() { + RootBeanDefinition bd = new RootBeanDefinition(ConstructorResourceInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + bf.registerBeanDefinition("nestedTestBean", new RootBeanDefinition(NestedTestBean.class)); + + ConstructorResourceInjectionBean bean = (ConstructorResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb); + assertThat(bean.getTestBean3()).isSameAs(tb); + assertThat(bean.getTestBean4()).isSameAs(tb); + assertThat(bean.getNestedTestBean()).isSameAs(bf.getBean("nestedTestBean")); + assertThat(bean.getBeanFactory()).isSameAs(bf); + + bf.removeBeanDefinition("nestedTestBean"); + + bean = (ConstructorResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb); + assertThat(bean.getTestBean3()).isSameAs(tb); + assertThat(bean.getTestBean4()).isSameAs(tb); + assertThat(bean.getNestedTestBean()).isNull(); + assertThat(bean.getBeanFactory()).isSameAs(bf); + + bf.registerBeanDefinition("nestedTestBean", new RootBeanDefinition(NestedTestBean.class)); + + bean = (ConstructorResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb); + assertThat(bean.getTestBean3()).isSameAs(tb); + assertThat(bean.getTestBean4()).isSameAs(tb); + assertThat(bean.getNestedTestBean()).isSameAs(bf.getBean("nestedTestBean")); + assertThat(bean.getBeanFactory()).isSameAs(bf); + } + @Test public void testConstructorResourceInjectionWithNullFromFactoryBean() { RootBeanDefinition bd = new RootBeanDefinition(ConstructorResourceInjectionBean.class); @@ -2209,7 +2378,6 @@ public NonPublicResourceInjectionBean() { @Override @Autowired - @Required @SuppressWarnings("deprecation") public void setTestBean2(TestBean testBean2) { super.setTestBean2(testBean2); @@ -2847,11 +3015,11 @@ public List forEachTestBeans() { } public List streamTestBeans() { - return this.testBeanProvider.stream().collect(Collectors.toList()); + return this.testBeanProvider.stream().toList(); } public List sortedTestBeans() { - return this.testBeanProvider.orderedStream().collect(Collectors.toList()); + return this.testBeanProvider.orderedStream().toList(); } } @@ -3468,11 +3636,8 @@ public static class MocksControl { @SuppressWarnings("unchecked") public T createMock(Class toMock) { return (T) Proxy.newProxyInstance(AutowiredAnnotationBeanPostProcessorTests.class.getClassLoader(), new Class[] {toMock}, - new InvocationHandler() { - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - throw new UnsupportedOperationException("mocked!"); - } + (InvocationHandler) (proxy, method, args) -> { + throw new UnsupportedOperationException("mocked!"); }); } } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanRegistrationAotContributionTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanRegistrationAotContributionTests.java new file mode 100644 index 000000000000..81773b397379 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanRegistrationAotContributionTests.java @@ -0,0 +1,232 @@ +/* + * Copyright 2002-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.beans.factory.annotation; + +import java.util.function.BiConsumer; +import java.util.function.BiFunction; + +import javax.lang.model.element.Modifier; + +import org.junit.jupiter.api.Test; + +import org.springframework.aot.generate.MethodReference; +import org.springframework.aot.generate.MethodReference.ArgumentCodeGenerator; +import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.factory.annotation.PackagePrivateFieldInjectionSample; +import org.springframework.beans.testfixture.beans.factory.annotation.PackagePrivateMethodInjectionSample; +import org.springframework.beans.testfixture.beans.factory.annotation.PrivateFieldInjectionSample; +import org.springframework.beans.testfixture.beans.factory.annotation.PrivateMethodInjectionSample; +import org.springframework.beans.testfixture.beans.factory.annotation.subpkg.PackagePrivateFieldInjectionFromParentSample; +import org.springframework.beans.testfixture.beans.factory.annotation.subpkg.PackagePrivateMethodInjectionFromParentSample; +import org.springframework.beans.testfixture.beans.factory.aot.MockBeanRegistrationCode; +import org.springframework.core.env.Environment; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.test.tools.CompileWithForkedClassLoader; +import org.springframework.core.test.tools.Compiled; +import org.springframework.core.test.tools.SourceFile; +import org.springframework.core.test.tools.TestCompiler; +import org.springframework.javapoet.CodeBlock; +import org.springframework.javapoet.MethodSpec; +import org.springframework.javapoet.ParameterizedTypeName; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link AutowiredAnnotationBeanPostProcessor} for AOT contributions. + * + * @author Phillip Webb + * @author Stephane Nicoll + * @since 6.0 + */ +class AutowiredAnnotationBeanRegistrationAotContributionTests { + + private final TestGenerationContext generationContext; + + private final MockBeanRegistrationCode beanRegistrationCode; + + private final DefaultListableBeanFactory beanFactory; + + + AutowiredAnnotationBeanRegistrationAotContributionTests() { + this.generationContext = new TestGenerationContext(); + this.beanRegistrationCode = new MockBeanRegistrationCode(this.generationContext); + this.beanFactory = new DefaultListableBeanFactory(); + } + + + @Test + void contributeWhenPrivateFieldInjectionInjectsUsingReflection() { + Environment environment = new StandardEnvironment(); + this.beanFactory.registerSingleton("environment", environment); + RegisteredBean registeredBean = getAndApplyContribution( + PrivateFieldInjectionSample.class); + assertThat(RuntimeHintsPredicates.reflection() + .onField(PrivateFieldInjectionSample.class, "environment")) + .accepts(this.generationContext.getRuntimeHints()); + compile(registeredBean, (postProcessor, compiled) -> { + PrivateFieldInjectionSample instance = new PrivateFieldInjectionSample(); + postProcessor.apply(registeredBean, instance); + assertThat(instance).extracting("environment").isSameAs(environment); + assertThat(getSourceFile(compiled, PrivateFieldInjectionSample.class)) + .contains("resolveAndSet("); + }); + } + + @Test + @CompileWithForkedClassLoader + void contributeWhenPackagePrivateFieldInjectionInjectsUsingConsumer() { + Environment environment = new StandardEnvironment(); + this.beanFactory.registerSingleton("environment", environment); + RegisteredBean registeredBean = getAndApplyContribution( + PackagePrivateFieldInjectionSample.class); + assertThat(RuntimeHintsPredicates.reflection() + .onField(PackagePrivateFieldInjectionSample.class, "environment")) + .accepts(this.generationContext.getRuntimeHints()); + compile(registeredBean, (postProcessor, compiled) -> { + PackagePrivateFieldInjectionSample instance = new PackagePrivateFieldInjectionSample(); + postProcessor.apply(registeredBean, instance); + assertThat(instance).extracting("environment").isSameAs(environment); + assertThat(getSourceFile(compiled, PackagePrivateFieldInjectionSample.class)) + .contains("instance.environment ="); + }); + } + + @Test + @CompileWithForkedClassLoader + void contributeWhenPackagePrivateFieldInjectionOnParentClassInjectsUsingReflection() { + Environment environment = new StandardEnvironment(); + this.beanFactory.registerSingleton("environment", environment); + RegisteredBean registeredBean = getAndApplyContribution( + PackagePrivateFieldInjectionFromParentSample.class); + assertThat(RuntimeHintsPredicates.reflection() + .onField(PackagePrivateFieldInjectionSample.class, "environment")) + .accepts(this.generationContext.getRuntimeHints()); + compile(registeredBean, (postProcessor, compiled) -> { + PackagePrivateFieldInjectionFromParentSample instance = new PackagePrivateFieldInjectionFromParentSample(); + postProcessor.apply(registeredBean, instance); + assertThat(instance).extracting("environment").isSameAs(environment); + assertThat(getSourceFile(compiled, PackagePrivateFieldInjectionFromParentSample.class)) + .contains("resolveAndSet"); + }); + } + + @Test + void contributeWhenPrivateMethodInjectionInjectsUsingReflection() { + Environment environment = new StandardEnvironment(); + this.beanFactory.registerSingleton("environment", environment); + RegisteredBean registeredBean = getAndApplyContribution( + PrivateMethodInjectionSample.class); + assertThat(RuntimeHintsPredicates.reflection() + .onMethod(PrivateMethodInjectionSample.class, "setTestBean").invoke()) + .accepts(this.generationContext.getRuntimeHints()); + compile(registeredBean, (postProcessor, compiled) -> { + PrivateMethodInjectionSample instance = new PrivateMethodInjectionSample(); + postProcessor.apply(registeredBean, instance); + assertThat(instance).extracting("environment").isSameAs(environment); + assertThat(getSourceFile(compiled, PrivateMethodInjectionSample.class)) + .contains("resolveAndInvoke("); + }); + } + + @Test + @CompileWithForkedClassLoader + void contributeWhenPackagePrivateMethodInjectionInjectsUsingConsumer() { + Environment environment = new StandardEnvironment(); + this.beanFactory.registerSingleton("environment", environment); + RegisteredBean registeredBean = getAndApplyContribution( + PackagePrivateMethodInjectionSample.class); + assertThat(RuntimeHintsPredicates.reflection() + .onMethod(PackagePrivateMethodInjectionSample.class, "setTestBean").introspect()) + .accepts(this.generationContext.getRuntimeHints()); + compile(registeredBean, (postProcessor, compiled) -> { + PackagePrivateMethodInjectionSample instance = new PackagePrivateMethodInjectionSample(); + postProcessor.apply(registeredBean, instance); + assertThat(instance.environment).isSameAs(environment); + assertThat(getSourceFile(compiled, PackagePrivateMethodInjectionSample.class)) + .contains("args -> instance.setTestBean("); + }); + } + + @Test + @CompileWithForkedClassLoader + void contributeWhenPackagePrivateMethodInjectionOnParentClassInjectsUsingReflection() { + Environment environment = new StandardEnvironment(); + this.beanFactory.registerSingleton("environment", environment); + RegisteredBean registeredBean = getAndApplyContribution( + PackagePrivateMethodInjectionFromParentSample.class); + assertThat(RuntimeHintsPredicates.reflection() + .onMethod(PackagePrivateMethodInjectionSample.class, "setTestBean")) + .accepts(this.generationContext.getRuntimeHints()); + compile(registeredBean, (postProcessor, compiled) -> { + PackagePrivateMethodInjectionFromParentSample instance = new PackagePrivateMethodInjectionFromParentSample(); + postProcessor.apply(registeredBean, instance); + assertThat(instance.environment).isSameAs(environment); + assertThat(getSourceFile(compiled, PackagePrivateMethodInjectionFromParentSample.class)) + .contains("resolveAndInvoke("); + }); + } + + private RegisteredBean getAndApplyContribution(Class beanClass) { + RegisteredBean registeredBean = registerBean(beanClass); + BeanRegistrationAotContribution contribution = new AutowiredAnnotationBeanPostProcessor() + .processAheadOfTime(registeredBean); + assertThat(contribution).isNotNull(); + contribution.applyTo(this.generationContext, this.beanRegistrationCode); + return registeredBean; + } + + private RegisteredBean registerBean(Class beanClass) { + String beanName = "testBean"; + this.beanFactory.registerBeanDefinition(beanName, + new RootBeanDefinition(beanClass)); + return RegisteredBean.of(this.beanFactory, beanName); + } + + private static SourceFile getSourceFile(Compiled compiled, Class sample) { + return compiled.getSourceFileFromPackage(sample.getPackageName()); + } + + @SuppressWarnings("unchecked") + private void compile(RegisteredBean registeredBean, + BiConsumer, Compiled> result) { + Class target = registeredBean.getBeanClass(); + MethodReference methodReference = this.beanRegistrationCode.getInstancePostProcessors().get(0); + this.beanRegistrationCode.getTypeBuilder().set(type -> { + CodeBlock methodInvocation = methodReference.toInvokeCodeBlock( + ArgumentCodeGenerator.of(RegisteredBean.class, "registeredBean").and(target, "instance"), + this.beanRegistrationCode.getClassName()); + type.addModifiers(Modifier.PUBLIC); + type.addSuperinterface(ParameterizedTypeName.get(BiFunction.class, RegisteredBean.class, target, target)); + type.addMethod(MethodSpec.methodBuilder("apply") + .addModifiers(Modifier.PUBLIC) + .addParameter(RegisteredBean.class, "registeredBean") + .addParameter(target, "instance").returns(target) + .addStatement("return $L", methodInvocation) + .build()); + + }); + this.generationContext.writeGeneratedContent(); + TestCompiler.forSystem().with(this.generationContext).compile(compiled -> + result.accept(compiled.getInstance(BiFunction.class), compiled)); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/InitDestroyAnnotationBeanPostProcessorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/InitDestroyAnnotationBeanPostProcessorTests.java new file mode 100644 index 000000000000..9856e9d1cf9c --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/InitDestroyAnnotationBeanPostProcessorTests.java @@ -0,0 +1,136 @@ +/* + * Copyright 2002-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.beans.factory.annotation; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.factory.generator.lifecycle.Destroy; +import org.springframework.beans.testfixture.beans.factory.generator.lifecycle.InferredDestroyBean; +import org.springframework.beans.testfixture.beans.factory.generator.lifecycle.Init; +import org.springframework.beans.testfixture.beans.factory.generator.lifecycle.InitDestroyBean; +import org.springframework.beans.testfixture.beans.factory.generator.lifecycle.MultiInitDestroyBean; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link InitDestroyAnnotationBeanPostProcessor}. + * + * @author Stephane Nicoll + * @author Phillip Webb + */ +class InitDestroyAnnotationBeanPostProcessorTests { + + private final DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + + @Test + void processAheadOfTimeWhenNoCallbackDoesNotMutateRootBeanDefinition() { + RootBeanDefinition beanDefinition = new RootBeanDefinition(NoInitDestroyBean.class); + processAheadOfTime(beanDefinition); + RootBeanDefinition mergedBeanDefinition = getMergedBeanDefinition(); + assertThat(mergedBeanDefinition.getInitMethodNames()).isNull(); + assertThat(mergedBeanDefinition.getDestroyMethodNames()).isNull(); + } + + @Test + void processAheadOfTimeWhenHasInitDestroyAnnotationsAddsMethodNames() { + RootBeanDefinition beanDefinition = new RootBeanDefinition(InitDestroyBean.class); + processAheadOfTime(beanDefinition); + RootBeanDefinition mergedBeanDefinition = getMergedBeanDefinition(); + assertThat(mergedBeanDefinition.getInitMethodNames()).containsExactly("initMethod"); + assertThat(mergedBeanDefinition.getDestroyMethodNames()).containsExactly("destroyMethod"); + } + + @Test + void processAheadOfTimeWhenHasInitDestroyAnnotationsAndCustomDefinedMethodNamesAddsMethodNames() { + RootBeanDefinition beanDefinition = new RootBeanDefinition(InitDestroyBean.class); + beanDefinition.setInitMethodName("customInitMethod"); + beanDefinition.setDestroyMethodNames("customDestroyMethod"); + processAheadOfTime(beanDefinition); + RootBeanDefinition mergedBeanDefinition = getMergedBeanDefinition(); + assertThat(mergedBeanDefinition.getInitMethodNames()).containsExactly("customInitMethod", "initMethod"); + assertThat(mergedBeanDefinition.getDestroyMethodNames()).containsExactly("customDestroyMethod", "destroyMethod"); + } + + @Test + void processAheadOfTimeWhenHasInitDestroyAnnotationsAndOverlappingCustomDefinedMethodNamesFiltersDuplicates() { + RootBeanDefinition beanDefinition = new RootBeanDefinition(InitDestroyBean.class); + beanDefinition.setInitMethodName("initMethod"); + beanDefinition.setDestroyMethodNames("destroyMethod"); + processAheadOfTime(beanDefinition); + RootBeanDefinition mergedBeanDefinition = getMergedBeanDefinition(); + assertThat(mergedBeanDefinition.getInitMethodNames()).containsExactly("initMethod"); + assertThat(mergedBeanDefinition.getDestroyMethodNames()).containsExactly("destroyMethod"); + } + + @Test + void processAheadOfTimeWhenHasInferredDestroyMethodAddsDestroyMethodName() { + RootBeanDefinition beanDefinition = new RootBeanDefinition(InferredDestroyBean.class); + beanDefinition.setDestroyMethodNames(AbstractBeanDefinition.INFER_METHOD); + processAheadOfTime(beanDefinition); + RootBeanDefinition mergedBeanDefinition = getMergedBeanDefinition(); + assertThat(mergedBeanDefinition.getInitMethodNames()).isNull(); + assertThat(mergedBeanDefinition.getDestroyMethodNames()).containsExactly("close"); + } + + @Test + void processAheadOfTimeWhenHasInferredDestroyMethodAndNoCandidateDoesNotMutateRootBeanDefinition() { + RootBeanDefinition beanDefinition = new RootBeanDefinition(NoInitDestroyBean.class); + beanDefinition.setDestroyMethodNames(AbstractBeanDefinition.INFER_METHOD); + processAheadOfTime(beanDefinition); + RootBeanDefinition mergedBeanDefinition = getMergedBeanDefinition(); + assertThat(mergedBeanDefinition.getInitMethodNames()).isNull(); + assertThat(mergedBeanDefinition.getDestroyMethodNames()).isNull(); + } + + @Test + void processAheadOfTimeWhenHasMultipleInitDestroyAnnotationsAddsAllMethodNames() { + RootBeanDefinition beanDefinition = new RootBeanDefinition(MultiInitDestroyBean.class); + processAheadOfTime(beanDefinition); + RootBeanDefinition mergedBeanDefinition = getMergedBeanDefinition(); + assertThat(mergedBeanDefinition.getInitMethodNames()).containsExactly("initMethod", "anotherInitMethod"); + assertThat(mergedBeanDefinition.getDestroyMethodNames()).containsExactly("anotherDestroyMethod", "destroyMethod"); + } + + private void processAheadOfTime(RootBeanDefinition beanDefinition) { + RegisteredBean registeredBean = registerBean(beanDefinition); + assertThat(createAotBeanPostProcessor().processAheadOfTime(registeredBean)).isNull(); + } + + private RegisteredBean registerBean(RootBeanDefinition beanDefinition) { + String beanName = "test"; + this.beanFactory.registerBeanDefinition(beanName, beanDefinition); + return RegisteredBean.of(this.beanFactory, beanName); + } + + private RootBeanDefinition getMergedBeanDefinition() { + return (RootBeanDefinition) this.beanFactory.getMergedBeanDefinition("test"); + } + + private InitDestroyAnnotationBeanPostProcessor createAotBeanPostProcessor() { + InitDestroyAnnotationBeanPostProcessor beanPostProcessor = new InitDestroyAnnotationBeanPostProcessor(); + beanPostProcessor.setInitAnnotationType(Init.class); + beanPostProcessor.setDestroyAnnotationType(Destroy.class); + return beanPostProcessor; + } + + static class NoInitDestroyBean {} + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/InjectAnnotationBeanPostProcessorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/InjectAnnotationBeanPostProcessorTests.java index aa4c1cb20bff..079f39556036 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/InjectAnnotationBeanPostProcessorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/InjectAnnotationBeanPostProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -23,10 +23,9 @@ import java.util.Map; import java.util.Optional; -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Provider; - +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Provider; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -51,7 +50,7 @@ /** * Unit tests for {@link org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor} - * processing the JSR-330 {@link javax.inject.Inject} annotation. + * processing the JSR-330 {@link jakarta.inject.Inject} annotation. * * @author Juergen Hoeller * @since 3.0 @@ -155,27 +154,6 @@ public void testExtendedResourceInjectionWithOverriding() { assertThat(bean.getBeanFactory()).isSameAs(bf); } - @Test - @SuppressWarnings("deprecation") - public void testExtendedResourceInjectionWithAtRequired() { - bf.addBeanPostProcessor(new RequiredAnnotationBeanPostProcessor()); - RootBeanDefinition bd = new RootBeanDefinition(TypedExtendedResourceInjectionBean.class); - bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); - bf.registerBeanDefinition("annotatedBean", bd); - TestBean tb = new TestBean(); - bf.registerSingleton("testBean", tb); - NestedTestBean ntb = new NestedTestBean(); - bf.registerSingleton("nestedTestBean", ntb); - - TypedExtendedResourceInjectionBean bean = (TypedExtendedResourceInjectionBean) bf.getBean("annotatedBean"); - assertThat(bean.getTestBean()).isSameAs(tb); - assertThat(bean.getTestBean2()).isSameAs(tb); - assertThat(bean.getTestBean3()).isSameAs(tb); - assertThat(bean.getTestBean4()).isSameAs(tb); - assertThat(bean.getNestedTestBean()).isSameAs(ntb); - assertThat(bean.getBeanFactory()).isSameAs(bf); - } - @Test public void testConstructorResourceInjection() { RootBeanDefinition bd = new RootBeanDefinition(ConstructorResourceInjectionBean.class); @@ -667,7 +645,6 @@ public ExtendedResourceInjectionBean() { @Override @Inject - @Required @SuppressWarnings("deprecation") public void setTestBean2(TestBean testBean2) { super.setTestBean2(testBean2); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/JakartaAnnotationsRuntimeHintsTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/JakartaAnnotationsRuntimeHintsTests.java new file mode 100644 index 000000000000..d56b851f93f3 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/JakartaAnnotationsRuntimeHintsTests.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-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.beans.factory.annotation; + + +import jakarta.inject.Inject; +import jakarta.inject.Qualifier; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; +import org.springframework.beans.factory.aot.AotServices; +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link JakartaAnnotationsRuntimeHints}. + * + * @author Brian Clozel + */ +class JakartaAnnotationsRuntimeHintsTests { + + private final RuntimeHints hints = new RuntimeHints(); + + @BeforeEach + void setup() { + AotServices.factories().load(RuntimeHintsRegistrar.class) + .forEach(registrar -> registrar.registerHints(this.hints, + ClassUtils.getDefaultClassLoader())); + } + + @Test + void jakartaInjectAnnotationHasHints() { + assertThat(RuntimeHintsPredicates.reflection().onType(Inject.class)).accepts(this.hints); + } + + @Test + void jakartaQualifierAnnotationHasHints() { + assertThat(RuntimeHintsPredicates.reflection().onType(Qualifier.class)).accepts(this.hints); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/LookupAnnotationTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/LookupAnnotationTests.java index bd30b5b44c24..afa61bd9a605 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/LookupAnnotationTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/LookupAnnotationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -121,6 +121,18 @@ public void testWithNullBean() { assertThat(beanFactory.getBean(BeanConsumer.class).abstractBean).isSameAs(bean); } + @Test + public void testWithGenericBean() { + beanFactory.registerBeanDefinition("numberBean", new RootBeanDefinition(NumberBean.class)); + beanFactory.registerBeanDefinition("doubleStore", new RootBeanDefinition(DoubleStore.class)); + beanFactory.registerBeanDefinition("floatStore", new RootBeanDefinition(FloatStore.class)); + + NumberBean bean = (NumberBean) beanFactory.getBean("numberBean"); + assertThat(bean).isNotNull(); + assertThat(beanFactory.getBean(DoubleStore.class)).isSameAs(bean.getDoubleStore()); + assertThat(beanFactory.getBean(FloatStore.class)).isSameAs(bean.getFloatStore()); + } + public static abstract class AbstractBean { @@ -147,4 +159,26 @@ public static class BeanConsumer { AbstractBean abstractBean; } + + public static class NumberStore { + } + + + public static class DoubleStore extends NumberStore { + } + + + public static class FloatStore extends NumberStore { + } + + + public static abstract class NumberBean { + + @Lookup + public abstract NumberStore getDoubleStore(); + + @Lookup + public abstract NumberStore getFloatStore(); + } + } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/RequiredAnnotationBeanPostProcessorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/RequiredAnnotationBeanPostProcessorTests.java deleted file mode 100644 index a88a756029b8..000000000000 --- a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/RequiredAnnotationBeanPostProcessorTests.java +++ /dev/null @@ -1,244 +0,0 @@ -/* - * Copyright 2002-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.beans.factory.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.BeanCreationException; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.BeanFactoryAware; -import org.springframework.beans.factory.BeanNameAware; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.support.BeanDefinitionBuilder; -import org.springframework.beans.factory.support.DefaultListableBeanFactory; -import org.springframework.beans.factory.support.RootBeanDefinition; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - -/** - * @author Rob Harrop - * @author Chris Beams - * @since 2.0 - */ -@Deprecated -public class RequiredAnnotationBeanPostProcessorTests { - - @Test - public void testWithRequiredPropertyOmitted() { - DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); - BeanDefinition beanDef = BeanDefinitionBuilder - .genericBeanDefinition(RequiredTestBean.class) - .addPropertyValue("name", "Rob Harrop") - .addPropertyValue("favouriteColour", "Blue") - .addPropertyValue("jobTitle", "Grand Poobah") - .getBeanDefinition(); - factory.registerBeanDefinition("testBean", beanDef); - factory.addBeanPostProcessor(new RequiredAnnotationBeanPostProcessor()); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy( - factory::preInstantiateSingletons) - .withMessageContaining("Property") - .withMessageContaining("age") - .withMessageContaining("testBean"); - } - - @Test - public void testWithThreeRequiredPropertiesOmitted() { - DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); - BeanDefinition beanDef = BeanDefinitionBuilder - .genericBeanDefinition(RequiredTestBean.class) - .addPropertyValue("name", "Rob Harrop") - .getBeanDefinition(); - factory.registerBeanDefinition("testBean", beanDef); - factory.addBeanPostProcessor(new RequiredAnnotationBeanPostProcessor()); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy( - factory::preInstantiateSingletons) - .withMessageContaining("Properties") - .withMessageContaining("age") - .withMessageContaining("favouriteColour") - .withMessageContaining("jobTitle") - .withMessageContaining("testBean"); - } - - @Test - public void testWithAllRequiredPropertiesSpecified() { - DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); - BeanDefinition beanDef = BeanDefinitionBuilder - .genericBeanDefinition(RequiredTestBean.class) - .addPropertyValue("age", "24") - .addPropertyValue("favouriteColour", "Blue") - .addPropertyValue("jobTitle", "Grand Poobah") - .getBeanDefinition(); - factory.registerBeanDefinition("testBean", beanDef); - factory.addBeanPostProcessor(new RequiredAnnotationBeanPostProcessor()); - factory.preInstantiateSingletons(); - RequiredTestBean bean = (RequiredTestBean) factory.getBean("testBean"); - assertThat(bean.getAge()).isEqualTo(24); - assertThat(bean.getFavouriteColour()).isEqualTo("Blue"); - } - - @Test - public void testWithCustomAnnotation() { - DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); - BeanDefinition beanDef = BeanDefinitionBuilder - .genericBeanDefinition(RequiredTestBean.class) - .getBeanDefinition(); - factory.registerBeanDefinition("testBean", beanDef); - RequiredAnnotationBeanPostProcessor rabpp = new RequiredAnnotationBeanPostProcessor(); - rabpp.setRequiredAnnotationType(MyRequired.class); - factory.addBeanPostProcessor(rabpp); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy( - factory::preInstantiateSingletons) - .withMessageContaining("Property") - .withMessageContaining("name") - .withMessageContaining("testBean"); - } - - @Test - public void testWithStaticFactoryMethod() { - DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); - BeanDefinition beanDef = BeanDefinitionBuilder - .genericBeanDefinition(RequiredTestBean.class) - .setFactoryMethod("create") - .addPropertyValue("name", "Rob Harrop") - .addPropertyValue("favouriteColour", "Blue") - .addPropertyValue("jobTitle", "Grand Poobah") - .getBeanDefinition(); - factory.registerBeanDefinition("testBean", beanDef); - factory.addBeanPostProcessor(new RequiredAnnotationBeanPostProcessor()); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy( - factory::preInstantiateSingletons) - .withMessageContaining("Property") - .withMessageContaining("age") - .withMessageContaining("testBean"); - } - - @Test - public void testWithStaticFactoryMethodAndRequiredPropertiesSpecified() { - DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); - BeanDefinition beanDef = BeanDefinitionBuilder - .genericBeanDefinition(RequiredTestBean.class) - .setFactoryMethod("create") - .addPropertyValue("age", "24") - .addPropertyValue("favouriteColour", "Blue") - .addPropertyValue("jobTitle", "Grand Poobah") - .getBeanDefinition(); - factory.registerBeanDefinition("testBean", beanDef); - factory.addBeanPostProcessor(new RequiredAnnotationBeanPostProcessor()); - factory.preInstantiateSingletons(); - RequiredTestBean bean = (RequiredTestBean) factory.getBean("testBean"); - assertThat(bean.getAge()).isEqualTo(24); - assertThat(bean.getFavouriteColour()).isEqualTo("Blue"); - } - - @Test - public void testWithFactoryBean() { - DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); - RootBeanDefinition beanDef = new RootBeanDefinition(RequiredTestBean.class); - beanDef.setFactoryBeanName("testBeanFactory"); - beanDef.setFactoryMethodName("create"); - factory.registerBeanDefinition("testBean", beanDef); - factory.registerBeanDefinition("testBeanFactory", new RootBeanDefinition(RequiredTestBeanFactory.class)); - RequiredAnnotationBeanPostProcessor bpp = new RequiredAnnotationBeanPostProcessor(); - bpp.setBeanFactory(factory); - factory.addBeanPostProcessor(bpp); - factory.preInstantiateSingletons(); - } - - - @Retention(RetentionPolicy.RUNTIME) - @Target(ElementType.METHOD) - public @interface MyRequired { - } - - - public static class RequiredTestBean implements BeanNameAware, BeanFactoryAware { - - private String name; - - private int age; - - private String favouriteColour; - - private String jobTitle; - - - public int getAge() { - return age; - } - - @Required - public void setAge(int age) { - this.age = age; - } - - public String getName() { - return name; - } - - @MyRequired - public void setName(String name) { - this.name = name; - } - - public String getFavouriteColour() { - return favouriteColour; - } - - @Required - public void setFavouriteColour(String favouriteColour) { - this.favouriteColour = favouriteColour; - } - - public String getJobTitle() { - return jobTitle; - } - - @Required - public void setJobTitle(String jobTitle) { - this.jobTitle = jobTitle; - } - - @Override - @Required - public void setBeanName(String name) { - } - - @Override - @Required - public void setBeanFactory(BeanFactory beanFactory) { - } - - public static RequiredTestBean create() { - return new RequiredTestBean(); - } - } - - - public static class RequiredTestBeanFactory { - - public RequiredTestBean create() { - return new RequiredTestBean(); - } - } - -} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/AotServicesTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/AotServicesTests.java new file mode 100644 index 000000000000..f1098d11b845 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/AotServicesTests.java @@ -0,0 +1,249 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +import java.io.IOException; +import java.net.URL; +import java.util.Enumeration; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.aot.AotServices.Source; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.core.Ordered; +import org.springframework.core.io.support.SpringFactoriesLoader; +import org.springframework.core.test.io.support.MockSpringFactoriesLoader; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link AotServices}. + * + * @author Phillip Webb + */ +class AotServicesTests { + + @Test + void factoriesLoadsFromAotFactoriesFiles() { + AotServices loaded = AotServices.factories() + .load(BeanFactoryInitializationAotProcessor.class); + assertThat(loaded) + .anyMatch(BeanFactoryInitializationAotProcessor.class::isInstance); + } + + @Test + void factoriesWithClassLoaderLoadsFromAotFactoriesFile() { + TestSpringFactoriesClassLoader classLoader = new TestSpringFactoriesClassLoader( + "aot-services.factories"); + AotServices loaded = AotServices.factories(classLoader) + .load(TestService.class); + assertThat(loaded).anyMatch(TestServiceImpl.class::isInstance); + } + + @Test + void factoriesWithSpringFactoriesLoaderWhenSpringFactoriesLoaderIsNullThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> AotServices.factories((SpringFactoriesLoader) null)) + .withMessage("'springFactoriesLoader' must not be null"); + } + + @Test + void factoriesWithSpringFactoriesLoaderLoadsFromSpringFactoriesLoader() { + MockSpringFactoriesLoader loader = new MockSpringFactoriesLoader(); + loader.addInstance(TestService.class, new TestServiceImpl()); + AotServices loaded = AotServices.factories(loader).load(TestService.class); + assertThat(loaded).anyMatch(TestServiceImpl.class::isInstance); + } + + @Test + void factoriesAndBeansWhenBeanFactoryIsNullThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> AotServices.factoriesAndBeans(null)) + .withMessage("'beanFactory' must not be null"); + } + + @Test + void factoriesAndBeansLoadsFromFactoriesAndBeanFactory() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.setBeanClassLoader( + new TestSpringFactoriesClassLoader("aot-services.factories")); + beanFactory.registerBeanDefinition("test", new RootBeanDefinition(TestBean.class)); + AotServices loaded = AotServices.factoriesAndBeans(beanFactory).load(TestService.class); + assertThat(loaded).anyMatch(TestServiceImpl.class::isInstance); + assertThat(loaded).anyMatch(TestBean.class::isInstance); + } + + @Test + void factoriesAndBeansWithSpringFactoriesLoaderLoadsFromSpringFactoriesLoaderAndBeanFactory() { + MockSpringFactoriesLoader loader = new MockSpringFactoriesLoader(); + loader.addInstance(TestService.class, new TestServiceImpl()); + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerBeanDefinition("test", new RootBeanDefinition(TestBean.class)); + AotServices loaded = AotServices.factoriesAndBeans(loader, beanFactory).load(TestService.class); + assertThat(loaded).anyMatch(TestServiceImpl.class::isInstance); + assertThat(loaded).anyMatch(TestBean.class::isInstance); + } + + @Test + void factoriesAndBeansWithSpringFactoriesLoaderWhenSpringFactoriesLoaderIsNullThrowsException() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + assertThatIllegalArgumentException() + .isThrownBy(() -> AotServices.factoriesAndBeans(null, beanFactory)) + .withMessage("'springFactoriesLoader' must not be null"); + } + + @Test + void iteratorReturnsServicesIterator() { + AotServices loaded = AotServices + .factories(new TestSpringFactoriesClassLoader("aot-services.factories")) + .load(TestService.class); + assertThat(loaded.iterator().next()).isInstanceOf(TestServiceImpl.class); + } + + @Test + void streamReturnsServicesStream() { + AotServices loaded = AotServices + .factories(new TestSpringFactoriesClassLoader("aot-services.factories")) + .load(TestService.class); + assertThat(loaded.stream()).anyMatch(TestServiceImpl.class::isInstance); + } + + @Test + void asListReturnsServicesList() { + AotServices loaded = AotServices + .factories(new TestSpringFactoriesClassLoader("aot-services.factories")) + .load(TestService.class); + assertThat(loaded.asList()).anyMatch(TestServiceImpl.class::isInstance); + } + + @Test + void findByBeanNameWhenMatchReturnsService() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerBeanDefinition("test", new RootBeanDefinition(TestBean.class)); + AotServices loaded = AotServices.factoriesAndBeans(beanFactory).load(TestService.class); + assertThat(loaded.findByBeanName("test")).isInstanceOf(TestBean.class); + } + + @Test + void findByBeanNameWhenNoMatchReturnsNull() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerBeanDefinition("test", new RootBeanDefinition(TestBean.class)); + AotServices loaded = AotServices.factoriesAndBeans(beanFactory).load(TestService.class); + assertThat(loaded.findByBeanName("missing")).isNull(); + } + + @Test + void loadLoadsFromBeanFactoryAndSpringFactoriesLoaderInOrder() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerSingleton("b1", new TestServiceImpl(0, "b1")); + beanFactory.registerSingleton("b2", new TestServiceImpl(2, "b2")); + MockSpringFactoriesLoader springFactoriesLoader = new MockSpringFactoriesLoader(); + springFactoriesLoader.addInstance(TestService.class, + new TestServiceImpl(1, "l1")); + springFactoriesLoader.addInstance(TestService.class, + new TestServiceImpl(3, "l2")); + Iterable loaded = AotServices + .factoriesAndBeans(springFactoriesLoader, beanFactory) + .load(TestService.class); + assertThat(loaded).map(Object::toString).containsExactly("b1", "l1", "b2", "l2"); + } + + @Test + void getSourceReturnsSource() { + MockSpringFactoriesLoader loader = new MockSpringFactoriesLoader(); + loader.addInstance(TestService.class, new TestServiceImpl()); + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerBeanDefinition("test", new RootBeanDefinition(TestBean.class)); + AotServices loaded = AotServices.factoriesAndBeans(loader, beanFactory).load(TestService.class); + assertThat(loaded.getSource(loaded.asList().get(0))).isEqualTo(Source.SPRING_FACTORIES_LOADER); + assertThat(loaded.getSource(loaded.asList().get(1))).isEqualTo(Source.BEAN_FACTORY); + TestService missing = mock(TestService.class); + assertThatIllegalStateException().isThrownBy(()->loaded.getSource(missing)); + } + + @Test + void getSourceWhenMissingThrowsException() { + AotServices loaded = AotServices.factories().load(TestService.class); + TestService missing = mock(TestService.class); + assertThatIllegalStateException().isThrownBy(()->loaded.getSource(missing)); + } + + interface TestService { + } + + + static class TestServiceImpl implements TestService, Ordered { + + private final int order; + + private final String name; + + + TestServiceImpl() { + this(0, "test"); + } + + TestServiceImpl(int order, String name) { + this.order = order; + this.name = name; + } + + + @Override + public int getOrder() { + return this.order; + } + + @Override + public String toString() { + return this.name; + } + + } + + + static class TestBean implements TestService { + + } + + + static class TestSpringFactoriesClassLoader extends ClassLoader { + + private final String factoriesName; + + + TestSpringFactoriesClassLoader(String factoriesName) { + super(Thread.currentThread().getContextClassLoader()); + this.factoriesName = factoriesName; + } + + + @Override + public Enumeration getResources(String name) throws IOException { + return (!"META-INF/spring/aot.factories".equals(name)) + ? super.getResources(name) + : super.getResources("org/springframework/beans/factory/aot/" + + this.factoriesName); + } + + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/AutowiredArgumentsCodeGeneratorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/AutowiredArgumentsCodeGeneratorTests.java new file mode 100644 index 000000000000..7f35baca3231 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/AutowiredArgumentsCodeGeneratorTests.java @@ -0,0 +1,200 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; + +import org.junit.jupiter.api.Test; + +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link AutowiredArgumentsCodeGenerator}. + * + * @author Phillip Webb + * @author Stephane Nicoll + */ +class AutowiredArgumentsCodeGeneratorTests { + + @Test + void generateCodeWhenNoArguments() { + Method method = ReflectionUtils.findMethod(UnambiguousMethods.class, "zero"); + AutowiredArgumentsCodeGenerator generator = new AutowiredArgumentsCodeGenerator( + UnambiguousMethods.class, method); + assertThat(generator.generateCode(method.getParameterTypes())).hasToString(""); + } + + @Test + void generatedCodeWhenSingleArgument() { + Method method = ReflectionUtils.findMethod(UnambiguousMethods.class, "one", + String.class); + AutowiredArgumentsCodeGenerator generator = new AutowiredArgumentsCodeGenerator( + UnambiguousMethods.class, method); + assertThat(generator.generateCode(method.getParameterTypes())) + .hasToString("args.get(0)"); + } + + @Test + void generateCodeWhenMultipleArguments() { + Method method = ReflectionUtils.findMethod(UnambiguousMethods.class, "three", + String.class, Integer.class, Boolean.class); + AutowiredArgumentsCodeGenerator generator = new AutowiredArgumentsCodeGenerator( + UnambiguousMethods.class, method); + assertThat(generator.generateCode(method.getParameterTypes())) + .hasToString("args.get(0), args.get(1), args.get(2)"); + } + + @Test + void generateCodeWhenMultipleArgumentsWithOffset() { + Constructor constructor = Outer.Nested.class.getDeclaredConstructors()[0]; + AutowiredArgumentsCodeGenerator generator = new AutowiredArgumentsCodeGenerator( + Outer.Nested.class, constructor); + assertThat(generator.generateCode(constructor.getParameterTypes(), 1)) + .hasToString("args.get(0), args.get(1)"); + } + + @Test + void generateCodeWhenAmbiguousConstructor() throws Exception { + Constructor constructor = AmbiguousConstructors.class + .getDeclaredConstructor(String.class, Integer.class); + AutowiredArgumentsCodeGenerator generator = new AutowiredArgumentsCodeGenerator( + AmbiguousConstructors.class, constructor); + assertThat(generator.generateCode(constructor.getParameterTypes())).hasToString( + "args.get(0, java.lang.String.class), args.get(1, java.lang.Integer.class)"); + } + + @Test + void generateCodeWhenUnambiguousConstructor() throws Exception { + Constructor constructor = UnambiguousConstructors.class + .getDeclaredConstructor(String.class, Integer.class); + AutowiredArgumentsCodeGenerator generator = new AutowiredArgumentsCodeGenerator( + UnambiguousConstructors.class, constructor); + assertThat(generator.generateCode(constructor.getParameterTypes())) + .hasToString("args.get(0), args.get(1)"); + } + + @Test + void generateCodeWhenAmbiguousMethod() { + Method method = ReflectionUtils.findMethod(AmbiguousMethods.class, "two", + String.class, Integer.class); + AutowiredArgumentsCodeGenerator generator = new AutowiredArgumentsCodeGenerator( + AmbiguousMethods.class, method); + assertThat(generator.generateCode(method.getParameterTypes())).hasToString( + "args.get(0, java.lang.String.class), args.get(1, java.lang.Integer.class)"); + } + + @Test + void generateCodeWhenAmbiguousSubclassMethod() { + Method method = ReflectionUtils.findMethod(UnambiguousMethods.class, "two", + String.class, Integer.class); + AutowiredArgumentsCodeGenerator generator = new AutowiredArgumentsCodeGenerator( + AmbiguousSubclassMethods.class, method); + assertThat(generator.generateCode(method.getParameterTypes())).hasToString( + "args.get(0, java.lang.String.class), args.get(1, java.lang.Integer.class)"); + } + + @Test + void generateCodeWhenUnambiguousMethod() { + Method method = ReflectionUtils.findMethod(UnambiguousMethods.class, "two", + String.class, Integer.class); + AutowiredArgumentsCodeGenerator generator = new AutowiredArgumentsCodeGenerator( + UnambiguousMethods.class, method); + assertThat(generator.generateCode(method.getParameterTypes())) + .hasToString("args.get(0), args.get(1)"); + } + + @Test + void generateCodeWithCustomArgVariable() { + Method method = ReflectionUtils.findMethod(UnambiguousMethods.class, "one", + String.class); + AutowiredArgumentsCodeGenerator generator = new AutowiredArgumentsCodeGenerator( + UnambiguousMethods.class, method); + assertThat(generator.generateCode(method.getParameterTypes(), 0, "objs")) + .hasToString("objs.get(0)"); + } + + static class Outer { + + class Nested { + + Nested(String a, Integer b) { + } + + } + + } + + static class UnambiguousMethods { + + void zero() { + } + + void one(String a) { + } + + void two(String a, Integer b) { + } + + void three(String a, Integer b, Boolean c) { + } + + } + + static class AmbiguousMethods { + + void two(String a, Integer b) { + } + + void two(Integer b, String a) { + } + + } + + static class AmbiguousSubclassMethods extends UnambiguousMethods { + + void two(Integer a, String b) { + } + + } + + static class UnambiguousConstructors { + + UnambiguousConstructors() { + } + + UnambiguousConstructors(String a) { + } + + UnambiguousConstructors(String a, Integer b) { + } + + } + + static class AmbiguousConstructors { + + AmbiguousConstructors(String a, Integer b) { + } + + AmbiguousConstructors(Integer b, String a) { + } + + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/AutowiredFieldValueResolverTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/AutowiredFieldValueResolverTests.java new file mode 100644 index 000000000000..2a2389c5c6a4 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/AutowiredFieldValueResolverTests.java @@ -0,0 +1,207 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.UnsatisfiedDependencyException; +import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.util.function.ThrowingConsumer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link AutowiredFieldValueResolver}. + * + * @author Phillip Webb + * @author Stephane Nicoll + */ +class AutowiredFieldValueResolverTests { + + private final DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + + @Test + void forFieldWhenFieldNameIsEmptyThrowsException() { + String message = "'fieldName' must not be empty"; + assertThatIllegalArgumentException() + .isThrownBy(() -> AutowiredFieldValueResolver.forField(null)) + .withMessage(message); + assertThatIllegalArgumentException() + .isThrownBy(() -> AutowiredFieldValueResolver.forField("")) + .withMessage(message); + assertThatIllegalArgumentException() + .isThrownBy(() -> AutowiredFieldValueResolver.forRequiredField(null)) + .withMessage(message); + assertThatIllegalArgumentException() + .isThrownBy(() -> AutowiredFieldValueResolver.forRequiredField(" ")) + .withMessage(message); + } + + @Test + void resolveWhenRegisteredBeanIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> + AutowiredFieldValueResolver.forField("string").resolve(null)) + .withMessage("'registeredBean' must not be null"); + } + + @Test + void resolveWhenFieldIsMissingThrowsException() { + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + assertThatIllegalArgumentException() + .isThrownBy(() -> AutowiredFieldValueResolver.forField("missing") + .resolve(registeredBean)) + .withMessage("No field 'missing' found on " + TestBean.class.getName()); + } + + @Test + void resolveReturnsValue() { + this.beanFactory.registerSingleton("one", "1"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + Object resolved = AutowiredFieldValueResolver.forField("string") + .resolve(registeredBean); + assertThat(resolved).isEqualTo("1"); + } + + @Test + void resolveWhenRequiredFieldAndBeanReturnsValue() { + this.beanFactory.registerSingleton("one", "1"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + Object resolved = AutowiredFieldValueResolver.forRequiredField("string") + .resolve(registeredBean); + assertThat(resolved).isEqualTo("1"); + } + + @Test + void resolveWhenRequiredFieldAndNoBeanReturnsNull() { + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + Object resolved = AutowiredFieldValueResolver.forField("string") + .resolve(registeredBean); + assertThat(resolved).isNull(); + } + + @Test + void resolveWhenRequiredFieldAndNoBeanThrowsException() { + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + AutowiredFieldValueResolver resolver = AutowiredFieldValueResolver + .forRequiredField("string"); + assertThatExceptionOfType(UnsatisfiedDependencyException.class) + .isThrownBy(() -> resolver.resolve(registeredBean)).satisfies(ex -> { + assertThat(ex.getBeanName()).isEqualTo("testBean"); + assertThat(ex.getInjectionPoint()).isNotNull(); + assertThat(ex.getInjectionPoint().getField().getName()) + .isEqualTo("string"); + }); + } + + @Test + void resolveAndSetWhenInstanceIsNullThrowsException() { + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + assertThatIllegalArgumentException() + .isThrownBy(() -> AutowiredFieldValueResolver.forField("string") + .resolveAndSet(registeredBean, null)) + .withMessage("'instance' must not be null"); + } + + @Test + void resolveAndSetSetsValue() { + this.beanFactory.registerSingleton("one", "1"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + TestBean testBean = new TestBean(); + AutowiredFieldValueResolver.forField("string").resolveAndSet(registeredBean, + testBean); + assertThat(testBean).extracting("string").isEqualTo("1"); + } + + @Test + void resolveWithActionWhenActionIsNullThrowsException() { + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + assertThatIllegalArgumentException() + .isThrownBy(() -> AutowiredFieldValueResolver.forField("string") + .resolve(registeredBean, (ThrowingConsumer) null)) + .withMessage("'action' must not be null"); + } + + @Test + void resolveWithActionCallsAction() { + this.beanFactory.registerSingleton("one", "1"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + List result = new ArrayList<>(); + AutowiredFieldValueResolver.forField("string").resolve(registeredBean, + result::add); + assertThat(result).containsExactly("1"); + } + + @Test + void resolveWithActionWhenDeducedGenericCallsAction() { + this.beanFactory.registerSingleton("one", "1"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + TestBean testBean = new TestBean(); + testBean.string = AutowiredFieldValueResolver.forField("string") + .resolve(registeredBean); + } + + @Test + void resolveObjectWhenUsingShortcutInjectsDirectly() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory() { + + @Override + protected Map findAutowireCandidates(String beanName, + Class requiredType, DependencyDescriptor descriptor) { + throw new AssertionError("Should be shortcut"); + } + + }; + beanFactory.registerSingleton("one", "1"); + RegisteredBean registeredBean = registerTestBean(beanFactory); + AutowiredFieldValueResolver resolver = AutowiredFieldValueResolver + .forField("string"); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> resolver.resolve(registeredBean)); + assertThat(resolver.withShortcut("one").resolveObject(registeredBean)) + .isEqualTo("1"); + } + + @Test + void resolveRegistersDependantBeans() { + this.beanFactory.registerSingleton("one", "1"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + AutowiredFieldValueResolver.forField("string").resolve(registeredBean); + assertThat(this.beanFactory.getDependentBeans("one")).containsExactly("testBean"); + } + + private RegisteredBean registerTestBean(DefaultListableBeanFactory beanFactory) { + beanFactory.registerBeanDefinition("testBean", + new RootBeanDefinition(TestBean.class)); + return RegisteredBean.of(beanFactory, "testBean"); + } + + static class TestBean { + + String string; + + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/AutowiredMethodArgumentsResolverTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/AutowiredMethodArgumentsResolverTests.java new file mode 100644 index 000000000000..857592f43232 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/AutowiredMethodArgumentsResolverTests.java @@ -0,0 +1,227 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.UnsatisfiedDependencyException; +import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.core.env.Environment; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link AutowiredMethodArgumentsResolver}. + * + * @author Phillip Webb + * @author Stephane Nicoll + */ +class AutowiredMethodArgumentsResolverTests { + + private final DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + + @Test + void forMethodWhenMethodNameIsEmptyThrowsException() { + String message = "'methodName' must not be empty"; + assertThatIllegalArgumentException() + .isThrownBy(() -> AutowiredMethodArgumentsResolver.forMethod(null)) + .withMessage(message); + assertThatIllegalArgumentException() + .isThrownBy(() -> AutowiredMethodArgumentsResolver.forMethod("")) + .withMessage(message); + assertThatIllegalArgumentException() + .isThrownBy( + () -> AutowiredMethodArgumentsResolver.forRequiredMethod(null)) + .withMessage(message); + assertThatIllegalArgumentException() + .isThrownBy(() -> AutowiredMethodArgumentsResolver.forRequiredMethod(" ")) + .withMessage(message); + } + + @Test + void resolveWhenRegisteredBeanIsNullThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> AutowiredMethodArgumentsResolver + .forMethod("injectString", String.class).resolve(null)) + .withMessage("'registeredBean' must not be null"); + } + + @Test + void resolveWhenMethodIsMissingThrowsException() { + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + AutowiredMethodArgumentsResolver resolver = AutowiredMethodArgumentsResolver.forMethod("missing", InputStream.class); + assertThatIllegalArgumentException() + .isThrownBy(() -> resolver.resolve(registeredBean)) + .withMessage("Method 'missing' with parameter types [java.io.InputStream] declared on %s could not be found.", + TestBean.class.getName()); + } + + @Test + void resolveRequiredWithSingleDependencyReturnsValue() { + this.beanFactory.registerSingleton("test", "testValue"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + AutowiredMethodArgumentsResolver resolver = AutowiredMethodArgumentsResolver + .forRequiredMethod("injectString", String.class); + AutowiredArguments resolved = resolver.resolve(registeredBean); + assertThat(resolved.toArray()).containsExactly("testValue"); + } + + @Test + void resolveRequiredWhenNoSuchBeanThrowsUnsatisfiedDependencyException() { + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + AutowiredMethodArgumentsResolver resolver = AutowiredMethodArgumentsResolver + .forRequiredMethod("injectString", String.class); + assertThatExceptionOfType(UnsatisfiedDependencyException.class) + .isThrownBy(() -> resolver.resolve(registeredBean)).satisfies(ex -> { + assertThat(ex.getBeanName()).isEqualTo("testBean"); + assertThat(ex.getInjectionPoint()).isNotNull(); + assertThat(ex.getInjectionPoint().getMember().getName()) + .isEqualTo("injectString"); + }); + } + + @Test + void resolveNonRequiredWhenNoSuchBeanReturnsNull() { + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + AutowiredMethodArgumentsResolver resolver = AutowiredMethodArgumentsResolver + .forMethod("injectString", String.class); + assertThat(resolver.resolve(registeredBean)).isNull(); + } + + @Test + void resolveRequiredWithMultipleDependenciesReturnsValue() { + Environment environment = mock(Environment.class); + this.beanFactory.registerSingleton("test", "testValue"); + this.beanFactory.registerSingleton("environment", environment); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + AutowiredMethodArgumentsResolver resolver = AutowiredMethodArgumentsResolver + .forRequiredMethod("injectStringAndEnvironment", String.class, + Environment.class); + AutowiredArguments resolved = resolver.resolve(registeredBean); + assertThat(resolved.toArray()).containsExactly("testValue", environment); + } + + @Test + void resolveAndInvokeWhenInstanceIsNullThrowsException() { + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + assertThatIllegalArgumentException() + .isThrownBy(() -> AutowiredMethodArgumentsResolver + .forMethod("injectString", String.class) + .resolveAndInvoke(registeredBean, null)) + .withMessage("'instance' must not be null"); + } + + @Test + void resolveAndInvokeInvokesMethod() { + this.beanFactory.registerSingleton("test", "testValue"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + AutowiredMethodArgumentsResolver resolver = AutowiredMethodArgumentsResolver + .forRequiredMethod("injectString", String.class); + TestBean instance = new TestBean(); + resolver.resolveAndInvoke(registeredBean, instance); + assertThat(instance.getString()).isEqualTo("testValue"); + } + + @Test + void resolveWithActionWhenActionIsNullThrowsException() { + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + assertThatIllegalArgumentException() + .isThrownBy(() -> AutowiredMethodArgumentsResolver + .forMethod("injectString", String.class) + .resolve(registeredBean, null)) + .withMessage("'action' must not be null"); + } + + @Test + void resolveWithActionCallsAction() { + this.beanFactory.registerSingleton("test", "testValue"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + List result = new ArrayList<>(); + AutowiredMethodArgumentsResolver.forMethod("injectString", String.class) + .resolve(registeredBean, result::add); + assertThat(result).hasSize(1); + assertThat(((AutowiredArguments) result.get(0)).toArray()) + .containsExactly("testValue"); + } + + @Test + void resolveWhenUsingShortcutsInjectsDirectly() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory() { + + @Override + protected Map findAutowireCandidates(String beanName, + Class requiredType, DependencyDescriptor descriptor) { + throw new AssertionError("Should be shortcut"); + } + + }; + beanFactory.registerSingleton("test", "testValue"); + RegisteredBean registeredBean = registerTestBean(beanFactory); + AutowiredMethodArgumentsResolver resolver = AutowiredMethodArgumentsResolver + .forRequiredMethod("injectString", String.class); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> resolver.resolve(registeredBean)); + assertThat(resolver.withShortcut("test").resolve(registeredBean).getObject(0)) + .isEqualTo("testValue"); + } + + @Test + void resolveRegistersDependantBeans() { + this.beanFactory.registerSingleton("test", "testValue"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + AutowiredMethodArgumentsResolver.forMethod("injectString", String.class) + .resolve(registeredBean); + assertThat(this.beanFactory.getDependentBeans("test")) + .containsExactly("testBean"); + } + + private RegisteredBean registerTestBean(DefaultListableBeanFactory beanFactory) { + beanFactory.registerBeanDefinition("testBean", + new RootBeanDefinition(TestBean.class)); + return RegisteredBean.of(beanFactory, "testBean"); + } + + @SuppressWarnings("unused") + static class TestBean { + + private String string; + + void injectString(String string) { + this.string = string; + } + + void injectStringAndEnvironment(String string, Environment environment) { + } + + String getString() { + return this.string; + } + + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGeneratorFactoryTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGeneratorFactoryTests.java new file mode 100644 index 000000000000..82e4a1184d19 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGeneratorFactoryTests.java @@ -0,0 +1,241 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.core.Ordered; +import org.springframework.core.test.io.support.MockSpringFactoriesLoader; +import org.springframework.lang.Nullable; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link BeanDefinitionMethodGeneratorFactory}. + * + * @author Phillip Webb + */ +class BeanDefinitionMethodGeneratorFactoryTests { + + @Test + void createWhenBeanRegistrationExcludeFilterBeanIsNotAotProcessorThrowsException() { + BeanRegistrationExcludeFilter filter = registeredBean -> false; + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerSingleton("filter", filter); + assertThatIllegalStateException() + .isThrownBy(() -> new BeanDefinitionMethodGeneratorFactory(beanFactory)) + .withMessageContaining("also implement an AOT processor interface"); + } + + @Test + void createWhenBeanRegistrationExcludeFilterFactoryIsNotAotProcessorLoads() { + BeanRegistrationExcludeFilter filter = registeredBean -> false; + MockSpringFactoriesLoader loader = new MockSpringFactoriesLoader(); + loader.addInstance(BeanRegistrationExcludeFilter.class, filter); + assertThatNoException().isThrownBy(() -> new BeanDefinitionMethodGeneratorFactory( + AotServices.factories(loader))); + } + + @Test + void getBeanDefinitionMethodGeneratorWhenExcludedByBeanRegistrationExcludeFilterReturnsNull() { + MockSpringFactoriesLoader springFactoriesLoader = new MockSpringFactoriesLoader(); + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + springFactoriesLoader.addInstance(BeanRegistrationExcludeFilter.class, + new MockBeanRegistrationExcludeFilter(true, 0)); + RegisteredBean registeredBean = registerTestBean(beanFactory); + BeanDefinitionMethodGeneratorFactory methodGeneratorFactory = new BeanDefinitionMethodGeneratorFactory( + AotServices.factoriesAndBeans(springFactoriesLoader, beanFactory)); + assertThat(methodGeneratorFactory.getBeanDefinitionMethodGenerator(registeredBean)).isNull(); + } + + @Test + void getBeanDefinitionMethodGeneratorWhenExcludedByBeanRegistrationExcludeFilterBeanReturnsNull() { + MockSpringFactoriesLoader springFactoriesLoader = new MockSpringFactoriesLoader(); + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + RegisteredBean registeredBean = registerTestBean(beanFactory); + beanFactory.registerSingleton("filter", + new MockBeanRegistrationExcludeFilter(true, 0)); + BeanDefinitionMethodGeneratorFactory methodGeneratorFactory = new BeanDefinitionMethodGeneratorFactory( + AotServices.factoriesAndBeans(springFactoriesLoader, beanFactory)); + assertThat(methodGeneratorFactory.getBeanDefinitionMethodGenerator(registeredBean)).isNull(); + } + + @Test + void getBeanDefinitionMethodGeneratorConsidersFactoryLoadedExcludeFiltersAndBeansInOrderedOrder() { + MockBeanRegistrationExcludeFilter filter1 = new MockBeanRegistrationExcludeFilter(false, 1); + MockBeanRegistrationExcludeFilter filter2 = new MockBeanRegistrationExcludeFilter(false, 2); + MockBeanRegistrationExcludeFilter filter3 = new MockBeanRegistrationExcludeFilter(false, 3); + MockBeanRegistrationExcludeFilter filter4 = new MockBeanRegistrationExcludeFilter(true, 4); + MockBeanRegistrationExcludeFilter filter5 = new MockBeanRegistrationExcludeFilter(true, 5); + MockBeanRegistrationExcludeFilter filter6 = new MockBeanRegistrationExcludeFilter(true, 6); + MockSpringFactoriesLoader springFactoriesLoader = new MockSpringFactoriesLoader(); + springFactoriesLoader.addInstance(BeanRegistrationExcludeFilter.class, filter3, filter1, filter5); + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerSingleton("filter4", filter4); + beanFactory.registerSingleton("filter2", filter2); + beanFactory.registerSingleton("filter6", filter6); + RegisteredBean registeredBean = registerTestBean(beanFactory); + BeanDefinitionMethodGeneratorFactory methodGeneratorFactory = new BeanDefinitionMethodGeneratorFactory( + AotServices.factoriesAndBeans(springFactoriesLoader, beanFactory)); + assertThat(methodGeneratorFactory.getBeanDefinitionMethodGenerator(registeredBean)).isNull(); + assertThat(filter1.wasCalled()).isTrue(); + assertThat(filter2.wasCalled()).isTrue(); + assertThat(filter3.wasCalled()).isTrue(); + assertThat(filter4.wasCalled()).isTrue(); + assertThat(filter5.wasCalled()).isFalse(); + assertThat(filter6.wasCalled()).isFalse(); + } + + @Test + void getBeanDefinitionMethodGeneratorAddsContributionsFromProcessors() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + BeanRegistrationAotContribution beanContribution = mock( + BeanRegistrationAotContribution.class); + BeanRegistrationAotProcessor processorBean = registeredBean -> beanContribution; + beanFactory.registerSingleton("processorBean", processorBean); + MockSpringFactoriesLoader springFactoriesLoader = new MockSpringFactoriesLoader(); + BeanRegistrationAotContribution loaderContribution = mock( + BeanRegistrationAotContribution.class); + BeanRegistrationAotProcessor loaderProcessor = registeredBean -> loaderContribution; + springFactoriesLoader.addInstance(BeanRegistrationAotProcessor.class, + loaderProcessor); + RegisteredBean registeredBean = registerTestBean(beanFactory); + BeanDefinitionMethodGeneratorFactory methodGeneratorFactory = new BeanDefinitionMethodGeneratorFactory( + AotServices.factoriesAndBeans(springFactoriesLoader, beanFactory)); + BeanDefinitionMethodGenerator methodGenerator = methodGeneratorFactory + .getBeanDefinitionMethodGenerator(registeredBean); + assertThat(methodGenerator).extracting("aotContributions").asList() + .containsExactly(beanContribution, loaderContribution); + } + + @Test + void getBeanDefinitionMethodGeneratorWhenRegisteredBeanIsAotProcessorFiltersBean() { + MockSpringFactoriesLoader springFactoriesLoader = new MockSpringFactoriesLoader(); + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerBeanDefinition("test1", BeanDefinitionBuilder + .rootBeanDefinition(TestBeanFactoryInitializationAotProcessorBean.class).getBeanDefinition()); + RegisteredBean registeredBean1 = RegisteredBean.of(beanFactory, "test1"); + beanFactory.registerBeanDefinition("test2", BeanDefinitionBuilder + .rootBeanDefinition(TestBeanRegistrationAotProcessorBean.class).getBeanDefinition()); + RegisteredBean registeredBean2 = RegisteredBean.of(beanFactory, "test2"); + BeanDefinitionMethodGeneratorFactory methodGeneratorFactory = new BeanDefinitionMethodGeneratorFactory( + AotServices.factoriesAndBeans(springFactoriesLoader, beanFactory)); + assertThat(methodGeneratorFactory.getBeanDefinitionMethodGenerator(registeredBean1)).isNull(); + assertThat(methodGeneratorFactory.getBeanDefinitionMethodGenerator(registeredBean2)).isNull(); + } + + @Test + void getBeanDefinitionMethodGeneratorWhenRegisteredBeanIsAotProcessorAndIsNotExcludedAndBeanRegistrationExcludeFilterDoesNotFilterBean() { + MockSpringFactoriesLoader springFactoriesLoader = new MockSpringFactoriesLoader(); + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerBeanDefinition("test", BeanDefinitionBuilder + .rootBeanDefinition(TestBeanRegistrationAotProcessorAndNotExcluded.class).getBeanDefinition()); + RegisteredBean registeredBean1 = RegisteredBean.of(beanFactory, "test"); + BeanDefinitionMethodGeneratorFactory methodGeneratorFactory = new BeanDefinitionMethodGeneratorFactory( + AotServices.factoriesAndBeans(springFactoriesLoader, beanFactory)); + assertThat(methodGeneratorFactory.getBeanDefinitionMethodGenerator(registeredBean1)).isNotNull(); + } + + private RegisteredBean registerTestBean(DefaultListableBeanFactory beanFactory) { + beanFactory.registerBeanDefinition("test", BeanDefinitionBuilder + .rootBeanDefinition(TestBean.class).getBeanDefinition()); + return RegisteredBean.of(beanFactory, "test"); + } + + + static class MockBeanRegistrationExcludeFilter implements + BeanRegistrationAotProcessor, BeanRegistrationExcludeFilter, Ordered { + + private final boolean excluded; + + private final int order; + + @Nullable + private RegisteredBean registeredBean; + + MockBeanRegistrationExcludeFilter(boolean excluded, int order) { + this.excluded = excluded; + this.order = order; + } + + @Override + public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + return null; + } + + @Override + public boolean isExcludedFromAotProcessing(RegisteredBean registeredBean) { + this.registeredBean = registeredBean; + return this.excluded; + } + + @Override + public int getOrder() { + return this.order; + } + + boolean wasCalled() { + return this.registeredBean != null; + } + + } + + static class TestBean { + + } + + static class TestBeanFactoryInitializationAotProcessorBean implements BeanFactoryInitializationAotProcessor { + + @Override + public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { + return null; + } + + } + + static class TestBeanRegistrationAotProcessorBean implements BeanRegistrationAotProcessor { + + @Override + public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + return null; + } + + } + + static class TestBeanRegistrationAotProcessorAndNotExcluded + extends TestBeanRegistrationAotProcessorBean { + + @Override + public boolean isBeanExcludedFromAotProcessing() { + return false; + } + + } + + @SuppressWarnings("unused") + static class InnerTestBean { + + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGeneratorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGeneratorTests.java new file mode 100644 index 000000000000..d90f7534e326 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGeneratorTests.java @@ -0,0 +1,545 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Predicate; +import java.util.function.Supplier; + +import javax.lang.model.element.Modifier; +import javax.xml.parsers.DocumentBuilderFactory; + +import org.junit.jupiter.api.Test; + +import org.springframework.aot.generate.GeneratedMethod; +import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.generate.MethodReference; +import org.springframework.aot.generate.MethodReference.ArgumentCodeGenerator; +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueHolder; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.InstanceSupplier; +import org.springframework.beans.factory.support.ManagedList; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.AnnotatedBean; +import org.springframework.beans.testfixture.beans.GenericBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.beans.testfixture.beans.factory.aot.InnerBeanConfiguration; +import org.springframework.beans.testfixture.beans.factory.aot.MockBeanRegistrationsCode; +import org.springframework.beans.testfixture.beans.factory.aot.SimpleBean; +import org.springframework.core.ResolvableType; +import org.springframework.core.test.io.support.MockSpringFactoriesLoader; +import org.springframework.core.test.tools.CompileWithForkedClassLoader; +import org.springframework.core.test.tools.Compiled; +import org.springframework.core.test.tools.SourceFile; +import org.springframework.core.test.tools.TestCompiler; +import org.springframework.javapoet.CodeBlock; +import org.springframework.javapoet.MethodSpec; +import org.springframework.javapoet.ParameterizedTypeName; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link BeanDefinitionMethodGenerator} and + * {@link DefaultBeanRegistrationCodeFragments}. + * + * @author Phillip Webb + * @author Stephane Nicoll + */ +class BeanDefinitionMethodGeneratorTests { + + private final TestGenerationContext generationContext; + + private final DefaultListableBeanFactory beanFactory; + + private final MockBeanRegistrationsCode beanRegistrationsCode; + + private final BeanDefinitionMethodGeneratorFactory methodGeneratorFactory; + + + BeanDefinitionMethodGeneratorTests() { + this.generationContext = new TestGenerationContext(); + this.beanFactory = new DefaultListableBeanFactory(); + this.methodGeneratorFactory = new BeanDefinitionMethodGeneratorFactory( + AotServices.factoriesAndBeans(new MockSpringFactoriesLoader(), this.beanFactory)); + this.beanRegistrationsCode = new MockBeanRegistrationsCode(this.generationContext); + } + + + @Test + void generateBeanDefinitionMethodGeneratesMethod() { + RegisteredBean registeredBean = registerBean( + new RootBeanDefinition(TestBean.class)); + BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator( + this.methodGeneratorFactory, registeredBean, null, + Collections.emptyList()); + MethodReference method = generator.generateBeanDefinitionMethod( + this.generationContext, this.beanRegistrationsCode); + compile(method, (actual, compiled) -> { + SourceFile sourceFile = compiled.getSourceFile(".*BeanDefinitions"); + assertThat(sourceFile).contains("Get the bean definition for 'testBean'"); + assertThat(sourceFile).contains("beanType = TestBean.class"); + assertThat(sourceFile).contains("setInstanceSupplier(TestBean::new)"); + assertThat(actual).isInstanceOf(RootBeanDefinition.class); + }); + } + + @Test + void generateBeanDefinitionMethodWhenHasInnerClassTargetMethodGeneratesMethod() { + this.beanFactory.registerBeanDefinition("testBeanConfiguration", new RootBeanDefinition( + InnerBeanConfiguration.Simple.class)); + RootBeanDefinition beanDefinition = new RootBeanDefinition(SimpleBean.class); + beanDefinition.setFactoryBeanName("testBeanConfiguration"); + beanDefinition.setFactoryMethodName("simpleBean"); + RegisteredBean registeredBean = registerBean(beanDefinition); + BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator( + this.methodGeneratorFactory, registeredBean, null, + Collections.emptyList()); + MethodReference method = generator.generateBeanDefinitionMethod( + this.generationContext, this.beanRegistrationsCode); + compile(method, (actual, compiled) -> { + SourceFile sourceFile = compiled.getSourceFile(".*BeanDefinitions"); + assertThat(sourceFile.getClassName()).endsWith("InnerBeanConfiguration__BeanDefinitions"); + assertThat(sourceFile).contains("public static class Simple__BeanDefinitions") + .contains("Bean definitions for {@link InnerBeanConfiguration.Simple}") + .doesNotContain("Another__BeanDefinitions"); + + }); + } + + @Test + void generateBeanDefinitionMethodWhenHasNestedInnerClassTargetMethodGeneratesMethod() { + this.beanFactory.registerBeanDefinition("testBeanConfiguration", new RootBeanDefinition( + InnerBeanConfiguration.Simple.Another.class)); + RootBeanDefinition beanDefinition = new RootBeanDefinition(SimpleBean.class); + beanDefinition.setFactoryBeanName("testBeanConfiguration"); + beanDefinition.setFactoryMethodName("anotherBean"); + RegisteredBean registeredBean = registerBean(beanDefinition); + BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator( + this.methodGeneratorFactory, registeredBean, null, + Collections.emptyList()); + MethodReference method = generator.generateBeanDefinitionMethod( + this.generationContext, this.beanRegistrationsCode); + compile(method, (actual, compiled) -> { + SourceFile sourceFile = compiled.getSourceFile(".*BeanDefinitions"); + assertThat(sourceFile.getClassName()).endsWith("InnerBeanConfiguration__BeanDefinitions"); + assertThat(sourceFile).contains("public static class Simple__BeanDefinitions") + .contains("Bean definitions for {@link InnerBeanConfiguration.Simple}") + .contains("public static class Another__BeanDefinitions") + .contains("Bean definitions for {@link InnerBeanConfiguration.Simple.Another}"); + }); + } + + @Test + void generateBeanDefinitionMethodWhenHasGenericsGeneratesMethod() { + RegisteredBean registeredBean = registerBean(new RootBeanDefinition( + ResolvableType.forClassWithGenerics(GenericBean.class, Integer.class))); + BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator( + this.methodGeneratorFactory, registeredBean, null, + Collections.emptyList()); + MethodReference method = generator.generateBeanDefinitionMethod( + this.generationContext, this.beanRegistrationsCode); + compile(method, (actual, compiled) -> { + assertThat(actual.getResolvableType().resolve()).isEqualTo(GenericBean.class); + SourceFile sourceFile = compiled.getSourceFile(".*BeanDefinitions"); + assertThat(sourceFile).contains("Get the bean definition for 'testBean'"); + assertThat(sourceFile).contains( + "beanType = ResolvableType.forClassWithGenerics(GenericBean.class, Integer.class)"); + assertThat(sourceFile).contains("setInstanceSupplier(GenericBean::new)"); + assertThat(actual).isInstanceOf(RootBeanDefinition.class); + }); + } + + @Test + void generateBeanDefinitionMethodWhenHasInstancePostProcessorGeneratesMethod() { + RegisteredBean registeredBean = registerBean( + new RootBeanDefinition(TestBean.class)); + BeanRegistrationAotContribution aotContribution = (generationContext, + beanRegistrationCode) -> { + GeneratedMethod generatedMethod = beanRegistrationCode.getMethods().add("postProcess", method -> + method.addModifiers(Modifier.STATIC) + .addParameter(RegisteredBean.class, "registeredBean") + .addParameter(TestBean.class, "testBean") + .returns(TestBean.class).addCode("return new $T($S);", TestBean.class, "postprocessed")); + beanRegistrationCode.addInstancePostProcessor(generatedMethod.toMethodReference()); + }; + List aotContributions = Collections + .singletonList(aotContribution); + BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator( + this.methodGeneratorFactory, registeredBean, null, aotContributions); + MethodReference method = generator.generateBeanDefinitionMethod( + this.generationContext, this.beanRegistrationsCode); + compile(method, (actual, compiled) -> { + assertThat(actual.getBeanClass()).isEqualTo(TestBean.class); + InstanceSupplier supplier = (InstanceSupplier) actual + .getInstanceSupplier(); + try { + TestBean instance = (TestBean) supplier.get(registeredBean); + assertThat(instance.getName()).isEqualTo("postprocessed"); + } + catch (Exception ex) { + } + SourceFile sourceFile = compiled.getSourceFile(".*BeanDefinitions"); + assertThat(sourceFile).contains("instanceSupplier.andThen("); + }); + } + + @Test // gh-28748 + void generateBeanDefinitionMethodWhenHasInstancePostProcessorAndFactoryMethodGeneratesMethod() { + this.beanFactory.registerBeanDefinition("testBeanConfiguration", new RootBeanDefinition(TestBeanConfiguration.class)); + RootBeanDefinition beanDefinition = new RootBeanDefinition(TestBean.class); + beanDefinition.setFactoryBeanName("testBeanConfiguration"); + beanDefinition.setFactoryMethodName("testBean"); + RegisteredBean registeredBean = registerBean(beanDefinition); + BeanRegistrationAotContribution aotContribution = (generationContext, + beanRegistrationCode) -> { + GeneratedMethod generatedMethod = beanRegistrationCode.getMethods().add("postProcess", method -> + method.addModifiers(Modifier.STATIC) + .addParameter(RegisteredBean.class, "registeredBean") + .addParameter(TestBean.class, "testBean") + .returns(TestBean.class).addCode("return new $T($S);", TestBean.class, "postprocessed")); + beanRegistrationCode.addInstancePostProcessor(generatedMethod.toMethodReference()); + }; + List aotContributions = Collections + .singletonList(aotContribution); + BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator( + this.methodGeneratorFactory, registeredBean, null, aotContributions); + MethodReference method = generator.generateBeanDefinitionMethod( + this.generationContext, this.beanRegistrationsCode); + compile(method, (actual, compiled) -> { + assertThat(compiled.getSourceFile(".*BeanDefinitions")).contains("BeanInstanceSupplier"); + assertThat(actual.getBeanClass()).isEqualTo(TestBean.class); + InstanceSupplier supplier = (InstanceSupplier) actual + .getInstanceSupplier(); + try { + TestBean instance = (TestBean) supplier.get(registeredBean); + assertThat(instance.getName()).isEqualTo("postprocessed"); + } + catch (Exception ex) { + } + SourceFile sourceFile = compiled.getSourceFile(".*BeanDefinitions"); + assertThat(sourceFile).contains("instanceSupplier.andThen("); + }); + } + + @Test + void generateBeanDefinitionMethodWhenHasCodeFragmentsCustomizerGeneratesMethod() { + RegisteredBean registeredBean = registerBean( + new RootBeanDefinition(TestBean.class)); + BeanRegistrationAotContribution aotContribution = BeanRegistrationAotContribution + .withCustomCodeFragments(this::customizeBeanDefinitionCode); + List aotContributions = Collections.singletonList(aotContribution); + BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator( + this.methodGeneratorFactory, registeredBean, null, aotContributions); + MethodReference method = generator.generateBeanDefinitionMethod( + this.generationContext, this.beanRegistrationsCode); + compile(method, (actual, compiled) -> { + assertThat(actual.getBeanClass()).isEqualTo(TestBean.class); + SourceFile sourceFile = compiled.getSourceFile(".*BeanDefinitions"); + assertThat(sourceFile).contains("I am custom"); + }); + } + + private BeanRegistrationCodeFragments customizeBeanDefinitionCode( + BeanRegistrationCodeFragments codeFragments) { + return new BeanRegistrationCodeFragmentsDecorator(codeFragments) { + + @Override + public CodeBlock generateNewBeanDefinitionCode( + GenerationContext generationContext, + ResolvableType beanType, + BeanRegistrationCode beanRegistrationCode) { + CodeBlock.Builder code = CodeBlock.builder(); + code.addStatement("// I am custom"); + code.add(super.generateNewBeanDefinitionCode(generationContext, beanType, beanRegistrationCode)); + return code.build(); + } + + }; + } + + @Test + void generateBeanDefinitionMethodDoesNotGenerateAttributesByDefault() { + RootBeanDefinition beanDefinition = new RootBeanDefinition(TestBean.class); + beanDefinition.setAttribute("a", "A"); + beanDefinition.setAttribute("b", "B"); + RegisteredBean registeredBean = registerBean(beanDefinition); + BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator( + this.methodGeneratorFactory, registeredBean, null, + Collections.emptyList()); + MethodReference method = generator.generateBeanDefinitionMethod( + this.generationContext, this.beanRegistrationsCode); + compile(method, (actual, compiled) -> { + assertThat(actual.hasAttribute("a")).isFalse(); + assertThat(actual.hasAttribute("b")).isFalse(); + }); + } + + @Test + void generateBeanDefinitionMethodWhenHasAttributeFilterGeneratesMethod() { + RootBeanDefinition beanDefinition = new RootBeanDefinition(TestBean.class); + beanDefinition.setAttribute("a", "A"); + beanDefinition.setAttribute("b", "B"); + RegisteredBean registeredBean = registerBean(beanDefinition); + BeanRegistrationAotContribution aotContribution = BeanRegistrationAotContribution + .withCustomCodeFragments(this::customizeAttributeFilter); + List aotContributions = Collections + .singletonList(aotContribution); + BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator( + this.methodGeneratorFactory, registeredBean, null, + aotContributions); + MethodReference method = generator.generateBeanDefinitionMethod( + this.generationContext, this.beanRegistrationsCode); + compile(method, (actual, compiled) -> { + assertThat(actual.getAttribute("a")).isEqualTo("A"); + assertThat(actual.getAttribute("b")).isNull(); + }); + } + + private BeanRegistrationCodeFragments customizeAttributeFilter( + BeanRegistrationCodeFragments codeFragments) { + return new BeanRegistrationCodeFragmentsDecorator(codeFragments) { + + @Override + public CodeBlock generateSetBeanDefinitionPropertiesCode( + GenerationContext generationContext, + BeanRegistrationCode beanRegistrationCode, + RootBeanDefinition beanDefinition, + Predicate attributeFilter) { + return super.generateSetBeanDefinitionPropertiesCode(generationContext, + beanRegistrationCode, beanDefinition, "a"::equals); + } + + }; + } + + @Test + void generateBeanDefinitionMethodWhenInnerBeanGeneratesMethod() { + RegisteredBean parent = registerBean(new RootBeanDefinition(TestBean.class)); + RegisteredBean innerBean = RegisteredBean.ofInnerBean(parent, + new RootBeanDefinition(AnnotatedBean.class)); + BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator( + this.methodGeneratorFactory, innerBean, "testInnerBean", + Collections.emptyList()); + MethodReference method = generator.generateBeanDefinitionMethod( + this.generationContext, this.beanRegistrationsCode); + compile(method, (actual, compiled) -> { + assertThat(compiled.getSourceFile(".*BeanDefinitions")) + .contains("Get the inner-bean definition for 'testInnerBean'"); + assertThat(actual).isInstanceOf(RootBeanDefinition.class); + }); + } + + @Test + void generateBeanDefinitionMethodWhenHasInnerBeanPropertyValueGeneratesMethod() { + RootBeanDefinition innerBeanDefinition = (RootBeanDefinition) BeanDefinitionBuilder + .rootBeanDefinition(AnnotatedBean.class) + .setRole(BeanDefinition.ROLE_INFRASTRUCTURE).setPrimary(true) + .getBeanDefinition(); + RootBeanDefinition beanDefinition = new RootBeanDefinition(TestBean.class); + beanDefinition.getPropertyValues().add("name", innerBeanDefinition); + RegisteredBean registeredBean = registerBean(beanDefinition); + BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator( + this.methodGeneratorFactory, registeredBean, null, + Collections.emptyList()); + MethodReference method = generator.generateBeanDefinitionMethod( + this.generationContext, this.beanRegistrationsCode); + compile(method, (actual, compiled) -> { + RootBeanDefinition actualInnerBeanDefinition = (RootBeanDefinition) actual + .getPropertyValues().get("name"); + assertThat(actualInnerBeanDefinition.isPrimary()).isTrue(); + assertThat(actualInnerBeanDefinition.getRole()) + .isEqualTo(BeanDefinition.ROLE_INFRASTRUCTURE); + Supplier innerInstanceSupplier = actualInnerBeanDefinition + .getInstanceSupplier(); + try { + assertThat(innerInstanceSupplier.get()).isInstanceOf(AnnotatedBean.class); + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + }); + } + + @SuppressWarnings("unchecked") + @Test + void generateBeanDefinitionMethodWhenHasListOfInnerBeansPropertyValueGeneratesMethod() { + RootBeanDefinition firstInnerBeanDefinition = (RootBeanDefinition) BeanDefinitionBuilder + .rootBeanDefinition(TestBean.class).addPropertyValue("name", "one") + .getBeanDefinition(); + RootBeanDefinition secondInnerBeanDefinition = (RootBeanDefinition) BeanDefinitionBuilder + .rootBeanDefinition(TestBean.class).addPropertyValue("name", "two") + .getBeanDefinition(); + ManagedList list = new ManagedList<>(); + list.add(firstInnerBeanDefinition); + list.add(secondInnerBeanDefinition); + RootBeanDefinition beanDefinition = new RootBeanDefinition(TestBean.class); + beanDefinition.getPropertyValues().add("someList", list); + RegisteredBean registeredBean = registerBean(beanDefinition); + BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator( + this.methodGeneratorFactory, registeredBean, null, + Collections.emptyList()); + MethodReference method = generator.generateBeanDefinitionMethod( + this.generationContext, this.beanRegistrationsCode); + compile(method, (actual, compiled) -> { + ManagedList actualPropertyValue = (ManagedList) actual + .getPropertyValues().get("someList"); + assertThat(actualPropertyValue).isNotNull().hasSize(2); + assertThat(actualPropertyValue.get(0).getPropertyValues().get("name")).isEqualTo("one"); + assertThat(actualPropertyValue.get(1).getPropertyValues().get("name")).isEqualTo("two"); + assertThat(compiled.getSourceFileFromPackage(TestBean.class.getPackageName())) + .contains("getSomeListBeanDefinition()", "getSomeListBeanDefinition1()"); + }); + } + + @Test + void generateBeanDefinitionMethodWhenHasInnerBeanConstructorValueGeneratesMethod() { + RootBeanDefinition innerBeanDefinition = (RootBeanDefinition) BeanDefinitionBuilder + .rootBeanDefinition(String.class) + .setRole(BeanDefinition.ROLE_INFRASTRUCTURE).setPrimary(true) + .getBeanDefinition(); + RootBeanDefinition beanDefinition = new RootBeanDefinition(TestBean.class); + ValueHolder valueHolder = new ValueHolder(innerBeanDefinition); + valueHolder.setName("second"); + beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0, + valueHolder); + RegisteredBean registeredBean = registerBean(beanDefinition); + BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator( + this.methodGeneratorFactory, registeredBean, null, + Collections.emptyList()); + MethodReference method = generator.generateBeanDefinitionMethod( + this.generationContext, this.beanRegistrationsCode); + compile(method, (actual, compiled) -> { + RootBeanDefinition actualInnerBeanDefinition = (RootBeanDefinition) actual + .getConstructorArgumentValues() + .getIndexedArgumentValue(0, RootBeanDefinition.class).getValue(); + assertThat(actualInnerBeanDefinition.isPrimary()).isTrue(); + assertThat(actualInnerBeanDefinition.getRole()) + .isEqualTo(BeanDefinition.ROLE_INFRASTRUCTURE); + Supplier innerInstanceSupplier = actualInnerBeanDefinition + .getInstanceSupplier(); + try { + assertThat(innerInstanceSupplier.get()).isInstanceOf(String.class); + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + assertThat(compiled.getSourceFile(".*BeanDefinitions")) + .contains("getSecondBeanDefinition()"); + }); + } + + @Test + void generateBeanDefinitionMethodWhenHasAotContributionsAppliesContributions() { + RegisteredBean registeredBean = registerBean( + new RootBeanDefinition(TestBean.class)); + List aotContributions = new ArrayList<>(); + aotContributions.add((generationContext, beanRegistrationCode) -> + beanRegistrationCode.getMethods().add("aotContributedMethod", method -> + method.addComment("Example Contribution"))); + BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator( + this.methodGeneratorFactory, registeredBean, null, aotContributions); + MethodReference method = generator.generateBeanDefinitionMethod( + this.generationContext, this.beanRegistrationsCode); + compile(method, (actual, compiled) -> { + SourceFile sourceFile = compiled.getSourceFile(".*BeanDefinitions"); + assertThat(sourceFile).contains("AotContributedMethod()"); + assertThat(sourceFile).contains("Example Contribution"); + }); + } + + @Test + @CompileWithForkedClassLoader + void generateBeanDefinitionMethodWhenPackagePrivateBean() { + RegisteredBean registeredBean = registerBean( + new RootBeanDefinition(PackagePrivateTestBean.class)); + BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator( + this.methodGeneratorFactory, registeredBean, null, + Collections.emptyList()); + MethodReference method = generator.generateBeanDefinitionMethod( + this.generationContext, this.beanRegistrationsCode); + compile(method, (actual, compiled) -> { + DefaultListableBeanFactory freshBeanFactory = new DefaultListableBeanFactory(); + freshBeanFactory.registerBeanDefinition("test", actual); + Object bean = freshBeanFactory.getBean("test"); + assertThat(bean).isInstanceOf(PackagePrivateTestBean.class); + assertThat(compiled.getSourceFileFromPackage( + PackagePrivateTestBean.class.getPackageName())).isNotNull(); + }); + } + + @Test + void generateBeanDefinitionMethodWhenBeanIsInJavaPackage() { + RootBeanDefinition beanDefinition = (RootBeanDefinition) BeanDefinitionBuilder + .rootBeanDefinition(String.class).addConstructorArgValue("test").getBeanDefinition(); + testBeanDefinitionMethodInCurrentFile(String.class, beanDefinition); + } + + @Test + void generateBeanDefinitionMethodWhenBeanIsInJavaxPackage() { + RootBeanDefinition beanDefinition = (RootBeanDefinition) BeanDefinitionBuilder + .rootBeanDefinition(DocumentBuilderFactory.class).setFactoryMethod("newDefaultInstance").getBeanDefinition(); + testBeanDefinitionMethodInCurrentFile(DocumentBuilderFactory.class, beanDefinition); + } + + private void testBeanDefinitionMethodInCurrentFile(Class targetType, RootBeanDefinition beanDefinition) { + RegisteredBean registeredBean = registerBean(new RootBeanDefinition(beanDefinition)); + BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator( + this.methodGeneratorFactory, registeredBean, null, + Collections.emptyList()); + MethodReference method = generator.generateBeanDefinitionMethod( + this.generationContext, this.beanRegistrationsCode); + compile(method, (actual, compiled) -> { + DefaultListableBeanFactory freshBeanFactory = new DefaultListableBeanFactory(); + freshBeanFactory.registerBeanDefinition("test", actual); + Object bean = freshBeanFactory.getBean("test"); + assertThat(bean).isInstanceOf(targetType); + assertThat(compiled.getSourceFiles().stream().filter(sourceFile -> + sourceFile.getClassName().startsWith(targetType.getPackageName()))).isEmpty(); + }); + } + + private RegisteredBean registerBean(RootBeanDefinition beanDefinition) { + String beanName = "testBean"; + this.beanFactory.registerBeanDefinition(beanName, beanDefinition); + return RegisteredBean.of(this.beanFactory, beanName); + } + + private void compile(MethodReference method, + BiConsumer result) { + this.beanRegistrationsCode.getTypeBuilder().set(type -> { + CodeBlock methodInvocation = method.toInvokeCodeBlock(ArgumentCodeGenerator.none(), + this.beanRegistrationsCode.getClassName()); + type.addModifiers(Modifier.PUBLIC); + type.addSuperinterface(ParameterizedTypeName.get(Supplier.class, BeanDefinition.class)); + type.addMethod(MethodSpec.methodBuilder("get") + .addModifiers(Modifier.PUBLIC) + .returns(BeanDefinition.class) + .addCode("return $L;", methodInvocation).build()); + }); + this.generationContext.writeGeneratedContent(); + TestCompiler.forSystem().with(this.generationContext).compile(compiled -> + result.accept((RootBeanDefinition) compiled.getInstance(Supplier.class).get(), compiled)); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGeneratorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGeneratorTests.java new file mode 100644 index 000000000000..2fb541a9696c --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGeneratorTests.java @@ -0,0 +1,515 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Predicate; +import java.util.function.Supplier; + +import javax.lang.model.element.Modifier; + +import org.junit.jupiter.api.Test; + +import org.springframework.aot.generate.GeneratedClass; +import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanReference; +import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueHolder; +import org.springframework.beans.factory.config.RuntimeBeanNameReference; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.ManagedList; +import org.springframework.beans.factory.support.ManagedMap; +import org.springframework.beans.factory.support.ManagedSet; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.factory.aot.DeferredTypeBuilder; +import org.springframework.core.test.tools.Compiled; +import org.springframework.core.test.tools.TestCompiler; +import org.springframework.javapoet.CodeBlock; +import org.springframework.javapoet.MethodSpec; +import org.springframework.javapoet.ParameterizedTypeName; +import org.springframework.lang.Nullable; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link BeanDefinitionPropertiesCodeGenerator}. + * + * @author Phillip Webb + * @author Stephane Nicoll + * @author Olga Maciaszek-Sharma + */ +class BeanDefinitionPropertiesCodeGeneratorTests { + + private final RootBeanDefinition beanDefinition = new RootBeanDefinition(); + + private final TestGenerationContext generationContext = new TestGenerationContext(); + + @Test + void setPrimaryWhenFalse() { + this.beanDefinition.setPrimary(false); + compile((actual, compiled) -> { + assertThat(compiled.getSourceFile()).doesNotContain("setPrimary"); + assertThat(actual.isPrimary()).isFalse(); + }); + } + + @Test + void setPrimaryWhenTrue() { + this.beanDefinition.setPrimary(true); + compile((actual, compiled) -> assertThat(actual.isPrimary()).isTrue()); + } + + @Test + void setScopeWhenEmptyString() { + this.beanDefinition.setScope(""); + compile((actual, compiled) -> { + assertThat(compiled.getSourceFile()).doesNotContain("setScope"); + assertThat(actual.getScope()).isEmpty(); + }); + } + + @Test + void setScopeWhenSingleton() { + this.beanDefinition.setScope("singleton"); + compile((actual, compiled) -> { + assertThat(compiled.getSourceFile()).doesNotContain("setScope"); + assertThat(actual.getScope()).isEmpty(); + }); + } + + @Test + void setScopeWhenOther() { + this.beanDefinition.setScope("prototype"); + compile((actual, compiled) -> assertThat(actual.getScope()) + .isEqualTo("prototype")); + } + + @Test + void setDependsOnWhenEmpty() { + this.beanDefinition.setDependsOn(); + compile((actual, compiled) -> { + assertThat(compiled.getSourceFile()).doesNotContain("setDependsOn"); + assertThat(actual.getDependsOn()).isNull(); + }); + } + + @Test + void setDependsOnWhenNotEmpty() { + this.beanDefinition.setDependsOn("a", "b", "c"); + compile((actual, compiled) -> assertThat(actual.getDependsOn()) + .containsExactly("a", "b", "c")); + } + + @Test + void setLazyInitWhenNoSet() { + compile((actual, compiled) -> { + assertThat(compiled.getSourceFile()).doesNotContain("setLazyInit"); + assertThat(actual.isLazyInit()).isFalse(); + assertThat(actual.getLazyInit()).isNull(); + }); + } + + @Test + void setLazyInitWhenFalse() { + this.beanDefinition.setLazyInit(false); + compile((actual, compiled) -> { + assertThat(actual.isLazyInit()).isFalse(); + assertThat(actual.getLazyInit()).isFalse(); + }); + } + + @Test + void setLazyInitWhenTrue() { + this.beanDefinition.setLazyInit(true); + compile((actual, compiled) -> { + assertThat(actual.isLazyInit()).isTrue(); + assertThat(actual.getLazyInit()).isTrue(); + }); + } + + @Test + void setAutowireCandidateWhenFalse() { + this.beanDefinition.setAutowireCandidate(false); + compile( + (actual, compiled) -> assertThat(actual.isAutowireCandidate()).isFalse()); + } + + @Test + void setAutowireCandidateWhenTrue() { + this.beanDefinition.setAutowireCandidate(true); + compile((actual, compiled) -> { + assertThat(compiled.getSourceFile()).doesNotContain("setAutowireCandidate"); + assertThat(actual.isAutowireCandidate()).isTrue(); + }); + } + + @Test + void setSyntheticWhenFalse() { + this.beanDefinition.setSynthetic(false); + compile((actual, compiled) -> { + assertThat(compiled.getSourceFile()).doesNotContain("setSynthetic"); + assertThat(actual.isSynthetic()).isFalse(); + }); + } + + @Test + void setSyntheticWhenTrue() { + this.beanDefinition.setSynthetic(true); + compile( + (actual, compiled) -> assertThat(actual.isSynthetic()).isTrue()); + } + + @Test + void setRoleWhenApplication() { + this.beanDefinition.setRole(BeanDefinition.ROLE_APPLICATION); + compile((actual, compiled) -> { + assertThat(compiled.getSourceFile()).doesNotContain("setRole"); + assertThat(actual.getRole()).isEqualTo(BeanDefinition.ROLE_APPLICATION); + }); + } + + @Test + void setRoleWhenInfrastructure() { + this.beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + compile((actual, compiled) -> { + assertThat(compiled.getSourceFile()) + .contains("setRole(BeanDefinition.ROLE_INFRASTRUCTURE);"); + assertThat(actual.getRole()).isEqualTo(BeanDefinition.ROLE_INFRASTRUCTURE); + }); + } + + @Test + void setRoleWhenSupport() { + this.beanDefinition.setRole(BeanDefinition.ROLE_SUPPORT); + compile((actual, compiled) -> { + assertThat(compiled.getSourceFile()) + .contains("setRole(BeanDefinition.ROLE_SUPPORT);"); + assertThat(actual.getRole()).isEqualTo(BeanDefinition.ROLE_SUPPORT); + }); + } + + @Test + void setRoleWhenOther() { + this.beanDefinition.setRole(999); + compile( + (actual, compiled) -> assertThat(actual.getRole()).isEqualTo(999)); + } + + @Test + void setInitMethodWhenSingleInitMethod() { + this.beanDefinition.setTargetType(InitDestroyBean.class); + this.beanDefinition.setInitMethodName("i1"); + compile((actual, compiled) -> assertThat(actual.getInitMethodNames()) + .containsExactly("i1")); + String[] methodNames = { "i1" }; + assertHasMethodInvokeHints(InitDestroyBean.class, methodNames); + } + + @Test + void setInitMethodWhenNoInitMethod() { + this.beanDefinition.setTargetType(InitDestroyBean.class); + compile((actual, compiled) -> assertThat(actual.getInitMethodNames()).isNull()); + } + + @Test + void setInitMethodWhenMultipleInitMethods() { + this.beanDefinition.setTargetType(InitDestroyBean.class); + this.beanDefinition.setInitMethodNames("i1", "i2"); + compile((actual, compiled) -> assertThat(actual.getInitMethodNames()) + .containsExactly("i1", "i2")); + String[] methodNames = { "i1", "i2" }; + assertHasMethodInvokeHints(InitDestroyBean.class, methodNames); + } + + @Test + void setDestroyMethodWhenDestroyInitMethod() { + this.beanDefinition.setTargetType(InitDestroyBean.class); + this.beanDefinition.setDestroyMethodName("d1"); + compile( + (actual, compiled) -> assertThat(actual.getDestroyMethodNames()) + .containsExactly("d1")); + String[] methodNames = { "d1" }; + assertHasMethodInvokeHints(InitDestroyBean.class, methodNames); + } + + @Test + void setDestroyMethodWhenNoDestroyMethod() { + this.beanDefinition.setTargetType(InitDestroyBean.class); + compile((actual, compiled) -> assertThat(actual.getDestroyMethodNames()).isNull()); + } + + @Test + void setDestroyMethodWhenMultipleDestroyMethods() { + this.beanDefinition.setTargetType(InitDestroyBean.class); + this.beanDefinition.setDestroyMethodNames("d1", "d2"); + compile( + (actual, compiled) -> assertThat(actual.getDestroyMethodNames()) + .containsExactly("d1", "d2")); + String[] methodNames = { "d1", "d2" }; + assertHasMethodInvokeHints(InitDestroyBean.class, methodNames); + } + + private void assertHasMethodInvokeHints(Class beanType, String... methodNames) { + assertThat(methodNames).allMatch(methodName -> RuntimeHintsPredicates.reflection() + .onMethod(beanType, methodName).invoke() + .test(this.generationContext.getRuntimeHints())); + } + + @Test + void constructorArgumentValuesWhenValues() { + this.beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0, + String.class); + this.beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(1, + "test"); + this.beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(2, + 123); + compile((actual, compiled) -> { + Map values = actual.getConstructorArgumentValues() + .getIndexedArgumentValues(); + assertThat(values.get(0).getValue()).isEqualTo(String.class); + assertThat(values.get(1).getValue()).isEqualTo("test"); + assertThat(values.get(2).getValue()).isEqualTo(123); + }); + } + + @Test + void propertyValuesWhenValues() { + this.beanDefinition.setTargetType(PropertyValuesBean.class); + this.beanDefinition.getPropertyValues().add("test", String.class); + this.beanDefinition.getPropertyValues().add("spring", "framework"); + compile((actual, compiled) -> { + assertThat(actual.getPropertyValues().get("test")).isEqualTo(String.class); + assertThat(actual.getPropertyValues().get("spring")).isEqualTo("framework"); + }); + String[] methodNames = { "setTest", "setSpring" }; + assertHasMethodInvokeHints(PropertyValuesBean.class, methodNames); + } + + @Test + void propertyValuesWhenContainsBeanReference() { + this.beanDefinition.getPropertyValues().add("myService", + new RuntimeBeanNameReference("test")); + compile((actual, compiled) -> { + assertThat(actual.getPropertyValues().contains("myService")).isTrue(); + assertThat(actual.getPropertyValues().get("myService")) + .isInstanceOfSatisfying(RuntimeBeanReference.class, + beanReference -> assertThat(beanReference.getBeanName()) + .isEqualTo("test")); + }); + } + + @Test + void propertyValuesWhenContainsManagedList() { + ManagedList managedList = new ManagedList<>(); + managedList.add(new RuntimeBeanNameReference("test")); + this.beanDefinition.getPropertyValues().add("value", managedList); + compile((actual, compiled) -> { + Object value = actual.getPropertyValues().get("value"); + assertThat(value).isInstanceOf(ManagedList.class); + assertThat(((List) value).get(0)).isInstanceOf(BeanReference.class); + }); + } + + @Test + void propertyValuesWhenContainsManagedSet() { + ManagedSet managedSet = new ManagedSet<>(); + managedSet.add(new RuntimeBeanNameReference("test")); + this.beanDefinition.getPropertyValues().add("value", managedSet); + compile((actual, compiled) -> { + Object value = actual.getPropertyValues().get("value"); + assertThat(value).isInstanceOf(ManagedSet.class); + assertThat(((Set) value).iterator().next()) + .isInstanceOf(BeanReference.class); + }); + } + + @Test + void propertyValuesWhenContainsManagedMap() { + ManagedMap managedMap = new ManagedMap<>(); + managedMap.put("test", new RuntimeBeanNameReference("test")); + this.beanDefinition.getPropertyValues().add("value", managedMap); + compile((actual, compiled) -> { + Object value = actual.getPropertyValues().get("value"); + assertThat(value).isInstanceOf(ManagedMap.class); + assertThat(((Map) value).get("test")).isInstanceOf(BeanReference.class); + }); + } + + @Test + void propertyValuesWhenValuesOnFactoryBeanClass() { + this.beanDefinition.setTargetType(String.class); + this.beanDefinition.setBeanClass(PropertyValuesFactoryBean.class); + this.beanDefinition.getPropertyValues().add("prefix", "Hello"); + this.beanDefinition.getPropertyValues().add("name", "World"); + compile((actual, compiled) -> { + assertThat(actual.getPropertyValues().get("prefix")).isEqualTo("Hello"); + assertThat(actual.getPropertyValues().get("name")).isEqualTo("World"); + }); + String[] methodNames = { "setPrefix", "setName" }; + assertHasMethodInvokeHints(PropertyValuesFactoryBean.class, methodNames); + } + + @Test + void attributesWhenAllFiltered() { + this.beanDefinition.setAttribute("a", "A"); + this.beanDefinition.setAttribute("b", "B"); + Predicate attributeFilter = attribute -> false; + compile(attributeFilter, (actual, compiled) -> { + assertThat(compiled.getSourceFile()).doesNotContain("setAttribute"); + assertThat(actual.getAttribute("a")).isNull(); + assertThat(actual.getAttribute("b")).isNull(); + }); + } + + @Test + void attributesWhenSomeFiltered() { + this.beanDefinition.setAttribute("a", "A"); + this.beanDefinition.setAttribute("b", "B"); + Predicate attributeFilter = "a"::equals; + compile(attributeFilter, (actual, compiled) -> { + assertThat(actual.getAttribute("a")).isEqualTo("A"); + assertThat(actual.getAttribute("b")).isNull(); + }); + } + + @Test + void multipleItems() { + this.beanDefinition.setPrimary(true); + this.beanDefinition.setScope("test"); + this.beanDefinition.setRole(BeanDefinition.ROLE_SUPPORT); + compile((actual, compiled) -> { + assertThat(actual.isPrimary()).isTrue(); + assertThat(actual.getScope()).isEqualTo("test"); + assertThat(actual.getRole()).isEqualTo(BeanDefinition.ROLE_SUPPORT); + }); + } + + private void compile(BiConsumer result) { + compile(attribute -> true, result); + } + + private void compile( + Predicate attributeFilter, + BiConsumer result) { + DeferredTypeBuilder typeBuilder = new DeferredTypeBuilder(); + GeneratedClass generatedClass = this.generationContext.getGeneratedClasses().addForFeature("TestCode", typeBuilder); + BeanDefinitionPropertiesCodeGenerator codeGenerator = new BeanDefinitionPropertiesCodeGenerator( + this.generationContext.getRuntimeHints(), attributeFilter, + generatedClass.getMethods(), (name, value) -> null); + CodeBlock generatedCode = codeGenerator.generateCode(this.beanDefinition); + typeBuilder.set(type -> { + type.addModifiers(Modifier.PUBLIC); + type.addSuperinterface(ParameterizedTypeName.get(Supplier.class, RootBeanDefinition.class)); + type.addMethod(MethodSpec.methodBuilder("get") + .addModifiers(Modifier.PUBLIC) + .returns(RootBeanDefinition.class) + .addStatement("$T beanDefinition = new $T()", RootBeanDefinition.class, RootBeanDefinition.class) + .addStatement("$T beanFactory = new $T()", DefaultListableBeanFactory.class, DefaultListableBeanFactory.class) + .addCode(generatedCode) + .addStatement("return beanDefinition").build()); + }); + this.generationContext.writeGeneratedContent(); + TestCompiler.forSystem().with(this.generationContext).compile(compiled -> { + RootBeanDefinition suppliedBeanDefinition = (RootBeanDefinition) compiled + .getInstance(Supplier.class).get(); + result.accept(suppliedBeanDefinition, compiled); + }); + } + + static class InitDestroyBean { + + void i1() { + } + + void i2() { + } + + void d1() { + } + + void d2() { + } + + } + + static class PropertyValuesBean { + + private Class test; + + private String spring; + + public Class getTest() { + return this.test; + } + + public void setTest(Class test) { + this.test = test; + } + + public String getSpring() { + return this.spring; + } + + public void setSpring(String spring) { + this.spring = spring; + } + + } + + static class PropertyValuesFactoryBean implements FactoryBean { + + private String prefix; + + private String name; + + public String getPrefix() { + return this.prefix; + } + + public void setPrefix(String prefix) { + this.prefix = prefix; + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + @Nullable + @Override + public String getObject() throws Exception { + return getPrefix() + " " + getName(); + } + + @Nullable + @Override + public Class getObjectType() { + return String.class; + } + + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorTests.java new file mode 100644 index 000000000000..ca3c51284194 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorTests.java @@ -0,0 +1,557 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.time.temporal.ChronoUnit; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Supplier; + +import javax.lang.model.element.Modifier; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.aot.generate.GeneratedClass; +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.beans.factory.config.BeanReference; +import org.springframework.beans.factory.config.RuntimeBeanNameReference; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.support.ManagedList; +import org.springframework.beans.factory.support.ManagedMap; +import org.springframework.beans.factory.support.ManagedSet; +import org.springframework.beans.testfixture.beans.factory.aot.DeferredTypeBuilder; +import org.springframework.core.ResolvableType; +import org.springframework.core.test.tools.Compiled; +import org.springframework.core.test.tools.TestCompiler; +import org.springframework.javapoet.CodeBlock; +import org.springframework.javapoet.MethodSpec; +import org.springframework.javapoet.ParameterizedTypeName; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link BeanDefinitionPropertyValueCodeGenerator}. + * + * @author Stephane Nicoll + * @author Phillip Webb + * @since 6.0 + * @see BeanDefinitionPropertyValueCodeGeneratorTests + */ +class BeanDefinitionPropertyValueCodeGeneratorTests { + + private static BeanDefinitionPropertyValueCodeGenerator createPropertyValuesCodeGenerator(GeneratedClass generatedClass) { + return new BeanDefinitionPropertyValueCodeGenerator(generatedClass.getMethods(), null); + } + + private void compile(Object value, BiConsumer result) { + TestGenerationContext generationContext = new TestGenerationContext(); + DeferredTypeBuilder typeBuilder = new DeferredTypeBuilder(); + GeneratedClass generatedClass = generationContext.getGeneratedClasses().addForFeature("TestCode", typeBuilder); + CodeBlock generatedCode = createPropertyValuesCodeGenerator(generatedClass).generateCode(value); + typeBuilder.set(type -> { + type.addModifiers(Modifier.PUBLIC); + type.addSuperinterface( + ParameterizedTypeName.get(Supplier.class, Object.class)); + type.addMethod(MethodSpec.methodBuilder("get").addModifiers(Modifier.PUBLIC) + .returns(Object.class).addStatement("return $L", generatedCode).build()); + }); + generationContext.writeGeneratedContent(); + TestCompiler.forSystem().with(generationContext).compile(compiled -> + result.accept(compiled.getInstance(Supplier.class).get(), compiled)); + } + + @Nested + class NullTests { + + @Test + void generateWhenNull() { + compile(null, (instance, compiled) -> assertThat(instance).isNull()); + } + + } + + @Nested + class PrimitiveTests { + + @Test + void generateWhenBoolean() { + compile(true, (instance, compiled) -> { + assertThat(instance).isEqualTo(Boolean.TRUE); + assertThat(compiled.getSourceFile()).contains("true"); + }); + } + + @Test + void generateWhenByte() { + compile((byte) 2, (instance, compiled) -> { + assertThat(instance).isEqualTo((byte) 2); + assertThat(compiled.getSourceFile()).contains("(byte) 2"); + }); + } + + @Test + void generateWhenShort() { + compile((short) 3, (instance, compiled) -> { + assertThat(instance).isEqualTo((short) 3); + assertThat(compiled.getSourceFile()).contains("(short) 3"); + }); + } + + @Test + void generateWhenInt() { + compile(4, (instance, compiled) -> { + assertThat(instance).isEqualTo(4); + assertThat(compiled.getSourceFile()).contains("return 4;"); + }); + } + + @Test + void generateWhenLong() { + compile(5L, (instance, compiled) -> { + assertThat(instance).isEqualTo(5L); + assertThat(compiled.getSourceFile()).contains("5L"); + }); + } + + @Test + void generateWhenFloat() { + compile(0.1F, (instance, compiled) -> { + assertThat(instance).isEqualTo(0.1F); + assertThat(compiled.getSourceFile()).contains("0.1F"); + }); + } + + @Test + void generateWhenDouble() { + compile(0.2, (instance, compiled) -> { + assertThat(instance).isEqualTo(0.2); + assertThat(compiled.getSourceFile()).contains("(double) 0.2"); + }); + } + + @Test + void generateWhenChar() { + compile('a', (instance, compiled) -> { + assertThat(instance).isEqualTo('a'); + assertThat(compiled.getSourceFile()).contains("'a'"); + }); + } + + @Test + void generateWhenSimpleEscapedCharReturnsEscaped() { + testEscaped('\b', "'\\b'"); + testEscaped('\t', "'\\t'"); + testEscaped('\n', "'\\n'"); + testEscaped('\f', "'\\f'"); + testEscaped('\r', "'\\r'"); + testEscaped('\"', "'\"'"); + testEscaped('\'', "'\\''"); + testEscaped('\\', "'\\\\'"); + } + + @Test + void generatedWhenUnicodeEscapedCharReturnsEscaped() { + testEscaped('\u007f', "'\\u007f'"); + } + + private void testEscaped(char value, String expectedSourceContent) { + compile(value, (instance, compiled) -> { + assertThat(instance).isEqualTo(value); + assertThat(compiled.getSourceFile()).contains(expectedSourceContent); + }); + } + + } + + @Nested + class StringTests { + + @Test + void generateWhenString() { + compile("test\n", (instance, compiled) -> { + assertThat(instance).isEqualTo("test\n"); + assertThat(compiled.getSourceFile()).contains("\n"); + }); + } + + } + + @Nested + class CharsetTests { + + @Test + void generateWhenCharset() { + compile(StandardCharsets.UTF_8, (instance, compiled) -> { + assertThat(instance).isEqualTo(Charset.forName("UTF-8")); + assertThat(compiled.getSourceFile()).contains("\"UTF-8\""); + }); + } + + } + + @Nested + class EnumTests { + + @Test + void generateWhenEnum() { + compile(ChronoUnit.DAYS, (instance, compiled) -> { + assertThat(instance).isEqualTo(ChronoUnit.DAYS); + assertThat(compiled.getSourceFile()).contains("ChronoUnit.DAYS"); + }); + } + + @Test + void generateWhenEnumWithClassBody() { + compile(EnumWithClassBody.TWO, (instance, compiled) -> { + assertThat(instance).isEqualTo(EnumWithClassBody.TWO); + assertThat(compiled.getSourceFile()).contains("EnumWithClassBody.TWO"); + }); + } + + } + + @Nested + class ClassTests { + + @Test + void generateWhenClass() { + compile(InputStream.class, (instance, compiled) -> assertThat(instance) + .isEqualTo(InputStream.class)); + } + + @Test + void generateWhenCglibClass() { + compile(ExampleClass$$GeneratedBy.class, (instance, + compiled) -> assertThat(instance).isEqualTo(ExampleClass.class)); + } + + } + + @Nested + class ResolvableTypeTests { + + @Test + void generateWhenSimpleResolvableType() { + ResolvableType resolvableType = ResolvableType.forClass(String.class); + compile(resolvableType, (instance, compiled) -> assertThat(instance) + .isEqualTo(resolvableType)); + } + + @Test + void generateWhenNoneResolvableType() { + ResolvableType resolvableType = ResolvableType.NONE; + compile(resolvableType, (instance, compiled) -> { + assertThat(instance).isEqualTo(resolvableType); + assertThat(compiled.getSourceFile()).contains("ResolvableType.NONE"); + }); + } + + @Test + void generateWhenGenericResolvableType() { + ResolvableType resolvableType = ResolvableType + .forClassWithGenerics(List.class, String.class); + compile(resolvableType, (instance, compiled) -> assertThat(instance) + .isEqualTo(resolvableType)); + } + + @Test + void generateWhenNestedGenericResolvableType() { + ResolvableType stringList = ResolvableType.forClassWithGenerics(List.class, + String.class); + ResolvableType resolvableType = ResolvableType.forClassWithGenerics(Map.class, + ResolvableType.forClass(Integer.class), stringList); + compile(resolvableType, (instance, compiled) -> assertThat(instance) + .isEqualTo(resolvableType)); + } + + } + + @Nested + class ArrayTests { + + @Test + void generateWhenPrimitiveArray() { + byte[] bytes = { 0, 1, 2 }; + compile(bytes, (instance, compiler) -> { + assertThat(instance).isEqualTo(bytes); + assertThat(compiler.getSourceFile()).contains("new byte[]"); + }); + } + + @Test + void generateWhenWrapperArray() { + Byte[] bytes = { 0, 1, 2 }; + compile(bytes, (instance, compiler) -> { + assertThat(instance).isEqualTo(bytes); + assertThat(compiler.getSourceFile()).contains("new Byte[]"); + }); + } + + @Test + void generateWhenClassArray() { + Class[] classes = new Class[] { InputStream.class, OutputStream.class }; + compile(classes, (instance, compiler) -> { + assertThat(instance).isEqualTo(classes); + assertThat(compiler.getSourceFile()).contains("new Class[]"); + }); + } + + } + + @Nested + class ManagedListTests { + + @Test + void generateWhenStringManagedList() { + ManagedList list = new ManagedList<>(); + list.add("a"); + list.add("b"); + list.add("c"); + compile(list, (instance, compiler) -> assertThat(instance).isEqualTo(list) + .isInstanceOf(ManagedList.class)); + } + + @Test + void generateWhenEmptyManagedList() { + ManagedList list = new ManagedList<>(); + compile(list, (instance, compiler) -> assertThat(instance).isEqualTo(list) + .isInstanceOf(ManagedList.class)); + } + + } + + @Nested + class ManagedSetTests { + + @Test + void generateWhenStringManagedSet() { + ManagedSet set = new ManagedSet<>(); + set.add("a"); + set.add("b"); + set.add("c"); + compile(set, (instance, compiler) -> assertThat(instance).isEqualTo(set) + .isInstanceOf(ManagedSet.class)); + } + + @Test + void generateWhenEmptyManagedSet() { + ManagedSet set = new ManagedSet<>(); + compile(set, (instance, compiler) -> assertThat(instance).isEqualTo(set) + .isInstanceOf(ManagedSet.class)); + } + + } + + @Nested + class ManagedMapTests { + + @Test + void generateWhenManagedMap() { + ManagedMap map = new ManagedMap<>(); + map.put("k1", "v1"); + map.put("k2", "v2"); + compile(map, (instance, compiler) -> assertThat(instance).isEqualTo(map) + .isInstanceOf(ManagedMap.class)); + } + + @Test + void generateWhenEmptyManagedMap() { + ManagedMap map = new ManagedMap<>(); + compile(map, (instance, compiler) -> assertThat(instance).isEqualTo(map) + .isInstanceOf(ManagedMap.class)); + } + + } + + @Nested + class ListTests { + + @Test + void generateWhenStringList() { + List list = List.of("a", "b", "c"); + compile(list, (instance, compiler) -> assertThat(instance).isEqualTo(list) + .isNotInstanceOf(ManagedList.class)); + } + + @Test + void generateWhenEmptyList() { + List list = List.of(); + compile(list, (instance, compiler) -> { + assertThat(instance).isEqualTo(list); + assertThat(compiler.getSourceFile()).contains("Collections.emptyList();"); + }); + } + + } + + @Nested + class SetTests { + + @Test + void generateWhenStringSet() { + Set set = Set.of("a", "b", "c"); + compile(set, (instance, compiler) -> assertThat(instance).isEqualTo(set) + .isNotInstanceOf(ManagedSet.class)); + } + + @Test + void generateWhenEmptySet() { + Set set = Set.of(); + compile(set, (instance, compiler) -> { + assertThat(instance).isEqualTo(set); + assertThat(compiler.getSourceFile()).contains("Collections.emptySet();"); + }); + } + + @Test + void generateWhenLinkedHashSet() { + Set set = new LinkedHashSet<>(List.of("a", "b", "c")); + compile(set, (instance, compiler) -> { + assertThat(instance).isEqualTo(set).isInstanceOf(LinkedHashSet.class); + assertThat(compiler.getSourceFile()) + .contains("new LinkedHashSet(List.of("); + }); + } + + } + + @Nested + class MapTests { + + @Test + void generateWhenSmallMap() { + Map map = Map.of("k1", "v1", "k2", "v2"); + compile(map, (instance, compiler) -> { + assertThat(instance).isEqualTo(map); + assertThat(compiler.getSourceFile()).contains("Map.of("); + }); + } + + @Test + void generateWhenMapWithOverTenElements() { + Map map = new HashMap<>(); + for (int i = 1; i <= 11; i++) { + map.put("k" + i, "v" + i); + } + compile(map, (instance, compiler) -> { + assertThat(instance).isEqualTo(map); + assertThat(compiler.getSourceFile()).contains("Map.ofEntries("); + }); + } + + @Test + void generateWhenLinkedHashMap() { + Map map = new LinkedHashMap<>(); + map.put("a", "A"); + map.put("b", "B"); + map.put("c", "C"); + compile(map, (instance, compiler) -> { + assertThat(instance).isEqualTo(map).isInstanceOf(LinkedHashMap.class); + assertThat(compiler.getSourceFile()).contains("getMap()"); + }); + } + + } + + @Nested + class BeanReferenceTests { + + @Test + void generatedWhenBeanNameReference() { + RuntimeBeanNameReference beanReference = new RuntimeBeanNameReference("test"); + compile(beanReference, (instance, compiler) -> { + RuntimeBeanReference actual = (RuntimeBeanReference) instance; + assertThat(actual.getBeanName()).isEqualTo(beanReference.getBeanName()); + }); + } + + @Test + void generatedWhenBeanReferenceByName() { + RuntimeBeanReference beanReference = new RuntimeBeanReference("test"); + compile(beanReference, (instance, compiler) -> { + RuntimeBeanReference actual = (RuntimeBeanReference) instance; + assertThat(actual.getBeanName()).isEqualTo(beanReference.getBeanName()); + assertThat(actual.getBeanType()).isEqualTo(beanReference.getBeanType()); + }); + } + + @Test + void generatedWhenBeanReferenceByType() { + BeanReference beanReference = new RuntimeBeanReference(String.class); + compile(beanReference, (instance, compiler) -> { + RuntimeBeanReference actual = (RuntimeBeanReference) instance; + assertThat(actual.getBeanType()).isEqualTo(String.class); + }); + } + + } + + @Nested + static class ExceptionTests { + + @Test + void generateWhenUnsupportedDataTypeThrowsException() { + SampleValue sampleValue = new SampleValue("one"); + assertThatIllegalArgumentException().isThrownBy(() -> generateCode(sampleValue)) + .withMessageContaining("Failed to generate code for") + .withMessageContaining(sampleValue.toString()) + .withMessageContaining(SampleValue.class.getName()) + .havingCause() + .withMessageContaining("Code generation does not support") + .withMessageContaining(SampleValue.class.getName()); + } + + @Test + void generateWhenListOfUnsupportedElement() { + SampleValue one = new SampleValue("one"); + SampleValue two = new SampleValue("two"); + List list = List.of(one, two); + assertThatIllegalArgumentException().isThrownBy(() -> generateCode(list)) + .withMessageContaining("Failed to generate code for") + .withMessageContaining(list.toString()) + .withMessageContaining(list.getClass().getName()) + .havingCause() + .withMessageContaining("Failed to generate code for") + .withMessageContaining(one.toString()) + .withMessageContaining("?") + .havingCause() + .withMessageContaining("Code generation does not support ?"); + } + + private void generateCode(Object value) { + TestGenerationContext context = new TestGenerationContext(); + GeneratedClass generatedClass = context.getGeneratedClasses() + .addForFeature("Test", type -> {}); + createPropertyValuesCodeGenerator(generatedClass).generateCode(value); + } + + record SampleValue(String name) {} + + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanInstanceSupplierTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanInstanceSupplierTests.java new file mode 100644 index 000000000000..85553b92cd8b --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanInstanceSupplierTests.java @@ -0,0 +1,933 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +import java.io.InputStream; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Executable; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.support.AnnotationConsumer; + +import org.springframework.beans.factory.BeanCurrentlyInCreationException; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.UnsatisfiedDependencyException; +import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor; +import org.springframework.beans.factory.aot.BeanInstanceSupplierTests.Enclosing.InnerSingleArgConstructor; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueHolder; +import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.InstanceSupplier; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.core.env.Environment; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.ResourceLoader; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.function.ThrowingBiFunction; +import org.springframework.util.function.ThrowingFunction; +import org.springframework.util.function.ThrowingSupplier; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.entry; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link BeanInstanceSupplier}. + * + * @author Phillip Webb + * @author Stephane Nicoll + */ +class BeanInstanceSupplierTests { + + private final DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + + @Test + void forConstructorWhenParameterTypesIsNullThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> BeanInstanceSupplier + .forConstructor((Class[]) null)) + .withMessage("'parameterTypes' must not be null"); + } + + @Test + void forConstructorWhenParameterTypesContainsNullThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> BeanInstanceSupplier + .forConstructor(String.class, null)) + .withMessage("'parameterTypes' must not contain null elements"); + } + + @Test + void forConstructorWhenNotFoundThrowsException() { + BeanInstanceSupplier resolver = BeanInstanceSupplier + .forConstructor(InputStream.class); + Source source = new Source(SingleArgConstructor.class, resolver); + RegisteredBean registerBean = source.registerBean(this.beanFactory); + assertThatIllegalArgumentException() + .isThrownBy(() -> resolver.get(registerBean)).withMessage( + "Constructor with parameter types [java.io.InputStream] cannot be found on " + + SingleArgConstructor.class.getName()); + } + + @Test + void forConstructorReturnsNullFactoryMethod() { + BeanInstanceSupplier resolver = BeanInstanceSupplier.forConstructor(String.class); + assertThat(resolver.getFactoryMethod()).isNull(); + } + + @Test + void forFactoryMethodWhenDeclaringClassIsNullThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> BeanInstanceSupplier + .forFactoryMethod(null, "test")) + .withMessage("'declaringClass' must not be null"); + } + + @Test + void forFactoryMethodWhenNameIsEmptyThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> BeanInstanceSupplier + .forFactoryMethod(SingleArgFactory.class, "")) + .withMessage("'methodName' must not be empty"); + } + + @Test + void forFactoryMethodWhenParameterTypesIsNullThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy( + () -> BeanInstanceSupplier.forFactoryMethod( + SingleArgFactory.class, "single", (Class[]) null)) + .withMessage("'parameterTypes' must not be null"); + } + + @Test + void forFactoryMethodWhenParameterTypesContainsNullThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy( + () -> BeanInstanceSupplier.forFactoryMethod( + SingleArgFactory.class, "single", String.class, null)) + .withMessage("'parameterTypes' must not contain null elements"); + } + + @Test + void forFactoryMethodWhenNotFoundThrowsException() { + BeanInstanceSupplier resolver = BeanInstanceSupplier + .forFactoryMethod(SingleArgFactory.class, "single", InputStream.class); + Source source = new Source(String.class, resolver); + RegisteredBean registerBean = source.registerBean(this.beanFactory); + assertThatIllegalArgumentException() + .isThrownBy(() -> resolver.get(registerBean)).withMessage( + "Factory method 'single' with parameter types [java.io.InputStream] declared on class " + + SingleArgFactory.class.getName() + " cannot be found"); + } + + @Test + void forFactoryMethodReturnsFactoryMethod() { + BeanInstanceSupplier resolver = BeanInstanceSupplier + .forFactoryMethod(SingleArgFactory.class, "single", String.class); + Method factoryMethod = ReflectionUtils.findMethod(SingleArgFactory.class, "single", String.class); + assertThat(factoryMethod).isNotNull(); + assertThat(resolver.getFactoryMethod()).isEqualTo(factoryMethod); + } + + @Test + void withGeneratorWhenBiFunctionIsNullThrowsException() { + BeanInstanceSupplier resolver = BeanInstanceSupplier + .forConstructor(); + assertThatIllegalArgumentException() + .isThrownBy(() -> resolver.withGenerator( + (ThrowingBiFunction) null)) + .withMessage("'generator' must not be null"); + } + + @Test + void withGeneratorWhenFunctionIsNullThrowsException() { + BeanInstanceSupplier resolver = BeanInstanceSupplier + .forConstructor(); + assertThatIllegalArgumentException() + .isThrownBy(() -> resolver.withGenerator( + (ThrowingFunction) null)) + .withMessage("'generator' must not be null"); + } + + @Test + void withGeneratorWhenSupplierIsNullThrowsException() { + BeanInstanceSupplier resolver = BeanInstanceSupplier + .forConstructor(); + assertThatIllegalArgumentException() + .isThrownBy(() -> resolver.withGenerator( + (ThrowingSupplier) null)) + .withMessage("'generator' must not be null"); + } + + @Test + void getWithConstructorDoesNotSetResolvedFactoryMethod() throws Exception { + BeanInstanceSupplier resolver = BeanInstanceSupplier + .forConstructor(String.class); + this.beanFactory.registerSingleton("one", "1"); + Source source = new Source(SingleArgConstructor.class, resolver); + RegisteredBean registerBean = source.registerBean(this.beanFactory); + assertThat(registerBean.getMergedBeanDefinition().getResolvedFactoryMethod()).isNull(); + source.getResolver().get(registerBean); + assertThat(registerBean.getMergedBeanDefinition().getResolvedFactoryMethod()).isNull(); + } + + @Test + void getWithFactoryMethodSetsResolvedFactoryMethod() { + Method factoryMethod = ReflectionUtils.findMethod(SingleArgFactory.class, "single", String.class); + assertThat(factoryMethod).isNotNull(); + BeanInstanceSupplier resolver = BeanInstanceSupplier + .forFactoryMethod(SingleArgFactory.class, "single", String.class); + RootBeanDefinition beanDefinition = new RootBeanDefinition(String.class); + assertThat(beanDefinition.getResolvedFactoryMethod()).isNull(); + beanDefinition.setInstanceSupplier(resolver); + assertThat(beanDefinition.getResolvedFactoryMethod()).isEqualTo(factoryMethod); + } + + @Test + void getWithGeneratorCallsBiFunction() throws Exception { + BeanRegistrar registrar = new BeanRegistrar(SingleArgConstructor.class); + this.beanFactory.registerSingleton("one", "1"); + RegisteredBean registerBean = registrar.registerBean(this.beanFactory); + List result = new ArrayList<>(); + BeanInstanceSupplier resolver = BeanInstanceSupplier + .forConstructor(String.class) + .withGenerator((registeredBean, args) -> result.add(args)); + resolver.get(registerBean); + assertThat(result).hasSize(1); + assertThat(((AutowiredArguments) result.get(0)).toArray()).containsExactly("1"); + } + + @Test + void getWithGeneratorCallsFunction() throws Exception { + BeanRegistrar registrar = new BeanRegistrar(SingleArgConstructor.class); + this.beanFactory.registerSingleton("one", "1"); + RegisteredBean registerBean = registrar.registerBean(this.beanFactory); + BeanInstanceSupplier resolver = BeanInstanceSupplier + .forConstructor(String.class) + .withGenerator(registeredBean -> "1"); + assertThat(resolver.get(registerBean)).isInstanceOf(String.class).isEqualTo("1"); + } + + @Test + void getWithGeneratorCallsSupplier() throws Exception { + BeanRegistrar registrar = new BeanRegistrar(SingleArgConstructor.class); + this.beanFactory.registerSingleton("one", "1"); + RegisteredBean registerBean = registrar.registerBean(this.beanFactory); + BeanInstanceSupplier resolver = BeanInstanceSupplier + .forConstructor(String.class) + .withGenerator(() -> "1"); + assertThat(resolver.get(registerBean)).isInstanceOf(String.class).isEqualTo("1"); + } + + @Test + void getWhenRegisteredBeanIsNullThrowsException() { + BeanInstanceSupplier resolver = BeanInstanceSupplier + .forConstructor(String.class); + assertThatIllegalArgumentException().isThrownBy(() -> resolver.get((RegisteredBean) null)) + .withMessage("'registeredBean' must not be null"); + } + + @ParameterizedResolverTest(Sources.SINGLE_ARG) + void getWithNoGeneratorUsesReflection(Source source) throws Exception { + this.beanFactory.registerSingleton("one", "1"); + this.beanFactory.registerSingleton("testFactory", new SingleArgFactory()); + RegisteredBean registerBean = source.registerBean(this.beanFactory); + Object instance = source.getResolver().get(registerBean); + if (instance instanceof SingleArgConstructor singleArgConstructor) { + instance = singleArgConstructor.getString(); + } + assertThat(instance).isEqualTo("1"); + } + + @ParameterizedResolverTest(Sources.INNER_CLASS_SINGLE_ARG) + void getNestedWithNoGeneratorUsesReflection(Source source) throws Exception { + this.beanFactory.registerSingleton("one", "1"); + this.beanFactory.registerSingleton("testFactory", + new Enclosing().new InnerSingleArgFactory()); + RegisteredBean registerBean = source.registerBean(this.beanFactory); + Object instance = source.getResolver().get(registerBean); + if (instance instanceof InnerSingleArgConstructor innerSingleArgConstructor) { + instance = innerSingleArgConstructor.getString(); + } + assertThat(instance).isEqualTo("1"); + } + + @Test + void resolveArgumentsWithNoArgConstructor() { + RootBeanDefinition beanDefinition = new RootBeanDefinition( + NoArgConstructor.class); + this.beanFactory.registerBeanDefinition("test", beanDefinition); + RegisteredBean registeredBean = RegisteredBean.of(this.beanFactory, "test"); + AutowiredArguments resolved = BeanInstanceSupplier + .forConstructor().resolveArguments(registeredBean); + assertThat(resolved.toArray()).isEmpty(); + } + + @ParameterizedResolverTest(Sources.SINGLE_ARG) + void resolveArgumentsWithSingleArgConstructor(Source source) { + this.beanFactory.registerSingleton("one", "1"); + RegisteredBean registeredBean = source.registerBean(this.beanFactory); + assertThat(source.getResolver().resolveArguments(registeredBean).toArray()) + .containsExactly("1"); + } + + @ParameterizedResolverTest(Sources.INNER_CLASS_SINGLE_ARG) + void resolveArgumentsWithNestedSingleArgConstructor(Source source) { + this.beanFactory.registerSingleton("one", "1"); + RegisteredBean registeredBean = source.registerBean(this.beanFactory); + assertThat(source.getResolver().resolveArguments(registeredBean).toArray()) + .containsExactly("1"); + } + + @ParameterizedResolverTest(Sources.SINGLE_ARG) + void resolveArgumentsWithRequiredDependencyNotPresentThrowsUnsatisfiedDependencyException( + Source source) { + RegisteredBean registeredBean = source.registerBean(this.beanFactory); + assertThatExceptionOfType(UnsatisfiedDependencyException.class) + .isThrownBy(() -> source.getResolver().resolveArguments(registeredBean)) + .satisfies(ex -> { + assertThat(ex.getBeanName()).isEqualTo("testBean"); + assertThat(ex.getInjectionPoint()).isNotNull(); + assertThat(ex.getInjectionPoint().getMember()) + .isEqualTo(source.lookupExecutable(registeredBean)); + }); + } + + @Test + void resolveArgumentsInInstanceSupplierWithSelfReferenceThrowsException() { + // SingleArgFactory.single(...) expects a String to be injected + // and our own bean is a String, so it's a valid candidate + this.beanFactory.addBeanPostProcessor(new AutowiredAnnotationBeanPostProcessor()); + RootBeanDefinition beanDefinition = new RootBeanDefinition(String.class); + beanDefinition.setInstanceSupplier(InstanceSupplier.of(registeredBean -> { + AutowiredArguments args = BeanInstanceSupplier + .forFactoryMethod(SingleArgFactory.class, "single", String.class) + .resolveArguments(registeredBean); + return new SingleArgFactory().single(args.get(0)); + })); + this.beanFactory.registerBeanDefinition("test", beanDefinition); + assertThatExceptionOfType(UnsatisfiedDependencyException.class) + .isThrownBy(() -> this.beanFactory.getBean("test")); + } + + @ParameterizedResolverTest(Sources.ARRAY_OF_BEANS) + void resolveArgumentsWithArrayOfBeans(Source source) { + this.beanFactory.registerSingleton("one", "1"); + this.beanFactory.registerSingleton("two", "2"); + RegisteredBean registerBean = source.registerBean(this.beanFactory); + AutowiredArguments arguments = source.getResolver().resolveArguments(registerBean); + assertThat(arguments.toArray()).hasSize(1); + assertThat((Object[]) arguments.get(0)).containsExactly("1", "2"); + } + + @ParameterizedResolverTest(Sources.ARRAY_OF_BEANS) + void resolveArgumentsWithRequiredArrayOfBeansInjectEmptyArray(Source source) { + RegisteredBean registerBean = source.registerBean(this.beanFactory); + AutowiredArguments arguments = source.getResolver().resolveArguments(registerBean); + assertThat(arguments.toArray()).hasSize(1); + assertThat((Object[]) arguments.get(0)).isEmpty(); + } + + @ParameterizedResolverTest(Sources.LIST_OF_BEANS) + void resolveArgumentsWithListOfBeans(Source source) { + this.beanFactory.registerSingleton("one", "1"); + this.beanFactory.registerSingleton("two", "2"); + RegisteredBean registerBean = source.registerBean(this.beanFactory); + AutowiredArguments arguments = source.getResolver().resolveArguments(registerBean); + assertThat(arguments.toArray()).hasSize(1); + assertThat(arguments.getObject(0)).isInstanceOf(List.class).asList() + .containsExactly("1", "2"); + } + + @ParameterizedResolverTest(Sources.LIST_OF_BEANS) + void resolveArgumentsWithRequiredListOfBeansInjectEmptyList(Source source) { + RegisteredBean registerBean = source.registerBean(this.beanFactory); + AutowiredArguments arguments = source.getResolver().resolveArguments(registerBean); + assertThat(arguments.toArray()).hasSize(1); + assertThat((List) arguments.get(0)).isEmpty(); + } + + @ParameterizedResolverTest(Sources.SET_OF_BEANS) + @SuppressWarnings("unchecked") + void resolveArgumentsWithSetOfBeans(Source source) { + this.beanFactory.registerSingleton("one", "1"); + this.beanFactory.registerSingleton("two", "2"); + RegisteredBean registerBean = source.registerBean(this.beanFactory); + AutowiredArguments arguments = source.getResolver().resolveArguments(registerBean); + assertThat(arguments.toArray()).hasSize(1); + assertThat((Set) arguments.get(0)).containsExactly("1", "2"); + } + + @ParameterizedResolverTest(Sources.SET_OF_BEANS) + void resolveArgumentsWithRequiredSetOfBeansInjectEmptySet(Source source) { + RegisteredBean registerBean = source.registerBean(this.beanFactory); + AutowiredArguments arguments = source.getResolver().resolveArguments(registerBean); + assertThat(arguments.toArray()).hasSize(1); + assertThat((Set) arguments.get(0)).isEmpty(); + } + + @ParameterizedResolverTest(Sources.MAP_OF_BEANS) + @SuppressWarnings("unchecked") + void resolveArgumentsWithMapOfBeans(Source source) { + this.beanFactory.registerSingleton("one", "1"); + this.beanFactory.registerSingleton("two", "2"); + RegisteredBean registerBean = source.registerBean(this.beanFactory); + AutowiredArguments arguments = source.getResolver().resolveArguments(registerBean); + assertThat(arguments.toArray()).hasSize(1); + assertThat((Map) arguments.get(0)) + .containsExactly(entry("one", "1"), entry("two", "2")); + } + + @ParameterizedResolverTest(Sources.MAP_OF_BEANS) + void resolveArgumentsWithRequiredMapOfBeansInjectEmptySet(Source source) { + RegisteredBean registerBean = source.registerBean(this.beanFactory); + AutowiredArguments arguments = source.getResolver().resolveArguments(registerBean); + assertThat(arguments.toArray()).hasSize(1); + assertThat((Map) arguments.get(0)).isEmpty(); + } + + @ParameterizedResolverTest(Sources.MULTI_ARGS) + void resolveArgumentsWithMultiArgsConstructor(Source source) { + ResourceLoader resourceLoader = new DefaultResourceLoader(); + Environment environment = mock(Environment.class); + this.beanFactory.registerResolvableDependency(ResourceLoader.class, + resourceLoader); + this.beanFactory.registerSingleton("environment", environment); + this.beanFactory.registerSingleton("one", "1"); + RegisteredBean registerBean = source.registerBean(this.beanFactory); + AutowiredArguments arguments = source.getResolver().resolveArguments(registerBean); + assertThat(arguments.toArray()).hasSize(3); + assertThat(arguments.getObject(0)).isEqualTo(resourceLoader); + assertThat(arguments.getObject(1)).isEqualTo(environment); + assertThat(((ObjectProvider) arguments.get(2)).getIfAvailable()) + .isEqualTo("1"); + } + + @ParameterizedResolverTest(Sources.MIXED_ARGS) + void resolveArgumentsWithMixedArgsConstructorWithUserValue(Source source) { + ResourceLoader resourceLoader = new DefaultResourceLoader(); + Environment environment = mock(Environment.class); + this.beanFactory.registerResolvableDependency(ResourceLoader.class, + resourceLoader); + this.beanFactory.registerSingleton("environment", environment); + RegisteredBean registerBean = source.registerBean(this.beanFactory, + beanDefinition -> { + beanDefinition + .setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR); + beanDefinition.getConstructorArgumentValues() + .addIndexedArgumentValue(1, "user-value"); + }); + AutowiredArguments arguments = source.getResolver().resolveArguments(registerBean); + assertThat(arguments.toArray()).hasSize(3); + assertThat(arguments.getObject(0)).isEqualTo(resourceLoader); + assertThat(arguments.getObject(1)).isEqualTo("user-value"); + assertThat(arguments.getObject(2)).isEqualTo(environment); + } + + @ParameterizedResolverTest(Sources.MIXED_ARGS) + void resolveArgumentsWithMixedArgsConstructorWithUserBeanReference(Source source) { + ResourceLoader resourceLoader = new DefaultResourceLoader(); + Environment environment = mock(Environment.class); + this.beanFactory.registerResolvableDependency(ResourceLoader.class, + resourceLoader); + this.beanFactory.registerSingleton("environment", environment); + this.beanFactory.registerSingleton("one", "1"); + this.beanFactory.registerSingleton("two", "2"); + RegisteredBean registerBean = source.registerBean(this.beanFactory, + beanDefinition -> { + beanDefinition + .setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR); + beanDefinition.getConstructorArgumentValues() + .addIndexedArgumentValue(1, new RuntimeBeanReference("two")); + }); + AutowiredArguments arguments = source.getResolver().resolveArguments(registerBean); + assertThat(arguments.toArray()).hasSize(3); + assertThat(arguments.getObject(0)).isEqualTo(resourceLoader); + assertThat(arguments.getObject(1)).isEqualTo("2"); + assertThat(arguments.getObject(2)).isEqualTo(environment); + } + + @Test + void resolveArgumentsWithUserValueWithTypeConversionRequired() { + Source source = new Source(CharDependency.class, + BeanInstanceSupplier.forConstructor(char.class)); + RegisteredBean registerBean = source.registerBean(this.beanFactory, + beanDefinition -> { + beanDefinition + .setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR); + beanDefinition.getConstructorArgumentValues() + .addIndexedArgumentValue(0, "\\"); + }); + AutowiredArguments arguments = source.getResolver().resolveArguments(registerBean); + assertThat(arguments.toArray()).hasSize(1); + assertThat(arguments.getObject(0)).isInstanceOf(Character.class).isEqualTo('\\'); + } + + @ParameterizedResolverTest(Sources.SINGLE_ARG) + void resolveArgumentsWithUserValueWithBeanReference(Source source) { + this.beanFactory.registerSingleton("stringBean", "string"); + RegisteredBean registerBean = source.registerBean(this.beanFactory, + beanDefinition -> beanDefinition.getConstructorArgumentValues() + .addIndexedArgumentValue(0, + new RuntimeBeanReference("stringBean"))); + AutowiredArguments arguments = source.getResolver().resolveArguments(registerBean); + assertThat(arguments.toArray()).hasSize(1); + assertThat(arguments.getObject(0)).isEqualTo("string"); + } + + @ParameterizedResolverTest(Sources.SINGLE_ARG) + void resolveArgumentsWithUserValueWithBeanDefinition(Source source) { + AbstractBeanDefinition userValue = BeanDefinitionBuilder + .rootBeanDefinition(String.class, () -> "string").getBeanDefinition(); + RegisteredBean registerBean = source.registerBean(this.beanFactory, + beanDefinition -> beanDefinition.getConstructorArgumentValues() + .addIndexedArgumentValue(0, userValue)); + AutowiredArguments arguments = source.getResolver().resolveArguments(registerBean); + assertThat(arguments.toArray()).hasSize(1); + assertThat(arguments.getObject(0)).isEqualTo("string"); + } + + @ParameterizedResolverTest(Sources.SINGLE_ARG) + void resolveArgumentsWithUserValueThatIsAlreadyResolved(Source source) { + RegisteredBean registerBean = source.registerBean(this.beanFactory); + BeanDefinition mergedBeanDefinition = this.beanFactory + .getMergedBeanDefinition("testBean"); + ValueHolder valueHolder = new ValueHolder('a'); + valueHolder.setConvertedValue("this is an a"); + mergedBeanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0, + valueHolder); + AutowiredArguments arguments = source.getResolver().resolveArguments(registerBean); + assertThat(arguments.toArray()).hasSize(1); + assertThat(arguments.getObject(0)).isEqualTo("this is an a"); + } + + @Test + void resolveArgumentsWhenUsingShortcutsInjectsDirectly() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory() { + + @Override + protected Map findAutowireCandidates(String beanName, + Class requiredType, DependencyDescriptor descriptor) { + throw new AssertionError("Should be shortcut"); + } + + }; + BeanInstanceSupplier resolver = BeanInstanceSupplier + .forConstructor(String.class); + Source source = new Source(String.class, resolver); + beanFactory.registerSingleton("one", "1"); + RegisteredBean registeredBean = source.registerBean(beanFactory); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> resolver.resolveArguments(registeredBean)); + assertThat(resolver.withShortcuts("one").resolveArguments(registeredBean).toArray()) + .containsExactly("1"); + } + + @Test + void resolveArgumentsRegistersDependantBeans() { + BeanInstanceSupplier resolver = BeanInstanceSupplier + .forConstructor(String.class); + Source source = new Source(SingleArgConstructor.class, resolver); + this.beanFactory.registerSingleton("one", "1"); + RegisteredBean registeredBean = source.registerBean(this.beanFactory); + resolver.resolveArguments(registeredBean); + assertThat(this.beanFactory.getDependentBeans("one")).containsExactly("testBean"); + } + + /** + * Parameterized test backed by a {@link Sources}. + */ + @Retention(RetentionPolicy.RUNTIME) + @ParameterizedTest + @ArgumentsSource(SourcesArguments.class) + @interface ParameterizedResolverTest { + + Sources value(); + + } + + /** + * {@link ArgumentsProvider} delegating to the {@link Sources}. + */ + static class SourcesArguments + implements ArgumentsProvider, AnnotationConsumer { + + private Sources source; + + @Override + public void accept(ParameterizedResolverTest annotation) { + this.source = annotation.value(); + } + + @Override + public Stream provideArguments(ExtensionContext context) { + return this.source.provideArguments(context); + } + + } + + /** + * Sources for parameterized tests. + */ + enum Sources { + + SINGLE_ARG { + @Override + protected void setup() { + add(SingleArgConstructor.class, BeanInstanceSupplier + .forConstructor(String.class)); + add(String.class, + BeanInstanceSupplier.forFactoryMethod( + SingleArgFactory.class, "single", String.class)); + } + + }, + + INNER_CLASS_SINGLE_ARG { + @Override + protected void setup() { + add(Enclosing.InnerSingleArgConstructor.class, + BeanInstanceSupplier + .forConstructor(String.class)); + add(String.class, + BeanInstanceSupplier.forFactoryMethod( + Enclosing.InnerSingleArgFactory.class, "single", + String.class)); + } + + }, + + ARRAY_OF_BEANS { + @Override + protected void setup() { + add(BeansCollectionConstructor.class, + BeanInstanceSupplier + .forConstructor(String[].class)); + add(String.class, + BeanInstanceSupplier.forFactoryMethod( + BeansCollectionFactory.class, "array", String[].class)); + } + + }, + + LIST_OF_BEANS { + @Override + protected void setup() { + add(BeansCollectionConstructor.class, + BeanInstanceSupplier + .forConstructor(List.class)); + add(String.class, + BeanInstanceSupplier.forFactoryMethod( + BeansCollectionFactory.class, "list", List.class)); + } + + }, + + SET_OF_BEANS { + @Override + protected void setup() { + add(BeansCollectionConstructor.class, + BeanInstanceSupplier + .forConstructor(Set.class)); + add(String.class, + BeanInstanceSupplier.forFactoryMethod( + BeansCollectionFactory.class, "set", Set.class)); + } + + }, + + MAP_OF_BEANS { + @Override + protected void setup() { + add(BeansCollectionConstructor.class, + BeanInstanceSupplier + .forConstructor(Map.class)); + add(String.class, + BeanInstanceSupplier.forFactoryMethod( + BeansCollectionFactory.class, "map", Map.class)); + } + + }, + + MULTI_ARGS { + @Override + protected void setup() { + add(MultiArgsConstructor.class, + BeanInstanceSupplier.forConstructor( + ResourceLoader.class, Environment.class, + ObjectProvider.class)); + add(String.class, + BeanInstanceSupplier.forFactoryMethod( + MultiArgsFactory.class, "multiArgs", ResourceLoader.class, + Environment.class, ObjectProvider.class)); + } + + }, + + MIXED_ARGS { + @Override + protected void setup() { + add(MixedArgsConstructor.class, + BeanInstanceSupplier.forConstructor( + ResourceLoader.class, String.class, Environment.class)); + add(String.class, + BeanInstanceSupplier.forFactoryMethod( + MixedArgsFactory.class, "mixedArgs", ResourceLoader.class, + String.class, Environment.class)); + } + + }; + + private final List arguments; + + Sources() { + this.arguments = new ArrayList<>(); + setup(); + } + + protected abstract void setup(); + + protected final void add(Class beanClass, + BeanInstanceSupplier resolver) { + this.arguments.add(Arguments.of(new Source(beanClass, resolver))); + } + + final Stream provideArguments(ExtensionContext context) { + return this.arguments.stream(); + } + + } + + static class BeanRegistrar { + + final Class beanClass; + + public BeanRegistrar(Class beanClass) { + this.beanClass = beanClass; + } + + RegisteredBean registerBean(DefaultListableBeanFactory beanFactory) { + return registerBean(beanFactory, beanDefinition -> { + }); + } + + RegisteredBean registerBean(DefaultListableBeanFactory beanFactory, + Consumer beanDefinitionCustomizer) { + String beanName = "testBean"; + RootBeanDefinition beanDefinition = new RootBeanDefinition(this.beanClass); + beanDefinition.setInstanceSupplier(() -> { + throw new BeanCurrentlyInCreationException(beanName); + }); + beanDefinitionCustomizer.accept(beanDefinition); + beanFactory.registerBeanDefinition(beanName, beanDefinition); + return RegisteredBean.of(beanFactory, beanName); + } + } + + static class Source extends BeanRegistrar { + + private final BeanInstanceSupplier resolver; + + public Source(Class beanClass, + BeanInstanceSupplier resolver) { + super(beanClass); + this.resolver = resolver; + } + + BeanInstanceSupplier getResolver() { + return this.resolver; + } + + Executable lookupExecutable(RegisteredBean registeredBean) { + return this.resolver.getLookup().get(registeredBean); + } + + @Override + public String toString() { + return this.resolver.getLookup() + " with bean class " + + ClassUtils.getShortName(this.beanClass); + } + + } + + static class NoArgConstructor { + + } + + static class SingleArgConstructor { + + private final String string; + + SingleArgConstructor(String string) { + this.string = string; + } + + String getString() { + return this.string; + } + + } + + static class SingleArgFactory { + + String single(String s) { + return s; + } + + } + + static class Enclosing { + + class InnerSingleArgConstructor { + + private final String string; + + InnerSingleArgConstructor(String string) { + this.string = string; + } + + String getString() { + return this.string; + } + + } + + class InnerSingleArgFactory { + + String single(String s) { + return s; + } + + } + + } + + static class BeansCollectionConstructor { + + public BeansCollectionConstructor(String[] beans) { + + } + + public BeansCollectionConstructor(List beans) { + + } + + public BeansCollectionConstructor(Set beans) { + + } + + public BeansCollectionConstructor(Map beans) { + + } + + } + + static class BeansCollectionFactory { + + public String array(String[] beans) { + return "test"; + } + + public String list(List beans) { + return "test"; + } + + public String set(Set beans) { + return "test"; + } + + public String map(Map beans) { + return "test"; + } + + } + + static class MultiArgsConstructor { + + public MultiArgsConstructor(ResourceLoader resourceLoader, + Environment environment, ObjectProvider provider) { + } + } + + static class MultiArgsFactory { + + String multiArgs(ResourceLoader resourceLoader, Environment environment, + ObjectProvider provider) { + return "test"; + } + } + + static class MixedArgsConstructor { + + public MixedArgsConstructor(ResourceLoader resourceLoader, String test, + Environment environment) { + + } + + } + + static class MixedArgsFactory { + + String mixedArgs(ResourceLoader resourceLoader, String test, + Environment environment) { + return "test"; + } + + } + + static class CharDependency { + + CharDependency(char escapeChar) { + } + + } + + interface MethodOnInterface { + + default String test() { + return "Test"; + } + + } + + static class MethodOnInterfaceImpl implements MethodOnInterface { + + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanRegistrationsAotContributionTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanRegistrationsAotContributionTests.java new file mode 100644 index 000000000000..6ed07a5fd53c --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanRegistrationsAotContributionTests.java @@ -0,0 +1,192 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import javax.lang.model.element.Modifier; + +import org.junit.jupiter.api.Test; + +import org.springframework.aot.generate.ClassNameGenerator; +import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.generate.MethodReference; +import org.springframework.aot.generate.MethodReference.ArgumentCodeGenerator; +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.beans.testfixture.beans.factory.aot.MockBeanFactoryInitializationCode; +import org.springframework.core.test.io.support.MockSpringFactoriesLoader; +import org.springframework.core.test.tools.Compiled; +import org.springframework.core.test.tools.SourceFile; +import org.springframework.core.test.tools.TestCompiler; +import org.springframework.javapoet.ClassName; +import org.springframework.javapoet.CodeBlock; +import org.springframework.javapoet.MethodSpec; +import org.springframework.javapoet.ParameterizedTypeName; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.beans.factory.aot.BeanRegistrationsAotContribution.Registration; + +/** + * Tests for {@link BeanRegistrationsAotContribution}. + * + * @author Phillip Webb + * @author Sebastien Deleuze + * @author Stephane Nicoll + */ +class BeanRegistrationsAotContributionTests { + + private final DefaultListableBeanFactory beanFactory; + + private final BeanDefinitionMethodGeneratorFactory methodGeneratorFactory; + + private TestGenerationContext generationContext; + + private MockBeanFactoryInitializationCode beanFactoryInitializationCode; + + + BeanRegistrationsAotContributionTests() { + MockSpringFactoriesLoader springFactoriesLoader = new MockSpringFactoriesLoader(); + this.beanFactory = new DefaultListableBeanFactory(); + this.methodGeneratorFactory = new BeanDefinitionMethodGeneratorFactory( + AotServices.factoriesAndBeans(springFactoriesLoader, this.beanFactory)); + this.generationContext = new TestGenerationContext(); + this.beanFactoryInitializationCode = new MockBeanFactoryInitializationCode(this.generationContext); + } + + + @Test + void applyToAppliesContribution() { + RegisteredBean registeredBean = registerBean( + new RootBeanDefinition(TestBean.class)); + BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator( + this.methodGeneratorFactory, registeredBean, null, + Collections.emptyList()); + BeanRegistrationsAotContribution contribution = createContribution(generator); + contribution.applyTo(this.generationContext, this.beanFactoryInitializationCode); + compile((consumer, compiled) -> { + DefaultListableBeanFactory freshBeanFactory = new DefaultListableBeanFactory(); + consumer.accept(freshBeanFactory); + assertThat(freshBeanFactory.getBean(TestBean.class)).isNotNull(); + }); + } + + @Test + void applyToAppliesContributionWithAliases() { + RegisteredBean registeredBean = registerBean( + new RootBeanDefinition(TestBean.class)); + BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator( + this.methodGeneratorFactory, registeredBean, null, + Collections.emptyList()); + BeanRegistrationsAotContribution contribution = createContribution(generator, "testAlias"); + contribution.applyTo(this.generationContext, this.beanFactoryInitializationCode); + compile((consumer, compiled) -> { + DefaultListableBeanFactory freshBeanFactory = new DefaultListableBeanFactory(); + consumer.accept(freshBeanFactory); + assertThat(freshBeanFactory.getAliases("testBean")).containsExactly("testAlias"); + }); + } + + @Test + void applyToWhenHasNameGeneratesPrefixedFeatureName() { + this.generationContext = new TestGenerationContext( + new ClassNameGenerator(TestGenerationContext.TEST_TARGET, "Management")); + this.beanFactoryInitializationCode = new MockBeanFactoryInitializationCode(this.generationContext); + RegisteredBean registeredBean = registerBean( + new RootBeanDefinition(TestBean.class)); + BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator( + this.methodGeneratorFactory, registeredBean, null, + Collections.emptyList()); + BeanRegistrationsAotContribution contribution = createContribution(generator); + contribution.applyTo(this.generationContext, this.beanFactoryInitializationCode); + compile((consumer, compiled) -> { + SourceFile sourceFile = compiled.getSourceFile(".*BeanDefinitions"); + assertThat(sourceFile.getClassName()).endsWith("__ManagementBeanDefinitions"); + }); + } + + @Test + void applyToCallsRegistrationsWithBeanRegistrationsCode() { + List beanRegistrationsCodes = new ArrayList<>(); + RegisteredBean registeredBean = registerBean( + new RootBeanDefinition(TestBean.class)); + BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator( + this.methodGeneratorFactory, registeredBean, null, + Collections.emptyList()) { + + @Override + MethodReference generateBeanDefinitionMethod( + GenerationContext generationContext, + BeanRegistrationsCode beanRegistrationsCode) { + beanRegistrationsCodes.add(beanRegistrationsCode); + return super.generateBeanDefinitionMethod(generationContext, + beanRegistrationsCode); + } + + }; + BeanRegistrationsAotContribution contribution = createContribution(generator); + contribution.applyTo(this.generationContext, this.beanFactoryInitializationCode); + assertThat(beanRegistrationsCodes).hasSize(1); + BeanRegistrationsCode actual = beanRegistrationsCodes.get(0); + assertThat(actual.getMethods()).isNotNull(); + } + + private RegisteredBean registerBean(RootBeanDefinition rootBeanDefinition) { + String beanName = "testBean"; + this.beanFactory.registerBeanDefinition(beanName, rootBeanDefinition); + return RegisteredBean.of(this.beanFactory, beanName); + } + + @SuppressWarnings({ "unchecked", "cast" }) + private void compile( + BiConsumer, Compiled> result) { + MethodReference beanRegistrationsMethodReference = this.beanFactoryInitializationCode + .getInitializers().get(0); + MethodReference aliasesMethodReference = this.beanFactoryInitializationCode + .getInitializers().get(1); + this.beanFactoryInitializationCode.getTypeBuilder().set(type -> { + ArgumentCodeGenerator beanFactory = ArgumentCodeGenerator.of(DefaultListableBeanFactory.class, "beanFactory"); + ClassName className = this.beanFactoryInitializationCode.getClassName(); + CodeBlock beanRegistrationsMethodInvocation = beanRegistrationsMethodReference.toInvokeCodeBlock(beanFactory, className); + CodeBlock aliasesMethodInvocation = aliasesMethodReference.toInvokeCodeBlock(beanFactory, className); + type.addModifiers(Modifier.PUBLIC); + type.addSuperinterface(ParameterizedTypeName.get(Consumer.class, DefaultListableBeanFactory.class)); + type.addMethod(MethodSpec.methodBuilder("accept").addModifiers(Modifier.PUBLIC) + .addParameter(DefaultListableBeanFactory.class, "beanFactory") + .addStatement(beanRegistrationsMethodInvocation) + .addStatement(aliasesMethodInvocation) + .build()); + }); + this.generationContext.writeGeneratedContent(); + TestCompiler.forSystem().with(this.generationContext).compile(compiled -> + result.accept(compiled.getInstance(Consumer.class), compiled)); + } + + private BeanRegistrationsAotContribution createContribution( + BeanDefinitionMethodGenerator methodGenerator,String... aliases) { + return new BeanRegistrationsAotContribution(Map.of("testBean", new Registration(methodGenerator, aliases))); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanRegistrationsAotProcessorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanRegistrationsAotProcessorTests.java new file mode 100644 index 000000000000..cb1e39f106db --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanRegistrationsAotProcessorTests.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.AnnotatedBean; +import org.springframework.beans.testfixture.beans.TestBean; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link BeanRegistrationsAotProcessor}. + * + * @author Phillip Webb + * @author Sebastien Deleuze + */ +class BeanRegistrationsAotProcessorTests { + + @Test + void beanRegistrationsAotProcessorIsRegistered() { + assertThat(AotServices.factoriesAndBeans(new DefaultListableBeanFactory()) + .load(BeanFactoryInitializationAotProcessor.class)) + .anyMatch(BeanRegistrationsAotProcessor.class::isInstance); + } + + @Test + void processAheadOfTimeReturnsBeanRegistrationsAotContributionWithRegistrations() { + BeanRegistrationsAotProcessor processor = new BeanRegistrationsAotProcessor(); + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerBeanDefinition("b1", new RootBeanDefinition(TestBean.class)); + beanFactory.registerBeanDefinition("b2", + new RootBeanDefinition(AnnotatedBean.class)); + BeanRegistrationsAotContribution contribution = processor + .processAheadOfTime(beanFactory); + assertThat(contribution).extracting("registrations") + .asInstanceOf(InstanceOfAssertFactories.MAP).containsKeys("b1", "b2"); + } + + @Test + void processAheadOfTimeReturnsBeanRegistrationsAotContributionWithAliases() { + BeanRegistrationsAotProcessor processor = new BeanRegistrationsAotProcessor(); + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerBeanDefinition("test", new RootBeanDefinition(TestBean.class)); + beanFactory.registerAlias("test", "testAlias"); + BeanRegistrationsAotContribution contribution = processor + .processAheadOfTime(beanFactory); + assertThat(contribution).extracting("registrations").asInstanceOf(InstanceOfAssertFactories.MAP) + .hasEntrySatisfying("test", registration -> + assertThat(registration).extracting("aliases").asInstanceOf(InstanceOfAssertFactories.ARRAY) + .singleElement().isEqualTo("testAlias")); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/DefaultBeanRegistrationCodeFragmentsTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/DefaultBeanRegistrationCodeFragmentsTests.java new file mode 100644 index 000000000000..64a4ecdb4fa7 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/DefaultBeanRegistrationCodeFragmentsTests.java @@ -0,0 +1,203 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.Test; + +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.annotation.InjectAnnotationBeanPostProcessorTests.StringFactoryBean; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.factory.DummyFactory; +import org.springframework.beans.testfixture.beans.factory.aot.GenericFactoryBean; +import org.springframework.beans.testfixture.beans.factory.aot.MockBeanRegistrationsCode; +import org.springframework.beans.testfixture.beans.factory.aot.NumberFactoryBean; +import org.springframework.beans.testfixture.beans.factory.aot.SimpleBean; +import org.springframework.beans.testfixture.beans.factory.aot.SimpleBeanConfiguration; +import org.springframework.beans.testfixture.beans.factory.aot.SimpleBeanFactoryBean; +import org.springframework.core.ResolvableType; +import org.springframework.javapoet.ClassName; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DefaultBeanRegistrationCodeFragments}. + * + * @author Stephane Nicoll + */ +class DefaultBeanRegistrationCodeFragmentsTests { + + private final BeanRegistrationsCode beanRegistrationsCode = new MockBeanRegistrationsCode(new TestGenerationContext()); + + private final DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + + @Test + void getTargetOnConstructor() { + RegisteredBean registeredBean = registerTestBean(SimpleBean.class); + assertTarget(createInstance(registeredBean).getTarget(registeredBean, + SimpleBean.class.getDeclaredConstructors()[0]), SimpleBean.class); + } + + @Test + void getTargetOnConstructorToPublicFactoryBean() { + RegisteredBean registeredBean = registerTestBean(SimpleBean.class); + assertTarget(createInstance(registeredBean).getTarget(registeredBean, + SimpleBeanFactoryBean.class.getDeclaredConstructors()[0]), SimpleBean.class); + } + + @Test + void getTargetOnConstructorToPublicGenericFactoryBeanExtractTargetFromFactoryBeanType() { + RegisteredBean registeredBean = registerTestBean(ResolvableType + .forClassWithGenerics(GenericFactoryBean.class, SimpleBean.class)); + assertTarget(createInstance(registeredBean).getTarget(registeredBean, + GenericFactoryBean.class.getDeclaredConstructors()[0]), SimpleBean.class); + } + + @Test + void getTargetOnConstructorToPublicGenericFactoryBeanWithBoundExtractTargetFromFactoryBeanType() { + RegisteredBean registeredBean = registerTestBean(ResolvableType + .forClassWithGenerics(NumberFactoryBean.class, Integer.class)); + assertTarget(createInstance(registeredBean).getTarget(registeredBean, + NumberFactoryBean.class.getDeclaredConstructors()[0]), Integer.class); + } + + @Test + void getTargetOnConstructorToPublicGenericFactoryBeanUseBeanTypeAsFallback() { + RegisteredBean registeredBean = registerTestBean(SimpleBean.class); + assertTarget(createInstance(registeredBean).getTarget(registeredBean, + GenericFactoryBean.class.getDeclaredConstructors()[0]), SimpleBean.class); + } + + @Test + void getTargetOnConstructorToProtectedFactoryBean() { + RegisteredBean registeredBean = registerTestBean(SimpleBean.class); + assertTarget(createInstance(registeredBean).getTarget(registeredBean, + PrivilegedTestBeanFactoryBean.class.getDeclaredConstructors()[0]), + PrivilegedTestBeanFactoryBean.class); + } + + @Test + void getTargetOnMethod() { + RegisteredBean registeredBean = registerTestBean(SimpleBean.class); + Method method = ReflectionUtils.findMethod(SimpleBeanConfiguration.class, "simpleBean"); + assertThat(method).isNotNull(); + assertTarget(createInstance(registeredBean).getTarget(registeredBean, method), + SimpleBeanConfiguration.class); + } + + @Test + void getTargetOnMethodWithInnerBeanInJavaPackage() { + RegisteredBean registeredBean = registerTestBean(SimpleBean.class); + RegisteredBean innerBean = RegisteredBean.ofInnerBean(registeredBean, "innerTestBean", + new RootBeanDefinition(String.class)); + Method method = ReflectionUtils.findMethod(getClass(), "createString"); + assertThat(method).isNotNull(); + assertTarget(createInstance(innerBean).getTarget(innerBean, method), getClass()); + } + + @Test + void getTargetOnConstructorWithInnerBeanInJavaPackage() { + RegisteredBean registeredBean = registerTestBean(SimpleBean.class); + RegisteredBean innerBean = RegisteredBean.ofInnerBean(registeredBean, "innerTestBean", new RootBeanDefinition(String.class)); + assertTarget(createInstance(innerBean).getTarget(innerBean, + String.class.getDeclaredConstructors()[0]), SimpleBean.class); + } + + @Test + void getTargetOnConstructorWithInnerBeanOnTypeInJavaPackage() { + RegisteredBean registeredBean = registerTestBean(SimpleBean.class); + RegisteredBean innerBean = RegisteredBean.ofInnerBean(registeredBean, "innerTestBean", + new RootBeanDefinition(StringFactoryBean.class)); + assertTarget(createInstance(innerBean).getTarget(innerBean, + StringFactoryBean.class.getDeclaredConstructors()[0]), SimpleBean.class); + } + + @Test + void getTargetOnMethodWithInnerBeanInRegularPackage() { + RegisteredBean registeredBean = registerTestBean(DummyFactory.class); + RegisteredBean innerBean = RegisteredBean.ofInnerBean(registeredBean, "innerTestBean", + new RootBeanDefinition(SimpleBean.class)); + Method method = ReflectionUtils.findMethod(SimpleBeanConfiguration.class, "simpleBean"); + assertThat(method).isNotNull(); + assertTarget(createInstance(innerBean).getTarget(innerBean, method), + SimpleBeanConfiguration.class); + } + + @Test + void getTargetOnConstructorWithInnerBeanInRegularPackage() { + RegisteredBean registeredBean = registerTestBean(DummyFactory.class); + RegisteredBean innerBean = RegisteredBean.ofInnerBean(registeredBean, "innerTestBean", + new RootBeanDefinition(SimpleBean.class)); + assertTarget(createInstance(innerBean).getTarget(innerBean, + SimpleBean.class.getDeclaredConstructors()[0]), SimpleBean.class); + } + + @Test + void getTargetOnConstructorWithInnerBeanOnFactoryBeanOnTypeInRegularPackage() { + RegisteredBean registeredBean = registerTestBean(DummyFactory.class); + RegisteredBean innerBean = RegisteredBean.ofInnerBean(registeredBean, "innerTestBean", + new RootBeanDefinition(SimpleBean.class)); + assertTarget(createInstance(innerBean).getTarget(innerBean, + SimpleBeanFactoryBean.class.getDeclaredConstructors()[0]), SimpleBean.class); + } + + private void assertTarget(ClassName target, Class expected) { + assertThat(target).isEqualTo(ClassName.get(expected)); + } + + + private RegisteredBean registerTestBean(Class beanType) { + this.beanFactory.registerBeanDefinition("testBean", + new RootBeanDefinition(beanType)); + return RegisteredBean.of(this.beanFactory, "testBean"); + } + + private RegisteredBean registerTestBean(ResolvableType beanType) { + this.beanFactory.registerBeanDefinition("testBean", + new RootBeanDefinition(beanType)); + return RegisteredBean.of(this.beanFactory, "testBean"); + } + + private BeanRegistrationCodeFragments createInstance(RegisteredBean registeredBean) { + return new DefaultBeanRegistrationCodeFragments(this.beanRegistrationsCode, registeredBean, + new BeanDefinitionMethodGeneratorFactory(this.beanFactory)); + } + + @SuppressWarnings("unused") + static String createString() { + return "Test"; + } + + static class PrivilegedTestBeanFactoryBean implements FactoryBean { + + @Override + public SimpleBean getObject() throws Exception { + return new SimpleBean(); + } + + @Override + public Class getObjectType() { + return SimpleBean.class; + } + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/EnumWithClassBody.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/EnumWithClassBody.java new file mode 100644 index 000000000000..a8064b97ace8 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/EnumWithClassBody.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +/** + * Test enum that include a class body. + * + * @author Phillip Webb + */ +public enum EnumWithClassBody { + + /** + * No class body. + */ + ONE, + + /** + * With class body. + */ + TWO { + @Override + public String toString() { + return "2"; + } + + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/ExampleClass$$GeneratedBy.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/ExampleClass$$GeneratedBy.java new file mode 100644 index 000000000000..8b40ef58defd --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/ExampleClass$$GeneratedBy.java @@ -0,0 +1,26 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +/** + * Fake CGLIB generated class. + * + * @author Phillip Webb + */ +class ExampleClass$$GeneratedBy extends ExampleClass { + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/ExampleClass.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/ExampleClass.java new file mode 100644 index 000000000000..c549b9befabb --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/ExampleClass.java @@ -0,0 +1,26 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +/** + * Public example class used for test. + * + * @author Phillip Webb + */ +public class ExampleClass { + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/InstanceSupplierCodeGeneratorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/InstanceSupplierCodeGeneratorTests.java new file mode 100644 index 000000000000..43e21771651b --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/InstanceSupplierCodeGeneratorTests.java @@ -0,0 +1,324 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +import java.lang.reflect.Executable; +import java.util.function.BiConsumer; +import java.util.function.Supplier; + +import javax.lang.model.element.Modifier; + +import org.assertj.core.api.ThrowingConsumer; +import org.junit.jupiter.api.Test; + +import org.springframework.aot.generate.GeneratedClass; +import org.springframework.aot.hint.ExecutableHint; +import org.springframework.aot.hint.ExecutableMode; +import org.springframework.aot.hint.ReflectionHints; +import org.springframework.aot.hint.TypeHint; +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.InstanceSupplier; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.beans.testfixture.beans.TestBeanWithPrivateConstructor; +import org.springframework.beans.testfixture.beans.factory.aot.DeferredTypeBuilder; +import org.springframework.beans.testfixture.beans.factory.generator.InnerComponentConfiguration; +import org.springframework.beans.testfixture.beans.factory.generator.InnerComponentConfiguration.EnvironmentAwareComponent; +import org.springframework.beans.testfixture.beans.factory.generator.InnerComponentConfiguration.NoDependencyComponent; +import org.springframework.beans.testfixture.beans.factory.generator.SimpleConfiguration; +import org.springframework.beans.testfixture.beans.factory.generator.factory.NumberHolder; +import org.springframework.beans.testfixture.beans.factory.generator.factory.NumberHolderFactoryBean; +import org.springframework.beans.testfixture.beans.factory.generator.factory.SampleFactory; +import org.springframework.beans.testfixture.beans.factory.generator.injection.InjectionComponent; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.test.tools.Compiled; +import org.springframework.core.test.tools.TestCompiler; +import org.springframework.javapoet.CodeBlock; +import org.springframework.javapoet.MethodSpec; +import org.springframework.javapoet.ParameterizedTypeName; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link InstanceSupplierCodeGenerator}. + * + * @author Phillip Webb + * @author Stephane Nicoll + */ +class InstanceSupplierCodeGeneratorTests { + + private final TestGenerationContext generationContext; + + + InstanceSupplierCodeGeneratorTests() { + this.generationContext = new TestGenerationContext(); + } + + + @Test + void generateWhenHasDefaultConstructor() { + BeanDefinition beanDefinition = new RootBeanDefinition(TestBean.class); + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + compile(beanFactory, beanDefinition, (instanceSupplier, compiled) -> { + TestBean bean = getBean(beanFactory, beanDefinition, instanceSupplier); + assertThat(bean).isInstanceOf(TestBean.class); + assertThat(compiled.getSourceFile()) + .contains("InstanceSupplier.using(TestBean::new)"); + }); + assertThat(getReflectionHints().getTypeHint(TestBean.class)) + .satisfies(hasConstructorWithMode(ExecutableMode.INTROSPECT)); + } + + @Test + void generateWhenHasConstructorWithParameter() { + BeanDefinition beanDefinition = new RootBeanDefinition(InjectionComponent.class); + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerSingleton("injected", "injected"); + compile(beanFactory, beanDefinition, (instanceSupplier, compiled) -> { + InjectionComponent bean = getBean(beanFactory, beanDefinition, + instanceSupplier); + assertThat(bean).isInstanceOf(InjectionComponent.class).extracting("bean") + .isEqualTo("injected"); + }); + assertThat(getReflectionHints().getTypeHint(InjectionComponent.class)) + .satisfies(hasConstructorWithMode(ExecutableMode.INTROSPECT)); + } + + @Test + void generateWhenHasConstructorWithInnerClassAndDefaultConstructor() { + RootBeanDefinition beanDefinition = new RootBeanDefinition( + NoDependencyComponent.class); + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerSingleton("configuration", new InnerComponentConfiguration()); + compile(beanFactory, beanDefinition, (instanceSupplier, compiled) -> { + NoDependencyComponent bean = getBean(beanFactory, beanDefinition, + instanceSupplier); + assertThat(bean).isInstanceOf(NoDependencyComponent.class); + assertThat(compiled.getSourceFile()).contains( + "getBeanFactory().getBean(InnerComponentConfiguration.class).new NoDependencyComponent()"); + }); + assertThat(getReflectionHints().getTypeHint(NoDependencyComponent.class)) + .satisfies(hasConstructorWithMode(ExecutableMode.INTROSPECT)); + } + + @Test + void generateWhenHasConstructorWithInnerClassAndParameter() { + BeanDefinition beanDefinition = new RootBeanDefinition( + EnvironmentAwareComponent.class); + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerSingleton("configuration", new InnerComponentConfiguration()); + beanFactory.registerSingleton("environment", new StandardEnvironment()); + compile(beanFactory, beanDefinition, (instanceSupplier, compiled) -> { + EnvironmentAwareComponent bean = getBean(beanFactory, beanDefinition, + instanceSupplier); + assertThat(bean).isInstanceOf(EnvironmentAwareComponent.class); + assertThat(compiled.getSourceFile()).contains( + "getBeanFactory().getBean(InnerComponentConfiguration.class).new EnvironmentAwareComponent("); + }); + assertThat(getReflectionHints().getTypeHint(EnvironmentAwareComponent.class)) + .satisfies(hasConstructorWithMode(ExecutableMode.INTROSPECT)); + } + + @Test + void generateWhenHasConstructorWithGeneric() { + BeanDefinition beanDefinition = new RootBeanDefinition( + NumberHolderFactoryBean.class); + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerSingleton("number", 123); + compile(beanFactory, beanDefinition, (instanceSupplier, compiled) -> { + NumberHolder bean = getBean(beanFactory, beanDefinition, instanceSupplier); + assertThat(bean).isInstanceOf(NumberHolder.class); + assertThat(bean).extracting("number").isNull(); // No property actually set + assertThat(compiled.getSourceFile()).contains("NumberHolderFactoryBean::new"); + }); + assertThat(getReflectionHints().getTypeHint(NumberHolderFactoryBean.class)) + .satisfies(hasConstructorWithMode(ExecutableMode.INTROSPECT)); + } + + @Test + void generateWhenHasPrivateConstructor() { + BeanDefinition beanDefinition = new RootBeanDefinition( + TestBeanWithPrivateConstructor.class); + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + compile(beanFactory, beanDefinition, (instanceSupplier, compiled) -> { + TestBeanWithPrivateConstructor bean = getBean(beanFactory, beanDefinition, + instanceSupplier); + assertThat(bean).isInstanceOf(TestBeanWithPrivateConstructor.class); + assertThat(compiled.getSourceFile()) + .contains("return BeanInstanceSupplier.forConstructor();"); + }); + assertThat(getReflectionHints().getTypeHint(TestBeanWithPrivateConstructor.class)) + .satisfies(hasConstructorWithMode(ExecutableMode.INVOKE)); + } + + @Test + void generateWhenHasFactoryMethodWithNoArg() { + BeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(String.class) + .setFactoryMethodOnBean("stringBean", "config").getBeanDefinition(); + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerBeanDefinition("config", BeanDefinitionBuilder + .genericBeanDefinition(SimpleConfiguration.class).getBeanDefinition()); + compile(beanFactory, beanDefinition, (instanceSupplier, compiled) -> { + String bean = getBean(beanFactory, beanDefinition, instanceSupplier); + assertThat(bean).isInstanceOf(String.class); + assertThat(bean).isEqualTo("Hello"); + assertThat(compiled.getSourceFile()).contains( + "getBeanFactory().getBean(SimpleConfiguration.class).stringBean()"); + }); + assertThat(getReflectionHints().getTypeHint(SimpleConfiguration.class)) + .satisfies(hasMethodWithMode(ExecutableMode.INTROSPECT)); + } + + @Test + void generateWhenHasPrivateStaticFactoryMethodWithNoArg() { + BeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(String.class) + .setFactoryMethodOnBean("privateStaticStringBean", "config") + .getBeanDefinition(); + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerBeanDefinition("config", BeanDefinitionBuilder + .genericBeanDefinition(SimpleConfiguration.class).getBeanDefinition()); + compile(beanFactory, beanDefinition, (instanceSupplier, compiled) -> { + String bean = getBean(beanFactory, beanDefinition, instanceSupplier); + assertThat(bean).isInstanceOf(String.class); + assertThat(bean).isEqualTo("Hello"); + assertThat(compiled.getSourceFile()) + .contains("forFactoryMethod") + .doesNotContain("withGenerator"); + }); + assertThat(getReflectionHints().getTypeHint(SimpleConfiguration.class)) + .satisfies(hasMethodWithMode(ExecutableMode.INVOKE)); + } + + @Test + void generateWhenHasStaticFactoryMethodWithNoArg() { + BeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(Integer.class) + .setFactoryMethodOnBean("integerBean", "config").getBeanDefinition(); + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerBeanDefinition("config", BeanDefinitionBuilder + .genericBeanDefinition(SimpleConfiguration.class).getBeanDefinition()); + compile(beanFactory, beanDefinition, (instanceSupplier, compiled) -> { + Integer bean = getBean(beanFactory, beanDefinition, instanceSupplier); + assertThat(bean).isInstanceOf(Integer.class); + assertThat(bean).isEqualTo(42); + assertThat(compiled.getSourceFile()) + .contains("SimpleConfiguration::integerBean"); + }); + assertThat(getReflectionHints().getTypeHint(SimpleConfiguration.class)) + .satisfies(hasMethodWithMode(ExecutableMode.INTROSPECT)); + } + + @Test + void generateWhenHasStaticFactoryMethodWithArg() { + RootBeanDefinition beanDefinition = (RootBeanDefinition) BeanDefinitionBuilder + .rootBeanDefinition(String.class) + .setFactoryMethodOnBean("create", "config").getBeanDefinition(); + beanDefinition.setResolvedFactoryMethod(ReflectionUtils + .findMethod(SampleFactory.class, "create", Number.class, String.class)); + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerBeanDefinition("config", BeanDefinitionBuilder + .genericBeanDefinition(SampleFactory.class).getBeanDefinition()); + beanFactory.registerSingleton("number", 42); + beanFactory.registerSingleton("string", "test"); + compile(beanFactory, beanDefinition, (instanceSupplier, compiled) -> { + String bean = getBean(beanFactory, beanDefinition, instanceSupplier); + assertThat(bean).isInstanceOf(String.class); + assertThat(bean).isEqualTo("42test"); + assertThat(compiled.getSourceFile()).contains("SampleFactory.create("); + }); + assertThat(getReflectionHints().getTypeHint(SampleFactory.class)) + .satisfies(hasMethodWithMode(ExecutableMode.INTROSPECT)); + } + + @Test + void generateWhenHasStaticFactoryMethodCheckedException() { + BeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(Integer.class) + .setFactoryMethodOnBean("throwingIntegerBean", "config") + .getBeanDefinition(); + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerBeanDefinition("config", BeanDefinitionBuilder + .genericBeanDefinition(SimpleConfiguration.class).getBeanDefinition()); + compile(beanFactory, beanDefinition, (instanceSupplier, compiled) -> { + Integer bean = getBean(beanFactory, beanDefinition, instanceSupplier); + assertThat(bean).isInstanceOf(Integer.class); + assertThat(bean).isEqualTo(42); + assertThat(compiled.getSourceFile()).doesNotContain(") throws Exception {"); + }); + assertThat(getReflectionHints().getTypeHint(SimpleConfiguration.class)) + .satisfies(hasMethodWithMode(ExecutableMode.INTROSPECT)); + } + + private ReflectionHints getReflectionHints() { + return this.generationContext.getRuntimeHints().reflection(); + } + + private ThrowingConsumer hasConstructorWithMode(ExecutableMode mode) { + return hint -> assertThat(hint.constructors()).anySatisfy(hasMode(mode)); + } + + private ThrowingConsumer hasMethodWithMode(ExecutableMode mode) { + return hint -> assertThat(hint.methods()).anySatisfy(hasMode(mode)); + } + + private ThrowingConsumer hasMode(ExecutableMode mode) { + return hint -> assertThat(hint.getMode()).isEqualTo(mode); + } + + @SuppressWarnings("unchecked") + private T getBean(DefaultListableBeanFactory beanFactory, + BeanDefinition beanDefinition, InstanceSupplier instanceSupplier) { + ((RootBeanDefinition) beanDefinition).setInstanceSupplier(instanceSupplier); + beanFactory.registerBeanDefinition("testBean", beanDefinition); + return (T) beanFactory.getBean("testBean"); + } + + private void compile(DefaultListableBeanFactory beanFactory, BeanDefinition beanDefinition, + BiConsumer, Compiled> result) { + + DefaultListableBeanFactory freshBeanFactory = new DefaultListableBeanFactory(beanFactory); + freshBeanFactory.registerBeanDefinition("testBean", beanDefinition); + RegisteredBean registeredBean = RegisteredBean.of(freshBeanFactory, "testBean"); + DeferredTypeBuilder typeBuilder = new DeferredTypeBuilder(); + GeneratedClass generateClass = this.generationContext.getGeneratedClasses().addForFeature("TestCode", typeBuilder); + InstanceSupplierCodeGenerator generator = new InstanceSupplierCodeGenerator( + this.generationContext, generateClass.getName(), + generateClass.getMethods(), false); + Executable constructorOrFactoryMethod = registeredBean.resolveConstructorOrFactoryMethod(); + assertThat(constructorOrFactoryMethod).isNotNull(); + CodeBlock generatedCode = generator.generateCode(registeredBean, constructorOrFactoryMethod); + typeBuilder.set(type -> { + type.addModifiers(Modifier.PUBLIC); + type.addSuperinterface(ParameterizedTypeName.get(Supplier.class, InstanceSupplier.class)); + type.addMethod(MethodSpec.methodBuilder("get") + .addModifiers(Modifier.PUBLIC) + .returns(InstanceSupplier.class) + .addStatement("return $L", generatedCode).build()); + }); + this.generationContext.writeGeneratedContent(); + TestCompiler.forSystem().with(this.generationContext).compile(compiled -> + result.accept((InstanceSupplier) compiled.getInstance(Supplier.class).get(), compiled)); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/PackagePrivateTestBean.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/PackagePrivateTestBean.java new file mode 100644 index 000000000000..52b6e88ebce3 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/PackagePrivateTestBean.java @@ -0,0 +1,26 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +/** + * Package-private test bean. + * + * @author Phillip Webb + */ +class PackagePrivateTestBean { + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/TestBeanConfiguration.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/TestBeanConfiguration.java new file mode 100644 index 000000000000..6b848fe4d100 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/TestBeanConfiguration.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +import org.springframework.beans.testfixture.beans.TestBean; + +/** + * Test {@code @Configuration} style class to create {@link TestBean}. + * + * @author Phillip Webb + */ +public class TestBeanConfiguration { + + public TestBean testBean() { + return new TestBean(); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/CustomEditorConfigurerTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/CustomEditorConfigurerTests.java index a94425ccf1aa..48a87a9a7739 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/config/CustomEditorConfigurerTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/CustomEditorConfigurerTests.java @@ -29,7 +29,6 @@ import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.PropertyEditorRegistrar; -import org.springframework.beans.PropertyEditorRegistry; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.propertyeditors.CustomDateEditor; @@ -50,12 +49,7 @@ public void testCustomEditorConfigurerWithPropertyEditorRegistrar() throws Parse CustomEditorConfigurer cec = new CustomEditorConfigurer(); final DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT, Locale.GERMAN); cec.setPropertyEditorRegistrars(new PropertyEditorRegistrar[] { - new PropertyEditorRegistrar() { - @Override - public void registerCustomEditors(PropertyEditorRegistry registry) { - registry.registerCustomEditor(Date.class, new CustomDateEditor(df, true)); - } - }}); + registry -> registry.registerCustomEditor(Date.class, new CustomDateEditor(df, true))}); cec.postProcessBeanFactory(bf); MutablePropertyValues pvs = new MutablePropertyValues(); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/MethodInvokingFactoryBeanTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/MethodInvokingFactoryBeanTests.java index fae712817bd5..afc921fa4fdb 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/config/MethodInvokingFactoryBeanTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/MethodInvokingFactoryBeanTests.java @@ -43,7 +43,7 @@ public class MethodInvokingFactoryBeanTests { @Test public void testParameterValidation() throws Exception { - // assert that only static OR non static are set, but not both or none + // assert that only static OR non-static are set, but not both or none MethodInvokingFactoryBean mcfb = new MethodInvokingFactoryBean(); assertThatIllegalArgumentException().isThrownBy(mcfb::afterPropertiesSet); @@ -113,7 +113,7 @@ public void testGetObjectType() throws Exception { mcfb = new MethodInvokingFactoryBean(); mcfb.setTargetClass(TestClass1.class); mcfb.setTargetMethod("supertypes"); - mcfb.setArguments(new ArrayList<>(), new ArrayList(), "hello"); + mcfb.setArguments(new ArrayList<>(), new ArrayList<>(), "hello"); mcfb.afterPropertiesSet(); mcfb.getObjectType(); @@ -184,7 +184,7 @@ public void testGetObject() throws Exception { mcfb = new MethodInvokingFactoryBean(); mcfb.setTargetClass(TestClass1.class); mcfb.setTargetMethod("supertypes"); - mcfb.setArguments(new ArrayList<>(), new ArrayList(), "hello"); + mcfb.setArguments(new ArrayList<>(), new ArrayList<>(), "hello"); // should pass mcfb.afterPropertiesSet(); } @@ -194,7 +194,7 @@ public void testArgumentConversion() throws Exception { MethodInvokingFactoryBean mcfb = new MethodInvokingFactoryBean(); mcfb.setTargetClass(TestClass1.class); mcfb.setTargetMethod("supertypes"); - mcfb.setArguments(new ArrayList<>(), new ArrayList(), "hello", "bogus"); + mcfb.setArguments(new ArrayList<>(), new ArrayList<>(), "hello", "bogus"); assertThatExceptionOfType(NoSuchMethodException.class).as( "Matched method with wrong number of args").isThrownBy( mcfb::afterPropertiesSet); @@ -210,14 +210,14 @@ public void testArgumentConversion() throws Exception { mcfb = new MethodInvokingFactoryBean(); mcfb.setTargetClass(TestClass1.class); mcfb.setTargetMethod("supertypes2"); - mcfb.setArguments(new ArrayList<>(), new ArrayList(), "hello", "bogus"); + mcfb.setArguments(new ArrayList<>(), new ArrayList<>(), "hello", "bogus"); mcfb.afterPropertiesSet(); assertThat(mcfb.getObject()).isEqualTo("hello"); mcfb = new MethodInvokingFactoryBean(); mcfb.setTargetClass(TestClass1.class); mcfb.setTargetMethod("supertypes2"); - mcfb.setArguments(new ArrayList<>(), new ArrayList(), new Object()); + mcfb.setArguments(new ArrayList<>(), new ArrayList<>(), new Object()); assertThatExceptionOfType(NoSuchMethodException.class).as( "Matched method when shouldn't have matched").isThrownBy( mcfb::afterPropertiesSet); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/ObjectFactoryCreatingFactoryBeanTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/ObjectFactoryCreatingFactoryBeanTests.java index 7357ba9069dd..d55d48bb64a7 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/config/ObjectFactoryCreatingFactoryBeanTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/ObjectFactoryCreatingFactoryBeanTests.java @@ -18,8 +18,7 @@ import java.util.Date; -import javax.inject.Provider; - +import jakarta.inject.Provider; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/PropertyResourceConfigurerTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/PropertyResourceConfigurerTests.java index 5dd76865de42..f7a52dbcdf09 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/config/PropertyResourceConfigurerTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/PropertyResourceConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -111,7 +111,7 @@ public void testPropertyOverrideConfigurer() { assertThat(tb1.getAge()).isEqualTo(99); assertThat(tb2.getAge()).isEqualTo(99); - assertThat(tb1.getName()).isEqualTo(null); + assertThat(tb1.getName()).isNull(); assertThat(tb2.getName()).isEqualTo("test"); } @@ -310,7 +310,7 @@ public void testPropertyOverrideConfigurerWithIgnoreInvalidKeys() { TestBean tb2 = (TestBean) factory.getBean("tb2"); assertThat(tb1.getAge()).isEqualTo(99); assertThat(tb2.getAge()).isEqualTo(99); - assertThat(tb1.getName()).isEqualTo(null); + assertThat(tb1.getName()).isNull(); assertThat(tb2.getName()).isEqualTo("test"); } @@ -357,22 +357,18 @@ private void doTestPropertyPlaceholderConfigurer(boolean parentChildSeparation) MutablePropertyValues pvs = new MutablePropertyValues(); pvs.add("stringArray", new String[] {"${os.name}", "${age}"}); - List friends = new ManagedList<>(); - friends.add("na${age}me"); - friends.add(new RuntimeBeanReference("${ref}")); + List friends = ManagedList.of("na${age}me", new RuntimeBeanReference("${ref}")); pvs.add("friends", friends); - Set someSet = new ManagedSet<>(); - someSet.add("na${age}me"); - someSet.add(new RuntimeBeanReference("${ref}")); - someSet.add(new TypedStringValue("${age}", Integer.class)); + Set someSet = ManagedSet.of("na${age}me", + new RuntimeBeanReference("${ref}"), new TypedStringValue("${age}", Integer.class)); pvs.add("someSet", someSet); - Map someMap = new ManagedMap<>(); - someMap.put(new TypedStringValue("key${age}"), new TypedStringValue("${age}")); - someMap.put(new TypedStringValue("key${age}ref"), new RuntimeBeanReference("${ref}")); - someMap.put("key1", new RuntimeBeanReference("${ref}")); - someMap.put("key2", "${age}name"); + Map someMap = ManagedMap.ofEntries( + Map.entry(new TypedStringValue("key${age}"), new TypedStringValue("${age}")), + Map.entry(new TypedStringValue("key${age}ref"), new RuntimeBeanReference("${ref}")), + Map.entry("key1", new RuntimeBeanReference("${ref}")), + Map.entry("key2", "${age}name")); MutablePropertyValues innerPvs = new MutablePropertyValues(); innerPvs.add("country", "${os.name}"); RootBeanDefinition innerBd = new RootBeanDefinition(TestBean.class); @@ -423,7 +419,7 @@ private void doTestPropertyPlaceholderConfigurer(boolean parentChildSeparation) TestBean inner1 = (TestBean) tb2.getSomeMap().get("key3"); TestBean inner2 = (TestBean) tb2.getSomeMap().get("mykey4"); assertThat(inner1.getAge()).isEqualTo(0); - assertThat(inner1.getName()).isEqualTo(null); + assertThat(inner1.getName()).isNull(); assertThat(inner1.getCountry()).isEqualTo(System.getProperty("os.name")); assertThat(inner2.getAge()).isEqualTo(98); assertThat(inner2.getName()).isEqualTo("namemyvarmyvar${"); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/ServiceLocatorFactoryBeanTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/ServiceLocatorFactoryBeanTests.java index 87139e363e3e..5390acad4003 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/config/ServiceLocatorFactoryBeanTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/ServiceLocatorFactoryBeanTests.java @@ -278,19 +278,19 @@ public static class TestService2 { } - public static interface TestServiceLocator { + public interface TestServiceLocator { TestService getTestService(); } - public static interface TestServiceLocator2 { + public interface TestServiceLocator2 { TestService getTestService(String id) throws CustomServiceLocatorException2; } - public static interface TestServiceLocator3 { + public interface TestServiceLocator3 { TestService getTestService(); @@ -302,13 +302,13 @@ public static interface TestServiceLocator3 { } - public static interface TestService2Locator { + public interface TestService2Locator { TestService2 getTestService() throws CustomServiceLocatorException3; } - public static interface ServiceLocatorInterfaceWithExtraNonCompliantMethod { + public interface ServiceLocatorInterfaceWithExtraNonCompliantMethod { TestService2 getTestService(); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlMapFactoryBeanTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlMapFactoryBeanTests.java index b7e05ed64747..b5d4459612ab 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlMapFactoryBeanTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlMapFactoryBeanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -120,7 +120,7 @@ public void testMapWithIntegerValue() { @SuppressWarnings("unchecked") Map sub = (Map) object; assertThat(sub.size()).isEqualTo(1); - assertThat(sub.get("key1.key2")).isEqualTo(Integer.valueOf(3)); + assertThat(sub.get("key1.key2")).isEqualTo(3); } @Test diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlProcessorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlProcessorTests.java index 60fbd272ce2c..edb1f994f33c 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlProcessorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -17,9 +17,11 @@ package org.springframework.beans.factory.config; import java.net.URL; +import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import org.junit.jupiter.api.Test; import org.yaml.snakeyaml.constructor.ConstructorException; @@ -28,7 +30,6 @@ import org.springframework.core.io.ByteArrayResource; -import static java.util.stream.Collectors.toList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.entry; @@ -39,10 +40,12 @@ * @author Dave Syer * @author Juergen Hoeller * @author Sam Brannen + * @author Brian Clozel */ class YamlProcessorTests { - private final YamlProcessor processor = new YamlProcessor() {}; + private final YamlProcessor processor = new YamlProcessor() { + }; @Test @@ -79,8 +82,8 @@ void badDocumentStart() { void badResource() { setYaml("foo: bar\ncd\nspam:\n foo: baz"); assertThatExceptionOfType(ScannerException.class) - .isThrownBy(() -> this.processor.process((properties, map) -> {})) - .withMessageContaining("line 3, column 1"); + .isThrownBy(() -> this.processor.process((properties, map) -> {})) + .withMessageContaining("line 3, column 1"); } @Test @@ -127,8 +130,8 @@ void flattenedMapIsSameAsPropertiesButOrdered() { Map bar = (Map) map.get("bar"); assertThat(bar.get("spam")).isEqualTo("bucket"); - List keysFromProperties = properties.keySet().stream().collect(toList()); - List keysFromFlattenedMap = flattenedMap.keySet().stream().collect(toList()); + List keysFromProperties = new ArrayList<>(properties.keySet()); + List keysFromFlattenedMap = new ArrayList<>(flattenedMap.keySet()); assertThat(keysFromProperties).containsExactlyInAnyOrderElementsOf(keysFromFlattenedMap); // Keys in the Properties object are sorted. assertThat(keysFromProperties).containsExactly("bar.spam", "cat", "foo"); @@ -138,16 +141,26 @@ void flattenedMapIsSameAsPropertiesButOrdered() { } @Test - void customTypeSupportedByDefault() throws Exception { - URL url = new URL("https://localhost:9000/"); - setYaml("value: !!java.net.URL [\"" + url + "\"]"); - + @SuppressWarnings("unchecked") + void standardTypesSupportedByDefault() throws Exception { + setYaml("value: !!set\n ? first\n ? second"); this.processor.process((properties, map) -> { - assertThat(properties).containsExactly(entry("value", url)); - assertThat(map).containsExactly(entry("value", url)); + assertThat(properties).containsExactly(entry("value[0]", "first"), entry("value[1]", "second")); + assertThat(map.get("value")).isInstanceOf(Set.class); + Set set = (Set) map.get("value"); + assertThat(set).containsExactly("first", "second"); }); } + @Test + void customTypeNotSupportedByDefault() throws Exception { + URL url = new URL("https://localhost:9000/"); + setYaml("value: !!java.net.URL [\"" + url + "\"]"); + assertThatExceptionOfType(ConstructorException.class) + .isThrownBy(() -> this.processor.process((properties, map) -> {})) + .withMessageContaining("Unsupported type encountered in YAML document: java.net.URL"); + } + @Test void customTypesSupportedDueToExplicitConfiguration() throws Exception { this.processor.setSupportedTypes(URL.class, String.class); @@ -168,8 +181,8 @@ void customTypeNotSupportedDueToExplicitConfiguration() { setYaml("value: !!java.net.URL [\"https://localhost:9000//"]"); assertThatExceptionOfType(ConstructorException.class) - .isThrownBy(() -> this.processor.process((properties, map) -> {})) - .withMessageContaining("Unsupported type encountered in YAML document: java.net.URL"); + .isThrownBy(() -> this.processor.process((properties, map) -> {})) + .withMessageContaining("Unsupported type encountered in YAML document: java.net.URL"); } private void setYaml(String yaml) { diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanDefinitionBuilderTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanDefinitionBuilderTests.java index ec157df512c2..7a6b938ea1ce 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanDefinitionBuilderTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanDefinitionBuilderTests.java @@ -17,22 +17,25 @@ package org.springframework.beans.factory.support; import java.util.Arrays; +import java.util.function.Function; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.ResolvableType; import static org.assertj.core.api.Assertions.assertThat; /** * @author Rod Johnson * @author Juergen Hoeller + * @author Stephane Nicoll */ -public class BeanDefinitionBuilderTests { +class BeanDefinitionBuilderTests { @Test - public void beanClassWithSimpleProperty() { + void builderWithBeanClassWithSimpleProperty() { String[] dependsOn = new String[] { "A", "B", "C" }; BeanDefinitionBuilder bdb = BeanDefinitionBuilder.rootBeanDefinition(TestBean.class); bdb.setScope(BeanDefinition.SCOPE_PROTOTYPE); @@ -49,7 +52,7 @@ public void beanClassWithSimpleProperty() { } @Test - public void beanClassWithFactoryMethod() { + void builderWithBeanClassAndFactoryMethod() { BeanDefinitionBuilder bdb = BeanDefinitionBuilder.rootBeanDefinition(TestBean.class, "create"); RootBeanDefinition rbd = (RootBeanDefinition) bdb.getBeanDefinition(); assertThat(rbd.hasBeanClass()).isTrue(); @@ -58,7 +61,7 @@ public void beanClassWithFactoryMethod() { } @Test - public void beanClassName() { + void builderWithBeanClassName() { BeanDefinitionBuilder bdb = BeanDefinitionBuilder.rootBeanDefinition(TestBean.class.getName()); RootBeanDefinition rbd = (RootBeanDefinition) bdb.getBeanDefinition(); assertThat(rbd.hasBeanClass()).isFalse(); @@ -66,7 +69,7 @@ public void beanClassName() { } @Test - public void beanClassNameWithFactoryMethod() { + void builderWithBeanClassNameAndFactoryMethod() { BeanDefinitionBuilder bdb = BeanDefinitionBuilder.rootBeanDefinition(TestBean.class.getName(), "create"); RootBeanDefinition rbd = (RootBeanDefinition) bdb.getBeanDefinition(); assertThat(rbd.hasBeanClass()).isFalse(); @@ -74,4 +77,78 @@ public void beanClassNameWithFactoryMethod() { assertThat(rbd.getFactoryMethodName()).isEqualTo("create"); } + @Test + void builderWithResolvableTypeAndInstanceSupplier() { + ResolvableType type = ResolvableType.forClassWithGenerics(Function.class, Integer.class, String.class); + Function function = i -> "value " + i; + RootBeanDefinition rbd = (RootBeanDefinition) BeanDefinitionBuilder + .rootBeanDefinition(type, () -> function).getBeanDefinition(); + assertThat(rbd.getResolvableType()).isEqualTo(type); + assertThat(rbd.getInstanceSupplier()).isNotNull(); + assertThat(rbd.getInstanceSupplier().get()).isInstanceOf(Function.class); + } + + @Test + void builderWithBeanClassAndInstanceSupplier() { + RootBeanDefinition rbd = (RootBeanDefinition) BeanDefinitionBuilder + .rootBeanDefinition(String.class, () -> "test").getBeanDefinition(); + assertThat(rbd.getResolvableType().resolve()).isEqualTo(String.class); + assertThat(rbd.getInstanceSupplier()).isNotNull(); + assertThat(rbd.getInstanceSupplier().get()).isEqualTo("test"); + } + + @Test + void builderWithAutowireMode() { + assertThat(BeanDefinitionBuilder.rootBeanDefinition(TestBean.class) + .setAutowireMode(RootBeanDefinition.AUTOWIRE_BY_TYPE).getBeanDefinition().getAutowireMode()) + .isEqualTo(RootBeanDefinition.AUTOWIRE_BY_TYPE); + } + + @Test + void builderWithDependencyCheck() { + assertThat(BeanDefinitionBuilder.rootBeanDefinition(TestBean.class) + .setDependencyCheck(RootBeanDefinition.DEPENDENCY_CHECK_ALL) + .getBeanDefinition().getDependencyCheck()) + .isEqualTo(RootBeanDefinition.DEPENDENCY_CHECK_ALL); + } + + @Test + void builderWithDependsOn() { + assertThat(BeanDefinitionBuilder.rootBeanDefinition(TestBean.class).addDependsOn("test") + .addDependsOn("test2").getBeanDefinition().getDependsOn()) + .containsExactly("test", "test2"); + } + + @Test + void builderWithPrimary() { + assertThat(BeanDefinitionBuilder.rootBeanDefinition(TestBean.class) + .setPrimary(true).getBeanDefinition().isPrimary()).isTrue(); + } + + @Test + void builderWithRole() { + assertThat(BeanDefinitionBuilder.rootBeanDefinition(TestBean.class) + .setRole(BeanDefinition.ROLE_INFRASTRUCTURE).getBeanDefinition().getRole()) + .isEqualTo(BeanDefinition.ROLE_INFRASTRUCTURE); + } + + @Test + void builderWithSynthetic() { + assertThat(BeanDefinitionBuilder.rootBeanDefinition(TestBean.class) + .setSynthetic(true).getBeanDefinition().isSynthetic()).isTrue(); + } + + @Test + void builderWithCustomizers() { + BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(TestBean.class) + .applyCustomizers(builder -> { + builder.setFactoryMethodName("create"); + builder.setRole(BeanDefinition.ROLE_SUPPORT); + }) + .applyCustomizers(builder -> builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE)) + .getBeanDefinition(); + assertThat(beanDefinition.getFactoryMethodName()).isEqualTo("create"); + assertThat(beanDefinition.getRole()).isEqualTo(BeanDefinition.ROLE_INFRASTRUCTURE); + } + } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanDefinitionTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanDefinitionTests.java index 88dc51e8b099..de770309e058 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanDefinitionTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanDefinitionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -133,6 +133,16 @@ public void genericBeanDefinitionEquality() { assertThat(bd.equals(otherBd)).isTrue(); assertThat(otherBd.equals(bd)).isTrue(); assertThat(bd.hashCode() == otherBd.hashCode()).isTrue(); + + bd.getPropertyValues(); + assertThat(bd.equals(otherBd)).isTrue(); + assertThat(otherBd.equals(bd)).isTrue(); + assertThat(bd.hashCode() == otherBd.hashCode()).isTrue(); + + bd.getConstructorArgumentValues(); + assertThat(bd.equals(otherBd)).isTrue(); + assertThat(otherBd.equals(bd)).isTrue(); + assertThat(bd.hashCode() == otherBd.hashCode()).isTrue(); } @Test diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanDefinitionValueResolverTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanDefinitionValueResolverTests.java new file mode 100644 index 000000000000..dde5fb27b248 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanDefinitionValueResolverTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-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.beans.factory.support; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link BeanDefinitionValueResolver}. + * + * @author Stephane Nicoll + */ +class BeanDefinitionValueResolverTests { + + @Test + void resolveInnerBean() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + RootBeanDefinition parentBd = new RootBeanDefinition(); + GenericBeanDefinition innerBd = new GenericBeanDefinition(); + innerBd.setAttribute("test", 42); + BeanDefinitionValueResolver bdvr = new BeanDefinitionValueResolver(beanFactory, "test", parentBd); + RootBeanDefinition resolvedInnerBd = bdvr.resolveInnerBean(null,innerBd, (name, mbd) -> { + assertThat(name).isNotNull().startsWith("(inner bean"); + return mbd; + }); + assertThat(resolvedInnerBd.getAttribute("test")).isEqualTo(42); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanFactoryGenericsTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanFactoryGenericsTests.java index e3ba7b5d93f1..5c6e02536317 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanFactoryGenericsTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanFactoryGenericsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -17,7 +17,6 @@ package org.springframework.beans.factory.support; import java.lang.reflect.InvocationHandler; -import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.net.MalformedURLException; import java.net.URI; @@ -34,8 +33,6 @@ import org.junit.jupiter.api.Test; import org.mockito.Mockito; -import org.springframework.beans.PropertyEditorRegistrar; -import org.springframework.beans.PropertyEditorRegistry; import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.beans.factory.ObjectProvider; @@ -64,10 +61,10 @@ * @author Sam Brannen * @since 20.01.2006 */ -public class BeanFactoryGenericsTests { +class BeanFactoryGenericsTests { @Test - public void testGenericSetProperty() { + void testGenericSetProperty() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); @@ -84,7 +81,7 @@ public void testGenericSetProperty() { } @Test - public void testGenericListProperty() throws Exception { + void testGenericListProperty() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); @@ -101,7 +98,7 @@ public void testGenericListProperty() throws Exception { } @Test - public void testGenericListPropertyWithAutowiring() throws Exception { + void testGenericListPropertyWithAutowiring() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.registerSingleton("resource1", new UrlResource("http://localhost:8080/")); bf.registerSingleton("resource2", new UrlResource("http://localhost:9090/")); @@ -116,7 +113,7 @@ public void testGenericListPropertyWithAutowiring() throws Exception { } @Test - public void testGenericListPropertyWithInvalidElementType() { + void testGenericListPropertyWithInvalidElementType() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); RootBeanDefinition rbd = new RootBeanDefinition(GenericIntegerBean.class); @@ -134,7 +131,7 @@ public void testGenericListPropertyWithInvalidElementType() { } @Test - public void testGenericListPropertyWithOptionalAutowiring() { + void testGenericListPropertyWithOptionalAutowiring() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); @@ -146,7 +143,7 @@ public void testGenericListPropertyWithOptionalAutowiring() { } @Test - public void testGenericMapProperty() { + void testGenericMapProperty() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); @@ -158,12 +155,12 @@ public void testGenericMapProperty() { bf.registerBeanDefinition("genericBean", rbd); GenericBean gb = (GenericBean) bf.getBean("genericBean"); - assertThat(gb.getShortMap().get(new Short("4"))).isEqualTo(5); - assertThat(gb.getShortMap().get(new Short("6"))).isEqualTo(7); + assertThat(gb.getShortMap().get(Short.valueOf("4"))).isEqualTo(5); + assertThat(gb.getShortMap().get(Short.valueOf("6"))).isEqualTo(7); } @Test - public void testGenericListOfArraysProperty() { + void testGenericListOfArraysProperty() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions( new ClassPathResource("genericBeanTests.xml", getClass())); @@ -171,14 +168,14 @@ public void testGenericListOfArraysProperty() { assertThat(gb.getListOfArrays().size()).isEqualTo(1); String[] array = gb.getListOfArrays().get(0); - assertThat(array.length).isEqualTo(2); + assertThat(array).hasSize(2); assertThat(array[0]).isEqualTo("value1"); assertThat(array[1]).isEqualTo("value2"); } @Test - public void testGenericSetConstructor() { + void testGenericSetConstructor() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); @@ -195,7 +192,7 @@ public void testGenericSetConstructor() { } @Test - public void testGenericSetConstructorWithAutowiring() { + void testGenericSetConstructorWithAutowiring() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.registerSingleton("integer1", 4); bf.registerSingleton("integer2", 5); @@ -210,7 +207,7 @@ public void testGenericSetConstructorWithAutowiring() { } @Test - public void testGenericSetConstructorWithOptionalAutowiring() { + void testGenericSetConstructorWithOptionalAutowiring() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); @@ -222,7 +219,7 @@ public void testGenericSetConstructorWithOptionalAutowiring() { } @Test - public void testGenericSetListConstructor() throws Exception { + void testGenericSetListConstructor() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); @@ -245,7 +242,7 @@ public void testGenericSetListConstructor() throws Exception { } @Test - public void testGenericSetListConstructorWithAutowiring() throws Exception { + void testGenericSetListConstructorWithAutowiring() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.registerSingleton("integer1", 4); bf.registerSingleton("integer2", 5); @@ -264,7 +261,7 @@ public void testGenericSetListConstructorWithAutowiring() throws Exception { } @Test - public void testGenericSetListConstructorWithOptionalAutowiring() throws Exception { + void testGenericSetListConstructorWithOptionalAutowiring() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.registerSingleton("resource1", new UrlResource("http://localhost:8080/")); bf.registerSingleton("resource2", new UrlResource("http://localhost:9090/")); @@ -279,7 +276,7 @@ public void testGenericSetListConstructorWithOptionalAutowiring() throws Excepti } @Test - public void testGenericSetMapConstructor() { + void testGenericSetMapConstructor() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); @@ -297,12 +294,12 @@ public void testGenericSetMapConstructor() { assertThat(gb.getIntegerSet().contains(4)).isTrue(); assertThat(gb.getIntegerSet().contains(5)).isTrue(); - assertThat(gb.getShortMap().get(new Short("4"))).isEqualTo(5); - assertThat(gb.getShortMap().get(new Short("6"))).isEqualTo(7); + assertThat(gb.getShortMap().get(Short.valueOf("4"))).isEqualTo(5); + assertThat(gb.getShortMap().get(Short.valueOf("6"))).isEqualTo(7); } @Test - public void testGenericMapResourceConstructor() throws Exception { + void testGenericMapResourceConstructor() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); @@ -315,13 +312,13 @@ public void testGenericMapResourceConstructor() throws Exception { bf.registerBeanDefinition("genericBean", rbd); GenericBean gb = (GenericBean) bf.getBean("genericBean"); - assertThat(gb.getShortMap().get(new Short("4"))).isEqualTo(5); - assertThat(gb.getShortMap().get(new Short("6"))).isEqualTo(7); + assertThat(gb.getShortMap().get(Short.valueOf("4"))).isEqualTo(5); + assertThat(gb.getShortMap().get(Short.valueOf("6"))).isEqualTo(7); assertThat(gb.getResourceList().get(0)).isEqualTo(new UrlResource("http://localhost:8080/")); } @Test - public void testGenericMapMapConstructor() { + void testGenericMapMapConstructor() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); @@ -338,16 +335,16 @@ public void testGenericMapMapConstructor() { GenericBean gb = (GenericBean) bf.getBean("genericBean"); assertThat(gb.getShortMap()).isNotSameAs(gb.getPlainMap()); - assertThat(gb.getPlainMap().size()).isEqualTo(2); + assertThat(gb.getPlainMap()).hasSize(2); assertThat(gb.getPlainMap().get("1")).isEqualTo("0"); assertThat(gb.getPlainMap().get("2")).isEqualTo("3"); assertThat(gb.getShortMap().size()).isEqualTo(2); - assertThat(gb.getShortMap().get(new Short("4"))).isEqualTo(5); - assertThat(gb.getShortMap().get(new Short("6"))).isEqualTo(7); + assertThat(gb.getShortMap().get(Short.valueOf("4"))).isEqualTo(5); + assertThat(gb.getShortMap().get(Short.valueOf("6"))).isEqualTo(7); } @Test - public void testGenericMapMapConstructorWithSameRefAndConversion() { + void testGenericMapMapConstructorWithSameRefAndConversion() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); @@ -361,22 +358,22 @@ public void testGenericMapMapConstructorWithSameRefAndConversion() { GenericBean gb = (GenericBean) bf.getBean("genericBean"); assertThat(gb.getShortMap()).isNotSameAs(gb.getPlainMap()); - assertThat(gb.getPlainMap().size()).isEqualTo(2); + assertThat(gb.getPlainMap()).hasSize(2); assertThat(gb.getPlainMap().get("1")).isEqualTo("0"); assertThat(gb.getPlainMap().get("2")).isEqualTo("3"); assertThat(gb.getShortMap().size()).isEqualTo(2); - assertThat(gb.getShortMap().get(new Short("1"))).isEqualTo(0); - assertThat(gb.getShortMap().get(new Short("2"))).isEqualTo(3); + assertThat(gb.getShortMap().get(Short.valueOf("1"))).isEqualTo(0); + assertThat(gb.getShortMap().get(Short.valueOf("2"))).isEqualTo(3); } @Test - public void testGenericMapMapConstructorWithSameRefAndNoConversion() { + void testGenericMapMapConstructorWithSameRefAndNoConversion() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); Map input = new HashMap<>(); - input.put(new Short((short) 1), 0); - input.put(new Short((short) 2), 3); + input.put((short) 1, 0); + input.put((short) 2, 3); rbd.getConstructorArgumentValues().addGenericArgumentValue(input); rbd.getConstructorArgumentValues().addGenericArgumentValue(input); @@ -384,13 +381,13 @@ public void testGenericMapMapConstructorWithSameRefAndNoConversion() { GenericBean gb = (GenericBean) bf.getBean("genericBean"); assertThat(gb.getShortMap()).isSameAs(gb.getPlainMap()); - assertThat(gb.getShortMap().size()).isEqualTo(2); - assertThat(gb.getShortMap().get(new Short("1"))).isEqualTo(0); - assertThat(gb.getShortMap().get(new Short("2"))).isEqualTo(3); + assertThat(gb.getShortMap()).hasSize(2); + assertThat(gb.getShortMap().get(Short.valueOf("1"))).isEqualTo(0); + assertThat(gb.getShortMap().get(Short.valueOf("2"))).isEqualTo(3); } @Test - public void testGenericMapWithKeyTypeConstructor() { + void testGenericMapWithKeyTypeConstructor() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); @@ -407,14 +404,9 @@ public void testGenericMapWithKeyTypeConstructor() { } @Test - public void testGenericMapWithCollectionValueConstructor() { + void testGenericMapWithCollectionValueConstructor() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - bf.addPropertyEditorRegistrar(new PropertyEditorRegistrar() { - @Override - public void registerCustomEditors(PropertyEditorRegistry registry) { - registry.registerCustomEditor(Number.class, new CustomNumberEditor(Integer.class, false)); - } - }); + bf.addPropertyEditorRegistrar(registry -> registry.registerCustomEditor(Number.class, new CustomNumberEditor(Integer.class, false))); RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); Map> input = new HashMap<>(); @@ -438,7 +430,7 @@ public void registerCustomEditors(PropertyEditorRegistry registry) { @Test - public void testGenericSetFactoryMethod() { + void testGenericSetFactoryMethod() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); rbd.setFactoryMethodName("createInstance"); @@ -456,7 +448,7 @@ public void testGenericSetFactoryMethod() { } @Test - public void testGenericSetListFactoryMethod() throws Exception { + void testGenericSetListFactoryMethod() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); rbd.setFactoryMethodName("createInstance"); @@ -480,7 +472,7 @@ public void testGenericSetListFactoryMethod() throws Exception { } @Test - public void testGenericSetMapFactoryMethod() { + void testGenericSetMapFactoryMethod() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); rbd.setFactoryMethodName("createInstance"); @@ -499,12 +491,12 @@ public void testGenericSetMapFactoryMethod() { assertThat(gb.getIntegerSet().contains(4)).isTrue(); assertThat(gb.getIntegerSet().contains(5)).isTrue(); - assertThat(gb.getShortMap().get(new Short("4"))).isEqualTo(5); - assertThat(gb.getShortMap().get(new Short("6"))).isEqualTo(7); + assertThat(gb.getShortMap().get(Short.valueOf("4"))).isEqualTo(5); + assertThat(gb.getShortMap().get(Short.valueOf("6"))).isEqualTo(7); } @Test - public void testGenericMapResourceFactoryMethod() throws Exception { + void testGenericMapResourceFactoryMethod() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); rbd.setFactoryMethodName("createInstance"); @@ -518,13 +510,13 @@ public void testGenericMapResourceFactoryMethod() throws Exception { bf.registerBeanDefinition("genericBean", rbd); GenericBean gb = (GenericBean) bf.getBean("genericBean"); - assertThat(gb.getShortMap().get(new Short("4"))).isEqualTo(5); - assertThat(gb.getShortMap().get(new Short("6"))).isEqualTo(7); + assertThat(gb.getShortMap().get(Short.valueOf("4"))).isEqualTo(5); + assertThat(gb.getShortMap().get(Short.valueOf("6"))).isEqualTo(7); assertThat(gb.getResourceList().get(0)).isEqualTo(new UrlResource("http://localhost:8080/")); } @Test - public void testGenericMapMapFactoryMethod() { + void testGenericMapMapFactoryMethod() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); rbd.setFactoryMethodName("createInstance"); @@ -543,12 +535,12 @@ public void testGenericMapMapFactoryMethod() { assertThat(gb.getPlainMap().get("1")).isEqualTo("0"); assertThat(gb.getPlainMap().get("2")).isEqualTo("3"); - assertThat(gb.getShortMap().get(new Short("4"))).isEqualTo(5); - assertThat(gb.getShortMap().get(new Short("6"))).isEqualTo(7); + assertThat(gb.getShortMap().get(Short.valueOf("4"))).isEqualTo(5); + assertThat(gb.getShortMap().get(Short.valueOf("6"))).isEqualTo(7); } @Test - public void testGenericMapWithKeyTypeFactoryMethod() { + void testGenericMapWithKeyTypeFactoryMethod() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); rbd.setFactoryMethodName("createInstance"); @@ -561,19 +553,14 @@ public void testGenericMapWithKeyTypeFactoryMethod() { bf.registerBeanDefinition("genericBean", rbd); GenericBean gb = (GenericBean) bf.getBean("genericBean"); - assertThat(gb.getLongMap().get(new Long("4"))).isEqualTo("5"); - assertThat(gb.getLongMap().get(new Long("6"))).isEqualTo("7"); + assertThat(gb.getLongMap().get(Long.valueOf("4"))).isEqualTo("5"); + assertThat(gb.getLongMap().get(Long.valueOf("6"))).isEqualTo("7"); } @Test - public void testGenericMapWithCollectionValueFactoryMethod() { + void testGenericMapWithCollectionValueFactoryMethod() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - bf.addPropertyEditorRegistrar(new PropertyEditorRegistrar() { - @Override - public void registerCustomEditors(PropertyEditorRegistry registry) { - registry.registerCustomEditor(Number.class, new CustomNumberEditor(Integer.class, false)); - } - }); + bf.addPropertyEditorRegistrar(registry -> registry.registerCustomEditor(Number.class, new CustomNumberEditor(Integer.class, false))); RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); rbd.setFactoryMethodName("createInstance"); @@ -597,7 +584,7 @@ public void registerCustomEditors(PropertyEditorRegistry registry) { } @Test - public void testGenericListBean() throws Exception { + void testGenericListBean() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions( new ClassPathResource("genericBeanTests.xml", getClass())); @@ -607,7 +594,7 @@ public void testGenericListBean() throws Exception { } @Test - public void testGenericSetBean() throws Exception { + void testGenericSetBean() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions( new ClassPathResource("genericBeanTests.xml", getClass())); @@ -617,18 +604,18 @@ public void testGenericSetBean() throws Exception { } @Test - public void testGenericMapBean() throws Exception { + void testGenericMapBean() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions( new ClassPathResource("genericBeanTests.xml", getClass())); Map map = (Map) bf.getBean("map"); - assertThat(map.size()).isEqualTo(1); + assertThat(map).hasSize(1); assertThat(map.keySet().iterator().next()).isEqualTo(10); assertThat(map.values().iterator().next()).isEqualTo(new URL("http://localhost:8080/")); } @Test - public void testGenericallyTypedIntegerBean() { + void testGenericallyTypedIntegerBean() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions( new ClassPathResource("genericBeanTests.xml", getClass())); @@ -639,7 +626,7 @@ public void testGenericallyTypedIntegerBean() { } @Test - public void testGenericallyTypedSetOfIntegerBean() { + void testGenericallyTypedSetOfIntegerBean() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions( new ClassPathResource("genericBeanTests.xml", getClass())); @@ -651,7 +638,7 @@ public void testGenericallyTypedSetOfIntegerBean() { @Test @EnabledForTestGroups(LONG_RUNNING) - public void testSetBean() throws Exception { + void testSetBean() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions( new ClassPathResource("genericBeanTests.xml", getClass())); @@ -671,7 +658,7 @@ public void testSetBean() throws Exception { *

    See SPR-9493 */ @Test - public void parameterizedStaticFactoryMethod() { + void parameterizedStaticFactoryMethod() { RootBeanDefinition rbd = new RootBeanDefinition(Mockito.class); rbd.setFactoryMethodName("mock"); rbd.getConstructorArgumentValues().addGenericArgumentValue(Runnable.class); @@ -682,7 +669,7 @@ public void parameterizedStaticFactoryMethod() { assertThat(bf.getType("mock")).isEqualTo(Runnable.class); assertThat(bf.getType("mock")).isEqualTo(Runnable.class); Map beans = bf.getBeansOfType(Runnable.class); - assertThat(beans.size()).isEqualTo(1); + assertThat(beans).hasSize(1); } /** @@ -697,7 +684,7 @@ public void parameterizedStaticFactoryMethod() { *

    See SPR-10411 */ @Test - public void parameterizedInstanceFactoryMethod() { + void parameterizedInstanceFactoryMethod() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); RootBeanDefinition rbd = new RootBeanDefinition(MocksControl.class); @@ -714,11 +701,11 @@ public void parameterizedInstanceFactoryMethod() { assertThat(bf.getType("mock")).isEqualTo(Runnable.class); assertThat(bf.getType("mock")).isEqualTo(Runnable.class); Map beans = bf.getBeansOfType(Runnable.class); - assertThat(beans.size()).isEqualTo(1); + assertThat(beans).hasSize(1); } @Test - public void parameterizedInstanceFactoryMethodWithNonResolvedClassName() { + void parameterizedInstanceFactoryMethodWithNonResolvedClassName() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); RootBeanDefinition rbd = new RootBeanDefinition(MocksControl.class); @@ -735,11 +722,11 @@ public void parameterizedInstanceFactoryMethodWithNonResolvedClassName() { assertThat(bf.getType("mock")).isEqualTo(Runnable.class); assertThat(bf.getType("mock")).isEqualTo(Runnable.class); Map beans = bf.getBeansOfType(Runnable.class); - assertThat(beans.size()).isEqualTo(1); + assertThat(beans).hasSize(1); } @Test - public void parameterizedInstanceFactoryMethodWithWrappedClassName() { + void parameterizedInstanceFactoryMethodWithWrappedClassName() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); RootBeanDefinition rbd = new RootBeanDefinition(); @@ -754,11 +741,11 @@ public void parameterizedInstanceFactoryMethodWithWrappedClassName() { assertThat(bf.getType("mock")).isEqualTo(Runnable.class); assertThat(bf.getType("mock")).isEqualTo(Runnable.class); Map beans = bf.getBeansOfType(Runnable.class); - assertThat(beans.size()).isEqualTo(1); + assertThat(beans).hasSize(1); } @Test - public void parameterizedInstanceFactoryMethodWithInvalidClassName() { + void parameterizedInstanceFactoryMethodWithInvalidClassName() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); RootBeanDefinition rbd = new RootBeanDefinition(MocksControl.class); @@ -775,11 +762,11 @@ public void parameterizedInstanceFactoryMethodWithInvalidClassName() { assertThat(bf.getType("mock")).isNull(); assertThat(bf.getType("mock")).isNull(); Map beans = bf.getBeansOfType(Runnable.class); - assertThat(beans.size()).isEqualTo(0); + assertThat(beans).hasSize(0); } @Test - public void parameterizedInstanceFactoryMethodWithIndexedArgument() { + void parameterizedInstanceFactoryMethodWithIndexedArgument() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); RootBeanDefinition rbd = new RootBeanDefinition(MocksControl.class); @@ -796,11 +783,11 @@ public void parameterizedInstanceFactoryMethodWithIndexedArgument() { assertThat(bf.getType("mock")).isEqualTo(Runnable.class); assertThat(bf.getType("mock")).isEqualTo(Runnable.class); Map beans = bf.getBeansOfType(Runnable.class); - assertThat(beans.size()).isEqualTo(1); + assertThat(beans).hasSize(1); } @Test // SPR-16720 - public void parameterizedInstanceFactoryMethodWithTempClassLoader() { + void parameterizedInstanceFactoryMethodWithTempClassLoader() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.setTempClassLoader(new OverridingClassLoader(getClass().getClassLoader())); @@ -818,11 +805,11 @@ public void parameterizedInstanceFactoryMethodWithTempClassLoader() { assertThat(bf.getType("mock")).isEqualTo(Runnable.class); assertThat(bf.getType("mock")).isEqualTo(Runnable.class); Map beans = bf.getBeansOfType(Runnable.class); - assertThat(beans.size()).isEqualTo(1); + assertThat(beans).hasSize(1); } @Test - public void testGenericMatchingWithBeanNameDifferentiation() { + void testGenericMatchingWithBeanNameDifferentiation() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.setAutowireCandidateResolver(new GenericTypeAwareAutowireCandidateResolver()); @@ -838,15 +825,15 @@ public void testGenericMatchingWithBeanNameDifferentiation() { String[] numberStoreNames = bf.getBeanNamesForType(ResolvableType.forClass(NumberStore.class)); String[] doubleStoreNames = bf.getBeanNamesForType(ResolvableType.forClassWithGenerics(NumberStore.class, Double.class)); String[] floatStoreNames = bf.getBeanNamesForType(ResolvableType.forClassWithGenerics(NumberStore.class, Float.class)); - assertThat(numberStoreNames.length).isEqualTo(2); + assertThat(numberStoreNames).hasSize(2); assertThat(numberStoreNames[0]).isEqualTo("doubleStore"); assertThat(numberStoreNames[1]).isEqualTo("floatStore"); - assertThat(doubleStoreNames.length).isEqualTo(0); - assertThat(floatStoreNames.length).isEqualTo(0); + assertThat(doubleStoreNames).hasSize(0); + assertThat(floatStoreNames).hasSize(0); } @Test - public void testGenericMatchingWithFullTypeDifferentiation() { + void testGenericMatchingWithFullTypeDifferentiation() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE); bf.setAutowireCandidateResolver(new GenericTypeAwareAutowireCandidateResolver()); @@ -867,12 +854,12 @@ public void testGenericMatchingWithFullTypeDifferentiation() { String[] numberStoreNames = bf.getBeanNamesForType(ResolvableType.forClass(NumberStore.class)); String[] doubleStoreNames = bf.getBeanNamesForType(ResolvableType.forClassWithGenerics(NumberStore.class, Double.class)); String[] floatStoreNames = bf.getBeanNamesForType(ResolvableType.forClassWithGenerics(NumberStore.class, Float.class)); - assertThat(numberStoreNames.length).isEqualTo(2); + assertThat(numberStoreNames).hasSize(2); assertThat(numberStoreNames[0]).isEqualTo("store1"); assertThat(numberStoreNames[1]).isEqualTo("store2"); - assertThat(doubleStoreNames.length).isEqualTo(1); + assertThat(doubleStoreNames).hasSize(1); assertThat(doubleStoreNames[0]).isEqualTo("store1"); - assertThat(floatStoreNames.length).isEqualTo(1); + assertThat(floatStoreNames).hasSize(1); assertThat(floatStoreNames[0]).isEqualTo("store2"); ObjectProvider> numberStoreProvider = bf.getBeanProvider(ResolvableType.forClass(NumberStore.class)); @@ -896,12 +883,12 @@ public void testGenericMatchingWithFullTypeDifferentiation() { assertThat(resolved.get(0)).isSameAs(bf.getBean("store1")); assertThat(resolved.get(1)).isSameAs(bf.getBean("store2")); - resolved = numberStoreProvider.stream().collect(Collectors.toList()); + resolved = numberStoreProvider.stream().toList(); assertThat(resolved.size()).isEqualTo(2); assertThat(resolved.get(0)).isSameAs(bf.getBean("store1")); assertThat(resolved.get(1)).isSameAs(bf.getBean("store2")); - resolved = numberStoreProvider.orderedStream().collect(Collectors.toList()); + resolved = numberStoreProvider.orderedStream().toList(); assertThat(resolved.size()).isEqualTo(2); assertThat(resolved.get(0)).isSameAs(bf.getBean("store2")); assertThat(resolved.get(1)).isSameAs(bf.getBean("store1")); @@ -938,7 +925,7 @@ public void testGenericMatchingWithFullTypeDifferentiation() { } @Test - public void testGenericMatchingWithUnresolvedOrderedStream() { + void testGenericMatchingWithUnresolvedOrderedStream() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE); bf.setAutowireCandidateResolver(new GenericTypeAwareAutowireCandidateResolver()); @@ -951,7 +938,7 @@ public void testGenericMatchingWithUnresolvedOrderedStream() { bf.registerBeanDefinition("store2", bd2); ObjectProvider> numberStoreProvider = bf.getBeanProvider(ResolvableType.forClass(NumberStore.class)); - List> resolved = numberStoreProvider.orderedStream().collect(Collectors.toList()); + List> resolved = numberStoreProvider.orderedStream().toList(); assertThat(resolved.size()).isEqualTo(2); assertThat(resolved.get(0)).isSameAs(bf.getBean("store2")); assertThat(resolved.get(1)).isSameAs(bf.getBean("store1")); @@ -1010,11 +997,8 @@ public static class MocksControl { @SuppressWarnings("unchecked") public T createMock(Class toMock) { return (T) Proxy.newProxyInstance(BeanFactoryGenericsTests.class.getClassLoader(), new Class[] {toMock}, - new InvocationHandler() { - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - throw new UnsupportedOperationException("mocked!"); - } + (InvocationHandler) (proxy, method, args) -> { + throw new UnsupportedOperationException("mocked!"); }); } } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanFactorySupplierTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanFactorySupplierTests.java new file mode 100644 index 000000000000..d07cd08abdaa --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanFactorySupplierTests.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-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.beans.factory.support; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.util.function.ThrowingSupplier; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link AbstractAutowireCapableBeanFactory} instance supplier + * support. + * + * @author Phillip Webb + */ +public class BeanFactorySupplierTests { + + @Test + void getBeanWhenUsingRegularSupplier() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + RootBeanDefinition beanDefinition = new RootBeanDefinition(); + beanDefinition.setInstanceSupplier(() -> "I am supplied"); + beanFactory.registerBeanDefinition("test", beanDefinition); + assertThat(beanFactory.getBean("test")).isEqualTo("I am supplied"); + } + + @Test + void getBeanWhenUsingInstanceSupplier() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + RootBeanDefinition beanDefinition = new RootBeanDefinition(); + beanDefinition.setInstanceSupplier(InstanceSupplier + .of(registeredBean -> "I am bean " + registeredBean.getBeanName())); + beanFactory.registerBeanDefinition("test", beanDefinition); + assertThat(beanFactory.getBean("test")).isEqualTo("I am bean test"); + } + + @Test + void getBeanWhenUsingThrowableSupplier() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + RootBeanDefinition beanDefinition = new RootBeanDefinition(); + beanDefinition.setInstanceSupplier(ThrowingSupplier.of(() -> "I am supplied")); + beanFactory.registerBeanDefinition("test", beanDefinition); + assertThat(beanFactory.getBean("test")).isEqualTo("I am supplied"); + } + + @Test + void getBeanWhenUsingThrowableSupplierThatThrowsCheckedException() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + RootBeanDefinition beanDefinition = new RootBeanDefinition(); + beanDefinition.setInstanceSupplier(ThrowingSupplier.of(() -> { + throw new IOException("fail"); + })); + beanFactory.registerBeanDefinition("test", beanDefinition); + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> beanFactory.getBean("test")) + .withCauseInstanceOf(IOException.class); + } + + @Test + void getBeanWhenUsingThrowableSupplierThatThrowsRuntimeException() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + RootBeanDefinition beanDefinition = new RootBeanDefinition(); + beanDefinition.setInstanceSupplier(ThrowingSupplier.of(() -> { + throw new IllegalStateException("fail"); + })); + beanFactory.registerBeanDefinition("test", beanDefinition); + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> beanFactory.getBean("test")) + .withCauseInstanceOf(IllegalStateException.class); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/ConstructorResolverAotTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/ConstructorResolverAotTests.java new file mode 100644 index 000000000000..da6b27d3609e --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/ConstructorResolverAotTests.java @@ -0,0 +1,491 @@ +/* + * Copyright 2002-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.beans.factory.support; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Executable; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.Executor; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.testfixture.beans.factory.generator.factory.NumberHolder; +import org.springframework.beans.testfixture.beans.factory.generator.factory.NumberHolderFactoryBean; +import org.springframework.beans.testfixture.beans.factory.generator.factory.SampleFactory; +import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; +import org.springframework.lang.Nullable; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link ConstructorResolver} focused on AOT constructor and factory + * method resolution. + * + * @author Stephane Nicoll + * @author Phillip Webb + */ +class ConstructorResolverAotTests { + + @Test + void detectBeanInstanceExecutableWithBeanClassAndFactoryMethodName() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerSingleton("testBean", "test"); + BeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(SampleFactory.class).setFactoryMethod("create") + .addConstructorArgReference("testBean").getBeanDefinition(); + Executable executable = resolve(beanFactory, beanDefinition); + assertThat(executable).isNotNull().isEqualTo( + ReflectionUtils.findMethod(SampleFactory.class, "create", String.class)); + } + + @Test + void detectBeanInstanceExecutableWithBeanClassNameAndFactoryMethodName() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerSingleton("testBean", "test"); + BeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(SampleFactory.class.getName()) + .setFactoryMethod("create").addConstructorArgReference("testBean") + .getBeanDefinition(); + Executable executable = resolve(beanFactory, beanDefinition); + assertThat(executable).isNotNull().isEqualTo( + ReflectionUtils.findMethod(SampleFactory.class, "create", String.class)); + } + + @Test + void beanDefinitionWithFactoryMethodNameAndAssignableConstructorArg() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerSingleton("testNumber", 1L); + beanFactory.registerSingleton("testBean", "test"); + BeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(SampleFactory.class).setFactoryMethod("create") + .addConstructorArgReference("testNumber") + .addConstructorArgReference("testBean").getBeanDefinition(); + Executable executable = resolve(beanFactory, beanDefinition); + assertThat(executable).isNotNull().isEqualTo(ReflectionUtils + .findMethod(SampleFactory.class, "create", Number.class, String.class)); + } + + @Test + void beanDefinitionWithFactoryMethodNameAndMatchingMethodNames() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + BeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(DummySampleFactory.class).setFactoryMethod("of") + .addConstructorArgValue(42).getBeanDefinition(); + Executable executable = resolve(beanFactory, beanDefinition); + assertThat(executable).isNotNull().isEqualTo(ReflectionUtils + .findMethod(DummySampleFactory.class, "of", Integer.class)); + } + + @Test + void beanDefinitionWithFactoryMethodNameAndOverriddenMethod() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerBeanDefinition("config", new RootBeanDefinition(ExtendedSampleFactory.class)); + BeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(String.class).setFactoryMethodOnBean("resolve", "config") + .addConstructorArgValue("test").getBeanDefinition(); + Executable executable = resolve(beanFactory, beanDefinition); + assertThat(executable).isNotNull().isEqualTo(ReflectionUtils + .findMethod(ExtendedSampleFactory.class, "resolve", String.class)); + } + + @Test + void detectBeanInstanceExecutableWithBeanClassAndFactoryMethodNameIgnoreTargetType() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerSingleton("testBean", "test"); + RootBeanDefinition beanDefinition = (RootBeanDefinition) BeanDefinitionBuilder + .rootBeanDefinition(SampleFactory.class).setFactoryMethod("create") + .addConstructorArgReference("testBean").getBeanDefinition(); + beanDefinition.setTargetType(String.class); + Executable executable = resolve(beanFactory, beanDefinition); + assertThat(executable).isNotNull().isEqualTo( + ReflectionUtils.findMethod(SampleFactory.class, "create", String.class)); + } + + @Test + void beanDefinitionWithConstructorArgsForMultipleConstructors() throws Exception { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerSingleton("testNumber", 1L); + beanFactory.registerSingleton("testBean", "test"); + BeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(SampleBeanWithConstructors.class) + .addConstructorArgReference("testNumber") + .addConstructorArgReference("testBean").getBeanDefinition(); + Executable executable = resolve(beanFactory, beanDefinition); + assertThat(executable).isNotNull().isEqualTo(SampleBeanWithConstructors.class + .getDeclaredConstructor(Number.class, String.class)); + } + + @Test + void beanDefinitionWithMultiArgConstructorAndMatchingValue() throws NoSuchMethodException { + BeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(MultiConstructorSample.class) + .addConstructorArgValue(42).getBeanDefinition(); + Executable executable = resolve(new DefaultListableBeanFactory(), beanDefinition); + assertThat(executable).isNotNull().isEqualTo( + MultiConstructorSample.class.getDeclaredConstructor(Integer.class)); + } + + @Test + void beanDefinitionWithMultiArgConstructorAndMatchingArrayValue() throws NoSuchMethodException { + BeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(MultiConstructorArraySample.class) + .addConstructorArgValue(42).getBeanDefinition(); + Executable executable = resolve(new DefaultListableBeanFactory(), beanDefinition); + assertThat(executable).isNotNull().isEqualTo(MultiConstructorArraySample.class + .getDeclaredConstructor(Integer[].class)); + } + + @Test + void beanDefinitionWithMultiArgConstructorAndMatchingListValue() throws NoSuchMethodException { + BeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(MultiConstructorListSample.class) + .addConstructorArgValue(42).getBeanDefinition(); + Executable executable = resolve(new DefaultListableBeanFactory(), beanDefinition); + assertThat(executable).isNotNull().isEqualTo( + MultiConstructorListSample.class.getDeclaredConstructor(List.class)); + } + + @Test + void beanDefinitionWithMultiArgConstructorAndMatchingValueAsInnerBean() throws NoSuchMethodException { + BeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(MultiConstructorSample.class) + .addConstructorArgValue( + BeanDefinitionBuilder.rootBeanDefinition(Integer.class, "valueOf") + .addConstructorArgValue("42").getBeanDefinition()) + .getBeanDefinition(); + Executable executable = resolve(new DefaultListableBeanFactory(), beanDefinition); + assertThat(executable).isNotNull().isEqualTo( + MultiConstructorSample.class.getDeclaredConstructor(Integer.class)); + } + + @Test + void beanDefinitionWithMultiArgConstructorAndMatchingValueAsInnerBeanFactory() throws NoSuchMethodException { + BeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(MultiConstructorSample.class) + .addConstructorArgValue(BeanDefinitionBuilder + .rootBeanDefinition(IntegerFactoryBean.class).getBeanDefinition()) + .getBeanDefinition(); + Executable executable = resolve(new DefaultListableBeanFactory(), beanDefinition); + assertThat(executable).isNotNull().isEqualTo( + MultiConstructorSample.class.getDeclaredConstructor(Integer.class)); + } + + @Test + void beanDefinitionWithMultiArgConstructorAndNonMatchingValue() { + BeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(MultiConstructorSample.class) + .addConstructorArgValue(Locale.ENGLISH).getBeanDefinition(); + assertThatIllegalStateException().isThrownBy(() -> resolve(new DefaultListableBeanFactory(), beanDefinition)) + .withMessageContaining(MultiConstructorSample.class.getName()) + .withMessageContaining("and argument types [java.util.Locale]"); + } + + @Test + void beanDefinitionWithMultiArgConstructorAndNonMatchingValueAsInnerBean() { + BeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(MultiConstructorSample.class) + .addConstructorArgValue(BeanDefinitionBuilder + .rootBeanDefinition(Locale.class, "getDefault") + .getBeanDefinition()) + .getBeanDefinition(); + assertThatIllegalStateException().isThrownBy(() -> resolve(new DefaultListableBeanFactory(), beanDefinition)) + .withMessageContaining(MultiConstructorSample.class.getName()) + .withMessageContaining("and argument types [java.util.Locale]"); + } + + @Test + void detectBeanInstanceExecutableWithFactoryBeanSetInBeanClass() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + RootBeanDefinition beanDefinition = new RootBeanDefinition(); + beanDefinition.setTargetType( + ResolvableType.forClassWithGenerics(NumberHolder.class, Integer.class)); + beanDefinition.setBeanClass(NumberHolderFactoryBean.class); + Executable executable = resolve(beanFactory, beanDefinition); + assertThat(executable).isNotNull() + .isEqualTo(NumberHolderFactoryBean.class.getDeclaredConstructors()[0]); + } + + @Test + void detectBeanInstanceExecutableWithFactoryBeanSetInBeanClassAndNoResolvableType() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + RootBeanDefinition beanDefinition = new RootBeanDefinition(); + beanDefinition.setBeanClass(NumberHolderFactoryBean.class); + Executable executable = resolve(beanFactory, beanDefinition); + assertThat(executable).isNotNull() + .isEqualTo(NumberHolderFactoryBean.class.getDeclaredConstructors()[0]); + } + + @Test + void detectBeanInstanceExecutableWithFactoryBeanSetInBeanClassThatDoesNotMatchTargetType() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + RootBeanDefinition beanDefinition = new RootBeanDefinition(); + beanDefinition.setTargetType( + ResolvableType.forClassWithGenerics(NumberHolder.class, String.class)); + beanDefinition.setBeanClass(NumberHolderFactoryBean.class); + assertThatIllegalStateException() + .isThrownBy(() -> resolve(beanFactory, beanDefinition)) + .withMessageContaining("Incompatible target type") + .withMessageContaining(NumberHolder.class.getName()) + .withMessageContaining(NumberHolderFactoryBean.class.getName()); + } + + @Test + void beanDefinitionWithClassArrayConstructorArgAndStringArrayValueType() throws NoSuchMethodException { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + BeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(ConstructorClassArraySample.class.getName()) + .addConstructorArgValue(new String[] { "test1, test2" }) + .getBeanDefinition(); + Executable executable = resolve(beanFactory, beanDefinition); + assertThat(executable).isNotNull().isEqualTo( + ConstructorClassArraySample.class.getDeclaredConstructor(Class[].class)); + } + + @Test + void beanDefinitionWithClassArrayConstructorArgAndStringValueType() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + BeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(ConstructorClassArraySample.class.getName()) + .addConstructorArgValue("test1").getBeanDefinition(); + Executable executable = resolve(beanFactory, beanDefinition); + assertThat(executable).isNotNull().isEqualTo( + ConstructorClassArraySample.class.getDeclaredConstructors()[0]); + } + + @Test + void beanDefinitionWithClassArrayConstructorArgAndAnotherMatchingConstructor() throws NoSuchMethodException { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + BeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(MultiConstructorClassArraySample.class.getName()) + .addConstructorArgValue(new String[] { "test1, test2" }) + .getBeanDefinition(); + Executable executable = resolve(beanFactory, beanDefinition); + assertThat(executable).isNotNull() + .isEqualTo(MultiConstructorClassArraySample.class + .getDeclaredConstructor(String[].class)); + } + + @Test + void beanDefinitionWithClassArrayFactoryMethodArgAndStringArrayValueType() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + BeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(ClassArrayFactoryMethodSample.class.getName()) + .setFactoryMethod("of") + .addConstructorArgValue(new String[] { "test1, test2" }) + .getBeanDefinition(); + Executable executable = resolve(beanFactory, beanDefinition); + assertThat(executable).isNotNull().isEqualTo(ReflectionUtils + .findMethod(ClassArrayFactoryMethodSample.class, "of", Class[].class)); + } + + @Test + void beanDefinitionWithClassArrayFactoryMethodArgAndAnotherMatchingConstructor() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition( + ClassArrayFactoryMethodSampleWithAnotherFactoryMethod.class.getName()) + .setFactoryMethod("of").addConstructorArgValue("test1") + .getBeanDefinition(); + Executable executable = resolve(beanFactory, beanDefinition); + assertThat(executable).isNotNull() + .isEqualTo(ReflectionUtils.findMethod( + ClassArrayFactoryMethodSampleWithAnotherFactoryMethod.class, "of", + String[].class)); + } + + @Test + void beanDefinitionWithMultiArgConstructorAndPrimitiveConversion() throws NoSuchMethodException { + BeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(ConstructorPrimitiveFallback.class) + .addConstructorArgValue("true").getBeanDefinition(); + Executable executable = resolve(new DefaultListableBeanFactory(), beanDefinition); + assertThat(executable).isEqualTo( + ConstructorPrimitiveFallback.class.getDeclaredConstructor(boolean.class)); + } + + @Test + void beanDefinitionWithFactoryWithOverloadedClassMethodsOnInterface() { + BeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(FactoryWithOverloadedClassMethodsOnInterface.class) + .setFactoryMethod("byAnnotation").addConstructorArgValue(Nullable.class) + .getBeanDefinition(); + Executable executable = resolve(new DefaultListableBeanFactory(), beanDefinition); + assertThat(executable).isEqualTo(ReflectionUtils.findMethod( + FactoryWithOverloadedClassMethodsOnInterface.class, "byAnnotation", + Class.class)); + } + + + private Executable resolve(DefaultListableBeanFactory beanFactory, BeanDefinition beanDefinition) { + return new ConstructorResolver(beanFactory).resolveConstructorOrFactoryMethod( + "testBean", (RootBeanDefinition) beanDefinition); + } + + + static class IntegerFactoryBean implements FactoryBean { + + @Override + public Integer getObject() { + return 42; + } + + @Override + public Class getObjectType() { + return Integer.class; + } + } + + @SuppressWarnings("unused") + static class MultiConstructorSample { + + MultiConstructorSample(String name) { + } + + MultiConstructorSample(Integer value) { + } + } + + @SuppressWarnings("unused") + static class MultiConstructorArraySample { + + public MultiConstructorArraySample(String... names) { + } + + public MultiConstructorArraySample(Integer... values) { + } + } + + @SuppressWarnings("unused") + static class MultiConstructorListSample { + + public MultiConstructorListSample(String name) { + } + + public MultiConstructorListSample(List values) { + } + } + + interface DummyInterface { + + static String of(Object o) { + return o.toString(); + } + } + + @SuppressWarnings("unused") + static class DummySampleFactory implements DummyInterface { + + static String of(Integer value) { + return value.toString(); + } + + protected String resolve(String value) { + return value; + } + } + + @SuppressWarnings("unused") + static class ExtendedSampleFactory extends DummySampleFactory { + + @Override + protected String resolve(String value) { + return super.resolve(value); + } + } + + @SuppressWarnings("unused") + static class ConstructorClassArraySample { + + ConstructorClassArraySample(Class... classArrayArg) { + } + + ConstructorClassArraySample(Executor somethingElse) { + } + } + + @SuppressWarnings("unused") + static class MultiConstructorClassArraySample { + + MultiConstructorClassArraySample(Class... classArrayArg) { + } + + MultiConstructorClassArraySample(String... stringArrayArg) { + } + } + + @SuppressWarnings("unused") + static class ClassArrayFactoryMethodSample { + + static String of(Class[] classArrayArg) { + return "test"; + } + } + + @SuppressWarnings("unused") + static class ClassArrayFactoryMethodSampleWithAnotherFactoryMethod { + + static String of(Class[] classArrayArg) { + return "test"; + } + + static String of(String[] classArrayArg) { + return "test"; + } + } + + @SuppressWarnings("unnused") + static class ConstructorPrimitiveFallback { + + public ConstructorPrimitiveFallback(boolean useDefaultExecutor) { + } + + public ConstructorPrimitiveFallback(Executor executor) { + } + } + + static class SampleBeanWithConstructors { + + public SampleBeanWithConstructors() { + } + + public SampleBeanWithConstructors(String name) { + } + + public SampleBeanWithConstructors(Number number, String name) { + } + } + + interface FactoryWithOverloadedClassMethodsOnInterface { + + static FactoryWithOverloadedClassMethodsOnInterface byAnnotation( + Class annotationType) { + return byAnnotation(annotationType, SearchStrategy.INHERITED_ANNOTATIONS); + } + + static FactoryWithOverloadedClassMethodsOnInterface byAnnotation( + Class annotationType, + SearchStrategy searchStrategy) { + return null; + } + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistryTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistryTests.java index 6e3f8465e9e8..7c22251d8ab3 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistryTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -18,8 +18,6 @@ import org.junit.jupiter.api.Test; -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.ObjectFactory; import org.springframework.beans.testfixture.beans.DerivedTestBean; import org.springframework.beans.testfixture.beans.TestBean; @@ -40,12 +38,7 @@ public void testSingletons() { beanRegistry.registerSingleton("tb", tb); assertThat(beanRegistry.getSingleton("tb")).isSameAs(tb); - TestBean tb2 = (TestBean) beanRegistry.getSingleton("tb2", new ObjectFactory() { - @Override - public Object getObject() throws BeansException { - return new TestBean(); - } - }); + TestBean tb2 = (TestBean) beanRegistry.getSingleton("tb2", TestBean::new); assertThat(beanRegistry.getSingleton("tb2")).isSameAs(tb2); assertThat(beanRegistry.getSingleton("tb")).isSameAs(tb); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/InstanceSupplierTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/InstanceSupplierTests.java new file mode 100644 index 000000000000..c66a897b3ba1 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/InstanceSupplierTests.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-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.beans.factory.support; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.Test; + +import org.springframework.util.function.ThrowingBiFunction; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link InstanceSupplier}. + * + * @author Phillip Webb + */ +class InstanceSupplierTests { + + private final RegisteredBean registeredBean = RegisteredBean + .of(new DefaultListableBeanFactory(), "test"); + + @Test + void getWithoutRegisteredBeanThrowsException() { + InstanceSupplier supplier = registeredBean -> "test"; + assertThatIllegalStateException().isThrownBy(() -> supplier.get()) + .withMessage("No RegisteredBean parameter provided"); + } + + @Test + void getWithExceptionWithoutRegisteredBeanThrowsException() { + InstanceSupplier supplier = registeredBean -> "test"; + assertThatIllegalStateException().isThrownBy(() -> supplier.getWithException()) + .withMessage("No RegisteredBean parameter provided"); + } + + @Test + void getReturnsResult() throws Exception { + InstanceSupplier supplier = registeredBean -> "test"; + assertThat(supplier.get(this.registeredBean)).isEqualTo("test"); + } + + @Test + void andThenWhenFunctionIsNullThrowsException() { + InstanceSupplier supplier = registeredBean -> "test"; + ThrowingBiFunction after = null; + assertThatIllegalArgumentException().isThrownBy(() -> supplier.andThen(after)) + .withMessage("'after' function must not be null"); + } + + @Test + void andThenAppliesFunctionToObtainResult() throws Exception { + InstanceSupplier supplier = registeredBean -> "bean"; + supplier = supplier.andThen( + (registeredBean, string) -> registeredBean.getBeanName() + "-" + string); + assertThat(supplier.get(this.registeredBean)).isEqualTo("test-bean"); + } + + @Test + void andThenWhenInstanceSupplierHasFactoryMethod() throws Exception { + Method factoryMethod = getClass().getDeclaredMethod("andThenWhenInstanceSupplierHasFactoryMethod"); + InstanceSupplier supplier = InstanceSupplier.using(factoryMethod, () -> "bean"); + supplier = supplier.andThen( + (registeredBean, string) -> registeredBean.getBeanName() + "-" + string); + assertThat(supplier.get(this.registeredBean)).isEqualTo("test-bean"); + assertThat(supplier.getFactoryMethod()).isSameAs(factoryMethod); + } + + @Test + void ofSupplierWhenInstanceSupplierReturnsSameInstance() { + InstanceSupplier supplier = registeredBean -> "test"; + assertThat(InstanceSupplier.of(supplier)).isSameAs(supplier); + } + + @Test + void usingSupplierAdaptsToInstanceSupplier() throws Exception { + InstanceSupplier instanceSupplier = InstanceSupplier.using(() -> "test"); + assertThat(instanceSupplier.get(this.registeredBean)).isEqualTo("test"); + } + + @Test + void ofInstanceSupplierAdaptsToInstanceSupplier() throws Exception { + InstanceSupplier instanceSupplier = InstanceSupplier + .of(registeredBean -> "test"); + assertThat(instanceSupplier.get(this.registeredBean)).isEqualTo("test"); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/LookupMethodTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/LookupMethodTests.java index eeb34b6f8f1e..414fd0157661 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/LookupMethodTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/LookupMethodTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -36,7 +36,7 @@ public class LookupMethodTests { @BeforeEach - public void setUp() { + public void setup() { beanFactory = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory); reader.loadBeanDefinitions(new ClassPathResource("lookupMethodTests.xml", getClass())); @@ -83,8 +83,8 @@ public void testWithTwoConstructorArg() { public void testWithThreeArgsShouldFail() { AbstractBean bean = (AbstractBean) beanFactory.getBean("abstractBean"); assertThat(bean).isNotNull(); - assertThatExceptionOfType(AbstractMethodError.class).as("does not have a three arg constructor").isThrownBy(() -> - bean.getThreeArguments("name", 1, 2)); + assertThatExceptionOfType(AbstractMethodError.class).as("does not have a three arg constructor") + .isThrownBy(() -> bean.getThreeArguments("name", 1, 2)); } @Test @@ -97,6 +97,21 @@ public void testWithOverriddenLookupMethod() { assertThat(expected.isJedi()).isTrue(); } + @Test + public void testWithGenericBean() { + RootBeanDefinition bd = new RootBeanDefinition(NumberBean.class); + bd.getMethodOverrides().addOverride(new LookupOverride("getDoubleStore", null)); + bd.getMethodOverrides().addOverride(new LookupOverride("getFloatStore", null)); + beanFactory.registerBeanDefinition("numberBean", bd); + beanFactory.registerBeanDefinition("doubleStore", new RootBeanDefinition(DoubleStore.class)); + beanFactory.registerBeanDefinition("floatStore", new RootBeanDefinition(FloatStore.class)); + + NumberBean bean = (NumberBean) beanFactory.getBean("numberBean"); + assertThat(bean).isNotNull(); + assertThat(beanFactory.getBean(DoubleStore.class)).isSameAs(bean.getDoubleStore()); + assertThat(beanFactory.getBean(FloatStore.class)).isSameAs(bean.getFloatStore()); + } + public static abstract class AbstractBean { @@ -111,4 +126,24 @@ public static abstract class AbstractBean { public abstract TestBean getThreeArguments(String name, int age, int anotherArg); } + + public static class NumberStore { + } + + + public static class DoubleStore extends NumberStore { + } + + + public static class FloatStore extends NumberStore { + } + + + public static abstract class NumberBean { + + public abstract NumberStore getDoubleStore(); + + public abstract NumberStore getFloatStore(); + } + } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/ManagedListTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/ManagedListTests.java index 9bf6576d518c..9d8a1cbe36e0 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/ManagedListTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/ManagedListTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -25,71 +25,61 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** + * Unit tests for {@link ManagedList}. + * * @author Rick Evans * @author Juergen Hoeller * @author Sam Brannen */ @SuppressWarnings({ "rawtypes", "unchecked" }) -public class ManagedListTests { +class ManagedListTests { @Test - public void mergeSunnyDay() { - ManagedList parent = new ManagedList(); - parent.add("one"); - parent.add("two"); - ManagedList child = new ManagedList(); - child.add("three"); + void mergeSunnyDay() { + ManagedList parent = ManagedList.of("one", "two"); + ManagedList child = ManagedList.of("three"); child.setMergeEnabled(true); List mergedList = child.merge(parent); - assertThat(mergedList.size()).as("merge() obviously did not work.").isEqualTo(3); + assertThat(mergedList).as("merge() obviously did not work.").containsExactly("one", "two", "three"); } @Test - public void mergeWithNullParent() { - ManagedList child = new ManagedList(); - child.add("one"); + void mergeWithNullParent() { + ManagedList child = ManagedList.of("one"); child.setMergeEnabled(true); assertThat(child.merge(null)).isSameAs(child); } @Test - public void mergeNotAllowedWhenMergeNotEnabled() { + void mergeNotAllowedWhenMergeNotEnabled() { ManagedList child = new ManagedList(); - assertThatIllegalStateException().isThrownBy(() -> - child.merge(null)); + assertThatIllegalStateException().isThrownBy(() -> child.merge(null)); } @Test - public void mergeWithNonCompatibleParentType() { - ManagedList child = new ManagedList(); - child.add("one"); + void mergeWithIncompatibleParentType() { + ManagedList child = ManagedList.of("one"); child.setMergeEnabled(true); - assertThatIllegalArgumentException().isThrownBy(() -> - child.merge("hello")); + assertThatIllegalArgumentException().isThrownBy(() -> child.merge("hello")); } @Test - public void mergeEmptyChild() { - ManagedList parent = new ManagedList(); - parent.add("one"); - parent.add("two"); + void mergeEmptyChild() { + ManagedList parent = ManagedList.of("one", "two"); ManagedList child = new ManagedList(); child.setMergeEnabled(true); List mergedList = child.merge(parent); - assertThat(mergedList.size()).as("merge() obviously did not work.").isEqualTo(2); + assertThat(mergedList).as("merge() obviously did not work.").containsExactly("one", "two"); } @Test - public void mergeChildValuesOverrideTheParents() { + void mergedChildValuesDoNotOverrideTheParents() { // doesn't make much sense in the context of a list... - ManagedList parent = new ManagedList(); - parent.add("one"); - parent.add("two"); - ManagedList child = new ManagedList(); - child.add("one"); + ManagedList parent = ManagedList.of("one", "two"); + ManagedList child = ManagedList.of("one"); child.setMergeEnabled(true); List mergedList = child.merge(parent); - assertThat(mergedList.size()).as("merge() obviously did not work.").isEqualTo(3); + assertThat(mergedList).as("merge() obviously did not work.").containsExactly("one", "two", "one"); } } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/ManagedMapTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/ManagedMapTests.java index 1f6dc5e416a4..aac2e43248ce 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/ManagedMapTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/ManagedMapTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -34,11 +34,9 @@ public class ManagedMapTests { @Test public void mergeSunnyDay() { - ManagedMap parent = new ManagedMap(); - parent.put("one", "one"); - parent.put("two", "two"); - ManagedMap child = new ManagedMap(); - child.put("three", "three"); + ManagedMap parent = ManagedMap.ofEntries(Map.entry("one", "one"), + Map.entry("two", "two")); + ManagedMap child = ManagedMap.ofEntries(Map.entry("tree", "three")); child.setMergeEnabled(true); Map mergedMap = (Map) child.merge(parent); assertThat(mergedMap.size()).as("merge() obviously did not work.").isEqualTo(3); @@ -67,9 +65,8 @@ public void mergeNotAllowedWhenMergeNotEnabled() { @Test public void mergeEmptyChild() { - ManagedMap parent = new ManagedMap(); - parent.put("one", "one"); - parent.put("two", "two"); + ManagedMap parent = ManagedMap.ofEntries(Map.entry("one", "one"), + Map.entry("two", "two")); ManagedMap child = new ManagedMap(); child.setMergeEnabled(true); Map mergedMap = (Map) child.merge(parent); @@ -78,11 +75,9 @@ public void mergeEmptyChild() { @Test public void mergeChildValuesOverrideTheParents() { - ManagedMap parent = new ManagedMap(); - parent.put("one", "one"); - parent.put("two", "two"); - ManagedMap child = new ManagedMap(); - child.put("one", "fork"); + ManagedMap parent = ManagedMap.ofEntries(Map.entry("one", "one"), + Map.entry("two", "two")); + ManagedMap child = ManagedMap.ofEntries(Map.entry("one", "fork")); child.setMergeEnabled(true); Map mergedMap = (Map) child.merge(parent); // child value for 'one' must override parent value... diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/ManagedSetTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/ManagedSetTests.java index 39c08997814c..01f330f1c5a7 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/ManagedSetTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/ManagedSetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -25,70 +25,62 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** + * Unit tests for {@link ManagedSet}. + * * @author Rick Evans * @author Juergen Hoeller * @author Sam Brannen */ @SuppressWarnings({ "rawtypes", "unchecked" }) -public class ManagedSetTests { +class ManagedSetTests { @Test - public void mergeSunnyDay() { - ManagedSet parent = new ManagedSet(); - parent.add("one"); - parent.add("two"); - ManagedSet child = new ManagedSet(); + void mergeSunnyDay() { + ManagedSet parent = ManagedSet.of("one", "two"); + ManagedSet child = ManagedSet.of("three"); child.add("three"); + child.add("four"); child.setMergeEnabled(true); Set mergedSet = child.merge(parent); - assertThat(mergedSet.size()).as("merge() obviously did not work.").isEqualTo(3); + assertThat(mergedSet).as("merge() obviously did not work.").containsExactly("one", "two", "three", "four"); } @Test - public void mergeWithNullParent() { - ManagedSet child = new ManagedSet(); - child.add("one"); + void mergeWithNullParent() { + ManagedSet child = ManagedSet.of("one"); child.setMergeEnabled(true); assertThat(child.merge(null)).isSameAs(child); } @Test - public void mergeNotAllowedWhenMergeNotEnabled() { - assertThatIllegalStateException().isThrownBy(() -> - new ManagedSet().merge(null)); + void mergeNotAllowedWhenMergeNotEnabled() { + assertThatIllegalStateException().isThrownBy(() -> new ManagedSet().merge(null)); } @Test - public void mergeWithNonCompatibleParentType() { - ManagedSet child = new ManagedSet(); - child.add("one"); + void mergeWithNonCompatibleParentType() { + ManagedSet child = ManagedSet.of("one"); child.setMergeEnabled(true); - assertThatIllegalArgumentException().isThrownBy(() -> - child.merge("hello")); + assertThatIllegalArgumentException().isThrownBy(() -> child.merge("hello")); } @Test - public void mergeEmptyChild() { - ManagedSet parent = new ManagedSet(); - parent.add("one"); - parent.add("two"); + void mergeEmptyChild() { + ManagedSet parent = ManagedSet.of("one", "two"); ManagedSet child = new ManagedSet(); child.setMergeEnabled(true); Set mergedSet = child.merge(parent); - assertThat(mergedSet.size()).as("merge() obviously did not work.").isEqualTo(2); + assertThat(mergedSet).as("merge() obviously did not work.").containsExactly("one", "two"); } @Test - public void mergeChildValuesOverrideTheParents() { + void mergeChildValuesOverrideTheParents() { // asserts that the set contract is not violated during a merge() operation... - ManagedSet parent = new ManagedSet(); - parent.add("one"); - parent.add("two"); - ManagedSet child = new ManagedSet(); - child.add("one"); + ManagedSet parent = ManagedSet.of("one", "two"); + ManagedSet child = ManagedSet.of("one"); child.setMergeEnabled(true); Set mergedSet = child.merge(parent); - assertThat(mergedSet.size()).as("merge() obviously did not work.").isEqualTo(2); + assertThat(mergedSet).as("merge() obviously did not work.").containsExactly("one", "two"); } } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireBeanFactoryTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireBeanFactoryTests.java index 17fd92dd5ff4..f660a8af020d 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireBeanFactoryTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireBeanFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -244,7 +244,7 @@ public String getName() { @Target({ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Qualifier - private static @interface TestQualifier { + private @interface TestQualifier { } } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/RegisteredBeanTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/RegisteredBeanTests.java new file mode 100644 index 000000000000..2939d662e4b9 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/RegisteredBeanTests.java @@ -0,0 +1,217 @@ +/* + * Copyright 2002-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.beans.factory.support; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link RegisteredBean}. + * + * @author Phillip Webb + * @since 6.0 + */ +class RegisteredBeanTests { + + + private DefaultListableBeanFactory beanFactory; + + + @BeforeEach + void setup() { + this.beanFactory = new DefaultListableBeanFactory(); + this.beanFactory.registerBeanDefinition("bd", + new RootBeanDefinition(TestBean.class)); + this.beanFactory.registerSingleton("sb", new TestBean()); + } + + @Test + void ofWhenBeanFactoryIsNullThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> RegisteredBean.of(null, "bd")) + .withMessage("'beanFactory' must not be null"); + } + + @Test + void ofWhenBeanNameIsEmptyThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> RegisteredBean.of(this.beanFactory, null)) + .withMessage("'beanName' must not be empty"); + } + + @Test + void ofInnerBeanWhenInnerBeanIsNullThrowsException() { + RegisteredBean parent = RegisteredBean.of(this.beanFactory, "bd"); + assertThatIllegalArgumentException().isThrownBy( + () -> RegisteredBean.ofInnerBean(parent, (BeanDefinitionHolder) null)) + .withMessage("'innerBean' must not be null"); + } + + @Test + void ofInnerBeanWhenParentIsNullThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> RegisteredBean.ofInnerBean(null, + new RootBeanDefinition(TestInnerBean.class))) + .withMessage("'parent' must not be null"); + } + + @Test + void ofInnerBeanWhenInnerBeanDefinitionIsNullThrowsException() { + RegisteredBean parent = RegisteredBean.of(this.beanFactory, "bd"); + assertThatIllegalArgumentException() + .isThrownBy(() -> RegisteredBean.ofInnerBean(parent, "ib", null)) + .withMessage("'innerBeanDefinition' must not be null"); + } + + @Test + void getBeanNameReturnsBeanName() { + RegisteredBean registeredBean = RegisteredBean.of(this.beanFactory, "bd"); + assertThat(registeredBean.getBeanName()).isEqualTo("bd"); + } + + @Test + void getBeanNameWhenNamedInnerBeanReturnsBeanName() { + RegisteredBean parent = RegisteredBean.of(this.beanFactory, "bd"); + RegisteredBean registeredBean = RegisteredBean.ofInnerBean(parent, "ib", + new RootBeanDefinition(TestInnerBean.class)); + assertThat(registeredBean.getBeanName()).isEqualTo("ib"); + } + + @Test + void getBeanNameWhenUnnamedInnerBeanReturnsBeanName() { + RegisteredBean parent = RegisteredBean.of(this.beanFactory, "bd"); + RegisteredBean registeredBean = RegisteredBean.ofInnerBean(parent, + new RootBeanDefinition(TestInnerBean.class)); + assertThat(registeredBean.getBeanName()).startsWith("(inner bean)#"); + } + + @Test + void getBeanClassReturnsBeanClass() { + RegisteredBean registeredBean = RegisteredBean.of(this.beanFactory, "bd"); + assertThat(registeredBean.getBeanClass()).isEqualTo(TestBean.class); + } + + @Test + void getBeanTypeReturnsBeanType() { + RegisteredBean registeredBean = RegisteredBean.of(this.beanFactory, "bd"); + assertThat(registeredBean.getBeanType().toClass()).isEqualTo(TestBean.class); + } + + @Test + void getBeanTypeWhenHasInstanceBackedByLambdaDoesNotReturnLambdaType() { + this.beanFactory.registerBeanDefinition("bfpp", new RootBeanDefinition( + BeanFactoryPostProcessor.class, RegisteredBeanTests::getBeanFactoryPostProcessorLambda)); + this.beanFactory.getBean("bfpp"); + RegisteredBean registeredBean = RegisteredBean.of(this.beanFactory, "bfpp"); + assertThat(registeredBean.getBeanType().toClass()).isEqualTo(BeanFactoryPostProcessor.class); + } + + static BeanFactoryPostProcessor getBeanFactoryPostProcessorLambda() { + return bf -> {}; + } + + @Test + void getMergedBeanDefinitionReturnsMergedBeanDefinition() { + RegisteredBean registeredBean = RegisteredBean.of(this.beanFactory, "bd"); + assertThat(registeredBean.getMergedBeanDefinition().getBeanClass()) + .isEqualTo(TestBean.class); + } + + @Test + void getMergedBeanDefinitionWhenSingletonThrowsException() { + RegisteredBean registeredBean = RegisteredBean.of(this.beanFactory, "sb"); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class) + .isThrownBy(() -> registeredBean.getMergedBeanDefinition()); + } + + @Test + void getMergedBeanDefinitionWhenInnerBeanReturnsMergedBeanDefinition() { + RegisteredBean parent = RegisteredBean.of(this.beanFactory, "bd"); + RegisteredBean registeredBean = RegisteredBean.ofInnerBean(parent, + new RootBeanDefinition(TestInnerBean.class)); + assertThat(registeredBean.getMergedBeanDefinition().getBeanClass()) + .isEqualTo(TestInnerBean.class); + } + + @Test + void isInnerBeanWhenInnerBeanReturnsTrue() { + RegisteredBean parent = RegisteredBean.of(this.beanFactory, "bd"); + RegisteredBean registeredBean = RegisteredBean.ofInnerBean(parent, + new RootBeanDefinition(TestInnerBean.class)); + assertThat(registeredBean.isInnerBean()).isTrue(); + } + + @Test + void isInnerBeanWhenNotInnerBeanReturnsTrue() { + RegisteredBean registeredBean = RegisteredBean.of(this.beanFactory, "bd"); + assertThat(registeredBean.isInnerBean()).isFalse(); + } + + @Test + void getParentWhenInnerBeanReturnsParent() { + RegisteredBean parent = RegisteredBean.of(this.beanFactory, "bd"); + RegisteredBean registeredBean = RegisteredBean.ofInnerBean(parent, + new RootBeanDefinition(TestInnerBean.class)); + assertThat(registeredBean.getParent()).isSameAs(parent); + } + + @Test + void getParentWhenNotInnerBeanReturnsNull() { + RegisteredBean registeredBean = RegisteredBean.of(this.beanFactory, "bd"); + assertThat(registeredBean.getParent()).isNull(); + } + + @Test + void isGeneratedBeanNameWhenInnerBeanWithoutNameReturnsTrue() { + RegisteredBean parent = RegisteredBean.of(this.beanFactory, "bd"); + RegisteredBean registeredBean = RegisteredBean.ofInnerBean(parent, + new RootBeanDefinition(TestInnerBean.class)); + assertThat(registeredBean.isGeneratedBeanName()).isTrue(); + } + + @Test + void isGeneratedBeanNameWhenInnerBeanWithNameReturnsFalse() { + RegisteredBean parent = RegisteredBean.of(this.beanFactory, "bd"); + RegisteredBean registeredBean = RegisteredBean.ofInnerBean(parent, + new BeanDefinitionHolder(new RootBeanDefinition(TestInnerBean.class), + "test")); + assertThat(registeredBean.isGeneratedBeanName()).isFalse(); + } + + @Test + void isGeneratedBeanNameWhenNotInnerBeanReturnsFalse() { + RegisteredBean registeredBean = RegisteredBean.of(this.beanFactory, "bd"); + assertThat(registeredBean.isGeneratedBeanName()).isFalse(); + } + + static class TestBean { + + } + + static class TestInnerBean { + + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/RootBeanDefinitionTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/RootBeanDefinitionTests.java new file mode 100644 index 000000000000..ff39d5faeb75 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/RootBeanDefinitionTests.java @@ -0,0 +1,95 @@ +/* + * Copyright 2002-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.beans.factory.support; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.Test; + +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link RootBeanDefinition}. + * + * @author Stephane Nicoll + */ +class RootBeanDefinitionTests { + + @Test + void setInstanceSetResolvedFactoryMethod() { + InstanceSupplier instanceSupplier = mock(InstanceSupplier.class); + Method method = ReflectionUtils.findMethod(String.class, "toString"); + given(instanceSupplier.getFactoryMethod()).willReturn(method); + RootBeanDefinition beanDefinition = new RootBeanDefinition(String.class); + beanDefinition.setInstanceSupplier(instanceSupplier); + assertThat(beanDefinition.getResolvedFactoryMethod()).isEqualTo(method); + verify(instanceSupplier).getFactoryMethod(); + } + + @Test + void setInstanceDoesNotOverrideResolvedFactoryMethodWithNull() { + InstanceSupplier instanceSupplier = mock(InstanceSupplier.class); + given(instanceSupplier.getFactoryMethod()).willReturn(null); + Method method = ReflectionUtils.findMethod(String.class, "toString"); + RootBeanDefinition beanDefinition = new RootBeanDefinition(String.class); + beanDefinition.setResolvedFactoryMethod(method); + beanDefinition.setInstanceSupplier(instanceSupplier); + assertThat(beanDefinition.getResolvedFactoryMethod()).isEqualTo(method); + verify(instanceSupplier).getFactoryMethod(); + } + + @Test + void resolveDestroyMethodWithMatchingCandidateReplacedInferredVaue() { + RootBeanDefinition beanDefinition = new RootBeanDefinition(BeanWithCloseMethod.class); + beanDefinition.setDestroyMethodName(AbstractBeanDefinition.INFER_METHOD); + beanDefinition.resolveDestroyMethodIfNecessary(); + assertThat(beanDefinition.getDestroyMethodNames()).containsExactly("close"); + } + + @Test + void resolveDestroyMethodWithNoCandidateSetDestroyMethodNameToNull() { + RootBeanDefinition beanDefinition = new RootBeanDefinition(BeanWithNoDestroyMethod.class); + beanDefinition.setDestroyMethodName(AbstractBeanDefinition.INFER_METHOD); + beanDefinition.resolveDestroyMethodIfNecessary(); + assertThat(beanDefinition.getDestroyMethodNames()).isNull(); + } + + @Test + void resolveDestroyMethodWithNoResolvableType() { + RootBeanDefinition beanDefinition = new RootBeanDefinition(); + beanDefinition.setDestroyMethodName(AbstractBeanDefinition.INFER_METHOD); + beanDefinition.resolveDestroyMethodIfNecessary(); + assertThat(beanDefinition.getDestroyMethodNames()).isNull(); + } + + static class BeanWithCloseMethod { + + public void close() { + } + + } + + static class BeanWithNoDestroyMethod { + + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/security/CallbacksSecurityTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/security/CallbacksSecurityTests.java deleted file mode 100644 index 368e0b664725..000000000000 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/security/CallbacksSecurityTests.java +++ /dev/null @@ -1,479 +0,0 @@ -/* - * Copyright 2002-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.beans.factory.support.security; - -import java.lang.reflect.Method; -import java.net.URL; -import java.security.AccessControlContext; -import java.security.AccessController; -import java.security.Permissions; -import java.security.Policy; -import java.security.Principal; -import java.security.PrivilegedAction; -import java.security.PrivilegedExceptionAction; -import java.security.ProtectionDomain; -import java.util.PropertyPermission; -import java.util.Set; -import java.util.function.Consumer; - -import javax.security.auth.AuthPermission; -import javax.security.auth.Subject; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.BeanClassLoaderAware; -import org.springframework.beans.factory.BeanCreationException; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.BeanFactoryAware; -import org.springframework.beans.factory.BeanNameAware; -import org.springframework.beans.factory.DisposableBean; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.beans.factory.SmartFactoryBean; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.support.BeanDefinitionBuilder; -import org.springframework.beans.factory.support.DefaultListableBeanFactory; -import org.springframework.beans.factory.support.SecurityContextProvider; -import org.springframework.beans.factory.support.security.support.ConstructorBean; -import org.springframework.beans.factory.support.security.support.CustomCallbackBean; -import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; -import org.springframework.core.NestedRuntimeException; -import org.springframework.core.io.DefaultResourceLoader; -import org.springframework.core.io.Resource; -import org.springframework.core.testfixture.security.TestPrincipal; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - -/** - * Security test case. Checks whether the container uses its privileges for its - * internal work but does not leak them when touching/calling user code. - * - *

    The first half of the test case checks that permissions are downgraded when - * calling user code while the second half that the caller code permission get - * through and Spring doesn't override the permission stack. - * - * @author Costin Leau - */ -public class CallbacksSecurityTests { - - private DefaultListableBeanFactory beanFactory; - private SecurityContextProvider provider; - - @SuppressWarnings("unused") - private static class NonPrivilegedBean { - - private String expectedName; - public static boolean destroyed = false; - - public NonPrivilegedBean(String expected) { - this.expectedName = expected; - checkCurrentContext(); - } - - public void init() { - checkCurrentContext(); - } - - public void destroy() { - checkCurrentContext(); - destroyed = true; - } - - public void setProperty(Object value) { - checkCurrentContext(); - } - - public Object getProperty() { - checkCurrentContext(); - return null; - } - - public void setListProperty(Object value) { - checkCurrentContext(); - } - - public Object getListProperty() { - checkCurrentContext(); - return null; - } - - private void checkCurrentContext() { - assertThat(getCurrentSubjectName()).isEqualTo(expectedName); - } - } - - @SuppressWarnings("unused") - private static class NonPrivilegedSpringCallbacksBean implements - InitializingBean, DisposableBean, BeanClassLoaderAware, - BeanFactoryAware, BeanNameAware { - - private String expectedName; - public static boolean destroyed = false; - - public NonPrivilegedSpringCallbacksBean(String expected) { - this.expectedName = expected; - checkCurrentContext(); - } - - @Override - public void afterPropertiesSet() { - checkCurrentContext(); - } - - @Override - public void destroy() { - checkCurrentContext(); - destroyed = true; - } - - @Override - public void setBeanName(String name) { - checkCurrentContext(); - } - - @Override - public void setBeanClassLoader(ClassLoader classLoader) { - checkCurrentContext(); - } - - @Override - public void setBeanFactory(BeanFactory beanFactory) - throws BeansException { - checkCurrentContext(); - } - - private void checkCurrentContext() { - assertThat(getCurrentSubjectName()).isEqualTo(expectedName); - } - } - - @SuppressWarnings({ "unused", "rawtypes" }) - private static class NonPrivilegedFactoryBean implements SmartFactoryBean { - private String expectedName; - - public NonPrivilegedFactoryBean(String expected) { - this.expectedName = expected; - checkCurrentContext(); - } - - @Override - public boolean isEagerInit() { - checkCurrentContext(); - return false; - } - - @Override - public boolean isPrototype() { - checkCurrentContext(); - return true; - } - - @Override - public Object getObject() throws Exception { - checkCurrentContext(); - return new Object(); - } - - @Override - public Class getObjectType() { - checkCurrentContext(); - return Object.class; - } - - @Override - public boolean isSingleton() { - checkCurrentContext(); - return false; - } - - private void checkCurrentContext() { - assertThat(getCurrentSubjectName()).isEqualTo(expectedName); - } - } - - @SuppressWarnings("unused") - private static class NonPrivilegedFactory { - - private final String expectedName; - - public NonPrivilegedFactory(String expected) { - this.expectedName = expected; - assertThat(getCurrentSubjectName()).isEqualTo(expectedName); - } - - public static Object makeStaticInstance(String expectedName) { - assertThat(getCurrentSubjectName()).isEqualTo(expectedName); - return new Object(); - } - - public Object makeInstance() { - assertThat(getCurrentSubjectName()).isEqualTo(expectedName); - return new Object(); - } - } - - private static String getCurrentSubjectName() { - final AccessControlContext acc = AccessController.getContext(); - - return AccessController.doPrivileged(new PrivilegedAction() { - - @Override - public String run() { - Subject subject = Subject.getSubject(acc); - if (subject == null) { - return null; - } - - Set principals = subject.getPrincipals(); - - if (principals == null) { - return null; - } - for (Principal p : principals) { - return p.getName(); - } - return null; - } - }); - } - - public CallbacksSecurityTests() { - // setup security - if (System.getSecurityManager() == null) { - Policy policy = Policy.getPolicy(); - URL policyURL = getClass() - .getResource( - "/org/springframework/beans/factory/support/security/policy.all"); - System.setProperty("java.security.policy", policyURL.toString()); - System.setProperty("policy.allowSystemProperty", "true"); - policy.refresh(); - - System.setSecurityManager(new SecurityManager()); - } - } - - @BeforeEach - public void setUp() throws Exception { - - final ProtectionDomain empty = new ProtectionDomain(null, - new Permissions()); - - provider = new SecurityContextProvider() { - private final AccessControlContext acc = new AccessControlContext( - new ProtectionDomain[] { empty }); - - @Override - public AccessControlContext getAccessControlContext() { - return acc; - } - }; - - DefaultResourceLoader drl = new DefaultResourceLoader(); - Resource config = drl - .getResource("/org/springframework/beans/factory/support/security/callbacks.xml"); - beanFactory = new DefaultListableBeanFactory(); - new XmlBeanDefinitionReader(beanFactory).loadBeanDefinitions(config); - beanFactory.setSecurityContextProvider(provider); - } - - @Test - public void testSecuritySanity() throws Exception { - AccessControlContext acc = provider.getAccessControlContext(); - assertThatExceptionOfType(SecurityException.class).as( - "Acc should not have any permissions").isThrownBy(() -> - acc.checkPermission(new PropertyPermission("*", "read"))); - - CustomCallbackBean bean = new CustomCallbackBean(); - Method method = bean.getClass().getMethod("destroy"); - method.setAccessible(true); - - assertThatExceptionOfType(Exception.class).isThrownBy(() -> - AccessController.doPrivileged((PrivilegedExceptionAction) () -> { - method.invoke(bean); - return null; - }, acc)); - - Class cl = ConstructorBean.class; - assertThatExceptionOfType(Exception.class).isThrownBy(() -> - AccessController.doPrivileged((PrivilegedExceptionAction) () -> - cl.newInstance(), acc)); - } - - @Test - public void testSpringInitBean() throws Exception { - assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> - beanFactory.getBean("spring-init")) - .withCauseInstanceOf(SecurityException.class); - } - - @Test - public void testCustomInitBean() throws Exception { - assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> - beanFactory.getBean("custom-init")) - .withCauseInstanceOf(SecurityException.class); - } - - @Test - public void testSpringDestroyBean() throws Exception { - beanFactory.getBean("spring-destroy"); - beanFactory.destroySingletons(); - assertThat(System.getProperty("security.destroy")).isNull(); - } - - @Test - public void testCustomDestroyBean() throws Exception { - beanFactory.getBean("custom-destroy"); - beanFactory.destroySingletons(); - assertThat(System.getProperty("security.destroy")).isNull(); - } - - @Test - public void testCustomFactoryObject() throws Exception { - assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> - beanFactory.getBean("spring-factory")) - .withCauseInstanceOf(SecurityException.class); - } - - @Test - public void testCustomFactoryType() throws Exception { - assertThat(beanFactory.getType("spring-factory")).isNull(); - assertThat(System.getProperty("factory.object.type")).isNull(); - } - - @Test - public void testCustomStaticFactoryMethod() throws Exception { - assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> - beanFactory.getBean("custom-static-factory-method")) - .satisfies(mostSpecificCauseOf(SecurityException.class)); - } - - @Test - public void testCustomInstanceFactoryMethod() throws Exception { - assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> - beanFactory.getBean("custom-factory-method")) - .satisfies(mostSpecificCauseOf(SecurityException.class)); - } - - @Test - public void testTrustedFactoryMethod() throws Exception { - assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> - beanFactory.getBean("privileged-static-factory-method")) - .satisfies(mostSpecificCauseOf(SecurityException.class)); - } - - @Test - public void testConstructor() throws Exception { - assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> - beanFactory.getBean("constructor")) - .satisfies(mostSpecificCauseOf(SecurityException.class)); - } - - @Test - public void testContainerPrivileges() throws Exception { - AccessControlContext acc = provider.getAccessControlContext(); - - AccessController.doPrivileged(new PrivilegedExceptionAction() { - - @Override - public Object run() throws Exception { - beanFactory.getBean("working-factory-method"); - beanFactory.getBean("container-execution"); - return null; - } - }, acc); - } - - @Test - public void testPropertyInjection() throws Exception { - assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> - beanFactory.getBean("property-injection")) - .withMessageContaining("security"); - beanFactory.getBean("working-property-injection"); - } - - @Test - public void testInitSecurityAwarePrototypeBean() { - final DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); - BeanDefinitionBuilder bdb = BeanDefinitionBuilder - .genericBeanDefinition(NonPrivilegedBean.class).setScope( - BeanDefinition.SCOPE_PROTOTYPE) - .setInitMethodName("init").setDestroyMethodName("destroy") - .addConstructorArgValue("user1"); - lbf.registerBeanDefinition("test", bdb.getBeanDefinition()); - final Subject subject = new Subject(); - subject.getPrincipals().add(new TestPrincipal("user1")); - - NonPrivilegedBean bean = Subject.doAsPrivileged( - subject, new PrivilegedAction() { - @Override - public NonPrivilegedBean run() { - return lbf.getBean("test", NonPrivilegedBean.class); - } - }, null); - assertThat(bean).isNotNull(); - } - - @Test - public void testTrustedExecution() throws Exception { - beanFactory.setSecurityContextProvider(null); - - Permissions perms = new Permissions(); - perms.add(new AuthPermission("getSubject")); - ProtectionDomain pd = new ProtectionDomain(null, perms); - - new AccessControlContext(new ProtectionDomain[] { pd }); - - final Subject subject = new Subject(); - subject.getPrincipals().add(new TestPrincipal("user1")); - - // request the beans from non-privileged code - Subject.doAsPrivileged(subject, new PrivilegedAction() { - - @Override - public Object run() { - // sanity check - assertThat(getCurrentSubjectName()).isEqualTo("user1"); - assertThat(NonPrivilegedBean.destroyed).isEqualTo(false); - - beanFactory.getBean("trusted-spring-callbacks"); - beanFactory.getBean("trusted-custom-init-destroy"); - // the factory is a prototype - ask for multiple instances - beanFactory.getBean("trusted-spring-factory"); - beanFactory.getBean("trusted-spring-factory"); - beanFactory.getBean("trusted-spring-factory"); - - beanFactory.getBean("trusted-factory-bean"); - beanFactory.getBean("trusted-static-factory-method"); - beanFactory.getBean("trusted-factory-method"); - beanFactory.getBean("trusted-property-injection"); - beanFactory.getBean("trusted-working-property-injection"); - - beanFactory.destroySingletons(); - assertThat(NonPrivilegedBean.destroyed).isEqualTo(true); - return null; - } - }, provider.getAccessControlContext()); - } - - private Consumer mostSpecificCauseOf(Class type) { - return ex -> assertThat(ex.getMostSpecificCause()).isInstanceOf(type); - - } - -} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/security/support/ConstructorBean.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/security/support/ConstructorBean.java deleted file mode 100644 index fc60fc3db145..000000000000 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/security/support/ConstructorBean.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2002-2013 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.beans.factory.support.security.support; - -/** - * @author Costin Leau - */ -public class ConstructorBean { - - public ConstructorBean() { - System.getProperties(); - } - - public ConstructorBean(Object obj) { - } -} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/security/support/CustomCallbackBean.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/security/support/CustomCallbackBean.java deleted file mode 100644 index 4874306e6e12..000000000000 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/security/support/CustomCallbackBean.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2002-2012 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.beans.factory.support.security.support; - -/** - * @author Costin Leau - */ -public class CustomCallbackBean { - - public void init() { - System.getProperties(); - } - - public void destroy() { - System.setProperty("security.destroy", "true"); - } -} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/security/support/CustomFactoryBean.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/security/support/CustomFactoryBean.java deleted file mode 100644 index 4ec3d7131bf9..000000000000 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/security/support/CustomFactoryBean.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2002-2012 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.beans.factory.support.security.support; - -import java.util.Properties; - -import org.springframework.beans.factory.FactoryBean; - -/** - * @author Costin Leau - */ -public class CustomFactoryBean implements FactoryBean { - - @Override - public Properties getObject() throws Exception { - return System.getProperties(); - } - - @Override - public Class getObjectType() { - System.setProperty("factory.object.type", "true"); - return Properties.class; - } - - @Override - public boolean isSingleton() { - return true; - } - -} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/security/support/DestroyBean.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/security/support/DestroyBean.java deleted file mode 100644 index 67005abf7836..000000000000 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/security/support/DestroyBean.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2002-2012 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.beans.factory.support.security.support; - -import org.springframework.beans.factory.DisposableBean; - -/** - * @author Costin Leau - */ -public class DestroyBean implements DisposableBean { - - @Override - public void destroy() throws Exception { - System.setProperty("security.destroy", "true"); - } -} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/security/support/FactoryBean.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/security/support/FactoryBean.java deleted file mode 100644 index 4f7fb62e5be2..000000000000 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/security/support/FactoryBean.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2002-2012 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.beans.factory.support.security.support; - -/** - * @author Costin Leau - */ -public class FactoryBean { - - public static Object makeStaticInstance() { - System.getProperties(); - return new Object(); - } - - protected static Object protectedStaticInstance() { - return "protectedStaticInstance"; - } - - public Object makeInstance() { - System.getProperties(); - return new Object(); - } -} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/security/support/InitBean.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/security/support/InitBean.java deleted file mode 100644 index 3693bb9d749e..000000000000 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/security/support/InitBean.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2002-2012 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.beans.factory.support.security.support; - -import org.springframework.beans.factory.InitializingBean; - -/** - * @author Costin Leau - */ -public class InitBean implements InitializingBean { - - @Override - public void afterPropertiesSet() throws Exception { - System.getProperties(); - } -} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/security/support/PropertyBean.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/security/support/PropertyBean.java deleted file mode 100644 index 51933137f0da..000000000000 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/security/support/PropertyBean.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2002-2012 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.beans.factory.support.security.support; - -/** - * @author Costin Leau - */ -public class PropertyBean { - - public void setSecurityProperty(Object property) { - System.getProperties(); - } - - public void setProperty(Object property) { - - } -} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/wiring/ClassNameBeanWiringInfoResolverTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/wiring/ClassNameBeanWiringInfoResolverTests.java index 67cef6f11665..becb7961f2a3 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/wiring/ClassNameBeanWiringInfoResolverTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/wiring/ClassNameBeanWiringInfoResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -26,18 +26,18 @@ * * @author Rick Evans */ -public class ClassNameBeanWiringInfoResolverTests { +class ClassNameBeanWiringInfoResolverTests { @Test - public void resolveWiringInfoWithNullBeanInstance() throws Exception { + void resolveWiringInfoWithNullBeanInstance() throws Exception { assertThatIllegalArgumentException().isThrownBy(() -> new ClassNameBeanWiringInfoResolver().resolveWiringInfo(null)); } @Test - public void resolveWiringInfo() { + void resolveWiringInfo() { ClassNameBeanWiringInfoResolver resolver = new ClassNameBeanWiringInfoResolver(); - Long beanInstance = new Long(1); + Long beanInstance = 1L; BeanWiringInfo info = resolver.resolveWiringInfo(beanInstance); assertThat(info).isNotNull(); assertThat(info.getBeanName()).as("Not resolving bean name to the class name of the supplied bean instance as per class contract.").isEqualTo(beanInstance.getClass().getName()); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/DuplicateBeanIdTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/DuplicateBeanIdTests.java index c52001463cdf..11b672c912e9 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/xml/DuplicateBeanIdTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/DuplicateBeanIdTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -23,8 +23,7 @@ import org.springframework.core.io.ClassPathResource; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - +import static org.assertj.core.api.Assertions.assertThatException; /** * With Spring 3.1, bean id attributes (and all other id attributes across the @@ -40,22 +39,23 @@ * @see org.springframework.beans.factory.xml.XmlBeanFactoryTests#withDuplicateName * @see org.springframework.beans.factory.xml.XmlBeanFactoryTests#withDuplicateNameInAlias */ -public class DuplicateBeanIdTests { +class DuplicateBeanIdTests { @Test - public void duplicateBeanIdsWithinSameNestingLevelRaisesError() { + void duplicateBeanIdsWithinSameNestingLevelRaisesError() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(bf); - assertThatExceptionOfType(Exception.class).as("duplicate ids in same nesting level").isThrownBy(() -> + assertThatException().as("duplicate ids in same nesting level").isThrownBy(() -> reader.loadBeanDefinitions(new ClassPathResource("DuplicateBeanIdTests-sameLevel-context.xml", this.getClass()))); } @Test - public void duplicateBeanIdsAcrossNestingLevels() { + void duplicateBeanIdsAcrossNestingLevels() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(bf); reader.loadBeanDefinitions(new ClassPathResource("DuplicateBeanIdTests-multiLevel-context.xml", this.getClass())); TestBean testBean = bf.getBean(TestBean.class); // there should be only one assertThat(testBean.getName()).isEqualTo("nested"); } + } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/FactoryMethodTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/FactoryMethodTests.java index d2f26ee0fe63..ced4e5098d8f 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/xml/FactoryMethodTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/FactoryMethodTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -100,17 +100,17 @@ public void testFactoryMethodsWithNullValue() { FactoryMethods fm = (FactoryMethods) xbf.getBean("fullWithNull"); assertThat(fm.getNum()).isEqualTo(27); - assertThat(fm.getName()).isEqualTo(null); + assertThat(fm.getName()).isNull(); assertThat(fm.getTestBean().getName()).isEqualTo("Juergen"); fm = (FactoryMethods) xbf.getBean("fullWithGenericNull"); assertThat(fm.getNum()).isEqualTo(27); - assertThat(fm.getName()).isEqualTo(null); + assertThat(fm.getName()).isNull(); assertThat(fm.getTestBean().getName()).isEqualTo("Juergen"); fm = (FactoryMethods) xbf.getBean("fullWithNamedNull"); assertThat(fm.getNum()).isEqualTo(27); - assertThat(fm.getName()).isEqualTo(null); + assertThat(fm.getName()).isNull(); assertThat(fm.getTestBean().getName()).isEqualTo("Juergen"); } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/UtilNamespaceHandlerTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/UtilNamespaceHandlerTests.java index 2260a86c52e9..083d6f9de35e 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/xml/UtilNamespaceHandlerTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/UtilNamespaceHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -342,7 +342,7 @@ public void testNestedInConstructor() { public void testLoadProperties() { Properties props = (Properties) this.beanFactory.getBean("myProperties"); assertThat(props.get("foo")).as("Incorrect property value").isEqualTo("bar"); - assertThat(props.get("foo2")).as("Incorrect property value").isEqualTo(null); + assertThat(props.get("foo2")).as("Incorrect property value").isNull(); Properties props2 = (Properties) this.beanFactory.getBean("myProperties"); assertThat(props == props2).isTrue(); } @@ -351,17 +351,17 @@ public void testLoadProperties() { public void testScopedProperties() { Properties props = (Properties) this.beanFactory.getBean("myScopedProperties"); assertThat(props.get("foo")).as("Incorrect property value").isEqualTo("bar"); - assertThat(props.get("foo2")).as("Incorrect property value").isEqualTo(null); + assertThat(props.get("foo2")).as("Incorrect property value").isNull(); Properties props2 = (Properties) this.beanFactory.getBean("myScopedProperties"); assertThat(props.get("foo")).as("Incorrect property value").isEqualTo("bar"); - assertThat(props.get("foo2")).as("Incorrect property value").isEqualTo(null); + assertThat(props.get("foo2")).as("Incorrect property value").isNull(); assertThat(props != props2).isTrue(); } @Test public void testLocalProperties() { Properties props = (Properties) this.beanFactory.getBean("myLocalProperties"); - assertThat(props.get("foo")).as("Incorrect property value").isEqualTo(null); + assertThat(props.get("foo")).as("Incorrect property value").isNull(); assertThat(props.get("foo2")).as("Incorrect property value").isEqualTo("bar2"); } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/XmlBeanCollectionTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/XmlBeanCollectionTests.java index e44f0d9df7a0..29564a9f7b09 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/xml/XmlBeanCollectionTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/XmlBeanCollectionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -283,7 +283,7 @@ public void testPopulatedSet() throws Exception { Iterator it = hasMap.getSet().iterator(); assertThat(it.next()).isEqualTo("bar"); assertThat(it.next()).isEqualTo(jenny); - assertThat(it.next()).isEqualTo(null); + assertThat(it.next()).isNull(); } @Test @@ -332,9 +332,9 @@ public void testObjectArray() throws Exception { public void testIntegerArray() throws Exception { HasMap hasMap = (HasMap) this.beanFactory.getBean("integerArray"); assertThat(hasMap.getIntegerArray().length == 3).isTrue(); - assertThat(hasMap.getIntegerArray()[0].intValue() == 0).isTrue(); - assertThat(hasMap.getIntegerArray()[1].intValue() == 1).isTrue(); - assertThat(hasMap.getIntegerArray()[2].intValue() == 2).isTrue(); + assertThat(hasMap.getIntegerArray()[0] == 0).isTrue(); + assertThat(hasMap.getIntegerArray()[1] == 1).isTrue(); + assertThat(hasMap.getIntegerArray()[2] == 2).isTrue(); } @Test diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/XmlListableBeanFactoryTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/XmlListableBeanFactoryTests.java index d48b55f2cb8d..4b5c005788da 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/xml/XmlListableBeanFactoryTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/XmlListableBeanFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -181,7 +181,7 @@ public void autoAliasing() { assertThat(beanNames.contains("aliasWithoutId3")).isFalse(); TestBean tb4 = (TestBean) getBeanFactory().getBean(TestBean.class.getName() + "#0"); - assertThat(tb4.getName()).isEqualTo(null); + assertThat(tb4.getName()).isNull(); Map drs = getListableBeanFactory().getBeansOfType(DummyReferencer.class, false, false); assertThat(drs.size()).isEqualTo(5); diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CustomEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CustomEditorTests.java index 0de98a10f002..aa3b733412f6 100644 --- a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CustomEditorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CustomEditorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -64,10 +64,10 @@ * @author Chris Beams * @since 10.06.2003 */ -public class CustomEditorTests { +class CustomEditorTests { @Test - public void testComplexObject() { + void testComplexObject() { TestBean tb = new TestBean(); String newName = "Rod"; String tbString = "Kerry_34"; @@ -80,12 +80,12 @@ public void testComplexObject() { pvs.addPropertyValue(new PropertyValue("touchy", "valid")); pvs.addPropertyValue(new PropertyValue("spouse", tbString)); bw.setPropertyValues(pvs); - assertThat(tb.getSpouse() != null).as("spouse is non-null").isTrue(); + assertThat(tb.getSpouse()).as("spouse is non-null").isNotNull(); assertThat(tb.getSpouse().getName().equals("Kerry") && tb.getSpouse().getAge() == 34).as("spouse name is Kerry and age is 34").isTrue(); } @Test - public void testComplexObjectWithOldValueAccess() { + void testComplexObjectWithOldValueAccess() { TestBean tb = new TestBean(); String newName = "Rod"; String tbString = "Kerry_34"; @@ -100,7 +100,7 @@ public void testComplexObjectWithOldValueAccess() { pvs.addPropertyValue(new PropertyValue("spouse", tbString)); bw.setPropertyValues(pvs); - assertThat(tb.getSpouse() != null).as("spouse is non-null").isTrue(); + assertThat(tb.getSpouse()).as("spouse is non-null").isNotNull(); assertThat(tb.getSpouse().getName().equals("Kerry") && tb.getSpouse().getAge() == 34).as("spouse name is Kerry and age is 34").isTrue(); ITestBean spouse = tb.getSpouse(); @@ -109,7 +109,7 @@ public void testComplexObjectWithOldValueAccess() { } @Test - public void testCustomEditorForSingleProperty() { + void testCustomEditorForSingleProperty() { TestBean tb = new TestBean(); BeanWrapper bw = new BeanWrapperImpl(tb); bw.registerCustomEditor(String.class, "name", new PropertyEditorSupport() { @@ -127,7 +127,7 @@ public void setAsText(String text) throws IllegalArgumentException { } @Test - public void testCustomEditorForAllStringProperties() { + void testCustomEditorForAllStringProperties() { TestBean tb = new TestBean(); BeanWrapper bw = new BeanWrapperImpl(tb); bw.registerCustomEditor(String.class, new PropertyEditorSupport() { @@ -145,7 +145,7 @@ public void setAsText(String text) throws IllegalArgumentException { } @Test - public void testCustomEditorForSingleNestedProperty() { + void testCustomEditorForSingleNestedProperty() { TestBean tb = new TestBean(); tb.setSpouse(new TestBean()); BeanWrapper bw = new BeanWrapperImpl(tb); @@ -164,7 +164,7 @@ public void setAsText(String text) throws IllegalArgumentException { } @Test - public void testCustomEditorForAllNestedStringProperties() { + void testCustomEditorForAllNestedStringProperties() { TestBean tb = new TestBean(); tb.setSpouse(new TestBean()); BeanWrapper bw = new BeanWrapperImpl(tb); @@ -183,7 +183,7 @@ public void setAsText(String text) throws IllegalArgumentException { } @Test - public void testDefaultBooleanEditorForPrimitiveType() { + void testDefaultBooleanEditorForPrimitiveType() { BooleanTestBean tb = new BooleanTestBean(); BeanWrapper bw = new BeanWrapperImpl(tb); @@ -229,7 +229,7 @@ public void testDefaultBooleanEditorForPrimitiveType() { } @Test - public void testDefaultBooleanEditorForWrapperType() { + void testDefaultBooleanEditorForWrapperType() { BooleanTestBean tb = new BooleanTestBean(); BeanWrapper bw = new BeanWrapperImpl(tb); @@ -239,28 +239,28 @@ public void testDefaultBooleanEditorForWrapperType() { bw.setPropertyValue("bool2", "false"); assertThat(Boolean.FALSE.equals(bw.getPropertyValue("bool2"))).as("Correct bool2 value").isTrue(); - boolean condition3 = !tb.getBool2().booleanValue(); + boolean condition3 = !tb.getBool2(); assertThat(condition3).as("Correct bool2 value").isTrue(); bw.setPropertyValue("bool2", "on"); assertThat(tb.getBool2().booleanValue()).as("Correct bool2 value").isTrue(); bw.setPropertyValue("bool2", "off"); - boolean condition2 = !tb.getBool2().booleanValue(); + boolean condition2 = !tb.getBool2(); assertThat(condition2).as("Correct bool2 value").isTrue(); bw.setPropertyValue("bool2", "yes"); assertThat(tb.getBool2().booleanValue()).as("Correct bool2 value").isTrue(); bw.setPropertyValue("bool2", "no"); - boolean condition1 = !tb.getBool2().booleanValue(); + boolean condition1 = !tb.getBool2(); assertThat(condition1).as("Correct bool2 value").isTrue(); bw.setPropertyValue("bool2", "1"); assertThat(tb.getBool2().booleanValue()).as("Correct bool2 value").isTrue(); bw.setPropertyValue("bool2", "0"); - boolean condition = !tb.getBool2().booleanValue(); + boolean condition = !tb.getBool2(); assertThat(condition).as("Correct bool2 value").isTrue(); bw.setPropertyValue("bool2", ""); @@ -268,7 +268,7 @@ public void testDefaultBooleanEditorForWrapperType() { } @Test - public void testCustomBooleanEditorWithAllowEmpty() { + void testCustomBooleanEditorWithAllowEmpty() { BooleanTestBean tb = new BooleanTestBean(); BeanWrapper bw = new BeanWrapperImpl(tb); bw.registerCustomEditor(Boolean.class, new CustomBooleanEditor(true)); @@ -279,37 +279,37 @@ public void testCustomBooleanEditorWithAllowEmpty() { bw.setPropertyValue("bool2", "false"); assertThat(Boolean.FALSE.equals(bw.getPropertyValue("bool2"))).as("Correct bool2 value").isTrue(); - boolean condition3 = !tb.getBool2().booleanValue(); + boolean condition3 = !tb.getBool2(); assertThat(condition3).as("Correct bool2 value").isTrue(); bw.setPropertyValue("bool2", "on"); assertThat(tb.getBool2().booleanValue()).as("Correct bool2 value").isTrue(); bw.setPropertyValue("bool2", "off"); - boolean condition2 = !tb.getBool2().booleanValue(); + boolean condition2 = !tb.getBool2(); assertThat(condition2).as("Correct bool2 value").isTrue(); bw.setPropertyValue("bool2", "yes"); assertThat(tb.getBool2().booleanValue()).as("Correct bool2 value").isTrue(); bw.setPropertyValue("bool2", "no"); - boolean condition1 = !tb.getBool2().booleanValue(); + boolean condition1 = !tb.getBool2(); assertThat(condition1).as("Correct bool2 value").isTrue(); bw.setPropertyValue("bool2", "1"); assertThat(tb.getBool2().booleanValue()).as("Correct bool2 value").isTrue(); bw.setPropertyValue("bool2", "0"); - boolean condition = !tb.getBool2().booleanValue(); + boolean condition = !tb.getBool2(); assertThat(condition).as("Correct bool2 value").isTrue(); bw.setPropertyValue("bool2", ""); - assertThat(bw.getPropertyValue("bool2") == null).as("Correct bool2 value").isTrue(); - assertThat(tb.getBool2() == null).as("Correct bool2 value").isTrue(); + assertThat(bw.getPropertyValue("bool2")).as("Correct bool2 value").isNull(); + assertThat(tb.getBool2()).as("Correct bool2 value").isNull(); } @Test - public void testCustomBooleanEditorWithSpecialTrueAndFalseStrings() throws Exception { + void testCustomBooleanEditorWithSpecialTrueAndFalseStrings() throws Exception { String trueString = "pechorin"; String falseString = "nash"; @@ -333,7 +333,7 @@ public void testCustomBooleanEditorWithSpecialTrueAndFalseStrings() throws Excep } @Test - public void testDefaultNumberEditor() { + void testDefaultNumberEditor() { NumberTestBean tb = new NumberTestBean(); BeanWrapper bw = new BeanWrapperImpl(tb); @@ -350,34 +350,34 @@ public void testDefaultNumberEditor() { bw.setPropertyValue("double2", "6.1"); bw.setPropertyValue("bigDecimal", "4.5"); - assertThat(new Short("1").equals(bw.getPropertyValue("short1"))).as("Correct short1 value").isTrue(); + assertThat(Short.valueOf("1").equals(bw.getPropertyValue("short1"))).as("Correct short1 value").isTrue(); assertThat(tb.getShort1() == 1).as("Correct short1 value").isTrue(); - assertThat(new Short("2").equals(bw.getPropertyValue("short2"))).as("Correct short2 value").isTrue(); - assertThat(new Short("2").equals(tb.getShort2())).as("Correct short2 value").isTrue(); - assertThat(new Integer("7").equals(bw.getPropertyValue("int1"))).as("Correct int1 value").isTrue(); + assertThat(Short.valueOf("2").equals(bw.getPropertyValue("short2"))).as("Correct short2 value").isTrue(); + assertThat(Short.valueOf("2").equals(tb.getShort2())).as("Correct short2 value").isTrue(); + assertThat(Integer.valueOf("7").equals(bw.getPropertyValue("int1"))).as("Correct int1 value").isTrue(); assertThat(tb.getInt1() == 7).as("Correct int1 value").isTrue(); - assertThat(new Integer("8").equals(bw.getPropertyValue("int2"))).as("Correct int2 value").isTrue(); - assertThat(new Integer("8").equals(tb.getInt2())).as("Correct int2 value").isTrue(); - assertThat(new Long("5").equals(bw.getPropertyValue("long1"))).as("Correct long1 value").isTrue(); + assertThat(Integer.valueOf("8").equals(bw.getPropertyValue("int2"))).as("Correct int2 value").isTrue(); + assertThat(Integer.valueOf("8").equals(tb.getInt2())).as("Correct int2 value").isTrue(); + assertThat(Long.valueOf("5").equals(bw.getPropertyValue("long1"))).as("Correct long1 value").isTrue(); assertThat(tb.getLong1() == 5).as("Correct long1 value").isTrue(); - assertThat(new Long("6").equals(bw.getPropertyValue("long2"))).as("Correct long2 value").isTrue(); - assertThat(new Long("6").equals(tb.getLong2())).as("Correct long2 value").isTrue(); + assertThat(Long.valueOf("6").equals(bw.getPropertyValue("long2"))).as("Correct long2 value").isTrue(); + assertThat(Long.valueOf("6").equals(tb.getLong2())).as("Correct long2 value").isTrue(); assertThat(new BigInteger("3").equals(bw.getPropertyValue("bigInteger"))).as("Correct bigInteger value").isTrue(); assertThat(new BigInteger("3").equals(tb.getBigInteger())).as("Correct bigInteger value").isTrue(); - assertThat(new Float("7.1").equals(bw.getPropertyValue("float1"))).as("Correct float1 value").isTrue(); - assertThat(new Float("7.1").equals(new Float(tb.getFloat1()))).as("Correct float1 value").isTrue(); - assertThat(new Float("8.1").equals(bw.getPropertyValue("float2"))).as("Correct float2 value").isTrue(); - assertThat(new Float("8.1").equals(tb.getFloat2())).as("Correct float2 value").isTrue(); - assertThat(new Double("5.1").equals(bw.getPropertyValue("double1"))).as("Correct double1 value").isTrue(); + assertThat(Float.valueOf("7.1").equals(bw.getPropertyValue("float1"))).as("Correct float1 value").isTrue(); + assertThat(Float.valueOf("7.1").equals(tb.getFloat1())).as("Correct float1 value").isTrue(); + assertThat(Float.valueOf("8.1").equals(bw.getPropertyValue("float2"))).as("Correct float2 value").isTrue(); + assertThat(Float.valueOf("8.1").equals(tb.getFloat2())).as("Correct float2 value").isTrue(); + assertThat(Double.valueOf("5.1").equals(bw.getPropertyValue("double1"))).as("Correct double1 value").isTrue(); assertThat(tb.getDouble1() == 5.1).as("Correct double1 value").isTrue(); - assertThat(new Double("6.1").equals(bw.getPropertyValue("double2"))).as("Correct double2 value").isTrue(); - assertThat(new Double("6.1").equals(tb.getDouble2())).as("Correct double2 value").isTrue(); + assertThat(Double.valueOf("6.1").equals(bw.getPropertyValue("double2"))).as("Correct double2 value").isTrue(); + assertThat(Double.valueOf("6.1").equals(tb.getDouble2())).as("Correct double2 value").isTrue(); assertThat(new BigDecimal("4.5").equals(bw.getPropertyValue("bigDecimal"))).as("Correct bigDecimal value").isTrue(); assertThat(new BigDecimal("4.5").equals(tb.getBigDecimal())).as("Correct bigDecimal value").isTrue(); } @Test - public void testCustomNumberEditorWithoutAllowEmpty() { + void testCustomNumberEditorWithoutAllowEmpty() { NumberFormat nf = NumberFormat.getNumberInstance(Locale.GERMAN); NumberTestBean tb = new NumberTestBean(); BeanWrapper bw = new BeanWrapperImpl(tb); @@ -407,34 +407,34 @@ public void testCustomNumberEditorWithoutAllowEmpty() { bw.setPropertyValue("double2", "6,1"); bw.setPropertyValue("bigDecimal", "4,5"); - assertThat(new Short("1").equals(bw.getPropertyValue("short1"))).as("Correct short1 value").isTrue(); + assertThat(bw.getPropertyValue("short1")).as("Correct short1 value").isEqualTo(Short.valueOf("1")); assertThat(tb.getShort1() == 1).as("Correct short1 value").isTrue(); - assertThat(new Short("2").equals(bw.getPropertyValue("short2"))).as("Correct short2 value").isTrue(); - assertThat(new Short("2").equals(tb.getShort2())).as("Correct short2 value").isTrue(); - assertThat(new Integer("7").equals(bw.getPropertyValue("int1"))).as("Correct int1 value").isTrue(); + assertThat(bw.getPropertyValue("short2")).as("Correct short2 value").isEqualTo(Short.valueOf("2")); + assertThat(tb.getShort2()).as("Correct short2 value").isEqualTo(Short.valueOf("2")); + assertThat(bw.getPropertyValue("int1")).as("Correct int1 value").isEqualTo(Integer.valueOf("7")); assertThat(tb.getInt1() == 7).as("Correct int1 value").isTrue(); - assertThat(new Integer("8").equals(bw.getPropertyValue("int2"))).as("Correct int2 value").isTrue(); - assertThat(new Integer("8").equals(tb.getInt2())).as("Correct int2 value").isTrue(); - assertThat(new Long("5").equals(bw.getPropertyValue("long1"))).as("Correct long1 value").isTrue(); + assertThat(bw.getPropertyValue("int2")).as("Correct int2 value").isEqualTo(Integer.valueOf("8")); + assertThat(tb.getInt2()).as("Correct int2 value").isEqualTo(Integer.valueOf("8")); + assertThat(bw.getPropertyValue("long1")).as("Correct long1 value").isEqualTo(Long.valueOf("5")); assertThat(tb.getLong1() == 5).as("Correct long1 value").isTrue(); - assertThat(new Long("6").equals(bw.getPropertyValue("long2"))).as("Correct long2 value").isTrue(); - assertThat(new Long("6").equals(tb.getLong2())).as("Correct long2 value").isTrue(); + assertThat(bw.getPropertyValue("long2")).as("Correct long2 value").isEqualTo(Long.valueOf("6")); + assertThat(tb.getLong2()).as("Correct long2 value").isEqualTo(Long.valueOf("6")); assertThat(new BigInteger("3").equals(bw.getPropertyValue("bigInteger"))).as("Correct bigInteger value").isTrue(); assertThat(new BigInteger("3").equals(tb.getBigInteger())).as("Correct bigInteger value").isTrue(); - assertThat(new Float("7.1").equals(bw.getPropertyValue("float1"))).as("Correct float1 value").isTrue(); - assertThat(new Float("7.1").equals(new Float(tb.getFloat1()))).as("Correct float1 value").isTrue(); - assertThat(new Float("8.1").equals(bw.getPropertyValue("float2"))).as("Correct float2 value").isTrue(); - assertThat(new Float("8.1").equals(tb.getFloat2())).as("Correct float2 value").isTrue(); - assertThat(new Double("5.1").equals(bw.getPropertyValue("double1"))).as("Correct double1 value").isTrue(); + assertThat(bw.getPropertyValue("float1")).as("Correct float1 value").isEqualTo(Float.valueOf("7.1")); + assertThat(Float.valueOf(tb.getFloat1())).as("Correct float1 value").isEqualTo(Float.valueOf("7.1")); + assertThat(bw.getPropertyValue("float2")).as("Correct float2 value").isEqualTo(Float.valueOf("8.1")); + assertThat(tb.getFloat2()).as("Correct float2 value").isEqualTo(Float.valueOf("8.1")); + assertThat(bw.getPropertyValue("double1")).as("Correct double1 value").isEqualTo(Double.valueOf("5.1")); assertThat(tb.getDouble1() == 5.1).as("Correct double1 value").isTrue(); - assertThat(new Double("6.1").equals(bw.getPropertyValue("double2"))).as("Correct double2 value").isTrue(); - assertThat(new Double("6.1").equals(tb.getDouble2())).as("Correct double2 value").isTrue(); + assertThat(bw.getPropertyValue("double2")).as("Correct double2 value").isEqualTo(Double.valueOf("6.1")); + assertThat(tb.getDouble2()).as("Correct double2 value").isEqualTo(Double.valueOf("6.1")); assertThat(new BigDecimal("4.5").equals(bw.getPropertyValue("bigDecimal"))).as("Correct bigDecimal value").isTrue(); assertThat(new BigDecimal("4.5").equals(tb.getBigDecimal())).as("Correct bigDecimal value").isTrue(); } @Test - public void testCustomNumberEditorWithAllowEmpty() { + void testCustomNumberEditorWithAllowEmpty() { NumberFormat nf = NumberFormat.getNumberInstance(Locale.GERMAN); NumberTestBean tb = new NumberTestBean(); BeanWrapper bw = new BeanWrapperImpl(tb); @@ -443,10 +443,10 @@ public void testCustomNumberEditorWithAllowEmpty() { bw.setPropertyValue("long1", "5"); bw.setPropertyValue("long2", "6"); - assertThat(new Long("5").equals(bw.getPropertyValue("long1"))).as("Correct long1 value").isTrue(); + assertThat(Long.valueOf("5").equals(bw.getPropertyValue("long1"))).as("Correct long1 value").isTrue(); assertThat(tb.getLong1() == 5).as("Correct long1 value").isTrue(); - assertThat(new Long("6").equals(bw.getPropertyValue("long2"))).as("Correct long2 value").isTrue(); - assertThat(new Long("6").equals(tb.getLong2())).as("Correct long2 value").isTrue(); + assertThat(Long.valueOf("6").equals(bw.getPropertyValue("long2"))).as("Correct long2 value").isTrue(); + assertThat(Long.valueOf("6").equals(tb.getLong2())).as("Correct long2 value").isTrue(); bw.setPropertyValue("long2", ""); assertThat(bw.getPropertyValue("long2") == null).as("Correct long2 value").isTrue(); @@ -458,7 +458,7 @@ public void testCustomNumberEditorWithAllowEmpty() { } @Test - public void testCustomNumberEditorWithFrenchBigDecimal() throws Exception { + void testCustomNumberEditorWithFrenchBigDecimal() throws Exception { NumberFormat nf = NumberFormat.getNumberInstance(Locale.FRENCH); NumberTestBean tb = new NumberTestBean(); BeanWrapper bw = new BeanWrapperImpl(tb); @@ -475,14 +475,14 @@ public void testCustomNumberEditorWithFrenchBigDecimal() throws Exception { } @Test - public void testParseShortGreaterThanMaxValueWithoutNumberFormat() { + void testParseShortGreaterThanMaxValueWithoutNumberFormat() { CustomNumberEditor editor = new CustomNumberEditor(Short.class, true); assertThatExceptionOfType(NumberFormatException.class).as("greater than Short.MAX_VALUE + 1").isThrownBy(() -> editor.setAsText(String.valueOf(Short.MAX_VALUE + 1))); } @Test - public void testByteArrayPropertyEditor() { + void testByteArrayPropertyEditor() { PrimitiveArrayBean bean = new PrimitiveArrayBean(); BeanWrapper bw = new BeanWrapperImpl(bean); bw.setPropertyValue("byteArray", "myvalue"); @@ -490,7 +490,7 @@ public void testByteArrayPropertyEditor() { } @Test - public void testCharArrayPropertyEditor() { + void testCharArrayPropertyEditor() { PrimitiveArrayBean bean = new PrimitiveArrayBean(); BeanWrapper bw = new BeanWrapperImpl(bean); bw.setPropertyValue("charArray", "myvalue"); @@ -498,11 +498,11 @@ public void testCharArrayPropertyEditor() { } @Test - public void testCharacterEditor() { + void testCharacterEditor() { CharBean cb = new CharBean(); BeanWrapper bw = new BeanWrapperImpl(cb); - bw.setPropertyValue("myChar", new Character('c')); + bw.setPropertyValue("myChar", 'c'); assertThat(cb.getMyChar()).isEqualTo('c'); bw.setPropertyValue("myChar", "c"); @@ -520,78 +520,78 @@ public void testCharacterEditor() { } @Test - public void testCharacterEditorWithAllowEmpty() { + void testCharacterEditorWithAllowEmpty() { CharBean cb = new CharBean(); BeanWrapper bw = new BeanWrapperImpl(cb); bw.registerCustomEditor(Character.class, new CharacterEditor(true)); - bw.setPropertyValue("myCharacter", new Character('c')); - assertThat(cb.getMyCharacter()).isEqualTo(new Character('c')); + bw.setPropertyValue("myCharacter", 'c'); + assertThat(cb.getMyCharacter()).isEqualTo(Character.valueOf('c')); bw.setPropertyValue("myCharacter", "c"); - assertThat(cb.getMyCharacter()).isEqualTo(new Character('c')); + assertThat(cb.getMyCharacter()).isEqualTo(Character.valueOf('c')); bw.setPropertyValue("myCharacter", "\u0041"); - assertThat(cb.getMyCharacter()).isEqualTo(new Character('A')); + assertThat(cb.getMyCharacter()).isEqualTo(Character.valueOf('A')); bw.setPropertyValue("myCharacter", " "); - assertThat(cb.getMyCharacter()).isEqualTo(new Character(' ')); + assertThat(cb.getMyCharacter()).isEqualTo(Character.valueOf(' ')); bw.setPropertyValue("myCharacter", ""); assertThat(cb.getMyCharacter()).isNull(); } @Test - public void testCharacterEditorSetAsTextWithStringLongerThanOneCharacter() throws Exception { + void testCharacterEditorSetAsTextWithStringLongerThanOneCharacter() throws Exception { PropertyEditor charEditor = new CharacterEditor(false); assertThatIllegalArgumentException().isThrownBy(() -> charEditor.setAsText("ColdWaterCanyon")); } @Test - public void testCharacterEditorGetAsTextReturnsEmptyStringIfValueIsNull() throws Exception { + void testCharacterEditorGetAsTextReturnsEmptyStringIfValueIsNull() throws Exception { PropertyEditor charEditor = new CharacterEditor(false); - assertThat(charEditor.getAsText()).isEqualTo(""); + assertThat(charEditor.getAsText()).isEmpty(); charEditor = new CharacterEditor(true); charEditor.setAsText(null); - assertThat(charEditor.getAsText()).isEqualTo(""); + assertThat(charEditor.getAsText()).isEmpty(); charEditor.setAsText(""); - assertThat(charEditor.getAsText()).isEqualTo(""); + assertThat(charEditor.getAsText()).isEmpty(); charEditor.setAsText(" "); assertThat(charEditor.getAsText()).isEqualTo(" "); } @Test - public void testCharacterEditorSetAsTextWithNullNotAllowingEmptyAsNull() throws Exception { + void testCharacterEditorSetAsTextWithNullNotAllowingEmptyAsNull() throws Exception { PropertyEditor charEditor = new CharacterEditor(false); assertThatIllegalArgumentException().isThrownBy(() -> charEditor.setAsText(null)); } @Test - public void testClassEditor() { + void testClassEditor() { PropertyEditor classEditor = new ClassEditor(); classEditor.setAsText(TestBean.class.getName()); assertThat(classEditor.getValue()).isEqualTo(TestBean.class); assertThat(classEditor.getAsText()).isEqualTo(TestBean.class.getName()); classEditor.setAsText(null); - assertThat(classEditor.getAsText()).isEqualTo(""); + assertThat(classEditor.getAsText()).isEmpty(); classEditor.setAsText(""); - assertThat(classEditor.getAsText()).isEqualTo(""); + assertThat(classEditor.getAsText()).isEmpty(); classEditor.setAsText("\t "); - assertThat(classEditor.getAsText()).isEqualTo(""); + assertThat(classEditor.getAsText()).isEmpty(); } @Test - public void testClassEditorWithNonExistentClass() throws Exception { + void testClassEditorWithNonExistentClass() throws Exception { PropertyEditor classEditor = new ClassEditor(); assertThatIllegalArgumentException().isThrownBy(() -> classEditor.setAsText("hairdresser.on.Fire")); } @Test - public void testClassEditorWithArray() { + void testClassEditorWithArray() { PropertyEditor classEditor = new ClassEditor(); classEditor.setAsText("org.springframework.beans.testfixture.beans.TestBean[]"); assertThat(classEditor.getValue()).isEqualTo(TestBean[].class); @@ -602,7 +602,7 @@ public void testClassEditorWithArray() { * SPR_2165 - ClassEditor is inconsistent with multidimensional arrays */ @Test - public void testGetAsTextWithTwoDimensionalArray() throws Exception { + void testGetAsTextWithTwoDimensionalArray() throws Exception { String[][] chessboard = new String[8][8]; ClassEditor editor = new ClassEditor(); editor.setValue(chessboard.getClass()); @@ -613,7 +613,7 @@ public void testGetAsTextWithTwoDimensionalArray() throws Exception { * SPR_2165 - ClassEditor is inconsistent with multidimensional arrays */ @Test - public void testGetAsTextWithRidiculousMultiDimensionalArray() throws Exception { + void testGetAsTextWithRidiculousMultiDimensionalArray() throws Exception { String[][][][][] ridiculousChessboard = new String[8][4][0][1][3]; ClassEditor editor = new ClassEditor(); editor.setValue(ridiculousChessboard.getClass()); @@ -621,7 +621,7 @@ public void testGetAsTextWithRidiculousMultiDimensionalArray() throws Exception } @Test - public void testFileEditor() { + void testFileEditor() { PropertyEditor fileEditor = new FileEditor(); fileEditor.setAsText("file:myfile.txt"); assertThat(fileEditor.getValue()).isEqualTo(new File("myfile.txt")); @@ -629,7 +629,7 @@ public void testFileEditor() { } @Test - public void testFileEditorWithRelativePath() { + void testFileEditorWithRelativePath() { PropertyEditor fileEditor = new FileEditor(); try { fileEditor.setAsText("myfile.txt"); @@ -641,7 +641,7 @@ public void testFileEditorWithRelativePath() { } @Test - public void testFileEditorWithAbsolutePath() { + void testFileEditorWithAbsolutePath() { PropertyEditor fileEditor = new FileEditor(); // testing on Windows if (new File("C:/myfile.txt").isAbsolute()) { @@ -656,18 +656,18 @@ public void testFileEditorWithAbsolutePath() { } @Test - public void testLocaleEditor() { + void testLocaleEditor() { PropertyEditor localeEditor = new LocaleEditor(); localeEditor.setAsText("en_CA"); assertThat(localeEditor.getValue()).isEqualTo(Locale.CANADA); assertThat(localeEditor.getAsText()).isEqualTo("en_CA"); localeEditor = new LocaleEditor(); - assertThat(localeEditor.getAsText()).isEqualTo(""); + assertThat(localeEditor.getAsText()).isEmpty(); } @Test - public void testPatternEditor() { + void testPatternEditor() { final String REGEX = "a.*"; PropertyEditor patternEditor = new PatternEditor(); @@ -676,15 +676,15 @@ public void testPatternEditor() { assertThat(patternEditor.getAsText()).isEqualTo(REGEX); patternEditor = new PatternEditor(); - assertThat(patternEditor.getAsText()).isEqualTo(""); + assertThat(patternEditor.getAsText()).isEmpty(); patternEditor = new PatternEditor(); patternEditor.setAsText(null); - assertThat(patternEditor.getAsText()).isEqualTo(""); + assertThat(patternEditor.getAsText()).isEmpty(); } @Test - public void testCustomBooleanEditor() { + void testCustomBooleanEditor() { CustomBooleanEditor editor = new CustomBooleanEditor(false); editor.setAsText("true"); @@ -696,15 +696,15 @@ public void testCustomBooleanEditor() { assertThat(editor.getAsText()).isEqualTo("false"); editor.setValue(null); - assertThat(editor.getValue()).isEqualTo(null); - assertThat(editor.getAsText()).isEqualTo(""); + assertThat(editor.getValue()).isNull(); + assertThat(editor.getAsText()).isEmpty(); assertThatIllegalArgumentException().isThrownBy(() -> editor.setAsText(null)); } @Test - public void testCustomBooleanEditorWithEmptyAsNull() { + void testCustomBooleanEditorWithEmptyAsNull() { CustomBooleanEditor editor = new CustomBooleanEditor(true); editor.setAsText("true"); @@ -716,34 +716,34 @@ public void testCustomBooleanEditorWithEmptyAsNull() { assertThat(editor.getAsText()).isEqualTo("false"); editor.setValue(null); - assertThat(editor.getValue()).isEqualTo(null); - assertThat(editor.getAsText()).isEqualTo(""); + assertThat(editor.getValue()).isNull(); + assertThat(editor.getAsText()).isEmpty(); } @Test - public void testCustomDateEditor() { - CustomDateEditor editor = new CustomDateEditor(null, false); + void testCustomDateEditor() { + CustomDateEditor editor = new CustomDateEditor(new SimpleDateFormat("MM/dd/yyyy"), false); editor.setValue(null); - assertThat(editor.getValue()).isEqualTo(null); - assertThat(editor.getAsText()).isEqualTo(""); + assertThat(editor.getValue()).isNull(); + assertThat(editor.getAsText()).isEmpty(); } @Test - public void testCustomDateEditorWithEmptyAsNull() { - CustomDateEditor editor = new CustomDateEditor(null, true); + void testCustomDateEditorWithEmptyAsNull() { + CustomDateEditor editor = new CustomDateEditor(new SimpleDateFormat("MM/dd/yyyy"), true); editor.setValue(null); - assertThat(editor.getValue()).isEqualTo(null); - assertThat(editor.getAsText()).isEqualTo(""); + assertThat(editor.getValue()).isNull(); + assertThat(editor.getAsText()).isEmpty(); } @Test - public void testCustomDateEditorWithExactDateLength() { + void testCustomDateEditorWithExactDateLength() { int maxLength = 10; String validDate = "01/01/2005"; String invalidDate = "01/01/05"; - assertThat(validDate.length() == maxLength).isTrue(); - assertThat(invalidDate.length() == maxLength).isFalse(); + assertThat(validDate).hasSize(maxLength); + assertThat(invalidDate.length()).isNotEqualTo(maxLength); CustomDateEditor editor = new CustomDateEditor(new SimpleDateFormat("MM/dd/yyyy"), true, maxLength); editor.setAsText(validDate); @@ -753,39 +753,39 @@ public void testCustomDateEditorWithExactDateLength() { } @Test - public void testCustomNumberEditor() { + void testCustomNumberEditor() { CustomNumberEditor editor = new CustomNumberEditor(Integer.class, false); editor.setAsText("5"); assertThat(editor.getValue()).isEqualTo(5); assertThat(editor.getAsText()).isEqualTo("5"); editor.setValue(null); - assertThat(editor.getValue()).isEqualTo(null); - assertThat(editor.getAsText()).isEqualTo(""); + assertThat(editor.getValue()).isNull(); + assertThat(editor.getAsText()).isEmpty(); } @Test - public void testCustomNumberEditorWithHex() { + void testCustomNumberEditorWithHex() { CustomNumberEditor editor = new CustomNumberEditor(Integer.class, false); editor.setAsText("0x" + Integer.toHexString(64)); assertThat(editor.getValue()).isEqualTo(64); } @Test - public void testCustomNumberEditorWithEmptyAsNull() { + void testCustomNumberEditorWithEmptyAsNull() { CustomNumberEditor editor = new CustomNumberEditor(Integer.class, true); editor.setAsText("5"); assertThat(editor.getValue()).isEqualTo(5); assertThat(editor.getAsText()).isEqualTo("5"); editor.setAsText(""); - assertThat(editor.getValue()).isEqualTo(null); - assertThat(editor.getAsText()).isEqualTo(""); + assertThat(editor.getValue()).isNull(); + assertThat(editor.getAsText()).isEmpty(); editor.setValue(null); - assertThat(editor.getValue()).isEqualTo(null); - assertThat(editor.getAsText()).isEqualTo(""); + assertThat(editor.getValue()).isNull(); + assertThat(editor.getAsText()).isEmpty(); } @Test - public void testStringTrimmerEditor() { + void testStringTrimmerEditor() { StringTrimmerEditor editor = new StringTrimmerEditor(false); editor.setAsText("test"); assertThat(editor.getValue()).isEqualTo("test"); @@ -795,15 +795,15 @@ public void testStringTrimmerEditor() { assertThat(editor.getAsText()).isEqualTo("test"); editor.setAsText(""); assertThat(editor.getValue()).isEqualTo(""); - assertThat(editor.getAsText()).isEqualTo(""); + assertThat(editor.getAsText()).isEmpty(); editor.setValue(null); - assertThat(editor.getAsText()).isEqualTo(""); + assertThat(editor.getAsText()).isEmpty(); editor.setAsText(null); - assertThat(editor.getAsText()).isEqualTo(""); + assertThat(editor.getAsText()).isEmpty(); } @Test - public void testStringTrimmerEditorWithEmptyAsNull() { + void testStringTrimmerEditorWithEmptyAsNull() { StringTrimmerEditor editor = new StringTrimmerEditor(true); editor.setAsText("test"); assertThat(editor.getValue()).isEqualTo("test"); @@ -812,14 +812,14 @@ public void testStringTrimmerEditorWithEmptyAsNull() { assertThat(editor.getValue()).isEqualTo("test"); assertThat(editor.getAsText()).isEqualTo("test"); editor.setAsText(" "); - assertThat(editor.getValue()).isEqualTo(null); - assertThat(editor.getAsText()).isEqualTo(""); + assertThat(editor.getValue()).isNull(); + assertThat(editor.getAsText()).isEmpty(); editor.setValue(null); - assertThat(editor.getAsText()).isEqualTo(""); + assertThat(editor.getAsText()).isEmpty(); } @Test - public void testStringTrimmerEditorWithCharsToDelete() { + void testStringTrimmerEditorWithCharsToDelete() { StringTrimmerEditor editor = new StringTrimmerEditor("\r\n\f", false); editor.setAsText("te\ns\ft"); assertThat(editor.getValue()).isEqualTo("test"); @@ -829,13 +829,13 @@ public void testStringTrimmerEditorWithCharsToDelete() { assertThat(editor.getAsText()).isEqualTo("test"); editor.setAsText(""); assertThat(editor.getValue()).isEqualTo(""); - assertThat(editor.getAsText()).isEqualTo(""); + assertThat(editor.getAsText()).isEmpty(); editor.setValue(null); - assertThat(editor.getAsText()).isEqualTo(""); + assertThat(editor.getAsText()).isEmpty(); } @Test - public void testStringTrimmerEditorWithCharsToDeleteAndEmptyAsNull() { + void testStringTrimmerEditorWithCharsToDeleteAndEmptyAsNull() { StringTrimmerEditor editor = new StringTrimmerEditor("\r\n\f", true); editor.setAsText("te\ns\ft"); assertThat(editor.getValue()).isEqualTo("test"); @@ -844,14 +844,14 @@ public void testStringTrimmerEditorWithCharsToDeleteAndEmptyAsNull() { assertThat(editor.getValue()).isEqualTo("test"); assertThat(editor.getAsText()).isEqualTo("test"); editor.setAsText(" \n\f "); - assertThat(editor.getValue()).isEqualTo(null); - assertThat(editor.getAsText()).isEqualTo(""); + assertThat(editor.getValue()).isNull(); + assertThat(editor.getAsText()).isEmpty(); editor.setValue(null); - assertThat(editor.getAsText()).isEqualTo(""); + assertThat(editor.getAsText()).isEmpty(); } @Test - public void testIndexedPropertiesWithCustomEditorForType() { + void testIndexedPropertiesWithCustomEditorForType() { IndexedTestBean bean = new IndexedTestBean(); BeanWrapper bw = new BeanWrapperImpl(bean); bw.registerCustomEditor(String.class, new PropertyEditorSupport() { @@ -904,7 +904,7 @@ public void setAsText(String text) throws IllegalArgumentException { } @Test - public void testIndexedPropertiesWithCustomEditorForProperty() { + void testIndexedPropertiesWithCustomEditorForProperty() { IndexedTestBean bean = new IndexedTestBean(false); BeanWrapper bw = new BeanWrapperImpl(bean); bw.registerCustomEditor(String.class, "array.name", new PropertyEditorSupport() { @@ -971,7 +971,7 @@ public void setAsText(String text) throws IllegalArgumentException { } @Test - public void testIndexedPropertiesWithIndividualCustomEditorForProperty() { + void testIndexedPropertiesWithIndividualCustomEditorForProperty() { IndexedTestBean bean = new IndexedTestBean(false); BeanWrapper bw = new BeanWrapperImpl(bean); bw.registerCustomEditor(String.class, "array[0].name", new PropertyEditorSupport() { @@ -1056,7 +1056,7 @@ public void setAsText(String text) throws IllegalArgumentException { } @Test - public void testNestedIndexedPropertiesWithCustomEditorForProperty() { + void testNestedIndexedPropertiesWithCustomEditorForProperty() { IndexedTestBean bean = new IndexedTestBean(); TestBean tb0 = bean.getArray()[0]; TestBean tb1 = bean.getArray()[1]; @@ -1140,7 +1140,7 @@ public String getAsText() { } @Test - public void testNestedIndexedPropertiesWithIndexedCustomEditorForProperty() { + void testNestedIndexedPropertiesWithIndexedCustomEditorForProperty() { IndexedTestBean bean = new IndexedTestBean(); TestBean tb0 = bean.getArray()[0]; TestBean tb1 = bean.getArray()[1]; @@ -1191,7 +1191,7 @@ public void setAsText(String text) throws IllegalArgumentException { } @Test - public void testIndexedPropertiesWithDirectAccessAndPropertyEditors() { + void testIndexedPropertiesWithDirectAccessAndPropertyEditors() { IndexedTestBean bean = new IndexedTestBean(); BeanWrapper bw = new BeanWrapperImpl(bean); bw.registerCustomEditor(TestBean.class, "array", new PropertyEditorSupport() { @@ -1245,7 +1245,7 @@ public String getAsText() { } @Test - public void testIndexedPropertiesWithDirectAccessAndSpecificPropertyEditors() { + void testIndexedPropertiesWithDirectAccessAndSpecificPropertyEditors() { IndexedTestBean bean = new IndexedTestBean(); BeanWrapper bw = new BeanWrapperImpl(bean); bw.registerCustomEditor(TestBean.class, "array[0]", new PropertyEditorSupport() { @@ -1332,7 +1332,7 @@ public String getAsText() { } @Test - public void testIndexedPropertiesWithListPropertyEditor() { + void testIndexedPropertiesWithListPropertyEditor() { IndexedTestBean bean = new IndexedTestBean(); BeanWrapper bw = new BeanWrapperImpl(bean); bw.registerCustomEditor(List.class, "list", new PropertyEditorSupport() { @@ -1350,7 +1350,7 @@ public void setAsText(String text) throws IllegalArgumentException { } @Test - public void testConversionToOldCollections() throws PropertyVetoException { + void testConversionToOldCollections() throws PropertyVetoException { OldCollectionsBean tb = new OldCollectionsBean(); BeanWrapper bw = new BeanWrapperImpl(tb); bw.registerCustomEditor(Vector.class, new CustomCollectionEditor(Vector.class)); @@ -1367,7 +1367,7 @@ public void testConversionToOldCollections() throws PropertyVetoException { } @Test - public void testUninitializedArrayPropertyWithCustomEditor() { + void testUninitializedArrayPropertyWithCustomEditor() { IndexedTestBean bean = new IndexedTestBean(false); BeanWrapper bw = new BeanWrapperImpl(bean); PropertyEditor pe = new CustomNumberEditor(Integer.class, true); @@ -1383,7 +1383,7 @@ public void testUninitializedArrayPropertyWithCustomEditor() { } @Test - public void testArrayToArrayConversion() throws PropertyVetoException { + void testArrayToArrayConversion() throws PropertyVetoException { IndexedTestBean tb = new IndexedTestBean(); BeanWrapper bw = new BeanWrapperImpl(tb); bw.registerCustomEditor(TestBean.class, new PropertyEditorSupport() { @@ -1399,7 +1399,7 @@ public void setAsText(String text) throws IllegalArgumentException { } @Test - public void testArrayToStringConversion() throws PropertyVetoException { + void testArrayToStringConversion() throws PropertyVetoException { TestBean tb = new TestBean(); BeanWrapper bw = new BeanWrapperImpl(tb); bw.registerCustomEditor(String.class, new PropertyEditorSupport() { @@ -1408,16 +1408,16 @@ public void setAsText(String text) throws IllegalArgumentException { setValue("-" + text + "-"); } }); - bw.setPropertyValue("name", new String[] {"a", "b"}); + bw.setPropertyValue("name", new String[]{"a", "b"}); assertThat(tb.getName()).isEqualTo("-a,b-"); } @Test - public void testClassArrayEditorSunnyDay() throws Exception { + void testClassArrayEditorSunnyDay() throws Exception { ClassArrayEditor classArrayEditor = new ClassArrayEditor(); classArrayEditor.setAsText("java.lang.String,java.util.HashMap"); Class[] classes = (Class[]) classArrayEditor.getValue(); - assertThat(classes.length).isEqualTo(2); + assertThat(classes).hasSize(2); assertThat(classes[0]).isEqualTo(String.class); assertThat(classes[1]).isEqualTo(HashMap.class); assertThat(classArrayEditor.getAsText()).isEqualTo("java.lang.String,java.util.HashMap"); @@ -1426,11 +1426,11 @@ public void testClassArrayEditorSunnyDay() throws Exception { } @Test - public void testClassArrayEditorSunnyDayWithArrayTypes() throws Exception { + void testClassArrayEditorSunnyDayWithArrayTypes() throws Exception { ClassArrayEditor classArrayEditor = new ClassArrayEditor(); classArrayEditor.setAsText("java.lang.String[],java.util.Map[],int[],float[][][]"); Class[] classes = (Class[]) classArrayEditor.getValue(); - assertThat(classes.length).isEqualTo(4); + assertThat(classes).hasSize(4); assertThat(classes[0]).isEqualTo(String[].class); assertThat(classes[1]).isEqualTo(Map[].class); assertThat(classes[2]).isEqualTo(int[].class); @@ -1441,31 +1441,31 @@ public void testClassArrayEditorSunnyDayWithArrayTypes() throws Exception { } @Test - public void testClassArrayEditorSetAsTextWithNull() throws Exception { + void testClassArrayEditorSetAsTextWithNull() throws Exception { ClassArrayEditor editor = new ClassArrayEditor(); editor.setAsText(null); assertThat(editor.getValue()).isNull(); - assertThat(editor.getAsText()).isEqualTo(""); + assertThat(editor.getAsText()).isEmpty(); } @Test - public void testClassArrayEditorSetAsTextWithEmptyString() throws Exception { + void testClassArrayEditorSetAsTextWithEmptyString() throws Exception { ClassArrayEditor editor = new ClassArrayEditor(); editor.setAsText(""); assertThat(editor.getValue()).isNull(); - assertThat(editor.getAsText()).isEqualTo(""); + assertThat(editor.getAsText()).isEmpty(); } @Test - public void testClassArrayEditorSetAsTextWithWhitespaceString() throws Exception { + void testClassArrayEditorSetAsTextWithWhitespaceString() throws Exception { ClassArrayEditor editor = new ClassArrayEditor(); editor.setAsText("\n"); assertThat(editor.getValue()).isNull(); - assertThat(editor.getAsText()).isEqualTo(""); + assertThat(editor.getAsText()).isEmpty(); } @Test - public void testCharsetEditor() throws Exception { + void testCharsetEditor() throws Exception { CharsetEditor editor = new CharsetEditor(); String name = "UTF-8"; editor.setAsText(name); diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/PathEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/PathEditorTests.java index 40354fc44643..f0c659bcbdb7 100644 --- a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/PathEditorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/PathEditorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -34,57 +34,78 @@ public class PathEditorTests { @Test - public void testClasspathPathName() throws Exception { + public void testClasspathPathName() { PropertyEditor pathEditor = new PathEditor(); pathEditor.setAsText("classpath:" + ClassUtils.classPackageAsResourcePath(getClass()) + "/" + ClassUtils.getShortName(getClass()) + ".class"); Object value = pathEditor.getValue(); - boolean condition = value instanceof Path; - assertThat(condition).isTrue(); + assertThat(value instanceof Path).isTrue(); Path path = (Path) value; assertThat(path.toFile().exists()).isTrue(); } @Test - public void testWithNonExistentResource() throws Exception { + public void testWithNonExistentResource() { PropertyEditor propertyEditor = new PathEditor(); assertThatIllegalArgumentException().isThrownBy(() -> propertyEditor.setAsText("classpath:/no_way_this_file_is_found.doc")); } @Test - public void testWithNonExistentPath() throws Exception { + public void testWithNonExistentPath() { PropertyEditor pathEditor = new PathEditor(); pathEditor.setAsText("file:/no_way_this_file_is_found.doc"); Object value = pathEditor.getValue(); - boolean condition1 = value instanceof Path; - assertThat(condition1).isTrue(); + assertThat(value instanceof Path).isTrue(); Path path = (Path) value; - boolean condition = !path.toFile().exists(); - assertThat(condition).isTrue(); + assertThat(!path.toFile().exists()).isTrue(); } @Test - public void testAbsolutePath() throws Exception { + public void testAbsolutePath() { PropertyEditor pathEditor = new PathEditor(); pathEditor.setAsText("/no_way_this_file_is_found.doc"); Object value = pathEditor.getValue(); - boolean condition1 = value instanceof Path; - assertThat(condition1).isTrue(); + assertThat(value instanceof Path).isTrue(); Path path = (Path) value; - boolean condition = !path.toFile().exists(); - assertThat(condition).isTrue(); + assertThat(!path.toFile().exists()).isTrue(); } @Test - public void testUnqualifiedPathNameFound() throws Exception { + public void testWindowsAbsolutePath() { + PropertyEditor pathEditor = new PathEditor(); + pathEditor.setAsText("C:\\no_way_this_file_is_found.doc"); + Object value = pathEditor.getValue(); + assertThat(value instanceof Path).isTrue(); + Path path = (Path) value; + assertThat(!path.toFile().exists()).isTrue(); + } + + @Test + public void testWindowsAbsoluteFilePath() { + PropertyEditor pathEditor = new PathEditor(); + try { + pathEditor.setAsText("file://C:\\no_way_this_file_is_found.doc"); + Object value = pathEditor.getValue(); + assertThat(value instanceof Path).isTrue(); + Path path = (Path) value; + assertThat(!path.toFile().exists()).isTrue(); + } + catch (IllegalArgumentException ex) { + if (File.separatorChar == '\\') { // on Windows, otherwise silently ignore + throw ex; + } + } + } + + @Test + public void testUnqualifiedPathNameFound() { PropertyEditor pathEditor = new PathEditor(); String fileName = ClassUtils.classPackageAsResourcePath(getClass()) + "/" + ClassUtils.getShortName(getClass()) + ".class"; pathEditor.setAsText(fileName); Object value = pathEditor.getValue(); - boolean condition = value instanceof Path; - assertThat(condition).isTrue(); + assertThat(value instanceof Path).isTrue(); Path path = (Path) value; File file = path.toFile(); assertThat(file.exists()).isTrue(); @@ -96,14 +117,13 @@ public void testUnqualifiedPathNameFound() throws Exception { } @Test - public void testUnqualifiedPathNameNotFound() throws Exception { + public void testUnqualifiedPathNameNotFound() { PropertyEditor pathEditor = new PathEditor(); String fileName = ClassUtils.classPackageAsResourcePath(getClass()) + "/" + ClassUtils.getShortName(getClass()) + ".clazz"; pathEditor.setAsText(fileName); Object value = pathEditor.getValue(); - boolean condition = value instanceof Path; - assertThat(condition).isTrue(); + assertThat(value instanceof Path).isTrue(); Path path = (Path) value; File file = path.toFile(); assertThat(file.exists()).isFalse(); diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ZoneIdEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ZoneIdEditorTests.java index 7a9162314b2f..b62f952fdb15 100644 --- a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ZoneIdEditorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ZoneIdEditorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -19,19 +19,26 @@ import java.time.ZoneId; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import static org.assertj.core.api.Assertions.assertThat; /** * @author Nicholas Williams + * @author Sam Brannen */ -public class ZoneIdEditorTests { +class ZoneIdEditorTests { private final ZoneIdEditor editor = new ZoneIdEditor(); - @Test - public void americaChicago() { - editor.setAsText("America/Chicago"); + @ParameterizedTest(name = "[{index}] text = ''{0}''") + @ValueSource(strings = { + "America/Chicago", + " America/Chicago ", + }) + void americaChicago(String text) { + editor.setAsText(text); ZoneId zoneId = (ZoneId) editor.getValue(); assertThat(zoneId).as("The zone ID should not be null.").isNotNull(); @@ -41,7 +48,7 @@ public void americaChicago() { } @Test - public void americaLosAngeles() { + void americaLosAngeles() { editor.setAsText("America/Los_Angeles"); ZoneId zoneId = (ZoneId) editor.getValue(); @@ -52,12 +59,12 @@ public void americaLosAngeles() { } @Test - public void getNullAsText() { + void getNullAsText() { assertThat(editor.getAsText()).as("The returned value is not correct.").isEqualTo(""); } @Test - public void getValueAsText() { + void getValueAsText() { editor.setValue(ZoneId.of("America/New_York")); assertThat(editor.getAsText()).as("The text version is not correct.").isEqualTo("America/New_York"); } diff --git a/spring-beans/src/test/java/org/springframework/beans/support/PagedListHolderTests.java b/spring-beans/src/test/java/org/springframework/beans/support/PagedListHolderTests.java index 0afce6483444..e5189de08358 100644 --- a/spring-beans/src/test/java/org/springframework/beans/support/PagedListHolderTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/support/PagedListHolderTests.java @@ -195,14 +195,22 @@ public void setExtendedInfo(String extendedInfo) { @Override public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof MockFilter)) return false; - - final MockFilter mockFilter = (MockFilter) o; - - if (!age.equals(mockFilter.age)) return false; - if (!extendedInfo.equals(mockFilter.extendedInfo)) return false; - if (!name.equals(mockFilter.name)) return false; + if (this == o) { + return true; + } + if (!(o instanceof MockFilter mockFilter)) { + return false; + } + + if (!age.equals(mockFilter.age)) { + return false; + } + if (!extendedInfo.equals(mockFilter.extendedInfo)) { + return false; + } + if (!name.equals(mockFilter.name)) { + return false; + } return true; } diff --git a/spring-beans/src/test/kotlin/org/springframework/beans/factory/ListableBeanFactoryExtensionsTests.kt b/spring-beans/src/test/kotlin/org/springframework/beans/factory/ListableBeanFactoryExtensionsTests.kt index a86a60aec9ba..f089cc6aef3f 100644 --- a/spring-beans/src/test/kotlin/org/springframework/beans/factory/ListableBeanFactoryExtensionsTests.kt +++ b/spring-beans/src/test/kotlin/org/springframework/beans/factory/ListableBeanFactoryExtensionsTests.kt @@ -16,9 +16,11 @@ package org.springframework.beans.factory +import io.mockk.every import io.mockk.mockk import io.mockk.verify import org.junit.jupiter.api.Test +import kotlin.reflect.full.createInstance /** * Mock object based tests for ListableBeanFactory Kotlin extensions @@ -77,10 +79,12 @@ class ListableBeanFactoryExtensionsTests { verify { lbf.getBeansWithAnnotation(Bar::class.java) } } + @Suppress("UNUSED_VARIABLE") @Test fun `findAnnotationOnBean with String and reified type parameters`() { val name = "bar" - lbf.findAnnotationOnBean(name) + every { lbf.findAnnotationOnBean(name, Bar::class.java) } returns Bar::class.createInstance() + val annotation: Bar? = lbf.findAnnotationOnBean(name) verify { lbf.findAnnotationOnBean(name, Bar::class.java) } } diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/aot/aot-services.factories b/spring-beans/src/test/resources/org/springframework/beans/factory/aot/aot-services.factories new file mode 100644 index 000000000000..f7e228096382 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/aot/aot-services.factories @@ -0,0 +1,2 @@ +org.springframework.beans.factory.aot.AotServicesTests$TestService=\ +org.springframework.beans.factory.aot.AotServicesTests$TestServiceImpl \ No newline at end of file diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/generator/bean-registration-contribution-provider-constructor.factories b/spring-beans/src/test/resources/org/springframework/beans/factory/generator/bean-registration-contribution-provider-constructor.factories new file mode 100644 index 000000000000..e63c90b2f966 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/generator/bean-registration-contribution-provider-constructor.factories @@ -0,0 +1,2 @@ +org.springframework.beans.factory.generator.BeanRegistrationContributionProvider= \ +org.springframework.beans.factory.generator.BeanDefinitionsContributionTests.TestConstructorBeanRegistrationContributionProvider \ No newline at end of file diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/factory/aot/TestBeanRegistrationsAotProcessor.java b/spring-beans/src/testFixtures/java/org/springframework/beans/factory/aot/TestBeanRegistrationsAotProcessor.java new file mode 100644 index 000000000000..079799b229c8 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/factory/aot/TestBeanRegistrationsAotProcessor.java @@ -0,0 +1,26 @@ +/* + * Copyright 2002-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.beans.factory.aot; + +/** + * Public variant of {@link BeanRegistrationAotProcessor} for use in tests. + * + * @author Phillip Webb + */ +public class TestBeanRegistrationsAotProcessor extends BeanRegistrationsAotProcessor { + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/DummyBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/DummyBean.java index cac5cced1c8a..82f40bae28ab 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/DummyBean.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/DummyBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-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. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.beans.testfixture.beans; /** @@ -65,4 +66,5 @@ public int getAge() { public TestBean getSpouse() { return spouse; } + } diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/ITestBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/ITestBean.java index 742b39c4ea7e..43ce2dd40ed8 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/ITestBean.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/ITestBean.java @@ -22,7 +22,7 @@ * Interface used for {@link org.springframework.beans.testfixture.beans.TestBean}. * *

    Two methods are the same as on Person, but if this - * extends person it breaks quite a few tests.. + * extends person it breaks quite a few tests. * * @author Rod Johnson * @author Juergen Hoeller diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/MustBeInitialized.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/MustBeInitialized.java index 2e53ce2f885a..3ca9a82d72f3 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/MustBeInitialized.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/MustBeInitialized.java @@ -40,8 +40,9 @@ public void afterPropertiesSet() throws Exception { * managed the bean's lifecycle correctly */ public void businessMethod() { - if (!this.inited) + if (!this.inited) { throw new RuntimeException("Factory didn't call afterPropertiesSet() on MustBeInitialized object"); + } } } diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/NestedTestBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/NestedTestBean.java index ea26ec0072c0..1fb6b60081d2 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/NestedTestBean.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/NestedTestBean.java @@ -44,10 +44,9 @@ public String getCompany() { @Override public boolean equals(Object obj) { - if (!(obj instanceof NestedTestBean)) { + if (!(obj instanceof NestedTestBean ntb)) { return false; } - NestedTestBean ntb = (NestedTestBean) obj; return this.company.equals(ntb.company); } diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/Pet.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/Pet.java index 661eff92feb7..7b725d39c9b5 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/Pet.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/Pet.java @@ -39,12 +39,18 @@ public String toString() { @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } final Pet pet = (Pet) o; - if (name != null ? !name.equals(pet.name) : pet.name != null) return false; + if (name != null ? !name.equals(pet.name) : pet.name != null) { + return false; + } return true; } diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/SerializablePerson.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/SerializablePerson.java index 6f9436906cc1..c406c5c47b06 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/SerializablePerson.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/SerializablePerson.java @@ -64,10 +64,9 @@ public Object echo(Object o) throws Throwable { @Override public boolean equals(Object other) { - if (!(other instanceof SerializablePerson)) { + if (!(other instanceof SerializablePerson p)) { return false; } - SerializablePerson p = (SerializablePerson) other; return p.age == age && ObjectUtils.nullSafeEquals(name, p.name); } diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBean.java index ed54d0d05f4b..e9f9e107e835 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBean.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -74,7 +74,7 @@ public class TestBean implements BeanNameAware, BeanFactoryAware, ITestBean, IOt private Date date = new Date(); - private Float myFloat = Float.valueOf(0.0f); + private Float myFloat = 0.0f; private Collection friends = new ArrayList<>(); @@ -468,10 +468,9 @@ public boolean equals(Object other) { if (this == other) { return true; } - if (!(other instanceof TestBean)) { + if (!(other instanceof TestBean tb2)) { return false; } - TestBean tb2 = (TestBean) other; return (ObjectUtils.nullSafeEquals(this.name, tb2.name) && this.age == tb2.age); } diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBeanWithPackagePrivateConstructor.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBeanWithPackagePrivateConstructor.java new file mode 100644 index 000000000000..b3032107ee5b --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBeanWithPackagePrivateConstructor.java @@ -0,0 +1,24 @@ +/* + * Copyright 2002-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.beans.testfixture.beans; + +public class TestBeanWithPackagePrivateConstructor { + + TestBeanWithPackagePrivateConstructor() { + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBeanWithPackagePrivateField.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBeanWithPackagePrivateField.java new file mode 100644 index 000000000000..d7cb24e27f05 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBeanWithPackagePrivateField.java @@ -0,0 +1,23 @@ +/* + * Copyright 2002-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.beans.testfixture.beans; + +public class TestBeanWithPackagePrivateField { + + int age; + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBeanWithPackagePrivateMethod.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBeanWithPackagePrivateMethod.java new file mode 100644 index 000000000000..6c6e3ee4c7cc --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBeanWithPackagePrivateMethod.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-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.beans.testfixture.beans; + +@SuppressWarnings("unused") +public class TestBeanWithPackagePrivateMethod { + + private int age; + + void setAge(int age) { + this.age = age; + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBeanWithPrivateConstructor.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBeanWithPrivateConstructor.java new file mode 100644 index 000000000000..cf848d6c2ed6 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBeanWithPrivateConstructor.java @@ -0,0 +1,24 @@ +/* + * Copyright 2002-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.beans.testfixture.beans; + +public class TestBeanWithPrivateConstructor { + + private TestBeanWithPrivateConstructor() { + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBeanWithPrivateMethod.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBeanWithPrivateMethod.java new file mode 100644 index 000000000000..222b43148864 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBeanWithPrivateMethod.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-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.beans.testfixture.beans; + +@SuppressWarnings("unused") +public class TestBeanWithPrivateMethod { + + private int age; + + private void setAge(int age) { + this.age = age; + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBeanWithPublicField.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBeanWithPublicField.java new file mode 100644 index 000000000000..7ebf18974bbf --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBeanWithPublicField.java @@ -0,0 +1,23 @@ +/* + * Copyright 2002-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.beans.testfixture.beans; + +public class TestBeanWithPublicField { + + public int age; + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/annotation/PackagePrivateFieldInjectionSample.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/annotation/PackagePrivateFieldInjectionSample.java new file mode 100644 index 000000000000..b1452ba360d7 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/annotation/PackagePrivateFieldInjectionSample.java @@ -0,0 +1,27 @@ +/* + * Copyright 2002-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.beans.testfixture.beans.factory.annotation; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; + +public class PackagePrivateFieldInjectionSample { + + @Autowired + Environment environment; + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/annotation/PackagePrivateMethodInjectionSample.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/annotation/PackagePrivateMethodInjectionSample.java new file mode 100644 index 000000000000..2ffcf726e72c --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/annotation/PackagePrivateMethodInjectionSample.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-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.beans.testfixture.beans.factory.annotation; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; + +public class PackagePrivateMethodInjectionSample { + + public Environment environment; + + @Autowired + void setTestBean(Environment environment) { + this.environment = environment; + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/annotation/PrivateFieldInjectionSample.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/annotation/PrivateFieldInjectionSample.java new file mode 100644 index 000000000000..9aa2c69187da --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/annotation/PrivateFieldInjectionSample.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-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.beans.testfixture.beans.factory.annotation; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; + +public class PrivateFieldInjectionSample { + + @Autowired + @SuppressWarnings("unused") + private Environment environment; + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/annotation/PrivateMethodInjectionSample.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/annotation/PrivateMethodInjectionSample.java new file mode 100644 index 000000000000..d741e69b4a4a --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/annotation/PrivateMethodInjectionSample.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-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.beans.testfixture.beans.factory.annotation; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; + +public class PrivateMethodInjectionSample { + + @SuppressWarnings("unused") + private Environment environment; + + @Autowired + private void setTestBean(Environment environment) { + this.environment = environment; + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/annotation/subpkg/PackagePrivateFieldInjectionFromParentSample.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/annotation/subpkg/PackagePrivateFieldInjectionFromParentSample.java new file mode 100644 index 000000000000..33e51f6e1f28 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/annotation/subpkg/PackagePrivateFieldInjectionFromParentSample.java @@ -0,0 +1,25 @@ +/* + * Copyright 2002-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.beans.testfixture.beans.factory.annotation.subpkg; + +import org.springframework.beans.testfixture.beans.factory.annotation.PackagePrivateFieldInjectionSample; + +public class PackagePrivateFieldInjectionFromParentSample extends PackagePrivateFieldInjectionSample { + + // see environment from parent + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/annotation/subpkg/PackagePrivateMethodInjectionFromParentSample.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/annotation/subpkg/PackagePrivateMethodInjectionFromParentSample.java new file mode 100644 index 000000000000..f2afef824b13 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/annotation/subpkg/PackagePrivateMethodInjectionFromParentSample.java @@ -0,0 +1,24 @@ +/* + * Copyright 2002-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.beans.testfixture.beans.factory.annotation.subpkg; + +import org.springframework.beans.testfixture.beans.factory.annotation.PackagePrivateMethodInjectionSample; + +public class PackagePrivateMethodInjectionFromParentSample extends PackagePrivateMethodInjectionSample { + + // see setTestBean from parent +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/DeferredTypeBuilder.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/DeferredTypeBuilder.java new file mode 100644 index 000000000000..3d4288278c8b --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/DeferredTypeBuilder.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-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.beans.testfixture.beans.factory.aot; + +import java.util.function.Consumer; + +import org.springframework.javapoet.TypeSpec; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * {@link TypeSpec.Builder} {@link Consumer} that can be used to defer the to + * another consumer that is set at a later point. + * + * @author Phillip Webb + * @since 6.0 + */ +public class DeferredTypeBuilder implements Consumer { + + @Nullable + private Consumer type; + + @Override + public void accept(TypeSpec.Builder type) { + Assert.notNull(this.type, "No type builder set"); + this.type.accept(type); + } + + public void set(Consumer type) { + this.type = type; + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/GenericFactoryBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/GenericFactoryBean.java new file mode 100644 index 000000000000..571b2d1509bc --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/GenericFactoryBean.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-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.beans.testfixture.beans.factory.aot; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.lang.Nullable; + +/** + * A public {@link FactoryBean} with a generic type. + * + * @author Stephane Nicoll + */ +public class GenericFactoryBean implements FactoryBean { + + private final Class beanType; + + public GenericFactoryBean(Class beanType) { + this.beanType = beanType; + } + + @Nullable + @Override + public T getObject() throws Exception { + return BeanUtils.instantiateClass(this.beanType); + } + + @Nullable + @Override + public Class getObjectType() { + return this.beanType; + } +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/InnerBeanConfiguration.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/InnerBeanConfiguration.java new file mode 100644 index 000000000000..c3e5a7afcde4 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/InnerBeanConfiguration.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-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.beans.testfixture.beans.factory.aot; + +/** + * A configuration with inner classes. + * + * @author Stephane Nicoll + */ +public class InnerBeanConfiguration { + + public static class Simple { + + public SimpleBean simpleBean() { + return new SimpleBean(); + } + + public static class Another { + + public SimpleBean anotherBean() { + return new SimpleBean(); + } + + } + } +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/MockBeanFactoryInitializationCode.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/MockBeanFactoryInitializationCode.java new file mode 100644 index 000000000000..c6986c7c4b04 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/MockBeanFactoryInitializationCode.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-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.beans.testfixture.beans.factory.aot; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.aot.generate.GeneratedClass; +import org.springframework.aot.generate.GeneratedMethods; +import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.generate.MethodReference; +import org.springframework.beans.factory.aot.BeanFactoryInitializationCode; +import org.springframework.javapoet.ClassName; + +/** + * Mock {@link BeanFactoryInitializationCode} implementation. + * + * @author Stephane Nicoll + * @author Phillip Webb + */ +public class MockBeanFactoryInitializationCode implements BeanFactoryInitializationCode { + + private final GeneratedClass generatedClass; + + private final List initializers = new ArrayList<>(); + + private final DeferredTypeBuilder typeBuilder = new DeferredTypeBuilder(); + + + public MockBeanFactoryInitializationCode(GenerationContext generationContext) { + this.generatedClass = generationContext.getGeneratedClasses() + .addForFeature("TestCode", this.typeBuilder); + } + + public ClassName getClassName() { + return this.generatedClass.getName(); + } + + public DeferredTypeBuilder getTypeBuilder() { + return this.typeBuilder; + } + + @Override + public GeneratedMethods getMethods() { + return this.generatedClass.getMethods(); + } + + @Override + public void addInitializer(MethodReference methodReference) { + this.initializers.add(methodReference); + } + + public List getInitializers() { + return Collections.unmodifiableList(this.initializers); + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/MockBeanRegistrationCode.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/MockBeanRegistrationCode.java new file mode 100644 index 000000000000..eeeceec3a122 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/MockBeanRegistrationCode.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-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.beans.testfixture.beans.factory.aot; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.aot.generate.GeneratedClass; +import org.springframework.aot.generate.GeneratedMethods; +import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.generate.MethodReference; +import org.springframework.beans.factory.aot.BeanRegistrationCode; +import org.springframework.javapoet.ClassName; + +/** + * Mock {@link BeanRegistrationCode} implementation. + * + * @author Stephane Nicoll + * @author Phillip Webb + */ +public class MockBeanRegistrationCode implements BeanRegistrationCode { + + private final GeneratedClass generatedClass; + + private final List instancePostProcessors = new ArrayList<>(); + + private final DeferredTypeBuilder typeBuilder = new DeferredTypeBuilder(); + + + public MockBeanRegistrationCode(GenerationContext generationContext) { + this.generatedClass = generationContext.getGeneratedClasses().addForFeature("TestCode", this.typeBuilder); + } + + + public DeferredTypeBuilder getTypeBuilder() { + return this.typeBuilder; + } + + @Override + public ClassName getClassName() { + return this.generatedClass.getName(); + } + + @Override + public GeneratedMethods getMethods() { + return this.generatedClass.getMethods(); + } + + @Override + public void addInstancePostProcessor(MethodReference methodReference) { + this.instancePostProcessors.add(methodReference); + } + + public List getInstancePostProcessors() { + return Collections.unmodifiableList(this.instancePostProcessors); + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/MockBeanRegistrationsCode.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/MockBeanRegistrationsCode.java new file mode 100644 index 000000000000..6298824b705d --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/MockBeanRegistrationsCode.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-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.beans.testfixture.beans.factory.aot; + +import org.springframework.aot.generate.GeneratedClass; +import org.springframework.aot.generate.GeneratedMethods; +import org.springframework.aot.generate.GenerationContext; +import org.springframework.beans.factory.aot.BeanRegistrationsCode; +import org.springframework.javapoet.ClassName; + +/** + * Mock {@link BeanRegistrationsCode} implementation. + * + * @author Phillip Webb + * @author Stephane Nicoll + */ +public class MockBeanRegistrationsCode implements BeanRegistrationsCode { + + private final GeneratedClass generatedClass; + + private final DeferredTypeBuilder typeBuilder = new DeferredTypeBuilder(); + + + public MockBeanRegistrationsCode(GenerationContext generationContext) { + this.generatedClass = generationContext.getGeneratedClasses().addForFeature("TestCode", this.typeBuilder); + } + + + public DeferredTypeBuilder getTypeBuilder() { + return this.typeBuilder; + } + + @Override + public ClassName getClassName() { + return this.generatedClass.getName(); + } + + @Override + public GeneratedMethods getMethods() { + return this.generatedClass.getMethods(); + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/NumberFactoryBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/NumberFactoryBean.java new file mode 100644 index 000000000000..bf333d05ce1a --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/NumberFactoryBean.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-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.beans.testfixture.beans.factory.aot; + +/** + * A {@link GenericFactoryBean} that has a bound for the target type. + * + * @author Stephane Nicoll + */ +public class NumberFactoryBean extends GenericFactoryBean { + + public NumberFactoryBean(Class beanType) { + super(beanType); + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/SimpleBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/SimpleBean.java new file mode 100644 index 000000000000..f27c2a8454b5 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/SimpleBean.java @@ -0,0 +1,25 @@ +/* + * Copyright 2002-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.beans.testfixture.beans.factory.aot; + +/** + * An empty test bean used by code generation. + * + * @author Stephane Nicoll + */ +public class SimpleBean { +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/SimpleBeanConfiguration.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/SimpleBeanConfiguration.java new file mode 100644 index 000000000000..dde310e37d3e --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/SimpleBeanConfiguration.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-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.beans.testfixture.beans.factory.aot; + +/** + * A sample configuration. + * + * @author Stephane Nicoll + */ +public class SimpleBeanConfiguration { + + public SimpleBean simpleBean() { + return new SimpleBean(); + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/SimpleBeanFactoryBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/SimpleBeanFactoryBean.java new file mode 100644 index 000000000000..6202888124f6 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/SimpleBeanFactoryBean.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-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.beans.testfixture.beans.factory.aot; + +import org.springframework.beans.factory.FactoryBean; + +/** + * A public {@link FactoryBean} with a resolved generic for {@link GenericFactoryBean}. + * + * @author Stephane Nicoll + */ +public class SimpleBeanFactoryBean extends GenericFactoryBean { + + public SimpleBeanFactoryBean() { + super(SimpleBean.class); + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/package-info.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/package-info.java new file mode 100644 index 000000000000..dc39666d1a57 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/package-info.java @@ -0,0 +1,9 @@ +/** + * Test fixtures for bean factories AOT support. + */ +@NonNullApi +@NonNullFields +package org.springframework.beans.testfixture.beans.factory.aot; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/BeanFactoryInitializer.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/BeanFactoryInitializer.java new file mode 100644 index 000000000000..548548410112 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/BeanFactoryInitializer.java @@ -0,0 +1,26 @@ +/* + * Copyright 2002-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.beans.testfixture.beans.factory.generator; + +import org.springframework.beans.factory.support.DefaultListableBeanFactory; + +@FunctionalInterface +public interface BeanFactoryInitializer { + + void initializeBeanFactory(DefaultListableBeanFactory beanFactory); + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/InnerComponentConfiguration.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/InnerComponentConfiguration.java new file mode 100644 index 000000000000..28188b2a52a0 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/InnerComponentConfiguration.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-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.beans.testfixture.beans.factory.generator; + +import org.springframework.core.env.Environment; + +public class InnerComponentConfiguration { + + public class NoDependencyComponent { + + public NoDependencyComponent() { + + } + } + + public class EnvironmentAwareComponent { + + public EnvironmentAwareComponent(Environment environment) { + + } + } +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/SimpleConfiguration.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/SimpleConfiguration.java new file mode 100644 index 000000000000..89d459297bfd --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/SimpleConfiguration.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-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.beans.testfixture.beans.factory.generator; + +import java.io.IOException; + +public class SimpleConfiguration { + + public SimpleConfiguration() { + } + + public String stringBean() { + return "Hello"; + } + + @SuppressWarnings("unused") + private static String privateStaticStringBean() { + return "Hello"; + } + + static String packageStaticStringBean() { + return "Hello"; + } + + public static Integer integerBean() { + return 42; + } + + public Integer throwingIntegerBean() throws IOException { + return 42; + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/factory/NumberHolder.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/factory/NumberHolder.java new file mode 100644 index 000000000000..c05dc8341e8e --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/factory/NumberHolder.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-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.beans.testfixture.beans.factory.generator.factory; + +import java.io.Serializable; + +/** + * A sample object with a generic type. + * + * @param the number type + * @author Stephane Nicoll + */ +@SuppressWarnings("serial") +public class NumberHolder implements Serializable { + + @SuppressWarnings("unused") + private final T number; + + public NumberHolder(T number) { + this.number = number; + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/factory/NumberHolderFactoryBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/factory/NumberHolderFactoryBean.java new file mode 100644 index 000000000000..a837a8dcaf53 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/factory/NumberHolderFactoryBean.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-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.beans.testfixture.beans.factory.generator.factory; + +import org.springframework.beans.factory.FactoryBean; + +/** + * A sample factory bean with a generic type. + * + * @param the type of the number generated by this factory bean + * @author Stephane Nicoll + */ +public class NumberHolderFactoryBean implements FactoryBean> { + + private T number; + + public void setNumber(T number) { + this.number = number; + } + + @Override + public NumberHolder getObject() { + return new NumberHolder<>(this.number); + } + + @Override + public Class getObjectType() { + return NumberHolder.class; + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/factory/SampleFactory.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/factory/SampleFactory.java new file mode 100644 index 000000000000..eca5e0d7bfdf --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/factory/SampleFactory.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-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.beans.testfixture.beans.factory.generator.factory; + +public abstract class SampleFactory { + + public static String create(String testBean) { + return testBean; + } + + public static String create(char character) { + return String.valueOf(character); + } + + public static String create(Number number, String test) { + return number + test; + } + + public static String create(Class type) { + return type.getName(); + } + + public static Integer integerBean() { + return 42; + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/injection/InjectionComponent.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/injection/InjectionComponent.java new file mode 100644 index 000000000000..9d772ade5a34 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/injection/InjectionComponent.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-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.beans.testfixture.beans.factory.generator.injection; + +import org.springframework.beans.factory.annotation.Autowired; + +@SuppressWarnings("unused") +public class InjectionComponent { + + private final String bean; + + private Integer counter; + + public InjectionComponent(String bean) { + this.bean = bean; + } + + @Autowired(required = false) + public void setCounter(Integer counter) { + this.counter = counter; + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/lifecycle/Destroy.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/lifecycle/Destroy.java new file mode 100644 index 000000000000..89db8539e28f --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/lifecycle/Destroy.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-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.beans.testfixture.beans.factory.generator.lifecycle; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Documented +@Retention(RUNTIME) +@Target(METHOD) +public @interface Destroy { +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/lifecycle/InferredDestroyBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/lifecycle/InferredDestroyBean.java new file mode 100644 index 000000000000..ffa538b2422e --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/lifecycle/InferredDestroyBean.java @@ -0,0 +1,26 @@ +/* + * Copyright 2002-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.beans.testfixture.beans.factory.generator.lifecycle; + +public class InferredDestroyBean { + + public void close() { + + } +} + + diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/lifecycle/Init.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/lifecycle/Init.java new file mode 100644 index 000000000000..8c56616b7477 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/lifecycle/Init.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-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.beans.testfixture.beans.factory.generator.lifecycle; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Documented +@Retention(RUNTIME) +@Target(METHOD) +public @interface Init { +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/lifecycle/InitDestroyBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/lifecycle/InitDestroyBean.java new file mode 100644 index 000000000000..838b16783e42 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/lifecycle/InitDestroyBean.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-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.beans.testfixture.beans.factory.generator.lifecycle; + +public class InitDestroyBean { + + @Init + public void initMethod() { + } + + public void customInitMethod() { + } + + @Destroy + public void destroyMethod() { + } + + public void customDestroyMethod() { + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/lifecycle/MultiInitDestroyBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/lifecycle/MultiInitDestroyBean.java new file mode 100644 index 000000000000..47cbde926280 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/lifecycle/MultiInitDestroyBean.java @@ -0,0 +1,29 @@ +/* + * Copyright 2002-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.beans.testfixture.beans.factory.generator.lifecycle; + +public class MultiInitDestroyBean extends InitDestroyBean { + + @Init + void anotherInitMethod() { + } + + @Destroy + void anotherDestroyMethod() { + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/property/ConfigurableBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/property/ConfigurableBean.java new file mode 100644 index 000000000000..047e738a9395 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/property/ConfigurableBean.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-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.beans.testfixture.beans.factory.generator.property; + +public class ConfigurableBean { + + @SuppressWarnings("unused") + private String name; + + @SuppressWarnings("unused") + private Integer counter; + + public void setName(String name) { + this.name = name; + } + + public void setCounter(Integer counter) { + this.counter = counter; + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/visibility/ProtectedConstructorComponent.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/visibility/ProtectedConstructorComponent.java new file mode 100644 index 000000000000..5b27a375cdd2 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/visibility/ProtectedConstructorComponent.java @@ -0,0 +1,24 @@ +/* + * Copyright 2002-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.beans.testfixture.beans.factory.generator.visibility; + +public class ProtectedConstructorComponent { + + ProtectedConstructorComponent() { + + } +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/visibility/ProtectedFactoryMethod.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/visibility/ProtectedFactoryMethod.java new file mode 100644 index 000000000000..7d2fd2b5574d --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/visibility/ProtectedFactoryMethod.java @@ -0,0 +1,25 @@ +/* + * Copyright 2002-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.beans.testfixture.beans.factory.generator.visibility; + +public class ProtectedFactoryMethod { + + String testBean(Integer number) { + return "test-" + number; + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/factory/xml/AbstractBeanFactoryTests.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/factory/xml/AbstractBeanFactoryTests.java index 307a9d64c12d..3062dc62dcdf 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/factory/xml/AbstractBeanFactoryTests.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/factory/xml/AbstractBeanFactoryTests.java @@ -261,11 +261,9 @@ public void rejectsFactoryGetOnNormalBean() { @Test public void aliasing() { BeanFactory bf = getBeanFactory(); - if (!(bf instanceof ConfigurableBeanFactory)) { + if (!(bf instanceof ConfigurableBeanFactory cbf)) { return; } - ConfigurableBeanFactory cbf = (ConfigurableBeanFactory) bf; - String alias = "rods alias"; assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> diff --git a/spring-context-indexer/spring-context-indexer.gradle b/spring-context-indexer/spring-context-indexer.gradle index a9769ada026a..40d69a22670d 100644 --- a/spring-context-indexer/spring-context-indexer.gradle +++ b/spring-context-indexer/spring-context-indexer.gradle @@ -1,9 +1,9 @@ description = "Spring Context Indexer" dependencies { - testCompile(project(":spring-context")) - testCompile("javax.inject:javax.inject") - testCompile("javax.annotation:javax.annotation-api") - testCompile("javax.transaction:javax.transaction-api") - testCompile("org.eclipse.persistence:javax.persistence") + testImplementation(project(":spring-context")) + testImplementation("jakarta.inject:jakarta.inject-api") + testImplementation("jakarta.annotation:jakarta.annotation-api") + testImplementation("jakarta.persistence:jakarta.persistence-api") + testImplementation("jakarta.transaction:jakarta.transaction-api") } diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/CandidateComponentsIndexer.java b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/CandidateComponentsIndexer.java index fcd0d63e12f4..9f1edf7f0cc6 100644 --- a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/CandidateComponentsIndexer.java +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/CandidateComponentsIndexer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -19,7 +19,6 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; -import java.util.EnumSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -37,7 +36,7 @@ import javax.lang.model.element.TypeElement; /** - * Annotation {@link Processor} that writes {@link CandidateComponentsMetadata} + * Annotation {@link Processor} that writes a {@link CandidateComponentsMetadata} * file for spring components. * * @author Stephane Nicoll @@ -46,9 +45,6 @@ */ public class CandidateComponentsIndexer implements Processor { - private static final Set TYPE_KINDS = - Collections.unmodifiableSet(EnumSet.of(ElementKind.CLASS, ElementKind.INTERFACE)); - private MetadataStore metadataStore; private MetadataCollector metadataCollector; @@ -136,7 +132,8 @@ private void writeMetaData() { private static List staticTypesIn(Iterable elements) { List list = new ArrayList<>(); for (Element element : elements) { - if (TYPE_KINDS.contains(element.getKind()) && element.getModifiers().contains(Modifier.STATIC)) { + if ((element.getKind().isClass() || element.getKind() == ElementKind.INTERFACE) && + element.getModifiers().contains(Modifier.STATIC) && element instanceof TypeElement) { list.add((TypeElement) element); } } diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/IndexedStereotypesProvider.java b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/IndexedStereotypesProvider.java index c8ef2b751851..6027c0e66801 100644 --- a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/IndexedStereotypesProvider.java +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/IndexedStereotypesProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -26,7 +26,7 @@ /** * A {@link StereotypesProvider} implementation that extracts the stereotypes - * flagged by the {@value INDEXED_ANNOTATION} annotation. This implementation + * flagged by the {@value #INDEXED_ANNOTATION} annotation. This implementation * honors stereotypes defined this way on meta-annotations. * * @author Stephane Nicoll @@ -48,7 +48,7 @@ public IndexedStereotypesProvider(TypeHelper typeHelper) { public Set getStereotypes(Element element) { Set stereotypes = new LinkedHashSet<>(); ElementKind kind = element.getKind(); - if (kind != ElementKind.CLASS && kind != ElementKind.INTERFACE) { + if (!kind.isClass() && kind != ElementKind.INTERFACE) { return stereotypes; } Set seen = new HashSet<>(); diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/MetadataStore.java b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/MetadataStore.java index c00f682b77ea..8585b89bea9f 100644 --- a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/MetadataStore.java +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/MetadataStore.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -62,12 +62,9 @@ public void writeMetadata(CandidateComponentsMetadata metadata) throws IOExcepti private CandidateComponentsMetadata readMetadata(InputStream in) throws IOException { - try { + try (in) { return PropertiesMarshaller.read(in); } - finally { - in.close(); - } } private FileObject getMetadataResource() throws IOException { diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/PackageInfoStereotypesProvider.java b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/PackageInfoStereotypesProvider.java index d35b4370c27b..685f4e258640 100644 --- a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/PackageInfoStereotypesProvider.java +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/PackageInfoStereotypesProvider.java @@ -24,7 +24,7 @@ /** * A {@link StereotypesProvider} implementation that provides the - * {@value STEREOTYPE} stereotype for each package-info. + * {@value #STEREOTYPE} stereotype for each package-info. * * @author Stephane Nicoll * @since 5.0 diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/PropertiesMarshaller.java b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/PropertiesMarshaller.java index 84a8d838c921..ab6c52aafe56 100644 --- a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/PropertiesMarshaller.java +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/PropertiesMarshaller.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -19,8 +19,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.Arrays; -import java.util.HashSet; import java.util.Properties; import java.util.Set; @@ -44,7 +42,7 @@ public static CandidateComponentsMetadata read(InputStream in) throws IOExceptio Properties props = new Properties(); props.load(in); props.forEach((type, value) -> { - Set candidates = new HashSet<>(Arrays.asList(((String) value).split(","))); + Set candidates = Set.of(((String) value).split(",")); result.add(new ItemMetadata((String) type, candidates)); }); return result; diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/SortedProperties.java b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/SortedProperties.java index 127a1a2b9614..816ec3ea3ee7 100644 --- a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/SortedProperties.java +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/SortedProperties.java @@ -88,7 +88,7 @@ class SortedProperties extends Properties { public void store(OutputStream out, String comments) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); super.store(baos, (this.omitComments ? null : comments)); - String contents = baos.toString(StandardCharsets.ISO_8859_1.name()); + String contents = baos.toString(StandardCharsets.ISO_8859_1); for (String line : contents.split(EOL)) { if (!(this.omitComments && line.startsWith("#"))) { out.write((line + EOL).getBytes(StandardCharsets.ISO_8859_1)); diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/StandardStereotypesProvider.java b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/StandardStereotypesProvider.java index 378343c6a33c..6b36136ba6a0 100644 --- a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/StandardStereotypesProvider.java +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/StandardStereotypesProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -24,8 +24,8 @@ import javax.lang.model.element.ElementKind; /** - * A {@link StereotypesProvider} that extract a stereotype for each - * {@code javax.*} annotation placed on a class or interface. + * A {@link StereotypesProvider} that extracts a stereotype for each + * {@code jakarta.*} annotation present on a class or interface. * * @author Stephane Nicoll * @since 5.0 @@ -49,7 +49,7 @@ public Set getStereotypes(Element element) { } for (AnnotationMirror annotation : this.typeHelper.getAllAnnotationMirrors(element)) { String type = this.typeHelper.getType(annotation); - if (type.startsWith("javax.")) { + if (type.startsWith("jakarta.")) { stereotypes.add(type); } } diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/StereotypesProvider.java b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/StereotypesProvider.java index e0e5f7a7422c..e25d16a3b705 100644 --- a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/StereotypesProvider.java +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/StereotypesProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -22,7 +22,8 @@ /** * Provide the list of stereotypes that match an {@link Element}. - * If an element has one more stereotypes, it is referenced in the index + * + *

    If an element has one or more stereotypes, it is referenced in the index * of candidate components and each stereotype can be queried individually. * * @author Stephane Nicoll diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/TypeHelper.java b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/TypeHelper.java index 470c0398a235..8789d3f530fd 100644 --- a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/TypeHelper.java +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/TypeHelper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -60,8 +60,7 @@ public String getType(TypeMirror type) { if (type == null) { return null; } - if (type instanceof DeclaredType) { - DeclaredType declaredType = (DeclaredType) type; + if (type instanceof DeclaredType declaredType) { Element enclosingElement = declaredType.asElement().getEnclosingElement(); if (enclosingElement instanceof TypeElement) { return getQualifiedName(enclosingElement) + "$" + declaredType.asElement().getSimpleName().toString(); @@ -81,7 +80,7 @@ private String getQualifiedName(Element element) { } /** - * Return the super class of the specified {@link Element} or null if this + * Return the superclass of the specified {@link Element} or null if this * {@code element} represents {@link Object}. */ public Element getSuperClass(Element element) { @@ -100,7 +99,7 @@ public Element getSuperClass(Element element) { public List getDirectInterfaces(Element element) { List superTypes = this.types.directSupertypes(element.asType()); List directInterfaces = new ArrayList<>(); - if (superTypes.size() > 1) { // index 0 is the super class + if (superTypes.size() > 1) { // index 0 is the superclass for (int i = 1; i < superTypes.size(); i++) { Element e = this.types.asElement(superTypes.get(i)); if (e != null) { diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/processor/CandidateComponentsIndexerTests.java b/spring-context-indexer/src/test/java/org/springframework/context/index/processor/CandidateComponentsIndexerTests.java index aa1a21d7367c..be3d30dc4207 100644 --- a/spring-context-indexer/src/test/java/org/springframework/context/index/processor/CandidateComponentsIndexerTests.java +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/processor/CandidateComponentsIndexerTests.java @@ -21,14 +21,13 @@ import java.io.IOException; import java.nio.file.Path; -import javax.annotation.ManagedBean; -import javax.inject.Named; -import javax.persistence.Converter; -import javax.persistence.Embeddable; -import javax.persistence.Entity; -import javax.persistence.MappedSuperclass; -import javax.transaction.Transactional; - +import jakarta.annotation.ManagedBean; +import jakarta.inject.Named; +import jakarta.persistence.Converter; +import jakarta.persistence.Embeddable; +import jakarta.persistence.Entity; +import jakarta.persistence.MappedSuperclass; +import jakarta.transaction.Transactional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/processor/Metadata.java b/spring-context-indexer/src/test/java/org/springframework/context/index/processor/Metadata.java index 30ca2caf1a26..f0464b9333e5 100644 --- a/spring-context-indexer/src/test/java/org/springframework/context/index/processor/Metadata.java +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/processor/Metadata.java @@ -18,7 +18,6 @@ import java.util.Arrays; import java.util.List; -import java.util.stream.Collectors; import org.assertj.core.api.Condition; @@ -30,7 +29,7 @@ class Metadata { public static Condition of(Class type, Class... stereotypes) { - return of(type.getName(), Arrays.stream(stereotypes).map(Class::getName).collect(Collectors.toList())); + return of(type.getName(), Arrays.stream(stereotypes).map(Class::getName).toList()); } public static Condition of(String type, String... stereotypes) { diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleNonStaticEmbedded.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleNonStaticEmbedded.java index 4e12931d879a..fb8fe8abc243 100644 --- a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleNonStaticEmbedded.java +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleNonStaticEmbedded.java @@ -19,7 +19,7 @@ import org.springframework.stereotype.Component; /** - * Candidate with a inner class that isn't static (and should therefore not be added). + * Candidate with an inner class that isn't static (and should therefore not be added). * * @author Stephane Nicoll */ diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/cdi/SampleManagedBean.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/cdi/SampleManagedBean.java index d3bf3dd8b785..fb34361664d6 100644 --- a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/cdi/SampleManagedBean.java +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/cdi/SampleManagedBean.java @@ -16,7 +16,7 @@ package org.springframework.context.index.sample.cdi; -import javax.annotation.ManagedBean; +import jakarta.annotation.ManagedBean; /** * Test candidate for a CDI {@link ManagedBean}. diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/cdi/SampleNamed.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/cdi/SampleNamed.java index 20ca0342e68f..d975af6b1f30 100644 --- a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/cdi/SampleNamed.java +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/cdi/SampleNamed.java @@ -16,7 +16,7 @@ package org.springframework.context.index.sample.cdi; -import javax.inject.Named; +import jakarta.inject.Named; /** * Test candidate for a CDI {@link Named} bean. diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/cdi/SampleTransactional.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/cdi/SampleTransactional.java index f104d5604e88..273aabcb14f3 100644 --- a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/cdi/SampleTransactional.java +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/cdi/SampleTransactional.java @@ -16,7 +16,7 @@ package org.springframework.context.index.sample.cdi; -import javax.transaction.Transactional; +import jakarta.transaction.Transactional; /** * Test candidate for {@link Transactional}. This verifies that the annotation processor diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleConverter.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleConverter.java index 129f090f5779..474dd5c1a298 100644 --- a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleConverter.java +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleConverter.java @@ -16,7 +16,7 @@ package org.springframework.context.index.sample.jpa; -import javax.persistence.Converter; +import jakarta.persistence.Converter; /** * Test candidate for {@link Converter}. diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleEmbeddable.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleEmbeddable.java index 79269507395a..15f3356ce70c 100644 --- a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleEmbeddable.java +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleEmbeddable.java @@ -16,7 +16,7 @@ package org.springframework.context.index.sample.jpa; -import javax.persistence.Embeddable; +import jakarta.persistence.Embeddable; /** * Test candidate for {@link Embeddable}. diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleEntity.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleEntity.java index 101c3891d902..ae6e87559d29 100644 --- a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleEntity.java +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleEntity.java @@ -16,7 +16,7 @@ package org.springframework.context.index.sample.jpa; -import javax.persistence.Entity; +import jakarta.persistence.Entity; /** * Test candidate for {@link Entity}. diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleMappedSuperClass.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleMappedSuperClass.java index 73737f4e98be..fbca3e43613b 100644 --- a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleMappedSuperClass.java +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleMappedSuperClass.java @@ -16,7 +16,7 @@ package org.springframework.context.index.sample.jpa; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.MappedSuperclass; /** * Test candidate for {@link MappedSuperclass}. diff --git a/spring-context-support/spring-context-support.gradle b/spring-context-support/spring-context-support.gradle index f8a8631293ad..160f8028985c 100644 --- a/spring-context-support/spring-context-support.gradle +++ b/spring-context-support/spring-context-support.gradle @@ -1,31 +1,28 @@ description = "Spring Context Support" dependencies { - compile(project(":spring-beans")) - compile(project(":spring-context")) - compile(project(":spring-core")) + api(project(":spring-beans")) + api(project(":spring-context")) + api(project(":spring-core")) optional(project(":spring-jdbc")) // for Quartz support optional(project(":spring-tx")) // for Quartz support - optional("javax.activation:javax.activation-api") - optional("javax.mail:javax.mail-api") + optional("jakarta.activation:jakarta.activation-api") + optional("jakarta.mail:jakarta.mail-api") optional("javax.cache:cache-api") optional("com.github.ben-manes.caffeine:caffeine") - optional("net.sf.ehcache:ehcache") optional("org.quartz-scheduler:quartz") - optional("org.codehaus.fabric3.api:commonj") optional("org.freemarker:freemarker") - testCompile(project(":spring-context")) - testCompile(testFixtures(project(":spring-beans"))) - testCompile(testFixtures(project(":spring-context"))) - testCompile(testFixtures(project(":spring-core"))) - testCompile(testFixtures(project(":spring-tx"))) - testCompile("org.hsqldb:hsqldb") - testCompile("org.hibernate:hibernate-validator") - testCompile("javax.annotation:javax.annotation-api") - testRuntime("org.ehcache:jcache") - testRuntime("org.ehcache:ehcache") - testRuntime("org.glassfish:javax.el") - testRuntime("com.sun.mail:javax.mail") + testImplementation(project(":spring-context")) + testImplementation(testFixtures(project(":spring-beans"))) + testImplementation(testFixtures(project(":spring-context"))) + testImplementation(testFixtures(project(":spring-core"))) + testImplementation(testFixtures(project(":spring-tx"))) + testImplementation("org.hsqldb:hsqldb") + testImplementation("jakarta.annotation:jakarta.annotation-api") + testRuntimeOnly("org.ehcache:jcache") + testRuntimeOnly("org.ehcache:ehcache") + testRuntimeOnly("org.glassfish:jakarta.el") + testRuntimeOnly("com.sun.mail:jakarta.mail") testFixturesApi("org.junit.jupiter:junit-jupiter-api") testFixturesImplementation("org.assertj:assertj-core") testFixturesImplementation("org.mockito:mockito-core") diff --git a/spring-context-support/src/main/java/org/springframework/cache/ehcache/EhCacheCache.java b/spring-context-support/src/main/java/org/springframework/cache/ehcache/EhCacheCache.java deleted file mode 100644 index 4309fa73a2cb..000000000000 --- a/spring-context-support/src/main/java/org/springframework/cache/ehcache/EhCacheCache.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright 2002-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.cache.ehcache; - -import java.util.concurrent.Callable; - -import net.sf.ehcache.Ehcache; -import net.sf.ehcache.Element; -import net.sf.ehcache.Status; - -import org.springframework.cache.Cache; -import org.springframework.cache.support.SimpleValueWrapper; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; - -/** - * {@link Cache} implementation on top of an {@link Ehcache} instance. - * - * @author Costin Leau - * @author Juergen Hoeller - * @author Stephane Nicoll - * @since 3.1 - * @see EhCacheCacheManager - */ -public class EhCacheCache implements Cache { - - private final Ehcache cache; - - - /** - * Create an {@link EhCacheCache} instance. - * @param ehcache the backing Ehcache instance - */ - public EhCacheCache(Ehcache ehcache) { - Assert.notNull(ehcache, "Ehcache must not be null"); - Status status = ehcache.getStatus(); - if (!Status.STATUS_ALIVE.equals(status)) { - throw new IllegalArgumentException( - "An 'alive' Ehcache is required - current cache is " + status.toString()); - } - this.cache = ehcache; - } - - - @Override - public final String getName() { - return this.cache.getName(); - } - - @Override - public final Ehcache getNativeCache() { - return this.cache; - } - - @Override - @Nullable - public ValueWrapper get(Object key) { - Element element = lookup(key); - return toValueWrapper(element); - } - - @SuppressWarnings("unchecked") - @Override - @Nullable - public T get(Object key, @Nullable Class type) { - Element element = this.cache.get(key); - Object value = (element != null ? element.getObjectValue() : null); - if (value != null && type != null && !type.isInstance(value)) { - throw new IllegalStateException( - "Cached value is not of required type [" + type.getName() + "]: " + value); - } - return (T) value; - } - - @SuppressWarnings("unchecked") - @Override - @Nullable - public T get(Object key, Callable valueLoader) { - Element element = lookup(key); - if (element != null) { - return (T) element.getObjectValue(); - } - else { - this.cache.acquireWriteLockOnKey(key); - try { - element = lookup(key); // one more attempt with the write lock - if (element != null) { - return (T) element.getObjectValue(); - } - else { - return loadValue(key, valueLoader); - } - } - finally { - this.cache.releaseWriteLockOnKey(key); - } - } - } - - private T loadValue(Object key, Callable valueLoader) { - T value; - try { - value = valueLoader.call(); - } - catch (Throwable ex) { - throw new ValueRetrievalException(key, valueLoader, ex); - } - put(key, value); - return value; - } - - @Override - public void put(Object key, @Nullable Object value) { - this.cache.put(new Element(key, value)); - } - - @Override - @Nullable - public ValueWrapper putIfAbsent(Object key, @Nullable Object value) { - Element existingElement = this.cache.putIfAbsent(new Element(key, value)); - return toValueWrapper(existingElement); - } - - @Override - public void evict(Object key) { - this.cache.remove(key); - } - - @Override - public boolean evictIfPresent(Object key) { - return this.cache.remove(key); - } - - @Override - public void clear() { - this.cache.removeAll(); - } - - @Override - public boolean invalidate() { - boolean notEmpty = (this.cache.getSize() > 0); - this.cache.removeAll(); - return notEmpty; - } - - - @Nullable - private Element lookup(Object key) { - return this.cache.get(key); - } - - @Nullable - private ValueWrapper toValueWrapper(@Nullable Element element) { - return (element != null ? new SimpleValueWrapper(element.getObjectValue()) : null); - } - -} diff --git a/spring-context-support/src/main/java/org/springframework/cache/ehcache/EhCacheCacheManager.java b/spring-context-support/src/main/java/org/springframework/cache/ehcache/EhCacheCacheManager.java deleted file mode 100644 index f3e58a55b288..000000000000 --- a/spring-context-support/src/main/java/org/springframework/cache/ehcache/EhCacheCacheManager.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright 2002-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.cache.ehcache; - -import java.util.Collection; -import java.util.LinkedHashSet; - -import net.sf.ehcache.Ehcache; -import net.sf.ehcache.Status; - -import org.springframework.cache.Cache; -import org.springframework.cache.transaction.AbstractTransactionSupportingCacheManager; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; - -/** - * CacheManager backed by an EhCache {@link net.sf.ehcache.CacheManager}. - * - * @author Costin Leau - * @author Juergen Hoeller - * @author Stephane Nicoll - * @since 3.1 - * @see EhCacheCache - */ -public class EhCacheCacheManager extends AbstractTransactionSupportingCacheManager { - - @Nullable - private net.sf.ehcache.CacheManager cacheManager; - - - /** - * Create a new EhCacheCacheManager, setting the target EhCache CacheManager - * through the {@link #setCacheManager} bean property. - */ - public EhCacheCacheManager() { - } - - /** - * Create a new EhCacheCacheManager for the given backing EhCache CacheManager. - * @param cacheManager the backing EhCache {@link net.sf.ehcache.CacheManager} - */ - public EhCacheCacheManager(net.sf.ehcache.CacheManager cacheManager) { - this.cacheManager = cacheManager; - } - - - /** - * Set the backing EhCache {@link net.sf.ehcache.CacheManager}. - */ - public void setCacheManager(@Nullable net.sf.ehcache.CacheManager cacheManager) { - this.cacheManager = cacheManager; - } - - /** - * Return the backing EhCache {@link net.sf.ehcache.CacheManager}. - */ - @Nullable - public net.sf.ehcache.CacheManager getCacheManager() { - return this.cacheManager; - } - - @Override - public void afterPropertiesSet() { - if (getCacheManager() == null) { - setCacheManager(EhCacheManagerUtils.buildCacheManager()); - } - super.afterPropertiesSet(); - } - - - @Override - protected Collection loadCaches() { - net.sf.ehcache.CacheManager cacheManager = getCacheManager(); - Assert.state(cacheManager != null, "No CacheManager set"); - - Status status = cacheManager.getStatus(); - if (!Status.STATUS_ALIVE.equals(status)) { - throw new IllegalStateException( - "An 'alive' EhCache CacheManager is required - current cache is " + status.toString()); - } - - String[] names = getCacheManager().getCacheNames(); - Collection caches = new LinkedHashSet<>(names.length); - for (String name : names) { - caches.add(new EhCacheCache(getCacheManager().getEhcache(name))); - } - return caches; - } - - @Override - protected Cache getMissingCache(String name) { - net.sf.ehcache.CacheManager cacheManager = getCacheManager(); - Assert.state(cacheManager != null, "No CacheManager set"); - - // Check the EhCache cache again (in case the cache was added at runtime) - Ehcache ehcache = cacheManager.getEhcache(name); - if (ehcache != null) { - return new EhCacheCache(ehcache); - } - return null; - } - -} diff --git a/spring-context-support/src/main/java/org/springframework/cache/ehcache/EhCacheFactoryBean.java b/spring-context-support/src/main/java/org/springframework/cache/ehcache/EhCacheFactoryBean.java deleted file mode 100644 index 3d4f839a3b93..000000000000 --- a/spring-context-support/src/main/java/org/springframework/cache/ehcache/EhCacheFactoryBean.java +++ /dev/null @@ -1,329 +0,0 @@ -/* - * Copyright 2002-2018 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.cache.ehcache; - -import java.util.Set; - -import net.sf.ehcache.Cache; -import net.sf.ehcache.CacheException; -import net.sf.ehcache.CacheManager; -import net.sf.ehcache.Ehcache; -import net.sf.ehcache.bootstrap.BootstrapCacheLoader; -import net.sf.ehcache.config.CacheConfiguration; -import net.sf.ehcache.constructs.blocking.BlockingCache; -import net.sf.ehcache.constructs.blocking.CacheEntryFactory; -import net.sf.ehcache.constructs.blocking.SelfPopulatingCache; -import net.sf.ehcache.constructs.blocking.UpdatingCacheEntryFactory; -import net.sf.ehcache.constructs.blocking.UpdatingSelfPopulatingCache; -import net.sf.ehcache.event.CacheEventListener; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.beans.factory.BeanNameAware; -import org.springframework.beans.factory.FactoryBean; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; - -/** - * {@link FactoryBean} that creates a named EhCache {@link net.sf.ehcache.Cache} instance - * (or a decorator that implements the {@link net.sf.ehcache.Ehcache} interface), - * representing a cache region within an EhCache {@link net.sf.ehcache.CacheManager}. - * - *

    If the specified named cache is not configured in the cache configuration descriptor, - * this FactoryBean will construct an instance of a Cache with the provided name and the - * specified cache properties and add it to the CacheManager for later retrieval. If some - * or all properties are not set at configuration time, this FactoryBean will use defaults. - * - *

    Note: If the named Cache instance is found, the properties will be ignored and the - * Cache instance will be retrieved from the CacheManager. - * - *

    Note: As of Spring 5.0, Spring's EhCache support requires EhCache 2.10 or higher. - * - * @author Juergen Hoeller - * @author Dmitriy Kopylenko - * @since 1.1.1 - * @see #setCacheManager - * @see EhCacheManagerFactoryBean - * @see net.sf.ehcache.Cache - */ -public class EhCacheFactoryBean extends CacheConfiguration implements FactoryBean, BeanNameAware, InitializingBean { - - protected final Log logger = LogFactory.getLog(getClass()); - - @Nullable - private CacheManager cacheManager; - - private boolean blocking = false; - - @Nullable - private CacheEntryFactory cacheEntryFactory; - - @Nullable - private BootstrapCacheLoader bootstrapCacheLoader; - - @Nullable - private Set cacheEventListeners; - - private boolean disabled = false; - - @Nullable - private String beanName; - - @Nullable - private Ehcache cache; - - - public EhCacheFactoryBean() { - setMaxEntriesLocalHeap(10000); - setMaxEntriesLocalDisk(10000000); - setTimeToLiveSeconds(120); - setTimeToIdleSeconds(120); - } - - - /** - * Set a CacheManager from which to retrieve a named Cache instance. - * By default, {@code CacheManager.getInstance()} will be called. - *

    Note that in particular for persistent caches, it is advisable to - * properly handle the shutdown of the CacheManager: Set up a separate - * EhCacheManagerFactoryBean and pass a reference to this bean property. - *

    A separate EhCacheManagerFactoryBean is also necessary for loading - * EhCache configuration from a non-default config location. - * @see EhCacheManagerFactoryBean - * @see net.sf.ehcache.CacheManager#getInstance - */ - public void setCacheManager(CacheManager cacheManager) { - this.cacheManager = cacheManager; - } - - /** - * Set a name for which to retrieve or create a cache instance. - * Default is the bean name of this EhCacheFactoryBean. - */ - public void setCacheName(String cacheName) { - setName(cacheName); - } - - /** - * Set the time to live. - * @see #setTimeToLiveSeconds(long) - */ - public void setTimeToLive(int timeToLive) { - setTimeToLiveSeconds(timeToLive); - } - - /** - * Set the time to idle. - * @see #setTimeToIdleSeconds(long) - */ - public void setTimeToIdle(int timeToIdle) { - setTimeToIdleSeconds(timeToIdle); - } - - /** - * Set the disk spool buffer size (in MB). - * @see #setDiskSpoolBufferSizeMB(int) - */ - public void setDiskSpoolBufferSize(int diskSpoolBufferSize) { - setDiskSpoolBufferSizeMB(diskSpoolBufferSize); - } - - /** - * Set whether to use a blocking cache that lets read attempts block - * until the requested element is created. - *

    If you intend to build a self-populating blocking cache, - * consider specifying a {@link #setCacheEntryFactory CacheEntryFactory}. - * @see net.sf.ehcache.constructs.blocking.BlockingCache - * @see #setCacheEntryFactory - */ - public void setBlocking(boolean blocking) { - this.blocking = blocking; - } - - /** - * Set an EhCache {@link net.sf.ehcache.constructs.blocking.CacheEntryFactory} - * to use for a self-populating cache. If such a factory is specified, - * the cache will be decorated with EhCache's - * {@link net.sf.ehcache.constructs.blocking.SelfPopulatingCache}. - *

    The specified factory can be of type - * {@link net.sf.ehcache.constructs.blocking.UpdatingCacheEntryFactory}, - * which will lead to the use of an - * {@link net.sf.ehcache.constructs.blocking.UpdatingSelfPopulatingCache}. - *

    Note: Any such self-populating cache is automatically a blocking cache. - * @see net.sf.ehcache.constructs.blocking.SelfPopulatingCache - * @see net.sf.ehcache.constructs.blocking.UpdatingSelfPopulatingCache - * @see net.sf.ehcache.constructs.blocking.UpdatingCacheEntryFactory - */ - public void setCacheEntryFactory(CacheEntryFactory cacheEntryFactory) { - this.cacheEntryFactory = cacheEntryFactory; - } - - /** - * Set an EhCache {@link net.sf.ehcache.bootstrap.BootstrapCacheLoader} - * for this cache, if any. - */ - public void setBootstrapCacheLoader(BootstrapCacheLoader bootstrapCacheLoader) { - this.bootstrapCacheLoader = bootstrapCacheLoader; - } - - /** - * Specify EhCache {@link net.sf.ehcache.event.CacheEventListener cache event listeners} - * to registered with this cache. - */ - public void setCacheEventListeners(Set cacheEventListeners) { - this.cacheEventListeners = cacheEventListeners; - } - - /** - * Set whether this cache should be marked as disabled. - * @see net.sf.ehcache.Cache#setDisabled - */ - public void setDisabled(boolean disabled) { - this.disabled = disabled; - } - - @Override - public void setBeanName(String name) { - this.beanName = name; - } - - - @Override - public void afterPropertiesSet() throws CacheException { - // If no cache name given, use bean name as cache name. - String cacheName = getName(); - if (cacheName == null) { - cacheName = this.beanName; - if (cacheName != null) { - setName(cacheName); - } - } - - // If no CacheManager given, fetch the default. - if (this.cacheManager == null) { - if (logger.isDebugEnabled()) { - logger.debug("Using default EhCache CacheManager for cache region '" + cacheName + "'"); - } - this.cacheManager = CacheManager.getInstance(); - } - - synchronized (this.cacheManager) { - // Fetch cache region: If none with the given name exists, create one on the fly. - Ehcache rawCache; - boolean cacheExists = this.cacheManager.cacheExists(cacheName); - - if (cacheExists) { - if (logger.isDebugEnabled()) { - logger.debug("Using existing EhCache cache region '" + cacheName + "'"); - } - rawCache = this.cacheManager.getEhcache(cacheName); - } - else { - if (logger.isDebugEnabled()) { - logger.debug("Creating new EhCache cache region '" + cacheName + "'"); - } - rawCache = createCache(); - rawCache.setBootstrapCacheLoader(this.bootstrapCacheLoader); - } - - if (this.cacheEventListeners != null) { - for (CacheEventListener listener : this.cacheEventListeners) { - rawCache.getCacheEventNotificationService().registerListener(listener); - } - } - - // Needs to happen after listener registration but before setStatisticsEnabled - if (!cacheExists) { - this.cacheManager.addCache(rawCache); - } - - if (this.disabled) { - rawCache.setDisabled(true); - } - - Ehcache decoratedCache = decorateCache(rawCache); - if (decoratedCache != rawCache) { - this.cacheManager.replaceCacheWithDecoratedCache(rawCache, decoratedCache); - } - this.cache = decoratedCache; - } - } - - /** - * Create a raw Cache object based on the configuration of this FactoryBean. - */ - protected Cache createCache() { - return new Cache(this); - } - - /** - * Decorate the given Cache, if necessary. - * @param cache the raw Cache object, based on the configuration of this FactoryBean - * @return the (potentially decorated) cache object to be registered with the CacheManager - */ - protected Ehcache decorateCache(Ehcache cache) { - if (this.cacheEntryFactory != null) { - if (this.cacheEntryFactory instanceof UpdatingCacheEntryFactory) { - return new UpdatingSelfPopulatingCache(cache, (UpdatingCacheEntryFactory) this.cacheEntryFactory); - } - else { - return new SelfPopulatingCache(cache, this.cacheEntryFactory); - } - } - if (this.blocking) { - return new BlockingCache(cache); - } - return cache; - } - - - @Override - @Nullable - public Ehcache getObject() { - return this.cache; - } - - /** - * Predict the particular {@code Ehcache} implementation that will be returned from - * {@link #getObject()} based on logic in {@link #createCache()} and - * {@link #decorateCache(Ehcache)} as orchestrated by {@link #afterPropertiesSet()}. - */ - @Override - public Class getObjectType() { - if (this.cache != null) { - return this.cache.getClass(); - } - if (this.cacheEntryFactory != null) { - if (this.cacheEntryFactory instanceof UpdatingCacheEntryFactory) { - return UpdatingSelfPopulatingCache.class; - } - else { - return SelfPopulatingCache.class; - } - } - if (this.blocking) { - return BlockingCache.class; - } - return Cache.class; - } - - @Override - public boolean isSingleton() { - return true; - } - -} diff --git a/spring-context-support/src/main/java/org/springframework/cache/ehcache/EhCacheManagerFactoryBean.java b/spring-context-support/src/main/java/org/springframework/cache/ehcache/EhCacheManagerFactoryBean.java deleted file mode 100644 index 068341965ad1..000000000000 --- a/spring-context-support/src/main/java/org/springframework/cache/ehcache/EhCacheManagerFactoryBean.java +++ /dev/null @@ -1,199 +0,0 @@ -/* - * Copyright 2002-2017 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.cache.ehcache; - -import net.sf.ehcache.CacheException; -import net.sf.ehcache.CacheManager; -import net.sf.ehcache.config.Configuration; -import net.sf.ehcache.config.ConfigurationFactory; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.beans.factory.DisposableBean; -import org.springframework.beans.factory.FactoryBean; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.core.io.Resource; -import org.springframework.lang.Nullable; - -/** - * {@link FactoryBean} that exposes an EhCache {@link net.sf.ehcache.CacheManager} - * instance (independent or shared), configured from a specified config location. - * - *

    If no config location is specified, a CacheManager will be configured from - * "ehcache.xml" in the root of the class path (that is, default EhCache initialization - * - as defined in the EhCache docs - will apply). - * - *

    Setting up a separate EhCacheManagerFactoryBean is also advisable when using - * EhCacheFactoryBean, as it provides a (by default) independent CacheManager instance - * and cares for proper shutdown of the CacheManager. EhCacheManagerFactoryBean is - * also necessary for loading EhCache configuration from a non-default config location. - * - *

    Note: As of Spring 5.0, Spring's EhCache support requires EhCache 2.10 or higher. - * - * @author Juergen Hoeller - * @author Dmitriy Kopylenko - * @since 1.1.1 - * @see #setConfigLocation - * @see #setShared - * @see EhCacheFactoryBean - * @see net.sf.ehcache.CacheManager - */ -public class EhCacheManagerFactoryBean implements FactoryBean, InitializingBean, DisposableBean { - - protected final Log logger = LogFactory.getLog(getClass()); - - @Nullable - private Resource configLocation; - - @Nullable - private String cacheManagerName; - - private boolean acceptExisting = false; - - private boolean shared = false; - - @Nullable - private CacheManager cacheManager; - - private boolean locallyManaged = true; - - - /** - * Set the location of the EhCache config file. A typical value is "/WEB-INF/ehcache.xml". - *

    Default is "ehcache.xml" in the root of the class path, or if not found, - * "ehcache-failsafe.xml" in the EhCache jar (default EhCache initialization). - * @see net.sf.ehcache.CacheManager#create(java.io.InputStream) - * @see net.sf.ehcache.CacheManager#CacheManager(java.io.InputStream) - */ - public void setConfigLocation(Resource configLocation) { - this.configLocation = configLocation; - } - - /** - * Set the name of the EhCache CacheManager (if a specific name is desired). - * @see net.sf.ehcache.config.Configuration#setName(String) - */ - public void setCacheManagerName(String cacheManagerName) { - this.cacheManagerName = cacheManagerName; - } - - /** - * Set whether an existing EhCache CacheManager of the same name will be accepted - * for this EhCacheManagerFactoryBean setup. Default is "false". - *

    Typically used in combination with {@link #setCacheManagerName "cacheManagerName"} - * but will simply work with the default CacheManager name if none specified. - * All references to the same CacheManager name (or the same default) in the - * same ClassLoader space will share the specified CacheManager then. - * @see #setCacheManagerName - * #see #setShared - * @see net.sf.ehcache.CacheManager#getCacheManager(String) - * @see net.sf.ehcache.CacheManager#CacheManager() - */ - public void setAcceptExisting(boolean acceptExisting) { - this.acceptExisting = acceptExisting; - } - - /** - * Set whether the EhCache CacheManager should be shared (as a singleton at the - * ClassLoader level) or independent (typically local within the application). - * Default is "false", creating an independent local instance. - *

    NOTE: This feature allows for sharing this EhCacheManagerFactoryBean's - * CacheManager with any code calling CacheManager.create() in the same - * ClassLoader space, with no need to agree on a specific CacheManager name. - * However, it only supports a single EhCacheManagerFactoryBean involved which will - * control the lifecycle of the underlying CacheManager (in particular, its shutdown). - *

    This flag overrides {@link #setAcceptExisting "acceptExisting"} if both are set, - * since it indicates the 'stronger' mode of sharing. - * @see #setCacheManagerName - * @see #setAcceptExisting - * @see net.sf.ehcache.CacheManager#create() - * @see net.sf.ehcache.CacheManager#CacheManager() - */ - public void setShared(boolean shared) { - this.shared = shared; - } - - - @Override - public void afterPropertiesSet() throws CacheException { - if (logger.isInfoEnabled()) { - logger.info("Initializing EhCache CacheManager" + - (this.cacheManagerName != null ? " '" + this.cacheManagerName + "'" : "")); - } - - Configuration configuration = (this.configLocation != null ? - EhCacheManagerUtils.parseConfiguration(this.configLocation) : ConfigurationFactory.parseConfiguration()); - if (this.cacheManagerName != null) { - configuration.setName(this.cacheManagerName); - } - - if (this.shared) { - // Old-school EhCache singleton sharing... - // No way to find out whether we actually created a new CacheManager - // or just received an existing singleton reference. - this.cacheManager = CacheManager.create(configuration); - } - else if (this.acceptExisting) { - // EhCache 2.5+: Reusing an existing CacheManager of the same name. - // Basically the same code as in CacheManager.getInstance(String), - // just storing whether we're dealing with an existing instance. - synchronized (CacheManager.class) { - this.cacheManager = CacheManager.getCacheManager(this.cacheManagerName); - if (this.cacheManager == null) { - this.cacheManager = new CacheManager(configuration); - } - else { - this.locallyManaged = false; - } - } - } - else { - // Throwing an exception if a CacheManager of the same name exists already... - this.cacheManager = new CacheManager(configuration); - } - } - - - @Override - @Nullable - public CacheManager getObject() { - return this.cacheManager; - } - - @Override - public Class getObjectType() { - return (this.cacheManager != null ? this.cacheManager.getClass() : CacheManager.class); - } - - @Override - public boolean isSingleton() { - return true; - } - - - @Override - public void destroy() { - if (this.cacheManager != null && this.locallyManaged) { - if (logger.isInfoEnabled()) { - logger.info("Shutting down EhCache CacheManager" + - (this.cacheManagerName != null ? " '" + this.cacheManagerName + "'" : "")); - } - this.cacheManager.shutdown(); - } - } - -} diff --git a/spring-context-support/src/main/java/org/springframework/cache/ehcache/EhCacheManagerUtils.java b/spring-context-support/src/main/java/org/springframework/cache/ehcache/EhCacheManagerUtils.java deleted file mode 100644 index 7d6654a86acd..000000000000 --- a/spring-context-support/src/main/java/org/springframework/cache/ehcache/EhCacheManagerUtils.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright 2002-2014 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.cache.ehcache; - -import java.io.IOException; -import java.io.InputStream; - -import net.sf.ehcache.CacheException; -import net.sf.ehcache.CacheManager; -import net.sf.ehcache.config.Configuration; -import net.sf.ehcache.config.ConfigurationFactory; - -import org.springframework.core.io.Resource; - -/** - * Convenient builder methods for EhCache 2.5+ {@link CacheManager} setup, - * providing easy programmatic bootstrapping from a Spring-provided resource. - * This is primarily intended for use within {@code @Bean} methods in a - * Spring configuration class. - * - *

    These methods are a simple alternative to custom {@link CacheManager} setup - * code. For any advanced purposes, consider using {@link #parseConfiguration}, - * customizing the configuration object, and then calling the - * {@link CacheManager#CacheManager(Configuration)} constructor. - * - * @author Juergen Hoeller - * @since 4.1 - */ -public abstract class EhCacheManagerUtils { - - /** - * Build an EhCache {@link CacheManager} from the default configuration. - *

    The CacheManager will be configured from "ehcache.xml" in the root of the class path - * (that is, default EhCache initialization - as defined in the EhCache docs - will apply). - * If no configuration file can be found, a fail-safe fallback configuration will be used. - * @return the new EhCache CacheManager - * @throws CacheException in case of configuration parsing failure - */ - public static CacheManager buildCacheManager() throws CacheException { - return new CacheManager(ConfigurationFactory.parseConfiguration()); - } - - /** - * Build an EhCache {@link CacheManager} from the default configuration. - *

    The CacheManager will be configured from "ehcache.xml" in the root of the class path - * (that is, default EhCache initialization - as defined in the EhCache docs - will apply). - * If no configuration file can be found, a fail-safe fallback configuration will be used. - * @param name the desired name of the cache manager - * @return the new EhCache CacheManager - * @throws CacheException in case of configuration parsing failure - */ - public static CacheManager buildCacheManager(String name) throws CacheException { - Configuration configuration = ConfigurationFactory.parseConfiguration(); - configuration.setName(name); - return new CacheManager(configuration); - } - - /** - * Build an EhCache {@link CacheManager} from the given configuration resource. - * @param configLocation the location of the configuration file (as a Spring resource) - * @return the new EhCache CacheManager - * @throws CacheException in case of configuration parsing failure - */ - public static CacheManager buildCacheManager(Resource configLocation) throws CacheException { - return new CacheManager(parseConfiguration(configLocation)); - } - - /** - * Build an EhCache {@link CacheManager} from the given configuration resource. - * @param name the desired name of the cache manager - * @param configLocation the location of the configuration file (as a Spring resource) - * @return the new EhCache CacheManager - * @throws CacheException in case of configuration parsing failure - */ - public static CacheManager buildCacheManager(String name, Resource configLocation) throws CacheException { - Configuration configuration = parseConfiguration(configLocation); - configuration.setName(name); - return new CacheManager(configuration); - } - - /** - * Parse EhCache configuration from the given resource, for further use with - * custom {@link CacheManager} creation. - * @param configLocation the location of the configuration file (as a Spring resource) - * @return the EhCache Configuration handle - * @throws CacheException in case of configuration parsing failure - * @see CacheManager#CacheManager(Configuration) - * @see CacheManager#create(Configuration) - */ - public static Configuration parseConfiguration(Resource configLocation) throws CacheException { - InputStream is = null; - try { - is = configLocation.getInputStream(); - return ConfigurationFactory.parseConfiguration(is); - } - catch (IOException ex) { - throw new CacheException("Failed to parse EhCache configuration resource", ex); - } - finally { - if (is != null) { - try { - is.close(); - } - catch (IOException ex) { - // ignore - } - } - } - } - -} diff --git a/spring-context-support/src/main/java/org/springframework/cache/ehcache/package-info.java b/spring-context-support/src/main/java/org/springframework/cache/ehcache/package-info.java deleted file mode 100644 index d786a802512f..000000000000 --- a/spring-context-support/src/main/java/org/springframework/cache/ehcache/package-info.java +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Support classes for the open source cache - * EhCache 2.x, - * allowing to set up an EhCache CacheManager and Caches - * as beans in a Spring context. - * - *

    Note: EhCache 3.x lives in a different package namespace - * and is not covered by the traditional support classes here. - * Instead, consider using it through JCache (JSR-107), with - * Spring's support in {@code org.springframework.cache.jcache}. - */ -@NonNullApi -@NonNullFields -package org.springframework.cache.ehcache; - -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/config/AbstractJCacheConfiguration.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/config/AbstractJCacheConfiguration.java index 1cd5ce612f30..4d2af4b9461b 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/config/AbstractJCacheConfiguration.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/config/AbstractJCacheConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -20,7 +20,6 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.cache.annotation.AbstractCachingConfiguration; -import org.springframework.cache.annotation.CachingConfigurer; import org.springframework.cache.interceptor.CacheResolver; import org.springframework.cache.jcache.interceptor.DefaultJCacheOperationSource; import org.springframework.cache.jcache.interceptor.JCacheOperationSource; @@ -46,11 +45,14 @@ public abstract class AbstractJCacheConfiguration extends AbstractCachingConfigu @Override - protected void useCachingConfigurer(CachingConfigurer config) { - super.useCachingConfigurer(config); - if (config instanceof JCacheConfigurer) { - this.exceptionCacheResolver = ((JCacheConfigurer) config)::exceptionCacheResolver; - } + protected void useCachingConfigurer(CachingConfigurerSupplier cachingConfigurerSupplier) { + super.useCachingConfigurer(cachingConfigurerSupplier); + this.exceptionCacheResolver = cachingConfigurerSupplier.adapt(config -> { + if (config instanceof JCacheConfigurer) { + return ((JCacheConfigurer) config).exceptionCacheResolver(); + } + return null; + }); } @Bean(name = "jCacheOperationSource") diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/config/JCacheConfigurer.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/config/JCacheConfigurer.java index 989e720aeb99..12f3d4fb7864 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/config/JCacheConfigurer.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/config/JCacheConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -26,8 +26,7 @@ *

    To be implemented by classes annotated with * {@link org.springframework.cache.annotation.EnableCaching} that wish * or need to specify explicitly how exception caches are resolved for - * annotation-driven cache management. Consider extending {@link JCacheConfigurerSupport}, - * which provides a stub implementation of all interface methods. + * annotation-driven cache management. * *

    See {@link org.springframework.cache.annotation.EnableCaching} for * general examples and context; see {@link #exceptionCacheResolver()} for @@ -36,7 +35,6 @@ * @author Stephane Nicoll * @since 4.1 * @see CachingConfigurer - * @see JCacheConfigurerSupport * @see org.springframework.cache.annotation.EnableCaching */ public interface JCacheConfigurer extends CachingConfigurer { @@ -48,7 +46,7 @@ public interface JCacheConfigurer extends CachingConfigurer { *

     	 * @Configuration
     	 * @EnableCaching
    -	 * public class AppConfig extends JCacheConfigurerSupport {
    +	 * public class AppConfig implements JCacheConfigurer {
     	 *     @Bean // important!
     	 *     @Override
     	 *     public CacheResolver exceptionCacheResolver() {
    @@ -60,6 +58,8 @@ public interface JCacheConfigurer extends CachingConfigurer {
     	 * See {@link org.springframework.cache.annotation.EnableCaching} for more complete examples.
     	 */
     	@Nullable
    -	CacheResolver exceptionCacheResolver();
    +	default CacheResolver exceptionCacheResolver() {
    +		return null;
    +	}
     
     }
    diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/config/JCacheConfigurerSupport.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/config/JCacheConfigurerSupport.java
    index e36c4fb6df94..76f4b4570266 100644
    --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/config/JCacheConfigurerSupport.java
    +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/config/JCacheConfigurerSupport.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2002-2018 the original author or authors.
    + * Copyright 2002-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.
    @@ -31,7 +31,9 @@
      * @since 4.1
      * @see JCacheConfigurer
      * @see CachingConfigurerSupport
    + * @deprecated as of 6.0 in favor of implementing {@link JCacheConfigurer} directly
      */
    +@Deprecated(since = "6.0")
     public class JCacheConfigurerSupport extends CachingConfigurerSupport implements JCacheConfigurer {
     
     	@Override
    diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractFallbackJCacheOperationSource.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractFallbackJCacheOperationSource.java
    index 3edf2a2c153a..8b20e4b14822 100644
    --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractFallbackJCacheOperationSource.java
    +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractFallbackJCacheOperationSource.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2002-2018 the original author or authors.
    + * Copyright 2002-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.
    @@ -79,7 +79,7 @@ public JCacheOperation getCacheOperation(Method method, @Nullable Class ta
     
     	@Nullable
     	private JCacheOperation computeCacheOperation(Method method, @Nullable Class targetClass) {
    -		// Don't allow no-public methods as required.
    +		// Don't allow non-public methods, as configured.
     		if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
     			return null;
     		}
    diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractJCacheOperation.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractJCacheOperation.java
    index d1771b1bd6e3..036a4f6cb587 100644
    --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractJCacheOperation.java
    +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractJCacheOperation.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2002-2019 the original author or authors.
    + * Copyright 2002-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.
    @@ -138,7 +138,7 @@ protected ExceptionTypeFilter createExceptionTypeFilter(
     
     	@Override
     	public String toString() {
    -		return getOperationDescription().append("]").toString();
    +		return getOperationDescription().append(']').toString();
     	}
     
     	/**
    @@ -148,7 +148,7 @@ public String toString() {
     	protected StringBuilder getOperationDescription() {
     		StringBuilder result = new StringBuilder();
     		result.append(getClass().getSimpleName());
    -		result.append("[");
    +		result.append('[');
     		result.append(this.methodDetails);
     		return result;
     	}
    diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheResolverAdapter.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheResolverAdapter.java
    index 88724adc12c2..041d9aea0ef7 100644
    --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheResolverAdapter.java
    +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheResolverAdapter.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2002-2018 the original author or authors.
    + * Copyright 2002-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.
    @@ -59,10 +59,9 @@ protected javax.cache.annotation.CacheResolver getTarget() {
     
     	@Override
     	public Collection resolveCaches(CacheOperationInvocationContext context) {
    -		if (!(context instanceof CacheInvocationContext)) {
    +		if (!(context instanceof CacheInvocationContext cacheInvocationContext)) {
     			throw new IllegalStateException("Unexpected context " + context);
     		}
    -		CacheInvocationContext cacheInvocationContext = (CacheInvocationContext) context;
     		javax.cache.Cache cache = this.target.resolveCache(cacheInvocationContext);
     		if (cache == null) {
     			throw new IllegalStateException("Could not resolve cache for " + context + " using " + this.target);
    diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheResultInterceptor.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheResultInterceptor.java
    index ea2f4a09eac5..cd3d91b37922 100644
    --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheResultInterceptor.java
    +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheResultInterceptor.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2002-2018 the original author or authors.
    + * Copyright 2002-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.
    @@ -150,7 +150,7 @@ private static CacheOperationInvoker.ThrowableWrapper rewriteCallStack(
     	@Nullable
     	private static  T cloneException(T exception) {
     		try {
    -			return (T) SerializationUtils.deserialize(SerializationUtils.serialize(exception));
    +			return SerializationUtils.clone(exception);
     		}
     		catch (Exception ex) {
     			return null;  // exception parameter cannot be cloned
    diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultJCacheOperationSource.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultJCacheOperationSource.java
    index c323cf449508..4bde292d8fa5 100644
    --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultJCacheOperationSource.java
    +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultJCacheOperationSource.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2002-2018 the original author or authors.
    + * Copyright 2002-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.
    @@ -166,7 +166,7 @@ public void setBeanFactory(BeanFactory beanFactory) {
     	public void afterSingletonsInstantiated() {
     		// Make sure that the cache resolver is initialized. An exception cache resolver is only
     		// required if the exceptionCacheName attribute is set on an operation.
    -		Assert.notNull(getDefaultCacheResolver(), "Cache resolver should have been initialized");
    +		Assert.state(getDefaultCacheResolver() != null, "Cache resolver should have been initialized");
     	}
     
     
    @@ -235,7 +235,7 @@ protected KeyGenerator getDefaultKeyGenerator() {
     	 * {@code CacheResolver} from a custom {@code CacheResolver} implementation so we have to
     	 * fall back on the {@code CacheManager}.
     	 * 

    This gives this weird situation of a perfectly valid configuration that breaks all - * the sudden because the JCache support is enabled. To avoid this we resolve the default + * of a sudden because the JCache support is enabled. To avoid this we resolve the default * exception {@code CacheResolver} as late as possible to avoid such hard requirement * in other cases. */ diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSourcePointcut.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSourcePointcut.java index 4b6d7e0fb940..1dbb4210894c 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSourcePointcut.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSourcePointcut.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -52,10 +52,9 @@ public boolean equals(@Nullable Object other) { if (this == other) { return true; } - if (!(other instanceof JCacheOperationSourcePointcut)) { + if (!(other instanceof JCacheOperationSourcePointcut otherPc)) { return false; } - JCacheOperationSourcePointcut otherPc = (JCacheOperationSourcePointcut) other; return ObjectUtils.nullSafeEquals(getCacheOperationSource(), otherPc.getCacheOperationSource()); } diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/SimpleExceptionCacheResolver.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/SimpleExceptionCacheResolver.java index f2091ab1fb31..1aa569546131 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/SimpleExceptionCacheResolver.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/SimpleExceptionCacheResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -43,10 +43,9 @@ public SimpleExceptionCacheResolver(CacheManager cacheManager) { @Override protected Collection getCacheNames(CacheOperationInvocationContext context) { BasicOperation operation = context.getOperation(); - if (!(operation instanceof CacheResultOperation)) { + if (!(operation instanceof CacheResultOperation cacheResultOperation)) { throw new IllegalStateException("Could not extract exception cache name from " + operation); } - CacheResultOperation cacheResultOperation = (CacheResultOperation) operation; String exceptionCacheName = cacheResultOperation.getExceptionCacheName(); if (exceptionCacheName != null) { return Collections.singleton(exceptionCacheName); diff --git a/spring-context-support/src/main/java/org/springframework/cache/transaction/TransactionAwareCacheDecorator.java b/spring-context-support/src/main/java/org/springframework/cache/transaction/TransactionAwareCacheDecorator.java index 22fcaffd5f1c..33571ffc905b 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/transaction/TransactionAwareCacheDecorator.java +++ b/spring-context-support/src/main/java/org/springframework/cache/transaction/TransactionAwareCacheDecorator.java @@ -27,7 +27,7 @@ /** * Cache decorator which synchronizes its {@link #put}, {@link #evict} and * {@link #clear} operations with Spring-managed transactions (through Spring's - * {@link TransactionSynchronizationManager}, performing the actual cache + * {@link TransactionSynchronizationManager}), performing the actual cache * put/evict/clear operation only in the after-commit phase of a successful * transaction. If no transaction is active, {@link #put}, {@link #evict} and * {@link #clear} operations will be performed immediately, as usual. diff --git a/spring-context-support/src/main/java/org/springframework/cache/transaction/TransactionAwareCacheManagerProxy.java b/spring-context-support/src/main/java/org/springframework/cache/transaction/TransactionAwareCacheManagerProxy.java index d6c51895f7c7..faf432e3891a 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/transaction/TransactionAwareCacheManagerProxy.java +++ b/spring-context-support/src/main/java/org/springframework/cache/transaction/TransactionAwareCacheManagerProxy.java @@ -27,7 +27,7 @@ /** * Proxy for a target {@link CacheManager}, exposing transaction-aware {@link Cache} objects * which synchronize their {@link Cache#put} operations with Spring-managed transactions - * (through Spring's {@link org.springframework.transaction.support.TransactionSynchronizationManager}, + * (through Spring's {@link org.springframework.transaction.support.TransactionSynchronizationManager}), * performing the actual cache put operation only in the after-commit phase of a successful transaction. * If no transaction is active, {@link Cache#put} operations will be performed immediately, as usual. * diff --git a/spring-context-support/src/main/java/org/springframework/mail/MailSendException.java b/spring-context-support/src/main/java/org/springframework/mail/MailSendException.java index 693082c840c2..af7f0417f29b 100644 --- a/spring-context-support/src/main/java/org/springframework/mail/MailSendException.java +++ b/spring-context-support/src/main/java/org/springframework/mail/MailSendException.java @@ -104,7 +104,7 @@ public MailSendException(Map failedMessages) { * be available after serialization as well. * @return the Map of failed messages as keys and thrown exceptions as values * @see SimpleMailMessage - * @see javax.mail.internet.MimeMessage + * @see jakarta.mail.internet.MimeMessage */ public final Map getFailedMessages() { return this.failedMessages; diff --git a/spring-context-support/src/main/java/org/springframework/mail/SimpleMailMessage.java b/spring-context-support/src/main/java/org/springframework/mail/SimpleMailMessage.java index 95268a2993e0..990b692a7873 100644 --- a/spring-context-support/src/main/java/org/springframework/mail/SimpleMailMessage.java +++ b/spring-context-support/src/main/java/org/springframework/mail/SimpleMailMessage.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -93,7 +93,7 @@ public SimpleMailMessage(SimpleMailMessage original) { @Override - public void setFrom(String from) { + public void setFrom(@Nullable String from) { this.from = from; } @@ -103,7 +103,7 @@ public String getFrom() { } @Override - public void setReplyTo(String replyTo) { + public void setReplyTo(@Nullable String replyTo) { this.replyTo = replyTo; } @@ -113,7 +113,7 @@ public String getReplyTo() { } @Override - public void setTo(String to) { + public void setTo(@Nullable String to) { this.to = new String[] {to}; } @@ -128,12 +128,12 @@ public String[] getTo() { } @Override - public void setCc(String cc) { + public void setCc(@Nullable String cc) { this.cc = new String[] {cc}; } @Override - public void setCc(String... cc) { + public void setCc(@Nullable String... cc) { this.cc = cc; } @@ -143,12 +143,12 @@ public String[] getCc() { } @Override - public void setBcc(String bcc) { + public void setBcc(@Nullable String bcc) { this.bcc = new String[] {bcc}; } @Override - public void setBcc(String... bcc) { + public void setBcc(@Nullable String... bcc) { this.bcc = bcc; } @@ -158,7 +158,7 @@ public String[] getBcc() { } @Override - public void setSentDate(Date sentDate) { + public void setSentDate(@Nullable Date sentDate) { this.sentDate = sentDate; } @@ -168,7 +168,7 @@ public Date getSentDate() { } @Override - public void setSubject(String subject) { + public void setSubject(@Nullable String subject) { this.subject = subject; } @@ -178,7 +178,7 @@ public String getSubject() { } @Override - public void setText(String text) { + public void setText(@Nullable String text) { this.text = text; } @@ -226,10 +226,9 @@ public boolean equals(@Nullable Object other) { if (this == other) { return true; } - if (!(other instanceof SimpleMailMessage)) { + if (!(other instanceof SimpleMailMessage otherMessage)) { return false; } - SimpleMailMessage otherMessage = (SimpleMailMessage) other; return (ObjectUtils.nullSafeEquals(this.from, otherMessage.from) && ObjectUtils.nullSafeEquals(this.replyTo, otherMessage.replyTo) && ObjectUtils.nullSafeEquals(this.to, otherMessage.to) && diff --git a/spring-context-support/src/main/java/org/springframework/mail/javamail/ConfigurableMimeFileTypeMap.java b/spring-context-support/src/main/java/org/springframework/mail/javamail/ConfigurableMimeFileTypeMap.java index ff6921b37183..7fe0a9aca896 100644 --- a/spring-context-support/src/main/java/org/springframework/mail/javamail/ConfigurableMimeFileTypeMap.java +++ b/spring-context-support/src/main/java/org/springframework/mail/javamail/ConfigurableMimeFileTypeMap.java @@ -20,8 +20,8 @@ import java.io.IOException; import java.io.InputStream; -import javax.activation.FileTypeMap; -import javax.activation.MimetypesFileTypeMap; +import jakarta.activation.FileTypeMap; +import jakarta.activation.MimetypesFileTypeMap; import org.springframework.beans.factory.InitializingBean; import org.springframework.core.io.ClassPathResource; @@ -58,7 +58,7 @@ * @since 1.2 * @see #setMappingLocation * @see #setMappings - * @see javax.activation.MimetypesFileTypeMap + * @see jakarta.activation.MimetypesFileTypeMap */ public class ConfigurableMimeFileTypeMap extends FileTypeMap implements InitializingBean { @@ -140,8 +140,8 @@ protected final FileTypeMap getFileTypeMap() { * @param mappings an array of MIME type mapping lines (can be {@code null}) * @return the compiled FileTypeMap * @throws IOException if resource access failed - * @see javax.activation.MimetypesFileTypeMap#MimetypesFileTypeMap(java.io.InputStream) - * @see javax.activation.MimetypesFileTypeMap#addMimeTypes(String) + * @see jakarta.activation.MimetypesFileTypeMap#MimetypesFileTypeMap(java.io.InputStream) + * @see jakarta.activation.MimetypesFileTypeMap#addMimeTypes(String) */ protected FileTypeMap createFileTypeMap(@Nullable Resource mappingLocation, @Nullable String[] mappings) throws IOException { MimetypesFileTypeMap fileTypeMap = null; diff --git a/spring-context-support/src/main/java/org/springframework/mail/javamail/InternetAddressEditor.java b/spring-context-support/src/main/java/org/springframework/mail/javamail/InternetAddressEditor.java index 7738328c563f..c4981581ebd3 100644 --- a/spring-context-support/src/main/java/org/springframework/mail/javamail/InternetAddressEditor.java +++ b/spring-context-support/src/main/java/org/springframework/mail/javamail/InternetAddressEditor.java @@ -18,8 +18,8 @@ import java.beans.PropertyEditorSupport; -import javax.mail.internet.AddressException; -import javax.mail.internet.InternetAddress; +import jakarta.mail.internet.AddressException; +import jakarta.mail.internet.InternetAddress; import org.springframework.util.StringUtils; @@ -32,7 +32,7 @@ * * @author Juergen Hoeller * @since 1.2.3 - * @see javax.mail.internet.InternetAddress + * @see jakarta.mail.internet.InternetAddress */ public class InternetAddressEditor extends PropertyEditorSupport { diff --git a/spring-context-support/src/main/java/org/springframework/mail/javamail/JavaMailMimeTypesRuntimeHints.java b/spring-context-support/src/main/java/org/springframework/mail/javamail/JavaMailMimeTypesRuntimeHints.java new file mode 100644 index 000000000000..165009856082 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/mail/javamail/JavaMailMimeTypesRuntimeHints.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-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.mail.javamail; + +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.lang.Nullable; + +/** + * {@link RuntimeHintsRegistrar} implementation that makes sure mime types + * are available in constrained environments. + * + * @author Sebastien Deleuze + * @since 6.0 + */ +class JavaMailMimeTypesRuntimeHints implements RuntimeHintsRegistrar { + + @Override + public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { + hints.resources().registerPattern("org/springframework/mail/javamail/mime.types"); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/mail/javamail/JavaMailSender.java b/spring-context-support/src/main/java/org/springframework/mail/javamail/JavaMailSender.java index f810be606dd8..61f4ecb01d77 100644 --- a/spring-context-support/src/main/java/org/springframework/mail/javamail/JavaMailSender.java +++ b/spring-context-support/src/main/java/org/springframework/mail/javamail/JavaMailSender.java @@ -18,7 +18,7 @@ import java.io.InputStream; -import javax.mail.internet.MimeMessage; +import jakarta.mail.internet.MimeMessage; import org.springframework.mail.MailException; import org.springframework.mail.MailSender; @@ -40,7 +40,7 @@ * mechanism, possibly using a {@link MimeMessageHelper} for populating the message. * See {@link MimeMessageHelper MimeMessageHelper's javadoc} for an example. * - *

    The entire JavaMail {@link javax.mail.Session} management is abstracted + *

    The entire JavaMail {@link jakarta.mail.Session} management is abstracted * by the JavaMailSender. Client code should not deal with a Session in any way, * rather leave the entire JavaMail configuration and resource handling to the * JavaMailSender implementation. This also increases testability. @@ -54,8 +54,8 @@ * * @author Juergen Hoeller * @since 07.10.2003 - * @see javax.mail.internet.MimeMessage - * @see javax.mail.Session + * @see jakarta.mail.internet.MimeMessage + * @see jakarta.mail.Session * @see JavaMailSenderImpl * @see MimeMessagePreparator * @see MimeMessageHelper diff --git a/spring-context-support/src/main/java/org/springframework/mail/javamail/JavaMailSenderImpl.java b/spring-context-support/src/main/java/org/springframework/mail/javamail/JavaMailSenderImpl.java index 9286d0ef665a..42509c3098b9 100644 --- a/spring-context-support/src/main/java/org/springframework/mail/javamail/JavaMailSenderImpl.java +++ b/spring-context-support/src/main/java/org/springframework/mail/javamail/JavaMailSenderImpl.java @@ -24,14 +24,14 @@ import java.util.Map; import java.util.Properties; -import javax.activation.FileTypeMap; -import javax.mail.Address; -import javax.mail.AuthenticationFailedException; -import javax.mail.MessagingException; -import javax.mail.NoSuchProviderException; -import javax.mail.Session; -import javax.mail.Transport; -import javax.mail.internet.MimeMessage; +import jakarta.activation.FileTypeMap; +import jakarta.mail.Address; +import jakarta.mail.AuthenticationFailedException; +import jakarta.mail.MessagingException; +import jakarta.mail.NoSuchProviderException; +import jakarta.mail.Session; +import jakarta.mail.Transport; +import jakarta.mail.internet.MimeMessage; import org.springframework.lang.Nullable; import org.springframework.mail.MailAuthenticationException; @@ -49,7 +49,7 @@ * plain {@link org.springframework.mail.MailSender} implementation. * *

    Allows for defining all settings locally as bean properties. - * Alternatively, a pre-configured JavaMail {@link javax.mail.Session} can be + * Alternatively, a pre-configured JavaMail {@link jakarta.mail.Session} can be * specified, possibly pulled from an application server's JNDI environment. * *

    Non-default properties in this object will always override the settings @@ -59,8 +59,8 @@ * @author Dmitriy Kopylenko * @author Juergen Hoeller * @since 10.09.2003 - * @see javax.mail.internet.MimeMessage - * @see javax.mail.Session + * @see jakarta.mail.internet.MimeMessage + * @see jakarta.mail.Session * @see #setSession * @see #setJavaMailProperties * @see #setHost @@ -132,10 +132,10 @@ public void setJavaMailProperties(Properties javaMailProperties) { } /** - * Allow Map access to the JavaMail properties of this sender, + * Allow {code Map} access to the JavaMail properties of this sender, * with the option to add or override specific entries. *

    Useful for specifying entries directly, for example via - * "javaMailProperties[mail.smtp.auth]". + * {code javaMailProperties[mail.smtp.auth]}. */ public Properties getJavaMailProperties() { return this.javaMailProperties; @@ -156,7 +156,7 @@ public synchronized void setSession(Session session) { /** * Return the JavaMail {@code Session}, - * lazily initializing it if hasn't been specified explicitly. + * lazily initializing it if it hasn't been specified explicitly. */ public synchronized Session getSession() { if (this.session == null) { @@ -523,7 +523,7 @@ protected Transport connectTransport() throws MessagingException { * Obtain a Transport object from the given JavaMail Session, * using the configured protocol. *

    Can be overridden in subclasses, e.g. to return a mock Transport object. - * @see javax.mail.Session#getTransport(String) + * @see jakarta.mail.Session#getTransport(String) * @see #getSession() * @see #getProtocol() */ diff --git a/spring-context-support/src/main/java/org/springframework/mail/javamail/MimeMailMessage.java b/spring-context-support/src/main/java/org/springframework/mail/javamail/MimeMailMessage.java index e62690a6d763..9161ea69d7ae 100644 --- a/spring-context-support/src/main/java/org/springframework/mail/javamail/MimeMailMessage.java +++ b/spring-context-support/src/main/java/org/springframework/mail/javamail/MimeMailMessage.java @@ -18,8 +18,8 @@ import java.util.Date; -import javax.mail.MessagingException; -import javax.mail.internet.MimeMessage; +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; import org.springframework.mail.MailMessage; import org.springframework.mail.MailParseException; @@ -35,7 +35,7 @@ * @author Juergen Hoeller * @since 1.1.5 * @see MimeMessageHelper - * @see javax.mail.internet.MimeMessage + * @see jakarta.mail.internet.MimeMessage */ public class MimeMailMessage implements MailMessage { diff --git a/spring-context-support/src/main/java/org/springframework/mail/javamail/MimeMessageHelper.java b/spring-context-support/src/main/java/org/springframework/mail/javamail/MimeMessageHelper.java index f795390fa24d..84400943ced9 100644 --- a/spring-context-support/src/main/java/org/springframework/mail/javamail/MimeMessageHelper.java +++ b/spring-context-support/src/main/java/org/springframework/mail/javamail/MimeMessageHelper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -23,20 +23,21 @@ import java.io.UnsupportedEncodingException; import java.util.Date; -import javax.activation.DataHandler; -import javax.activation.DataSource; -import javax.activation.FileDataSource; -import javax.activation.FileTypeMap; -import javax.mail.BodyPart; -import javax.mail.Message; -import javax.mail.MessagingException; -import javax.mail.internet.AddressException; -import javax.mail.internet.InternetAddress; -import javax.mail.internet.MimeBodyPart; -import javax.mail.internet.MimeMessage; -import javax.mail.internet.MimeMultipart; -import javax.mail.internet.MimePart; -import javax.mail.internet.MimeUtility; +import jakarta.activation.DataHandler; +import jakarta.activation.DataSource; +import jakarta.activation.FileDataSource; +import jakarta.activation.FileTypeMap; +import jakarta.mail.BodyPart; +import jakarta.mail.Message; +import jakarta.mail.MessagingException; +import jakarta.mail.Part; +import jakarta.mail.internet.AddressException; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.MimeBodyPart; +import jakarta.mail.internet.MimeMessage; +import jakarta.mail.internet.MimeMultipart; +import jakarta.mail.internet.MimePart; +import jakarta.mail.internet.MimeUtility; import org.springframework.core.io.InputStreamSource; import org.springframework.core.io.Resource; @@ -44,7 +45,7 @@ import org.springframework.util.Assert; /** - * Helper class for populating a {@link javax.mail.internet.MimeMessage}. + * Helper class for populating a {@link jakarta.mail.internet.MimeMessage}. * *

    Mirrors the simple setters of {@link org.springframework.mail.SimpleMailMessage}, * directly applying the values to the underlying MimeMessage. Allows for defining @@ -91,6 +92,7 @@ * on the MULTIPART_MODE constants contains more detailed information. * * @author Juergen Hoeller + * @author Sam Brannen * @since 19.01.2004 * @see #setText(String, boolean) * @see #setText(String, String) @@ -186,8 +188,8 @@ public class MimeMessageHelper { * the passed-in MimeMessage object, if carried there. Else, * JavaMail's default encoding will be used. * @param mimeMessage the mime message to work on - * @see #MimeMessageHelper(javax.mail.internet.MimeMessage, boolean) - * @see #getDefaultEncoding(javax.mail.internet.MimeMessage) + * @see #MimeMessageHelper(jakarta.mail.internet.MimeMessage, boolean) + * @see #getDefaultEncoding(jakarta.mail.internet.MimeMessage) * @see JavaMailSenderImpl#setDefaultEncoding */ public MimeMessageHelper(MimeMessage mimeMessage) { @@ -200,7 +202,7 @@ public MimeMessageHelper(MimeMessage mimeMessage) { * i.e. no alternative texts and no inline elements or attachments). * @param mimeMessage the mime message to work on * @param encoding the character encoding to use for the message - * @see #MimeMessageHelper(javax.mail.internet.MimeMessage, boolean) + * @see #MimeMessageHelper(jakarta.mail.internet.MimeMessage, boolean) */ public MimeMessageHelper(MimeMessage mimeMessage, @Nullable String encoding) { this.mimeMessage = mimeMessage; @@ -223,8 +225,8 @@ public MimeMessageHelper(MimeMessage mimeMessage, @Nullable String encoding) { * supports alternative texts, inline elements and attachments * (corresponds to MULTIPART_MODE_MIXED_RELATED) * @throws MessagingException if multipart creation failed - * @see #MimeMessageHelper(javax.mail.internet.MimeMessage, int) - * @see #getDefaultEncoding(javax.mail.internet.MimeMessage) + * @see #MimeMessageHelper(jakarta.mail.internet.MimeMessage, int) + * @see #getDefaultEncoding(jakarta.mail.internet.MimeMessage) * @see JavaMailSenderImpl#setDefaultEncoding */ public MimeMessageHelper(MimeMessage mimeMessage, boolean multipart) throws MessagingException { @@ -244,7 +246,7 @@ public MimeMessageHelper(MimeMessage mimeMessage, boolean multipart) throws Mess * (corresponds to MULTIPART_MODE_MIXED_RELATED) * @param encoding the character encoding to use for the message * @throws MessagingException if multipart creation failed - * @see #MimeMessageHelper(javax.mail.internet.MimeMessage, int, String) + * @see #MimeMessageHelper(jakarta.mail.internet.MimeMessage, int, String) */ public MimeMessageHelper(MimeMessage mimeMessage, boolean multipart, @Nullable String encoding) throws MessagingException { @@ -267,7 +269,7 @@ public MimeMessageHelper(MimeMessage mimeMessage, boolean multipart, @Nullable S * @see #MULTIPART_MODE_MIXED * @see #MULTIPART_MODE_RELATED * @see #MULTIPART_MODE_MIXED_RELATED - * @see #getDefaultEncoding(javax.mail.internet.MimeMessage) + * @see #getDefaultEncoding(jakarta.mail.internet.MimeMessage) * @see JavaMailSenderImpl#setDefaultEncoding */ public MimeMessageHelper(MimeMessage mimeMessage, int multipartMode) throws MessagingException { @@ -331,20 +333,18 @@ public final MimeMessage getMimeMessage() { */ protected void createMimeMultiparts(MimeMessage mimeMessage, int multipartMode) throws MessagingException { switch (multipartMode) { - case MULTIPART_MODE_NO: - setMimeMultiparts(null, null); - break; - case MULTIPART_MODE_MIXED: + case MULTIPART_MODE_NO -> setMimeMultiparts(null, null); + case MULTIPART_MODE_MIXED -> { MimeMultipart mixedMultipart = new MimeMultipart(MULTIPART_SUBTYPE_MIXED); mimeMessage.setContent(mixedMultipart); setMimeMultiparts(mixedMultipart, mixedMultipart); - break; - case MULTIPART_MODE_RELATED: + } + case MULTIPART_MODE_RELATED -> { MimeMultipart relatedMultipart = new MimeMultipart(MULTIPART_SUBTYPE_RELATED); mimeMessage.setContent(relatedMultipart); setMimeMultiparts(relatedMultipart, relatedMultipart); - break; - case MULTIPART_MODE_MIXED_RELATED: + } + case MULTIPART_MODE_MIXED_RELATED -> { MimeMultipart rootMixedMultipart = new MimeMultipart(MULTIPART_SUBTYPE_MIXED); mimeMessage.setContent(rootMixedMultipart); MimeMultipart nestedRelatedMultipart = new MimeMultipart(MULTIPART_SUBTYPE_RELATED); @@ -352,8 +352,8 @@ protected void createMimeMultiparts(MimeMessage mimeMessage, int multipartMode) relatedBodyPart.setContent(nestedRelatedMultipart); rootMixedMultipart.addBodyPart(relatedBodyPart); setMimeMultiparts(rootMixedMultipart, nestedRelatedMultipart); - break; - default: + } + default -> throw new IllegalArgumentException("Only multipart modes MIXED_RELATED, RELATED and NO supported"); } } @@ -388,7 +388,7 @@ public final boolean isMultipart() { * @throws IllegalStateException if this helper is not in multipart mode * @see #isMultipart * @see #getMimeMessage - * @see javax.mail.internet.MimeMultipart#addBodyPart + * @see jakarta.mail.internet.MimeMultipart#addBodyPart */ public final MimeMultipart getRootMimeMultipart() throws IllegalStateException { if (this.rootMimeMultipart == null) { @@ -407,7 +407,7 @@ public final MimeMultipart getRootMimeMultipart() throws IllegalStateException { * @throws IllegalStateException if this helper is not in multipart mode * @see #isMultipart * @see #getRootMimeMultipart - * @see javax.mail.internet.MimeMultipart#addBodyPart + * @see jakarta.mail.internet.MimeMultipart#addBodyPart */ public final MimeMultipart getMimeMultipart() throws IllegalStateException { if (this.mimeMultipart == null) { @@ -427,8 +427,8 @@ public final MimeMultipart getMimeMultipart() throws IllegalStateException { */ @Nullable protected String getDefaultEncoding(MimeMessage mimeMessage) { - if (mimeMessage instanceof SmartMimeMessage) { - return ((SmartMimeMessage) mimeMessage).getDefaultEncoding(); + if (mimeMessage instanceof SmartMimeMessage smartMimeMessage) { + return smartMimeMessage.getDefaultEncoding(); } return null; } @@ -449,8 +449,8 @@ public String getEncoding() { * @see ConfigurableMimeFileTypeMap */ protected FileTypeMap getDefaultFileTypeMap(MimeMessage mimeMessage) { - if (mimeMessage instanceof SmartMimeMessage) { - FileTypeMap fileTypeMap = ((SmartMimeMessage) mimeMessage).getDefaultFileTypeMap(); + if (mimeMessage instanceof SmartMimeMessage smartMimeMessage) { + FileTypeMap fileTypeMap = smartMimeMessage.getDefaultFileTypeMap(); if (fileTypeMap != null) { return fileTypeMap; } @@ -469,9 +469,9 @@ protected FileTypeMap getDefaultFileTypeMap(MimeMessage mimeMessage) { * {@code FileTypeMap} instance else. * @see #addInline * @see #addAttachment - * @see #getDefaultFileTypeMap(javax.mail.internet.MimeMessage) + * @see #getDefaultFileTypeMap(jakarta.mail.internet.MimeMessage) * @see JavaMailSenderImpl#setDefaultFileTypeMap - * @see javax.activation.FileTypeMap#getDefaultFileTypeMap + * @see jakarta.activation.FileTypeMap#getDefaultFileTypeMap * @see ConfigurableMimeFileTypeMap */ public void setFileTypeMap(@Nullable FileTypeMap fileTypeMap) { @@ -538,7 +538,7 @@ public boolean isValidateAddresses() { * @param address the address to validate * @throws AddressException if validation failed * @see #isValidateAddresses() - * @see javax.mail.internet.InternetAddress#validate() + * @see jakarta.mail.internet.InternetAddress#validate() */ protected void validateAddress(InternetAddress address) throws AddressException { if (isValidateAddresses()) { @@ -889,16 +889,16 @@ private void setHtmlTextToMimePart(MimePart mimePart, String text) throws Messag /** * Add an inline element to the MimeMessage, taking the content from a - * {@code javax.activation.DataSource}. + * {@code jakarta.activation.DataSource}. *

    Note that the InputStream returned by the DataSource implementation * needs to be a fresh one on each call, as JavaMail will invoke * {@code getInputStream()} multiple times. *

    NOTE: Invoke {@code addInline} after {@link #setText}; * else, mail readers might not be able to resolve inline references correctly. * @param contentId the content ID to use. Will end up as "Content-ID" header - * in the body part, surrounded by angle brackets: e.g. "myId" -> "<myId>". + * in the body part, surrounded by angle brackets: e.g. "myId" → "<myId>". * Can be referenced in HTML source via src="/service/cid:myId" expressions. - * @param dataSource the {@code javax.activation.DataSource} to take + * @param dataSource the {@code jakarta.activation.DataSource} to take * the content from, determining the InputStream and the content type * @throws MessagingException in case of errors * @see #addInline(String, java.io.File) @@ -908,7 +908,7 @@ public void addInline(String contentId, DataSource dataSource) throws MessagingE Assert.notNull(contentId, "Content ID must not be null"); Assert.notNull(dataSource, "DataSource must not be null"); MimeBodyPart mimeBodyPart = new MimeBodyPart(); - mimeBodyPart.setDisposition(MimeBodyPart.INLINE); + mimeBodyPart.setDisposition(Part.INLINE); mimeBodyPart.setContentID("<" + contentId + ">"); mimeBodyPart.setDataHandler(new DataHandler(dataSource)); getMimeMultipart().addBodyPart(mimeBodyPart); @@ -923,13 +923,13 @@ public void addInline(String contentId, DataSource dataSource) throws MessagingE *

    NOTE: Invoke {@code addInline} after {@link #setText}; * else, mail readers might not be able to resolve inline references correctly. * @param contentId the content ID to use. Will end up as "Content-ID" header - * in the body part, surrounded by angle brackets: e.g. "myId" -> "<myId>". + * in the body part, surrounded by angle brackets: e.g. "myId" → "<myId>". * Can be referenced in HTML source via src="/service/cid:myId" expressions. * @param file the File resource to take the content from * @throws MessagingException in case of errors * @see #setText * @see #addInline(String, org.springframework.core.io.Resource) - * @see #addInline(String, javax.activation.DataSource) + * @see #addInline(String, jakarta.activation.DataSource) */ public void addInline(String contentId, File file) throws MessagingException { Assert.notNull(file, "File must not be null"); @@ -950,13 +950,13 @@ public void addInline(String contentId, File file) throws MessagingException { *

    NOTE: Invoke {@code addInline} after {@link #setText}; * else, mail readers might not be able to resolve inline references correctly. * @param contentId the content ID to use. Will end up as "Content-ID" header - * in the body part, surrounded by angle brackets: e.g. "myId" -> "<myId>". + * in the body part, surrounded by angle brackets: e.g. "myId" → "<myId>". * Can be referenced in HTML source via src="/service/cid:myId" expressions. * @param resource the resource to take the content from * @throws MessagingException in case of errors * @see #setText * @see #addInline(String, java.io.File) - * @see #addInline(String, javax.activation.DataSource) + * @see #addInline(String, jakarta.activation.DataSource) */ public void addInline(String contentId, Resource resource) throws MessagingException { Assert.notNull(resource, "Resource must not be null"); @@ -976,7 +976,7 @@ public void addInline(String contentId, Resource resource) throws MessagingExcep *

    NOTE: Invoke {@code addInline} after {@code setText}; * else, mail readers might not be able to resolve inline references correctly. * @param contentId the content ID to use. Will end up as "Content-ID" header - * in the body part, surrounded by angle brackets: e.g. "myId" -> "<myId>". + * in the body part, surrounded by angle brackets: e.g. "myId" → "<myId>". * Can be referenced in HTML source via src="/service/cid:myId" expressions. * @param inputStreamSource the resource to take the content from * @param contentType the content type to use for the element @@ -984,13 +984,13 @@ public void addInline(String contentId, Resource resource) throws MessagingExcep * @see #setText * @see #getFileTypeMap * @see #addInline(String, org.springframework.core.io.Resource) - * @see #addInline(String, javax.activation.DataSource) + * @see #addInline(String, jakarta.activation.DataSource) */ public void addInline(String contentId, InputStreamSource inputStreamSource, String contentType) throws MessagingException { Assert.notNull(inputStreamSource, "InputStreamSource must not be null"); - if (inputStreamSource instanceof Resource && ((Resource) inputStreamSource).isOpen()) { + if (inputStreamSource instanceof Resource resource && resource.isOpen()) { throw new IllegalArgumentException( "Passed-in Resource contains an open stream: invalid argument. " + "JavaMail requires an InputStreamSource that creates a fresh stream for every call."); @@ -1001,13 +1001,13 @@ public void addInline(String contentId, InputStreamSource inputStreamSource, Str /** * Add an attachment to the MimeMessage, taking the content from a - * {@code javax.activation.DataSource}. + * {@code jakarta.activation.DataSource}. *

    Note that the InputStream returned by the DataSource implementation * needs to be a fresh one on each call, as JavaMail will invoke * {@code getInputStream()} multiple times. * @param attachmentFilename the name of the attachment as it will * appear in the mail (the content type will be determined by this) - * @param dataSource the {@code javax.activation.DataSource} to take + * @param dataSource the {@code jakarta.activation.DataSource} to take * the content from, determining the InputStream and the content type * @throws MessagingException in case of errors * @see #addAttachment(String, org.springframework.core.io.InputStreamSource) @@ -1018,7 +1018,7 @@ public void addAttachment(String attachmentFilename, DataSource dataSource) thro Assert.notNull(dataSource, "DataSource must not be null"); try { MimeBodyPart mimeBodyPart = new MimeBodyPart(); - mimeBodyPart.setDisposition(MimeBodyPart.ATTACHMENT); + mimeBodyPart.setDisposition(Part.ATTACHMENT); mimeBodyPart.setFileName(isEncodeFilenames() ? MimeUtility.encodeText(attachmentFilename) : attachmentFilename); mimeBodyPart.setDataHandler(new DataHandler(dataSource)); @@ -1040,7 +1040,7 @@ public void addAttachment(String attachmentFilename, DataSource dataSource) thro * @param file the File resource to take the content from * @throws MessagingException in case of errors * @see #addAttachment(String, org.springframework.core.io.InputStreamSource) - * @see #addAttachment(String, javax.activation.DataSource) + * @see #addAttachment(String, jakarta.activation.DataSource) */ public void addAttachment(String attachmentFilename, File file) throws MessagingException { Assert.notNull(file, "File must not be null"); @@ -1064,7 +1064,7 @@ public void addAttachment(String attachmentFilename, File file) throws Messaging * (all of Spring's Resource implementations can be passed in here) * @throws MessagingException in case of errors * @see #addAttachment(String, java.io.File) - * @see #addAttachment(String, javax.activation.DataSource) + * @see #addAttachment(String, jakarta.activation.DataSource) * @see org.springframework.core.io.Resource */ public void addAttachment(String attachmentFilename, InputStreamSource inputStreamSource) @@ -1087,7 +1087,7 @@ public void addAttachment(String attachmentFilename, InputStreamSource inputStre * @param contentType the content type to use for the element * @throws MessagingException in case of errors * @see #addAttachment(String, java.io.File) - * @see #addAttachment(String, javax.activation.DataSource) + * @see #addAttachment(String, jakarta.activation.DataSource) * @see org.springframework.core.io.Resource */ public void addAttachment( @@ -1095,7 +1095,7 @@ public void addAttachment( throws MessagingException { Assert.notNull(inputStreamSource, "InputStreamSource must not be null"); - if (inputStreamSource instanceof Resource && ((Resource) inputStreamSource).isOpen()) { + if (inputStreamSource instanceof Resource resource && resource.isOpen()) { throw new IllegalArgumentException( "Passed-in Resource contains an open stream: invalid argument. " + "JavaMail requires an InputStreamSource that creates a fresh stream for every call."); @@ -1121,7 +1121,7 @@ public InputStream getInputStream() throws IOException { } @Override public OutputStream getOutputStream() { - throw new UnsupportedOperationException("Read-only javax.activation.DataSource"); + throw new UnsupportedOperationException("Read-only jakarta.activation.DataSource"); } @Override public String getContentType() { diff --git a/spring-context-support/src/main/java/org/springframework/mail/javamail/MimeMessagePreparator.java b/spring-context-support/src/main/java/org/springframework/mail/javamail/MimeMessagePreparator.java index 5973d17104b6..04e6cc34cd29 100644 --- a/spring-context-support/src/main/java/org/springframework/mail/javamail/MimeMessagePreparator.java +++ b/spring-context-support/src/main/java/org/springframework/mail/javamail/MimeMessagePreparator.java @@ -16,7 +16,7 @@ package org.springframework.mail.javamail; -import javax.mail.internet.MimeMessage; +import jakarta.mail.internet.MimeMessage; /** * Callback interface for the preparation of JavaMail MIME messages. @@ -42,7 +42,7 @@ public interface MimeMessagePreparator { /** * Prepare the given new MimeMessage instance. * @param mimeMessage the message to prepare - * @throws javax.mail.MessagingException passing any exceptions thrown by MimeMessage + * @throws jakarta.mail.MessagingException passing any exceptions thrown by MimeMessage * methods through for automatic conversion to the MailException hierarchy * @throws java.io.IOException passing any exceptions thrown by MimeMessage methods * through for automatic conversion to the MailException hierarchy diff --git a/spring-context-support/src/main/java/org/springframework/mail/javamail/SmartMimeMessage.java b/spring-context-support/src/main/java/org/springframework/mail/javamail/SmartMimeMessage.java index 2fa44c910af9..e41d4a226558 100644 --- a/spring-context-support/src/main/java/org/springframework/mail/javamail/SmartMimeMessage.java +++ b/spring-context-support/src/main/java/org/springframework/mail/javamail/SmartMimeMessage.java @@ -16,9 +16,9 @@ package org.springframework.mail.javamail; -import javax.activation.FileTypeMap; -import javax.mail.Session; -import javax.mail.internet.MimeMessage; +import jakarta.activation.FileTypeMap; +import jakarta.mail.Session; +import jakarta.mail.internet.MimeMessage; import org.springframework.lang.Nullable; @@ -34,8 +34,8 @@ * @author Juergen Hoeller * @since 1.2 * @see JavaMailSenderImpl#createMimeMessage() - * @see MimeMessageHelper#getDefaultEncoding(javax.mail.internet.MimeMessage) - * @see MimeMessageHelper#getDefaultFileTypeMap(javax.mail.internet.MimeMessage) + * @see MimeMessageHelper#getDefaultEncoding(jakarta.mail.internet.MimeMessage) + * @see MimeMessageHelper#getDefaultFileTypeMap(jakarta.mail.internet.MimeMessage) */ class SmartMimeMessage extends MimeMessage { diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/commonj/DelegatingTimerListener.java b/spring-context-support/src/main/java/org/springframework/scheduling/commonj/DelegatingTimerListener.java deleted file mode 100644 index d6d493e1cac1..000000000000 --- a/spring-context-support/src/main/java/org/springframework/scheduling/commonj/DelegatingTimerListener.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2002-2012 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.scheduling.commonj; - -import commonj.timers.Timer; -import commonj.timers.TimerListener; - -import org.springframework.util.Assert; - -/** - * Simple TimerListener adapter that delegates to a given Runnable. - * - * @author Juergen Hoeller - * @since 2.0 - * @see commonj.timers.TimerListener - * @see java.lang.Runnable - * @deprecated as of 5.1, in favor of EE 7's - * {@link org.springframework.scheduling.concurrent.DefaultManagedTaskScheduler} - */ -@Deprecated -public class DelegatingTimerListener implements TimerListener { - - private final Runnable runnable; - - - /** - * Create a new DelegatingTimerListener. - * @param runnable the Runnable implementation to delegate to - */ - public DelegatingTimerListener(Runnable runnable) { - Assert.notNull(runnable, "Runnable is required"); - this.runnable = runnable; - } - - - /** - * Delegates execution to the underlying Runnable. - */ - @Override - public void timerExpired(Timer timer) { - this.runnable.run(); - } - -} diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/commonj/DelegatingWork.java b/spring-context-support/src/main/java/org/springframework/scheduling/commonj/DelegatingWork.java deleted file mode 100644 index 9f7eb9110ae8..000000000000 --- a/spring-context-support/src/main/java/org/springframework/scheduling/commonj/DelegatingWork.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2002-2012 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.scheduling.commonj; - -import commonj.work.Work; - -import org.springframework.scheduling.SchedulingAwareRunnable; -import org.springframework.util.Assert; - -/** - * Simple Work adapter that delegates to a given Runnable. - * - * @author Juergen Hoeller - * @since 2.0 - * @deprecated as of 5.1, in favor of EE 7's - * {@link org.springframework.scheduling.concurrent.DefaultManagedTaskExecutor} - */ -@Deprecated -public class DelegatingWork implements Work { - - private final Runnable delegate; - - - /** - * Create a new DelegatingWork. - * @param delegate the Runnable implementation to delegate to - * (may be a SchedulingAwareRunnable for extended support) - * @see org.springframework.scheduling.SchedulingAwareRunnable - * @see #isDaemon() - */ - public DelegatingWork(Runnable delegate) { - Assert.notNull(delegate, "Delegate must not be null"); - this.delegate = delegate; - } - - /** - * Return the wrapped Runnable implementation. - */ - public final Runnable getDelegate() { - return this.delegate; - } - - - /** - * Delegates execution to the underlying Runnable. - */ - @Override - public void run() { - this.delegate.run(); - } - - /** - * This implementation delegates to - * {@link org.springframework.scheduling.SchedulingAwareRunnable#isLongLived()}, - * if available. - */ - @Override - public boolean isDaemon() { - return (this.delegate instanceof SchedulingAwareRunnable && - ((SchedulingAwareRunnable) this.delegate).isLongLived()); - } - - /** - * This implementation is empty, since we expect the Runnable - * to terminate based on some specific shutdown signal. - */ - @Override - public void release() { - } - -} diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/commonj/ScheduledTimerListener.java b/spring-context-support/src/main/java/org/springframework/scheduling/commonj/ScheduledTimerListener.java deleted file mode 100644 index 52ca0afe1265..000000000000 --- a/spring-context-support/src/main/java/org/springframework/scheduling/commonj/ScheduledTimerListener.java +++ /dev/null @@ -1,229 +0,0 @@ -/* - * Copyright 2002-2017 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.scheduling.commonj; - -import commonj.timers.TimerListener; - -import org.springframework.lang.Nullable; - -/** - * JavaBean that describes a scheduled TimerListener, consisting of - * the TimerListener itself (or a Runnable to create a TimerListener for) - * and a delay plus period. Period needs to be specified; - * there is no point in a default for it. - * - *

    The CommonJ TimerManager does not offer more sophisticated scheduling - * options such as cron expressions. Consider using Quartz for such - * advanced needs. - * - *

    Note that the TimerManager uses a TimerListener instance that is - * shared between repeated executions, in contrast to Quartz which - * instantiates a new Job for each execution. - * - * @author Juergen Hoeller - * @since 2.0 - * @deprecated as of 5.1, in favor of EE 7's - * {@link org.springframework.scheduling.concurrent.DefaultManagedTaskScheduler} - */ -@Deprecated -public class ScheduledTimerListener { - - @Nullable - private TimerListener timerListener; - - private long delay = 0; - - private long period = -1; - - private boolean fixedRate = false; - - - /** - * Create a new ScheduledTimerListener, - * to be populated via bean properties. - * @see #setTimerListener - * @see #setDelay - * @see #setPeriod - * @see #setFixedRate - */ - public ScheduledTimerListener() { - } - - /** - * Create a new ScheduledTimerListener, with default - * one-time execution without delay. - * @param timerListener the TimerListener to schedule - */ - public ScheduledTimerListener(TimerListener timerListener) { - this.timerListener = timerListener; - } - - /** - * Create a new ScheduledTimerListener, with default - * one-time execution with the given delay. - * @param timerListener the TimerListener to schedule - * @param delay the delay before starting the task for the first time (ms) - */ - public ScheduledTimerListener(TimerListener timerListener, long delay) { - this.timerListener = timerListener; - this.delay = delay; - } - - /** - * Create a new ScheduledTimerListener. - * @param timerListener the TimerListener to schedule - * @param delay the delay before starting the task for the first time (ms) - * @param period the period between repeated task executions (ms) - * @param fixedRate whether to schedule as fixed-rate execution - */ - public ScheduledTimerListener(TimerListener timerListener, long delay, long period, boolean fixedRate) { - this.timerListener = timerListener; - this.delay = delay; - this.period = period; - this.fixedRate = fixedRate; - } - - /** - * Create a new ScheduledTimerListener, with default - * one-time execution without delay. - * @param timerTask the Runnable to schedule as TimerListener - */ - public ScheduledTimerListener(Runnable timerTask) { - setRunnable(timerTask); - } - - /** - * Create a new ScheduledTimerListener, with default - * one-time execution with the given delay. - * @param timerTask the Runnable to schedule as TimerListener - * @param delay the delay before starting the task for the first time (ms) - */ - public ScheduledTimerListener(Runnable timerTask, long delay) { - setRunnable(timerTask); - this.delay = delay; - } - - /** - * Create a new ScheduledTimerListener. - * @param timerTask the Runnable to schedule as TimerListener - * @param delay the delay before starting the task for the first time (ms) - * @param period the period between repeated task executions (ms) - * @param fixedRate whether to schedule as fixed-rate execution - */ - public ScheduledTimerListener(Runnable timerTask, long delay, long period, boolean fixedRate) { - setRunnable(timerTask); - this.delay = delay; - this.period = period; - this.fixedRate = fixedRate; - } - - - /** - * Set the Runnable to schedule as TimerListener. - * @see DelegatingTimerListener - */ - public void setRunnable(Runnable timerTask) { - this.timerListener = new DelegatingTimerListener(timerTask); - } - - /** - * Set the TimerListener to schedule. - */ - public void setTimerListener(@Nullable TimerListener timerListener) { - this.timerListener = timerListener; - } - - /** - * Return the TimerListener to schedule. - */ - @Nullable - public TimerListener getTimerListener() { - return this.timerListener; - } - - /** - * Set the delay before starting the task for the first time, - * in milliseconds. Default is 0, immediately starting the - * task after successful scheduling. - *

    If the "firstTime" property is specified, this property will be ignored. - * Specify one or the other, not both. - */ - public void setDelay(long delay) { - this.delay = delay; - } - - /** - * Return the delay before starting the job for the first time. - */ - public long getDelay() { - return this.delay; - } - - /** - * Set the period between repeated task executions, in milliseconds. - *

    Default is -1, leading to one-time execution. In case of zero or a - * positive value, the task will be executed repeatedly, with the given - * interval in-between executions. - *

    Note that the semantics of the period value vary between fixed-rate - * and fixed-delay execution. - *

    Note: A period of 0 (for example as fixed delay) is - * supported, because the CommonJ specification defines this as a legal value. - * Hence a value of 0 will result in immediate re-execution after a job has - * finished (not in one-time execution like with {@code java.util.Timer}). - * @see #setFixedRate - * @see #isOneTimeTask() - * @see commonj.timers.TimerManager#schedule(commonj.timers.TimerListener, long, long) - */ - public void setPeriod(long period) { - this.period = period; - } - - /** - * Return the period between repeated task executions. - */ - public long getPeriod() { - return this.period; - } - - /** - * Is this task only ever going to execute once? - * @return {@code true} if this task is only ever going to execute once - * @see #getPeriod() - */ - public boolean isOneTimeTask() { - return (this.period < 0); - } - - /** - * Set whether to schedule as fixed-rate execution, rather than - * fixed-delay execution. Default is "false", i.e. fixed delay. - *

    See TimerManager javadoc for details on those execution modes. - * @see commonj.timers.TimerManager#schedule(commonj.timers.TimerListener, long, long) - * @see commonj.timers.TimerManager#scheduleAtFixedRate(commonj.timers.TimerListener, long, long) - */ - public void setFixedRate(boolean fixedRate) { - this.fixedRate = fixedRate; - } - - /** - * Return whether to schedule as fixed-rate execution. - */ - public boolean isFixedRate() { - return this.fixedRate; - } - -} diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/commonj/TimerManagerAccessor.java b/spring-context-support/src/main/java/org/springframework/scheduling/commonj/TimerManagerAccessor.java deleted file mode 100644 index b5c0e15a068c..000000000000 --- a/spring-context-support/src/main/java/org/springframework/scheduling/commonj/TimerManagerAccessor.java +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Copyright 2002-2017 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.scheduling.commonj; - -import javax.naming.NamingException; - -import commonj.timers.TimerManager; - -import org.springframework.beans.factory.DisposableBean; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.context.Lifecycle; -import org.springframework.jndi.JndiLocatorSupport; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; - -/** - * Base class for classes that are accessing a CommonJ {@link commonj.timers.TimerManager} - * Defines common configuration settings and common lifecycle handling. - * - * @author Juergen Hoeller - * @since 3.0 - * @see commonj.timers.TimerManager - * @deprecated as of 5.1, in favor of EE 7's - * {@link org.springframework.scheduling.concurrent.DefaultManagedTaskScheduler} - */ -@Deprecated -public abstract class TimerManagerAccessor extends JndiLocatorSupport - implements InitializingBean, DisposableBean, Lifecycle { - - @Nullable - private TimerManager timerManager; - - @Nullable - private String timerManagerName; - - private boolean shared = false; - - - /** - * Specify the CommonJ TimerManager to delegate to. - *

    Note that the given TimerManager's lifecycle will be managed - * by this FactoryBean. - *

    Alternatively (and typically), you can specify the JNDI name - * of the target TimerManager. - * @see #setTimerManagerName - */ - public void setTimerManager(TimerManager timerManager) { - this.timerManager = timerManager; - } - - /** - * Set the JNDI name of the CommonJ TimerManager. - *

    This can either be a fully qualified JNDI name, or the JNDI name relative - * to the current environment naming context if "resourceRef" is set to "true". - * @see #setTimerManager - * @see #setResourceRef - */ - public void setTimerManagerName(String timerManagerName) { - this.timerManagerName = timerManagerName; - } - - /** - * Specify whether the TimerManager obtained by this FactoryBean - * is a shared instance ("true") or an independent instance ("false"). - * The lifecycle of the former is supposed to be managed by the application - * server, while the lifecycle of the latter is up to the application. - *

    Default is "false", i.e. managing an independent TimerManager instance. - * This is what the CommonJ specification suggests that application servers - * are supposed to offer via JNDI lookups, typically declared as a - * {@code resource-ref} of type {@code commonj.timers.TimerManager} - * in {@code web.xml}, with {@code res-sharing-scope} set to 'Unshareable'. - *

    Switch this flag to "true" if you are obtaining a shared TimerManager, - * typically through specifying the JNDI location of a TimerManager that - * has been explicitly declared as 'Shareable'. Note that WebLogic's - * cluster-aware Job Scheduler is a shared TimerManager too. - *

    The sole difference between this FactoryBean being in shared or - * non-shared mode is that it will only attempt to suspend / resume / stop - * the underlying TimerManager in case of an independent (non-shared) instance. - * This only affects the {@link org.springframework.context.Lifecycle} support - * as well as application context shutdown. - * @see #stop() - * @see #start() - * @see #destroy() - * @see commonj.timers.TimerManager - */ - public void setShared(boolean shared) { - this.shared = shared; - } - - - @Override - public void afterPropertiesSet() throws NamingException { - if (this.timerManager == null) { - if (this.timerManagerName == null) { - throw new IllegalArgumentException("Either 'timerManager' or 'timerManagerName' must be specified"); - } - this.timerManager = lookup(this.timerManagerName, TimerManager.class); - } - } - - /** - * Return the configured TimerManager, if any. - * @return the TimerManager, or {@code null} if not available - */ - @Nullable - protected final TimerManager getTimerManager() { - return this.timerManager; - } - - /** - * Obtain the TimerManager for actual use. - * @return the TimerManager (never {@code null}) - * @throws IllegalStateException in case of no TimerManager set - * @since 5.0 - */ - protected TimerManager obtainTimerManager() { - Assert.notNull(this.timerManager, "No TimerManager set"); - return this.timerManager; - } - - - //--------------------------------------------------------------------- - // Implementation of Lifecycle interface - //--------------------------------------------------------------------- - - /** - * Resumes the underlying TimerManager (if not shared). - * @see commonj.timers.TimerManager#resume() - */ - @Override - public void start() { - if (!this.shared) { - obtainTimerManager().resume(); - } - } - - /** - * Suspends the underlying TimerManager (if not shared). - * @see commonj.timers.TimerManager#suspend() - */ - @Override - public void stop() { - if (!this.shared) { - obtainTimerManager().suspend(); - } - } - - /** - * Considers the underlying TimerManager as running if it is - * neither suspending nor stopping. - * @see commonj.timers.TimerManager#isSuspending() - * @see commonj.timers.TimerManager#isStopping() - */ - @Override - public boolean isRunning() { - TimerManager tm = obtainTimerManager(); - return (!tm.isSuspending() && !tm.isStopping()); - } - - - //--------------------------------------------------------------------- - // Implementation of DisposableBean interface - //--------------------------------------------------------------------- - - /** - * Stops the underlying TimerManager (if not shared). - * @see commonj.timers.TimerManager#stop() - */ - @Override - public void destroy() { - // Stop the entire TimerManager, if necessary. - if (this.timerManager != null && !this.shared) { - // May return early, but at least we already cancelled all known Timers. - this.timerManager.stop(); - } - } - -} diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/commonj/TimerManagerFactoryBean.java b/spring-context-support/src/main/java/org/springframework/scheduling/commonj/TimerManagerFactoryBean.java deleted file mode 100644 index 1d6db6c820fb..000000000000 --- a/spring-context-support/src/main/java/org/springframework/scheduling/commonj/TimerManagerFactoryBean.java +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Copyright 2002-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.scheduling.commonj; - -import java.util.ArrayList; -import java.util.List; - -import javax.naming.NamingException; - -import commonj.timers.Timer; -import commonj.timers.TimerManager; - -import org.springframework.beans.factory.DisposableBean; -import org.springframework.beans.factory.FactoryBean; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.context.Lifecycle; -import org.springframework.lang.Nullable; - -/** - * {@link org.springframework.beans.factory.FactoryBean} that retrieves a - * CommonJ {@link commonj.timers.TimerManager} and exposes it for bean references. - * - *

    This is the central convenience class for setting up a - * CommonJ TimerManager in a Spring context. - * - *

    Allows for registration of ScheduledTimerListeners. This is the main - * purpose of this class; the TimerManager itself could also be fetched - * from JNDI via {@link org.springframework.jndi.JndiObjectFactoryBean}. - * In scenarios that just require static registration of tasks at startup, - * there is no need to access the TimerManager itself in application code. - * - *

    Note that the TimerManager uses a TimerListener instance that is - * shared between repeated executions, in contrast to Quartz which - * instantiates a new Job for each execution. - * - * @author Juergen Hoeller - * @since 2.0 - * @see ScheduledTimerListener - * @see commonj.timers.TimerManager - * @see commonj.timers.TimerListener - * @deprecated as of 5.1, in favor of EE 7's - * {@link org.springframework.scheduling.concurrent.DefaultManagedTaskScheduler} - */ -@Deprecated -public class TimerManagerFactoryBean extends TimerManagerAccessor - implements FactoryBean, InitializingBean, DisposableBean, Lifecycle { - - @Nullable - private ScheduledTimerListener[] scheduledTimerListeners; - - @Nullable - private List timers; - - - /** - * Register a list of ScheduledTimerListener objects with the TimerManager - * that this FactoryBean creates. Depending on each ScheduledTimerListener's settings, - * it will be registered via one of TimerManager's schedule methods. - * @see commonj.timers.TimerManager#schedule(commonj.timers.TimerListener, long) - * @see commonj.timers.TimerManager#schedule(commonj.timers.TimerListener, long, long) - * @see commonj.timers.TimerManager#scheduleAtFixedRate(commonj.timers.TimerListener, long, long) - */ - public void setScheduledTimerListeners(ScheduledTimerListener[] scheduledTimerListeners) { - this.scheduledTimerListeners = scheduledTimerListeners; - } - - - //--------------------------------------------------------------------- - // Implementation of InitializingBean interface - //--------------------------------------------------------------------- - - @Override - public void afterPropertiesSet() throws NamingException { - super.afterPropertiesSet(); - - if (this.scheduledTimerListeners != null) { - this.timers = new ArrayList<>(this.scheduledTimerListeners.length); - TimerManager timerManager = obtainTimerManager(); - for (ScheduledTimerListener scheduledTask : this.scheduledTimerListeners) { - Timer timer; - if (scheduledTask.isOneTimeTask()) { - timer = timerManager.schedule(scheduledTask.getTimerListener(), scheduledTask.getDelay()); - } - else { - if (scheduledTask.isFixedRate()) { - timer = timerManager.scheduleAtFixedRate( - scheduledTask.getTimerListener(), scheduledTask.getDelay(), scheduledTask.getPeriod()); - } - else { - timer = timerManager.schedule( - scheduledTask.getTimerListener(), scheduledTask.getDelay(), scheduledTask.getPeriod()); - } - } - this.timers.add(timer); - } - } - } - - - //--------------------------------------------------------------------- - // Implementation of FactoryBean interface - //--------------------------------------------------------------------- - - @Override - @Nullable - public TimerManager getObject() { - return getTimerManager(); - } - - @Override - public Class getObjectType() { - TimerManager timerManager = getTimerManager(); - return (timerManager != null ? timerManager.getClass() : TimerManager.class); - } - - @Override - public boolean isSingleton() { - return true; - } - - - //--------------------------------------------------------------------- - // Implementation of DisposableBean interface - //--------------------------------------------------------------------- - - /** - * Cancels all statically registered Timers on shutdown, - * and stops the underlying TimerManager (if not shared). - * @see commonj.timers.Timer#cancel() - * @see commonj.timers.TimerManager#stop() - */ - @Override - public void destroy() { - // Cancel all registered timers. - if (this.timers != null) { - for (Timer timer : this.timers) { - try { - timer.cancel(); - } - catch (Throwable ex) { - logger.debug("Could not cancel CommonJ Timer", ex); - } - } - this.timers.clear(); - } - - // Stop the TimerManager itself. - super.destroy(); - } - -} diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/commonj/TimerManagerTaskScheduler.java b/spring-context-support/src/main/java/org/springframework/scheduling/commonj/TimerManagerTaskScheduler.java deleted file mode 100644 index 97ea5b86a81d..000000000000 --- a/spring-context-support/src/main/java/org/springframework/scheduling/commonj/TimerManagerTaskScheduler.java +++ /dev/null @@ -1,201 +0,0 @@ -/* - * Copyright 2002-2018 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.scheduling.commonj; - -import java.util.Date; -import java.util.concurrent.Delayed; -import java.util.concurrent.FutureTask; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; - -import commonj.timers.Timer; -import commonj.timers.TimerListener; - -import org.springframework.lang.Nullable; -import org.springframework.scheduling.TaskScheduler; -import org.springframework.scheduling.Trigger; -import org.springframework.scheduling.support.SimpleTriggerContext; -import org.springframework.scheduling.support.TaskUtils; -import org.springframework.util.Assert; -import org.springframework.util.ErrorHandler; - -/** - * Implementation of Spring's {@link TaskScheduler} interface, wrapping - * a CommonJ {@link commonj.timers.TimerManager}. - * - * @author Juergen Hoeller - * @author Mark Fisher - * @since 3.0 - * @deprecated as of 5.1, in favor of EE 7's - * {@link org.springframework.scheduling.concurrent.DefaultManagedTaskScheduler} - */ -@Deprecated -public class TimerManagerTaskScheduler extends TimerManagerAccessor implements TaskScheduler { - - @Nullable - private volatile ErrorHandler errorHandler; - - - /** - * Provide an {@link ErrorHandler} strategy. - */ - public void setErrorHandler(ErrorHandler errorHandler) { - this.errorHandler = errorHandler; - } - - - @Override - @Nullable - public ScheduledFuture schedule(Runnable task, Trigger trigger) { - return new ReschedulingTimerListener(errorHandlingTask(task, true), trigger).schedule(); - } - - @Override - public ScheduledFuture schedule(Runnable task, Date startTime) { - TimerScheduledFuture futureTask = new TimerScheduledFuture(errorHandlingTask(task, false)); - Timer timer = obtainTimerManager().schedule(futureTask, startTime); - futureTask.setTimer(timer); - return futureTask; - } - - @Override - public ScheduledFuture scheduleAtFixedRate(Runnable task, Date startTime, long period) { - TimerScheduledFuture futureTask = new TimerScheduledFuture(errorHandlingTask(task, true)); - Timer timer = obtainTimerManager().scheduleAtFixedRate(futureTask, startTime, period); - futureTask.setTimer(timer); - return futureTask; - } - - @Override - public ScheduledFuture scheduleAtFixedRate(Runnable task, long period) { - TimerScheduledFuture futureTask = new TimerScheduledFuture(errorHandlingTask(task, true)); - Timer timer = obtainTimerManager().scheduleAtFixedRate(futureTask, 0, period); - futureTask.setTimer(timer); - return futureTask; - } - - @Override - public ScheduledFuture scheduleWithFixedDelay(Runnable task, Date startTime, long delay) { - TimerScheduledFuture futureTask = new TimerScheduledFuture(errorHandlingTask(task, true)); - Timer timer = obtainTimerManager().schedule(futureTask, startTime, delay); - futureTask.setTimer(timer); - return futureTask; - } - - @Override - public ScheduledFuture scheduleWithFixedDelay(Runnable task, long delay) { - TimerScheduledFuture futureTask = new TimerScheduledFuture(errorHandlingTask(task, true)); - Timer timer = obtainTimerManager().schedule(futureTask, 0, delay); - futureTask.setTimer(timer); - return futureTask; - } - - private Runnable errorHandlingTask(Runnable delegate, boolean isRepeatingTask) { - return TaskUtils.decorateTaskWithErrorHandler(delegate, this.errorHandler, isRepeatingTask); - } - - - /** - * ScheduledFuture adapter that wraps a CommonJ Timer. - */ - private static class TimerScheduledFuture extends FutureTask implements TimerListener, ScheduledFuture { - - @Nullable - protected transient Timer timer; - - protected transient boolean cancelled = false; - - public TimerScheduledFuture(Runnable runnable) { - super(runnable, null); - } - - public void setTimer(Timer timer) { - this.timer = timer; - } - - @Override - public void timerExpired(Timer timer) { - runAndReset(); - } - - @Override - public boolean cancel(boolean mayInterruptIfRunning) { - boolean result = super.cancel(mayInterruptIfRunning); - if (this.timer != null) { - this.timer.cancel(); - } - this.cancelled = true; - return result; - } - - @Override - public long getDelay(TimeUnit unit) { - Assert.state(this.timer != null, "No Timer available"); - return unit.convert(this.timer.getScheduledExecutionTime() - System.currentTimeMillis(), TimeUnit.MILLISECONDS); - } - - @Override - public int compareTo(Delayed other) { - if (this == other) { - return 0; - } - long diff = getDelay(TimeUnit.MILLISECONDS) - other.getDelay(TimeUnit.MILLISECONDS); - return (diff == 0 ? 0 : ((diff < 0) ? -1 : 1)); - } - } - - - /** - * ScheduledFuture adapter for trigger-based rescheduling. - */ - private class ReschedulingTimerListener extends TimerScheduledFuture { - - private final Trigger trigger; - - private final SimpleTriggerContext triggerContext = new SimpleTriggerContext(); - - private volatile Date scheduledExecutionTime = new Date(); - - public ReschedulingTimerListener(Runnable runnable, Trigger trigger) { - super(runnable); - this.trigger = trigger; - } - - @Nullable - public ScheduledFuture schedule() { - Date nextExecutionTime = this.trigger.nextExecutionTime(this.triggerContext); - if (nextExecutionTime == null) { - return null; - } - this.scheduledExecutionTime = nextExecutionTime; - setTimer(obtainTimerManager().schedule(this, this.scheduledExecutionTime)); - return this; - } - - @Override - public void timerExpired(Timer timer) { - Date actualExecutionTime = new Date(); - super.timerExpired(timer); - Date completionTime = new Date(); - this.triggerContext.update(this.scheduledExecutionTime, actualExecutionTime, completionTime); - if (!this.cancelled) { - schedule(); - } - } - } - -} diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/commonj/WorkManagerTaskExecutor.java b/spring-context-support/src/main/java/org/springframework/scheduling/commonj/WorkManagerTaskExecutor.java deleted file mode 100644 index a9adcc823d5a..000000000000 --- a/spring-context-support/src/main/java/org/springframework/scheduling/commonj/WorkManagerTaskExecutor.java +++ /dev/null @@ -1,234 +0,0 @@ -/* - * Copyright 2002-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.scheduling.commonj; - -import java.util.Collection; -import java.util.concurrent.Callable; -import java.util.concurrent.Future; -import java.util.concurrent.FutureTask; - -import javax.naming.NamingException; - -import commonj.work.Work; -import commonj.work.WorkException; -import commonj.work.WorkItem; -import commonj.work.WorkListener; -import commonj.work.WorkManager; -import commonj.work.WorkRejectedException; - -import org.springframework.beans.factory.InitializingBean; -import org.springframework.core.task.AsyncListenableTaskExecutor; -import org.springframework.core.task.TaskDecorator; -import org.springframework.core.task.TaskRejectedException; -import org.springframework.jndi.JndiLocatorSupport; -import org.springframework.lang.Nullable; -import org.springframework.scheduling.SchedulingException; -import org.springframework.scheduling.SchedulingTaskExecutor; -import org.springframework.util.Assert; -import org.springframework.util.concurrent.ListenableFuture; -import org.springframework.util.concurrent.ListenableFutureTask; - -/** - * TaskExecutor implementation that delegates to a CommonJ WorkManager, - * implementing the {@link commonj.work.WorkManager} interface, - * which either needs to be specified as reference or through the JNDI name. - * - *

    This is the central convenience class for setting up a - * CommonJ WorkManager in a Spring context. - * - *

    Also implements the CommonJ WorkManager interface itself, delegating all - * calls to the target WorkManager. Hence, a caller can choose whether it wants - * to talk to this executor through the Spring TaskExecutor interface or the - * CommonJ WorkManager interface. - * - *

    The CommonJ WorkManager will usually be retrieved from the application - * server's JNDI environment, as defined in the server's management console. - * - *

    Note: On EE 7/8 compliant versions of WebLogic and WebSphere, a - * {@link org.springframework.scheduling.concurrent.DefaultManagedTaskExecutor} - * should be preferred, following JSR-236 support in Java EE 7/8. - * - * @author Juergen Hoeller - * @since 2.0 - * @deprecated as of 5.1, in favor of the EE 7/8 based - * {@link org.springframework.scheduling.concurrent.DefaultManagedTaskExecutor} - */ -@Deprecated -public class WorkManagerTaskExecutor extends JndiLocatorSupport - implements AsyncListenableTaskExecutor, SchedulingTaskExecutor, WorkManager, InitializingBean { - - @Nullable - private WorkManager workManager; - - @Nullable - private String workManagerName; - - @Nullable - private WorkListener workListener; - - @Nullable - private TaskDecorator taskDecorator; - - - /** - * Specify the CommonJ WorkManager to delegate to. - *

    Alternatively, you can also specify the JNDI name of the target WorkManager. - * @see #setWorkManagerName - */ - public void setWorkManager(WorkManager workManager) { - this.workManager = workManager; - } - - /** - * Set the JNDI name of the CommonJ WorkManager. - *

    This can either be a fully qualified JNDI name, or the JNDI name relative - * to the current environment naming context if "resourceRef" is set to "true". - * @see #setWorkManager - * @see #setResourceRef - */ - public void setWorkManagerName(String workManagerName) { - this.workManagerName = workManagerName; - } - - /** - * Specify a CommonJ WorkListener to apply, if any. - *

    This shared WorkListener instance will be passed on to the - * WorkManager by all {@link #execute} calls on this TaskExecutor. - */ - public void setWorkListener(WorkListener workListener) { - this.workListener = workListener; - } - - /** - * Specify a custom {@link TaskDecorator} to be applied to any {@link Runnable} - * about to be executed. - *

    Note that such a decorator is not necessarily being applied to the - * user-supplied {@code Runnable}/{@code Callable} but rather to the actual - * execution callback (which may be a wrapper around the user-supplied task). - *

    The primary use case is to set some execution context around the task's - * invocation, or to provide some monitoring/statistics for task execution. - *

    NOTE: Exception handling in {@code TaskDecorator} implementations - * is limited to plain {@code Runnable} execution via {@code execute} calls. - * In case of {@code #submit} calls, the exposed {@code Runnable} will be a - * {@code FutureTask} which does not propagate any exceptions; you might - * have to cast it and call {@code Future#get} to evaluate exceptions. - * @since 4.3 - */ - public void setTaskDecorator(TaskDecorator taskDecorator) { - this.taskDecorator = taskDecorator; - } - - @Override - public void afterPropertiesSet() throws NamingException { - if (this.workManager == null) { - if (this.workManagerName == null) { - throw new IllegalArgumentException("Either 'workManager' or 'workManagerName' must be specified"); - } - this.workManager = lookup(this.workManagerName, WorkManager.class); - } - } - - private WorkManager obtainWorkManager() { - Assert.state(this.workManager != null, "No WorkManager specified"); - return this.workManager; - } - - - //------------------------------------------------------------------------- - // Implementation of the Spring SchedulingTaskExecutor interface - //------------------------------------------------------------------------- - - @Override - public void execute(Runnable task) { - Work work = new DelegatingWork(this.taskDecorator != null ? this.taskDecorator.decorate(task) : task); - try { - if (this.workListener != null) { - obtainWorkManager().schedule(work, this.workListener); - } - else { - obtainWorkManager().schedule(work); - } - } - catch (WorkRejectedException ex) { - throw new TaskRejectedException("CommonJ WorkManager did not accept task: " + task, ex); - } - catch (WorkException ex) { - throw new SchedulingException("Could not schedule task on CommonJ WorkManager", ex); - } - } - - @Override - public void execute(Runnable task, long startTimeout) { - execute(task); - } - - @Override - public Future submit(Runnable task) { - FutureTask future = new FutureTask<>(task, null); - execute(future); - return future; - } - - @Override - public Future submit(Callable task) { - FutureTask future = new FutureTask<>(task); - execute(future); - return future; - } - - @Override - public ListenableFuture submitListenable(Runnable task) { - ListenableFutureTask future = new ListenableFutureTask<>(task, null); - execute(future); - return future; - } - - @Override - public ListenableFuture submitListenable(Callable task) { - ListenableFutureTask future = new ListenableFutureTask<>(task); - execute(future); - return future; - } - - - //------------------------------------------------------------------------- - // Implementation of the CommonJ WorkManager interface - //------------------------------------------------------------------------- - - @Override - public WorkItem schedule(Work work) throws WorkException, IllegalArgumentException { - return obtainWorkManager().schedule(work); - } - - @Override - public WorkItem schedule(Work work, WorkListener workListener) throws WorkException { - return obtainWorkManager().schedule(work, workListener); - } - - @Override - @SuppressWarnings("rawtypes") - public boolean waitForAll(Collection workItems, long timeout) throws InterruptedException { - return obtainWorkManager().waitForAll(workItems, timeout); - } - - @Override - @SuppressWarnings("rawtypes") - public Collection waitForAny(Collection workItems, long timeout) throws InterruptedException { - return obtainWorkManager().waitForAny(workItems, timeout); - } - -} diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/commonj/package-info.java b/spring-context-support/src/main/java/org/springframework/scheduling/commonj/package-info.java deleted file mode 100644 index ca0fbead6816..000000000000 --- a/spring-context-support/src/main/java/org/springframework/scheduling/commonj/package-info.java +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Convenience classes for scheduling based on the CommonJ WorkManager/TimerManager - * facility, as supported by IBM WebSphere 6.0+ and BEA WebLogic 9.0+. - */ -@NonNullApi -@NonNullFields -package org.springframework.scheduling.commonj; - -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/JobDetailFactoryBean.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/JobDetailFactoryBean.java index 78210ab29e90..32ee0b90de7b 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/JobDetailFactoryBean.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/JobDetailFactoryBean.java @@ -141,7 +141,7 @@ public void setDurability(boolean durability) { } /** - * Set the recovery flag for this job, i.e. whether or not the job should + * Set the recovery flag for this job, i.e. whether the job should * get re-executed if a 'recovery' or 'fail-over' situation is encountered. */ public void setRequestsRecovery(boolean requestsRecovery) { diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/LocalDataSourceJobStore.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/LocalDataSourceJobStore.java index e0b12f443f91..f6042bee9347 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/LocalDataSourceJobStore.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/LocalDataSourceJobStore.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -39,6 +39,8 @@ * Subclass of Quartz's {@link JobStoreCMT} class that delegates to a Spring-managed * {@link DataSource} instead of using a Quartz-managed JDBC connection pool. * This JobStore will be used if SchedulerFactoryBean's "dataSource" property is set. + * You may also configure it explicitly, possibly as a custom subclass of this + * {@code LocalDataSourceJobStore} or as an equivalent {@code JobStoreCMT} variant. * *

    Supports both transactional and non-transactional DataSource access. * With a non-XA DataSource and local Spring transactions, a single DataSource @@ -58,6 +60,8 @@ * @since 1.1 * @see SchedulerFactoryBean#setDataSource * @see SchedulerFactoryBean#setNonTransactionalDataSource + * @see SchedulerFactoryBean#getConfigTimeDataSource() + * @see SchedulerFactoryBean#getConfigTimeNonTransactionalDataSource() * @see org.springframework.jdbc.datasource.DataSourceUtils#doGetConnection * @see org.springframework.jdbc.datasource.DataSourceUtils#releaseConnection */ diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/LocalTaskExecutorThreadPool.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/LocalTaskExecutorThreadPool.java index 42af660341b0..624b72cdc24f 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/LocalTaskExecutorThreadPool.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/LocalTaskExecutorThreadPool.java @@ -24,6 +24,7 @@ import org.quartz.SchedulerConfigException; import org.quartz.spi.ThreadPool; +import org.springframework.aot.hint.annotation.Reflective; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -45,10 +46,12 @@ public class LocalTaskExecutorThreadPool implements ThreadPool { @Override + @Reflective public void setInstanceId(String schedInstId) { } @Override + @Reflective public void setInstanceName(String schedName) { } diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/MethodInvokingJobDetailFactoryBean.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/MethodInvokingJobDetailFactoryBean.java index f4658c7c92b5..93470b37c100 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/MethodInvokingJobDetailFactoryBean.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/MethodInvokingJobDetailFactoryBean.java @@ -119,7 +119,7 @@ public void setGroup(String group) { } /** - * Specify whether or not multiple jobs should be run in a concurrent fashion. + * Specify whether multiple jobs should be run in a concurrent fashion. * The behavior when one does not want concurrent jobs to be executed is * realized through adding the {@code @PersistJobDataAfterExecution} and * {@code @DisallowConcurrentExecution} markers. @@ -286,7 +286,7 @@ protected void executeInternal(JobExecutionContext context) throws JobExecutionE /** * Extension of the MethodInvokingJob, implementing the StatefulJob interface. - * Quartz checks whether or not jobs are stateful and if so, + * Quartz checks whether jobs are stateful and if so, * won't let jobs interfere with each other. */ @PersistJobDataAfterExecution diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/QuartzJobBean.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/QuartzJobBean.java index c63934d0d3d3..6ceb4877fc76 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/QuartzJobBean.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/QuartzJobBean.java @@ -41,7 +41,7 @@ *

    Note that the preferred way to apply dependency injection * to Job instances is via a JobFactory: that is, to specify * {@link SpringBeanJobFactory} as Quartz JobFactory (typically via - * {@link SchedulerFactoryBean#setJobFactory} SchedulerFactoryBean's "jobFactory" property}). + * {@link SchedulerFactoryBean#setJobFactory SchedulerFactoryBean's "jobFactory" property}). * This allows to implement dependency-injected Quartz Jobs without * a dependency on Spring base classes. * diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerAccessorBean.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerAccessorBean.java index 42a80e2a33bf..ea136c6e4fb1 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerAccessorBean.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerAccessorBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-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. @@ -99,8 +99,7 @@ public void afterPropertiesSet() throws SchedulerException { } protected Scheduler findScheduler(String schedulerName) throws SchedulerException { - if (this.beanFactory instanceof ListableBeanFactory) { - ListableBeanFactory lbf = (ListableBeanFactory) this.beanFactory; + if (this.beanFactory instanceof ListableBeanFactory lbf) { String[] beanNames = lbf.getBeanNamesForType(Scheduler.class); for (String beanName : beanNames) { Scheduler schedulerBean = (Scheduler) lbf.getBean(beanName); diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBean.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBean.java index 50ef456cfc9a..15185bba6c37 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBean.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -310,9 +310,11 @@ public void setTaskExecutor(Executor taskExecutor) { /** * Set the default {@link DataSource} to be used by the Scheduler. - * If set, this will override corresponding settings in Quartz properties. *

    Note: If this is set, the Quartz settings should not define * a job store "dataSource" to avoid meaningless double configuration. + * Also, do not define a "org.quartz.jobStore.class" property at all. + * (You may explicitly define Spring's {@link LocalDataSourceJobStore} + * but that's the default when using this method anyway.) *

    A Spring-specific subclass of Quartz' JobStoreCMT will be used. * It is therefore strongly recommended to perform all operations on * the Scheduler within Spring-managed (or plain JTA) transactions. @@ -570,7 +572,7 @@ private void initSchedulerFactory(StdSchedulerFactory schedulerFactory) throws S CollectionUtils.mergePropertiesIntoMap(this.quartzProperties, mergedProps); if (this.dataSource != null) { - mergedProps.setProperty(StdSchedulerFactory.PROP_JOB_STORE_CLASS, LocalDataSourceJobStore.class.getName()); + mergedProps.putIfAbsent(StdSchedulerFactory.PROP_JOB_STORE_CLASS, LocalDataSourceJobStore.class.getName()); } // Determine scheduler name across local settings and Quartz properties... diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBeanRuntimeHints.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBeanRuntimeHints.java new file mode 100644 index 000000000000..c2d727a45588 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBeanRuntimeHints.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-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.scheduling.quartz; + +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.aot.hint.TypeHint.Builder; +import org.springframework.aot.hint.TypeReference; +import org.springframework.aot.hint.annotation.ReflectiveRuntimeHintsRegistrar; +import org.springframework.util.ClassUtils; + +/** + * {@link RuntimeHintsRegistrar} implementation that makes sure {@link SchedulerFactoryBean} + * reflection entries are registered. + * + * @author Sebastien Deleuze + * @author Stephane Nicoll + * @since 6.0 + */ +class SchedulerFactoryBeanRuntimeHints implements RuntimeHintsRegistrar { + + private static final String SCHEDULER_FACTORY_CLASS_NAME = "org.quartz.impl.StdSchedulerFactory"; + + private final ReflectiveRuntimeHintsRegistrar reflectiveRegistrar = new ReflectiveRuntimeHintsRegistrar(); + + + @Override + public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + if (!ClassUtils.isPresent(SCHEDULER_FACTORY_CLASS_NAME, classLoader)) { + return; + } + hints.reflection() + .registerType(TypeReference.of(SCHEDULER_FACTORY_CLASS_NAME), this::typeHint) + .registerTypes(TypeReference.listOf(ResourceLoaderClassLoadHelper.class, + LocalTaskExecutorThreadPool.class, LocalDataSourceJobStore.class), this::typeHint); + this.reflectiveRegistrar.registerRuntimeHints(hints, LocalTaskExecutorThreadPool.class); + } + + private void typeHint(Builder typeHint) { + typeHint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS).onReachableType(SchedulerFactoryBean.class); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SimpleThreadPoolTaskExecutor.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SimpleThreadPoolTaskExecutor.java index 35534962d9f9..8be2a3534ac1 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SimpleThreadPoolTaskExecutor.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SimpleThreadPoolTaskExecutor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -47,6 +47,7 @@ * @see org.springframework.core.task.TaskExecutor * @see SchedulerFactoryBean#setTaskExecutor */ +@SuppressWarnings("deprecation") public class SimpleThreadPoolTaskExecutor extends SimpleThreadPool implements AsyncListenableTaskExecutor, SchedulingTaskExecutor, InitializingBean, DisposableBean { @@ -76,6 +77,7 @@ public void execute(Runnable task) { } } + @Deprecated @Override public void execute(Runnable task, long startTimeout) { execute(task); diff --git a/spring-context-support/src/main/resources/META-INF/spring/aot.factories b/spring-context-support/src/main/resources/META-INF/spring/aot.factories new file mode 100644 index 000000000000..a0f69394acdf --- /dev/null +++ b/spring-context-support/src/main/resources/META-INF/spring/aot.factories @@ -0,0 +1,3 @@ +org.springframework.aot.hint.RuntimeHintsRegistrar= \ +org.springframework.mail.javamail.JavaMailMimeTypesRuntimeHints,\ +org.springframework.scheduling.quartz.SchedulerFactoryBeanRuntimeHints \ No newline at end of file diff --git a/spring-context-support/src/main/resources/org/springframework/mail/javamail/mime.types b/spring-context-support/src/main/resources/org/springframework/mail/javamail/mime.types index b03625f63316..f325ad37d8b5 100644 --- a/spring-context-support/src/main/resources/org/springframework/mail/javamail/mime.types +++ b/spring-context-support/src/main/resources/org/springframework/mail/javamail/mime.types @@ -50,7 +50,7 @@ video/x-msvideo avi ################################################################################ # # Additional file types adapted from -# http://sites.utoronto.ca/webdocs/HTMLdocs/Book/Book-3ed/appb/mimetype.html +# https://web.archive.org/web/20220119153325/http%3A//sites.utoronto.ca/webdocs/HTMLdocs/Book/Book-3ed/appb/mimetype.html # kindly re-licensed to Apache Software License 2.0 by Ian Graham. # ################################################################################ diff --git a/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheManagerTests.java b/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheManagerTests.java index e90a3ece9363..f8c0de21f2d7 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheManagerTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheManagerTests.java @@ -178,14 +178,11 @@ public void setCacheNameNullRestoreDynamicMode() { @Test public void cacheLoaderUseLoadingCache() { CaffeineCacheManager cm = new CaffeineCacheManager("c1"); - cm.setCacheLoader(new CacheLoader() { - @Override - public Object load(Object key) throws Exception { - if ("ping".equals(key)) { - return "pong"; - } - throw new IllegalArgumentException("I only know ping"); + cm.setCacheLoader(key -> { + if ("ping".equals(key)) { + return "pong"; } + throw new IllegalArgumentException("I only know ping"); }); Cache cache1 = cm.getCache("c1"); Cache.ValueWrapper value = cache1.get("ping"); diff --git a/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheTests.java b/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheTests.java index 215720353304..7f760354d8b9 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -106,7 +106,7 @@ void testPutIfAbsentNullValue() { Cache.ValueWrapper wrapper = cache.putIfAbsent(key, "anotherValue"); // A value is set but is 'null' assertThat(wrapper).isNotNull(); - assertThat(wrapper.get()).isEqualTo(null); + assertThat(wrapper.get()).isNull(); // not changed assertThat(cache.get(key).get()).isEqualTo(value); } diff --git a/spring-context-support/src/test/java/org/springframework/cache/ehcache/EhCacheCacheManagerTests.java b/spring-context-support/src/test/java/org/springframework/cache/ehcache/EhCacheCacheManagerTests.java deleted file mode 100644 index fd33777c4bbf..000000000000 --- a/spring-context-support/src/test/java/org/springframework/cache/ehcache/EhCacheCacheManagerTests.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2002-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.cache.ehcache; - -import net.sf.ehcache.CacheManager; -import net.sf.ehcache.config.CacheConfiguration; -import net.sf.ehcache.config.Configuration; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; - -import org.springframework.cache.transaction.AbstractTransactionSupportingCacheManagerTests; - -/** - * @author Stephane Nicoll - */ -public class EhCacheCacheManagerTests extends AbstractTransactionSupportingCacheManagerTests { - - private CacheManager nativeCacheManager; - - private EhCacheCacheManager cacheManager; - - private EhCacheCacheManager transactionalCacheManager; - - - @BeforeEach - public void setup() { - nativeCacheManager = new CacheManager(new Configuration().name("EhCacheCacheManagerTests") - .defaultCache(new CacheConfiguration("default", 100))); - addNativeCache(CACHE_NAME); - - cacheManager = new EhCacheCacheManager(nativeCacheManager); - cacheManager.setTransactionAware(false); - cacheManager.afterPropertiesSet(); - - transactionalCacheManager = new EhCacheCacheManager(nativeCacheManager); - transactionalCacheManager.setTransactionAware(true); - transactionalCacheManager.afterPropertiesSet(); - } - - @AfterEach - public void shutdown() { - nativeCacheManager.shutdown(); - } - - - @Override - protected EhCacheCacheManager getCacheManager(boolean transactionAware) { - if (transactionAware) { - return transactionalCacheManager; - } - else { - return cacheManager; - } - } - - @Override - protected Class getCacheType() { - return EhCacheCache.class; - } - - @Override - protected void addNativeCache(String cacheName) { - nativeCacheManager.addCache(cacheName); - } - - @Override - protected void removeNativeCache(String cacheName) { - nativeCacheManager.removeCache(cacheName); - } - -} diff --git a/spring-context-support/src/test/java/org/springframework/cache/ehcache/EhCacheCacheTests.java b/spring-context-support/src/test/java/org/springframework/cache/ehcache/EhCacheCacheTests.java deleted file mode 100644 index b9bb1cc8542a..000000000000 --- a/spring-context-support/src/test/java/org/springframework/cache/ehcache/EhCacheCacheTests.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright 2002-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.cache.ehcache; - -import net.sf.ehcache.CacheManager; -import net.sf.ehcache.Ehcache; -import net.sf.ehcache.Element; -import net.sf.ehcache.config.CacheConfiguration; -import net.sf.ehcache.config.Configuration; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import org.springframework.context.testfixture.cache.AbstractCacheTests; -import org.springframework.core.testfixture.EnabledForTestGroups; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING; - -/** - * @author Costin Leau - * @author Stephane Nicoll - * @author Juergen Hoeller - */ -public class EhCacheCacheTests extends AbstractCacheTests { - - private CacheManager cacheManager; - - private Ehcache nativeCache; - - private EhCacheCache cache; - - - @BeforeEach - public void setup() { - cacheManager = new CacheManager(new Configuration().name("EhCacheCacheTests") - .defaultCache(new CacheConfiguration("default", 100))); - nativeCache = new net.sf.ehcache.Cache(new CacheConfiguration(CACHE_NAME, 100)); - cacheManager.addCache(nativeCache); - - cache = new EhCacheCache(nativeCache); - } - - @AfterEach - public void shutdown() { - cacheManager.shutdown(); - } - - - @Override - protected EhCacheCache getCache() { - return cache; - } - - @Override - protected Ehcache getNativeCache() { - return nativeCache; - } - - - @Test - @EnabledForTestGroups(LONG_RUNNING) - public void testExpiredElements() throws Exception { - String key = "brancusi"; - String value = "constantin"; - Element brancusi = new Element(key, value); - // ttl = 10s - brancusi.setTimeToLive(3); - nativeCache.put(brancusi); - - assertThat(cache.get(key).get()).isEqualTo(value); - // wait for the entry to expire - Thread.sleep(5 * 1000); - assertThat(cache.get(key)).isNull(); - } - -} diff --git a/spring-context-support/src/test/java/org/springframework/cache/ehcache/EhCacheSupportTests.java b/spring-context-support/src/test/java/org/springframework/cache/ehcache/EhCacheSupportTests.java deleted file mode 100644 index c663b0fba335..000000000000 --- a/spring-context-support/src/test/java/org/springframework/cache/ehcache/EhCacheSupportTests.java +++ /dev/null @@ -1,287 +0,0 @@ -/* - * Copyright 2002-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.cache.ehcache; - -import net.sf.ehcache.Cache; -import net.sf.ehcache.CacheException; -import net.sf.ehcache.CacheManager; -import net.sf.ehcache.Ehcache; -import net.sf.ehcache.config.CacheConfiguration; -import net.sf.ehcache.constructs.blocking.BlockingCache; -import net.sf.ehcache.constructs.blocking.SelfPopulatingCache; -import net.sf.ehcache.constructs.blocking.UpdatingCacheEntryFactory; -import net.sf.ehcache.constructs.blocking.UpdatingSelfPopulatingCache; -import org.junit.jupiter.api.Test; - -import org.springframework.core.io.ClassPathResource; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - -/** - * @author Juergen Hoeller - * @author Dmitriy Kopylenko - * @since 27.09.2004 - */ -public class EhCacheSupportTests { - - @Test - public void testBlankCacheManager() { - EhCacheManagerFactoryBean cacheManagerFb = new EhCacheManagerFactoryBean(); - cacheManagerFb.setCacheManagerName("myCacheManager"); - assertThat(cacheManagerFb.getObjectType()).isEqualTo(CacheManager.class); - assertThat(cacheManagerFb.isSingleton()).as("Singleton property").isTrue(); - cacheManagerFb.afterPropertiesSet(); - try { - CacheManager cm = cacheManagerFb.getObject(); - assertThat(cm.getCacheNames().length == 0).as("Loaded CacheManager with no caches").isTrue(); - Cache myCache1 = cm.getCache("myCache1"); - assertThat(myCache1 == null).as("No myCache1 defined").isTrue(); - } - finally { - cacheManagerFb.destroy(); - } - } - - @Test - public void testCacheManagerConflict() { - EhCacheManagerFactoryBean cacheManagerFb = new EhCacheManagerFactoryBean(); - try { - cacheManagerFb.setCacheManagerName("myCacheManager"); - assertThat(cacheManagerFb.getObjectType()).isEqualTo(CacheManager.class); - assertThat(cacheManagerFb.isSingleton()).as("Singleton property").isTrue(); - cacheManagerFb.afterPropertiesSet(); - CacheManager cm = cacheManagerFb.getObject(); - assertThat(cm.getCacheNames().length == 0).as("Loaded CacheManager with no caches").isTrue(); - Cache myCache1 = cm.getCache("myCache1"); - assertThat(myCache1 == null).as("No myCache1 defined").isTrue(); - - EhCacheManagerFactoryBean cacheManagerFb2 = new EhCacheManagerFactoryBean(); - cacheManagerFb2.setCacheManagerName("myCacheManager"); - assertThatExceptionOfType(CacheException.class).as("because of naming conflict").isThrownBy( - cacheManagerFb2::afterPropertiesSet); - } - finally { - cacheManagerFb.destroy(); - } - } - - @Test - public void testAcceptExistingCacheManager() { - EhCacheManagerFactoryBean cacheManagerFb = new EhCacheManagerFactoryBean(); - cacheManagerFb.setCacheManagerName("myCacheManager"); - assertThat(cacheManagerFb.getObjectType()).isEqualTo(CacheManager.class); - assertThat(cacheManagerFb.isSingleton()).as("Singleton property").isTrue(); - cacheManagerFb.afterPropertiesSet(); - try { - CacheManager cm = cacheManagerFb.getObject(); - assertThat(cm.getCacheNames().length == 0).as("Loaded CacheManager with no caches").isTrue(); - Cache myCache1 = cm.getCache("myCache1"); - assertThat(myCache1 == null).as("No myCache1 defined").isTrue(); - - EhCacheManagerFactoryBean cacheManagerFb2 = new EhCacheManagerFactoryBean(); - cacheManagerFb2.setCacheManagerName("myCacheManager"); - cacheManagerFb2.setAcceptExisting(true); - cacheManagerFb2.afterPropertiesSet(); - CacheManager cm2 = cacheManagerFb2.getObject(); - assertThat(cm2).isSameAs(cm); - cacheManagerFb2.destroy(); - } - finally { - cacheManagerFb.destroy(); - } - } - - public void testCacheManagerFromConfigFile() { - EhCacheManagerFactoryBean cacheManagerFb = new EhCacheManagerFactoryBean(); - cacheManagerFb.setConfigLocation(new ClassPathResource("testEhcache.xml", getClass())); - cacheManagerFb.setCacheManagerName("myCacheManager"); - cacheManagerFb.afterPropertiesSet(); - try { - CacheManager cm = cacheManagerFb.getObject(); - assertThat(cm.getCacheNames().length == 1).as("Correct number of caches loaded").isTrue(); - Cache myCache1 = cm.getCache("myCache1"); - assertThat(myCache1.getCacheConfiguration().isEternal()).as("myCache1 is not eternal").isFalse(); - assertThat(myCache1.getCacheConfiguration().getMaxEntriesLocalHeap() == 300).as("myCache1.maxElements == 300").isTrue(); - } - finally { - cacheManagerFb.destroy(); - } - } - - @Test - public void testEhCacheFactoryBeanWithDefaultCacheManager() { - doTestEhCacheFactoryBean(false); - } - - @Test - public void testEhCacheFactoryBeanWithExplicitCacheManager() { - doTestEhCacheFactoryBean(true); - } - - private void doTestEhCacheFactoryBean(boolean useCacheManagerFb) { - Cache cache; - EhCacheManagerFactoryBean cacheManagerFb = null; - boolean cacheManagerFbInitialized = false; - try { - EhCacheFactoryBean cacheFb = new EhCacheFactoryBean(); - Class objectType = cacheFb.getObjectType(); - assertThat(Ehcache.class.isAssignableFrom(objectType)).isTrue(); - assertThat(cacheFb.isSingleton()).as("Singleton property").isTrue(); - if (useCacheManagerFb) { - cacheManagerFb = new EhCacheManagerFactoryBean(); - cacheManagerFb.setConfigLocation(new ClassPathResource("testEhcache.xml", getClass())); - cacheManagerFb.setCacheManagerName("cache"); - cacheManagerFb.afterPropertiesSet(); - cacheManagerFbInitialized = true; - cacheFb.setCacheManager(cacheManagerFb.getObject()); - } - - cacheFb.setCacheName("myCache1"); - cacheFb.afterPropertiesSet(); - cache = (Cache) cacheFb.getObject(); - Class objectType2 = cacheFb.getObjectType(); - assertThat(objectType2).isSameAs(objectType); - CacheConfiguration config = cache.getCacheConfiguration(); - assertThat(cache.getName()).isEqualTo("myCache1"); - if (useCacheManagerFb){ - assertThat(config.getMaxEntriesLocalHeap()).as("myCache1.maxElements").isEqualTo(300); - } - else { - assertThat(config.getMaxEntriesLocalHeap()).as("myCache1.maxElements").isEqualTo(10000); - } - - // Cache region is not defined. Should create one with default properties. - cacheFb = new EhCacheFactoryBean(); - if (useCacheManagerFb) { - cacheFb.setCacheManager(cacheManagerFb.getObject()); - } - cacheFb.setCacheName("undefinedCache"); - cacheFb.afterPropertiesSet(); - cache = (Cache) cacheFb.getObject(); - config = cache.getCacheConfiguration(); - assertThat(cache.getName()).isEqualTo("undefinedCache"); - assertThat(config.getMaxEntriesLocalHeap() == 10000).as("default maxElements is correct").isTrue(); - assertThat(config.isEternal()).as("default eternal is correct").isFalse(); - assertThat(config.getTimeToLiveSeconds() == 120).as("default timeToLive is correct").isTrue(); - assertThat(config.getTimeToIdleSeconds() == 120).as("default timeToIdle is correct").isTrue(); - assertThat(config.getDiskExpiryThreadIntervalSeconds() == 120).as("default diskExpiryThreadIntervalSeconds is correct").isTrue(); - - // overriding the default properties - cacheFb = new EhCacheFactoryBean(); - if (useCacheManagerFb) { - cacheFb.setCacheManager(cacheManagerFb.getObject()); - } - cacheFb.setBeanName("undefinedCache2"); - cacheFb.setMaxEntriesLocalHeap(5); - cacheFb.setTimeToLive(8); - cacheFb.setTimeToIdle(7); - cacheFb.setDiskExpiryThreadIntervalSeconds(10); - cacheFb.afterPropertiesSet(); - cache = (Cache) cacheFb.getObject(); - config = cache.getCacheConfiguration(); - - assertThat(cache.getName()).isEqualTo("undefinedCache2"); - assertThat(config.getMaxEntriesLocalHeap() == 5).as("overridden maxElements is correct").isTrue(); - assertThat(config.getTimeToLiveSeconds() == 8).as("default timeToLive is correct").isTrue(); - assertThat(config.getTimeToIdleSeconds() == 7).as("default timeToIdle is correct").isTrue(); - assertThat(config.getDiskExpiryThreadIntervalSeconds() == 10).as("overridden diskExpiryThreadIntervalSeconds is correct").isTrue(); - } - finally { - if (cacheManagerFbInitialized) { - cacheManagerFb.destroy(); - } - else { - CacheManager.getInstance().shutdown(); - } - } - } - - @Test - public void testEhCacheFactoryBeanWithBlockingCache() { - EhCacheManagerFactoryBean cacheManagerFb = new EhCacheManagerFactoryBean(); - cacheManagerFb.afterPropertiesSet(); - try { - CacheManager cm = cacheManagerFb.getObject(); - EhCacheFactoryBean cacheFb = new EhCacheFactoryBean(); - cacheFb.setCacheManager(cm); - cacheFb.setCacheName("myCache1"); - cacheFb.setBlocking(true); - assertThat(BlockingCache.class).isEqualTo(cacheFb.getObjectType()); - cacheFb.afterPropertiesSet(); - Ehcache myCache1 = cm.getEhcache("myCache1"); - boolean condition = myCache1 instanceof BlockingCache; - assertThat(condition).isTrue(); - } - finally { - cacheManagerFb.destroy(); - } - } - - @Test - public void testEhCacheFactoryBeanWithSelfPopulatingCache() { - EhCacheManagerFactoryBean cacheManagerFb = new EhCacheManagerFactoryBean(); - cacheManagerFb.afterPropertiesSet(); - try { - CacheManager cm = cacheManagerFb.getObject(); - EhCacheFactoryBean cacheFb = new EhCacheFactoryBean(); - cacheFb.setCacheManager(cm); - cacheFb.setCacheName("myCache1"); - cacheFb.setCacheEntryFactory(key -> key); - assertThat(SelfPopulatingCache.class).isEqualTo(cacheFb.getObjectType()); - cacheFb.afterPropertiesSet(); - Ehcache myCache1 = cm.getEhcache("myCache1"); - boolean condition = myCache1 instanceof SelfPopulatingCache; - assertThat(condition).isTrue(); - assertThat(myCache1.get("myKey1").getObjectValue()).isEqualTo("myKey1"); - } - finally { - cacheManagerFb.destroy(); - } - } - - @Test - public void testEhCacheFactoryBeanWithUpdatingSelfPopulatingCache() { - EhCacheManagerFactoryBean cacheManagerFb = new EhCacheManagerFactoryBean(); - cacheManagerFb.afterPropertiesSet(); - try { - CacheManager cm = cacheManagerFb.getObject(); - EhCacheFactoryBean cacheFb = new EhCacheFactoryBean(); - cacheFb.setCacheManager(cm); - cacheFb.setCacheName("myCache1"); - cacheFb.setCacheEntryFactory(new UpdatingCacheEntryFactory() { - @Override - public Object createEntry(Object key) { - return key; - } - @Override - public void updateEntryValue(Object key, Object value) { - } - }); - assertThat(UpdatingSelfPopulatingCache.class).isEqualTo(cacheFb.getObjectType()); - cacheFb.afterPropertiesSet(); - Ehcache myCache1 = cm.getEhcache("myCache1"); - boolean condition = myCache1 instanceof UpdatingSelfPopulatingCache; - assertThat(condition).isTrue(); - assertThat(myCache1.get("myKey1").getObjectValue()).isEqualTo("myKey1"); - } - finally { - cacheManagerFb.destroy(); - } - } - -} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCache3AnnotationTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCache3AnnotationTests.java deleted file mode 100644 index 105e4e620995..000000000000 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCache3AnnotationTests.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2002-2017 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.cache.jcache; - -import javax.cache.Caching; -import javax.cache.spi.CachingProvider; - -/** - * Just here to be run against EHCache 3, whereas the original JCacheEhCacheAnnotationTests - * runs against EhCache 2.x with the EhCache-JCache add-on. - * - * @author Juergen Hoeller - */ -public class JCacheEhCache3AnnotationTests extends JCacheEhCacheAnnotationTests { - - @Override - protected CachingProvider getCachingProvider() { - return Caching.getCachingProvider("org.ehcache.jsr107.EhcacheCachingProvider"); - } - -} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCache3ApiTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCache3ApiTests.java deleted file mode 100644 index 32a2585c9f85..000000000000 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCache3ApiTests.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2002-2017 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.cache.jcache; - -import javax.cache.Caching; -import javax.cache.spi.CachingProvider; - -/** - * Just here to be run against EHCache 3, whereas the original JCacheEhCacheAnnotationTests - * runs against EhCache 2.x with the EhCache-JCache add-on. - * - * @author Stephane Nicoll - */ -public class JCacheEhCache3ApiTests extends JCacheEhCacheApiTests { - - @Override - protected CachingProvider getCachingProvider() { - return Caching.getCachingProvider("org.ehcache.jsr107.EhcacheCachingProvider"); - } - -} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCacheAnnotationTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCacheAnnotationTests.java index 188cc293c974..e77e948c802b 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCacheAnnotationTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCacheAnnotationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -26,7 +26,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.cache.annotation.CachingConfigurerSupport; +import org.springframework.cache.annotation.CachingConfigurer; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.interceptor.KeyGenerator; import org.springframework.cache.interceptor.SimpleKeyGenerator; @@ -64,7 +64,7 @@ protected ConfigurableApplicationContext getApplicationContext() { } protected CachingProvider getCachingProvider() { - return Caching.getCachingProvider("org.ehcache.jcache.JCacheCachingProvider"); + return Caching.getCachingProvider("org.ehcache.jsr107.EhcacheCachingProvider"); } @AfterEach @@ -104,7 +104,7 @@ public void testEvictAllEarlyWithTransaction() { @Configuration @EnableCaching - static class EnableCachingConfig extends CachingConfigurerSupport { + static class EnableCachingConfig implements CachingConfigurer { @Autowired CachingProvider cachingProvider; diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCacheApiTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCacheApiTests.java index b826e2a3fb37..3f56f2e06c5a 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCacheApiTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCacheApiTests.java @@ -54,7 +54,7 @@ public void setup() { } protected CachingProvider getCachingProvider() { - return Caching.getCachingProvider("org.ehcache.jcache.JCacheCachingProvider"); + return Caching.getCachingProvider("org.ehcache.jsr107.EhcacheCachingProvider"); } @AfterEach diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheCustomInterceptorTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheCustomInterceptorTests.java index 6d5714d09574..c5eb097dc082 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheCustomInterceptorTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheCustomInterceptorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -40,7 +40,7 @@ import org.springframework.contextsupport.testfixture.jcache.JCacheableService; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatRuntimeException; /** * @author Stephane Nicoll @@ -86,8 +86,8 @@ public void customInterceptorAppliesWithRuntimeException() { @Test public void customInterceptorAppliesWithCheckedException() { - assertThatExceptionOfType(RuntimeException.class).isThrownBy(() -> - cs.cacheWithCheckedException("id", true)) + assertThatRuntimeException() + .isThrownBy(() -> cs.cacheWithCheckedException("id", true)) .withCauseExactlyInstanceOf(IOException.class); } diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheJavaConfigTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheJavaConfigTests.java index 5c12aceac022..1dcc12540d1e 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheJavaConfigTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheJavaConfigTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -180,7 +180,7 @@ public CacheResolver exceptionCacheResolver() { @Configuration @EnableCaching - public static class EmptyConfigSupportConfig extends JCacheConfigurerSupport { + public static class EmptyConfigSupportConfig implements JCacheConfigurer { @Bean public CacheManager cm() { return new NoOpCacheManager(); @@ -190,7 +190,7 @@ public CacheManager cm() { @Configuration @EnableCaching - static class FullCachingConfigSupport extends JCacheConfigurerSupport { + static class FullCachingConfigSupport implements JCacheConfigurer { @Override @Bean @@ -220,7 +220,7 @@ public CacheResolver exceptionCacheResolver() { @Configuration @EnableCaching - static class NoExceptionCacheResolverConfig extends JCacheConfigurerSupport { + static class NoExceptionCacheResolverConfig implements JCacheConfigurer { @Override @Bean diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/AbstractCacheOperationTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/AbstractCacheOperationTests.java index c7a0cfcd4776..18ca02df38f8 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/AbstractCacheOperationTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/AbstractCacheOperationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -62,7 +62,7 @@ protected CacheMethodDetails create(Class annotatio Class targetType, String methodName, Class... parameterTypes) { Method method = ReflectionUtils.findMethod(targetType, methodName, parameterTypes); - Assert.notNull(method, "requested method '" + methodName + "'does not exist"); + Assert.notNull(method, () -> "requested method '" + methodName + "'does not exist"); A cacheAnnotation = method.getAnnotation(annotationType); return new DefaultCacheMethodDetails<>(method, cacheAnnotation, getCacheName(cacheAnnotation)); } diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/AnnotationCacheOperationSourceTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/AnnotationCacheOperationSourceTests.java index bcc9276243bd..693db9b13c71 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/AnnotationCacheOperationSourceTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/AnnotationCacheOperationSourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -202,7 +202,7 @@ protected > T getCacheOperation( private JCacheOperation getCacheOperation(Class targetType, String methodName, Class... parameterTypes) { Method method = ReflectionUtils.findMethod(targetType, methodName, parameterTypes); - Assert.notNull(method, "requested method '" + methodName + "'does not exist"); + Assert.notNull(method, () -> "requested method '" + methodName + "'does not exist"); return source.getCacheOperation(method, targetType); } diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheErrorHandlerTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheErrorHandlerTests.java index 3b341a0c4df0..561c0af54d1b 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheErrorHandlerTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheErrorHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -34,7 +34,7 @@ import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.interceptor.CacheErrorHandler; import org.springframework.cache.interceptor.SimpleKeyGenerator; -import org.springframework.cache.jcache.config.JCacheConfigurerSupport; +import org.springframework.cache.jcache.config.JCacheConfigurer; import org.springframework.cache.support.SimpleCacheManager; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; @@ -141,7 +141,7 @@ public void clearFail() { @Configuration @EnableCaching - static class Config extends JCacheConfigurerSupport { + static class Config implements JCacheConfigurer { @Bean @Override diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheKeyGeneratorTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheKeyGeneratorTests.java index dd5497764b0c..35db912df971 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheKeyGeneratorTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheKeyGeneratorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -34,7 +34,7 @@ import org.springframework.cache.interceptor.KeyGenerator; import org.springframework.cache.interceptor.SimpleKey; import org.springframework.cache.interceptor.SimpleKeyGenerator; -import org.springframework.cache.jcache.config.JCacheConfigurerSupport; +import org.springframework.cache.jcache.config.JCacheConfigurer; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -97,7 +97,7 @@ public void getFiltered() { @Configuration @EnableCaching - static class Config extends JCacheConfigurerSupport { + static class Config implements JCacheConfigurer { @Bean @Override @@ -151,7 +151,7 @@ private void expect(Object... params) { @Override public Object generate(Object target, Method method, Object... params) { assertThat(Arrays.equals(expectedParams, params)).as("Unexpected parameters: expected: " - + Arrays.toString(this.expectedParams) + " but got: " + Arrays.toString(params)).isTrue(); + + Arrays.toString(this.expectedParams) + " but got: " + Arrays.toString(params)).isTrue(); return new SimpleKey(params); } } diff --git a/spring-context-support/src/test/java/org/springframework/mail/javamail/JavaMailSenderTests.java b/spring-context-support/src/test/java/org/springframework/mail/javamail/JavaMailSenderTests.java index 3bd467b9106e..503044c97838 100644 --- a/spring-context-support/src/test/java/org/springframework/mail/javamail/JavaMailSenderTests.java +++ b/spring-context-support/src/test/java/org/springframework/mail/javamail/JavaMailSenderTests.java @@ -24,18 +24,17 @@ import java.util.List; import java.util.Properties; -import javax.activation.FileTypeMap; -import javax.mail.Address; -import javax.mail.Message; -import javax.mail.MessagingException; -import javax.mail.NoSuchProviderException; -import javax.mail.Session; -import javax.mail.Transport; -import javax.mail.URLName; -import javax.mail.internet.AddressException; -import javax.mail.internet.InternetAddress; -import javax.mail.internet.MimeMessage; - +import jakarta.activation.FileTypeMap; +import jakarta.mail.Address; +import jakarta.mail.Message; +import jakarta.mail.MessagingException; +import jakarta.mail.NoSuchProviderException; +import jakarta.mail.Session; +import jakarta.mail.Transport; +import jakarta.mail.URLName; +import jakarta.mail.internet.AddressException; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.MimeMessage; import org.junit.jupiter.api.Test; import org.springframework.mail.MailParseException; @@ -182,12 +181,9 @@ public void javaMailSenderWithMimeMessagePreparator() { final List messages = new ArrayList<>(); - MimeMessagePreparator preparator = new MimeMessagePreparator() { - @Override - public void prepare(MimeMessage mimeMessage) throws MessagingException { - mimeMessage.setRecipient(Message.RecipientType.TO, new InternetAddress("you@mail.org")); - messages.add(mimeMessage); - } + MimeMessagePreparator preparator = mimeMessage -> { + mimeMessage.setRecipient(Message.RecipientType.TO, new InternetAddress("you@mail.org")); + messages.add(mimeMessage); }; sender.send(preparator); @@ -208,19 +204,13 @@ public void javaMailSenderWithMimeMessagePreparators() { final List messages = new ArrayList<>(); - MimeMessagePreparator preparator1 = new MimeMessagePreparator() { - @Override - public void prepare(MimeMessage mimeMessage) throws MessagingException { - mimeMessage.setRecipient(Message.RecipientType.TO, new InternetAddress("he@mail.org")); - messages.add(mimeMessage); - } + MimeMessagePreparator preparator1 = mimeMessage -> { + mimeMessage.setRecipient(Message.RecipientType.TO, new InternetAddress("he@mail.org")); + messages.add(mimeMessage); }; - MimeMessagePreparator preparator2 = new MimeMessagePreparator() { - @Override - public void prepare(MimeMessage mimeMessage) throws MessagingException { - mimeMessage.setRecipient(Message.RecipientType.TO, new InternetAddress("she@mail.org")); - messages.add(mimeMessage); - } + MimeMessagePreparator preparator2 = mimeMessage -> { + mimeMessage.setRecipient(Message.RecipientType.TO, new InternetAddress("she@mail.org")); + messages.add(mimeMessage); }; sender.send(preparator1, preparator2); @@ -323,12 +313,7 @@ public void javaMailSenderWithParseExceptionOnSimpleMessage() { @Test public void javaMailSenderWithParseExceptionOnMimeMessagePreparator() { MockJavaMailSender sender = new MockJavaMailSender(); - MimeMessagePreparator preparator = new MimeMessagePreparator() { - @Override - public void prepare(MimeMessage mimeMessage) throws MessagingException { - mimeMessage.setFrom(new InternetAddress("")); - } - }; + MimeMessagePreparator preparator = mimeMessage -> mimeMessage.setFrom(new InternetAddress("")); try { sender.send(preparator); } diff --git a/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSupportTests.java b/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSupportTests.java index 6c7d5b8691c7..9d461d2e400f 100644 --- a/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSupportTests.java +++ b/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSupportTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -18,6 +18,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.Properties; import javax.sql.DataSource; @@ -30,6 +31,7 @@ import org.quartz.SchedulerFactory; import org.quartz.impl.JobDetailImpl; import org.quartz.impl.SchedulerRepository; +import org.quartz.impl.jdbcjobstore.JobStoreTX; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; @@ -40,6 +42,8 @@ import org.springframework.core.task.TaskExecutor; import org.springframework.core.testfixture.EnabledForTestGroups; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -57,10 +61,10 @@ * @author Sam Brannen * @since 20.02.2004 */ -public class QuartzSupportTests { +class QuartzSupportTests { @Test - public void schedulerFactoryBeanWithApplicationContext() throws Exception { + void schedulerFactoryBeanWithApplicationContext() throws Exception { TestBean tb = new TestBean("tb", 99); StaticApplicationContext ac = new StaticApplicationContext(); @@ -97,7 +101,7 @@ protected Scheduler createScheduler(SchedulerFactory schedulerFactory, String sc @Test @EnabledForTestGroups(LONG_RUNNING) - public void schedulerWithTaskExecutor() throws Exception { + void schedulerWithTaskExecutor() throws Exception { CountingTaskExecutor taskExecutor = new CountingTaskExecutor(); DummyJob.count = 0; @@ -130,7 +134,7 @@ public void schedulerWithTaskExecutor() throws Exception { @Test @SuppressWarnings({ "unchecked", "rawtypes" }) - public void jobDetailWithRunnableInsteadOfJob() { + void jobDetailWithRunnableInsteadOfJob() { JobDetailImpl jobDetail = new JobDetailImpl(); assertThatIllegalArgumentException().isThrownBy(() -> jobDetail.setJobClass((Class) DummyRunnable.class)); @@ -138,7 +142,7 @@ public void jobDetailWithRunnableInsteadOfJob() { @Test @EnabledForTestGroups(LONG_RUNNING) - public void schedulerWithQuartzJobBean() throws Exception { + void schedulerWithQuartzJobBean() throws Exception { DummyJob.param = 0; DummyJob.count = 0; @@ -171,7 +175,7 @@ public void schedulerWithQuartzJobBean() throws Exception { @Test @EnabledForTestGroups(LONG_RUNNING) - public void schedulerWithSpringBeanJobFactory() throws Exception { + void schedulerWithSpringBeanJobFactory() throws Exception { DummyJob.param = 0; DummyJob.count = 0; @@ -206,7 +210,7 @@ public void schedulerWithSpringBeanJobFactory() throws Exception { @Test @EnabledForTestGroups(LONG_RUNNING) - public void schedulerWithSpringBeanJobFactoryAndParamMismatchNotIgnored() throws Exception { + void schedulerWithSpringBeanJobFactoryAndParamMismatchNotIgnored() throws Exception { DummyJob.param = 0; DummyJob.count = 0; @@ -242,7 +246,7 @@ public void schedulerWithSpringBeanJobFactoryAndParamMismatchNotIgnored() throws @Test @EnabledForTestGroups(LONG_RUNNING) - public void schedulerWithSpringBeanJobFactoryAndQuartzJobBean() throws Exception { + void schedulerWithSpringBeanJobFactoryAndQuartzJobBean() throws Exception { DummyJobBean.param = 0; DummyJobBean.count = 0; @@ -276,7 +280,7 @@ public void schedulerWithSpringBeanJobFactoryAndQuartzJobBean() throws Exception @Test @EnabledForTestGroups(LONG_RUNNING) - public void schedulerWithSpringBeanJobFactoryAndJobSchedulingData() throws Exception { + void schedulerWithSpringBeanJobFactoryAndJobSchedulingData() throws Exception { DummyJob.param = 0; DummyJob.count = 0; @@ -294,7 +298,7 @@ public void schedulerWithSpringBeanJobFactoryAndJobSchedulingData() throws Excep } @Test // SPR-772 - public void multipleSchedulers() throws Exception { + void multipleSchedulers() throws Exception { try (ClassPathXmlApplicationContext ctx = context("multipleSchedulers.xml")) { Scheduler scheduler1 = (Scheduler) ctx.getBean("scheduler1"); Scheduler scheduler2 = (Scheduler) ctx.getBean("scheduler2"); @@ -305,7 +309,7 @@ public void multipleSchedulers() throws Exception { } @Test // SPR-16884 - public void multipleSchedulersWithQuartzProperties() throws Exception { + void multipleSchedulersWithQuartzProperties() throws Exception { try (ClassPathXmlApplicationContext ctx = context("multipleSchedulersWithQuartzProperties.xml")) { Scheduler scheduler1 = (Scheduler) ctx.getBean("scheduler1"); Scheduler scheduler2 = (Scheduler) ctx.getBean("scheduler2"); @@ -317,12 +321,13 @@ public void multipleSchedulersWithQuartzProperties() throws Exception { @Test @EnabledForTestGroups(LONG_RUNNING) - public void twoAnonymousMethodInvokingJobDetailFactoryBeans() throws Exception { - Thread.sleep(3000); + void twoAnonymousMethodInvokingJobDetailFactoryBeans() throws Exception { try (ClassPathXmlApplicationContext ctx = context("multipleAnonymousMethodInvokingJobDetailFB.xml")) { QuartzTestBean exportService = (QuartzTestBean) ctx.getBean("exportService"); QuartzTestBean importService = (QuartzTestBean) ctx.getBean("importService"); + Thread.sleep(400); + assertThat(exportService.getImportCount()).as("doImport called exportService").isEqualTo(0); assertThat(exportService.getExportCount()).as("doExport not called on exportService").isEqualTo(2); assertThat(importService.getImportCount()).as("doImport not called on importService").isEqualTo(2); @@ -332,12 +337,13 @@ public void twoAnonymousMethodInvokingJobDetailFactoryBeans() throws Exception { @Test @EnabledForTestGroups(LONG_RUNNING) - public void schedulerAccessorBean() throws Exception { - Thread.sleep(3000); + void schedulerAccessorBean() throws Exception { try (ClassPathXmlApplicationContext ctx = context("schedulerAccessorBean.xml")) { QuartzTestBean exportService = (QuartzTestBean) ctx.getBean("exportService"); QuartzTestBean importService = (QuartzTestBean) ctx.getBean("importService"); + Thread.sleep(400); + assertThat(exportService.getImportCount()).as("doImport called exportService").isEqualTo(0); assertThat(exportService.getExportCount()).as("doExport not called on exportService").isEqualTo(2); assertThat(importService.getImportCount()).as("doImport not called on importService").isEqualTo(2); @@ -347,7 +353,7 @@ public void schedulerAccessorBean() throws Exception { @Test @SuppressWarnings("resource") - public void schedulerAutoStartsOnContextRefreshedEventByDefault() throws Exception { + void schedulerAutoStartsOnContextRefreshedEventByDefault() throws Exception { StaticApplicationContext context = new StaticApplicationContext(); context.registerBeanDefinition("scheduler", new RootBeanDefinition(SchedulerFactoryBean.class)); Scheduler bean = context.getBean("scheduler", Scheduler.class); @@ -358,7 +364,7 @@ public void schedulerAutoStartsOnContextRefreshedEventByDefault() throws Excepti @Test @SuppressWarnings("resource") - public void schedulerAutoStartupFalse() throws Exception { + void schedulerAutoStartupFalse() throws Exception { StaticApplicationContext context = new StaticApplicationContext(); BeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(SchedulerFactoryBean.class) .addPropertyValue("autoStartup", false).getBeanDefinition(); @@ -370,7 +376,7 @@ public void schedulerAutoStartupFalse() throws Exception { } @Test - public void schedulerRepositoryExposure() throws Exception { + void schedulerRepositoryExposure() throws Exception { try (ClassPathXmlApplicationContext ctx = context("schedulerRepositoryExposure.xml")) { assertThat(ctx.getBean("scheduler")).isSameAs(SchedulerRepository.getInstance().lookup("myScheduler")); } @@ -381,7 +387,7 @@ public void schedulerRepositoryExposure() throws Exception { * TODO: Against Quartz 2.2, this test's job doesn't actually execute anymore... */ @Test - public void schedulerWithHsqlDataSource() throws Exception { + void schedulerWithHsqlDataSource() throws Exception { DummyJob.param = 0; DummyJob.count = 0; @@ -396,12 +402,36 @@ public void schedulerWithHsqlDataSource() throws Exception { } } + @Test + @SuppressWarnings("resource") + void schedulerFactoryBeanWithCustomJobStore() throws Exception { + StaticApplicationContext context = new StaticApplicationContext(); + + String dbName = "mydb"; + EmbeddedDatabase database = new EmbeddedDatabaseBuilder().setName(dbName).build(); + + Properties properties = new Properties(); + properties.setProperty("org.quartz.jobStore.class", JobStoreTX.class.getName()); + properties.setProperty("org.quartz.jobStore.dataSource", dbName); + + BeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(SchedulerFactoryBean.class) + .addPropertyValue("autoStartup", false) + .addPropertyValue("dataSource", database) + .addPropertyValue("quartzProperties", properties) + .getBeanDefinition(); + context.registerBeanDefinition("scheduler", beanDefinition); + + Scheduler scheduler = context.getBean(Scheduler.class); + + assertThat(scheduler.getMetaData().getJobStoreClass()).isEqualTo(JobStoreTX.class); + } + private ClassPathXmlApplicationContext context(String path) { return new ClassPathXmlApplicationContext(path, getClass()); } - public static class CountingTaskExecutor implements TaskExecutor { + private static class CountingTaskExecutor implements TaskExecutor { private int count; @@ -413,12 +443,14 @@ public void execute(Runnable task) { } - public static class DummyJob implements Job { + private static class DummyJob implements Job { private static int param; private static int count; + @SuppressWarnings("unused") + // Must be public public void setParam(int value) { if (param > 0) { throw new IllegalStateException("Param already set"); @@ -433,12 +465,13 @@ public synchronized void execute(JobExecutionContext jobExecutionContext) throws } - public static class DummyJobBean extends QuartzJobBean { + private static class DummyJobBean extends QuartzJobBean { private static int param; private static int count; + @SuppressWarnings("unused") public void setParam(int value) { if (param > 0) { throw new IllegalStateException("Param already set"); @@ -453,7 +486,7 @@ protected synchronized void executeInternal(JobExecutionContext jobExecutionCont } - public static class DummyRunnable implements Runnable { + private static class DummyRunnable implements Runnable { @Override public void run() { diff --git a/spring-context-support/src/test/java/org/springframework/scheduling/quartz/SchedulerFactoryBeanRuntimeHintsTests.java b/spring-context-support/src/test/java/org/springframework/scheduling/quartz/SchedulerFactoryBeanRuntimeHintsTests.java new file mode 100644 index 000000000000..7e422ed9b331 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/scheduling/quartz/SchedulerFactoryBeanRuntimeHintsTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-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.scheduling.quartz; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.aot.hint.TypeReference; +import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; +import org.springframework.beans.factory.aot.AotServices; +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link SchedulerFactoryBeanRuntimeHints}. + * + * @author Sebastien Deleuze + */ +public class SchedulerFactoryBeanRuntimeHintsTests { + + private final RuntimeHints hints = new RuntimeHints(); + + @BeforeEach + void setup() { + AotServices.factories().load(RuntimeHintsRegistrar.class) + .forEach(registrar -> registrar.registerHints(this.hints, + ClassUtils.getDefaultClassLoader())); + } + + @Test + void stdSchedulerFactoryHasHints() { + assertThat(RuntimeHintsPredicates.reflection().onType(TypeReference.of("org.quartz.impl.StdSchedulerFactory")) + .withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)).accepts(this.hints); + } + + @Test + void defaultClassLoadHelperHasHints() { + assertThat(RuntimeHintsPredicates.reflection().onType(ResourceLoaderClassLoadHelper.class) + .withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)).accepts(this.hints); + } + + @Test + void defaultThreadPoolHasHints() { + assertThat(RuntimeHintsPredicates.reflection().onType(LocalTaskExecutorThreadPool.class) + .withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)) + .accepts(this.hints); + } + + @Test + void defaultJobStoreHasHints() { + assertThat(RuntimeHintsPredicates.reflection().onType(LocalDataSourceJobStore.class) + .withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)) + .accepts(this.hints); + } +} diff --git a/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/BeanValidationPostProcessorTests.java b/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/BeanValidationPostProcessorTests.java deleted file mode 100644 index c5da1e4b1521..000000000000 --- a/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/BeanValidationPostProcessorTests.java +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Copyright 2002-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.validation.beanvalidation2; - -import javax.annotation.PostConstruct; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; - -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.BeanCreationException; -import org.springframework.beans.factory.support.RootBeanDefinition; -import org.springframework.beans.testfixture.beans.TestBean; -import org.springframework.context.annotation.CommonAnnotationBeanPostProcessor; -import org.springframework.context.support.GenericApplicationContext; -import org.springframework.validation.beanvalidation.BeanValidationPostProcessor; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - -/** - * @author Juergen Hoeller - */ -public class BeanValidationPostProcessorTests { - - @Test - public void testNotNullConstraint() { - GenericApplicationContext ac = new GenericApplicationContext(); - ac.registerBeanDefinition("bvpp", new RootBeanDefinition(BeanValidationPostProcessor.class)); - ac.registerBeanDefinition("capp", new RootBeanDefinition(CommonAnnotationBeanPostProcessor.class)); - ac.registerBeanDefinition("bean", new RootBeanDefinition(NotNullConstrainedBean.class)); - assertThatExceptionOfType(BeanCreationException.class) - .isThrownBy(ac::refresh) - .havingRootCause() - .withMessageContainingAll("testBean", "invalid"); - ac.close(); - } - - @Test - public void testNotNullConstraintSatisfied() { - GenericApplicationContext ac = new GenericApplicationContext(); - ac.registerBeanDefinition("bvpp", new RootBeanDefinition(BeanValidationPostProcessor.class)); - ac.registerBeanDefinition("capp", new RootBeanDefinition(CommonAnnotationBeanPostProcessor.class)); - RootBeanDefinition bd = new RootBeanDefinition(NotNullConstrainedBean.class); - bd.getPropertyValues().add("testBean", new TestBean()); - ac.registerBeanDefinition("bean", bd); - ac.refresh(); - ac.close(); - } - - @Test - public void testNotNullConstraintAfterInitialization() { - GenericApplicationContext ac = new GenericApplicationContext(); - RootBeanDefinition bvpp = new RootBeanDefinition(BeanValidationPostProcessor.class); - bvpp.getPropertyValues().add("afterInitialization", true); - ac.registerBeanDefinition("bvpp", bvpp); - ac.registerBeanDefinition("capp", new RootBeanDefinition(CommonAnnotationBeanPostProcessor.class)); - ac.registerBeanDefinition("bean", new RootBeanDefinition(AfterInitConstraintBean.class)); - ac.refresh(); - ac.close(); - } - - @Test - public void testSizeConstraint() { - GenericApplicationContext ac = new GenericApplicationContext(); - ac.registerBeanDefinition("bvpp", new RootBeanDefinition(BeanValidationPostProcessor.class)); - RootBeanDefinition bd = new RootBeanDefinition(NotNullConstrainedBean.class); - bd.getPropertyValues().add("testBean", new TestBean()); - bd.getPropertyValues().add("stringValue", "s"); - ac.registerBeanDefinition("bean", bd); - assertThatExceptionOfType(BeanCreationException.class) - .isThrownBy(ac::refresh) - .havingRootCause() - .withMessageContainingAll("stringValue", "invalid"); - ac.close(); - } - - @Test - public void testSizeConstraintSatisfied() { - GenericApplicationContext ac = new GenericApplicationContext(); - ac.registerBeanDefinition("bvpp", new RootBeanDefinition(BeanValidationPostProcessor.class)); - RootBeanDefinition bd = new RootBeanDefinition(NotNullConstrainedBean.class); - bd.getPropertyValues().add("testBean", new TestBean()); - bd.getPropertyValues().add("stringValue", "ss"); - ac.registerBeanDefinition("bean", bd); - ac.refresh(); - ac.close(); - } - - - public static class NotNullConstrainedBean { - - @NotNull - private TestBean testBean; - - @Size(min = 2) - private String stringValue; - - public TestBean getTestBean() { - return testBean; - } - - public void setTestBean(TestBean testBean) { - this.testBean = testBean; - } - - public String getStringValue() { - return stringValue; - } - - public void setStringValue(String stringValue) { - this.stringValue = stringValue; - } - - @PostConstruct - public void init() { - assertThat(this.testBean).as("Shouldn't be here after constraint checking").isNotNull(); - } - } - - - public static class AfterInitConstraintBean { - - @NotNull - private TestBean testBean; - - public TestBean getTestBean() { - return testBean; - } - - public void setTestBean(TestBean testBean) { - this.testBean = testBean; - } - - @PostConstruct - public void init() { - this.testBean = new TestBean(); - } - } - -} diff --git a/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/MethodValidationTests.java b/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/MethodValidationTests.java deleted file mode 100644 index 763bc156f684..000000000000 --- a/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/MethodValidationTests.java +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Copyright 2002-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.validation.beanvalidation2; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -import javax.validation.ValidationException; -import javax.validation.Validator; -import javax.validation.constraints.Max; -import javax.validation.constraints.NotNull; -import javax.validation.groups.Default; - -import org.junit.jupiter.api.Test; - -import org.springframework.aop.framework.ProxyFactory; -import org.springframework.beans.MutablePropertyValues; -import org.springframework.beans.factory.FactoryBean; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Lazy; -import org.springframework.context.support.StaticApplicationContext; -import org.springframework.scheduling.annotation.Async; -import org.springframework.scheduling.annotation.AsyncAnnotationAdvisor; -import org.springframework.scheduling.annotation.AsyncAnnotationBeanPostProcessor; -import org.springframework.validation.annotation.Validated; -import org.springframework.validation.beanvalidation.CustomValidatorBean; -import org.springframework.validation.beanvalidation.MethodValidationInterceptor; -import org.springframework.validation.beanvalidation.MethodValidationPostProcessor; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - -/** - * @author Juergen Hoeller - */ -public class MethodValidationTests { - - @Test - @SuppressWarnings("unchecked") - public void testMethodValidationInterceptor() { - MyValidBean bean = new MyValidBean(); - ProxyFactory proxyFactory = new ProxyFactory(bean); - proxyFactory.addAdvice(new MethodValidationInterceptor()); - proxyFactory.addAdvisor(new AsyncAnnotationAdvisor()); - doTestProxyValidation((MyValidInterface) proxyFactory.getProxy()); - } - - @Test - @SuppressWarnings("unchecked") - public void testMethodValidationPostProcessor() { - StaticApplicationContext ac = new StaticApplicationContext(); - ac.registerSingleton("mvpp", MethodValidationPostProcessor.class); - MutablePropertyValues pvs = new MutablePropertyValues(); - pvs.add("beforeExistingAdvisors", false); - ac.registerSingleton("aapp", AsyncAnnotationBeanPostProcessor.class, pvs); - ac.registerSingleton("bean", MyValidBean.class); - ac.refresh(); - doTestProxyValidation(ac.getBean("bean", MyValidInterface.class)); - ac.close(); - } - - private void doTestProxyValidation(MyValidInterface proxy) { - assertThat(proxy.myValidMethod("value", 5)).isNotNull(); - assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> - proxy.myValidMethod("value", 15)); - assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> - proxy.myValidMethod(null, 5)); - assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> - proxy.myValidMethod("value", 0)); - proxy.myValidAsyncMethod("value", 5); - assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> - proxy.myValidAsyncMethod("value", 15)); - assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> - proxy.myValidAsyncMethod(null, 5)); - assertThat(proxy.myGenericMethod("myValue")).isEqualTo("myValue"); - assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> - proxy.myGenericMethod(null)); - } - - @Test - public void testLazyValidatorForMethodValidation() { - @SuppressWarnings("resource") - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext( - LazyMethodValidationConfig.class, CustomValidatorBean.class, - MyValidBean.class, MyValidFactoryBean.class); - ctx.getBeansOfType(MyValidInterface.class).values().forEach(bean -> bean.myValidMethod("value", 5)); - } - - @Test - public void testLazyValidatorForMethodValidationWithProxyTargetClass() { - @SuppressWarnings("resource") - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext( - LazyMethodValidationConfigWithProxyTargetClass.class, CustomValidatorBean.class, - MyValidBean.class, MyValidFactoryBean.class); - ctx.getBeansOfType(MyValidInterface.class).values().forEach(bean -> bean.myValidMethod("value", 5)); - } - - - @MyStereotype - public static class MyValidBean implements MyValidInterface { - - @Override - public Object myValidMethod(String arg1, int arg2) { - return (arg2 == 0 ? null : "value"); - } - - @Override - public void myValidAsyncMethod(String arg1, int arg2) { - } - - @Override - public String myGenericMethod(String value) { - return value; - } - } - - - @MyStereotype - public static class MyValidFactoryBean implements FactoryBean, MyValidInterface { - - @Override - public String getObject() { - return null; - } - - @Override - public Class getObjectType() { - return String.class; - } - - @Override - public Object myValidMethod(String arg1, int arg2) { - return (arg2 == 0 ? null : "value"); - } - - @Override - public void myValidAsyncMethod(String arg1, int arg2) { - } - - @Override - public String myGenericMethod(String value) { - return value; - } - } - - - public interface MyValidInterface { - - @NotNull Object myValidMethod(@NotNull(groups = MyGroup.class) String arg1, @Max(10) int arg2); - - @MyValid - @Async void myValidAsyncMethod(@NotNull(groups = OtherGroup.class) String arg1, @Max(10) int arg2); - - T myGenericMethod(@NotNull T value); - } - - - public interface MyGroup { - } - - - public interface OtherGroup { - } - - - @Validated({MyGroup.class, Default.class}) - @Retention(RetentionPolicy.RUNTIME) - public @interface MyStereotype { - } - - - @Validated({OtherGroup.class, Default.class}) - @Retention(RetentionPolicy.RUNTIME) - public @interface MyValid { - } - - - @Configuration - public static class LazyMethodValidationConfig { - - @Bean - public static MethodValidationPostProcessor methodValidationPostProcessor(@Lazy Validator validator) { - MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor(); - postProcessor.setValidator(validator); - return postProcessor; - } - } - - - @Configuration - public static class LazyMethodValidationConfigWithProxyTargetClass { - - @Bean - public static MethodValidationPostProcessor methodValidationPostProcessor(@Lazy Validator validator) { - MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor(); - postProcessor.setValidator(validator); - postProcessor.setProxyTargetClass(true); - return postProcessor; - } - } - -} diff --git a/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/SpringValidatorAdapterTests.java b/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/SpringValidatorAdapterTests.java deleted file mode 100644 index 813111adb05c..000000000000 --- a/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/SpringValidatorAdapterTests.java +++ /dev/null @@ -1,563 +0,0 @@ -/* - * Copyright 2002-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.validation.beanvalidation2; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Inherited; -import java.lang.annotation.Repeatable; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; - -import javax.validation.Constraint; -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; -import javax.validation.ConstraintViolation; -import javax.validation.Payload; -import javax.validation.Valid; -import javax.validation.Validation; -import javax.validation.Validator; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Pattern; -import javax.validation.constraints.Size; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import org.springframework.beans.BeanWrapper; -import org.springframework.beans.BeanWrapperImpl; -import org.springframework.context.support.StaticMessageSource; -import org.springframework.core.testfixture.io.SerializationTestUtils; -import org.springframework.util.ObjectUtils; -import org.springframework.validation.BeanPropertyBindingResult; -import org.springframework.validation.FieldError; -import org.springframework.validation.beanvalidation.SpringValidatorAdapter; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Kazuki Shimizu - * @author Juergen Hoeller - */ -public class SpringValidatorAdapterTests { - - private final Validator nativeValidator = Validation.buildDefaultValidatorFactory().getValidator(); - - private final SpringValidatorAdapter validatorAdapter = new SpringValidatorAdapter(nativeValidator); - - private final StaticMessageSource messageSource = new StaticMessageSource(); - - - @BeforeEach - public void setupSpringValidatorAdapter() { - messageSource.addMessage("Size", Locale.ENGLISH, "Size of {0} must be between {2} and {1}"); - messageSource.addMessage("Same", Locale.ENGLISH, "{2} must be same value as {1}"); - messageSource.addMessage("password", Locale.ENGLISH, "Password"); - messageSource.addMessage("confirmPassword", Locale.ENGLISH, "Password(Confirm)"); - } - - - @Test - public void testUnwrap() { - Validator nativeValidator = validatorAdapter.unwrap(Validator.class); - assertThat(nativeValidator).isSameAs(this.nativeValidator); - } - - @Test // SPR-13406 - public void testNoStringArgumentValue() throws Exception { - TestBean testBean = new TestBean(); - testBean.setPassword("pass"); - testBean.setConfirmPassword("pass"); - - BeanPropertyBindingResult errors = new BeanPropertyBindingResult(testBean, "testBean"); - validatorAdapter.validate(testBean, errors); - - assertThat(errors.getFieldErrorCount("password")).isEqualTo(1); - assertThat(errors.getFieldValue("password")).isEqualTo("pass"); - FieldError error = errors.getFieldError("password"); - assertThat(error).isNotNull(); - assertThat(messageSource.getMessage(error, Locale.ENGLISH)).isEqualTo("Size of Password must be between 8 and 128"); - assertThat(error.contains(ConstraintViolation.class)).isTrue(); - assertThat(error.unwrap(ConstraintViolation.class).getPropertyPath().toString()).isEqualTo("password"); - assertThat(SerializationTestUtils.serializeAndDeserialize(error.toString())).isEqualTo(error.toString()); - } - - @Test // SPR-13406 - public void testApplyMessageSourceResolvableToStringArgumentValueWithResolvedLogicalFieldName() throws Exception { - TestBean testBean = new TestBean(); - testBean.setPassword("password"); - testBean.setConfirmPassword("PASSWORD"); - - BeanPropertyBindingResult errors = new BeanPropertyBindingResult(testBean, "testBean"); - validatorAdapter.validate(testBean, errors); - - assertThat(errors.getFieldErrorCount("password")).isEqualTo(1); - assertThat(errors.getFieldValue("password")).isEqualTo("password"); - FieldError error = errors.getFieldError("password"); - assertThat(error).isNotNull(); - assertThat(messageSource.getMessage(error, Locale.ENGLISH)).isEqualTo("Password must be same value as Password(Confirm)"); - assertThat(error.contains(ConstraintViolation.class)).isTrue(); - assertThat(error.unwrap(ConstraintViolation.class).getPropertyPath().toString()).isEqualTo("password"); - assertThat(SerializationTestUtils.serializeAndDeserialize(error.toString())).isEqualTo(error.toString()); - } - - @Test // SPR-13406 - public void testApplyMessageSourceResolvableToStringArgumentValueWithUnresolvedLogicalFieldName() { - TestBean testBean = new TestBean(); - testBean.setEmail("test@example.com"); - testBean.setConfirmEmail("TEST@EXAMPLE.IO"); - - BeanPropertyBindingResult errors = new BeanPropertyBindingResult(testBean, "testBean"); - validatorAdapter.validate(testBean, errors); - - assertThat(errors.getFieldErrorCount("email")).isEqualTo(1); - assertThat(errors.getFieldValue("email")).isEqualTo("test@example.com"); - assertThat(errors.getFieldErrorCount("confirmEmail")).isEqualTo(1); - FieldError error1 = errors.getFieldError("email"); - FieldError error2 = errors.getFieldError("confirmEmail"); - assertThat(error1).isNotNull(); - assertThat(error2).isNotNull(); - assertThat(messageSource.getMessage(error1, Locale.ENGLISH)).isEqualTo("email must be same value as confirmEmail"); - assertThat(messageSource.getMessage(error2, Locale.ENGLISH)).isEqualTo("Email required"); - assertThat(error1.contains(ConstraintViolation.class)).isTrue(); - assertThat(error1.unwrap(ConstraintViolation.class).getPropertyPath().toString()).isEqualTo("email"); - assertThat(error2.contains(ConstraintViolation.class)).isTrue(); - assertThat(error2.unwrap(ConstraintViolation.class).getPropertyPath().toString()).isEqualTo("confirmEmail"); - } - - @Test // SPR-15123 - public void testApplyMessageSourceResolvableToStringArgumentValueWithAlwaysUseMessageFormat() { - messageSource.setAlwaysUseMessageFormat(true); - - TestBean testBean = new TestBean(); - testBean.setEmail("test@example.com"); - testBean.setConfirmEmail("TEST@EXAMPLE.IO"); - - BeanPropertyBindingResult errors = new BeanPropertyBindingResult(testBean, "testBean"); - validatorAdapter.validate(testBean, errors); - - assertThat(errors.getFieldErrorCount("email")).isEqualTo(1); - assertThat(errors.getFieldValue("email")).isEqualTo("test@example.com"); - assertThat(errors.getFieldErrorCount("confirmEmail")).isEqualTo(1); - FieldError error1 = errors.getFieldError("email"); - FieldError error2 = errors.getFieldError("confirmEmail"); - assertThat(error1).isNotNull(); - assertThat(error2).isNotNull(); - assertThat(messageSource.getMessage(error1, Locale.ENGLISH)).isEqualTo("email must be same value as confirmEmail"); - assertThat(messageSource.getMessage(error2, Locale.ENGLISH)).isEqualTo("Email required"); - assertThat(error1.contains(ConstraintViolation.class)).isTrue(); - assertThat(error1.unwrap(ConstraintViolation.class).getPropertyPath().toString()).isEqualTo("email"); - assertThat(error2.contains(ConstraintViolation.class)).isTrue(); - assertThat(error2.unwrap(ConstraintViolation.class).getPropertyPath().toString()).isEqualTo("confirmEmail"); - } - - @Test - public void testPatternMessage() { - TestBean testBean = new TestBean(); - testBean.setEmail("X"); - testBean.setConfirmEmail("X"); - - BeanPropertyBindingResult errors = new BeanPropertyBindingResult(testBean, "testBean"); - validatorAdapter.validate(testBean, errors); - - assertThat(errors.getFieldErrorCount("email")).isEqualTo(1); - assertThat(errors.getFieldValue("email")).isEqualTo("X"); - FieldError error = errors.getFieldError("email"); - assertThat(error).isNotNull(); - assertThat(messageSource.getMessage(error, Locale.ENGLISH)).contains("[\\w.'-]{1,}@[\\w.'-]{1,}"); - assertThat(error.contains(ConstraintViolation.class)).isTrue(); - assertThat(error.unwrap(ConstraintViolation.class).getPropertyPath().toString()).isEqualTo("email"); - } - - @Test // SPR-16177 - public void testWithList() { - Parent parent = new Parent(); - parent.setName("Parent whit list"); - parent.getChildList().addAll(createChildren(parent)); - - BeanPropertyBindingResult errors = new BeanPropertyBindingResult(parent, "parent"); - validatorAdapter.validate(parent, errors); - - assertThat(errors.getErrorCount() > 0).isTrue(); - } - - @Test // SPR-16177 - public void testWithSet() { - Parent parent = new Parent(); - parent.setName("Parent with set"); - parent.getChildSet().addAll(createChildren(parent)); - - BeanPropertyBindingResult errors = new BeanPropertyBindingResult(parent, "parent"); - validatorAdapter.validate(parent, errors); - - assertThat(errors.getErrorCount() > 0).isTrue(); - } - - private List createChildren(Parent parent) { - Child child1 = new Child(); - child1.setName("Child1"); - child1.setAge(null); - child1.setParent(parent); - - Child child2 = new Child(); - child2.setName(null); - child2.setAge(17); - child2.setParent(parent); - - return Arrays.asList(child1, child2); - } - - @Test // SPR-15839 - public void testListElementConstraint() { - BeanWithListElementConstraint bean = new BeanWithListElementConstraint(); - bean.setProperty(Arrays.asList("no", "element", "can", "be", null)); - - BeanPropertyBindingResult errors = new BeanPropertyBindingResult(bean, "bean"); - validatorAdapter.validate(bean, errors); - - assertThat(errors.getFieldErrorCount("property[4]")).isEqualTo(1); - assertThat(errors.getFieldValue("property[4]")).isNull(); - } - - @Test // SPR-15839 - public void testMapValueConstraint() { - Map property = new HashMap<>(); - property.put("no value can be", null); - - BeanWithMapEntryConstraint bean = new BeanWithMapEntryConstraint(); - bean.setProperty(property); - - BeanPropertyBindingResult errors = new BeanPropertyBindingResult(bean, "bean"); - validatorAdapter.validate(bean, errors); - - assertThat(errors.getFieldErrorCount("property[no value can be]")).isEqualTo(1); - assertThat(errors.getFieldValue("property[no value can be]")).isNull(); - } - - @Test // SPR-15839 - public void testMapEntryConstraint() { - Map property = new HashMap<>(); - property.put(null, null); - - BeanWithMapEntryConstraint bean = new BeanWithMapEntryConstraint(); - bean.setProperty(property); - - BeanPropertyBindingResult errors = new BeanPropertyBindingResult(bean, "bean"); - validatorAdapter.validate(bean, errors); - - assertThat(errors.hasFieldErrors("property[]")).isTrue(); - assertThat(errors.getFieldValue("property[]")).isNull(); - } - - - @Same(field = "password", comparingField = "confirmPassword") - @Same(field = "email", comparingField = "confirmEmail") - static class TestBean { - - @Size(min = 8, max = 128) - private String password; - - private String confirmPassword; - - @Pattern(regexp = "[\\w.'-]{1,}@[\\w.'-]{1,}") - private String email; - - @Pattern(regexp = "[\\p{L} -]*", message = "Email required") - private String confirmEmail; - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } - - public String getConfirmPassword() { - return confirmPassword; - } - - public void setConfirmPassword(String confirmPassword) { - this.confirmPassword = confirmPassword; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getConfirmEmail() { - return confirmEmail; - } - - public void setConfirmEmail(String confirmEmail) { - this.confirmEmail = confirmEmail; - } - } - - - @Documented - @Constraint(validatedBy = {SameValidator.class}) - @Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE}) - @Retention(RetentionPolicy.RUNTIME) - @Repeatable(SameGroup.class) - @interface Same { - - String message() default "{org.springframework.validation.beanvalidation.Same.message}"; - - Class[] groups() default {}; - - Class[] payload() default {}; - - String field(); - - String comparingField(); - - @Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE}) - @Retention(RetentionPolicy.RUNTIME) - @Documented - @interface List { - Same[] value(); - } - } - - - @Documented - @Inherited - @Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE}) - @Retention(RetentionPolicy.RUNTIME) - @interface SameGroup { - - Same[] value(); - } - - - public static class SameValidator implements ConstraintValidator { - - private String field; - - private String comparingField; - - private String message; - - @Override - public void initialize(Same constraintAnnotation) { - field = constraintAnnotation.field(); - comparingField = constraintAnnotation.comparingField(); - message = constraintAnnotation.message(); - } - - @Override - public boolean isValid(Object value, ConstraintValidatorContext context) { - BeanWrapper beanWrapper = new BeanWrapperImpl(value); - Object fieldValue = beanWrapper.getPropertyValue(field); - Object comparingFieldValue = beanWrapper.getPropertyValue(comparingField); - boolean matched = ObjectUtils.nullSafeEquals(fieldValue, comparingFieldValue); - if (matched) { - return true; - } - else { - context.disableDefaultConstraintViolation(); - context.buildConstraintViolationWithTemplate(message) - .addPropertyNode(field) - .addConstraintViolation(); - return false; - } - } - } - - - public static class Parent { - - private Integer id; - - @NotNull - private String name; - - @Valid - private Set childSet = new LinkedHashSet<>(); - - @Valid - private List childList = new ArrayList<>(); - - public Integer getId() { - return id; - } - - public void setId(Integer id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Set getChildSet() { - return childSet; - } - - public void setChildSet(Set childSet) { - this.childSet = childSet; - } - - public List getChildList() { - return childList; - } - - public void setChildList(List childList) { - this.childList = childList; - } - } - - - @AnythingValid - public static class Child { - - private Integer id; - - @NotNull - private String name; - - @NotNull - private Integer age; - - @NotNull - private Parent parent; - - public Integer getId() { - return id; - } - - public void setId(Integer id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Integer getAge() { - return age; - } - - public void setAge(Integer age) { - this.age = age; - } - - public Parent getParent() { - return parent; - } - - public void setParent(Parent parent) { - this.parent = parent; - } - } - - - @Constraint(validatedBy = AnythingValidator.class) - @Retention(RetentionPolicy.RUNTIME) - public @interface AnythingValid { - - String message() default "{AnythingValid.message}"; - - Class[] groups() default {}; - - Class[] payload() default {}; - } - - - public static class AnythingValidator implements ConstraintValidator { - - private static final String ID = "id"; - - @Override - public void initialize(AnythingValid constraintAnnotation) { - } - - @Override - public boolean isValid(Object value, ConstraintValidatorContext context) { - List fieldsErrors = new ArrayList<>(); - Arrays.asList(value.getClass().getDeclaredFields()).forEach(field -> { - field.setAccessible(true); - try { - if (!field.getName().equals(ID) && field.get(value) == null) { - fieldsErrors.add(field); - context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()) - .addPropertyNode(field.getName()) - .addConstraintViolation(); - } - } - catch (IllegalAccessException ex) { - throw new IllegalStateException(ex); - } - }); - return fieldsErrors.isEmpty(); - } - } - - - public class BeanWithListElementConstraint { - - @Valid - private List<@NotNull String> property; - - public List getProperty() { - return property; - } - - public void setProperty(List property) { - this.property = property; - } - } - - - public class BeanWithMapEntryConstraint { - - @Valid - private Map<@NotNull String, @NotNull String> property; - - public Map getProperty() { - return property; - } - - public void setProperty(Map property) { - this.property = property; - } - } - -} diff --git a/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/ValidatorFactoryTests.java b/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/ValidatorFactoryTests.java deleted file mode 100644 index a383ae5f8bf7..000000000000 --- a/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/ValidatorFactoryTests.java +++ /dev/null @@ -1,505 +0,0 @@ -/* - * Copyright 2002-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.validation.beanvalidation2; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Iterator; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; - -import javax.validation.Constraint; -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; -import javax.validation.ConstraintViolation; -import javax.validation.Payload; -import javax.validation.Valid; -import javax.validation.Validator; -import javax.validation.ValidatorFactory; -import javax.validation.constraints.NotNull; - -import org.hibernate.validator.HibernateValidator; -import org.hibernate.validator.HibernateValidatorFactory; -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.core.convert.support.DefaultConversionService; -import org.springframework.core.env.Environment; -import org.springframework.validation.BeanPropertyBindingResult; -import org.springframework.validation.Errors; -import org.springframework.validation.FieldError; -import org.springframework.validation.ObjectError; -import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Juergen Hoeller - */ -@SuppressWarnings("resource") -public class ValidatorFactoryTests { - - @Test - @SuppressWarnings("cast") - public void testSimpleValidation() { - LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); - validator.afterPropertiesSet(); - - ValidPerson person = new ValidPerson(); - Set> result = validator.validate(person); - assertThat(result.size()).isEqualTo(2); - for (ConstraintViolation cv : result) { - String path = cv.getPropertyPath().toString(); - assertThat(path).matches(actual -> "name".equals(actual) || "address.street".equals(actual)); - assertThat(cv.getConstraintDescriptor().getAnnotation()).isInstanceOf(NotNull.class); - } - - Validator nativeValidator = validator.unwrap(Validator.class); - assertThat(nativeValidator.getClass().getName().startsWith("org.hibernate")).isTrue(); - assertThat(validator.unwrap(ValidatorFactory.class) instanceof HibernateValidatorFactory).isTrue(); - assertThat(validator.unwrap(HibernateValidatorFactory.class) instanceof HibernateValidatorFactory).isTrue(); - - validator.destroy(); - } - - @Test - @SuppressWarnings("cast") - public void testSimpleValidationWithCustomProvider() { - LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); - validator.setProviderClass(HibernateValidator.class); - validator.afterPropertiesSet(); - - ValidPerson person = new ValidPerson(); - Set> result = validator.validate(person); - assertThat(result.size()).isEqualTo(2); - for (ConstraintViolation cv : result) { - String path = cv.getPropertyPath().toString(); - assertThat(path).matches(actual -> "name".equals(actual) || "address.street".equals(actual)); - assertThat(cv.getConstraintDescriptor().getAnnotation()).isInstanceOf(NotNull.class); - } - - Validator nativeValidator = validator.unwrap(Validator.class); - assertThat(nativeValidator.getClass().getName().startsWith("org.hibernate")).isTrue(); - assertThat(validator.unwrap(ValidatorFactory.class) instanceof HibernateValidatorFactory).isTrue(); - assertThat(validator.unwrap(HibernateValidatorFactory.class) instanceof HibernateValidatorFactory).isTrue(); - - validator.destroy(); - } - - @Test - public void testSimpleValidationWithClassLevel() { - LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); - validator.afterPropertiesSet(); - - ValidPerson person = new ValidPerson(); - person.setName("Juergen"); - person.getAddress().setStreet("Juergen's Street"); - Set> result = validator.validate(person); - assertThat(result.size()).isEqualTo(1); - Iterator> iterator = result.iterator(); - ConstraintViolation cv = iterator.next(); - assertThat(cv.getPropertyPath().toString()).isEqualTo(""); - assertThat(cv.getConstraintDescriptor().getAnnotation() instanceof NameAddressValid).isTrue(); - } - - @Test - public void testSpringValidationFieldType() { - LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); - validator.afterPropertiesSet(); - - ValidPerson person = new ValidPerson(); - person.setName("Phil"); - person.getAddress().setStreet("Phil's Street"); - BeanPropertyBindingResult errors = new BeanPropertyBindingResult(person, "person"); - validator.validate(person, errors); - assertThat(errors.getErrorCount()).isEqualTo(1); - assertThat(errors.getFieldError("address").getRejectedValue()).isInstanceOf(ValidAddress.class); - } - - @Test - public void testSpringValidation() { - LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); - validator.afterPropertiesSet(); - - ValidPerson person = new ValidPerson(); - BeanPropertyBindingResult result = new BeanPropertyBindingResult(person, "person"); - validator.validate(person, result); - assertThat(result.getErrorCount()).isEqualTo(2); - FieldError fieldError = result.getFieldError("name"); - assertThat(fieldError.getField()).isEqualTo("name"); - List errorCodes = Arrays.asList(fieldError.getCodes()); - assertThat(errorCodes.size()).isEqualTo(4); - assertThat(errorCodes.contains("NotNull.person.name")).isTrue(); - assertThat(errorCodes.contains("NotNull.name")).isTrue(); - assertThat(errorCodes.contains("NotNull.java.lang.String")).isTrue(); - assertThat(errorCodes.contains("NotNull")).isTrue(); - fieldError = result.getFieldError("address.street"); - assertThat(fieldError.getField()).isEqualTo("address.street"); - errorCodes = Arrays.asList(fieldError.getCodes()); - assertThat(errorCodes.size()).isEqualTo(5); - assertThat(errorCodes.contains("NotNull.person.address.street")).isTrue(); - assertThat(errorCodes.contains("NotNull.address.street")).isTrue(); - assertThat(errorCodes.contains("NotNull.street")).isTrue(); - assertThat(errorCodes.contains("NotNull.java.lang.String")).isTrue(); - assertThat(errorCodes.contains("NotNull")).isTrue(); - } - - @Test - public void testSpringValidationWithClassLevel() { - LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); - validator.afterPropertiesSet(); - - ValidPerson person = new ValidPerson(); - person.setName("Juergen"); - person.getAddress().setStreet("Juergen's Street"); - BeanPropertyBindingResult result = new BeanPropertyBindingResult(person, "person"); - validator.validate(person, result); - assertThat(result.getErrorCount()).isEqualTo(1); - ObjectError globalError = result.getGlobalError(); - List errorCodes = Arrays.asList(globalError.getCodes()); - assertThat(errorCodes.size()).isEqualTo(2); - assertThat(errorCodes.contains("NameAddressValid.person")).isTrue(); - assertThat(errorCodes.contains("NameAddressValid")).isTrue(); - } - - @Test - public void testSpringValidationWithAutowiredValidator() { - ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext( - LocalValidatorFactoryBean.class); - LocalValidatorFactoryBean validator = ctx.getBean(LocalValidatorFactoryBean.class); - - ValidPerson person = new ValidPerson(); - person.expectsAutowiredValidator = true; - person.setName("Juergen"); - person.getAddress().setStreet("Juergen's Street"); - BeanPropertyBindingResult result = new BeanPropertyBindingResult(person, "person"); - validator.validate(person, result); - assertThat(result.getErrorCount()).isEqualTo(1); - ObjectError globalError = result.getGlobalError(); - List errorCodes = Arrays.asList(globalError.getCodes()); - assertThat(errorCodes.size()).isEqualTo(2); - assertThat(errorCodes.contains("NameAddressValid.person")).isTrue(); - assertThat(errorCodes.contains("NameAddressValid")).isTrue(); - ctx.close(); - } - - @Test - public void testSpringValidationWithErrorInListElement() { - LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); - validator.afterPropertiesSet(); - - ValidPerson person = new ValidPerson(); - person.getAddressList().add(new ValidAddress()); - BeanPropertyBindingResult result = new BeanPropertyBindingResult(person, "person"); - validator.validate(person, result); - assertThat(result.getErrorCount()).isEqualTo(3); - FieldError fieldError = result.getFieldError("name"); - assertThat(fieldError.getField()).isEqualTo("name"); - fieldError = result.getFieldError("address.street"); - assertThat(fieldError.getField()).isEqualTo("address.street"); - fieldError = result.getFieldError("addressList[0].street"); - assertThat(fieldError.getField()).isEqualTo("addressList[0].street"); - } - - @Test - public void testSpringValidationWithErrorInSetElement() { - LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); - validator.afterPropertiesSet(); - - ValidPerson person = new ValidPerson(); - person.getAddressSet().add(new ValidAddress()); - BeanPropertyBindingResult result = new BeanPropertyBindingResult(person, "person"); - validator.validate(person, result); - assertThat(result.getErrorCount()).isEqualTo(3); - FieldError fieldError = result.getFieldError("name"); - assertThat(fieldError.getField()).isEqualTo("name"); - fieldError = result.getFieldError("address.street"); - assertThat(fieldError.getField()).isEqualTo("address.street"); - fieldError = result.getFieldError("addressSet[].street"); - assertThat(fieldError.getField()).isEqualTo("addressSet[].street"); - } - - @Test - public void testInnerBeanValidation() { - LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); - validator.afterPropertiesSet(); - - MainBean mainBean = new MainBean(); - Errors errors = new BeanPropertyBindingResult(mainBean, "mainBean"); - validator.validate(mainBean, errors); - Object rejected = errors.getFieldValue("inner.value"); - assertThat(rejected).isNull(); - } - - @Test - public void testValidationWithOptionalField() { - LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); - validator.afterPropertiesSet(); - - MainBeanWithOptional mainBean = new MainBeanWithOptional(); - Errors errors = new BeanPropertyBindingResult(mainBean, "mainBean"); - validator.validate(mainBean, errors); - Object rejected = errors.getFieldValue("inner.value"); - assertThat(rejected).isNull(); - } - - @Test - public void testListValidation() { - LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); - validator.afterPropertiesSet(); - - ListContainer listContainer = new ListContainer(); - listContainer.addString("A"); - listContainer.addString("X"); - - BeanPropertyBindingResult errors = new BeanPropertyBindingResult(listContainer, "listContainer"); - errors.initConversion(new DefaultConversionService()); - validator.validate(listContainer, errors); - - FieldError fieldError = errors.getFieldError("list[1]"); - assertThat(fieldError).isNotNull(); - assertThat(fieldError.getRejectedValue()).isEqualTo("X"); - assertThat(errors.getFieldValue("list[1]")).isEqualTo("X"); - } - - - @NameAddressValid - public static class ValidPerson { - - @NotNull - private String name; - - @Valid - private ValidAddress address = new ValidAddress(); - - @Valid - private List addressList = new ArrayList<>(); - - @Valid - private Set addressSet = new LinkedHashSet<>(); - - public boolean expectsAutowiredValidator = false; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public ValidAddress getAddress() { - return address; - } - - public void setAddress(ValidAddress address) { - this.address = address; - } - - public List getAddressList() { - return addressList; - } - - public void setAddressList(List addressList) { - this.addressList = addressList; - } - - public Set getAddressSet() { - return addressSet; - } - - public void setAddressSet(Set addressSet) { - this.addressSet = addressSet; - } - } - - - public static class ValidAddress { - - @NotNull - private String street; - - public String getStreet() { - return street; - } - - public void setStreet(String street) { - this.street = street; - } - } - - - @Target(ElementType.TYPE) - @Retention(RetentionPolicy.RUNTIME) - @Constraint(validatedBy = NameAddressValidator.class) - public @interface NameAddressValid { - - String message() default "Street must not contain name"; - - Class[] groups() default {}; - - Class[] payload() default {}; - } - - - public static class NameAddressValidator implements ConstraintValidator { - - @Autowired - private Environment environment; - - @Override - public void initialize(NameAddressValid constraintAnnotation) { - } - - @Override - public boolean isValid(ValidPerson value, ConstraintValidatorContext context) { - if (value.expectsAutowiredValidator) { - assertThat(this.environment).isNotNull(); - } - boolean valid = (value.name == null || !value.address.street.contains(value.name)); - if (!valid && "Phil".equals(value.name)) { - context.buildConstraintViolationWithTemplate( - context.getDefaultConstraintMessageTemplate()).addPropertyNode("address").addConstraintViolation().disableDefaultConstraintViolation(); - } - return valid; - } - } - - - public static class MainBean { - - @InnerValid - private InnerBean inner = new InnerBean(); - - public InnerBean getInner() { - return inner; - } - } - - - public static class MainBeanWithOptional { - - @InnerValid - private InnerBean inner = new InnerBean(); - - public Optional getInner() { - return Optional.ofNullable(inner); - } - } - - - public static class InnerBean { - - private String value; - - public String getValue() { - return value; - } - public void setValue(String value) { - this.value = value; - } - } - - - @Retention(RetentionPolicy.RUNTIME) - @Target(ElementType.FIELD) - @Constraint(validatedBy=InnerValidator.class) - public static @interface InnerValid { - - String message() default "NOT VALID"; - - Class[] groups() default { }; - - Class[] payload() default {}; - } - - - public static class InnerValidator implements ConstraintValidator { - - @Override - public void initialize(InnerValid constraintAnnotation) { - } - - @Override - public boolean isValid(InnerBean bean, ConstraintValidatorContext context) { - context.disableDefaultConstraintViolation(); - if (bean.getValue() == null) { - context.buildConstraintViolationWithTemplate("NULL").addPropertyNode("value").addConstraintViolation(); - return false; - } - return true; - } - } - - - public static class ListContainer { - - @NotXList - private List list = new ArrayList<>(); - - public void addString(String value) { - list.add(value); - } - - public List getList() { - return list; - } - } - - - @Retention(RetentionPolicy.RUNTIME) - @Target(ElementType.FIELD) - @Constraint(validatedBy = NotXListValidator.class) - public @interface NotXList { - - String message() default "Should not be X"; - - Class[] groups() default {}; - - Class[] payload() default {}; - } - - - public static class NotXListValidator implements ConstraintValidator> { - - @Override - public void initialize(NotXList constraintAnnotation) { - } - - @Override - public boolean isValid(List list, ConstraintValidatorContext context) { - context.disableDefaultConstraintViolation(); - boolean valid = true; - for (int i = 0; i < list.size(); i++) { - if ("X".equals(list.get(i))) { - context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()).addBeanNode().inIterable().atIndex(i).addConstraintViolation(); - valid = false; - } - } - return valid; - } - } - -} diff --git a/spring-context-support/src/test/resources/org/springframework/scheduling/quartz/multipleAnonymousMethodInvokingJobDetailFB.xml b/spring-context-support/src/test/resources/org/springframework/scheduling/quartz/multipleAnonymousMethodInvokingJobDetailFB.xml index e45ccd195feb..58e771f3ad4a 100644 --- a/spring-context-support/src/test/resources/org/springframework/scheduling/quartz/multipleAnonymousMethodInvokingJobDetailFB.xml +++ b/spring-context-support/src/test/resources/org/springframework/scheduling/quartz/multipleAnonymousMethodInvokingJobDetailFB.xml @@ -19,7 +19,7 @@ - + @@ -30,7 +30,7 @@ - + diff --git a/spring-context-support/src/test/resources/org/springframework/scheduling/quartz/schedulerAccessorBean.xml b/spring-context-support/src/test/resources/org/springframework/scheduling/quartz/schedulerAccessorBean.xml index 0ba9c4a57a84..3634002664cc 100644 --- a/spring-context-support/src/test/resources/org/springframework/scheduling/quartz/schedulerAccessorBean.xml +++ b/spring-context-support/src/test/resources/org/springframework/scheduling/quartz/schedulerAccessorBean.xml @@ -21,7 +21,7 @@ - + @@ -32,7 +32,7 @@ - + diff --git a/spring-context-support/src/testFixtures/java/org/springframework/contextsupport/testfixture/jcache/AbstractJCacheAnnotationTests.java b/spring-context-support/src/testFixtures/java/org/springframework/contextsupport/testfixture/jcache/AbstractJCacheAnnotationTests.java index db5bb038e94f..b1fc5cda8bff 100644 --- a/spring-context-support/src/testFixtures/java/org/springframework/contextsupport/testfixture/jcache/AbstractJCacheAnnotationTests.java +++ b/spring-context-support/src/testFixtures/java/org/springframework/contextsupport/testfixture/jcache/AbstractJCacheAnnotationTests.java @@ -354,7 +354,7 @@ public void earlyRemoveWithExceptionVetoRemove() { assertThatNullPointerException().isThrownBy(() -> service.earlyRemoveWithException(this.keyItem, false)); - // This will be remove anyway as the earlyRemove has removed the cache before + // This will be removed anyway as the earlyRemove has removed the cache before assertThat(cache.get(key)).isNull(); } @@ -428,7 +428,7 @@ public void earlyRemoveAllWithExceptionVetoRemove() { assertThatNullPointerException().isThrownBy(() -> service.earlyRemoveAllWithException(false)); - // This will be remove anyway as the earlyRemove has removed the cache before + // This will be removed anyway as the earlyRemove has removed the cache before assertThat(isEmpty(cache)).isTrue(); } diff --git a/spring-context/spring-context.gradle b/spring-context/spring-context.gradle index 0b06a7c56f0f..5f5ac1001ec7 100644 --- a/spring-context/spring-context.gradle +++ b/spring-context/spring-context.gradle @@ -1,47 +1,47 @@ +plugins { + id 'org.springframework.build.runtimehints-agent' +} + description = "Spring Context" -apply plugin: "groovy" apply plugin: "kotlin" dependencies { - compile(project(":spring-aop")) - compile(project(":spring-beans")) - compile(project(":spring-core")) - compile(project(":spring-expression")) + api(project(":spring-aop")) + api(project(":spring-beans")) + api(project(":spring-core")) + api(project(":spring-expression")) optional(project(":spring-instrument")) - optional("javax.annotation:javax.annotation-api") - optional("javax.ejb:javax.ejb-api") - optional("javax.enterprise.concurrent:javax.enterprise.concurrent-api") - optional("javax.inject:javax.inject") - optional("javax.interceptor:javax.interceptor-api") + optional("jakarta.annotation:jakarta.annotation-api") + optional("jakarta.ejb:jakarta.ejb-api") + optional("jakarta.enterprise.concurrent:jakarta.enterprise.concurrent-api") + optional("jakarta.inject:jakarta.inject-api") + optional("jakarta.interceptor:jakarta.interceptor-api") + optional("jakarta.validation:jakarta.validation-api") optional("javax.money:money-api") - // Overriding 2.0.1.Final due to Bean Validation 1.1 compatibility in LocalValidatorFactoryBean - optional("javax.validation:validation-api:1.1.0.Final") - optional("javax.xml.ws:jaxws-api") optional("org.aspectj:aspectjweaver") - optional("org.codehaus.groovy:groovy") + optional("org.apache.groovy:groovy") optional("org.apache-extras.beanshell:bsh") - optional("joda-time:joda-time") - optional("org.hibernate:hibernate-validator:5.4.3.Final") + optional("org.hibernate:hibernate-validator") optional("org.jetbrains.kotlin:kotlin-reflect") optional("org.jetbrains.kotlin:kotlin-stdlib") optional("org.reactivestreams:reactive-streams") - testCompile(testFixtures(project(":spring-aop"))) - testCompile(testFixtures(project(":spring-beans"))) - testCompile(testFixtures(project(":spring-core"))) - testCompile("io.projectreactor:reactor-core") - testCompile("org.codehaus.groovy:groovy-jsr223") - testCompile("org.codehaus.groovy:groovy-test") - testCompile("org.codehaus.groovy:groovy-xml") - testCompile("org.apache.commons:commons-pool2") - testCompile("javax.inject:javax.inject-tck") - testCompile("org.awaitility:awaitility") - testRuntime("javax.xml.bind:jaxb-api") - testRuntime("org.glassfish:javax.el") + testImplementation(project(":spring-core-test")) + testImplementation(testFixtures(project(":spring-aop"))) + testImplementation(testFixtures(project(":spring-beans"))) + testImplementation(testFixtures(project(":spring-core"))) + testImplementation("io.projectreactor:reactor-core") + testImplementation("org.apache.groovy:groovy-jsr223") + testImplementation("org.apache.groovy:groovy-xml") + testImplementation("org.apache.commons:commons-pool2") + testImplementation("org.awaitility:awaitility") + testImplementation("jakarta.inject:jakarta.inject-tck") + testRuntimeOnly("jakarta.xml.bind:jakarta.xml.bind-api") + testRuntimeOnly("org.glassfish:jakarta.el") // Substitute for javax.management:jmxremote_optional:1.0.1_04 (not available on Maven Central) - testRuntime("org.glassfish.external:opendmk_jmxremote_optional_jar") - testRuntime("org.javamoney:moneta") - testRuntime("org.junit.vintage:junit-vintage-engine") // for @Inject TCK + testRuntimeOnly("org.glassfish.external:opendmk_jmxremote_optional_jar") + testRuntimeOnly("org.javamoney:moneta") + testRuntimeOnly("org.junit.vintage:junit-vintage-engine") // for @Inject TCK testFixturesApi("org.junit.jupiter:junit-jupiter-api") testFixturesImplementation(testFixtures(project(":spring-beans"))) testFixturesImplementation("com.google.code.findbugs:jsr305") diff --git a/spring-context/src/jmh/java/org/springframework/context/annotation/AnnotationProcessorBenchmark.java b/spring-context/src/jmh/java/org/springframework/context/annotation/AnnotationProcessorBenchmark.java index c00cf0ddfaec..f78c6718173b 100644 --- a/spring-context/src/jmh/java/org/springframework/context/annotation/AnnotationProcessorBenchmark.java +++ b/spring-context/src/jmh/java/org/springframework/context/annotation/AnnotationProcessorBenchmark.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -16,8 +16,7 @@ package org.springframework.context.annotation; -import javax.annotation.Resource; - +import jakarta.annotation.Resource; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Mode; @@ -85,7 +84,6 @@ private static class ResourceAnnotatedTestBean extends org.springframework.beans @Override @Resource @SuppressWarnings("deprecation") - @org.springframework.beans.factory.annotation.Required public void setSpouse(ITestBean spouse) { super.setSpouse(spouse); } @@ -96,7 +94,6 @@ private static class AutowiredAnnotatedTestBean extends TestBean { @Override @Autowired @SuppressWarnings("deprecation") - @org.springframework.beans.factory.annotation.Required public void setSpouse(ITestBean spouse) { super.setSpouse(spouse); } diff --git a/spring-context/src/main/java/org/springframework/cache/Cache.java b/spring-context/src/main/java/org/springframework/cache/Cache.java index dea4505d9ad2..8a3b904f4904 100644 --- a/spring-context/src/main/java/org/springframework/cache/Cache.java +++ b/spring-context/src/main/java/org/springframework/cache/Cache.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -24,7 +24,7 @@ * Interface that defines common cache operations. * * Note: Due to the generic use of caching, it is recommended that - * implementations allow storage of null values (for example to + * implementations allow storage of {@code null} values (for example to * cache methods that return {@code null}). * * @author Costin Leau diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/AbstractCachingConfiguration.java b/spring-context/src/main/java/org/springframework/cache/annotation/AbstractCachingConfiguration.java index c49d8d20c6e0..502c64fe2b35 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/AbstractCachingConfiguration.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/AbstractCachingConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -16,9 +16,11 @@ package org.springframework.cache.annotation; -import java.util.Collection; +import java.util.List; +import java.util.function.Function; import java.util.function.Supplier; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.CacheManager; import org.springframework.cache.interceptor.CacheErrorHandler; @@ -30,6 +32,7 @@ import org.springframework.core.type.AnnotationMetadata; import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; +import org.springframework.util.function.SingletonSupplier; /** * Abstract base {@code @Configuration} class providing common structure @@ -63,36 +66,67 @@ public abstract class AbstractCachingConfiguration implements ImportAware { @Override public void setImportMetadata(AnnotationMetadata importMetadata) { this.enableCaching = AnnotationAttributes.fromMap( - importMetadata.getAnnotationAttributes(EnableCaching.class.getName(), false)); + importMetadata.getAnnotationAttributes(EnableCaching.class.getName())); if (this.enableCaching == null) { throw new IllegalArgumentException( "@EnableCaching is not present on importing class " + importMetadata.getClassName()); } } - @Autowired(required = false) - void setConfigurers(Collection configurers) { - if (CollectionUtils.isEmpty(configurers)) { - return; - } - if (configurers.size() > 1) { - throw new IllegalStateException(configurers.size() + " implementations of " + - "CachingConfigurer were found when only 1 was expected. " + - "Refactor the configuration such that CachingConfigurer is " + - "implemented only once or not at all."); - } - CachingConfigurer configurer = configurers.iterator().next(); - useCachingConfigurer(configurer); + @Autowired + void setConfigurers(ObjectProvider configurers) { + Supplier configurer = () -> { + List candidates = configurers.stream().toList(); + if (CollectionUtils.isEmpty(candidates)) { + return null; + } + if (candidates.size() > 1) { + throw new IllegalStateException(candidates.size() + " implementations of " + + "CachingConfigurer were found when only 1 was expected. " + + "Refactor the configuration such that CachingConfigurer is " + + "implemented only once or not at all."); + } + return candidates.get(0); + }; + useCachingConfigurer(new CachingConfigurerSupplier(configurer)); } /** * Extract the configuration from the nominated {@link CachingConfigurer}. */ - protected void useCachingConfigurer(CachingConfigurer config) { - this.cacheManager = config::cacheManager; - this.cacheResolver = config::cacheResolver; - this.keyGenerator = config::keyGenerator; - this.errorHandler = config::errorHandler; + protected void useCachingConfigurer(CachingConfigurerSupplier cachingConfigurerSupplier) { + this.cacheManager = cachingConfigurerSupplier.adapt(CachingConfigurer::cacheManager); + this.cacheResolver = cachingConfigurerSupplier.adapt(CachingConfigurer::cacheResolver); + this.keyGenerator = cachingConfigurerSupplier.adapt(CachingConfigurer::keyGenerator); + this.errorHandler = cachingConfigurerSupplier.adapt(CachingConfigurer::errorHandler); + } + + + protected static class CachingConfigurerSupplier { + + private final Supplier supplier; + + public CachingConfigurerSupplier(Supplier supplier) { + this.supplier = SingletonSupplier.of(supplier); + } + + /** + * Adapt the {@link CachingConfigurer} supplier to another supplier + * provided by the specified mapping function. If the underlying + * {@link CachingConfigurer} is {@code null}, {@code null} is returned + * and the mapping function is not invoked. + * @param provider the provider to use to adapt the supplier + * @param the type of the supplier + * @return another supplier mapped by the specified function + */ + @Nullable + public Supplier adapt(Function provider) { + return () -> { + CachingConfigurer cachingConfigurer = this.supplier.get(); + return (cachingConfigurer != null ? provider.apply(cachingConfigurer) : null); + }; + } + } } diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/AnnotationCacheOperationSource.java b/spring-context/src/main/java/org/springframework/cache/annotation/AnnotationCacheOperationSource.java index 5d34d0e1031f..230a36d18766 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/AnnotationCacheOperationSource.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/AnnotationCacheOperationSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -168,10 +168,9 @@ public boolean equals(@Nullable Object other) { if (this == other) { return true; } - if (!(other instanceof AnnotationCacheOperationSource)) { + if (!(other instanceof AnnotationCacheOperationSource otherCos)) { return false; } - AnnotationCacheOperationSource otherCos = (AnnotationCacheOperationSource) other; return (this.annotationParsers.equals(otherCos.annotationParsers) && this.publicMethodsOnly == otherCos.publicMethodsOnly); } diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/CacheEvict.java b/spring-context/src/main/java/org/springframework/cache/annotation/CacheEvict.java index 5aa397885212..43f095236a0a 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/CacheEvict.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/CacheEvict.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-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. @@ -23,6 +23,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.aot.hint.annotation.Reflective; import org.springframework.core.annotation.AliasFor; /** @@ -42,6 +43,7 @@ @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented +@Reflective public @interface CacheEvict { /** @@ -111,7 +113,8 @@ /** * Spring Expression Language (SpEL) expression used for making the cache - * eviction operation conditional. + * eviction operation conditional. Evict that cache if the condition evaluates + * to {@code true}. *

    Default is {@code ""}, meaning the cache eviction is always performed. *

    The SpEL expression evaluates against a dedicated context that provides the * following meta-data: diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/CachePut.java b/spring-context/src/main/java/org/springframework/cache/annotation/CachePut.java index 8743df7e668b..9bf3c5706c05 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/CachePut.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/CachePut.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-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. @@ -23,6 +23,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.aot.hint.annotation.Reflective; import org.springframework.core.annotation.AliasFor; /** @@ -31,7 +32,8 @@ * *

    In contrast to the {@link Cacheable @Cacheable} annotation, this annotation * does not cause the advised method to be skipped. Rather, it always causes the - * method to be invoked and its result to be stored in the associated cache. Note + * method to be invoked and its result to be stored in the associated cache if the + * {@link #condition()} and {@link #unless()} expressions match accordingly. Note * that Java8's {@code Optional} return types are automatically handled and its * content is stored in the cache if present. * @@ -49,6 +51,7 @@ @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented +@Reflective public @interface CachePut { /** @@ -117,11 +120,17 @@ /** * Spring Expression Language (SpEL) expression used for making the cache - * put operation conditional. + * put operation conditional. Update the cache if the condition evaluates to + * {@code true}. + *

    This expression is evaluated after the method has been called due to the + * nature of the put operation and can therefore refer to the {@code result}. *

    Default is {@code ""}, meaning the method result is always cached. *

    The SpEL expression evaluates against a dedicated context that provides the * following meta-data: *