23、拦截器在企业应用中的使用及相关注意事项

拦截器在企业应用中的使用及相关注意事项

1. 拦截顺序的重要性

在拦截器的使用中,拦截顺序至关重要。例如以下代码:

bindInterceptor(subclassesOf(Template.class), any(), new BoldDecoratingInterceptor());
bindInterceptor(subclassesOf(Template.class), any(), new HtmlDecoratingInterceptor());

这会输出 <b><html><body>Hello, Bob!</body></html></b> 。显然,这里的拦截顺序出现了问题。我们可以通过调整顺序来修正:

import static com.google.inject.matcherMatchers.*;
//...
bindInterceptor(subclassesOf(Template.class), any(), new HtmlDecoratingInterceptor());
bindInterceptor(subclassesOf(Template.class), any(), new BoldDecoratingInterceptor());

调整后,就能正确输出 <html><body><b>Hello, Bob!</b></body></html> 。这表明,如果我们仅依赖单元测试来验证应用程序的行为,可能会忽略一些严重的问题。在实际使用 AOP 时,应该谨慎操作,确保拦截顺序正确。

2. 企业场景下的拦截器应用

拦截器在企业应用中有诸多实际用途,尤其是在事务处理和安全方面。

2.1 使用 warp - persist 实现事务方法

warp - persist 是一个为 Guice 提供持久化和事务支持的轻量级模块库,它能与以下流行的持久化引擎集成:
- Hibernate(流行的对象/关系映射工具)
- Java Persistence API(JPA,Java 的对象/关系映射标准)
- Db4objects(对象数据库)

下面以在数据库库存中存储新车为例,展示如何使用 warp - persist 实现事务方法。
首先,定义 CarInventory 类:

import javax.persistence.EntityManager;
import com.wideplay.warp.persist.Transactional;

public class CarInventory {
    private final EntityManager em;

    public CarInventory(EntityManager em) {
        this.em = em;
    }

    @Transactional
    public void newCar(Car car) {
        em.persist(car);
    }
}

在上述代码中, @Transactional 注解是 warp - persist 提供的,用于将 newCar 方法标记为事务性方法。如果不使用这种声明式方法,我们需要手动编写事务处理代码:

public void newCar(Car car) {
    EntityTransaction txn = em.getTransaction();
    txn.begin();
    boolean succeed = true;
    try {
        em.persist(car);
    } catch (RuntimeException e) {
        txn.rollback();
        succeed = false;
    } finally {
        if (succeed)
            txn.commit();
    }
}

可以看到,声明式事务不仅能避免大量重复的样板代码,还能让我们对事务状态进行语义控制。

接下来,我们需要告诉 Guice 注入器使用 warp - persist:

import com.wideplay.warp.persist.PersistenceService;
import com.wideplay.warp.persist.jpa.JpaUnit;

public class CarModule extends AbstractModule {
    @Override
    protected void configure() {
        //...
        install(PersistenceService.usingJpa().buildModule());
        bindConstant().annotatedWith(JpaUnit.class).to("carDB");
    }
}

这里的 install 方法用于将另一个模块添加到当前模块中,等同于将每个模块单独传递给 createInjector 方法。 bindConstant().annotatedWith(JpaUnit.class).to("carDB") 用于指定要连接的持久化单元。

持久化单元在 persistence.xml 配置文件中指定,示例如下:

<?xml version="1.0" encoding="UTF-8" ?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
    http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" 
    version="1.0">
    <!-- A JPA Persistence Unit -->
    <persistence-unit name="carDB" transaction-type="RESOURCE_LOCAL">
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <!-- JPA entities must be registered here -->
        <class>example.Car</class>
        <properties>
            <!-- vendor-specific properties go here -->
        </properties>
    </persistence-unit>
</persistence>

Car 类的定义如下:

@Entity
public class Car {
    @Id @GeneratedValue
    private Integer id;
    private String name;
    private String model;
    //get + set methods
}

当运行应用程序并调用 newCar 方法时,会自动启动并提交事务,确保 EntityManager Car 对象插入到相应的数据库表中。

下面是使用 warp - persist 实现事务方法的流程图:

graph LR
    A[创建 Car 对象] --> B[调用 newCar 方法]
    B --> C{是否使用 @Transactional 注解}
    C -- 是 --> D[自动管理事务]
    C -- 否 --> E[手动编写事务代码]
    D --> F[插入数据到数据库]
    E --> F
2.2 使用 Spring Security 保护方法

授权是拦截方法的另一个常见用例。Spring Security 是 Spring 的一个扩展,为应用程序提供了许多安全功能。

