Skip to content

Commit 4b2e401

Browse files
committed
Merge pull request #32243 from ttddyy
* pr/32243: Polish "Add ThreadLocalAccessor for LocaleContext and RequestAttributes" Add ThreadLocalAccessor for LocaleContext and RequestAttributes Closes gh-32243
2 parents 5d22aa9 + aef4b21 commit 4b2e401

File tree

7 files changed

+301
-1
lines changed

7 files changed

+301
-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: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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
23+
* to assist the Micrometer Context Propagation library with {@link LocaleContext}
24+
* propagation.
25+
*
26+
* @author Tadaya Tsuyukubo
27+
* @since 6.2
28+
*/
29+
public class LocaleContextThreadLocalAccessor implements ThreadLocalAccessor<LocaleContext> {
30+
31+
/**
32+
* Key under which this accessor is registered in
33+
* {@link io.micrometer.context.ContextRegistry}.
34+
*/
35+
public static final String KEY = LocaleContextThreadLocalAccessor.class.getName() + ".KEY";
36+
37+
@Override
38+
public Object key() {
39+
return KEY;
40+
}
41+
42+
@Override
43+
public LocaleContext getValue() {
44+
return LocaleContextHolder.getLocaleContext();
45+
}
46+
47+
@Override
48+
public void setValue(LocaleContext value) {
49+
LocaleContextHolder.setLocaleContext(value);
50+
}
51+
52+
@Override
53+
public void setValue() {
54+
LocaleContextHolder.resetLocaleContext();
55+
}
56+
57+
}
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,57 @@
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
23+
* to assist the Micrometer Context Propagation library with
24+
* {@link RequestAttributes} propagation.
25+
*
26+
* @author Tadaya Tsuyukubo
27+
* @since 6.2
28+
*/
29+
public class RequestAttributesThreadLocalAccessor implements ThreadLocalAccessor<RequestAttributes> {
30+
31+
/**
32+
* Key under which this accessor is registered in
33+
* {@link io.micrometer.context.ContextRegistry}.
34+
*/
35+
public static final String KEY = RequestAttributesThreadLocalAccessor.class.getName() + ".KEY";
36+
37+
@Override
38+
public Object key() {
39+
return KEY;
40+
}
41+
42+
@Override
43+
public RequestAttributes getValue() {
44+
return RequestContextHolder.getRequestAttributes();
45+
}
46+
47+
@Override
48+
public void setValue(RequestAttributes value) {
49+
RequestContextHolder.setRequestAttributes(value);
50+
}
51+
52+
@Override
53+
public void setValue() {
54+
RequestContextHolder.resetRequestAttributes();
55+
}
56+
57+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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.ContextSnapshot.Scope;
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+
import static org.mockito.Mockito.mock;
37+
38+
/**
39+
* Tests for {@link RequestAttributesThreadLocalAccessor}.
40+
*
41+
* @author Tadaya Tsuyukubo
42+
*/
43+
class RequestAttributesThreadLocalAccessorTests {
44+
45+
private final ContextRegistry registry = new ContextRegistry()
46+
.registerThreadLocalAccessor(new RequestAttributesThreadLocalAccessor());
47+
48+
@AfterEach
49+
void cleanUp() {
50+
RequestContextHolder.resetRequestAttributes();
51+
}
52+
53+
@ParameterizedTest
54+
@MethodSource
55+
@SuppressWarnings({ "try", "unused" })
56+
void propagation(@Nullable RequestAttributes previous, RequestAttributes current) throws Exception {
57+
RequestContextHolder.setRequestAttributes(current);
58+
ContextSnapshot snapshot = ContextSnapshotFactory.builder()
59+
.contextRegistry(this.registry)
60+
.clearMissing(true)
61+
.build()
62+
.captureAll();
63+
64+
AtomicReference<RequestAttributes> previousHolder = new AtomicReference<>();
65+
AtomicReference<RequestAttributes> currentHolder = new AtomicReference<>();
66+
CountDownLatch latch = new CountDownLatch(1);
67+
new Thread(() -> {
68+
RequestContextHolder.setRequestAttributes(previous);
69+
try (Scope scope = snapshot.setThreadLocals()) {
70+
currentHolder.set(RequestContextHolder.getRequestAttributes());
71+
}
72+
previousHolder.set(RequestContextHolder.getRequestAttributes());
73+
latch.countDown();
74+
}).start();
75+
76+
latch.await(1, TimeUnit.SECONDS);
77+
assertThat(previousHolder).hasValueSatisfying(value -> assertThat(value).isSameAs(previous));
78+
assertThat(currentHolder).hasValueSatisfying(value -> assertThat(value).isSameAs(current));
79+
}
80+
81+
private static Stream<Arguments> propagation() {
82+
RequestAttributes previous = mock(RequestAttributes.class);
83+
RequestAttributes current = mock(RequestAttributes.class);
84+
return Stream.of(
85+
Arguments.of(null, current),
86+
Arguments.of(previous, current)
87+
);
88+
}
89+
90+
}

0 commit comments

Comments
 (0)