move netflix code from sandbox

This commit is contained in:
Spencer Gibb
2014-07-11 15:53:27 -06:00
parent d454e42ac9
commit 998b7dc784
44 changed files with 2757 additions and 73 deletions

3
.gitignore vendored
View File

@@ -7,4 +7,5 @@
.settings/
.springBeans
target/
.idea
*.iml

163
pom.xml
View File

@@ -1,74 +1,107 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.platform</groupId>
<artifactId>spring-platform-netflix</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<groupId>org.springframework.platform</groupId>
<artifactId>spring-platform-netflix</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<packaging>pom</packaging>
<name>spring-platform-netflix</name>
<description>Spring Platform Netflix</description>
<name>spring-platform-netflix</name>
<description>Eureka integration project</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.5.BUILD-SNAPSHOT</version>
<relativePath/>
<!-- lookup parent from repository -->
</parent>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.5.BUILD-SNAPSHOT</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.netflix.eureka</groupId>
<artifactId>eureka-client</artifactId>
<version>1.1.135</version>
</dependency>
<dependency>
<groupId>com.netflix.eureka</groupId>
<artifactId>eureka-client</artifactId>
<version>1.1.135</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.12.6</version>
</dependency>
</dependencies>
</dependencyManagement>
<modules>
<module>spring-platform-netflix-core</module>
<module>spring-platform-netflix-zuul</module>
</modules>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.eureka</groupId>
<artifactId>eureka-client</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.eureka</groupId>
<artifactId>eureka-client</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.netflix.archaius</groupId>
<artifactId>archaius-core</artifactId>
<version>${archaius.version}</version>
</dependency>
<dependency>
<groupId>com.netflix.eureka</groupId>
<artifactId>eureka-client</artifactId>
<version>${eureka.version}</version>
</dependency>
<dependency>
<groupId>com.netflix.feign</groupId>
<artifactId>feign-core</artifactId>
<version>${feign.version}</version>
</dependency>
<dependency>
<groupId>com.netflix.feign</groupId>
<artifactId>feign-ribbon</artifactId>
<version>${feign.version}</version>
</dependency>
<!--<dependency>
<groupId>com.netflix.feign</groupId>
<artifactId>feign-slf4j</artifactId>
<version>${feign.version}</version>
</dependency>-->
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-core</artifactId>
<version>${hystrix.version}</version>
</dependency>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-metrics-event-stream</artifactId>
<version>${hystrix.version}</version>
</dependency>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-javanica</artifactId>
<version>${hystrix.version}</version>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-core</artifactId>
<version>${ribbon.version}</version>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-httpclient</artifactId>
<version>${ribbon.version}</version>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-eureka</artifactId>
<version>${ribbon.version}</version>
</dependency>
<dependency>
<groupId>com.netflix.zuul</groupId>
<artifactId>zuul-core</artifactId>
<version>${zuul.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.12.6</version>
</dependency>
</dependencies>
</dependencyManagement>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.7</java.version>
</properties>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.7</java.version>
<archaius.version>0.6.0</archaius.version>
<eureka.version>1.1.135</eureka.version>
<feign.version>6.1.2</feign.version>
<hystrix.version>1.4.0-RC4</hystrix.version>
<ribbon.version>0.3.12</ribbon.version>
<zuul.version>1.0.24</zuul.version>
</properties>
</project>

View File

@@ -0,0 +1,85 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.platform</groupId>
<artifactId>spring-platform-netflix-core</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-platform-netflix-core</name>
<description>Spring Platform Netflix Core</description>
<parent>
<groupId>org.springframework.platform</groupId>
<artifactId>spring-platform-netflix</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.archaius</groupId>
<artifactId>archaius-core</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.eureka</groupId>
<artifactId>eureka-client</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.eureka</groupId>
<artifactId>eureka-client</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.netflix.feign</groupId>
<artifactId>feign-core</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.feign</groupId>
<artifactId>feign-ribbon</artifactId>
</dependency>
<!--<dependency>
<groupId>com.netflix.feign</groupId>
<artifactId>feign-slf4j</artifactId>
</dependency>-->
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-core</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-metrics-event-stream</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-javanica</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-core</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,78 @@
package org.springframework.platform.netflix.archaius;
import com.netflix.config.ConcurrentCompositeConfiguration;
import com.netflix.config.ConfigurationManager;
import com.netflix.config.DynamicURLConfiguration;
import org.apache.commons.configuration.EnvironmentConfiguration;
import org.apache.commons.configuration.SystemConfiguration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.ConfigurableEnvironment;
import static com.netflix.config.ConfigurationBasedDeploymentContext.DEPLOYMENT_APPLICATION_ID_PROPERTY;
import static com.netflix.config.ConfigurationManager.*;
import static com.netflix.config.ConfigurationManager.APPLICATION_PROPERTIES;
import static com.netflix.config.ConfigurationManager.ENV_CONFIG_NAME;
/**
* Created by sgibb on 7/3/14.
*/
@Configuration
public class ArchaiusAutoConfiguration {
@Autowired
ConfigurableEnvironment env;
@Bean
ConfigurableEnvironmentConfiguration configurableEnvironmentConfiguration() {
ConfigurableEnvironmentConfiguration envConfig = new ConfigurableEnvironmentConfiguration(env);
configureArchaius(envConfig);
return envConfig;
}
protected void configureArchaius(ConfigurableEnvironmentConfiguration envConfig) {
String appName = env.getProperty("spring.application.name");
if (appName == null) {
throw new IllegalStateException("spring.application.name may not be null");
}
//this is deprecated, but currently it seams the only way to set it initially
System.setProperty(DEPLOYMENT_APPLICATION_ID_PROPERTY, appName);
//TODO: support for other DeploymentContexts
ConcurrentCompositeConfiguration config = new ConcurrentCompositeConfiguration();
//support to add other Configurations (Jdbc, DynamoDb, Zookeeper, jclouds, etc...)
/*if (factories != null && !factories.isEmpty()) {
for (PropertiesSourceFactory factory: factories) {
config.addConfiguration(factory.getConfiguration(), factory.getName());
}
}*/
config.addConfiguration(envConfig, ConfigurableEnvironmentConfiguration.class.getSimpleName());
//below come from ConfigurationManager.createDefaultConfigInstance()
try {
DynamicURLConfiguration defaultURLConfig = new DynamicURLConfiguration();
config.addConfiguration(defaultURLConfig, URL_CONFIG_NAME);
} catch (Throwable e) {
e.printStackTrace(); //TODO: log error
}
//TODO: sys/env above urls?
if (!Boolean.getBoolean(DISABLE_DEFAULT_SYS_CONFIG)) {
SystemConfiguration sysConfig = new SystemConfiguration();
config.addConfiguration(sysConfig, SYS_CONFIG_NAME);
}
if (!Boolean.getBoolean(DISABLE_DEFAULT_ENV_CONFIG)) {
EnvironmentConfiguration environmentConfiguration = new EnvironmentConfiguration();
config.addConfiguration(environmentConfiguration, ENV_CONFIG_NAME);
}
ConcurrentCompositeConfiguration appOverrideConfig = new ConcurrentCompositeConfiguration();
config.addConfiguration(appOverrideConfig, APPLICATION_PROPERTIES);
config.setContainerConfigurationIndex(config.getIndexOfConfiguration(appOverrideConfig));
ConfigurationManager.install(config);
}
}

View File

@@ -0,0 +1,97 @@
package org.springframework.platform.netflix.archaius;
import org.apache.commons.configuration.AbstractConfiguration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.*;
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Field;
import java.util.*;
/**
* Created by sgibb on 6/27/14.
*/
public class ConfigurableEnvironmentConfiguration extends AbstractConfiguration {
ConfigurableEnvironment environment;
public ConfigurableEnvironmentConfiguration(ConfigurableEnvironment environment) {
this.environment = environment;
}
@Override
protected void addPropertyDirect(String key, Object value) {
}
@Override
public boolean isEmpty() {
return !getKeys().hasNext(); //TODO: find a better way to do this
}
@Override
public boolean containsKey(String key) {
return environment.containsProperty(key);
}
@Override
public Object getProperty(String key) {
return environment.getProperty(key);
}
@Override
public Iterator<String> getKeys() {
List<String> result = new ArrayList<>();
for (Map.Entry<String, PropertySource<?>> entry : getPropertySources().entrySet()) {
PropertySource<?> source = entry.getValue();
if (source instanceof EnumerablePropertySource) {
EnumerablePropertySource<?> enumerable = (EnumerablePropertySource<?>) source;
for (String name : enumerable.getPropertyNames()) {
result.add(name);
}
}
}
return result.iterator();
}
private Map<String, PropertySource<?>> getPropertySources() {
Map<String, PropertySource<?>> map = new LinkedHashMap<String, PropertySource<?>>();
MutablePropertySources sources = null;
if (this.environment != null
&& this.environment instanceof ConfigurableEnvironment) {
sources = ((ConfigurableEnvironment) this.environment).getPropertySources();
}
else {
sources = new StandardEnvironment().getPropertySources();
}
for (PropertySource<?> source : sources) {
extract("", map, source);
}
return map;
}
private void extract(String root, Map<String, PropertySource<?>> map,
PropertySource<?> source) {
if (source instanceof CompositePropertySource) {
Set<PropertySource<?>> nested = getNestedPropertySources((CompositePropertySource) source);
for (PropertySource<?> nest : nested) {
extract(source.getName() + ":", map, nest);
}
}
else {
map.put(root + source.getName(), source);
}
}
@SuppressWarnings("unchecked")
private Set<PropertySource<?>> getNestedPropertySources(CompositePropertySource source) {
try {
Field field = ReflectionUtils.findField(CompositePropertySource.class,
"propertySources");
field.setAccessible(true);
return (Set<PropertySource<?>>) field.get(source);
}
catch (Exception ex) {
return Collections.emptySet();
}
}
}

View File

@@ -0,0 +1,56 @@
package org.springframework.platform.netflix.circuitbreaker;
import com.netflix.hystrix.contrib.javanica.aop.aspectj.HystrixCommandAspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportAware;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.platform.netflix.circuitbreaker.annotations.EnableCircuitBreaker;
import org.springframework.platform.netflix.endpoint.HystrixStreamEndpoint;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import java.util.Collection;
/**
* Created by sgibb on 6/19/14.
*/
@Configuration
public class CircuitBreakerConfiguration implements ImportAware {
private AnnotationAttributes enableCircuitBreaker;
@Bean
HystrixCommandAspect hystrixCommandAspect() {
return new HystrixCommandAspect();
}
@Bean
//TODO: add enable/disable
public HystrixStreamEndpoint hystrixStreamEndpoint() {
return new HystrixStreamEndpoint();
}
@Override
public void setImportMetadata(AnnotationMetadata importMetadata) {
this.enableCircuitBreaker = AnnotationAttributes.fromMap(
importMetadata.getAnnotationAttributes(EnableCircuitBreaker.class.getName(), false));
Assert.notNull(this.enableCircuitBreaker,
"@EnableCircuitBreaker is not present on importing class " + importMetadata.getClassName());
}
@Autowired(required=false)
void setConfigurers(Collection<CircuitBreakerConfigurer> configurers) {
if (CollectionUtils.isEmpty(configurers)) {
return;
}
if (configurers.size() > 1) {
throw new IllegalStateException("Only one TransactionManagementConfigurer may exist");
}
//TODO: create CircuitBreakerConfigurer API
CircuitBreakerConfigurer configurer = configurers.iterator().next();
//this.txManager = configurer.annotationDrivenTransactionManager();
}
}

View File

@@ -0,0 +1,29 @@
package org.springframework.platform.netflix.circuitbreaker;
import org.springframework.context.annotation.AdviceMode;
import org.springframework.context.annotation.AdviceModeImportSelector;
import org.springframework.context.annotation.AutoProxyRegistrar;
import org.springframework.platform.netflix.circuitbreaker.annotations.EnableCircuitBreaker;
/**
* Created by sgibb on 6/19/14.
*/
public class CircuitBreakerConfigurationSelector extends AdviceModeImportSelector<EnableCircuitBreaker> {
/**
* The name of the AspectJ transaction management @{@code Configuration} class.
*/
public static final String TRANSACTION_ASPECT_CONFIGURATION_CLASS_NAME =
"org.springframework.transaction.aspectj.AspectJTransactionManagementConfiguration";
@Override
protected String[] selectImports(AdviceMode adviceMode) {
switch (adviceMode) {
case PROXY:
return new String[]{AutoProxyRegistrar.class.getName(), CircuitBreakerConfiguration.class.getName()};
case ASPECTJ:
return new String[]{TRANSACTION_ASPECT_CONFIGURATION_CLASS_NAME};
default:
return null;
}
}
}

View File

@@ -0,0 +1,7 @@
package org.springframework.platform.netflix.circuitbreaker;
/**
* Created by sgibb on 6/19/14.
*/
public interface CircuitBreakerConfigurer {
}

View File

@@ -0,0 +1,47 @@
package org.springframework.platform.netflix.circuitbreaker.annotations;
import org.springframework.context.annotation.AdviceMode;
import org.springframework.context.annotation.Import;
import org.springframework.core.Ordered;
import org.springframework.platform.netflix.circuitbreaker.CircuitBreakerConfigurationSelector;
import java.lang.annotation.*;
/**
* Created by sgibb on 6/19/14.
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(CircuitBreakerConfigurationSelector.class)
public @interface EnableCircuitBreaker {
/**
* Indicate whether subclass-based (CGLIB) proxies are to be created ({@code true}) as
* opposed to standard Java interface-based proxies ({@code false}). The default is
* {@code false}. <strong>Applicable only if {@link #mode()} is set to
* {@link org.springframework.context.annotation.AdviceMode#PROXY}</strong>.
* <p>Note that setting this attribute to {@code true} will affect <em>all</em>
* Spring-managed beans requiring proxying, not just those marked with
* {@code @Transactional}. For example, other beans marked with Spring's
* {@code @Async} annotation will be upgraded to subclass proxying at the same
* time. This approach has no negative impact in practice unless one is explicitly
* expecting one type of proxy vs another, e.g. in tests.
*/
boolean proxyTargetClass() default false;
/**
* Indicate how transactional advice should be applied. The default is
* {@link org.springframework.context.annotation.AdviceMode#PROXY}.
* @see org.springframework.context.annotation.AdviceMode
*/
AdviceMode mode() default AdviceMode.PROXY;
/**
* Indicate the ordering of the execution of the transaction advisor
* when multiple advices are applied at a specific joinpoint.
* The default is {@link org.springframework.core.Ordered#LOWEST_PRECEDENCE}.
*/
int order() default Ordered.LOWEST_PRECEDENCE;
}

