MyBatis3源码深度解析(二十二)MyBatis拦截器的原理及应用(一)拦截器的实现原理与执行过程

文章目录

  • 前言
  • 第九章 MyBatis拦截器的原理及应用
    • 9.1 拦截器的实现原理
      • 9.1.1 拦截器的注册
      • 9.1.2 自定义拦截器
      • 9.1.3 拦截器的实现原理
        • 9.1.3.1 拦截器支持的类和方法
        • 9.1.3.2 Interceptor
        • 9.1.3.3 Invocation
        • 9.1.3.4 Plugin
          • 9.1.3.4.1 getSignatureMap()
          • 9.1.3.4.2 getAllInterfaces()
          • 9.1.3.4.3 Plugin#invoke()
      • 9.1.4 拦截器的执行过程

前言

MyBatis框架支持用户通过自定义插件的方式改变SQL的执行行为,例如在SQL执行时追加SQL分页语法,从而达到简化分页查询的目的。用户自定义的插件也被称为MyBatis拦截器

本章研究MyBatis拦截器的实现原理及其应用。为方便阅读,本文均采用“拦截器”这一话语,而不使用“插件”这一话语。

第九章 MyBatis拦截器的原理及应用

9.1 拦截器的实现原理

9.1.1 拦截器的注册

在【MyBatis3源码深度解析(十二)MyBatis的核心组件(一)Configuration 4.3.6 插件】中已经知道,在MyBatis的配置文件中,拦截器(插件)使用<plugins>标签进行配置

在Configuration组件中,组合了一个拦截器链对象(InterceptorChain),用于存放通过<plugins>标签定义的拦截器。 调用Configuration对象的addInterceptor()方法就可以向InterceptorChain对象中注册一个拦截器:

