彻底搞清mybatis plus实现原理 mybatis plus 原理
yuyutoo 2024-10-12 00:03 7 浏览 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然后扩展出这个批量插入的方法,下一篇文章再讲解吧。
相关推荐
- 史上最全的浏览器兼容性问题和解决方案
-
微信ID:WEB_wysj(点击关注)◎◎◎◎◎◎◎◎◎一┳═┻︻▄(页底留言开放,欢迎来吐槽)●●●...
-
- 平面设计基础知识_平面设计基础知识实验收获与总结
-
CSS构造颜色,背景与图像1.使用span更好的控制文本中局部区域的文本:文本;2.使用display属性提供区块转变:display:inline(是内联的...
-
2025-02-21 16:01 yuyutoo
- 写作排版简单三步就行-工具篇_作文排版模板
-
和我们工作中日常word排版内部交流不同,这篇教程介绍的写作排版主要是用于“微信公众号、头条号”网络展示。写作展现的是我的思考,排版是让写作在网格上更好地展现。在写作上花费时间是有累积复利优势的,在排...
- 写一个2048的游戏_2048小游戏功能实现
-
1.创建HTML文件1.打开一个文本编辑器,例如Notepad++、SublimeText、VisualStudioCode等。2.将以下HTML代码复制并粘贴到文本编辑器中:html...
- 今天你穿“短袖”了吗?青岛最高23℃!接下来几天气温更刺激……
-
最近的天气暖和得让很多小伙伴们喊“热”!!! 昨天的气温到底升得有多高呢?你家有没有榜上有名?...
- CSS不规则卡片,纯CSS制作优惠券样式,CSS实现锯齿样式
-
之前也有写过CSS优惠券样式《CSS3径向渐变实现优惠券波浪造型》,这次再来温习一遍,并且将更为详细的讲解,从布局到具体样式说明,最后定义CSS变量,自定义主题颜色。布局...
- 你的自我界限够强大吗?_你的自我界限够强大吗英文
-
我的结果:A、该设立新的界限...
- 行内元素与块级元素,以及区别_行内元素和块级元素有什么区别?
-
行内元素与块级元素首先,CSS规范规定,每个元素都有display属性,确定该元素的类型,每个元素都有默认的display值,分别为块级(block)、行内(inline)。块级元素:(以下列举比较常...
-
- 让“成都速度”跑得潇潇洒洒,地上地下共享轨交繁华
-
去年的两会期间,习近平总书记在参加人大会议四川代表团审议时,对治蜀兴川提出了明确要求,指明了前行方向,并带来了“祝四川人民的生活越来越安逸”的美好祝福。又是一年...
-
2025-02-21 16:00 yuyutoo
- 今年国家综合性消防救援队伍计划招录消防员15000名
-
记者24日从应急管理部获悉,国家综合性消防救援队伍2023年消防员招录工作已正式启动。今年共计划招录消防员15000名,其中高校应届毕业生5000名、退役士兵5000名、社会青年5000名。本次招录的...
- 一起盘点最新 Chrome v133 的5大主流特性 ?
-
1.CSS的高级attr()方法CSSattr()函数是CSSLevel5中用于检索DOM元素的属性值并将其用于CSS属性值,类似于var()函数替换自定义属性值的方式。...
- 竞走团体世锦赛5月太仓举行 世界冠军杨家玉担任形象大使
-
style="text-align:center;"data-mce-style="text-align:...
- 学物理能做什么?_学物理能做什么 卢昌海
-
作者:曹则贤中国科学院物理研究所原标题:《物理学:ASourceofPowerforMan》在2006年中央电视台《对话》栏目的某期节目中,主持人问过我一个的问题:“学物理的人,如果日后不...
-
- 你不知道的关于这只眯眼兔的6个小秘密
-
在你们忙着给熊本君做表情包的时候,要知道,最先在网络上引起轰动的可是这只脸上只有两条缝的兔子——兔斯基。今年,它更是迎来了自己的10岁生日。①关于德艺双馨“老艺...
-
2025-02-21 16:00 yuyutoo
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- 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)