Filter 实现过滤符合条件的请求并落库

2024-02-09 08:37:17 浏览数 (2)

前言

Java过滤器(Filter)在Java Servlet API中是一个非常有用的组件,它允许你在请求到达Servlet或JSP之前或之后执行某些操作。

需求:当请求进入系统时进行拦截,如果符合拦截规则就将请求详情落库。

背景:SpringCloud 项目,注册中心是 Nacos。


一、配置过滤器类

首先,你需要在你的Spring Boot应用中添加Nacos的依赖。

我们选择 OncePerRequestFilter。

OncePerRequestFilter定义: OncePerRequestFilter 是 Spring Framework 中的一个过滤器接口,用于处理每个请求只执行一次的逻辑。这个过滤器类型是为了确保某个特定的逻辑只会在一个请求中被执行一次,无论该请求经过了多少个过滤器链。 使用 OncePerRequestFilter 的一个常见场景是,你可能希望在每个请求处理之前或之后执行某些操作,但又不希望这些操作在每个过滤器链中被重复执行。

然后,你可以创建一个过滤器类,如下所示:

代码语言:javascript复制
@Configuration
public class FilterConfig {
    @Bean
    public FilterRegistrationBean<OncePerRequestFilter> logFilter() {
        FilterRegistrationBean<OncePerRequestFilter> registration = new FilterRegistrationBean<>();
        registration.setFilter(new RequestLogFilter());
        registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
        return registration;
    }
}

这个配置类定义了一个过滤器,名为logFilter,它在每个请求上只执行一次(由其实现的OncePerRequestFilter接口保证)。

这个过滤器用于请求日志记录,其顺序被设置为最高优先级。


二、定义数据表、实体类、Mapper

2.1 DDL

请求时间入库自动生成。

代码语言:javascript复制
create table C##YYTXD.SHUXX_REQUEST_LOGS
(
    METHOD  VARCHAR2(10),
    URI     VARCHAR2(255),
    HEADERS VARCHAR2(4000),
    BODY    VARCHAR2(4000),
    IP      VARCHAR2(255),
    TIME    TIMESTAMP(6) default CURRENT_TIMESTAMP
)
/

2.2 实体类

定义一个Java实体类,用于映射数据库中的REQUEST_LOGS表。该类使用了Lombok库来简化代码的编写,同时使用了MyBatis Plus库的注解来方便地与数据库交互。

如下所示:

代码语言:javascript复制
@TableName(value ="REQUEST_LOGS")
@Data
public class RequestLogs implements Serializable {

    private String method;

    private String uri;

    private String headers;

    private String body;

    private String ip;

    @TableField(exist = false)
    private Date time;

    @TableField(exist = false)
    private static final long serialVersionUID = 1L;
}

这个实体类主要用于封装HTTP请求的日志信息,方便存储到数据库中。

每个日志记录可以包含请求的方法、URI、头部信息、正文内容、发起请求的IP地址以及请求的时间等信息。

2.3 Mapper

代码语言:javascript复制
@Mapper
public interface RequestLogsMapper extends BaseMapper<RequestLogs> {

}
代码语言:javascript复制
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mapper.RequestLogsMapper">

    <resultMap id="BaseResultMap" type="com.domain.po.RequestLogs">
            <result property="method" column="METHOD" jdbcType="VARCHAR"/>
            <result property="uri" column="URI" jdbcType="VARCHAR"/>
            <result property="headers" column="HEADERS" jdbcType="VARCHAR"/>
            <result property="body" column="BODY" jdbcType="VARCHAR"/>
            <result property="ip" column="IP" jdbcType="VARCHAR"/>
            <result property="time" column="TIME" jdbcType="TIMESTAMP"/>
    </resultMap>

    <sql id="Base_Column_List">
        METHOD,URL,HEADERS,
        BODY,IP,TIME
    </sql>

</mapper>

三、创建一个过滤器

该过滤器用于记录HTTP请求日志。这个类继承了OncePerRequestFilter,这意味着它会在每个请求上只执行一次。如下所示:

代码语言:javascript复制
@Component
public class RequestLogFilter extends OncePerRequestFilter {
    @Resource
    private RequestLogUriProperties requestLogUrlProperties;

    @Resource
    private RequestLogsMapper requestLogsMapper;

    public RequestLogFilter() {
    }

    public static RequestLogFilter requestLogFilter;

    @PostConstruct
    public void init() {
        requestLogFilter = this;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String uri = request.getRequestURI();

        AntPathMatcher matcher = new AntPathMatcher();

        HttpServletRequest requestWrapper = new RequestWrapper(request);

        for (String filterUri : requestLogFilter.requestLogUrlProperties.getUris()) {
            if (!matcher.match(filterUri, uri)) continue;

            String method = request.getMethod();
            String ip = request.getRemoteAddr();
            String body = RequestWrapper.getBodyString(requestWrapper);

            Enumeration<String> headerNames = request.getHeaderNames();
            Map<String, String> headers = new HashMap<>();
            // 遍历所有请求头,并存入Map中
            while (headerNames.hasMoreElements()) {
                String headerName = headerNames.nextElement();
                String headerValue = request.getHeader(headerName);
                headers.put(headerName, headerValue);
            }

            RequestLogs logsDto = new RequestLogs();
            logsDto.setMethod(method);
            logsDto.setUri(uri);
            logsDto.setHeaders(headers.toString());
            logsDto.setBody(body);
            logsDto.setIp(ip);
            requestLogFilter.shuxxRequestLogsMapper.insert(logsDto);
        }
        // 继续传递请求
        filterChain.doFilter(requestWrapper, response);
    }
}

这个过滤器的主要目的是捕获与特定URI模式匹配的所有HTTP请求,并将这些请求的相关信息记录到日志中。

