Spring AOP 企业级应用 - 统一功能处理

news2025/2/25 16:59:16

1.统一用户登录权限效验

统一用户登录权限效验使用传统的 AOP 能否解决问题呢 ?

@Component
@Aspect // 标识当前类为一个切面
public class LoginAOP {
    // 定义切点 (拦截的规则) - 拦截 UserController 中的所有方法
    @Pointcut("execution(* com.example.demo.controller.TestController.*(..))")
    public void pointcut() {
    }

    // 环绕通知
    @Around("pointcut()")
    public Object doAround(ProceedingJoinPoint joinPoint) {
        Object obj = null;
        // 前置业务代码
        System.out.println("环绕通知的前置执行方法");
        try {
            // 执行目标方法
            obj = joinPoint.proceed();
        } catch (Throwable e) {
            e.printStackTrace();
        }
        // 后置业务代码
        System.out.println("环绕通知的后置执行方法");
        return obj;
    }
}

能解决, 但是相对来说, 比较麻烦:

  • 首先, 环绕通知没有内置 HttpServletRequest 对象, 就不好拿到 session 对象.

  • 其次, 对于一些特殊的场景: 我们要对一部分方法进行拦截, 而另一部分方法不拦截时, 切点中的拦截规则很难定义, 甚至没办法定义.

1.1 Spring 拦截器

对于上述问题, Spring 提供的拦截器就可以很好地解决.

Spring 拦截器和传统 AOP的区别就类似 Servlet 和 Spring 的区别, 拦截器也是将传统 AOP 进行了封装, 内置了 reuqest, response 对象, 提供了更加方便的功能.

一个项目里面实现统一用户验证登录的处理, 一般有三种解决方案:

  • 使用传统的 AOP,

  • 使用拦截器,

  • 使用过滤器.

既然有三种解决方案, 为什么要选择使用拦截器呢 ?

1. 对于传统的 AOP, 功能比较简单, 写法过于复杂, 所以不使用.
2. 对于过滤器 (web容器提供的), 因为它的执行时机太靠前了, Spring 框架还没初始化, 也就是说触发过滤器的时候, request, response 对象还没有实例化. 所以过滤器用的也比较少.

🍁实现拦截器的两大步骤

  1. 创建自定义拦截器, 实现 HandlerInterceptor 接口并重写preHandle (执行方法前的预处理) 方法.

  1. 将自定义拦截器加入 WebMvcConfigureraddInterceptors 方法中. 【配置拦截规则】

1.1.1 创建自定义拦截器

@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 用户登录校验
        HttpSession session = request.getSession(false);
        if(session != null && session.getAttribute("userinfo") != null) {
            return true;
        }
        log.error("当前用户没有访问权限");
        response.setStatus(401);
        return false;
    }
}

自定义的拦截器是一个普通的类, 如果返回 true, 才会继续执行后续代码.

1.1.2 将自定义拦截器加入到系统配置中

前面写的自定义拦截器, 只是一个普通的类, 需要把它加入到系统配置中, 并配置拦截规则, 才是一个真正有用的拦截器.

@Configuration  // 将拦截器加入到框架当中
public class MyConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)  // 添加拦截器, 可以添加多个
                .addPathPatterns("/**") // 拦截所有请求
                .excludePathPatterns("/user/login") // 排除不拦截的 url
                .excludePathPatterns("/user/reg"); // 排除不拦截的 url
    }
}
1. addInterceptor 方法的作用 : 将自定义拦截器添加到系统配置中.
2. addPathPatterns : 表示需要拦截的 URL.
3. excludePathPatterns : 表示不拦截, 需要排除的 URL.
4. 拦截器不仅可以拦截方法, 还可以拦截静态文件 (.png, .js, .css)

业务代码:

