24、拦截与代理的陷阱及对象设计最佳实践

拦截与代理的陷阱及对象设计最佳实践

在软件开发中,拦截和代理是强大的技术,可用于添加动态行为、横切问题域,但同时也伴随着诸多陷阱。此外,对象设计的最佳实践对于确保程序的正确性和性能也至关重要。下面将详细介绍拦截与代理的陷阱以及对象设计的相关知识。

拦截与代理的陷阱
  1. 静态方法的问题
    静态方法看似可以像实例方法一样被“重写”,但实际上并非如此。例如,在 Super 类和 Sub 类中都定义了静态方法 babble() ,当从 Sub 类调用 babble() 时,使用的是子类的版本。但这只是因为 Sub 类的 babble() 方法通过词法上下文隐藏了 Super 类的同名方法。如果在 Sub Super 类之外调用,必须明确指定调用的是哪个类的方法。因此,当需要动态行为时,不应将逻辑放在静态方法中。
  2. 私有方法无法被代理
    在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() {
        ...
    }
}
  1. 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;
        }
    }
}
对象设计的最佳实践
  1. 对象的可见性问题
    在多线程环境中,对象的可见性是一个核心问题。以哈希表为例,当多个线程同时访问一个共享的哈希表时,可能会出现不安全发布的问题。例如:
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[集合选择]

在实际开发中,我们应该根据具体的需求和场景,灵活运用这些知识,不断优化代码,提高系统的性能和可靠性。

无界云图(开源在线图片编辑器源码)是由四川爱趣五科技推出的一款类似可画、创客贴、图怪兽的在线图片编辑器。该项目采用了React Hooks、Typescript、Vite、Leaferjs等主流技术进行开发,旨在提供一个开箱即用的图片编辑解决方案。项目采用 MIT 协议,可免费商用。 无界云图提供了一系列强大的图片编辑功能,包括但不限于: 素材管理:支持用户上传、删除和批量管理素材。 操作便捷:提供右键菜单,支持撤销、重做、导出图层、删除、复制、剪切、锁定、上移一层、下移一层、置顶、置底等操作。 保存机制:支持定时保存,确保用户的工作不会丢失。 主题切换:提供黑白主题切换功能,满足不同用户的视觉偏好。 多语言支持:支持多种语言,方便全球用户使用。 快捷键操作:支持快捷键操作,提高工作效率。 产品特色 开箱即用:无界云图采用了先进的前端技术,用户无需进行复杂的配置即可直接使用。 免费商用:项目采用MIT协议,用户可以免费使用和商用,降低了使用成本。 技术文档齐全:提供了详细的技术文档,包括技术文档、插件开发文档和SDK使用文档,方便开发者进行二次开发和集成。 社区支持:提供了微信技术交流群,用户可以在群里进行技术交流和问题讨论。 环境要求 Node.js:需要安装Node.js环境,用于运行和打包项目。 Yarn:建议使用Yarn作为包管理工具,用于安装项目依赖。 安装使用 // 安装依赖 yarn install // 启动项目 yarn dev // 打包项目 yarn build 总结 无界云图是一款功能强大且易于使用的开源在线图片编辑器。它不仅提供了丰富的图片编辑功能,还支持免费商用,极大地降低了用户的使用成本。同时,详细的文档和活跃的社区支持也为开发者提供了便利的二次开发和集成条件。无论是个人用户还是企业用户,都可以通过无界云图轻
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值