mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
Compare commits
267 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8404537474 | |||
| c873a597c7 | |||
| 4dc93bc485 | |||
| 68f2b0ca59 | |||
| 1edc0d8002 | |||
| a6dc020dc4 | |||
| 07422d709e | |||
| 768fc7e341 | |||
| 8ecedb81b3 | |||
| f19433f2d8 | |||
| 0b02a5e073 | |||
| e2b24f3c12 | |||
| c375fb1f70 | |||
| ad3e5425d4 | |||
| c418118683 | |||
| fb3f30832c | |||
| a275d942d2 | |||
| 20afa3265a | |||
| df50c8db3e | |||
| 679b668bbb | |||
| 676daa990b | |||
| 7c7fa69558 | |||
| 9ad92b16b0 | |||
| 3b899fe7e2 | |||
| c91041b675 | |||
| a17cf742b2 | |||
| a102cd5f32 | |||
| d03b6aa1d6 | |||
| b32b4f3a59 | |||
| 502997d8e9 | |||
| fb4ad2f3ba | |||
| b3de1b8e95 | |||
| fb17e283d1 | |||
| 0b7a24fc14 | |||
| 75b540f25c | |||
| 8bf79cc9c4 | |||
| 7ff80bc09d | |||
| 8d6b0eb191 | |||
| 35c7e3960e | |||
| 8e8c3f5a7c | |||
| f50b230fb3 | |||
| 02ba06953f | |||
| df22ba39f8 | |||
| 826776f321 | |||
| 29f92c8a2b | |||
| 64e04b7bc2 | |||
| ad05b02ff5 | |||
| 430a24e6bc | |||
| b7b9f2cb6b | |||
| b76664e757 | |||
| ae13823851 | |||
| 58b4286216 | |||
| df079feea9 | |||
| 372282457f | |||
| 79df1da792 | |||
| c5771bc7c8 | |||
| 2365581265 | |||
| c1a8b9a14d | |||
| e945e7426e | |||
| f07b9fd217 | |||
| 80a20488fd | |||
| 1dc9dffc70 | |||
| 0226580773 | |||
| 3ef1b7d83c | |||
| 08bce69d3d | |||
| 56b60120fe | |||
| 1364a179a9 | |||
| 2161e865d7 | |||
| 0b2c2d04b2 | |||
| 07fe8eea83 | |||
| 5aac35b99e | |||
| 040ea0a97c | |||
| 3c05679a97 | |||
| d8729a7c67 | |||
| c95426a616 | |||
| 1e403d1606 | |||
| f1567fb21a | |||
| 60865eae4b | |||
| 0c39fff831 | |||
| e902f9551a | |||
| 0a20c8a44a | |||
| 3cb746c358 | |||
| 6526e79eea | |||
| b77d4d01c5 | |||
| 599ac58baa | |||
| 449174c7d4 | |||
| 9266e6d29e | |||
| 062d701ae1 | |||
| 7137b22e6b | |||
| acb786d359 | |||
| fa7300c1de | |||
| db17a97ce8 | |||
| 55f946c5a0 | |||
| 3181dca5ef | |||
| f86a69ebfb | |||
| 6d63890c56 | |||
| 489c89b912 | |||
| 39bc7566df | |||
| d3a249e34d | |||
| 81f1edbaf2 | |||
| 23ecb50137 | |||
| 3d33d2baa9 | |||
| 3745224646 | |||
| 3bf78d6f8c | |||
| 563b2a8505 | |||
| 9ccbeec947 | |||
| 29248dff15 | |||
| 65d450ab6d | |||
| 271f2dc665 | |||
| da323d3335 | |||
| 32f061a3e0 | |||
| 9b5cbc1334 | |||
| 40e378a5a6 | |||
| 8eb0a0b94e | |||
| 09cf489c6b | |||
| 9a5290ea27 | |||
| b3176208e2 | |||
| 1dfe737d0e | |||
| dc2f513619 | |||
| 0eb33d09ac | |||
| f67f98a1a7 | |||
| 6c42f374c8 | |||
| 4879b56bb9 | |||
| 2fd83aa764 | |||
| 93218a06ba | |||
| 714c3c59eb | |||
| adcdefce43 | |||
| 049a024dea | |||
| 20bbebb299 | |||
| 26f006509f | |||
| 089503aab7 | |||
| 74155e3d88 | |||
| 564f33d5ef | |||
| 83acd5b050 | |||
| 67798a7b52 | |||
| 2b981651e1 | |||
| 294cdba80c | |||
| 072a86149d | |||
| 8bb4c167e4 | |||
| dff7aa4d4b | |||
| c634acd9ff | |||
| ed5c19f53e | |||
| 03420f811b | |||
| 4565bcd757 | |||
| f22f439a68 | |||
| 5672284f53 | |||
| 367f381fea | |||
| b9221656cc | |||
| c30f6aa427 | |||
| aa2028127f | |||
| a82659c837 | |||
| 3415b04c73 | |||
| a73ad52a8a | |||
| 13c32d80ba | |||
| 3171a8b0e2 | |||
| 927d27b121 | |||
| 1df5e9f30c | |||
| ed74b04520 | |||
| 6cfb11a360 | |||
| 3b263c5c3d | |||
| f18a85e193 | |||
| f7d195782f | |||
| bbab4faf7a | |||
| 3fb98b6a97 | |||
| dc4f46df9f | |||
| 80af7bedef | |||
| f8c8873c99 | |||
| cc8c852c2b | |||
| fe7f8e2de5 | |||
| 0a5aff1b60 | |||
| 3d61d9e0d8 | |||
| 1a26e17f41 | |||
| 439bcd6715 | |||
| 4e33d0c293 | |||
| 9b4e0e9837 | |||
| 071d6a2a5a | |||
| d7d0292654 | |||
| 2817dce78f | |||
| 698acd32a7 | |||
| f36327b380 | |||
| ea9c0ae6c0 | |||
| c16f582ed8 | |||
| 6931106c5e | |||
| 045df81f14 | |||
| e210f08dce | |||
| 2317bef021 | |||
| ce5189a0a0 | |||
| 7a4ed38cd4 | |||
| c1fe57135e | |||
| 2c8d1b7bff | |||
| bd63c35b0b | |||
| c052a02592 | |||
| 5ad853ef5b | |||
| dfbed616ba | |||
| 94214562d0 | |||
| be94e1a2fb | |||
| f2ae106c32 | |||
| 7b20aefecf | |||
| cdc4497664 | |||
| 6cc084dbde | |||
| 974e10379a | |||
| 22ef48bec2 | |||
| 4be813bbee | |||
| 65617f1e75 | |||
| aaebf5749c | |||
| 24fa8793b1 | |||
| 4f10f559f7 | |||
| 2f35e7756b | |||
| cc50af08e4 | |||
| d08e4081c2 | |||
| 9751987dc1 | |||
| d2906253f1 | |||
| 4b8adf2dcc | |||
| c68552556f | |||
| b738a20233 | |||
| cca8fbd3de | |||
| 21397a67c6 | |||
| e8ab53e76d | |||
| 5b471a5349 | |||
| 08240bbcac | |||
| d719f3fc06 | |||
| 938dff7bbe | |||
| 162ccdd155 | |||
| f1594312cd | |||
| 7629ea5672 | |||
| 454a85978f | |||
| 2cbc7eed73 | |||
| 7a05f81844 | |||
| 71b2154ec8 | |||
| a716391aab | |||
| 09b2e5d0fb | |||
| c6b2d2e1d9 | |||
| 1cf6d1dd9d | |||
| 8b8d147480 | |||
| 8fe21b8ef9 | |||
| 7c9f5d05db | |||
| 110b809b7d | |||
| 5e625c8d2e | |||
| 3f648f54c5 | |||
| d61074268a | |||
| cfb3a45479 | |||
| 7d7d7bcced | |||
| 6dfd330fa5 | |||
| 7f9349b7ae | |||
| 4c8f1910c8 | |||
| 3b364c2a3d | |||
| 0930407d0e | |||
| 4bb12c4ba4 | |||
| 8d6d99731f | |||
| b5902f4fbf | |||
| a455317122 | |||
| a8b400803b | |||
| fa85657801 | |||
| 424daede2f | |||
| e92c9c5619 | |||
| 7660dbfd77 | |||
| 15a7f43c7b | |||
| 1721e42988 | |||
| eabb846d07 | |||
| 280b0de646 | |||
| 2738b3e50f | |||
| 169795d673 | |||
| 085565a771 | |||
| 806519f78a | |||
| c3c5eaf914 | |||
| a9b94241af | |||
| 7066dc4ba7 |
+10
-15
@@ -4,13 +4,11 @@ plugins {
|
||||
// kotlinVersion is managed in gradle.properties
|
||||
id 'org.jetbrains.kotlin.plugin.serialization' version "${kotlinVersion}" apply false
|
||||
id 'org.jetbrains.dokka' version '1.8.10'
|
||||
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.46.0'
|
||||
id 'com.github.johnrengelman.shadow' version '8.1.1' apply false
|
||||
id 'de.undercouch.download' version '5.4.0'
|
||||
id 'me.champeau.jmh' version '0.7.0' apply false
|
||||
id 'me.champeau.jmh' version '0.7.1' apply false
|
||||
}
|
||||
|
||||
ext {
|
||||
@@ -28,7 +26,6 @@ configure(allprojects) { project ->
|
||||
includeGroup 'io.projectreactor.netty'
|
||||
}
|
||||
}
|
||||
maven { url "https://repo.spring.io/libs-spring-framework-build" }
|
||||
if (version.contains('-')) {
|
||||
maven { url "https://repo.spring.io/milestone" }
|
||||
}
|
||||
@@ -78,7 +75,7 @@ configure([rootProject] + javaProjects) { project ->
|
||||
}
|
||||
|
||||
checkstyle {
|
||||
toolVersion = "10.10.0"
|
||||
toolVersion = "10.12.1"
|
||||
configDirectory.set(rootProject.file("src/checkstyle"))
|
||||
}
|
||||
|
||||
@@ -115,11 +112,11 @@ configure([rootProject] + javaProjects) { project ->
|
||||
ext.javadocLinks = [
|
||||
"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/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://www.eclipse.org/aspectj/doc/released/aspectj5rt-api/",
|
||||
"https://docs.oracle.com/cd/E13222_01/wls/docs90/javadocs/", // CommonJ and weblogic.* packages
|
||||
"https://www.ibm.com/docs/api/v1/content/SSEQTP_8.5.5/com.ibm.websphere.javadoc.doc/web/apidocs/", // com.ibm.*
|
||||
"https://docs.jboss.org/jbossas/javadoc/4.0.5/connector/", // org.jboss.resource.*
|
||||
"https://docs.jboss.org/hibernate/orm/5.6/javadocs/",
|
||||
"https://eclipse.dev/aspectj/doc/released/aspectj5rt-api",
|
||||
"https://www.quartz-scheduler.org/api/2.3.0/",
|
||||
"https://www.javadoc.io/doc/com.fasterxml.jackson.core/jackson-core/2.14.1/",
|
||||
"https://www.javadoc.io/doc/com.fasterxml.jackson.core/jackson-databind/2.14.1/",
|
||||
@@ -134,11 +131,9 @@ configure([rootProject] + javaProjects) { project ->
|
||||
"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.
|
||||
// Previously there could be a split-package issue between JSR250 and JSR305 javax.annotation packages,
|
||||
// but since 6.0 JSR 250 annotations such as @Resource and @PostConstruct have been replaced by their
|
||||
// JakartaEE equivalents in the jakarta.annotation package.
|
||||
"https://www.javadoc.io/doc/com.google.code.findbugs/jsr305/3.0.2/"
|
||||
] as String[]
|
||||
}
|
||||
|
||||
@@ -64,12 +64,6 @@ resource_types:
|
||||
<<: *docker-resource-source
|
||||
repository: dpb587/github-status-resource
|
||||
tag: master
|
||||
- name: pull-request
|
||||
type: registry-image
|
||||
source:
|
||||
<<: *docker-resource-source
|
||||
repository: teliaoss/github-pr-resource
|
||||
tag: v0.23.0
|
||||
- name: slack-notification
|
||||
type: registry-image
|
||||
source:
|
||||
@@ -111,14 +105,6 @@ resources:
|
||||
username: ((artifactory-username))
|
||||
password: ((artifactory-password))
|
||||
build_name: ((build-name))
|
||||
- name: git-pull-request
|
||||
type: pull-request
|
||||
icon: source-pull
|
||||
source:
|
||||
access_token: ((github-ci-pull-request-token))
|
||||
repository: ((github-repo-name))
|
||||
base_branch: ((branch))
|
||||
ignore_paths: ["ci/*"]
|
||||
- name: repo-status-build
|
||||
type: github-status-resource
|
||||
icon: eye-check-outline
|
||||
@@ -259,37 +245,6 @@ jobs:
|
||||
<<: *slack-fail-params
|
||||
- put: repo-status-jdk20-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:
|
||||
@@ -446,5 +401,3 @@ groups:
|
||||
jobs: ["stage-milestone", "stage-rc", "stage-release", "promote-milestone", "promote-rc", "promote-release", "create-github-release"]
|
||||
- name: "ci-images"
|
||||
jobs: ["build-ci-images"]
|
||||
- name: "pull-requests"
|
||||
jobs: [ "build-pull-requests" ]
|
||||
|
||||
@@ -5,5 +5,5 @@ source $(dirname $0)/common.sh
|
||||
|
||||
pushd git-repo > /dev/null
|
||||
./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false -Porg.gradle.java.installations.fromEnv=JDK17,JDK20 \
|
||||
-PmainToolchain=${MAIN_TOOLCHAIN} -PtestToolchain=${TEST_TOOLCHAIN} --no-daemon --max-workers=4 check
|
||||
-PmainToolchain=${MAIN_TOOLCHAIN} -PtestToolchain=${TEST_TOOLCHAIN} --no-daemon --max-workers=4 check antora
|
||||
popd > /dev/null
|
||||
|
||||
@@ -11,7 +11,7 @@ apply from: "${rootDir}/gradle/publications.gradle"
|
||||
|
||||
antora {
|
||||
version = '3.2.0-alpha.2'
|
||||
playbook = layout.buildDirectory.file('cached-antora-playbook.yml').get().getAsFile()
|
||||
playbook = 'cached-antora-playbook.yml'
|
||||
playbookProvider {
|
||||
repository = 'spring-projects/spring-framework'
|
||||
branch = 'docs-build'
|
||||
@@ -45,9 +45,11 @@ tasks.create("generateAntoraResources") {
|
||||
dependsOn 'generateAntoraYml'
|
||||
}
|
||||
|
||||
tasks.named("check") {
|
||||
dependsOn 'antora'
|
||||
}
|
||||
// Commented out for now:
|
||||
// https://github.com/spring-projects/spring-framework/issues/30481
|
||||
// tasks.named("check") {
|
||||
// dependsOn 'antora'
|
||||
// }
|
||||
|
||||
jar {
|
||||
enabled = false
|
||||
@@ -102,7 +104,7 @@ task api(type: Javadoc) {
|
||||
overview = "framework-docs/src/docs/api/overview.html"
|
||||
splitIndex = true
|
||||
links(project.ext.javadocLinks)
|
||||
addBooleanOption('Xdoclint:syntax', true) // only check syntax with doclint
|
||||
addBooleanOption('Xdoclint:syntax,reference', true) // only check syntax and reference with doclint
|
||||
addBooleanOption('Werror', true) // fail build on Javadoc warnings
|
||||
}
|
||||
source moduleProjects.collect { project ->
|
||||
@@ -227,4 +229,4 @@ publishing {
|
||||
artifact distZip
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ The following table lists all currently supported Spring properties.
|
||||
| `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`]
|
||||
{api-spring-framework}++/beans/StandardBeanInfoFactory.html#IGNORE_BEANINFO_PROPERTY_NAME++[`CachedIntrospectionResults`]
|
||||
for details.
|
||||
|
||||
| `spring.expression.compiler.mode`
|
||||
|
||||
@@ -154,7 +154,6 @@ Java::
|
||||
----
|
||||
package com.xyz;
|
||||
|
||||
@Aspect
|
||||
public class Pointcuts {
|
||||
|
||||
@Pointcut("execution(public * *(..))")
|
||||
@@ -179,7 +178,6 @@ Kotlin::
|
||||
----
|
||||
package com.xyz
|
||||
|
||||
@Aspect
|
||||
class Pointcuts {
|
||||
|
||||
@Pointcut("execution(public * *(..))")
|
||||
@@ -211,9 +209,9 @@ pointcut matching.
|
||||
|
||||
When working with enterprise applications, developers often have the need to refer to
|
||||
modules of the application and particular sets of operations from within several aspects.
|
||||
We recommend defining a dedicated aspect that captures commonly used _named pointcut_
|
||||
expressions for this purpose. Such an aspect typically resembles the following
|
||||
`CommonPointcuts` example (though what you name the aspect is up to you):
|
||||
We recommend defining a dedicated class that captures commonly used _named pointcut_
|
||||
expressions for this purpose. Such a class typically resembles the following
|
||||
`CommonPointcuts` example (though what you name the class is up to you):
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -223,10 +221,8 @@ Java::
|
||||
----
|
||||
package com.xyz;
|
||||
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Pointcut;
|
||||
|
||||
@Aspect
|
||||
public class CommonPointcuts {
|
||||
|
||||
/**
|
||||
@@ -287,10 +283,8 @@ Kotlin::
|
||||
----
|
||||
package com.xyz
|
||||
|
||||
import org.aspectj.lang.annotation.Aspect
|
||||
import org.aspectj.lang.annotation.Pointcut
|
||||
|
||||
@Aspect
|
||||
class CommonPointcuts {
|
||||
|
||||
/**
|
||||
@@ -346,9 +340,9 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
You can refer to the pointcuts defined in such an aspect anywhere you need a pointcut
|
||||
expression by referencing the fully-qualified name of the `@Aspect` class combined with
|
||||
the `@Pointcut` method's name. For example, to make the service layer transactional, you
|
||||
You can refer to the pointcuts defined in such a class anywhere you need a pointcut
|
||||
expression by referencing the fully-qualified name of the class combined with the
|
||||
`@Pointcut` method's name. For example, to make the service layer transactional, you
|
||||
could write the following which references the
|
||||
`com.xyz.CommonPointcuts.businessService()` _named pointcut_:
|
||||
|
||||
|
||||
@@ -318,6 +318,51 @@ Java::
|
||||
----
|
||||
======
|
||||
|
||||
[[aot.bestpractices.jpa]]
|
||||
=== JPA
|
||||
|
||||
The JPA persistence unit has to be known upfront for certain optimizations to apply. Consider the following basic example:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
@Bean
|
||||
LocalContainerEntityManagerFactoryBean customDBEntityManagerFactory(DataSource dataSource) {
|
||||
LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean();
|
||||
factoryBean.setDataSource(dataSource);
|
||||
factoryBean.setPackagesToScan("com.example.app");
|
||||
return factoryBean;
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
To make sure the scanning occurs ahead of time, a `PersistenceManagedTypes` bean must be declared and used by the
|
||||
factory bean definition, as shown by the following example:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
@Bean
|
||||
PersistenceManagedTypes persistenceManagedTypes(ResourceLoader resourceLoader) {
|
||||
return new PersistenceManagedTypesScanner(resourceLoader)
|
||||
.scan("com.example.app");
|
||||
}
|
||||
|
||||
@Bean
|
||||
LocalContainerEntityManagerFactoryBean customDBEntityManagerFactory(DataSource dataSource, PersistenceManagedTypes managedTypes) {
|
||||
LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean();
|
||||
factoryBean.setDataSource(dataSource);
|
||||
factoryBean.setManagedTypes(managedTypes);
|
||||
return factoryBean;
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
[[aot.hints]]
|
||||
== Runtime Hints
|
||||
|
||||
@@ -38,7 +38,6 @@ The expression language supports the following functionality:
|
||||
* Class expressions
|
||||
* Accessing properties, arrays, lists, and maps
|
||||
* Method invocation
|
||||
* Relational operators
|
||||
* Assignment
|
||||
* Calling constructors
|
||||
* Bean references
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
= Null-safety
|
||||
|
||||
Although Java does not let you express null-safety with its type system, the Spring Framework
|
||||
now provides the following annotations in the `org.springframework.lang` package to let you
|
||||
provides the following annotations in the `org.springframework.lang` package to let you
|
||||
declare nullability of APIs and fields:
|
||||
|
||||
* {api-spring-framework}/lang/Nullable.html[`@Nullable`]: Annotation to indicate that a
|
||||
specific parameter, return value, or field can be `null`.
|
||||
* {api-spring-framework}/lang/NonNull.html[`@NonNull`]: Annotation to indicate that a specific
|
||||
parameter, return value, or field cannot be `null` (not needed on parameters / return values
|
||||
parameter, return value, or field cannot be `null` (not needed on parameters, return values,
|
||||
and fields where `@NonNullApi` and `@NonNullFields` apply, respectively).
|
||||
* {api-spring-framework}/lang/NonNullApi.html[`@NonNullApi`]: Annotation at the package level
|
||||
that declares non-null as the default semantics for parameters and return values.
|
||||
@@ -17,11 +17,10 @@ level that declares non-null as the default semantics for fields.
|
||||
|
||||
The Spring Framework itself leverages these annotations, but they can also be used in any
|
||||
Spring-based Java project to declare null-safe APIs and optionally null-safe fields.
|
||||
Generic type arguments, varargs and array elements nullability are not supported yet but
|
||||
should be in an upcoming release, see https://jira.spring.io/browse/SPR-15942[SPR-15942]
|
||||
for up-to-date information. Nullability declarations are expected to be fine-tuned between
|
||||
Spring Framework releases, including minor ones. Nullability of types used inside method
|
||||
bodies is outside of the scope of this feature.
|
||||
Nullability declarations for generic type arguments, varargs, and array elements are not supported yet.
|
||||
Nullability declarations are expected to be fine-tuned between Spring Framework releases,
|
||||
including minor ones. Nullability of types used inside method bodies is outside the
|
||||
scope of this feature.
|
||||
|
||||
NOTE: Other common libraries such as Reactor and Spring Data provide null-safe APIs that
|
||||
use a similar nullability arrangement, delivering a consistent overall experience for
|
||||
@@ -37,8 +36,8 @@ In addition to providing an explicit declaration for Spring Framework API nullab
|
||||
these annotations can be used by an IDE (such as IDEA or Eclipse) to provide useful
|
||||
warnings related to null-safety in order to avoid `NullPointerException` at runtime.
|
||||
|
||||
They are also used to make Spring API null-safe in Kotlin projects, since Kotlin natively
|
||||
supports https://kotlinlang.org/docs/reference/null-safety.html[null-safety]. More details
|
||||
They are also used to make Spring APIs null-safe in Kotlin projects, since Kotlin natively
|
||||
supports https://kotlinlang.org/docs/null-safety.html[null-safety]. More details
|
||||
are available in the xref:languages/kotlin/null-safety.adoc[Kotlin support documentation].
|
||||
|
||||
|
||||
@@ -48,11 +47,11 @@ are available in the xref:languages/kotlin/null-safety.adoc[Kotlin support docum
|
||||
== JSR-305 meta-annotations
|
||||
|
||||
Spring annotations are meta-annotated with https://jcp.org/en/jsr/detail?id=305[JSR 305]
|
||||
annotations (a dormant but wide-spread JSR). JSR-305 meta-annotations let tooling vendors
|
||||
annotations (a dormant but widespread JSR). JSR-305 meta-annotations let tooling vendors
|
||||
like IDEA or Kotlin provide null-safety support in a generic way, without having to
|
||||
hard-code support for Spring annotations.
|
||||
|
||||
It is not necessary nor recommended to add a JSR-305 dependency to the project classpath to
|
||||
take advantage of Spring null-safe API. Only projects such as Spring-based libraries that use
|
||||
It is neither necessary nor recommended to add a JSR-305 dependency to the project classpath to
|
||||
take advantage of Spring's null-safe APIs. Only projects such as Spring-based libraries that use
|
||||
null-safety annotations in their codebase should add `com.google.code.findbugs:jsr305:3.0.2`
|
||||
with `compileOnly` Gradle configuration or Maven `provided` scope to avoid compile warnings.
|
||||
with `compileOnly` Gradle configuration or Maven `provided` scope to avoid compiler warnings.
|
||||
|
||||
@@ -151,10 +151,10 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
The last example we show here is for typical JDBC support. You could have the
|
||||
`DataSource` injected into an initialization method or a constructor, where you would create a
|
||||
`JdbcTemplate` and other data access support classes (such as `SimpleJdbcCall` and others) by using
|
||||
this `DataSource`. The following example autowires a `DataSource`:
|
||||
The last example we show here is for typical JDBC support. You could have the `DataSource`
|
||||
injected into an initialization method or a constructor, where you would create a `JdbcTemplate`
|
||||
and other data access support classes (such as `SimpleJdbcCall` and others) by using this
|
||||
`DataSource`. The following example autowires a `DataSource`:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
|
||||
@@ -9,13 +9,13 @@ to the database.
|
||||
[[jdbc-batch-classic]]
|
||||
== Basic Batch Operations with `JdbcTemplate`
|
||||
|
||||
You accomplish `JdbcTemplate` batch processing by implementing two methods of a special
|
||||
interface, `BatchPreparedStatementSetter`, and passing that implementation in as the second parameter
|
||||
You accomplish `JdbcTemplate` batch processing by implementing two methods of a special interface,
|
||||
`BatchPreparedStatementSetter`, and passing that implementation in as the second parameter
|
||||
in your `batchUpdate` method call. You can use the `getBatchSize` method to provide the size of
|
||||
the current batch. You can use the `setValues` method to set the values for the parameters of
|
||||
the prepared statement. This method is called the number of times that you
|
||||
specified in the `getBatchSize` call. The following example updates the `t_actor` table
|
||||
based on entries in a list, and the entire list is used as the batch:
|
||||
the prepared statement. This method is called the number of times that you specified in the
|
||||
`getBatchSize` call. The following example updates the `t_actor` table based on entries in a list,
|
||||
and the entire list is used as the batch:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
|
||||
@@ -10,7 +10,7 @@ This section covers:
|
||||
* xref:data-access/jdbc/connections.adoc#jdbc-SingleConnectionDataSource[Using `SingleConnectionDataSource`]
|
||||
* xref:data-access/jdbc/connections.adoc#jdbc-DriverManagerDataSource[Using `DriverManagerDataSource`]
|
||||
* xref:data-access/jdbc/connections.adoc#jdbc-TransactionAwareDataSourceProxy[Using `TransactionAwareDataSourceProxy`]
|
||||
* xref:data-access/jdbc/connections.adoc#jdbc-DataSourceTransactionManager[Using `DataSourceTransactionManager`]
|
||||
* xref:data-access/jdbc/connections.adoc#jdbc-DataSourceTransactionManager[Using `DataSourceTransactionManager` / `JdbcTransactionManager`]
|
||||
|
||||
|
||||
[[jdbc-datasource]]
|
||||
@@ -125,8 +125,12 @@ The following example shows C3P0 configuration:
|
||||
== Using `DataSourceUtils`
|
||||
|
||||
The `DataSourceUtils` class is a convenient and powerful helper class that provides
|
||||
`static` methods to obtain connections from JNDI and close connections if necessary. It
|
||||
supports thread-bound connections with, for example, `DataSourceTransactionManager`.
|
||||
`static` methods to obtain connections from JNDI and close connections if necessary.
|
||||
It supports a thread-bound JDBC `Connection` with `DataSourceTransactionManager` but
|
||||
also with `JtaTransactionManager` and `JpaTransactionManager`.
|
||||
|
||||
Note that `JdbcTemplate` implies `DataSourceUtils` connection access, using it
|
||||
behind every JDBC operation, implicitly participating in an ongoing transaction.
|
||||
|
||||
|
||||
[[jdbc-SmartDataSource]]
|
||||
@@ -165,7 +169,6 @@ In contrast to `DriverManagerDataSource`, it reuses the same connection all the
|
||||
avoiding excessive creation of physical connections.
|
||||
|
||||
|
||||
|
||||
[[jdbc-DriverManagerDataSource]]
|
||||
== Using `DriverManagerDataSource`
|
||||
|
||||
@@ -201,29 +204,44 @@ javadoc for more details.
|
||||
|
||||
|
||||
[[jdbc-DataSourceTransactionManager]]
|
||||
== Using `DataSourceTransactionManager`
|
||||
== Using `DataSourceTransactionManager` / `JdbcTransactionManager`
|
||||
|
||||
The `DataSourceTransactionManager` class is a `PlatformTransactionManager`
|
||||
implementation for single JDBC data sources. It binds a JDBC connection from the
|
||||
specified data source to the currently executing thread, potentially allowing for one
|
||||
thread connection per data source.
|
||||
implementation for a single JDBC `DataSource`. It binds a JDBC `Connection`
|
||||
from the specified `DataSource` to the currently executing thread, potentially
|
||||
allowing for one thread-bound `Connection` per `DataSource`.
|
||||
|
||||
Application code is required to retrieve the JDBC connection through
|
||||
`DataSourceUtils.getConnection(DataSource)` instead of Jakarta EE's standard
|
||||
Application code is required to retrieve the JDBC `Connection` through
|
||||
`DataSourceUtils.getConnection(DataSource)` instead of Java 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
|
||||
behaves exactly like the common one. Thus, it can be used in any case.
|
||||
instead of checked `SQLExceptions`. All framework classes (such as `JdbcTemplate`) use
|
||||
this strategy implicitly. If not used with a transaction manager, the lookup strategy
|
||||
behaves exactly like `DataSource.getConnection` and can therefore be used in any case.
|
||||
|
||||
The `DataSourceTransactionManager` class supports custom isolation levels and timeouts
|
||||
that get applied as appropriate JDBC statement query timeouts. To support the latter,
|
||||
application code must either use `JdbcTemplate` or call the
|
||||
`DataSourceUtils.applyTransactionTimeout(..)` method for each created statement.
|
||||
The `DataSourceTransactionManager` class supports savepoints (`PROPAGATION_NESTED`),
|
||||
custom isolation levels, and timeouts that get applied as appropriate JDBC statement
|
||||
query timeouts. To support the latter, application code must either use `JdbcTemplate` or
|
||||
call the `DataSourceUtils.applyTransactionTimeout(..)` method for each created statement.
|
||||
|
||||
You can use this implementation instead of `JtaTransactionManager` in the single-resource
|
||||
case, as it does not require the container to support JTA. Switching between
|
||||
both is just a matter of configuration, provided you stick to the required connection lookup
|
||||
pattern. JTA does not support custom isolation levels.
|
||||
You can use `DataSourceTransactionManager` instead of `JtaTransactionManager` in the
|
||||
single-resource case, as it does not require the container to support a JTA transaction
|
||||
coordinator. Switching between these transaction managers is just a matter of configuration,
|
||||
provided you stick to the required connection lookup pattern. Note that JTA does not support
|
||||
savepoints or custom isolation levels and has a different timeout mechanism but otherwise
|
||||
exposes similar behavior in terms of JDBC resources and JDBC commit/rollback management.
|
||||
|
||||
NOTE: As of 5.3, Spring provides an extended `JdbcTransactionManager` variant which adds
|
||||
exception translation capabilities on commit/rollback (aligned with `JdbcTemplate`).
|
||||
Where `DataSourceTransactionManager` will only ever throw `TransactionSystemException`
|
||||
(analogous to JTA), `JdbcTransactionManager` translates database locking failures etc to
|
||||
corresponding `DataAccessException` subclasses. Note that application code needs to be
|
||||
prepared for such exceptions, not exclusively expecting `TransactionSystemException`.
|
||||
In scenarios where that is the case, `JdbcTransactionManager` is the recommended choice.
|
||||
|
||||
In terms of exception behavior, `JdbcTransactionManager` is roughly equivalent to
|
||||
`JpaTransactionManager` and also to `R2dbcTransactionManager`, serving as an immediate
|
||||
companion/replacement for each other. `DataSourceTransactionManager` on the other hand
|
||||
is equivalent to `JtaTransactionManager` and can serve as a direct replacement there.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -718,12 +718,22 @@ See also xref:data-access/jdbc/core.adoc#jdbc-JdbcTemplate-idioms[`JdbcTemplate`
|
||||
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.
|
||||
codes) for greater precision. This exception translation mechanism is used behind the
|
||||
the common `JdbcTemplate` and `JdbcTransactionManager` entry points which do not
|
||||
propagate `SQLException` but rather `DataAccessException`.
|
||||
|
||||
NOTE: As of 6.0, the default exception translator is `SQLExceptionSubclassTranslator`,
|
||||
detecting JDBC 4 `SQLException` subclasses with a few extra checks, and with a fallback
|
||||
to `SQLState` introspection through `SQLStateSQLExceptionTranslator`. This is usually
|
||||
sufficient for common database access and does not require vendor-specific detection.
|
||||
For backwards compatibility, consider using `SQLErrorCodeSQLExceptionTranslator` as
|
||||
described below, potentially with custom error code mappings.
|
||||
|
||||
`SQLErrorCodeSQLExceptionTranslator` is the implementation of `SQLExceptionTranslator`
|
||||
that is used by default. This implementation uses specific vendor codes. It is more
|
||||
precise than the `SQLState` implementation. The error code translations are based on
|
||||
codes held in a JavaBean type class called `SQLErrorCodes`. This class is created and
|
||||
that is used by default when a file named `sql-error-codes.xml` is present in the root
|
||||
of the classpath. This implementation uses specific vendor codes. It is more precise than
|
||||
`SQLState` or `SQLException` subclass translation. The error code translations are based
|
||||
on codes held in a JavaBean type class called `SQLErrorCodes`. This class is created and
|
||||
populated by an `SQLErrorCodesFactory`, which (as the name suggests) is a factory for
|
||||
creating `SQLErrorCodes` based on the contents of a configuration file named
|
||||
`sql-error-codes.xml`. This file is populated with vendor codes and based on the
|
||||
@@ -744,8 +754,8 @@ The `SQLErrorCodeSQLExceptionTranslator` applies matching rules in the following
|
||||
translator. If this translation is not available, the next fallback translator is
|
||||
the `SQLStateSQLExceptionTranslator`.
|
||||
|
||||
NOTE: The `SQLErrorCodesFactory` is used by default to define `Error` codes and custom exception
|
||||
translations. They are looked up in a file named `sql-error-codes.xml` from the
|
||||
NOTE: The `SQLErrorCodesFactory` is used by default to define error codes and custom
|
||||
exception translations. They are looked up in a file named `sql-error-codes.xml` from the
|
||||
classpath, and the matching `SQLErrorCodes` instance is located based on the database
|
||||
name from the database metadata of the database in use.
|
||||
|
||||
@@ -784,12 +794,12 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
In the preceding example, the specific error code (`-12345`) is translated, while other errors are
|
||||
left to be translated by the default translator implementation. To use this custom
|
||||
translator, you must pass it to the `JdbcTemplate` through the method
|
||||
`setExceptionTranslator`, and you must use this `JdbcTemplate` for all of the data access
|
||||
processing where this translator is needed. The following example shows how you can use this custom
|
||||
translator:
|
||||
In the preceding example, the specific error code (`-12345`) is translated while
|
||||
other errors are left to be translated by the default translator implementation.
|
||||
To use this custom translator, you must pass it to the `JdbcTemplate` through the
|
||||
method `setExceptionTranslator`, and you must use this `JdbcTemplate` for all of the
|
||||
data access processing where this translator is needed. The following example shows
|
||||
how you can use this custom translator:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -800,7 +810,6 @@ Java::
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
public void setDataSource(DataSource dataSource) {
|
||||
|
||||
// create a JdbcTemplate and set data source
|
||||
this.jdbcTemplate = new JdbcTemplate();
|
||||
this.jdbcTemplate.setDataSource(dataSource);
|
||||
@@ -809,7 +818,6 @@ Java::
|
||||
CustomSQLErrorCodesTranslator tr = new CustomSQLErrorCodesTranslator();
|
||||
tr.setDataSource(dataSource);
|
||||
this.jdbcTemplate.setExceptionTranslator(tr);
|
||||
|
||||
}
|
||||
|
||||
public void updateShippingCharge(long orderId, long pct) {
|
||||
|
||||
@@ -3,30 +3,30 @@
|
||||
|
||||
The Spring Framework's JDBC abstraction framework consists of four different packages:
|
||||
|
||||
* `core`: The `org.springframework.jdbc.core` package contains the `JdbcTemplate` class and its
|
||||
various callback interfaces, plus a variety of related classes. A subpackage named
|
||||
`org.springframework.jdbc.core.simple` contains the `SimpleJdbcInsert` and
|
||||
* `core`: The `org.springframework.jdbc.core` package contains the `JdbcTemplate` class
|
||||
and its various callback interfaces, plus a variety of related classes. A subpackage
|
||||
named `org.springframework.jdbc.core.simple` contains the `SimpleJdbcInsert` and
|
||||
`SimpleJdbcCall` classes. Another subpackage named
|
||||
`org.springframework.jdbc.core.namedparam` contains the `NamedParameterJdbcTemplate`
|
||||
class and the related support classes. See xref:data-access/jdbc/core.adoc[Using the JDBC Core Classes to Control Basic JDBC Processing and Error Handling], xref:data-access/jdbc/advanced.adoc[JDBC Batch Operations], and
|
||||
xref:data-access/jdbc/simple.adoc[Simplifying JDBC Operations with the `SimpleJdbc` Classes].
|
||||
|
||||
* `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 Jakarta EE container. A subpackage
|
||||
named `org.springfamework.jdbc.datasource.embedded` provides support for creating
|
||||
* `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 Jakarta EE container. A subpackage
|
||||
named `org.springframework.jdbc.datasource.embedded` provides support for creating
|
||||
embedded databases by using Java database engines, such as HSQL, H2, and Derby. See
|
||||
xref:data-access/jdbc/connections.adoc[Controlling Database Connections] and xref:data-access/jdbc/embedded-database-support.adoc[Embedded Database Support].
|
||||
|
||||
* `object`: The `org.springframework.jdbc.object` package contains classes that represent RDBMS
|
||||
queries, updates, and stored procedures as thread-safe, reusable objects. See
|
||||
* `object`: The `org.springframework.jdbc.object` package contains classes that represent
|
||||
RDBMS queries, updates, and stored procedures as thread-safe, reusable objects. See
|
||||
xref:data-access/jdbc/object.adoc[Modeling JDBC Operations as Java Objects]. This approach is modeled by JDO, although objects returned by queries
|
||||
are naturally disconnected from the database. This higher-level of JDBC abstraction
|
||||
depends on the lower-level abstraction in the `org.springframework.jdbc.core` package.
|
||||
|
||||
* `support`: The `org.springframework.jdbc.support` package provides `SQLException` translation
|
||||
functionality and some utility classes. Exceptions thrown during JDBC processing are
|
||||
translated to exceptions defined in the `org.springframework.dao` package. This means
|
||||
* `support`: The `org.springframework.jdbc.support` package provides `SQLException`
|
||||
translation functionality and some utility classes. Exceptions thrown during JDBC processing
|
||||
are translated to exceptions defined in the `org.springframework.dao` package. This means
|
||||
that code using the Spring JDBC abstraction layer does not need to implement JDBC or
|
||||
RDBMS-specific error handling. All translated exceptions are unchecked, which gives you
|
||||
the option of catching the exceptions from which you can recover while letting other
|
||||
|
||||
@@ -8,10 +8,16 @@ implementations and transaction demarcation. Most of these patterns can be direc
|
||||
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
|
||||
[NOTE]
|
||||
====
|
||||
As of Spring Framework 6.0, Spring requires Hibernate ORM 5.5+ for Spring's
|
||||
`HibernateJpaVendorAdapter` as well as for a native Hibernate `SessionFactory` setup.
|
||||
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.
|
||||
We recommend Hibernate ORM 5.6 as the last feature branch in that Hibernate generation.
|
||||
|
||||
Hibernate ORM 6.x is only supported as a JPA provider (`HibernateJpaVendorAdapter`).
|
||||
Plain `SessionFactory` setup with the `orm.hibernate5` package is not supported anymore.
|
||||
We recommend Hibernate ORM 6.1/6.2 with JPA-style setup for new development projects.
|
||||
====
|
||||
|
||||
|
||||
[[orm-session-factory-setup]]
|
||||
|
||||
@@ -495,7 +495,7 @@ features supported by Spring, usually in a vendor-specific manner:
|
||||
* Applying specific transaction semantics (such as custom isolation level or transaction
|
||||
timeout)
|
||||
* Retrieving the transactional JDBC `Connection` (for exposure to JDBC-based DAOs)
|
||||
* Advanced translation of `PersistenceExceptions` to Spring `DataAccessExceptions`
|
||||
* Advanced translation of `PersistenceException` to Spring's `DataAccessException`
|
||||
|
||||
This is particularly valuable for special transaction semantics and for advanced
|
||||
translation of exception. The default implementation (`DefaultJpaDialect`) does
|
||||
|
||||
@@ -417,9 +417,9 @@ Do not pass `Collection<String>` or the like as an array parameter.
|
||||
[[r2dbc-DatabaseClient-filter]]
|
||||
==== Statement Filters
|
||||
|
||||
Sometimes it you need to fine-tune options on the actual `Statement`
|
||||
before it gets run. Register a `Statement` filter
|
||||
(`StatementFilterFunction`) through `DatabaseClient` to intercept and
|
||||
Sometimes you need to fine-tune options on the actual `Statement`
|
||||
before it gets run. To do so, register a `Statement` filter
|
||||
(`StatementFilterFunction`) with the `DatabaseClient` to intercept and
|
||||
modify statements in their execution, as the following example shows:
|
||||
|
||||
[tabs]
|
||||
@@ -445,7 +445,8 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
`DatabaseClient` exposes also simplified `filter(…)` overload accepting `Function<Statement, Statement>`:
|
||||
`DatabaseClient` also exposes a simplified `filter(…)` overload that accepts
|
||||
a `Function<Statement, Statement>`:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -717,19 +718,15 @@ javadoc for more details.
|
||||
=== Using `R2dbcTransactionManager`
|
||||
|
||||
The `R2dbcTransactionManager` class is a `ReactiveTransactionManager` implementation for
|
||||
single R2DBC data sources. It binds an R2DBC connection from the specified connection factory
|
||||
to the subscriber `Context`, potentially allowing for one subscriber connection for each
|
||||
connection factory.
|
||||
a single R2DBC `ConnectionFactory`. It binds an R2DBC `Connection` from the specified
|
||||
`ConnectionFactory` to the subscriber `Context`, potentially allowing for one subscriber
|
||||
`Connection` for each `ConnectionFactory`.
|
||||
|
||||
Application code is required to retrieve the R2DBC connection through
|
||||
Application code is required to retrieve the R2DBC `Connection` through
|
||||
`ConnectionFactoryUtils.getConnection(ConnectionFactory)`, instead of R2DBC's standard
|
||||
`ConnectionFactory.create()`.
|
||||
|
||||
All framework classes (such as `DatabaseClient`) use this strategy implicitly.
|
||||
If not used with this transaction manager, the lookup strategy behaves exactly like the common one.
|
||||
Thus, it can be used in any case.
|
||||
|
||||
The `R2dbcTransactionManager` class supports custom isolation levels that get applied to the connection.
|
||||
`ConnectionFactory.create()`. All framework classes (such as `DatabaseClient`) use this
|
||||
strategy implicitly. If not used with a transaction manager, the lookup strategy behaves
|
||||
exactly like `ConnectionFactory.create()` and can therefore be used in any case.
|
||||
|
||||
|
||||
|
||||
|
||||
+3
-4
@@ -124,7 +124,6 @@ In XML configuration, the `<tx:annotation-driven/>` tag provides similar conveni
|
||||
----
|
||||
<1> The line that makes the bean instance transactional.
|
||||
|
||||
|
||||
TIP: You can omit the `transaction-manager` attribute in the `<tx:annotation-driven/>`
|
||||
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
|
||||
@@ -522,17 +521,17 @@ The following listing shows the bean declarations:
|
||||
----
|
||||
<tx:annotation-driven/>
|
||||
|
||||
<bean id="transactionManager1" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
|
||||
<bean id="transactionManager1" class="org.springframework.jdbc.support.JdbcTransactionManager">
|
||||
...
|
||||
<qualifier value="order"/>
|
||||
</bean>
|
||||
|
||||
<bean id="transactionManager2" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
|
||||
<bean id="transactionManager2" class="org.springframework.jdbc.support.JdbcTransactionManager">
|
||||
...
|
||||
<qualifier value="account"/>
|
||||
</bean>
|
||||
|
||||
<bean id="transactionManager3" class="org.springframework.data.r2dbc.connectionfactory.R2dbcTransactionManager">
|
||||
<bean id="transactionManager3" class="org.springframework.data.r2dbc.connection.R2dbcTransactionManager">
|
||||
...
|
||||
<qualifier value="reactive-account"/>
|
||||
</bean>
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
|
||||
For more information about the Spring Framework's transaction support, see:
|
||||
|
||||
* 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.
|
||||
* link:++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.
|
||||
* https://www.infoq.com/minibooks/JTDS[_Java Transaction Design Strategies_] is a book
|
||||
available from https://www.infoq.com/[InfoQ] that provides a well-paced introduction
|
||||
to transactions in Java. It also includes side-by-side examples of how to configure
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
= Observability Support
|
||||
|
||||
Micrometer defines an https://micrometer.io/docs/observation[Observation concept that enables both Metrics and Traces] in applications.
|
||||
Metrics support offers a way to create timers, gauges or counters for collecting statistics about the runtime behavior of your application.
|
||||
Metrics can help you to track error rates, usage patterns, performance and more.
|
||||
Metrics support offers a way to create timers, gauges, or counters for collecting statistics about the runtime behavior of your application.
|
||||
Metrics can help you to track error rates, usage patterns, performance, and more.
|
||||
Traces provide a holistic view of an entire system, crossing application boundaries; you can zoom in on particular user requests and follow their entire completion across applications.
|
||||
|
||||
Spring Framework instruments various parts of its own codebase to publish observations if an `ObservationRegistry` is configured.
|
||||
@@ -36,16 +36,16 @@ https://micrometer.io/docs/concepts#_naming_meters[to the format preferred by th
|
||||
[[observability.concepts]]
|
||||
== Micrometer Observation concepts
|
||||
|
||||
If you are not familiar with Micrometer Observation, here's a quick summary of the new concepts you should know about.
|
||||
If you are not familiar with Micrometer Observation, here's a quick summary of the concepts you should know about.
|
||||
|
||||
* `Observation` is the actual recording of something happening in your application. This is processed by `ObservationHandler` implementations to produce metrics or traces.
|
||||
* Each observation has a corresponding `ObservationContext` implementation; this type holds all the relevant information for extracting metadata for it.
|
||||
In the case of an HTTP server observation, the context implementation could hold the HTTP request, the HTTP response, any Exception thrown during processing...
|
||||
* Each `Observation` holds `KeyValues` metadata. In the case of a server HTTP observation, this could be the HTTP request method, the HTTP response status...
|
||||
In the case of an HTTP server observation, the context implementation could hold the HTTP request, the HTTP response, any exception thrown during processing, and so forth.
|
||||
* Each `Observation` holds `KeyValues` metadata. In the case of an HTTP server observation, this could be the HTTP request method, the HTTP response status, and so forth.
|
||||
This metadata is contributed by `ObservationConvention` implementations which should declare the type of `ObservationContext` they support.
|
||||
* `KeyValues` are said to be "low cardinality" if there is a low, bounded number of possible values for the `KeyValue` tuple (HTTP method is a good example).
|
||||
Low cardinality values are contributed to metrics only.
|
||||
High cardinality values are on the other hand unbounded (for example, HTTP request URIs) and are only contributed to Traces.
|
||||
Conversely, "high cardinality" values are unbounded (for example, HTTP request URIs) and are only contributed to traces.
|
||||
* An `ObservationDocumentation` documents all observations in a particular domain, listing the expected key names and their meaning.
|
||||
|
||||
|
||||
@@ -63,16 +63,16 @@ Each instrumented component will provide two extension points:
|
||||
=== Using custom Observation conventions
|
||||
|
||||
Let's take the example of the Spring MVC "http.server.requests" metrics instrumentation with the `ServerHttpObservationFilter`.
|
||||
This observation is using a `ServerRequestObservationConvention` with a `ServerRequestObservationContext`; custom conventions can be configured on the Servlet filter.
|
||||
This observation uses a `ServerRequestObservationConvention` with a `ServerRequestObservationContext`; custom conventions can be configured on the Servlet filter.
|
||||
If you would like to customize the metadata produced with the observation, you can extend the `DefaultServerRequestObservationConvention` for your requirements:
|
||||
|
||||
include-code::./ExtendedServerRequestObservationConvention[]
|
||||
|
||||
If you want full control, you can then implement the entire convention contract for the observation you're interested in:
|
||||
If you want full control, you can implement the entire convention contract for the observation you're interested in:
|
||||
|
||||
include-code::./CustomServerRequestObservationConvention[]
|
||||
|
||||
You can also achieve similar goals using a custom `ObservationFilter` - adding or removing key values for an observation.
|
||||
You can also achieve similar goals using a custom `ObservationFilter` – adding or removing key values for an observation.
|
||||
Filters do not replace the default convention and are used as a post-processing component.
|
||||
|
||||
include-code::./ServerRequestObservationFilter[]
|
||||
@@ -83,20 +83,24 @@ You can configure `ObservationFilter` instances on the `ObservationRegistry`.
|
||||
[[observability.http-server]]
|
||||
== HTTP Server instrumentation
|
||||
|
||||
HTTP server exchanges observations are created with the name `"http.server.requests"` for Servlet and Reactive applications.
|
||||
HTTP server exchange observations are created with the name `"http.server.requests"` for Servlet and Reactive applications.
|
||||
|
||||
[[observability.http-server.servlet]]
|
||||
=== Servlet applications
|
||||
|
||||
Applications need to configure the `org.springframework.web.filter.ServerHttpObservationFilter` Servlet filter in their application.
|
||||
It is using the `org.springframework.http.server.observation.DefaultServerRequestObservationConvention` by default, backed by the `ServerRequestObservationContext`.
|
||||
It uses the `org.springframework.http.server.observation.DefaultServerRequestObservationConvention` by default, backed by the `ServerRequestObservationContext`.
|
||||
|
||||
This will only record an observation as an error if the `Exception` has not been handled by the web Framework and has bubbled up to the Servlet filter.
|
||||
This will only record an observation as an error if the `Exception` has not been handled by the web framework and has bubbled up to the Servlet filter.
|
||||
Typically, all exceptions handled by Spring MVC's `@ExceptionHandler` and xref:web/webmvc/mvc-ann-rest-exceptions.adoc[`ProblemDetail` support] will not be recorded with the observation.
|
||||
You can, at any point during request processing, set the error field on the `ObservationContext` yourself:
|
||||
|
||||
include-code::./UserController[]
|
||||
|
||||
NOTE: Because the instrumentation is done at the Servlet Filter level, the observation scope only covers the filters ordered after this one as well as the handling of the request.
|
||||
Typically, Servlet container error handling is performed at a lower level and won't have any active observation or span.
|
||||
For this use case, a container-specific implementation is required, such as a `org.apache.catalina.Valve` for Tomcat; this is outside of the scope of this project.
|
||||
|
||||
By default, the following `KeyValues` are created:
|
||||
|
||||
.Low cardinality Keys
|
||||
@@ -122,9 +126,9 @@ By default, the following `KeyValues` are created:
|
||||
=== Reactive applications
|
||||
|
||||
Applications need to configure the `org.springframework.web.filter.reactive.ServerHttpObservationFilter` reactive `WebFilter` in their application.
|
||||
It is using the `org.springframework.http.server.reactive.observation.DefaultServerRequestObservationConvention` by default, backed by the `ServerRequestObservationContext`.
|
||||
It uses the `org.springframework.http.server.reactive.observation.DefaultServerRequestObservationConvention` by default, backed by the `ServerRequestObservationContext`.
|
||||
|
||||
This will only record an observation as an error if the `Exception` has not been handled by the web Framework and has bubbled up to the `WebFilter`.
|
||||
This will only record an observation as an error if the `Exception` has not been handled by the web framework and has bubbled up to the `WebFilter`.
|
||||
Typically, all exceptions handled by Spring WebFlux's `@ExceptionHandler` and xref:web/webflux/ann-rest-exceptions.adoc[`ProblemDetail` support] will not be recorded with the observation.
|
||||
You can, at any point during request processing, set the error field on the `ObservationContext` yourself:
|
||||
|
||||
@@ -153,9 +157,9 @@ By default, the following `KeyValues` are created:
|
||||
|
||||
|
||||
[[observability.http-client]]
|
||||
== HTTP Client instrumentation
|
||||
== HTTP Client Instrumentation
|
||||
|
||||
HTTP client exchanges observations are created with the name `"http.client.requests"` for blocking and reactive clients.
|
||||
HTTP client exchange observations are created with the name `"http.client.requests"` for blocking and reactive clients.
|
||||
Unlike their server counterparts, the instrumentation is implemented directly in the client so the only required step is to configure an `ObservationRegistry` on the client.
|
||||
|
||||
[[observability.http-client.resttemplate]]
|
||||
@@ -164,7 +168,7 @@ Unlike their server counterparts, the instrumentation is implemented directly in
|
||||
Applications must configure an `ObservationRegistry` on `RestTemplate` instances to enable the instrumentation; without that, observations are "no-ops".
|
||||
Spring Boot will auto-configure `RestTemplateBuilder` beans with the observation registry already set.
|
||||
|
||||
Instrumentation is using the `org.springframework.http.client.observation.ClientRequestObservationConvention` by default, backed by the `ClientRequestObservationContext`.
|
||||
Instrumentation uses the `org.springframework.http.client.observation.ClientRequestObservationConvention` by default, backed by the `ClientRequestObservationContext`.
|
||||
|
||||
.Low cardinality Keys
|
||||
[cols="a,a"]
|
||||
@@ -193,7 +197,7 @@ Instrumentation is using the `org.springframework.http.client.observation.Client
|
||||
Applications must configure an `ObservationRegistry` on the `WebClient` builder to enable the instrumentation; without that, observations are "no-ops".
|
||||
Spring Boot will auto-configure `WebClient.Builder` beans with the observation registry already set.
|
||||
|
||||
Instrumentation is using the `org.springframework.web.reactive.function.client.ClientRequestObservationConvention` by default, backed by the `ClientRequestObservationContext`.
|
||||
Instrumentation uses the `org.springframework.web.reactive.function.client.ClientRequestObservationConvention` by default, backed by the `ClientRequestObservationContext`.
|
||||
|
||||
.Low cardinality Keys
|
||||
[cols="a,a"]
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
The Spring Framework provides the following choices for making calls to REST endpoints:
|
||||
|
||||
* xref:integration/rest-clients.adoc#rest-webclient[`WebClient`] - non-blocking, reactive client w fluent API.
|
||||
* xref:integration/rest-clients.adoc#rest-webclient[`WebClient`] - non-blocking, reactive client with fluent API.
|
||||
* xref:integration/rest-clients.adoc#rest-resttemplate[`RestTemplate`] - synchronous client with template method API.
|
||||
* xref:integration/rest-clients.adoc#rest-http-interface[HTTP Interface] - annotated interface with generated, dynamic proxy implementation.
|
||||
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ Resource locations are typically XML configuration files or Groovy scripts locat
|
||||
classpath, while component classes are typically `@Configuration` classes. However,
|
||||
resource locations can also refer to files and scripts in the file system, and component
|
||||
classes can be `@Component` classes, `@Service` classes, and so on. See
|
||||
xref:testing/testcontext-framework/ctx-management/javaconfig.adoc#testcontext-ctx-management-javaconfig-component-classes[null] for further details.
|
||||
xref:testing/testcontext-framework/ctx-management/javaconfig.adoc#testcontext-ctx-management-javaconfig-component-classes[Component Classes] for further details.
|
||||
|
||||
The following example shows a `@ContextConfiguration` annotation that refers to an XML
|
||||
file:
|
||||
|
||||
+5
-1
@@ -12,7 +12,11 @@ metadata.
|
||||
You can use `@DirtiesContext` as both a class-level and a method-level annotation within
|
||||
the same class or class hierarchy. In such scenarios, the `ApplicationContext` is marked
|
||||
as dirty before or after any such annotated method as well as before or after the current
|
||||
test class, depending on the configured `methodMode` and `classMode`.
|
||||
test class, depending on the configured `methodMode` and `classMode`. When
|
||||
`@DirtiesContext` is declared at both the class level and the method level, the
|
||||
configured modes from both annotations will be honored. For example, if the class mode is
|
||||
set to `BEFORE_EACH_TEST_METHOD` and the method mode is set to `AFTER_METHOD`, the
|
||||
context will be marked as dirty both before and after the given test method.
|
||||
|
||||
The following examples explain when the context would be dirtied for various
|
||||
configuration scenarios:
|
||||
|
||||
@@ -12,6 +12,8 @@ by default, exactly in the following order:
|
||||
xref:testing/testcontext-framework/application-events.adoc[`ApplicationEvents`].
|
||||
* `DependencyInjectionTestExecutionListener`: Provides dependency injection for the test
|
||||
instance.
|
||||
* `MicrometerObservationRegistryTestExecutionListener`: Provides support for
|
||||
Micrometer's `ObservationRegistry`.
|
||||
* `DirtiesContextTestExecutionListener`: Handles the `@DirtiesContext` annotation for
|
||||
"`after`" modes.
|
||||
* `TransactionalTestExecutionListener`: Provides transactional test execution with
|
||||
|
||||
@@ -189,6 +189,9 @@ lets applications use the Servlet API directly if they need to. Spring WebFlux
|
||||
relies on Servlet non-blocking I/O and uses the Servlet API behind a low-level
|
||||
adapter. It is not exposed for direct use.
|
||||
|
||||
NOTE: It is strongly advised not to map Servlet filters or directly manipulate the Servlet API in the context of a WebFlux application.
|
||||
For the reasons listed above, mixing blocking I/O and non-blocking I/O in the same context will cause runtime issues.
|
||||
|
||||
For Undertow, Spring WebFlux uses Undertow APIs directly without the Servlet API.
|
||||
|
||||
|
||||
@@ -197,9 +200,9 @@ For Undertow, Spring WebFlux uses Undertow APIs directly without the Servlet API
|
||||
== Performance
|
||||
|
||||
Performance has many characteristics and meanings. Reactive and non-blocking generally
|
||||
do not make applications run faster. They can, in some cases, (for example, if using the
|
||||
`WebClient` to run remote calls in parallel). On the whole, it requires more work to do
|
||||
things the non-blocking way and that can slightly increase the required processing time.
|
||||
do not make applications run faster. They can in some cases – for example, if using the
|
||||
`WebClient` to run remote calls in parallel. However, it requires more work to do
|
||||
things the non-blocking way, and that can slightly increase the required processing time.
|
||||
|
||||
The key expected benefit of reactive and non-blocking is the ability to scale with a small,
|
||||
fixed number of threads and less memory. That makes applications more resilient under load,
|
||||
@@ -221,10 +224,10 @@ block the current thread, (for example, for remote calls). For this reason, serv
|
||||
use a large thread pool to absorb potential blocking during request handling.
|
||||
|
||||
In Spring WebFlux (and non-blocking servers in general), it is assumed that applications
|
||||
do not block. Therefore, non-blocking servers use a small, fixed-size thread pool
|
||||
do not block. Therefore, non-blocking servers use a small, fixed-size thread pool
|
||||
(event loop workers) to handle requests.
|
||||
|
||||
TIP: "`To scale`" and "`small number of threads`" may sound contradictory but to never block the
|
||||
TIP: "`To scale`" and "`small number of threads`" may sound contradictory, but to never block the
|
||||
current thread (and rely on callbacks instead) means that you do not need extra threads, as
|
||||
there are no blocking calls to absorb.
|
||||
|
||||
@@ -250,7 +253,7 @@ application code within that pipeline is never invoked concurrently.
|
||||
|
||||
What threads should you expect to see on a server running with Spring WebFlux?
|
||||
|
||||
* On a "`vanilla`" Spring WebFlux server (for example, no data access nor other optional
|
||||
* On a "`vanilla`" Spring WebFlux server (for example, no data access or other optional
|
||||
dependencies), you can expect one thread for the server and several others for request
|
||||
processing (typically as many as the number of CPU cores). Servlet containers, however,
|
||||
may start with more threads (for example, 10 on Tomcat), in support of both servlet (blocking) I/O
|
||||
|
||||
@@ -69,9 +69,10 @@ written to the response and computing an MD5 hash from it. The next time a clien
|
||||
it does the same, but it also compares the computed value against the `If-None-Match`
|
||||
request header and, if the two are equal, returns a 304 (NOT_MODIFIED).
|
||||
|
||||
This strategy saves network bandwidth but not CPU, as the full response must be computed
|
||||
for each request. Other strategies at the controller level, described earlier, can avoid
|
||||
the computation. See xref:web/webmvc/mvc-caching.adoc[HTTP Caching].
|
||||
This strategy saves network bandwidth but not CPU, as the full response must be computed for each request.
|
||||
State-changing HTTP methods and other HTTP conditional request headers such as `If-Match` and `If-Unmodified-Since` are outside the scope of this filter.
|
||||
Other strategies at the controller level can avoid the computation and have a broader support for HTTP conditional requests.
|
||||
See xref:web/webmvc/mvc-caching.adoc[HTTP Caching].
|
||||
|
||||
This filter has a `writeWeakETag` parameter that configures the filter to write weak ETags
|
||||
similar to the following: `W/"02a2d595e6ed9a0b24f027f2b63b134d6"` (as defined in
|
||||
|
||||
@@ -3,11 +3,18 @@
|
||||
|
||||
[.small]#xref:web/webflux/config.adoc#webflux-config-message-codecs[See equivalent in the Reactive stack]#
|
||||
|
||||
You can customize `HttpMessageConverter` in Java configuration by overriding
|
||||
{api-spring-framework}/web/servlet/config/annotation/WebMvcConfigurer.html#configureMessageConverters-java.util.List-[`configureMessageConverters()`]
|
||||
(to replace the default converters created by Spring MVC) or by overriding
|
||||
{api-spring-framework}/web/servlet/config/annotation/WebMvcConfigurer.html#extendMessageConverters-java.util.List-[`extendMessageConverters()`]
|
||||
(to customize the default converters or add additional converters to the default ones).
|
||||
You can set the `HttpMessageConverter` instances to use in Java configuration,
|
||||
replacing the ones used by default, by overriding
|
||||
{api-spring-framework}/web/servlet/config/annotation/WebMvcConfigurer.html#configureMessageConverters-java.util.List-[`configureMessageConverters()`].
|
||||
You can also customize the list of configured message converters at the end by overriding
|
||||
{api-spring-framework}/web/servlet/config/annotation/WebMvcConfigurer.html#extendMessageConverters-java.util.List-[`extendMessageConverters()`].
|
||||
|
||||
TIP: In a Spring Boot application, the `WebMvcAutoConfiguration` adds any
|
||||
`HttpMessageConverter` beans it detects, in addition to default converters. Hence, in a
|
||||
Boot application, prefer to use the
|
||||
https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-config/message-converters.html[HttpMessageConverters]
|
||||
mechanism. Or alternatively, use `extendMessageConverters` to modify message converters
|
||||
at the end.
|
||||
|
||||
The following example adds XML and Jackson JSON converters with a customized
|
||||
`ObjectMapper` instead of the default ones:
|
||||
|
||||
@@ -3,7 +3,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 xref:web/websocket/fallback.adoc[SockJS Fallback], as the following example shows:
|
||||
endpoint over WebSocket, as the following example shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@@ -16,7 +16,7 @@ endpoints, over WebSocket with xref:web/websocket/fallback.adoc[SockJS Fallback]
|
||||
|
||||
@Override
|
||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||
registry.addEndpoint("/portfolio").withSockJS(); // <1>
|
||||
registry.addEndpoint("/portfolio"); // <1>
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -32,7 +32,7 @@ client needs to connect for the WebSocket handshake.
|
||||
<2> STOMP messages whose destination header begins with `/app` are routed to
|
||||
`@MessageMapping` methods in `@Controller` classes.
|
||||
<3> Use the built-in message broker for subscriptions and broadcasting and
|
||||
route messages whose destination header begins with `/topic `or `/queue` to the broker.
|
||||
route messages whose destination header begins with `/topic` or `/queue` to the broker.
|
||||
|
||||
|
||||
The following example shows the XML configuration equivalent of the preceding example:
|
||||
@@ -49,9 +49,7 @@ The following example shows the XML configuration equivalent of the preceding ex
|
||||
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
|
||||
|
||||
<websocket:message-broker application-destination-prefix="/app">
|
||||
<websocket:stomp-endpoint path="/portfolio">
|
||||
<websocket:sockjs/>
|
||||
</websocket:stomp-endpoint>
|
||||
<websocket:stomp-endpoint path="/portfolio" />
|
||||
<websocket:simple-broker prefix="/topic, /queue"/>
|
||||
</websocket:message-broker>
|
||||
|
||||
@@ -64,34 +62,27 @@ messaging (that is, many subscribers versus one consumer). When you use an exter
|
||||
check the STOMP page of the broker to understand what kind of STOMP destinations and
|
||||
prefixes it supports.
|
||||
|
||||
To connect from a browser, for SockJS, you can use the
|
||||
https://github.com/sockjs/sockjs-client[`sockjs-client`]. For STOMP, many applications have
|
||||
used the https://github.com/jmesnil/stomp-websocket[jmesnil/stomp-websocket] library
|
||||
(also known as stomp.js), which is feature-complete and has been used in production for
|
||||
years but is no longer maintained. At present the
|
||||
https://github.com/JSteunou/webstomp-client[JSteunou/webstomp-client] is the most
|
||||
actively maintained and evolving successor of that library. The following example code
|
||||
is based on it:
|
||||
To connect from a browser, for STOMP, you can use
|
||||
https://github.com/stomp-js/stompjs[`stomp-js/stompjs`] which is the most
|
||||
actively maintained JavaScript library.
|
||||
|
||||
The following example code is based on it:
|
||||
|
||||
[source,javascript,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
var socket = new SockJS("/spring-websocket-portfolio/portfolio");
|
||||
var stompClient = webstomp.over(socket);
|
||||
|
||||
stompClient.connect({}, function(frame) {
|
||||
}
|
||||
const stompClient = new StompJs.Client({
|
||||
brokerURL: 'ws://domain.com/portfolio',
|
||||
onConnect: () => {
|
||||
// ...
|
||||
}
|
||||
});
|
||||
----
|
||||
|
||||
Alternatively, if you connect through WebSocket (without SockJS), you can use the following code:
|
||||
|
||||
[source,javascript,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
var socket = new WebSocket("/spring-websocket-portfolio/portfolio");
|
||||
var stompClient = Stomp.over(socket);
|
||||
|
||||
stompClient.connect({}, function(frame) {
|
||||
}
|
||||
----
|
||||
Alternatively, if you connect through SockJS, you can enable the
|
||||
xref:web/websocket/fallback.adoc[SockJS Fallback] on server-side with
|
||||
`registry.addEndpoint("/portfolio").withSockJS()` and on JavaScript side,
|
||||
by following
|
||||
https://stomp-js.github.io/guide/stompjs/rx-stomp/using-stomp-with-sockjs.html[those instructions].
|
||||
|
||||
Note that `stompClient` in the preceding example does not need to specify `login`
|
||||
and `passcode` headers. Even if it did, they would be ignored (or, rather,
|
||||
|
||||
@@ -8,18 +8,18 @@ javaPlatform {
|
||||
|
||||
dependencies {
|
||||
api(platform("com.fasterxml.jackson:jackson-bom:2.14.3"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.10.7"))
|
||||
api(platform("io.netty:netty-bom:4.1.92.Final"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.10.9"))
|
||||
api(platform("io.netty:netty-bom:4.1.94.Final"))
|
||||
api(platform("io.netty:netty5-bom:5.0.0.Alpha5"))
|
||||
api(platform("io.projectreactor:reactor-bom:2022.0.7"))
|
||||
api(platform("io.projectreactor:reactor-bom:2022.0.9"))
|
||||
api(platform("io.rsocket:rsocket-bom:1.1.3"))
|
||||
api(platform("org.apache.groovy:groovy-bom:4.0.11"))
|
||||
api(platform("org.apache.groovy:groovy-bom:4.0.13"))
|
||||
api(platform("org.apache.logging.log4j:log4j-bom:2.20.0"))
|
||||
api(platform("org.eclipse.jetty:jetty-bom:11.0.15"))
|
||||
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.3"))
|
||||
api(platform("org.mockito:mockito-bom:5.3.1"))
|
||||
api(platform("org.mockito:mockito-bom:5.4.0"))
|
||||
|
||||
constraints {
|
||||
api("com.fasterxml:aalto-xml:1.3.2")
|
||||
@@ -29,7 +29,7 @@ dependencies {
|
||||
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.10.1")
|
||||
api("com.google.protobuf:protobuf-java-util:3.23.0")
|
||||
api("com.google.protobuf:protobuf-java-util:3.23.2")
|
||||
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.8.0")
|
||||
@@ -45,8 +45,8 @@ dependencies {
|
||||
api("com.thoughtworks.xstream:xstream:1.4.20")
|
||||
api("commons-io:commons-io:2.11.0")
|
||||
api("de.bechte.junit:junit-hierarchicalcontextrunner:4.12.2")
|
||||
api("info.picocli:picocli:4.7.3")
|
||||
api("io.micrometer:context-propagation:1.0.0")
|
||||
api("info.picocli:picocli:4.7.4")
|
||||
api("io.micrometer:context-propagation:1.0.4")
|
||||
api("io.mockk:mockk:1.13.4")
|
||||
api("io.projectreactor.netty:reactor-netty5-http:2.0.0-M3")
|
||||
api("io.projectreactor.tools:blockhound:1.0.8.RELEASE")
|
||||
@@ -55,9 +55,9 @@ dependencies {
|
||||
api("io.r2dbc:r2dbc-spi:1.0.0.RELEASE")
|
||||
api("io.reactivex.rxjava3:rxjava:3.1.6")
|
||||
api("io.smallrye.reactive:mutiny:1.9.0")
|
||||
api("io.undertow:undertow-core:2.3.5.Final")
|
||||
api("io.undertow:undertow-servlet:2.3.5.Final")
|
||||
api("io.undertow:undertow-websockets-jsr:2.3.5.Final")
|
||||
api("io.undertow:undertow-core:2.3.7.Final")
|
||||
api("io.undertow:undertow-servlet:2.3.7.Final")
|
||||
api("io.undertow:undertow-websockets-jsr:2.3.7.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")
|
||||
@@ -82,6 +82,7 @@ dependencies {
|
||||
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.annotation:javax.annotation-api:1.3.2")
|
||||
api("javax.cache:cache-api:1.1.1")
|
||||
api("javax.money:money-api:1.1")
|
||||
api("jaxen:jaxen:1.2.0")
|
||||
@@ -99,10 +100,10 @@ dependencies {
|
||||
api("org.apache.httpcomponents.client5:httpclient5:5.2.1")
|
||||
api("org.apache.httpcomponents.core5:httpcore5-reactive:5.2.1")
|
||||
api("org.apache.poi:poi-ooxml:5.2.3")
|
||||
api("org.apache.tomcat.embed:tomcat-embed-core:10.1.8")
|
||||
api("org.apache.tomcat.embed:tomcat-embed-websocket:10.1.8")
|
||||
api("org.apache.tomcat:tomcat-util:10.1.8")
|
||||
api("org.apache.tomcat:tomcat-websocket:10.1.8")
|
||||
api("org.apache.tomcat.embed:tomcat-embed-core:10.1.10")
|
||||
api("org.apache.tomcat.embed:tomcat-embed-websocket:10.1.10")
|
||||
api("org.apache.tomcat:tomcat-util:10.1.10")
|
||||
api("org.apache.tomcat:tomcat-websocket:10.1.10")
|
||||
api("org.aspectj:aspectjrt:1.9.19")
|
||||
api("org.aspectj:aspectjtools:1.9.19")
|
||||
api("org.aspectj:aspectjweaver:1.9.19")
|
||||
@@ -126,9 +127,9 @@ dependencies {
|
||||
api("org.hamcrest:hamcrest:2.2")
|
||||
api("org.hibernate:hibernate-core-jakarta:5.6.15.Final")
|
||||
api("org.hibernate:hibernate-validator:7.0.5.Final")
|
||||
api("org.hsqldb:hsqldb:2.7.1")
|
||||
api("org.hsqldb:hsqldb:2.7.2")
|
||||
api("org.javamoney:moneta:1.4.2")
|
||||
api("org.jruby:jruby:9.4.2.0")
|
||||
api("org.jruby:jruby:9.4.3.0")
|
||||
api("org.junit.support:testng-engine:1.0.4")
|
||||
api("org.mozilla:rhino:1.7.14")
|
||||
api("org.ogce:xpp3:1.1.6")
|
||||
@@ -138,9 +139,9 @@ dependencies {
|
||||
api("org.seleniumhq.selenium:selenium-java:3.141.59")
|
||||
api("org.skyscreamer:jsonassert:1.5.1")
|
||||
api("org.slf4j:slf4j-api:2.0.7")
|
||||
api("org.testng:testng:7.7.1")
|
||||
api("org.testng:testng:7.8.0")
|
||||
api("org.webjars:underscorejs:1.8.3")
|
||||
api("org.webjars:webjars-locator-core:0.52")
|
||||
api("org.webjars:webjars-locator-core:0.53")
|
||||
api("org.xmlunit:xmlunit-assertj:2.9.1")
|
||||
api("org.xmlunit:xmlunit-matchers:2.9.1")
|
||||
api("org.yaml:snakeyaml:1.33")
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
version=6.0.9-SNAPSHOT
|
||||
version=6.0.11
|
||||
|
||||
org.gradle.caching=true
|
||||
org.gradle.jvmargs=-Xmx2048m
|
||||
|
||||
@@ -72,10 +72,13 @@ javadoc {
|
||||
options.header = project.name
|
||||
options.use = true
|
||||
options.links(project.ext.javadocLinks)
|
||||
options.addStringOption("Xdoclint:none", "-quiet")
|
||||
// Check for syntax during linting. 'none' doesn't seem to work in suppressing
|
||||
// all linting warnings all the time (see/link references most notably).
|
||||
options.addStringOption("Xdoclint:syntax", "-quiet")
|
||||
|
||||
// Suppress warnings due to cross-module @see and @link references.
|
||||
// Note that global 'api' task does display all warnings.
|
||||
// Note that global 'api' task does display all warnings, and
|
||||
// checks for 'reference' on top of 'syntax'.
|
||||
logging.captureStandardError LogLevel.INFO
|
||||
logging.captureStandardOutput LogLevel.INFO // suppress "## warnings" message
|
||||
}
|
||||
|
||||
Vendored
BIN
Binary file not shown.
+2
-1
@@ -1,6 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -130,10 +130,13 @@ location of your Java installation."
|
||||
fi
|
||||
else
|
||||
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.
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
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
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
|
||||
+8
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -21,11 +21,15 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.expression.MethodExecutor;
|
||||
import org.springframework.expression.TypedValue;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class Spr7538Tests {
|
||||
|
||||
@@ -49,8 +53,9 @@ class Spr7538Tests {
|
||||
ReflectiveMethodResolver resolver = new ReflectiveMethodResolver();
|
||||
MethodExecutor executor = resolver.resolve(context, target, "checkCompleteness", argumentTypes);
|
||||
|
||||
Object result = executor.execute(context, target, arguments);
|
||||
System.out.println("Result: " + result);
|
||||
TypedValue typedValue = executor.execute(context, target, arguments);
|
||||
assertThat(typedValue.getValue()).asInstanceOf(InstanceOfAssertFactories.BOOLEAN).isTrue();
|
||||
assertThat(typedValue.getTypeDescriptor().getType()).isEqualTo(Boolean.class);
|
||||
}
|
||||
|
||||
static class AlwaysTrueReleaseStrategy {
|
||||
|
||||
@@ -10,4 +10,5 @@ dependencies {
|
||||
testImplementation(testFixtures(project(":spring-core")))
|
||||
testFixturesImplementation(testFixtures(project(":spring-beans")))
|
||||
testFixturesImplementation(testFixtures(project(":spring-core")))
|
||||
testFixturesImplementation("com.google.code.findbugs:jsr305")
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -62,8 +62,11 @@ public interface Advisor {
|
||||
* Typical Advisor implementations always return {@code true}.
|
||||
* Use singleton/prototype bean definitions or appropriate programmatic
|
||||
* proxy creation to ensure that Advisors have the correct lifecycle model.
|
||||
* <p>As of 6.0.10, the default implementation returns {@code true}.
|
||||
* @return whether this advice is associated with a particular target instance
|
||||
*/
|
||||
boolean isPerInstance();
|
||||
default boolean isPerInstance() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -302,7 +302,7 @@ public class AspectJAdviceParameterNameDiscoverer implements ParameterNameDiscov
|
||||
}
|
||||
|
||||
|
||||
private void bindParameterName(int index, String name) {
|
||||
private void bindParameterName(int index, @Nullable String name) {
|
||||
this.parameterNameBindings[index] = name;
|
||||
this.numberOfRemainingUnboundArguments--;
|
||||
}
|
||||
|
||||
+1
-6
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -67,11 +67,6 @@ public class AspectJPointcutAdvisor implements PointcutAdvisor, Ordered {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPerInstance() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Advice getAdvice() {
|
||||
return this.advice;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -91,11 +91,6 @@ public class DeclareParentsAdvisor implements IntroductionAdvisor {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPerInstance() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Advice getAdvice() {
|
||||
return this.advice;
|
||||
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.aop.aspectj.annotation;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.aop.Advisor;
|
||||
import org.springframework.aop.aspectj.AbstractAspectJAdvice;
|
||||
import org.springframework.aot.generate.GenerationContext;
|
||||
import org.springframework.aot.hint.ExecutableMode;
|
||||
import org.springframework.aot.hint.ReflectionHints;
|
||||
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution;
|
||||
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor;
|
||||
import org.springframework.beans.factory.aot.BeanFactoryInitializationCode;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* {@link BeanFactoryInitializationAotProcessor} implementation responsible for registering
|
||||
* hints for AOP advices.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @author Stephane Nicoll
|
||||
* @since 6.0.11
|
||||
*/
|
||||
class AspectJBeanFactoryInitializationAotProcessor implements BeanFactoryInitializationAotProcessor {
|
||||
|
||||
private static final boolean aspectJPresent = ClassUtils.isPresent(
|
||||
"org.aspectj.lang.annotation.Pointcut", AspectJBeanFactoryInitializationAotProcessor.class.getClassLoader());
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) {
|
||||
if (aspectJPresent) {
|
||||
return AspectDelegate.processAheadOfTime(beanFactory);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inner class to avoid a hard dependency on AspectJ at runtime.
|
||||
*/
|
||||
private static class AspectDelegate {
|
||||
|
||||
@Nullable
|
||||
private static AspectContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) {
|
||||
BeanFactoryAspectJAdvisorsBuilder builder = new BeanFactoryAspectJAdvisorsBuilder(beanFactory);
|
||||
List<Advisor> advisors = builder.buildAspectJAdvisors();
|
||||
return advisors.isEmpty() ? null : new AspectContribution(advisors);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private static class AspectContribution implements BeanFactoryInitializationAotContribution {
|
||||
|
||||
private final List<Advisor> advisors;
|
||||
|
||||
public AspectContribution(List<Advisor> advisors) {
|
||||
this.advisors = advisors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyTo(GenerationContext generationContext, BeanFactoryInitializationCode beanFactoryInitializationCode) {
|
||||
ReflectionHints reflectionHints = generationContext.getRuntimeHints().reflection();
|
||||
for (Advisor advisor : this.advisors) {
|
||||
if (advisor.getAdvice() instanceof AbstractAspectJAdvice aspectJAdvice) {
|
||||
reflectionHints.registerMethod(aspectJAdvice.getAspectJAdviceMethod(), ExecutableMode.INVOKE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -22,6 +22,7 @@ import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -32,6 +33,8 @@ import org.springframework.aop.Advisor;
|
||||
import org.springframework.aop.DynamicIntroductionAdvice;
|
||||
import org.springframework.aop.IntroductionAdvisor;
|
||||
import org.springframework.aop.IntroductionInfo;
|
||||
import org.springframework.aop.Pointcut;
|
||||
import org.springframework.aop.PointcutAdvisor;
|
||||
import org.springframework.aop.TargetSource;
|
||||
import org.springframework.aop.support.DefaultIntroductionAdvisor;
|
||||
import org.springframework.aop.support.DefaultPointcutAdvisor;
|
||||
@@ -41,6 +44,7 @@ import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* Base class for AOP proxy configuration managers.
|
||||
@@ -72,15 +76,13 @@ 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();
|
||||
private AdvisorChainFactory advisorChainFactory;
|
||||
|
||||
/** Cache with Method as key and advisor chain List as value. */
|
||||
private transient Map<MethodCacheKey, List<Object>> methodCache;
|
||||
@@ -89,21 +91,22 @@ 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<Class<?>> 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<Advisor> advisors = new ArrayList<>();
|
||||
|
||||
private List<Advisor> advisorKey = this.advisors;
|
||||
|
||||
|
||||
/**
|
||||
* No-arg constructor for use as a JavaBean.
|
||||
*/
|
||||
public AdvisedSupport() {
|
||||
this.advisorChainFactory = DefaultAdvisorChainFactory.INSTANCE;
|
||||
this.methodCache = new ConcurrentHashMap<>(32);
|
||||
}
|
||||
|
||||
@@ -116,6 +119,15 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
|
||||
setInterfaces(interfaces);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal constructor for {@link #getConfigurationOnlyCopy()}.
|
||||
* @since 6.0.10
|
||||
*/
|
||||
private AdvisedSupport(AdvisorChainFactory advisorChainFactory, Map<MethodCacheKey, List<Object>> methodCache) {
|
||||
this.advisorChainFactory = advisorChainFactory;
|
||||
this.methodCache = methodCache;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the given object as target.
|
||||
@@ -520,15 +532,27 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
|
||||
* replacing the TargetSource.
|
||||
*/
|
||||
AdvisedSupport getConfigurationOnlyCopy() {
|
||||
AdvisedSupport copy = new AdvisedSupport();
|
||||
AdvisedSupport copy = new AdvisedSupport(this.advisorChainFactory, this.methodCache);
|
||||
copy.copyFrom(this);
|
||||
copy.targetSource = EmptyTargetSource.forClass(getTargetClass(), getTargetSource().isStatic());
|
||||
copy.advisorChainFactory = this.advisorChainFactory;
|
||||
copy.interfaces = new ArrayList<>(this.interfaces);
|
||||
copy.advisors = new ArrayList<>(this.advisors);
|
||||
copy.advisorKey = new ArrayList<>(this.advisors.size());
|
||||
for (Advisor advisor : this.advisors) {
|
||||
copy.advisorKey.add(new AdvisorKeyEntry(advisor));
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
void reduceToAdvisorKey() {
|
||||
this.advisors = this.advisorKey;
|
||||
this.methodCache = Collections.emptyMap();
|
||||
}
|
||||
|
||||
Object getAdvisorKey() {
|
||||
return this.advisorKey;
|
||||
}
|
||||
|
||||
|
||||
//---------------------------------------------------------------------
|
||||
// Serialization support
|
||||
@@ -604,4 +628,51 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Stub for an Advisor instance that is just needed for key purposes,
|
||||
* allowing for efficient equals and hashCode comparisons against the
|
||||
* advice class and the pointcut.
|
||||
* @since 6.0.10
|
||||
* @see #getConfigurationOnlyCopy()
|
||||
* @see #getAdvisorKey()
|
||||
*/
|
||||
private static class AdvisorKeyEntry implements Advisor {
|
||||
|
||||
private final Class<?> adviceType;
|
||||
|
||||
@Nullable
|
||||
private String classFilterKey;
|
||||
|
||||
@Nullable
|
||||
private String methodMatcherKey;
|
||||
|
||||
public AdvisorKeyEntry(Advisor advisor) {
|
||||
this.adviceType = advisor.getAdvice().getClass();
|
||||
if (advisor instanceof PointcutAdvisor pointcutAdvisor) {
|
||||
Pointcut pointcut = pointcutAdvisor.getPointcut();
|
||||
this.classFilterKey = ObjectUtils.identityToString(pointcut.getClassFilter());
|
||||
this.methodMatcherKey = ObjectUtils.identityToString(pointcut.getMethodMatcher());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Advice getAdvice() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return (this == other || (other instanceof AdvisorKeyEntry otherEntry &&
|
||||
this.adviceType == otherEntry.adviceType &&
|
||||
ObjectUtils.nullSafeEquals(this.classFilterKey, otherEntry.classFilterKey) &&
|
||||
ObjectUtils.nullSafeEquals(this.methodMatcherKey, otherEntry.methodMatcherKey)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return this.adviceType.hashCode();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -26,14 +26,11 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
import org.aopalliance.aop.Advice;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.aop.Advisor;
|
||||
import org.springframework.aop.AopInvocationException;
|
||||
import org.springframework.aop.PointcutAdvisor;
|
||||
import org.springframework.aop.RawTargetAccess;
|
||||
import org.springframework.aop.TargetSource;
|
||||
import org.springframework.aop.support.AopUtils;
|
||||
@@ -205,12 +202,21 @@ class CglibAopProxy implements AopProxy, Serializable {
|
||||
types[x] = callbacks[x].getClass();
|
||||
}
|
||||
// fixedInterceptorMap only populated at this point, after getCallbacks call above
|
||||
enhancer.setCallbackFilter(new ProxyCallbackFilter(
|
||||
this.advised.getConfigurationOnlyCopy(), this.fixedInterceptorMap, this.fixedInterceptorOffset));
|
||||
ProxyCallbackFilter filter = new ProxyCallbackFilter(
|
||||
this.advised.getConfigurationOnlyCopy(), this.fixedInterceptorMap, this.fixedInterceptorOffset);
|
||||
enhancer.setCallbackFilter(filter);
|
||||
enhancer.setCallbackTypes(types);
|
||||
|
||||
// Generate the proxy class and create a proxy instance.
|
||||
return (classOnly ? createProxyClass(enhancer) : createProxyClassAndInstance(enhancer, callbacks));
|
||||
// ProxyCallbackFilter has method introspection capability with Advisor access.
|
||||
try {
|
||||
return (classOnly ? createProxyClass(enhancer) : createProxyClassAndInstance(enhancer, callbacks));
|
||||
}
|
||||
finally {
|
||||
// Reduce ProxyCallbackFilter to key-only state for its class cache role
|
||||
// in the CGLIB$CALLBACK_FILTER field, not leaking any Advisor state...
|
||||
filter.advised.reduceToAdvisorKey();
|
||||
}
|
||||
}
|
||||
catch (CodeGenerationException | IllegalArgumentException ex) {
|
||||
throw new AopConfigException("Could not generate CGLIB subclass of " + this.advised.getTargetClass() +
|
||||
@@ -294,9 +300,9 @@ class CglibAopProxy implements AopProxy, Serializable {
|
||||
|
||||
private Callback[] getCallbacks(Class<?> rootClass) throws Exception {
|
||||
// Parameters used for optimization choices...
|
||||
boolean exposeProxy = this.advised.isExposeProxy();
|
||||
boolean isFrozen = this.advised.isFrozen();
|
||||
boolean isStatic = this.advised.getTargetSource().isStatic();
|
||||
boolean isFrozen = this.advised.isFrozen();
|
||||
boolean exposeProxy = this.advised.isExposeProxy();
|
||||
|
||||
// Choose an "aop" interceptor (used for AOP calls).
|
||||
Callback aopInterceptor = new DynamicAdvisedInterceptor(this.advised);
|
||||
@@ -776,7 +782,7 @@ class CglibAopProxy implements AopProxy, Serializable {
|
||||
*/
|
||||
private static class ProxyCallbackFilter implements CallbackFilter {
|
||||
|
||||
private final AdvisedSupport advised;
|
||||
final AdvisedSupport advised;
|
||||
|
||||
private final Map<Method, Integer> fixedInterceptorMap;
|
||||
|
||||
@@ -857,9 +863,9 @@ class CglibAopProxy implements AopProxy, Serializable {
|
||||
// Proxy is not yet available, but that shouldn't matter.
|
||||
List<?> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
|
||||
boolean haveAdvice = !chain.isEmpty();
|
||||
boolean exposeProxy = this.advised.isExposeProxy();
|
||||
boolean isStatic = this.advised.getTargetSource().isStatic();
|
||||
boolean isFrozen = this.advised.isFrozen();
|
||||
boolean exposeProxy = this.advised.isExposeProxy();
|
||||
if (haveAdvice || !isFrozen) {
|
||||
// If exposing the proxy, then AOP_PROXY must be used.
|
||||
if (exposeProxy) {
|
||||
@@ -921,63 +927,18 @@ class CglibAopProxy implements AopProxy, Serializable {
|
||||
return false;
|
||||
}
|
||||
AdvisedSupport otherAdvised = otherCallbackFilter.advised;
|
||||
if (this.advised.isFrozen() != otherAdvised.isFrozen()) {
|
||||
return false;
|
||||
}
|
||||
if (this.advised.isExposeProxy() != otherAdvised.isExposeProxy()) {
|
||||
return false;
|
||||
}
|
||||
if (this.advised.getTargetSource().isStatic() != otherAdvised.getTargetSource().isStatic()) {
|
||||
return false;
|
||||
}
|
||||
if (!AopProxyUtils.equalsProxiedInterfaces(this.advised, otherAdvised)) {
|
||||
return false;
|
||||
}
|
||||
// Advice instance identity is unimportant to the proxy class:
|
||||
// All that matters is type and ordering.
|
||||
if (this.advised.getAdvisorCount() != otherAdvised.getAdvisorCount()) {
|
||||
return false;
|
||||
}
|
||||
Advisor[] thisAdvisors = this.advised.getAdvisors();
|
||||
Advisor[] thatAdvisors = otherAdvised.getAdvisors();
|
||||
for (int i = 0; i < thisAdvisors.length; i++) {
|
||||
Advisor thisAdvisor = thisAdvisors[i];
|
||||
Advisor thatAdvisor = thatAdvisors[i];
|
||||
if (!equalsAdviceClasses(thisAdvisor, thatAdvisor)) {
|
||||
return false;
|
||||
}
|
||||
if (!equalsPointcuts(thisAdvisor, thatAdvisor)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean equalsAdviceClasses(Advisor a, Advisor b) {
|
||||
return (a.getAdvice().getClass() == b.getAdvice().getClass());
|
||||
}
|
||||
|
||||
private static boolean equalsPointcuts(Advisor a, Advisor b) {
|
||||
// If only one of the advisor (but not both) is PointcutAdvisor, then it is a mismatch.
|
||||
// Takes care of the situations where an IntroductionAdvisor is used (see SPR-3959).
|
||||
return (!(a instanceof PointcutAdvisor pointcutAdvisor1) ||
|
||||
(b instanceof PointcutAdvisor pointcutAdvisor2 &&
|
||||
ObjectUtils.nullSafeEquals(pointcutAdvisor1.getPointcut(), pointcutAdvisor2.getPointcut())));
|
||||
return (this.advised.getAdvisorKey().equals(otherAdvised.getAdvisorKey()) &&
|
||||
AopProxyUtils.equalsProxiedInterfaces(this.advised, otherAdvised) &&
|
||||
ObjectUtils.nullSafeEquals(this.advised.getTargetClass(), otherAdvised.getTargetClass()) &&
|
||||
this.advised.getTargetSource().isStatic() == otherAdvised.getTargetSource().isStatic() &&
|
||||
this.advised.isFrozen() == otherAdvised.isFrozen() &&
|
||||
this.advised.isExposeProxy() == otherAdvised.isExposeProxy() &&
|
||||
this.advised.isOpaque() == otherAdvised.isOpaque());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int hashCode = 0;
|
||||
Advisor[] advisors = this.advised.getAdvisors();
|
||||
for (Advisor advisor : advisors) {
|
||||
Advice advice = advisor.getAdvice();
|
||||
hashCode = 13 * hashCode + advice.getClass().hashCode();
|
||||
}
|
||||
hashCode = 13 * hashCode + (this.advised.isFrozen() ? 1 : 0);
|
||||
hashCode = 13 * hashCode + (this.advised.isExposeProxy() ? 1 : 0);
|
||||
hashCode = 13 * hashCode + (this.advised.isOptimize() ? 1 : 0);
|
||||
hashCode = 13 * hashCode + (this.advised.isOpaque() ? 1 : 0);
|
||||
return hashCode;
|
||||
return this.advised.getAdvisorKey().hashCode();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+7
@@ -47,6 +47,13 @@ import org.springframework.lang.Nullable;
|
||||
@SuppressWarnings("serial")
|
||||
public class DefaultAdvisorChainFactory implements AdvisorChainFactory, Serializable {
|
||||
|
||||
/**
|
||||
* Singleton instance of this class.
|
||||
* @since 6.0.10
|
||||
*/
|
||||
public static final DefaultAdvisorChainFactory INSTANCE = new DefaultAdvisorChainFactory();
|
||||
|
||||
|
||||
@Override
|
||||
public List<Object> getInterceptorsAndDynamicInterceptionAdvice(
|
||||
Advised config, Method method, @Nullable Class<?> targetClass) {
|
||||
|
||||
+7
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -48,6 +48,12 @@ import org.springframework.util.ClassUtils;
|
||||
*/
|
||||
public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {
|
||||
|
||||
/**
|
||||
* Singleton instance of this class.
|
||||
* @since 6.0.10
|
||||
*/
|
||||
public static final DefaultAopProxyFactory INSTANCE = new DefaultAopProxyFactory();
|
||||
|
||||
private static final long serialVersionUID = 7930414337282325166L;
|
||||
|
||||
|
||||
|
||||
@@ -120,18 +120,39 @@ final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializa
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Creating JDK dynamic proxy: " + this.advised.getTargetSource());
|
||||
}
|
||||
if (classLoader == null || classLoader.getParent() == null) {
|
||||
// JDK bootstrap loader or platform loader suggested ->
|
||||
// use higher-level loader which can see Spring infrastructure classes
|
||||
classLoader = getClass().getClassLoader();
|
||||
}
|
||||
return Proxy.newProxyInstance(classLoader, this.proxiedInterfaces, this);
|
||||
return Proxy.newProxyInstance(determineClassLoader(classLoader), this.proxiedInterfaces, this);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public Class<?> getProxyClass(@Nullable ClassLoader classLoader) {
|
||||
return Proxy.getProxyClass(classLoader, this.proxiedInterfaces);
|
||||
return Proxy.getProxyClass(determineClassLoader(classLoader), this.proxiedInterfaces);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the JDK bootstrap or platform loader has been suggested ->
|
||||
* use higher-level loader which can see Spring infrastructure classes instead.
|
||||
*/
|
||||
private ClassLoader determineClassLoader(@Nullable ClassLoader classLoader) {
|
||||
if (classLoader == null) {
|
||||
// JDK bootstrap loader -> use spring-aop ClassLoader instead.
|
||||
return getClass().getClassLoader();
|
||||
}
|
||||
if (classLoader.getParent() == null) {
|
||||
// Potentially the JDK platform loader on JDK 9+
|
||||
ClassLoader aopClassLoader = getClass().getClassLoader();
|
||||
ClassLoader aopParent = aopClassLoader.getParent();
|
||||
while (aopParent != null) {
|
||||
if (classLoader == aopParent) {
|
||||
// Suggested ClassLoader is ancestor of spring-aop ClassLoader
|
||||
// -> use spring-aop ClassLoader itself instead.
|
||||
return aopClassLoader;
|
||||
}
|
||||
aopParent = aopParent.getParent();
|
||||
}
|
||||
}
|
||||
// Regular case: use suggested ClassLoader as-is.
|
||||
return classLoader;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -44,7 +44,7 @@ public class ProxyCreatorSupport extends AdvisedSupport {
|
||||
* Create a new ProxyCreatorSupport instance.
|
||||
*/
|
||||
public ProxyCreatorSupport() {
|
||||
this.aopProxyFactory = new DefaultAopProxyFactory();
|
||||
this.aopProxyFactory = DefaultAopProxyFactory.INSTANCE;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -606,11 +606,6 @@ public class ProxyFactoryBean extends ProxyCreatorSupport
|
||||
throw new UnsupportedOperationException("Cannot invoke methods: " + this.message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPerInstance() {
|
||||
throw new UnsupportedOperationException("Cannot invoke methods: " + this.message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.message;
|
||||
|
||||
+8
-2
@@ -34,6 +34,7 @@ import org.springframework.beans.factory.config.ConfigurableBeanFactory;
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.beans.factory.support.GenericBeanDefinition;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Convenient superclass for
|
||||
@@ -82,6 +83,11 @@ public abstract class AbstractBeanFactoryBasedTargetSourceCreator
|
||||
return this.beanFactory;
|
||||
}
|
||||
|
||||
private ConfigurableBeanFactory getConfigurableBeanFactory() {
|
||||
Assert.state(this.beanFactory != null, "BeanFactory not set");
|
||||
return this.beanFactory;
|
||||
}
|
||||
|
||||
|
||||
//---------------------------------------------------------------------
|
||||
// Implementation of the TargetSourceCreator interface
|
||||
@@ -105,7 +111,7 @@ public abstract class AbstractBeanFactoryBasedTargetSourceCreator
|
||||
// We need to override just this bean definition, as it may reference other beans
|
||||
// and we're happy to take the parent's definition for those.
|
||||
// Always use prototype scope if demanded.
|
||||
BeanDefinition bd = this.beanFactory.getMergedBeanDefinition(beanName);
|
||||
BeanDefinition bd = getConfigurableBeanFactory().getMergedBeanDefinition(beanName);
|
||||
GenericBeanDefinition bdCopy = new GenericBeanDefinition(bd);
|
||||
if (isPrototypeBased()) {
|
||||
bdCopy.setScope(BeanDefinition.SCOPE_PROTOTYPE);
|
||||
@@ -127,7 +133,7 @@ public abstract class AbstractBeanFactoryBasedTargetSourceCreator
|
||||
protected DefaultListableBeanFactory getInternalBeanFactoryForBean(String beanName) {
|
||||
synchronized (this.internalBeanFactories) {
|
||||
return this.internalBeanFactories.computeIfAbsent(beanName,
|
||||
name -> buildInternalBeanFactory(this.beanFactory));
|
||||
name -> buildInternalBeanFactory(getConfigurableBeanFactory()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-14
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -58,22 +58,12 @@ public abstract class AbstractPointcutAdvisor implements PointcutAdvisor, Ordere
|
||||
return Ordered.LOWEST_PRECEDENCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPerInstance() {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (!(other instanceof PointcutAdvisor otherAdvisor)) {
|
||||
return false;
|
||||
}
|
||||
return (ObjectUtils.nullSafeEquals(getAdvice(), otherAdvisor.getAdvice()) &&
|
||||
ObjectUtils.nullSafeEquals(getPointcut(), otherAdvisor.getPointcut()));
|
||||
return (this == other || (other instanceof PointcutAdvisor otherAdvisor &&
|
||||
ObjectUtils.nullSafeEquals(getAdvice(), otherAdvisor.getAdvice()) &&
|
||||
ObjectUtils.nullSafeEquals(getPointcut(), otherAdvisor.getPointcut())));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+4
-9
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -196,14 +196,9 @@ public abstract class AbstractRegexpMethodPointcut extends StaticMethodMatcherPo
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (!(other instanceof AbstractRegexpMethodPointcut otherPointcut)) {
|
||||
return false;
|
||||
}
|
||||
return (Arrays.equals(this.patterns, otherPointcut.patterns) &&
|
||||
Arrays.equals(this.excludedPatterns, otherPointcut.excludedPatterns));
|
||||
return (this == other || (other instanceof AbstractRegexpMethodPointcut otherPointcut &&
|
||||
Arrays.equals(this.patterns, otherPointcut.patterns) &&
|
||||
Arrays.equals(this.excludedPatterns, otherPointcut.excludedPatterns)));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -187,14 +187,9 @@ public class ComposablePointcut implements Pointcut, Serializable {
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (!(other instanceof ComposablePointcut otherPointcut)) {
|
||||
return false;
|
||||
}
|
||||
return (this.classFilter.equals(otherPointcut.classFilter) &&
|
||||
this.methodMatcher.equals(otherPointcut.methodMatcher));
|
||||
return (this == other || (other instanceof ComposablePointcut otherPointcut &&
|
||||
this.classFilter.equals(otherPointcut.classFilter) &&
|
||||
this.methodMatcher.equals(otherPointcut.methodMatcher)));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -125,13 +125,9 @@ public class ControlFlowPointcut implements Pointcut, ClassFilter, MethodMatcher
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (!(other instanceof ControlFlowPointcut that)) {
|
||||
return false;
|
||||
}
|
||||
return (this.clazz.equals(that.clazz)) && ObjectUtils.nullSafeEquals(this.methodName, that.methodName);
|
||||
return (this == other || (other instanceof ControlFlowPointcut that &&
|
||||
this.clazz.equals(that.clazz)) &&
|
||||
ObjectUtils.nullSafeEquals(this.methodName, that.methodName));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+4
-13
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -134,11 +134,6 @@ public class DefaultIntroductionAdvisor implements IntroductionAdvisor, ClassFil
|
||||
return this.advice;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPerInstance() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClassFilter getClassFilter() {
|
||||
return this;
|
||||
@@ -152,13 +147,9 @@ public class DefaultIntroductionAdvisor implements IntroductionAdvisor, ClassFil
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (!(other instanceof DefaultIntroductionAdvisor otherAdvisor)) {
|
||||
return false;
|
||||
}
|
||||
return (this.advice.equals(otherAdvisor.advice) && this.interfaces.equals(otherAdvisor.interfaces));
|
||||
return (this == other || (other instanceof DefaultIntroductionAdvisor otherAdvisor &&
|
||||
this.advice.equals(otherAdvisor.advice) &&
|
||||
this.interfaces.equals(otherAdvisor.interfaces)));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -143,13 +143,8 @@ public abstract class MethodMatchers {
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (!(other instanceof UnionMethodMatcher that)) {
|
||||
return false;
|
||||
}
|
||||
return (this.mm1.equals(that.mm1) && this.mm2.equals(that.mm2));
|
||||
return (this == other || (other instanceof UnionMethodMatcher that &&
|
||||
this.mm1.equals(that.mm1) && this.mm2.equals(that.mm2)));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -307,13 +302,8 @@ public abstract class MethodMatchers {
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (!(other instanceof IntersectionMethodMatcher that)) {
|
||||
return false;
|
||||
}
|
||||
return (this.mm1.equals(that.mm1) && this.mm2.equals(that.mm2));
|
||||
return (this == other || (other instanceof IntersectionMethodMatcher that &&
|
||||
this.mm1.equals(that.mm1) && this.mm2.equals(that.mm2)));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+1
-6
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -77,11 +77,6 @@ public abstract class StaticMethodMatcherPointcutAdvisor extends StaticMethodMat
|
||||
return this.advice;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPerInstance() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pointcut getPointcut() {
|
||||
return this;
|
||||
|
||||
+4
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -68,13 +68,9 @@ public class AnnotationClassFilter implements ClassFilter {
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (!(other instanceof AnnotationClassFilter otherCf)) {
|
||||
return false;
|
||||
}
|
||||
return (this.annotationType.equals(otherCf.annotationType) && this.checkInherited == otherCf.checkInherited);
|
||||
return (this == other || (other instanceof AnnotationClassFilter otherCf &&
|
||||
this.annotationType.equals(otherCf.annotationType) &&
|
||||
this.checkInherited == otherCf.checkInherited));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+6
-16
@@ -121,14 +121,9 @@ public class AnnotationMatchingPointcut implements Pointcut {
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (!(other instanceof AnnotationMatchingPointcut otherPointcut)) {
|
||||
return false;
|
||||
}
|
||||
return (this.classFilter.equals(otherPointcut.classFilter) &&
|
||||
this.methodMatcher.equals(otherPointcut.methodMatcher));
|
||||
return (this == other || (other instanceof AnnotationMatchingPointcut otherPointcut &&
|
||||
this.classFilter.equals(otherPointcut.classFilter) &&
|
||||
this.methodMatcher.equals(otherPointcut.methodMatcher)));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -183,14 +178,9 @@ public class AnnotationMatchingPointcut implements Pointcut {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (!(obj instanceof AnnotationCandidateClassFilter that)) {
|
||||
return false;
|
||||
}
|
||||
return this.annotationType.equals(that.annotationType);
|
||||
public boolean equals(@Nullable Object other) {
|
||||
return (this == other || (other instanceof AnnotationCandidateClassFilter that &&
|
||||
this.annotationType.equals(that.annotationType)));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+7
-12
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -27,10 +27,9 @@ import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* 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).
|
||||
* 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
|
||||
@@ -90,13 +89,9 @@ public class AnnotationMethodMatcher extends StaticMethodMatcher {
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (!(other instanceof AnnotationMethodMatcher otherMm)) {
|
||||
return false;
|
||||
}
|
||||
return (this.annotationType.equals(otherMm.annotationType) && this.checkInherited == otherMm.checkInherited);
|
||||
return (this == other || (other instanceof AnnotationMethodMatcher otherMm &&
|
||||
this.annotationType.equals(otherMm.annotationType) &&
|
||||
this.checkInherited == otherMm.checkInherited));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+12
-8
@@ -25,6 +25,7 @@ import org.springframework.aop.TargetSource;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
@@ -58,16 +59,18 @@ public abstract class AbstractBeanFactoryBasedTargetSource implements TargetSour
|
||||
protected final transient Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
/** Name of the target bean we will create on each invocation. */
|
||||
@Nullable
|
||||
private String targetBeanName;
|
||||
|
||||
/** Class of the target. */
|
||||
@Nullable
|
||||
private volatile Class<?> targetClass;
|
||||
|
||||
/**
|
||||
* BeanFactory that owns this TargetSource. We need to hold onto this
|
||||
* reference so that we can create new prototype instances as necessary.
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
@Nullable
|
||||
private BeanFactory beanFactory;
|
||||
|
||||
|
||||
@@ -88,6 +91,7 @@ public abstract class AbstractBeanFactoryBasedTargetSource implements TargetSour
|
||||
* Return the name of the target bean in the factory.
|
||||
*/
|
||||
public String getTargetBeanName() {
|
||||
Assert.state(this.targetBeanName != null, "Target bean name not set");
|
||||
return this.targetBeanName;
|
||||
}
|
||||
|
||||
@@ -117,11 +121,13 @@ public abstract class AbstractBeanFactoryBasedTargetSource implements TargetSour
|
||||
* Return the owning BeanFactory.
|
||||
*/
|
||||
public BeanFactory getBeanFactory() {
|
||||
Assert.state(this.beanFactory != null, "BeanFactory not set");
|
||||
return this.beanFactory;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Class<?> getTargetClass() {
|
||||
Class<?> targetClass = this.targetClass;
|
||||
if (targetClass != null) {
|
||||
@@ -130,7 +136,7 @@ public abstract class AbstractBeanFactoryBasedTargetSource implements TargetSour
|
||||
synchronized (this) {
|
||||
// Full check within synchronization, entering the BeanFactory interaction algorithm only once...
|
||||
targetClass = this.targetClass;
|
||||
if (targetClass == null && this.beanFactory != null) {
|
||||
if (targetClass == null && this.beanFactory != null && this.targetBeanName != null) {
|
||||
// Determine type of the target bean.
|
||||
targetClass = this.beanFactory.getType(this.targetBeanName);
|
||||
if (targetClass == null) {
|
||||
@@ -184,18 +190,16 @@ public abstract class AbstractBeanFactoryBasedTargetSource implements TargetSour
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int hashCode = getClass().hashCode();
|
||||
hashCode = 13 * hashCode + ObjectUtils.nullSafeHashCode(this.beanFactory);
|
||||
hashCode = 13 * hashCode + ObjectUtils.nullSafeHashCode(this.targetBeanName);
|
||||
return hashCode;
|
||||
return getClass().hashCode() * 13 + ObjectUtils.nullSafeHashCode(this.targetBeanName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder(getClass().getSimpleName());
|
||||
sb.append(" for target bean '").append(this.targetBeanName).append('\'');
|
||||
if (this.targetClass != null) {
|
||||
sb.append(" of type [").append(this.targetClass.getName()).append(']');
|
||||
Class<?> targetClass = this.targetClass;
|
||||
if (targetClass != null) {
|
||||
sb.append(" of type [").append(targetClass.getName()).append(']');
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
+2
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -46,6 +46,7 @@ public abstract class AbstractLazyCreationTargetSource implements TargetSource {
|
||||
protected final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
/** The lazily initialized target object. */
|
||||
@Nullable
|
||||
private Object lazyTarget;
|
||||
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ public final class EmptyTargetSource implements TargetSource, Serializable {
|
||||
// Instance implementation
|
||||
//---------------------------------------------------------------------
|
||||
|
||||
@Nullable
|
||||
private final Class<?> targetClass;
|
||||
|
||||
private final boolean isStatic;
|
||||
|
||||
+3
-4
@@ -97,12 +97,11 @@ public class HotSwappableTargetSource implements TargetSource, Serializable {
|
||||
|
||||
|
||||
/**
|
||||
* Two HotSwappableTargetSources are equal if the current target
|
||||
* objects are equal.
|
||||
* Two HotSwappableTargetSources are equal if the current target objects are equal.
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
return (this == obj || (obj instanceof HotSwappableTargetSource that &&
|
||||
public boolean equals(@Nullable Object other) {
|
||||
return (this == other || (other instanceof HotSwappableTargetSource that &&
|
||||
this.target.equals(that.target)));
|
||||
}
|
||||
|
||||
|
||||
@@ -84,13 +84,8 @@ public class SingletonTargetSource implements TargetSource, Serializable {
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(@Nullable Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (!(other instanceof SingletonTargetSource otherTargetSource)) {
|
||||
return false;
|
||||
}
|
||||
return this.target.equals(otherTargetSource.target);
|
||||
return (this == other || (other instanceof SingletonTargetSource that &&
|
||||
this.target.equals(that.target)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+7
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -58,7 +58,12 @@ public class ThreadLocalTargetSource extends AbstractPrototypeBasedTargetSource
|
||||
* is meant to be per thread per instance of the ThreadLocalTargetSource class.
|
||||
*/
|
||||
private final ThreadLocal<Object> targetInThread =
|
||||
new NamedThreadLocal<>("Thread-local instance of bean '" + getTargetBeanName() + "'");
|
||||
new NamedThreadLocal<>("Thread-local instance of bean") {
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString() + " '" + getTargetBeanName() + "'";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Set of managed targets, enabling us to keep track of the targets we've created.
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=\
|
||||
org.springframework.aop.scope.ScopedProxyBeanRegistrationAotProcessor
|
||||
|
||||
org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor= \
|
||||
org.springframework.aop.aspectj.annotation.AspectJBeanFactoryInitializationAotProcessor
|
||||
|
||||
+8
-5
@@ -637,17 +637,20 @@ abstract class AbstractAspectJAdvisorFactoryTests {
|
||||
}
|
||||
|
||||
|
||||
static class CommonPointcuts {
|
||||
|
||||
@Pointcut("execution(* getAge())")
|
||||
void getAge() {
|
||||
}
|
||||
}
|
||||
|
||||
@Aspect
|
||||
static class NamedPointcutAspectWithFQN {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private ITestBean fieldThatShouldBeIgnoredBySpringAtAspectJProcessing = new TestBean();
|
||||
|
||||
@Pointcut("execution(* getAge())")
|
||||
void getAge() {
|
||||
}
|
||||
|
||||
@Around("org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactoryTests.NamedPointcutAspectWithFQN.getAge()")
|
||||
@Around("org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactoryTests.CommonPointcuts.getAge()()")
|
||||
int changeReturnValue(ProceedingJoinPoint pjp) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.aop.aspectj.annotation;
|
||||
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Pointcut;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.aot.generate.GenerationContext;
|
||||
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
|
||||
import org.springframework.aot.test.generate.TestGenerationContext;
|
||||
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution;
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link AspectJBeanFactoryInitializationAotProcessor}.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
*/
|
||||
class AspectJBeanFactoryInitializationAotProcessorTests {
|
||||
|
||||
private final GenerationContext generationContext = new TestGenerationContext();
|
||||
|
||||
@Test
|
||||
void shouldSkipEmptyClass() {
|
||||
assertThat(createContribution(EmptyClass.class)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldProcessAspect() {
|
||||
process(TestAspect.class);
|
||||
assertThat(RuntimeHintsPredicates.reflection().onMethod(TestAspect.class, "alterReturnValue").invoke())
|
||||
.accepts(this.generationContext.getRuntimeHints());
|
||||
}
|
||||
|
||||
private void process(Class<?> beanClass) {
|
||||
BeanFactoryInitializationAotContribution contribution = createContribution(beanClass);
|
||||
if (contribution != null) {
|
||||
contribution.applyTo(this.generationContext, mock());
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static BeanFactoryInitializationAotContribution createContribution(Class<?> beanClass) {
|
||||
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
|
||||
beanFactory.registerBeanDefinition(beanClass.getName(), new RootBeanDefinition(beanClass));
|
||||
return new AspectJBeanFactoryInitializationAotProcessor().processAheadOfTime(beanFactory);
|
||||
}
|
||||
|
||||
|
||||
static class EmptyClass { }
|
||||
|
||||
@Aspect
|
||||
static class TestAspect {
|
||||
|
||||
@Around("pointcut()")
|
||||
public Object alterReturnValue(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
joinPoint.proceed();
|
||||
return "A-from-aspect";
|
||||
}
|
||||
|
||||
@Pointcut("execution(* com.example.aspect.Test*.methodA(..))")
|
||||
private void pointcut() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+13
-10
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -19,9 +19,9 @@ package org.springframework.aop.config;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.BeanDefinitionStoreException;
|
||||
import org.springframework.beans.factory.parsing.BeanDefinitionParsingException;
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.springframework.core.testfixture.io.ResourceTestUtils.qualifiedResource;
|
||||
@@ -29,25 +29,28 @@ import static org.springframework.core.testfixture.io.ResourceTestUtils.qualifie
|
||||
/**
|
||||
* @author Mark Fisher
|
||||
* @author Chris Beams
|
||||
* @author Sam Brannen
|
||||
*/
|
||||
class AopNamespaceHandlerPointcutErrorTests {
|
||||
|
||||
@Test
|
||||
void duplicatePointcutConfig() {
|
||||
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
|
||||
assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(() ->
|
||||
new XmlBeanDefinitionReader(bf).loadBeanDefinitions(
|
||||
qualifiedResource(getClass(), "pointcutDuplication.xml")))
|
||||
.satisfies(ex -> ex.contains(BeanDefinitionParsingException.class));
|
||||
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(bf);
|
||||
ClassPathResource resource = qualifiedResource(getClass(), "pointcutDuplication.xml");
|
||||
|
||||
assertThatExceptionOfType(BeanDefinitionStoreException.class)
|
||||
.isThrownBy(() -> reader.loadBeanDefinitions(resource));
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingPointcutConfig() {
|
||||
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
|
||||
assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(() ->
|
||||
new XmlBeanDefinitionReader(bf).loadBeanDefinitions(
|
||||
qualifiedResource(getClass(), "pointcutMissing.xml")))
|
||||
.satisfies(ex -> ex.contains(BeanDefinitionParsingException.class));
|
||||
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(bf);
|
||||
ClassPathResource resource = qualifiedResource(getClass(), "pointcutMissing.xml");
|
||||
|
||||
assertThatExceptionOfType(BeanDefinitionStoreException.class)
|
||||
.isThrownBy(() -> reader.loadBeanDefinitions(resource));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+4
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -82,6 +82,8 @@ public class IntroductionBenchmarkTests {
|
||||
}
|
||||
sw.stop();
|
||||
|
||||
System.out.println(sw.prettyPrint());
|
||||
// TODO Add reasonable assertions.
|
||||
// System.out.println(sw.prettyPrint());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -389,7 +389,9 @@ public class ProxyFactoryTests {
|
||||
CharSequence target = "test";
|
||||
ProxyFactory pf = new ProxyFactory(target);
|
||||
ClassLoader cl = target.getClass().getClassLoader();
|
||||
assertThat(((CharSequence) pf.getProxy(cl)).toString()).isEqualTo(target);
|
||||
CharSequence proxy = (CharSequence) pf.getProxy(cl);
|
||||
assertThat(proxy.toString()).isEqualTo(target);
|
||||
assertThat(pf.getProxyClass(cl)).isSameAs(proxy.getClass());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -398,7 +400,9 @@ public class ProxyFactoryTests {
|
||||
ProxyFactory pf = new ProxyFactory(target);
|
||||
pf.setProxyTargetClass(true);
|
||||
ClassLoader cl = target.getClass().getClassLoader();
|
||||
assertThat(((Date) pf.getProxy(cl)).getTime()).isEqualTo(target.getTime());
|
||||
Date proxy = (Date) pf.getProxy(cl);
|
||||
assertThat(proxy.getTime()).isEqualTo(target.getTime());
|
||||
assertThat(pf.getProxyClass(cl)).isSameAs(proxy.getClass());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -415,7 +419,9 @@ public class ProxyFactoryTests {
|
||||
};
|
||||
ProxyFactory pf = new ProxyFactory(target);
|
||||
ClassLoader cl = Savepoint.class.getClassLoader();
|
||||
assertThat(((Savepoint) pf.getProxy(cl)).getSavepointName()).isEqualTo("sp");
|
||||
Savepoint proxy = (Savepoint) pf.getProxy(cl);
|
||||
assertThat(proxy.getSavepointName()).isEqualTo("sp");
|
||||
assertThat(pf.getProxyClass(cl)).isSameAs(proxy.getClass());
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -14,4 +14,5 @@ dependencies {
|
||||
testImplementation("jakarta.annotation:jakarta.annotation-api")
|
||||
testFixturesApi("org.junit.jupiter:junit-jupiter-api")
|
||||
testFixturesImplementation("org.assertj:assertj-core")
|
||||
testFixturesImplementation("com.google.code.findbugs:jsr305")
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.beans;
|
||||
|
||||
import org.springframework.aot.hint.MemberCategory;
|
||||
import org.springframework.aot.hint.ReflectionHints;
|
||||
import org.springframework.aot.hint.RuntimeHints;
|
||||
import org.springframework.aot.hint.RuntimeHintsRegistrar;
|
||||
import org.springframework.core.io.ResourceEditor;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* {@link RuntimeHintsRegistrar} to register hints for popular conventions in
|
||||
* {@link BeanUtils#findEditorByConvention(Class)}.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @since 6.0.10
|
||||
*/
|
||||
class BeanUtilsRuntimeHints implements RuntimeHintsRegistrar {
|
||||
|
||||
@Override
|
||||
public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
|
||||
ReflectionHints reflectionHints = hints.reflection();
|
||||
reflectionHints.registerType(ResourceEditor.class, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
|
||||
reflectionHints.registerTypeIfPresent(classLoader, "org.springframework.http.MediaTypeEditor",
|
||||
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -63,8 +63,8 @@ public abstract class TypeConverterSupport extends PropertyEditorRegistrySupport
|
||||
(field != null ? new TypeDescriptor(field) : TypeDescriptor.valueOf(requiredType)));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
@Nullable
|
||||
public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType,
|
||||
@Nullable TypeDescriptor typeDescriptor) throws TypeMismatchException {
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -136,7 +136,10 @@ public interface BeanFactory {
|
||||
* <p>Translates aliases back to the corresponding canonical bean name.
|
||||
* <p>Will ask the parent factory if the bean cannot be found in this factory instance.
|
||||
* @param name the name of the bean to retrieve
|
||||
* @return an instance of the bean
|
||||
* @return an instance of the bean.
|
||||
* Note that the return value will never be {@code null} but possibly a stub for
|
||||
* {@code null} returned from a factory method, to be checked via {@code equals(null)}.
|
||||
* Consider using {@link #getBeanProvider(Class)} for resolving optional dependencies.
|
||||
* @throws NoSuchBeanDefinitionException if there is no bean with the specified name
|
||||
* @throws BeansException if the bean could not be obtained
|
||||
*/
|
||||
@@ -152,7 +155,11 @@ public interface BeanFactory {
|
||||
* <p>Will ask the parent factory if the bean cannot be found in this factory instance.
|
||||
* @param name the name of the bean to retrieve
|
||||
* @param requiredType type the bean must match; can be an interface or superclass
|
||||
* @return an instance of the bean
|
||||
* @return an instance of the bean.
|
||||
* Note that the return value will never be {@code null}. In case of a stub for
|
||||
* {@code null} from a factory method having been resolved for the requested bean, a
|
||||
* {@code BeanNotOfRequiredTypeException} against the NullBean stub will be raised.
|
||||
* Consider using {@link #getBeanProvider(Class)} for resolving optional dependencies.
|
||||
* @throws NoSuchBeanDefinitionException if there is no such bean definition
|
||||
* @throws BeanNotOfRequiredTypeException if the bean is not of the required type
|
||||
* @throws BeansException if the bean could not be created
|
||||
|
||||
+30
-22
@@ -286,7 +286,8 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA
|
||||
String beanName = registeredBean.getBeanName();
|
||||
RootBeanDefinition beanDefinition = registeredBean.getMergedBeanDefinition();
|
||||
InjectionMetadata metadata = findInjectionMetadata(beanName, beanClass, beanDefinition);
|
||||
Collection<AutowiredElement> autowiredElements = getAutowiredElements(metadata);
|
||||
Collection<AutowiredElement> autowiredElements = getAutowiredElements(metadata,
|
||||
beanDefinition.getPropertyValues());
|
||||
if (!ObjectUtils.isEmpty(autowiredElements)) {
|
||||
return new AotContribution(beanClass, autowiredElements, getAutowireCandidateResolver());
|
||||
}
|
||||
@@ -295,8 +296,8 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA
|
||||
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
private Collection<AutowiredElement> getAutowiredElements(InjectionMetadata metadata) {
|
||||
return (Collection) metadata.getInjectedElements();
|
||||
private Collection<AutowiredElement> getAutowiredElements(InjectionMetadata metadata, PropertyValues propertyValues) {
|
||||
return (Collection) metadata.getInjectedElements(propertyValues);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -637,7 +638,7 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA
|
||||
* Resolve the specified cached method argument or field value.
|
||||
*/
|
||||
@Nullable
|
||||
private Object resolvedCachedArgument(@Nullable String beanName, @Nullable Object cachedArgument) {
|
||||
private Object resolveCachedArgument(@Nullable String beanName, @Nullable Object cachedArgument) {
|
||||
if (cachedArgument instanceof DependencyDescriptor descriptor) {
|
||||
Assert.state(this.beanFactory != null, "No BeanFactory available");
|
||||
return this.beanFactory.resolveDependency(descriptor, beanName, null, null);
|
||||
@@ -682,10 +683,12 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA
|
||||
Object value;
|
||||
if (this.cached) {
|
||||
try {
|
||||
value = resolvedCachedArgument(beanName, this.cachedFieldValue);
|
||||
value = resolveCachedArgument(beanName, this.cachedFieldValue);
|
||||
}
|
||||
catch (NoSuchBeanDefinitionException ex) {
|
||||
// Unexpected removal of target bean for cached argument -> re-resolve
|
||||
catch (BeansException ex) {
|
||||
// Unexpected target bean mismatch for cached argument -> re-resolve
|
||||
this.cached = false;
|
||||
logger.debug("Failed to resolve cached argument", ex);
|
||||
value = resolveFieldValue(field, bean, beanName);
|
||||
}
|
||||
}
|
||||
@@ -714,11 +717,10 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA
|
||||
}
|
||||
synchronized (this) {
|
||||
if (!this.cached) {
|
||||
Object cachedFieldValue = null;
|
||||
if (value != null || this.required) {
|
||||
cachedFieldValue = desc;
|
||||
Object cachedFieldValue = desc;
|
||||
registerDependentBeans(beanName, autowiredBeanNames);
|
||||
if (autowiredBeanNames.size() == 1) {
|
||||
if (value != null && autowiredBeanNames.size() == 1) {
|
||||
String autowiredBeanName = autowiredBeanNames.iterator().next();
|
||||
if (beanFactory.containsBean(autowiredBeanName) &&
|
||||
beanFactory.isTypeMatch(autowiredBeanName, field.getType())) {
|
||||
@@ -726,9 +728,13 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA
|
||||
desc, autowiredBeanName, field.getType());
|
||||
}
|
||||
}
|
||||
this.cachedFieldValue = cachedFieldValue;
|
||||
this.cached = true;
|
||||
}
|
||||
else {
|
||||
this.cachedFieldValue = null;
|
||||
// cached flag remains false
|
||||
}
|
||||
this.cachedFieldValue = cachedFieldValue;
|
||||
this.cached = true;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
@@ -752,17 +758,19 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA
|
||||
|
||||
@Override
|
||||
protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
|
||||
if (checkPropertySkipping(pvs)) {
|
||||
if (!shouldInject(pvs)) {
|
||||
return;
|
||||
}
|
||||
Method method = (Method) this.member;
|
||||
Object[] arguments;
|
||||
if (this.cached) {
|
||||
try {
|
||||
arguments = resolveCachedArguments(beanName);
|
||||
arguments = resolveCachedArguments(beanName, this.cachedMethodArguments);
|
||||
}
|
||||
catch (NoSuchBeanDefinitionException ex) {
|
||||
// Unexpected removal of target bean for cached argument -> re-resolve
|
||||
catch (BeansException ex) {
|
||||
// Unexpected target bean mismatch for cached argument -> re-resolve
|
||||
this.cached = false;
|
||||
logger.debug("Failed to resolve cached argument", ex);
|
||||
arguments = resolveMethodArguments(method, bean, beanName);
|
||||
}
|
||||
}
|
||||
@@ -781,14 +789,13 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Object[] resolveCachedArguments(@Nullable String beanName) {
|
||||
Object[] cachedMethodArguments = this.cachedMethodArguments;
|
||||
private Object[] resolveCachedArguments(@Nullable String beanName, @Nullable Object[] cachedMethodArguments) {
|
||||
if (cachedMethodArguments == null) {
|
||||
return null;
|
||||
}
|
||||
Object[] arguments = new Object[cachedMethodArguments.length];
|
||||
for (int i = 0; i < arguments.length; i++) {
|
||||
arguments[i] = resolvedCachedArgument(beanName, cachedMethodArguments[i]);
|
||||
arguments[i] = resolveCachedArgument(beanName, cachedMethodArguments[i]);
|
||||
}
|
||||
return arguments;
|
||||
}
|
||||
@@ -821,14 +828,14 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA
|
||||
synchronized (this) {
|
||||
if (!this.cached) {
|
||||
if (arguments != null) {
|
||||
DependencyDescriptor[] cachedMethodArguments = Arrays.copyOf(descriptors, arguments.length);
|
||||
DependencyDescriptor[] cachedMethodArguments = Arrays.copyOf(descriptors, argumentCount);
|
||||
registerDependentBeans(beanName, autowiredBeans);
|
||||
if (autowiredBeans.size() == argumentCount) {
|
||||
Iterator<String> it = autowiredBeans.iterator();
|
||||
Class<?>[] paramTypes = method.getParameterTypes();
|
||||
for (int i = 0; i < paramTypes.length; i++) {
|
||||
String autowiredBeanName = it.next();
|
||||
if (beanFactory.containsBean(autowiredBeanName) &&
|
||||
if (arguments[i] != null && beanFactory.containsBean(autowiredBeanName) &&
|
||||
beanFactory.isTypeMatch(autowiredBeanName, paramTypes[i])) {
|
||||
cachedMethodArguments[i] = new ShortcutDependencyDescriptor(
|
||||
descriptors[i], autowiredBeanName, paramTypes[i]);
|
||||
@@ -836,11 +843,12 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA
|
||||
}
|
||||
}
|
||||
this.cachedMethodArguments = cachedMethodArguments;
|
||||
this.cached = true;
|
||||
}
|
||||
else {
|
||||
this.cachedMethodArguments = null;
|
||||
// cached flag remains false
|
||||
}
|
||||
this.cached = true;
|
||||
}
|
||||
}
|
||||
return arguments;
|
||||
|
||||
+133
-89
@@ -24,7 +24,6 @@ import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
@@ -79,6 +78,7 @@ import org.springframework.util.ReflectionUtils;
|
||||
* @author Juergen Hoeller
|
||||
* @author Stephane Nicoll
|
||||
* @author Phillip Webb
|
||||
* @author Sam Brannen
|
||||
* @since 2.5
|
||||
* @see #setInitAnnotationType
|
||||
* @see #setDestroyAnnotationType
|
||||
@@ -90,7 +90,7 @@ public class InitDestroyAnnotationBeanPostProcessor implements DestructionAwareB
|
||||
private final transient LifecycleMetadata emptyLifecycleMetadata =
|
||||
new LifecycleMetadata(Object.class, Collections.emptyList(), Collections.emptyList()) {
|
||||
@Override
|
||||
public void checkConfigMembers(RootBeanDefinition beanDefinition) {
|
||||
public void checkInitDestroyMethods(RootBeanDefinition beanDefinition) {
|
||||
}
|
||||
@Override
|
||||
public void invokeInitMethods(Object target, String beanName) {
|
||||
@@ -107,11 +107,9 @@ public class InitDestroyAnnotationBeanPostProcessor implements DestructionAwareB
|
||||
|
||||
protected transient Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
@Nullable
|
||||
private Class<? extends Annotation> initAnnotationType;
|
||||
private final Set<Class<? extends Annotation>> initAnnotationTypes = new LinkedHashSet<>(2);
|
||||
|
||||
@Nullable
|
||||
private Class<? extends Annotation> destroyAnnotationType;
|
||||
private final Set<Class<? extends Annotation>> destroyAnnotationTypes = new LinkedHashSet<>(2);
|
||||
|
||||
private int order = Ordered.LOWEST_PRECEDENCE;
|
||||
|
||||
@@ -125,9 +123,23 @@ public class InitDestroyAnnotationBeanPostProcessor implements DestructionAwareB
|
||||
* <p>Any custom annotation can be used, since there are no required
|
||||
* annotation attributes. There is no default, although a typical choice
|
||||
* is the {@link jakarta.annotation.PostConstruct} annotation.
|
||||
* @see #addInitAnnotationType
|
||||
*/
|
||||
public void setInitAnnotationType(Class<? extends Annotation> initAnnotationType) {
|
||||
this.initAnnotationType = initAnnotationType;
|
||||
this.initAnnotationTypes.clear();
|
||||
this.initAnnotationTypes.add(initAnnotationType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an init annotation to check for, indicating initialization
|
||||
* methods to call after configuration of a bean.
|
||||
* @since 6.0.11
|
||||
* @see #setInitAnnotationType
|
||||
*/
|
||||
public void addInitAnnotationType(@Nullable Class<? extends Annotation> initAnnotationType) {
|
||||
if (initAnnotationType != null) {
|
||||
this.initAnnotationTypes.add(initAnnotationType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -136,9 +148,23 @@ public class InitDestroyAnnotationBeanPostProcessor implements DestructionAwareB
|
||||
* <p>Any custom annotation can be used, since there are no required
|
||||
* annotation attributes. There is no default, although a typical choice
|
||||
* is the {@link jakarta.annotation.PreDestroy} annotation.
|
||||
* @see #addDestroyAnnotationType
|
||||
*/
|
||||
public void setDestroyAnnotationType(Class<? extends Annotation> destroyAnnotationType) {
|
||||
this.destroyAnnotationType = destroyAnnotationType;
|
||||
this.destroyAnnotationTypes.clear();
|
||||
this.destroyAnnotationTypes.add(destroyAnnotationType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a destroy annotation to check for, indicating destruction
|
||||
* methods to call when the context is shutting down.
|
||||
* @since 6.0.11
|
||||
* @see #setDestroyAnnotationType
|
||||
*/
|
||||
public void addDestroyAnnotationType(@Nullable Class<? extends Annotation> destroyAnnotationType) {
|
||||
if (destroyAnnotationType != null) {
|
||||
this.destroyAnnotationTypes.add(destroyAnnotationType);
|
||||
}
|
||||
}
|
||||
|
||||
public void setOrder(int order) {
|
||||
@@ -152,8 +178,8 @@ public class InitDestroyAnnotationBeanPostProcessor implements DestructionAwareB
|
||||
|
||||
|
||||
@Override
|
||||
public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
|
||||
findInjectionMetadata(beanDefinition, beanType);
|
||||
public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanClass, String beanName) {
|
||||
findLifecycleMetadata(beanDefinition, beanClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -161,7 +187,7 @@ public class InitDestroyAnnotationBeanPostProcessor implements DestructionAwareB
|
||||
public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) {
|
||||
RootBeanDefinition beanDefinition = registeredBean.getMergedBeanDefinition();
|
||||
beanDefinition.resolveDestroyMethodIfNecessary();
|
||||
LifecycleMetadata metadata = findInjectionMetadata(beanDefinition, registeredBean.getBeanClass());
|
||||
LifecycleMetadata metadata = findLifecycleMetadata(beanDefinition, registeredBean.getBeanClass());
|
||||
if (!CollectionUtils.isEmpty(metadata.initMethods)) {
|
||||
String[] initMethodNames = safeMerge(beanDefinition.getInitMethodNames(), metadata.initMethods);
|
||||
beanDefinition.setInitMethodNames(initMethodNames);
|
||||
@@ -173,16 +199,16 @@ public class InitDestroyAnnotationBeanPostProcessor implements DestructionAwareB
|
||||
return null;
|
||||
}
|
||||
|
||||
private LifecycleMetadata findInjectionMetadata(RootBeanDefinition beanDefinition, Class<?> beanType) {
|
||||
LifecycleMetadata metadata = findLifecycleMetadata(beanType);
|
||||
metadata.checkConfigMembers(beanDefinition);
|
||||
private LifecycleMetadata findLifecycleMetadata(RootBeanDefinition beanDefinition, Class<?> beanClass) {
|
||||
LifecycleMetadata metadata = findLifecycleMetadata(beanClass);
|
||||
metadata.checkInitDestroyMethods(beanDefinition);
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private String[] safeMerge(@Nullable String[] existingNames, Collection<LifecycleElement> detectedElements) {
|
||||
Stream<String> detectedNames = detectedElements.stream().map(LifecycleElement::getIdentifier);
|
||||
private static String[] safeMerge(@Nullable String[] existingNames, Collection<LifecycleMethod> detectedMethods) {
|
||||
Stream<String> detectedNames = detectedMethods.stream().map(LifecycleMethod::getIdentifier);
|
||||
Stream<String> mergedNames = (existingNames != null ?
|
||||
Stream.concat(Stream.of(existingNames), detectedNames) : detectedNames);
|
||||
Stream.concat(detectedNames, Stream.of(existingNames)) : detectedNames);
|
||||
return mergedNames.distinct().toArray(String[]::new);
|
||||
}
|
||||
|
||||
@@ -217,12 +243,14 @@ public class InitDestroyAnnotationBeanPostProcessor implements DestructionAwareB
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.warn(msg, ex.getTargetException());
|
||||
}
|
||||
else {
|
||||
else if (logger.isWarnEnabled()) {
|
||||
logger.warn(msg + ": " + ex.getTargetException());
|
||||
}
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
logger.warn("Failed to invoke destroy method on bean with name '" + beanName + "'", ex);
|
||||
if (logger.isWarnEnabled()) {
|
||||
logger.warn("Failed to invoke destroy method on bean with name '" + beanName + "'", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,19 +260,19 @@ public class InitDestroyAnnotationBeanPostProcessor implements DestructionAwareB
|
||||
}
|
||||
|
||||
|
||||
private LifecycleMetadata findLifecycleMetadata(Class<?> clazz) {
|
||||
private LifecycleMetadata findLifecycleMetadata(Class<?> beanClass) {
|
||||
if (this.lifecycleMetadataCache == null) {
|
||||
// Happens after deserialization, during destruction...
|
||||
return buildLifecycleMetadata(clazz);
|
||||
return buildLifecycleMetadata(beanClass);
|
||||
}
|
||||
// Quick check on the concurrent map first, with minimal locking.
|
||||
LifecycleMetadata metadata = this.lifecycleMetadataCache.get(clazz);
|
||||
LifecycleMetadata metadata = this.lifecycleMetadataCache.get(beanClass);
|
||||
if (metadata == null) {
|
||||
synchronized (this.lifecycleMetadataCache) {
|
||||
metadata = this.lifecycleMetadataCache.get(clazz);
|
||||
metadata = this.lifecycleMetadataCache.get(beanClass);
|
||||
if (metadata == null) {
|
||||
metadata = buildLifecycleMetadata(clazz);
|
||||
this.lifecycleMetadataCache.put(clazz, metadata);
|
||||
metadata = buildLifecycleMetadata(beanClass);
|
||||
this.lifecycleMetadataCache.put(beanClass, metadata);
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
@@ -252,43 +280,47 @@ public class InitDestroyAnnotationBeanPostProcessor implements DestructionAwareB
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private LifecycleMetadata buildLifecycleMetadata(final Class<?> clazz) {
|
||||
if (!AnnotationUtils.isCandidateClass(clazz, Arrays.asList(this.initAnnotationType, this.destroyAnnotationType))) {
|
||||
private LifecycleMetadata buildLifecycleMetadata(final Class<?> beanClass) {
|
||||
if (!AnnotationUtils.isCandidateClass(beanClass, this.initAnnotationTypes) &&
|
||||
!AnnotationUtils.isCandidateClass(beanClass, this.destroyAnnotationTypes)) {
|
||||
return this.emptyLifecycleMetadata;
|
||||
}
|
||||
|
||||
List<LifecycleElement> initMethods = new ArrayList<>();
|
||||
List<LifecycleElement> destroyMethods = new ArrayList<>();
|
||||
Class<?> targetClass = clazz;
|
||||
List<LifecycleMethod> initMethods = new ArrayList<>();
|
||||
List<LifecycleMethod> destroyMethods = new ArrayList<>();
|
||||
Class<?> currentClass = beanClass;
|
||||
|
||||
do {
|
||||
final List<LifecycleElement> currInitMethods = new ArrayList<>();
|
||||
final List<LifecycleElement> currDestroyMethods = new ArrayList<>();
|
||||
final List<LifecycleMethod> currInitMethods = new ArrayList<>();
|
||||
final List<LifecycleMethod> currDestroyMethods = new ArrayList<>();
|
||||
|
||||
ReflectionUtils.doWithLocalMethods(targetClass, method -> {
|
||||
if (this.initAnnotationType != null && method.isAnnotationPresent(this.initAnnotationType)) {
|
||||
LifecycleElement element = new LifecycleElement(method);
|
||||
currInitMethods.add(element);
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Found init method on class [" + clazz.getName() + "]: " + method);
|
||||
ReflectionUtils.doWithLocalMethods(currentClass, method -> {
|
||||
for (Class<? extends Annotation> initAnnotationType : this.initAnnotationTypes) {
|
||||
if (initAnnotationType != null && method.isAnnotationPresent(initAnnotationType)) {
|
||||
currInitMethods.add(new LifecycleMethod(method, beanClass));
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Found init method on class [" + beanClass.getName() + "]: " + method);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.destroyAnnotationType != null && method.isAnnotationPresent(this.destroyAnnotationType)) {
|
||||
currDestroyMethods.add(new LifecycleElement(method));
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Found destroy method on class [" + clazz.getName() + "]: " + method);
|
||||
for (Class<? extends Annotation> destroyAnnotationType : this.destroyAnnotationTypes) {
|
||||
if (destroyAnnotationType != null && method.isAnnotationPresent(destroyAnnotationType)) {
|
||||
currDestroyMethods.add(new LifecycleMethod(method, beanClass));
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Found destroy method on class [" + beanClass.getName() + "]: " + method);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
initMethods.addAll(0, currInitMethods);
|
||||
destroyMethods.addAll(currDestroyMethods);
|
||||
targetClass = targetClass.getSuperclass();
|
||||
currentClass = currentClass.getSuperclass();
|
||||
}
|
||||
while (targetClass != null && targetClass != Object.class);
|
||||
while (currentClass != null && currentClass != Object.class);
|
||||
|
||||
return (initMethods.isEmpty() && destroyMethods.isEmpty() ? this.emptyLifecycleMetadata :
|
||||
new LifecycleMetadata(clazz, initMethods, destroyMethods));
|
||||
new LifecycleMetadata(beanClass, initMethods, destroyMethods));
|
||||
}
|
||||
|
||||
|
||||
@@ -310,46 +342,46 @@ public class InitDestroyAnnotationBeanPostProcessor implements DestructionAwareB
|
||||
*/
|
||||
private class LifecycleMetadata {
|
||||
|
||||
private final Class<?> targetClass;
|
||||
private final Class<?> beanClass;
|
||||
|
||||
private final Collection<LifecycleElement> initMethods;
|
||||
private final Collection<LifecycleMethod> initMethods;
|
||||
|
||||
private final Collection<LifecycleElement> destroyMethods;
|
||||
private final Collection<LifecycleMethod> destroyMethods;
|
||||
|
||||
@Nullable
|
||||
private volatile Set<LifecycleElement> checkedInitMethods;
|
||||
private volatile Set<LifecycleMethod> checkedInitMethods;
|
||||
|
||||
@Nullable
|
||||
private volatile Set<LifecycleElement> checkedDestroyMethods;
|
||||
private volatile Set<LifecycleMethod> checkedDestroyMethods;
|
||||
|
||||
public LifecycleMetadata(Class<?> targetClass, Collection<LifecycleElement> initMethods,
|
||||
Collection<LifecycleElement> destroyMethods) {
|
||||
public LifecycleMetadata(Class<?> beanClass, Collection<LifecycleMethod> initMethods,
|
||||
Collection<LifecycleMethod> destroyMethods) {
|
||||
|
||||
this.targetClass = targetClass;
|
||||
this.beanClass = beanClass;
|
||||
this.initMethods = initMethods;
|
||||
this.destroyMethods = destroyMethods;
|
||||
}
|
||||
|
||||
public void checkConfigMembers(RootBeanDefinition beanDefinition) {
|
||||
Set<LifecycleElement> checkedInitMethods = new LinkedHashSet<>(this.initMethods.size());
|
||||
for (LifecycleElement element : this.initMethods) {
|
||||
String methodIdentifier = element.getIdentifier();
|
||||
public void checkInitDestroyMethods(RootBeanDefinition beanDefinition) {
|
||||
Set<LifecycleMethod> checkedInitMethods = new LinkedHashSet<>(this.initMethods.size());
|
||||
for (LifecycleMethod lifecycleMethod : this.initMethods) {
|
||||
String methodIdentifier = lifecycleMethod.getIdentifier();
|
||||
if (!beanDefinition.isExternallyManagedInitMethod(methodIdentifier)) {
|
||||
beanDefinition.registerExternallyManagedInitMethod(methodIdentifier);
|
||||
checkedInitMethods.add(element);
|
||||
checkedInitMethods.add(lifecycleMethod);
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Registered init method on class [" + this.targetClass.getName() + "]: " + methodIdentifier);
|
||||
logger.trace("Registered init method on class [" + this.beanClass.getName() + "]: " + methodIdentifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
Set<LifecycleElement> checkedDestroyMethods = new LinkedHashSet<>(this.destroyMethods.size());
|
||||
for (LifecycleElement element : this.destroyMethods) {
|
||||
String methodIdentifier = element.getIdentifier();
|
||||
Set<LifecycleMethod> checkedDestroyMethods = new LinkedHashSet<>(this.destroyMethods.size());
|
||||
for (LifecycleMethod lifecycleMethod : this.destroyMethods) {
|
||||
String methodIdentifier = lifecycleMethod.getIdentifier();
|
||||
if (!beanDefinition.isExternallyManagedDestroyMethod(methodIdentifier)) {
|
||||
beanDefinition.registerExternallyManagedDestroyMethod(methodIdentifier);
|
||||
checkedDestroyMethods.add(element);
|
||||
checkedDestroyMethods.add(lifecycleMethod);
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Registered destroy method on class [" + this.targetClass.getName() + "]: " + methodIdentifier);
|
||||
logger.trace("Registered destroy method on class [" + this.beanClass.getName() + "]: " + methodIdentifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -358,36 +390,36 @@ public class InitDestroyAnnotationBeanPostProcessor implements DestructionAwareB
|
||||
}
|
||||
|
||||
public void invokeInitMethods(Object target, String beanName) throws Throwable {
|
||||
Collection<LifecycleElement> checkedInitMethods = this.checkedInitMethods;
|
||||
Collection<LifecycleElement> initMethodsToIterate =
|
||||
Collection<LifecycleMethod> checkedInitMethods = this.checkedInitMethods;
|
||||
Collection<LifecycleMethod> initMethodsToIterate =
|
||||
(checkedInitMethods != null ? checkedInitMethods : this.initMethods);
|
||||
if (!initMethodsToIterate.isEmpty()) {
|
||||
for (LifecycleElement element : initMethodsToIterate) {
|
||||
for (LifecycleMethod lifecycleMethod : initMethodsToIterate) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Invoking init method on bean '" + beanName + "': " + element.getMethod());
|
||||
logger.trace("Invoking init method on bean '" + beanName + "': " + lifecycleMethod.getMethod());
|
||||
}
|
||||
element.invoke(target);
|
||||
lifecycleMethod.invoke(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void invokeDestroyMethods(Object target, String beanName) throws Throwable {
|
||||
Collection<LifecycleElement> checkedDestroyMethods = this.checkedDestroyMethods;
|
||||
Collection<LifecycleElement> destroyMethodsToUse =
|
||||
Collection<LifecycleMethod> checkedDestroyMethods = this.checkedDestroyMethods;
|
||||
Collection<LifecycleMethod> destroyMethodsToUse =
|
||||
(checkedDestroyMethods != null ? checkedDestroyMethods : this.destroyMethods);
|
||||
if (!destroyMethodsToUse.isEmpty()) {
|
||||
for (LifecycleElement element : destroyMethodsToUse) {
|
||||
for (LifecycleMethod lifecycleMethod : destroyMethodsToUse) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Invoking destroy method on bean '" + beanName + "': " + element.getMethod());
|
||||
logger.trace("Invoking destroy method on bean '" + beanName + "': " + lifecycleMethod.getMethod());
|
||||
}
|
||||
element.invoke(target);
|
||||
lifecycleMethod.invoke(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasDestroyMethods() {
|
||||
Collection<LifecycleElement> checkedDestroyMethods = this.checkedDestroyMethods;
|
||||
Collection<LifecycleElement> destroyMethodsToUse =
|
||||
Collection<LifecycleMethod> checkedDestroyMethods = this.checkedDestroyMethods;
|
||||
Collection<LifecycleMethod> destroyMethodsToUse =
|
||||
(checkedDestroyMethods != null ? checkedDestroyMethods : this.destroyMethods);
|
||||
return !destroyMethodsToUse.isEmpty();
|
||||
}
|
||||
@@ -395,20 +427,20 @@ public class InitDestroyAnnotationBeanPostProcessor implements DestructionAwareB
|
||||
|
||||
|
||||
/**
|
||||
* Class representing injection information about an annotated method.
|
||||
* Class representing an annotated init or destroy method.
|
||||
*/
|
||||
private static class LifecycleElement {
|
||||
private static class LifecycleMethod {
|
||||
|
||||
private final Method method;
|
||||
|
||||
private final String identifier;
|
||||
|
||||
public LifecycleElement(Method method) {
|
||||
public LifecycleMethod(Method method, Class<?> beanClass) {
|
||||
if (method.getParameterCount() != 0) {
|
||||
throw new IllegalStateException("Lifecycle method annotation requires a no-arg method: " + method);
|
||||
throw new IllegalStateException("Lifecycle annotation requires a no-arg method: " + method);
|
||||
}
|
||||
this.method = method;
|
||||
this.identifier = (Modifier.isPrivate(method.getModifiers()) ?
|
||||
this.identifier = (isPrivateOrNotVisible(method, beanClass) ?
|
||||
ClassUtils.getQualifiedMethodName(method) : method.getName());
|
||||
}
|
||||
|
||||
@@ -422,24 +454,36 @@ public class InitDestroyAnnotationBeanPostProcessor implements DestructionAwareB
|
||||
|
||||
public void invoke(Object target) throws Throwable {
|
||||
ReflectionUtils.makeAccessible(this.method);
|
||||
this.method.invoke(target, (Object[]) null);
|
||||
this.method.invoke(target);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (!(other instanceof LifecycleElement otherElement)) {
|
||||
return false;
|
||||
}
|
||||
return (this.identifier.equals(otherElement.identifier));
|
||||
return (this == other || (other instanceof LifecycleMethod that &&
|
||||
this.identifier.equals(that.identifier)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return this.identifier.hashCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the supplied lifecycle {@link Method} is private or not
|
||||
* visible to the supplied bean {@link Class}.
|
||||
* @since 6.0.11
|
||||
*/
|
||||
private static boolean isPrivateOrNotVisible(Method method, Class<?> beanClass) {
|
||||
int modifiers = method.getModifiers();
|
||||
if (Modifier.isPrivate(modifiers)) {
|
||||
return true;
|
||||
}
|
||||
// Method is declared in a class that resides in a different package
|
||||
// than the bean class and the method is neither public nor protected?
|
||||
return (!method.getDeclaringClass().getPackageName().equals(beanClass.getPackageName()) &&
|
||||
!(Modifier.isPublic(modifiers) || Modifier.isProtected(modifiers)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+29
-4
@@ -97,6 +97,18 @@ public class InjectionMetadata {
|
||||
return Collections.unmodifiableCollection(this.injectedElements);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the {@link InjectedElement elements} to inject based on the
|
||||
* specified {@link PropertyValues}. If a property is already defined
|
||||
* for an {@link InjectedElement}, it is excluded.
|
||||
* @param pvs the property values to consider
|
||||
* @return the elements to inject
|
||||
* @since 6.0.10
|
||||
*/
|
||||
public Collection<InjectedElement> getInjectedElements(@Nullable PropertyValues pvs) {
|
||||
return this.injectedElements.stream().filter(candidate -> candidate.shouldInject(pvs)).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether this metadata instance needs to be refreshed.
|
||||
* @param clazz the current target class
|
||||
@@ -104,7 +116,7 @@ public class InjectionMetadata {
|
||||
* @since 5.2.4
|
||||
*/
|
||||
protected boolean needsRefresh(Class<?> clazz) {
|
||||
return this.targetClass != clazz;
|
||||
return (this.targetClass != clazz);
|
||||
}
|
||||
|
||||
public void checkConfigMembers(RootBeanDefinition beanDefinition) {
|
||||
@@ -230,21 +242,34 @@ public class InjectionMetadata {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the property values should be injected.
|
||||
* @param pvs property values to check
|
||||
* @return whether the property values should be injected
|
||||
* @since 6.0.10
|
||||
*/
|
||||
protected boolean shouldInject(@Nullable PropertyValues pvs) {
|
||||
if (this.isField) {
|
||||
return true;
|
||||
}
|
||||
return !checkPropertySkipping(pvs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Either this or {@link #getResourceToInject} needs to be overridden.
|
||||
*/
|
||||
protected void inject(Object target, @Nullable String requestingBeanName, @Nullable PropertyValues pvs)
|
||||
throws Throwable {
|
||||
|
||||
if (!shouldInject(pvs)) {
|
||||
return;
|
||||
}
|
||||
if (this.isField) {
|
||||
Field field = (Field) this.member;
|
||||
ReflectionUtils.makeAccessible(field);
|
||||
field.set(target, getResourceToInject(target, requestingBeanName));
|
||||
}
|
||||
else {
|
||||
if (checkPropertySkipping(pvs)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Method method = (Method) this.member;
|
||||
ReflectionUtils.makeAccessible(method);
|
||||
|
||||
+45
-38
@@ -56,24 +56,23 @@ import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Internal code generator to set {@link RootBeanDefinition} properties.
|
||||
* <p>
|
||||
* Generates code in the following form:<blockquote><pre class="code">
|
||||
*
|
||||
* <p>Generates code in the following form:<pre class="code">
|
||||
* beanDefinition.setPrimary(true);
|
||||
* beanDefinition.setScope(BeanDefinition.SCOPE_PROTOTYPE);
|
||||
* ...
|
||||
* </pre></blockquote>
|
||||
* <p>
|
||||
* The generated code expects the following variables to be available:
|
||||
* <p>
|
||||
* </pre>
|
||||
*
|
||||
* <p>The generated code expects the following variables to be available:
|
||||
* <ul>
|
||||
* <li>{@code beanDefinition} - The {@link RootBeanDefinition} to
|
||||
* configure.</li>
|
||||
* <li>{@code beanDefinition}: the {@link RootBeanDefinition} to configure</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* Note that this generator does <b>not</b> set the {@link InstanceSupplier}.
|
||||
*
|
||||
* <p>Note that this generator does <b>not</b> set the {@link InstanceSupplier}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Stephane Nicoll
|
||||
* @author Sam Brannen
|
||||
* @since 6.0
|
||||
*/
|
||||
class BeanDefinitionPropertiesCodeGenerator {
|
||||
@@ -127,8 +126,8 @@ class BeanDefinitionPropertiesCodeGenerator {
|
||||
return code.build();
|
||||
}
|
||||
|
||||
private void addInitDestroyMethods(Builder code,
|
||||
AbstractBeanDefinition beanDefinition, @Nullable String[] methodNames, String format) {
|
||||
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));
|
||||
@@ -140,17 +139,33 @@ class BeanDefinitionPropertiesCodeGenerator {
|
||||
}
|
||||
|
||||
private void addInitDestroyHint(Class<?> beanUserClass, String methodName) {
|
||||
Method method = ReflectionUtils.findMethod(beanUserClass, methodName);
|
||||
Class<?> methodDeclaringClass = beanUserClass;
|
||||
|
||||
// Parse fully-qualified method name if necessary.
|
||||
int indexOfDot = methodName.lastIndexOf('.');
|
||||
if (indexOfDot > 0) {
|
||||
String className = methodName.substring(0, indexOfDot);
|
||||
methodName = methodName.substring(indexOfDot + 1);
|
||||
if (!beanUserClass.getName().equals(className)) {
|
||||
try {
|
||||
methodDeclaringClass = ClassUtils.forName(className, beanUserClass.getClassLoader());
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
throw new IllegalStateException("Failed to load Class [" + className +
|
||||
"] from ClassLoader [" + beanUserClass.getClassLoader() + "]", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Method method = ReflectionUtils.findMethod(methodDeclaringClass, methodName);
|
||||
if (method != null) {
|
||||
this.hints.reflection().registerMethod(method, ExecutableMode.INVOKE);
|
||||
}
|
||||
}
|
||||
|
||||
private void addConstructorArgumentValues(CodeBlock.Builder code,
|
||||
BeanDefinition beanDefinition) {
|
||||
|
||||
Map<Integer, ValueHolder> argumentValues = beanDefinition
|
||||
.getConstructorArgumentValues().getIndexedArgumentValues();
|
||||
private void addConstructorArgumentValues(CodeBlock.Builder code, BeanDefinition beanDefinition) {
|
||||
Map<Integer, ValueHolder> argumentValues =
|
||||
beanDefinition.getConstructorArgumentValues().getIndexedArgumentValues();
|
||||
if (!argumentValues.isEmpty()) {
|
||||
argumentValues.forEach((index, valueHolder) -> {
|
||||
CodeBlock valueCode = generateValue(valueHolder.getName(), valueHolder.getValue());
|
||||
@@ -161,9 +176,7 @@ class BeanDefinitionPropertiesCodeGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
private void addPropertyValues(CodeBlock.Builder code,
|
||||
RootBeanDefinition beanDefinition) {
|
||||
|
||||
private void addPropertyValues(CodeBlock.Builder code, RootBeanDefinition beanDefinition) {
|
||||
MutablePropertyValues propertyValues = beanDefinition.getPropertyValues();
|
||||
if (!propertyValues.isEmpty()) {
|
||||
for (PropertyValue propertyValue : propertyValues) {
|
||||
@@ -185,9 +198,7 @@ class BeanDefinitionPropertiesCodeGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
private void addQualifiers(CodeBlock.Builder code,
|
||||
RootBeanDefinition beanDefinition) {
|
||||
|
||||
private void addQualifiers(CodeBlock.Builder code, RootBeanDefinition beanDefinition) {
|
||||
Set<AutowireCandidateQualifier> qualifiers = beanDefinition.getQualifiers();
|
||||
if (!qualifiers.isEmpty()) {
|
||||
for (AutowireCandidateQualifier qualifier : qualifiers) {
|
||||
@@ -246,8 +257,8 @@ class BeanDefinitionPropertiesCodeGenerator {
|
||||
}
|
||||
|
||||
private boolean hasScope(String defaultValue, String actualValue) {
|
||||
return StringUtils.hasText(actualValue)
|
||||
&& !ConfigurableBeanFactory.SCOPE_SINGLETON.equals(actualValue);
|
||||
return StringUtils.hasText(actualValue) &&
|
||||
!ConfigurableBeanFactory.SCOPE_SINGLETON.equals(actualValue);
|
||||
}
|
||||
|
||||
private boolean hasDependsOn(String[] defaultValue, String[] actualValue) {
|
||||
@@ -259,16 +270,15 @@ class BeanDefinitionPropertiesCodeGenerator {
|
||||
}
|
||||
|
||||
private CodeBlock toStringVarArgs(String[] strings) {
|
||||
return Arrays.stream(strings).map(string -> CodeBlock.of("$S", string))
|
||||
.collect(CodeBlock.joining(","));
|
||||
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();
|
||||
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;
|
||||
};
|
||||
}
|
||||
@@ -278,16 +288,14 @@ class BeanDefinitionPropertiesCodeGenerator {
|
||||
Function<B, T> getter, String format) {
|
||||
|
||||
addStatementForValue(code, beanDefinition, getter,
|
||||
(defaultValue, actualValue) -> !Objects.equals(defaultValue, actualValue),
|
||||
format);
|
||||
(defaultValue, actualValue) -> !Objects.equals(defaultValue, actualValue), format);
|
||||
}
|
||||
|
||||
private <B extends BeanDefinition, T> void addStatementForValue(
|
||||
CodeBlock.Builder code, BeanDefinition beanDefinition,
|
||||
Function<B, T> getter, BiPredicate<T, T> filter, String format) {
|
||||
|
||||
addStatementForValue(code, beanDefinition, getter, filter, format,
|
||||
actualValue -> actualValue);
|
||||
addStatementForValue(code, beanDefinition, getter, filter, format, actualValue -> actualValue);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -299,8 +307,7 @@ class BeanDefinitionPropertiesCodeGenerator {
|
||||
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));
|
||||
code.addStatement(format, BEAN_DEFINITION_VARIABLE, formatter.apply(actualValue));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -172,7 +172,9 @@ public final class BeanInstanceSupplier<T> extends AutowiredElementResolver impl
|
||||
* {@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
|
||||
* @deprecated in favor of {@link #withGenerator(ThrowingFunction)}
|
||||
*/
|
||||
@Deprecated(since = "6.0.11", forRemoval = true)
|
||||
public BeanInstanceSupplier<T> withGenerator(ThrowingSupplier<T> generator) {
|
||||
Assert.notNull(generator, "'generator' must not be null");
|
||||
return new BeanInstanceSupplier<>(this.lookup,
|
||||
|
||||
+34
-13
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -18,6 +18,7 @@ package org.springframework.beans.factory.aot;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Executable;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
@@ -47,12 +48,6 @@ import org.springframework.util.ClassUtils;
|
||||
*/
|
||||
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;
|
||||
@@ -118,19 +113,45 @@ class DefaultBeanRegistrationCodeFragments implements BeanRegistrationCodeFragme
|
||||
ResolvableType beanType, BeanRegistrationCode beanRegistrationCode) {
|
||||
|
||||
CodeBlock.Builder code = CodeBlock.builder();
|
||||
code.addStatement(generateBeanTypeCode(beanType));
|
||||
RootBeanDefinition mergedBeanDefinition = this.registeredBean.getMergedBeanDefinition();
|
||||
Class<?> beanClass = (mergedBeanDefinition.hasBeanClass()
|
||||
? ClassUtils.getUserClass(mergedBeanDefinition.getBeanClass()) : null);
|
||||
CodeBlock beanClassCode = generateBeanClassCode(
|
||||
beanRegistrationCode.getClassName().packageName(), beanClass);
|
||||
code.addStatement("$T $L = new $T($L)", RootBeanDefinition.class,
|
||||
BEAN_DEFINITION_VARIABLE, RootBeanDefinition.class, BEAN_TYPE_VARIABLE);
|
||||
BEAN_DEFINITION_VARIABLE, RootBeanDefinition.class, beanClassCode);
|
||||
if (targetTypeNecessary(beanType, beanClass)) {
|
||||
code.addStatement("$L.setTargetType($L)", BEAN_DEFINITION_VARIABLE,
|
||||
generateBeanTypeCode(beanType));
|
||||
}
|
||||
return code.build();
|
||||
}
|
||||
|
||||
private CodeBlock generateBeanClassCode(String targetPackage, @Nullable Class<?> beanClass) {
|
||||
if (beanClass != null) {
|
||||
if (Modifier.isPublic(beanClass.getModifiers()) || targetPackage.equals(beanClass.getPackageName())) {
|
||||
return CodeBlock.of("$T.class", beanClass);
|
||||
}
|
||||
else {
|
||||
return CodeBlock.of("$S", beanClass.getName());
|
||||
}
|
||||
}
|
||||
return CodeBlock.of("");
|
||||
}
|
||||
|
||||
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.class", ClassUtils.getUserClass(beanType.toClass()));
|
||||
}
|
||||
return CodeBlock.of("$T $L = $L", ResolvableType.class, BEAN_TYPE_VARIABLE,
|
||||
ResolvableTypeCodeGenerator.generateCode(beanType));
|
||||
return ResolvableTypeCodeGenerator.generateCode(beanType);
|
||||
}
|
||||
|
||||
private boolean targetTypeNecessary(ResolvableType beanType, @Nullable Class<?> beanClass) {
|
||||
if (beanType.hasGenerics() || beanClass == null) {
|
||||
return true;
|
||||
}
|
||||
return (!beanType.toClass().equals(beanClass)
|
||||
|| this.registeredBean.getMergedBeanDefinition().getFactoryMethodName() != null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+2
-1
@@ -220,7 +220,8 @@ class InstanceSupplierCodeGenerator {
|
||||
CodeBlock.Builder code = CodeBlock.builder();
|
||||
code.add("$T.<$T>forFactoryMethod($T.class, $S)", BeanInstanceSupplier.class,
|
||||
suppliedType, declaringClass, factoryMethod.getName());
|
||||
code.add(".withGenerator($T::$L)", declaringClass, factoryMethod.getName());
|
||||
code.add(".withGenerator(($L) -> $T.$L())", REGISTERED_BEAN_PARAMETER_NAME,
|
||||
declaringClass, factoryMethod.getName());
|
||||
return code.build();
|
||||
}
|
||||
|
||||
|
||||
+17
-12
@@ -1794,10 +1794,11 @@ public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFac
|
||||
}
|
||||
|
||||
/**
|
||||
* Give a bean a chance to react now all its properties are set,
|
||||
* Give a bean a chance to initialize itself after all its properties are set,
|
||||
* and a chance to know about its owning bean factory (this object).
|
||||
* This means checking whether the bean implements InitializingBean or defines
|
||||
* a custom init method, and invoking the necessary callback(s) if it does.
|
||||
* <p>This means checking whether the bean implements {@link InitializingBean}
|
||||
* or defines any custom init methods, and invoking the necessary callback(s)
|
||||
* if it does.
|
||||
* @param beanName the bean name in the factory (for debugging purposes)
|
||||
* @param bean the new bean instance we may need to initialize
|
||||
* @param mbd the merged bean definition that the bean was created with
|
||||
@@ -1832,26 +1833,30 @@ public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFac
|
||||
|
||||
/**
|
||||
* Invoke the specified custom init method on the given bean.
|
||||
* Called by invokeInitMethods.
|
||||
* <p>Can be overridden in subclasses for custom resolution of init
|
||||
* methods with arguments.
|
||||
* <p>Called by {@link #invokeInitMethods(String, Object, RootBeanDefinition)}.
|
||||
* <p>Can be overridden in subclasses for custom resolution of init methods
|
||||
* with arguments.
|
||||
* @see #invokeInitMethods
|
||||
*/
|
||||
protected void invokeCustomInitMethod(String beanName, Object bean, RootBeanDefinition mbd, String initMethodName)
|
||||
throws Throwable {
|
||||
|
||||
Class<?> beanClass = bean.getClass();
|
||||
MethodDescriptor descriptor = MethodDescriptor.create(beanName, beanClass, initMethodName);
|
||||
String methodName = descriptor.methodName();
|
||||
|
||||
Method initMethod = (mbd.isNonPublicAccessAllowed() ?
|
||||
BeanUtils.findMethod(bean.getClass(), initMethodName) :
|
||||
ClassUtils.getMethodIfAvailable(bean.getClass(), initMethodName));
|
||||
BeanUtils.findMethod(descriptor.declaringClass(), methodName) :
|
||||
ClassUtils.getMethodIfAvailable(beanClass, methodName));
|
||||
|
||||
if (initMethod == null) {
|
||||
if (mbd.isEnforceInitMethod()) {
|
||||
throw new BeanDefinitionValidationException("Could not find an init method named '" +
|
||||
initMethodName + "' on bean with name '" + beanName + "'");
|
||||
methodName + "' on bean with name '" + beanName + "'");
|
||||
}
|
||||
else {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("No default init method named '" + initMethodName +
|
||||
logger.trace("No default init method named '" + methodName +
|
||||
"' found on bean with name '" + beanName + "'");
|
||||
}
|
||||
// Ignore non-existent default lifecycle methods.
|
||||
@@ -1860,9 +1865,9 @@ public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFac
|
||||
}
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Invoking init method '" + initMethodName + "' on bean with name '" + beanName + "'");
|
||||
logger.trace("Invoking init method '" + methodName + "' on bean with name '" + beanName + "'");
|
||||
}
|
||||
Method methodToInvoke = ClassUtils.getInterfaceMethodIfPossible(initMethod, bean.getClass());
|
||||
Method methodToInvoke = ClassUtils.getInterfaceMethodIfPossible(initMethod, beanClass);
|
||||
|
||||
try {
|
||||
ReflectionUtils.makeAccessible(methodToInvoke);
|
||||
|
||||
+3
-2
@@ -390,7 +390,8 @@ public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccess
|
||||
@Override
|
||||
@Nullable
|
||||
public String getBeanClassName() {
|
||||
return (this.beanClass instanceof Class<?> clazz ? clazz.getName() : (String) this.beanClass);
|
||||
Object beanClassObject = this.beanClass; // defensive access to volatile beanClass field
|
||||
return (beanClassObject instanceof Class<?> clazz ? clazz.getName() : (String) beanClassObject);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -423,7 +424,7 @@ public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccess
|
||||
* @see #resolveBeanClass(ClassLoader)
|
||||
*/
|
||||
public Class<?> getBeanClass() throws IllegalStateException {
|
||||
Object beanClassObject = this.beanClass;
|
||||
Object beanClassObject = this.beanClass; // defensive access to volatile beanClass field
|
||||
if (beanClassObject == null) {
|
||||
throw new IllegalStateException("No bean class specified on bean definition");
|
||||
}
|
||||
|
||||
+3
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -125,7 +125,8 @@ public final class BeanDefinitionBuilder {
|
||||
* @since 5.3.9
|
||||
*/
|
||||
public static <T> BeanDefinitionBuilder rootBeanDefinition(ResolvableType beanType, Supplier<T> instanceSupplier) {
|
||||
RootBeanDefinition beanDefinition = new RootBeanDefinition(beanType);
|
||||
RootBeanDefinition beanDefinition = new RootBeanDefinition();
|
||||
beanDefinition.setTargetType(beanType);
|
||||
beanDefinition.setInstanceSupplier(instanceSupplier);
|
||||
return new BeanDefinitionBuilder(beanDefinition);
|
||||
}
|
||||
|
||||
+1
-1
@@ -979,7 +979,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
||||
for (String beanName : beanNames) {
|
||||
Object singletonInstance = getSingleton(beanName);
|
||||
if (singletonInstance instanceof SmartInitializingSingleton smartSingleton) {
|
||||
StartupStep smartInitialize = this.getApplicationStartup().start("spring.beans.smart-initialize")
|
||||
StartupStep smartInitialize = getApplicationStartup().start("spring.beans.smart-initialize")
|
||||
.tag("beanName", beanName);
|
||||
smartSingleton.afterSingletonsInstantiated();
|
||||
smartInitialize.end();
|
||||
|
||||
+7
-7
@@ -102,7 +102,7 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
|
||||
private boolean singletonsCurrentlyInDestruction = false;
|
||||
|
||||
/** Disposable bean instances: bean name to disposable instance. */
|
||||
private final Map<String, Object> disposableBeans = new LinkedHashMap<>();
|
||||
private final Map<String, DisposableBean> disposableBeans = new LinkedHashMap<>();
|
||||
|
||||
/** Map between containing bean names: bean name to Set of bean names that the bean contains. */
|
||||
private final Map<String, Set<String>> containedBeanMap = new ConcurrentHashMap<>(16);
|
||||
@@ -447,17 +447,17 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
|
||||
}
|
||||
String canonicalName = canonicalName(beanName);
|
||||
Set<String> dependentBeans = this.dependentBeanMap.get(canonicalName);
|
||||
if (dependentBeans == null) {
|
||||
if (dependentBeans == null || dependentBeans.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
if (dependentBeans.contains(dependentBeanName)) {
|
||||
return true;
|
||||
}
|
||||
if (alreadySeen == null) {
|
||||
alreadySeen = new HashSet<>();
|
||||
}
|
||||
alreadySeen.add(beanName);
|
||||
for (String transitiveDependency : dependentBeans) {
|
||||
if (alreadySeen == null) {
|
||||
alreadySeen = new HashSet<>();
|
||||
}
|
||||
alreadySeen.add(beanName);
|
||||
if (isDependent(transitiveDependency, dependentBeanName, alreadySeen)) {
|
||||
return true;
|
||||
}
|
||||
@@ -554,7 +554,7 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
|
||||
// Destroy the corresponding DisposableBean instance.
|
||||
DisposableBean disposableBean;
|
||||
synchronized (this.disposableBeans) {
|
||||
disposableBean = (DisposableBean) this.disposableBeans.remove(beanName);
|
||||
disposableBean = this.disposableBeans.remove(beanName);
|
||||
}
|
||||
destroyBean(beanName, disposableBean);
|
||||
}
|
||||
|
||||
+10
-8
@@ -65,8 +65,8 @@ class DisposableBeanAdapter implements DisposableBean, Runnable, Serializable {
|
||||
|
||||
private static final String SHUTDOWN_METHOD_NAME = "shutdown";
|
||||
|
||||
private static final Log logger = LogFactory.getLog(DisposableBeanAdapter.class);
|
||||
|
||||
private static final Log logger = LogFactory.getLog(DisposableBeanAdapter.class);
|
||||
|
||||
private final Object bean;
|
||||
|
||||
@@ -116,8 +116,7 @@ class DisposableBeanAdapter implements DisposableBean, Runnable, Serializable {
|
||||
if (!this.invokeAutoCloseable) {
|
||||
this.destroyMethodNames = destroyMethodNames;
|
||||
List<Method> destroyMethods = new ArrayList<>(destroyMethodNames.length);
|
||||
for (int i = 0; i < destroyMethodNames.length; i++) {
|
||||
String destroyMethodName = destroyMethodNames[i];
|
||||
for (String destroyMethodName : destroyMethodNames) {
|
||||
Method destroyMethod = determineDestroyMethod(destroyMethodName);
|
||||
if (destroyMethod == null) {
|
||||
if (beanDefinition.isEnforceDestroyMethod()) {
|
||||
@@ -240,7 +239,7 @@ class DisposableBeanAdapter implements DisposableBean, Runnable, Serializable {
|
||||
}
|
||||
}
|
||||
else if (this.destroyMethodNames != null) {
|
||||
for (String destroyMethodName: this.destroyMethodNames) {
|
||||
for (String destroyMethodName : this.destroyMethodNames) {
|
||||
Method destroyMethod = determineDestroyMethod(destroyMethodName);
|
||||
if (destroyMethod != null) {
|
||||
invokeCustomDestroyMethod(
|
||||
@@ -252,15 +251,18 @@ class DisposableBeanAdapter implements DisposableBean, Runnable, Serializable {
|
||||
|
||||
|
||||
@Nullable
|
||||
private Method determineDestroyMethod(String name) {
|
||||
private Method determineDestroyMethod(String destroyMethodName) {
|
||||
try {
|
||||
Class<?> beanClass = this.bean.getClass();
|
||||
Method destroyMethod = findDestroyMethod(beanClass, name);
|
||||
MethodDescriptor descriptor = MethodDescriptor.create(this.beanName, beanClass, destroyMethodName);
|
||||
String methodName = descriptor.methodName();
|
||||
|
||||
Method destroyMethod = findDestroyMethod(descriptor.declaringClass(), methodName);
|
||||
if (destroyMethod != null) {
|
||||
return destroyMethod;
|
||||
}
|
||||
for (Class<?> beanInterface : beanClass.getInterfaces()) {
|
||||
destroyMethod = findDestroyMethod(beanInterface, name);
|
||||
destroyMethod = findDestroyMethod(beanInterface, methodName);
|
||||
if (destroyMethod != null) {
|
||||
return destroyMethod;
|
||||
}
|
||||
@@ -288,7 +290,7 @@ class DisposableBeanAdapter implements DisposableBean, Runnable, Serializable {
|
||||
*/
|
||||
private void invokeCustomDestroyMethod(Method destroyMethod) {
|
||||
int paramCount = destroyMethod.getParameterCount();
|
||||
final Object[] args = new Object[paramCount];
|
||||
Object[] args = new Object[paramCount];
|
||||
if (paramCount == 1) {
|
||||
args[0] = Boolean.TRUE;
|
||||
}
|
||||
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.beans.factory.support;
|
||||
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Descriptor for a {@link java.lang.reflect.Method Method} which holds a
|
||||
* reference to the method's {@linkplain #declaringClass declaring class},
|
||||
* {@linkplain #methodName name}, and {@linkplain #parameterTypes parameter types}.
|
||||
*
|
||||
* @param declaringClass the method's declaring class
|
||||
* @param methodName the name of the method
|
||||
* @param parameterTypes the types of parameters accepted by the method
|
||||
* @author Sam Brannen
|
||||
* @since 6.0.11
|
||||
*/
|
||||
record MethodDescriptor(Class<?> declaringClass, String methodName, Class<?>... parameterTypes) {
|
||||
|
||||
/**
|
||||
* Create a {@link MethodDescriptor} for the supplied bean class and method name.
|
||||
* <p>The supplied {@code methodName} may be a {@linkplain Method#getName()
|
||||
* simple method name} or a
|
||||
* {@linkplain org.springframework.util.ClassUtils#getQualifiedMethodName(Method)
|
||||
* qualified method name}.
|
||||
* <p>If the method name is fully qualified, this utility will parse the
|
||||
* method name and its declaring class from the qualified method name and then
|
||||
* attempt to load the method's declaring class using the {@link ClassLoader}
|
||||
* of the supplied {@code beanClass}. Otherwise, the returned descriptor will
|
||||
* reference the supplied {@code beanClass} and {@code methodName}.
|
||||
* @param beanName the bean name in the factory (for debugging purposes)
|
||||
* @param beanClass the bean class
|
||||
* @param methodName the name of the method
|
||||
* @return a new {@code MethodDescriptor}; never {@code null}
|
||||
*/
|
||||
static MethodDescriptor create(String beanName, Class<?> beanClass, String methodName) {
|
||||
try {
|
||||
Class<?> declaringClass = beanClass;
|
||||
String methodNameToUse = methodName;
|
||||
|
||||
// Parse fully-qualified method name if necessary.
|
||||
int indexOfDot = methodName.lastIndexOf('.');
|
||||
if (indexOfDot > 0) {
|
||||
String className = methodName.substring(0, indexOfDot);
|
||||
methodNameToUse = methodName.substring(indexOfDot + 1);
|
||||
if (!beanClass.getName().equals(className)) {
|
||||
declaringClass = ClassUtils.forName(className, beanClass.getClassLoader());
|
||||
}
|
||||
}
|
||||
return new MethodDescriptor(declaringClass, methodNameToUse);
|
||||
}
|
||||
catch (Exception | LinkageError ex) {
|
||||
throw new BeanDefinitionValidationException(
|
||||
"Could not create MethodDescriptor for method '%s' on bean with name '%s': %s"
|
||||
.formatted(methodName, beanName, ex.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+9
@@ -212,6 +212,15 @@ public final class RegisteredBean {
|
||||
.resolveConstructorOrFactoryMethod(getBeanName(), getMergedBeanDefinition());
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an autowired argument.
|
||||
* @param descriptor the descriptor for the dependency (field/method/constructor)
|
||||
* @param typeConverter the TypeConverter to use for populating arrays and collections
|
||||
* @param autowiredBeans a Set that all names of autowired beans (used for
|
||||
* resolving the given dependency) are supposed to be added to
|
||||
* @return the resolved object, or {@code null} if none found
|
||||
* @since 6.0.9
|
||||
*/
|
||||
@Nullable
|
||||
public Object resolveAutowiredArgument(DependencyDescriptor descriptor, TypeConverter typeConverter,
|
||||
Set<String> autowiredBeans) {
|
||||
|
||||
+25
-29
@@ -161,7 +161,9 @@ public class RootBeanDefinition extends AbstractBeanDefinition {
|
||||
* @param beanType the type of bean to instantiate
|
||||
* @since 6.0
|
||||
* @see #setTargetType(ResolvableType)
|
||||
* @deprecated as of 6.0.11, in favor of an extra {@link #setTargetType(ResolvableType)} call
|
||||
*/
|
||||
@Deprecated(since = "6.0.11")
|
||||
public RootBeanDefinition(@Nullable ResolvableType beanType) {
|
||||
setTargetType(beanType);
|
||||
}
|
||||
@@ -495,14 +497,15 @@ public class RootBeanDefinition extends AbstractBeanDefinition {
|
||||
|
||||
/**
|
||||
* Register an externally managed configuration initialization method —
|
||||
* for example, a method annotated with JSR-250's
|
||||
* {@link jakarta.annotation.PostConstruct} annotation.
|
||||
* <p>The supplied {@code initMethod} may be the
|
||||
* {@linkplain Method#getName() simple method name} for non-private methods or the
|
||||
* for example, a method annotated with JSR-250's {@code javax.annotation.PostConstruct}
|
||||
* or Jakarta's {@link jakarta.annotation.PostConstruct} annotation.
|
||||
* <p>The supplied {@code initMethod} may be a
|
||||
* {@linkplain Method#getName() simple method name} or a
|
||||
* {@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.
|
||||
* qualified method name} for package-private and {@code private} methods.
|
||||
* A qualified name is necessary for package-private and {@code private} methods
|
||||
* in order to disambiguate between multiple such methods with the same name
|
||||
* within a type hierarchy.
|
||||
*/
|
||||
public void registerExternallyManagedInitMethod(String initMethod) {
|
||||
synchronized (this.postProcessingLock) {
|
||||
@@ -541,23 +544,12 @@ public class RootBeanDefinition extends AbstractBeanDefinition {
|
||||
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 hasAnyExternallyManagedMethod(this.externallyManagedInitMethods, initMethod);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all externally managed initialization methods (as an immutable Set).
|
||||
* Get all externally managed initialization methods (as an immutable Set).
|
||||
* <p>See {@link #registerExternallyManagedInitMethod} for details
|
||||
* regarding the format for the initialization methods in the returned set.
|
||||
* @since 5.3.11
|
||||
@@ -627,19 +619,23 @@ public class RootBeanDefinition extends AbstractBeanDefinition {
|
||||
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 hasAnyExternallyManagedMethod(this.externallyManagedDestroyMethods, destroyMethod);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean hasAnyExternallyManagedMethod(Set<String> candidates, String methodName) {
|
||||
if (candidates != null) {
|
||||
for (String candidate : candidates) {
|
||||
int indexOfDot = candidate.lastIndexOf('.');
|
||||
if (indexOfDot > 0) {
|
||||
String candidateMethodName = candidate.substring(indexOfDot + 1);
|
||||
if (candidateMethodName.equals(methodName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
org.springframework.aot.hint.RuntimeHintsRegistrar=\
|
||||
org.springframework.beans.factory.annotation.JakartaAnnotationsRuntimeHints
|
||||
org.springframework.beans.factory.annotation.JakartaAnnotationsRuntimeHints,\
|
||||
org.springframework.beans.BeanUtilsRuntimeHints
|
||||
|
||||
org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor=\
|
||||
org.springframework.beans.factory.aot.BeanRegistrationsAotProcessor
|
||||
|
||||
+6
-6
@@ -667,22 +667,22 @@ abstract class AbstractPropertyAccessorTests {
|
||||
accessor.setPropertyValue("myDouble", doubleValue);
|
||||
|
||||
assertThat(target.getMyPrimitiveByte()).isEqualTo(Byte.MAX_VALUE);
|
||||
assertThat(target.getMyByte().byteValue()).isEqualTo(Byte.MAX_VALUE);
|
||||
assertThat(target.getMyByte()).isEqualTo(Byte.MAX_VALUE);
|
||||
|
||||
assertThat(target.getMyPrimitiveShort()).isEqualTo(Short.MAX_VALUE);
|
||||
assertThat(target.getMyShort().shortValue()).isEqualTo(Short.MAX_VALUE);
|
||||
assertThat(target.getMyShort()).isEqualTo(Short.MAX_VALUE);
|
||||
|
||||
assertThat(target.getMyPrimitiveInt()).isEqualTo(Integer.MAX_VALUE);
|
||||
assertThat(target.getMyInteger().intValue()).isEqualTo(Integer.MAX_VALUE);
|
||||
assertThat(target.getMyInteger()).isEqualTo(Integer.MAX_VALUE);
|
||||
|
||||
assertThat(target.getMyPrimitiveLong()).isEqualTo(Long.MAX_VALUE);
|
||||
assertThat(target.getMyLong().longValue()).isEqualTo(Long.MAX_VALUE);
|
||||
assertThat(target.getMyLong()).isEqualTo(Long.MAX_VALUE);
|
||||
|
||||
assertThat((double) target.getMyPrimitiveFloat()).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));
|
||||
assertThat(target.getMyDouble()).isCloseTo(Double.MAX_VALUE, within(0.001));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1994,6 +1994,7 @@ abstract class AbstractPropertyAccessorTests {
|
||||
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static class Spr13837Bean implements Spr13837 {
|
||||
|
||||
protected Integer something;
|
||||
@@ -2004,7 +2005,6 @@ abstract class AbstractPropertyAccessorTests {
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Spr13837Bean setSomething(final Integer something) {
|
||||
this.something = something;
|
||||
return this;
|
||||
|
||||
+2
-4
@@ -35,8 +35,7 @@ public abstract class AbstractPropertyValuesTests {
|
||||
assertThat(pvs.contains("forname")).as("Contains forname").isTrue();
|
||||
assertThat(pvs.contains("surname")).as("Contains surname").isTrue();
|
||||
assertThat(pvs.contains("age")).as("Contains age").isTrue();
|
||||
boolean condition1 = !pvs.contains("tory");
|
||||
assertThat(condition1).as("Doesn't contain tory").isTrue();
|
||||
assertThat(!pvs.contains("tory")).as("Doesn't contain tory").isTrue();
|
||||
|
||||
PropertyValue[] ps = pvs.getPropertyValues();
|
||||
Map<String, String> m = new HashMap<>();
|
||||
@@ -46,8 +45,7 @@ public abstract class AbstractPropertyValuesTests {
|
||||
for (PropertyValue element : ps) {
|
||||
Object val = m.get(element.getName());
|
||||
assertThat(val).as("Can't have unexpected value").isNotNull();
|
||||
boolean condition = val instanceof String;
|
||||
assertThat(condition).as("Val i string").isTrue();
|
||||
assertThat(val instanceof String).as("Val i string").isTrue();
|
||||
assertThat(val.equals(element.getValue())).as("val matches expected").isTrue();
|
||||
m.remove(element.getName());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.beans;
|
||||
|
||||
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.predicate.RuntimeHintsPredicates;
|
||||
import org.springframework.core.io.ResourceEditor;
|
||||
import org.springframework.core.io.support.SpringFactoriesLoader;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link BeanUtilsRuntimeHints}.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @since 6.0.10
|
||||
* @see org.springframework.http.WebBeanUtilsRuntimeHintsTests
|
||||
*/
|
||||
class BeanUtilsRuntimeHintsTests {
|
||||
|
||||
private final RuntimeHints hints = new RuntimeHints();
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
SpringFactoriesLoader.forResourceLocation("META-INF/spring/aot.factories")
|
||||
.load(RuntimeHintsRegistrar.class)
|
||||
.forEach(registrar -> registrar.registerHints(this.hints, ClassUtils.getDefaultClassLoader()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void resourceEditorHasHints() {
|
||||
assertThat(RuntimeHintsPredicates.reflection().onType(ResourceEditor.class)
|
||||
.withMemberCategories(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)).accepts(this.hints);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -335,7 +335,7 @@ class BeanWrapperGenericsTests {
|
||||
|
||||
Object obj = gb.getMapOfListOfInteger().get("testKey").get(0);
|
||||
assertThat(obj).isInstanceOf(Integer.class);
|
||||
assertThat(((Integer) obj).intValue()).isEqualTo(1);
|
||||
assertThat(obj).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -351,7 +351,7 @@ class BeanWrapperGenericsTests {
|
||||
|
||||
Object obj = gb.getListOfMapOfInteger().get(0).get("testKey");
|
||||
assertThat(obj).isInstanceOf(Integer.class);
|
||||
assertThat(((Integer) obj).intValue()).isEqualTo(5);
|
||||
assertThat(obj).isEqualTo(5);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -366,7 +366,7 @@ class BeanWrapperGenericsTests {
|
||||
|
||||
Object obj = gb.getMapOfListOfListOfInteger().get("testKey").get(0).get(0);
|
||||
assertThat(obj).isInstanceOf(Integer.class);
|
||||
assertThat(((Integer) obj).intValue()).isEqualTo(1);
|
||||
assertThat(obj).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+5
-10
@@ -155,8 +155,7 @@ public class BeanFactoryUtilsTests {
|
||||
assertThat(beans.get("t1")).isEqualTo(t1);
|
||||
assertThat(beans.get("t2")).isEqualTo(t2);
|
||||
assertThat(beans.get("t3")).isEqualTo(t3.getObject());
|
||||
boolean condition = beans.get("t4") instanceof TestBean;
|
||||
assertThat(condition).isTrue();
|
||||
assertThat(beans.get("t4") instanceof TestBean).isTrue();
|
||||
|
||||
beans = BeanFactoryUtils.beansOfTypeIncludingAncestors(lbf, DummyFactory.class, true, true);
|
||||
assertThat(beans).hasSize(2);
|
||||
@@ -192,8 +191,7 @@ public class BeanFactoryUtilsTests {
|
||||
assertThat(beans.get("t1")).isEqualTo(t1);
|
||||
assertThat(beans.get("t2")).isEqualTo(t2);
|
||||
assertThat(beans.get("t3")).isEqualTo(t3.getObject());
|
||||
boolean condition2 = beans.get("t4") instanceof TestBean;
|
||||
assertThat(condition2).isTrue();
|
||||
assertThat(beans.get("t4") instanceof TestBean).isTrue();
|
||||
// t3 and t4 are found here as of Spring 2.0, since they are pre-registered
|
||||
// singleton instances, while testFactory1 and testFactory are *not* found
|
||||
// because they are FactoryBean definitions that haven't been initialized yet.
|
||||
@@ -212,13 +210,11 @@ public class BeanFactoryUtilsTests {
|
||||
assertThat(beans.get("test3")).isEqualTo(test3);
|
||||
assertThat(beans.get("test")).isEqualTo(test);
|
||||
assertThat(beans.get("testFactory1")).isEqualTo(testFactory1);
|
||||
boolean condition1 = beans.get("testFactory2") instanceof TestBean;
|
||||
assertThat(condition1).isTrue();
|
||||
assertThat(beans.get("testFactory2") instanceof TestBean).isTrue();
|
||||
assertThat(beans.get("t1")).isEqualTo(t1);
|
||||
assertThat(beans.get("t2")).isEqualTo(t2);
|
||||
assertThat(beans.get("t3")).isEqualTo(t3.getObject());
|
||||
boolean condition = beans.get("t4") instanceof TestBean;
|
||||
assertThat(condition).isTrue();
|
||||
assertThat(beans.get("t4") instanceof TestBean).isTrue();
|
||||
|
||||
beans = BeanFactoryUtils.beansOfTypeIncludingAncestors(this.listableBeanFactory, DummyFactory.class, true, true);
|
||||
assertThat(beans).hasSize(4);
|
||||
@@ -261,8 +257,7 @@ public class BeanFactoryUtilsTests {
|
||||
assertThat(beans.get("test3")).isEqualTo(test3);
|
||||
assertThat(beans.get("test")).isEqualTo(test);
|
||||
assertThat(beans.get("testFactory1")).isEqualTo(testFactory1);
|
||||
boolean condition = beans.get("testFactory2") instanceof TestBean;
|
||||
assertThat(condition).isTrue();
|
||||
assertThat(beans.get("testFactory2") instanceof TestBean).isTrue();
|
||||
|
||||
beans = BeanFactoryUtils.beansOfTypeIncludingAncestors(this.listableBeanFactory, DummyFactory.class, true, true);
|
||||
assertThat(beans).hasSize(2);
|
||||
|
||||
+3
-5
@@ -3190,6 +3190,7 @@ class DefaultListableBeanFactoryTests {
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
private static class CustomTypeConverter implements TypeConverter {
|
||||
|
||||
private final NumberFormat numberFormat;
|
||||
@@ -3199,11 +3200,10 @@ class DefaultListableBeanFactoryTests {
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
public Object convertIfNecessary(Object value, @Nullable Class requiredType) {
|
||||
if (value instanceof String && Float.class.isAssignableFrom(requiredType)) {
|
||||
if (value instanceof String text && Float.class.isAssignableFrom(requiredType)) {
|
||||
try {
|
||||
return this.numberFormat.parse((String) value).floatValue();
|
||||
return this.numberFormat.parse(text).floatValue();
|
||||
}
|
||||
catch (ParseException ex) {
|
||||
throw new TypeMismatchException(value, requiredType, ex);
|
||||
@@ -3218,13 +3218,11 @@ class DefaultListableBeanFactoryTests {
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
public Object convertIfNecessary(Object value, @Nullable Class requiredType, @Nullable MethodParameter methodParam) {
|
||||
return convertIfNecessary(value, requiredType);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
public Object convertIfNecessary(Object value, @Nullable Class requiredType, @Nullable Field field) {
|
||||
return convertIfNecessary(value, requiredType);
|
||||
}
|
||||
|
||||
+237
-144
File diff suppressed because it is too large
Load Diff
+28
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -65,11 +65,15 @@ class AutowiredAnnotationBeanRegistrationAotContributionTests {
|
||||
|
||||
private final DefaultListableBeanFactory beanFactory;
|
||||
|
||||
private final AutowiredAnnotationBeanPostProcessor beanPostProcessor;
|
||||
|
||||
|
||||
AutowiredAnnotationBeanRegistrationAotContributionTests() {
|
||||
this.generationContext = new TestGenerationContext();
|
||||
this.beanRegistrationCode = new MockBeanRegistrationCode(this.generationContext);
|
||||
this.beanFactory = new DefaultListableBeanFactory();
|
||||
this.beanPostProcessor = new AutowiredAnnotationBeanPostProcessor();
|
||||
this.beanPostProcessor.setBeanFactory(this.beanFactory);
|
||||
}
|
||||
|
||||
|
||||
@@ -185,10 +189,19 @@ class AutowiredAnnotationBeanRegistrationAotContributionTests {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void contributeWhenMethodInjectionHasMatchingPropertyValue() {
|
||||
RootBeanDefinition beanDefinition = new RootBeanDefinition(InjectionBean.class);
|
||||
beanDefinition.getPropertyValues().addPropertyValue("counter", 42);
|
||||
this.beanFactory.registerBeanDefinition("test", beanDefinition);
|
||||
BeanRegistrationAotContribution contribution = this.beanPostProcessor
|
||||
.processAheadOfTime(RegisteredBean.of(this.beanFactory, "test"));
|
||||
assertThat(contribution).isNull();
|
||||
}
|
||||
|
||||
private RegisteredBean getAndApplyContribution(Class<?> beanClass) {
|
||||
RegisteredBean registeredBean = registerBean(beanClass);
|
||||
BeanRegistrationAotContribution contribution = new AutowiredAnnotationBeanPostProcessor()
|
||||
.processAheadOfTime(registeredBean);
|
||||
BeanRegistrationAotContribution contribution = this.beanPostProcessor.processAheadOfTime(registeredBean);
|
||||
assertThat(contribution).isNotNull();
|
||||
contribution.applyTo(this.generationContext, this.beanRegistrationCode);
|
||||
return registeredBean;
|
||||
@@ -229,4 +242,16 @@ class AutowiredAnnotationBeanRegistrationAotContributionTests {
|
||||
result.accept(compiled.getInstance(BiFunction.class), compiled));
|
||||
}
|
||||
|
||||
static class InjectionBean {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private Integer counter;
|
||||
|
||||
@Autowired
|
||||
public void setCounter(Integer counter) {
|
||||
this.counter = counter;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user