@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
    @RequestMapping("/login")
    public boolean login(HttpServletRequest request,
                         String username, String password) {
        // 1. 非空判断
        if(StringUtils.hasLength(username) && StringUtils.hasLength(password)) {
            // 2. 验证用户名和密码是否正确
            if("admin".equals(username) && "admin".equals(password)) {
                // 登陆成功
                HttpSession session = request.getSession();
                // 存储用户信息
                session.setAttribute("userinfo", "admin");
                return true;
            } else {
                // 用户名或密码错误
                return false;
            }
        }
        return false;
    }

    @RequestMapping("/get_info")
    public String getInfo() {
        log.debug("执行了 getInfo 方法");
        return "执行了 getInfo 方法";
    }

    @RequestMapping("/reg")
    public String reg() {
        log.debug("执行了 reg 方法");
        return "执行了 reg 方法";
    }
}

前两个步骤我们已经做好了准具工作, 并配置好了拦截规则, 规定除了登录和注册功能不拦截外, 拦截其他所有 URL (getInfo). 下面来进行验证一下拦截器是否生效.

🍁验证拦截器是否生效

  1. 访问注册方法: 127.0.0.1:8080/user/reg

通过浏览器结果来看, 我们自定义的拦截器并没有拦截注册功能, 这符合我们的预期.

再来看看控制台的日志打印:

此处的日志打印的确实有点莫名其妙, 但是不是拦截器的锅, 这只是网页加载图标时报的错, 因为拦截器拦截不了 favicon.ico 【不重要】. 我们可以通过开发者工具抓包进行查看: 发现并不是代码得问题, reg 返回的状态码是 200 , 所以符合预期.【重要】

  1. 访问 getInfo() 方法: 127.0.0.1:8080/user/get_info

通过浏览器结果来看, 我们自定义的拦截器确实拦截了 getInfo() 方法, 并且设置了状态码 401, 这也符合我们的预期. 通过控制台查看, 此时就可以看到 "当前用户没有访问权限" 的日志信息了, 并且这是由访问 getInfo 方法触发的 (抓包查看就知道了).

  1. 访问登录方法

🍃当我直接通过访问 127.0.0.1:8080/user/login 的时候, 浏览器上会显示一个 false, 这和注册方法的效果一样, 只不过尚未登录 >>

🍃当我给上面的 URL 加上正确的参数时 (admin), 这时候浏览器上就能显示一个 true, 并且此时再次访问 getInfo() 方法时, 就不会出现 401 了>>

再次访问 getInfo() 时, 就不会出现 401 了 >>

由此可得, 以上的自定义拦截器实现了统一登陆验证功能. 但是要注意的点是: 我的用户信息只是挂在 session 上了, 那么它的作用域就是浏览器作用域, 你如果使用其他的浏览器访问, 依旧还是会出现 401.

1.2 拦截器的实现原理

首先我们要知道 Controller 的执行都会通过一个调度器 (DispatcherServlet) 来实现.

随便访问 controller 中的一个方法就能在控制台的打印信息就能看到, 这个可以类比到线程的调度上.

然后所有 Controller 中方法都会执行 DispatcherServlet 中的调度方法 doDispatch().

我们通过分析源码, 发现源码中的这两个主要步骤.预处理的过程就 和 前边代码 LoginInterceptor 拦截器做的事情差不多,判断拦截的方法是否符合要求, 如果符合要求, 就返回 true,然后继续执行后续业务代码, 否则, 后面的代码都不执行.

进入 applyPreHandle() 方法继续分析:

我们发现源码中就是通过遍历存放拦截器的 List, 然后不断判断每一个拦截器是否都返回 true 了, 但凡其中有一个拦截器返回 false, 后面的拦截器都不要走了, 并且后面的业务代码也不执行了. 看到这, 我们恍然大悟了.

添加拦截器前后程序执行流程:

通过前面的分析, 我们就能发现 Spring 中的拦截器其实就是封装了传统的 AOP , 它也是通过 动态代理的和环绕通知的思想来实现的

