[Javaの小枝] インスタンスの挙動を外部から変更する & 追加する (Proxyの黒魔術)

# はじめに
インスタンスのメソッドの挙動を外部から変更するために Proxy という黒魔術が使われることがある。個人的にはあまり使ったことがなかったのだが、JDBCコネクションを普通に取得して普通に close しているアプリを外部から無理矢理コネクションプール的な挙動にかえるために初めて使用してみた。思ったより簡単に使えたので自分用のメモとして内容を残しておく。

# 準備
```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_); // メソッド呼び出し時のargument部分の加工を行なう
						Object result = applyModifier(methodReplacer, method, Tuple.of(src, method, args), () -> method.invoke(src, args));// メソッド自体を置きかえる 
						return applyModifier(resultModifier, method, Tuple.of(src, method, args, result), () -> result); // メソッドからの結果を加工する
					}
				}
		);
	}

	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();
	}

```

必要な準備はたったこれだけだった。

# 使い方

例として Integer を二つ取り、それを + 記号でつなげた文字列を生成する BiFunction のインスタンスの挙動を外から変えるコードを以下に示す。

```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( // applyが呼ばれた際、一番目の引数に +2、二番目の引数に -2 する
				hoge, new Class<?>[]{BiFunction.class}, 
				new HashMap<Function<Method, Boolean>, Function<Tuple3<Object, Method, Object[]>, Object[]>>() {{
					put(
							method -> "apply".equals(method.getName()), // メソッド名が apply の場合に以下を実行
							tpl3 -> {
								Object[] args = tpl3.cdr.cdr.car; // argument 部分を取り出し 最初の引数に2を足し、次の引数から2を引く
								args[0] = (Integer)args[0] + 2;
								args[1] = (Integer)args[1] - 2;
								return args;
							}
					);
				}}, 
				null, 
				null
		);
		BiFunction<Integer, Integer, String> hoge2 = ProxyUtils.makeProxy( // a + b を表示するのではなく a - 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( // 結果文字列を2回繰替えすように変える
				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; // 元のオブジェクトで実行した結果を取り出し
								return result + " " + result;        // それを2回繰替えす
							}
					);
				}}
		);
	
		System.out.println(hoge.apply(2, 3)); // 2 + 3 と表示
		System.out.println(hoge1.apply(2, 3)); // 4 + 1 と表示
		System.out.println(hoge2.apply(2, 3)); // 2 - 3 と表示
		System.out.println(hoge3.apply(2, 3)); // 2 + 3 2 + 3  と表示
	}
```

# trait もどき

ここまでコードを書いて初めて気がついたのだが、元のインスタンスのクラスに定義されてさえいないメソッドすら invocation handler に書き加えることができるようだ。つまり以下のような準備をしておくと…

```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()); // オリジナルのメソッドは置きかえないことにする
						}
					};
					@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);
					}
				}
		);
	}
```

こんなことができる。

```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); // Object のインスタンスに Person の機能と Place の機能を後付けする
		
		((Person)o).setName("Ichiro");
		System.out.println(((Person)o).getName()); // 正しく機能している
		((Place)o).setAddress("Tokyo");
		System.out.println(((Place)o).getAddress()); // 正しく機能している

		System.out.println("Hash " + o.hashCode()); // Object の本来のメソッドは呼びだせる
	}
```
traitまがいのキモいコードのできあがりだ。

**ついカッとなってやった、今は反省している。**