源码1org.apache.ibatis.session.Configurationprotected final InterceptorChain interceptorChain = new InterceptorChain();public void addInterceptor(Interceptor interceptor) {interceptorChain.addInterceptor(interceptor);
}
源码2org.apache.ibatis.plugin.InterceptorChainpublic class InterceptorChain {// 内部维护了一个List集合,用于保存自定义的拦截器private final List<Interceptor> interceptors = new ArrayList<>();// 为全部拦截器创建代理对象public Object pluginAll(Object target) {for (Interceptor interceptor : interceptors) {target = interceptor.plugin(target);}return target;}// 往集合中添加一个新的拦截器public void addInterceptor(Interceptor interceptor) {interceptors.add(interceptor);}public List<Interceptor> getInterceptors() {return Collections.unmodifiableList(interceptors);}}

由 源码1-2 可知,InterceptorChain对象内部维护了一个List集合,用于保存自定义的拦截器,通过其addInterceptor()方法即可往集合中添加一个新的拦截器。

值得注意的是pluginAll()方法,该方法会遍历List集合中的所有拦截器,逐一调用其plugin()方法创建一个目标对象的动态代理对象。(分析到这里可能还无法理解,没关系,继续往下读)

在【MyBatis3源码深度解析(十四)SqlSession的创建与执行(一)Configuration与SqlSession的创建过程 5.2 Configuration实例创建过程】中已经知道,在XMLConfigBuilder类的parseConfiguration()方法中,会对MyBatis配置文件中的<plugins>标签进行解析。

源码3org.apache.ibatis.builder.xml.XMLConfigBuilderprivate void parseConfiguration(XNode root) {try {// ......// 处理<plugins>标签pluginsElement(root.evalNode("plugins"));// ......} // catch ......
}
源码4org.apache.ibatis.builder.xml.XMLConfigBuilderprivate void pluginsElement(XNode context) throws Exception {if (context != null) {// 遍历<plugins>标签的子标签<plugin>标签对应的XNode对象for (XNode child : context.getChildren()) {// 获取<plugin>标签的interceptor属性String interceptor = child.getStringAttribute("interceptor");// 获取拦截器属性,转换为Properties对象Properties properties = child.getChildrenAsProperties();// 创建拦截器实例Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();// 设置拦截器实例的属性interceptorInstance.setProperties(properties);// 将拦截器实例添加到Configuration对象的拦截器链中configuration.addInterceptor(interceptorInstance);}}
}

由 源码3-4 可知,在XMLConfigBuilder类的pluginsElement()方法中,会遍历<plugins>标签的子标签<plugin>所对应的XNode对象,逐一获取<plugin>标签的属性并转换为Properties对象,然后通过Java的反射机制实例化拦截器对象,设置完拦截器对象的属性后将其添加到Configuration对象的拦截器链中。

至此,拦截器的注册过程完成。

9.1.2 自定义拦截器

想要自定义一个MyBatis拦截器,只需要创建一个类实现Interceptor接口,重写Interceptor接口的方法,并标注@Intercepts注解配置拦截规则,最后在配置文件中的<plugins>标签中配置即可。

例如:

@Intercepts({@Signature(type= Executor.class,method = "update",args = {MappedStatement.class, Object.class})})
public class ExamplePlugin implements Interceptor {private Properties properties;@Overridepublic Object intercept(Invocation invocation) throws Throwable {System.out.println("在这里执行自定义的拦截逻辑...");return invocation.proceed();}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {this.properties = properties;}}
<!--mybatis-config.xml-->
<plugins><plugin interceptor="com.star.mybatis.plugin.ExamplePlugin"><property name="name" value="ExamplePlugin"/></plugin>
</plugins>

关于这个自定义拦截器的解释,可以在下文的分析中慢慢展开。

借助Debug工具,可以查看到Configuration对象创建完毕后,内部的InterceptorChain对象已经封装了自定义的拦截器ExamplePlugin:

9.1.3 拦截器的实现原理

9.1.3.1 拦截器支持的类和方法

在 MyBatis官方文档-插件 部分,由如下内容:

意思是,允许被拦截的类或接口只有4个:Executor、ParameterHandler、ResultSetHandler、StatementHandler,允许被拦截的方法是这4个类或接口中定义的方法。

可以利用IDE的查找功能来理解这个规定。利用IDEA的 Find Usages 功能,查找InterceptorChain类的pluginAll方法都在哪些地方被调用了,结果如下:

由图可知,只有4处地方调用,恰好是Configuration对象中的newExecutor()newParameterHandler()newResultSetHandler()newStatementHandler()方法。这就和MyBatis官方文档的说法对应起来了。

源码5org.apache.ibatis.session.Configurationpublic Executor newExecutor(Transaction transaction, ExecutorType executorType) {// ......// 创建Executor代理对象return (Executor) interceptorChain.pluginAll(executor);
}public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject,
BoundSql boundSql) {ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement,parameterObject, boundSql);// 创建ParameterHandler代理对象return (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
}public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds,
ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) {ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler,resultHandler, boundSql, rowBounds);// 创建ResultSetHandler代理对象return (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
}public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement,
Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject,rowBounds, resultHandler, boundSql);// 创建StatementHandler代理对象return (StatementHandler) interceptorChain.pluginAll(statementHandler);
}

由 源码5 可知,Configuration对象的这4个工厂方法,都调用了InterceptorChain对象的pluginAll()方法,创建对应的动态代理对象。有了动态代理对象,就可以在执行目标方法时执行额外的逻辑。

9.1.3.2 Interceptor

在自定义的拦截器ExamplePlugin中,实现了Interceptor接口。该接口的定义如下:

