Mybatis - 预编译

  • 一. 什么是预编译
    • 1.1 Mybatis中预编译的运用
    • 1.2 预编译的原理
      • 1.2.1 动态SQL的分类
      • 1.2.2 预编译的处理(占位符的替换)
      • 1.2.3 执行的时候如何替换参数(参数赋值)
    • 1.3 总结

一. 什么是预编译

首先我们来说下预编译的一个背景:我们知道一条SQL语句到达Mysql之后,Mysql并不是会马上执行它,而是需要经过几个阶段性的动作(细节的可以查看Mysql复习计划(一)- 字符集、文件系统和SQL执行流程):

  1. 缓存的检查。
  2. 解析器解析。
  3. 优化器解析。
  4. 执行器执行。

那么这几个阶段肯定是需要一定的时间的。而有时候我们一条SQL语句可能需要反复的执行,只不过里面的参数可能不一样,比如where子句中的条件。如果每次都需要经过上面的几个步骤,那么效率就会下降。因此为了解决这种问题,就出现了预编译。

预编译语句就是将这类语句中的值用占位符替代,可以视为将SQL语句模板化或者说参数化。一次编译、多次运行,省去了解析优化等过程。

1.1 Mybatis中预编译的运用

Mybatis中,默认是开启预编译的功能的,我们来测试一下(用的SpringBoot):

1.pom依赖:

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.1.3</version>
</dependency>
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.2.1</version>
</dependency>
<dependency><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId><version>3.4.6</version>
</dependency>

2.配置文件:application.yml文件。

server:port: 8080spring:datasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://IP地址:3306/数据库名称username: xxxpassword: xxx
# 别名,这样写Mapper配置文件的时候可以省略前缀
mybatis:type-aliases-package: com.application.bean

application.properties文件:

