jdbc-plus简介

jdbc-plus是一款基于JdbcTemplate增强工具包,基于JdbcTemplate已实现分页、多租户、动态表名等插件,可与mybatis、mybatis-plus等混合使用,还可以十分简单的扩展自定义插件。

特性

  • 使用简单,对代码入侵很小,可与mybatis、mybatis-plus等混合使用
  • 已实现分页、多租户、动态表名等插件,还可以十分简单的扩展自定义插件
  • 免费开源,可任意使用修改代码
  • 只对ORM框架增强不做任何改变,当需要动态执行SQL不是很方面使用ORM框架执行SQL时,jdbc-plus就能发挥作用

插件(持续扩展中)

已内置以下插件,开箱即用,还可以自行扩展插件,扩展插件方法十分简单。

  • 分页插件:与PageHelper使用方法一致,还可以注册不支持的数据库
  • 多租户插件:与mybatis-plus多租户插件使用方法一致,理论上与mybatis-plus多租户插件支持度一样
  • 动态表名插件:与mybatis-plus动态表名插件使用方法一致
  • 更多插件:持续关注jdbc-plus仓库,仓库包含所有插件源代码以及使用示例

项目开源地址

https://github.com/deeround/jdbc-plus

快速开始

  1. 引入jdbc-plus-spring-boot-starter
<dependency>
<groupId>com.github.deeround</groupId>
<artifactId>jdbc-plus-spring-boot-starter</artifactId>
<version>${version}</version>
</dependency>
  1. 注入需要使用的插件(需要哪个注入哪个,不需要的注释掉即可)
@Configuration
public class JdbcPlusConfig { /**
* TenantLineInterceptor是内置的多租户插件
*/
@Bean
@Order(1)
public IInterceptor tenantLineInterceptor() {
return new TenantLineInterceptor(new TenantLineHandler() {
/**
* 当前租户ID
*/
@Override
public Expression getTenantId() {
String currentTenantId = "test_tenant_1";//可以从请求上下文中获取(cookie、session、header等)
return new StringValue(currentTenantId);
} /**
* 租户字段名
*/
@Override
public String getTenantIdColumn() {
return "tenant_id";
} /**
* 根据表名判断是否忽略拼接多租户条件
*/
@Override
public boolean ignoreTable(String tableName) {
return TenantLineHandler.super.ignoreTable(tableName);
}
});
} /**
* DynamicTableNameInterceptor是内置的动态表名插件
*/
@Bean
@Order(2)
public IInterceptor dynamicTableNameInterceptor() {
return new DynamicTableNameInterceptor(new TableNameHandler() {
@Override
public String dynamicTableName(String sql, String tableName) {
if ("test_log".equals(tableName)) {
return tableName + "_" + LocalDateTime.now().getYear();
}
return tableName;
}
});
} /**
* PaginationInterceptor是内置的分页插件(分页插件一般情况放置最后)
*/
@Bean
@Order(9)
public IInterceptor paginationInterceptor() {
return new PaginationInterceptor();
} /**
* 自定义插件注入,注入位置按实际情况
*/
@Bean
@Order(0)
public IInterceptor myStatInterceptor() {
return new MyStatInterceptor();
}
}
  1. 正常使用JdbcTemplate执行SQL语句,代码零入侵,使用体验超棒
    @Autowired
JdbcTemplate jdbcTemplate; public void insert() {
this.jdbcTemplate.update("insert into test_user(id,name) values('1','wangwu')");
//最终执行SQL:insert into test_user(id,name,tenant_id) values('1','wangwu','test_tenant_1')
} public void delete() {
this.jdbcTemplate.update("delete from test_user where id='1'");
//最终执行SQL:delete from test_user where id='1' and tenant_id='test_tenant_1'
} public void update() {
this.jdbcTemplate.update("update test_user set name='lisi' where id='1'");
//最终执行SQL:update test_user set name='lisi' where id='1' and tenant_id='test_tenant_1'
} public List<Map<String, Object>> query() {
return this.jdbcTemplate.queryForList("select * from test_user");
//最终执行SQL:select * from test_user where tenant_id='test_tenant_1'
} public PageInfo<Map<String, Object>> page1() {
PageHelper.startPage(1, 2);
List<Map<String, Object>> list = this.jdbcTemplate.queryForList("select * from test_user");
//最终执行SQL:select * from test_user LIMIT 0,2
PageInfo<Map<String, Object>> page = new PageInfo<>(list);
//PageInfo对象包含了分页信息(总行数等)
return page;
}

