彻底搞清mybatis plus实现原理 mybatis plus 原理
yuyutoo 2024-10-12 00:03 3 浏览 0 评论
讨论主题
主要想搞清楚几个问题
- mybatis plus是依赖了mybatis,他们之间的关系是什么?
- mybatis plus中“字段自动填充功能”实现源码,包括id自动生成的原理。
- mybatis plus 基本的增删改查为什么不用写sql?以及sql注入器的原理。
本篇文章需要理解mybaties的源码为基础,否则看本篇文章会吃力。mybaties源码分析可以看我上一篇文章,如下 彻底看懂springboot mybaties源码流程
mybaties plus是依赖了mybaties,他们之间的关系是什么?
mybatis plus是基于mybatis实现的,下面来具体看看他们直接的关系,以及是怎样依赖的?
首先看mybatis plus自动配置类中的核心方法sqlSessionFactory,如下:
MybatisPlusAutoConfiguration
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
// mybaties plus使用 MybatisSqlSessionFactoryBean ,mybaties使用SqlSessionFactoryBean
MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
factory.setDataSource(dataSource);
factory.setVfs(SpringBootVFS.class);
if (StringUtils.hasText(this.properties.getConfigLocation())) {
factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
}
applyConfiguration(factory);
if (this.properties.getConfigurationProperties() != null) {
factory.setConfigurationProperties(this.properties.getConfigurationProperties());
}
if (!ObjectUtils.isEmpty(this.interceptors)) {
factory.setPlugins(this.interceptors);
}
if (this.databaseIdProvider != null) {
factory.setDatabaseIdProvider(this.databaseIdProvider);
}
if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
}
if (this.properties.getTypeAliasesSuperType() != null) {
factory.setTypeAliasesSuperType(this.properties.getTypeAliasesSuperType());
}
if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
}
if (!ObjectUtils.isEmpty(this.typeHandlers)) {
factory.setTypeHandlers(this.typeHandlers);
}
if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
factory.setMapperLocations(this.properties.resolveMapperLocations());
}
this.getBeanThen(TransactionFactory.class, factory::setTransactionFactory);
Class<? extends LanguageDriver> defaultLanguageDriver = this.properties.getDefaultScriptingLanguageDriver();
if (!ObjectUtils.isEmpty(this.languageDrivers)) {
factory.setScriptingLanguageDrivers(this.languageDrivers);
}
Optional.ofNullable(defaultLanguageDriver).ifPresent(factory::setDefaultScriptingLanguageDriver);
// 提供了可定制MybatisSqlSessionFactoryBean的类,如果你想定制它,你可以自定定义SqlSessionFactoryBeanCustomizer类型的类。
applySqlSessionFactoryBeanCustomizers(factory);
// mybaties plus定义的全局的配置类,供后续方便使用。
GlobalConfig globalConfig = this.properties.getGlobalConfig();
// 从spring容器中获取定义的MetaObjectHandler类型的实例,并设置到globalConfig供后续使用,这个就是
// 字段自动填充功能的类。
this.getBeanThen(MetaObjectHandler.class, globalConfig::setMetaObjectHandler);
// 从spring容器中获取IKeyGenerator类型实例(主要是实现主键生成器),并设置到globalConfig供后续使用
this.getBeansThen(IKeyGenerator.class, i -> globalConfig.getDbConfig().setKeyGenerators(i));
// 从spring容器中获取ISqlInjector类型实例(Sql注入器),并设置到globalConfig供后续使用
this.getBeanThen(ISqlInjector.class, globalConfig::setSqlInjector);
// 从spring容器中获取IdentifierGenerator类型实例(主要是实现ID生成器),并设置到globalConfig供后续使用
this.getBeanThen(IdentifierGenerator.class, globalConfig::setIdentifierGenerator);
// 设置 GlobalConfig 到 MybatisSqlSessionFactoryBean
factory.setGlobalConfig(globalConfig);
return factory.getObject();
}
可以看到mybatis plus使用 MybatisSqlSessionFactoryBean (mybaties中使用的是SqlSessionFactoryBean),然后针对mybatis plus进行了一系列的初始化操作,并把相关的实例都设置到了GlobalConfig中,这块是mybatis plus扩展的初始化位置。
mybaties plus定制了mybaties很多核心类,总结如下:
mybaties plus | mybaties | 功能描述 |
MybatisSqlSessionFactoryBean | SqlSessionFactoryBean | 调用buildSqlSessionFactory创建SqlSessionFactory类 |
MybatisConfiguration | Configuration | 用于描述 MyBatis 主配置文件信息,MyBatis 框架在启动时自动配置类中,会加载mapper配置文件,将配置信息转换为 Configuration 对象,然后把该对象传入给sqlSessionFactory供后续使用 |
MybatisMapperAnnotationBuilder | MapperAnnotationBuilder | 解析Mapper方法中用注解方式定义的sql。 |
MybatisMapperRegistry | MapperRegistry | Mapper注册器,其实就是加入到一个内部数组中。 |
MybatisParameterHandler | DefaultParameterHandler | 用于处理 SQL 中的参数占位符,为参数占位符设置值 |
上面是mybaties plus扩展mybaties的核心类。
答疑时刻
mybatis plus扩展了mybatis的很多功能,添加了很多实用功能。比如最主要的基于对象的增删改查(不需要写sql),基于雪花算法的ID生成器,字段自动填充功能,逻辑删除功能等。
mybatis plus中“字段自动填充功能”实现源码,包括id自动生成的原理。
字段自动填充功能可以干什么?
在开发过程中表中经常会建创建时间,创建人,更新时间,更新人字段,正常自己维护这几个字段的时候,插入数据的时候需要自己给创建时间创建人赋值。更新数据的时候需要自己给更新时间,更新人字段赋值。
mybaties plus “字段自动填充功能”就是来解决这个问题的,可以在插入数据的时候指定要更新哪些字段,更新数据的时候指定更新哪些字段。用起来还是很方便的。
下面是使用例子:
public class User {
// 注意!这里需要标记为填充字段
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT)
private String createUserName;
@TableField(fill = FieldFill.UPDATE)
private LocalDateTime updateTime;
@TableField(fill = FieldFill.UPDATE)
private String updateUserName;
}
@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
log.info("start insert fill ....");
this.strictInsertFill(metaObject, "createTime", () -> LocalDateTime.now(), LocalDateTime.class);
this.strictInsertFill(metaObject, "createUserName", () -> "wanglining", String.class);
}
@Override
public void updateFill(MetaObject metaObject) {
log.info("start update fill ....");
this.strictUpdateFill(metaObject, "updateTime", () -> LocalDateTime.now(), LocalDateTime.class);
this.strictInsertFill(metaObject, "updateUserName", () -> "wanglining", String.class)
}
}
主要两步:
- 在对象上标记要填充的字段。
- 定义字段填充handler。
经过上面两个步骤,自动填充功能就完成了,当你调用mybaties plus的BaseMapper中提供的insert方法的时候,会给createTime,createUserName两个字段填充你在handler中指定的值。相应的调用BaseMapper提供的update方法的时候,会给updateTime,updateUserName两个字段填充你在handler中指定的值。
注意:一定得是调用BaseMapper中提供的方法,如果你自己再xml中定义sql语句是不会有作用的。
实现原理
下面开始讲解它的实现原理。
在上面提到mybaties plus定制了参数处理器(ParameterHandler),mybaties plus中的实现为MybatisParameterHandler,它的作用是“用于处理 SQL 中的参数占位符,为参数占位符设置值”, mybaties plus就是用在jdbc真正执行sql前,通过MetaObjectHandler给对象相应字段赋值。进而mybaties把对象解析到了sql中进行执行。
MybatisParameterHandler
private void process(Object parameter) {
if (parameter != null) {
TableInfo tableInfo = null;
Object entity = parameter;
if (parameter instanceof Map) {
// 处理单参数使用注解标记的时候,尝试提取et来获取实体参数
Map<?, ?> map = (Map<?, ?>) parameter;
if (map.containsKey(Constants.ENTITY)) {
Object et = map.get(Constants.ENTITY);
if (et != null) {
entity = et;
tableInfo = TableInfoHelper.getTableInfo(entity.getClass());
}
}
} else {
tableInfo = TableInfoHelper.getTableInfo(parameter.getClass());
}
if (tableInfo != null) {
//到这里就应该转换到实体参数对象了,因为填充和ID处理都是针对实体对象处理的,不用传递原参数对象下去.
MetaObject metaObject = this.configuration.newMetaObject(entity);
if (SqlCommandType.INSERT == this.sqlCommandType) {
// 这个方法实现了给主键ID赋值的,根据你指定的策略生成id值,并赋值给主键ID。
populateKeys(tableInfo, metaObject, entity);
// 这里会进一步调用上面定义的handler里面的方法
insertFill(metaObject, tableInfo);
} else {
// 这里会进一步调用上面定义的handler里面的方法
updateFill(metaObject, tableInfo);
}
}
}
}
protected void insertFill(MetaObject metaObject, TableInfo tableInfo) {
// 这里会先获取到你上面定义的MyMetaObjectHandler。
GlobalConfigUtils.getMetaObjectHandler(this.configuration).ifPresent(metaObjectHandler -> {
// 判断openInsertFill是否为true,模式的就是返回true
// isWithInsertFill判断是否为true,这个方法返回的就是TableInfo类中的withInsertFill属性
// TableInfo就是dao层对象以及字段注解解析出来的一个对应数据库表的对象,其中withInsertFill属性
// 就是根据类的字段上是否有@TableField(fill = FieldFill.INSERT)注解,如果有那么withInsertFill
// 最终会是true
if (metaObjectHandler.openInsertFill() && tableInfo.isWithInsertFill()) {
// 调用MyMetaObjectHandler的insertFill方法
metaObjectHandler.insertFill(metaObject);
}
});
}
// 这个方法就不写注释了,跟上面一样。
protected void updateFill(MetaObject metaObject, TableInfo tableInfo) {
GlobalConfigUtils.getMetaObjectHandler(this.configuration).ifPresent(metaObjectHandler -> {
if (metaObjectHandler.openUpdateFill() && tableInfo.isWithUpdateFill()) {
metaObjectHandler.updateFill(metaObject);
}
});
}
metaObjectHandler.insertFill会调用上面自己定义的MyMetaObjectHandler的insertFill方法,然后会继续调用strictInsertFill方法。
分析下strictInsertFill方法如下:
MetaObjectHandler
default MetaObjectHandler strictInsertFill(TableInfo tableInfo, MetaObject metaObject, List<StrictFill<?, ?>> strictFills) {
return strictFill(true, tableInfo, metaObject, strictFills);
}
/**
* 严格填充,只针对非主键的字段,只有该表注解了fill 并且 字段名和字段属性 能匹配到才会进行填充(null 值不填充)
*
* @param insertFill 是否验证在 insert 时填充
* @param tableInfo cache 缓存
* @param metaObject metaObject meta object parameter
* @param strictFills 填充信息
* @return this
* @since 3.3.0
*/
default MetaObjectHandler strictFill(boolean insertFill, TableInfo tableInfo, MetaObject metaObject, List<StrictFill<?, ?>> strictFills) {
// 先过滤表上withInsertFill或withUpdateFill属性是否为true,其实就是看dao层对应的类属性上是否有
// @TableField(fill = FieldFill.INSERT)或@TableField(fill = FieldFill.UPDATE)注解
if ((insertFill && tableInfo.isWithInsertFill()) || (!insertFill && tableInfo.isWithUpdateFill())) {
strictFills.forEach(i -> {
final String fieldName = i.getFieldName();
final Class<?> fieldType = i.getFieldType();
tableInfo.getFieldList().stream()
// 过滤对象字段,把字段上有@TableField(fill = FieldFill.INSERT)或@TableField(fill = FieldFill.UPDATE)注解
// 的字段过滤出来。
.filter(j -> j.getProperty().equals(fieldName) && fieldType.equals(j.getPropertyType()) &&
((insertFill && j.isWithInsertFill()) || (!insertFill && j.isWithUpdateFill()))).findFirst()
// 针对过滤出来的字段赋值。
.ifPresent(j -> strictFillStrategy(metaObject, fieldName, i.getFieldVal()));
});
}
return this;
}
上面分析完了MybatisParameterHandler处理字段自动填充流程的核心逻辑,是从它的方法process说起的,那这个方法又是怎么被触发的?怎么和mybaties执行流程对接上的?
下面是mybaties 查询方法完整的调用时序图
上面重点关注下MybatisParameterHandler类的方法,主要两步:
- 在执行器进行具体jdbc操作前,先初始化了StatementHandler,StatementHandler里面会创建MybatisParameterHandler,在MybatisParameterHandler的构造方法中最终会调用process方法,进而为对象进行字段填充操作。
- 在prepareStatement阶段,jdbc执行prepare操作完成后会返回一个Statement,StatementHandler会调用MybatisParameterHandler的setParameters方法,解析dao层对象属性,并把参数值设置到prepare返回的Statement中。
经过上面两步ParameterHandler的任务就完成了。
mybatis plus 基本的增删改查为什么不用写sql?以及sql注入器的原理。
mybatis plus通过定义sql注入器实现了此功能。核心类
DefaultSqlInjector
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {
Stream.Builder<AbstractMethod> builder = Stream.<AbstractMethod>builder()
// 注入BaseMapper的insert方法
.add(new Insert())
// 注入BaseMapper的delete方法
.add(new Delete())
.add(new DeleteByMap())
.add(new Update())
.add(new SelectByMap())
.add(new SelectCount())
.add(new SelectMaps())
.add(new SelectMapsPage())
.add(new SelectObjs())
// 注入BaseMapper的selectList方法
.add(new SelectList())
.add(new SelectPage());
if (tableInfo.havePK()) {
builder.add(new DeleteById())
.add(new DeleteBatchByIds())
.add(new UpdateById())
.add(new SelectById())
.add(new SelectBatchByIds());
} else {
logger.warn(String.format("%s ,Not found @TableId annotation, Cannot use Mybatis-Plus 'xxById' Method.",
tableInfo.getEntityType()));
}
return builder.build().collect(toList());
}
这里以insert举例看下Insert内部实现。
Insert
/**
* 插入一条数据(选择字段插入)
*
* @author hubin
* @since 2018-04-06
*/
public class Insert extends AbstractMethod {
public Insert() {
super(SqlMethod.INSERT_ONE.getMethod());
}
/**
* @param name 方法名
* @since 3.5.0
*/
public Insert(String name) {
super(name);
}
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
// INSERT_ONE("insert", "插入一条数据(选择字段插入)", "<script>\nINSERT INTO %s %s VALUES %s\n</script>")
// INSERT_ONE枚举里面定义了sql脚本,以及BaseMapper中对应方法名
SqlMethod sqlMethod = SqlMethod.INSERT_ONE;
// 通过dao类对应表信息,获取表中的列并且拼“接插入sql”的列部分
String columnScript = SqlScriptUtils.convertTrim(tableInfo.getAllInsertSqlColumnMaybeIf(null),
LEFT_BRACKET, RIGHT_BRACKET, null, COMMA);
// 通过dao类对应表信息,获取表中的列并且拼接“插入sql”的值部分
String valuesScript = SqlScriptUtils.convertTrim(tableInfo.getAllInsertSqlPropertyMaybeIf(null),
LEFT_BRACKET, RIGHT_BRACKET, null, COMMA);
String keyProperty = null;
String keyColumn = null;
// 表包含主键处理逻辑,如果不包含主键当普通字段处理
// 获取主键部分的属性和对应列信息
if (StringUtils.isNotBlank(tableInfo.getKeyProperty())) {
if (tableInfo.getIdType() == IdType.AUTO) {
/* 自增主键 */
keyGenerator = Jdbc3KeyGenerator.INSTANCE;
keyProperty = tableInfo.getKeyProperty();
keyColumn = tableInfo.getKeyColumn();
} else if (null != tableInfo.getKeySequence()) {
keyGenerator = TableInfoHelper.genKeyGenerator(this.methodName, tableInfo, builderAssistant);
keyProperty = tableInfo.getKeyProperty();
keyColumn = tableInfo.getKeyColumn();
}
}
// 通过上面获取的插入sql脚本,以及列和值部分,拼接成最终的sql脚本字符串。
String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), columnScript, valuesScript);
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
// 通过上面的信息生成对应的MapperStatement,并且注册到mybaties容器中。供后续真正调用BaseMapper对应方法
// 的时候再取出来使用。
return this.addInsertMappedStatement(mapperClass, modelClass, getMethod(sqlMethod), sqlSource, keyGenerator, keyProperty, keyColumn);
}
通过上面的代码,可以想象到mybaties plus初始化过程中肯定会调用到Insert的injectMappedStatement方法,把它对应的MapperStatement注入到mybaties容器中,然后后续通过动态代理调用BaseMapper相关方法的时候就可以根据MapperStatement对应的信息去执行对应的sql了。这块是mybaties的标准流程可以看前面文章 彻底看懂springboot mybaties源码流程
现在分析下injectMappedStatement方法是怎么被调用到的?
调用时序图如下:
这里关注下Insert的injectMappedStatement调用流程。
这里再重点分析MybatisMapperRegistry的addMapper代码,因为它的信息量比较大。如下:
MybatisMapperRegistry
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
if (hasMapper(type)) {
// TODO 如果之前注入 直接返回
return;
// TODO 这里就不抛异常了
// throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
// 这里也换成 MybatisMapperProxyFactory 而不是 MapperProxyFactory
// 对于mybaties plus用的MybatisMapperProxyFactory,mybaties用MapperProxyFactory
// 这句代码很重要,把动态代理工厂加入到了knownMappers中,这里的type是你定义的Mapper类。
// 下面的getMapper会通过MybatisMapperProxyFactory生成对应mapper的动态代理,进而执行mapper
// 的各个方法。
knownMappers.put(type, new MybatisMapperProxyFactory<>(type));
// It's important that the type is added before the parser is run
// otherwise the binding may automatically be attempted by the
// mapper parser. If the type is already known, it won't try.
// TODO 这里也换成 MybatisMapperAnnotationBuilder 而不是 MapperAnnotationBuilder
// 解析Mapper定义的方法中用注解方式定义的sql。这里最终会在你定义的Mapper类中添加mybaties plus
// 为你注入的Insert,Delete ,Update,Select等方法。其实就是把对应的MapperStatement注入到了Mybaties中。
MybatisMapperAnnotationBuilder parser = new MybatisMapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
@Override
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// TODO 这里换成 MybatisMapperProxyFactory 而不是 MapperProxyFactory
// fix https://github.com/baomidou/mybatis-plus/issues/4247
MybatisMapperProxyFactory<T> mapperProxyFactory = (MybatisMapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
// 上面addMapper会在knownMappers添加对应的Factory
mapperProxyFactory = (MybatisMapperProxyFactory<T>) knownMappers.entrySet().stream()
.filter(t -> t.getKey().getName().equals(type.getName())).findFirst().map(Map.Entry::getValue)
.orElseThrow(() -> new BindingException("Type " + type + " is not known to the MybatisPlusMapperRegistry."));
}
try {
// 生成mapper对应的动态代理对象
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
答疑时刻
mybatis plus定义了增删改查对应的sql注入器,在解析注册mapper的时候,会把相应的方法作为MapperStatement注入到mybaties中。看起来没有定义对应的xml文件,其实是mybaties plus用sql注入器的方式,在代码中默认都提供了对应的sql了。
后面当你调用Mapper对应方法的时候,会通过对应mapper的动态代理获取到方法对应的MapperStatement,进而再通过执行器进行一系列的处理,最终执行该方法对应的sql。
通过上面的原理分析,也可以注入自己的sql,比如批量插入方法,mybaties plus默认没有提供该方法,你可以自己定义一个BaseMapper然后扩展出这个批量插入的方法,下一篇文章再讲解吧。
相关推荐
- jQuery VS AngularJS 你更钟爱哪个?
-
在这一次的Web开发教程中,我会尽力解答有关于jQuery和AngularJS的两个非常常见的问题,即jQuery和AngularJS之间的区别是什么?也就是说jQueryVSAngularJS?...
- Jquery实时校验,指定长度的「负小数」,小数位未满末尾补0
-
在可以输入【负小数】的输入框获取到焦点时,移除千位分隔符,在输入数据时,实时校验输入内容是否正确,失去焦点后,添加千位分隔符格式化数字。同时小数位未满时末尾补0。HTML代码...
- 如何在pbootCMS前台调用自定义表单?pbootCMS自定义调用代码示例
-
要在pbootCMS前台调用自定义表单,您需要在后台创建表单并为其添加字段,然后在前台模板文件中添加相关代码,如提交按钮和表单验证代码。您还可以自定义表单数据的存储位置、添加文件上传字段、日期选择器、...
- 编程技巧:Jquery实时验证,指定长度的「负小数」
-
为了保障【负小数】的正确性,做成了通过Jquery,在用户端,实时验证指定长度的【负小数】的方法。HTML代码<inputtype="text"class="forc...
- 一篇文章带你用jquery mobile设计颜色拾取器
-
【一、项目背景】现实生活中,我们经常会遇到配色的问题,这个时候去百度一下RGB表。而RGB表只提供相对于的颜色的RGB值而没有可以验证的模块。我们可以通过jquerymobile去设计颜色的拾取器...
- 编程技巧:Jquery实时验证,指定长度的「正小数」
-
为了保障【正小数】的正确性,做成了通过Jquery,在用户端,实时验证指定长度的【正小数】的方法。HTML做成方法<inputtype="text"class="fo...
- jquery.validate检查数组全部验证
-
问题:html中有多个name[],每个参数都要进行验证是否为空,这个时候直接用required:true话,不能全部验证,只要这个数组中有一个有值就可以通过的。解决方法使用addmethod...
- Vue进阶(幺叁肆):npm查看包版本信息
-
第一种方式npmviewjqueryversions这种方式可以查看npm服务器上所有的...
- layui中使用lay-verify进行条件校验
-
一、layui的校验很简单,主要有以下步骤:1.在form表单内加上class="layui-form"2.在提交按钮上加上lay-submit3.在想要校验的标签,加上lay-...
- jQuery是什么?如何使用? jquery是什么功能组件
-
jQuery于2006年1月由JohnResig在BarCampNYC首次发布。它目前由TimmyWilson领导,并由一组开发人员维护。jQuery是一个JavaScript库,它简化了客户...
- django框架的表单form的理解和用法-9
-
表单呈现...
- jquery对上传文件的检测判断 jquery实现文件上传
-
总体思路:在前端使用jquery对上传文件做部分初步的判断,验证通过的文件利用ajaxFileUpload上传到服务器端,并将文件的存储路径保存到数据库。<asp:FileUploadI...
- Nodejs之MEAN栈开发(四)-- form验证及图片上传
-
这一节增加推荐图书的提交和删除功能,来学习node的form提交以及node的图片上传功能。开始之前需要源码同学可以先在git上fork:https://github.com/stoneniqiu/R...
- 大数据开发基础之JAVA jquery 大数据java实战
-
上一篇我们讲解了JAVAscript的基础知识、特点及基本语法以及组成及基本用途,本期就给大家带来了JAVAweb的第二个知识点jquery,大数据开发基础之JAVAjquery,这是本篇文章的主要...
- 推荐四个开源的jQuery可视化表单设计器
-
jquery开源在线表单拖拉设计器formBuilder(推荐)jQueryformBuilder是一个开源的WEB在线html表单设计器,开发人员可以通过拖拉实现一个可视化的表单。支持表单常用控件...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- mybatis plus (70)
- scheduledtask (71)
- css滚动条 (60)
- java学生成绩管理系统 (59)
- 结构体数组 (69)
- databasemetadata (64)
- javastatic (68)
- jsp实用教程 (53)
- fontawesome (57)
- widget开发 (57)
- vb net教程 (62)
- hibernate 教程 (63)
- case语句 (57)
- svn连接 (74)
- directoryindex (69)
- session timeout (58)
- textbox换行 (67)
- extension_dir (64)
- linearlayout (58)
- vba高级教程 (75)
- iframe用法 (58)
- sqlparameter (59)
- trim函数 (59)
- flex布局 (63)
- contextloaderlistener (56)