2. 统一异常的处理

为什么要统一异常的处理呢 ??

就拿用户在银行取钱这件事来说, 如果用户在办理业务的时候, 后端程序报错了, 它不返回任何信息, 或者它返回的信息不统一, 这都会让前端程序猿不知道咋办, 他不知道咋办, 那么就无法给用户提供相应的提示. 此时用户见程序没反应, 他自己也会怀疑是自己没点到, 还是程序出 bug 了. 所以需要进行统一异常的处理.

实现统一异常的处理是需要两个注解来实现的:

  • @ControllerAdvice : 控制通知类.

  • @ExceptionHandler : 异常处理器

二者结合表示, 当出现异常的时候执行某个通知 (执行某个方法事件)

【代码实现】

对于前面的 reg 方法, 我们写一个除0 异常.

    @RequestMapping("/reg")
    public String reg() {
        int number = 1 / 0;
        log.debug("执行了 reg 方法");
        return "执行了 reg 方法";
    }

异常处理类:

@ControllerAdvice
public class ErrorAdvice {

    @ExceptionHandler(Exception.class) // 异常类型
    @ResponseBody
    public HashMap<String, Object> exceptionAdvice(Exception e) {
        HashMap<String, Object> res = new HashMap<>();
        res.put("code", "-1");
        res.put("msg", e.getMessage());
        return res;
    }
}

程序运行, 浏览器访问结果:

这样处理之后, 前端程序猿就知道什么状况了, 这时候就可以友好的告诉用户 "系统繁忙, 请稍后再试"
如果没有做相应的处理, 程序就会报错, 并且啥都不返回, 此时前端程序猿就会懵逼.

上面的异常处理使用了一个大的异常类来处理, 我们还可以更加细化:

@ControllerAdvice
public class ErrorAdvice {

    @ExceptionHandler(ArithmeticException.class) // 异常类型
    @ResponseBody
    public HashMap<String, Object> arithmeticAdvice(Exception e) {
        HashMap<String, Object> res = new HashMap<>();
        res.put("code", "-1");
        res.put("msg", e.getMessage());
        return res;
    }
}

3.统一数据返回格式

为什么要统一数据返回格式 ?? 【优点】

1. 方便前端程序猿更好的接收和解析后端数据接口返回的数据, 降低前后端程序猿沟通成本!!
2. 有利于项目统一数据的维护和修改 等等...

【代码实现】

@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        // 返回 true, 表示走底下的方法
        return true;
    }

    // 方法返回之前调用此方法
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        HashMap<String, Object> res = new HashMap<>();
        res.put("code", 200);
        res.put("msg", "");
        res.put("data", body);
        return res;
    }
}

实现统一数据格式的关键点:

1. 添加@ ControllerAdvice 注解;

2. 实现 ResponseBodyAdvice 接口, 并重写 supports() 和 beforeBodyWrite() 两个方法.

使用拦截器的业务代码, 验证功能是否正确 >>>

访问 login() 方法:

经过统一格式的处理之后, 返回的数据都是统一的 Json 格式.

3.1 针对返回 String 类型的特殊处理

上述代码貌似没有问题了, 但是当我们去访问 reg() 方法时 :

    @RequestMapping("/reg")
    public String reg() {
        log.debug("执行了 reg 方法");
        return "执行了 reg 方法";
    }

发现程序竟然报错了, 错误原因是 HashMap 不能转换为 String >>

🍔报错的原因

首先, HashMap 在转换为 Json格式的 String时, 框架使用的是转换器. 通过打断点调试, 发现出现这个错误的原因就是转换器不同导致的:

  1. 返回值为 String , 使用的转换器 :

  1. 返回值为其他类型使用的换器:

返回 String 类型, 封装成 HashMap , 转换成 Json 格式的字符串时, 使用的是 org.springframework.http.converter.StringHttpMessageConverter 转换器, 而返回其他类型时, 使用的是 org.springframework.http.converter.json.MappingJackson2HttpMessageConverter 转换器.