多租户插件

  1. 注入多租户插件
    /**
* TenantLineInterceptor是内置的多租户插件插件
*/
@Bean
@Order(1)
public IInterceptor tenantLineInterceptor() {
return new TenantLineInterceptor(new TenantLineHandler() {
/**
* 当前租户ID
*/
@Override
public Expression getTenantId() {
String currentTenantId = "test_tenant_1";//可以从请求上下文中获取(cookie、session、header等)
return new StringValue(currentTenantId);
} /**
* 租户字段名
*/
@Override
public String getTenantIdColumn() {
return "tenant_id";
} /**
* 根据表名判断是否忽略拼接多租户条件
*/
@Override
public boolean ignoreTable(String tableName) {
return TenantLineHandler.super.ignoreTable(tableName);
}
});
}
  1. service层执行SQL时自动添加租户字段
    public void insert() {
this.jdbcTemplate.update("insert into test_user(id,name) values('1','wangwu')");
//最终执行SQL:insert into test_user(id,name,tenant_id) values('1','wangwu','test_tenant_1')
} public void delete() {
this.jdbcTemplate.update("delete from test_user");
//最终执行SQL:delete from test_user where tenant_id='test_tenant_1'
} public void update() {
this.jdbcTemplate.update("update test_user set name='lisi' where id='1'");
//最终执行SQL:update test_user set name='lisi' where id='1' and tenant_id='test_tenant_1'
} public List<Map<String, Object>> query() {
return this.jdbcTemplate.queryForList("select * from test_user");
//最终执行SQL:select * from test_user where tenant_id='test_tenant_1'
}

分页插件

  1. 注入分页插件
    /**
* PaginationInterceptor是内置的分页插件(分页插件一般情况放置最后)
*/
@Bean
@Order(9)
public IInterceptor paginationInterceptor() {
return new PaginationInterceptor();
}
  1. service层执行SQL时自动对SQL进行分页查询
    public PageInfo<Map<String, Object>> page1() {
PageHelper.startPage(1, 2);
List<Map<String, Object>> list = this.jdbcTemplate.queryForList("select * from test_user");
//最终执行SQL:select * from test_user LIMIT 0,2
PageInfo<Map<String, Object>> page = new PageInfo<>(list);
//PageInfo对象包含了分页信息(总行数等)
return page;
} public PageInfo<Map<String, Object>> page2() {
PageHelper.startPage(2, 2);
List<Map<String, Object>> list = this.jdbcTemplate.queryForList("select * from test_user");
//最终执行SQL:select * from test_user LIMIT 2,2
PageInfo<Map<String, Object>> page = new PageInfo<>(list);
//PageInfo对象包含了分页信息(总行数等)
return page;
}
  1. 自定义分页

当插件不支持的数据库分页,可以通过PageHelper.registerDialectAlias(String alias, Class clazz) 注册一个自己分页实现类即可,也可以覆盖已支持的数据库分页。

动态表名插件

  1. 注入分页插件
    /**
* DynamicTableNameInterceptor是内置的动态表名插件
*/
@Bean
@Order(2)
public IInterceptor dynamicTableNameInterceptor() {
return new DynamicTableNameInterceptor(new TableNameHandler() {
@Override
public String dynamicTableName(String sql, String tableName) {
if ("test_log".equals(tableName)) {
return tableName + "_" + LocalDateTime.now().getYear();
}
return tableName;
}
});
}
  1. service层执行SQL时自动对SQL进行分页查询
    public List<Map<String, Object>> getTestLogList() {
return this.jdbcTemplate.queryForList("select * from test_log");
//最终执行SQL:select * from test_log_2023
}

自定义插件

示例:写一个打印SQL语句、执行参数、以及执行SQL耗时的监控插件。

  1. 编写MyStatInterceptor插件
