拦截与代理的陷阱及对象设计最佳实践
在软件开发中,拦截和代理是强大的技术,可用于添加动态行为、横切问题域,但同时也伴随着诸多陷阱。此外,对象设计的最佳实践对于确保程序的正确性和性能也至关重要。下面将详细介绍拦截与代理的陷阱以及对象设计的相关知识。
拦截与代理的陷阱
-
静态方法的问题
静态方法看似可以像实例方法一样被“重写”,但实际上并非如此。例如,在Super类和Sub类中都定义了静态方法babble(),当从Sub类调用babble()时,使用的是子类的版本。但这只是因为Sub类的babble()方法通过词法上下文隐藏了Super类的同名方法。如果在Sub或Super类之外调用,必须明确指定调用的是哪个类的方法。因此,当需要动态行为时,不应将逻辑放在静态方法中。 -
私有方法无法被代理
在Java中,私有方法不能被外部类调用,包括子类。这意味着动态代理(通常是子类)无法拦截私有方法。例如,对于FrenchChef类:
public class FrenchChef {
public void cook() {
...
}
private void clean() {
...
}
}
当添加
main
方法并运行时:
public static void main(String...args) {
FrenchChef chef = Guice.createInjector(new AbstractModule() {
@Override
protected void configure() {
bind(FrenchChef.class);
bindInterceptor(any(), any(),
new TracingInterceptor());
}
}).getInstance(FrenchChef.class);
chef.cook();
chef.clean();
}
输出结果只会显示
cook
方法的拦截信息,而不会显示
clean
方法的。解决这个问题的方法是将
clean
方法的可见性提升为
protected
或使用包级私有(省略访问修饰符)。
// protected 方法示例
public class FrenchChef {
public void cook() {
...
}
protected void clean() {
...
}
}
// 包级私有方法示例
public class FrenchChef {
public void cook() {
...
}
void clean() {
...
}
}
-
final方法和类无法被代理
final方法不能被子类重写,因此代理也无法拦截这些方法进行行为修改。例如:
public class FrenchChef {
public final void cook() {
...
}
void clean() {
...
}
}
如果尝试在子类中重写
cook
方法,会导致编译错误。解决方法是移除
final
修饰符,如果无法移除,可以使用委托模式。
public class FrenchChefDelegator implements Chef {
private final FrenchChef delegate;
public void cook() {
delegate.cook();
}
...
}
final
类不能被继承,同样无法生成子类进行代理。也可以使用委托模式来解决这个问题。
4.
字段无法被拦截
Spring和Guice不提供对字段的拦截功能。例如,
FrenchChef
类依赖于
RecipeBook
:
public class FrenchChef implements Chef {
private final RecipeBook recipes;
public void cook() {
...
}
public void clean() {
...
}
}
拦截器只能对
cook
和
clean
方法进行拦截,无法对
recipes
字段进行拦截。如果要拦截
RecipeBook
的方法,需要扩展匹配策略。
bindInterceptor(subclassesOf(Chef.class)
.or(subclassesOf(RecipeBook.class)), any(), new TracingInterceptor());
此外,标量字段(如基本类型或字符串)的访问也无法被拦截。
5.
单元测试与拦截
依赖注入通常不会影响测试的编写,但当涉及拦截时,情况会变得复杂。例如,对于
TicketBooth
类:
public class TicketBooth {
public Ticket purchase(Money money) {
if (money.equals(Money.inDollars("200")))
return new Ticket();
return Ticket.INSUFFICIENT_FUNDS;
}
}
可以很容易地编写单元测试:
public class TicketBoothTest {
@Test
public void purchaseATicketSuccessfully() {
Ticket ticket = new TicketBooth().purchase(Money.inDollars("200"));
assert Ticket.INSUFFICIENT_FUNDS != ticket;
}
}
如果通过拦截器修改了行为,例如为每第100个客户打印折扣券,原类的语义就发生了变化。此时,不仅需要为拦截器编写单元测试,还需要编写集成测试来验证组合行为的正确性。
public class DiscountingInterceptorIntegrationTest {
@Test
public void discountEveryHundredthTicket() {
Injector injector = Guice.createInjector(new AbstractModule() {
@Override
protected void configure() {
bind(TicketBooth.class);
bindInterceptor(any(), any(),
new DiscountingInterceptor());
}
});
TicketBooth booth = injector.getInstance(TicketBooth.class);
for(int i = 1; i < 101; i++) {
Ticket ticket =
booth.purchase(Money.inDollars("200"));
if (i == 100)
assert ticket instanceof DiscountedTicket;
}
}
}
对象设计的最佳实践
-
对象的可见性问题
在多线程环境中,对象的可见性是一个核心问题。以哈希表为例,当多个线程同时访问一个共享的哈希表时,可能会出现不安全发布的问题。例如:
public class UnsafePublication {
private Map<String, Email> emails = new HashMap<String, Email>();
public void putEmails() {
emails.put("Dhanji", new Email("dhanji@gmail.com"));
emails.put("Josh", new Email("josh@noemail.com"));
}
public void read() {
System.out.println("Dhanji's email address really is "
+ emails.get("Dhanji"));
}
}
当
putEmails
方法由线程A调用,
read
方法由线程B调用时,即使代码看起来有词法顺序,但无法保证线程A会在线程B之前执行。此外,即使线程A先执行,也不能保证线程B能看到最新的数据。
为了更好地理解这个问题,我们可以用一个流程图来表示:
graph LR
A[线程A: putEmails] -->|可能被调度或指令重排| B[线程B: read]
B -->|可能看不到最新数据| C[输出结果可能为null]
总结
拦截和代理是强大的技术,但在使用时需要注意各种陷阱。静态方法、私有方法、
final
方法和类以及字段都存在无法被拦截的情况,需要采取相应的解决措施。在进行单元测试时,不仅要编写单元测试,还需要编写集成测试来验证组合行为的正确性。同时,在多线程环境中,要注意对象的可见性问题,避免出现不安全发布的情况。
拦截与代理的陷阱及对象设计最佳实践
不同类型方法拦截问题总结
为了更清晰地展示不同类型方法在拦截时的问题及解决方法,我们可以用表格进行总结:
| 方法类型 | 拦截问题 | 解决方法 |
| ---- | ---- | ---- |
| 静态方法 | 不能实现真正的重写,无法通过代理实现动态行为 | 避免将逻辑放在静态方法中 |
| 私有方法 | 代理无法拦截 | 提升方法可见性为
protected
或使用包级私有 |
|
final
方法 | 不能被子类重写,代理无法拦截 | 移除
final
修饰符,若无法移除使用委托模式 |
|
final
类 | 不能被继承,无法生成子类进行代理 | 使用委托模式 |
| 字段 | 无法被拦截 | 扩展匹配策略,拦截依赖对象的方法 |
拦截顺序的影响
拦截顺序也是一个需要注意的问题。一个拦截器如果替换了方法的返回值或参数,可能会意外地破坏其他尚未执行的拦截器的行为。例如,有两个拦截器
InterceptorA
和
InterceptorB
,
InterceptorA
修改了方法的返回值,那么
InterceptorB
接收到的返回值就不是原始方法的返回值,可能会导致其行为异常。
为了避免这种情况,我们可以通过编写集成测试来验证拦截器的组合行为。集成测试将预期的拦截器组合在一起,模拟实际应用环境,从而增强我们对系统行为的信心。
代理的其他注意事项
除了上述提到的方法和字段的拦截问题,代理还有一些其他需要注意的地方:
-
相同性测试
:使用
==
运算符测试引用是否相等时,可能会因为代理是动态生成的子类而导致结果不符合预期。在这种情况下,应该使用
equals
方法进行相等性测试。
-
类比较
:直接使用
getClass()
反射方法比较对象的类时,也会因为代理的存在而出现问题。同样,应该使用更合适的方式进行类的比较。
拦截与代理的应用场景
拦截和代理技术在很多场景中都有广泛的应用,例如:
-
安全
:可以通过拦截器验证用户的权限,确保只有具有足够权限的用户才能执行某些方法。
-
日志
:在方法执行前后记录日志,方便调试和监控系统运行状态。
-
事务
:对方法进行事务管理,确保数据的一致性和完整性。
下面是一个使用
Warp-persist
为
Hibernate
、
JPA
和
Db4objects
驱动的应用程序声明式地应用事务的示例:
// 使用 @Transactional 注解为方法添加事务性
@Transactional
public void someTransactionalMethod() {
// 执行数据库操作
}
多线程环境下的对象设计优化
在多线程环境中,除了要注意对象的可见性问题,还可以通过以下方式优化对象设计:
-
线程安全设计
:确保对象的状态在多线程环境下是安全的,可以使用
volatile
关键字、
synchronized
方法或
Lock
接口等。
-
并发优化
:使用并发容器(如
ConcurrentHashMap
)来替代普通的容器,提高并发性能。
以下是一个使用
ConcurrentHashMap
的示例:
import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;
public class ConcurrentExample {
private Map<String, String> concurrentMap = new ConcurrentHashMap<>();
public void putData(String key, String value) {
concurrentMap.put(key, value);
}
public String getData(String key) {
return concurrentMap.get(key);
}
}
类型安全与集合使用
在使用对象时,还需要注意类型安全和集合的使用。
-
类型安全
:在编写代码时,要确保类型的正确性,避免出现类型转换异常。可以使用泛型来提高类型安全性。
-
集合使用
:在使用集合存储对象时,要根据实际需求选择合适的集合类型。例如,如果需要快速查找元素,可以使用
HashMap
;如果需要有序存储元素,可以使用
TreeMap
。
下面是一个使用泛型和
HashMap
的示例:
import java.util.HashMap;
import java.util.Map;
public class GenericExample {
private Map<String, Integer> map = new HashMap<>();
public void addEntry(String key, Integer value) {
map.put(key, value);
}
public Integer getEntry(String key) {
return map.get(key);
}
}
总结
拦截和代理技术为我们提供了强大的功能,可以在不修改原有代码的情况下添加额外的行为。但在使用过程中,我们需要注意各种陷阱,如方法拦截问题、拦截顺序问题等,并采取相应的解决措施。同时,在多线程环境中,要关注对象的可见性和线程安全问题,优化对象设计。此外,类型安全和集合的正确使用也是确保代码质量的重要因素。通过合理运用这些技术和原则,我们可以编写出更加健壮、高效的代码。
通过下面的流程图,我们可以总结整个拦截与代理以及对象设计的要点:
graph LR
A[拦截与代理] --> B[方法拦截问题]
B --> B1[静态方法]
B --> B2[私有方法]
B --> B3[final方法和类]
B --> B4[字段]
A --> C[拦截顺序问题]
A --> D[代理注意事项]
E[对象设计] --> F[多线程环境]
F --> F1[可见性问题]
F --> F2[线程安全设计]
F --> F3[并发优化]
E --> G[类型安全与集合使用]
G --> G1[类型安全]
G --> G2[集合选择]
在实际开发中,我们应该根据具体的需求和场景,灵活运用这些知识,不断优化代码,提高系统的性能和可靠性。
超级会员免费看
8

被折叠的 条评论
为什么被折叠?



