springboot+scala+slick+react实现微服务

springboot是搭建web微服务的简化框架,本文将springboot与scala和slick以及react集成,实现简单的前后端分离的restful api形式的web服务demo

代码结构

springboot+scala+slick+react实现微服务_第1张图片

其中

  • java_scala目录下代码支持scala和java混编
  • frontend里面是前端的reactjs代码

步骤

1 创建springboot项目

可以通过官网generate模板项目也可以建立空的maven工程再添加组件

2 添加scala支持

在maven配置pom.xml里面需要加入
scala语言版本

.version>2.12.3.version>
.compat.version>2.12.compat.version>

scala语言编译

<dependency>
    <groupId>org.scala-langgroupId>
    <artifactId>scala-libraryartifactId>
    <version>${scala.version}version>
dependency>
<dependency>
    <groupId>org.scala-langgroupId>
        <artifactId>scala-compilerartifactId>
        <version>${scala.version}version>
dependency>

scala maven插件

<plugin>
    <groupId>net.alchim31.mavengroupId>
    <artifactId>scala-maven-pluginartifactId>
    <version>3.2.1version>
    <executions>
        <execution>
            <id>compile-scalaid>
            <phase>compilephase>
            <goals>
                <goal>add-sourcegoal>
                <goal>compilegoal>
            goals>
        execution>
        <execution>
            <id>test-compile-scalaid>
            <phase>test-compilephase>
            <goals>
                <goal>add-sourcegoal>
                <goal>testCompilegoal>
            goals>
        execution>
    executions>
    <configuration>
        <recompileMode>incrementalrecompileMode>
        <scalaVersion>${scala.version}scalaVersion>
        <args>
            <arg>-deprecationarg>
        args>
        <jvmArgs>
            <jvmArg>-Xms64mjvmArg>
            <jvmArg>-Xmx1024mjvmArg>
        jvmArgs>
    configuration>
plugin>

3 编写scala启动文件

主类

package org.tashaxing.SpringbootScalaDemo

import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
import org.springframework.boot.autoconfigure.{EnableAutoConfiguration, SpringBootApplication}

@SpringBootApplication
class BootConfig

object SpringbootScalaDemoApplication extends App {
    SpringApplication.run(classOf[BootConfig])
}

controller里面的request mapping

@RestController // here must be restcontroller
@RequestMapping(Array("/scalatest"))
class ScalaTestController @Autowired()(private val scalaModelQuery: ScalaModelQuery)
{
    // ---- normal operation
    // root test
    @GetMapping
    def root = "hello scala springboot"

    // get test with short mapping
    @GetMapping(Array("/string1"))
    def getTest1(): String = {
        return "scala test1 results"
    }

    // get test with whole mapping
    @RequestMapping(value = Array("/string2"), method = Array(RequestMethod.GET))
    def getTest2(): String = {
        return "scala test2 results"
    }

    // get int list
    @GetMapping(Array("/list"))
    def getIntList(): Array[Int] = {
        return Array(1, 2, 3, 4)
    }
}

4 返回json数据

springboot处理网络请求,如果想返回json格式的数据,需要根据springboot的规则定义数据结构,否则常规的class或者map之类数据无法正常被序列化

首先需要定义entity数据结构,可以写成case class和普通class,注意各种annotation的限定

package org.tashaxing.SpringbootScalaDemo.model

import java.lang.Long
import javax.persistence.{Entity, GeneratedValue, Id, Table}
import javax.validation.constraints.NotNull

import scala.beans.BeanProperty
import org.hibernate.validator.constraints.{NotBlank, NotEmpty}

import scala.annotation.meta.field

//@Table(name = "scala_model") // define mysql table name
//@Entity
//case class ScalaModel(
//    @(Id @field) @(GeneratedValue @field) @BeanProperty var id: Long,
//    @BeanProperty @(NotEmpty @field) var name: String, // @field is a must
//    @BeanProperty @(NotEmpty @field) var age: Int
//)

@Table(name = "scala_model") // define mysql table name
@Entity
class ScalaModel
{
    @Id
    @GeneratedValue
    @BeanProperty
    var id: Long = _

    @BeanProperty
    @NotBlank            // we can make sure it not empty
    var name: String = _

    @BeanProperty
    @NotNull
    var age: Int = 18 // we can define default value here
}

在controller中