View File

@@ -0,0 +1,15 @@
package org.springframework.platform.netflix.endpoint;
import com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsStreamServlet;
/**
* User: spencergibb
* Date: 4/22/14
* Time: 3:16 PM
*/
public class HystrixStreamEndpoint extends ServletWrappingEndpoint {
public HystrixStreamEndpoint() {
super(HystrixMetricsStreamServlet.class, "hystrixStream", "hystrix.stream", false, true);
}
}

View File

@@ -0,0 +1,76 @@
package org.springframework.platform.netflix.endpoint;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.actuate.endpoint.Endpoint;
import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.ServletContextAware;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.ServletWrappingController;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* TODO: move to spring-boot?
* User: spencergibb
* Date: 4/24/14
* Time: 9:13 PM
*/
public abstract class ServletWrappingEndpoint implements InitializingBean,
ApplicationContextAware, ServletContextAware, MvcEndpoint {
protected String path;
protected boolean sensitive;
protected boolean enabled = true;
protected final ServletWrappingController controller = new ServletWrappingController();
@Override
public void afterPropertiesSet() throws Exception {
this.controller.afterPropertiesSet();
}
@Override
public void setServletContext(ServletContext servletContext) {
this.controller.setServletContext(servletContext);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.controller.setApplicationContext(applicationContext);
}
protected ServletWrappingEndpoint(Class<?> servletClass, String servletName, String path,
boolean sensitive, boolean enabled) {
controller.setServletClass(servletClass);
controller.setServletName(servletName);
this.path = path;
this.sensitive = sensitive;
this.enabled = enabled;
}
@RequestMapping("**")
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response) throws Exception {
return this.controller.handleRequest(request, response);
}
@Override
public String getPath() {
return path;
}
@Override
public boolean isSensitive() {
return sensitive;
}
@Override
public Class<? extends Endpoint> getEndpointType() {
return null;
}
}

View File