/**
* SQL监控插件
*/
@Slf4j
public class MyStatInterceptor implements IInterceptor {
/**
* 自定义插件是否支持
*/
@Override
public boolean supportMethod(final MethodInvocationInfo methodInfo) {
return IInterceptor.super.supportMethod(methodInfo);
} /**
* SQL执行前方法(主要用于对SQL进行修改)
*/
@Override
public void beforePrepare(final MethodInvocationInfo methodInfo, JdbcTemplate jdbcTemplate) {
log.info("执行SQL开始时间:{}", LocalDateTime.now());
log.info("原始SQL:{}", Arrays.toString(methodInfo.getBatchSql()));
log.info("调用方法名称:{}", methodInfo.getName());
log.info("调用方法入参:{}", Arrays.toString(methodInfo.getArgs())); methodInfo.putUserAttribute("startTime", LocalDateTime.now());
} /**
* SQL执行完成后方法(主要用于对返回值修改)
*
* @param result 原始返回对象
* @return 处理后的返回对象
*/
@Override
public Object beforeFinish(Object result, final MethodInvocationInfo methodInfo, JdbcTemplate jdbcTemplate) {
log.info("执行SQL结束时间:{}", LocalDateTime.now());
LocalDateTime startTime = (LocalDateTime) methodInfo.getUserAttribute("startTime");
log.info("执行SQL耗时:{}毫秒", Duration.between(startTime, LocalDateTime.now()).toMillis());
return result;
}
}
  1. 注入自定义插件
    /**
* 自定义插件注入,注入位置按实际情况
*/
@Bean
@Order(0)
public IInterceptor myStatInterceptor() {
return new MyStatInterceptor();
}
  1. 查看效果(查看打印日志)
c.g.d.j.p.s.config.MyStatInterceptor     : 原始SQL:select * from test_user
c.g.d.j.p.s.config.MyStatInterceptor : 入参:[select * from test_user]
c.g.d.j.p.s.config.MyStatInterceptor : 执行SQL开始时间:2023-04-23T16:35:58.151
c.g.d.j.p.s.config.MyStatInterceptor : 执行SQL结束时间:2023-04-23T16:35:58.655
c.g.d.j.p.s.config.MyStatInterceptor : 执行SQL耗时:503毫秒

JdbcTemplate支持

  • 所有batchUpdate、update、query、queryForList、queryForMap、queryForObject、queryForRowSet、queryForStream都支持(但是方法第一个入参必须是sql语句的才支持)
  • 所有query开头的方法都支持分页插件(但是只有返回类型是List<?>的才支持查询汇总信息)
  • 如果有其他方法需要支持的,可以联系作者
  • 也可以手动调用MethodActionRegister.register(Class<JdbcTemplate> clazz, MethodActionInfo actionInfo, String name, Class<?>... parameterTypes)方法进行注册使jdbc-plus支持该方法

