我的编程空间,编程开发者的网络收藏夹
学习永远不晚

怎么使用MyBatisPlus拦截器实现数据权限控制

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

怎么使用MyBatisPlus拦截器实现数据权限控制

这篇文章主要介绍“怎么使用MyBatisPlus拦截器实现数据权限控制”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“怎么使用MyBatisPlus拦截器实现数据权限控制”文章能帮助大家解决问题。

前言背景

平时开发中遇到根据当前用户的角色,只能查看数据权限范围的数据需求。列表实现方案有两种,一是在开发初期就做好判断赛选,但如果这个需求是中途加的,或不希望每个接口都加一遍,就可以方案二加拦截器的方式。在mybatis执行sql前修改语句,限定where范围。

当然拦截器生效后是全局性的,如何保证只对需要的接口进行拦截和转化,就可以应用注解进行识别

因此具体需要哪些步骤就明确了

  • 创建注解类

  • 创建拦截器实现InnerInterceptor接口,重写查询方法

  • 创建处理类,获取数据权限 SQL 片段,设置where

  • 将拦截器加到MyBatis-Plus插件中

上代码(基础版)

自定义注解

import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;@Target({ElementType.METHOD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)public @interface UserDataPermission {}

拦截器

import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;import com.baomidou.mybatisplus.core.toolkit.PluginUtils;import com.baomidou.mybatisplus.extension.parser.JsqlParserSupport;import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;import lombok.*;import net.sf.jsqlparser.expression.Expression;import net.sf.jsqlparser.statement.select.PlainSelect;import net.sf.jsqlparser.statement.select.Select;import net.sf.jsqlparser.statement.select.SelectBody;import net.sf.jsqlparser.statement.select.SetOperationList;import org.apache.ibatis.executor.Executor;import org.apache.ibatis.mapping.BoundSql;import org.apache.ibatis.mapping.MappedStatement;import org.apache.ibatis.session.ResultHandler;import org.apache.ibatis.session.RowBounds;import java.sql.SQLException;import java.util.List;@Data@NoArgsConstructor@AllArgsConstructor@ToString(callSuper = true)@EqualsAndHashCode(callSuper = true)public class MyDataPermissionInterceptor extends JsqlParserSupport implements InnerInterceptor {        private MyDataPermissionHandler dataPermissionHandler;    @Override    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {        if (InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) {            return;        }        PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);        mpBs.sql(this.parserSingle(mpBs.sql(), ms.getId()));    }    @Override    protected void processSelect(Select select, int index, String sql, Object obj) {        SelectBody selectBody = select.getSelectBody();        if (selectBody instanceof PlainSelect) {            this.setWhere((PlainSelect) selectBody, (String) obj);        } else if (selectBody instanceof SetOperationList) {            SetOperationList setOperationList = (SetOperationList) selectBody;            List<SelectBody> selectBodyList = setOperationList.getSelects();            selectBodyList.forEach(s -> this.setWhere((PlainSelect) s, (String) obj));        }    }        private void setWhere(PlainSelect plainSelect, String whereSegment) {        Expression sqlSegment = this.dataPermissionHandler.getSqlSegment(plainSelect, whereSegment);        if (null != sqlSegment) {            plainSelect.setWhere(sqlSegment);        }    }}

拦截器处理器

基础只涉及 = 表达式,要查询集合范围 in 看进阶版用例

