Skip to content

Commit

Permalink
Merge branch 'spring-projects:main' into LogFactory_setAttribute
Browse files Browse the repository at this point in the history
  • Loading branch information
maartenc committed Jun 20, 2023
2 parents a8bbf0e + 089d938 commit cc47252
Show file tree
Hide file tree
Showing 90 changed files with 1,958 additions and 333 deletions.
3 changes: 2 additions & 1 deletion ci/scripts/stage-version.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ git add gradle.properties > /dev/null
git commit -m"Release v$stageVersion" > /dev/null
git tag -a "v$stageVersion" -m"Release v$stageVersion" > /dev/null

./gradlew --no-daemon --max-workers=4 -PdeploymentRepository=${repository} build publishAllPublicationsToDeploymentRepository
./gradlew --no-daemon --max-workers=4 -PdeploymentRepository=${repository} -Porg.gradle.java.installations.fromEnv=JDK17,JDK21 \
build publishAllPublicationsToDeploymentRepository

git reset --hard HEAD^ > /dev/null
if [[ $nextVersion != $snapshotVersion ]]; then
Expand Down
32 changes: 30 additions & 2 deletions framework-docs/modules/ROOT/pages/integration/observability.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,14 @@ As outlined xref:integration/observability.adoc[at the beginning of this section
|===
|Observation name |Description

|xref:integration/observability.adoc#http-client[`"http.client.requests"`]
|xref:integration/observability.adoc#observability.http-client[`"http.client.requests"`]
|Time spent for HTTP client exchanges

|xref:integration/observability.adoc#http-server[`"http.server.requests"`]
|xref:integration/observability.adoc#observability.http-server[`"http.server.requests"`]
|Processing time for HTTP server exchanges at the Framework level

|xref:integration/observability.adoc#observability.tasks-scheduled[`"tasks.scheduled.execution"`]
|Processing time for an execution of a `@Scheduled` task
|===

NOTE: Observations are using Micrometer's official naming convention, but Metrics names will be automatically converted
Expand Down Expand Up @@ -79,6 +82,31 @@ include-code::./ServerRequestObservationFilter[]

You can configure `ObservationFilter` instances on the `ObservationRegistry`.

[[observability.tasks-scheduled]]
== @Scheduled tasks instrumentation

An Observation is created for xref:integration/scheduling.adoc#scheduling-enable-annotation-support[each execution of an `@Scheduled` task].
Applications need to configure the `ObservationRegistry` on the `ScheduledTaskRegistrar` to enable the recording of observations.
This can be done by declaring a `SchedulingConfigurer` bean that sets the observation registry:

include-code::./ObservationSchedulingConfigurer[]

It is using the `org.springframework.scheduling.config.DefaultScheduledTaskObservationConvention` by default, backed by the `ScheduledTaskObservationContext`.
You can configure a custom implementation on the `ObservationRegistry` directly.
During the execution of the scheduled method, the current observation is restored in the `ThreadLocal` context or the Reactor context (if the scheduled method returns a `Mono` or `Flux` type).

By default, the following `KeyValues` are created:

.Low cardinality Keys
[cols="a,a"]
|===
|Name | Description
|`exception` _(required)_|Name of the exception thrown during the execution, or `KeyValue#NONE_VALUE`} if no exception happened.
|`method.name` _(required)_|Name of Java `Method` that is scheduled for execution.
|`outcome` _(required)_|Outcome of the method execution. Can be `"SUCCESS"`, `"ERROR"` or `"UNKNOWN"` (if for example the operation was cancelled during execution.
|`target.type` _(required)_|Simple class name of the bean instance that holds the scheduled method.
|===


[[observability.http-server]]
== HTTP Server instrumentation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,20 +71,21 @@ from an existing `ProblemDetail`. This could be done centrally, e.g. from an
[.small]#xref:web/webmvc/mvc-ann-rest-exceptions.adoc#mvc-ann-rest-exceptions-i18n[See equivalent in the Servlet stack]#

It is a common requirement to internationalize error response details, and good practice
to customize the problem details for Spring WebFlux exceptions. This is supported as follows:
to customize the problem details for Spring WebFlux exceptions. This section describes the
support for that.

- Each `ErrorResponse` exposes a message code and arguments to resolve the "detail" field
through a xref:core/beans/context-introduction.adoc#context-functionality-messagesource[MessageSource].
The actual message code value is parameterized with placeholders, e.g.
`+"HTTP method {0} not supported"+` to be expanded from the arguments.
- Each `ErrorResponse` also exposes a message code to resolve the "title" field.
- `ResponseEntityExceptionHandler` uses the message code and arguments to resolve the
"detail" and the "title" fields.
`ErrorResponse` exposes message codes for "type", "title", and "detail", in addition to
message code arguments for the "detail" field. `ResponseEntityExceptionHandler` resolves
these through a xref:core/beans/context-introduction.adoc#context-functionality-messagesource[MessageSource]
and updates the `ProblemDetail` accordingly.

By default, the message code for the "detail" field is "problemDetail." + the fully
qualified exception class name. Some exceptions may expose additional message codes in
which case a suffix is added to the default message code. The table below lists message
arguments and codes for Spring WebFlux exceptions:
The default strategy for message codes follows the pattern:

`problemDetail.[type|title|detail].[fully qualified exception class name]`

Some `ErrorResponse` may expose more than one message code, typically adding a suffix
to the default message code. The table below lists message codes, and arguments for
Spring WebFlux exceptions:

[[webflux-ann-rest-exceptions-codes]]
[cols="1,1,2", options="header"]
Expand Down Expand Up @@ -131,9 +132,6 @@ via `MessageSource`.

|===

By default, the message code for the "title" field is "problemDetail.title." + the fully
qualified exception class name.




Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,20 +71,21 @@ from an existing `ProblemDetail`. This could be done centrally, e.g. from an
[.small]#xref:web/webflux/ann-rest-exceptions.adoc#webflux-ann-rest-exceptions-i18n[See equivalent in the Reactive stack]#

It is a common requirement to internationalize error response details, and good practice
to customize the problem details for Spring MVC exceptions. This is supported as follows:
to customize the problem details for Spring WebFlux exceptions. This section describes the
support for that.

- Each `ErrorResponse` exposes a message code and arguments to resolve the "detail" field
through a xref:core/beans/context-introduction.adoc#context-functionality-messagesource[MessageSource].
The actual message code value is parameterized with placeholders, e.g.
`+"HTTP method {0} not supported"+` to be expanded from the arguments.
- Each `ErrorResponse` also exposes a message code to resolve the "title" field.
- `ResponseEntityExceptionHandler` uses the message code and arguments to resolve the
"detail" and the "title" fields.
`ErrorResponse` exposes message codes for "type", "title", and "detail", in addition to
message code arguments for the "detail" field. `ResponseEntityExceptionHandler` resolves
these through a xref:core/beans/context-introduction.adoc#context-functionality-messagesource[MessageSource]
and updates the `ProblemDetail` accordingly.

By default, the message code for the "detail" field is "problemDetail." + the fully
qualified exception class name. Some exceptions may expose additional message codes in
which case a suffix is added to the default message code. The table below lists message
arguments and codes for Spring MVC exceptions:
The default strategy for message codes follows the pattern:

`problemDetail.[type|title|detail].[fully qualified exception class name]`

Some `ErrorResponse` may expose more than one message code, typically adding a suffix
to the default message code. The table below lists message codes, and arguments for
Spring MVC exceptions:

[[mvc-ann-rest-exceptions-codes]]
[cols="1,1,2", options="header"]
Expand Down Expand Up @@ -161,6 +162,10 @@ arguments and codes for Spring MVC exceptions:
| (default)
|

| `NoResourceFoundException`
| (default)
|

| `TypeMismatchException`
| (default)
| `+{0}+` property name, `+{1}+` property value
Expand All @@ -171,8 +176,6 @@ arguments and codes for Spring MVC exceptions:

|===

By default, the message code for the "title" field is "problemDetail.title." + the fully
qualified exception class name.



Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,7 @@ initialization parameters (`init-param` elements) to the Servlet declaration in
The exception can then be caught with a `HandlerExceptionResolver` (for example, by using an
`@ExceptionHandler` controller method) and handled as any others.

By default, this is set to `false`, in which case the `DispatcherServlet` sets the
response status to 404 (NOT_FOUND) without raising an exception.
As of 6.1, this property is set to `true` and deprecated.

Note that, if xref:web/webmvc/mvc-config/default-servlet-handler.adoc[default servlet handling] is
also configured, unresolved requests are always forwarded to the default servlet
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public class UserController {
@ExceptionHandler(MissingUserException.class)
ResponseEntity<Void> handleMissingUser(ServerWebExchange exchange, MissingUserException exception) {
// We want to record this exception with the observation
ServerRequestObservationContext.findCurrent(exchange)
ServerRequestObservationContext.findCurrent(exchange.getAttributes())
.ifPresent(context -> context.setError(exception));
return ResponseEntity.notFound().build();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* 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.docs.integration.observability.tasksscheduled;


import io.micrometer.observation.ObservationRegistry;

import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;

public class ObservationSchedulingConfigurer implements SchedulingConfigurer {

private final ObservationRegistry observationRegistry;

public ObservationSchedulingConfigurer(ObservationRegistry observationRegistry) {
this.observationRegistry = observationRegistry;
}

@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setObservationRegistry(this.observationRegistry);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* 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;

/**
* {@link BeanFactoryInitializationAotProcessor} implementation responsible for registering
* hints for AOP advices.
*
* @author Sebastien Deleuze
* @since 6.0.11
*/
class AspectJBeanFactoryInitializationAotProcessor implements BeanFactoryInitializationAotProcessor {

@Nullable
@Override
public BeanFactoryInitializationAotContribution 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);
}
}
}

}

}
3 changes: 3 additions & 0 deletions spring-aop/src/main/resources/META-INF/spring/aot.factories
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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() {
}

}

}
Loading

0 comments on commit cc47252

Please sign in to comment.