支持的方法明细(里面注释内容就是支持的JdbcTemlate方法):

    /**
* int[] batchUpdate(String sql, final BatchPreparedStatementSetter pss)
*/
BATCHUPDATE_SQL_PSS, /**
* int[][] batchUpdate(String sql, final Collection<T> batchArgs, final int batchSize,
* final ParameterizedPreparedStatementSetter<T> pss)
*/
BATCHUPDATE_SQL_BATCHARGS_BATCHSIZE_PSS, /**
* int[] batchUpdate(String sql, List<Object[]> batchArgs)
*/
BATCHUPDATE_SQL_BATCHARGS, /**
* int[] batchUpdate(String sql, List<Object[]> batchArgs, final int[] argTypes)
*/
BATCHUPDATE_SQL_BATCHARGS_ARGTYPES, /**
* int[] batchUpdate(final String... sql)
*/
BATCHUPDATE_SQL, /**
* int update(final String sql)
*/
UPDATE_SQL, /**
* int update(String sql, @Nullable Object... args)
*/
UPDATE_SQL_ARGS, /**
* update(String sql, Object[] args, int[] argTypes)
*/
UPDATE_SQL_ARGS_ARGTYPES, /**
* int update(String sql, @Nullable PreparedStatementSetter pss)
*/
UPDATE_SQL_PSS, /**
* T query(String sql, Object[] args, int[] argTypes, ResultSetExtractor<T> rse)
*/
QUERY_SQL_ARGS_ARGTYPES_RSE, /**
* void query(String sql, Object[] args, int[] argTypes, RowCallbackHandler rch)
*/
QUERY_SQL_ARGS_ARGTYPES_RCH, /**
* List<T> query(String sql, Object[] args, int[] argTypes, RowMapper<T> rowMapper)
*/
QUERY_SQL_ARGS_ARGTYPES_ROWMAPPER, /**
* T query(String sql, @Nullable Object[] args, ResultSetExtractor<T> rse)
*/
QUERY_SQL_ARGS_RSE, /**
* void query(String sql, @Nullable Object[] args, RowCallbackHandler rch)
*/
QUERY_SQL_ARGS_RCH, /**
* List<T> query(String sql, @Nullable Object[] args, RowMapper<T> rowMapper)
*/
QUERY_SQL_ARGS_ROWMAPPER, /**
* T query(String sql, @Nullable PreparedStatementSetter pss, ResultSetExtractor<T> rse)
*/
QUERY_SQL_PSS_RSE, /**
* void query(String sql, @Nullable PreparedStatementSetter pss, RowCallbackHandler rch)
*/
QUERY_SQL_PSS_RCH, /**
* List<T> query(String sql, @Nullable PreparedStatementSetter pss, RowMapper<T> rowMapper)
*/
QUERY_SQL_PSS_ROWMAPPER, /**
* T query(final String sql, final ResultSetExtractor<T> rse)
*/
QUERY_SQL_RSE, /**
* T query(String sql, ResultSetExtractor<T> rse, @Nullable Object... args)
*/
QUERY_SQL_RSE_ARGS, /**
* void query(String sql, RowCallbackHandler rch)
*/
QUERY_SQL_RCH, /**
* void query(String sql, RowCallbackHandler rch, @Nullable Object... args)
*/
QUERY_SQL_RCH_ARGS, /**
* List<T> query(String sql, RowMapper<T> rowMapper)
*/
QUERY_SQL_ROWMAPPER, /**
* List<T> query(String sql, RowMapper<T> rowMapper, @Nullable Object... args)
*/
QUERY_SQL_ROWMAPPER_ARGS, /**
* List<Map<String, Object>> queryForList(String sql)
*/
QUERYFORLIST_SQL, /**
* List<T> queryForList(String sql, Class<T> elementType)
*/
QUERYFORLIST_SQL_ELEMENTTYPE, /**
* List<T> queryForList(String sql, Class<T> elementType, @Nullable Object... args)
*/
QUERYFORLIST_SQL_ELEMENTTYPE_ARGS, /**
* List<Map<String, Object>> queryForList(String sql, @Nullable Object... args)
*/
QUERYFORLIST_SQL_ARGS, /**
* List<T> queryForList(String sql, @Nullable Object[] args, Class<T> elementType)
*/
QUERYFORLIST_SQL_ARGS_ELEMENTTYPE, /**
* List<Map<String, Object>> queryForList(String sql, Object[] args, int[] argTypes)
*/
QUERYFORLIST_SQL_ARGS_ARGTYPES, /**
* List<T> queryForList(String sql, Object[] args, int[] argTypes, Class<T> elementType)
*/
QUERYFORLIST_SQL_ARGS_ARGTYPES_ELEMENTTYPE, /**
* Map<String, Object> queryForMap(String sql)
*/
QUERYFORMAP_SQL, /**
* Map<String, Object> queryForMap(String sql, @Nullable Object... args)
*/
QUERYFORMAP_SQL_ARGS, /**
* Map<String, Object> queryForMap(String sql, Object[] args, int[] argTypes)
*/
QUERYFORMAP_SQL_ARGS_ARGTYPES, /**
* T queryForObject(String sql, Class<T> requiredType)
*/
QUERYFOROBJECT_SQL_REQUIREDTYPE, /**
* T queryForObject(String sql, Class<T> requiredType, @Nullable Object... args)
*/
QUERYFOROBJECT_SQL_REQUIREDTYPE_ARGS, /**
* T queryForObject(String sql, @Nullable Object[] args, Class<T> requiredType)
*/
QUERYFOROBJECT_SQL_ARGS_REQUIREDTYPE, /**
* T queryForObject(String sql, Object[] args, int[] argTypes, Class<T> requiredType)
*/
QUERYFOROBJECT_SQL_ARGS_ARGTYPES_REQUIREDTYPE, /**
* T queryForObject(String sql, Object[] args, int[] argTypes, RowMapper<T> rowMapper)
*/
QUERYFOROBJECT_SQL_ARGS_ARGTYPES_ROWMAPPER, /**
* T queryForObject(String sql, @Nullable Object[] args, RowMapper<T> rowMapper)
*/
QUERYFOROBJECT_SQL_ARGS_ROWMAPPER, /**
* T queryForObject(String sql, RowMapper<T> rowMapper)
*/
QUERYFOROBJECT_SQL_ROWMAPPER, /**
* T queryForObject(String sql, RowMapper<T> rowMapper, @Nullable Object... args)
*/
QUERYFOROBJECT_SQL_ROWMAPPER_ARGS, /**
* SqlRowSet queryForRowSet(String sql)
*/
QUERYFORROWSET_SQL, /**
* SqlRowSet queryForRowSet(String sql, @Nullable Object... args)
*/
QUERYFORROWSET_SQL_ARGS, /**
* SqlRowSet queryForRowSet(String sql, Object[] args, int[] argTypes)
*/
QUERYFORROWSET_SQL_ARGS_ARGTYPES, /**
* Stream<T> queryForStream(String sql, @Nullable PreparedStatementSetter pss, RowMapper<T> rowMapper)
*/
QUERYFORSTREAM_SQL_PSS_ROWMAPPER, /**
* Stream<T> queryForStream(String sql, RowMapper<T> rowMapper)
*/
QUERYFORSTREAM_SQL_ROWMAPPER, /**
* Stream<T> queryForStream(String sql, RowMapper<T> rowMapper, @Nullable Object... args)
*/
QUERYFORSTREAM_SQL_ROWMAPPER_ARGS,