特定URI模式匹配使用的是 ant url。匹配规则定义在配置文件中。


四、实现 Nacos 配置热更新

配置和初始化一个名为RequestLogUriProperties的bean。

这个bean主要用于存储和获取需要记录日志的URL列表。如下所示:

代码语言:javascript复制
@Configuration
@ConfigurationProperties(prefix = "request-log")
@RefreshScope
//Nacos配置热更新
public class RequestLogUriProperties {
    public List<String> getUris() {
        return uris;
    }

    public void setUris(List<String> uris) {
        this.uris = uris;
    }

    private List<String> uris;

}

通过与Spring的属性绑定机制结合,在 Nacos 配置文件中定义这些URL,并通过setter方法将其设置到bean中。同时,由于使用了@RefreshScope注解,当这些URL的配置发生变化时,bean会被重新初始化,从而实现配置的热更新。

注解解释:

  • @Configuration: 这是Spring框架的注解,表示该类是一个配置类,用于定义和注册beans。
  • @ConfigurationProperties(prefix = "request-log"): 这个注解将RequestLogUriProperties类与Spring的属性绑定机制结合,使得你可以在外部配置文件中使用request-log前缀来定义属性,并这些属性会自动填充到RequestLogUriProperties类的字段中。
  • @RefreshScope: 这是Spring Cloud的注解,用于支持配置的热更新。当配置发生变化时,带有此注解的bean会被重新初始化。

Nacos 中配置:

代码语言:javascript复制
request-log:
  uris:
    - /index/*
    - ......

这个配置会拦截所以 uri 是 /index/* 的请求。


五、自定义 RequestWrapper

spring boot项目,在过滤器、拦截器或自定义 aop 做统一处理时,获取了request中的inputstream来获取RequestBody里数据,获取之后在Controller里使用@RequestBody注解再获取的话。

就报错:Stream closed。

这是因为 HttpServletRequest 中的 inputstream 是不可重复读的。

所以我们要自定义 RequestWrapper ,对 HttpServletRequest 进行处理。

代码语言:javascript复制
public class RequestWrapper extends HttpServletRequestWrapper {
    private final byte[] body;

    public RequestWrapper(HttpServletRequest request) {
        super(request);
        // 获取 requestBody 中的数据
        body = getBodyString(request).getBytes(StandardCharsets.UTF_8);
    }

    //通过覆盖getReader和getInputStream方法,将request中的body数据存储到内存中的输入流,使得body数据能够被多次读取。
    @Override
    public BufferedReader getReader() {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }

    @Override
    public ServletInputStream getInputStream() {
        // 定义内存中的输入流
        final ByteArrayInputStream stream = new ByteArrayInputStream(body);

        return new ServletInputStream() {
            @Override
            public int read() {
                // 使用内存输入流读取数据
                return stream.read();
            }

            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener) {

            }
        };
    }

    //getBodyString方法用于获取request的body数据并转换为字符串返回。
    public static String getBodyString(HttpServletRequest request) {
        StringBuilder sb = new StringBuilder();
        InputStream inputStream = null;
        BufferedReader reader = null;
        try {
            inputStream = request.getInputStream();
            reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
            String line;
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return sb.toString();
    }
}

这个类包装了一个HttpServletRequest对象。这个类的主要目的是重写HttpServletRequestgetReadergetInputStream方法,以便将请求体的数据存储在内存中的输入流,从而允许多次读取请求体的数据。


六、容易踩的坑

6.1 Java 工具类 Mapper 层报空指针

问题:

在使用Spring框架时,尝试将Service注入到非Spring管理的静态方法或工具类中。在Spring中,依赖注入主要依赖于@Autowired@Resource注解,但是这些注解不适用于静态方法或非Spring管理的类。

原因:

当你在Controller层使用Service时,可以通过@Resource或@Autowired注解轻松注入Service。但在普通类或工具类中使用Service时,会遇到找不到注解的属性值的问题,导致Service为null并报空指针异常。 即使在调用Service的类中添加了@Component注解并加入了Spring容器管理,问题仍然存在。 另外,由于工具类或普通类是静态方法,而Service和Mapper是非静态的,因此无法直接注入到静态方法中。 即使将Service和Mapper注入为静态的,仍然会报空指针异常。 为了解决这个问题,你可以考虑使用单例模式、使用ApplicationContext、重构代码或避免在工具类或普通类中使用静态方法。

解决方法如下:

代码语言:javascript复制
    public RequestLogFilter() {
    }

    public static RequestLogFilter requestLogFilter;

    @PostConstruct
    public void init() {
        requestLogFilter = this;
    }

在类的实例化完成后,它的当前实例会被设置为静态字段requestLogFilter的引用。这种模式通常用于单例模式或确保只有一个实例存在的其他模式。

6.2 工具类中使用 @Value 给静态变量注入值失败

问题:

在SpringBoot中使用@value注解只能给普通变量注入值,不能直接给静态变量赋值,直接给静态变量赋值的话这些值会一直为null。

解决方案:

若要给静态变量赋值,可以使用set()方法,首先在对应的类上加上@Component注解,在set方法上使用value注解(注意set方法不是静态的,否则无法赋值)。

代码语言:javascript复制
    private static String uri;
 
    @Value("${uri}")
    public void seturi(String uri) {
        this.uri= uri;
    }

七、总结

实现一个高效的过滤器需要仔细考虑多个方面,包括规则定义、拦截机制、处理逻辑、性能优化、异常处理、配置管理和安全性。

通过合理地设计和实现过滤器,可以帮助提高系统的安全性、可维护性和可靠性。

此外,了解不同过滤器框架和技术的特点可以帮助你选择最适合你的特定需求的解决方案。


0 人点赞