import cn.hutool.core.collection.CollectionUtil;import lombok.SneakyThrows;import lombok.extern.slf4j.Slf4j;import net.sf.jsqlparser.expression.Alias;import net.sf.jsqlparser.expression.Expression;import net.sf.jsqlparser.expression.HexValue;import net.sf.jsqlparser.expression.StringValue;import net.sf.jsqlparser.expression.operators.conditional.AndExpression;import net.sf.jsqlparser.expression.operators.relational.EqualsTo;import net.sf.jsqlparser.expression.operators.relational.ExpressionList;import net.sf.jsqlparser.expression.operators.relational.InExpression;import net.sf.jsqlparser.expression.operators.relational.ItemsList;import net.sf.jsqlparser.schema.Column;import net.sf.jsqlparser.schema.Table;import net.sf.jsqlparser.statement.select.PlainSelect;import java.lang.reflect.Method;import java.util.List;import java.util.Objects;import java.util.Set;import java.util.stream.Collectors;@Slf4jpublic class MyDataPermissionHandler {        @SneakyThrows(Exception.class)    public Expression getSqlSegment(PlainSelect plainSelect, String whereSegment) {        // 待执行 SQL Where 条件表达式        Expression where = plainSelect.getWhere();        if (where == null) {            where = new HexValue(" 1 = 1 ");        }        log.info("开始进行权限过滤,where: {},mappedStatementId: {}", where, whereSegment);        //获取mapper名称        String className = whereSegment.substring(0, whereSegment.lastIndexOf("."));        //获取方法名        String methodName = whereSegment.substring(whereSegment.lastIndexOf(".") + 1);        Table fromItem = (Table) plainSelect.getFromItem();        // 有别名用别名,无别名用表名,防止字段冲突报错        Alias fromItemAlias = fromItem.getAlias();        String mainTableName = fromItemAlias == null ? fromItem.getName() : fromItemAlias.getName();        //获取当前mapper 的方法        Method[] methods = Class.forName(className).getMethods();        //遍历判断mapper 的所以方法,判断方法上是否有 UserDataPermission        for (Method m : methods) {            if (Objects.equals(m.getName(), methodName)) {                UserDataPermission annotation = m.getAnnotation(UserDataPermission.class);                if (annotation == null) {                    return where;                }                // 1、当前用户Code                User user = SecurityUtils.getUser();                // 查看自己的数据                 //  = 表达式                 EqualsTo usesEqualsTo = new EqualsTo();                 usesEqualsTo.setLeftExpression(new Column(mainTableName + ".creator_code"));                 usesEqualsTo.setRightExpression(new StringValue(user.getUserCode()));                 return new AndExpression(where, usesEqualsTo);            }        }        //说明无权查看,        where = new HexValue(" 1 = 2 ");        return where;    }}

将拦截器加到MyBatis-Plus插件中

如果你之前项目配插件 ,直接用下面方式就行

    @Bean    public MybatisPlusInterceptor mybatisPlusInterceptor() {        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();        // 添加数据权限插件        MyDataPermissionInterceptor dataPermissionInterceptor = new MyDataPermissionInterceptor();        // 添加自定义的数据权限处理器        dataPermissionInterceptor.setDataPermissionHandler(new MyDataPermissionHandler());        interceptor.addInnerInterceptor(dataPermissionInterceptor);        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));        return interceptor;    }

但如果你项目之前是依赖包依赖,或有公司内部统一拦截设置好,也可以往MybatisPlusInterceptor进行插入,避免影响原有项目配置

    @Bean    public MyDataPermissionInterceptor myInterceptor(MybatisPlusInterceptor mybatisPlusInterceptor) {        MyDataPermissionInterceptor sql = new MyDataPermissionInterceptor();        sql.setDataPermissionHandler(new MyDataPermissionHandler());        List<InnerInterceptor> list = new ArrayList<>();        // 添加数据权限插件        list.add(sql);        // 分页插件        mybatisPlusInterceptor.setInterceptors(list);        list.add(new PaginationInnerInterceptor(DbType.MYSQL));        return sql;    }

以上就是简单版的是拦截器修改语句使用

使用方式

在mapper层添加注解即可

    @UserDataPermission    List<CustomerAllVO> selectAllCustomerPage(IPage<CustomerAllVO> page, @Param("customerName")String customerName);

进阶版

基础班只是能用,业务功能没有特别约束,先保证能跑起来

进阶版 解决两个问题:

  • 加了角色,用角色决定范围

  • 解决不是mapper层自定义sql查询问题。

两个是完全独立的问题 ,可根据情况分开解决

解决不是mapper层自定义sql查询问题。

例如我们名称简单的sql语句 直接在Service层用mybatisPluse自带的方法

xxxxService.list(Wrapper<T> queryWrapper)xxxxService.page(new Page<>(),Wrapper<T> queryWrapper)

以上这种我应该把注解加哪里呢

因为service层,本质上还是调mapper层, 所以还是在mapper层做文章,原来的mapper实现了extends BaseMapper 接口,所以能够查询,我们要做的就是在 mapper层中间套一个中间接口,来方便我们加注解

xxxxxMapper &mdash;&mdash;》DataPermissionMapper(中间) &mdash;&mdash;》BaseMapper

根据自身需要,在重写的接口方法上加注解即可,这样就影响原先的代码

怎么使用MyBatisPlus拦截器实现数据权限控制

import com.baomidou.mybatisplus.core.conditions.Wrapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;import com.baomidou.mybatisplus.core.metadata.IPage;import com.baomidou.mybatisplus.core.toolkit.Constants;import org.apache.ibatis.annotations.Param;import java.io.Serializable;import java.util.Collection;import java.util.List;import java.util.Map;public interface DataPermissionMapper<T> extends BaseMapper<T> {        @Override    @UserDataPermission    T selectById(Serializable id);        @Override    @UserDataPermission    List<T> selectBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);        @Override    @UserDataPermission    List<T> selectByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);        @Override    @UserDataPermission    T selectOne(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);        @Override    @UserDataPermission    Integer selectCount(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);        @Override    @UserDataPermission    List<T> selectList(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);        @Override    @UserDataPermission    List<Map<String, Object>> selectMaps(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);        @Override    @UserDataPermission    List<Object> selectObjs(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);        @Override    @UserDataPermission    <E extends IPage<T>> E selectPage(E page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);        @Override    @UserDataPermission    <E extends IPage<Map<String, Object>>> E selectMapsPage(E page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);}

解决角色控制查询范围

引入角色,我们先假设有三种角色,按照常规的业务需求,一种是管理员查看全部、一种是部门管理查看本部门、一种是仅查看自己。

有了以上假设,就可以设置枚举类编写业务逻辑, 对是业务逻辑,所以我们只需要更改”拦截器处理器类“

  • 建立范围枚举

  • 建立角色枚举以及范围关联关系

  • 重写拦截器处理方法

范围枚举

@AllArgsConstructor@Getterpublic enum DataScope {    // Scope 数据权限范围 : ALL(全部)、DEPT(部门)、MYSELF(自己)    ALL("ALL"),    DEPT("DEPT"),    MYSELF("MYSELF");    private String name;}

角色枚举

@AllArgsConstructor@Getterpublic enum DataPermission {    // 枚举类型根据范围从前往后排列,避免影响getScope    // Scope 数据权限范围 : ALL(全部)、DEPT(部门)、MYSELF(自己)    DATA_MANAGER("数据管理员", "DATA_MANAGER",DataScope.ALL),    DATA_AUDITOR("数据审核员", "DATA_AUDITOR",DataScope.DEPT),    DATA_OPERATOR("数据业务员", "DATA_OPERATOR",DataScope.MYSELF);    private String name;    private String code;    private DataScope scope;    public static String getName(String code) {        for (DataPermission type : DataPermission.values()) {            if (type.getCode().equals(code)) {                return type.getName();            }        }        return null;    }    public static String getCode(String name) {        for (DataPermission type : DataPermission.values()) {            if (type.getName().equals(name)) {                return type.getCode();            }        }        return null;    }    public static DataScope getScope(Collection<String> code) {        for (DataPermission type : DataPermission.values()) {            for (String v : code) {                if (type.getCode().equals(v)) {                    return type.getScope();                }            }        }        return DataScope.MYSELF;    }}

重写拦截器处理类 MyDataPermissionHandler

import lombok.SneakyThrows;import lombok.extern.slf4j.Slf4j;import net.sf.jsqlparser.expression.Alias;import net.sf.jsqlparser.expression.Expression;import net.sf.jsqlparser.expression.HexValue;import net.sf.jsqlparser.expression.StringValue;import net.sf.jsqlparser.expression.operators.conditional.AndExpression;import net.sf.jsqlparser.expression.operators.relational.EqualsTo;import net.sf.jsqlparser.expression.operators.relational.ExpressionList;import net.sf.jsqlparser.expression.operators.relational.InExpression;import net.sf.jsqlparser.expression.operators.relational.ItemsList;import net.sf.jsqlparser.schema.Column;import net.sf.jsqlparser.schema.Table;import net.sf.jsqlparser.statement.select.PlainSelect;import java.lang.reflect.Method;import java.util.List;import java.util.Objects;import java.util.Set;import java.util.stream.Collectors;@Slf4jpublic class MyDataPermissionHandler {    private RemoteRoleService remoteRoleService;    private RemoteUserService remoteUserService;        @SneakyThrows(Exception.class)    public Expression getSqlSegment(PlainSelect plainSelect, String whereSegment) {        remoteRoleService = SpringUtil.getBean(RemoteRoleService.class);        remoteUserService = SpringUtil.getBean(RemoteUserService.class);        // 待执行 SQL Where 条件表达式        Expression where = plainSelect.getWhere();        if (where == null) {            where = new HexValue(" 1 = 1 ");        }        log.info("开始进行权限过滤,where: {},mappedStatementId: {}", where, whereSegment);        //获取mapper名称        String className = whereSegment.substring(0, whereSegment.lastIndexOf("."));        //获取方法名        String methodName = whereSegment.substring(whereSegment.lastIndexOf(".") + 1);        Table fromItem = (Table) plainSelect.getFromItem();        // 有别名用别名,无别名用表名,防止字段冲突报错        Alias fromItemAlias = fromItem.getAlias();        String mainTableName = fromItemAlias == null ? fromItem.getName() : fromItemAlias.getName();        //获取当前mapper 的方法        Method[] methods = Class.forName(className).getMethods();        //遍历判断mapper 的所以方法,判断方法上是否有 UserDataPermission        for (Method m : methods) {            if (Objects.equals(m.getName(), methodName)) {                UserDataPermission annotation = m.getAnnotation(UserDataPermission.class);                if (annotation == null) {                    return where;                }                // 1、当前用户Code                User user = SecurityUtils.getUser();                // 2、当前角色即角色或角色类型(可能多种角色)                Set<String> roleTypeSet = remoteRoleService.currentUserRoleType();                                DataScope scopeType = DataPermission.getScope(roleTypeSet);                switch (scopeType) {                    // 查看全部                    case ALL:                        return where;                    case DEPT:                        // 查看本部门用户数据                        // 创建IN 表达式                        // 创建IN范围的元素集合                        List<String> deptUserList = remoteUserService.listUserCodesByDeptCodes(user.getDeptCode());                        // 把集合转变为JSQLParser需要的元素列表                        ItemsList deptList = new ExpressionList(deptUserList.stream().map(StringValue::new).collect(Collectors.toList()));                        InExpression inExpressiondept = new InExpression(new Column(mainTableName + ".creator_code"), deptList);                        return new AndExpression(where, inExpressiondept);                    case MYSELF:                        // 查看自己的数据                        //  = 表达式                        EqualsTo usesEqualsTo = new EqualsTo();                        usesEqualsTo.setLeftExpression(new Column(mainTableName + ".creator_code"));                        usesEqualsTo.setRightExpression(new StringValue(user.getUserCode()));                        return new AndExpression(where, usesEqualsTo);                    default:                        break;                }            }        }        //说明无权查看,        where = new HexValue(" 1 = 2 ");        return where;    }}

关于“怎么使用MyBatisPlus拦截器实现数据权限控制”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识,可以关注编程网行业资讯频道,小编每天都会为大家更新不同的知识点。

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

怎么使用MyBatisPlus拦截器实现数据权限控制

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

怎么使用MyBatisPlus拦截器实现数据权限控制

这篇文章主要介绍“怎么使用MyBatisPlus拦截器实现数据权限控制”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“怎么使用MyBatisPlus拦截器实现数据权限控制”文章能帮助大家解决问题。前言
2023-07-05

MyBatis Plus 拦截器实现数据权限控制

一、介绍 上篇文章介绍的MyBatis Plus 插件实际上就是用拦截器实现的,MyBatis Plus拦截器对MyBatis的拦截器进行了包装处理,操作起来更加方便 二、自定义拦截器 2.1、InnerInterceptor MyBati
2023-08-20

MyBatis-Plus拦截器实现数据权限控制的示例

本文主要介绍了MyBatis-Plus拦截器实现数据权限控制的示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
2023-02-24

使用struts2拦截器如何实现对用户进行权限控制

使用struts2拦截器如何实现对用户进行权限控制?针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。大多数网站会设置用户权限,如过滤非法用户,用户不登录时不能进行访问,或者设置
2023-05-31

Mybatisplus数据权限DataPermissionInterceptor怎么实现

这篇文章主要讲解了“Mybatisplus数据权限DataPermissionInterceptor怎么实现”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Mybatisplus数据权限Dat
2023-07-02

使用springmvc怎么实现一个限流拦截器

这期内容当中小编将会给大家带来有关使用springmvc怎么实现一个限流拦截器,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。限流器算法目前常用限流器算法为两种:令牌桶算法和漏桶算法,主要区别在于:漏桶算法
2023-05-30

怎么在mybatisplus 中使用SQL拦截器实现关联查询功能

本篇文章为大家展示了怎么在mybatisplus 中使用SQL拦截器实现关联查询功能,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。环境信息jdk: 1.8springboot: 2.3.4.RELE
2023-06-15

SpringBoot怎么整合Springsecurity实现数据库登录及权限控制

这篇文章主要为大家展示了“SpringBoot怎么整合Springsecurity实现数据库登录及权限控制”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下“SpringBoot怎么整合Springs
2023-06-22

如何使用SQL语句在MongoDB中实现数据权限控制和访问管理?

如何使用SQL语句在MongoDB中实现数据权限控制和访问管理?MongoDB是一种面向文档的NoSQL数据库,而SQL(Structured Query Language)是关系型数据库管理系统的标准语言。尽管MongoDB不支持SQL的
如何使用SQL语句在MongoDB中实现数据权限控制和访问管理?
2023-12-17

Aurora数据库中怎么管理用户权限和访问控制

Aurora数据库使用IAM来管理用户权限和访问控制。您可以通过IAM控制台创建IAM用户、角色和策略,然后将这些IAM实体与Aurora数据库资源关联,从而控制谁可以访问数据库和执行哪些操作。您可以使用IAM策略来定义哪些操作可以被执行
Aurora数据库中怎么管理用户权限和访问控制
2024-04-09

怎么使用Spring MVC拦截器实现一个登录功能

怎么使用Spring MVC拦截器实现一个登录功能?相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。HandlerInterceptor接口Spring MVC中的Intercep
2023-05-31

使用Asp.NET怎么实现一个限流控制功能

使用Asp.NET怎么实现一个限流控制功能?很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。一、AspNetCoreRateLimit 介绍AspNetCoreRa
2023-06-08

怎么用Springboot+mybatis-plus+注解实现数据权限隔离

今天小编给大家分享一下怎么用Springboot+mybatis-plus+注解实现数据权限隔离的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们
2023-06-08

JavaScript怎么使用Promise实现并发请求数限制

本篇内容主要讲解“JavaScript怎么使用Promise实现并发请求数限制”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“JavaScript怎么使用Promise实现并发请求数限制”吧!没有
2023-07-06

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录