@@ -147,16 +147,16 @@ public class EurekaClientConfigBean implements EurekaClientConfig {
@Override
public List<String> getEurekaServerServiceUrls(String myZone) {
String serviceUrls = serviceUrl .get(myZone);
String serviceUrls = serviceUrl.get(myZone);
if (serviceUrls == null || serviceUrls.isEmpty()) {
serviceUrls = serviceUrl.get("default" + myZone);
serviceUrls = serviceUrl.get("default");
}
if (serviceUrls != null) {
return Arrays.asList(serviceUrls.split(","));
}
return new ArrayList<String>();
return new ArrayList<>();
}
@Override

View File

@@ -46,7 +46,7 @@ public class EurekaInstanceConfigBean implements EurekaInstanceConfig {
@Getter(AccessLevel.PRIVATE) @Setter(AccessLevel.PRIVATE)
private String[] hostInfo = initHostInfo();
@Value("${spring.application.name:unkown}")
@Value("${spring.application.name:unknown}")
private String appname = "unknown";
private String appGroupName;
@@ -66,7 +66,7 @@ public class EurekaInstanceConfigBean implements EurekaInstanceConfig {
private int leaseExpirationDurationInSeconds = 90;
@Value("${spring.application.name:unkown}.mydomain.net")
@Value("${spring.application.name:unknown}.mydomain.net")
private String virtualHostName;
private String secureVirtualHostName;

View File

@@ -0,0 +1,36 @@
package org.springframework.platform.netflix.feign;
import feign.Contract;
import feign.Logger;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.platform.netflix.archaius.ArchaiusAutoConfiguration;
/**
* Created by sgibb on 7/3/14.
*/
@Configuration
@AutoConfigureAfter(ArchaiusAutoConfiguration.class)
public class FeignAutoConfiguration {
@Bean
SpringDecoder feignDecoder() {
return new SpringDecoder();
}
@Bean
SpringEncoder feignEncoder() {
return new SpringEncoder();
}
@Bean
public Logger feignLogger() {
//return new Slf4jLogger(); //TODO pass Client classname in
return new Logger.JavaLogger();
}
@Bean
public Contract feignContract() {
return new SpringMvcContract();
}
}

View File

@@ -0,0 +1,101 @@
package org.springframework.platform.netflix.feign;
import org.springframework.http.HttpHeaders;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.ResourceHttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.feed.AtomFeedHttpMessageConverter;
import org.springframework.http.converter.feed.RssChannelHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter;
import org.springframework.http.converter.xml.SourceHttpMessageConverter;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import javax.xml.transform.Source;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* Created by sgibb on 6/27/14.
*/
public class FeignBase {
public static final boolean romePresent =
ClassUtils.isPresent("com.sun.syndication.feed.WireFeed", FeignBase.class.getClassLoader());
public static final boolean jaxb2Present =
ClassUtils.isPresent("javax.xml.bind.Binder", FeignBase.class.getClassLoader());
public static final boolean jackson2Present =
ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", FeignBase.class.getClassLoader()) &&
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", FeignBase.class.getClassLoader());
public static final boolean jacksonPresent =
ClassUtils.isPresent("org.codehaus.jackson.map.ObjectMapper", FeignBase.class.getClassLoader()) &&
ClassUtils.isPresent("org.codehaus.jackson.JsonGenerator", FeignBase.class.getClassLoader());
protected final List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
public FeignBase() {
addDefaultConverters(messageConverters);
}
/**
* Create a new instance of the {@link SpringDecoder} using the given list of
* {@link HttpMessageConverter} to use
* @param messageConverters the list of {@link HttpMessageConverter} to use
*/
public FeignBase(List<HttpMessageConverter<?>> messageConverters) {
Assert.notEmpty(messageConverters, "'messageConverters' must not be empty");
this.messageConverters.addAll(messageConverters);
}
protected void addDefaultConverters(List<HttpMessageConverter<?>> messageConverters) {
messageConverters.add(new ByteArrayHttpMessageConverter());
messageConverters.add(new StringHttpMessageConverter());
messageConverters.add(new ResourceHttpMessageConverter());
messageConverters.add(new SourceHttpMessageConverter<Source>());
messageConverters.add(new AllEncompassingFormHttpMessageConverter());
if (romePresent) {
messageConverters.add(new AtomFeedHttpMessageConverter());
messageConverters.add(new RssChannelHttpMessageConverter());
}
if (jaxb2Present) {
messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
}
if (jackson2Present) {
messageConverters.add(new MappingJackson2HttpMessageConverter());
}
else if (jacksonPresent) {
messageConverters.add(new org.springframework.http.converter.json.MappingJacksonHttpMessageConverter());
}
}
/**
* Set the message body converters to use.
* <p>These converters are used to convert from and to HTTP requests and responses.
*/
public void setMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
Assert.notEmpty(messageConverters, "'messageConverters' must not be empty");
this.messageConverters.clear();
this.messageConverters.addAll(messageConverters);
}
public List<HttpMessageConverter<?>> getMessageConverters() {
return messageConverters;
}
protected HttpHeaders getHttpHeaders(Map<String, Collection<String>> headers) {
HttpHeaders httpHeaders = new HttpHeaders();
for (Map.Entry<String, Collection<String>> entry : headers.entrySet()) {
httpHeaders.put(entry.getKey(), new ArrayList<>(entry.getValue()));
}
return httpHeaders;
}
}

View File

@@ -0,0 +1,65 @@
package org.springframework.platform.netflix.feign;
import com.netflix.config.ConfigurationManager;
import com.netflix.niws.loadbalancer.DiscoveryEnabledNIWSServerList;
import feign.Contract;
import feign.Feign;
import feign.Logger;
import feign.codec.Decoder;
import feign.codec.Encoder;
import feign.ribbon.LoadBalancingTarget;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.platform.netflix.archaius.ConfigurableEnvironmentConfiguration;
import java.net.URI;
/**
* Created by sgibb on 7/3/14.
*/
@Configuration
public class FeignConfigurer {
@Autowired
ConfigurableEnvironmentConfiguration envConfig; //FIXME: howto enforce this?
@Autowired
Decoder decoder;
@Autowired
Encoder encoder;
@Autowired
Logger logger;
@Autowired
Contract contract;
protected Feign.Builder feign() {
//ConfigurationManager.getConfigInstance().setProperty("exampleBackend.ribbon.listOfServers", "localhost:7080");
//exampleBackend.ribbon.NIWSServerListClassName=my.package.MyServerList
return Feign.builder()
.logger(logger)
.encoder(encoder)
.decoder(decoder)
.contract(contract);
}
protected <T> T loadBalance(Class<T> type, String schemeName) {
return loadBalance(feign(), type, schemeName);
}
protected <T> T loadBalance(Feign.Builder builder, Class<T> type, String schemeName) {
String name = URI.create(schemeName).getHost();
setServiceListClass(name);
return builder.target(LoadBalancingTarget.create(type, schemeName));
}
public static void setServiceListClass(String serviceId) {
setProp(serviceId, "NIWSServerListClassName", DiscoveryEnabledNIWSServerList.class.getName());
setProp(serviceId, "DeploymentContextBasedVipAddresses", serviceId); //FIXME: what should this be?
}
private static void setProp(String serviceId, String suffix, String value) {
ConfigurationManager.getConfigInstance().setProperty(serviceId + ".ribbon." + suffix, value);
}
}

View File

@@ -0,0 +1,87 @@
package org.springframework.platform.netflix.feign;
import feign.FeignException;
import feign.Response;
import feign.codec.DecodeException;
import feign.codec.Decoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.client.HttpMessageConverterExtractor;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.util.List;
/**
* Created by sgibb on 6/26/14.
*/
public class SpringDecoder extends FeignBase implements Decoder {
private static final Logger logger = LoggerFactory.getLogger(SpringDecoder.class);
public SpringDecoder() {
}
public SpringDecoder(List<HttpMessageConverter<?>> messageConverters) {
super(messageConverters);
}
@Override
public Object decode(final Response response, Type type) throws IOException, DecodeException, FeignException {
if (type instanceof Class) {
HttpMessageConverterExtractor<?> extractor =
new HttpMessageConverterExtractor((Class<?>) type, getMessageConverters());
Object data = extractor.extractData(new FeignResponseAdapter(response));
return data;
}
throw new DecodeException("type is not an instance of Class: "+type);
}
private class FeignResponseAdapter implements ClientHttpResponse {
private final Response response;
private FeignResponseAdapter(Response response) {
this.response = response;
}
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.valueOf(response.status());
}
@Override
public int getRawStatusCode() throws IOException {
return response.status();
}
@Override
public String getStatusText() throws IOException {
return response.reason();
}
@Override
public void close() {
try {
response.body().close();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public InputStream getBody() throws IOException {
return response.body().asInputStream();
}
@Override
public HttpHeaders getHeaders() {
return getHttpHeaders(response.headers());
}
}
}

View File

@@ -0,0 +1,101 @@
package org.springframework.platform.netflix.feign;
import com.google.common.base.Charsets;
import feign.RequestTemplate;
import feign.codec.EncodeException;
import feign.codec.Encoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Collection;
import java.util.List;
/**
* Created by sgibb on 6/26/14.
*/
public class SpringEncoder extends FeignBase implements Encoder {
private static final Logger logger = LoggerFactory.getLogger(SpringEncoder.class);
public SpringEncoder() {
}
public SpringEncoder(List<HttpMessageConverter<?>> messageConverters) {
super(messageConverters);
}
@Override
public void encode(Object requestBody, RequestTemplate request) throws EncodeException {
//template.body(conversionService.convert(object, String.class));
if (requestBody != null) {
Class<?> requestType = requestBody.getClass();
Collection<String> contentTypes = request.headers().get("Content-Type");
MediaType requestContentType = null;
if (contentTypes != null && !contentTypes.isEmpty()) {
String type = contentTypes.iterator().next();
requestContentType = MediaType.valueOf(type);
}
for (HttpMessageConverter<?> messageConverter : getMessageConverters()) {
if (messageConverter.canWrite(requestType, requestContentType)) {
if (logger.isDebugEnabled()) {
if (requestContentType != null) {
logger.debug("Writing [" + requestBody + "] as \"" + requestContentType +
"\" using [" + messageConverter + "]");
}
else {
logger.debug("Writing [" + requestBody + "] using [" + messageConverter + "]");
}
}
FeignOutputMessage outputMessage = new FeignOutputMessage(request);
try {
((HttpMessageConverter<Object>) messageConverter).write(
requestBody, requestContentType, outputMessage);
} catch (IOException e) {
throw new EncodeException("Error converting request body", e);
}
request.body(outputMessage.getOutputStream().toByteArray(), Charsets.UTF_8); //TODO: set charset
return;
}
}
String message = "Could not write request: no suitable HttpMessageConverter found for request type [" +
requestType.getName() + "]";
if (requestContentType != null) {
message += " and content type [" + requestContentType + "]";
}
throw new EncodeException(message);
}
}
private class FeignOutputMessage implements HttpOutputMessage {
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
RequestTemplate request;
private FeignOutputMessage(RequestTemplate request) {
this.request = request;
}
@Override
public OutputStream getBody() throws IOException {
return outputStream;
}
@Override
public HttpHeaders getHeaders() {
return getHttpHeaders(request.headers());
}
public ByteArrayOutputStream getOutputStream() {
return outputStream;
}
}
}

View File

@@ -0,0 +1,84 @@
package org.springframework.platform.netflix.feign;
import feign.Contract;
import feign.MethodMetadata;
import org.springframework.web.bind.annotation.RequestMapping;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
import static feign.Util.checkState;
import static feign.Util.emptyToNull;
/**
* Created by sgibb on 6/27/14.
*/
public class SpringMvcContract extends Contract.BaseContract {
static final String ACCEPT = "Accept";
static final String CONTENT_TYPE = "Content-Type";
@Override
protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) {
Class<? extends Annotation> annotationType = methodAnnotation.annotationType();
RequestMapping mapping = RequestMapping.class.cast(methodAnnotation);
if (mapping != null) {
//HTTP Method
checkOne(method, mapping.method(), "method");
data.template().method(mapping.method()[0].name());
//path
checkOne(method, mapping.value(), "value");
String methodAnnotationValue = mapping.value()[0];
String pathValue = emptyToNull(methodAnnotationValue);
checkState(pathValue != null, "value was empty on method %s", method.getName());
if (!methodAnnotationValue.startsWith("/") && !data.template().toString().endsWith("/")) {
methodAnnotationValue = "/" + methodAnnotationValue;
}
data.template().append(methodAnnotationValue);
//produces
checkAtMostOne(method, mapping.produces(), "produces");
String[] serverProduces = mapping.produces();
String clientAccepts = serverProduces.length == 0 ? null: emptyToNull(serverProduces[0]);
if (clientAccepts != null) {
data.template().header(ACCEPT, clientAccepts);
}
//consumes
checkAtMostOne(method, mapping.consumes(), "consumes");
String[] serverConsumes = mapping.consumes();
String clientProduces = serverConsumes.length == 0 ? null: emptyToNull(serverConsumes[0]);
if (clientProduces != null) {
data.template().header(CONTENT_TYPE, clientProduces);
}
//headers
//TODO: only supports one header value per key
if (mapping.headers() != null && mapping.headers().length > 0)
for (String header : mapping.headers()) {
int colon = header.indexOf(':');
data.template().header(header.substring(0, colon), header.substring(colon + 2));
}
}
}
private void checkAtMostOne(Method method, Object[] values, String fieldName) {
checkState(values != null && (values.length == 0 || values.length == 1),
"Method %s can only contain at most 1 %s field. Found: %s", method.getName(), fieldName,
values == null ? null : Arrays.asList(values));
}
private void checkOne(Method method, Object[] values, String fieldName) {
checkState(values != null && values.length == 1,
"Method %s can only contain 1 %s field. Found: %s", method.getName(), fieldName,
values == null ? null : Arrays.asList(values));
}
@Override
protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex) {
//TODO: support spring parameter annotations?
return false;
}
}

View File

@@ -0,0 +1,4 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.platform.netflix.archaius.ArchaiusAutoConfiguration,\
org.springframework.platform.netflix.eureka.EurekaClientAutoConfiguration,\
org.springframework.platform.netflix.feign.FeignAutoConfiguration

View File

@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.platform</groupId>
<artifactId>spring-platform-netflix-zuul</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Spring Platform Netflix Zuul</name>
<url>http://projects.spring.io/spring-platform/</url>
<parent>
<groupId>org.springframework.platform</groupId>
<artifactId>spring-platform-netflix</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<properties>
<spring-platform-config.version>1.0.0.BUILD-SNAPSHOT</spring-platform-config.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.platform</groupId>
<artifactId>spring-platform-config</artifactId>
<version>${spring-platform-config.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.platform</groupId>
<artifactId>spring-platform-netflix-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.platform</groupId>
<artifactId>spring-platform-config-client</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-core</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-core</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-httpclient</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.zuul</groupId>
<artifactId>zuul-core</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,67 @@
package org.springframework.platform.netflix.zuul;
import com.netflix.zuul.context.ContextLifecycleFilter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.embedded.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.platform.netflix.endpoint.HystrixStreamEndpoint;
import org.springframework.scheduling.annotation.EnableScheduling;
import java.util.ArrayList;
import java.util.Collection;
/**
* User: spencergibb
* Date: 4/24/14
* Time: 8:57 PM
*/
@Configuration
@EnableAutoConfiguration
@EnableScheduling
@ComponentScan(basePackageClasses = Application.class)
public class Application {
@Bean
ZuulProperties routerProperties() {
return new ZuulProperties();
}
@Bean
public FilterRegistrationBean contextLifecycleFilter() {
Collection<String> urlPatterns = new ArrayList<>();
urlPatterns.add("/*");
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new ContextLifecycleFilter());
filterRegistrationBean.setUrlPatterns(urlPatterns);
return filterRegistrationBean;
}
//TODO: replace with auto config
@Bean
HystrixStreamEndpoint hystrixStreamEndpoint() {
return new HystrixStreamEndpoint();
}
@Bean
Routes routes() {
return new Routes();
}
@Bean
FilterIntializer filterIntializer() {
return new FilterIntializer();
}
@Bean
ZuulController zuulController() {
return new ZuulController();
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

View File

@@ -0,0 +1,72 @@
package org.springframework.platform.netflix.zuul;
import com.netflix.zuul.FilterFileManager;
import com.netflix.zuul.FilterLoader;
import com.netflix.zuul.groovy.GroovyCompiler;
import com.netflix.zuul.groovy.GroovyFileFilter;
import com.netflix.zuul.monitoring.MonitoringHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
//import javax.servlet.http.HttpSessionEvent;
/**
* User: spencergibb
* Date: 4/24/14
* Time: 9:23 PM
* TODO: .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
*/
public class FilterIntializer implements ServletContextListener/*, HttpSessionListener*/ {
private static final Logger LOGGER = LoggerFactory.getLogger(FilterIntializer.class);
@Autowired
ZuulProperties props;
@Override
/*public void sessionCreated(HttpSessionEvent se) {
contextInitialized(null);
}*/
public void contextInitialized(ServletContextEvent sce) {
LOGGER.info("Starting filter initialzer context listener");
//FIXME: mocks monitoring infrastructure as we don't need it for this simple app
MonitoringHelper.initMocks();
// initializes groovy filesystem poller
initGroovyFilterManager();
}
@Override
/*public void sessionDestroyed(HttpSessionEvent se) {
contextDestroyed(null);
}*/
public void contextDestroyed(ServletContextEvent sce) {
LOGGER.info("Stopping filter initializer context listener");
}
private void initGroovyFilterManager() {
//TODO: pluggable filter initialzer
FilterLoader.getInstance().setCompiler(new GroovyCompiler());
final String scriptRoot = props.getFilterRoot();
LOGGER.info("Using file system script: " + scriptRoot);
try {
FilterFileManager.setFilenameFilter(new GroovyFileFilter());
FilterFileManager.init(5,
scriptRoot + "/pre",
scriptRoot + "/route",
scriptRoot + "/post"
);
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,111 @@
package org.springframework.platform.netflix.zuul;
import com.netflix.client.http.HttpRequest;
import com.netflix.client.http.HttpResponse;
import com.netflix.config.DynamicPropertyFactory;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandProperties;
import com.netflix.niws.client.http.RestClient;
import com.netflix.zuul.constants.ZuulConstants;
import com.netflix.zuul.context.RequestContext;
import javax.ws.rs.core.MultivaluedMap;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import static com.netflix.client.http.HttpRequest.Builder;
import static com.netflix.client.http.HttpRequest.Verb;
/**
* Hystrix wrapper around Eureka Ribbon command
*
* see original https://github.com/Netflix/zuul/blob/master/zuul-netflix/src/main/java/com/netflix/zuul/dependency/ribbon/hystrix/RibbonCommand.java
*/
public class RibbonCommand extends HystrixCommand<HttpResponse> {
RestClient restClient;
Verb verb;
URI uri;
MultivaluedMap<String, String> headers;
MultivaluedMap<String, String> params;
InputStream requestEntity;
public RibbonCommand(RestClient restClient,
Verb verb,
String uri,
MultivaluedMap<String, String> headers,
MultivaluedMap<String, String> params,
InputStream requestEntity) throws URISyntaxException {
this("default", restClient, verb, uri, headers, params, requestEntity);
}
public RibbonCommand(String commandKey,
RestClient restClient,
Verb verb,
String uri,
MultivaluedMap<String, String> headers,
MultivaluedMap<String, String> params,
InputStream requestEntity) throws URISyntaxException {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(commandKey)).andCommandPropertiesDefaults(
// we want to default to semaphore-isolation since this wraps
// 2 others commands that are already thread isolated
HystrixCommandProperties.Setter().withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE)
.withExecutionIsolationSemaphoreMaxConcurrentRequests(DynamicPropertyFactory.getInstance().
getIntProperty(ZuulConstants.ZUUL_EUREKA + commandKey + ".semaphore.maxSemaphores", 100).get())));
this.restClient = restClient;
this.verb = verb;
this.uri = new URI(uri);
this.headers = headers;
this.params = params;
this.requestEntity = requestEntity;
}
@Override
protected HttpResponse run() throws Exception {
try {
return forward();
} catch (Exception e) {
throw e;
}
}
HttpResponse forward() throws Exception {
RequestContext context = RequestContext.getCurrentContext();
Builder builder = HttpRequest.newBuilder().
verb(verb).
uri(uri).
entity(requestEntity);
for (String name : headers.keySet()) {
List<String> values = headers.get(name);
for (String value : values) {
builder.header(name, value);
}
}
for (String name : params.keySet()) {
List<String> values = params.get(name);
for (String value : values) {
builder.queryParams(name, value);
}
}
HttpRequest httpClientRequest = builder.build();
HttpResponse response = restClient.executeWithLoadBalancer(httpClientRequest);
context.set("ribbonResponse", response);
return response;
}
}

View File

@@ -0,0 +1,78 @@
package org.springframework.platform.netflix.zuul;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.bind.PropertySourceUtils;
import org.springframework.core.env.*;
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Field;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
/**
* Created by sgibb on 7/10/14.
*/
public class Routes {
private static final Logger logger = LoggerFactory.getLogger(Routes.class);
public static final String DEFAULT_ROUTE = "/";
@Autowired
ConfigurableEnvironment env;
private final Field propertySourcesField;
public Routes() {
propertySourcesField = ReflectionUtils.findField(CompositePropertySource.class, "propertySources");
propertySourcesField.setAccessible(true);
}
//TODO: cache routes or respond to environment event and refresh all routes
public LinkedHashMap<String, String> getRoutes() {
LinkedHashMap<String, String> routes = new LinkedHashMap<>();
MutablePropertySources propertySources = env.getPropertySources();
for (PropertySource<?> propertySource : propertySources) {
getRoutes(propertySource, routes);
}
String defaultServiceId = routes.get(DEFAULT_ROUTE);
if (defaultServiceId != null) {
//move the defaultServiceId to the end
routes.remove(DEFAULT_ROUTE);
routes.put(DEFAULT_ROUTE, defaultServiceId);
}
return routes;
}
public void getRoutes(PropertySource<?> propertySource, LinkedHashMap<String, String> routes) {
if (propertySource instanceof CompositePropertySource) {
try {
@SuppressWarnings("unchecked")
Set<PropertySource<?>> sources = (Set<PropertySource<?>>) propertySourcesField.get(propertySource);
for (PropertySource<?> source : sources) {
getRoutes(source, routes);
}
} catch (IllegalAccessException e) {
return;
}
} else {
//EnumerablePropertySource enumerable = (EnumerablePropertySource) propertySource;
MutablePropertySources propertySources = new MutablePropertySources();
propertySources.addLast(propertySource);
Map<String, Object> routeEntries = PropertySourceUtils.getSubProperties(propertySources, "zuul.route.");
for (Map.Entry<String, Object> entry : routeEntries.entrySet()) {
String serviceId = entry.getKey();
String route = entry.getValue().toString();
if (routes.containsKey(route)) {
logger.warn("Overwriting route {}: already defined by {}", route, routes.get(route));
}
routes.put(route, serviceId);
}
}
}
}

View File

@@ -0,0 +1,31 @@
package org.springframework.platform.netflix.zuul;
import com.netflix.zuul.ZuulFilter;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.context.support.WebApplicationContextUtils;
import javax.servlet.http.HttpServletRequest;
/**
* User: spencergibb
* Date: 5/1/14
* Time: 10:59 PM
*/
public abstract class SpringFilter extends ZuulFilter {
protected <T> T getBean(Class<T> beanClass) {
//FIXME: hack because zuul uses servlet 2.5?
RequestAttributes requestAttr = RequestContextHolder.currentRequestAttributes();
if (!(requestAttr instanceof ServletRequestAttributes)) {
throw new IllegalStateException("Current request is not a servlet request");
}
ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttr;
HttpServletRequest request = attributes.getRequest();
WebApplicationContext context = WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext());
return context.getBean(beanClass);
}
}

View File

@@ -0,0 +1,28 @@
package org.springframework.platform.netflix.zuul;
import com.netflix.zuul.http.ZuulServlet;
import org.springframework.platform.util.AbstractServletWrappingController;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* User: spencergibb
* Date: 4/24/14
* Time: 9:12 PM
*/
@Controller
public class ZuulController extends AbstractServletWrappingController {
public ZuulController() {
super(ZuulServlet.class, "zuulServlet");
}
@RequestMapping("/**")
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response) throws Exception {
return this.controller.handleRequest(request, response);
}
}

View File

@@ -0,0 +1,32 @@
package org.springframework.platform.netflix.zuul;
import org.springframework.boot.context.properties.ConfigurationProperties;
import static java.util.concurrent.TimeUnit.MINUTES;
/**
* User: spencergibb
* Date: 5/2/14
* Time: 9:20 PM
*/
@ConfigurationProperties("zuul")
public class ZuulProperties {
private String filterRoot;
private long cacheRefresh = MINUTES.toMillis(5L);
public String getFilterRoot() {
return filterRoot;
}
public void setFilterRoot(String filterRoot) {
this.filterRoot = filterRoot;
}
public long getCacheRefresh() {
return cacheRefresh;
}
public void setCacheRefresh(long cacheRefresh) {
this.cacheRefresh = cacheRefresh;
}
}

View File

@@ -0,0 +1,42 @@
package org.springframework.platform.util;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.web.context.ServletContextAware;
import org.springframework.web.servlet.mvc.ServletWrappingController;
import javax.servlet.ServletContext;
/**
* User: spencergibb
* Date: 5/1/14
* Time: 10:03 AM
*/
public abstract class AbstractServletWrappingController implements InitializingBean,
ApplicationContextAware, ServletContextAware {
protected final ServletWrappingController controller = new ServletWrappingController();
public AbstractServletWrappingController(Class<?> servletClass, String servletName) {
controller.setServletClass(servletClass);
controller.setServletName(servletName);
}
@Override
public void afterPropertiesSet() throws Exception {
this.controller.afterPropertiesSet();
}
@Override
public void setServletContext(ServletContext servletContext) {
this.controller.setServletContext(servletContext);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.controller.setApplicationContext(applicationContext);
}
}

View File

@@ -0,0 +1,8 @@
#org.springframework.context.ApplicationListener=\
#org.springframework.platform.netflix.eureka.EurekaStartingListener,\
#org.springframework.platform.netflix.eureka.EurekaUpListener,\
#org.springframework.platform.netflix.eureka.EurekaOutOfServiceListener
#org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
#org.springframework.platform.netflix.archaius.ArchaiusAutoConfiguration,\
#org.springframework.platform.netflix.feign.FeignAutoConfiguration

View File

@@ -0,0 +1,48 @@
info:
component: Platform Router (Zuul)
endpoints:
restart:
enabled: true
shutdown:
enabled: true
server:
port: 6080
management:
port: 6081
logging:
level: INFO
logging:
level:
com:
netflix: WARN
eureka:
client:
#Region where eureka is deployed -For AWS specify one of the AWS regions, for other datacenters specify a arbitrary string
#indicating the region.This is normally specified as a -D option (eg) -Deureka.region=us-east-1
region: default
#For eureka clients running in eureka server, it needs to connect to servers in other zones
preferSameZone: false
#Change this if you want to use a DNS based lookup for determining other eureka servers. For example
#of specifying the DNS entries, check the eureka-client-test.properties, eureka-client-prod.properties
#shouldUseDns: false
us-east-1:
availabilityZones: default
serviceUrl:
default: http://localhost:8080/v2/
defaultZone: http://localhost:8080/v2/
instance:
#Virtual host name by which the clients identifies this service
virtualHostName: ${spring.application.name}

View File

@@ -0,0 +1,6 @@
spring:
application:
name: zuul
platform:
config:
uri: http://localhost:${config.port:8888}

View File

@@ -0,0 +1,170 @@
package filters.post
import com.netflix.config.DynamicBooleanProperty
import com.netflix.config.DynamicIntProperty
import com.netflix.config.DynamicPropertyFactory
import com.netflix.util.Pair
import com.netflix.zuul.ZuulFilter
import com.netflix.zuul.constants.ZuulConstants
import com.netflix.zuul.constants.ZuulHeaders
import com.netflix.zuul.context.Debug
import com.netflix.zuul.context.RequestContext
import javax.servlet.http.HttpServletResponse
import java.util.zip.GZIPInputStream
class SendResponseFilter extends ZuulFilter {
static DynamicBooleanProperty INCLUDE_DEBUG_HEADER =
DynamicPropertyFactory.getInstance().getBooleanProperty(ZuulConstants.ZUUL_INCLUDE_DEBUG_HEADER, false);
static DynamicIntProperty INITIAL_STREAM_BUFFER_SIZE =
DynamicPropertyFactory.getInstance().getIntProperty(ZuulConstants.ZUUL_INITIAL_STREAM_BUFFER_SIZE, 1024);
static DynamicBooleanProperty SET_CONTENT_LENGTH = DynamicPropertyFactory.getInstance().getBooleanProperty(ZuulConstants.ZUUL_SET_CONTENT_LENGTH, false);
@Override
String filterType() {
return 'post'
}
@Override
int filterOrder() {
return 1000
}
boolean shouldFilter() {
return !RequestContext.currentContext.getZuulResponseHeaders().isEmpty() ||
RequestContext.currentContext.getResponseDataStream() != null ||
RequestContext.currentContext.responseBody != null
}
Object run() {
addResponseHeaders()
writeResponse()
}
void writeResponse() {
RequestContext context = RequestContext.currentContext
// there is no body to send
if (context.getResponseBody() == null && context.getResponseDataStream() == null) return;
HttpServletResponse servletResponse = context.getResponse()
servletResponse.setCharacterEncoding("UTF-8")
OutputStream outStream = servletResponse.getOutputStream();
InputStream is = null
try {
if (RequestContext.currentContext.responseBody != null) {
String body = RequestContext.currentContext.responseBody
writeResponse(new ByteArrayInputStream(body.bytes), outStream)
return;
}
boolean isGzipRequested = false
final String requestEncoding = context.getRequest().getHeader(ZuulHeaders.ACCEPT_ENCODING)
if (requestEncoding != null && requestEncoding.equals("gzip"))
isGzipRequested = true;
is = context.getResponseDataStream();
InputStream inputStream = is
if (is != null) {
if (context.sendZuulResponse()) {
// if origin response is gzipped, and client has not requested gzip, decompress stream
// before sending to client
// else, stream gzip directly to client
if (context.getResponseGZipped() && !isGzipRequested)
try {
inputStream = new GZIPInputStream(is);
} catch (java.util.zip.ZipException e) {
println("gzip expected but not received assuming unencoded response" + RequestContext.currentContext.getRequest().getRequestURL().toString())
inputStream = is
}
else if (context.getResponseGZipped() && isGzipRequested)
servletResponse.setHeader(ZuulHeaders.CONTENT_ENCODING, "gzip")
writeResponse(inputStream, outStream)
}
}
} finally {
try {
is?.close();
outStream.flush()
outStream.close()
} catch (IOException e) {
}
}
}
def writeResponse(InputStream zin, OutputStream out) {
byte[] bytes = new byte[INITIAL_STREAM_BUFFER_SIZE.get()];
int bytesRead = -1;
while ((bytesRead = zin.read(bytes)) != -1) {
// if (Debug.debugRequest() && !Debug.debugRequestHeadersOnly()) {
// Debug.addRequestDebug("OUTBOUND: < " + new String(bytes, 0, bytesRead));
// }
try {
out.write(bytes, 0, bytesRead);
out.flush();
} catch (IOException e) {
//ignore
e.printStackTrace()
}
// doubles buffer size if previous read filled it
if (bytesRead == bytes.length) {
bytes = new byte[bytes.length * 2]
}
}
}
private void addResponseHeaders() {
RequestContext context = RequestContext.getCurrentContext();
HttpServletResponse servletResponse = context.getResponse();
List<Pair<String, String>> zuulResponseHeaders = context.getZuulResponseHeaders();
String debugHeader = ""
List<String> rd
rd = (List<String>) RequestContext.getCurrentContext().get("routingDebug");
rd?.each {
debugHeader += "[[[${it}]]]";
}
/*
rd = (List<String>) RequestContext.getCurrentContext().get("requestDebug");
rd?.each {
debugHeader += "[[[REQUEST_DEBUG::${it}]]]";
}
*/
if (INCLUDE_DEBUG_HEADER.get()) servletResponse.addHeader("X-Zuul-Debug-Header", debugHeader)
if (Debug.debugRequest()) {
zuulResponseHeaders?.each { Pair<String, String> it ->
servletResponse.addHeader(it.first(), it.second())
Debug.addRequestDebug("OUTBOUND: < " + it.first() + ":" + it.second())
}
} else {
zuulResponseHeaders?.each { Pair<String, String> it ->
servletResponse.addHeader(it.first(), it.second())
}
}
RequestContext ctx = RequestContext.getCurrentContext()
Integer contentLength = ctx.getOriginContentLength()
// only inserts Content-Length if origin provides it and origin response is not gzipped
if (SET_CONTENT_LENGTH.get()) {
if (contentLength != null && !ctx.getResponseGZipped())
servletResponse.setContentLength(contentLength)
}
}
}

View File

@@ -0,0 +1,43 @@
package filters.post
import com.netflix.zuul.ZuulFilter
import com.netflix.zuul.context.RequestContext
class Stats extends ZuulFilter {
@Override
String filterType() {
return "post"
}
@Override
int filterOrder() {
return 2000
}
@Override
boolean shouldFilter() {
return true
}
@Override
Object run() {
dumpRoutingDebug()
dumpRequestDebug()
}
public void dumpRequestDebug() {
List<String> rd = (List<String>) RequestContext.getCurrentContext().get("requestDebug");
rd?.each {
println("REQUEST_DEBUG::${it}");
}
}
public void dumpRoutingDebug() {
List<String> rd = (List<String>) RequestContext.getCurrentContext().get("routingDebug");
rd?.each {
println("ZUUL_DEBUG::${it}");
}
}
}

View File

@@ -0,0 +1,42 @@
package filters.pre
import com.netflix.config.DynamicBooleanProperty
import com.netflix.config.DynamicPropertyFactory
import com.netflix.config.DynamicStringProperty
import com.netflix.zuul.ZuulFilter
import com.netflix.zuul.constants.ZuulConstants
import com.netflix.zuul.context.RequestContext
class DebugFilter extends ZuulFilter {
static final DynamicBooleanProperty routingDebug = DynamicPropertyFactory.getInstance().getBooleanProperty(ZuulConstants.ZUUL_DEBUG_REQUEST, true)
static final DynamicStringProperty debugParameter = DynamicPropertyFactory.getInstance().getStringProperty(ZuulConstants.ZUUL_DEBUG_PARAMETER, "d")
@Override
String filterType() {
return 'pre'
}
@Override
int filterOrder() {
return 1
}
boolean shouldFilter() {
if ("true".equals(RequestContext.getCurrentContext().getRequest().getParameter(debugParameter.get()))) return true;
return routingDebug.get();
}
Object run() {
RequestContext ctx = RequestContext.getCurrentContext()
ctx.setDebugRouting(true)
ctx.setDebugRequest(true)
return null;
}
}

View File

@@ -0,0 +1,55 @@
package filters.pre
import com.netflix.zuul.ZuulFilter
import com.netflix.zuul.context.Debug
import com.netflix.zuul.context.RequestContext
import javax.servlet.http.HttpServletRequest
class DebugRequest extends ZuulFilter {
@Override
String filterType() {
return 'pre'
}
@Override
int filterOrder() {
return 10000
}
@Override
boolean shouldFilter() {
return Debug.debugRequest()
}
@Override
Object run() {
HttpServletRequest req = RequestContext.currentContext.request as HttpServletRequest
Debug.addRequestDebug("REQUEST:: " + req.getScheme() + " " + req.getRemoteAddr() + ":" + req.getRemotePort())
Debug.addRequestDebug("REQUEST:: > " + req.getMethod() + " " + req.getRequestURI() + " " + req.getProtocol())
Iterator headerIt = req.getHeaderNames().iterator()
while (headerIt.hasNext()) {
String name = (String) headerIt.next()
String value = req.getHeader(name)
Debug.addRequestDebug("REQUEST:: > " + name + ":" + value)
}
final RequestContext ctx = RequestContext.getCurrentContext()
if (!ctx.isChunkedRequestBody()) {
InputStream inp = ctx.request.getInputStream()
String body = null
if (inp != null) {
body = inp.getText()
Debug.addRequestDebug("REQUEST:: > " + body)
}
}
return null;
}
}

View File

@@ -0,0 +1,52 @@
package filters.pre
import com.netflix.zuul.context.RequestContext
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.platform.netflix.zuul.Routes
import org.springframework.platform.netflix.zuul.SpringFilter
class PreDecorationFilter extends SpringFilter {
private static Logger LOG = LoggerFactory.getLogger(PreDecorationFilter.class);
@Override
int filterOrder() {
return 5
}
@Override
String filterType() {
return "pre"
}
@Override
boolean shouldFilter() {
return true;
}
@Override
Object run() {
RequestContext ctx = RequestContext.getCurrentContext()
def requestURI = ctx.getRequest().getRequestURI()
Routes routes = getBean(Routes.class)
def routesMap = routes.getRoutes()
def route = routesMap.keySet().find { path ->
//TODO: use ant matchers?
if (requestURI.startsWith(path)) {
return true
}
return false
}
def serviceId = routesMap.get(route)
if (serviceId != null) {
// set serviceId for use in filters.route.RibbonRequest
ctx.set("serviceId", serviceId)
ctx.setRouteHost(null)
ctx.addOriginResponseHeader("X-Zuul-ServiceId", serviceId);
}
}
}

View File

@@ -0,0 +1,284 @@
package filters.route
import com.netflix.client.ClientException
import com.netflix.client.ClientFactory
import com.netflix.client.IClient
import com.netflix.client.http.HttpRequest
import com.netflix.client.http.HttpResponse
import com.netflix.hystrix.exception.HystrixRuntimeException
import com.netflix.niws.client.http.RestClient
import com.netflix.zuul.ZuulFilter
import com.netflix.zuul.context.Debug
import com.netflix.zuul.context.RequestContext
import com.netflix.zuul.exception.ZuulException
import com.netflix.zuul.util.HTTPRequestUtils
import com.sun.jersey.core.util.MultivaluedMapImpl
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.platform.netflix.zuul.RibbonCommand
import javax.servlet.http.HttpServletRequest
import javax.ws.rs.core.MultivaluedMap
import java.util.zip.GZIPInputStream
import static HttpRequest.Verb
import static org.springframework.platform.netflix.feign.FeignConfigurer.setServiceListClass
class RibbonRequest extends ZuulFilter {
private static final Logger LOG = LoggerFactory.getLogger(RibbonRequest.class);
public static final String CONTENT_ENCODING = "Content-Encoding";
@Override
String filterType() {
return 'route'
}
@Override
int filterOrder() {
return 10
}
boolean shouldFilter() {
def ctx = RequestContext.currentContext
return (ctx.getRouteHost() == null && ctx.get("serviceId") != null && ctx.sendZuulResponse())
}
Object run() {
RequestContext context = RequestContext.currentContext
HttpServletRequest request = context.getRequest();
MultivaluedMap<String, String> headers = buildZuulRequestHeaders(request)
MultivaluedMap<String, String> params = buildZuulRequestQueryParams(request)
Verb verb = getVerb(request);
Object requestEntity = getRequestBody(request)
def serviceId = context.get("serviceId")
//TODO: can this be set be default? or an implementation of an interface?
setServiceListClass(serviceId)
IClient restClient = ClientFactory.getNamedClient(serviceId);
String uri = request.getRequestURI()
if (context.requestURI != null) {
uri = context.requestURI
}
//remove double slashes
uri = uri.replace("//", "/")
HttpResponse response = forward(restClient, verb, uri, headers, params, requestEntity)
setResponse(response)
return response
}
void debug(RestClient restClient, Verb verb, uri, MultivaluedMap<String, String> headers, MultivaluedMap<String, String> params, InputStream requestEntity) {
if (Debug.debugRequest()) {
headers.each {
Debug.addRequestDebug("ZUUL:: > ${it.key} ${it.value[0]}")
}
String query = ""
params.each {
it.value.each { v ->
query += it.key + "=" + v + "&"
}
}
Debug.addRequestDebug("ZUUL:: > ${verb.verb()} ${uri}?${query} HTTP/1.1")
RequestContext ctx = RequestContext.getCurrentContext()
if (!ctx.isChunkedRequestBody()) {
if (requestEntity != null) {
debugRequestEntity(ctx.request.getInputStream())
}
}
}
}
void debugRequestEntity(InputStream inputStream) {
if (!Debug.debugRequestHeadersOnly()) {
String entity = inputStream.getText()
Debug.addRequestDebug("ZUUL:: > ${entity}")
}
}
def HttpResponse forward(RestClient restClient, Verb verb, uri, MultivaluedMap<String, String> headers,
MultivaluedMap<String, String> params, InputStream requestEntity) {
debug(restClient, verb, uri, headers, params, requestEntity)
RibbonCommand command = new RibbonCommand(restClient, verb, uri, headers, params, requestEntity);
try {
HttpResponse response = command.execute();
return response
} catch (HystrixRuntimeException e) {
if (e?.fallbackException?.cause instanceof ClientException) {
ClientException ex = e.fallbackException.cause as ClientException
throw new ZuulException(ex, "Forwarding error", 500, ex.getErrorType().toString())
}
throw new ZuulException(e, "Forwarding error", 500, e.failureType.toString())
}
}
def getRequestBody(HttpServletRequest request) {
Object requestEntity = null;
try {
requestEntity = RequestContext.currentContext.requestEntity
if (requestEntity == null) {
requestEntity = request.getInputStream();
}
} catch (IOException e) {
LOG.error(e);
}
return requestEntity
}
def MultivaluedMap<String, String> buildZuulRequestQueryParams(HttpServletRequest request) {
Map<String, List<String>> map = HTTPRequestUtils.getInstance().getQueryParams()
MultivaluedMap<String, String> params = new MultivaluedMapImpl<String, String>();
if (map == null) return params;
map.entrySet().each {
it.value.each { v ->
params.add(it.key, v)
}
}
return params
}
def MultivaluedMap<String, String> buildZuulRequestHeaders(HttpServletRequest request) {
RequestContext context = RequestContext.currentContext
MultivaluedMap<String, String> headers = new MultivaluedMapImpl<String, String>();
Enumeration headerNames = request.getHeaderNames();
while (headerNames?.hasMoreElements()) {
String name = (String) headerNames.nextElement();
String value = request.getHeader(name);
if (!name.toLowerCase().contains("content-length")) headers.putSingle(name, value);
}
Map zuulRequestHeaders = context.getZuulRequestHeaders();
zuulRequestHeaders.keySet().each {
headers.putSingle((String) it, (String) zuulRequestHeaders[it])
}
headers.putSingle("accept-encoding", "deflate, gzip")
if (headers.containsKey("transfer-encoding"))
headers.remove("transfer-encoding")
return headers
}
Verb getVerb(HttpServletRequest request) {
String sMethod = request.getMethod();
return getVerb(sMethod);
}
Verb getVerb(String sMethod) {
if (sMethod == null) return Verb.GET;
sMethod = sMethod.toLowerCase();
if (sMethod.equals("post")) return Verb.POST;
if (sMethod.equals("put")) return Verb.PUT;
if (sMethod.equals("delete")) return Verb.DELETE;
if (sMethod.equals("options")) return Verb.OPTIONS;
if (sMethod.equals("head")) return Verb.HEAD;
return Verb.GET;
}
void setResponse(HttpResponse resp) {
RequestContext context = RequestContext.getCurrentContext()
context.setResponseStatusCode(resp.getStatus());
if (resp.hasEntity()) {
context.responseDataStream = resp.inputStream;
}
String contentEncoding = resp.getHeaders().get(CONTENT_ENCODING)?.first();
if (contentEncoding != null && HTTPRequestUtils.getInstance().isGzipped(contentEncoding)) {
context.setResponseGZipped(true);
} else {
context.setResponseGZipped(false);
}
if (Debug.debugRequest()) {
resp.getHeaders().keySet().each { key ->
boolean isValidHeader = isValidHeader(key)
Collection<String> list = resp.getHeaders().get(key)
list.each { header ->
context.addOriginResponseHeader(key, header)
if (key.equalsIgnoreCase("content-length"))
context.setOriginContentLength(header);
if (isValidHeader) {
context.addZuulResponseHeader(key, header);
Debug.addRequestDebug("ORIGIN_RESPONSE:: < ${key} ${header}")
}
}
}
if (context.responseDataStream) {
byte[] origBytes = context.getResponseDataStream().bytes
InputStream inStream = new ByteArrayInputStream(origBytes);
if (context.getResponseGZipped())
inStream = new GZIPInputStream(inStream);
String responseEntity = inStream.getText()
Debug.addRequestDebug("ORIGIN_RESPONSE:: < ${responseEntity}")
context.setResponseDataStream(new ByteArrayInputStream(origBytes))
}
} else {
resp.getHeaders().keySet().each { key ->
boolean isValidHeader = isValidHeader(key)
Collection<java.lang.String> list = resp.getHeaders().get(key)
list.each { header ->
context.addOriginResponseHeader(key, header)
if (key.equalsIgnoreCase("content-length"))
context.setOriginContentLength(header);
if (isValidHeader) {
context.addZuulResponseHeader(key, header);
}
}
}
}
}
boolean isValidHeader(String headerName) {
switch (headerName.toLowerCase()) {
case "connection":
case "content-length":
case "content-encoding":
case "server":
case "transfer-encoding":
return false
default:
return true
}
}
}

View File

@@ -0,0 +1,434 @@
package filters.route
import com.netflix.config.DynamicIntProperty
import com.netflix.config.DynamicPropertyFactory
import com.netflix.zuul.ZuulFilter
import com.netflix.zuul.constants.ZuulConstants
import com.netflix.zuul.context.Debug
import com.netflix.zuul.context.RequestContext
import com.netflix.zuul.util.HTTPRequestUtils
import org.apache.http.Header
import org.apache.http.HttpHost
import org.apache.http.HttpRequest
import org.apache.http.HttpResponse
import org.apache.http.client.HttpClient
import org.apache.http.client.methods.HttpPost
import org.apache.http.client.methods.HttpPut
import org.apache.http.client.params.ClientPNames
import org.apache.http.conn.ClientConnectionManager
import org.apache.http.conn.scheme.PlainSocketFactory
import org.apache.http.conn.scheme.Scheme
import org.apache.http.conn.scheme.SchemeRegistry
import org.apache.http.conn.ssl.SSLSocketFactory
import org.apache.http.entity.InputStreamEntity
import org.apache.http.impl.client.DefaultHttpClient
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager
import org.apache.http.message.BasicHeader
import org.apache.http.message.BasicHttpRequest
import org.apache.http.params.CoreConnectionPNames
import org.apache.http.params.HttpParams
import org.apache.http.protocol.HttpContext
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
import javax.servlet.http.HttpServletRequest
import java.security.*
import java.security.cert.CertificateException
import java.security.cert.X509Certificate
import java.util.concurrent.atomic.AtomicReference
import java.util.zip.GZIPInputStream
class SimpleHostRoutingFilter extends ZuulFilter {
public static final String CONTENT_ENCODING = "Content-Encoding";
private static final Logger LOG = LoggerFactory.getLogger(SimpleHostRoutingFilter.class);
private static final Runnable CLIENTLOADER = new Runnable() {
@Override
void run() {
loadClient();
}
}
private static final DynamicIntProperty SOCKET_TIMEOUT = DynamicPropertyFactory.getInstance().getIntProperty(ZuulConstants.ZUUL_HOST_SOCKET_TIMEOUT_MILLIS, 10000)
private static final DynamicIntProperty CONNECTION_TIMEOUT = DynamicPropertyFactory.getInstance().getIntProperty(ZuulConstants.ZUUL_HOST_CONNECT_TIMEOUT_MILLIS, 2000)
private static final AtomicReference<HttpClient> CLIENT = new AtomicReference<HttpClient>(newClient());
private static final Timer CONNECTION_MANAGER_TIMER = new Timer(true);
// cleans expired connections at an interval
static {
SOCKET_TIMEOUT.addCallback(CLIENTLOADER)
CONNECTION_TIMEOUT.addCallback(CLIENTLOADER)
CONNECTION_MANAGER_TIMER.schedule(new TimerTask() {
@Override
void run() {
try {
final HttpClient hc = CLIENT.get();
if (hc == null) return;
hc.getConnectionManager().closeExpiredConnections();
} catch (Throwable t) {
LOG.error("error closing expired connections", t);
}
}
}, 30000, 5000)
}
public SimpleHostRoutingFilter() {}
private static final ClientConnectionManager newConnectionManager() {
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load(null, null);
SSLSocketFactory sf = new MySSLSocketFactory(trustStore);
sf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
SchemeRegistry registry = new SchemeRegistry();
registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
registry.register(new Scheme("https", sf, 443));
registry.register(new Scheme("https", sf, 8443));
ClientConnectionManager cm = new ThreadSafeClientConnManager(registry);
cm.setMaxTotal(Integer.parseInt(System.getProperty("zuul.max.host.connections", "200")));
cm.setDefaultMaxPerRoute(Integer.parseInt(System.getProperty("zuul.max.host.connections", "20")));
return cm;
}
@Override
String filterType() {
return 'route'
}
@Override
int filterOrder() {
return 100
}
boolean shouldFilter() {
return RequestContext.currentContext.getRouteHost() != null && RequestContext.currentContext.sendZuulResponse()
}
private static final void loadClient() {
final HttpClient oldClient = CLIENT.get();
CLIENT.set(newClient())
if (oldClient != null) {
CONNECTION_MANAGER_TIMER.schedule(new TimerTask() {
@Override
void run() {
try {
oldClient.getConnectionManager().shutdown();
} catch (Throwable t) {
LOG.error("error shutting down old connection manager", t);
}
}
}, 30000);
}
}
private static final HttpClient newClient() {
// I could statically cache the connection manager but we will probably want to make some of its properties
// dynamic in the near future also
HttpClient httpclient = new DefaultHttpClient(newConnectionManager());
HttpParams httpParams = httpclient.getParams();
httpParams.setIntParameter(CoreConnectionPNames.SO_TIMEOUT, SOCKET_TIMEOUT.get())
httpParams.setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, CONNECTION_TIMEOUT.get())
httpclient.setHttpRequestRetryHandler(new DefaultHttpRequestRetryHandler(0, false))
httpParams.setParameter(ClientPNames.COOKIE_POLICY, org.apache.http.client.params.CookiePolicy.IGNORE_COOKIES);
httpclient.setRedirectStrategy(new org.apache.http.client.RedirectStrategy() {
@Override
boolean isRedirected(HttpRequest httpRequest, HttpResponse httpResponse, HttpContext httpContext) {
return false
}
@Override
org.apache.http.client.methods.HttpUriRequest getRedirect(HttpRequest httpRequest, HttpResponse httpResponse, HttpContext httpContext) {
return null
}
})
return httpclient
}
Object run() {
HttpServletRequest request = RequestContext.currentContext.getRequest();
Header[] headers = buildZuulRequestHeaders(request)
String verb = getVerb(request);
InputStream requestEntity = getRequestBody(request)
HttpClient httpclient = CLIENT.get()
String uri = request.getRequestURI()
if (RequestContext.currentContext.requestURI != null) {
uri = RequestContext.currentContext.requestURI
}
try {
HttpResponse response = forward(httpclient, verb, uri, request, headers, requestEntity)
setResponse(response)
}
catch (Exception e) {
if (Debug.debugRequest()) {
Debug.addRequestDebug("ZUUL:: ERROR " + e.getMessage())
}
throw e;
}
return null
}
def InputStream debug(HttpClient httpclient, String verb, String uri, HttpServletRequest request, Header[] headers, InputStream requestEntity) {
if (Debug.debugRequest()) {
Debug.addRequestDebug("ZUUL:: host=${RequestContext.currentContext.getRouteHost()}")
headers.each {
Debug.addRequestDebug("ZUUL::> ${it.name} ${it.value}")
}
String query = request.queryString
Debug.addRequestDebug("ZUUL:: > ${verb} ${uri}?${query} HTTP/1.1")
if (requestEntity != null) {
requestEntity = debugRequestEntity(requestEntity)
}
}
return requestEntity
}
InputStream debugRequestEntity(InputStream inputStream) {
if (Debug.debugRequestHeadersOnly()) return inputStream
if (inputStream == null) return null
String entity = inputStream.getText()
Debug.addRequestDebug("ZUUL::> ${entity}")
return new ByteArrayInputStream(entity.bytes)
}
def HttpResponse forward(HttpClient httpclient, String verb, String uri, HttpServletRequest request, Header[] headers, InputStream requestEntity) {
requestEntity = debug(httpclient, verb, uri, request, headers, requestEntity)
org.apache.http.HttpHost httpHost
httpHost = getHttpHost()
org.apache.http.HttpRequest httpRequest;
switch (verb) {
case 'POST':
httpRequest = new HttpPost(uri + getQueryString())
InputStreamEntity entity = new InputStreamEntity(requestEntity, request.getContentLength())
httpRequest.setEntity(entity)
break
case 'PUT':
httpRequest = new HttpPut(uri + getQueryString())
InputStreamEntity entity = new InputStreamEntity(requestEntity, request.getContentLength())
httpRequest.setEntity(entity)
break;
default:
httpRequest = new BasicHttpRequest(verb, uri + getQueryString())
LOG.debug(uri + getQueryString())
}
try {
httpRequest.setHeaders(headers)
LOG.debug(httpHost.getHostName() + " " + httpHost.getPort() + " " + httpHost.getSchemeName())
HttpResponse zuulResponse = forwardRequest(httpclient, httpHost, httpRequest)
return zuulResponse
} finally {
// When HttpClient instance is no longer needed,
// shut down the connection manager to ensure
// immediate deallocation of all system resources
// httpclient.getConnectionManager().shutdown();
}
}
HttpResponse forwardRequest(HttpClient httpclient, HttpHost httpHost, HttpRequest httpRequest) {
return httpclient.execute(httpHost, httpRequest);
}
String getQueryString() {
HttpServletRequest request = RequestContext.currentContext.getRequest();
String query = request.getQueryString()
return (query != null) ? "?${query}" : "";
}
HttpHost getHttpHost() {
HttpHost httpHost
URL host = RequestContext.currentContext.getRouteHost()
httpHost = new HttpHost(host.getHost(), host.getPort(), host.getProtocol())
return httpHost
}
def getRequestBody(HttpServletRequest request) {
Object requestEntity = null;
try {
requestEntity = request.getInputStream();
} catch (IOException e) {
//no requestBody is ok.
}
return requestEntity
}
boolean isValidHeader(String name) {
if (name.toLowerCase().contains("content-length")) return false;
if (!RequestContext.currentContext.responseGZipped) {
if (name.toLowerCase().contains("accept-encoding")) return false;
}
return true;
}
def Header[] buildZuulRequestHeaders(HttpServletRequest request) {
def headers = new ArrayList()
Enumeration headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String name = (String) headerNames.nextElement();
String value = request.getHeader(name);
if (isValidHeader(name)) headers.add(new BasicHeader(name, value))
}
Map zuulRequestHeaders = RequestContext.getCurrentContext().getZuulRequestHeaders();
zuulRequestHeaders.keySet().each {
String name = it.toLowerCase()
BasicHeader h = headers.find { BasicHeader he -> he.name == name }
if (h != null) {
headers.remove(h)
}
headers.add(new BasicHeader((String) it, (String) zuulRequestHeaders[it]))
}
if (RequestContext.currentContext.responseGZipped) {
headers.add(new BasicHeader("accept-encoding", "deflate, gzip"))
}
return headers
}
String getVerb(HttpServletRequest request) {
String sMethod = request.getMethod();
return sMethod.toUpperCase();
}
String getVerb(String sMethod) {
if (sMethod == null) return "GET";
sMethod = sMethod.toLowerCase();
if (sMethod.equalsIgnoreCase("post")) return "POST"
if (sMethod.equalsIgnoreCase("put")) return "PUT"
if (sMethod.equalsIgnoreCase("delete")) return "DELETE"
if (sMethod.equalsIgnoreCase("options")) return "OPTIONS"
if (sMethod.equalsIgnoreCase("head")) return "HEAD"
return "GET"
}
void setResponse(HttpResponse response) {
RequestContext context = RequestContext.getCurrentContext()
RequestContext.currentContext.set("hostZuulResponse", response)
RequestContext.getCurrentContext().setResponseStatusCode(response.getStatusLine().statusCode)
RequestContext.getCurrentContext().responseDataStream = response?.entity?.content
boolean isOriginResponseGzipped = false
for (Header h : response.getHeaders(CONTENT_ENCODING)) {
if (HTTPRequestUtils.getInstance().isGzipped(h.value)) {
isOriginResponseGzipped = true;
break;
}
}
context.setResponseGZipped(isOriginResponseGzipped);
if (Debug.debugRequest()) {
response.getAllHeaders()?.each { Header header ->
if (isValidHeader(header)) {
RequestContext.getCurrentContext().addZuulResponseHeader(header.name, header.value);
Debug.addRequestDebug("ORIGIN_RESPONSE:: < ${header.name}, ${header.value}")
}
}
if (context.responseDataStream) {
byte[] origBytes = context.getResponseDataStream().bytes
ByteArrayInputStream byteStream = new ByteArrayInputStream(origBytes)
InputStream inputStream = byteStream
if (RequestContext.currentContext.responseGZipped) {
inputStream = new GZIPInputStream(byteStream);
}
context.setResponseDataStream(new ByteArrayInputStream(origBytes))
}
} else {
response.getAllHeaders()?.each { Header header ->
RequestContext ctx = RequestContext.getCurrentContext()
ctx.addOriginResponseHeader(header.name, header.value)
if (header.name.equalsIgnoreCase("content-length"))
ctx.setOriginContentLength(header.value)
if (isValidHeader(header)) {
ctx.addZuulResponseHeader(header.name, header.value);
}
}
}
}
boolean isValidHeader(Header header) {
switch (header.name.toLowerCase()) {
case "connection":
case "content-length":
case "content-encoding":
case "server":
case "transfer-encoding":
return false
default:
return true
}
}
public static class MySSLSocketFactory extends SSLSocketFactory {
SSLContext sslContext = SSLContext.getInstance("TLS");
public MySSLSocketFactory(KeyStore truststore) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
super(truststore);
TrustManager tm = new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
public X509Certificate[] getAcceptedIssuers() {
return null;
}
};
TrustManager[] tms = new TrustManager[1];
tms[0] = tm;
sslContext.init(null, tms, null);
}
@Override
public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException, UnknownHostException {
return sslContext.getSocketFactory().createSocket(socket, host, port, autoClose);
}
@Override
public Socket createSocket() throws IOException {
return sslContext.getSocketFactory().createSocket();
}
}
}

View File

@@ -1,2 +0,0 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.platform.netflix.eureka.EurekaClientAutoConfiguration