分類: 3C資訊

  • 【Spring註解驅動開發】組件註冊-@ComponentScan-自動掃描組件&指定掃描規則

    寫在前面

    在實際項目中,我們更多的是使用Spring的包掃描功能對項目中的包進行掃描,凡是在指定的包或子包中的類上標註了@Repository、@Service、@Controller、@Component註解的類都會被掃描到,並將這個類注入到Spring容器中。Spring包掃描功能可以使用XML文件進行配置,也可以直接使用@ComponentScan註解進行設置,使用@ComponentScan註解進行設置比使用XML文件配置要簡單的多。

    項目工程源碼已經提交到GitHub:https://github.com/sunshinelyz/spring-annotation

    使用XML文件配置包掃描

    我們可以在Spring的XML配置文件中配置包的掃描,在配置包掃描時,需要在Spring的XML文件中的beans節點中引入context標籤,如下所示。

    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
                               http://www.springframework.org/schema/beans/spring-beans.xsd
                               http://www.springframework.org/schema/context
                               http://www.springframework.org/context/spring-context.xsd ">
    

    接下來,我們就可以在XML文件中定義要掃描的包了,如下所示。

    <context:component-scan base-package="io.mykit.spring"/>
    

    整個beans.xml文件如下所示。

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/context
           http://www.springframework.org/schema/context.xsd">
    
        <context:component-scan base-package="io.mykit.spring"/>
    
        <bean id = "person" class="io.mykit.spring.bean.Person">
            <property name="name" value="binghe"></property>
            <property name="age" value="18"></property>
        </bean>
    </beans>
    

    此時,只要在io.mykit.spring包下,或者io.mykit.spring的子包下標註了@Repository、@Service、@Controller、@Component註解的類都會被掃描到,並自動注入到Spring容器中。

    此時,我們分別創建PersonDao、PersonService、和PersonController類,並在這三個類中分別添加@Repository、@Service、@Controller註解,如下所示。

    • PersonDao
    package io.mykit.spring.plugins.register.dao;
    
    import org.springframework.stereotype.Repository;
    
    /**
     * @author binghe
     * @version 1.0.0
     * @description 測試的dao
     */
    @Repository
    public class PersonDao {
    }
    
    • PersonService
    package io.mykit.spring.plugins.register.service;
    
    import org.springframework.stereotype.Service;
    
    /**
     * @author binghe
     * @version 1.0.0
     * @description 測試的Service
     */
    @Service
    public class PersonService {
    }
    
    • PersonController
    package io.mykit.spring.plugins.register.controller;
    
    import org.springframework.stereotype.Controller;
    
    /**
     * @author binghe
     * @version 1.0.0
     * @description 測試的controller
     */
    @Controller
    public class PersonController {
    }
    

    接下來,我們在SpringBeanTest類中新建一個測試方法testComponentScanByXml()進行測試,如下所示。

    @Test
    public void testComponentScanByXml(){
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        String[] names = context.getBeanDefinitionNames();
        Arrays.stream(names).forEach(System.out::println);
    }
    

    運行測試用例,輸出的結果信息如下所示。

    personConfig
    personController
    personDao
    personService
    org.springframework.context.annotation.internalConfigurationAnnotationProcessor
    org.springframework.context.annotation.internalAutowiredAnnotationProcessor
    org.springframework.context.annotation.internalCommonAnnotationProcessor
    org.springframework.context.event.internalEventListenerProcessor
    org.springframework.context.event.internalEventListenerFactory
    person
    

    可以看到,除了輸出我們自己創建的bean名稱之外,也輸出了Spring內部使用的一些重要的bean名稱。

    接下來,我們使用註解來完成這些功能。

    使用註解配置包掃描

    使用@ComponentScan註解之前我們先將beans.xml文件中的下述配置註釋。

    <context:component-scan base-package="io.mykit.spring"></context:component-scan>
    

    註釋后如下所示。

    <!--<context:component-scan base-package="io.mykit.spring"></context:component-scan>-->
    

    使用@ComponentScan註解配置包掃描就非常Easy了!在我們的PersonConfig類上添加@ComponentScan註解,並將掃描的包指定為io.mykit.spring即可,整個的PersonConfig類如下所示。

    package io.mykit.spring.plugins.register.config;
    
    import io.mykit.spring.bean.Person;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    
    /**
     * @author binghe
     * @version 1.0.0
     * @description 以註解的形式來配置Person
     */
    @Configuration
    @ComponentScan(value = "io.mykit.spring")
    public class PersonConfig {
    
        @Bean("person")
        public Person person01(){
            return new Person("binghe001", 18);
        }
    }
    

    沒錯,就是這麼簡單,只需要在類上添加@ComponentScan(value = “io.mykit.spring”)註解即可。

    接下來,我們在SpringBeanTest類中新增testComponentScanByAnnotation()方法,如下所示。

    @Test
    public void testComponentScanByAnnotation(){
        ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig.class);
        String[] names = context.getBeanDefinitionNames();
        Arrays.stream(names).forEach(System.out::println);
    }
    

    運行testComponentScanByAnnotation()方法輸出的結果信息如下所示。

    org.springframework.context.annotation.internalConfigurationAnnotationProcessor
    org.springframework.context.annotation.internalAutowiredAnnotationProcessor
    org.springframework.context.annotation.internalCommonAnnotationProcessor
    org.springframework.context.event.internalEventListenerProcessor
    org.springframework.context.event.internalEventListenerFactory
    personConfig
    personController
    personDao
    personService
    person
    

    可以看到使用@ComponentScan註解同樣輸出了bean的名稱。

    既然使用XML文件和註解的方式都能夠將相應的類注入到Spring容器當中,那我們是使用XML文件還是使用註解呢?我更傾向於使用註解,如果你確實喜歡使用XML文件進行配置,也可以,哈哈,個人喜好嘛!好了,我們繼續。

    關於@ComponentScan註解

    我們點開ComponentScan註解類,如下所示。

    package org.springframework.context.annotation;
    
    import java.lang.annotation.Documented;
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Repeatable;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    import org.springframework.beans.factory.support.BeanNameGenerator;
    import org.springframework.core.annotation.AliasFor;
    import org.springframework.core.type.filter.TypeFilter;
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Documented
    @Repeatable(ComponentScans.class)
    public @interface ComponentScan {
    
    	@AliasFor("basePackages")
    	String[] value() default {};
    
    	@AliasFor("value")
    	String[] basePackages() default {};
    
    	Class<?>[] basePackageClasses() default {};
    
    	Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;
    
    	Class<? extends ScopeMetadataResolver> scopeResolver() default AnnotationScopeMetadataResolver.class;
    
    	ScopedProxyMode scopedProxy() default ScopedProxyMode.DEFAULT;
    
    	String resourcePattern() default ClassPathScanningCandidateComponentProvider.DEFAULT_RESOURCE_PATTERN;
    
    	boolean useDefaultFilters() default true;
    
    	Filter[] includeFilters() default {};
    
    	Filter[] excludeFilters() default {};
    
    	boolean lazyInit() default false;
    
    	@Retention(RetentionPolicy.RUNTIME)
    	@Target({})
    	@interface Filter {
    		FilterType type() default FilterType.ANNOTATION;
            
    		@AliasFor("classes")
    		Class<?>[] value() default {};
            
    		@AliasFor("value")
    		Class<?>[] classes() default {};
            
    		String[] pattern() default {};
    	}
    }
    

    這裏,我們着重來看ComponentScan類的兩個方法,如下所示。

    Filter[] includeFilters() default {};
    Filter[] excludeFilters() default {};
    

    includeFilters()方法表示Spring掃描的時候,只包含哪些註解,而excludeFilters()方法表示不包含哪些註解。兩個方法的返回值都是Filter[]數組,在ComponentScan註解類的內部存在Filter註解類,大家可以看下上面的代碼。

    1.掃描時排除註解標註的類

    例如,我們現在排除@Controller、@Service和@Repository註解,我們可以在PersonConfig類上通過@ComponentScan註解的excludeFilters()實現。例如,我們在PersonConfig類上添加了如下的註解。

    @ComponentScan(value = "io.mykit.spring", excludeFilters = {
            @Filter(type = FilterType.ANNOTATION, classes = {Controller.class, Service.class, Repository.class})
    })
    

    這樣,我們就使得Spring在掃描包的時候排除了使用@Controller、@Service和@Repository註解標註的類。運行SpringBeanTest類中的testComponentScanByAnnotation()方法,輸出的結果信息如下所示。

    org.springframework.context.annotation.internalConfigurationAnnotationProcessor
    org.springframework.context.annotation.internalAutowiredAnnotationProcessor
    org.springframework.context.annotation.internalCommonAnnotationProcessor
    org.springframework.context.event.internalEventListenerProcessor
    org.springframework.context.event.internalEventListenerFactory
    personConfig
    person
    

    可以看到,輸出的結果信息中不再輸出personController、personService和personDao說明Spring在進行包掃描時,忽略了@Controller、@Service和@Repository註解標註的類。

    2.掃描時只包含註解標註的類

    我們也可以使用ComponentScan註解類的includeFilters()來指定Spring在進行包掃描時,只包含哪些註解標註的類。

    這裏需要注意的是,當我們使用includeFilters()來指定只包含哪些註解標註的類時,需要禁用默認的過濾規則。

    例如,我們需要Spring在掃描時,只包含@Controller註解標註的類,可以在PersonConfig類上添加@ComponentScan註解,設置只包含@Controller註解標註的類,並禁用默認的過濾規則,如下所示。

    @ComponentScan(value = "io.mykit.spring", includeFilters = {
            @Filter(type = FilterType.ANNOTATION, classes = {Controller.class})
    }, useDefaultFilters = false)
    

    此時,我們再次運行SpringBeanTest類的testComponentScanByAnnotation()方法,輸出的結果信息如下所示。

    org.springframework.context.annotation.internalConfigurationAnnotationProcessor
    org.springframework.context.annotation.internalAutowiredAnnotationProcessor
    org.springframework.context.annotation.internalCommonAnnotationProcessor
    org.springframework.context.event.internalEventListenerProcessor
    org.springframework.context.event.internalEventListenerFactory
    personConfig
    personController
    person
    

    可以看到,在輸出的結果中,只包含了@Controller註解標註的組件名稱,並沒有輸出@Service和@Repository註解標註的組件名稱。

    注意:在使用includeFilters()來指定只包含哪些註解標註的類時,結果信息中會一同輸出Spring內部的組件名稱。

    3.重複註解

    不知道小夥伴們有沒有注意到ComponentScan註解類上有一個如下所示的註解。

    @Repeatable(ComponentScans.class)
    

    我們先來看看@ComponentScans註解是個啥,如下所示。

    package org.springframework.context.annotation;
    
    import java.lang.annotation.Documented;
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Documented
    public @interface ComponentScans {
    	ComponentScan[] value();
    }
    

    可以看到,在ComponentScans註解類中只聲明了一個返回ComponentScan[]數組的value(),說到這裏,大家是不是就明白了,沒錯,這在Java8中是一個重複註解。

    對於Java8不熟悉的小夥伴,可以到【Java8新特性】專欄查看關於Java8新特性的文章。專欄地址小夥伴們可以猛戳下面的鏈接地址進行查看:

    https://mp.weixin.qq.com/mp/appmsgalbum?action=getalbum&__biz=Mzg3MzE1NTIzNA==&scene=1&album_id=1325066823947321344#wechat_redirect

    在Java8中表示@ComponentScan註解是一個重複註解,可以在一個類上重複使用這個註解,如下所示。

    @Configuration
    @ComponentScan(value = "io.mykit.spring", includeFilters = {
            @Filter(type = FilterType.ANNOTATION, classes = {Controller.class})
    }, useDefaultFilters = false)
    @ComponentScan(value = "io.mykit.spring", includeFilters = {
            @Filter(type = FilterType.ANNOTATION, classes = {Service.class})
    }, useDefaultFilters = false)
    public class PersonConfig {
    
        @Bean("person")
        public Person person01(){
            return new Person("binghe001", 18);
        }
    }
    

    運行SpringBeanTest類的testComponentScanByAnnotation()方法,輸出的結果信息如下所示。

    org.springframework.context.annotation.internalConfigurationAnnotationProcessor
    org.springframework.context.annotation.internalAutowiredAnnotationProcessor
    org.springframework.context.annotation.internalCommonAnnotationProcessor
    org.springframework.context.event.internalEventListenerProcessor
    org.springframework.context.event.internalEventListenerFactory
    personConfig
    personController
    personService
    person
    

    可以看到,同時輸出了@Controller註解和@Service註解標註的組件名稱。

    如果使用的是Java8之前的版本,我們就不能直接在類上寫多個@ComponentScan註解了。此時,我們可以在PersonConfig類上使用@ComponentScans註解,如下所示。

    @ComponentScans(value = {
            @ComponentScan(value = "io.mykit.spring", includeFilters = {
                    @Filter(type = FilterType.ANNOTATION, classes = {Controller.class})
            }, useDefaultFilters = false),
            @ComponentScan(value = "io.mykit.spring", includeFilters = {
                    @Filter(type = FilterType.ANNOTATION, classes = {Service.class})
            }, useDefaultFilters = false)
    })
    

    再次運行SpringBeanTest類的testComponentScanByAnnotation()方法,輸出的結果信息如下所示。

    org.springframework.context.annotation.internalConfigurationAnnotationProcessor
    org.springframework.context.annotation.internalAutowiredAnnotationProcessor
    org.springframework.context.annotation.internalCommonAnnotationProcessor
    org.springframework.context.event.internalEventListenerProcessor
    org.springframework.context.event.internalEventListenerFactory
    personConfig
    personController
    personService
    person
    

    與使用多個@ComponentScan註解輸出的結果信息相同。

    總結:我們可以使用@ComponentScan註解來指定Spring掃描哪些包,可以使用excludeFilters()指定掃描時排除哪些組件,也可以使用includeFilters()指定掃描時只包含哪些組件。當使用includeFilters()指定只包含哪些組件時,需要禁用默認的過濾規則

    好了,咱們今天就聊到這兒吧!別忘了給個在看和轉發,讓更多的人看到,一起學習一起進步!!

    項目工程源碼已經提交到GitHub:https://github.com/sunshinelyz/spring-annotation

    寫在最後

    如果覺得文章對你有點幫助,請微信搜索並關注「 冰河技術 」微信公眾號,跟冰河學習Spring註解驅動開發。公眾號回復“spring註解”關鍵字,領取Spring註解驅動開發核心知識圖,讓Spring註解驅動開發不再迷茫。

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

    南投搬家公司費用需注意的眉眉角角,別等搬了再說!

    ※教你寫出一流的銷售文案?

    ※回頭車貨運收費標準

    ※別再煩惱如何寫文案,掌握八大原則!

  • 天哪!手動編寫mybatis雛形竟然這麼簡單

    天哪!手動編寫mybatis雛形竟然這麼簡單

    前言

    mybaits 在ORM 框架中,可算是半壁江山了,由於它是輕量級,半自動加載,靈活性和易拓展性。深受廣大公司的喜愛,所以我們程序開發也離不開mybatis 。但是我們有對mabtis 源碼進行研究嗎?或者想看但是不知道怎麼看的苦惱嗎?

    歸根結底,我們還是需要知道為什麼會有mybatis ,mybatis 解決了什麼問題?
    想要知道mybatis 解決了什麼問題,就要知道傳統的JDBC 操作存在哪些痛點才促使mybatis 的誕生。
    我們帶着這些疑問,再來一步步學習吧。

    原始JDBC 存在的問題

    所以我們先來來看下原始JDBC 的操作:
    我們知道最原始的數據庫操作。分為以下幾步:
    1、獲取connection 連接
    2、獲取preparedStatement
    3、參數替代佔位符
    4、獲取執行結果resultSet
    5、解析封裝resultSet 到對象中返回。

    如下是原始JDBC 的查詢代碼,存在哪些問題?

    public static void main(String[] args) {
            String dirver="com.mysql.jdbc.Driver";
            String url="jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8";
            String userName="root";
            String password="123456";
    
            Connection connection=null;
            List<User> userList=new ArrayList<>();
            try {
                Class.forName(dirver);
                connection= DriverManager.getConnection(url,userName,password);
    
                String sql="select * from user where username=?";
                PreparedStatement preparedStatement=connection.prepareStatement(sql);
                preparedStatement.setString(1,"張三");
                System.out.println(sql);
                ResultSet resultSet=preparedStatement.executeQuery();
    
                User user=null;
                while(resultSet.next()){
                    user=new User();
                    user.setId(resultSet.getInt("id"));
                    user.setUsername(resultSet.getString("username"));
                    user.setPassword(resultSet.getString("password"));
                    userList.add(user);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                try {
                    connection.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
    
            if (!userList.isEmpty()) {
                for (User user : userList) {
                    System.out.println(user.toString());
                }
            }
    
        }
    

    小夥伴們發現了上面有哪些不友好的地方?
    我這裏總結了以下幾點:
    1、數據庫的連接信息存在硬編碼,即是寫死在代碼中的。
    2、每次操作都會建立和釋放connection 連接,操作資源的不必要的浪費。
    3、sql 和參數存在硬編碼。
    4、將返回結果集封裝成實體類麻煩,要創建不同的實體類,並通過set方法一個個的注入。

    存在上面的問題,所以mybatis 就對上述問題進行了改進。
    對於硬編碼,我們很容易就想到配置文件來解決。mybatis 也是這麼解決的。
    對於資源浪費,我們想到是用連接池,mybatis 也是這個解決的。
    對於封裝結果集麻煩,我們想到是用JDK的反射機制,好巧,mybatis 也是這麼解決的。

    設計思路

    既然如此,我們就來寫一個自定義吃持久層框架,來解決上述問題,當然是參照mybatis 的設計思路,這樣我們在寫完之後,再來看mybatis 的源碼就恍然大悟,這個地方這樣配置原來是因為這樣啊。
    我們分為使用端和框架端兩部分。

    使用端

    我們在使用mybatis 的時候是不是需要使用SqlMapConfig.xml 配置文件,用來存放數據庫的連接信息,以及mapper.xml 的指向信息。mapper.xml 配置文件用來存放sql 信息。
    所以我們在使用端來創建兩個文件SqlMapConfig.xml 和mapper.xml。

    框架端

    框架端要做哪些事情呢?如下:
    1、獲取配置文件。也就是獲取到使用端的SqlMapConfig.xml 以及mapper.xml的 文件
    2、解析配置文件。對獲取到的文件進行解析,獲取到連接信息,sql,參數,返回類型等等。這些信息都會保存在configuration 這個對象中。
    3、創建SqlSessionFactory,目的是創建SqlSession的一個實例。
    4、創建SqlSession ,用來完成上面原始JDBC 的那些操作。

    那在SqlSession 中 進行了哪些操作呢?
    1、獲取數據庫連接
    2、獲取sql,並對sql 進行解析
    3、通過內省,將參數注入到preparedStatement 中
    4、執行sql
    5、通過反射將結果集封裝成對象

    使用端實現

    好了,上面說了一下,大概的設計思路,主要也是仿照mybatis 主要的類實現的,保證類名一致,方便我們後面閱讀源碼。我們先來配置好使用端吧,我們創建一個maven 項目。
    在項目中,我們創建一個User實體類

    public class User {
        private Integer id;
        private String username;
        private String password;
        private String birthday;
        //getter()和setter()方法
    }
    

    創建SqlMapConfig.xml 和Mapper.xml
    SqlMapConfig.xml

    <?xml version="1.0" encoding="UTF-8" ?>
    <configuration>
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&amp;characterEncoding=utf8&amp;useUnicode=true&amp;useSSL=false"></property>
        <property name="userName" value="root"></property>
        <property name="password" value="123456"></property>
        
        <mapper resource="UserMapper.xml">
        </mapper>
    </configuration>
    

    可以看到我們xml 中就配置了數據庫的連接信息,以及mapper 一個索引。mybatis中的SqlMapConfig.xml 中還包含其他的標籤,只是豐富了功能而已,所以我們只用最主要的。

    mapper.xml
    是每個類的sql 都會生成一個對應的mapper.xml 。我們這裏就用User 類來說吧,所以我們就創建一個UserMapper.xml

    <?xml version="1.0" encoding="UTF-8" ?>
    <mapper namespace="cn.quellanan.dao.UserDao">
        <select id="selectAll" resultType="cn.quellanan.pojo.User">
            select * from user
        </select>
        <select id="selectByName" resultType="cn.quellanan.pojo.User" paramType="cn.quellanan.pojo.User">
            select * from user where username=#{username}
        </select>
    </mapper>
    

    可以看到有點mybatis 裏面文件的味道,有namespace表示命名空間,id 唯一標識,resultType 返回結果集的類型,paramType 參數的類型。
    我們使用端先創建到這,主要是兩個配置文件,我們接下來看看框架端是怎麼實現的。

    加油哈哈。

    框架端實現

    框架端,我們按照上面的設計思路一步一步來。

    獲取配置

    怎麼樣獲取配置文件呢?我們可以使用JDK自帶自帶的類Resources加載器來獲取文件。我們創建一個自定義Resource類來封裝一下:

    import java.io.InputStream;
    public class Resources {
        public  static InputStream getResources(String path){
            //使用系統自帶的類Resources加載器來獲取文件。
            return Resources.class.getClassLoader().getResourceAsStream(path);
        }
    }
    

    這樣通過傳入路徑,就可以獲取到對應的文件流啦。

    解析配置文件

    上面獲取到了SqlMapConfig.xml 配置文件,我們現在來解析它。
    不過在此之前,我們需要做一點準備工作,就是解析的內存放到什麼地方?
    所以我們來創建兩個實體類Mapper 和Configuration。

    Mapper
    Mapper 實體類用來存放使用端寫的mapper.xml 文件的內容,我們前面說了裏面有.id、sql、resultType 和paramType .所以我們創建的Mapper實體如下:

    public class Mapper {
        private String id;
        private Class<?> resultType;
        private Class<?> parmType;
        private String sql;
        //getter()和setter()方法
    }
    

    這裏我們為什麼不添加namespace 的值呢?
    聰明的你肯定發現了,因為mapper裏面這些屬性表明每個sql 都對應一個mapper,而namespace 是一個命名空間,算是sql 的上一層,所以在mapper中暫時使用不到,就沒有添加了。

    Configuration
    Configuration 實體用來保存SqlMapConfig 中的信息。所以需要保存數據庫連接,我們這裏直接用JDK提供的 DataSource。還有一個就是mapper 的信息。每個mapper 有自己的標識,所以這裏採用hashMap來存儲。如下:

    public class Configuration {
    
        private DataSource dataSource;
        HashMap <String,Mapper> mapperMap=new HashMap<>();
        //getter()和setter方法
        }
    

    XmlMapperBuilder

    做好了上面的準備工作,我們先來解析mapper 吧。我們創建一個XmlMapperBuilder 類來解析。通過dom4j 的工具類來解析XML 文件。我這裏用的dom4j 依賴為:

    		<dependency>
                <groupId>org.dom4j</groupId>
                <artifactId>dom4j</artifactId>
                <version>2.1.3</version>
            </dependency>
    

    思路:
    1、獲取文件流,轉成document。
    2、獲取根節點,也就是mapper。獲取根節點的namespace屬性值
    3、獲取select 節點,獲取其id,sql,resultType,paramType
    4、將select 節點的屬性封裝到Mapper 實體類中。
    5、同理獲取update/insert/delete 節點的屬性值封裝到Mapper 中
    6、通過namespace.id 生成key 值將mapper對象保存到Configuration實體中的HashMap 中。
    7、返回 Configuration實體
    代碼如下:

    
    public class XmlMapperBuilder {
        private Configuration configuration;
        public XmlMapperBuilder(Configuration configuration){
            this.configuration=configuration;
        }
    
        public Configuration loadXmlMapper(InputStream in) throws DocumentException, ClassNotFoundException {
            Document document=new SAXReader().read(in);
    
            Element rootElement=document.getRootElement();
            String namespace=rootElement.attributeValue("namespace");
    
            List<Node> list=rootElement.selectNodes("//select");
    
            for (int i = 0; i < list.size(); i++) {
                Mapper mapper=new Mapper();
                Element element= (Element) list.get(i);
                String id=element.attributeValue("id");
                mapper.setId(id);
                String paramType = element.attributeValue("paramType");
                if(paramType!=null && !paramType.isEmpty()){
                    mapper.setParmType(Class.forName(paramType));
                }
                String resultType = element.attributeValue("resultType");
                if (resultType != null && !resultType.isEmpty()) {
                    mapper.setResultType(Class.forName(resultType));
                }
                mapper.setSql(element.getTextTrim());
                String key=namespace+"."+id;
                configuration.getMapperMap().put(key,mapper);
            }
            return configuration;
        }
    
    }
    

    上面我只解析了select 標籤。大家可以解析對應insert/delete/uupdate 標籤,操作都是一樣的。

    XmlConfigBuilder

    我們再來解析一下SqlMapConfig.xml 配置信息思路是一樣的,
    1、獲取文件流,轉成document。
    2、獲取根節點,也就是configuration。
    3、獲取根節點中所有的property 節點,並獲取值,也就是獲取數據庫連接信息
    4、創建一個dataSource 連接池
    5、將連接池信息保存到Configuration實體中
    6、獲取根節點的所有mapper 節點
    7、調用XmlMapperBuilder 類解析對應mapper 並封裝到Configuration實體中
    8、完
    代碼如下:

    public class XmlConfigBuilder {
        private Configuration configuration;
        public XmlConfigBuilder(Configuration configuration){
            this.configuration=configuration;
        }
    
        public Configuration loadXmlConfig(InputStream in) throws DocumentException, PropertyVetoException, ClassNotFoundException {
    
            Document document=new SAXReader().read(in);
    
            Element rootElement=document.getRootElement();
    
            //獲取連接信息
            List<Node> propertyList=rootElement.selectNodes("//property");
            Properties properties=new Properties();
    
            for (int i = 0; i < propertyList.size(); i++) {
                Element element = (Element) propertyList.get(i);
                properties.setProperty(element.attributeValue("name"),element.attributeValue("value"));
            }
    		//是用連接池
            ComboPooledDataSource dataSource = new ComboPooledDataSource();
            dataSource.setDriverClass(properties.getProperty("driverClass"));
            dataSource.setJdbcUrl(properties.getProperty("jdbcUrl"));
            dataSource.setUser(properties.getProperty("userName"));
            dataSource.setPassword(properties.getProperty("password"));
            configuration.setDataSource(dataSource);
    
            //獲取mapper 信息
            List<Node> mapperList=rootElement.selectNodes("//mapper");
            for (int i = 0; i < mapperList.size(); i++) {
                Element element= (Element) mapperList.get(i);
                String mapperPath=element.attributeValue("resource");
                XmlMapperBuilder xmlMapperBuilder = new XmlMapperBuilder(configuration);
                configuration=xmlMapperBuilder.loadXmlMapper(Resources.getResources(mapperPath));
            }
            return configuration;
        }
    }
    

    創建SqlSessionFactory

    完成解析后我們創建SqlSessionFactory 用來創建Sqlseesion 的實體,這裏為了盡量還原mybatis 設計思路,也也採用的工廠設計模式。
    SqlSessionFactory 是一個接口,裏面就一個用來創建SqlSessionf的方法。
    如下:

    public interface SqlSessionFactory {
        public SqlSession openSqlSession();
    }
    

    單單這個接口是不夠的,我們還得寫一個接口的實現類,所以我們創建一個DefaultSqlSessionFactory。
    如下:

    public class DefaultSqlSessionFactory implements SqlSessionFactory {
    
        private Configuration configuration;
    
        public DefaultSqlSessionFactory(Configuration configuration) {
            this.configuration = configuration;
        }
        public SqlSession openSqlSession() {
            return new DefaultSqlSeeion(configuration);
        }
    }
    

    可以看到就是創建一個DefaultSqlSeeion並將包含配置信息的configuration 傳遞下去。DefaultSqlSeeion 就是SqlSession 的一個實現類。

    創建SqlSession

    在SqlSession 中我們就要來處理各種操作了,比如selectList,selectOne,insert.update,delete 等等。
    我們這裏SqlSession 就先寫一個selectList 方法。
    如下:

    public interface SqlSession {
    
        /**
         * 條件查找
         * @param statementid  唯一標識,namespace.selectid
         * @param parm  傳參,可以不傳也可以一個,也可以多個
         * @param <E>
         * @return
         */
        public <E> List<E> selectList(String statementid,Object...parm) throws Exception;
    
    

    然後我們創建DefaultSqlSeeion 來實現SqlSeesion 。

    public class DefaultSqlSeeion implements SqlSession {
        private Configuration configuration;
    	private Executer executer=new SimpleExecuter();
    	
        public DefaultSqlSeeion(Configuration configuration) {
            this.configuration = configuration;
        }
    
    	@Override
        public <E> List<E> selectList(String statementid, Object... parm) throws Exception {
            Mapper mapper=configuration.getMapperMap().get(statementid);
            List<E> query = executer.query(configuration, mapper, parm);
            return query;
        }
    
    }
    

    我們可以看到DefaultSqlSeeion 獲取到了configuration,並通過statementid 從configuration 中獲取mapper。 然後具體實現交給了Executer 類來實現。我們這裏先不管Executer 是怎麼實現的,就假裝已經實現了。那麼整個框架端就完成了。通過調用Sqlsession.selectList() 方法,來獲取結果。

    感覺我們都還沒有處理,就框架搭建好了?騙鬼呢,確實前面我們從獲取文件解析文件,然後創建工廠。都是做好準備工作。下面開始我們JDBC的實現。

    SqlSession 具體實現

    我們前面說SqlSeesion 的具體實現有下面5步
    1、獲取數據庫連接
    2、獲取sql,並對sql 進行解析
    3、通過內省,將參數注入到preparedStatement 中
    4、執行sql
    5、通過反射將結果集封裝成對象

    但是我們在DefaultSqlSeeion 中將實現交給了Executer來執行。所以我們就要在Executer中來實現這些操作。

    我們首先來創建一個Executer 接口,並寫一個DefaultSqlSeeion中調用的query 方法。

    public interface Executer {
    
        <E> List<E> query(Configuration configuration,Mapper mapper,Object...parm) throws Exception;
    
    }
    

    接着我們寫一個SimpleExecuter 類來實現Executer 。
    然後SimpleExecuter.query()方法中,我們一步一步的實現。

    獲取數據庫連接

    因為數據庫連接信息保存在configuration,所以直接獲取就好了。

    //獲取連接
            connection=configuration.getDataSource().getConnection();
    

    獲取sql,並對sql 進行解析

    我們這裏想一下,我們在Usermapper.xml寫的sql 是什麼樣子?

    select * from user where username=#{username}
    

    {username} 這樣的sql 我們改怎麼解析呢?

    分兩步
    1、將sql 找到#{***},並將這部分替換成 ?號

    2、對 #{***} 進行解析獲取到裏面的參數對應的paramType 中的值。

    具體實現用到下面幾個類。
    GenericTokenParser類,可以看到有三個參數,開始標記,就是我們的“#{” ,結束標記就是 “}”, 標記處理器就是處理標記裏面的內容也就是username。

    public class GenericTokenParser {
    
      private final String openToken; //開始標記
      private final String closeToken; //結束標記
      private final TokenHandler handler; //標記處理器
    
      public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
        this.openToken = openToken;
        this.closeToken = closeToken;
        this.handler = handler;
      }
    
      /**
       * 解析${}和#{}
       * @param text
       * @return
       * 該方法主要實現了配置文件、腳本等片段中佔位符的解析、處理工作,並返回最終需要的數據。
       * 其中,解析工作由該方法完成,處理工作是由處理器handler的handleToken()方法來實現
       */
      public String parse(String text) {
     	 //具體實現
     	 }
      }
    

    主要的就是parse() 方法,用來獲取操作1 的sql。獲取結果例如:

    select * from user where username=?
    

    那上面用到TokenHandler 來處理參數。
    ParameterMappingTokenHandler實現TokenHandler的類

    
    public class ParameterMappingTokenHandler implements TokenHandler {
    	private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();
    
    	// context是參數名稱 #{id} #{username}
    
    	@Override
    	public String handleToken(String content) {
    		parameterMappings.add(buildParameterMapping(content));
    		return "?";
    	}
    
    	private ParameterMapping buildParameterMapping(String content) {
    		ParameterMapping parameterMapping = new ParameterMapping(content);
    		return parameterMapping;
    	}
    
    	public List<ParameterMapping> getParameterMappings() {
    		return parameterMappings;
    	}
    
    	public void setParameterMappings(List<ParameterMapping> parameterMappings) {
    		this.parameterMappings = parameterMappings;
    	}
    
    }
    
    

    可以看到將參數名稱存放 ParameterMapping 的集合中了。
    ParameterMapping 類就是一個實體,用來保存參數名稱的。

    public class ParameterMapping {
    
        private String content;
    
        public ParameterMapping(String content) {
            this.content = content;
        }
    	//getter()和setter() 方法。
    }
    

    所以我們在我們通過GenericTokenParser類,就可以獲取到解析后的sql,以及參數名稱。我們將這些信息封裝到BoundSql實體類中。

    public class BoundSql {
    
        private String sqlText;
        private List<ParameterMapping> parameterMappingList=new ArrayList<>();
        public BoundSql(String sqlText, List<ParameterMapping> parameterMappingList) {
            this.sqlText = sqlText;
            this.parameterMappingList = parameterMappingList;
        }
        ////getter()和setter() 方法。
      }
    

    好了,那麼分兩步走,先獲取,后解析
    獲取
    獲取原始sql 很簡單,sql 信息就存在mapper 對象中,直接獲取就好了。

    String sql=mapper.getSql()
    

    解析
    1、創建一個ParameterMappingTokenHandler 處理器
    2、創建一個GenericTokenParser 類,並初始化開始標記,結束標記,處理器
    3、執行genericTokenParser.parse(sql);獲取解析后的sql‘’,以及在parameterMappingTokenHandler 中存放了參數名稱的集合。
    4、將解析后的sql 和參數封裝到BoundSql 實體類中。

    /**
         * 解析自定義佔位符
         * @param sql
         * @return
         */
        private BoundSql getBoundSql(String sql){
            ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();
            GenericTokenParser genericTokenParser = new GenericTokenParser("#{","}",parameterMappingTokenHandler);
            String parse = genericTokenParser.parse(sql);
            return new BoundSql(parse,parameterMappingTokenHandler.getParameterMappings());
    
        }
    

    將參數注入到preparedStatement 中

    上面的就完成了sql,的解析,但是我們知道上面得到的sql 還是包含 JDBC的 佔位符,所以我們需要將參數注入到preparedStatement 中。
    1、通過boundSql.getSqlText()獲取帶有佔位符的sql.
    2、接收參數名稱集合 parameterMappingList
    3、通過mapper.getParmType() 獲取到參數的類。
    4、通過getDeclaredField(content)方法獲取到參數類的Field。
    5、通過Field.get() 從參數類中獲取對應的值
    6、注入到preparedStatement 中

    		BoundSql boundSql=getBoundSql(mapper.getSql());
            String sql=boundSql.getSqlText();
            List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList();
    
            //獲取preparedStatement,並傳遞參數值
            PreparedStatement preparedStatement=connection.prepareStatement(sql);
            Class<?> parmType = mapper.getParmType();
    
            for (int i = 0; i < parameterMappingList.size(); i++) {
                ParameterMapping parameterMapping = parameterMappingList.get(i);
                String content = parameterMapping.getContent();
                Field declaredField = parmType.getDeclaredField(content);
                declaredField.setAccessible(true);
                Object o = declaredField.get(parm[0]);
                preparedStatement.setObject(i+1,o);
            }
            System.out.println(sql);
            return preparedStatement;
    

    執行sql

    其實還是調用JDBC 的executeQuery()方法或者execute()方法

    //執行sql
     ResultSet resultSet = preparedStatement.executeQuery();
    

    通過反射將結果集封裝成對象

    在獲取到resultSet 后,我們進行封裝處理,和參數處理是類似的。
    1、創建一個ArrayList
    2、獲取返回類型的類
    3、循環從resultSet中取數據
    4、獲取屬性名和屬性值
    5、創建屬性生成器
    6、為屬性生成寫方法,並將屬性值寫入到屬性中
    7、將這條記錄添加到list 中
    8、返回list

    /**
         * 封裝結果集
         * @param mapper
         * @param resultSet
         * @param <E>
         * @return
         * @throws Exception
         */
        private <E> List<E> resultHandle(Mapper mapper,ResultSet resultSet) throws Exception{
            ArrayList<E> list=new ArrayList<>();
            //封裝結果集
            Class<?> resultType = mapper.getResultType();
            while (resultSet.next()) {
                ResultSetMetaData metaData = resultSet.getMetaData();
                Object o = resultType.newInstance();
                int columnCount = metaData.getColumnCount();
                for (int i = 1; i <= columnCount; i++) {
                    //屬性名
                    String columnName = metaData.getColumnName(i);
                    //屬性值
                    Object value = resultSet.getObject(columnName);
                    //創建屬性描述器,為屬性生成讀寫方法
                    PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName,resultType);
                    Method writeMethod = propertyDescriptor.getWriteMethod();
                    writeMethod.invoke(o,value);
                }
                list.add((E) o);
            }
            return list;
        }
    

    創建SqlSessionFactoryBuilder

    我們現在來創建一個SqlSessionFactoryBuilder 類,來為使用端提供一個人口。

    public class SqlSessionFactoryBuilder {
    
        private Configuration configuration;
    
        public SqlSessionFactoryBuilder(){
            configuration=new Configuration();
        }
    
        public SqlSessionFactory build(InputStream in) throws DocumentException, PropertyVetoException, ClassNotFoundException {
            XmlConfigBuilder xmlConfigBuilder = new XmlConfigBuilder(configuration);
            configuration=xmlConfigBuilder.loadXmlConfig(in);
    
            SqlSessionFactory sqlSessionFactory = new DefaultSqlSessionFactory(configuration);
            return sqlSessionFactory;
        }
    }
    

    可以看到就一個build 方法,通過SqlMapConfig的文件流將信息解析到configuration,創建並返回一個sqlSessionFactory 。

    到此,整個框架端已經搭建完成了,但是我們可以看到,只實現了select 的操作,update、inster、delete 的操作我們在我後面提供的源碼中會有實現,這裏只是將整體的設計思路和流程。

    測試

    終於到了測試的環節啦。我們前面寫了自定義的持久層,我們現在來測試一下能不能正常的使用吧。
    見證奇迹的時刻到啦

    我們先引入我們自定義的框架依賴。以及數據庫和單元測試

    <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>8.0.11</version>
            </dependency>
            <dependency>
                <groupId>cn.quellanan</groupId>
                <artifactId>myself-mybatis</artifactId>
                <version>1.0.0</version>
            </dependency>
    
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>4.10</version>
            </dependency>
    

    然後我們寫一個測試類
    1、獲取SqlMapperConfig.xml的文件流
    2、獲取Sqlsession
    3、執行查找操作

    @org.junit.Test
        public void test() throws Exception{
            InputStream inputStream= Resources.getResources("SqlMapperConfig.xml");
            SqlSession sqlSession = new SqlSessionFactoryBuilder().build(inputStream).openSqlSession();
            List<User> list = sqlSession.selectList("cn.quellanan.dao.UserDao.selectAll");
    
            for (User parm : list) {
                System.out.println(parm.toString());
            }
            System.out.println();
    
            User user=new User();
            user.setUsername("張三");
            List<User> list1 = sqlSession.selectList("cn.quellanan.dao.UserDao.selectByName", user);
            for (User user1 : list1) {
                System.out.println(user1);
            }
    
        }
    

    可以看到已經可以了,看來我們自定義的持久層框架生效啦。

    優化

    但是不要高興的太早哈哈,我們看上面的測試方法,是不是感覺和平時用的不一樣,每次都都寫死statementId ,這樣不太友好,所以我們接下來來點騷操作,通用mapper 配置。
    我們在SqlSession中增加一個getMapper方法,接收的參數是一個類。我們通過這個類就可以知道statementId .

    /**
         * 使用代理模式來創建接口的代理對象
         * @param mapperClass
         * @param <T>
         * @return
         */
        public <T> T getMapper(Class<T> mapperClass);
    

    具體實現就是利用JDK 的動態代理機制。
    1、通過Proxy.newProxyInstance() 獲取一個代理對象
    2、返回代理對象
    那代理對象執行了哪些操作呢?
    創建代理對象的時候,會實現一個InvocationHandler接口,重寫invoke() 方法,讓所有走這個代理的方法都會執行這個invoke() 方法。那這個方法做了什麼操作?
    這個方法就是通過傳入的類對象,獲取到對象的類名和方法名。用來生成statementid 。所以我們在mapper.xml 配置文件中的namespace 就需要制定為類路徑,以及id 為方法名。
    實現方法:

    @Override
        public <T> T getMapper(Class<T> mapperClass) {
    
            Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSeeion.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
                    //獲取到方法名
                    String name = method.getName();
                    //類型
                    String className = method.getDeclaringClass().getName();
                    String statementid=className+"."+name;
    
                    return selectList(statementid,args);
                }
            });
    
    
            return (T) proxyInstance;
        }
    

    我們寫一個UserDao

    public interface UserDao {
        List<User> selectAll();
    
        List<User> selectByName(User user);
    }
    

    這個是不是我們熟悉的味道哈哈,就是mapper層的接口。
    然後我們在mapper.xml 中指定namespace 和id

    接下來我們在寫一個測試方法

    @org.junit.Test
        public void test2() throws Exception{
            InputStream inputStream= Resources.getResources("SqlMapperConfig.xml");
            SqlSession sqlSession = new SqlSessionFactoryBuilder().build(inputStream).openSqlSession();
    
            UserDao mapper = sqlSession.getMapper(UserDao.class);
            List<User> users = mapper.selectAll();
            for (User user1 : users) {
                System.out.println(user1);
            }
    
            User user=new User();
            user.setUsername("張三");
            List<User> users1 = mapper.selectByName(user);
            for (User user1 : users1) {
                System.out.println(user1);
            }
    
        }
    

    番外

    自定義的持久層框架,我們就寫完了。這個實際上就是mybatis 的雛形,我們通過自己手動寫一個持久層框架,然後在來看mybatis 的源碼,就會清晰很多。下面這些類名在mybatis 中都有體現。

    這裏拋磚引玉,祝君閱讀源碼愉快。
    覺得有用的兄弟們記得收藏啊。

    厚顏無恥的求波點贊!!!

    本文由博客一文多發平台 OpenWrite 發布!

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    ※想知道最厲害的網頁設計公司"嚨底家"!

    ※別再煩惱如何寫文案,掌握八大原則!

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!

    ※回頭車貨運收費標準

    台中搬家公司費用怎麼算?

  • 海水溫度創高 極端氣候警鈴大作

    摘錄自2020年4月19日經濟日報報導

    根據美國國家環境資訊中心(NCEI),太平洋、大西洋及印度洋的部分海域溫度同創歷史新高,致使研究機構日益擔憂,未來六個月將醞釀適合颶風、野火及強烈雷暴雨等極端氣候發展的環境,使未來半年的極端氣候事件增加。

    科羅拉多州立大學(CSU)研究人員庫拉茲巴赫說,墨西哥灣的海水溫度為華氏76.3度(攝氏24.6度),比長期均溫高出華氏1.7度。若墨灣維持溫暖水溫,將增強從墨灣登陸風暴的威力。

    CSU發布的第一個2020年風暴報告預測,大西洋今年形成八個颶風的機率將高於平均值,而且6月1日起、為期六個月的颶風季期間,至少會有一個颶風將登陸美國。美國將在下月發布颶風預報。

    海洋
    全球變遷
    生態保育
    氣候變遷
    生物多樣性
    國際新聞
    太平洋
    大西洋
    印度洋
    颶風
    森林野火
    極端氣候
    全球暖化

    本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※別再煩惱如何寫文案,掌握八大原則!

    ※教你寫出一流的銷售文案?

    ※超省錢租車方案

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!

    ※回頭車貨運收費標準

  • 日本研究發現大氣污染導致心臟驟停者增加

    摘錄自2020年4月18日共同社報導

    日本川崎醫科大學(岡山縣)等團隊17日在美國醫學雜誌上發表研究結果稱,如果大氣污染物之一、細顆粒物PM2.5在大氣中的濃度上升,因在家中、室外等地心臟驟停被緊急送醫的人就會增加。

    研究團隊分析了2011年至2016年的約6年期間全國47個都道府縣被送醫的10萬人數據與各地點PM2.5濃度之間的關係。團隊成員、川崎醫科大學循環器內科學教授小島淳表示:「在首次全國範圍的研究中,弄清了大氣污染與心臟驟停有關。」

    據該團隊分析,若PM2.5在大氣中的濃度從某一標準上升每立方米10微克(1微克為百萬分之一克),因心臟驟停被送醫的人在全國增加1.6%。

    將日本分為三個大區進行調查後發現,愛知到大阪、高知的中央地區14個府縣上升5.9%。此外若僅限於5至10月溫暖的時期,全國增加2.3%。據稱均不清楚詳細原因。進行調查的6年間全國PM2.5平均濃度為每立方米13.9微克。各地濃度使用了各都道府縣政府所在地的觀測數值。

    空氣污染
    公害污染
    污染治理
    國際新聞
    日本
    心臟病
    PM2.5

    本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※別再煩惱如何寫文案,掌握八大原則!

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    ※超省錢租車方案

    ※教你寫出一流的銷售文案?

    網頁設計最專業,超強功能平台可客製化

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!

    台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

  • 武漢肺炎對全球食品業的衝擊 麵粉大缺貨 牛奶、啤酒、茶葉過剩成廚餘

    環境資訊中心綜合外電;姜唯 編譯;林大利 審校

    本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※教你寫出一流的銷售文案?

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※回頭車貨運收費標準

    ※別再煩惱如何寫文案,掌握八大原則!

    ※超省錢租車方案

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!

    ※推薦台中搬家公司優質服務,可到府估價

  • 人類全都隔離去了!海獅成群上岸逛大街 嚇壞流浪狗

    摘錄自2020年4月20日自由時報報導

    武漢肺炎疫情延燒全球,世界多國正在實施隔離封鎖令,民眾非必要不得外出,這也使得許多野生動物活動範圍因而變廣,在南美洲阿根廷的海岸城市,巨大的海豹跑進市區遊蕩,把流浪狗嚇壞了。

    根據《俄羅斯衛星通信社》報導,阿根廷知名旅遊勝地馬德普拉塔(Mar del Plata)因實施隔離政策,街道沒人,遊客不來,巨大的海獅得以進入市區四處散步,並沿著海岸追逐難得一見的人類,對平時少見的景象感到相當好奇,但也嚇壞不少原本生活在當地的流浪狗。

    生活環境
    國際新聞
    阿根廷
    控制疫情
    隔離
    海豹

    本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※超省錢租車方案

    ※別再煩惱如何寫文案,掌握八大原則!

    ※回頭車貨運收費標準

    ※教你寫出一流的銷售文案?

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    網頁設計最專業,超強功能平台可客製化

  • IUCN:亞當峰朝聖者腳下 斯里蘭卡瀕危兩棲類正在消失

    環境資訊中心綜合外電;黃鈺婷 翻譯;林大利 審校;稿源:Mongabay

    本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    網頁設計最專業,超強功能平台可客製化

    ※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

    ※回頭車貨運收費標準

    ※推薦評價好的iphone維修中心

    ※教你寫出一流的銷售文案?

    台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

    台中搬家公司費用怎麼算?

  • 英封城期間 販賣機賣肉和菜、直接向漁夫買 掀新商機

    環境資訊中心綜合外電;姜唯 編譯;林大利 審校

    本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!

    ※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

    ※回頭車貨運收費標準

    ※推薦評價好的iphone維修中心

    ※超省錢租車方案

    台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

    ※推薦台中搬家公司優質服務,可到府估價

  • 印度全國封鎖後 恆河上游部分河段可飲用

    摘錄自2020年4月24日中央社報導

    一般帶大量超級細菌的恆河,在印度全國封鎖近一個月後,因為幾乎沒有人類污染,瑜伽聖城瑞詩凱詩(Rishikesh)這段恆河河水,經政府檢測水質達到可飲用的標準。

    印度北部北阿坎德省(Uttarakhand)污染控制局最近從瑞詩凱詩及哈里德瓦(Haridwar)的恆河河段抽樣進行化驗,發現水質可以飲用。

    官方報告表示,哈基寶里河壇段的恆河水,生化需氧量(Biochemical oxygen demand)也下降20%。專家說,這意味著恆河水含氧量變高,水族可舒適地在河中呼吸。

    印度中央污染控制局(Central Pollution Control Board)近日發表的調查報告顯示,恆河36個監測站中,有27個監測站測得恆河水質已變為可安全沐浴和水中生物可安全生活的水準。

    生活環境
    土地水文
    土地利用
    國際新聞
    印度
    封城
    武漢肺炎
    恆河

    本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※回頭車貨運收費標準

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!

    ※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

    ※推薦評價好的iphone維修中心

    ※教你寫出一流的銷售文案?

    台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

    台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

  • 既能穿西裝又能幹苦活的全能选手,教授試駕全新菱智M5

    既能穿西裝又能幹苦活的全能选手,教授試駕全新菱智M5

    賬面數據來看,它不會讓你有很高昂的駕駛激情,但實際駕駛過程中,也是足夠使用的。菱智M5的離合器匹配很成熟,離合踏板幅度較大,需要稍微適應一下,等離合器到位后順勢給油就能完美起步,熟練的司機可以做到毫無頓挫感。

    自2007年第一代菱智面世以來,這款MpV已經在市場上走了9年之久。親民的定價、務實的用途,使得菱智在三四線城市中備受歡迎,而在大城市的貨運車中,也常能看見它的身影。但說實在的,把菱智系列當商務用車、甚至家用車的車主屈指可少,很大原因是因為它的產品價值被死死定在了貨運上,“撐不住面子”的它,很難上檯面。

    在新款菱智M5上,我們能看到它悄悄打上了“新商務”的標籤,不僅可以貨物運輸,還能兼顧商務接待,這不禁讓對這次試駕充滿了期待,畢竟之前試駕都以乘用車為主,MpV還真的不多。

    外觀設計、空間表現如何?

    新款菱智M5的外觀設計比起舊款有了不少的提升,鷹眼似的前大燈看上去銳利無比,四條格柵呈“展翼”狀,採用鍍鉻處理。外觀的升級是值得肯定的,起碼整體的氣質變化很明顯。

    菱智M5車身尺寸為:4745*1720*1940,軸距2800。內部空間是十分夠用的,特別是第三排座椅,1米83的身高坐進去一點壓迫感都沒有,別說更寬敞的第二排了。不過有利有弊,如果第三排座椅立起的情況下,後備箱的空間就顯得不足了。

    不過如果放下後排座椅,這輛菱智M5也會很好地發揮它的貨運本能,可輕鬆放進一台電腦桌。

    開起來是什麼感覺?

    試駕的這款菱智M5,搭載的是三菱4A92發動機以及5MT變速箱,最大轉速6000轉,最大馬力122,最高扭矩153牛米。賬面數據來看,它不會讓你有很高昂的駕駛激情,但實際駕駛過程中,也是足夠使用的。

    菱智M5的離合器匹配很成熟,離合踏板幅度較大,需要稍微適應一下,等離合器到位后順勢給油就能完美起步,熟練的司機可以做到毫無頓挫感。

    試駕的時候車上載着5個成年人,並開啟着空調。油門響應比較平緩,需要稍微深踩,降檔地板油時,轉速也不會提高的很积極,所以超車能力一般,但滿載情況下,也滿足市區需求了。不過如果做商務接待的話,平穩的駕駛才是最重要的,即便是貨運的話,這個動力也完全足夠了。

    菱智M5的底盤調教功底還是有的,採用雙橫臂扭桿彈簧獨立前懸架和鋼板彈簧后懸架。這樣的懸挂設計,側重承載能力,為商用物流運輸提供了可靠的保障,而且耐用性更能從容應對山路、爛路等複雜路況。

    順帶說一句,新款菱智M5的隔音做得不錯,怠速抖動在接受範圍內,基本上只有細微的風噪和胎噪,做工用料值得肯定。

    最後總結:

    菱智系列一直以來的銷量都很穩定,今年以來一直在1萬浮動上下,這與它空間大、外觀好看、內飾美觀、底盤耐用…等等優點是分不開的。更何況菱智M5的定價僅為7.19萬~8.49萬,所以也不必用更苛刻的眼光去看待了。務實耐用,貨運和商務都兼顧,這款車已經做得足夠好了。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

    ※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

    ※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※教你寫出一流的銷售文案?

    ※別再煩惱如何寫文案,掌握八大原則!