springboot 實戰之一站式開發體驗


  都說springboot是新形勢的主流框架工具,然而我的工作中並沒有真正用到springboot;

  都說springboot里面並沒有什么新技術,不過是組合了現有的組件而已,但是自己卻說不出來;

  都說springboot讓開發更簡單,然而對於剛轉換過來使用的時候總會發現各種不適應;

  網上查過許多的教程,下載過demo來玩,卻無法用於實戰,着實可惜。

  最近有個項目終於用springboot來開發了,一切從0開始,剛好可以練練手。來談談幾點經驗吧!(注:本文非教程,請當閑聊談資)

1. 入門?

  springboot 的入門demo在spring官網可以直接下載,可以使用 maven 開發,https://start.spring.io/ 下載下來,運行main()方法就可以啟動服務了。

  一個簡單的helloworld就ok了,是不是超簡單?再也不用復雜的搭建過程了。(不過說實話,這個過程相當於我之前有一套有一套代碼模板,然后改改名字就成了新項目代碼一樣,沒什么了不起)

  不過,有空的話還是有必要看一下完整點的入門demo教程: https://spring.io/guides/gs/rest-service/  (手動搭建服務很快這是真的)

2. 如何接入各常用組件及配置?

  這個需求是很強烈的,一個空白的框架是沒有啥用的,因為我們必定要基於: 數據庫、緩存、zk、mq、日志、mongo、頁面模板等等。。。
  所以,如何配置?
  三個步驟:
    1. 引入組件依賴 dependency;
    2. 在 bootstrap-xx.properties 文件中加入配置屬性;
    3. 在配置java文件中,new出相應實例或框架自己初始化實例以備用;
  就單是這點來說,其實springboot和spring的xml配置方式步驟是一樣一樣的,三步式導入。不過顯然java代碼寫得更復雜和難找,xml更直觀!(這里先忽略dependecy依賴的個數對比)

3. 如何做到加載動態配置?

  在使用xml配置的方式時,我們可以使用 spring 的 org.springframework.beans.factory.config.PropertyPlaceholderConfigurer 

  組件,去加載一個配置中心的值,從而實現替換各種連接的作用,使其脫離代碼的硬編碼;

    <!-- spring的屬性加載器,加載properties文件中的屬性 -->
    <bean class="com.xx.zk.property.PropertyPlaceholderConfigurer">
        <property name="systemPropertiesModeName" value="SYSTEM_PROPERTIES_MODE_OVERRIDE" />
        <property name="ignoreResourceNotFound" value="true" />
        <property name="locations">
            <list>
                <value>classpath*:/spring/conf.properties</value>
            </list>
        </property>
    </bean>

  那么,在springboot中是怎么做的呢? springboot 提供了多種配置文件共存的方式,比如: bootstrap-prod.properties, bootstrap-dev.properties, 用於區分測試環境和生產環境的配置而不互相影響;

  其大致原理為,環境准備好時,會觸發監聽器,然后加載相應配置文件:

    // - org.springframework.boot.context.event.EventPublishingRunListener
    @Override
    public void contextLoaded(ConfigurableApplicationContext context) {
        for (ApplicationListener<?> listener : this.application.getListeners()) {
            if (listener instanceof ApplicationContextAware) {
                ((ApplicationContextAware) listener).setApplicationContext(context);
            }
            context.addApplicationListener(listener);
        }
        // 加載 bootstrap.properties, bootstrap-dev.properties...
        // 在 ConfigFileApplicationListener 的 ApplicationPreparedEvent 事件中觸發
        this.initialMulticaster.multicastEvent(
                new ApplicationPreparedEvent(this.application, this.args, context));
    }

  如果要使用配置中心,可以直接使用 spring-cloud-config 組件,配置即可,不過說實話這種配置中心着實難用,有能力的話都應自行定制開發一個統一配置中心(畢竟配置中心也是個技術活);spring cloud config 使用可以參考這篇博文: http://blog.51cto.com/zero01/2171735 ,或者查看官網教程!

