8.3 Spring Boot集成Scala混合Java开发
本章我们使用Spring Boot集成Scala混合Java开发一个Web性能测试平台。
使用到的相关技术:
后端:
- phantomjs
- scala
- java
- springboot
- velocity
- jpa
- maven
- mysql
前端:
- jquery
- bootstrap
- adminLTE
- html/css
Scala是一门JVM上的语言。它精心整合了面向对象和函数式编程语言,支持面向对象编程范式,支持函数式编程范式,语法动态简洁表达力丰富,具备静态强类型和丰富的泛型。
新建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>
</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>
</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应用的开发实例。
关于SpringBoot集成Scala开发,还可以参考本书中的另外的工程实例源码:
HTTP接口测试平台:
https://github.com/EasySpringBoot/lightsword
Teda自动化用例调度执行平台:
https://github.com/EasySpringBoot/teda