联系我

方式一:GitHub上提Issue

方式二:发送邮件(GitHub上有邮箱地址)

鸣谢

欢迎各路好汉一起来参与完善 jdbc-plus,感兴趣的可以在github点个 ,有任何问题和建议欢迎提交 Issue !

感谢以下开源项目:

  • mybatis-plus

  • Mybatis-PageHelper

jdbc-plus是一款基于JdbcTemplate增强工具包,基于JdbcTemplate已实现分页、多租户、动态表名等插件,可与mybatis、mybatis-plus等混合使用的更多相关文章

  1. KC705E 增强版 基于FMC接口的Xilinx Kintex-7 FPGA K7 XC7K325T PCIeX8 接口卡

    KC705E 增强版 基于FMC接口的Xilinx Kintex-7 FPGA K7 XC7K325T PCIeX8 接口卡 一.板卡概述 本板卡基于Xilinx公司的FPGAXC7K325T-2FF ...

  2. 270-VC709E 增强版 基于FMC接口的Xilinx Vertex-7 FPGA V7 XC7VX690T PCIeX8 接口卡

    VC709E 增强版 基于FMC接口的Xilinx Vertex-7 FPGA V7 XC7VX690T PCIeX8 接口卡 一.板卡概述       本板卡基于Xilinx公司的FPGA XC7V ...

  3. 提高工作效率的神器:基于前端表格实现Chrome Excel扩展插件

    Chrome插件,官方名称extensions(扩展程序):为了方便理解,以下都称为插件. 我们开发的插件需要在浏览器里面运行,打开浏览器,通过右上角的三个点(自定义及控制)-更多工具-拓展程序-打开 ...

  4. ExtJS4.2学习(13)基于表格的扩展插件---rowEditing

    鸣谢:http://www.shuyangyang.com.cn/jishuliangongfang/qianduanjishu/2013-11-24/182.html --------------- ...

  5. 基于 HtmlHelper 的自定义扩展Container

    基于 HtmlHelper 的自定义扩展Container Intro 基于 asp.net mvc 的权限控制系统的一部分,适用于对UI层数据呈现的控制,基于 HtmlHelper 的扩展组件 Co ...

  6. Spring学习之旅(七)基于XML配置与基于AspectJ注解配置的AOP编程比较

    本篇博文用一个稍复杂点的案例来对比一下基于XML配置与基于AspectJ注解配置的AOP编程的不同. 相关引入包等Spring  AOP编程准备,请参考小编的其他博文,这里不再赘述. 案例要求: 写一 ...

  7. 基于AOP的插件化(扩展)方案

    在项目迭代开发中经常会遇到对已有功能的改造需求,尽管我们可能已经预留了扩展点,并且尝试通过接口或扩展类完成此类任务.可是,仍然有很多难以预料的场景无法通过上述方式解决.修改原有代码当然能够做到,但是这 ...

  8. 推荐一款VS2008代码增强插件——MetalScroll

    时光如水,岁月如歌.虽然现在已经是2013年底马上就要步入2014了,但还是有很多人在使用VS2008开发项目,今天要推荐一款VS2008(同时支持VS2005,但不支持VS2010)代码增强插件给仍 ...

  9. Cmder | 一款命令行增强工具

    文章目录 什么是cmder 安装cmder 让cmder便于使用 将cmder添加到右键菜单中 在设置中添加语言环境 设置默认使用cmd.PowerShell还是bash 调节背景的透明度 添加 ll ...

  10. KC705E增强版基于FMC接口的 Kintex-7 XC7K325T PCIeX8 接口卡

    一.板卡概述 本板卡基于Xilinx公司的FPGAXC7K325T-2FFG900 芯片,pin_to_pin兼容FPGAXC7K410T-2FFG900 ,支持PCIeX8.64bit DDR3容量 ...

