Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support instrumenting non-final concrete types (#62)
Browse files Browse the repository at this point in the history
* Support instrumenting non-final concrete types

Add support for instrumenting non-final concrete types utilizing cglib
and objenesis.

* Publish tritium-lib with shaded dependencies

Shadow tritium-lib internal dependencies such as cglib, objenesis,
HdrHistogram.
schlosna authored and GitHub Enterprise committed Nov 22, 2016
1 parent 6cbb671 commit f2a87d6
Showing 6 changed files with 170 additions and 41 deletions.
5 changes: 3 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -6,11 +6,12 @@ buildscript {
}

dependencies {
classpath "com.palantir:gradle-baseline-java:${baselineVersion}"
classpath "com.palantir:jacoco-coverage:${jacocoCoverageVersion}"
classpath "com.github.jengelman.gradle.plugins:shadow:${shadowVersion}"
classpath "com.palantir.exartisi:gradle-exartisi-plugin:${exartisiVersion}"
classpath "com.palantir.gradle.gitversion:gradle-git-version:${gitVersionVersion}"
classpath "com.palantir.internalpublish:gradle-internal-publish-plugin:${internalPublishVersion}"
classpath "com.palantir:gradle-baseline-java:${baselineVersion}"
classpath "com.palantir:jacoco-coverage:${jacocoCoverageVersion}"
classpath "gradle.plugin.com.palantir.configurationresolver:gradle-configuration-resolver-plugin:${configurationResolverVersion}"
classpath "gradle.plugin.org.inferred:gradle-processors:${processorsVersion}"
}
5 changes: 5 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -3,13 +3,15 @@ javaVersion = 1.8
## Compile
braveVersion = 3.11.1
caffeineVersion = 2.0.3
cglibVersion = 3.2.4
dropwizardMetricsVersion = 3.1.2
findbugsVersion = 3.0.0
guavaVersion = 18.0
hdrHistogramMetricsReservoirVersion = 1.1.2
hdrHistogramVersion = 2.1.9
httpRemotingVersion = 1.0.0
immutablesVersion = 2.1.10
objenesisVersion = 2.4
slf4jVersion = 1.7.12

## Testing
@@ -25,12 +27,15 @@ logbackClassicVersion = 1.1.2

# Build System
antipatternsVersion = 1.0-beta.5
aptGradleVersion = 0.9
baselineVersion = 0.7.1
configurationResolverVersion = 0.1.0
exartisiVersion = 1.2.2
gitVersionVersion = 0.5.2
internalPublishVersion = 0.1.7
jacocoCoverageVersion = 0.4.0
javaDistributionVersion = 0.7.2
jmhGradleVersion = 0.3.1
launchConfigVersion = 0.3.3
processorsVersion = 1.2.1
shadowVersion = 1.2.4
14 changes: 7 additions & 7 deletions tritium-jmh/build.gradle
Original file line number Diff line number Diff line change
@@ -7,17 +7,17 @@ buildscript {
url 'https://plugins.gradle.org/m2/'
}
}
dependencies {
classpath "me.champeau.gradle:jmh-gradle-plugin:${jmhGradleVersion}"
classpath "net.ltgt.gradle:gradle-apt-plugin:${aptGradleVersion}"
}
}

plugins {
id 'java'
id 'com.github.johnrengelman.shadow' version '1.2.3'
id 'me.champeau.gradle.jmh' version '0.3.1'
id 'net.ltgt.apt' version '0.9'
}

apply plugin: 'java'
apply plugin: 'com.github.johnrengelman.shadow'
apply plugin: 'com.palantir.git-version'
apply plugin: 'me.champeau.gradle.jmh'
apply plugin: 'net.ltgt.apt'
apply from: "${rootDir}/gradle/java.gradle"

sourceCompatibility = '1.8'
25 changes: 25 additions & 0 deletions tritium-lib/build.gradle
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
apply plugin: 'com.palantir.publish-jar'
apply plugin: 'com.github.johnrengelman.shadow'

