Skip to content

Commit ab81d99

Browse files
mbhavephilwebb
authored andcommitted
Add CloudFoundryDiscoveryMvcEndpoint
Update Cloud Foundry support with a discovery endpoint that shows what endpoints are available. See gh-7108
1 parent 7afb161 commit ab81d99

9 files changed

+495
-168
lines changed

spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcManagementContextConfiguration.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public class EndpointWebMvcManagementContextConfiguration {
7979
@Bean
8080
@ConditionalOnMissingBean
8181
public EndpointHandlerMapping endpointHandlerMapping() {
82-
Set<? extends MvcEndpoint> endpoints = mvcEndpoints().getEndpoints();
82+
Set<MvcEndpoint> endpoints = mvcEndpoints().getEndpoints();
8383
CorsConfiguration corsConfiguration = getCorsConfiguration(this.corsProperties);
8484
EndpointHandlerMapping mapping = new EndpointHandlerMapping(endpoints,
8585
corsConfiguration);

spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementWebSecurityAutoConfiguration.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -406,8 +406,7 @@ private EndpointHandlerMapping getRequiredEndpointHandlerMapping() {
406406
EndpointHandlerMapping endpointHandlerMapping = null;
407407
ApplicationContext context = this.contextResolver.getApplicationContext();
408408
if (context.getBeanNamesForType(EndpointHandlerMapping.class).length > 0) {
409-
endpointHandlerMapping = context.getBean("endpointHandlerMapping",
410-
EndpointHandlerMapping.class);
409+
endpointHandlerMapping = context.getBean(EndpointHandlerMapping.class);
411410
}
412411
if (endpointHandlerMapping == null) {
413412
// Maybe there are actually no endpoints (e.g. management.port=-1)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright 2012-2016 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+
* http://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.boot.actuate.cloudfoundry;
18+
19+
import java.util.Collections;
20+
import java.util.LinkedHashMap;
21+
import java.util.Map;
22+
import java.util.Set;
23+
24+
import javax.servlet.http.HttpServletRequest;
25+
26+
import org.springframework.boot.actuate.endpoint.mvc.AbstractMvcEndpoint;
27+
import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint;
28+
import org.springframework.boot.actuate.endpoint.mvc.NamedMvcEndpoint;
29+
import org.springframework.http.MediaType;
30+
import org.springframework.web.bind.annotation.RequestMapping;
31+
import org.springframework.web.bind.annotation.ResponseBody;
32+
33+
/**
34+
* {@link MvcEndpoint} to expose HAL-formatted JSON for Cloud Foundry specific actuator
35+
* endpoints.
36+
*
37+
* @author Madhura Bhave
38+
*/
39+
class CloudFoundryDiscoveryMvcEndpoint extends AbstractMvcEndpoint {
40+
41+
private final Set<NamedMvcEndpoint> endpoints;
42+
43+
CloudFoundryDiscoveryMvcEndpoint(Set<NamedMvcEndpoint> endpoints) {
44+
super("", false);
45+
this.endpoints = endpoints;
46+
}
47+
48+
@RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE)
49+
@ResponseBody
50+
public Map<String, Map<String, Link>> links(HttpServletRequest request) {
51+
Map<String, Link> links = new LinkedHashMap<String, Link>();
52+
String url = request.getRequestURL().toString();
53+
if (url.endsWith("/")) {
54+
url = url.substring(0, url.length() - 1);
55+
}
56+
links.put("self", Link.withHref(url));
57+
for (NamedMvcEndpoint endpoint : this.endpoints) {
58+
links.put(endpoint.getName(), Link.withHref(url + "/" + endpoint.getName()));
59+
}
60+
return Collections.singletonMap("_links", links);
61+
}
62+
63+
/**
64+
* Details for a link in the HAL response.
65+
*/
66+
static class Link {
67+
68+
private String href;
69+
70+
public String getHref() {
71+
return this.href;
72+
}
73+
74+
public void setHref(String href) {
75+
this.href = href;
76+
}
77+
78+
static Link withHref(Object href) {
79+
Link link = new Link();
80+
link.setHref(href.toString());
81+
return link;
82+
}
83+
84+
}
85+
86+
}

spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryEndpointHandlerMapping.java

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,16 @@
1919
import java.util.ArrayList;
2020
import java.util.Arrays;
2121
import java.util.Collection;
22+
import java.util.Iterator;
2223
import java.util.List;
2324
import java.util.Set;
2425

2526
import javax.servlet.http.HttpServletRequest;
2627
import javax.servlet.http.HttpServletResponse;
2728

2829
import org.springframework.boot.actuate.endpoint.Endpoint;
29-
import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMapping;
30+
import org.springframework.boot.actuate.endpoint.mvc.AbstractEndpointHandlerMapping;
31+
import org.springframework.boot.actuate.endpoint.mvc.HalJsonMvcEndpoint;
3032
import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint;
3133
import org.springframework.boot.actuate.endpoint.mvc.NamedMvcEndpoint;
3234
import org.springframework.web.cors.CorsConfiguration;
@@ -40,7 +42,8 @@
4042
*
4143
* @author Madhura Bhave
4244
*/
43-
class CloudFoundryEndpointHandlerMapping extends EndpointHandlerMapping {
45+
class CloudFoundryEndpointHandlerMapping
46+
extends AbstractEndpointHandlerMapping<NamedMvcEndpoint> {
4447

4548
CloudFoundryEndpointHandlerMapping(Collection<? extends NamedMvcEndpoint> endpoints) {
4649
super(endpoints);
@@ -51,6 +54,23 @@ class CloudFoundryEndpointHandlerMapping extends EndpointHandlerMapping {
5154
super(endpoints, corsConfiguration);
5255
}
5356

57+
@Override
58+
protected void postProcessEndpoints(Set<NamedMvcEndpoint> endpoints) {
59+
super.postProcessEndpoints(endpoints);
60+
Iterator<NamedMvcEndpoint> iterator = endpoints.iterator();
61+
while (iterator.hasNext()) {
62+
if (iterator.next() instanceof HalJsonMvcEndpoint) {
63+
iterator.remove();
64+
}
65+
}
66+
}
67+
68+
@Override
69+
public void afterPropertiesSet() {
70+
super.afterPropertiesSet();
71+
detectHandlerMethods(new CloudFoundryDiscoveryMvcEndpoint(getEndpoints()));
72+
}
73+
5474
@Override
5575
protected String getPath(MvcEndpoint endpoint) {
5676
if (endpoint instanceof NamedMvcEndpoint) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
/*
2+
* Copyright 2012-2015 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+
* http://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.boot.actuate.endpoint.mvc;
18+
19+
import java.lang.reflect.Method;
20+
import java.util.ArrayList;
21+
import java.util.Collection;
22+
import java.util.Collections;
23+
import java.util.HashSet;
24+
import java.util.List;
25+
import java.util.Set;
26+
27+
import org.springframework.boot.actuate.endpoint.Endpoint;
28+
import org.springframework.context.ApplicationContext;
29+
import org.springframework.util.Assert;
30+
import org.springframework.util.ObjectUtils;
31+
import org.springframework.util.StringUtils;
32+
import org.springframework.web.cors.CorsConfiguration;
33+
import org.springframework.web.servlet.HandlerMapping;
34+
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
35+
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
36+
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
37+
38+
/**
39+
* {@link HandlerMapping} to map {@link Endpoint}s to URLs via {@link Endpoint#getId()}.
40+
* The semantics of {@code @RequestMapping} should be identical to a normal
41+
* {@code @Controller}, but the endpoints should not be annotated as {@code @Controller}
42+
* (otherwise they will be mapped by the normal MVC mechanisms).
43+
* <p>
44+
* One of the aims of the mapping is to support endpoints that work as HTTP endpoints but
45+
* can still provide useful service interfaces when there is no HTTP server (and no Spring
46+
* MVC on the classpath). Note that any endpoints having method signatures will break in a
47+
* non-servlet environment.
48+
*
49+
* @param <E> The endpoint type
50+
* @author Phillip Webb
51+
* @author Christian Dupuis
52+
* @author Dave Syer
53+
* @author Madhura Bhave
54+
*/
55+
public class AbstractEndpointHandlerMapping<E extends MvcEndpoint>
56+
extends RequestMappingHandlerMapping {
57+
58+
private final Set<E> endpoints;
59+
60+
private final CorsConfiguration corsConfiguration;
61+
62+
private String prefix = "";
63+
64+
private boolean disabled = false;
65+
66+
/**
67+
* Create a new {@link AbstractEndpointHandlerMapping} instance. All {@link Endpoint}s
68+
* will be detected from the {@link ApplicationContext}. The endpoints will not accept
69+
* CORS requests.
70+
* @param endpoints the endpoints
71+
*/
72+
public AbstractEndpointHandlerMapping(Collection<? extends E> endpoints) {
73+
this(endpoints, null);
74+
}
75+
76+
/**
77+
* Create a new {@link AbstractEndpointHandlerMapping} instance. All {@link Endpoint}s
78+
* will be detected from the {@link ApplicationContext}. The endpoints will accepts
79+
* CORS requests based on the given {@code corsConfiguration}.
80+
* @param endpoints the endpoints
81+
* @param corsConfiguration the CORS configuration for the endpoints
82+
* @since 1.3.0
83+
*/
84+
public AbstractEndpointHandlerMapping(Collection<? extends E> endpoints,
85+
CorsConfiguration corsConfiguration) {
86+
this.endpoints = new HashSet<E>(endpoints);
87+
postProcessEndpoints(this.endpoints);
88+
this.corsConfiguration = corsConfiguration;
89+
// By default the static resource handler mapping is LOWEST_PRECEDENCE - 1
90+
// and the RequestMappingHandlerMapping is 0 (we ideally want to be before both)
91+
setOrder(-100);
92+
setUseSuffixPatternMatch(false);
93+
}
94+
95+
/**
96+
* Post process the endpoint setting before they are used. Subclasses can add or
97+
* modify the endpoints as necessary.
98+
* @param endpoints the endpoints to post process
99+
*/
100+
protected void postProcessEndpoints(Set<E> endpoints) {
101+
}
102+
103+
@Override
104+
public void afterPropertiesSet() {
105+
super.afterPropertiesSet();
106+
if (!this.disabled) {
107+
for (MvcEndpoint endpoint : this.endpoints) {
108+
detectHandlerMethods(endpoint);
109+
}
110+
}
111+
}
112+
113+
/**
114+
* Since all handler beans are passed into the constructor there is no need to detect
115+
* anything here.
116+
*/
117+
@Override
118+
protected boolean isHandler(Class<?> beanType) {
119+
return false;
120+
}
121+
122+
@Override
123+
@Deprecated
124+
protected void registerHandlerMethod(Object handler, Method method,
125+
RequestMappingInfo mapping) {
126+
if (mapping == null) {
127+
return;
128+
}
129+
String[] patterns = getPatterns(handler, mapping);
130+
if (!ObjectUtils.isEmpty(patterns)) {
131+
super.registerHandlerMethod(handler, method,
132+
withNewPatterns(mapping, patterns));
133+
}
134+
}
135+
136+
private String[] getPatterns(Object handler, RequestMappingInfo mapping) {
137+
if (handler instanceof String) {
138+
handler = getApplicationContext().getBean((String) handler);
139+
}
140+
Assert.state(handler instanceof MvcEndpoint, "Only MvcEndpoints are supported");
141+
String path = getPath((MvcEndpoint) handler);
142+
return (path == null ? null : getEndpointPatterns(path, mapping));
143+
}
144+
145+
/**
146+
* Return the path that should be used to map the given {@link MvcEndpoint}.
147+
* @param endpoint the endpoint to map
148+
* @return the path to use for the endpoint or {@code null} if no mapping is required
149+
*/
150+
protected String getPath(MvcEndpoint endpoint) {
151+
return endpoint.getPath();
152+
}
153+
154+
private String[] getEndpointPatterns(String path, RequestMappingInfo mapping) {
155+
String patternPrefix = StringUtils.hasText(this.prefix) ? this.prefix + path
156+
: path;
157+
Set<String> defaultPatterns = mapping.getPatternsCondition().getPatterns();
158+
if (defaultPatterns.isEmpty()) {
159+
return new String[] { patternPrefix, patternPrefix + ".json" };
160+
}
161+
List<String> patterns = new ArrayList<String>(defaultPatterns);
162+
for (int i = 0; i < patterns.size(); i++) {
163+
patterns.set(i, patternPrefix + patterns.get(i));
164+
}
165+
return patterns.toArray(new String[patterns.size()]);
166+
}
167+
168+
private RequestMappingInfo withNewPatterns(RequestMappingInfo mapping,
169+
String[] patternStrings) {
170+
PatternsRequestCondition patterns = new PatternsRequestCondition(patternStrings,
171+
null, null, useSuffixPatternMatch(), useTrailingSlashMatch(), null);
172+
return new RequestMappingInfo(patterns, mapping.getMethodsCondition(),
173+
mapping.getParamsCondition(), mapping.getHeadersCondition(),
174+
mapping.getConsumesCondition(), mapping.getProducesCondition(),
175+
mapping.getCustomCondition());
176+
}
177+
178+
/**
179+
* Set the prefix used in mappings.
180+
* @param prefix the prefix
181+
*/
182+
public void setPrefix(String prefix) {
183+
Assert.isTrue("".equals(prefix) || StringUtils.startsWithIgnoreCase(prefix, "/"),
184+
"prefix must start with '/'");
185+
this.prefix = prefix;
186+
}
187+
188+
/**
189+
* Get the prefix used in mappings.
190+
* @return the prefix
191+
*/
192+
public String getPrefix() {
193+
return this.prefix;
194+
}
195+
196+
/**
197+
* Get the path of the endpoint.
198+
* @param endpoint the endpoint
199+
* @return the path used in mappings
200+
*/
201+
public String getPath(String endpoint) {
202+
return this.prefix + endpoint;
203+
}
204+
205+
/**
206+
* Sets if this mapping is disabled.
207+
* @param disabled if the mapping is disabled
208+
*/
209+
public void setDisabled(boolean disabled) {
210+
this.disabled = disabled;
211+
}
212+
213+
/**
214+
* Returns if this mapping is disabled.
215+
* @return if the mapping is disabled
216+
*/
217+
public boolean isDisabled() {
218+
return this.disabled;
219+
}
220+
221+
/**
222+
* Return the endpoints.
223+
* @return the endpoints
224+
*/
225+
public Set<E> getEndpoints() {
226+
return Collections.unmodifiableSet(this.endpoints);
227+
}
228+
229+
@Override
230+
protected CorsConfiguration initCorsConfiguration(Object handler, Method method,
231+
RequestMappingInfo mappingInfo) {
232+
return this.corsConfiguration;
233+
}
234+
235+
}

0 commit comments

Comments
 (0)