源码6org.apache.ibatis.plugin.Interceptorpublic interface Interceptor {// 用于定义拦截器逻辑,在目标方法被调用时执行Object intercept(Invocation invocation) throws Throwable;// 用于创建代理对象default Object plugin(Object target) {return Plugin.wrap(target, this);}// 用于设置插件的属性值default void setProperties(Properties properties) {// NOP}}

由 源码6 可知,Interceptor接口中定义了3个方法,它们的作用如代码中的注释所示。需要注意的有以下两点:

(1)intercept()方法接收一个Invocation对象作为参数,该对象封装了目标对象的方法及参数信息;
(2)plugin()方法中使用Plugin类的wrap()方法创建动态代理对象。

9.1.3.3 Invocation
源码7org.apache.ibatis.plugin.Invocationpublic class Invocation {// 目标对象,即要拦截的类private final Object target;// 目标方法,即要拦截的方法private final Method method;// 目标方法对应的参数private final Object[] args;public Invocation(Object target, Method method, Object[] args) {this.target = target;this.method = method;this.args = args;}// getter ......// 执行目标方法public Object proceed() throws InvocationTargetException, IllegalAccessException {return method.invoke(target, args);}}

由 源码7 可知,Invocation类中封装了目标对象、目标方法以及参数信息,还提供了一个proceed()方法,用于执行目标方法。

因此,在自定义的拦截器中,执行完拦截逻辑后,一般都要调用invocation.proceed()执行目标方法的原有逻辑。

9.1.3.4 Plugin

MyBatis提供了一个Plugin工具类,用于创建动态代理对象。

源码8org.apache.ibatis.plugin.Pluginpublic class Plugin implements InvocationHandler {// 目标对象private final Object target;// 自定义的拦截器实例private final Interceptor interceptor;// @Intercepts注解制定的参数private final Map<Class<?>, Set<Method>> signatureMap;public static Object wrap(Object target, Interceptor interceptor) {// 获取自定义插件中,通过@Intercepts注解制定的方法Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);// 获取目标对象的类型Class<?> type = target.getClass();// Class<?>[] interfaces = getAllInterfaces(type, signatureMap);if (interfaces.length > 0) {return Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap));}return target;}
}

由 源码8 可知,Plugin类实现了InvocationHandler接口,可见它是采用JDK内置的动态代理方式创建代理对象的。 它的wrap()方法的核心步骤有三步:

(1)调用getSignatureMap()方法获取自定义拦截器中通过@Intercepts注解指定的拦截类和方法等信息;
(2)调用getAllInterfaces()方法获取@Signature注解的type属性所指定的拦截类所实现的接口信息;
(3)调用Proxy类的newProxyInstance()方法创建一个动态代理对象。

9.1.3.4.1 getSignatureMap()

在解读getSignatureMap()方法之前,先研究一下@Intercepts注解和@Signature注解:

源码9org.apache.ibatis.plugin.Intercepts@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {Signature[] value();
}

由 源码9 可知,@Intercepts注解的value属性又指定了一个由@Signature注解组成的数组。

@Signature注解的定义如下:

源码10@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Signature {// 定义要拦截的类Class<?> type();// 定义要拦截的方法,即要拦截的类中的方法String method();// 定义要拦截的方法所对应的参数Class<?>[] args();
}

由 源码10 可知,@Signature注解包含3个属性,分别是:type属性用于定义要拦截的类或接口,即前面提到的4种;method属性定义要拦截的方法,即要拦截的类或接口中的方法;args属性用于定义要拦截的方法所对应的参数。

下面再来解读一下getSignatureMap()方法:

源码11org.apache.ibatis.plugin.Pluginprivate static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {// 获取自定义拦截器的@Intercepts注解Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);// @Intercepts注解为空时抛出异常if (interceptsAnnotation == null) {throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());}// 获取@Intercepts注解的值,即由@Signature注解组成的数组Signature[] sigs = interceptsAnnotation.value();Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();// 遍历@Signature注解组成的数组for (Signature sig : sigs) {Set<Method> methods = MapUtil.computeIfAbsent(signatureMap, sig.type(), k -> new HashSet<>());try {// 获取@Signature注解的method属性所应对的Method对象,并添加到Set集合中Method method = sig.type().getMethod(sig.method(), sig.args());methods.add(method);} catch (NoSuchMethodException e) {throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e,e);}}// 最终返回的Map对象,Key值是@Signature注解的type属性// Value值是一个Set<Method>集合,Method对象封装了@Signature注解的method属性与args属性return signatureMap;
}

由 源码11 可知,getSignatureMap()方法的解读如代码种的注释所示。该方法的作用是解析@Intercepts注解中的@Signature注解,将@Signature注解配置的要拦截的类、方法及参数信息提取出来,并保存到一个Map集合中。

最终返回的Map对象,Key值是@Signature注解的type属性,Value值是一个Set<Method>集合,Method对象封装了@Signature注解的method属性与args属性.

借助Debug工具,可以查看自定义的拦截器ExamplePlugin被解析时得到的信息:

9.1.3.4.2 getAllInterfaces()
源码12:org.apache.ibatis.plugin.Pluginprivate static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {Set<Class<?>> interfaces = new HashSet<>();// 从@Signature注解的type属性所指定的类开始// 一路向父级查找接口while (type != null) {// 遍历拦截类for (Class<?> c : type.getInterfaces()) {if (signatureMap.containsKey(c)) {interfaces.add(c);}}type = type.getSuperclass();}return interfaces.toArray(new Class<?>[0]);
}

由 源码12 可知,getAllInterfaces()方法会遍历拦截类实现的所有接口,如果该接口在从@Signature注解的type属性中配置了,则保存起来并返回。

借助Debug工具,可以查看自定义的拦截器ExamplePlugin被解析时执行getAllInterfaces()方法的结果:

以上信息收集完后,调用Proxy类的newProxyInstance()方法创建一个动态代理对象。

借助Debug工具,可以查看自定义的拦截器ExamplePlugin被解析时创建的动态代理对象:

9.1.3.4.3 Plugin#invoke()

Plugin类实现了InvocationHandler接口,因此它是采用JDK内置的动态代理方式创建代理对象的。那在执行代理对象的方法时,会调用Plugin类的invoke()方法。

源码13org.apache.ibatis.plugin.Plugin@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {try {// 根据当前执行的方法所在类,取出signatureMap集合中封装的拦截方法信息Set<Method> methods = signatureMap.get(method.getDeclaringClass());// 如果当前执行的方法,是注解配置好的拦截方法// 则直接调用拦截器的intercept()方法,以执行拦截器逻辑if (methods != null && methods.contains(method)) {return interceptor.intercept(new Invocation(target, method, args));}// 否则直接执行目标方法return method.invoke(target, args);} catch (Exception e) {throw ExceptionUtil.unwrapThrowable(e);}
}

由 源码13 可知,Plugin类的invoke()方法会判断当前要执行的目标方法,是否是被@Intercepts注解指定的要被拦截的方法,如果是,则调用自定义拦截器的intercept()方法,并把目标方法的信息封装为一个Invocation对象作为参数。如果不是,则直接执行目标方法。

9.1.4 拦截器的执行过程

经过以上分析,再来解释自定义拦截器ExamplePlugin就很清晰了:

  • intercept()方法用于编写自定义的拦截逻辑;
  • plugin()方法用于创建动态代理对象;
  • setProperties()方法用于设置自定义拦截器的属性;
  • @Signature注解的type属性指定要拦截的类或接口,即Executor;method属性指定要拦截的方法,即update()方法;args属性指定拦截方法的参数。

最后,以上文的案例为例,梳理一下拦截器的执行过程:

(1)SqlSession对象创建完毕后,调用Configuration对象的newExecutor()方法创建Executor对象。
(2)在newExecutor()方法中,会调用InterceptorChain对象的pluginAll()方法,该方法会调用自定义拦截器ExamplePlugin类的plugin()方法。
(3)在plugin()方法中,调用Plugin类的wrap()方法创建一个Executor的动态代理对象。
(4)根据JDK动态代理机制,在Executor执行动态代理对象的update()方法时,会执行Plugin类的invoke()方法。
(5)Plugin类的invoke()方法会调用自定义拦截器对象的intercept()方法执行拦截逻辑。
(6)在自定义拦截器对象的intercept()方法最后,会执行invocation.proceed()方法继续执行目标对象的方法,最终返回结果。

编写一个单元测试如下:

@Test
public void testPlugin() throws IOException, NoSuchMethodException {Reader reader = Resources.getResourceAsReader("mybatis-config.xml");SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);SqlSession sqlSession = sqlSessionFactory.openSession();UserMapper userMapper = sqlSession.getMapper(UserMapper.class);User user = new User();user.setId(1);user.setName("testPlugin");Long row = userMapper.updateById(user);System.out.println("row = " + row);
}

控制台打印执行结果:

在这里执行自定义的拦截逻辑...
row = 1

可见,自定义的拦截器确实生效了。

······

本节完,更多内容请查阅分类专栏:MyBatis3源码深度解析

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2024年大模型面试准备(四):大模型面试必会的位置编码(绝对位置编码sinusoidal,旋转位置编码RoPE,以及相对位置编码ALiBi)

节前&#xff0c;我们组织了一场算法岗技术&面试讨论会&#xff0c;邀请了一些互联网大厂朋友、参加社招和校招面试的同学&#xff0c;针对大模型技术趋势、大模型落地项目经验分享、新手如何入门算法岗、该如何备战、面试常考点分享等热门话题进行了深入的讨论。 合集在这…

生成可读取配置文件的独立运行jar程序

前言: 周五刚躺下,前线打来语音要个下载文件的小程序,下载路径和下载码需要根据配置获取,程序需要在服务器执行。当然配置的设计是个人设计的,不然每次更新下载码都要重新出具jar包,太麻烦。多年没写独立运行的jar包了,翻阅了相关资料,最终还是功夫不负有心人。想着这种…

鸿蒙 HarmonyOS应用开发之API:Context

Context 是应用中对象的上下文&#xff0c;其提供了应用的一些基础信息&#xff0c;例如resourceManager&#xff08;资源管理&#xff09;、applicationInfo&#xff08;当前应用信息&#xff09;、dir&#xff08;应用文件路径&#xff09;、area&#xff08;文件分区&#x…

uni-app攻略:如何对接驰腾打印机

一.引言 在当前的移动开发生态中&#xff0c;跨平台框架如uni-app因其高效、灵活的特点受到了开发者们的青睐。同时&#xff0c;随着物联网技术的飞速发展&#xff0c;智能打印设备已成为许多业务场景中不可或缺的一环。今天&#xff0c;我们就来探讨如何使用uni-app轻松对接驰…

阿赵UE学习笔记——22、动画合成

阿赵UE学习笔记目录 大家好&#xff0c;我是阿赵。   继续学习虚幻引擎的使用。这次来看看动画合成功能。   所谓的动画合成&#xff0c;意思就是把多段已经存在的动画拼接在一起&#xff0c;成为一段新的动画。比如之前做的钢铁侠例子里面&#xff0c;钢铁侠的待机动作感觉…

零基础机器学习(3)之机器学习的一般过程

文章目录 一、机器学习一般过程1.数据获取2.特征提取3.数据预处理①去除唯一属性②缺失值处理A. 均值插补法B. 同类均值插补法 ③重复值处理④异常值⑤数据定量化 4.数据标准化①min-max标准化&#xff08;归一化&#xff09;②z-score标准化&#xff08;规范化&#xff09; 5.…

AI入侵游戏业:是颠覆者还是创新助手?揭秘未来游戏新趋势!

在科技日新月异的今天&#xff0c;人工智能&#xff08;AI&#xff09;已经成为各行各业的关注焦点。而在娱乐产业中&#xff0c;AI技术的引入也让人们对电子游戏的未来发展产生了无限遐想。那么&#xff0c;AI究竟会给电子游戏行业带来怎样的变革&#xff1f;它会成为行业的颠…

【嵌入式学习】Qtday03.26

一、思维导图 二、练习 头文件 #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QTime> #include <QTextToSpeech>QT_BEGIN_NAMESPACE namespace Ui { class Widget; } QT_END_NAMESPACEclass Widget : public QWidget {Q_OBJECTpublic:Wi…

128天创作纪念日

分享的力量 hellohello~大家好✨✨我是大耳朵土土垚&#x1f973;&#x1f973;&#x1f973;今天是我创作128天的纪念日&#x1f389;&#x1f389;&#x1f389;&#xff0c;今天听到一句话——分享自己开心的事情就像有丝分裂一样会将快乐一直扩散&#x1f496;&#x1f496…

基于前端技术实现的全面预算编制系统

前言 在现代商业环境中&#xff0c;预测销售数据和实际成本是每个公司CEO和领导都极为重视的关键指标。然而&#xff0c;由于市场的不断变化&#xff0c;准确地预测和管理这些数据变得愈发具有挑战性。为了应对这一挑战&#xff0c;建立一个高效的系统来管理和审查销售数据的重…

机器人路径规划:基于斑翠鸟优化算法(Pied Kingfisher Optimizer ,PKO)的机器人路径规划(提供MATLAB代码)

一、机器人路径规划介绍 移动机器人&#xff08;Mobile robot&#xff0c;MR&#xff09;的路径规划是 移动机器人研究的重要分支之&#xff0c;是对其进行控制的基础。根据环境信息的已知程度不同&#xff0c;路径规划分为基于环境信息已知的全局路径规划和基于环境信息未知或…

【go从入门到精通】if else 条件控制

作者简介&#xff1a; 高科&#xff0c;先后在 IBM PlatformComputing从事网格计算&#xff0c;淘米网&#xff0c;网易从事游戏服务器开发&#xff0c;拥有丰富的C&#xff0c;go等语言开发经验&#xff0c;mysql&#xff0c;mongo&#xff0c;redis等数据库&#xff0c;设计模…

行业研究数据/报告网站 - 好用免费

前言 转眼进互联网 12 年了&#xff0c;先后在百度、汽车之家、某独角兽做业务和商业分析。与CEO、VP、业务owner等都对接过&#xff0c;1 个明显共性&#xff0c;就是大家都很关注外部行业数据&#xff0c;为决策提供参考。接下来&#xff0c;就和大家分享一下我收藏的 34 个…

迷宫(蓝桥杯)——DFS和BFS

迷宫 题目描述 下图给出了一个迷宫的平面图&#xff0c;其中标记为 1 的为障碍&#xff0c;标记为 0 的为可以通行的地方。 010000 000100 001001 110000迷宫的入口为左上角&#xff0c;出口为右下角&#xff0c;在迷宫中&#xff0c;只能从一个位置走到这 个它的上、下、左…

【数据结构】双向奔赴的爱恋 --- 双向链表

关注小庄 顿顿解馋๑ᵒᯅᵒ๑ 引言&#xff1a;上回我们讲解了单链表(单向不循环不带头链表)&#xff0c;我们可以发现他是存在一定缺陷的&#xff0c;比如尾删的时候需要遍历一遍链表&#xff0c;这会大大降低我们的性能&#xff0c;再比如对于链表中的一个结点我们是无法直接…

思通舆情 是一款开源免费的舆情系统 介绍

思通舆情 是一款开源免费的舆情系统。 支持本地化部署&#xff0c;支持在线体验。 支持对海量舆情数据分析和挖掘。 无论你是使用者还是共同完善的开发者&#xff0c;欢迎 pull request 或者 留言对我们提出建议。 您的支持和参与就是我们坚持开源的动力&#xff01;请 sta…

超越Sora!StreamingT2V AI视频模型,轻松打造120秒视觉盛宴

近日&#xff0c;来自美国德克萨斯大学奥斯汀分校&#xff08;UT奥斯丁&#xff09;等机构的研究人员提出了一项名为StreamingT2V的AI视频生成技术&#xff0c;引起了业界的广泛关注。这项技术打破了传统视频生成的局限&#xff0c;实现了高度一致且长度可扩展的视频生成&#…

C语言(结构体,联合体,枚举的讲解)

这期我们来讲解结构体&#xff0c;联合体&#xff0c;以及枚举的讲解&#xff0c;首先我们从概念开始一步一步的了解。 1&#xff0c;结构体 1.1概念 C 语言中的结构体是一种用户自定义的数据类型&#xff0c;它允许你将不同类型的变量组合在一起&#xff0c;从而形成一个新…

1978-2022年全国31省社会消费品零售总额数据

1978-2022年全国31省社会消费品零售总额数据 1、时间&#xff1a;1978-2022年 2、指标&#xff1a;社会消费品零售总额 3、范围&#xff1a;31省市 4、来源&#xff1a;整理自国家统计J和各省年鉴 5、缺失情况说明&#xff1a;1997-2022年31省市均无缺失&#xff0c; 199…

海外媒体发稿:9种高效的媒体套餐内容发稿策略分析-华媒舍

海外媒体发稿&#xff1a;9种高效的媒体套餐内容发稿策略分析高效的媒体发布和营销推广策略对公司、本人的成就尤为重要。下面我们就对于媒体套餐内容发稿营销推广策略开展全面解析&#xff0c;帮助读者掌握并应用这9种合理的思路&#xff0c;进而获得更好的媒体营销效果。 1.媒…