|
28 | 28 | import org.aopalliance.intercept.MethodInvocation;
|
29 | 29 |
|
30 | 30 | import org.springframework.aop.framework.ProxyFactory;
|
| 31 | +import org.springframework.beans.factory.InitializingBean; |
| 32 | +import org.springframework.context.EmbeddedValueResolverAware; |
31 | 33 | import org.springframework.core.MethodIntrospector;
|
32 | 34 | import org.springframework.core.ReactiveAdapterRegistry;
|
33 | 35 | import org.springframework.core.annotation.AnnotatedElementUtils;
|
|
42 | 44 | * Factory for creating a client proxy given an HTTP service interface with
|
43 | 45 | * {@link HttpExchange @HttpExchange} methods.
|
44 | 46 | *
|
| 47 | + * <p>This class is intended to be declared as a bean in a Spring configuration. |
| 48 | + * |
45 | 49 | * @author Rossen Stoyanchev
|
46 | 50 | * @since 6.0
|
47 | 51 | */
|
48 |
| -public final class HttpServiceProxyFactory { |
| 52 | +public final class HttpServiceProxyFactory implements InitializingBean, EmbeddedValueResolverAware { |
49 | 53 |
|
50 | 54 | private final HttpClientAdapter clientAdapter;
|
51 | 55 |
|
52 |
| - private final List<HttpServiceArgumentResolver> argumentResolvers; |
| 56 | + @Nullable |
| 57 | + private List<HttpServiceArgumentResolver> customArgumentResolvers; |
| 58 | + |
| 59 | + @Nullable |
| 60 | + private List<HttpServiceArgumentResolver> argumentResolvers; |
53 | 61 |
|
54 | 62 | @Nullable
|
55 |
| - private final StringValueResolver embeddedValueResolver; |
| 63 | + private ConversionService conversionService; |
56 | 64 |
|
57 |
| - private final ReactiveAdapterRegistry reactiveAdapterRegistry; |
| 65 | + @Nullable |
| 66 | + private StringValueResolver embeddedValueResolver; |
58 | 67 |
|
59 |
| - private final Duration blockTimeout; |
| 68 | + private ReactiveAdapterRegistry reactiveAdapterRegistry = ReactiveAdapterRegistry.getSharedInstance(); |
60 | 69 |
|
| 70 | + private Duration blockTimeout = Duration.ofSeconds(5); |
61 | 71 |
|
62 |
| - private HttpServiceProxyFactory( |
63 |
| - HttpClientAdapter clientAdapter, List<HttpServiceArgumentResolver> argumentResolvers, |
64 |
| - @Nullable StringValueResolver embeddedValueResolver, ReactiveAdapterRegistry reactiveAdapterRegistry, |
65 |
| - Duration blockTimeout) { |
66 | 72 |
|
| 73 | + /** |
| 74 | + * Create an instance with the underlying HTTP client to use. |
| 75 | + * @param clientAdapter an adapter for the client |
| 76 | + */ |
| 77 | + public HttpServiceProxyFactory(HttpClientAdapter clientAdapter) { |
| 78 | + Assert.notNull(clientAdapter, "HttpClientAdapter is required"); |
67 | 79 | this.clientAdapter = clientAdapter;
|
68 |
| - this.argumentResolvers = argumentResolvers; |
69 |
| - this.embeddedValueResolver = embeddedValueResolver; |
70 |
| - this.reactiveAdapterRegistry = reactiveAdapterRegistry; |
71 |
| - this.blockTimeout = blockTimeout; |
72 | 80 | }
|
73 | 81 |
|
74 | 82 |
|
75 | 83 | /**
|
76 |
| - * Return a proxy that implements the given HTTP service interface to perform |
77 |
| - * HTTP requests and retrieve responses through an HTTP client. |
78 |
| - * @param serviceType the HTTP service to create a proxy for |
79 |
| - * @param <S> the HTTP service type |
80 |
| - * @return the created proxy |
| 84 | + * Register a custom argument resolver, invoked ahead of default resolvers. |
| 85 | + * @param resolver the resolver to add |
81 | 86 | */
|
82 |
| - public <S> S createClient(Class<S> serviceType) { |
83 |
| - |
84 |
| - List<HttpServiceMethod> methods = |
85 |
| - MethodIntrospector.selectMethods(serviceType, this::isExchangeMethod) |
86 |
| - .stream() |
87 |
| - .map(method -> |
88 |
| - new HttpServiceMethod( |
89 |
| - method, serviceType, this.argumentResolvers, this.clientAdapter, |
90 |
| - this.embeddedValueResolver, this.reactiveAdapterRegistry, this.blockTimeout)) |
91 |
| - .toList(); |
92 |
| - |
93 |
| - return ProxyFactory.getProxy(serviceType, new HttpServiceMethodInterceptor(methods)); |
| 87 | + public void addCustomArgumentResolver(HttpServiceArgumentResolver resolver) { |
| 88 | + if (this.customArgumentResolvers == null) { |
| 89 | + this.customArgumentResolvers = new ArrayList<>(); |
| 90 | + } |
| 91 | + this.customArgumentResolvers.add(resolver); |
94 | 92 | }
|
95 | 93 |
|
96 |
| - private boolean isExchangeMethod(Method method) { |
97 |
| - return AnnotatedElementUtils.hasAnnotation(method, HttpExchange.class); |
| 94 | + /** |
| 95 | + * Set the custom argument resolvers to use, ahead of default resolvers. |
| 96 | + * @param resolvers the resolvers to use |
| 97 | + */ |
| 98 | + public void setCustomArgumentResolvers(List<HttpServiceArgumentResolver> resolvers) { |
| 99 | + this.customArgumentResolvers = new ArrayList<>(resolvers); |
98 | 100 | }
|
99 | 101 |
|
100 |
| - |
101 | 102 | /**
|
102 |
| - * Return a builder for an {@link HttpServiceProxyFactory}. |
103 |
| - * @param adapter an adapter for the underlying HTTP client |
104 |
| - * @return the builder |
| 103 | + * Set the {@link ConversionService} to use where input values need to |
| 104 | + * be formatted as Strings. |
| 105 | + * <p>By default this is {@link DefaultFormattingConversionService}. |
105 | 106 | */
|
106 |
| - public static Builder builder(HttpClientAdapter adapter) { |
107 |
| - return new Builder(adapter); |
| 107 | + public void setConversionService(ConversionService conversionService) { |
| 108 | + this.conversionService = conversionService; |
108 | 109 | }
|
109 | 110 |
|
110 |
| - |
111 | 111 | /**
|
112 |
| - * Builder for {@link HttpServiceProxyFactory}. |
| 112 | + * Set the StringValueResolver to use for resolving placeholders and |
| 113 | + * expressions in {@link HttpExchange#url()}. |
| 114 | + * @param resolver the resolver to use |
113 | 115 | */
|
114 |
| - public final static class Builder { |
115 |
| - |
116 |
| - private final HttpClientAdapter clientAdapter; |
117 |
| - |
118 |
| - private final List<HttpServiceArgumentResolver> customResolvers = new ArrayList<>(); |
119 |
| - |
120 |
| - @Nullable |
121 |
| - private ConversionService conversionService; |
122 |
| - |
123 |
| - @Nullable |
124 |
| - private StringValueResolver embeddedValueResolver; |
| 116 | + @Override |
| 117 | + public void setEmbeddedValueResolver(StringValueResolver resolver) { |
| 118 | + this.embeddedValueResolver = resolver; |
| 119 | + } |
125 | 120 |
|
126 |
| - private ReactiveAdapterRegistry reactiveAdapterRegistry = ReactiveAdapterRegistry.getSharedInstance(); |
| 121 | + /** |
| 122 | + * Set the {@link ReactiveAdapterRegistry} to use to support different |
| 123 | + * asynchronous types for HTTP service method return values. |
| 124 | + * <p>By default this is {@link ReactiveAdapterRegistry#getSharedInstance()}. |
| 125 | + */ |
| 126 | + public void setReactiveAdapterRegistry(ReactiveAdapterRegistry registry) { |
| 127 | + this.reactiveAdapterRegistry = registry; |
| 128 | + } |
127 | 129 |
|
128 |
| - private Duration blockTimeout = Duration.ofSeconds(5); |
| 130 | + /** |
| 131 | + * Configure how long to wait for a response for an HTTP service method |
| 132 | + * with a synchronous (blocking) method signature. |
| 133 | + * <p>By default this is 5 seconds. |
| 134 | + * @param blockTimeout the timeout value |
| 135 | + */ |
| 136 | + public void setBlockTimeout(Duration blockTimeout) { |
| 137 | + this.blockTimeout = blockTimeout; |
| 138 | + } |
129 | 139 |
|
130 |
| - private Builder(HttpClientAdapter clientAdapter) { |
131 |
| - Assert.notNull(clientAdapter, "HttpClientAdapter is required"); |
132 |
| - this.clientAdapter = clientAdapter; |
133 |
| - } |
134 | 140 |
|
135 |
| - /** |
136 |
| - * Register a custom argument resolver. This will be inserted ahead of |
137 |
| - * default resolvers. |
138 |
| - * @return the same builder instance |
139 |
| - */ |
140 |
| - public Builder addCustomResolver(HttpServiceArgumentResolver resolver) { |
141 |
| - this.customResolvers.add(resolver); |
142 |
| - return this; |
143 |
| - } |
| 141 | + @Override |
| 142 | + public void afterPropertiesSet() throws Exception { |
144 | 143 |
|
145 |
| - /** |
146 |
| - * Set the {@link ConversionService} to use where input values need to |
147 |
| - * be formatted as Strings. |
148 |
| - * <p>By default this is {@link DefaultFormattingConversionService}. |
149 |
| - * @return the same builder instance |
150 |
| - */ |
151 |
| - public Builder setConversionService(ConversionService conversionService) { |
152 |
| - this.conversionService = conversionService; |
153 |
| - return this; |
154 |
| - } |
| 144 | + this.conversionService = (this.conversionService != null ? |
| 145 | + this.conversionService : new DefaultFormattingConversionService()); |
155 | 146 |
|
156 |
| - /** |
157 |
| - * Set the StringValueResolver to use for resolving placeholders and |
158 |
| - * expressions in {@link HttpExchange#url()}. |
159 |
| - * @param embeddedValueResolver the resolver to use |
160 |
| - * @return the same builder instance |
161 |
| - * @see org.springframework.context.EmbeddedValueResolverAware |
162 |
| - */ |
163 |
| - public Builder setEmbeddedValueResolver(@Nullable StringValueResolver embeddedValueResolver) { |
164 |
| - this.embeddedValueResolver = embeddedValueResolver; |
165 |
| - return this; |
166 |
| - } |
| 147 | + this.argumentResolvers = initArgumentResolvers(this.conversionService); |
| 148 | + } |
167 | 149 |
|
168 |
| - /** |
169 |
| - * Set the {@link ReactiveAdapterRegistry} to use to support different |
170 |
| - * asynchronous types for HTTP service method return values. |
171 |
| - * <p>By default this is {@link ReactiveAdapterRegistry#getSharedInstance()}. |
172 |
| - * @return the same builder instance |
173 |
| - */ |
174 |
| - public Builder setReactiveAdapterRegistry(ReactiveAdapterRegistry registry) { |
175 |
| - this.reactiveAdapterRegistry = registry; |
176 |
| - return this; |
177 |
| - } |
| 150 | + private List<HttpServiceArgumentResolver> initArgumentResolvers(ConversionService conversionService) { |
| 151 | + List<HttpServiceArgumentResolver> resolvers = new ArrayList<>(); |
178 | 152 |
|
179 |
| - /** |
180 |
| - * Configure how long to wait for a response for an HTTP service method |
181 |
| - * with a synchronous (blocking) method signature. |
182 |
| - * <p>By default this is 5 seconds. |
183 |
| - * @param blockTimeout the timeout value |
184 |
| - * @return the same builder instance |
185 |
| - */ |
186 |
| - public Builder setBlockTimeout(Duration blockTimeout) { |
187 |
| - this.blockTimeout = blockTimeout; |
188 |
| - return this; |
| 153 | + // Custom |
| 154 | + if (this.customArgumentResolvers != null) { |
| 155 | + resolvers.addAll(this.customArgumentResolvers); |
189 | 156 | }
|
190 | 157 |
|
191 |
| - /** |
192 |
| - * Build and return the {@link HttpServiceProxyFactory} instance. |
193 |
| - */ |
194 |
| - public HttpServiceProxyFactory build() { |
| 158 | + // Annotation-based |
| 159 | + resolvers.add(new RequestHeaderArgumentResolver(conversionService)); |
| 160 | + resolvers.add(new RequestBodyArgumentResolver(this.reactiveAdapterRegistry)); |
| 161 | + resolvers.add(new PathVariableArgumentResolver(conversionService)); |
| 162 | + resolvers.add(new RequestParamArgumentResolver(conversionService)); |
| 163 | + resolvers.add(new CookieValueArgumentResolver(conversionService)); |
| 164 | + resolvers.add(new RequestAttributeArgumentResolver()); |
195 | 165 |
|
196 |
| - ConversionService conversionService = initConversionService(); |
197 |
| - List<HttpServiceArgumentResolver> resolvers = initArgumentResolvers(conversionService); |
| 166 | + // Specific type |
| 167 | + resolvers.add(new UrlArgumentResolver()); |
| 168 | + resolvers.add(new HttpMethodArgumentResolver()); |
198 | 169 |
|
199 |
| - return new HttpServiceProxyFactory( |
200 |
| - this.clientAdapter, resolvers, this.embeddedValueResolver, this.reactiveAdapterRegistry, |
201 |
| - this.blockTimeout); |
202 |
| - } |
| 170 | + return resolvers; |
| 171 | + } |
203 | 172 |
|
204 |
| - private ConversionService initConversionService() { |
205 |
| - return (this.conversionService != null ? |
206 |
| - this.conversionService : new DefaultFormattingConversionService()); |
207 |
| - } |
208 | 173 |
|
209 |
| - private List<HttpServiceArgumentResolver> initArgumentResolvers(ConversionService conversionService) { |
| 174 | + /** |
| 175 | + * Return a proxy that implements the given HTTP service interface to perform |
| 176 | + * HTTP requests and retrieve responses through an HTTP client. |
| 177 | + * @param serviceType the HTTP service to create a proxy for |
| 178 | + * @param <S> the HTTP service type |
| 179 | + * @return the created proxy |
| 180 | + */ |
| 181 | + public <S> S createClient(Class<S> serviceType) { |
210 | 182 |
|
211 |
| - List<HttpServiceArgumentResolver> resolvers = new ArrayList<>(this.customResolvers); |
| 183 | + List<HttpServiceMethod> httpServiceMethods = |
| 184 | + MethodIntrospector.selectMethods(serviceType, this::isExchangeMethod).stream() |
| 185 | + .map(method -> createHttpServiceMethod(serviceType, method)) |
| 186 | + .toList(); |
212 | 187 |
|
213 |
| - // Annotation-based |
214 |
| - resolvers.add(new RequestHeaderArgumentResolver(conversionService)); |
215 |
| - resolvers.add(new RequestBodyArgumentResolver(this.reactiveAdapterRegistry)); |
216 |
| - resolvers.add(new PathVariableArgumentResolver(conversionService)); |
217 |
| - resolvers.add(new RequestParamArgumentResolver(conversionService)); |
218 |
| - resolvers.add(new CookieValueArgumentResolver(conversionService)); |
219 |
| - resolvers.add(new RequestAttributeArgumentResolver()); |
| 188 | + return ProxyFactory.getProxy(serviceType, new HttpServiceMethodInterceptor(httpServiceMethods)); |
| 189 | + } |
220 | 190 |
|
221 |
| - // Specific type |
222 |
| - resolvers.add(new UrlArgumentResolver()); |
223 |
| - resolvers.add(new HttpMethodArgumentResolver()); |
| 191 | + private boolean isExchangeMethod(Method method) { |
| 192 | + return AnnotatedElementUtils.hasAnnotation(method, HttpExchange.class); |
| 193 | + } |
224 | 194 |
|
225 |
| - return resolvers; |
226 |
| - } |
| 195 | + private <S> HttpServiceMethod createHttpServiceMethod(Class<S> serviceType, Method method) { |
| 196 | + Assert.notNull(this.argumentResolvers, |
| 197 | + "No argument resolvers: afterPropertiesSet was not called"); |
227 | 198 |
|
| 199 | + return new HttpServiceMethod( |
| 200 | + method, serviceType, this.argumentResolvers, this.clientAdapter, |
| 201 | + this.embeddedValueResolver, this.reactiveAdapterRegistry, this.blockTimeout); |
228 | 202 | }
|
229 | 203 |
|
230 | 204 |
|
|
0 commit comments