Relocate projects to spring-boot-project

Move projects to better reflect the way that Spring Boot is released.

The following projects are under `spring-boot-project`:

  - `spring-boot`
  - `spring-boot-autoconfigure`
  - `spring-boot-tools`
  - `spring-boot-starters`
  - `spring-boot-actuator`
  - `spring-boot-actuator-autoconfigure`
  - `spring-boot-test`
  - `spring-boot-test-autoconfigure`
  - `spring-boot-devtools`
  - `spring-boot-cli`
  - `spring-boot-docs`

See gh-9316
This commit is contained in:
Phillip Webb
2017-09-19 14:29:46 -07:00
parent 0419d42b7c
commit 0ba4830b4f
4023 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.amqp;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.boot.actuate.health.AbstractHealthIndicator;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.util.Assert;
/**
* Simple implementation of a {@link HealthIndicator} returning status information for the
* RabbitMQ messaging system.
*
* @author Christian Dupuis
* @since 1.1.0
*/
public class RabbitHealthIndicator extends AbstractHealthIndicator {
private final RabbitTemplate rabbitTemplate;
public RabbitHealthIndicator(RabbitTemplate rabbitTemplate) {
Assert.notNull(rabbitTemplate, "RabbitTemplate must not be null");
this.rabbitTemplate = rabbitTemplate;
}
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
builder.up().withDetail("version", getVersion());
}
private String getVersion() {
return this.rabbitTemplate.execute((channel) -> channel.getConnection()
.getServerProperties().get("version").toString());
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2017 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
*
* http://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.
*/
/**
* Actuator support for AMQP and RabbitMQ.
*/
package org.springframework.boot.actuate.amqp;

View File

@@ -0,0 +1,149 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.audit;
import java.io.Serializable;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.util.Assert;
/**
* A value object representing an audit event: at a particular time, a particular user or
* agent carried out an action of a particular type. This object records the details of
* such an event.
* <p>
* Users can inject a {@link AuditEventRepository} to publish their own events or
* alternatively use Spring's {@link ApplicationEventPublisher} (usually obtained by
* implementing {@link ApplicationEventPublisherAware}) to publish AuditApplicationEvents
* (wrappers for AuditEvent).
*
* @author Dave Syer
* @see AuditEventRepository
*/
@JsonInclude(Include.NON_EMPTY)
public class AuditEvent implements Serializable {
private final Date timestamp;
private final String principal;
private final String type;
private final Map<String, Object> data;
/**
* Create a new audit event for the current time.
* @param principal The user principal responsible
* @param type the event type
* @param data The event data
*/
public AuditEvent(String principal, String type, Map<String, Object> data) {
this(new Date(), principal, type, data);
}
/**
* Create a new audit event for the current time from data provided as name-value
* pairs.
* @param principal The user principal responsible
* @param type the event type
* @param data The event data in the form 'key=value' or simply 'key'
*/
public AuditEvent(String principal, String type, String... data) {
this(new Date(), principal, type, convert(data));
}
/**
* Create a new audit event.
* @param timestamp The date/time of the event
* @param principal The user principal responsible
* @param type the event type
* @param data The event data
*/
public AuditEvent(Date timestamp, String principal, String type,
Map<String, Object> data) {
Assert.notNull(timestamp, "Timestamp must not be null");
Assert.notNull(principal, "Principal must not be null");
Assert.notNull(type, "Type must not be null");
this.timestamp = timestamp;
this.principal = principal;
this.type = type;
this.data = Collections.unmodifiableMap(data);
}
private static Map<String, Object> convert(String[] data) {
Map<String, Object> result = new HashMap<>();
for (String entry : data) {
if (entry.contains("=")) {
int index = entry.indexOf("=");
result.put(entry.substring(0, index), entry.substring(index + 1));
}
else {
result.put(entry, null);
}
}
return result;
}
/**
* Returns the date/time that the even was logged.
* @return the time stamp
*/
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ssZ")
public Date getTimestamp() {
return this.timestamp;
}
/**
* Returns the user principal responsible for the event.
* @return the principal
*/
public String getPrincipal() {
return this.principal;
}
/**
* Returns the type of event.
* @return the event type
*/
public String getType() {
return this.type;
}
/**
* Returns the event data.
* @return the event data
*/
public Map<String, Object> getData() {
return this.data;
}
@Override
public String toString() {
return "AuditEvent [timestamp=" + this.timestamp + ", principal=" + this.principal
+ ", type=" + this.type + ", data=" + this.data + "]";
}
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2012-2016 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
*
* http://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.boot.actuate.audit;
import java.util.Date;
import java.util.List;
/**
* Repository for {@link AuditEvent}s.
*
* @author Dave Syer
* @author Vedran Pavic
*/
public interface AuditEventRepository {
/**
* Log an event.
* @param event the audit event to log
*/
void add(AuditEvent event);
/**
* Find audit events since the time provided.
* @param after timestamp of earliest result required (or {@code null} if
* unrestricted)
* @return audit events
* @since 1.4.0
*/
List<AuditEvent> find(Date after);
/**
* Find audit events relating to the specified principal since the time provided.
* @param principal the principal name to search for (or {@code null} if unrestricted)
* @param after timestamp of earliest result required (or {@code null} if
* unrestricted)
* @return audit events relating to the principal
*/
List<AuditEvent> find(String principal, Date after);
/**
* Find audit events of specified type relating to the specified principal since the
* time provided.
* @param principal the principal name to search for (or {@code null} if unrestricted)
* @param after timestamp of earliest result required (or {@code null} if
* unrestricted)
* @param type the event type to search for (or {@code null} if unrestricted)
* @return audit events of specified type relating to the principal
* @since 1.4.0
*/
List<AuditEvent> find(String principal, Date after, String type);
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.audit;
import java.util.Date;
import java.util.List;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.util.Assert;
/**
* {@link Endpoint} to expose audit events.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
@Endpoint(id = "auditevents")
public class AuditEventsEndpoint {
private final AuditEventRepository auditEventRepository;
public AuditEventsEndpoint(AuditEventRepository auditEventRepository) {
Assert.notNull(auditEventRepository, "AuditEventRepository must not be null");
this.auditEventRepository = auditEventRepository;
}
@ReadOperation
public AuditEventsDescriptor eventsWithPrincipalDateAfterAndType(String principal,
Date after, String type) {
return new AuditEventsDescriptor(
this.auditEventRepository.find(principal, after, type));
}
/**
* A description of an application's {@link AuditEvent audit events}. Primarily
* intended for serialization to JSON.
*/
public static final class AuditEventsDescriptor {
private final List<AuditEvent> events;
private AuditEventsDescriptor(List<AuditEvent> events) {
this.events = events;
}
public List<AuditEvent> getEvents() {
return this.events;
}
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.audit;
import java.util.Date;
import org.springframework.boot.actuate.audit.AuditEventsEndpoint.AuditEventsDescriptor;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.jmx.annotation.JmxEndpointExtension;
/**
* JMX-specific extension of the {@link AuditEventsEndpoint}.
*
* @author Vedran Pavic
* @author Andy Wilkinson
* @since 2.0.0
*/
@JmxEndpointExtension(endpoint = AuditEventsEndpoint.class)
public class AuditEventsJmxEndpointExtension {
private final AuditEventsEndpoint delegate;
public AuditEventsJmxEndpointExtension(AuditEventsEndpoint delegate) {
this.delegate = delegate;
}
@ReadOperation
public AuditEventsDescriptor eventsWithDateAfter(Date dateAfter) {
return this.delegate.eventsWithPrincipalDateAfterAndType(null, dateAfter, null);
}
@ReadOperation
public AuditEventsDescriptor eventsWithPrincipalAndDateAfter(String principal,
Date dateAfter) {
return this.delegate.eventsWithPrincipalDateAfterAndType(principal, dateAfter,
null);
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.audit;
import java.util.Date;
import org.springframework.boot.actuate.audit.AuditEventsEndpoint.AuditEventsDescriptor;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpointExtension;
import org.springframework.lang.Nullable;
/**
* {@link WebEndpointExtension} for the {@link AuditEventsEndpoint}.
*
* @author Vedran Pavic
* @since 2.0.0
*/
@WebEndpointExtension(endpoint = AuditEventsEndpoint.class)
public class AuditEventsWebEndpointExtension {
private final AuditEventsEndpoint delegate;
public AuditEventsWebEndpointExtension(AuditEventsEndpoint delegate) {
this.delegate = delegate;
}
@ReadOperation
public WebEndpointResponse<AuditEventsDescriptor> eventsWithPrincipalDateAfterAndType(
@Nullable String principal, Date after, @Nullable String type) {
AuditEventsDescriptor auditEvents = this.delegate
.eventsWithPrincipalDateAfterAndType(principal, after, type);
return new WebEndpointResponse<>(auditEvents);
}
}

View File

@@ -0,0 +1,109 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.audit;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import org.springframework.util.Assert;
/**
* In-memory {@link AuditEventRepository} implementation.
*
* @author Dave Syer
* @author Phillip Webb
* @author Vedran Pavic
*/
public class InMemoryAuditEventRepository implements AuditEventRepository {
private static final int DEFAULT_CAPACITY = 1000;
private final Object monitor = new Object();
/**
* Circular buffer of the event with tail pointing to the last element.
*/
private AuditEvent[] events;
private volatile int tail = -1;
public InMemoryAuditEventRepository() {
this(DEFAULT_CAPACITY);
}
public InMemoryAuditEventRepository(int capacity) {
this.events = new AuditEvent[capacity];
}
/**
* Set the capacity of this event repository.
* @param capacity the capacity
*/
public void setCapacity(int capacity) {
synchronized (this.monitor) {
this.events = new AuditEvent[capacity];
}
}
@Override
public void add(AuditEvent event) {
Assert.notNull(event, "AuditEvent must not be null");
synchronized (this.monitor) {
this.tail = (this.tail + 1) % this.events.length;
this.events[this.tail] = event;
}
}
@Override
public List<AuditEvent> find(Date after) {
return find(null, after, null);
}
@Override
public List<AuditEvent> find(String principal, Date after) {
return find(principal, after, null);
}
@Override
public List<AuditEvent> find(String principal, Date after, String type) {
LinkedList<AuditEvent> events = new LinkedList<>();
synchronized (this.monitor) {
for (int i = 0; i < this.events.length; i++) {
AuditEvent event = resolveTailEvent(i);
if (event != null && isMatch(principal, after, type, event)) {
events.addFirst(event);
}
}
}
return events;
}
private boolean isMatch(String principal, Date after, String type, AuditEvent event) {
boolean match = true;
match = match && (principal == null || event.getPrincipal().equals(principal));
match = match && (after == null || event.getTimestamp().compareTo(after) >= 0);
match = match && (type == null || event.getType().equals(type));
return match;
}
private AuditEvent resolveTailEvent(int offset) {
int index = ((this.tail + this.events.length - offset) % this.events.length);
return this.events[index];
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2012-2016 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
*
* http://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.boot.actuate.audit.listener;
import org.springframework.boot.actuate.audit.AuditEvent;
import org.springframework.context.ApplicationListener;
/**
* Abstract {@link ApplicationListener} to handle {@link AuditApplicationEvent}s.
*
* @author Vedran Pavic
* @since 1.4.0
*/
public abstract class AbstractAuditListener
implements ApplicationListener<AuditApplicationEvent> {
@Override
public void onApplicationEvent(AuditApplicationEvent event) {
onAuditEvent(event.getAuditEvent());
}
protected abstract void onAuditEvent(AuditEvent event);
}

View File

@@ -0,0 +1,93 @@
/*
* Copyright 2012-2016 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
*
* http://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.boot.actuate.audit.listener;
import java.util.Date;
import java.util.Map;
import org.springframework.boot.actuate.audit.AuditEvent;
import org.springframework.context.ApplicationEvent;
import org.springframework.util.Assert;
/**
* Spring {@link ApplicationEvent} to encapsulate {@link AuditEvent}s.
*
* @author Dave Syer
*/
public class AuditApplicationEvent extends ApplicationEvent {
private final AuditEvent auditEvent;
/**
* Create a new {@link AuditApplicationEvent} that wraps a newly created
* {@link AuditEvent}.
* @param principal the principal
* @param type the event type
* @param data the event data
* @see AuditEvent#AuditEvent(String, String, Map)
*/
public AuditApplicationEvent(String principal, String type,
Map<String, Object> data) {
this(new AuditEvent(principal, type, data));
}
/**
* Create a new {@link AuditApplicationEvent} that wraps a newly created
* {@link AuditEvent}.
* @param principal the principal
* @param type the event type
* @param data the event data
* @see AuditEvent#AuditEvent(String, String, String...)
*/
public AuditApplicationEvent(String principal, String type, String... data) {
this(new AuditEvent(principal, type, data));
}
/**
* Create a new {@link AuditApplicationEvent} that wraps a newly created
* {@link AuditEvent}.
* @param timestamp the time stamp
* @param principal the principal
* @param type the event type
* @param data the event data
* @see AuditEvent#AuditEvent(Date, String, String, Map)
*/
public AuditApplicationEvent(Date timestamp, String principal, String type,
Map<String, Object> data) {
this(new AuditEvent(timestamp, principal, type, data));
}
/**
* Create a new {@link AuditApplicationEvent} that wraps the specified
* {@link AuditEvent}.
* @param auditEvent the source of this event
*/
public AuditApplicationEvent(AuditEvent auditEvent) {
super(auditEvent);
Assert.notNull(auditEvent, "AuditEvent must not be null");
this.auditEvent = auditEvent;
}
/**
* Get the audit event.
* @return the audit event
*/
public AuditEvent getAuditEvent() {
return this.auditEvent;
}
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2012-2016 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
*
* http://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.boot.actuate.audit.listener;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.actuate.audit.AuditEvent;
import org.springframework.boot.actuate.audit.AuditEventRepository;
/**
* The default {@link AbstractAuditListener} implementation. Listens for
* {@link AuditApplicationEvent}s and stores them in a {@link AuditEventRepository}.
*
* @author Dave Syer
* @author Stephane Nicoll
* @author Vedran Pavic
*/
public class AuditListener extends AbstractAuditListener {
private static final Log logger = LogFactory.getLog(AuditListener.class);
private final AuditEventRepository auditEventRepository;
public AuditListener(AuditEventRepository auditEventRepository) {
this.auditEventRepository = auditEventRepository;
}
@Override
protected void onAuditEvent(AuditEvent event) {
if (logger.isDebugEnabled()) {
logger.debug(event);
}
this.auditEventRepository.add(event);
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2015 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
*
* http://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.
*/
/**
* Actuator auditing listeners.
*/
package org.springframework.boot.actuate.audit.listener;

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2015 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
*
* http://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.
*/
/**
* Core actuator auditing classes.
*/
package org.springframework.boot.actuate.audit;

View File

@@ -0,0 +1,185 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.beans;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.util.StringUtils;
/**
* {@link Endpoint} to expose details of an application's beans, grouped by application
* context.
*
* @author Dave Syer
* @author Andy Wilkinson
* @since 2.0.0
*/
@Endpoint(id = "beans")
public class BeansEndpoint {
private final ConfigurableApplicationContext context;
/**
* Creates a new {@code BeansEndpoint} that will describe the beans in the given
* {@code context} and all of its ancestors.
*
* @param context the application context
* @see ConfigurableApplicationContext#getParent()
*/
public BeansEndpoint(ConfigurableApplicationContext context) {
this.context = context;
}
@ReadOperation
public ApplicationContextDescriptor beans() {
return ApplicationContextDescriptor.describing(this.context);
}
/**
* A description of an application context, primarily intended for serialization to
* JSON.
*/
public static final class ApplicationContextDescriptor {
private final String id;
private final Map<String, BeanDescriptor> beans;
private final ApplicationContextDescriptor parent;
private ApplicationContextDescriptor(String id, Map<String, BeanDescriptor> beans,
ApplicationContextDescriptor parent) {
this.id = id;
this.beans = beans;
this.parent = parent;
}
public String getId() {
return this.id;
}
public ApplicationContextDescriptor getParent() {
return this.parent;
}
public Map<String, BeanDescriptor> getBeans() {
return this.beans;
}
private static ApplicationContextDescriptor describing(
ConfigurableApplicationContext context) {
if (context == null) {
return null;
}
return new ApplicationContextDescriptor(context.getId(),
describeBeans(context.getBeanFactory()),
describing(getConfigurableParent(context)));
}
private static Map<String, BeanDescriptor> describeBeans(
ConfigurableListableBeanFactory beanFactory) {
Map<String, BeanDescriptor> beans = new HashMap<>();
for (String beanName : beanFactory.getBeanDefinitionNames()) {
BeanDefinition definition = beanFactory.getBeanDefinition(beanName);
if (isBeanEligible(beanName, definition, beanFactory)) {
beans.put(beanName, describeBean(beanName, definition, beanFactory));
}
}
return beans;
}
private static BeanDescriptor describeBean(String name, BeanDefinition definition,
ConfigurableListableBeanFactory factory) {
return new BeanDescriptor(factory.getAliases(name), definition.getScope(),
factory.getType(name), definition.getResourceDescription(),
factory.getDependenciesForBean(name));
}
private static boolean isBeanEligible(String beanName, BeanDefinition bd,
ConfigurableBeanFactory bf) {
return (bd.getRole() != BeanDefinition.ROLE_INFRASTRUCTURE
&& (!bd.isLazyInit() || bf.containsSingleton(beanName)));
}
private static ConfigurableApplicationContext getConfigurableParent(
ConfigurableApplicationContext context) {
ApplicationContext parent = context.getParent();
if (parent instanceof ConfigurableApplicationContext) {
return (ConfigurableApplicationContext) parent;
}
return null;
}
}
/**
* A description of a bean in an application context, primarily intended for
* serialization to JSON.
*/
public static final class BeanDescriptor {
private final String[] aliases;
private final String scope;
private final Class<?> type;
private final String resource;
private final String[] dependencies;
private BeanDescriptor(String[] aliases, String scope, Class<?> type,
String resource, String[] dependencies) {
this.aliases = aliases;
this.scope = StringUtils.hasText(scope) ? scope
: BeanDefinition.SCOPE_SINGLETON;
this.type = type;
this.resource = resource;
this.dependencies = dependencies;
}
public String[] getAliases() {
return this.aliases;
}
public String getScope() {
return this.scope;
}
public Class<?> getType() {
return this.type;
}
public String getResource() {
return this.resource;
}
public String[] getDependencies() {
return this.dependencies;
}
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2017 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
*
* http://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.
*/
/**
* Actuator support relating to Spring Beans.
*/
package org.springframework.boot.actuate.beans;

View File

@@ -0,0 +1,68 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.cassandra;
import com.datastax.driver.core.ResultSet;
import com.datastax.driver.core.querybuilder.QueryBuilder;
import com.datastax.driver.core.querybuilder.Select;
import org.springframework.boot.actuate.health.AbstractHealthIndicator;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.data.cassandra.core.CassandraOperations;
import org.springframework.util.Assert;
/**
* Simple implementation of a {@link HealthIndicator} returning status information for
* Cassandra data stores.
*
* @author Julien Dubois
* @since 2.0.0
*/
public class CassandraHealthIndicator extends AbstractHealthIndicator {
private CassandraOperations cassandraOperations;
/**
* Create a new {@link CassandraHealthIndicator} instance.
* @param cassandraOperations the Cassandra operations
*/
public CassandraHealthIndicator(CassandraOperations cassandraOperations) {
Assert.notNull(cassandraOperations, "CassandraOperations must not be null");
this.cassandraOperations = cassandraOperations;
}
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
try {
Select select = QueryBuilder.select("release_version").from("system",
"local");
ResultSet results = this.cassandraOperations.getCqlOperations()
.queryForResultSet(select);
if (results.isExhausted()) {
builder.up();
return;
}
String version = results.one().getString(0);
builder.up().withDetail("version", version);
}
catch (Exception ex) {
builder.down(ex);
}
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2017 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
*
* http://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.
*/
/**
* Actuator support for Cassandra.
*/
package org.springframework.boot.actuate.cassandra;

View File

@@ -0,0 +1,83 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.context;
import java.util.Collections;
import java.util.Map;
import org.springframework.beans.BeansException;
import org.springframework.boot.actuate.endpoint.DefaultEnablement;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
/**
* {@link Endpoint} to shutdown the {@link ApplicationContext}.
*
* @author Dave Syer
* @author Christian Dupuis
* @author Andy Wilkinson
* @since 2.0.0
*/
@Endpoint(id = "shutdown", defaultEnablement = DefaultEnablement.DISABLED)
public class ShutdownEndpoint implements ApplicationContextAware {
private static final Map<String, String> NO_CONTEXT_MESSAGE = Collections
.unmodifiableMap(Collections.<String, String>singletonMap("message",
"No context to shutdown."));
private static final Map<String, String> SHUTDOWN_MESSAGE = Collections
.unmodifiableMap(Collections.<String, String>singletonMap("message",
"Shutting down, bye..."));
private ConfigurableApplicationContext context;
@WriteOperation
public Map<String, String> shutdown() {
if (this.context == null) {
return NO_CONTEXT_MESSAGE;
}
try {
return SHUTDOWN_MESSAGE;
}
finally {
Thread thread = new Thread(this::performShutdown);
thread.setContextClassLoader(getClass().getClassLoader());
thread.start();
}
}
private void performShutdown() {
try {
Thread.sleep(500L);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
this.context.close();
}
@Override
public void setApplicationContext(ApplicationContext context) throws BeansException {
if (context instanceof ConfigurableApplicationContext) {
this.context = (ConfigurableApplicationContext) context;
}
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2017 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
*
* http://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.
*/
/**
* Actuator support relating to Spring Context.
*/
package org.springframework.boot.actuate.context;

View File

@@ -0,0 +1,410 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.context.properties;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.introspect.Annotated;
import com.fasterxml.jackson.databind.introspect.AnnotatedMethod;
import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
import com.fasterxml.jackson.databind.ser.BeanPropertyWriter;
import com.fasterxml.jackson.databind.ser.BeanSerializerFactory;
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
import com.fasterxml.jackson.databind.ser.PropertyWriter;
import com.fasterxml.jackson.databind.ser.SerializerFactory;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import org.springframework.beans.BeansException;
import org.springframework.boot.actuate.endpoint.Sanitizer;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.context.properties.ConfigurationBeanFactoryMetaData;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
* {@link Endpoint} to expose application properties from {@link ConfigurationProperties}
* annotated beans.
*
* <p>
* To protect sensitive information from being exposed, certain property values are masked
* if their names end with a set of configurable values (default "password" and "secret").
* Configure property names by using {@code endpoints.configprops.keys_to_sanitize} in
* your Spring Boot application configuration.
*
* @author Christian Dupuis
* @author Dave Syer
* @author Stephane Nicoll
* @since 2.0.0
*/
@Endpoint(id = "configprops")
public class ConfigurationPropertiesReportEndpoint implements ApplicationContextAware {
private static final String CGLIB_FILTER_ID = "cglibFilter";
private final Sanitizer sanitizer = new Sanitizer();
private ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext context) throws BeansException {
this.context = context;
}
public void setKeysToSanitize(String... keysToSanitize) {
this.sanitizer.setKeysToSanitize(keysToSanitize);
}
@ReadOperation
public ConfigurationPropertiesDescriptor configurationProperties() {
return extract(this.context);
}
private ConfigurationPropertiesDescriptor extract(ApplicationContext context) {
ObjectMapper mapper = new ObjectMapper();
configureObjectMapper(mapper);
return describeConfigurationProperties(context, mapper);
}
private ConfigurationPropertiesDescriptor describeConfigurationProperties(
ApplicationContext context, ObjectMapper mapper) {
if (context == null) {
return null;
}
ConfigurationBeanFactoryMetaData beanFactoryMetaData = getBeanFactoryMetaData(
context);
Map<String, Object> beans = getConfigurationPropertiesBeans(context,
beanFactoryMetaData);
Map<String, ConfigurationPropertiesBeanDescriptor> beanDescriptors = new HashMap<>();
for (Map.Entry<String, Object> entry : beans.entrySet()) {
String beanName = entry.getKey();
Object bean = entry.getValue();
String prefix = extractPrefix(context, beanFactoryMetaData, beanName, bean);
beanDescriptors.put(beanName, new ConfigurationPropertiesBeanDescriptor(
prefix, sanitize(prefix, safeSerialize(mapper, bean, prefix))));
}
return new ConfigurationPropertiesDescriptor(beanDescriptors,
describeConfigurationProperties(context.getParent(), mapper));
}
private ConfigurationBeanFactoryMetaData getBeanFactoryMetaData(
ApplicationContext context) {
Map<String, ConfigurationBeanFactoryMetaData> beans = context
.getBeansOfType(ConfigurationBeanFactoryMetaData.class);
if (beans.size() == 1) {
return beans.values().iterator().next();
}
return null;
}
private Map<String, Object> getConfigurationPropertiesBeans(
ApplicationContext context,
ConfigurationBeanFactoryMetaData beanFactoryMetaData) {
Map<String, Object> beans = new HashMap<>();
beans.putAll(context.getBeansWithAnnotation(ConfigurationProperties.class));
if (beanFactoryMetaData != null) {
beans.putAll(beanFactoryMetaData
.getBeansWithFactoryAnnotation(ConfigurationProperties.class));
}
return beans;
}
/**
* Cautiously serialize the bean to a map (returning a map with an error message
* instead of throwing an exception if there is a problem).
* @param mapper the object mapper
* @param bean the source bean
* @param prefix the prefix
* @return the serialized instance
*/
private Map<String, Object> safeSerialize(ObjectMapper mapper, Object bean,
String prefix) {
try {
@SuppressWarnings("unchecked")
Map<String, Object> result = new HashMap<>(
mapper.convertValue(bean, Map.class));
return result;
}
catch (Exception ex) {
return new HashMap<>(Collections.<String, Object>singletonMap("error",
"Cannot serialize '" + prefix + "'"));
}
}
/**
* Configure Jackson's {@link ObjectMapper} to be used to serialize the
* {@link ConfigurationProperties} objects into a {@link Map} structure.
* @param mapper the object mapper
*/
protected void configureObjectMapper(ObjectMapper mapper) {
mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
mapper.setSerializationInclusion(Include.NON_NULL);
applyCglibFilters(mapper);
applySerializationModifier(mapper);
}
/**
* Ensure only bindable and non-cyclic bean properties are reported.
* @param mapper the object mapper
*/
private void applySerializationModifier(ObjectMapper mapper) {
SerializerFactory factory = BeanSerializerFactory.instance
.withSerializerModifier(new GenericSerializerModifier());
mapper.setSerializerFactory(factory);
}
/**
* Configure PropertyFilter to make sure Jackson doesn't process CGLIB generated bean
* properties.
* @param mapper the object mapper
*/
private void applyCglibFilters(ObjectMapper mapper) {
mapper.setAnnotationIntrospector(new CglibAnnotationIntrospector());
mapper.setFilterProvider(new SimpleFilterProvider().addFilter(CGLIB_FILTER_ID,
new CglibBeanPropertyFilter()));
}
/**
* Extract configuration prefix from {@link ConfigurationProperties} annotation.
* @param context the application context
* @param beanFactoryMetaData the bean factory meta-data
* @param beanName the bean name
* @param bean the bean
* @return the prefix
*/
private String extractPrefix(ApplicationContext context,
ConfigurationBeanFactoryMetaData beanFactoryMetaData, String beanName,
Object bean) {
ConfigurationProperties annotation = context.findAnnotationOnBean(beanName,
ConfigurationProperties.class);
if (beanFactoryMetaData != null) {
ConfigurationProperties override = beanFactoryMetaData
.findFactoryAnnotation(beanName, ConfigurationProperties.class);
if (override != null) {
// The @Bean-level @ConfigurationProperties overrides the one at type
// level when binding. Arguably we should render them both, but this one
// might be the most relevant for a starting point.
annotation = override;
}
}
return annotation.prefix();
}
/**
* Sanitize all unwanted configuration properties to avoid leaking of sensitive
* information.
* @param prefix the property prefix
* @param map the source map
* @return the sanitized map
*/
@SuppressWarnings("unchecked")
private Map<String, Object> sanitize(String prefix, Map<String, Object> map) {
for (Map.Entry<String, Object> entry : map.entrySet()) {
String key = entry.getKey();
String qualifiedKey = (prefix.isEmpty() ? prefix : prefix + ".") + key;
Object value = entry.getValue();
if (value instanceof Map) {
map.put(key, sanitize(qualifiedKey, (Map<String, Object>) value));
}
else if (value instanceof List) {
map.put(key, sanitize(qualifiedKey, (List<Object>) value));
}
else {
value = this.sanitizer.sanitize(key, value);
value = this.sanitizer.sanitize(qualifiedKey, value);
map.put(key, value);
}
}
return map;
}
@SuppressWarnings("unchecked")
private List<Object> sanitize(String prefix, List<Object> list) {
List<Object> sanitized = new ArrayList<>();
for (Object item : list) {
if (item instanceof Map) {
sanitized.add(sanitize(prefix, (Map<String, Object>) item));
}
else if (item instanceof List) {
sanitized.add(sanitize(prefix, (List<Object>) item));
}
else {
sanitized.add(this.sanitizer.sanitize(prefix, item));
}
}
return sanitized;
}
/**
* Extension to {@link JacksonAnnotationIntrospector} to suppress CGLIB generated bean
* properties.
*/
@SuppressWarnings("serial")
private static class CglibAnnotationIntrospector
extends JacksonAnnotationIntrospector {
@Override
public Object findFilterId(Annotated a) {
Object id = super.findFilterId(a);
if (id == null) {
id = CGLIB_FILTER_ID;
}
return id;
}
}
/**
* {@link SimpleBeanPropertyFilter} to filter out all bean properties whose names
* start with '$$'.
*/
private static class CglibBeanPropertyFilter extends SimpleBeanPropertyFilter {
@Override
protected boolean include(BeanPropertyWriter writer) {
return include(writer.getFullName().getSimpleName());
}
@Override
protected boolean include(PropertyWriter writer) {
return include(writer.getFullName().getSimpleName());
}
private boolean include(String name) {
return !name.startsWith("$$");
}
}
/**
* {@link BeanSerializerModifier} to return only relevant configuration properties.
*/
protected static class GenericSerializerModifier extends BeanSerializerModifier {
@Override
public List<BeanPropertyWriter> changeProperties(SerializationConfig config,
BeanDescription beanDesc, List<BeanPropertyWriter> beanProperties) {
List<BeanPropertyWriter> result = new ArrayList<>();
for (BeanPropertyWriter writer : beanProperties) {
boolean readable = isReadable(beanDesc, writer);
if (readable) {
result.add(writer);
}
}
return result;
}
private boolean isReadable(BeanDescription beanDesc, BeanPropertyWriter writer) {
Class<?> parentType = beanDesc.getType().getRawClass();
Class<?> type = writer.getType().getRawClass();
AnnotatedMethod setter = findSetter(beanDesc, writer);
// If there's a setter, we assume it's OK to report on the value,
// similarly, if there's no setter but the package names match, we assume
// that its a nested class used solely for binding to config props, so it
// should be kosher. Lists and Maps are also auto-detected by default since
// that's what the metadata generator does. This filter is not used if there
// is JSON metadata for the property, so it's mainly for user-defined beans.
return (setter != null)
|| ClassUtils.getPackageName(parentType)
.equals(ClassUtils.getPackageName(type))
|| Map.class.isAssignableFrom(type)
|| Collection.class.isAssignableFrom(type);
}
private AnnotatedMethod findSetter(BeanDescription beanDesc,
BeanPropertyWriter writer) {
String name = "set" + StringUtils.capitalize(writer.getName());
Class<?> type = writer.getType().getRawClass();
AnnotatedMethod setter = beanDesc.findMethod(name, new Class<?>[] { type });
// The enabled property of endpoints returns a boolean primitive but is set
// using a Boolean class
if (setter == null && type.equals(Boolean.TYPE)) {
setter = beanDesc.findMethod(name, new Class<?>[] { Boolean.class });
}
return setter;
}
}
/**
* A description of an application context's {@link ConfigurationProperties} beans.
* Primarily intended for serialization to JSON.
*/
public static final class ConfigurationPropertiesDescriptor {
private final Map<String, ConfigurationPropertiesBeanDescriptor> beans;
private final ConfigurationPropertiesDescriptor parent;
private ConfigurationPropertiesDescriptor(
Map<String, ConfigurationPropertiesBeanDescriptor> beans,
ConfigurationPropertiesDescriptor parent) {
this.beans = beans;
this.parent = parent;
}
public Map<String, ConfigurationPropertiesBeanDescriptor> getBeans() {
return this.beans;
}
public ConfigurationPropertiesDescriptor getParent() {
return this.parent;
}
}
/**
* A description of a {@link ConfigurationProperties} bean. Primarily intended for
* serialization to JSON.
*/
public static final class ConfigurationPropertiesBeanDescriptor {
private final String prefix;
private final Map<String, Object> properties;
private ConfigurationPropertiesBeanDescriptor(String prefix,
Map<String, Object> properties) {
this.prefix = prefix;
this.properties = properties;
}
public String getPrefix() {
return this.prefix;
}
public Map<String, Object> getProperties() {
return this.properties;
}
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2017 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
*
* http://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.
*/
/**
* Actuator support relating to external configuration properties.
*/
package org.springframework.boot.actuate.context.properties;

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.couchbase;
import java.util.List;
import com.couchbase.client.java.util.features.Version;
import org.springframework.boot.actuate.health.AbstractHealthIndicator;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.data.couchbase.core.CouchbaseOperations;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* {@link HealthIndicator} for Couchbase.
*
* @author Eddú Meléndez
* @since 2.0.0
*/
public class CouchbaseHealthIndicator extends AbstractHealthIndicator {
private CouchbaseOperations couchbaseOperations;
public CouchbaseHealthIndicator(CouchbaseOperations couchbaseOperations) {
Assert.notNull(couchbaseOperations, "CouchbaseOperations must not be null");
this.couchbaseOperations = couchbaseOperations;
}
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
List<Version> versions = this.couchbaseOperations.getCouchbaseClusterInfo()
.getAllVersions();
builder.up().withDetail("versions",
StringUtils.collectionToCommaDelimitedString(versions));
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2017 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
*
* http://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.
*/
/**
* Actuator support for Couchbase.
*/
package org.springframework.boot.actuate.couchbase;

View File

@@ -0,0 +1,99 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.elasticsearch;
import java.util.List;
import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest;
import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse;
import org.elasticsearch.client.Client;
import org.elasticsearch.client.Requests;
import org.springframework.boot.actuate.health.AbstractHealthIndicator;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.util.ObjectUtils;
/**
* {@link HealthIndicator} for an Elasticsearch cluster.
*
* @author Binwei Yang
* @author Andy Wilkinson
* @since 2.0.0
*/
public class ElasticsearchHealthIndicator extends AbstractHealthIndicator {
private static final String[] ALL_INDICES = { "_all" };
private final Client client;
private final String[] indices;
private final long responseTimeout;
/**
* Create a new {@link ElasticsearchHealthIndicator} instance.
* @param client the Elasticsearch client
* @param responseTimeout the request timeout in milliseconds
* @param indices the indices to check
*/
public ElasticsearchHealthIndicator(Client client, long responseTimeout,
List<String> indices) {
this(client, responseTimeout, (indices == null ? (String[]) null
: indices.toArray(new String[indices.size()])));
}
/**
* Create a new {@link ElasticsearchHealthIndicator} instance.
* @param client the Elasticsearch client
* @param responseTimeout the request timeout in milliseconds
* @param indices the indices to check
*/
public ElasticsearchHealthIndicator(Client client, long responseTimeout,
String... indices) {
this.client = client;
this.responseTimeout = responseTimeout;
this.indices = indices;
}
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
ClusterHealthRequest request = Requests.clusterHealthRequest(
ObjectUtils.isEmpty(this.indices) ? ALL_INDICES : this.indices);
ClusterHealthResponse response = this.client.admin().cluster().health(request)
.actionGet(this.responseTimeout);
switch (response.getStatus()) {
case GREEN:
case YELLOW:
builder.up();
break;
case RED:
default:
builder.down();
break;
}
builder.withDetail("clusterName", response.getClusterName());
builder.withDetail("numberOfNodes", response.getNumberOfNodes());
builder.withDetail("numberOfDataNodes", response.getNumberOfDataNodes());
builder.withDetail("activePrimaryShards", response.getActivePrimaryShards());
builder.withDetail("activeShards", response.getActiveShards());
builder.withDetail("relocatingShards", response.getRelocatingShards());
builder.withDetail("initializingShards", response.getInitializingShards());
builder.withDetail("unassignedShards", response.getUnassignedShards());
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.elasticsearch;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import io.searchbox.client.JestClient;
import io.searchbox.client.JestResult;
import io.searchbox.indices.Stats;
import org.springframework.boot.actuate.health.AbstractHealthIndicator;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
/**
* {@link HealthIndicator} for Elasticsearch using a {@link JestClient}.
*
* @author Stephane Nicoll
* @since 2.0.0
*/
public class ElasticsearchJestHealthIndicator extends AbstractHealthIndicator {
private final JestClient jestClient;
private final JsonParser jsonParser = new JsonParser();
public ElasticsearchJestHealthIndicator(JestClient jestClient) {
this.jestClient = jestClient;
}
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
JestResult aliases = this.jestClient.execute(new Stats.Builder().build());
JsonElement root = this.jsonParser.parse(aliases.getJsonString());
JsonObject shards = root.getAsJsonObject().get("_shards").getAsJsonObject();
int failedShards = shards.get("failed").getAsInt();
if (failedShards != 0) {
builder.outOfService();
}
else {
builder.up();
}
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2017 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
*
* http://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.
*/
/**
* Actuator support for Elasticsearch.
*/
package org.springframework.boot.actuate.elasticsearch;

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint;
/**
* Enumerate the enablement options for an endpoint.
*
* @author Stephane Nicoll
* @since 2.0.0
*/
public enum DefaultEnablement {
/**
* The endpoint is enabled unless explicitly disabled.
*/
ENABLED,
/**
* The endpoint is disabled unless explicitly enabled.
*/
DISABLED,
/**
* The endpoint's enablement defaults to the "default" settings.
*/
NEUTRAL
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint;
import java.util.Collection;
/**
* Discovers endpoints and provides an {@link EndpointInfo} for each of them.
*
* @param <T> the type of the operation
* @author Andy Wilkinson
* @author Stephane Nicoll
* @since 2.0.0
*/
@FunctionalInterface
public interface EndpointDiscoverer<T extends Operation> {
/**
* Perform endpoint discovery.
* @return the discovered endpoints
*/
Collection<EndpointInfo<T>> discoverEndpoints();
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint;
/**
* An enumeration of the available exposure technologies for an endpoint.
*
* @author Stephane Nicoll
* @since 2.0.0
*/
public enum EndpointExposure {
/**
* Expose the endpoint as a JMX MBean.
*/
JMX(true),
/**
* Expose the endpoint as a Web endpoint.
*/
WEB(false);
private final boolean enabledByDefault;
EndpointExposure(boolean enabledByDefault) {
this.enabledByDefault = enabledByDefault;
}
public boolean isEnabledByDefault() {
return this.enabledByDefault;
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint;
import java.util.Collection;
/**
* Information describing an endpoint.
*
* @param <T> the type of the endpoint's operations
* @author Andy Wilkinson
* @since 2.0.0
*/
public class EndpointInfo<T extends Operation> {
private final String id;
private final DefaultEnablement defaultEnablement;
private final Collection<T> operations;
/**
* Creates a new {@code EndpointInfo} describing an endpoint with the given {@code id}
* and {@code operations}.
* @param id the id of the endpoint
* @param defaultEnablement the {@link DefaultEnablement} of the endpoint
* @param operations the operations of the endpoint
*/
public EndpointInfo(String id, DefaultEnablement defaultEnablement,
Collection<T> operations) {
this.id = id;
this.defaultEnablement = defaultEnablement;
this.operations = operations;
}
/**
* Returns the id of the endpoint.
* @return the id
*/
public String getId() {
return this.id;
}
/**
* Return the {@link DefaultEnablement} of the endpoint.
* @return the default enablement
*/
public DefaultEnablement getDefaultEnablement() {
return this.defaultEnablement;
}
/**
* Returns the operations of the endpoint.
* @return the operations
*/
public Collection<T> getOperations() {
return this.operations;
}
}

View File

@@ -0,0 +1,120 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
/**
* Utility class that can be used to filter source data using a name regular expression.
* Detects if the name is classic "single value" key or a regular expression. Subclasses
* must provide implementations of {@link #getValue(Object, String)} and
* {@link #getNames(Object, NameCallback)}.
*
* @param <T> the source data type
* @author Phillip Webb
* @author Sergei Egorov
* @author Andy Wilkinson
* @author Dylian Bego
*/
abstract class NamePatternFilter<T> {
private static final String[] REGEX_PARTS = { "*", "$", "^", "+", "[" };
private final T source;
NamePatternFilter(T source) {
this.source = source;
}
public Map<String, Object> getResults(String name) {
Pattern pattern = compilePatternIfNecessary(name);
if (pattern == null) {
Object value = getValue(this.source, name);
Map<String, Object> result = new HashMap<>();
result.put(name, value);
return result;
}
ResultCollectingNameCallback resultCollector = new ResultCollectingNameCallback(
pattern);
getNames(this.source, resultCollector);
return resultCollector.getResults();
}
private Pattern compilePatternIfNecessary(String name) {
for (String part : REGEX_PARTS) {
if (name.contains(part)) {
try {
return Pattern.compile(name);
}
catch (PatternSyntaxException ex) {
return null;
}
}
}
return null;
}
protected abstract void getNames(T source, NameCallback callback);
protected abstract Object getValue(T source, String name);
protected abstract Object getOptionalValue(T source, String name);
/**
* Callback used to add a name.
*/
interface NameCallback {
void addName(String name);
}
/**
* {@link NameCallback} implementation to collect results.
*/
private class ResultCollectingNameCallback implements NameCallback {
private final Pattern pattern;
private final Map<String, Object> results = new LinkedHashMap<>();
ResultCollectingNameCallback(Pattern pattern) {
this.pattern = pattern;
}
@Override
public void addName(String name) {
if (this.pattern.matcher(name).matches()) {
Object value = getOptionalValue(NamePatternFilter.this.source, name);
if (value != null) {
this.results.put(name, value);
}
}
}
public Map<String, Object> getResults() {
return this.results;
}
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint;
/**
* An operation on an endpoint.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
public class Operation {
private final OperationType type;
private final OperationInvoker invoker;
private final boolean blocking;
/**
* Creates a new {@code EndpointOperation} for an operation of the given {@code type}.
* The operation can be performed using the given {@code operationInvoker}.
* @param type the type of the operation
* @param operationInvoker used to perform the operation
* @param blocking whether or not this is a blocking operation
*/
public Operation(OperationType type, OperationInvoker operationInvoker,
boolean blocking) {
this.type = type;
this.invoker = operationInvoker;
this.blocking = blocking;
}
/**
* Returns the {@link OperationType type} of the operation.
* @return the type
*/
public OperationType getType() {
return this.type;
}
/**
* Returns the {@code OperationInvoker} that can be used to invoke this endpoint
* operation.
* @return the operation invoker
*/
public OperationInvoker getInvoker() {
return this.invoker;
}
/**
* Whether or not this is a blocking operation.
*
* @return {@code true} if it is a blocking operation, otherwise {@code false}.
*/
public boolean isBlocking() {
return this.blocking;
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint;
import java.util.Map;
/**
* An {@code OperationInvoker} is used to invoke an operation on an endpoint.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
@FunctionalInterface
public interface OperationInvoker {
/**
* Invoke the underlying operation using the given {@code arguments}.
* @param arguments the arguments to pass to the operation
* @return the result of the operation, may be {@code null}
*/
Object invoke(Map<String, Object> arguments);
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint;
/**
* An {@code OperationParameterMapper} is used to map parameters to the required type when
* invoking an endpoint.
*
* @author Stephane Nicoll
* @since 2.0.0
*/
@FunctionalInterface
public interface OperationParameterMapper {
/**
* Map the specified {@code input} parameter to the given {@code parameterType}.
* @param input a parameter value
* @param parameterType the required type of the parameter
* @return a value suitable for that parameter
* @param <T> the actual type of the parameter
* @throws ParameterMappingException when a mapping failure occurs
*/
<T> T mapParameter(Object input, Class<T> parameterType);
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint;
/**
* An enumeration of the different types of operation supported by an endpoint.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
public enum OperationType {
/**
* A read operation.
*/
READ,
/**
* A write operation.
*/
WRITE,
/**
* A delete operation.
*/
DELETE
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint;
/**
* A {@code ParameterMappingException} is thrown when a failure occurs during
* {@link OperationParameterMapper#mapParameter(Object, Class) operation parameter
* mapping}.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
public class ParameterMappingException extends RuntimeException {
private final Object input;
private final Class<?> type;
/**
* Creates a new {@code ParameterMappingException} for a failure that occurred when
* trying to map the given {@code input} to the given {@code type}.
*
* @param input the input that was being mapped
* @param type the type that was being mapped to
* @param cause the cause of the mapping failure
*/
public ParameterMappingException(Object input, Class<?> type, Throwable cause) {
super("Failed to map " + input + " of type " + input.getClass() + " to type "
+ type, cause);
this.input = input;
this.type = type;
}
/**
* Returns the input that was to be mapped.
* @return the input
*/
public Object getInput() {
return this.input;
}
/**
* Returns the type to be mapped to.
* @return the type
*/
public Class<?> getType() {
return this.type;
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint;
import java.util.Set;
import org.springframework.util.StringUtils;
/**
* {@link RuntimeException} thrown when an endpoint invocation does not contain required
* parameters.
*
* @author Madhura Bhave
* @since 2.0.0
*/
public class ParametersMissingException extends RuntimeException {
private final Set<String> parameters;
public ParametersMissingException(Set<String> parameters) {
super("Failed to invoke operation because the following required "
+ "parameters were missing: "
+ StringUtils.collectionToCommaDelimitedString(parameters));
this.parameters = parameters;
}
/**
* Returns the parameters that were missing.
* @return the parameters
*/
public Set<String> getParameters() {
return this.parameters;
}
}

View File

@@ -0,0 +1,96 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.lang.Nullable;
import org.springframework.util.ReflectionUtils;
/**
* An {@code OperationInvoker} that invokes an operation using reflection.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
public class ReflectiveOperationInvoker implements OperationInvoker {
private final OperationParameterMapper parameterMapper;
private final Object target;
private final Method method;
/**
* Creates a new {code ReflectiveOperationInvoker} that will invoke the given
* {@code method} on the given {@code target}. The given {@code parameterMapper} will
* be used to map parameters to the required types.
* @param parameterMapper the parameter mapper
* @param target the target of the reflective call
* @param method the method to call
*/
public ReflectiveOperationInvoker(OperationParameterMapper parameterMapper,
Object target, Method method) {
this.parameterMapper = parameterMapper;
this.target = target;
ReflectionUtils.makeAccessible(method);
this.method = method;
}
@Override
public Object invoke(Map<String, Object> arguments) {
validateRequiredParameters(arguments);
return ReflectionUtils.invokeMethod(this.method, this.target,
resolveArguments(arguments));
}
private void validateRequiredParameters(Map<String, Object> arguments) {
Set<String> missingParameters = Stream.of(this.method.getParameters())
.filter((p) -> isMissing(p, arguments)).map(Parameter::getName)
.collect(Collectors.toSet());
if (!missingParameters.isEmpty()) {
throw new ParametersMissingException(missingParameters);
}
}
private boolean isMissing(Parameter parameter, Map<String, Object> arguments) {
Object resolved = arguments.get(parameter.getName());
return (resolved == null && !isExplicitNullable(parameter));
}
private boolean isExplicitNullable(Parameter parameter) {
return (parameter.getAnnotationsByType(Nullable.class).length != 0);
}
private Object[] resolveArguments(Map<String, Object> arguments) {
return Stream.of(this.method.getParameters())
.map((parameter) -> resolveArgument(parameter, arguments))
.collect(Collectors.collectingAndThen(Collectors.toList(),
(list) -> list.toArray(new Object[list.size()])));
}
private Object resolveArgument(Parameter parameter, Map<String, Object> arguments) {
Object resolved = arguments.get(parameter.getName());
return this.parameterMapper.mapParameter(resolved, parameter.getType());
}
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint;
import java.util.regex.Pattern;
import org.springframework.util.Assert;
/**
* Strategy that be be used by endpoint implementations to sanitize potentially sensitive
* keys.
*
* @author Christian Dupuis
* @author Toshiaki Maki
* @author Phillip Webb
* @author Nicolas Lejeune
* @author Stephane Nicoll
* @since 2.0.0
*/
public class Sanitizer {
private static final String[] REGEX_PARTS = { "*", "$", "^", "+" };
private Pattern[] keysToSanitize;
public Sanitizer() {
this("password", "secret", "key", "token", ".*credentials.*", "vcap_services");
}
public Sanitizer(String... keysToSanitize) {
setKeysToSanitize(keysToSanitize);
}
/**
* Keys that should be sanitized. Keys can be simple strings that the property ends
* with or regular expressions.
* @param keysToSanitize the keys to sanitize
*/
public void setKeysToSanitize(String... keysToSanitize) {
Assert.notNull(keysToSanitize, "KeysToSanitize must not be null");
this.keysToSanitize = new Pattern[keysToSanitize.length];
for (int i = 0; i < keysToSanitize.length; i++) {
this.keysToSanitize[i] = getPattern(keysToSanitize[i]);
}
}
private Pattern getPattern(String value) {
if (isRegex(value)) {
return Pattern.compile(value, Pattern.CASE_INSENSITIVE);
}
return Pattern.compile(".*" + value + "$", Pattern.CASE_INSENSITIVE);
}
private boolean isRegex(String value) {
for (String part : REGEX_PARTS) {
if (value.contains(part)) {
return true;
}
}
return false;
}
/**
* Sanitize the given value if necessary.
* @param key the key to sanitize
* @param value the value
* @return the potentially sanitized value
*/
public Object sanitize(String key, Object value) {
for (Pattern pattern : this.keysToSanitize) {
if (pattern.matcher(key).matches()) {
return (value == null ? null : "******");
}
}
return value;
}
}

View File

@@ -0,0 +1,413 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint.annotation;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.boot.actuate.endpoint.DefaultEnablement;
import org.springframework.boot.actuate.endpoint.EndpointDiscoverer;
import org.springframework.boot.actuate.endpoint.EndpointExposure;
import org.springframework.boot.actuate.endpoint.EndpointInfo;
import org.springframework.boot.actuate.endpoint.Operation;
import org.springframework.boot.actuate.endpoint.OperationType;
import org.springframework.boot.actuate.endpoint.cache.CachingConfiguration;
import org.springframework.boot.actuate.endpoint.cache.CachingConfigurationFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.core.MethodIntrospector;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.ObjectUtils;
/**
* A base {@link EndpointDiscoverer} implementation that discovers {@link Endpoint} beans
* in an application context.
*
* @param <T> the type of the operation
* @param <K> the type of the operation key
* @author Andy Wilkinson
* @author Stephane Nicoll
* @since 2.0.0
*/
public abstract class AnnotationEndpointDiscoverer<T extends Operation, K>
implements EndpointDiscoverer<T> {
private final ApplicationContext applicationContext;
private final EndpointOperationFactory<T> operationFactory;
private final Function<T, K> operationKeyFactory;
private final CachingConfigurationFactory cachingConfigurationFactory;
protected AnnotationEndpointDiscoverer(ApplicationContext applicationContext,
EndpointOperationFactory<T> operationFactory,
Function<T, K> operationKeyFactory,
CachingConfigurationFactory cachingConfigurationFactory) {
this.applicationContext = applicationContext;
this.operationFactory = operationFactory;
this.operationKeyFactory = operationKeyFactory;
this.cachingConfigurationFactory = cachingConfigurationFactory;
}
/**
* Perform endpoint discovery, including discovery and merging of extensions.
* @param extensionType the annotation type of the extension
* @param exposure the {@link EndpointExposure} that should be considered
* @return the list of {@link EndpointInfo EndpointInfos} that describes the
* discovered endpoints matching the specified {@link EndpointExposure}
*/
protected Collection<EndpointInfoDescriptor<T, K>> discoverEndpoints(
Class<? extends Annotation> extensionType, EndpointExposure exposure) {
Map<Class<?>, EndpointInfo<T>> endpoints = discoverEndpoints(exposure);
Map<Class<?>, EndpointExtensionInfo<T>> extensions = discoverExtensions(endpoints,
extensionType, exposure);
Collection<EndpointInfoDescriptor<T, K>> result = new ArrayList<>();
endpoints.forEach((endpointClass, endpointInfo) -> {
EndpointExtensionInfo<T> extension = extensions.remove(endpointClass);
result.add(createDescriptor(endpointClass, endpointInfo, extension));
});
return result;
}
private Map<Class<?>, EndpointInfo<T>> discoverEndpoints(EndpointExposure exposure) {
String[] beanNames = BeanFactoryUtils.beanNamesForAnnotationIncludingAncestors(
this.applicationContext, Endpoint.class);
Map<Class<?>, EndpointInfo<T>> endpoints = new LinkedHashMap<>();
Map<String, EndpointInfo<T>> endpointsById = new LinkedHashMap<>();
for (String beanName : beanNames) {
Class<?> beanType = this.applicationContext.getType(beanName);
AnnotationAttributes attributes = AnnotatedElementUtils
.findMergedAnnotationAttributes(beanType, Endpoint.class, true, true);
if (isExposedOver(attributes, exposure)) {
EndpointInfo<T> info = createEndpointInfo(beanName, beanType, attributes);
EndpointInfo<T> previous = endpointsById.putIfAbsent(info.getId(), info);
Assert.state(previous == null, () -> "Found two endpoints with the id '"
+ info.getId() + "': " + info + " and " + previous);
endpoints.put(beanType, info);
}
}
return endpoints;
}
private EndpointInfo<T> createEndpointInfo(String beanName, Class<?> beanType,
AnnotationAttributes attributes) {
String id = attributes.getString("id");
DefaultEnablement defaultEnablement = (DefaultEnablement) attributes
.get("defaultEnablement");
Map<Method, T> operations = discoverOperations(id, beanName, beanType);
return new EndpointInfo<>(id, defaultEnablement, operations.values());
}
private Map<Class<?>, EndpointExtensionInfo<T>> discoverExtensions(
Map<Class<?>, EndpointInfo<T>> endpoints,
Class<? extends Annotation> extensionType, EndpointExposure exposure) {
if (extensionType == null) {
return Collections.emptyMap();
}
String[] beanNames = BeanFactoryUtils.beanNamesForAnnotationIncludingAncestors(
this.applicationContext, extensionType);
Map<Class<?>, EndpointExtensionInfo<T>> extensions = new HashMap<>();
for (String beanName : beanNames) {
Class<?> beanType = this.applicationContext.getType(beanName);
Class<?> endpointType = getEndpointType(extensionType, beanType);
AnnotationAttributes endpointAttributes = AnnotatedElementUtils
.getMergedAnnotationAttributes(endpointType, Endpoint.class);
Assert.state(isExposedOver(endpointAttributes, exposure),
"Invalid extension " + beanType.getName() + "': endpoint '"
+ endpointType.getName()
+ "' does not support such extension");
EndpointInfo<T> info = getEndpointInfo(endpoints, beanType, endpointType);
Map<Method, T> operations = discoverOperations(info.getId(), beanName,
beanType);
EndpointExtensionInfo<T> extension = new EndpointExtensionInfo<>(beanType,
operations.values());
EndpointExtensionInfo<T> previous = extensions.putIfAbsent(endpointType,
extension);
Assert.state(previous == null,
() -> "Found two extensions for the same endpoint '"
+ endpointType.getName() + "': "
+ extension.getExtensionType().getName() + " and "
+ previous.getExtensionType().getName());
}
return extensions;
}
private EndpointInfo<T> getEndpointInfo(Map<Class<?>, EndpointInfo<T>> endpoints,
Class<?> beanType, Class<?> endpointClass) {
EndpointInfo<T> endpoint = endpoints.get(endpointClass);
Assert.state(endpoint != null, "Invalid extension '" + beanType.getName()
+ "': no endpoint found with type '" + endpointClass.getName() + "'");
return endpoint;
}
private Class<?> getEndpointType(Class<? extends Annotation> extensionType,
Class<?> beanType) {
AnnotationAttributes attributes = AnnotatedElementUtils
.getMergedAnnotationAttributes(beanType, extensionType);
return (Class<?>) attributes.get("endpoint");
}
private EndpointInfoDescriptor<T, K> createDescriptor(Class<?> type,
EndpointInfo<T> info, EndpointExtensionInfo<T> extension) {
Map<OperationKey<K>, List<T>> operations = indexOperations(info.getId(), type,
info.getOperations());
if (extension != null) {
operations.putAll(indexOperations(info.getId(), extension.getExtensionType(),
extension.getOperations()));
return new EndpointInfoDescriptor<>(mergeEndpoint(info, extension),
operations);
}
return new EndpointInfoDescriptor<>(info, operations);
}
private EndpointInfo<T> mergeEndpoint(EndpointInfo<T> endpoint,
EndpointExtensionInfo<T> extension) {
Map<K, T> operations = new HashMap<>();
Consumer<T> consumer = (operation) -> operations
.put(this.operationKeyFactory.apply(operation), operation);
endpoint.getOperations().forEach(consumer);
extension.getOperations().forEach(consumer);
return new EndpointInfo<>(endpoint.getId(), endpoint.getDefaultEnablement(),
operations.values());
}
private Map<OperationKey<K>, List<T>> indexOperations(String endpointId,
Class<?> target, Collection<T> operations) {
LinkedMultiValueMap<OperationKey<K>, T> result = new LinkedMultiValueMap<>();
operations.forEach((operation) -> {
K key = this.operationKeyFactory.apply(operation);
result.add(new OperationKey<>(endpointId, target, key), operation);
});
return result;
}
private boolean isExposedOver(AnnotationAttributes attributes,
EndpointExposure exposure) {
if (exposure == null) {
return true;
}
EndpointExposure[] supported = (EndpointExposure[]) attributes.get("exposure");
return ObjectUtils.isEmpty(supported)
|| ObjectUtils.containsElement(supported, exposure);
}
private Map<Method, T> discoverOperations(String id, String name, Class<?> type) {
return MethodIntrospector.selectMethods(type,
(MethodIntrospector.MetadataLookup<T>) (
method) -> createOperationIfPossible(id, name, method));
}
private T createOperationIfPossible(String endpointId, String beanName,
Method method) {
T operation = createReadOperationIfPossible(endpointId, beanName, method);
if (operation != null) {
return operation;
}
operation = createWriteOperationIfPossible(endpointId, beanName, method);
if (operation != null) {
return operation;
}
return createDeleteOperationIfPossible(endpointId, beanName, method);
}
private T createReadOperationIfPossible(String endpointId, String beanName,
Method method) {
return createOperationIfPossible(endpointId, beanName, method,
ReadOperation.class, OperationType.READ);
}
private T createWriteOperationIfPossible(String endpointId, String beanName,
Method method) {
return createOperationIfPossible(endpointId, beanName, method,
WriteOperation.class, OperationType.WRITE);
}
private T createDeleteOperationIfPossible(String endpointId, String beanName,
Method method) {
return createOperationIfPossible(endpointId, beanName, method,
DeleteOperation.class, OperationType.DELETE);
}
private T createOperationIfPossible(String endpointId, String beanName, Method method,
Class<? extends Annotation> operationAnnotation,
OperationType operationType) {
AnnotationAttributes operationAttributes = AnnotatedElementUtils
.getMergedAnnotationAttributes(method, operationAnnotation);
if (operationAttributes == null) {
return null;
}
CachingConfiguration cachingConfiguration = this.cachingConfigurationFactory
.getCachingConfiguration(endpointId);
return this.operationFactory.createOperation(endpointId, operationAttributes,
this.applicationContext.getBean(beanName), method, operationType,
determineTimeToLive(cachingConfiguration, operationType, method));
}
private long determineTimeToLive(CachingConfiguration cachingConfiguration,
OperationType operationType, Method method) {
if (cachingConfiguration != null && cachingConfiguration.getTimeToLive() > 0
&& operationType == OperationType.READ
&& method.getParameters().length == 0) {
return cachingConfiguration.getTimeToLive();
}
return 0;
}
/**
* An {@code EndpointOperationFactory} creates an {@link Operation} for an operation
* on an endpoint.
*
* @param <T> the {@link Operation} type
*/
@FunctionalInterface
protected interface EndpointOperationFactory<T extends Operation> {
/**
* Creates an {@code EndpointOperation} for an operation on an endpoint.
* @param endpointId the id of the endpoint
* @param operationAttributes the annotation attributes for the operation
* @param target the target that implements the operation
* @param operationMethod the method on the bean that implements the operation
* @param operationType the type of the operation
* @param timeToLive the caching period in milliseconds
* @return the operation info that describes the operation
*/
T createOperation(String endpointId, AnnotationAttributes operationAttributes,
Object target, Method operationMethod, OperationType operationType,
long timeToLive);
}
/**
* Describes a tech-specific extension of an endpoint.
* @param <T> the type of the operation
*/
private static final class EndpointExtensionInfo<T extends Operation> {
private final Class<?> extensionType;
private final Collection<T> operations;
private EndpointExtensionInfo(Class<?> extensionType, Collection<T> operations) {
this.extensionType = extensionType;
this.operations = operations;
}
private Class<?> getExtensionType() {
return this.extensionType;
}
private Collection<T> getOperations() {
return this.operations;
}
}
/**
* Describes an {@link EndpointInfo endpoint} and whether or not it is valid.
*
* @param <T> the type of the operation
* @param <K> the type of the operation key
*/
protected static class EndpointInfoDescriptor<T extends Operation, K> {
private final EndpointInfo<T> endpointInfo;
private final Map<OperationKey<K>, List<T>> operations;
protected EndpointInfoDescriptor(EndpointInfo<T> endpointInfo,
Map<OperationKey<K>, List<T>> operations) {
this.endpointInfo = endpointInfo;
this.operations = operations;
}
public EndpointInfo<T> getEndpointInfo() {
return this.endpointInfo;
}
public Map<OperationKey<K>, List<T>> findDuplicateOperations() {
Map<OperationKey<K>, List<T>> duplicateOperations = new HashMap<>();
this.operations.forEach((k, list) -> {
if (list.size() > 1) {
duplicateOperations.put(k, list);
}
});
return duplicateOperations;
}
}
/**
* Define the key of an operation in the context of an operation's implementation.
*
* @param <K> the type of the key
*/
protected static final class OperationKey<K> {
private final String endpointId;
private final Class<?> endpointType;
private final K key;
public OperationKey(String endpointId, Class<?> endpointType, K key) {
this.endpointId = endpointId;
this.endpointType = endpointType;
this.key = key;
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
OperationKey<?> other = (OperationKey<?>) o;
Boolean result = true;
result = result && this.endpointId.equals(other.endpointId);
result = result && this.endpointType.equals(other.endpointType);
result = result && this.key.equals(other.key);
return result;
}
@Override
public int hashCode() {
int result = this.endpointId.hashCode();
result = 31 * result + this.endpointType.hashCode();
result = 31 * result + this.key.hashCode();
return result;
}
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Identifies a method on an {@link Endpoint} as being a delete operation.
*
* @author Stephane Nicoll
* @author Andy Wilkinson
* @since 2.0.0
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DeleteOperation {
/**
* The media type of the result of the operation.
*
* @return the media type
*/
String[] produces() default {};
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.actuate.endpoint.DefaultEnablement;
import org.springframework.boot.actuate.endpoint.EndpointExposure;
/**
* Identifies a type as being an endpoint.
*
* @author Andy Wilkinson
* @since 2.0.0
* @see AnnotationEndpointDiscoverer
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Endpoint {
/**
* The id of the endpoint.
* @return the id
*/
String id();
/**
* Defines the {@link EndpointExposure technologies} over which the endpoint should be
* exposed. By default, all technologies are supported.
* @return the supported endpoint exposure technologies
*/
EndpointExposure[] exposure() default {};
/**
* Defines the {@link DefaultEnablement} of the endpoint. By default, the endpoint's
* enablement defaults to the "default" settings.
* @return the default enablement
*/
DefaultEnablement defaultEnablement() default DefaultEnablement.NEUTRAL;
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Identifies a method on an {@link Endpoint} as being a read operation.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ReadOperation {
/**
* The media type of the result of the operation.
*
* @return the media type
*/
String[] produces() default {};
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* A {@code Selector} can be used on a parameter of an {@link Endpoint} method to indicate
* that the parameter is used to select a subset of the endpoint's data.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Selector {
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Identifies a method on an {@link Endpoint} as being a write operation.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WriteOperation {
/**
* The media type of the result of the operation.
*
* @return the media type
*/
String[] produces() default {};
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2017 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
*
* http://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.
*/
/**
* Annotation support for actuator endpoints.
*/
package org.springframework.boot.actuate.endpoint.annotation;

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint.cache;
/**
* The caching configuration of an endpoint.
*
* @author Stephane Nicoll
* @since 2.0.0
*/
public class CachingConfiguration {
private final long timeToLive;
/**
* Create a new instance with the given {@code timeToLive}.
* @param timeToLive the time to live of an operation result in milliseconds
*/
public CachingConfiguration(long timeToLive) {
this.timeToLive = timeToLive;
}
/**
* Returns the time to live of a cached operation result.
* @return the time to live of an operation result
*/
public long getTimeToLive() {
return this.timeToLive;
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint.cache;
/**
* Factory used to return a {@link CachingConfiguration} for an endpoint ID.
*
* @author Stephane Nicoll
* @since 2.0.0
*/
@FunctionalInterface
public interface CachingConfigurationFactory {
/**
* Return the {@link CachingConfiguration} for the given endpoint ID.
* @param endpointId the endpoint ID
* @return the caching configuration
*/
CachingConfiguration getCachingConfiguration(String endpointId);
}

View File

@@ -0,0 +1,96 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint.cache;
import java.util.Map;
import org.springframework.boot.actuate.endpoint.OperationInvoker;
import org.springframework.util.Assert;
/**
* An {@link OperationInvoker} that caches the response of an operation with a
* configurable time to live.
*
* @author Stephane Nicoll
* @since 2.0.0
*/
public class CachingOperationInvoker implements OperationInvoker {
private final OperationInvoker target;
private final long timeToLive;
private volatile CachedResponse cachedResponse;
/**
* Create a new instance with the target {@link OperationInvoker} to use to compute
* the response and the time to live for the cache.
* @param target the {@link OperationInvoker} this instance wraps
* @param timeToLive the maximum time in milliseconds that a response can be cached
*/
public CachingOperationInvoker(OperationInvoker target, long timeToLive) {
Assert.state(timeToLive > 0, "TimeToLive must be strictly positive");
this.target = target;
this.timeToLive = timeToLive;
}
/**
* Return the maximum time in milliseconds that a response can be cached.
* @return the time to live of a response
*/
public long getTimeToLive() {
return this.timeToLive;
}
@Override
public Object invoke(Map<String, Object> arguments) {
long accessTime = System.currentTimeMillis();
CachedResponse cached = this.cachedResponse;
if (cached == null || cached.isStale(accessTime, this.timeToLive)) {
Object response = this.target.invoke(arguments);
this.cachedResponse = new CachedResponse(response, accessTime);
return response;
}
return cached.getResponse();
}
/**
* A cached response that encapsulates the response itself and the time at which it
* was created.
*/
static class CachedResponse {
private final Object response;
private final long creationTime;
CachedResponse(Object response, long creationTime) {
this.response = response;
this.creationTime = creationTime;
}
public boolean isStale(long accessTime, long timeToLive) {
return (accessTime - this.creationTime) >= timeToLive;
}
public Object getResponse() {
return this.response;
}
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2017 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
*
* http://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.
*/
/**
* Caching support for actuator endpoints.
*/
package org.springframework.boot.actuate.endpoint.cache;

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint.convert;
import org.springframework.boot.actuate.endpoint.OperationParameterMapper;
import org.springframework.boot.actuate.endpoint.ParameterMappingException;
import org.springframework.boot.context.properties.bind.convert.BinderConversionService;
import org.springframework.core.convert.ConversionService;
import org.springframework.format.support.DefaultFormattingConversionService;
/**
* {@link OperationParameterMapper} that uses a {@link ConversionService} to map parameter
* values if necessary.
*
* @author Stephane Nicoll
* @author Phillip Webb
* @since 2.0.0
*/
public class ConversionServiceOperationParameterMapper
implements OperationParameterMapper {
private final ConversionService conversionService;
public ConversionServiceOperationParameterMapper() {
this(createDefaultConversionService());
}
/**
* Create a new instance with the {@link ConversionService} to use.
* @param conversionService the conversion service
*/
public ConversionServiceOperationParameterMapper(
ConversionService conversionService) {
this.conversionService = new BinderConversionService(conversionService);
}
@Override
public <T> T mapParameter(Object input, Class<T> parameterType) {
try {
return this.conversionService.convert(input, parameterType);
}
catch (Exception ex) {
throw new ParameterMappingException(input, parameterType, ex);
}
}
private static ConversionService createDefaultConversionService() {
DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
IsoOffsetDateTimeConverter.registerConverter(conversionService);
return conversionService;
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint.convert;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterRegistry;
import org.springframework.util.StringUtils;
/**
* A {@link String} to {@link Date} {@link Converter} that uses
* {@link DateTimeFormatter#ISO_OFFSET_DATE_TIME ISO offset} parsing.
*
* @author Andy Wilkinson
* @author Stephane Nicoll
* @since 2.0.0
*/
public class IsoOffsetDateTimeConverter implements Converter<String, Date> {
@Override
public Date convert(String source) {
if (StringUtils.hasLength(source)) {
OffsetDateTime offsetDateTime = OffsetDateTime.parse(source,
DateTimeFormatter.ISO_OFFSET_DATE_TIME);
return new Date(TimeUnit.SECONDS.toMillis(offsetDateTime.toEpochSecond()));
}
return null;
}
public static void registerConverter(ConverterRegistry registry) {
registry.addConverter(new IsoOffsetDateTimeConverter());
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2017 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
*
* http://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.
*/
/**
* Converter support for actuator endpoints.
*/
package org.springframework.boot.actuate.endpoint.convert;

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint.http;
/**
* Media types that can be consumed and produced by Actuator endpoints.
*
* @author Andy Wilkinson
* @author Madhura Bhave
* @since 2.0.0
*/
public final class ActuatorMediaType {
/**
* Constant for the Actuator V1 media type.
*/
public static final String V1_JSON = "application/vnd.spring-boot.actuator.v1+json";
/**
* Constant for the Actuator V2 media type.
*/
public static final String V2_JSON = "application/vnd.spring-boot.actuator.v2+json";
private ActuatorMediaType() {
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2017 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
*
* http://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.
*/
/**
* Actuator endpoint HTTP concerns (not tied to any specific implementation).
*/
package org.springframework.boot.actuate.endpoint.http;

View File

@@ -0,0 +1,143 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint.jmx;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import javax.management.Attribute;
import javax.management.AttributeList;
import javax.management.AttributeNotFoundException;
import javax.management.DynamicMBean;
import javax.management.InvalidAttributeValueException;
import javax.management.MBeanException;
import javax.management.MBeanInfo;
import javax.management.ReflectionException;
import reactor.core.publisher.Mono;
import org.springframework.boot.actuate.endpoint.EndpointInfo;
import org.springframework.boot.actuate.endpoint.ParameterMappingException;
import org.springframework.boot.actuate.endpoint.ParametersMissingException;
import org.springframework.util.ClassUtils;
/**
* A {@link DynamicMBean} that invokes operations on an {@link EndpointInfo endpoint}.
*
* @author Stephane Nicoll
* @author Andy Wilkinson
* @since 2.0.0
* @see EndpointMBeanInfoAssembler
*/
public class EndpointMBean implements DynamicMBean {
private static final boolean REACTOR_PRESENT = ClassUtils.isPresent(
"reactor.core.publisher.Mono", EndpointMBean.class.getClassLoader());
private final Function<Object, Object> operationResponseConverter;
private final EndpointMBeanInfo endpointInfo;
EndpointMBean(Function<Object, Object> operationResponseConverter,
EndpointMBeanInfo endpointInfo) {
this.operationResponseConverter = operationResponseConverter;
this.endpointInfo = endpointInfo;
}
/**
* Return the id of the related endpoint.
* @return the endpoint id
*/
public String getEndpointId() {
return this.endpointInfo.getEndpointId();
}
@Override
public MBeanInfo getMBeanInfo() {
return this.endpointInfo.getMbeanInfo();
}
@Override
public Object invoke(String actionName, Object[] params, String[] signature)
throws MBeanException, ReflectionException {
JmxEndpointOperation operation = this.endpointInfo.getOperations()
.get(actionName);
if (operation != null) {
Map<String, Object> arguments = getArguments(params,
operation.getParameters());
try {
Object result = operation.getInvoker().invoke(arguments);
if (REACTOR_PRESENT) {
result = ReactiveHandler.handle(result);
}
return this.operationResponseConverter.apply(result);
}
catch (ParametersMissingException | ParameterMappingException ex) {
throw new IllegalArgumentException(ex.getMessage());
}
}
throw new ReflectionException(new IllegalArgumentException(
String.format("Endpoint with id '%s' has no operation named %s",
this.endpointInfo.getEndpointId(), actionName)));
}
private Map<String, Object> getArguments(Object[] params,
List<JmxEndpointOperationParameterInfo> parameters) {
Map<String, Object> arguments = new HashMap<>();
for (int i = 0; i < params.length; i++) {
arguments.put(parameters.get(i).getName(), params[i]);
}
return arguments;
}
@Override
public Object getAttribute(String attribute)
throws AttributeNotFoundException, MBeanException, ReflectionException {
throw new AttributeNotFoundException();
}
@Override
public void setAttribute(Attribute attribute) throws AttributeNotFoundException,
InvalidAttributeValueException, MBeanException, ReflectionException {
throw new AttributeNotFoundException();
}
@Override
public AttributeList getAttributes(String[] attributes) {
return new AttributeList();
}
@Override
public AttributeList setAttributes(AttributeList attributes) {
return new AttributeList();
}
private static class ReactiveHandler {
public static Object handle(Object result) {
if (result instanceof Mono) {
return ((Mono<?>) result).block();
}
return result;
}
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint.jmx;
import java.util.Map;
import javax.management.MBeanInfo;
import org.springframework.boot.actuate.endpoint.EndpointInfo;
import org.springframework.boot.actuate.endpoint.Operation;
/**
* The {@link MBeanInfo} for a particular {@link EndpointInfo endpoint}. Maps operation
* names to an {@link Operation}.
*
* @author Stephane Nicoll
* @since 2.0.0
*/
public final class EndpointMBeanInfo {
private final String endpointId;
private final MBeanInfo mBeanInfo;
private final Map<String, JmxEndpointOperation> operations;
public EndpointMBeanInfo(String endpointId, MBeanInfo mBeanInfo,
Map<String, JmxEndpointOperation> operations) {
this.endpointId = endpointId;
this.mBeanInfo = mBeanInfo;
this.operations = operations;
}
public String getEndpointId() {
return this.endpointId;
}
public MBeanInfo getMbeanInfo() {
return this.mBeanInfo;
}
public Map<String, JmxEndpointOperation> getOperations() {
return this.operations;
}
}

View File

@@ -0,0 +1,126 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint.jmx;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;
import javax.management.MBeanInfo;
import javax.management.MBeanOperationInfo;
import javax.management.MBeanParameterInfo;
import javax.management.modelmbean.ModelMBeanAttributeInfo;
import javax.management.modelmbean.ModelMBeanConstructorInfo;
import javax.management.modelmbean.ModelMBeanInfoSupport;
import javax.management.modelmbean.ModelMBeanNotificationInfo;
import javax.management.modelmbean.ModelMBeanOperationInfo;
import org.springframework.boot.actuate.endpoint.EndpointInfo;
import org.springframework.boot.actuate.endpoint.OperationType;
/**
* Gathers the management operations of a particular {@link EndpointInfo endpoint}.
*
* @author Stephane Nicoll
* @author Andy Wilkinson
*/
class EndpointMBeanInfoAssembler {
private final JmxOperationResponseMapper responseMapper;
EndpointMBeanInfoAssembler(JmxOperationResponseMapper responseMapper) {
this.responseMapper = responseMapper;
}
/**
* Creates the {@link EndpointMBeanInfo} for the specified {@link EndpointInfo
* endpoint}.
* @param endpointInfo the endpoint to handle
* @return the mbean info for the endpoint
*/
EndpointMBeanInfo createEndpointMBeanInfo(
EndpointInfo<JmxEndpointOperation> endpointInfo) {
Map<String, OperationInfos> operationsMapping = getOperationInfo(endpointInfo);
ModelMBeanOperationInfo[] operationsMBeanInfo = operationsMapping.values()
.stream().map((t) -> t.mBeanOperationInfo).collect(Collectors.toList())
.toArray(new ModelMBeanOperationInfo[] {});
Map<String, JmxEndpointOperation> operationsInfo = new LinkedHashMap<>();
operationsMapping.forEach((name, t) -> operationsInfo.put(name, t.operation));
MBeanInfo info = new ModelMBeanInfoSupport(EndpointMBean.class.getName(),
getDescription(endpointInfo), new ModelMBeanAttributeInfo[0],
new ModelMBeanConstructorInfo[0], operationsMBeanInfo,
new ModelMBeanNotificationInfo[0]);
return new EndpointMBeanInfo(endpointInfo.getId(), info, operationsInfo);
}
private String getDescription(EndpointInfo<?> endpointInfo) {
return "MBean operations for endpoint " + endpointInfo.getId();
}
private Map<String, OperationInfos> getOperationInfo(
EndpointInfo<JmxEndpointOperation> endpointInfo) {
Map<String, OperationInfos> operationInfos = new HashMap<>();
endpointInfo.getOperations().forEach((operationInfo) -> {
String name = operationInfo.getOperationName();
ModelMBeanOperationInfo mBeanOperationInfo = new ModelMBeanOperationInfo(
operationInfo.getOperationName(), operationInfo.getDescription(),
getMBeanParameterInfos(operationInfo), this.responseMapper
.mapResponseType(operationInfo.getOutputType()).getName(),
mapOperationType(operationInfo.getType()));
operationInfos.put(name,
new OperationInfos(mBeanOperationInfo, operationInfo));
});
return operationInfos;
}
private MBeanParameterInfo[] getMBeanParameterInfos(JmxEndpointOperation operation) {
return operation.getParameters().stream()
.map((operationParameter) -> new MBeanParameterInfo(
operationParameter.getName(),
operationParameter.getType().getName(),
operationParameter.getDescription()))
.collect(Collectors.collectingAndThen(Collectors.toList(),
(parameterInfos) -> parameterInfos
.toArray(new MBeanParameterInfo[parameterInfos.size()])));
}
private int mapOperationType(OperationType type) {
if (type == OperationType.READ) {
return MBeanOperationInfo.INFO;
}
if (type == OperationType.WRITE || type == OperationType.DELETE) {
return MBeanOperationInfo.ACTION;
}
return MBeanOperationInfo.UNKNOWN;
}
private static class OperationInfos {
private final ModelMBeanOperationInfo mBeanOperationInfo;
private final JmxEndpointOperation operation;
OperationInfos(ModelMBeanOperationInfo mBeanOperationInfo,
JmxEndpointOperation operation) {
this.mBeanOperationInfo = mBeanOperationInfo;
this.operation = operation;
}
}
}

View File

@@ -0,0 +1,115 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint.jmx;
import javax.management.InstanceNotFoundException;
import javax.management.MBeanRegistrationException;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.jmx.JmxException;
import org.springframework.jmx.export.MBeanExportException;
import org.springframework.jmx.export.MBeanExporter;
import org.springframework.util.Assert;
/**
* JMX Registrar for {@link EndpointMBean}.
*
* @author Stephane Nicoll
* @since 2.0.0
* @see EndpointObjectNameFactory
*/
public class EndpointMBeanRegistrar {
private static final Log logger = LogFactory.getLog(EndpointMBeanRegistrar.class);
private final MBeanServer mBeanServer;
private final EndpointObjectNameFactory objectNameFactory;
/**
* Create a new instance with the {@link MBeanExporter} and
* {@link EndpointObjectNameFactory} to use.
* @param mBeanServer the mbean exporter
* @param objectNameFactory the {@link ObjectName} factory
*/
public EndpointMBeanRegistrar(MBeanServer mBeanServer,
EndpointObjectNameFactory objectNameFactory) {
Assert.notNull(mBeanServer, "MBeanServer must not be null");
Assert.notNull(objectNameFactory, "ObjectNameFactory must not be null");
this.mBeanServer = mBeanServer;
this.objectNameFactory = objectNameFactory;
}
/**
* Register the specified {@link EndpointMBean} and return its {@link ObjectName}.
* @param endpoint the endpoint to register
* @return the {@link ObjectName} used to register the {@code endpoint}
*/
public ObjectName registerEndpointMBean(EndpointMBean endpoint) {
Assert.notNull(endpoint, "Endpoint must not be null");
try {
if (logger.isDebugEnabled()) {
logger.debug("Registering endpoint with id '" + endpoint.getEndpointId()
+ "' to the JMX domain");
}
ObjectName objectName = this.objectNameFactory.generate(endpoint);
this.mBeanServer.registerMBean(endpoint, objectName);
return objectName;
}
catch (MalformedObjectNameException ex) {
throw new IllegalStateException("Invalid ObjectName for endpoint with id '"
+ endpoint.getEndpointId() + "'", ex);
}
catch (Exception ex) {
throw new MBeanExportException(
"Failed to register MBean for endpoint with id '"
+ endpoint.getEndpointId() + "'",
ex);
}
}
/**
* Unregister the specified {@link ObjectName} if necessary.
* @param objectName the {@link ObjectName} of the endpoint to unregister
* @return {@code true} if the endpoint was unregistered, {@code false} if no such
* endpoint was found
*/
public boolean unregisterEndpointMbean(ObjectName objectName) {
try {
if (logger.isDebugEnabled()) {
logger.debug("Unregister endpoint with ObjectName '" + objectName + "' "
+ "from the JMX domain");
}
this.mBeanServer.unregisterMBean(objectName);
return true;
}
catch (InstanceNotFoundException ex) {
return false;
}
catch (MBeanRegistrationException ex) {
throw new JmxException(
"Failed to unregister MBean with ObjectName '" + objectName + "'",
ex);
}
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint.jmx;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
/**
* A factory to create an {@link ObjectName} for an {@link EndpointMBean}.
*
* @author Stephane Nicoll
* @since 2.0.0
*/
@FunctionalInterface
public interface EndpointObjectNameFactory {
/**
* Generate an {@link ObjectName} for the specified {@link EndpointMBean endpoint}.
* @param mBean the endpoint to handle
* @return the {@link ObjectName} to use for the endpoint
* @throws MalformedObjectNameException if the object name is invalid
*/
ObjectName generate(EndpointMBean mBean) throws MalformedObjectNameException;
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint.jmx;
import java.util.Collection;
import java.util.stream.Collectors;
import org.springframework.boot.actuate.endpoint.EndpointInfo;
/**
* A factory for creating JMX MBeans for endpoint operations.
*
* @author Stephane Nicoll
* @author Andy Wilkinson
* @since 2.0.0
*/
public class JmxEndpointMBeanFactory {
private final EndpointMBeanInfoAssembler assembler;
private final JmxOperationResponseMapper resultMapper;
/**
* Create a new {@link JmxEndpointMBeanFactory} instance that will use the given
* {@code responseMapper} to convert an operation's response to a JMX-friendly form.
* @param responseMapper the response mapper
*/
public JmxEndpointMBeanFactory(JmxOperationResponseMapper responseMapper) {
this.assembler = new EndpointMBeanInfoAssembler(responseMapper);
this.resultMapper = responseMapper;
}
/**
* Creates MBeans for the given {@code endpoints}.
* @param endpoints the endpoints
* @return the MBeans
*/
public Collection<EndpointMBean> createMBeans(
Collection<EndpointInfo<JmxEndpointOperation>> endpoints) {
return endpoints.stream().map(this::createMBean).collect(Collectors.toList());
}
private EndpointMBean createMBean(EndpointInfo<JmxEndpointOperation> endpointInfo) {
EndpointMBeanInfo endpointMBeanInfo = this.assembler
.createEndpointMBeanInfo(endpointInfo);
return new EndpointMBean(this.resultMapper::mapResponse, endpointMBeanInfo);
}
}

View File

@@ -0,0 +1,95 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint.jmx;
import java.util.Collections;
import java.util.List;
import org.springframework.boot.actuate.endpoint.Operation;
import org.springframework.boot.actuate.endpoint.OperationInvoker;
import org.springframework.boot.actuate.endpoint.OperationType;
/**
* An operation on a JMX endpoint.
*
* @author Stephane Nicoll
* @author Andy Wilkinson
* @since 2.0.0
*/
public class JmxEndpointOperation extends Operation {
private final String operationName;
private final Class<?> outputType;
private final String description;
private final List<JmxEndpointOperationParameterInfo> parameters;
/**
* Creates a new {@code JmxEndpointOperation} for an operation of the given
* {@code type}. The operation can be performed using the given {@code invoker}.
* @param type the type of the operation
* @param invoker used to perform the operation
* @param operationName the name of the operation
* @param outputType the type of the output of the operation
* @param description the description of the operation
* @param parameters the parameters of the operation
*/
public JmxEndpointOperation(OperationType type, OperationInvoker invoker,
String operationName, Class<?> outputType, String description,
List<JmxEndpointOperationParameterInfo> parameters) {
super(type, invoker, true);
this.operationName = operationName;
this.outputType = outputType;
this.description = description;
this.parameters = parameters;
}
/**
* Returns the name of the operation.
* @return the operation name
*/
public String getOperationName() {
return this.operationName;
}
/**
* Returns the type of the output of the operation.
* @return the output type
*/
public Class<?> getOutputType() {
return this.outputType;
}
/**
* Returns the description of the operation.
* @return the operation description
*/
public String getDescription() {
return this.description;
}
/**
* Returns the parameters of the operation.
* @return the operation parameters
*/
public List<JmxEndpointOperationParameterInfo> getParameters() {
return Collections.unmodifiableList(this.parameters);
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint.jmx;
/**
* Describes the parameters of an operation on a JMX endpoint.
*
* @author Stephane Nicoll
* @since 2.0.0
*/
public class JmxEndpointOperationParameterInfo {
private final String name;
private final Class<?> type;
private final String description;
public JmxEndpointOperationParameterInfo(String name, Class<?> type,
String description) {
this.name = name;
this.type = type;
this.description = description;
}
public String getName() {
return this.name;
}
public Class<?> getType() {
return this.type;
}
/**
* Return the description of the parameter or {@code null} if none is available.
* @return the description or {@code null}
*/
public String getDescription() {
return this.description;
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint.jmx;
/**
* A {@code JmxOperationResponseMapper} maps an operation's response to a JMX-friendly
* form.
*
* @author Stephane Nicoll
* @since 2.0.0
*/
public interface JmxOperationResponseMapper {
/**
* Map the operation's response so that it can be consumed by a JMX compliant client.
* @param response the operation's response
* @return the {@code response}, in a JMX compliant format
*/
Object mapResponse(Object response);
/**
* Map the response type to its JMX compliant counterpart.
* @param responseType the operation's response type
* @return the JMX compliant type
*/
Class<?> mapResponseType(Class<?> responseType);
}

View File

@@ -0,0 +1,200 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint.jmx.annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.springframework.boot.actuate.endpoint.EndpointExposure;
import org.springframework.boot.actuate.endpoint.EndpointInfo;
import org.springframework.boot.actuate.endpoint.OperationInvoker;
import org.springframework.boot.actuate.endpoint.OperationParameterMapper;
import org.springframework.boot.actuate.endpoint.OperationType;
import org.springframework.boot.actuate.endpoint.ReflectiveOperationInvoker;
import org.springframework.boot.actuate.endpoint.annotation.AnnotationEndpointDiscoverer;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.cache.CachingConfiguration;
import org.springframework.boot.actuate.endpoint.cache.CachingConfigurationFactory;
import org.springframework.boot.actuate.endpoint.cache.CachingOperationInvoker;
import org.springframework.boot.actuate.endpoint.jmx.JmxEndpointOperation;
import org.springframework.boot.actuate.endpoint.jmx.JmxEndpointOperationParameterInfo;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource;
import org.springframework.jmx.export.metadata.ManagedOperation;
import org.springframework.jmx.export.metadata.ManagedOperationParameter;
import org.springframework.util.StringUtils;
/**
* Discovers the {@link Endpoint endpoints} in an {@link ApplicationContext} with
* {@link JmxEndpointExtension JMX extensions} applied to them.
*
* @author Stephane Nicoll
* @author Andy Wilkinson
* @since 2.0.0
*/
public class JmxAnnotationEndpointDiscoverer
extends AnnotationEndpointDiscoverer<JmxEndpointOperation, String> {
private static final AnnotationJmxAttributeSource jmxAttributeSource = new AnnotationJmxAttributeSource();
/**
* Creates a new {@link JmxAnnotationEndpointDiscoverer} that will discover
* {@link Endpoint endpoints} and {@link JmxEndpointExtension jmx extensions} using
* the given {@link ApplicationContext}.
* @param applicationContext the application context
* @param parameterMapper the {@link OperationParameterMapper} used to convert
* arguments when an operation is invoked
* @param cachingConfigurationFactory the {@link CachingConfiguration} factory to use
*/
public JmxAnnotationEndpointDiscoverer(ApplicationContext applicationContext,
OperationParameterMapper parameterMapper,
CachingConfigurationFactory cachingConfigurationFactory) {
super(applicationContext, new JmxEndpointOperationFactory(parameterMapper),
JmxEndpointOperation::getOperationName, cachingConfigurationFactory);
}
@Override
public Collection<EndpointInfo<JmxEndpointOperation>> discoverEndpoints() {
Collection<EndpointInfoDescriptor<JmxEndpointOperation, String>> endpointDescriptors = discoverEndpoints(
JmxEndpointExtension.class, EndpointExposure.JMX);
verifyThatOperationsHaveDistinctName(endpointDescriptors);
return endpointDescriptors.stream().map(EndpointInfoDescriptor::getEndpointInfo)
.collect(Collectors.toList());
}
private void verifyThatOperationsHaveDistinctName(
Collection<EndpointInfoDescriptor<JmxEndpointOperation, String>> endpointDescriptors) {
List<List<JmxEndpointOperation>> clashes = new ArrayList<>();
endpointDescriptors.forEach((descriptor) -> clashes
.addAll(descriptor.findDuplicateOperations().values()));
if (!clashes.isEmpty()) {
StringBuilder message = new StringBuilder();
message.append(
String.format("Found multiple JMX operations with the same name:%n"));
clashes.forEach((clash) -> {
message.append(" ").append(clash.get(0).getOperationName())
.append(String.format(":%n"));
clash.forEach((operation) -> message.append(" ")
.append(String.format("%s%n", operation)));
});
throw new IllegalStateException(message.toString());
}
}
private static class JmxEndpointOperationFactory
implements EndpointOperationFactory<JmxEndpointOperation> {
private final OperationParameterMapper parameterMapper;
JmxEndpointOperationFactory(OperationParameterMapper parameterMapper) {
this.parameterMapper = parameterMapper;
}
@Override
public JmxEndpointOperation createOperation(String endpointId,
AnnotationAttributes operationAttributes, Object target, Method method,
OperationType type, long timeToLive) {
String operationName = method.getName();
Class<?> outputType = mapParameterType(method.getReturnType());
String description = getDescription(method,
() -> "Invoke " + operationName + " for endpoint " + endpointId);
List<JmxEndpointOperationParameterInfo> parameters = getParameters(method);
OperationInvoker invoker = new ReflectiveOperationInvoker(
this.parameterMapper, target, method);
if (timeToLive > 0) {
invoker = new CachingOperationInvoker(invoker, timeToLive);
}
return new JmxEndpointOperation(type, invoker, operationName, outputType,
description, parameters);
}
private String getDescription(Method method, Supplier<String> fallback) {
ManagedOperation managedOperation = jmxAttributeSource
.getManagedOperation(method);
if (managedOperation != null
&& StringUtils.hasText(managedOperation.getDescription())) {
return managedOperation.getDescription();
}
return fallback.get();
}
private List<JmxEndpointOperationParameterInfo> getParameters(Method method) {
Parameter[] methodParameters = method.getParameters();
if (methodParameters.length == 0) {
return Collections.emptyList();
}
ManagedOperationParameter[] managedOperationParameters = jmxAttributeSource
.getManagedOperationParameters(method);
if (managedOperationParameters.length == 0) {
return getParametersInfo(methodParameters);
}
return getParametersInfo(methodParameters, managedOperationParameters);
}
private List<JmxEndpointOperationParameterInfo> getParametersInfo(
Parameter[] methodParameters) {
List<JmxEndpointOperationParameterInfo> parameters = new ArrayList<>();
for (Parameter methodParameter : methodParameters) {
String name = methodParameter.getName();
Class<?> type = mapParameterType(methodParameter.getType());
parameters.add(new JmxEndpointOperationParameterInfo(name, type, null));
}
return parameters;
}
private List<JmxEndpointOperationParameterInfo> getParametersInfo(
Parameter[] methodParameters,
ManagedOperationParameter[] managedOperationParameters) {
List<JmxEndpointOperationParameterInfo> parameters = new ArrayList<>();
for (int i = 0; i < managedOperationParameters.length; i++) {
ManagedOperationParameter managedOperationParameter = managedOperationParameters[i];
Parameter methodParameter = methodParameters[i];
parameters.add(new JmxEndpointOperationParameterInfo(
managedOperationParameter.getName(),
mapParameterType(methodParameter.getType()),
managedOperationParameter.getDescription()));
}
return parameters;
}
private Class<?> mapParameterType(Class<?> parameter) {
if (parameter.isEnum()) {
return String.class;
}
if (Date.class.isAssignableFrom(parameter)) {
return String.class;
}
if (parameter.getName().startsWith("java.")) {
return parameter;
}
if (parameter.equals(Void.TYPE)) {
return parameter;
}
return Object.class;
}
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint.jmx.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
/**
* Identifies a type as being a JMX-specific extension of an {@link Endpoint}.
*
* @author Stephane Nicoll
* @since 2.0.0
* @see Endpoint
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JmxEndpointExtension {
/**
* The {@link Endpoint endpoint} class to which this JMX extension relates.
* @return the endpoint class
*/
Class<?> endpoint();
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2017 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
*
* http://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.
*/
/**
* Annotation support for actuator JMX endpoints.
*/
package org.springframework.boot.actuate.endpoint.jmx.annotation;

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2017 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
*
* http://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.
*/
/**
* JMX support for actuator endpoints.
*/
package org.springframework.boot.actuate.endpoint.jmx;

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2017 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
*
* http://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.
*/
/**
* Actuator endpoint infrastructure.
*/
package org.springframework.boot.actuate.endpoint;

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint.web;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.boot.actuate.endpoint.EndpointInfo;
/**
* A resolver for {@link Link links} to web endpoints.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
public class EndpointLinksResolver {
/**
* Resolves links to the operations of the given {code webEndpoints} based on a
* request with the given {@code requestUrl}.
* @param webEndpoints the web endpoints
* @param requestUrl the url of the request for the endpoint links
* @return the links
*/
public Map<String, Link> resolveLinks(
Collection<EndpointInfo<WebEndpointOperation>> webEndpoints,
String requestUrl) {
String normalizedUrl = normalizeRequestUrl(requestUrl);
Map<String, Link> links = new LinkedHashMap<>();
links.put("self", new Link(normalizedUrl));
for (EndpointInfo<WebEndpointOperation> endpoint : webEndpoints) {
for (WebEndpointOperation operation : endpoint.getOperations()) {
webEndpoints.stream().map(EndpointInfo::getId).forEach((id) -> links
.put(operation.getId(), createLink(normalizedUrl, operation)));
}
}
return links;
}
private String normalizeRequestUrl(String requestUrl) {
if (requestUrl.endsWith("/")) {
return requestUrl.substring(0, requestUrl.length() - 1);
}
return requestUrl;
}
private Link createLink(String requestUrl, WebEndpointOperation operation) {
String path = operation.getRequestPredicate().getPath();
return new Link(requestUrl + (path.startsWith("/") ? path : "/" + path));
}
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint.web;
import org.springframework.core.style.ToStringCreator;
/**
* Details for a link in a
* <a href="https://tools.ietf.org/html/draft-kelly-json-hal-08">HAL</a>-formatted
* response.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
public class Link {
private final String href;
private final boolean templated;
/**
* Creates a new {@link Link} with the given {@code href}.
* @param href the href
*/
public Link(String href) {
this.href = href;
this.templated = href.contains("{");
}
/**
* Returns the href of the link.
* @return the href
*/
public String getHref() {
return this.href;
}
/**
* Returns whether or not the {@link #getHref() href} is templated.
* @return {@code true} if the href is templated, otherwise {@code false}
*/
public boolean isTemplated() {
return this.templated;
}
@Override
public String toString() {
return new ToStringCreator(this).append("href", this.href).toString();
}
}

View File

@@ -0,0 +1,126 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint.web;
import java.util.Collection;
import java.util.Collections;
import org.springframework.core.style.ToStringCreator;
/**
* A predicate for a request to an operation on a web endpoint.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
public class OperationRequestPredicate {
private final String path;
private final String canonicalPath;
private final WebEndpointHttpMethod httpMethod;
private final Collection<String> consumes;
private final Collection<String> produces;
/**
* Creates a new {@code OperationRequestPredicate}.
*
* @param path the path for the operation
* @param httpMethod the HTTP method that the operation supports
* @param produces the media types that the operation produces
* @param consumes the media types that the operation consumes
*/
public OperationRequestPredicate(String path, WebEndpointHttpMethod httpMethod,
Collection<String> consumes, Collection<String> produces) {
this.path = path;
this.canonicalPath = path.replaceAll("\\{.*?}", "{*}");
this.httpMethod = httpMethod;
this.consumes = consumes;
this.produces = produces;
}
/**
* Returns the path for the operation.
* @return the path
*/
public String getPath() {
return this.path;
}
/**
* Returns the HTTP method for the operation.
* @return the HTTP method
*/
public WebEndpointHttpMethod getHttpMethod() {
return this.httpMethod;
}
/**
* Returns the media types that the operation consumes.
* @return the consumed media types
*/
public Collection<String> getConsumes() {
return Collections.unmodifiableCollection(this.consumes);
}
/**
* Returns the media types that the operation produces.
* @return the produced media types
*/
public Collection<String> getProduces() {
return Collections.unmodifiableCollection(this.produces);
}
@Override
public String toString() {
return new ToStringCreator(this).append("httpMethod", this.httpMethod)
.append("path", this.path).append("consumes", this.consumes)
.append("produces", this.produces).toString();
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + this.consumes.hashCode();
result = prime * result + this.httpMethod.hashCode();
result = prime * result + this.canonicalPath.hashCode();
result = prime * result + this.produces.hashCode();
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
OperationRequestPredicate other = (OperationRequestPredicate) obj;
boolean result = true;
result = result && this.consumes.equals(other.consumes);
result = result && this.httpMethod == other.httpMethod;
result = result && this.canonicalPath.equals(other.canonicalPath);
result = result && this.produces.equals(other.produces);
return result;
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint.web;
/**
* An enumeration of HTTP methods supported by web endpoint operations.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
public enum WebEndpointHttpMethod {
/**
* An HTTP GET request.
*/
GET,
/**
* An HTTP POST request.
*/
POST,
/**
* An HTTP DELETE request.
*/
DELETE
}

View File

@@ -0,0 +1,68 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint.web;
import org.springframework.boot.actuate.endpoint.Operation;
import org.springframework.boot.actuate.endpoint.OperationInvoker;
import org.springframework.boot.actuate.endpoint.OperationType;
/**
* An operation on a web endpoint.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
public class WebEndpointOperation extends Operation {
private final OperationRequestPredicate requestPredicate;
private final String id;
/**
* Creates a new {@code WebEndpointOperation} with the given {@code type}. The
* operation can be performed using the given {@code operationInvoker}. The operation
* can handle requests that match the given {@code requestPredicate}.
* @param type the type of the operation
* @param operationInvoker used to perform the operation
* @param blocking whether or not this is a blocking operation
* @param requestPredicate the predicate for requests that can be handled by the
* @param id the id of the operation, unique within its endpoint operation
*/
public WebEndpointOperation(OperationType type, OperationInvoker operationInvoker,
boolean blocking, OperationRequestPredicate requestPredicate, String id) {
super(type, operationInvoker, blocking);
this.requestPredicate = requestPredicate;
this.id = id;
}
/**
* Returns the predicate for requests that can be handled by this operation.
* @return the predicate
*/
public OperationRequestPredicate getRequestPredicate() {
return this.requestPredicate;
}
/**
* Returns the ID of the operation that uniquely identifies it within its endpoint.
* @return the ID
*/
public String getId() {
return this.id;
}
}

View File

@@ -0,0 +1,119 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint.web;
import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpointExtension;
/**
* A {@code WebEndpointResponse} can be returned by an operation on a
* {@link WebEndpointExtension} to provide additional, web-specific information such as
* the HTTP status code.
*
* @param <T> the type of the response body
* @author Stephane Nicoll
* @author Andy Wilkinson
* @author Vedran Pavic
* @since 2.0.0
*/
public final class WebEndpointResponse<T> {
/**
* {@code 200 OK}.
*/
public static final int STATUS_OK = 200;
/**
* {@code 400 Bad Request}.
*/
public static final int STATUS_BAD_REQUEST = 400;
/**
* {@code 404 Not Found}.
*/
public static final int STATUS_NOT_FOUND = 404;
/**
* {@code 429 Too Many Requests}.
*/
public static final int STATUS_TOO_MANY_REQUESTS = 429;
/**
* {@code 500 Internal Server Error}.
*/
public static final int STATUS_INTERNAL_SERVER_ERROR = 500;
/**
* {@code 503 Service Unavailable}.
*/
public static final int STATUS_SERVICE_UNAVAILABLE = 503;
private final T body;
private final int status;
/**
* Creates a new {@code WebEndpointResponse} with no body and a 200 (OK) status.
*/
public WebEndpointResponse() {
this(null);
}
/**
* Creates a new {@code WebEndpointResponse} with no body and the given
* {@code status}.
* @param status the HTTP status
*/
public WebEndpointResponse(int status) {
this(null, status);
}
/**
* Creates a new {@code WebEndpointResponse} with then given body and a 200 (OK)
* status.
* @param body the body
*/
public WebEndpointResponse(T body) {
this(body, STATUS_OK);
}
/**
* Creates a new {@code WebEndpointResponse} with then given body and status.
* @param body the body
* @param status the HTTP status
*/
public WebEndpointResponse(T body, int status) {
this.body = body;
this.status = status;
}
/**
* Returns the body for the response.
* @return the body
*/
public T getBody() {
return this.body;
}
/**
* Returns the status for the response.
* @return the status
*/
public int getStatus() {
return this.status;
}
}

View File

@@ -0,0 +1,231 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint.web.annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.reactivestreams.Publisher;
import org.springframework.boot.actuate.endpoint.EndpointExposure;
import org.springframework.boot.actuate.endpoint.EndpointInfo;
import org.springframework.boot.actuate.endpoint.OperationInvoker;
import org.springframework.boot.actuate.endpoint.OperationParameterMapper;
import org.springframework.boot.actuate.endpoint.OperationType;
import org.springframework.boot.actuate.endpoint.ReflectiveOperationInvoker;
import org.springframework.boot.actuate.endpoint.annotation.AnnotationEndpointDiscoverer;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.Selector;
import org.springframework.boot.actuate.endpoint.cache.CachingConfiguration;
import org.springframework.boot.actuate.endpoint.cache.CachingConfigurationFactory;
import org.springframework.boot.actuate.endpoint.cache.CachingOperationInvoker;
import org.springframework.boot.actuate.endpoint.web.OperationRequestPredicate;
import org.springframework.boot.actuate.endpoint.web.WebEndpointHttpMethod;
import org.springframework.boot.actuate.endpoint.web.WebEndpointOperation;
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
import org.springframework.context.ApplicationContext;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.io.Resource;
import org.springframework.util.ClassUtils;
/**
* Discovers the {@link Endpoint endpoints} in an {@link ApplicationContext} with
* {@link WebEndpointExtension web extensions} applied to them.
*
* @author Andy Wilkinson
* @author Stephane Nicoll
* @since 2.0.0
*/
public class WebAnnotationEndpointDiscoverer extends
AnnotationEndpointDiscoverer<WebEndpointOperation, OperationRequestPredicate> {
/**
* Creates a new {@link WebAnnotationEndpointDiscoverer} that will discover
* {@link Endpoint endpoints} and {@link WebEndpointExtension web extensions} using
* the given {@link ApplicationContext}.
* @param applicationContext the application context
* @param operationParameterMapper the {@link OperationParameterMapper} used to
* convert arguments when an operation is invoked
* @param cachingConfigurationFactory the {@link CachingConfiguration} factory to use
* @param consumedMediaTypes the media types consumed by web endpoint operations
* @param producedMediaTypes the media types produced by web endpoint operations
*/
public WebAnnotationEndpointDiscoverer(ApplicationContext applicationContext,
OperationParameterMapper operationParameterMapper,
CachingConfigurationFactory cachingConfigurationFactory,
Collection<String> consumedMediaTypes,
Collection<String> producedMediaTypes) {
super(applicationContext,
new WebEndpointOperationFactory(operationParameterMapper,
consumedMediaTypes, producedMediaTypes),
WebEndpointOperation::getRequestPredicate, cachingConfigurationFactory);
}
@Override
public Collection<EndpointInfo<WebEndpointOperation>> discoverEndpoints() {
Collection<EndpointInfoDescriptor<WebEndpointOperation, OperationRequestPredicate>> endpoints = discoverEndpoints(
WebEndpointExtension.class, EndpointExposure.WEB);
verifyThatOperationsHaveDistinctPredicates(endpoints);
return endpoints.stream().map(EndpointInfoDescriptor::getEndpointInfo)
.collect(Collectors.toList());
}
private void verifyThatOperationsHaveDistinctPredicates(
Collection<EndpointInfoDescriptor<WebEndpointOperation, OperationRequestPredicate>> endpointDescriptors) {
List<List<WebEndpointOperation>> clashes = new ArrayList<>();
endpointDescriptors.forEach((descriptor) -> clashes
.addAll(descriptor.findDuplicateOperations().values()));
if (!clashes.isEmpty()) {
StringBuilder message = new StringBuilder();
message.append(String.format(
"Found multiple web operations with matching request predicates:%n"));
clashes.forEach((clash) -> {
message.append(" ").append(clash.get(0).getRequestPredicate())
.append(String.format(":%n"));
clash.forEach((operation) -> message.append(" ")
.append(String.format("%s%n", operation)));
});
throw new IllegalStateException(message.toString());
}
}
private static final class WebEndpointOperationFactory
implements EndpointOperationFactory<WebEndpointOperation> {
private static final boolean REACTIVE_STREAMS_PRESENT = ClassUtils.isPresent(
"org.reactivestreams.Publisher",
WebEndpointOperationFactory.class.getClassLoader());
private final OperationParameterMapper parameterMapper;
private final Collection<String> consumedMediaTypes;
private final Collection<String> producedMediaTypes;
private WebEndpointOperationFactory(OperationParameterMapper parameterMapper,
Collection<String> consumedMediaTypes,
Collection<String> producedMediaTypes) {
this.parameterMapper = parameterMapper;
this.consumedMediaTypes = consumedMediaTypes;
this.producedMediaTypes = producedMediaTypes;
}
@Override
public WebEndpointOperation createOperation(String endpointId,
AnnotationAttributes operationAttributes, Object target, Method method,
OperationType type, long timeToLive) {
WebEndpointHttpMethod httpMethod = determineHttpMethod(type);
OperationRequestPredicate requestPredicate = new OperationRequestPredicate(
determinePath(endpointId, method), httpMethod,
determineConsumedMediaTypes(httpMethod, method),
determineProducedMediaTypes(
operationAttributes.getStringArray("produces"), method));
OperationInvoker invoker = new ReflectiveOperationInvoker(
this.parameterMapper, target, method);
if (timeToLive > 0) {
invoker = new CachingOperationInvoker(invoker, timeToLive);
}
return new WebEndpointOperation(type, invoker, determineBlocking(method),
requestPredicate, determineId(endpointId, method));
}
private String determinePath(String endpointId, Method operationMethod) {
StringBuilder path = new StringBuilder(endpointId);
Stream.of(operationMethod.getParameters())
.filter((
parameter) -> parameter.getAnnotation(Selector.class) != null)
.map((parameter) -> "/{" + parameter.getName() + "}")
.forEach(path::append);
return path.toString();
}
private String determineId(String endpointId, Method operationMethod) {
StringBuilder path = new StringBuilder(endpointId);
Stream.of(operationMethod.getParameters())
.filter((
parameter) -> parameter.getAnnotation(Selector.class) != null)
.map((parameter) -> "-" + parameter.getName()).forEach(path::append);
return path.toString();
}
private Collection<String> determineConsumedMediaTypes(
WebEndpointHttpMethod httpMethod, Method method) {
if (WebEndpointHttpMethod.POST == httpMethod && consumesRequestBody(method)) {
return this.consumedMediaTypes;
}
return Collections.emptyList();
}
private Collection<String> determineProducedMediaTypes(String[] produces,
Method method) {
if (produces.length > 0) {
return Arrays.asList(produces);
}
if (Void.class.equals(method.getReturnType())
|| void.class.equals(method.getReturnType())) {
return Collections.emptyList();
}
if (producesResourceResponseBody(method)) {
return Collections.singletonList("application/octet-stream");
}
return this.producedMediaTypes;
}
private boolean producesResourceResponseBody(Method method) {
if (Resource.class.equals(method.getReturnType())) {
return true;
}
if (WebEndpointResponse.class.isAssignableFrom(method.getReturnType())) {
ResolvableType returnType = ResolvableType.forMethodReturnType(method);
if (ResolvableType.forClass(Resource.class)
.isAssignableFrom(returnType.getGeneric(0))) {
return true;
}
}
return false;
}
private boolean consumesRequestBody(Method method) {
return Stream.of(method.getParameters()).anyMatch(
(parameter) -> parameter.getAnnotation(Selector.class) == null);
}
private WebEndpointHttpMethod determineHttpMethod(OperationType operationType) {
if (operationType == OperationType.WRITE) {
return WebEndpointHttpMethod.POST;
}
if (operationType == OperationType.DELETE) {
return WebEndpointHttpMethod.DELETE;
}
return WebEndpointHttpMethod.GET;
}
private boolean determineBlocking(Method method) {
return !REACTIVE_STREAMS_PRESENT
|| !Publisher.class.isAssignableFrom(method.getReturnType());
}
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint.web.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
/**
* Identifies a type as being a Web-specific extension of an {@link Endpoint}.
*
* @author Andy Wilkinson
* @author Stephane Nicoll
* @since 2.0.0
* @see Endpoint
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WebEndpointExtension {
/**
* The {@link Endpoint endpoint} class to which this Web extension relates.
* @return the endpoint class
*/
Class<?> endpoint();
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2017 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
*
* http://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.
*/
/**
* Annotation support for actuator web endpoints.
*/
package org.springframework.boot.actuate.endpoint.web.annotation;

View File

@@ -0,0 +1,266 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint.web.jersey;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import org.glassfish.jersey.process.Inflector;
import org.glassfish.jersey.server.ContainerRequest;
import org.glassfish.jersey.server.model.Resource;
import org.glassfish.jersey.server.model.Resource.Builder;
import reactor.core.publisher.Mono;
import org.springframework.boot.actuate.endpoint.EndpointInfo;
import org.springframework.boot.actuate.endpoint.OperationInvoker;
import org.springframework.boot.actuate.endpoint.ParameterMappingException;
import org.springframework.boot.actuate.endpoint.ParametersMissingException;
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
import org.springframework.boot.actuate.endpoint.web.Link;
import org.springframework.boot.actuate.endpoint.web.OperationRequestPredicate;
import org.springframework.boot.actuate.endpoint.web.WebEndpointOperation;
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
import org.springframework.boot.endpoint.web.EndpointMapping;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/**
* A factory for creating Jersey {@link Resource Resources} for
* {@link WebEndpointOperation web endpoint operations}.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
public class JerseyEndpointResourceFactory {
private final EndpointLinksResolver endpointLinksResolver = new EndpointLinksResolver();
/**
* Creates {@link Resource Resources} for the operations of the given
* {@code webEndpoints}.
* @param endpointMapping the base mapping for all endpoints
* @param webEndpoints the web endpoints
* @return the resources for the operations
*/
public Collection<Resource> createEndpointResources(EndpointMapping endpointMapping,
Collection<EndpointInfo<WebEndpointOperation>> webEndpoints) {
List<Resource> resources = new ArrayList<>();
webEndpoints.stream()
.flatMap((endpointInfo) -> endpointInfo.getOperations().stream())
.map((operation) -> createResource(endpointMapping, operation))
.forEach(resources::add);
if (StringUtils.hasText(endpointMapping.getPath())) {
resources.add(
createEndpointLinksResource(endpointMapping.getPath(), webEndpoints));
}
return resources;
}
private Resource createResource(EndpointMapping endpointMapping,
WebEndpointOperation operation) {
OperationRequestPredicate requestPredicate = operation.getRequestPredicate();
Builder resourceBuilder = Resource.builder()
.path(endpointMapping.createSubPath(requestPredicate.getPath()));
resourceBuilder.addMethod(requestPredicate.getHttpMethod().name())
.consumes(toStringArray(requestPredicate.getConsumes()))
.produces(toStringArray(requestPredicate.getProduces()))
.handledBy(new EndpointInvokingInflector(operation.getInvoker(),
!requestPredicate.getConsumes().isEmpty()));
return resourceBuilder.build();
}
private String[] toStringArray(Collection<String> collection) {
return collection.toArray(new String[collection.size()]);
}
private Resource createEndpointLinksResource(String endpointPath,
Collection<EndpointInfo<WebEndpointOperation>> webEndpoints) {
Builder resourceBuilder = Resource.builder().path(endpointPath);
resourceBuilder.addMethod("GET").produces(MediaType.APPLICATION_JSON).handledBy(
new EndpointLinksInflector(webEndpoints, this.endpointLinksResolver));
return resourceBuilder.build();
}
private static final class EndpointInvokingInflector
implements Inflector<ContainerRequestContext, Object> {
private static final List<Function<Object, Object>> bodyConverters;
static {
List<Function<Object, Object>> converters = new ArrayList<>();
converters.add(new ResourceBodyConverter());
if (ClassUtils.isPresent("reactor.core.publisher.Mono",
EndpointInvokingInflector.class.getClassLoader())) {
converters.add(new MonoBodyConverter());
}
bodyConverters = Collections.unmodifiableList(converters);
}
private final OperationInvoker operationInvoker;
private final boolean readBody;
private EndpointInvokingInflector(OperationInvoker operationInvoker,
boolean readBody) {
this.operationInvoker = operationInvoker;
this.readBody = readBody;
}
@Override
public Response apply(ContainerRequestContext data) {
Map<String, Object> arguments = new HashMap<>();
if (this.readBody) {
arguments.putAll(extractBodyArguments(data));
}
arguments.putAll(extractPathParameters(data));
arguments.putAll(extractQueryParameters(data));
try {
Object response = this.operationInvoker.invoke(arguments);
return convertToJaxRsResponse(response, data.getRequest().getMethod());
}
catch (ParametersMissingException | ParameterMappingException ex) {
return Response.status(Status.BAD_REQUEST).build();
}
}
@SuppressWarnings("unchecked")
private Map<String, Object> extractBodyArguments(ContainerRequestContext data) {
Map<?, ?> entity = ((ContainerRequest) data).readEntity(Map.class);
if (entity == null) {
return Collections.emptyMap();
}
return (Map<String, Object>) entity;
}
private Map<String, Object> extractPathParameters(
ContainerRequestContext requestContext) {
return extract(requestContext.getUriInfo().getPathParameters());
}
private Map<String, Object> extractQueryParameters(
ContainerRequestContext requestContext) {
return extract(requestContext.getUriInfo().getQueryParameters());
}
private Map<String, Object> extract(
MultivaluedMap<String, String> multivaluedMap) {
Map<String, Object> result = new HashMap<>();
multivaluedMap.forEach((name, values) -> {
if (!CollectionUtils.isEmpty(values)) {
result.put(name, values.size() == 1 ? values.get(0) : values);
}
});
return result;
}
private Response convertToJaxRsResponse(Object response, String httpMethod) {
if (response == null) {
boolean isGet = HttpMethod.GET.equals(httpMethod);
Status status = (isGet ? Status.NOT_FOUND : Status.NO_CONTENT);
return Response.status(status).build();
}
try {
if (!(response instanceof WebEndpointResponse)) {
return Response.status(Status.OK).entity(convertIfNecessary(response))
.build();
}
WebEndpointResponse<?> webEndpointResponse = (WebEndpointResponse<?>) response;
return Response.status(webEndpointResponse.getStatus())
.entity(convertIfNecessary(webEndpointResponse.getBody()))
.build();
}
catch (IOException ex) {
return Response.status(Status.INTERNAL_SERVER_ERROR).build();
}
}
private Object convertIfNecessary(Object body) throws IOException {
for (Function<Object, Object> converter : bodyConverters) {
body = converter.apply(body);
}
return body;
}
}
private static final class ResourceBodyConverter implements Function<Object, Object> {
@Override
public Object apply(Object body) {
if (body instanceof org.springframework.core.io.Resource) {
try {
return ((org.springframework.core.io.Resource) body).getInputStream();
}
catch (IOException ex) {
throw new IllegalStateException();
}
}
return body;
}
}
private static final class MonoBodyConverter implements Function<Object, Object> {
@Override
public Object apply(Object body) {
if (body instanceof Mono) {
return ((Mono<?>) body).block();
}
return body;
}
}
private static final class EndpointLinksInflector
implements Inflector<ContainerRequestContext, Response> {
private final Collection<EndpointInfo<WebEndpointOperation>> endpoints;
private final EndpointLinksResolver linksResolver;
private EndpointLinksInflector(
Collection<EndpointInfo<WebEndpointOperation>> endpoints,
EndpointLinksResolver linksResolver) {
this.endpoints = endpoints;
this.linksResolver = linksResolver;
}
@Override
public Response apply(ContainerRequestContext request) {
Map<String, Link> links = this.linksResolver.resolveLinks(this.endpoints,
request.getUriInfo().getAbsolutePath().toString());
return Response.ok(Collections.singletonMap("_links", links)).build();
}
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2017 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
*
* http://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.
*/
/**
* Jersey support for actuator endpoints.
*/
package org.springframework.boot.actuate.endpoint.web.jersey;

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2017 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
*
* http://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.
*/
/**
* Web support for actuator endpoints.
*/
package org.springframework.boot.actuate.endpoint.web;

View File

@@ -0,0 +1,306 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint.web.reactive;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoSink;
import reactor.core.scheduler.Schedulers;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.actuate.endpoint.EndpointInfo;
import org.springframework.boot.actuate.endpoint.OperationInvoker;
import org.springframework.boot.actuate.endpoint.OperationType;
import org.springframework.boot.actuate.endpoint.ParameterMappingException;
import org.springframework.boot.actuate.endpoint.ParametersMissingException;
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
import org.springframework.boot.actuate.endpoint.web.Link;
import org.springframework.boot.actuate.endpoint.web.OperationRequestPredicate;
import org.springframework.boot.actuate.endpoint.web.WebEndpointOperation;
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
import org.springframework.boot.endpoint.web.EndpointMapping;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.reactive.result.condition.ConsumesRequestCondition;
import org.springframework.web.reactive.result.condition.PatternsRequestCondition;
import org.springframework.web.reactive.result.condition.ProducesRequestCondition;
import org.springframework.web.reactive.result.condition.RequestMethodsRequestCondition;
import org.springframework.web.reactive.result.method.RequestMappingInfo;
import org.springframework.web.reactive.result.method.RequestMappingInfoHandlerMapping;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.pattern.PathPatternParser;
/**
* A custom {@link HandlerMapping} that makes web endpoints available over HTTP using
* Spring WebFlux.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
public class WebFluxEndpointHandlerMapping extends RequestMappingInfoHandlerMapping
implements InitializingBean {
private static final PathPatternParser pathPatternParser = new PathPatternParser();
private final Method handleRead = ReflectionUtils
.findMethod(ReadOperationHandler.class, "handle", ServerWebExchange.class);
private final Method handleWrite = ReflectionUtils.findMethod(
WriteOperationHandler.class, "handle", ServerWebExchange.class, Map.class);
private final Method links = ReflectionUtils.findMethod(getClass(), "links",
ServerHttpRequest.class);
private final EndpointLinksResolver endpointLinksResolver = new EndpointLinksResolver();
private final EndpointMapping endpointMapping;
private final Collection<EndpointInfo<WebEndpointOperation>> webEndpoints;
private final CorsConfiguration corsConfiguration;
/**
* Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the
* operations of the given {@code webEndpoints}.
* @param endpointMapping the base mapping for all endpoints
* @param collection the web endpoints
*/
public WebFluxEndpointHandlerMapping(EndpointMapping endpointMapping,
Collection<EndpointInfo<WebEndpointOperation>> collection) {
this(endpointMapping, collection, null);
}
/**
* Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the
* operations of the given {@code webEndpoints}.
* @param endpointMapping the path beneath which all endpoints should be mapped
* @param webEndpoints the web endpoints
* @param corsConfiguration the CORS configuration for the endpoints
*/
public WebFluxEndpointHandlerMapping(EndpointMapping endpointMapping,
Collection<EndpointInfo<WebEndpointOperation>> webEndpoints,
CorsConfiguration corsConfiguration) {
this.endpointMapping = endpointMapping;
this.webEndpoints = webEndpoints;
this.corsConfiguration = corsConfiguration;
setOrder(-100);
}
@Override
protected void initHandlerMethods() {
this.webEndpoints.stream()
.flatMap((webEndpoint) -> webEndpoint.getOperations().stream())
.forEach(this::registerMappingForOperation);
if (StringUtils.hasText(this.endpointMapping.getPath())) {
registerLinksMapping();
}
}
private void registerLinksMapping() {
registerMapping(new RequestMappingInfo(
new PatternsRequestCondition(
pathPatternParser.parse(this.endpointMapping.getPath())),
new RequestMethodsRequestCondition(RequestMethod.GET), null, null, null,
null, null), this, this.links);
}
@Override
protected CorsConfiguration initCorsConfiguration(Object handler, Method method,
RequestMappingInfo mapping) {
return this.corsConfiguration;
}
private void registerMappingForOperation(WebEndpointOperation operation) {
OperationType operationType = operation.getType();
OperationInvoker operationInvoker = operation.getInvoker();
if (operation.isBlocking()) {
operationInvoker = new ElasticSchedulerOperationInvoker(operationInvoker);
}
registerMapping(createRequestMappingInfo(operation),
operationType == OperationType.WRITE
? new WriteOperationHandler(operationInvoker)
: new ReadOperationHandler(operationInvoker),
operationType == OperationType.WRITE ? this.handleWrite
: this.handleRead);
}
private RequestMappingInfo createRequestMappingInfo(
WebEndpointOperation operationInfo) {
OperationRequestPredicate requestPredicate = operationInfo.getRequestPredicate();
PatternsRequestCondition patterns = new PatternsRequestCondition(pathPatternParser
.parse(this.endpointMapping.createSubPath(requestPredicate.getPath())));
RequestMethodsRequestCondition methods = new RequestMethodsRequestCondition(
RequestMethod.valueOf(requestPredicate.getHttpMethod().name()));
ConsumesRequestCondition consumes = new ConsumesRequestCondition(
toStringArray(requestPredicate.getConsumes()));
ProducesRequestCondition produces = new ProducesRequestCondition(
toStringArray(requestPredicate.getProduces()));
return new RequestMappingInfo(null, patterns, methods, null, null, consumes,
produces, null);
}
private String[] toStringArray(Collection<String> collection) {
return collection.toArray(new String[collection.size()]);
}
@Override
protected boolean isHandler(Class<?> beanType) {
return false;
}
@Override
protected RequestMappingInfo getMappingForMethod(Method method,
Class<?> handlerType) {
return null;
}
@ResponseBody
private Map<String, Map<String, Link>> links(ServerHttpRequest request) {
return Collections.singletonMap("_links",
this.endpointLinksResolver.resolveLinks(this.webEndpoints,
UriComponentsBuilder.fromUri(request.getURI()).replaceQuery(null)
.toUriString()));
}
/**
* Base class for handlers for endpoint operations.
*/
abstract class AbstractOperationHandler {
private final OperationInvoker operationInvoker;
AbstractOperationHandler(OperationInvoker operationInvoker) {
this.operationInvoker = operationInvoker;
}
@SuppressWarnings({ "unchecked" })
Publisher<ResponseEntity<Object>> doHandle(ServerWebExchange exchange,
Map<String, String> body) {
Map<String, Object> arguments = new HashMap<>((Map<String, String>) exchange
.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE));
if (body != null) {
arguments.putAll(body);
}
exchange.getRequest().getQueryParams().forEach((name, values) -> arguments
.put(name, values.size() == 1 ? values.get(0) : values));
return handleResult((Publisher<?>) this.operationInvoker.invoke(arguments),
exchange.getRequest().getMethod());
}
private Publisher<ResponseEntity<Object>> handleResult(Publisher<?> result,
HttpMethod httpMethod) {
return Mono.from(result).map(this::toResponseEntity)
.onErrorReturn(ParametersMissingException.class,
new ResponseEntity<>(HttpStatus.BAD_REQUEST))
.onErrorReturn(ParameterMappingException.class,
new ResponseEntity<>(HttpStatus.BAD_REQUEST))
.defaultIfEmpty(new ResponseEntity<>(httpMethod == HttpMethod.GET
? HttpStatus.NOT_FOUND : HttpStatus.NO_CONTENT));
}
private ResponseEntity<Object> toResponseEntity(Object response) {
if (!(response instanceof WebEndpointResponse)) {
return new ResponseEntity<>(response, HttpStatus.OK);
}
WebEndpointResponse<?> webEndpointResponse = (WebEndpointResponse<?>) response;
return new ResponseEntity<>(webEndpointResponse.getBody(),
HttpStatus.valueOf(webEndpointResponse.getStatus()));
}
}
/**
* A handler for an endpoint write operation.
*/
final class WriteOperationHandler extends AbstractOperationHandler {
WriteOperationHandler(OperationInvoker operationInvoker) {
super(operationInvoker);
}
@ResponseBody
public Publisher<ResponseEntity<Object>> handle(ServerWebExchange exchange,
@RequestBody(required = false) Map<String, String> body) {
return doHandle(exchange, body);
}
}
/**
* A handler for an endpoint write operation.
*/
final class ReadOperationHandler extends AbstractOperationHandler {
ReadOperationHandler(OperationInvoker operationInvoker) {
super(operationInvoker);
}
@ResponseBody
public Publisher<ResponseEntity<Object>> handle(ServerWebExchange exchange) {
return doHandle(exchange, null);
}
}
/**
* An {@link OperationInvoker} that performs the invocation of a blocking operation on
* a separate thread using Reactor's {@link Schedulers#elastic() elastic scheduler}.
*/
private static final class ElasticSchedulerOperationInvoker
implements OperationInvoker {
private final OperationInvoker delegate;
private ElasticSchedulerOperationInvoker(OperationInvoker delegate) {
this.delegate = delegate;
}
@Override
public Object invoke(Map<String, Object> arguments) {
return Mono.create((sink) -> Schedulers.elastic()
.schedule(() -> invoke(arguments, sink)));
}
private void invoke(Map<String, Object> arguments, MonoSink<Object> sink) {
try {
Object result = this.delegate.invoke(arguments);
sink.success(result);
}
catch (Exception ex) {
sink.error(ex);
}
}
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2017 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
*
* http://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.
*/
/**
* Spring WebFlux support for actuator endpoints.
*/
package org.springframework.boot.actuate.endpoint.web.reactive;

View File

@@ -0,0 +1,184 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint.web.servlet;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.actuate.endpoint.EndpointInfo;
import org.springframework.boot.actuate.endpoint.web.OperationRequestPredicate;
import org.springframework.boot.actuate.endpoint.web.WebEndpointOperation;
import org.springframework.boot.endpoint.web.EndpointMapping;
import org.springframework.util.StringUtils;
import org.springframework.web.accept.PathExtensionContentNegotiationStrategy;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import org.springframework.web.servlet.mvc.condition.ConsumesRequestCondition;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
import org.springframework.web.servlet.mvc.condition.ProducesRequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
/**
* A custom {@link HandlerMapping} that makes web endpoints available over HTTP using
* Spring MVC.
*
* @author Andy Wilkinson
* @author Madhura Bhave
* @since 2.0.0
*/
public abstract class AbstractWebMvcEndpointHandlerMapping
extends RequestMappingInfoHandlerMapping implements InitializingBean {
private final EndpointMapping endpointMapping;
private final Collection<EndpointInfo<WebEndpointOperation>> webEndpoints;
private final CorsConfiguration corsConfiguration;
/**
* Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the
* operations of the given {@code webEndpoints}.
* @param endpointMapping the base mapping for all endpoints
* @param collection the web endpoints operations
*/
public AbstractWebMvcEndpointHandlerMapping(EndpointMapping endpointMapping,
Collection<EndpointInfo<WebEndpointOperation>> collection) {
this(endpointMapping, collection, null);
}
/**
* Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the
* operations of the given {@code webEndpoints}.
* @param endpointMapping the base mapping for all endpoints
* @param webEndpoints the web endpoints
* @param corsConfiguration the CORS configuration for the endpoints
*/
public AbstractWebMvcEndpointHandlerMapping(EndpointMapping endpointMapping,
Collection<EndpointInfo<WebEndpointOperation>> webEndpoints,
CorsConfiguration corsConfiguration) {
this.endpointMapping = endpointMapping;
this.webEndpoints = webEndpoints;
this.corsConfiguration = corsConfiguration;
setOrder(-100);
}
public Collection<EndpointInfo<WebEndpointOperation>> getEndpoints() {
return this.webEndpoints;
}
public EndpointMapping getEndpointMapping() {
return this.endpointMapping;
}
@Override
protected void initHandlerMethods() {
this.webEndpoints.stream()
.flatMap((webEndpoint) -> webEndpoint.getOperations().stream())
.forEach(this::registerMappingForOperation);
if (StringUtils.hasText(this.endpointMapping.getPath())) {
registerLinksRequestMapping();
}
}
private void registerLinksRequestMapping() {
PatternsRequestCondition patterns = patternsRequestConditionForPattern("");
RequestMethodsRequestCondition methods = new RequestMethodsRequestCondition(
RequestMethod.GET);
RequestMappingInfo mapping = new RequestMappingInfo(patterns, methods, null, null,
null, null, null);
registerMapping(mapping, this, getLinks());
}
@Override
protected CorsConfiguration initCorsConfiguration(Object handler, Method method,
RequestMappingInfo mapping) {
return this.corsConfiguration;
}
protected abstract Method getLinks();
protected abstract void registerMappingForOperation(WebEndpointOperation operation);
protected RequestMappingInfo createRequestMappingInfo(
WebEndpointOperation operationInfo) {
OperationRequestPredicate requestPredicate = operationInfo.getRequestPredicate();
PatternsRequestCondition patterns = patternsRequestConditionForPattern(
requestPredicate.getPath());
RequestMethodsRequestCondition methods = new RequestMethodsRequestCondition(
RequestMethod.valueOf(requestPredicate.getHttpMethod().name()));
ConsumesRequestCondition consumes = new ConsumesRequestCondition(
toStringArray(requestPredicate.getConsumes()));
ProducesRequestCondition produces = new ProducesRequestCondition(
toStringArray(requestPredicate.getProduces()));
return new RequestMappingInfo(null, patterns, methods, null, null, consumes,
produces, null);
}
private PatternsRequestCondition patternsRequestConditionForPattern(String path) {
String[] patterns = new String[] { this.endpointMapping.createSubPath(path) };
return new PatternsRequestCondition(patterns, null, null, false, false);
}
private String[] toStringArray(Collection<String> collection) {
return collection.toArray(new String[collection.size()]);
}
@Override
protected boolean isHandler(Class<?> beanType) {
return false;
}
@Override
protected RequestMappingInfo getMappingForMethod(Method method,
Class<?> handlerType) {
return null;
}
@Override
protected void extendInterceptors(List<Object> interceptors) {
interceptors.add(new SkipPathExtensionContentNegotiation());
}
/**
* {@link HandlerInterceptorAdapter} to ensure that
* {@link PathExtensionContentNegotiationStrategy} is skipped for web endpoints.
*/
private static final class SkipPathExtensionContentNegotiation
extends HandlerInterceptorAdapter {
private static final String SKIP_ATTRIBUTE = PathExtensionContentNegotiationStrategy.class
.getName() + ".SKIP";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
request.setAttribute(SKIP_ATTRIBUTE, Boolean.TRUE);
return true;
}
}
}

View File

@@ -0,0 +1,151 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.endpoint.web.servlet;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.boot.actuate.endpoint.EndpointInfo;
import org.springframework.boot.actuate.endpoint.OperationInvoker;
import org.springframework.boot.actuate.endpoint.ParameterMappingException;
import org.springframework.boot.actuate.endpoint.ParametersMissingException;
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
import org.springframework.boot.actuate.endpoint.web.Link;
import org.springframework.boot.actuate.endpoint.web.WebEndpointOperation;
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
import org.springframework.boot.endpoint.web.EndpointMapping;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.servlet.HandlerMapping;
/**
* A custom {@link HandlerMapping} that makes web endpoints available over HTTP using
* Spring MVC.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
public class WebMvcEndpointHandlerMapping extends AbstractWebMvcEndpointHandlerMapping {
private final Method handle = ReflectionUtils.findMethod(OperationHandler.class,
"handle", HttpServletRequest.class, Map.class);
private final Method links = ReflectionUtils.findMethod(
WebMvcEndpointHandlerMapping.class, "links", HttpServletRequest.class);
private final EndpointLinksResolver endpointLinksResolver = new EndpointLinksResolver();
/**
* Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the
* operations of the given {@code webEndpoints}.
* @param endpointMapping the base mapping for all endpoints
* @param collection the web endpoints operations
*/
public WebMvcEndpointHandlerMapping(EndpointMapping endpointMapping,
Collection<EndpointInfo<WebEndpointOperation>> collection) {
this(endpointMapping, collection, null);
}
/**
* Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the
* operations of the given {@code webEndpoints}.
* @param endpointMapping the base mapping for all endpoints
* @param webEndpoints the web endpoints
* @param corsConfiguration the CORS configuration for the endpoints
*/
public WebMvcEndpointHandlerMapping(EndpointMapping endpointMapping,
Collection<EndpointInfo<WebEndpointOperation>> webEndpoints,
CorsConfiguration corsConfiguration) {
super(endpointMapping, webEndpoints, corsConfiguration);
setOrder(-100);
}
@Override
protected void registerMappingForOperation(WebEndpointOperation operation) {
registerMapping(createRequestMappingInfo(operation),
new OperationHandler(operation.getInvoker()), this.handle);
}
@Override
protected Method getLinks() {
return this.links;
}
@ResponseBody
private Map<String, Map<String, Link>> links(HttpServletRequest request) {
return Collections.singletonMap("_links", this.endpointLinksResolver
.resolveLinks(getEndpoints(), request.getRequestURL().toString()));
}
/**
* A handler for an endpoint operation.
*/
final class OperationHandler {
private final OperationInvoker operationInvoker;
OperationHandler(OperationInvoker operationInvoker) {
this.operationInvoker = operationInvoker;
}
@SuppressWarnings("unchecked")
@ResponseBody
public Object handle(HttpServletRequest request,
@RequestBody(required = false) Map<String, String> body) {
Map<String, Object> arguments = new HashMap<>((Map<String, String>) request
.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE));
HttpMethod httpMethod = HttpMethod.valueOf(request.getMethod());
if (body != null && HttpMethod.POST == httpMethod) {
arguments.putAll(body);
}
request.getParameterMap().forEach((name, values) -> arguments.put(name,
values.length == 1 ? values[0] : Arrays.asList(values)));
try {
return handleResult(this.operationInvoker.invoke(arguments), httpMethod);
}
catch (ParametersMissingException | ParameterMappingException ex) {
return new ResponseEntity<Void>(HttpStatus.BAD_REQUEST);
}
}
private Object handleResult(Object result, HttpMethod httpMethod) {
if (result == null) {
return new ResponseEntity<>(httpMethod == HttpMethod.GET
? HttpStatus.NOT_FOUND : HttpStatus.NO_CONTENT);
}
if (!(result instanceof WebEndpointResponse)) {
return result;
}
WebEndpointResponse<?> response = (WebEndpointResponse<?>) result;
return new ResponseEntity<Object>(response.getBody(),
HttpStatus.valueOf(response.getStatus()));
}
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2017 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
*
* http://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.
*/
/**
* Spring MVC support for actuator endpoints.
*/
package org.springframework.boot.actuate.endpoint.web.servlet;

View File

@@ -0,0 +1,391 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.env;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import com.fasterxml.jackson.annotation.JsonInclude;
import org.springframework.boot.actuate.endpoint.Sanitizer;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.Selector;
import org.springframework.boot.context.properties.bind.PlaceholdersResolver;
import org.springframework.boot.context.properties.bind.PropertySourcesPlaceholdersResolver;
import org.springframework.boot.context.properties.source.ConfigurationPropertySources;
import org.springframework.boot.origin.OriginLookup;
import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.lang.Nullable;
import org.springframework.util.PropertyPlaceholderHelper;
import org.springframework.util.StringUtils;
import org.springframework.util.SystemPropertyUtils;
/**
* {@link Endpoint} to expose {@link ConfigurableEnvironment environment} information.
*
* @author Dave Syer
* @author Phillip Webb
* @author Christian Dupuis
* @author Madhura Bhave
* @author Stephane Nicoll
* @since 2.0.0
*/
@Endpoint(id = "env")
public class EnvironmentEndpoint {
private final Sanitizer sanitizer = new Sanitizer();
private final Environment environment;
public EnvironmentEndpoint(Environment environment) {
this.environment = environment;
}
public void setKeysToSanitize(String... keysToSanitize) {
this.sanitizer.setKeysToSanitize(keysToSanitize);
}
@ReadOperation
public EnvironmentDescriptor environment(@Nullable String pattern) {
if (StringUtils.hasText(pattern)) {
return getEnvironmentDescriptor(Pattern.compile(pattern).asPredicate());
}
return getEnvironmentDescriptor((name) -> true);
}
@ReadOperation
public EnvironmentEntryDescriptor environmentEntry(@Selector String toMatch) {
return getEnvironmentEntryDescriptor(toMatch);
}
private EnvironmentDescriptor getEnvironmentDescriptor(
Predicate<String> propertyNamePredicate) {
PlaceholdersResolver resolver = getResolver();
List<PropertySourceDescriptor> propertySources = new ArrayList<>();
getPropertySourcesAsMap().forEach((sourceName, source) -> {
if (source instanceof EnumerablePropertySource) {
propertySources.add(
describeSource(sourceName, (EnumerablePropertySource<?>) source,
resolver, propertyNamePredicate));
}
});
return new EnvironmentDescriptor(
Arrays.asList(this.environment.getActiveProfiles()), propertySources);
}
private EnvironmentEntryDescriptor getEnvironmentEntryDescriptor(
String propertyName) {
Map<String, PropertyValueDescriptor> descriptors = getPropertySourceDescriptors(
propertyName);
PropertySummaryDescriptor summary = getPropertySummaryDescriptor(descriptors);
return new EnvironmentEntryDescriptor(summary,
Arrays.asList(this.environment.getActiveProfiles()),
toPropertySourceDescriptors(descriptors));
}
private List<PropertySourceEntryDescriptor> toPropertySourceDescriptors(
Map<String, PropertyValueDescriptor> descriptors) {
List<PropertySourceEntryDescriptor> result = new ArrayList<>();
descriptors.forEach((name, property) -> result
.add(new PropertySourceEntryDescriptor(name, property)));
return result;
}
private PropertySummaryDescriptor getPropertySummaryDescriptor(
Map<String, PropertyValueDescriptor> descriptors) {
for (Map.Entry<String, PropertyValueDescriptor> entry : descriptors.entrySet()) {
if (entry.getValue() != null) {
return new PropertySummaryDescriptor(entry.getKey(),
entry.getValue().getValue());
}
}
return null;
}
private Map<String, PropertyValueDescriptor> getPropertySourceDescriptors(
String propertyName) {
Map<String, PropertyValueDescriptor> propertySources = new LinkedHashMap<>();
PlaceholdersResolver resolver = getResolver();
getPropertySourcesAsMap()
.forEach((sourceName, source) -> propertySources.put(sourceName,
source.containsProperty(propertyName)
? describeValueOf(propertyName, source, resolver)
: null));
return propertySources;
}
private PropertySourceDescriptor describeSource(String sourceName,
EnumerablePropertySource<?> source, PlaceholdersResolver resolver,
Predicate<String> namePredicate) {
Map<String, PropertyValueDescriptor> properties = new LinkedHashMap<>();
Stream.of(source.getPropertyNames()).filter(namePredicate).forEach(
(name) -> properties.put(name, describeValueOf(name, source, resolver)));
return new PropertySourceDescriptor(sourceName, properties);
}
@SuppressWarnings("unchecked")
private PropertyValueDescriptor describeValueOf(String name, PropertySource<?> source,
PlaceholdersResolver resolver) {
Object resolved = resolver.resolvePlaceholders(source.getProperty(name));
String origin = (source instanceof OriginLookup)
? ((OriginLookup<Object>) source).getOrigin(name).toString() : null;
return new PropertyValueDescriptor(sanitize(name, resolved), origin);
}
private PlaceholdersResolver getResolver() {
return new PropertySourcesPlaceholdersSanitizingResolver(getPropertySources(),
this.sanitizer);
}
private Map<String, PropertySource<?>> getPropertySourcesAsMap() {
Map<String, PropertySource<?>> map = new LinkedHashMap<>();
for (PropertySource<?> source : getPropertySources()) {
if (!ConfigurationPropertySources
.isAttachedConfigurationPropertySource(source)) {
extract("", map, source);
}
}
return map;
}
private MutablePropertySources getPropertySources() {
if (this.environment instanceof ConfigurableEnvironment) {
return ((ConfigurableEnvironment) this.environment).getPropertySources();
}
return new StandardEnvironment().getPropertySources();
}
private void extract(String root, Map<String, PropertySource<?>> map,
PropertySource<?> source) {
if (source instanceof CompositePropertySource) {
for (PropertySource<?> nest : ((CompositePropertySource) source)
.getPropertySources()) {
extract(source.getName() + ":", map, nest);
}
}
else {
map.put(root + source.getName(), source);
}
}
public Object sanitize(String name, Object object) {
return this.sanitizer.sanitize(name, object);
}
/**
* {@link PropertySourcesPlaceholdersResolver} that sanitizes sensitive placeholders
* if present.
*/
private static class PropertySourcesPlaceholdersSanitizingResolver
extends PropertySourcesPlaceholdersResolver {
private final Sanitizer sanitizer;
PropertySourcesPlaceholdersSanitizingResolver(Iterable<PropertySource<?>> sources,
Sanitizer sanitizer) {
super(sources,
new PropertyPlaceholderHelper(SystemPropertyUtils.PLACEHOLDER_PREFIX,
SystemPropertyUtils.PLACEHOLDER_SUFFIX,
SystemPropertyUtils.VALUE_SEPARATOR, true));
this.sanitizer = sanitizer;
}
@Override
protected String resolvePlaceholder(String placeholder) {
String value = super.resolvePlaceholder(placeholder);
if (value == null) {
return null;
}
return (String) this.sanitizer.sanitize(placeholder, value);
}
}
/**
* A description of an {@link Environment}.
*/
public static final class EnvironmentDescriptor {
private final List<String> activeProfiles;
private final List<PropertySourceDescriptor> propertySources;
private EnvironmentDescriptor(List<String> activeProfiles,
List<PropertySourceDescriptor> propertySources) {
this.activeProfiles = activeProfiles;
this.propertySources = propertySources;
}
public List<String> getActiveProfiles() {
return this.activeProfiles;
}
public List<PropertySourceDescriptor> getPropertySources() {
return this.propertySources;
}
}
/**
* A description of an entry of the {@link Environment}.
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public static final class EnvironmentEntryDescriptor {
private final PropertySummaryDescriptor property;
private final List<String> activeProfiles;
private final List<PropertySourceEntryDescriptor> propertySources;
private EnvironmentEntryDescriptor(PropertySummaryDescriptor property,
List<String> activeProfiles,
List<PropertySourceEntryDescriptor> propertySources) {
this.property = property;
this.activeProfiles = activeProfiles;
this.propertySources = propertySources;
}
public PropertySummaryDescriptor getProperty() {
return this.property;
}
public List<String> getActiveProfiles() {
return this.activeProfiles;
}
public List<PropertySourceEntryDescriptor> getPropertySources() {
return this.propertySources;
}
}
/**
* A summary of a particular entry of the {@link Environment}.
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public static final class PropertySummaryDescriptor {
private final String source;
private final Object value;
public PropertySummaryDescriptor(String source, Object value) {
this.source = source;
this.value = value;
}
public String getSource() {
return this.source;
}
public Object getValue() {
return this.value;
}
}
/**
* A description of a {@link PropertySource}.
*/
public static final class PropertySourceDescriptor {
private final String name;
private final Map<String, PropertyValueDescriptor> properties;
private PropertySourceDescriptor(String name,
Map<String, PropertyValueDescriptor> properties) {
this.name = name;
this.properties = properties;
}
public String getName() {
return this.name;
}
public Map<String, PropertyValueDescriptor> getProperties() {
return this.properties;
}
}
/**
* A description of a particular entry of {@link PropertySource}.
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public static final class PropertySourceEntryDescriptor {
private final String name;
private final PropertyValueDescriptor property;
private PropertySourceEntryDescriptor(String name,
PropertyValueDescriptor property) {
this.name = name;
this.property = property;
}
public String getName() {
return this.name;
}
public PropertyValueDescriptor getProperty() {
return this.property;
}
}
/**
* A description of a property's value, including its origin if available.
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public static final class PropertyValueDescriptor {
private final Object value;
private final String origin;
private PropertyValueDescriptor(Object value, String origin) {
this.value = value;
this.origin = origin;
}
public Object getValue() {
return this.value;
}
public String getOrigin() {
return this.origin;
}
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.env;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.Selector;
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpointExtension;
import org.springframework.boot.actuate.env.EnvironmentEndpoint.EnvironmentEntryDescriptor;
/**
* {@link WebEndpointExtension} for the {@link EnvironmentEndpoint}.
*
* @author Stephane Nicoll
* @since 2.0.0
*/
@WebEndpointExtension(endpoint = EnvironmentEndpoint.class)
public class EnvironmentWebEndpointExtension {
private final EnvironmentEndpoint delegate;
public EnvironmentWebEndpointExtension(EnvironmentEndpoint delegate) {
this.delegate = delegate;
}
@ReadOperation
public WebEndpointResponse<EnvironmentEntryDescriptor> environmentEntry(
@Selector String toMatch) {
EnvironmentEntryDescriptor descriptor = this.delegate.environmentEntry(toMatch);
return new WebEndpointResponse<>(descriptor, getStatus(descriptor));
}
private int getStatus(EnvironmentEntryDescriptor descriptor) {
if (descriptor.getProperty() == null) {
return WebEndpointResponse.STATUS_NOT_FOUND;
}
return WebEndpointResponse.STATUS_OK;
}
}

View File

@@ -0,0 +1,21 @@
/*
* Copyright 2012-2017 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
*
* http://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.
*/
/**
* Actuator support for Spring Framework's
* {@link org.springframework.core.env.Environment}.
*/
package org.springframework.boot.actuate.env;

View File

@@ -0,0 +1,167 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.flyway;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.flywaydb.core.Flyway;
import org.flywaydb.core.api.MigrationInfo;
import org.flywaydb.core.api.MigrationState;
import org.flywaydb.core.api.MigrationType;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.util.Assert;
/**
* {@link Endpoint} to expose flyway info.
*
* @author Eddú Meléndez
* @author Phillip Webb
* @author Andy Wilkinson
* @since 2.0.0
*/
@Endpoint(id = "flyway")
public class FlywayEndpoint {
private final Map<String, Flyway> flywayBeans;
public FlywayEndpoint(Map<String, Flyway> flywayBeans) {
Assert.notEmpty(flywayBeans, "FlywayBeans must be specified");
this.flywayBeans = flywayBeans;
}
@ReadOperation
public Map<String, FlywayReport> flywayReports() {
Map<String, FlywayReport> reports = new HashMap<>();
this.flywayBeans.forEach((name, flyway) -> reports.put(name,
new FlywayReport(flyway.info().all())));
return reports;
}
/**
* Report for one {@link Flyway} instance.
*/
public static class FlywayReport {
private final List<FlywayMigration> migrations;
public FlywayReport(MigrationInfo[] migrations) {
this.migrations = Stream.of(migrations).map(FlywayMigration::new)
.collect(Collectors.toList());
}
public FlywayReport(List<FlywayMigration> migrations) {
this.migrations = migrations;
}
public List<FlywayMigration> getMigrations() {
return this.migrations;
}
}
/**
* Details of a migration performed by Flyway.
*/
public static class FlywayMigration {
private final MigrationType type;
private final Integer checksum;
private final String version;
private final String description;
private final String script;
private final MigrationState state;
private final String installedBy;
private final Date installedOn;
private final Integer installedRank;
private final Integer executionTime;
public FlywayMigration(MigrationInfo info) {
this.type = info.getType();
this.checksum = info.getChecksum();
this.version = nullSafeToString(info.getVersion());
this.description = info.getDescription();
this.script = info.getScript();
this.state = info.getState();
this.installedBy = info.getInstalledBy();
this.installedOn = info.getInstalledOn();
this.installedRank = info.getInstalledRank();
this.executionTime = info.getExecutionTime();
}
private String nullSafeToString(Object obj) {
return (obj == null ? null : obj.toString());
}
public MigrationType getType() {
return this.type;
}
public Integer getChecksum() {
return this.checksum;
}
public String getVersion() {
return this.version;
}
public String getDescription() {
return this.description;
}
public String getScript() {
return this.script;
}
public MigrationState getState() {
return this.state;
}
public String getInstalledBy() {
return this.installedBy;
}
public Date getInstalledOn() {
return this.installedOn;
}
public Integer getInstalledRank() {
return this.installedRank;
}
public Integer getExecutionTime() {
return this.executionTime;
}
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2017 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
*
* http://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.
*/
/**
* Actuator support for Flyway.
*/
package org.springframework.boot.actuate.flyway;

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.health;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Base {@link HealthAggregator} implementation to allow subclasses to focus on
* aggregating the {@link Status} instances and not deal with contextual details etc.
*
* @author Christian Dupuis
* @author Vedran Pavic
* @since 1.1.0
*/
public abstract class AbstractHealthAggregator implements HealthAggregator {
@Override
public final Health aggregate(Map<String, Health> healths) {
List<Status> statusCandidates = new ArrayList<>();
for (Map.Entry<String, Health> entry : healths.entrySet()) {
statusCandidates.add(entry.getValue().getStatus());
}
Status status = aggregateStatus(statusCandidates);
Map<String, Object> details = aggregateDetails(healths);
return new Health.Builder(status, details).build();
}
/**
* Return the single 'aggregate' status that should be used from the specified
* candidates.
* @param candidates the candidates
* @return a single status
*/
protected abstract Status aggregateStatus(List<Status> candidates);
/**
* Return the map of 'aggregate' details that should be used from the specified
* healths.
* @param healths the health instances to aggregate
* @return a map of details
* @since 1.3.1
*/
protected Map<String, Object> aggregateDetails(Map<String, Health> healths) {
return new LinkedHashMap<>(healths);
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.health;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.actuate.health.Health.Builder;
/**
* Base {@link HealthIndicator} implementations that encapsulates creation of
* {@link Health} instance and error handling.
* <p>
* This implementation is only suitable if an {@link Exception} raised from
* {@link #doHealthCheck(org.springframework.boot.actuate.health.Health.Builder)} should
* create a {@link Status#DOWN} health status.
*
* @author Christian Dupuis
* @since 1.1.0
*/
public abstract class AbstractHealthIndicator implements HealthIndicator {
private final Log logger = LogFactory.getLog(getClass());
@Override
public final Health health() {
Health.Builder builder = new Health.Builder();
try {
doHealthCheck(builder);
}
catch (Exception ex) {
this.logger.warn("Health check failed", ex);
builder.down(ex);
}
return builder.build();
}
/**
* Actual health check logic.
* @param builder the {@link Builder} to report health status and details
* @throws Exception any {@link Exception} that should create a {@link Status#DOWN}
* system status.
*/
protected abstract void doHealthCheck(Health.Builder builder) throws Exception;
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.health;
import reactor.core.publisher.Mono;
/**
* Base {@link ReactiveHealthIndicator} implementations that encapsulates creation of
* {@link Health} instance and error handling.
*
* @author Stephane Nicoll
* @since 2.0.0
*/
public abstract class AbstractReactiveHealthIndicator implements ReactiveHealthIndicator {
@Override
public final Mono<Health> health() {
return doHealthCheck(new Health.Builder())
.onErrorResume((ex) -> Mono.just(new Health.Builder().down(ex).build()));
}
/**
* Actual health check logic. If an error occurs in the pipeline it will be handled
* automatically.
* @param builder the {@link Health.Builder} to report health status and details
* @return a {@link Mono} that provides the {@link Health}
*/
protected abstract Mono<Health> doHealthCheck(Health.Builder builder);
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2012-2014 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
*
* http://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.boot.actuate.health;
/**
* Default implementation of {@link HealthIndicator} that returns {@link Status#UP}.
*
* @author Dave Syer
* @author Christian Dupuis
* @see Status#UP
*/
public class ApplicationHealthIndicator extends AbstractHealthIndicator {
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
builder.up();
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.health;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.util.Assert;
/**
* {@link HealthIndicator} that returns health indications from all registered delegates.
*
* @author Tyler J. Frederick
* @author Phillip Webb
* @author Christian Dupuis
* @since 1.1.0
*/
public class CompositeHealthIndicator implements HealthIndicator {
private final Map<String, HealthIndicator> indicators;
private final HealthAggregator healthAggregator;
/**
* Create a new {@link CompositeHealthIndicator}.
* @param healthAggregator the health aggregator
*/
public CompositeHealthIndicator(HealthAggregator healthAggregator) {
this(healthAggregator, new LinkedHashMap<>());
}
/**
* Create a new {@link CompositeHealthIndicator} from the specified indicators.
* @param healthAggregator the health aggregator
* @param indicators a map of {@link HealthIndicator}s with the key being used as an
* indicator name.
*/
public CompositeHealthIndicator(HealthAggregator healthAggregator,
Map<String, HealthIndicator> indicators) {
Assert.notNull(healthAggregator, "HealthAggregator must not be null");
Assert.notNull(indicators, "Indicators must not be null");
this.indicators = new LinkedHashMap<>(indicators);
this.healthAggregator = healthAggregator;
}
public void addHealthIndicator(String name, HealthIndicator indicator) {
this.indicators.put(name, indicator);
}
@Override
public Health health() {
Map<String, Health> healths = new LinkedHashMap<>();
for (Map.Entry<String, HealthIndicator> entry : this.indicators.entrySet()) {
healths.put(entry.getKey(), entry.getValue().health());
}
return this.healthAggregator.aggregate(healths);
}
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.health;
import java.util.Map;
import java.util.function.Function;
import org.springframework.util.Assert;
/**
* Factory to create a {@link CompositeHealthIndicator}.
*
* @author Stephane Nicoll
* @since 2.0.0
*/
public class CompositeHealthIndicatorFactory {
private final Function<String, String> healthIndicatorNameFactory;
public CompositeHealthIndicatorFactory(
Function<String, String> healthIndicatorNameFactory) {
this.healthIndicatorNameFactory = healthIndicatorNameFactory;
}
public CompositeHealthIndicatorFactory() {
this(new HealthIndicatorNameFactory());
}
/**
* Create a {@link CompositeHealthIndicator} based on the specified health indicators.
* @param healthAggregator the {@link HealthAggregator}
* @param healthIndicators the {@link HealthIndicator} instances mapped by name
* @return a {@link HealthIndicator} that delegates to the specified
* {@code healthIndicators}.
*/
public CompositeHealthIndicator createHealthIndicator(
HealthAggregator healthAggregator,
Map<String, HealthIndicator> healthIndicators) {
Assert.notNull(healthAggregator, "HealthAggregator must not be null");
Assert.notNull(healthIndicators, "HealthIndicators must not be null");
CompositeHealthIndicator healthIndicator = new CompositeHealthIndicator(
healthAggregator);
for (Map.Entry<String, HealthIndicator> entry : healthIndicators.entrySet()) {
String name = this.healthIndicatorNameFactory.apply(entry.getKey());
healthIndicator.addHealthIndicator(name, entry.getValue());
}
return healthIndicator;
}
}

View File

@@ -0,0 +1,102 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.health;
import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Function;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.function.Tuple2;
import org.springframework.util.Assert;
/**
* {@link ReactiveHealthIndicator} that returns health indications from all registered
* delegates. Provides an alternative {@link Health} for a delegate that reaches a
* configurable timeout.
*
* @author Stephane Nicoll
* @since 2.0.0
*/
public class CompositeReactiveHealthIndicator implements ReactiveHealthIndicator {
private final Map<String, ReactiveHealthIndicator> indicators;
private final HealthAggregator healthAggregator;
private Long timeout;
private Health timeoutHealth;
private final Function<Mono<Health>, Mono<Health>> timeoutCompose;
public CompositeReactiveHealthIndicator(HealthAggregator healthAggregator) {
this(healthAggregator, new LinkedHashMap<>());
}
public CompositeReactiveHealthIndicator(HealthAggregator healthAggregator,
Map<String, ReactiveHealthIndicator> indicators) {
Assert.notNull(healthAggregator, "HealthAggregator must not be null");
Assert.notNull(indicators, "Indicators must not be null");
this.indicators = new LinkedHashMap<>(indicators);
this.healthAggregator = healthAggregator;
this.timeoutCompose = (mono) -> this.timeout != null ? mono.timeout(
Duration.ofMillis(this.timeout), Mono.just(this.timeoutHealth)) : mono;
}
/**
* Add a {@link ReactiveHealthIndicator} with the specified name.
* @param name the name of the health indicator
* @param indicator the health indicator to add
* @return this instance
*/
public CompositeReactiveHealthIndicator addHealthIndicator(String name,
ReactiveHealthIndicator indicator) {
this.indicators.put(name, indicator);
return this;
}
/**
* Specify an alternative timeout {@link Health} if a {@link HealthIndicator} failed
* to reply after specified {@code timeout}.
* @param timeout number of milliseconds to wait before using the
* {@code timeoutHealth}
* @param timeoutHealth the {@link Health} to use if an health indicator reached the
* {@code timeout}
* @return this instance
*/
public CompositeReactiveHealthIndicator timeoutStrategy(long timeout,
Health timeoutHealth) {
this.timeout = timeout;
this.timeoutHealth = (timeoutHealth != null ? timeoutHealth
: Health.unknown().build());
return this;
}
@Override
public Mono<Health> health() {
return Flux.fromIterable(this.indicators.entrySet())
.flatMap((entry) -> Mono.zip(Mono.just(entry.getKey()),
entry.getValue().health().compose(this.timeoutCompose)))
.collectMap(Tuple2::getT1, Tuple2::getT2)
.map(this.healthAggregator::aggregate);
}
}

View File

@@ -0,0 +1,91 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.health;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Function;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/**
* Factory to create a {@link CompositeReactiveHealthIndicator}.
*
* @author Stephane Nicoll
* @since 2.0.0
*/
public class CompositeReactiveHealthIndicatorFactory {
private final Function<String, String> healthIndicatorNameFactory;
public CompositeReactiveHealthIndicatorFactory(
Function<String, String> healthIndicatorNameFactory) {
this.healthIndicatorNameFactory = healthIndicatorNameFactory;
}
public CompositeReactiveHealthIndicatorFactory() {
this(new HealthIndicatorNameFactory());
}
/**
* Create a {@link CompositeReactiveHealthIndicator} based on the specified health
* indicators. Each {@link HealthIndicator} are wrapped to a
* {@link HealthIndicatorReactiveAdapter}. If two instances share the same name, the
* reactive variant takes precedence.
* @param healthAggregator the {@link HealthAggregator}
* @param reactiveHealthIndicators the {@link ReactiveHealthIndicator} instances
* mapped by name
* @param healthIndicators the {@link HealthIndicator} instances mapped by name if
* any.
* @return a {@link ReactiveHealthIndicator} that delegates to the specified
* {@code reactiveHealthIndicators}.
*/
public CompositeReactiveHealthIndicator createReactiveHealthIndicator(
HealthAggregator healthAggregator,
Map<String, ReactiveHealthIndicator> reactiveHealthIndicators,
Map<String, HealthIndicator> healthIndicators) {
Assert.notNull(healthAggregator, "HealthAggregator must not be null");
Assert.notNull(reactiveHealthIndicators,
"ReactiveHealthIndicators must not be null");
CompositeReactiveHealthIndicator healthIndicator = new CompositeReactiveHealthIndicator(
healthAggregator);
merge(reactiveHealthIndicators, healthIndicators)
.forEach((beanName, indicator) -> {
String name = this.healthIndicatorNameFactory.apply(beanName);
healthIndicator.addHealthIndicator(name, indicator);
});
return healthIndicator;
}
private Map<String, ReactiveHealthIndicator> merge(
Map<String, ReactiveHealthIndicator> reactiveHealthIndicators,
Map<String, HealthIndicator> healthIndicators) {
if (ObjectUtils.isEmpty(healthIndicators)) {
return reactiveHealthIndicators;
}
Map<String, ReactiveHealthIndicator> allIndicators = new LinkedHashMap<>(
reactiveHealthIndicators);
healthIndicators.forEach((beanName, indicator) -> {
String name = this.healthIndicatorNameFactory.apply(beanName);
allIndicators.computeIfAbsent(name,
(n) -> new HealthIndicatorReactiveAdapter(indicator));
});
return allIndicators;
}
}

View File

@@ -0,0 +1,303 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.actuate.health;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import org.springframework.util.Assert;
/**
* Carries information about the health of a component or subsystem.
* <p>
* {@link Health} contains a {@link Status} to express the state of a component or
* subsystem and some additional details to carry some contextual information.
* <p>
* {@link Health} instances can be created by using {@link Builder}'s fluent API. Typical
* usage in a {@link HealthIndicator} would be:
*
* <pre class="code">
* try {
* // do some test to determine state of component
* return new Health.Builder().up().withDetail("version", "1.1.2").build();
* }
* catch (Exception ex) {
* return new Health.Builder().down(ex).build();
* }
* </pre>
*
* @author Christian Dupuis
* @author Phillip Webb
* @since 1.1.0
*/
@JsonInclude(Include.NON_EMPTY)
public final class Health {
private final Status status;
private final Map<String, Object> details;
/**
* Create a new {@link Health} instance with the specified status and details.
* @param builder the Builder to use
*/
private Health(Builder builder) {
Assert.notNull(builder, "Builder must not be null");
this.status = builder.status;
this.details = Collections.unmodifiableMap(builder.details);
}
/**
* Return the status of the health.
* @return the status (never {@code null})
*/
@JsonUnwrapped
public Status getStatus() {
return this.status;
}
/**
* Return the details of the health.
* @return the details (or an empty map)
*/
public Map<String, Object> getDetails() {
return this.details;
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj != null && obj instanceof Health) {
Health other = (Health) obj;
return this.status.equals(other.status) && this.details.equals(other.details);
}
return false;
}
@Override
public int hashCode() {
int hashCode = this.status.hashCode();
return 13 * hashCode + this.details.hashCode();
}
@Override
public String toString() {
return getStatus() + " " + getDetails();
}
/**
* Create a new {@link Builder} instance with an {@link Status#UNKNOWN} status.
* @return a new {@link Builder} instance
*/
public static Builder unknown() {
return status(Status.UNKNOWN);
}
/**
* Create a new {@link Builder} instance with an {@link Status#UP} status.
* @return a new {@link Builder} instance
*/
public static Builder up() {
return status(Status.UP);
}
/**
* Create a new {@link Builder} instance with an {@link Status#DOWN} status and the
* specified exception details.
* @param ex the exception
* @return a new {@link Builder} instance
*/
public static Builder down(Exception ex) {
return down().withException(ex);
}
/**
* Create a new {@link Builder} instance with a {@link Status#DOWN} status.
* @return a new {@link Builder} instance
*/
public static Builder down() {
return status(Status.DOWN);
}
/**
* Create a new {@link Builder} instance with an {@link Status#OUT_OF_SERVICE} status.
* @return a new {@link Builder} instance
*/
public static Builder outOfService() {
return status(Status.OUT_OF_SERVICE);
}
/**
* Create a new {@link Builder} instance with a specific status code.
* @param statusCode the status code
* @return a new {@link Builder} instance
*/
public static Builder status(String statusCode) {
return status(new Status(statusCode));
}
/**
* Create a new {@link Builder} instance with a specific {@link Status}.
* @param status the status
* @return a new {@link Builder} instance
*/
public static Builder status(Status status) {
return new Builder(status);
}
/**
* Builder for creating immutable {@link Health} instances.
*/
public static class Builder {
private Status status;
private Map<String, Object> details;
/**
* Create new Builder instance.
*/
public Builder() {
this.status = Status.UNKNOWN;
this.details = new LinkedHashMap<>();
}
/**
* Create new Builder instance, setting status to given {@code status}.
* @param status the {@link Status} to use
*/
public Builder(Status status) {
Assert.notNull(status, "Status must not be null");
this.status = status;
this.details = new LinkedHashMap<>();
}
/**
* Create new Builder instance, setting status to given {@code status} and details
* to given {@code details}.
* @param status the {@link Status} to use
* @param details the details {@link Map} to use
*/
public Builder(Status status, Map<String, ?> details) {
Assert.notNull(status, "Status must not be null");
Assert.notNull(details, "Details must not be null");
this.status = status;
this.details = new LinkedHashMap<>(details);
}
/**
* Record detail for given {@link Exception}.
* @param ex the exception
* @return this {@link Builder} instance
*/
public Builder withException(Throwable ex) {
Assert.notNull(ex, "Exception must not be null");
return withDetail("error", ex.getClass().getName() + ": " + ex.getMessage());
}
/**
* Record detail using given {@code key} and {@code value}.
* @param key the detail key
* @param value the detail value
* @return this {@link Builder} instance
*/
public Builder withDetail(String key, Object value) {
Assert.notNull(key, "Key must not be null");
Assert.notNull(value, "Value must not be null");
this.details.put(key, value);
return this;
}
/**
* Set status to {@link Status#UNKNOWN} status.
* @return this {@link Builder} instance
*/
public Builder unknown() {
return status(Status.UNKNOWN);
}
/**
* Set status to {@link Status#UP} status.
* @return this {@link Builder} instance
*/
public Builder up() {
return status(Status.UP);
}
/**
* Set status to {@link Status#DOWN} and add details for given {@link Throwable}.
* @param ex the exception
* @return this {@link Builder} instance
*/
public Builder down(Throwable ex) {
return down().withException(ex);
}
/**
* Set status to {@link Status#DOWN}.
* @return this {@link Builder} instance
*/
public Builder down() {
return status(Status.DOWN);
}
/**
* Set status to {@link Status#OUT_OF_SERVICE}.
* @return this {@link Builder} instance
*/
public Builder outOfService() {
return status(Status.OUT_OF_SERVICE);
}
/**
* Set status to given {@code statusCode}.
* @param statusCode the status code
* @return this {@link Builder} instance
*/
public Builder status(String statusCode) {
return status(new Status(statusCode));
}
/**
* Set status to given {@link Status} instance.
* @param status the status
* @return this {@link Builder} instance
*/
public Builder status(Status status) {
this.status = status;
return this;
}
/**
* Create a new {@link Health} instance with the previously specified code and
* details.
* @return a new {@link Health} instance
*/
public Health build() {
return new Health(this);
}
}
}

Some files were not shown because too many files have changed in this diff Show More