4. 如何注冊 beans ?

  1. 和spring一樣,直接使用 @Service, @Controller, @Component... 注解直接注冊簡單的 bean; 

  2. 對於一些復合bean組件,需要單獨配置,如數據庫連接:

    如 spring 中druid連接池的xml配置是這樣的:

    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
          destroy-method="close">
        <property name="url" value="${jdbc.url}" />
        <property name="driverClassName" value="${jdbc.driver}" />
        <property name="maxActive" value="${pool.maxPoolSize}" />
        <property name="username" value="${jdbc.username}" />
        <property name="password" value="${jdbc.password}" />
        <property name="removeAbandoned" value="true" />
        <property name="removeAbandonedTimeout" value="${pool.removeAbandonedTimeout}" />
        <property name="maxWait" value="${pool.maxWait}" />
        <property name="timeBetweenEvictionRunsMillis" value="${pool.timeBetweenEvictionRunsMillis}" />
        <property name="minEvictableIdleTimeMillis" value="${pool.minEvictableIdleTimeMillis}" />
        <property name="validationQuery" value="${pool.validationQuery} " />
        <property name="testWhileIdle" value="true" />
        <property name="testOnBorrow" value="false" />
        <property name="testOnReturn" value="false" />
    </bean>

  而在 springboot 中,則是使用 java 代碼直接創建:

    @Bean(name = "druidDataSource")
    public DruidDataSource druidDataSource(){
        DruidDataSource ds = new DruidDataSource();
        ds.setUrl(config.getJdbcUrl());
        ds.setDriverClassName(config.getDriverName());
        ds.setMaxActive(config.getMaxPoolSize());
        ds.setUsername(config.getJdbcUserName());
        ds.setPassword(config.getJdbcPwd());
        ds.setRemoveAbandoned(true);
        ds.setMaxWait(config.getJdbcMaxWait());
        ds.setTimeBetweenEvictionRunsMillis(config.getTimeBetweenEvictionRunsMillis());
        ds.setMinEvictableIdleTimeMillis(config.getMinEvictableIdleTimeMillis());
        ds.setValidationQuery(config.getValidationQuery());
        ds.setTestWhileIdle(true);
        ds.setTestOnBorrow(false);
        ds.setTestOnReturn(false);
        return ds;
    }

  3. 還有一種特殊的加載方式,值得注意,就是使用了 @Bean 注解,但是其直接new了一對象返回:

    @Bean(name = "directHelloService")
    public HelloService directHelloService(){
        HelloService service = new HelloService();
        return service;
    }

  這個有什么問題呢?因為我們的 service 一般都會依賴於其他的服務,所以,往往都會有依賴注入的過程,但是你使用了一個new創建,則沒有了依賴注入問題了。因此,當你想直接使用這個服務的時候,很可能就會拿到一些空對象;

  那怎么辦?三個辦法: 

    1. 沒事就不要直接new有依賴的對象了;
    2. 如果實在要new,需要在new的對象上添加注解 @DependsOn 注解標明需要依賴的組件,這樣,在使用的時候就會再次去檢測依賴,從而完成依賴注入了;
    3. 自己手動完成依賴注入;
    4. 將加載動作委托給springContext, 比如使用 getBean("xxx") 的方式獲取,使其回歸spring的自動依賴注入過程;(沒有試驗過)

  單從這一點來講,想完全擺脫 xml 束縛的 springboot, 還是顯得有些力不從心! 另外,使用 java 配置文件的另一個不好的地方是,配置文件散落在各處,很不直觀!

5. 日志如何記錄?

  日志是必備工具。所以 springboot 默認集成了 logback 的日志組件,所以,我們要做的只是,配置好打印屬性就好了;如在 bootstrap.properties 文件中添加如下:

logging.config=classpath:logback.xml

   意思就是說,你將配置寫入到 resources/logback.xml 中,其中的配置規則同理自不必細說;

6. 如何自定義 RequestMappingHandlerMapping ?

  做一個web應用時,對webmvc的定制化配置是一定的。因為我們的包路徑查找,可能會有自己一些特定的規則,所以需要自定義 RequestMappingHandlerMapping。這在 spring 中,則只需要注冊一個requestMappingBean就可以了(要先排除系統自動掃描 @Controller 注解),如:

    <bean name='requestMappingHandlerMapping'
        class='com.xxx.cust.URLRequestMappingHandlerMapping'>
        <property name="interceptors">
            <list>
                <!-- 添加會話攔截器 -->
                <bean class="com.xxx.interceptor.SessionInterceptor">
                </bean>
            </list>
        </property>
    </bean>

  而在springboot中,好像就不是那么回事了,它變成是這樣的,先繼承一個 WebMvcConfigurationSupport 的基礎組件,然后自定義各種配置如:

@Configuration
public class SpringMVCConfig extends WebMvcConfigurationSupport {
    @Resource
    private SessionInterceptor sessionInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        registry.addInterceptor(sessionInterceptor)
                .excludePathPatterns(
                "/error");
    }

    /**
     * 添加自定義的Converters和Formatters.
     */
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new StringToDateConverter("yyyy-MM-dd HH:mm:ss"));
    }
    
    /**
     * Protected method for plugging in a custom subclass of
     * {@link RequestMappingHandlerMapping}.
     * @since 4.0
     */
    protected RequestMappingHandlerMapping createRequestMappingHandlerMapping() {
        return new RequestMappingHandlerMapping();
    }
    
    /**
     * Protected method for plugging in a custom subclass of
     * {@link RequestMappingHandlerAdapter}.
     * @since 4.3
     */
    protected RequestMappingHandlerAdapter createRequestMappingHandlerAdapter() {
        return new RequestMappingHandlerAdapter();
    }

}

  如上配置的依據是,在其父類 WebMvcConfigurationSupport 中,會創建一個 requestMappingHandlerMapping 的 bean, 而創建的過程,將方法暴露給了一個可供繼承覆寫的 createRequestMappingHandlerMapping 的方法,從而達到自定義 RequestMappingHandlerMapping 的目的。同理於 RequestMappingHandlerAdapter 。

  而對於其他的各種自定義組件的接入,則按照文檔說明來即可。對於一些通用的組件,一般都會有 xxx-starter 提供,從而可以避免n多的依賴配置,這也是springboot的一重要開發優勢吧。畢竟,spring里面,你需要知道的太多了!

  綜上,咱們就可以規規矩矩地寫業務代碼了。總體的步驟就是:寫配置變量到properties文件,使用 @Configuration 讀取配置;實例化 bean 以供使用;

  至於 xml 和 properties 的習慣問題,咱們就先不說了。

7. 最后,還有一個關鍵問題,打包部署?

  springboot 往往是直接啟動一個 main() 方法來運行的,和 基於web容器的應用是不一樣的。(內嵌容器)

  在spring中,我們一般是通過maven打一個war包,然后部署到tomcat中。而在 springboot 中,則不一定要這么干了(甚至是不建議這么干),所以需要打一個 jar 包。

  打jar包部署有兩個問題:

    1. jar包中的其他第三方依賴怎么辦?

    2. 部署維護交給誰?

  一、針對第三方的jar包依賴問題,我們可以有兩個解決方法:1. 將第三方的jar包打包進項目的jar包中; 2. 將依賴的jar包放抽離出來放到一個獨立的lib庫文件夾中,啟動應用時再指定加載位置;各有優劣,一個是會導致jar包體積變大,一個是會導致開發維護困難(這是個大問題)。當然我們應該會選將其打包到一個jar中,一點體積是不會難倒我們的。3. 其實我覺得還有一種打包方式,就是將所有可能用到的class文件,全部解壓出來打包到最終的jar包中,這樣既做到體積小,又做到代碼維護容易,但是可能會有些難度,因為你很難確定哪些class文件是不用的,所以一般也不敢排除(白干了);

    打jar包的依賴,可以參照如下插件配置:

    <build>
        <resources>
            <resource>
                <directory>src/main/java</directory>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
            </resource>
        </resources>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>com.xxx.service.StartApplication</mainClass>
                            <addClasspath>true</addClasspath>
                            <classpathPrefix>lib/</classpathPrefix>
                        </manifest>
                    </archive>
                    <classesDirectory>
                    </classesDirectory>
                </configuration>
            </plugin>
        </plugins>
    </build>

  注意:錯誤的配置可能導致依賴包嵌入有問題,或者切換環境不成功!

  二、針對部署維護的問題,則依賴於你想運行的環境,如果你想使用原來的 tomcat 這種web容器運行服務,則無需另外擔心維護問題,因為tomcat已經有了這些設備。而如果你使用jar包運行,則需要自行編寫維護腳本了,其實功能也不外乎幾個:

    1. 啟動;

    2. 停止;

    3. 查看狀態;

    4. springboot 需要的功能,就是支持動態修改配置屬性,從而使測試環境與生產環境隔離;

