Skip to content

Commit 5bd1c1f

Browse files
ttddyysnicoll
authored andcommitted
Add ThreadLocalAccessor for LocaleContext and RequestAttributes
Add `ThreadLocalAccessor` implementations: - `LocaleThreadLocalAccessor` - `RequestAttributesThreadLocalAccessor` See gh-32243
1 parent 5d22aa9 commit 5bd1c1f

File tree

7 files changed

+295
-1
lines changed

7 files changed

+295
-1
lines changed

framework-docs/modules/ROOT/pages/web/webmvc/mvc-ann-async.adoc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,13 @@ directly. For example:
445445
}
446446
----
447447

448+
The following `ThreadLocalAccessor` implementations are provided out of the box:
449+
450+
* `LocaleContextThreadLocalAccessor` -- propagates `LocaleContext` via `LocaleContextHolder`
451+
* `RequestAttributesThreadLocalAccessor` -- propagates `RequestAttributes` via `RequestContextHolder`
452+
453+
The above are not registered automatically. You need to register them via `ContextRegistry.getInstance()` on startup.
454+
448455
For more details, see the
449456
https://micrometer.io/docs/contextPropagation[documentation] of the Micrometer Context
450457
Propagation library.

spring-context/spring-context.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ dependencies {
1313
api(project(":spring-expression"))
1414
api("io.micrometer:micrometer-observation")
1515
optional(project(":spring-instrument"))
16+
optional("io.micrometer:context-propagation")
1617
optional("io.projectreactor:reactor-core")
1718
optional("jakarta.annotation:jakarta.annotation-api")
1819
optional("jakarta.ejb:jakarta.ejb-api")
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.context.i18n;
18+
19+
import io.micrometer.context.ThreadLocalAccessor;
20+
21+
/**
22+
* Adapt {@link LocaleContextHolder} to the {@link ThreadLocalAccessor} contract to assist
23+
* the Micrometer Context Propagation library with {@link LocaleContext} propagation.
24+
* @author Tadaya Tsuyukubo
25+
* @since 6.2
26+
*/
27+
public class LocaleContextThreadLocalAccessor implements ThreadLocalAccessor<LocaleContext> {
28+
29+
/**
30+
* Key under which this accessor is registered in
31+
* {@link io.micrometer.context.ContextRegistry}.
32+
*/
33+
public static final String KEY = LocaleContextThreadLocalAccessor.class.getName() + ".KEY";
34+
35+
@Override
36+
public Object key() {
37+
return KEY;
38+
}
39+
40+
@Override
41+
public LocaleContext getValue() {
42+
return LocaleContextHolder.getLocaleContext();
43+
}
44+
45+
@Override
46+
public void setValue(LocaleContext value) {
47+
LocaleContextHolder.setLocaleContext(value);
48+
}
49+
50+
@Override
51+
public void setValue() {
52+
LocaleContextHolder.resetLocaleContext();
53+
}
54+
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.context.i18n;
18+
19+
import java.util.Locale;
20+
import java.util.concurrent.CountDownLatch;
21+
import java.util.concurrent.TimeUnit;
22+
import java.util.concurrent.atomic.AtomicReference;
23+
import java.util.stream.Stream;
24+
25+
import io.micrometer.context.ContextRegistry;
26+
import io.micrometer.context.ContextSnapshot;
27+
import io.micrometer.context.ContextSnapshotFactory;
28+
import org.junit.jupiter.api.AfterEach;
29+
import org.junit.jupiter.params.ParameterizedTest;
30+
import org.junit.jupiter.params.provider.Arguments;
31+
import org.junit.jupiter.params.provider.MethodSource;
32+
33+
import org.springframework.lang.Nullable;
34+
35+
import static org.assertj.core.api.Assertions.assertThat;
36+
37+
/**
38+
* Tests for {@link LocaleContextThreadLocalAccessor}.
39+
*
40+
* @author Tadaya Tsuyukubo
41+
*/
42+
class LocaleContextThreadLocalAccessorTests {
43+
44+
private final ContextRegistry registry = new ContextRegistry()
45+
.registerThreadLocalAccessor(new LocaleContextThreadLocalAccessor());
46+
47+
@AfterEach
48+
void cleanUp() {
49+
LocaleContextHolder.resetLocaleContext();
50+
}
51+
52+
@ParameterizedTest
53+
@MethodSource
54+
void propagation(@Nullable LocaleContext previous, LocaleContext current) throws Exception {
55+
LocaleContextHolder.setLocaleContext(current);
56+
ContextSnapshot snapshot = ContextSnapshotFactory.builder()
57+
.contextRegistry(this.registry)
58+
.clearMissing(true)
59+
.build()
60+
.captureAll();
61+
62+
AtomicReference<LocaleContext> previousHolder = new AtomicReference<>();
63+
AtomicReference<LocaleContext> currentHolder = new AtomicReference<>();
64+
CountDownLatch latch = new CountDownLatch(1);
65+
new Thread(() -> {
66+
LocaleContextHolder.setLocaleContext(previous);
67+
try (ContextSnapshot.Scope scope = snapshot.setThreadLocals()) {
68+
currentHolder.set(LocaleContextHolder.getLocaleContext());
69+
}
70+
previousHolder.set(LocaleContextHolder.getLocaleContext());
71+
latch.countDown();
72+
}).start();
73+
74+
latch.await(1, TimeUnit.SECONDS);
75+
assertThat(previousHolder).hasValueSatisfying(value -> assertThat(value).isSameAs(previous));
76+
assertThat(currentHolder).hasValueSatisfying(value -> assertThat(value).isSameAs(current));
77+
}
78+
79+
private static Stream<Arguments> propagation() {
80+
LocaleContext previous = new SimpleLocaleContext(Locale.ENGLISH);
81+
LocaleContext current = new SimpleLocaleContext(Locale.ENGLISH);
82+
return Stream.of(
83+
Arguments.of(null, current),
84+
Arguments.of(previous, current)
85+
);
86+
}
87+
}

spring-web/spring-web.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ dependencies {
2020
optional("com.google.protobuf:protobuf-java-util")
2121
optional("com.rometools:rome")
2222
optional("com.squareup.okhttp3:okhttp")
23-
optional("io.reactivex.rxjava3:rxjava")
23+
optional("io.micrometer:context-propagation")
2424
optional("io.netty:netty-buffer")
2525
optional("io.netty:netty-handler")
2626
optional("io.netty:netty-codec-http")
@@ -31,6 +31,7 @@ dependencies {
3131
optional("io.netty:netty5-transport")
3232
optional("io.projectreactor.netty:reactor-netty-http")
3333
optional("io.projectreactor.netty:reactor-netty5-http")
34+
optional("io.reactivex.rxjava3:rxjava")
3435
optional("io.undertow:undertow-core")
3536
optional("jakarta.el:jakarta.el-api")
3637
optional("jakarta.faces:jakarta.faces-api")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.context.request;
18+
19+
import io.micrometer.context.ThreadLocalAccessor;
20+
21+
/**
22+
* Adapt {@link RequestContextHolder} to the {@link ThreadLocalAccessor} contract to assist
23+
* the Micrometer Context Propagation library with {@link RequestAttributes} propagation.
24+
* @author Tadaya Tsuyukubo
25+
* @since 6.2
26+
*/
27+
public class RequestAttributesThreadLocalAccessor implements ThreadLocalAccessor<RequestAttributes> {
28+
29+
/**
30+
* Key under which this accessor is registered in
31+
* {@link io.micrometer.context.ContextRegistry}.
32+
*/
33+
public static final String KEY = RequestAttributesThreadLocalAccessor.class.getName() + ".KEY";
34+
35+
@Override
36+
public Object key() {
37+
return KEY;
38+
}
39+
40+
@Override
41+
public RequestAttributes getValue() {
42+
return RequestContextHolder.getRequestAttributes();
43+
}
44+
45+
@Override
46+
public void setValue(RequestAttributes value) {
47+
RequestContextHolder.setRequestAttributes(value);
48+
}
49+
50+
@Override
51+
public void setValue() {
52+
RequestContextHolder.resetRequestAttributes();
53+
}
54+
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.context.request;
18+
19+
import java.util.concurrent.CountDownLatch;
20+
import java.util.concurrent.TimeUnit;
21+
import java.util.concurrent.atomic.AtomicReference;
22+
import java.util.stream.Stream;
23+
24+
import io.micrometer.context.ContextRegistry;
25+
import io.micrometer.context.ContextSnapshot;
26+
import io.micrometer.context.ContextSnapshotFactory;
27+
import org.junit.jupiter.api.AfterEach;
28+
import org.junit.jupiter.params.ParameterizedTest;
29+
import org.junit.jupiter.params.provider.Arguments;
30+
import org.junit.jupiter.params.provider.MethodSource;
31+
32+
import org.springframework.lang.Nullable;
33+
34+
import static org.assertj.core.api.Assertions.assertThat;
35+
import static org.mockito.Mockito.mock;
36+
37+
/**
38+
* Tests for {@link RequestAttributesThreadLocalAccessor}.
39+
*
40+
* @author Tadaya Tsuyukubo
41+
*/
42+
class RequestAttributesThreadLocalAccessorTests {
43+
44+
private final ContextRegistry registry = new ContextRegistry()
45+
.registerThreadLocalAccessor(new RequestAttributesThreadLocalAccessor());
46+
47+
@AfterEach
48+
void cleanUp() {
49+
RequestContextHolder.resetRequestAttributes();
50+
}
51+
52+
@ParameterizedTest
53+
@MethodSource
54+
void propagation(@Nullable RequestAttributes previous, RequestAttributes current) throws Exception {
55+
RequestContextHolder.setRequestAttributes(current);
56+
ContextSnapshot snapshot = ContextSnapshotFactory.builder()
57+
.contextRegistry(this.registry)
58+
.clearMissing(true)
59+
.build()
60+
.captureAll();
61+
62+
AtomicReference<RequestAttributes> previousHolder = new AtomicReference<>();
63+
AtomicReference<RequestAttributes> currentHolder = new AtomicReference<>();
64+
CountDownLatch latch = new CountDownLatch(1);
65+
new Thread(() -> {
66+
RequestContextHolder.setRequestAttributes(previous);
67+
try (ContextSnapshot.Scope scope = snapshot.setThreadLocals()) {
68+
currentHolder.set(RequestContextHolder.getRequestAttributes());
69+
}
70+
previousHolder.set(RequestContextHolder.getRequestAttributes());
71+
latch.countDown();
72+
}).start();
73+
74+
latch.await(1, TimeUnit.SECONDS);
75+
assertThat(previousHolder).hasValueSatisfying(value -> assertThat(value).isSameAs(previous));
76+
assertThat(currentHolder).hasValueSatisfying(value -> assertThat(value).isSameAs(current));
77+
}
78+
79+
private static Stream<Arguments> propagation() {
80+
RequestAttributes previous = mock(RequestAttributes.class);
81+
RequestAttributes current = mock(RequestAttributes.class);
82+
return Stream.of(
83+
Arguments.of(null, current),
84+
Arguments.of(previous, current)
85+
);
86+
}
87+
88+
}

0 commit comments

Comments
 (0)