#druid数据库连接池
type=com.alibaba.druid.pool.DruidDataSource
#配置mapper
mybatis.mapper-locations=classpath:mapper/*.xml
#配置日志输出
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

3.Mapper.xml文件:
对应目录:

文件内容:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--关联UserMapper接口 ,下面的id要和接口中对应的方法名称一致,实现一对一的对应-->
<mapper namespace="com.application.mapper.UserMapper"><select id="getUserById" parameterType="String" resultType="user">select * from user where userName=#{userName}</select>
</mapper>

4.实体类:

public class User {private String userName;private int id;// get/set
}

5.mapper接口:

@Mapper
@Repository
public interface UserMapper {User getUserById(String userName);
}

6.Service类:

@Service
public class UserProcess {@Autowiredprivate UserMapper userMapper;public User getUser(String name){return userMapper.getUserById(name);}
}

7.Controller类:

@RestController
public class MyController {@Autowiredprivate UserProcess userProcess;@PostMapping("/hello")public User hello(){return userProcess.getUser("tom");}
}

8.运行后的日志输出:

当你看到了占位符 的时候,就说明预编译成功了,Mybatis就是将#{}这样的参数用占位符问号来代替。形成一个SQL模板。以达到预编译的目的。

1.2 预编译的原理

Mybatis中,对于SQL的处理解析动作在于XMLStatementBuilder.parseStatementNode这个类的函数中,我们来从这里为入口来看。我们先来看本文案例中的Mapper文件:

<mapper namespace="com.application.mapper.UserMapper"><select id="getUserById" parameterType="String" resultType="user">select * from user where userName=#{userName}</select>
</mapper>

1.2.1 动态SQL的分类

再看下源码:

public void parseStatementNode() {// 命名空间中的唯一标识,一般可以用方法名String id = context.getStringAttribute("id");// 对应的数据库标识String databaseId = context.getStringAttribute("databaseId");if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {return;}// 驱动程序每次批量返回的结果行数Integer fetchSize = context.getIntAttribute("fetchSize");// 等待数据库返回结果的超时时间Integer timeout = context.getIntAttribute("timeout");String parameterMap = context.getStringAttribute("parameterMap");// 该SQL中,传入的参数的完全限定名或者别名。本文是userString parameterType = context.getStringAttribute("parameterType");Class<?> parameterTypeClass = resolveClass(parameterType);// Map结果集String resultMap = context.getStringAttribute("resultMap");// 该SQL返回类型String resultType = context.getStringAttribute("resultType");String lang = context.getStringAttribute("lang");LanguageDriver langDriver = getLanguageDriver(lang);Class<?> resultTypeClass = resolveClass(resultType);// 结果集的相关配置,下文给出详细的点String resultSetType = context.getStringAttribute("resultSetType");// 语句类型的处理,下文给出详细的点StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);String nodeName = context.getNode().getNodeName();// 得到SQL命令的类型:对应着增删改查,有这么几种枚举值:UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH;SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));boolean isSelect = sqlCommandType == SqlCommandType.SELECT;// 设置为true的时候,只要语句被调用,就会让本地缓存和二级缓存被清空。默认值为false(针对select)boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);// true代表将本次查询的语句通过二级缓存缓存起来,针对select,为true。boolean useCache = context.getBooleanAttribute("useCache", isSelect);boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);// Include Fragments before parsingXMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);includeParser.applyIncludes(context.getNode());// Parse selectKey after includes and remove them.processSelectKeyNodes(id, parameterTypeClass, langDriver);// 生成SQL的地方。SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);// ...省略
}

resultSetType有哪些类型?

  • FORWARD_ONLY:结果集的游标只能向下滚动。
  • SCROLL_INSENSITIVE:结果集的游标可以上下移动,当数据库变化时,当前结果集不变。
  • SCROLL_SENSITIVE:返回可滚动的结果集,当数据库变化时,当前结果集同步改变。

statementType有哪些类型?

  • STATEMENT:普通语句。
  • PREPARED:预处理。
  • CALLABLE:存储过程。

那么本文只关注于预编译的处理过程。我们继续看上述函数中的关键代码:

SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

这段代码就是通过LanguageDriverSQL语句进行解析,返回的结果中,就可能包含动态SQL或者占位符。我们来看下这个函数的源码:LanguageDriver的默认实现是XMLLanguageDriver

public class XMLLanguageDriver implements LanguageDriver {// 接着到这里@Overridepublic SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {// 创建一个XMLScriptBuilder对象。并通过它来解析SQL脚本XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);return builder.parseScriptNode();}↓↓↓↓↓↓↓public SqlSource parseScriptNode() {MixedSqlNode rootSqlNode = parseDynamicTags(context);SqlSource sqlSource = null;// 这里就是重点了,判断这个SQL是否是动态的if (isDynamic) {sqlSource = new DynamicSqlSource(configuration, rootSqlNode);} else {sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);}return sqlSource;}
}

从上面源码可以得出,最终SQL构建起来,有两个分支:

  • 动态的:通过DynamicSqlSource来构建。
  • 非动态的:通过RawSqlSource来构建。

那么是否为动态的判断标准是啥呢?关键看下面这行代码:

MixedSqlNode rootSqlNode = parseDynamicTags(context);
↓↓↓↓↓↓↓
protected MixedSqlNode parseDynamicTags(XNode node) {List<SqlNode> contents = new ArrayList<SqlNode>();NodeList children = node.getNode().getChildNodes();for (int i = 0; i < children.getLength(); i++) {XNode child = node.newXNode(children.item(i));if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {String data = child.getStringBody("");// 将文本节点封装成TextSqlNode ,然后判断是否是动态的TextSqlNode textSqlNode = new TextSqlNode(data);if (textSqlNode.isDynamic()) {contents.add(textSqlNode);isDynamic = true;} else {contents.add(new StaticTextSqlNode(data));}} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628String nodeName = child.getNode().getNodeName();NodeHandler handler = nodeHandlerMap.get(nodeName);if (handler == null) {throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");}handler.handleNode(child, contents);isDynamic = true;}}return new MixedSqlNode(contents);
}
↓↓↓↓↓↓↓
textSqlNode.isDynamic()
↓↓↓↓↓↓↓
public class TextSqlNode implements SqlNode {public boolean isDynamic() {DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();GenericTokenParser parser = createParser(checker);parser.parse(text);return checker.isDynamic();}↓↓↓↓↓↓↓private GenericTokenParser createParser(TokenHandler handler) {return new GenericTokenParser("${", "}", handler);}
}

到这里为止,我们可以观察到,在判断这个SQL是否为动态的时候,底层会创建一个GenericTokenParser类型的解析器。而这个解析器呢则是一个以 ${ 为开始和以 } 为结尾的解析器。如果解析成功,说明该SQL中包含了${},那么该SQL就会被标记为动态SQL标签。

那么反之,我们回到这段代码中:

对于#{}这样的语句,就是一个非动态标签。总结下就是:

  • 如果SQL中的参数是用${}作为占位符的,那么该SQL属于动态SQL,封装为DynamicSqlSource
  • 否则其他的都是非动态SQL,封装为RawSqlSource

1.2.2 预编译的处理(占位符的替换)

那么我们来看下RawSqlSource的构造函数:

public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);Class<?> clazz = parameterType == null ? Object.class : parameterType;// SQL的解析sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<String, Object>());
}public class SqlSourceBuilder extends BaseBuilder {public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {// 主要的一个解析器ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);// 获取真实的可执行性的sql语句String sql = parser.parse(originalSql);// 包装成StaticSqlSource然后返回return new StaticSqlSource(configuration, sql, handler.getParameterMappings());}
}

这里我们可以看出来,对于非动态SQL,会生成一个以 #{ 为开头,} 为结尾的解析器。紧接着就会创建一个StaticSqlSource 类,在这里做个区分:

  • RawSqlSource : 存储的是只有 #{} 或者没有标签的纯文本SQL信息
  • DynamicSqlSource : 存储的是写有 ${} 或者具有动态SQL标签的SQL信息
  • StaticSqlSource : 是DynamicSqlSourceRawSqlSource解析为BoundSql的一个中间态对象类型。
  • BoundSql:用于生成我们最终执行的SQL语句,属性包括参数值、映射关系、以及SQL(带问号的)

接着我们看下解析器ParameterMappingTokenHandler 的处理,它是SqlSourceBuilder的一个静态内部类:

private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {@Overridepublic String handleToken(String content) {parameterMappings.add(buildParameterMapping(content));return "?";}
}

这段代码在哪里用到呢?就是在GenericTokenParser.parse()这段代码中执行到的:

public class GenericTokenParser {public String parse(String text) {// ...while (start > -1) {if (start > 0 && src[start - 1] == '\\') {// this open token is escaped. remove the backslash and continue.builder.append(src, offset, start - offset - 1).append(openToken);offset = start + openToken.length();} else {// ...if (end == -1) {// close token was not found.builder.append(src, start, src.length - start);offset = src.length;} else {// 就是找到了#{}的结束标识},然后将中间的内容替换成?builder.append(handler.handleToken(expression.toString()));offset = end + closeToken.length();}}start = text.indexOf(openToken, offset);}if (offset < src.length) {builder.append(src, offset, src.length - offset);}return builder.toString();}
}

这段代码执行完毕之后,我们发现最后返回了一个StaticSqlSource 对象。

1.2.3 执行的时候如何替换参数(参数赋值)

那么在执行SQL的时候,则会去根据BoundSql来完成参数的赋值等操作。我们来看下RawSqlSource.getBoundSql这个函数:

public class RawSqlSource implements SqlSource {private final SqlSource sqlSource;@Overridepublic BoundSql getBoundSql(Object parameterObject) {return sqlSource.getBoundSql(parameterObject);}
}

因为只有#{}SQL语句,在上文中可以看到最后会生成一个StaticSqlSource对象,而这个类中就重写了getBoundSql函数,里面主要构造了一个BoundSql对象。

public class StaticSqlSource implements SqlSource {@Overridepublic BoundSql getBoundSql(Object parameterObject) {return new BoundSql(configuration, sql, parameterMappings, parameterObject);}
}

Mybatis在执行SQL的时候,主要看的是SimpleExecutor.prepareStatement这个入口:

public class SimpleExecutor extends BaseExecutor {private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {Statement stmt;// 获取JDBK的数据库连接Connection connection = getConnection(statementLog);// 准备工作,初始化Statement连接stmt = handler.prepare(connection, transaction.getTimeout());// 使用ParameterHandler处理入参handler.parameterize(stmt);return stmt;}
}

我们主要关注入参的处理函数上:

public class PreparedStatementHandler extends BaseStatementHandler {@Overridepublic void parameterize(Statement statement) throws SQLException {parameterHandler.setParameters((PreparedStatement) statement);}
}
↓↓↓↓↓
public class DefaultParameterHandler implements ParameterHandler {@Overridepublic void setParameters(PreparedStatement ps) {ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());// 从boundSql中拿到我们传入的参数List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();if (parameterMappings != null) {// 循环处理每一个参数,需要应用到类型转换器for (int i = 0; i < parameterMappings.size(); i++) {ParameterMapping parameterMapping = parameterMappings.get(i);// mode属性有三种:IN, OUT, INOUT。如果参数为 OUT 或 INOUT,参数对象属性的真实值将会被改变if (parameterMapping.getMode() != ParameterMode.OUT) {Object value;String propertyName = parameterMapping.getProperty();if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional paramsvalue = boundSql.getAdditionalParameter(propertyName);} else if (parameterObject == null) {value = null;} // 如果类型处理器里面有这个类型,直接赋值即可。else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {value = parameterObject;} // 否则转化为元数据处理,通过反射来完成get/set赋值else {MetaObject metaObject = configuration.newMetaObject(parameterObject);value = metaObject.getValue(propertyName);}TypeHandler typeHandler = parameterMapping.getTypeHandler();JdbcType jdbcType = parameterMapping.getJdbcType();if (value == null && jdbcType == null) {jdbcType = configuration.getJdbcTypeForNull();}try {// 使用不同的类型处理器向jdbc中的PreparedStatement设置参数typeHandler.setParameter(ps, i + 1, value, jdbcType);} catch (TypeException e) {throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);} catch (SQLException e) {throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);}}}}}
}

前面一部分逻辑主要是对参数进行类型转换。最后则对SQL进行参数的赋值和替换。那么我们主要关注最后的赋值代码:

typeHandler.setParameter(ps, i + 1, value, jdbcType);
↓↓↓↓↓
public abstract class BaseTypeHandler<T> extends TypeReference<T> implements TypeHandler<T> {@Overridepublic void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {if (parameter == null) {if (jdbcType == null) {throw new TypeException("JDBC requires that the JdbcType must be specified for all nullable parameters.");}try {ps.setNull(i, jdbcType.TYPE_CODE);} catch (SQLException e) {throw new TypeException("Error setting null for parameter #" + i + " with JdbcType " + jdbcType + " . " +"Try setting a different JdbcType for this parameter or a different jdbcTypeForNull configuration property. " +"Cause: " + e, e);}} else {try {setNonNullParameter(ps, i, parameter, jdbcType);} catch (Exception e) {throw new TypeException("Error setting non null for parameter #" + i + " with JdbcType " + jdbcType + " . " +"Try setting a different JdbcType for this parameter or a different configuration property. " +"Cause: " + e, e);}}}
}

我们重点关注那些非null值的赋值,看下Mybatis是怎么替换SQL中的 的:

setNonNullParameter(ps, i, parameter, jdbcType);

这个函数是一个抽象函数,有很多具体的实现,对应的参数是什么类型的,就用对应类型的处理方式去完成,如图:

无论是哪种类型,本质上都是对 占位符进行值的替换操作。

1.3 总结

因为本篇文章主要是将Mybatis对于预处理的原理。因此整个Mybatis的机制或者是二级缓存、一级缓存的知识点不在本文内容范围中(准备再写几篇内容补上)。本文主要讲了:什么是预编译。Mybatis中对预编译的处理、如何生成模板SQL、如何进行值的替换。

因此这里做个小总结:

首先预编译对于Mybatis而言,相当于构建出了一条SQL的模板,将#{}对应的参数改为?而已。届时只需要更改参数的值即可,无需再对SQL进行语法解析等操作。


对于动态SQL的判断,就是在于是否包含${}占位符。如果包含了就通过DynamicSqlSource来解析。而这里则影响到SQL的解析:

  • DynamicSqlSource:解析包含${}的语句,其实也会解析#{}的语句。
  • RawSqlSource:解析只包含#{}的语句。

这两种类型到最后都会转化为StaticSqlSource,然后由他创建一个BoundSql对象。包括参数值、映射关系、以及转化好的SQL


最后是关于SQL的执行,即如何将真实的参数赋值到我们上面生成的模板SQL中。这部分逻辑发生在SQL的执行过程中,其入口SimpleExecutor.prepareStatement主要做了这么几件事。

  1. 根据我们上面生成的BoundSql对象。拿到我们传入的参数。
  2. 对每个参数进行解析,转化成对应的类型。
  3. 如果转化出的参数值为null,则直接赋值,否则,还要通过类型处理器来完成赋值操作。typeHandler.setParameter(ps, i + 1, value, jdbcType);
  4. 每种类型处理器,则会对对应的参数进行赋值。

备注,Mybatis支持的类型处理器可以看TypeHandlerRegistry这个类,相关处理器的注册在其构造函数中。这里贴出部分截图:

Mybatis - 预编译的运用和原理相关推荐

  1. mybatis 预编译

    MyBatis预编译 <select id="getUserById" resultMap="UserMap" parameterType="j ...

  2. mybatis以及预编译如何防止SQL注入

    SQL注入是一种代码注入技术,用于攻击数据驱动的应用,恶意的SQL语句被插入到执行的实体字段中(例如,为了转储数据库内容给攻击者).[摘自] SQL injection - Wikipedia SQL ...

  3. Javascript作用域原理---预编译

    问题的提出 首先看一个例子: var name = 'laruence'; function echo() { alert(name); var name = 'eve'; alert(name); ...

  4. mybatis深入理解(一)之 # 与 $ 区别以及 sql 预编译

    mybatis 中使用 sqlMap 进行 sql 查询时,经常需要动态传递参数,例如我们需要根据用户的姓名来筛选用户时,sql 如下: select * from user where name = ...

  5. .NET1.1中预编译ASP.NET页面实现原理浅析[1]自动预编译机制浅析

    .NET1.1中预编译ASP.NET页面实现原理浅析[1]自动预编译机制浅析 .NET1.1中预编译ASP.NET页面实现原理浅析[1]自动预编译机制浅析 作者:&;nbsp来自:网络 htt ...

  6. 深入剖析ASP.NET的编译原理之二:预编译(Precompilation)

    (转载)在本篇文章的第一部分:[原创]深入剖析ASP.NET的编译原理之一:动态编译(Dynamical Compilation),详细讨论了ASP.NET如何进行动态编译的,现在我们来谈谈另外一种重 ...

  7. JavaWeb篇之二------sql注入的原理和解决方法(预编译)

    引言 在上一篇最末,我展示了sql注入现象,接下来我们来探究sql注入的本质原理 Sql注入及解决方法 我们打个断点,debug调试一下(不清楚代码的可以看上一篇) 我们可以看到"泊进之介& ...

  8. JavaScript中函数作用域之精辟,函数原理的浅入深出,及程序执行预编译之通天编译???

    1.程序执行的前一刻会先将代码预编译一遍,如果有语法错误则直接终止程序运行 //预编译之通天编译 --> 在执行的前一刻,会把文件通天扫描一遍 /** //预编译 函数整体提升(即函数会放到程序 ...

  9. JavaScript作用域原理——预编译

    JavaScript是一种脚本语言, 它的执行过程, 是一种翻译执行的过程.并且JavaScript是有预编译过程的,在执行每一段脚本代码之前, 都会首先处理var关键字和function定义式(函数 ...

最新文章

  1. 简析正则表达式的使用
  2. python人工智能方向面试准备_高薪直通车丨人工智能+Python面试经验分享(西安**思数据)...
  3. Git复习(十)之常见报错和疑问
  4. Palo Alto Networks全球化安全堡垒理念 提升企业防御能力
  5. C++unique函数应用举例
  6. 网站改title的后果到底有多惨?
  7. matlab createtask,Matlab批量与createjob
  8. python redis 集群_python与java中使用redis集群
  9. Missing Push Notification Entitlement警告-----以及解决方法
  10. 云存储云计算选择开源还是商业版
  11. LimeSDR常见问题及解决方法
  12. 智慧海洋task04 利用数据进行建模并调参
  13. 模型的参数verbose用法详解
  14. iOS m3u8本地缓存播放(控制下载并发、暂停恢复)
  15. 外部数据的合规引入助力银行用户营销系统冷启动
  16. b站视频详情数据抓取,自动打包并发送到指定邮箱(单个或者群发)
  17. 北京国际学校IB考试均分稳得一匹,IB考试结果揭秘
  18. funny_upload
  19. 弱引用(WeakReference)初识
  20. 【立创EDA开源推荐】001期 | 基于航顺HK32F030R8串行Flash烧录器

热门文章

  1. 这样清理运行内存,你的iphone就不会卡了
  2. 酷炫的个人动态引导页
  3. 有哪些好用的pdf修改器?思路提供
  4. Java8 Lambda表达式 ArrayList排序
  5. Installshield环境变量的追加与删除设置
  6. 《kafka问答100例 -4》 如果我手动在zk中添加/brokers/topics/{TopicName}节点会怎么样?
  7. Salesforce练习Case
  8. SP8093 JZPGYZ - Sevenk Love Oimaster(广义SAM)
  9. MLeaksFinder :腾讯开源的 iOS 内存泄漏检测工具
  10. 沈坤:中国餐饮严重缺乏创新意识