首先,需要在 web.xml 中配置 HTTP 过滤器:

<filter>
    <filter-name>springSecurity</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>springSecurity</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

这个过滤器映射会将所有请求路由到 Spring Security 的 DelegatingFilterProxy ,该过滤器能够调用 Spring 的注入器并确定要应用的安全约束。

然后,通过 XML 配置来保护 PhoneService 类的方法:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:security="http://www.springframework.org/schema/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
        http://www.springframework.org/schema/security
        http://www.springframework.org/schema/security/spring-security-2.0.xsd"> 
    <security:http auto-config="true">
        <security:intercept-url pattern="/**" access="ROLE_USER" />
    </security:http>
    <security:authentication-provider>
        <security:user-service>
            <security:user name="josh" password="supersecret"
                authorities="ROLE_USER, ROLE_ADMIN" />
            <security:user name="bob" password="bobby"
                authorities="ROLE_USER" />
        </security:user-service>
    </security:authentication-provider>
    <security:global-method-security>
        <security:protect-pointcut 
            expression="execution(* example.PhoneService.*(..))" 
            access="ROLE_ADMIN" />
    </security:global-method-security>
</beans>

上述配置的具体含义如下:
- <security:http auto-config="true"> :使用 Spring Security 提供的默认配置。
- <security:intercept-url pattern="/**" access="ROLE_USER" /> :拦截所有请求,只允许具有 ROLE_USER 权限的用户访问。
- <security:authentication-provider> :配置用户信息,这里创建了 josh bob 两个用户,其中 josh 具有 ROLE_USER ROLE_ADMIN 权限, bob 只有 ROLE_USER 权限。
- <security:global-method-security> :通过切入点表达式 execution(* example.PhoneService.*(..)) 匹配 PhoneService 类的所有方法,并要求只有具有 ROLE_ADMIN 权限的用户才能访问。

PhoneService 类的定义如下:

public class PhoneService {
    public Response call(String number) {
        //...
    }
    public void hangUp() {
        //...
    }
}

当用户触发 PhoneCaller 类的 doAction 方法时:

public class PhoneCaller {
    private final PhoneService phone;

    public PhoneCaller(PhoneService phone) {
        this.phone = phone;
    }

    public void doAction() {
        Response response = phone.call("555-1212");
        if (response.notBusy())
            phone.hangUp();
    }
}

Spring Security 会拦截对 PhoneService 类方法的调用,确保用户具有相应的权限。如果用户权限不足,会抛出异常并显示授权失败消息。

以下是 Spring Security 保护方法的操作步骤:
1. 在 web.xml 中配置 DelegatingFilterProxy 过滤器。
2. 在 XML 配置文件中设置安全规则,包括拦截 URL、用户信息和方法保护规则。
3. 编写需要保护的业务类和调用类。
4. 当用户发起请求时,Spring Security 会根据配置进行权限验证。

下面是 Spring Security 保护方法的流程图:

graph LR
    A[用户发起请求] --> B[请求进入 Spring Security 过滤器]
    B --> C{是否具有 ROLE_USER 权限}
    C -- 是 --> D{是否访问 PhoneService 方法}
    D -- 是 --> E{是否具有 ROLE_ADMIN 权限}
    E -- 是 --> F[执行方法]
    E -- 否 --> G[抛出授权失败异常]
    C -- 否 --> G
    D -- 否 --> F
3. 拦截和代理的陷阱与假设

在使用代理和拦截器时,存在一些容易陷入的陷阱和错误假设。

3.1 相同性测试不可靠

不要依赖相同性( == )测试来断言对象是否为预期对象。例如:

final Painting picasso = new Painting("Picasso");
Injector injector = Guice.createInjector(new AbstractModule() {
    protected void configure() {
        bind(Painting.class).toInstance(picasso);
        bindInterceptor(any(), any(), new TracingInterceptor());
    }
});
assert picasso == injector.getInstance(Painting.class);  //will fail

上述代码中,虽然语义上 picasso 和从注入器获取的实例是同一个对象,但由于注入器可能对对象进行了代理,实际的物理实例不同,所以断言会失败。

另外,很多人会错误地假设可用对象的类与绑定的类相同。例如:

import java.util.logging.*;
public class ANoisyService {
    public final Logger log = Logger.getLogger(getClass().getName());
    //...
}

这里 getClass() 方法可能返回动态生成的代理类,导致日志记录的类别错误。为了避免这个问题,应该明确指定日志记录的类别:

import java.util.logging.*;
public class ANoisyService {
    public final Logger log = Logger.getLogger(ANoisyService.class.getName());
    //...
}
3.2 静态方法无法被拦截

由于代理只是子类,而 Java 不支持重写静态方法,所以代理无法拦截静态方法。例如以下代码:

public class Super {
    public static void babble() {
        System.out.println("yakity yak");
    }
}
public class Sub extends Super {
    public static void babble() {
        System.out.println("eternal quiet");
    }
    public static void main(String...args) {
        babble();
    }
}

当从命令行运行 Sub 类时,会调用 Sub 类的 babble 方法,输出 eternal quiet 。这表明静态方法的调用是基于类的,而不是基于对象的,因此无法被代理拦截。

综上所述,在使用拦截器和代理时,我们需要注意拦截顺序、事务处理、安全保护以及避免陷入相同性测试和静态方法拦截的陷阱,以确保应用程序的正确性和稳定性。

拦截器在企业应用中的使用及相关注意事项(续)

4. 对拦截和代理陷阱的深入分析及解决建议

上半部分我们提到了拦截和代理存在的一些陷阱,接下来我们深入分析这些陷阱产生的原因,并给出相应的解决建议。

4.1 相同性测试不可靠的深入分析与解决

相同性测试不可靠主要是因为代理机制会在运行时生成新的对象实例。当使用 == 进行对象比较时,比较的是对象的引用地址,而不是对象的语义内容。在依赖注入框架中,为了实现拦截功能,常常会对对象进行代理包装,这就导致即使语义上是同一个对象,其物理实例也可能不同。

为了避免这个问题,在进行对象比较时,应该使用 equals() 方法。 equals() 方法可以根据对象的具体属性进行比较,而不是仅仅比较引用地址。例如:

final Painting picasso = new Painting("Picasso");
Injector injector = Guice.createInjector(new AbstractModule() {
    protected void configure() {
        bind(Painting.class).toInstance(picasso);
        bindInterceptor(any(), any(), new TracingInterceptor());
    }
});
Painting injectedPicasso = injector.getInstance(Painting.class);
assert picasso.equals(injectedPicasso); // 这样比较更可靠

同时,在使用 getClass() 方法时,为了避免获取到代理类的信息,应该明确指定类名。比如在日志记录中,使用类的字面量来获取日志记录器:

import java.util.logging.*;
public class ANoisyService {
    public final Logger log = Logger.getLogger(ANoisyService.class.getName());
    //...
}

这样可以确保日志记录的类别始终是正确的,不会因为代理类的影响而出现混乱。

4.2 静态方法无法被拦截的深入分析与替代方案

静态方法无法被拦截是由 Java 的语言特性决定的。静态方法属于类,而不是对象,它们在类加载时就已经确定,不会随着对象的实例化而改变。代理机制是基于对象的,通过创建子类来实现拦截功能,因此无法对静态方法进行拦截。

虽然静态方法无法直接被拦截,但在实际开发中,我们可以采用一些替代方案来实现类似的功能。例如,可以将静态方法的功能封装到实例方法中,然后对实例方法进行拦截。示例如下:

public class Super {
    public void babbleInstance() {
        babbleStatic();
    }

    public static void babbleStatic() {
        System.out.println("yakity yak");
    }
}

public class Sub extends Super {
    @Override
    public void babbleInstance() {
        System.out.println("eternal quiet");
    }

    public static void main(String... args) {
        Sub sub = new Sub();
        sub.babbleInstance(); // 可以通过实例方法实现类似拦截的效果
    }
}

通过这种方式,我们可以将原本的静态方法调用转换为实例方法调用,从而利用代理机制对其进行拦截。

5. 拦截器在不同场景下的性能考虑

在企业应用中使用拦截器时,性能是一个需要重点考虑的因素。不同的拦截器应用场景可能会对系统性能产生不同的影响。

5.1 事务拦截器的性能影响

事务拦截器在保证数据一致性方面起着重要作用,但如果使用不当,可能会对系统性能产生负面影响。例如,在高并发场景下,频繁开启和提交事务会增加数据库的负担,导致性能下降。

为了优化事务拦截器的性能,可以采取以下措施:
- 减少事务范围 :只在必要的代码块中使用事务,避免将不必要的操作包含在事务中。例如,在 CarInventory 类的 newCar 方法中,如果有一些数据验证操作可以在事务外部完成,就应该将其移出事务范围。

@Transactional
public void newCar(Car car) {
    // 数据验证操作可以先在事务外部完成
    if (isValidCar(car)) {
        em.persist(car);
    }
}