// get object(frontend will get json structure)
   @GetMapping(Array("/object"))
   def getObject(): ScalaModel = {
       val model = new ScalaModel // if it is defined with class
       model.id = 3L
       model.name = "lucy"
       model.age = 21

//        val model = ScalaModel(5L, "lily", 23) // if it is defined with abstract class
       return model
   }

5 数据库访问

pom.xml中添加jpa和mysql组件

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-data-jpaartifactId>
dependency>
<dependency>
    <groupId>mysqlgroupId>
    <artifactId>mysql-connector-javaartifactId>
dependency>

创建数据库

mysql> create database db_example; 
mysql> create user 'springuser'@'localhost' identified by 'ThePassword'; 
mysql> grant all on db_example.* to 'springuser'@'localhost';  the new user on the newly created database

springboot+scala+slick+react实现微服务_第2张图片

jdbc连接数据库

配置数据库,在项目的properties文件中

#datasource
spring.jpa.hibernate.ddl-auto=update
spring.datasource.url=jdbc:mysql://localhost:3306/db_example?useUnicode=true&characterEncoding=UTF-8&useSSL=false
spring.datasource.username=springuser
spring.datasource.password=ThePassword

注意要在model里面绑定表名
@Table(name = "scala_model")

构造repository和service套接层
repository用来绑定数据库表

package org.tashaxing.SpringbootScalaDemo.repository

import org.springframework.data.jpa.repository.{JpaRepository, Query}
import org.tashaxing.SpringbootScalaDemo.model.ScalaModel

// remember to use java List and Long here
import java.util.List
import java.lang.Long

trait ScalaModelRepository extends JpaRepository[ScalaModel, Long]
{
    // normal find function from db
    def findByName(name: String): List[ScalaModel]

    // self defined function from db by sql query
//    @Query(value = "select name from scala_model where age < 22")
//    def findNameOfAge(): List[Object]
}

service用来定义实际的数据库操作

package org.tashaxing.SpringbootScalaDemo.repository

import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.beans.factory.annotation.Autowired
import scala.reflect.ClassTag
import java.lang.Long
import org.springframework.data.domain.Page
import java.util.List
import org.springframework.stereotype.Service
import javax.transaction.Transactional
import java.lang.Boolean
import org.springframework.data.domain.PageRequest

// base query helper for all datatype and table
@Service
abstract class BaseQuery[T: ClassTag] {

  /** spring data jpa dao */
  @Autowired val jpaRepository: JpaRepository[T, Long] = null

  /**
    * add record
    *
    * @param s
    * @return T
    */
  def save[S <: T](s: S): T = jpaRepository.save(s)

  /**
    * delete record by id
    *
    * @param id 数据Id
    * @return Unit
    */
  @Transactional
  def delete(id: Long): Unit = jpaRepository.delete(id)

  /**
    * delete by batch
    *
    * @param lists
    * @return Unit
    */
  @Transactional
  def delete(lists: List[T]): Unit = jpaRepository.delete(lists);

  /**
    * update recorde by id
    *
    * @param s
    * @return T
    */
  @Transactional
  def update[S <: T](s: S) = jpaRepository.save(s)

  /**
    * find recorde by id
    *
    * @param id 数据Id
    * @return T
    */
  def find[S <: T](id: Long): T = jpaRepository.findOne(id)

  /**
    * query all
    *
    * @return List[T]
    */
  def findAll[S <: T]: List[T] = jpaRepository.findAll

  /**
    * query by id batch
    *
    * @return List[T]
    */
  def findAll[S <: T](ids: List[Long]): List[T] = jpaRepository.findAll(ids)

  /**
    * count
    *
    * @return Long
    */
  def count: Long = jpaRepository.count

  /**
    * check recorde exits
    *
    * @param id
    * @return Boolean
    */
  def exist(id: Long): Boolean = jpaRepository.exists(id)

  /**
    * query page
    *
    * @param page     start page
    * @param pageSize page number
    * @return Page[T]
    */
  def page[S <: T](page: Int, pageSize: Int): Page[T] = {
    var rpage = if (page < 1) 1 else page;
    var rpageSize = if (pageSize < 1) 5 else pageSize;
    jpaRepository.findAll(new PageRequest(rpage - 1, pageSize))
  }

}

controller中添加restful的api用于数据库增删改查

// get
@GetMapping(value = Array("/findid/{id}"))
def findById(@PathVariable(value = "id") id: Long): ScalaModel =
{
    return scalaModelQuery.find(id)
}

