都說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 的差異,總體來說,思路並沒有變化,基本上只是習慣上的變化。