[Java twig] Changer et ajouter le comportement de l'instance de l'extérieur (Proxy black magic)

introduction

Une magie noire appelée Proxy est parfois utilisée pour modifier le comportement d'une méthode d'une instance de l'extérieur. Personnellement, je ne l'ai pas beaucoup utilisé, mais j'ai essayé de l'utiliser pour la première fois pour modifier de force le comportement d'une application qui acquiert normalement une connexion JDBC et se ferme normalement à un comportement de type pool de connexions de l'extérieur. C'était plus facile à utiliser que ce à quoi je m'attendais, je vais donc le laisser comme mémo pour moi-même.

Préparation

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_); //Traiter la partie argument lors de l'appel de la méthode
						Object result = applyModifier(methodReplacer, method, Tuple.of(src, method, args), () -> method.invoke(src, args));//Remplacez la méthode elle-même
						return applyModifier(resultModifier, method, Tuple.of(src, method, args, result), () -> result); //Traiter le résultat de la méthode
					}
				}
		);
	}

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

C'était tout ce dont j'avais besoin pour me préparer.

Comment utiliser

À titre d'exemple, le code qui modifie le comportement d'une instance de BiFunction qui prend deux entiers et les connecte avec un signe + pour générer une chaîne de caractères est illustré ci-dessous.

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( //Lorsque apply est appelé, le premier argument+2, pour le deuxième argument-2 faire
				hoge, new Class<?>[]{BiFunction.class}, 
				new HashMap<Function<Method, Boolean>, Function<Tuple3<Object, Method, Object[]>, Object[]>>() {{
					put(
							method -> "apply".equals(method.getName()), //Exécutez ce qui suit lorsque le nom de la méthode est appliqué
							tpl3 -> {
								Object[] args = tpl3.cdr.cdr.car; //Retirez la partie argument, ajoutez 2 au premier argument et soustrayez 2 de l'argument suivant
								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 au lieu d'afficher b-Changer pour afficher 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( //Changer la chaîne de résultat pour répéter deux fois
				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; //Extraire le résultat de l'exécution sur l'objet d'origine
								return result + " " + result;        //Répétez-le deux fois
							}
					);
				}}
		);
	
		System.out.println(hoge.apply(2, 3)); // 2 +Afficher comme 3
		System.out.println(hoge1.apply(2, 3)); // 4 +Afficher comme 1
		System.out.println(hoge2.apply(2, 3)); // 2 -Afficher comme 3
		System.out.println(hoge3.apply(2, 3)); // 2 + 3 2 +Afficher comme 3
	}

trait simulé

Ce n'est que lorsque j'ai écrit le code jusqu'à présent que j'ai remarqué que même les méthodes qui ne sont même pas définies dans la classe de l'instance d'origine peuvent être ajoutées au gestionnaire d'appel. En d'autres termes, si vous faites les préparatifs suivants ...

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()); //Je remplacerai la méthode originale
						}
					};
					@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);
					}
				}
		);
	}

Tu peux le faire.

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); //Rénover la fonctionnalité Personne et Placer dans une instance d'objet
		
		((Person)o).setName("Ichiro");
		System.out.println(((Person)o).getName()); //Ça marche correctement
		((Place)o).setAddress("Tokyo");
		System.out.println(((Place)o).getAddress()); //Ça marche correctement

		System.out.println("Hash " + o.hashCode()); //La méthode originale d'Object peut être appelée
	}

C'est un code semblable à un trait.

** Je viens de me mettre en colère, et maintenant j'y réfléchis. ** **

Recommended Posts

[Java twig] Changer et ajouter le comportement de l'instance de l'extérieur (Proxy black magic)
Java, instance à partir du débutant