[Java twig] Change & add instance behavior from outside (Proxy black magic)

Introduction

A black art called Proxy is sometimes used to change the behavior of instance methods from the outside. Personally, I haven't used it much, but I tried using it for the first time to forcibly change the behavior of an application that normally acquires a JDBC connection and normally closes to a connection pool-like behavior from the outside. It was easier to use than I expected, so I will leave the contents as a memo for myself.

Preparation

ProxyUtils.java


	@SuppressWarnings("unchecked")
	public static <T> T makeProxy(
			T src, 
			Class<?>[] classes, 
			Map<Function<Method, Boolean>, Function<Tuple3<Object, Method, Object[]>, Object[]>> argumentModifier, 
			Map<Function<Method, Boolean>, Function<Tuple3<Object, Method, Object[]>, Object>> methodReplacer, 
			Map<Function<Method, Boolean>, Function<Tuple4<Object, Method, Object[], Object>, Object>> resultModifier
	) {
		return (T) Proxy.newProxyInstance(
				src.getClass().getClassLoader(),
				classes, 
				new InvocationHandler() {
					@Override public Object invoke(Object proxy, Method method, Object[] args_) throws Exception {
						Object[] args = applyModifier(argumentModifier, method, Tuple.of(src, method, args_), () -> args_); //Process the argument part when calling the method
						Object result = applyModifier(methodReplacer, method, Tuple.of(src, method, args), () -> method.invoke(src, args));//Replace the method itself
						return applyModifier(resultModifier, method, Tuple.of(src, method, args, result), () -> result); //Process the result from the method
					}
				}
		);
	}

	private static interface SupplierThrowsException<A> {public A get() throws Exception;}
	private static <C, A, R> R applyModifier(Map<Function<C, Boolean>, Function<A, R>> map, C cond, A arg, SupplierThrowsException<R> defaultResult) throws Exception {
		if (map != null) {
			for (Entry<Function<C, Boolean>, Function<A, R>> e : map.entrySet()) {
				if (e.getKey().apply(cond)) return e.getValue().apply(arg);
			}
		}
		return defaultResult.get();
	}

That was all I needed to prepare.

How to use

As an example, the code that changes the behavior of an instance of BiFunction that takes two Integers and connects them with a + sign to generate a character string is shown below.

Main.java


	public static void main(String[] args) {
		
		final BiFunction<Integer, Integer, String> hoge = new BiFunction<Integer, Integer, String>() {
			@Override public String apply(Integer t, Integer u) {return "" + t + " + " + u;}
		};
		
		BiFunction<Integer, Integer, String> hoge1 = ProxyUtils.makeProxy( //When apply is called, the first argument+2, for the second argument-2 do
				hoge, new Class<?>[]{BiFunction.class}, 
				new HashMap<Function<Method, Boolean>, Function<Tuple3<Object, Method, Object[]>, Object[]>>() {{
					put(
							method -> "apply".equals(method.getName()), //Execute the following when the method name is apply
							tpl3 -> {
								Object[] args = tpl3.cdr.cdr.car; //Take out the argument part, add 2 to the first argument and subtract 2 from the next argument
								args[0] = (Integer)args[0] + 2;
								args[1] = (Integer)args[1] - 2;
								return args;
							}
					);
				}}, 
				null, 
				null
		);
		BiFunction<Integer, Integer, String> hoge2 = ProxyUtils.makeProxy( // a +a instead of displaying b-Change to display b
				hoge, new Class<?>[]{BiFunction.class}, 
				null,
				new HashMap<Function<Method, Boolean>, Function<Tuple3<Object, Method, Object[]>, Object>>() {{
					put(
							method -> "apply".equals(method.getName()),
							tpl3 -> {
								Object[] args = tpl3.cdr.cdr.car;
								return "" + args[0] + " - " + args[1];
							}
					);
				}}, 
				null
		);

		BiFunction<Integer, Integer, String> hoge3 = ProxyUtils.makeProxy( //Change the result string to repeat twice
				hoge, new Class<?>[]{BiFunction.class}, 
				null, 
				null, 
				new HashMap<Function<Method, Boolean>, Function<Tuple4<Object, Method, Object[], Object>, Object>>() {{
					put(
							method -> "apply".equals(method.getName()),
							tpl4 -> {
								Object result = tpl4.cdr.cdr.cdr.car; //Extract the result of execution on the original object
								return result + " " + result;        //Repeat it twice
							}
					);
				}}
		);
	
		System.out.println(hoge.apply(2, 3)); // 2 +Display as 3
		System.out.println(hoge1.apply(2, 3)); // 4 +Display as 1
		System.out.println(hoge2.apply(2, 3)); // 2 -Display as 3
		System.out.println(hoge3.apply(2, 3)); // 2 + 3 2 +Display as 3
	}

trait mock

It wasn't until I wrote the code so far that I realized that I could even add methods to the invocation handler that weren't even defined in the class of the original instance. In other words, if you make the following preparations ...

ProxyUtils.java


	public static interface TraitInterface<T extends TraitInterface<T>> {public Class<T> interfaceClass();}
	
	private static interface FunctionThrowsException<A, B> {public B apply(A a) throws Exception;}
	@SuppressWarnings("unchecked")
	public static <S> S trait(S src, TraitInterface<?>... trait) {
		return (S) Proxy.newProxyInstance(
				trait.getClass().getClassLoader(),
				Arrays.asList(trait).stream().map(t -> t.interfaceClass()).collect(Collectors.toList()).toArray(new Class<?>[0]),
				new InvocationHandler() {
					Map<String, FunctionThrowsException<Object[], Object>> handler = new HashMap<String, FunctionThrowsException<Object[], Object>>() {
						private static final long serialVersionUID = 6154196791279856396L;
						{
							for (TraitInterface<?> t : trait) for (Method m : t.getClass().getMethods()) put(m.getName(), args -> m.invoke(t, args));
							for (Method m : src.getClass().getMethods()) remove(m.getName()); //I will replace the original method
						}
					};
					@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
						if (handler.containsKey(method.getName())) return handler.get(method.getName()).apply(args); else return method.invoke(src, args);
					}
				}
		);
	}

You can do this.

Main.java


	public static interface Person extends TraitInterface<Person> {
		public default Class<Person> interfaceClass() {return Person.class;}
		public void setName(String name_);
		public String getName();
	}

	public static interface Place extends TraitInterface<Place> {
		public default Class<Place> interfaceClass() {return Place.class;}
		public void setAddress(String address_);
		public String getAddress();
	}
	
	public static void main(String[] args) {
		
		Object o = new Object();
		
		Person m = new Person() {
			private String name;
			@Override public void setName(String name_) {name = name_;}
			@Override public String getName() {return name;}
		};
		
		Place p = new Place() {
			private String address;
			@Override public void setAddress(String address_) {address = address_;}
			@Override public String getAddress() {return address;}
		};
		
		System.out.println("Hash " + o.hashCode());
		
		o = trait(o, m, p); //Retrofit Person and Place functionality to an Object instance
		
		((Person)o).setName("Ichiro");
		System.out.println(((Person)o).getName()); //It's working properly
		((Place)o).setAddress("Tokyo");
		System.out.println(((Place)o).getAddress()); //It's working properly

		System.out.println("Hash " + o.hashCode()); //The original method of Object can be called
	}

It's a trait-like chord.

** I just got angry, and now I'm reflecting on it. ** **

Recommended Posts

[Java twig] Change & add instance behavior from outside (Proxy black magic)
Java, instance starting from beginner