为什么使用 StringHttpMessage 转换器就会报错呢 >>

因为 StringHttpMessage 转换器的执行时机比较晚, 在进行类型转换的时候, 该转换器还没加载好, 所以就会报错, 而 MappingJackson2 转换器的执行时机比较早, 所以不会报错.

如何解决>>

@SneakyThrows
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        HashMap<String, Object> res = new HashMap<>();
        res.put("code", 200);
        res.put("msg", "");
        res.put("data", body);
        // 处理返回类型为 String 的
        if(body instanceof String) {
            ObjectMapper objectMapper = new ObjectMapper();
            return objectMapper.writeValueAsString(res);
        }
        return res;
    }

浏览器访问 reg 方法:

当我们单独处理 String 类型时,遇到 String类型, 就使用 ObjectMapper 对象将其转换成 Json 字符串, 此时就不会报错了.

3.2 企业级统一数据返回

之前的统一数据返回的 beforeBodyWrite() 存在的问题:

  • 之前的统一数据返回, 太过笼统了, 相当于把除异常之外的所有的数据的返回的状态码都设为 200 了. 这样非常不利于业务的分类.

  • 之前的统一数据返回, 如果 本身就是封装好的数据, 返回时调用 beforeBodyWrite() 方法, 就还会再被封装一次, 这不符合预期.

【正确做法】

  1. 创建自定义统一数据返回类型的类:

public class AjaxResult {
    /**
     * 业务执行成功时进行返回的方法
     * @param data
     * @return
     */
    public static HashMap<String, Object> success(Object data) {
        HashMap<String, Object> result = new HashMap<>();
        result.put("code", 200);
        result.put("msg", "");
        result.put("data", data);
        return result;
    }

    /**
     * 业务执行成功时进行返回的方法
     * @param data
     * @return
     */
    public static HashMap<String, Object> success(String msg, Object data) {
        HashMap<String, Object> result = new HashMap<>();
        result.put("code", 200);
        result.put("msg", msg);
        result.put("data", data);
        return result;
    }

    /**
     * 业务执行失败时进行返回的方法
     * @param code
     * @param msg
     * @return
     */
    public static HashMap<String, Object> fail(int code, String msg) {
        HashMap<String, Object> result = new HashMap<>();
        result.put("code", code);
        result.put("msg", msg);
        result.put("data", "");
        return result;
    }

    /**
     * 业务执行失败时进行返回的方法
     * @param code
     * @param msg
     * @param data
     * @return
     */
    public static HashMap<String, Object> fail(int code, String msg, Object data) {
        HashMap<String, Object> result = new HashMap<>();
        result.put("code", code);
        result.put("msg", msg);
        result.put("data", data);
        return result;
    }
}
  1. 修改 beforeBodyWrite() 方法:

@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }

    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        // 1.本身已经是封装好的对象
        if(body instanceof  HashMap) {
            return body;
        }
        // 2.返回类型是 String (特殊)
        if(body instanceof String) {
            ObjectMapper objectMapper = new ObjectMapper();
            return objectMapper.writeValueAsString(AjaxResult.success(body));
        }
        return AjaxResult.success(body);
    }
}
这样处理之后, 以上的两个问题就都得到了解决, 这才是企业级的统一数据返回格式的处理.

【其他代码的变化】

  1. 此时 reg() 方法完全可以这样写了:

    @RequestMapping("/reg")
    public Object reg(String username, String password) {
        // return AjaxResult.success("注册成功!", 1);
        return AjaxResult.fail(-1, "数据库添加出错!");
    }
我想指定状态码为 -1, 就传 -1, 想指定状态码为 -2, 就传 -2, 变得更加灵活了. 而且此处就算方法本身返回的就是一个封装好的对象, 也能得到有效的处理了.
  1. 统一异常的处理也可以变得简单了:

@ControllerAdvice
@ResponseBody
public class ExceptionAdvice {

    @ExceptionHandler(Exception.class) // 异常类型
    public Object exceptionAdvice(Exception e) {
        return AjaxResult.fail(-1, e.getMessage());
    }
}

本篇文章就到这里了, 谢谢观看!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/146687.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

React Hooks 基础、实现、原理

React Hooks 基础、实现、原理题外话为什么要有Hooks&#xff1f;但是Class Component 的用法也有缺陷&#xff1a;1.组件复用变的困难2.JavaScript本身的缺陷函数式React HooksuseStateuseEffectuseCallback、useMemouseReducer最后题外话 2023了&#xff0c;新年快乐&#x…

【javascript】DOM 案例

点击查看密码 &#xff1a;就是把type等于password改为text即可&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><…

电力系统强大的Gurobi 求解器的学习(PythonMatlab)

到底有多强大&#xff0c;看看就知道&#xff0c;必须&#x1f44d;&#x1f44d;&#x1f44d;&#xff1a; 目录 1 概述 2 算例理解【Python】 2.1 算例1——详细入门 2.2 算例2——一般线性规划问题 2.3 算例3——非凸问题 3 算例升级【Matlab】 3.1 模型 3.2 电力系统…

Python2.x 与 3​​.x 版本到底有啥区别?

嗨害大家好鸭&#xff01;我是小熊猫~ 今天给大家带来一点小干货~ 很多人对于python的版本有些许疑问&#xff0c; 今天就来给大家说说看~ Python学习资料电子书点击此处跳转文末名片 Python 的 3​​.0 版本&#xff0c;常被称为 Python 3000&#xff0c;或简称 Py3k。 相对…

Mybatis-Plus“读-批量写-读”数据不一致的问题分享

在日常开发过程中&#xff0c;时常会遇到一个如下场景&#xff1a; 根据条件x&#xff0c;读取表A&#xff0c;得到多行数据&#xff1b;遍历读取到的数据&#xff0c;对条件x以外的字段进行修改&#xff0c;并进行保存&#xff1b;&#xff08;重点&#xff09;修改后&#x…

基础算法(七)——离散化

离散化 介绍 这里的离散化&#xff0c;特指整数的、保序的离散化 有些题目可能需要以数据作为下标来操作&#xff0c;但题目给出的数据的值比较大&#xff0c;但是数据个数比较小。此时就需要将数据映射到和数据个数数量级相同的区间&#xff0c;这就是离散化&#xff0c;即…

基于imx6ull第一个Linux驱动

在编译第一个驱动之前&#xff0c;需要把基本的环境准备好&#xff0c;可以参照这两篇文章&#xff1a;https://wlink.blog.csdn.net/article/details/128590747https://wlink.blog.csdn.net/article/details/128591216我们之前写过一个基于ubuntu最基本的字符设备驱动&#xf…

关于固态硬盘冷数据掉速问题解决方案

20230107 By wdhuag 前言&#xff1a; 我有一个西数蓝盘500G固态&#xff0c;系统盘&#xff0c;一年没开机&#xff0c;这个月开机后发现系统很卡&#xff0c;持续读取假死严重。测试没有坏块&#xff0c;网上说的是冷数据掉速问题。 参考&#xff1a; 如何看待西数/闪迪多…

排序算法:插入、希尔、选择、冒泡

目录 一.插入排序 1.算法描述&#xff1a; 2.实现思路&#xff1a; 3.时间复杂度&#xff1a; 代码如下&#xff1a; 二.希尔排序 &#xff08;插入排序的优化升级&#xff09; 1.算法描述&#xff1a; 2.实现思路&#xff1a; 3.时间复杂度&#xff1a; 代码如下&a…

【算法笔记】最近公共祖先(LCA)问题求解——倍增算法

