这是有关Java动态类的第三篇文章。在 第一篇文章中, 我讨论了Java标准库中内置的代理功能。在 第二篇文章中, 我讨论了如何使用CGLib通过动态子类增强具体类。在本文中,我想介绍一个使用CGLib展示其其他功能的更为复杂的示例。
在前面的示例中,我们创建了一个增强的类来添加审核功能。我们要增强的类是一个没有接口的具体类,我们很乐意为其创建单个实例,因为它是(伪)服务。但是,如果我们想将增强的行为应用于许多不同种类的类,并通过接口访问增强的行为,以便从常规代码中轻松使用, 该怎么办?在这种情况下,我们还需要做几件事。
在本文中,我将展示一个示例,该示例允许任何JavaBean类(即,遵循 JavaBeans 方法的getter和setter方法的类)变为“可观察的”,以便任何侦听器都可以向其注册以收到属性更改的通知。
示例源代码
在本示例中,我将详解每个用到的类,源码可以 在GitHub上找到。
首先,我们需要一个基本的JavaBean类。这没有什么特别的。它只是遵循getter / setter约定。
public class SampleBean {
private String stringValue;
private int intValue;
public String getStringValue() {
return stringValue;
}
public void setStringValue(String stringValue) {
this.stringValue = stringValue;
}
public int getIntValue() {
return intValue;
}
public void setIntValue(int intValue) {
this.intValue = intValue;
}
}
如果要手动使该 bean 变为“可观察”的,可以利用 Java内置的 PropertyChangeSupport 类来为我们管理侦听器和属性更改事件。我们将不得不提供添加和删除侦听器的方法,并将调用传递给中的相同方法 PropertyChangeSupport
。我们还必须修改所有的setter方法,以将属性更改事件触发给侦听器。然后,我们将不得不向所有的bean类添加类似的逻辑。即使我们都从某个基类继承它们,我们仍然需要将逻辑添加到每个唯一的setter方法中。
取而代之的是,让我们看一种执行此方法的方法,其中我们只需执行一次逻辑即可。为了使事情变得容易,我将重用 标准库中的现有 类PropertyChangeSupport
和 PropertyChangeEvent
类。但是,我将声明一个 Observable
接口,因为它在标准库中不存在。这里仍然是常规Java,尚无动态:
import java.beans.PropertyChangeListener;
public interface Observable {
void addPropertyChangeListener(PropertyChangeListener listener);
void removePropertyChangeListener(PropertyChangeListener listener);
}
我们想要的是让我们所有的bean类都似乎实现此接口,以便侦听器可以注册。但是首先,在此示例中,我们还有另一个帮助程序类。我们需要一个用于属性更改的示例侦听器,因此我们只需要制作一个可以侦听其接收到的任何事件的侦听器即可:
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class LoggingPropertyChangeListener implements PropertyChangeListener {
private static final Log LOG = LogFactory.getLog(LoggingPropertyChangeListener.class);
@Override
public void propertyChange(PropertyChangeEvent evt) {
LOG.info("Property change: " + evt.getPropertyName() + "; Old value: " + evt.getOldValue() + ", New value: "
+ evt.getNewValue());
}
}
请注意,属性更改事件标识了更改的属性,旧值和新值。如果我们从设置员那里手动触发事件,那么收集这些数据将非常容易。以动态的方式来做会比较困难。
方法拦截器
现在我们已经准备好阶段,接下来可以看一下示例的内容了。首先,像前面的CGLib示例一样,我们需要一个方法拦截器。这是在我们的增强类上调用方法时将调用的代码。它将获得有关调用目标,被调用方法及其参数的信息。它将决定采取什么措施,包括调用常规的,未增强的超类方法(如果有)。
拦截器有很多部件,因此我将分阶段进行描述。这是此示例的拦截器的第一部分:
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
public class PropertyChangeInterceptor implements MethodInterceptor {
private PropertyChangeSupport pcs;
public void setTarget(Object target) {
this.pcs = new PropertyChangeSupport(target);
}
private void addPropertyChangeListener(PropertyChangeListener listener) {
if (null != pcs) {
pcs.addPropertyChangeListener(listener);
}
}
private void removePropertyChangeListener(PropertyChangeListener listener) {
if (null != pcs) {
pcs.removePropertyChangeListener(listener);
}
}
private void firePropertyChange(String propName, Object oldValue, Object newValue) {
if (null != pcs) {
pcs.firePropertyChange(propName, oldValue, newValue);
}
}
对于初学者,请注意,我们正在用 PropertyChangeSupport
来帮助我们跟踪侦听器和事件触发。 PropertyChangeSupport
类需要在构造时传递对事件源的引用。我们希望此事件源是我们的bean增强版本,而不是拦截器。因此,我们必须将其视为依赖项并提供一种 setTarget()
方法,以便可以告知拦截器它正在增强什么对象。
这个示例的一个小缺陷,因为这意味着我们将需要为每个我们要增强的对象创建一个拦截器实例。为了解决这个问题,我们将不得不放弃使用 PropertyChangeSupport
并推出自己的侦听器逻辑。也不需要费多大劲,但此示例不需要。
接下来,我们提供一种使用反射来查找并使用设置器名称调用属性的getter的方法:
private Object tryForGetter(String setterName, Object target) {
String getterName = "get" + setterName.substring(3);
try {
return target.getClass().getMethod(getterName, new Class<?>[]{}).invoke(target, (Object[]) null);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException
| SecurityException e) {
return null;
}
}
我们将使用此方法来获取属性的先前值,以便将其包含在中 PropertyChangeEvent
。如果该属性没有getter,或发生其他问题,我们只返回null。该事件将丢失旧值,但是否则其他一切都会正常运行。(顺便说一句,如果我出于生产目的编写此方法,则可能会使用 Apache BeanUtils,因为它有很多不错的方法来通过反射处理getter和setter。)
有了这些可用的方法,我们现在可以编写重要的方法,它是实际处理被拦截的方法调用的方法。请注意,所有方法调用都将通过此intercept()
方法进行路由 ,因此我们必须处理添加或删除侦听器的请求,以及与bean的常规交互。
@Override
public Object intercept(Object target, Method method, Object[] args, MethodProxy proxy) throws Throwable {
Object targetReturn = null;
// See if this method call should stop here
if (method.getName().equals("addPropertyChangeListener")) {
Class<?>[] paramTypes = method.getParameterTypes();
if (paramTypes.length == 1 && paramTypes[0].equals(PropertyChangeListener.class)) {
addPropertyChangeListener((PropertyChangeListener) args[0]);
return null;
}
} else if (method.getName().equals("removePropertyChangeListener")) {
Class<?>[] paramTypes = method.getParameterTypes();
if (paramTypes.length == 1 && paramTypes[0].equals(PropertyChangeListener.class)) {
removePropertyChangeListener((PropertyChangeListener) args[0]);
return null;
}
}
// Otherwise pass through to the real object
Object oldValue = null;
String name = method.getName();
boolean isSetter = (name.startsWith("set") && args.length == 1 && method.getReturnType() == Void.TYPE);
if (isSetter) {
oldValue = tryForGetter(name, target);
}
if (!Modifier.isAbstract(method.getModifiers())) {
targetReturn = proxy.invokeSuper(target, args);
}
if (isSetter) {
String propName = Character.toLowerCase(name.charAt(3)) + name.substring(4);
firePropertyChange(propName, oldValue, args[0]);
}
return targetReturn;
}
}
谁说Java没有 鸭子类型?这看起来很像Ruby method_missing 实现。
如果我们将方法调用识别为添加或删除侦听器的方法调用,则将完全拦截并在此处进行处理。否则,我们要确保将其委托给我们要增强的超类(因为bean类除设置器外可能还有其他方法)。
如果是设置方法,则使用JavaBeans命名约定来确定属性的名称。然后,我们尝试在调用实际的setter之前获取属性的旧值,这当然会更改该值。一旦尝试获取旧值,我们将调用真正的setter,然后向可能正在监听的任何人触发事件。请注意,通过等待直到调用真实的setter之后,我们才能避免在setter引发异常(例如,由于验证)的情况下引发更改事件,并且避免在事件监听器在bean的状态达到状态之前调用方法的竞争条件已完全更新。
增强类工厂
因此,现在我们有了一种通过添加/删除侦听器行为以及在调用setter方法时触发属性更改事件的行为来增强类的方法。要将其连接起来,以便获得增强的bean,我们需要使用CGLib的 Enhancer
。我们希望能够增强任何用JavaBean编写的类,因此我们真正想要的是一个工厂,它将向用户隐藏CGLib的工作并制作增强的bean实例。这是我们的工厂班级:
import net.sf.cglib.proxy.Enhancer;
public final class ObservableBeanFactory {
public static <T> T createObservableBean(Class<T> beanClass) {
PropertyChangeInterceptor interceptor = new PropertyChangeInterceptor();
Enhancer e = new Enhancer();
e.setSuperclass(beanClass);
e.setCallback(interceptor);
e.setInterfaces(new Class[] { Observable.class });
@SuppressWarnings("unchecked")
T bean = (T) e.create();
interceptor.setTarget(bean);
return bean;
}
}
使用通用静态方法只会使用户的工作变得更干净,因此他们的工作量减少了。首先,我们为属性更改拦截器创建了一个新实例(因为如上所述,我们无法重用它)。然后我们创建一个 Enhancer
实例。请注意,根据CGLib文档,我们不应尝试重用 Enhancer
实例。我们将增强类的超类设置为传入的任何类,然后提供将处理方法调用的拦截器。最后,我们告诉CGLib创建一个实现上述Observable
接口的bean。这样,对用户来说,我们的bean类将提供 addPropertyChangeListener
和removePropertyChangeListener
方法。当然,我们确实支持这些方法,因为拦截器通过签名查找它们并进行处理。再次,谁说Java没有鸭子类型?
最后,我们创建刚刚创建的新增强型bean类的实例,并将其传递给拦截器,以便 PropertyChangeSupport
正确设置它。
这是一个简单的工厂,我们可以使它几乎任意复杂。例如,尽管Enhancer
不应重复使用CGLib,但使用CGLib 制作的每个增强类都实现一个Factory
接口,该 接口可用于创建更多实例。如果我们要改进拦截器,以便可以在所有bean实例中重用它,那么我们可以缓存每个增强的bean类,并在请求一个我们已经见过的bean实例时重用它。这样会好得多,因为PermGen可能会一直存在此代码的潜在问题:它将为每个bean实例创建一个新类,而这个类永远不会消失。如果要缓存Bean类,则可能必须处理线程安全性问题,以确保没有并发映射修改或同一增强Bean类的两个版本在周围浮动。
完成示例
无论如何,既然我们有了工厂,就可以用它来制造增强型bean。这是说明用户如何与工厂和增强型bean交互的主要方法:
public class PropertyChangeExample {
public static void main(String[] args) {
SampleBean regular = new SampleBean();
SampleBean observableBean = ObservableBeanFactory.createObservableBean(SampleBean.class);
((Observable) observableBean).addPropertyChangeListener(new LoggingPropertyChangeListener());
/* Will not be observed */
regular.setStringValue("abc");
regular.setStringValue("def");
regular.setIntValue(1);
regular.setIntValue(2);
/* Will be observed */
observableBean.setStringValue("zyx");
observableBean.setStringValue("wvu");
observableBean.setIntValue(10);
observableBean.setIntValue(20);
}
}
请注意,在此示例中,CGLib没有什么特别的,并且可以像常规bean一样使用增强型bean。但是,增强的bean也可以强制转换为 Observable
接口,并且可以注册侦听器。
在运行时,增强型bean的类名称如下:
org.anvard.introtojava.dynamic.cglib.SampleBean$EnhancerByCGLIB$140acd09
并且 observableBean.getClass().getSuperclass()
将会 SampleBean.class
。
结论
向任何JavaBean添加“可观察的”行为的能力使我印象深刻的是,它展示了CGLib,而不是我想要在生产环境中尝试的东西。我们为每个方法调用添加了一个额外的步骤,并为对setter的每个调用添加了Java反射(因为我们使用反射来调用getter来获取先前的值)。我们可能会找到使该性能提高的一些方法,但这将是一个罕见的用例,在这种情况下,值得花费精力来构建此代码的线程安全的高性能版本并进行维护。
但是,它很好地说明了CGLib的工作方式以及为什么它对框架代码如此有价值。因此,当我教Java EE和Spring Framework时,我喜欢使用它。最重要的是,它有助于理解我希望学生在学习这些框架时获得的要点,这是不涉及魔术的。当内部发生故障并向您呈现大量堆栈跟踪信息时,编写框架可能会令人生畏。了解正在使用哪种技术可以帮助您了解那些在堆栈跟踪中要忽略的内容和要注意的内容。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。