Автоматически делегировать все методы Java-класса
Скажем, у меня есть класс со многими публичными методами:
public class MyClass {
public void method1() {}
public void method2() {}
(...)
public void methodN() {}
}
Теперь я хотел бы создать класс- оболочку, который делегировал бы все методы для обернутого экземпляра (делегата):
public class WrapperClass extends MyClass {
private final MyClass delegate;
public WrapperClass(MyClass delegate) {
this.delagate = delegate;
}
public void method1() { delegate.method1(); }
public void method2() { delegate.method2(); }
(...)
public void methodN() { delegate.methodN(); }
}
Теперь, если в MyClass есть много методов, мне нужно переопределить каждый из них, более или менее похожий код, который просто "делегирует". Мне было интересно, можно ли сделать какую-то магию для автоматического вызова метода в Java (поэтому класс Wrapper должен будет сказать: "Эй, если вы вызовете метод для меня, просто перейдите к делегированию объекта и вызовите этот метод для него).
Кстати: я не могу использовать наследование, потому что делегат не находится под моим контролем. Я просто получаю его экземпляр из другого места (другой случай был бы, если бы MyClass был финальным).
ПРИМЕЧАНИЕ: я не хочу генерации IDE. Я знаю, что могу сделать это с помощью IntelliJ/Eclipse, но мне любопытно, можно ли это сделать в коде.
Любые предложения, как добиться чего-то подобного? (ПРИМЕЧАНИЕ: я, вероятно, смогу сделать это на некоторых языках сценариев, таких как php, где я мог бы использовать магические функции php для перехвата вызова).
9 ответов
Возможно, динамический Proxy
Ява может помочь вам. Это работает, только если вы, следовательно, используете интерфейсы. В этом случае я буду называть интерфейс MyInterface
и установить реализацию по умолчанию:
public class MyClass implements MyInterface {
@Override
public void method1() {
System.out.println("foo1");
}
@Override
public void method2() {
System.out.println("foo2");
}
@Override
public void methodN() {
System.out.println("fooN");
}
public static void main(String[] args) {
MyClass wrapped = new MyClass();
wrapped.method1();
wrapped.method2();
MyInterface wrapper = WrapperClass.wrap(wrapped);
wrapper.method1();
wrapper.method2();
}
}
Реализация класса-оболочки выглядит так:
public class WrapperClass extends MyClass implements MyInterface, InvocationHandler {
private final MyClass delegate;
public WrapperClass(MyClass delegate) {
this.delegate = delegate;
}
public static MyInterface wrap(MyClass wrapped) {
return (MyInterface) Proxy.newProxyInstance(MyClass.class.getClassLoader(), new Class[] { MyInterface.class }, new WrapperClass(wrapped));
}
//you may skip this definition, it is only for demonstration
public void method1() {
System.out.println("bar");
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Method m = findMethod(this.getClass(), method);
if (m != null) {
return m.invoke(this, args);
}
m = findMethod(delegate.getClass(), method);
if (m != null) {
return m.invoke(delegate, args);
}
return null;
}
private Method findMethod(Class<?> clazz, Method method) throws Throwable {
try {
return clazz.getDeclaredMethod(method.getName(), method.getParameterTypes());
} catch (NoSuchMethodException e) {
return null;
}
}
}
Обратите внимание, что этот класс:
- продолжается
MyClass
, чтобы наследовать реализацию по умолчанию (любой другой сделал бы) - инвентарь
Invocationhandler
, чтобы прокси сделал отражение - опционально реализовать
MyInterface
(чтобы удовлетворить шаблон декоратора)
Это решение позволяет переопределять специальные методы, но делегировать все остальные. Это будет работать даже с подклассами класса Wrapper.
Обратите внимание, что метод findMethod
пока не фиксируются особые случаи.
Этому вопросу уже 6 месяцев, и замечательный ответ @CoronA был удовлетворен и принят @walkeros, но я подумал, что хотел бы добавить кое-что здесь, поскольку я думаю, что это может быть добавлено на дополнительный шаг.
Как обсуждалось с @CoronA в комментариях к его ответу, вместо того, чтобы создавать и поддерживать длинный список MyClass
методы в WrapperClass
(т.е. public void methodN() { delegate.methodN(); }
), решение динамического прокси перемещает это в интерфейс. Проблема заключается в том, что вам все еще нужно создать и поддерживать длинный список подписей для MyClass
методы в интерфейсе, который, возможно, немного проще, но не полностью решает проблему. Это особенно актуально, если у вас нет доступа к MyClass
чтобы знать все методы.
Согласно трем подходам для украшения вашего кода,
Для более длинных классов программист должен выбрать меньшее из двух зол: реализовать много методов-оболочек и сохранить тип декорированного объекта или сохранить простую реализацию декоратора и пожертвовать, сохраняя тип декорированного объекта.
Так что, возможно, это ожидаемое ограничение шаблона декоратора.
@Mark-Bramnik, однако, предлагает увлекательное решение с использованием CGLIB при добавлении методов класса Java (без интерфейсов). Мне удалось объединить это с решением @CoronaA, чтобы создать оболочку, которая может переопределять отдельные методы, но затем передавать все остальное в обернутый объект, не требуя интерфейса.
Вот MyClass
,
public class MyClass {
public void method1() { System.out.println("This is method 1 - " + this); }
public void method2() { System.out.println("This is method 2 - " + this); }
public void method3() { System.out.println("This is method 3 - " + this); }
public void methodN() { System.out.println("This is method N - " + this); }
}
Вот WrapperClass
который только переопределяет method2()
, Как вы увидите ниже, не переопределенные методы фактически не передаются делегату, что может быть проблемой.
public class WrapperClass extends MyClass {
private MyClass delagate;
public WrapperClass(MyClass delegate) { this.delagate = delegate; }
@Override
public void method2() {
System.out.println("This is overridden method 2 - " + delagate);
}
}
Вот MyInterceptor
который расширяется MyClass
, В нем используется прокси-решение с использованием CGLIB, как описано @Mark-Bramnik. Он также использует метод @CononA для определения, следует ли отправлять метод в оболочку (если она переопределена) или в обернутый объект (если это не так).
import java.lang.reflect.Method;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
public class MyInterceptor extends MyClass implements MethodInterceptor {
private Object realObj;
public MyInterceptor(Object obj) { this.realObj = obj; }
@Override
public void method2() {
System.out.println("This is overridden method 2 - " + realObj);
}
@Override
public Object intercept(Object arg0, Method method, Object[] objects,
MethodProxy methodProxy) throws Throwable {
Method m = findMethod(this.getClass(), method);
if (m != null) { return m.invoke(this, objects); }
Object res = method.invoke(realObj, objects);
return res;
}
private Method findMethod(Class<?> clazz, Method method) throws Throwable {
try {
return clazz.getDeclaredMethod(method.getName(), method.getParameterTypes());
} catch (NoSuchMethodException e) {
return null;
}
}
}
Вот Main
и результаты, которые вы получите, если запустить его.
import net.sf.cglib.proxy.Enhancer;
public class Main {
private static MyClass unwrapped;
private static WrapperClass wrapped;
private static MyClass proxified;
public static void main(String[] args) {
unwrapped = new MyClass();
System.out.println(">>> Methods from the unwrapped object:");
unwrapped.method1();
unwrapped.method2();
unwrapped.method3();
wrapped = new WrapperClass(unwrapped);
System.out.println(">>> Methods from the wrapped object:");
wrapped.method1();
wrapped.method2();
wrapped.method3();
proxified = createProxy(unwrapped);
System.out.println(">>> Methods from the proxy object:");
proxified.method1();
proxified.method2();
proxified.method3();
}
@SuppressWarnings("unchecked")
public static <T> T createProxy(T obj) {
Enhancer e = new Enhancer();
e.setSuperclass(obj.getClass());
e.setCallback(new MyInterceptor(obj));
T proxifiedObj = (T) e.create();
return proxifiedObj;
}
}
>>> Methods from the unwrapped object:
This is method 1 - MyClass@e26db62
This is method 2 - MyClass@e26db62
This is method 3 - MyClass@e26db62
>>> Methods from the wrapped object:
This is method 1 - WrapperClass@7b7035c6
This is overridden method 2 - MyClass@e26db62
This is method 3 - WrapperClass@7b7035c6
>>> Methods from the proxy object:
This is method 1 - MyClass@e26db62
This is overridden method 2 - MyClass@e26db62
This is method 3 - MyClass@e26db62
Как вы можете видеть, когда вы запускаете методы на wrapped
вы получаете оболочку для методов, которые не переопределяются (т.е. method1()
а также method3()
). Когда вы запускаете методы на proxified
однако все методы выполняются на обернутом объекте без необходимости делегировать их все в WrapperClass
или поместите все сигнатуры метода в интерфейс. Спасибо @CoronA и @Mark-Bramnik за то, что кажется довольно классным решением этой проблемы.
Проверьте аннотацию @Delegation из фреймворка Lombok: https://projectlombok.org/features/Delegate.html
Переключиться на Groovy:-)
@CompileStatic
public class WrapperClass extends MyClass {
@Delegate private final MyClass delegate;
public WrapperClass(MyClass delegate) {
this.delagate = delegate;
}
//Done. That's it.
}
http://mrhaki.blogspot.com/2009/08/groovy-goodness-delegate-to-simplify.html
Вам не нужно этого делать - ваш класс Wrapper является подклассом исходного класса, поэтому он наследует все свои общедоступные методы - и если вы не реализуете их, будет вызван оригинальный метод.
Вы не должны иметь extends Myclass
вместе с частным MyClass
объект - это действительно действительно излишне, и я не могу придумать шаблон проектирования, где делать это правильно. Ваш WrapperClass
это MyClass
и, следовательно, вы можете просто использовать его собственные поля и методы вместо вызова delegate
,
РЕДАКТИРОВАТЬ: в случае MyClass
являющийся final
вы бы обошли декларацию willfull, чтобы не разрешать создание подклассов путем "поддельного" наследования; Я не могу думать ни о ком, желающем сделать это, кроме вас, кто контролирует WrapperClass
; но, так как вы контролируете WrapperClass
не упаковывать все, что вам не нужно, на самом деле больше, чем просто опцию - это правильно, потому что ваш объект не является MyClass
и должен вести себя как единое целое в тех случаях, которые вы мысленно рассматривали.
РЕДАКТИРОВАТЬ вы только что изменили свой вопрос, чтобы означать что-то совершенно другое, удалив MyClass
суперкласс вашего WrapperClass
; это немного плохо, потому что это делает недействительными все ответы, данные до сих пор. Вы должны были открыть еще один вопрос.
Кредиты отправляются в CoronA для указания классов Proxy и InvocationHandler. Я разработал более полезный класс многократного использования на основе его решения, используя дженерики:
public class DelegationUtils {
public static <I> I wrap(Class<I> iface, I wrapped) {
return wrapInternally(iface, wrapped, new SimpleDecorator(wrapped));
}
private static <I> I wrapInternally (Class<I> iface, I wrapped, InvocationHandler handler) {
return (I) Proxy.newProxyInstance(wrapped.getClass().getClassLoader(), new Class[] { iface }, handler);
}
private static class SimpleDecorator<T> implements InvocationHandler {
private final T delegate;
private SimpleDecorator(T delegate) {
this.delegate = delegate;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Method m = findMethod(delegate.getClass(), method);
if (m == null) {
throw new NullPointerException("Found no method " + method + " in delegate: " + delegate);
}
return m.invoke(delegate, args);
}
}
private static Method findMethod(Class<?> clazz, Method method) throws Throwable {
try {
return clazz.getDeclaredMethod(method.getName(), method.getParameterTypes());
} catch (NoSuchMethodException e) {
return null;
}
}
}
Попробуй это:
public class Test {
public interface Test {
public void sayHello ();
}
public static class TestImpl implements Test {
@Override
public void sayHello() {
System.out.println("HELLO!");
}
}
public static void main(String[] args) {
Test proxy = DelegationUtils.wrap(Test.class, new TestImpl());
proxy.sayHello();
}
}
Я хотел создать класс автоматического делегирования, который выполняет методы делегата в EDT. С помощью этого класса вы просто создаете новый служебный метод, который будет использовать EDTDecorator, в который будет добавлена реализация m.invoke
в SwingUtilities.invokeLater
,
Однако, если я поразмышляю над этим, я, возможно, захочу пересмотреть создание прокси-сервера без отражений для каждого имеющегося у меня интерфейса - это может быть чище, быстрее и понятнее. Но это возможно.
Позвольте мне переопределить проблему для конкретного случая. Я хочу переопределить метод close интерфейса ResultSet в jdbc. Моя цель - закрыть подготовленное заявление в закрытом методе набора результатов. Я не мог получить доступ к классу (DelegatingResultSet), который реализуется в интерфейсе ResultSet. Есть много методов в интерфейсе ResultSet, и переопределение их один за другим, и вызов соответствующего метода из объекта ResultSet является одним из решений. Для динамического решения я использовал Dynamic ProxyClasses ( https://docs.oracle.com/javase/1.5.0/docs/guide/reflection/proxy.html).
// New ResultSet implementation
public class MyResultSet implements InvocationHandler {
ResultSet rs;
PreparedStatement ps;
private Method closeMethod;
public MyResultSet(ResultSet rs, PreparedStatement ps) {
super();
this.rs = rs;
this.ps = ps;
try {
closeMethod = ResultSet.class.getMethod("close",null);
} catch (NoSuchMethodException | SecurityException e) {
e.printStackTrace();
}
}
public void close() {
try {
rs.close();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
public static Object newInstance(ResultSet rs, PreparedStatement ps) {
return java.lang.reflect.Proxy.newProxyInstance(rs.getClass().getClassLoader(), rs.getClass().getInterfaces(),
new MyResultSet(rs,ps));
}
public Object invoke(Object proxy, Method m, Object[] args)
throws Throwable {
Object result = null;
try {
Class declaringClass = m.getDeclaringClass();
if (m.getName().compareTo("close")==0) {
close();
} else {
result = m.invoke(rs, args);
}
} catch (InvocationTargetException e) {
throw e.getTargetException();
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
} finally {
}
return result;
}
}
// Как это назвать:
ResultSet prs = (ResultSet) MyResultSet.newInstance(rs,ps);
Определите метод в WrapperClass
т.е. delegate()
который возвращает экземпляр MyClass
ИЛИ ЖЕ
Вы можете использовать рефлексию, чтобы сделать это, но вызывающая сторона должна передать имя метода в качестве аргумента для открытого метода. И будут сложности с аргументами метода / перегруженными методами и т. Д.
Кстати: я не могу использовать наследование, потому что делегат не находится под моим контролем. Я просто получаю его экземпляр из другого места (другой случай был бы, если бы MyClass был финальным)
Код, который вы опубликовали, имеет public class WrapperClass extends MyClass
На самом деле ваша текущая реализация WrapperClass
на самом деле декоратор поверх MyClass
Я очень ценю ответ @CoronA. Я также просмотрел ответ @Mark Cramer, но, если я что-то не упустил, я думаю, что всегда есть как минимум два экземпляра «проксифицированного» класса со странными отношениями между двумя объектами.
Это, наряду с тем фактом, что cglib теперь устарела, подтолкнуло меня к поиску новой реализации, основанной на ByteBuddy.
Вот что я придумал:
public class MyClass {
public String testMethod() {
return "11111";
}
public String testMethod2() {
return "aaaaa";
}
}
public class MyClassWithDelegate extends MyClass {
private static final Constructor<? extends MyClassWithDelegate> CONSTRUCTOR_WITH_DELEGATE;
static {
Constructor<? extends MyClassWithDelegate> temp = null;
try {
final var instrumentedMyClassWithDelegateType =
new ByteBuddy()
.subclass(MyClassWithDelegate.class)
.method(ElementMatchers.any())
.intercept(MethodDelegation.to(MethodInterceptor.class))
.make()
.load(MyClassWithDelegate.class.getClassLoader())
.getLoaded();
temp = instrumentedMyClassWithDelegateType.getConstructor(MyClass.class);
} catch (final Exception e) {
LOGGER.error("Cannot instrument class {}", MyClassWithDelegate.class, e);
}
CONSTRUCTOR_WITH_DELEGATE = temp;
}
public static MyClassWithDelegate getInstanceWithDelegate(final MyClass myClass) {
try {
return CONSTRUCTOR_WITH_DELEGATE.newInstance(myClass);
} catch (final Exception e) {
LOGGER.error("Cannot get instance of {}", MyClassWithDelegate.class, e);
throw new IllegalStateException();
}
}
private final boolean initialized;
private final MyClass delegate;
public MyClassWithDelegate(final MyClass delegate) {
super();
this.delegate = delegate;
this.initialized = true;
}
public String testMethod() {
return "22222";
}
public static class MethodInterceptor {
@RuntimeType
public static Object intercept(@This final MyClassWithDelegate self,
@Origin final Method method,
@AllArguments final Object[] args,
@SuperMethod final Method superMethod) throws Throwable {
if (!self.initialized || method.getDeclaringClass().equals(MyClassWithDelegate.class)) {
return superMethod.invoke(self, args);
} else {
return method.invoke(self.delegate, args);
}
}
}
}
В
initialized
поле используется для предотвращения вызова метода
super
конструктор от перенаправления делегату до его назначения (в этом случае это не было бы проблемой, но я хотел создать универсальное решение).
Каждый метод, вызываемый для экземпляра, будет перенаправлен делегату, за исключением методов, объявленных внутри него самого.
В этом примере вызов
testMethod()
на экземпляре вернет "22222", а
testMethod2()
вернет "ааааа".
Очевидно, делегирование действительно будет работать только в том случае, если каждый экземпляр
MyClassWithDelegate
получается при вызове
getInstanceWithDelegate
заводской метод.