Skip to content

Commit

Permalink
#26 Allow PostConstructHandler to edit object to be stored
Browse files Browse the repository at this point in the history
- Change PostConstructHandler signature to return another object when desired (otherwise null)
- Create test handler implementation that wraps an object in a proxy sometimes (Javassist proxy impl. example)
- Fix: if class gets remapped in PreConstructHandler, save both the requested type (by the user) and the remapped type
  • Loading branch information
ljacqu committed Aug 13, 2016
1 parent e359320 commit 8a5668a
Show file tree
Hide file tree
Showing 12 changed files with 222 additions and 23 deletions.
29 changes: 13 additions & 16 deletions src/main/java/ch/jalu/injector/InjectorImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ public <T> T getSingleton(Class<T> clazz) {

@Override
public <T> void register(Class<? super T> clazz, T object) {
register0(clazz, object);
}

private void register0(Class<?> clazz, Object object) {
if (objects.containsKey(clazz)) {
throw new InjectorException("There is already an object present for " + clazz);
}
Expand Down Expand Up @@ -92,6 +96,10 @@ public <T> Collection<T> retrieveAllOfType(Class<T> clazz) {
return instances;
}

public InjectorConfig getConfig() {
return config;
}

/**
* Returns an instance of the given class by retrieving it or by instantiating it if not yet present.
*
Expand Down Expand Up @@ -123,7 +131,10 @@ private <T> T get(Class<T> clazz, Set<Class<?>> traversedClasses) {
traversedClasses = new HashSet<>(traversedClasses);
traversedClasses.add(mappedClass);
T object = instantiate(mappedClass, traversedClasses);
storeObject(object);
register(clazz, object);
if (mappedClass != clazz) {
register0(mappedClass, object);
}
return object;
}

Expand All @@ -144,7 +155,7 @@ private <T> T instantiate(Class<T> clazz, Set<Class<?>> traversedClasses) {
T object = instantiation.instantiateWith(dependencies);
for (PostConstructHandler postConstructHandler : config.getPostConstructHandlers()) {
try {
postConstructHandler.process(object);
object = firstNotNull(postConstructHandler.process(object), object);
} catch (Exception e) {
InjectorUtils.rethrowException(e);
}
Expand Down Expand Up @@ -207,20 +218,6 @@ private Object resolveByAnnotation(DependencyDescription dependencyDescription)
return null;
}

/**
* Stores the given object with its class as key. Throws an exception if the key already has
* a value associated to it.
*
* @param object the object to store
*/
private void storeObject(Object object) {
if (objects.containsKey(object.getClass())) {
throw new IllegalStateException("There is already an object present for " + object.getClass());
}
InjectorUtils.checkNotNull(object, null); // should never happen
objects.put(object.getClass(), object);
}

/**
* Validates that none of the dependencies' types are present in the given collection
* of traversed classes. This prevents circular dependencies.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import ch.jalu.injector.handlers.Handler;

import javax.annotation.Nullable;

/**
* Handler for objects after their construction.
*/
Expand All @@ -11,8 +13,11 @@ public interface PostConstructHandler extends Handler {
* Processes the newly created object.
*
* @param object the object that was instantiated
* @param <T> the object's type
* @return the new object to replace the instance with, null to keep the object the same
* @throws Exception for validation errors or similar
*/
void process(Object object) throws Exception;
@Nullable
<T> T process(T object) throws Exception;

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
public class PostConstructMethodInvoker implements PostConstructHandler {

@Override
public void process(Object object) {
public <T> T process(T object) {
Class<?> clazz = object.getClass();
while (clazz != null) {
Method postConstructMethod = getAndValidatePostConstructMethod(clazz);
Expand All @@ -23,6 +23,7 @@ public void process(Object object) {
}
clazz = clazz.getSuperclass();
}
return null;
}

private static Method getAndValidatePostConstructMethod(Class<?> clazz) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,15 @@ public PreConstructPackageValidator(String rootPackage) {
@Override
public void process(Class<?> clazz) {
if (clazz.getPackage() == null) {
throw new InjectorException("Cannot instantiate '" + clazz + "'. Primitive types must be provided"
+ " explicitly (or use an annotation).");
if (clazz.isPrimitive()) {
throw new InjectorException("Cannot instantiate '" + clazz + "'. Primitive types must be provided"
+ " explicitly (or use an annotation).");
} else if (clazz.isArray()) {
throw new InjectorException("Found array class '" + clazz + "'. Unknown how to inject (did you forget"
+ " to add a custom handler?).");
} else {
throw new InjectorException("Unknown class '" + clazz + "'.");
}
}
String packageName = clazz.getPackage().getName();
if (!packageName.startsWith(rootPackage)) {
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/ch/jalu/injector/utils/ReflectionUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ public static <T> Object toSuitableCollectionType(Class<?> rawType, Set<T> resul
return new ArrayList<>(result);
}
throw new InjectorException("Cannot convert @AllTypes result to '" + rawType + "'. "
+ "Supported: Set, List, or any subtype thereof, and array");
+ "Supported: Set, List, or any supertype thereof, and array");
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package ch.jalu.injector.handlers.postconstruct;

import ch.jalu.injector.InjectorBuilder;
import ch.jalu.injector.InjectorImpl;
import ch.jalu.injector.handlers.dependency.AllInstancesAnnotationHandler;
import ch.jalu.injector.handlers.testimplementations.ProfilePostConstructHandler;
import ch.jalu.injector.samples.animals.Sparrow;
import ch.jalu.injector.samples.animals.services.Configuration;
import ch.jalu.injector.samples.animals.services.NameService;
import ch.jalu.injector.samples.animals.services.SoundServiceSupervisor;
import javassist.util.proxy.Proxy;
import org.junit.Test;

import static java.util.Collections.singletonList;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.not;

/**
* Tests that {@link PostConstructHandler} implementations
* can change the object that will be stored.
*/
public class PostConstructRemappingTest {

private static final String PACKAGE = "ch.jalu.injector";

@Test
public void shouldInitializeProfiledClassesWithProxy() {
// given
InjectorImpl injector = (InjectorImpl) new InjectorBuilder()
.addDefaultHandlers(PACKAGE)
.addHandlers(new AllInstancesAnnotationHandler(PACKAGE))
.create();
ProfilePostConstructHandler profileHandler = new ProfilePostConstructHandler(injector);
injector.getConfig().addPostConstructHandlers(singletonList(profileHandler));

// when (trigger initialization + invoke some methods)
SoundServiceSupervisor supervisor = injector.getSingleton(SoundServiceSupervisor.class);
supervisor.muteAll();
injector.getSingleton(Sparrow.class).getName();

// then
assertThat(profileHandler.getInvocations(), contains("SoundServiceSupervisor#muteAll", "Configuration#getLang",
"NameService#constructName", "Configuration#getLang"));
assertThat(injector.getIfAvailable(Configuration.class), instanceOf(Proxy.class));
assertThat(injector.getIfAvailable(Sparrow.class), not(instanceOf(Proxy.class)));
assertThat(injector.getIfAvailable(NameService.class), instanceOf(Proxy.class));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package ch.jalu.injector.handlers.testimplementations;

import ch.jalu.injector.InjectorImpl;
import ch.jalu.injector.handlers.instantiation.ConstructorInjection;
import ch.jalu.injector.handlers.instantiation.DependencyDescription;
import ch.jalu.injector.handlers.instantiation.Instantiation;
import ch.jalu.injector.handlers.instantiation.InstantiationProvider;
import ch.jalu.injector.handlers.postconstruct.PostConstructHandler;
import javassist.util.proxy.MethodFilter;
import javassist.util.proxy.MethodHandler;
import javassist.util.proxy.Proxy;
import javassist.util.proxy.ProxyFactory;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

/**
* Sample post construct handler that wraps a constructed class into a proxy
* for logging purposes when methods annotated with {@link Profile} are found.
*/
public class ProfilePostConstructHandler implements PostConstructHandler {

private static final MethodFilter METHOD_FILTER = new MethodFilter() {
@Override
public boolean isHandled(Method m) {
return m.isAnnotationPresent(Profile.class);
}
};

private final InjectorImpl injector;
private final List<String> invocations = new ArrayList<>();

public ProfilePostConstructHandler(InjectorImpl injector) {
this.injector = injector;
}

@Override
public <T> T process(final T object) throws ReflectiveOperationException {
final Class<?> clazz = object.getClass();
if (!hasProfileMethod(clazz)) {
return null;
}

// The Proxy generated by javassist will have the same constructor args as on the real class
// We can easily check and satisfy this by getting the instantiation method: if it is constructor injection
// get the args from it. This approach requires some refactoring to support custom instantiation methods.
Instantiation<?> instantiation = getInstantiation(clazz);
Class<?>[] constructorTypes = instantiation instanceof ConstructorInjection<?>
? getConstructorType((ConstructorInjection<?>) instantiation)
: new Class<?>[0];
Object[] constructorArgs = resolveConstructorArgs(constructorTypes);

ProxyFactory pf = new ProxyFactory();
pf.setSuperclass(clazz);
Class<?> proxyClass = pf.createClass(METHOD_FILTER);

T proxy = (T) proxyClass
.getConstructor(constructorTypes)
.newInstance(constructorArgs);

((Proxy) proxy).setHandler(new MethodHandler() {
@Override
public Object invoke(Object self, Method thisMethod, Method proceed, Object[] args)
throws ReflectiveOperationException {
invocations.add(clazz.getSimpleName() + "#" + thisMethod.getName());
return thisMethod.invoke(object, args);
}
});
return proxy;
}

public List<String> getInvocations() {
return invocations;
}

private <T> Instantiation<T> getInstantiation(Class<T> clazz) {
for (InstantiationProvider provider : injector.getConfig().getInstantiationProviders()) {
Instantiation<T> instantiation = provider.get(clazz);
if (instantiation != null) {
return instantiation;
}
}
throw new IllegalStateException("Could not get instantiation for '" + clazz + "': are the instantiation "
+ "methods not in sync with the injector's?");
}

private static Class<?>[] getConstructorType(ConstructorInjection<?> injection) {
Class<?>[] classes = new Class<?>[injection.getDependencies().size()];
int i = 0;
for (DependencyDescription description : injection.getDependencies()) {
classes[i] = description.getType();
++i;
}
return classes;
}

private Object[] resolveConstructorArgs(Class<?>[] classes) {
List<Object> list = new ArrayList<>(classes.length);
for (Class<?> clazz : classes) {
list.add(injector.getIfAvailable(clazz));
}
return list.toArray();
}

private static boolean hasProfileMethod(Class<?> clazz) {
for (Method method : clazz.getDeclaredMethods()) {
if (METHOD_FILTER.isHandled(method)) {
return true;
}
}
return false;
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Profile {

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ public ThrowingPostConstructHandler(Class<?>... throwForClasses) {
}

@Override
public void process(Object object) throws Exception {
public <T> T process(T object) throws Exception {
increment();
for (Class<?> clazz : throwForClasses) {
if (clazz.isInstance(object)) {
throw new IllegalStateException("Class not allowed!");
}
}
return null;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package ch.jalu.injector.samples.animals.services;

import ch.jalu.injector.handlers.testimplementations.ProfilePostConstructHandler;

/**
* General configuration.
*/
Expand All @@ -11,6 +13,7 @@ public void setLang(String lang) {
this.lang = lang;
}

@ProfilePostConstructHandler.Profile
public String getLang() {
return lang;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package ch.jalu.injector.samples.animals.services;

import ch.jalu.injector.handlers.testimplementations.ProfilePostConstructHandler;

/**
* Service for hissing.
*/
Expand All @@ -8,6 +10,7 @@ public class HissService implements SoundService {
private boolean isMuted;

@Override
@ProfilePostConstructHandler.Profile
public String makeSound() {
return "Hiss";
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package ch.jalu.injector.samples.animals.services;

import ch.jalu.injector.handlers.testimplementations.ProfilePostConstructHandler;
import ch.jalu.injector.samples.animals.Animal;

import javax.inject.Inject;
Expand All @@ -9,9 +10,14 @@
*/
public class NameService {

@Inject
private LanguageService languageService;

@Inject
NameService(LanguageService languageService) {
this.languageService = languageService;
}

@ProfilePostConstructHandler.Profile
public String constructName(Animal animal) {
return languageService.translate(animal.getClass().getSimpleName());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ch.jalu.injector.samples.animals.services;

import ch.jalu.injector.annotations.AllInstances;
import ch.jalu.injector.handlers.testimplementations.ProfilePostConstructHandler;

import javax.inject.Inject;

Expand All @@ -17,6 +18,7 @@ public class SoundServiceSupervisor {
private SoundService[] soundServices;

// true if successful, false otherwise
@ProfilePostConstructHandler.Profile
public boolean muteAll() {
if ("en".equals(configuration.getLang())) {
for (SoundService soundService : soundServices) {
Expand Down

0 comments on commit 8a5668a

Please sign in to comment.