dependencies {
compile project(":tritium-api")
compile project(":tritium-core")
compile project(":tritium-metrics")
compile project(":tritium-slf4j")

compile "cglib:cglib-nodep:${cglibVersion}"
compile "com.google.code.findbugs:annotations:${findbugsVersion}"
compile "com.google.guava:guava:${guavaVersion}"
compile "org.objenesis:objenesis:${objenesisVersion}"
compile "org.slf4j:slf4j-api:${slf4jVersion}"

processor "org.immutables:value:${immutablesVersion}"
@@ -22,3 +26,24 @@ dependencies {
testCompile "org.mockito:mockito-all:${mockitoVersion}"
testCompile "org.slf4j:slf4j-simple:${slf4jVersion}"
}

shadowJar {
mergeServiceFiles()

dependencies {
// Exclude Guava, dropwizard metrics, and slf4j
exclude 'com/google/common/**'
exclude 'com/codahale/metrics/**'
exclude 'org/slf4j/**'
}

relocate('net.sf.cglib', 'com.palantir.tritium.shaded.net.sf.cglib')
relocate('org.HdrHistogram', 'com.palantir.tritium.shaded.org.HdrHistogram')
relocate('org.mpierce', 'com.palantir.tritium.shaded.org.mpierce')
relocate('org.objenesis', 'com.palantir.tritium.shaded.org.objenesis')

}

publishing.publications.nebula {
artifact(shadowJar)
}
Original file line number Diff line number Diff line change
@@ -27,8 +27,13 @@
import com.palantir.tritium.event.log.LoggingLevel;
import com.palantir.tritium.event.metrics.MetricsInvocationEventHandler;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.List;
import net.sf.cglib.proxy.Callback;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.InvocationHandler;
import org.objenesis.ObjenesisHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@@ -41,18 +46,27 @@ private Instrumentation() {
throw new UnsupportedOperationException();
}

public static <T, U extends T> T wrap(
final Class<T> interfaceClass,
static <T, U extends T> T wrap(
final Class<T> type,
final U delegate,
final List<InvocationEventHandler<InvocationContext>> handlers) {
checkNotNull(interfaceClass);
checkNotNull(delegate);
checkNotNull(handlers);
checkNotNull(type, "type");
checkNotNull(delegate, "delegate");
checkNotNull(handlers, "handlers");

if (handlers.isEmpty()) {
return delegate;
}

if (type.isInterface()) {
return createInterfaceProxy(type, delegate, handlers);
} else {
return createConcreteProxy(type, delegate, handlers);
}
}

private static <T, U extends T> T createInterfaceProxy(Class<T> interfaceClass, final U delegate,
final List<InvocationEventHandler<InvocationContext>> handlers) {
return Proxies.newProxy(interfaceClass, delegate,
new InvocationEventProxy<InvocationContext>(handlers) {
@Override
@@ -62,6 +76,21 @@ U getDelegate() {
});
}

private static <T, U extends T> T createConcreteProxy(Class<T> interfaceClass,
final U delegate,
final List<InvocationEventHandler<InvocationContext>> handlers) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(interfaceClass);
enhancer.setCallbackType(InvocationEventHandlerAdapter.class);
// use objenesis so that we can proxy classes which do not have a null constructor
@SuppressWarnings("unchecked")
Class<T> newClass = (Class<T>) enhancer.createClass();
Enhancer.registerCallbacks(newClass, new Callback[] {
InvocationEventHandlerAdapter.create(delegate, handlers)
});
return ObjenesisHelper.newInstance(newClass);
}

/**
* Return an instrumented proxy of the specified service interface and delegate that records aggregated invocation
* metrics and performance trace logging.
@@ -75,9 +104,9 @@ U getDelegate() {
@Deprecated
public static <T, U extends T> T instrument(Class<T> serviceInterface, U delegate, MetricRegistry metricRegistry) {
return builder(serviceInterface, delegate)
.withMetrics(metricRegistry)
.withPerformanceTraceLogging()
.build();
.withMetrics(metricRegistry)
.withPerformanceTraceLogging()
.build();
}

public static <T> Logger getPerformanceLoggerForInterface(Class<T> serviceInterface) {
@@ -136,4 +165,27 @@ public T build() {
}
}

private static class InvocationEventHandlerAdapter implements InvocationHandler {
private InvocationEventProxy<InvocationContext> proxy;

InvocationEventHandlerAdapter(InvocationEventProxy<InvocationContext> proxy) {
this.proxy = proxy;
}

static <T> InvocationEventHandlerAdapter create(final T delegate,
List<InvocationEventHandler<InvocationContext>> handlers) {
return new InvocationEventHandlerAdapter(
new InvocationEventProxy<InvocationContext>(handlers) {
@Override
T getDelegate() {
return delegate;
}
});
}

@Override
public Object invoke(Object instance, Method method, Object[] args) throws Throwable {
return proxy.invoke(instance, method, args);
}
}
}
Original file line number Diff line number Diff line change
@@ -16,15 +16,16 @@

package com.palantir.tritium.proxy;

import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

import com.codahale.metrics.ConsoleReporter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Slf4jReporter;
import com.codahale.metrics.Slf4jReporter.LoggingLevel;
@@ -41,28 +42,41 @@
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.util.Collections;
import java.util.Set;
import java.util.SortedMap;
import org.hamcrest.CoreMatchers;
import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.slf4j.Logger;

@RunWith(MockitoJUnitRunner.class)
public class InstrumentationTest {

private static final String EXPECTED_METRIC_NAME = TestInterface.class.getName() + ".test";

// Exceed the HotSpot JIT thresholds
private static final int INVOCATION_ITERATIONS = 1500000;

@Mock
private InvocationEventHandler<InvocationContext> mockHandler;

private MetricRegistry metrics = MetricRegistries.createWithHdrHistogramReservoirs();

@After
public void after() throws Exception {
ConsoleReporter reporter = ConsoleReporter.forRegistry(metrics).build();
reporter.report();
reporter.close();
}

@Test
public void testEmptyHandlers() {
TestInterface delegate = new TestImplementation();
TestInterface instrumented = Instrumentation.wrap(TestInterface.class, delegate,
Collections.<InvocationEventHandler<InvocationContext>>emptyList());
assertThat(instrumented, is(equalTo(delegate)));
assertThat(Proxy.isProxyClass(instrumented.getClass()), equalTo(false));
assertThat(instrumented).isEqualTo(delegate);
assertThat(Proxy.isProxyClass(instrumented.getClass())).isFalse();
}

@Test
@@ -76,23 +90,22 @@ public void testBuilder() {
.withPerformanceTraceLogging()
.build();

assertThat(delegate.invocationCount(), equalTo(0));
assertThat(metricRegistry.getTimers().get(Runnable.class.getName()), nullValue());
assertThat(delegate.invocationCount()).isEqualTo(0);
assertThat(metricRegistry.getTimers().get(Runnable.class.getName())).isNull();

instrumentedService.test();
assertThat(delegate.invocationCount(), equalTo(1));
assertThat(delegate.invocationCount()).isEqualTo(1);

SortedMap<String, Timer> timers = metricRegistry.getTimers();
assertThat(timers.keySet(), hasSize(1));
assertThat(timers.keySet(), Matchers.<Set<String>>equalTo(ImmutableSet.of(EXPECTED_METRIC_NAME)));
assertThat(timers.get(EXPECTED_METRIC_NAME), notNullValue());
assertThat(timers.get(EXPECTED_METRIC_NAME).getCount(), equalTo(1L));
assertThat(timers.keySet()).hasSize(1);
assertThat(timers.keySet()).isEqualTo(ImmutableSet.of(EXPECTED_METRIC_NAME));
assertThat(timers.get(EXPECTED_METRIC_NAME)).isNotNull();
assertThat(timers.get(EXPECTED_METRIC_NAME).getCount()).isEqualTo(1);

executeManyTimes(instrumentedService, INVOCATION_ITERATIONS);
Slf4jReporter.forRegistry(metricRegistry).withLoggingLevel(LoggingLevel.INFO).build().report();

assertThat(Long.valueOf(timers.get(EXPECTED_METRIC_NAME).getCount()).intValue(),
equalTo(delegate.invocationCount()));
assertThat(timers.get(EXPECTED_METRIC_NAME).getCount()).isEqualTo(delegate.invocationCount());
assertTrue(timers.get(EXPECTED_METRIC_NAME).getSnapshot().getMax() >= 0L);

Slf4jReporter.forRegistry(metricRegistry).withLoggingLevel(LoggingLevel.INFO).build().report();
@@ -137,8 +150,8 @@ public void testCheckedExceptions() {
instrumentedService.throwsCheckedException();
fail("Expected exception");
} catch (Exception expected) {
assertThat(expected, CoreMatchers.instanceOf(TestImplementation.TestException.class));
assertThat(expected.getCause(), equalTo(null));
assertThat(expected).isInstanceOf(TestImplementation.TestException.class);
assertThat(expected.getCause()).isNull();
}
}
}
@@ -158,13 +171,46 @@ public void testThrowables() {
instrumentedService.throwsThrowable();
fail("Expected throwable");
} catch (Throwable throwable) {
assertThat(throwable, CoreMatchers.instanceOf(AssertionError.class));
assertThat(throwable.getCause(), equalTo(null));
assertThat(throwable).isInstanceOf(AssertionError.class);
assertThat(throwable.getCause()).isNull();
}
}
}
}

@Test
public void testWrapConcreteType() throws Exception {
when(mockHandler.isEnabled()).thenReturn(true);
Number instrumentedFourtyTwo = Instrumentation.builder(Number.class, Integer.valueOf(42))
.withMetrics(metrics)
.withHandler(mockHandler)
.build();

assertThat(instrumentedFourtyTwo.intValue()).isEqualTo(42);
assertThat(metrics.timer(MetricRegistry.name(Number.class, "intValue")).getCount()).isEqualTo(1);

assertThat(instrumentedFourtyTwo.longValue()).isEqualTo(42);
assertThat(metrics.timer(MetricRegistry.name(Number.class, "longValue")).getCount()).isEqualTo(1);

verify(mockHandler, times(2)).onSuccess(any(InvocationContext.class), any(Object.class));
}

@Test
public void testCannotWrapFinalConcreteType() throws Exception {
try {
Instrumentation.builder(Integer.class, Integer.valueOf(42))
.withMetrics(metrics)
.withHandler(mockHandler)
.build();
fail("Integer is a final type and cannot be instrumented");
} catch (IllegalArgumentException expected) {
assertThat(expected.getMessage()).isEqualTo(
"Cannot subclass final class java.lang.Integer");
}

verifyNoMoreInteractions(mockHandler);
}

@Test(expected = NullPointerException.class)
public void testNullInterface() {
//noinspection ConstantConditions

0 comments on commit f2a87d6

Please sign in to comment.