• Mybatis springmvc面试题


    $与#

    #有预编译

    mybatis找不到xml的问题。

        <!--maven打包时默认不会将srac/main.java下的xml打包,此步用于进行配置-->
        <resources>
          <resource>
            <directory>src/main/java</directory>
            <includes>
              <include>**/*.xml</include>
            </includes>
          </resource>
        </resources>

    maven约定大于配置,此时无法通过配置修改路径,只能手动添加扫描会将mapper放至resources下。

    https://blog.csdn.net/fanfanzk1314/article/details/71480954

    SPringMVC拦截器

         第一种方式是要定义的Interceptor类要实现了Spring的HandlerInterceptor 接口

         第二种方式是继承实现了HandlerInterceptor接口的类,比如Spring已经提供的实现了HandlerInterceptor接口的抽象类HandlerInterceptorAdapter

    HandlerInterceptor 接口中定义了三个方法,我们就是通过这三个方法来对用户的请求进行拦截处理的。

    preHandle(): 这个方法在业务处理器处理请求之前被调用,SpringMVC 中的Interceptor 是链式的调用的,在一个应用中或者说是在一个请求中可以同时存在多个Interceptor 。每个Interceptor 的调用会依据它的声明顺序依次执行,而且最先执行的都是Interceptor 中的preHandle 方法,所以可以在这个方法中进行一些前置初始化操作或者是对当前请求的一个预处理,也可以在这个方法中进行一些判断来决定请求是否要继续进行下去。该方法的返回值是布尔值Boolean 类型的,当它返回为false 时,表示请求结束,后续的Interceptor 和Controller 都不会再执行;当返回值为true 时就会继续调用下一个Interceptor 的preHandle 方法,如果已经是最后一个Interceptor 的时候就会是调用当前请求的Controller 方法。

         postHandle():这个方法在当前请求进行处理之后,也就是Controller 方法调用之后执行,但是它会在DispatcherServlet 进行视图返回渲染之前被调用,所以我们可以在这个方法中对Controller 处理之后的ModelAndView 对象进行操作。postHandle 方法被调用的方向跟preHandle 是相反的,也就是说先声明的Interceptor 的postHandle 方法反而会后执行。

         afterCompletion():该方法也是需要当前对应的Interceptor 的preHandle 方法的返回值为true 时才会执行。顾名思义,该方法将在整个请求结束之后,也就是在DispatcherServlet 渲染了对应的视图之后执行。这个方法的主要作用是用于进行资源清理工作的。

    在拦截器中实现跳转:添加response。sendredirect即可。

    spirngMVC参数封装

    Jquery发起ajax请求,提交checkbox时,必须使用

    traditional :true, //必须加上该句话来序列化

     来序列化获取的数组值。不然发给服务器的值会加上[],无法识别!!!!

    mysql jion问题

    当使用on后接多个条件与on加where的区别

    SELECT m.blogLabelId blogLabelId,m.blogId blogId,m.labelId labelId,l.labelName labelName,l.labelDescription labelDescription
    FROM blogLabelMap m LEFT JOIN blogLabel l
    ON m.labelId=l.labelId WHERE m.blogId=3

    where只会查出来符合要求的数据。

    SELECT m.blogLabelId blogLabelId,m.blogId blogId,m.labelId labelId,l.labelName labelName,l.labelDescription labelDescription
    FROM blogLabelMap m LEFT JOIN blogLabel l
    ON m.labelId=l.labelId AND m.blogId=3

    on加多条件会查出来左表所有内容,然后再查另一个条件符合要求的。

    on是一起查询出来,where是查询后筛选。

    mybatis 参数绑定问题

    https://blog.csdn.net/weixin_42325327/article/details/103208976

    文件上传处理

    bootstrap 文件上传插件+springmvc+mybatis+mysql。

    1、将文件保存至数据库

    前端:使用bootstrap的文件上传插件fileinput。class=file使用fileinput的样式。通过id与.fileinput绑定进行一些初始化显示样式的设置。文件上传完成后模拟点击移除按钮(带有

    fileinput-remove-button属性

    )来清除图片。清空input似乎没有效果。

    通过uploadUrl来配置上传到服务器的路径。如果显示了上传按钮,点击后会自动上传至该url.(如何通过js上传文件?)

    <div class="form-group">
       <label for="exampleFormControlFile">新头像</label>
       <input type="file" name="image" class="file" id="exampleFormControlFile"/>
       <p class="help-block">支持jpg、jpeg、png、gif格式,大小不超过10.0M</p>
    </div>

    JS:
    $("#exampleFormControlFile").fileinput({
    language: 'zh',
    uploadUrl: "user/header", //上传的地址
    allowedFileExtensions : ['jpg', 'png','gif','jpeg'],//接收的文件后缀
    showUpload: true, //是否显示上传按钮
    showCaption: true,//是否显示标题
    browseClass: "btn btn-primary", //按钮样式
    dropZoneEnabled: false,//是否显示拖拽区域
    maxImageWidth: 1000,//图片的最大宽度
    maxImageHeight: 1000,//图片的最大高度
    maxFileSize: 10240,//单位为kb,如果为0表示不限制文件大小
    maxFileCount: 1 //最大文件上传数目
    });

    $("#exampleFormControlFile").on("fileuploaded", function (event, data, previewId, index) {
    if(data.response == "success"){
    //模拟按钮点击清空
    $(".fileinput-remove-button").click();
    $('#headerChangeModal').modal('hide');
    alert("上传成功");
    }else{
    alert("上传失败");
    }
    });

    springMVC:

    发起post请求后,springMVC通过MultipartFile 类接受,如果变量名与前端 file 名不一致,需要使用@Requestparam绑定。(多文件上传)springmvc文件上传本质是通过

    CommonsMultipartResolver
    对request进行请求解析,拿到FileItem。封装进
    CommonsMultipartFile
    在进行各种文件操作。
        @PostMapping("/header")/*/*/
        @ResponseBody
        public String updateUserHeaderPic(HttpServletRequest request,HttpSession session, @RequestParam("image") MultipartFile upload){
    
            int i=0;
            UserHeadPic userHeadPic = new UserHeadPic();
            userHeadPic.setUserId((Integer) session.getAttribute("userId"));
            byte[] b = new byte[10240000];//文件最大10M
            userHeadPic.setUserHeaderPic(b);
            try {
                upload.getInputStream().read(userHeadPic.getUserHeaderPic());
            } catch (IOException e) {
                log.error("springmvc文件上传获取输入流出错");
                //e.printStackTrace();
            }
            i = userService.insertOrUpdateUserHeader(userHeadPic);
    
            if(i>=1){
                return new Gson().toJson("success") ;
            }else{
                return new Gson().toJson("fail") ;
            }
        }

    xml中配置文件上传处理器,并可以配置一些属性

        <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
            <property name="defaultEncoding" value="UTF-8"></property>
            <property name="maxUploadSize" value="10240000"></property>
        </bean>

    Mybatis保存与读取:

    将上传文件封装再一个类中。

    public class UserHeadPic {
    
        private Integer userId;
    
        private byte[] userHeaderPic;
    }

    xml文件配置如下:

    数据库中图片用blob保存,对象中为byte[],mybatis默认可以将blob映射为byte[],需要在配置resultMap时指明类型处理器

    typeHandler="org.apache.ibatis.type.BlobTypeHandler"  否则mybatis在查询时不能将blob映射到byte[].
    如果要同时执行多条语句。连接url需要添加
    &allowMultiQueries=true
        <resultMap id="BaseResultMap" type="com.jkblog.bean.UserHeadPic">
            <!--
              WARNING - @mbg.generated
              This element is automatically generated by MyBatis Generator, do not modify.
              This element was generated on Mon Nov 02 23:27:58 CST 2020.
            -->
            <id column="userId" jdbcType="INTEGER" property="userId" />
            <!--添加映射处理器,不然mybatis不会处理-->
            <result column="userHeaderPic" javaType="byte[]" jdbcType="BLOB" property="userHeaderPic"
                    typeHandler="org.apache.ibatis.type.BlobTypeHandler"/>
        </resultMap>
    <update id="insertOrUpdate" parameterType="com.jkblog.bean.UserHeadPic">
    delete from bloguserhead where userId=#{userId};
    insert into bloguserhead values(#{userId},#{userHeaderPic});
    </update>
        <select id="getUserHeadPicById" parameterType="int" resultMap="BaseResultMap">
            select * from bloguserhead where userId=#{userId}
        </select>

    直接掉用对象即可实现图片直接插入数据库及从数据库中读取至对象。

    img元素显示可以使用如下方式。

                        <img src="user/header/${requestScope.user.userId}" class="rounded card-img-top" alt="头像不见了"
                             style=" 80px;height: 80px" onclick="changeHeader()">

    直接向后台发送请求,后台返回response的输出流,将图片直接显示。

            UserHeadPic userHeadPicById = userHeaderMapper.getUserHeadPicById(userId);
            byte[] headerPic = userHeadPicById.getUserHeaderPic();
            /*写出到页面*/
            OutputStream out = null;
            try {
                out = response.getOutputStream();
                out.write(headerPic);
            } catch (IOException e) {
                e.printStackTrace();
            }finally {
                try {
                    out.flush();
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

    2、将文件保存至磁盘指定位置

    文件保存到硬盘:

    先配置映射的url及本地url

        /*虚拟路径的映射*/
        public static final String localUrl;
        public static final String mappedUrl;
    
        static {
            Properties properties = new Properties();
            InputStream inputStream = BlogController.class.getClassLoader().getResourceAsStream("path.properties");
            try {
                properties.load(inputStream);
            } catch (IOException e) {
                e.printStackTrace();
            }
            localUrl = properties.getProperty("localurl");
            mappedUrl = properties.getProperty("mappedurl");
        }

    通过MultipartFile将文件保存在本地而将映射的url地址返回。

        @Override
        public String uploadImages(MultipartFile[] files,Integer blogId) {
            ImageResult result = new ImageResult();
            List<String> data = new ArrayList<>();
            for(
                    MultipartFile file: files){
                if(file != null){
                    String originalName = file.getOriginalFilename();
                    String[] lists = originalName.split("\.");
    
                    //新名字
                    String newName = UUID.randomUUID()+"_"+System.currentTimeMillis()+"."+lists[lists.length-1];
                    log.debug("新名字为:"+newName);
    
                    /*本地磁盘路径*/
                    File localUrlAddr = new File(localUrl+"/"+lists[lists.length-1]);
                    if(!localUrlAddr.exists()){
                        localUrlAddr.mkdirs();
                    }
                    /*可以通过localhost:8080/fianlMappedUrl访问finalUrl路径下的文件*/
                    String finalUrl = localUrlAddr +"/"+newName;
                    String fianlMappedUrl = mappedUrl + "/"+lists[lists.length-1]+"/"+newName;
    
                    try {
                        file.transferTo(new File(finalUrl));
                    } catch (IOException e) {
                        log.error("文件保存出错");
                    }
    
                    data.add(fianlMappedUrl);
                }
            }

    前端wangEditor设置:

        const E = window.wangEditor
        const editor = new E('#div1')
        // 或者 const editor = new E( document.getElementById('div1') )
        editor.config.zIndex = 500
        editor.config.height = 650
        /*上传图片路径*/
        editor.config.uploadImgServer = 'blog/image'  /*/${requestScope.blog.blogId}*/
        /*图片最大值*/
        editor.config.uploadImgMaxLength = 5 // 一次最多上传 5 个图片
        editor.config.uploadImgMaxSize = 50 * 1024 * 1024 // 50M
        editor.config.uploadFileName = 'images'
        editor.config.uploadImgTimeout = 15 * 1000
        editor.create()

    mybatis缓存

    一级缓存

      Mybatis对缓存提供支持,但是在没有配置的默认情况下,它只开启一级缓存,一级缓存只是相对于同一个SqlSession而言。所以在参数和SQL完全一样的情况下,我们使用同一个SqlSession对象调用一个Mapper方法,往往只执行一次SQL,因为使用SelSession第一次查询后,MyBatis会将其放在缓存中,以后再查询的时候,如果没有声明需要刷新,并且缓存没有超时的情况下,SqlSession都会取出当前缓存的数据,而不会再次发送SQL到数据库。

    一级缓存的范围有SESSION和STATEMENT两种,默认是SESSION,如果不想使用一级缓存,可以把一级缓存的范围指定为STATEMENT,这样每次执行完一个Mapper中的语句后都会将一级缓存清除。
    如果需要更改一级缓存的范围,可以在Mybatis的配置文件中,在下通过localCacheScope指定。

     <setting name="localCacheScope" value="STATEMENT"/>

    需要注意的是
    当Mybatis整合Spring后,直接通过Spring注入Mapper的形式,如果不是在同一个事务中每个Mapper的每次查询操作都对应一个全新的SqlSession实例,这个时候就不会有一级缓存的命中,但是在同一个事务中时共用的是同一个SqlSession。
    如有需要可以启用二级缓存。

    二级缓存

    Mybatis的二级缓存是指mapper映射文件。二级缓存的作用域是同一个namespace下的mapper映射文件内容,多个SqlSession共享。Mybatis需要手动设置启动二级缓存。

    二级缓存是默认启用的(要生效需要对每个Mapper进行配置),如想取消,则可以通过Mybatis配置文件中的元素下的子元素来指定cacheEnabled为false。

    <settings>
      <setting name="cacheEnabled" value="false" />
    </settings>

    cacheEnabled默认是启用的,只有在该值为true的时候,底层使用的Executor才是支持二级缓存的CachingExecutor。具体可参考Mybatis的核心配置类org.apache.ibatis.session.Configuration的newExecutor方法实现。

    要使用二级缓存除了上面一个配置外,我们还需要在我们每个DAO对应的Mapper.xml文件中定义需要使用的cache

    ...
    <mapper namespace="...UserMapper">
        <cache/><!-- 加上该句即可,使用默认配置、还有另外一种方式,在后面写出 -->
        ...
    </mapper>

    还有一个条件就是需要当前的查询语句是配置了使用cache的,即上面源码的useCache()是返回true的,默认情况下所有select语句的useCache都是true,如果我们在启用了二级缓存后,有某个查询语句是我们不想缓存的,则可以通过指定其useCache为false来达到对应的效果。
    如果我们不想该语句缓存,可使用useCache=”false

    <select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.String" useCache="false">
            select
            <include refid="Base_Column_List"/>
            from tuser
            where id = #{id,jdbcType=VARCHAR}
        </select>

    打个比方我想在MenuMapper.xml中的查询都使用在UserMapper.xml中定义的Cache,则可以通过cache-ref元素的namespace属性指定需要引用的Cache所在的namespace,即UserMapper.xml中的定义的namespace,假设在UserMapper.xml中定义的namespace是cn.chenhaoxiang.dao.UserMapper,则在MenuMapper.xml的cache-ref应该定义如下。

    <cache-ref namespace="cn.chenhaoxiang.dao.UserMapper"/>

    这样这两个Mapper就共享同一个缓存了

    二级缓存的使用原则

      1. 只能在一个命名空间下使用二级缓存
        由于二级缓存中的数据是基于namespace的,即不同namespace中的数据互不干扰。在多个namespace中若均存在对同一个表的操作,那么这多个namespace中的数据可能就会出现不一致现象。
      2. 在单表上使用二级缓存
        如果一个表与其它表有关联关系,那么久非常有可能存在多个namespace对同一数据的操作。而不同namespace中的数据互补干扰,所以就有可能出现多个namespace中的数据不一致现象。
      3. 查询多于修改时使用二级缓存
        在查询操作远远多于增删改操作的情况下可以使用二级缓存。因为任何增删改操作都将刷新二级缓存,对二级缓存的频繁刷新将降低系统性能。

    总结:mybatis的的一级缓存是SqlSession级别的缓存,一级缓存缓存的是对象,当SqlSession提交、关闭以及其他的更新数据库的操作发生后,一级缓存就会清空。二级缓存是SqlSessionFactory级别的缓存,同一个SqlSessionFactory产生的SqlSession都共享一个二级缓存,二级缓存中存储的是数据,当命中二级缓存时,通过存储的数据构造对象返回。查询数据的时候,查询的流程是二级缓存>一级缓存>数据库

    一级缓存如何标识同一次查询?

    除去hashcode、checksum和count的比较外,只要updatelist中的元素一一对应相等,那么就可以认为是CacheKey相等。只要两条SQL的下列五个值相同,即可以认为是相同的SQL。

    Statement Id + Offset + Limmit + Sql + Params

    总结

    1. MyBatis一级缓存的生命周期和SqlSession一致。
    2. MyBatis一级缓存内部设计简单,只是一个没有容量限定的HashMap,在缓存的功能性上有所欠缺。
    3. MyBatis的一级缓存最大范围是SqlSession内部,有多个SqlSession或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为Statement。

    验证MyBatis的二级缓存不适应用于映射文件中存在多表查询的情况。

    通常我们会为每个单表创建单独的映射文件,由于MyBatis的二级缓存是基于namespace的,多表查询语句所在的namspace无法感应到其他namespace中的语句对多表查询中涉及的表进行的修改,引发脏数据问题。

    在这个实验中,我们引入了两张新的表,一张class,一张classroom。class中保存了班级的id和班级名,classroom中保存了班级id和学生id。我们在StudentMapper中增加了一个查询方法getStudentByIdWithClassInfo,用于查询学生所在的班级,涉及到多表查询。在ClassMapper中添加了updateClassName,根据班级id更新班级名的操作。

    sqlsession1studentmapper查询数据后,二级缓存生效。保存在StudentMapper的namespace下的cache中。当sqlSession3classMapperupdateClassName方法对class表进行更新时,updateClassName不属于StudentMappernamespace,所以StudentMapper下的cache没有感应到变化,没有刷新缓存。当StudentMapper中同样的查询再次发起时,从缓存中读取了脏数据。

    实验5

    为了解决实验4的问题呢,可以使用Cache ref,让ClassMapper引用StudenMapper命名空间,这样两个映射文件对应的SQL操作都使用的是同一块缓存了。

    https://tech.meituan.com/2018/01/19/mybatis-cache.html

    mybatis字符串拼接

    MyBatis 中拼接字符串有两种方式。

    1、 使用CONCAT 函数

    SELECT * FROM user WHERE name LIKE CONCAT(CONCAT('%', #{name}), '%')

    2、 使用${ } 代替 #{ }

    因为${ }直接传入SQL,而#{ }传入的是字符串带有引号

    SELECT * FROM user WHERE name LIKE '%${name}%'

    mybatis in语句查询

    如果参数的类型是List, 则在使用时,collection属性要必须指定为 list
    
    List<User> selectByIdSet(List idList);
     
    <select id="selectByIdSet" resultMap="BaseResultMap">
        SELECT
        <include refid="Base_Column_List" />
        from t_user
        WHERE id IN
        <foreach collection="list" item="id" index="index" open="(" close=")" separator=",">
          #{id}
        </foreach>

    如果参数的类型是Array,则在使用时,collection属性要必须指定为 array

    SELECT * FROM `WmRecommendpic` 
            WHERE type_id IN (
                SELECT DISTINCT type_id FROM `wm_relationship` 
                WHERE recommend_id IN (${recommendIds})
                AND is_valid=TRUE)
            AND is_valid=TRUE
            ORDER BY id ASC

    为啥能这样改?

    当参数采用:#{} : 解析为一个 JDBC 预编译语句(prepared statement)的参数标记符,一个 #{ } 被解析为一个参数占位符 。${}: 仅仅为一个纯碎的 string 替换,在动态 SQL 解析阶段将会进行变量替换。

    Spring事务管理

    Spring事务管理器的接口是org.springframework.transaction.PlatformTransactionManager,Spring框架并不直接管理事务,而是通过这个接口为不同的持久层框架提供了不同的PlatformTransactionManager接口实现类,也就是将事务管理的职责委托给Hibernate或者iBatis等持久化框架的事务来实现

    org.springframework.jdbc.datasource.DataSourceTransactionManager:使用JDBC或者iBatis进行持久化数据时使用
    org.springframework.orm.hibernate5.HibernateTransactionManager:使用hibernate5版本进行持久化数据时使用
    org.springframework.orm.jpa.JpaTransactionManager:使用JPA进行持久化数据时使用
    org.springframework.jdo.JdoTransactionManager:当持久化机制是jdo时使用
    org.springframework.transaction.jta.JtaTransactionManager:使用一个JTA实现来管理事务,在一个事务跨越多个资源时必须使用

    Spring 事务管理实现方式

    Spring 事务管理有两种方式:编程式事务管理声明式事务管理
    编程式事务管理通过TransactionTemplate手动管理事务,在实际应用中很少使用,我们来重点学习声明式事务管理
    声明式事务管理有三种实现方式:基于TransactionProxyFactoryBean的方式基于AspectJ的XML方式基于注解的方式

    总结

    在声明式事务管理的三种实现方式中,基于TransactionProxyFactoryBean的方式需要为每个进行事务管理的类配置一个TransactionProxyFactoryBean对象进行增强,所以开发中很少使用;基于AspectJ的XML方式一旦在XML文件中配置好后,不需要修改源代码,所以开发中经常使用;基于注解的方式开发较为简单,配置好后只需要在事务类上或方法上添加@Transaction注解即可,所以开发中也经常使用

    Mybatis拦截器

    https://blog.csdn.net/wuyuxing24/article/details/89343951

    一般拦截StatementHandler里面的prepare方法,实现自己的功能。

     

    如果想要对这四大对象进行操作可以用插件实现拦截操作,故插件就是拦截器

    它们都是sqlSession的底层类实现,也是插件能够拦截的四大对象。所以这里已经触及了MyBATIS的底层,动态代理,反射随时可以看到。了解他们的协作,是插件编写的基础之一,所以这是十分的重要。

    拦截器实现:https://blog.csdn.net/huyiju/article/details/82454735?utm_medium=distribute.pc_relevant.none-task-blog-title-2&spm=1001.2101.3001.4242

    拦截器原理:https://www.cnblogs.com/anyiz/p/10670731.html

    Spring单元测试

    spring5+junit5使用以下注解

    @SpringJUnitConfig(locations = {"/ApplicationTest.xml"})

    然后进行测试。

    https://blog.csdn.net/love20yh/article/details/81327584

    SpringMVC拦截器

    拦截器 :是在面向切面编程的就是在你的service或者一个方法,前调用一个方法,或者在方法后调用一个方法比如动态代理就是拦截器的简单实现,在你调用方法前打印出字符串(或者做其它业务逻辑的操作),也可以在你调用方法后打印出字符串,甚至在你抛出异常的时候做业务逻辑的操作。

    过滤器:是在javaweb中,你传入的request、response提前过滤掉一些信息,或者提前设置一些参数,然后再传入servlet或者struts的action进行业务逻辑,比如过滤掉非法url(不是login.do的地址请求,如果用户没有登陆都过滤掉),或者在传入servlet或者 struts的action前统一设置字符集,或者去除掉一些非法字符.。

    拦截器和过滤器比较
    ①拦截器是基于Java的反射机制的,而过滤器是基于函数回调。
    ②拦截器不依赖与servlet容器,依赖于web框架,在SpringMVC中就是依赖于SpringMVC框架。过滤器依赖与servlet容器。
    ③拦截器只能对action(也就是controller)请求起作用,而过滤器则可以对几乎所有的请求起作用,并且可以对请求的资源进行起作用,但是缺点是一个过滤器实例只能在容器初始化时调用一次。
    ④拦截器可以访问action上下文、值栈里的对象,而过滤器不能访问。
    ⑤在action的生命周期中,拦截器可以多次被调用,而过滤器只能在容器初始化时被调用一次。
    ⑥拦截器可以获取IOC容器中的各个bean,而过滤器就不行,这点很重要,在拦截器里注入一个service,可以调用业务逻辑

    mybatis懒加载和延迟加载

    在进行表关联查询时,且使用嵌套查询时,可以配置懒加载,只有需要使用数据时才进行查询。

    <settings>
        <!-- 开启延迟加载 -->
        <setting name="lazyLoadingEnabled" value="true" />
        <setting name="aggressiveLazyLoading" value="false" />
    </settings>

    可以单独使用fentchType配置懒加载。

     

    经过查阅文档,我们知道了,如果想要开始延迟加载功能,就需要在总配置文件 SqlMapConfig.xml 中配置 setting 属性,也就是将延迟加载 lazyLoadingEnable 的开关设置成 teue ,由于是按需加载,所以还需要将积极加载修改为消极加载,也就是将 aggressiveLazyLoading 改为 false

  • 相关阅读:
    原码, 反码, 补码 详解
    位移运算符
    ASP.NET中httpmodules与httphandlers全解析
    MySQL count
    真正的能理解CSS中的line-height,height与line-height
    IfcEvent
    IfcWorkCalendarTypeEnum
    IfcSingleProjectInstance
    转换模型
    IfcTypeProduct
  • 原文地址:https://www.cnblogs.com/baldprogrammer/p/13923689.html
Copyright © 2020-2023  润新知