第9章 Spring Boot內建Scala混合Java開發
本章我們使用Spring Boot內建Scala混合Java開發一個Web性能測試平台。
使用到的核心技術:
後端:
- phantomjs
- scala
- java
- springboot
- velocity
- jpa
- maven
- mysql
前端:
- jquery
- bootstrap
- adminLTE
- html/css
建立maven工程,配置pom
添加SpringBoot parent依賴
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.5.RELEASE</version>
</parent>
因為我們要使用scala + java混合開發,添加scala依賴
<!-- scala -->
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
<version>${scala.version}</version>
</dependency>
然後,我們使用velocity模闆引擎,資料庫ORM層使用jpa
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-velocity</artifactId>
</dependency>
建構周期build插件配置:
<plugin>
<groupId>org.scala-tools</groupId>
<artifactId>maven-scala-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
<configuration>
<recompileMode>incremental</recompileMode>
<scalaVersion>${scala.version}</scalaVersion>
<launchers>
<launcher>
<id>app</id>
<mainClass>com.light.sword.ylazy.LightSwordApplication</mainClass>
<args>
<arg>-deprecation</arg>
</args>
<jvmArgs>
<jvmArg>-Xms256m</jvmArg>
<jvmArg>-Xmx2048m</jvmArg>
</jvmArgs>
</launcher>
</launchers>
</configuration>
<dependencies>
<!-- spring熱部署-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>springloaded</artifactId>
<version>1.2.6.RELEASE</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<!--<configuration>-->
<!--<fork>true</fork><!– 如果沒有該項配置,肯呢個devtools不會起作用,即應用不會restart –>-->
<!--</configuration>-->
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
</execution>
</executions>
<configuration>
<includeScope>system</includeScope>
</configuration>
</plugin>
其中build周期中的maven-scala-plugin是編譯期依賴,scala代碼需要scala的compiler,是以在maven構建過程中,使用一個編譯scala代碼的maven插件.這是typesafe(scala背後的公司)的工程師Josh Suereth開發的,遵循maven插件開發規範.
然後,org.scala-lang:scala-library是Scala應用運行時的依賴.這樣,我們就可以像使用scala來開發SpringBoot應用了。
建構标準maven工程目錄
工程目錄如下:
配置資料庫
我們資料庫使用mysql,ORM架構使用spring-jpa,在application.properties配置如下:
#mysql
spring.datasource.url = jdbc:mysql://localhost:3306/ylazy?useUnicode=true&characterEncoding=UTF8
spring.datasource.username = root
spring.datasource.password = root
spring.datasource.driverClassName = com.mysql.jdbc.Driver
spring.datasource.max-active=0
spring.datasource.max-idle=0
spring.datasource.min-idle=0
spring.datasource.max-wait=10000
spring.datasource.max-wait-millis=31536000
# Specify the DBMS
spring.jpa.database = MYSQL
# Show or not log for each sql query
spring.jpa.show-sql = true
# Hibernate ddl auto (create, create-drop, update)
spring.jpa.hibernate.ddl-auto = update
# Naming strategy
spring.jpa.hibernate.naming-strategy = org.hibernate.cfg.ImprovedNamingStrategy
# stripped before adding them to the entity manager)
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5Dialect
寫領域實體層代碼
package com.light.sword.ylazy.entity
import java.util.Date
import javax.persistence.{Entity, GeneratedValue, GenerationType, Id}
import scala.beans.BeanProperty
import scala.language.implicitConversions
@Entity
class LazyTask {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@BeanProperty
var id: Integer = _
@BeanProperty
var url: String = _
@BeanProperty
var name: String = _
//用例狀态: -1未執行 0失敗 1成功
@BeanProperty
var state: Integer = _
@BeanProperty
var owner: String = _
@BeanProperty
var resultJson: String = _
@BeanProperty
var executeTimes: Integer = _
@BeanProperty
var executor: Integer = _
@BeanProperty
var gmtCreate: Date = _
@BeanProperty
var gmtModify: Date = _
}
Scala中可以為類、方法、字段、局部變量和參數添加注解,與Java一樣。可以同時添加多個注解,先後順序沒有影響。 在Scala中,注解可以影響編譯過程,比如@BeanProperty注解。
我們使用@Entity注解标記資料庫實體類LazyTask,jpa會自動對應到資料表lazy_task, 同時我們使用@BeanProperty标記實體bean裡面的屬性字段,jpa會自動映射到表裡面的字段,自動映射對應的類型。用scala的@BeanProperty注解,會自動生成JavaBeans的getter,setter方法。
Dao層代碼
package com.light.sword.ylazy.dao
import java.util.List
import com.light.sword.ylazy.entity.LazyTask
import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.CrudRepository
// JavaConversions
import scala.language.implicitConversions
trait LazyTaskDao extends CrudRepository[LazyTask, Integer] {
def findAll(): List[LazyTask]
def save(t: LazyTask): LazyTask
def findOne(id: Integer): LazyTask
@Query(value = "SELECT * FROM lazy_task where url like '%?1%'", nativeQuery = true)
def listByUrl(url: String): List[LazyTask]
@Query(value = "SELECT * FROM lazy_task where name like '%?1%'", nativeQuery = true)
def listByName(name: String): List[LazyTask]
}
Controller層代碼
package com.light.sword.ylazy.controller
import java.util.Date
import com.light.sword.ylazy.config.DomainConfig
import com.light.sword.ylazy.dao.LazyTaskDao
import com.light.sword.ylazy.engine.PhantomjsExecutor
import com.light.sword.ylazy.entity.LazyTask
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.ui.Model
import org.springframework.web.bind.annotation._
import org.springframework.web.servlet.ModelAndView
@RestController
class LazyTaskController @Autowired()(val lazyTaskDao: LazyTaskDao,
val phantomjsExecutor: PhantomjsExecutor,
val domainConfig: DomainConfig) {
@RequestMapping(value = {
Array("/newTask.do")
})
def newTask_do() = {
new ModelAndView("ylazy/newTask")
}
@RequestMapping(value = {
Array("/ylazy/newTask")
}, method = Array(RequestMethod.POST))
@ResponseBody
def newTask(@ModelAttribute lazyTask: LazyTask) = {
lazyTask.gmtCreate = new Date
lazyTask.gmtModify = new Date
lazyTask.executeTimes = 0
lazyTask.state = -1
lazyTaskDao.save(lazyTask)
}
@RequestMapping(value = {
Array("/list.do")
})
def list_do(model: Model) = {
model.addAttribute("lazyTaskList", lazyTaskDao.findAll())
model.addAttribute("domainName", domainConfig.getDomainName)
model.addAttribute("port", domainConfig.getPort)
new ModelAndView("ylazy/list")
}
/**
* 擷取一條任務記錄
*
* @param id
* @return
*/
@RequestMapping(value = {
Array("/lazytask")
}, method = Array(RequestMethod.GET))
@ResponseBody
def findOne(@RequestParam(value = "id") id: Integer) = lazyTaskDao.findOne(id)
/**
* 擷取一條任務記錄的傳回json
*
* @param id
* @return
*/
@RequestMapping(value = {
Array("/result/{id}")
}, method = Array(RequestMethod.GET))
@ResponseBody
def findResultJson(@PathVariable(value = "id") id: Integer) = lazyTaskDao.findOne(id).resultJson
/**
* 執行任務
* @param id
* @return
*/
@RequestMapping(value = {
Array("/ylazy/runTask")
}, method = Array(RequestMethod.GET))
@ResponseBody
def runTask(@RequestParam(value = "id") id: Integer) = {
phantomjsExecutor.ylazyById(id)
}
@RequestMapping(value = {
Array("/ylazy")
}, method = Array(RequestMethod.GET))
@ResponseBody
def ylazy(@RequestParam(value = "url") url: String) = phantomjsExecutor.ylazy(url)
}
前端代碼
對應的前端模闆代碼我們放在src/main/resources/templates/ylazy/list.html
#parse("/common/header.html")
#parse("/common/aside.html")
<!-- Content Wrapper. Contains page content -->
<div class="content-wrapper">
<!-- Content Header (Page header) -->
<section class="content-header">
<h1>
YLazy
<small>Web性能測試平台</small>
</h1>
<!--<ol class="breadcrumb">-->
<!--<li><a href="#"><i class="fa fa-dashboard"></i> Home</a></li>-->
<!--<li class="active">Dashboard</li>-->
<!--</ol>-->
</section>
<section class="content">
<div class="row">
<div class="box box-success">
<div class="box-header">
<i class="fa fa-sticky-note-o"></i>
<h3 class="box-title">新增任務</h3>
</div>
<form class="box-body" id="newTaskForm">
<div class="form-group">
<label>任務名稱</label>
<input name='name' class="form-control">
</div>
<div class="form-group">
<label>URL</label>
<input name='url' class="form-control">
</div>
<div class="form-group">
<button id='newTaskBtn' type="button" class="btn-sm btn-success">
<i class="fa fa-plus"></i>
添加
</button>
</div>
</form>
</div>
</div>
<div class="row">
<div class="box box-success">
<div class="box-header">
<i class="fa fa-list-alt"></i>
<h3 class="box-title">任務清單</h3>
</div>
<div class="box-body">
<table id="dataTable"
class="table table-hover table-condensed table-responsive">
<thead>
<tr>
<th>Id</th>
<th>名稱</th>
<th>URL</th>
<th>運作次數</th>
<th>更新時間</th>
<th>執行結果</th>
<th>操作</th>
<th>運作狀态</th>
</tr>
</thead>
<tbody>
#foreach ($t in $lazyTaskList)
<tr>
<td>$!t.id</td>
<td>$!t.name</td>
<td><a target="_blank" href="$!t.url">$!t.url</a></td>
<td>$!t.executeTimes</td>
#set($testTime=$!DateTool.format('yyyy-MM-dd HH:mm:ss', $t.gmtModify))
<td>$testTime</td>
<td>
<button onclick='reportDetail("$testTime","$t.url",$t.id)' type="button"
class="btn-sm btn-link">
檢視
</button>
</td>
<td>
<button id='btn-$t.id'
type="button"
data-loading-text="執行中"
class='btn-sm btn-success text-center'
autocomplete="off"
onclick='runTest($t.id,this)'>運作
</button>
<p id="msg-$t.id"></p>
</td>
<td id="result-$t.id"></td>
</tr>
#end
</tbody>
</table>
</div>
</div>
</div>
</section>
</div>
<script>
function reportDetail(testTime, url, id) {
var detailUrl = "harviewer/index.htm?testTime=" + encodeURIComponent(testTime) +
"&testUrl=" + url +
"&path=http://$domainName:$port/result/" + id;
window.open(detailUrl, "_blank");
}
function runTest(id, thisBtn) {
$(thisBtn).button('loading');
$(thisBtn).attr('disabled', 'disabled');
var resultId = '#result-' + id;
/**
* 運作任務
*/
var url = 'ylazy/runTask?id=' + id;
$.ajax({
url: url,
success: function (data) {
if (data) {
$(thisBtn).button('reset');
$(resultId).html('<p style="color: #00a65a;font-size: 12px;">執行成功</p>');
} else {
$(thisBtn).button('reset');
$(resultId).html('<p style="color:red;font-size: 12px;">執行失敗</p>');
}
}
});
}
$(function () {
$('#newTaskBtn').on('click', function () {
var data = $('#newTaskForm').serialize();
var url = 'ylazy/newTask';
//新增任務
$.ajax({
url: url,
data: data,
type: 'POST',
success: function (result) {
if (result) {
BootstrapDialog.show({
title: '新增任務',
message: '響應結果:' + JSON.stringify(result, null, 2),
type: BootstrapDialog.TYPE_SUCCESS,
closable: false,
cssClass: 'dialog_mar',
buttons: [{
label: '确認',
cssClass: 'con_btn',
action: function (dialogRef) {
dialogRef.close();
location.reload();
}
}, {
label: '取消',
action: function (dialogRef) {
dialogRef.close();
}
}]
});
} else {
BootstrapDialog.show({
title: '新增任務',
message: '添加失敗', type: BootstrapDialog.TYPE_DANGER,
closable: false,
cssClass: 'dialog_mar',
buttons: [{
label: '确認',
cssClass: 'con_btn',
action: function (dialogRef) {
dialogRef.close();
location.reload();
}
}, {
label: '取消',
action: function (dialogRef) {
dialogRef.close();
}
}]
});
}
}
});
});
//任務清單datatables
var dataTableOptions = {
"bDestroy": true,
dom: 'lfrtip',
"paging": true,
"lengthChange": true,
"searching": true,
"ordering": true,
"info": true,
"autoWidth": true,
"processing": true,
"stateSave": true,
responsive: true,
fixedHeader: false,
order: [[3, "desc"]],
"aLengthMenu": [7, 10, 20, 50, 100, 200],
language: {
"search": "<div style='border-radius:10px;margin-left:auto;margin-right:2px;width:760px;'>_INPUT_ <span class='btn-sm btn-success'>搜尋</span></div>",
paginate: {//分頁的樣式内容
previous: "上一頁",
next: "下一頁",
first: "第一頁",
last: "最後"
}
},
zeroRecords: "沒有内容",//table tbody内容為空時,tbody的内容。
//下面三者構成了總體的左下角的内容。
info: "總計 _TOTAL_ 條,共 _PAGES_ 頁,_START_ - _END_ ",//左下角的資訊顯示,大寫的詞為關鍵字。
infoEmpty: "0條記錄",//篩選為空時左下角的顯示。
infoFiltered: ""//篩選之後的左下角篩選提示
};
$('#dataTable').DataTable(dataTableOptions);
})
</script>
#parse("/common/footer.html")
完整的工程源代碼:
https://github.com/EasySpringBoot/ylazy運作測試
在pom.xml所在目錄,指令行運作:
mvn clean scala:compile scala:run -Dlauncher=app
浏覽器通路:
http://localhost:9050/list.do你将看到如下頁面:
小結
本章給出了一個使用Scala進行SpringBoot應用的開發執行個體。
Scala是一門JVM上的語言。它精心整合了面向對象和函數式程式設計語言,支援面向對象程式設計範式,支援函數式程式設計範式,文法動态簡潔表達力豐富,具備靜态強類型和豐富的泛型。