private boolean isValidCar(Car car) {
    // 数据验证逻辑
    return car.getName() != null && car.getModel() != null;
}
  • 使用批量操作 :如果需要处理大量数据,可以考虑使用批量操作来减少事务的数量。例如,在插入多条记录时,可以使用批量插入的方式,而不是逐条插入。
5.2 安全拦截器的性能影响

安全拦截器可以保护系统免受非法访问,但过多的安全检查也会影响系统的响应速度。特别是在处理大量请求时,频繁的权限验证会消耗大量的系统资源。

为了优化安全拦截器的性能,可以采取以下措施:
- 缓存权限信息 :对于一些频繁使用的权限信息,可以进行缓存,避免每次请求都进行数据库查询。例如,可以使用内存缓存(如 Redis)来存储用户的权限信息。
- 优化权限验证逻辑 :尽量简化权限验证的逻辑,避免复杂的查询和计算。例如,可以使用角色和权限的映射表来快速判断用户是否具有相应的权限。

6. 拦截器的配置与管理

在企业应用中,拦截器的配置和管理是一个重要的环节。合理的配置可以提高系统的可维护性和灵活性。

6.1 拦截器的配置方式

不同的框架提供了不同的拦截器配置方式。例如,在 Guice 中,可以使用 bindInterceptor 方法来配置拦截器:

bindInterceptor(subclassesOf(Template.class), any(), new BoldDecoratingInterceptor());

在 Spring 中,可以使用 XML 配置或注解来配置拦截器。例如,在 Spring AOP 中,可以使用 @Aspect @Around 注解来定义切面和拦截逻辑:

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class MyInterceptor {
    @Around("execution(* com.example.service.*.*(..))")
    public Object intercept(ProceedingJoinPoint joinPoint) throws Throwable {
        // 拦截前的逻辑
        Object result = joinPoint.proceed();
        // 拦截后的逻辑
        return result;
    }
}
6.2 拦截器的管理与维护

随着项目的不断发展,拦截器的数量可能会逐渐增加,这就需要对拦截器进行有效的管理和维护。可以采取以下措施:
- 模块化管理 :将不同功能的拦截器进行模块化管理,例如将事务拦截器、安全拦截器等分别放在不同的模块中,便于维护和扩展。
- 日志记录与监控 :在拦截器中添加日志记录功能,记录拦截的请求信息和处理结果,便于后续的问题排查和性能分析。同时,可以使用监控工具对拦截器的性能进行监控,及时发现和解决性能问题。

7. 总结

拦截器在企业应用中具有重要的作用,特别是在事务处理、安全保护等方面。但在使用拦截器时,我们需要注意拦截顺序、避免陷入代理和拦截的陷阱,同时要考虑性能和配置管理等方面的问题。通过合理使用拦截器,可以提高系统的可维护性、安全性和性能,为企业应用的稳定运行提供保障。

以下是一个总结拦截器使用要点的表格:
| 要点 | 详细说明 | 解决建议 |
| — | — | — |
| 拦截顺序 | 拦截顺序会影响最终的处理结果,需要确保顺序正确 | 仔细规划拦截器的绑定顺序,进行充分的测试 |
| 相同性测试不可靠 | 代理会导致对象的物理实例不同, == 比较不可靠 | 使用 equals() 方法进行比较,明确指定类名获取信息 |
| 静态方法无法被拦截 | 由于 Java 语言特性,静态方法不能被代理拦截 | 将静态方法功能封装到实例方法中 |
| 性能考虑 | 事务和安全拦截器可能影响系统性能 | 减少事务范围、缓存权限信息等 |
| 配置与管理 | 不同框架有不同的配置方式,需要有效管理拦截器 | 模块化管理、添加日志记录和监控 |

最后,用一个流程图来总结拦截器的使用流程:

graph LR
    A[需求分析] --> B{选择框架}
    B -- Guice --> C[使用 bindInterceptor 配置拦截器]
    B -- Spring --> D[使用 XML 或注解配置拦截器]
    C --> E{考虑性能}
    D --> E
    E -- 是 --> F[优化事务和安全拦截器性能]
    E -- 否 --> G[正常使用拦截器]
    F --> H[进行拦截操作]
    G --> H
    H --> I{检查拦截顺序和陷阱}
    I -- 有问题 --> J[调整顺序和避免陷阱]
    I -- 无问题 --> K[系统稳定运行]
    J --> K

通过以上的分析和建议,希望能帮助开发者更好地使用拦截器,在企业应用中发挥其最大的价值。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值