随机推荐

  1. 最新版本 Stable Diffusion 开源 AI 绘画工具之中文自动提词篇

    目录 标签生成器 提示词自动补全 标签生成器 由于输入正向提示词 prompt 和反向提示词 negative prompt 都是使用英文,所以对学习母语的我们非常不友好 使用网址:https://t ...

  2. VUE3企业级项目基础框架搭建流程(1)

    开发环境和技术栈 操作系统 windows11 开发工具 vscode.phpstudy(小皮):nginx1.15.11, mysql5.7.26, php7.4,Navicat for MySQL ...

  3. 从原理聊JVM(二):从串行收集器到分区收集开创者G1

    作者:京东科技 康志兴 1 前言 随着Java的进化过程,涌现出各种不同的垃圾回收器,从串行执行到并行执行,从高吞吐到低延迟,终极目标就是让开发人员专注于程序的代码书写而无需关注内存管理. JDK早期 ...

  4. SPSS计算极值、平均值、中位数、方差、偏度、峰度、变异系数

      本文介绍基于SPSS软件的经典统计学分析与偏度.峰度等常用统计学指标的计算方法.   首先需要说明,本文所述数据的经典统计学分析,包括计算数据的极值.平均值.中位数.标准差.方差.变异系数.偏度与 ...

  5. 2023-03-25:若两个正整数的和为素数,则这两个正整数称之为“素数伴侣“。 给定N(偶数)个正整数中挑选出若干对,组成“素数伴侣“, 例如有4个正整数:2,5,6,13, 如果将5和6分为一组的

    2023-03-25:若两个正整数的和为素数,则这两个正整数称之为"素数伴侣". 给定N(偶数)个正整数中挑选出若干对,组成"素数伴侣", 例如有4个正整数:2 ...

  6. 2021-05-06:给定一个二维数组matrix, 你可以从任何位置出发,走向上下左右四个方向 。返回能走出来的最长的递增链长度。

    2021-05-06:给定一个二维数组matrix, 你可以从任何位置出发,走向上下左右四个方向 .返回能走出来的最长的递增链长度. 福大大 答案2021-05-06: 自然智慧即可. 动态规划.二维 ...

  7. 2021-05-17:数组中所有数都异或起来的结果,叫做异或和。给定一个数组arr,可以任意切分成若干个不相交的子数组。其中一定存在一种最优方案,使得切出异或和为0的子数组最多。返回这个最多数量。

    2021-05-17:数组中所有数都异或起来的结果,叫做异或和.给定一个数组arr,可以任意切分成若干个不相交的子数组.其中一定存在一种最优方案,使得切出异或和为0的子数组最多.返回这个最多数量. 福 ...

  8. Module not found: Error: Can‘t resolve ‘js-cookie‘

    Module not found: Error: Can't resolve 'js-cookie' 原因:没有安装js-cookie 解决:npm install -save js-cookie

  9. drf——反序列化校验源码(了解)、断言、drf之请求和响应、视图之两个视图基类

    1.模块与包 # 模块与包 模块:一个py文件 被别的py文件导入使用,这个py文件称之为模块,运行的这个py文件称之为脚本文件 包:一个文件夹下有__init__.py # 模块与包的导入问题 '' ...

  10. Uncaught TypeError: imageStyle.getImageState is not a function

    这个错误也是遇得到哟,柑橘自己好无辜呀,我哪里错了,找了半天原来还是自己找的错误 看 import Circle from 'ol/geom/Circle'; feature.setStyle(new ...