0. 前言 最近公共祖先简称 LCA&#xff08;Lowest Common Ancestor&#xff09;。两个节点的最近公共祖先&#xff0c;就是这两个点的公共祖先里面&#xff0c;离根最远的那个。 这种算法应用很广泛&#xff0c;可以很容易解决树上最短路等问题。 为了方便&#xff0c;我们记…

星光不负赶路人|2022年终总结

时间真快&#xff0c;转眼又是年末。整理一篇文章来给自己好好做一次年终盘点&#xff0c;本着陈述事实&#xff0c;提炼精华&#xff0c;总结不足的思路&#xff0c;给自己这一年的工作、生活、成长画个句号。 工作 &#x1f3e2; 从经海路到中关村 去年换了工作&#xff0c…

Java设计模式中的创建者模式/单例模式是啥?单例模式其中的饿汉式与懒汉式又是啥?又可以用在哪些地方

继续整理记录这段时间来的收获&#xff0c;详细代码可在我的Gitee仓库SpringBoot克隆下载学习使用&#xff01; 4. 创建者模式 4.1 特点 使用者不需要知道对象的创建细节 4.2 单例模式 4.2.1使用场景 单例类&#xff1a;且仅能创建一个实例类访问类&#xff1a;使用单例类…

七、Gtk4-Defining a final class

1 定义一个最终类 1.1 一个非常简单的编辑器 在上一节中&#xff0c;我们创建了一个非常简单的文件查看器。现在我们继续重写它&#xff0c;并将其转换为非常简单的编辑器。它的源文件是tfe目录下的tfe1.c(文本文件编辑器1)。 GtkTextView是一个多行编辑器。因此&#xff0c…

java学习day71(乐友商城)购物车实现

今日目标&#xff1a; 1.实现未登录状态的购物车 2.实现登陆状态下的购物车 1.搭建购物车服务 1.1.创建module 1.2.pom依赖 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi&…

软件测试~测试分类

目录 1.按照是否查看代码划分 ① 黑盒测试(Black-box Testing) ② 白盒测试(White-box Testing) ③ 灰盒测试(Gray-Box Testing) 2.按照开发阶段划分 ① 单元测试(Unit Testing) ② 集成测试(Integration Testing) ③ 系统测试(System Testing) ④ 验收测试(Acceptance…

kNN分类

一、 概述 kNN(k nearest neighbor,k近邻)是一种基础分类算法&#xff0c;基于“物以类聚”的思想&#xff0c;将一个样本的类别归于它的邻近样本。 ![在这里插入图片描述] 二、算法描述 1.基本原理 给定训练数据集 T{(x1,y1),(x2,y2),...,(xN,yN)}T\left\{ \left( x_1,y_1 …

17. XML

文章目录一、XML概念二、XML语法1、基础语法2、快速入门3、组成部分4、约束1. 约束概述2. 分类3. DTD4. Schema三、XML解析1、操作xml文档2、 解析xml的方式1. DOM2. SAX3. xml常见的解析器&#xff08;工具包&#xff09;4. Jsoup&#xff08;1&#xff09;快速入门&#xff0…

VUE3 学习笔记(一):环境配置、项目创建

一、首先需要安装node.jsnodejs官网&#xff1a;Node.js (nodejs.org)下载安装包&#xff1a;下载稳定版本即可&#xff0c;目前&#xff08;2023-01-07&#xff09;是18.13.0版本c. 检查当前版本&#xff08;CMD&#xff09;&#xff1a;至此&#xff0c;nodejs已经安装成功&a…

电力系统机组组合(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️❤️&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清…

EM算法学习总结

序 这个和我的硕士毕业论文的题目就有一定关系&#xff0c;我的导师让我按时向她汇报学习进度。然而我还在进行实习&#xff0c;还要准备自己明年的秋招&#xff0c;只能想办法游走于三者之间。 EM算法是一个常用的数据挖掘算法&#xff0c;想必从事数据挖掘的相关工作的同学…