@GetMapping(Array("/findname/{name}"))
def findByName(@PathVariable("name") name: String): List[ScalaModel] =
{
    return scalaModelQuery.findByName(name)
}

// post
@PostMapping(Array("/delete/{id}"))
def deleteById(@PathVariable("id") id: Long): Unit =
{
    val res = scalaModelQuery.delete(id)
    return res
}

@RequestMapping(value = Array("/add"), method = Array(RequestMethod.POST))
def save(@RequestBody scalaModel: ScalaModel): ScalaModel =
{
    // notice here use RequestBody
    val res = scalaModelQuery.save(scalaModel)
    return res
}

@PostMapping(Array("/update"))
def update(@RequestBody scalaModel: ScalaModel, bindingResult: BindingResult): ScalaModel =
{
    val res = scalaModelQuery.update(scalaModel)
    return res
}

slick连接数据库

slick是基于scala的数据库连接中间件,具备跟scala一样方便的函数式编程接口,可以简化数据访问过程,推荐使用,这里将在springboot中集成slick

首先在maven中添加slick的依赖

<dependency>
    <groupId>com.typesafe.slickgroupId>
    <artifactId>slick_2.12artifactId>
    <version>3.2.1version>
dependency>

配置连接

val db = Database.forURL(
    url = "jdbc:mysql://localhost:3306/db_example?useUnicode=true&characterEncoding=UTF-8&useSSL=false",
    driver = "com.mysql.jdbc.Driver",
    user = "springuser",
    password = "ThePassword")

添加slick绑定的model

case class UserInfo(id: Long, name: String, age: Int)

// define an entity class with contructor used for spring serialize
@Entity
class UserInfoObject
{
    @Id
    @GeneratedValue
    @BeanProperty
    var id: Long = _

    @BeanProperty
    @NotBlank            // we can make sure it not empty
    var name: String = _

    @BeanProperty
    @NotNull
    var age: Int = 18 // we can define default value here
}

class SlickModelTable(tag: Tag) extends Table[UserInfo](tag, "scala_model")
{
    def id = column[Long]("id", O.PrimaryKey)
    def name = column[String]("name")
    def age = column[Int]("age")
    def * = (id, name, age) <> (UserInfo.tupled, UserInfo.unapply)
}
def slick_table = TableQuery[SlickModelTable]

controller中添加api用数据库增删改查

@GetMapping(Array("/getslick/{name}"))
def getSlickModel(@PathVariable("name") name: String): UserInfoObject =
{
    val slickres = Await.result(db.run(slick_table.filter(_.name === name).result), Duration.Inf)

    val userInfoObject = new UserInfoObject
    userInfoObject.id = slickres(0).id
    userInfoObject.name = slickres(0).name
    userInfoObject.age = slickres(0).age

    return userInfoObject
}

@GetMapping(Array("/listslick"))
def listSlickModel(): Array[UserInfoObject] =
{
    val slickres = Await.result(db.run(slick_table.result), Duration.Inf)
//        val slickres = Await.result(db.run(slick_table.filter(_.age < 22).result), Duration.Inf)
    val res = mutable.ArrayBuffer[UserInfoObject]()
    slickres.map(
        record => {
            val userInfoObject = new UserInfoObject
            userInfoObject.id = record.id
            userInfoObject.name = record.name
            userInfoObject.age = record.age
            res += userInfoObject
    })
    return res.toArray
}

@PostMapping(Array("/slickadd"))
def slicksave(@RequestBody userInfoObject: UserInfoObject): String =
{
    val userInfo = UserInfo(userInfoObject.id, userInfoObject.name, userInfoObject.age)
    val userArray = Array[UserInfo](userInfo)
    Await.result(db.run(slick_table ++= userArray), Duration.Inf)

    return "save success"
}

@PostMapping(Array("/slickupdate"))
def slickupdate(@RequestBody userInfoObject: UserInfoObject): String =
{
    val userInfo = UserInfo(userInfoObject.id, userInfoObject.name, userInfoObject.age)
    val userArray = Array[UserInfo](userInfo)
    Await.result(db.run(slick_table.filter(_.name === userInfo.name).delete), Duration.Inf)
    Await.result(db.run(slick_table ++= userArray), Duration.Inf)

    return "update success"
}

@PostMapping(Array("/slickdelete/{id}"))
def slickdelete(@PathVariable("id") id: Long): String =
{
    Await.result(db.run(slick_table.filter(_.id === id).delete), Duration.Inf)

    return "delete success"
}

6 跨域访问

为了调试方便,可以设置springboot的cors,支持外部IP的跨域访问

package org.tashaxing.SpringbootScalaDemo.config

import org.springframework.context.annotation.Configuration
import org.springframework.web.servlet.config.annotation.{CorsRegistry, WebMvcConfigurerAdapter}

@Configuration
class OriginConfig extends WebMvcConfigurerAdapter
{
    override def addCorsMappings(registry: CorsRegistry): Unit =
    {
        // cors setting to allow origin access
        registry.addMapping("/**")                      // allow any path
            .allowedOrigins("http://192.168.1.97")      // allow exact ip /** to map any ip
            .allowedMethods("GET", "POST")              // allow methods
            .allowedHeaders("*")                        // allow any header
    }

}

或者单独在controller某个function的mapping前面加入

@CrossOrigin(origins = "http://192.168.1.97:8080", maxAge = 3600)
@GetMapping(Array("/string1"))

7 添加前端

构造react前端项目,用webpack打包到springboot的recource的静态页面目录
react向后台发送网络请求来操作数据库

// update one record
axios({
    method: 'post',
    url: '/scalatest/update',
    data: {
        id: 3,
        name: "tashaxing",
        age: 25
    }
})
    .then(function (response) {
        console.log(response.data);
    })
    .catch(function (error) {
        console.log(error);
    });

8 页面重定向

scala在springboot中的重定向写法与java类似

package org.tashaxing.SpringbootScalaDemo.controller

import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.RequestMapping

@Controller
class PageController
{
    // root page
    @RequestMapping(Array("/"))
    def index(): String = "redirect:/index.html"

    // an other page
    // use forward instead of redirect will not show xxx.html in browser address bar
    @RequestMapping(Array("/subpage"))
    def sub(): String = "forward:/subpage/newpage.html"

    // no need to redirect apitest
//    @RequestMapping(Array("/apitest"))
//    def apitest(): String = "redirect:/apitest.html"
}

9 部署和运行

springboot

命令行

Action Maven
clean mvn clean
run mvn scala:run
package mvn package

如果使用IDE
直接启动SpringbootScalaDemoApplication运行即可

前端

(1)由springboot来作为webserver和http服务器
springboot+scala+slick+react实现微服务_第3张图片
react项目build到springboot文件夹,然后启动springboot主程序,会启动自带的tomcat web容器,将react的静态页面host起来,前端和后台完全分离,通过restful api交互
比如:
在浏览器中输入

http://localhost:7777/scalatest/list

会得到

[1,2,3,4]

浏览器定向到react的页面

http://localhost:7777/apitest.html

在前端执行添加数据的请求

// save one record
axios({
      method: 'post',
      url: '/scalatest/add',
      data: {
          id: 7,
          name: "tashaxing",
          age: 25
      }
  })
  .then(function (response) {
      console.log(response.data);
  })
  .catch(function (error) {
      console.log(error);
  });

会在数据库增加一条记录,返回

age:25
id:7
name:"tashaxing"

(2)用nginx作为webserver并添加反向代理
原理是用nginx来host静态页面,而该页面的http请求会重定向到springboot的http服务,实现完美的前后端分离
nginx配置

server {
        listen       80;
        server_name  localhost;

        #access_log  logs/host.access.log  main;

        location / {
            root   html;
            index  index.html index.htm;
        }

        location /scalatest {
            proxy_pass http://localhost:7700/scalatest/;
            proxy_set_header      Host $host; # 非常重要,缺少的话POST请求通不过
        }
}

这样用户访问localhost:80会打开静态页面,而localhost:80/scalatest开头的请求会被路由到springboot,当然前提是跨域功能要打开

备注

  • scala和java混编,互相调用,在springboot里面都是支持的
  • scala的数据库绑定方式与java有区别
  • 必须同entity绑定model之后才能序列化为json通过restful api返回前端
  • 数据的配置,如果不想对表结构修改,可以设置spring.jpa.hibernate.ddl-auto=none
  • springboot中使用slick,需要额外定义用于springboot序列化json的class,case class没有默认构造函数,所以无效
  • react的html页面不建议写在controller里面redirect,会有问题,直接url访问即可
  • 这里前端页面资源放在springboot自带的容器里,也可以用nginx之类的工具装载然后建立重定向代理

源码下载

csdn:springboot_scala_react
github:springboot_scala_react

你可能感兴趣的:(Scala,JavaScript,springboot,scala,react,slick)