#!/bin/sh

## project info
SERVICE_DIR=/www/xxx
SERVICE_NAME=myproject-1.0.0-SNAPSHOT
SPRING_PROFILES_ACTIVE=prod

## java env
JAVA_HOME=/usr/java/jdk1.8.0_101
pidfile="/opt/springboot/xxx.pid"
JAVA_OPTS="$JAVA_OPTS -server -Xms512m -Xmx2048m -Dfile.encoding=UTF-8 -Xloggc:/opt/springboot/logs/xxx_gc.log -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/opt/springboot/logs/"

case "$1" in 
    start)
        pid=`ps -ef | grep -w "${SERVICE_NAME}" | grep -w "java"| grep -v "grep" | awk '{print $2}'`
        if [ "${pid}" = "" ]; then
            if [ "$2" != "" ]; then
                SPRING_PROFILES_ACTIVE=$2
            fi
            echo " - Starting ${SERVICE_NAME} ... "
            echo " - Using JAVA_HOME: $JAVA_HOME ..."
            echo " - Using Environment: spring.profiles.active=${SPRING_PROFILES_ACTIVE}"
            exec nohup ${JAVA_HOME}/bin/java ${JAVA_OPTS} -jar ${SERVICE_DIR}/${SERVICE_NAME}\.jar --spring.profiles.active=${SPRING_PROFILES_ACTIVE} >/dev/null 2>&1 &
            echo "$!" > ${pidfile};
            echo " - Congraduations!!! Started project [${SERVICE_DIR}/${SERVICE_NAME}.jar] success, pid=$! ."
        else
            echo "- Oops!!! ${SERVICE_NAME} is alreaddy started @pid=${pid}, kill it ?"
        fi
        ;;
        
    stop)
        pid=`ps -ef | grep -w "${SERVICE_NAME}" | grep -w "java" | grep -v "grep" | awk '{print $2}'`
        rm -rf ${pidfile};
        if [ "${pid}" = "" ]; then
            echo " - ${SERVICE_NAME} is Already stopped."
        else
            echo " - Stopping ${SERVICE_NAME} by kill -15 ${pid} ...";
            kill -15 ${pid}
            sleep 1
            pid2=`ps -ef | grep -w "${SERVICE_NAME}" | grep -w "java" | grep -v "grep" | awk '{print $2}'`
            if [ "${pid2}" = "" ]; then
                echo " - ${SERVICE_NAME} stopped success !!! "
            else
                kill -9 ${pid2}
                echo " - Stop Failed! ${SERVICE_NAME} stop error, force kill ${pid2} !!!"
            fi
        fi
        ;;
        
    restart)
        $0 stop
        sleep 1
        $0 start $2
        ;;  
    status)
        pid=`ps -ef | grep -w "${SERVICE_NAME}" | grep -w "java" | grep -v "grep" | awk '{print $2}'`;
        if [ "${pid}" = "" ]; then
            echo " - Oops!!! ${SERVICE_NAME} is Already stopped."
        else
            # echo -e " - ${SERVICE_NAME} is ruuning, \033[36m pid=${pid} \033[0m .";
            echo -e " - ${SERVICE_NAME} is ruuning, pid=${pid} .";
            echo -e " - ${SERVICE_NAME} 's server port is: `netstat -tunlp | grep "${pid}/" | awk '{print $1 " " $4;}'`.";
            echo -e " - ${SERVICE_NAME} up info:`ps -eo pid,lstart,etime,cmd | grep ${SERVICE_NAME} | grep -v "grep" | awk '{print "startTime:"$2" "$3" "$4" "$5" "$6", uptime:" $7}'`.";
            echo -e " - Current MEMORY Usage: `free -h | grep "Mem:" | awk '{print "total: "$2", used: "$3".";}'`.";
            echo -e " - Current CPU Usage: `top -bn 1 -i -c | sed -n '3p'` ";
            
        fi
        ;;        
    *)
        echo " - Wrong command!!! Usage: $0 [start|stop|restart|status] [dev|test|prod]"
        ;;  
esac

  運行方式如下:

springboot_xxx [start|stop|restart|status] [dev|test|prod]

  以上,就是一些關於 springboot 使用的一些實踐歷程,對比 spring 和 springboot 的差異,總體來說,思路並沒有變化,基本上只是習慣上的變化。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM