├── .gitignore ├── Jenkinsfile ├── README.md ├── _lib └── sbt-launch.jar ├── akka-rest-api ├── README.md ├── build.sbt ├── docs │ └── scripts.txt ├── project │ ├── build.properties │ ├── plugins.sbt │ └── sbt-launch.jar ├── sbt └── src │ └── main │ ├── resources │ ├── application.conf │ └── logback.xml │ └── scala │ └── me │ └── yangbajing │ └── akkarestapi │ ├── Main.scala │ ├── SystemUtils.scala │ ├── model │ ├── NewsResult.scala │ ├── NewsStatus.scala │ └── ResponseMessage.scala │ ├── repo │ ├── MongoRepo.scala │ ├── MongoSettings.scala │ └── NewsRepo.scala │ ├── routes │ ├── ApiRoute.scala │ ├── MongoApiRoute.scala │ └── NewsApiRoute.scala │ ├── service │ ├── ContextProps.scala │ └── NewsService.scala │ └── utils │ ├── DocumentSupport.scala │ ├── JsonSupport.scala │ └── Utils.scala ├── combine-request ├── README.md ├── build.sbt ├── project │ └── build.properties ├── sbt └── src │ └── main │ └── scala │ └── combinerequest │ ├── CombineExample.scala │ ├── RepetitionExample.scala │ ├── domain │ └── Company.scala │ ├── message │ └── Messages.scala │ └── service │ ├── CompanyMaster.scala │ ├── CorpDetailActor.scala │ ├── EnterpriseService.scala │ ├── ForwardCompanyActor.scala │ ├── InfraMongodbRepo.scala │ └── InfraResource.scala ├── email-server ├── README.md ├── project │ ├── Build.scala │ ├── build.properties │ ├── plugins.sbt │ └── sbt-launch.jar ├── sbt └── src │ ├── main │ ├── resources │ │ └── application.conf │ └── scala │ │ └── me │ │ └── yangbajing │ │ └── emailserver │ │ ├── ActorService.scala │ │ ├── JsonImplicits.scala │ │ ├── Main.scala │ │ ├── common │ │ ├── enums │ │ │ └── MimeType.scala │ │ └── settings │ │ │ └── Settings.scala │ │ ├── demo │ │ ├── ConsumerSynchronous.scala │ │ ├── EmailProducers.scala │ │ └── ProducerSynchronous.scala │ │ ├── domain │ │ └── Emails.scala │ │ ├── route │ │ └── Routes.scala │ │ ├── service │ │ ├── EmailService.scala │ │ ├── MQConsumerService.scala │ │ └── actors │ │ │ ├── EmailGroupActor.scala │ │ │ └── EmailMaster.scala │ │ └── util │ │ └── Utils.scala │ └── test │ └── scala │ ├── me │ └── yangbajing │ │ └── emailserver │ │ └── common │ │ └── settings │ │ └── SettingsTest.scala │ └── sw.sc ├── file-upload ├── .gitignore ├── .scalafmt.conf ├── Jenkinsfile ├── README.md ├── build.sbt ├── project │ ├── build.properties │ ├── plugins.sbt │ └── sbt-launch.jar ├── sbt ├── sbt-dist │ ├── bin │ │ ├── sbt │ │ ├── sbt-launch-lib.bash │ │ ├── sbt-launch.jar │ │ └── sbt.bat │ └── conf │ │ ├── sbtconfig.txt │ │ └── sbtopts ├── sbt.bat ├── src │ ├── main │ │ └── scala │ │ │ └── me │ │ │ └── yangbajing │ │ │ └── fileupload │ │ │ ├── Constants.scala │ │ │ ├── Main.scala │ │ │ ├── controller │ │ │ ├── FileRoute.scala │ │ │ └── HtmlRoute.scala │ │ │ ├── io │ │ │ └── CustomFileSource.scala │ │ │ ├── model │ │ │ └── FileInfo.scala │ │ │ ├── service │ │ │ ├── FileService.scala │ │ │ └── FileServiceImpl.scala │ │ │ └── util │ │ │ ├── FileUtils.scala │ │ │ ├── Jackson.scala │ │ │ ├── JacksonSupport.scala │ │ │ └── Utils.scala │ └── test │ │ └── scala │ │ └── me │ │ └── yangbajing │ │ └── fileupload │ │ └── controller │ │ └── FileRouteTest.scala └── web │ ├── css │ ├── bootstrap-grid.css │ ├── bootstrap-reboot.css │ └── bootstrap.css │ ├── js │ ├── axios.js │ ├── bootstrap.js │ ├── jquery.js │ └── sha256.js │ └── upload.html ├── jdbc-slick ├── Dockerfile ├── README.md ├── build.sbt ├── init.sql ├── project │ └── build.properties ├── sbt └── src │ ├── main │ └── scala │ │ ├── combinerequest │ │ ├── CombineExample.scala │ │ ├── RepetitionExample.scala │ │ ├── domain │ │ │ └── Company.scala │ │ ├── message │ │ │ └── Messages.scala │ │ └── service │ │ │ ├── CompanyMaster.scala │ │ │ ├── CorpDetailActor.scala │ │ │ ├── EnterpriseService.scala │ │ │ ├── ForwardCompanyActor.scala │ │ │ ├── InfraMongodbRepo.scala │ │ │ └── InfraResource.scala │ │ └── sample │ │ ├── model │ │ └── Org.scala │ │ ├── respository │ │ ├── OrgRepository.scala │ │ ├── Schema.scala │ │ └── SlickProfile.scala │ │ ├── route │ │ └── OrgRoute.scala │ │ ├── service │ │ └── OrgService.scala │ │ └── util │ │ ├── Jackson.java │ │ ├── JacksonHelper.java │ │ └── JacksonSupport.scala │ └── test │ └── scala │ └── sample │ └── route │ └── OrgRouteTest.scala ├── scala-js ├── build.sbt ├── index-dev.html ├── project │ ├── build.properties │ └── plugins.sbt ├── scalajs-tutorial.html └── src │ ├── main │ └── scala │ │ └── tutorial │ │ └── webapp │ │ └── TutorialApp.scala │ └── test │ └── scala │ └── tutorial │ └── webapp │ └── TutorialAppTest.scala ├── scala-script ├── build.sbt ├── sbt └── src │ └── main │ └── scala │ ├── Demo.scala │ └── ScalaExample.scala ├── scalatest ├── README.md ├── build.sbt ├── project │ └── build.properties ├── sbt └── src │ └── test │ └── scala │ └── first │ └── FirstTest.scala ├── spark-startup ├── .gitignore ├── build.sbt ├── project │ ├── build.properties │ └── plugins.sbt ├── sbt ├── scripts │ └── SparkApp.sh └── src │ ├── main │ └── scala │ │ └── example │ │ └── SparkApp.scala │ └── test │ └── scala │ └── example │ └── SparkAppTest.scala └── springscala ├── .gitignore ├── build.gradle ├── common ├── build.gradle └── src │ └── main │ ├── java │ └── me │ │ └── yangbajing │ │ └── demo │ │ └── common │ │ └── Constants.java │ └── scala │ └── me │ └── yangbajing │ └── demo │ └── common │ └── Utils.scala ├── settings.gradle └── web ├── build.gradle └── src ├── main ├── java │ └── me │ │ └── yangbajing │ │ └── demo │ │ ├── SpringscalaApplication.java │ │ ├── config │ │ └── SpringscalaConfig.java │ │ └── web │ │ └── controllers │ │ └── WebController.java ├── resources │ └── application.properties └── scala │ └── me │ └── yangbajing │ └── demo │ ├── data │ └── domain │ │ └── Message.scala │ └── web │ └── controllers │ └── ApiController.scala └── test └── java └── me └── yangbajing └── demo └── SpringscalaApplicationTests.java /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.class 3 | *.log 4 | 5 | .~lock.* 6 | .*.swp 7 | .cache 8 | *.error 9 | .history 10 | .DS_Store 11 | .ensime 12 | .classpath 13 | .project 14 | .settings 15 | .Rhistory 16 | .target/ 17 | .ensime_lucene/ 18 | .worksheet/ 19 | .metadata/ 20 | .idea_modules/ 21 | .idea/ 22 | 23 | Backup/ 24 | 25 | fine-food/web/src/main/webapp/upload/ 26 | 27 | # sbt specific 28 | dist/* 29 | target/ 30 | lib_managed/ 31 | src_managed/ 32 | project/boot/ 33 | project/plugins/project/ 34 | cqust/jpkc/src/main/webapp/upload/ 35 | 36 | # Scala-IDE specific 37 | .scala_dependencies 38 | 39 | # Sphinx rst 40 | *.pyc 41 | docs/_build/ 42 | docs/_sphinx/exts/__pycache__/ 43 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent { 3 | // 此处设定构建环境,目前可选有 4 | // default, java-8, python-3.5, ruby-2.3, go-1.11 等 5 | // 详情请阅 https://dev.tencent.com/help/knowledge-base/how-to-use-ci#agents 6 | label "default" 7 | } 8 | stages { 9 | 10 | stage("检出") { 11 | steps { 12 | sh 'ci-init' 13 | checkout( 14 | [$class: 'GitSCM', branches: [[name: env.GIT_BUILD_REF]], 15 | userRemoteConfigs: [[url: env.GIT_REPO_URL]]] 16 | ) 17 | } 18 | } 19 | 20 | stage("构建") { 21 | steps { 22 | echo "构建中..." 23 | sh 'go version' 24 | sh 'node -v' 25 | sh 'java -version' 26 | sh 'php -v' 27 | sh 'python -V' 28 | sh 'gcc -v' 29 | sh 'make -v' 30 | // 请在这里放置您项目代码的单元测试调用过程,例如: 31 | // sh 'mvn package' // mvn 示例 32 | // sh 'make' // make 示例 33 | echo "构建完成." 34 | // archiveArtifacts artifacts: '**/target/*.jar', fingerprint: true // 收集构建产物 35 | } 36 | } 37 | 38 | stage("测试") { 39 | steps { 40 | echo "单元测试中..." 41 | // 请在这里放置您项目代码的单元测试调用过程,例如: 42 | // sh 'mvn test' // mvn 示例 43 | // sh 'make test' // make 示例 44 | echo "单元测试完成." 45 | // junit 'target/surefire-reports/*.xml' // 收集单元测试报告的调用过程 46 | } 47 | } 48 | 49 | stage("部署") { 50 | steps { 51 | echo "部署中..." 52 | // 请在这里放置收集单元测试报告的调用过程,例如: 53 | // sh 'mvn tomcat7:deploy' // Maven tomcat7 插件示例: 54 | // sh './deploy.sh' // 自研部署脚本 55 | echo "部署完成" 56 | } 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scala-applications 2 | 3 | 一些Scala应用程序的尝试 4 | 5 | - [email-server](https://github.com/yangbajing/scala-applications/tree/master/email-server): 6 | 一个邮件发送服务器,使用actor邮箱来保持发送队列。支持REST API和ActiveMQ两种提交发送邮件方式。 7 | - [akka-rest-api](https://github.com/yangbajing/scala-applications/tree/master/akka-rest-api): 使用Akka Http设计REST API。 8 | 9 | -------------------------------------------------------------------------------- /_lib/sbt-launch.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yangbajing/scala-applications/a41bd2b1e5eef086cf54fe58630bb47d7c16df39/_lib/sbt-launch.jar -------------------------------------------------------------------------------- /akka-rest-api/README.md: -------------------------------------------------------------------------------- 1 | # 使用Akka Http设计REST API 2 | 3 | - akka-http 4 | - mongo-scala-driver 5 | 6 | -------------------------------------------------------------------------------- /akka-rest-api/build.sbt: -------------------------------------------------------------------------------- 1 | description := "Akka REST API" 2 | 3 | version := "0.0.2" 4 | 5 | homepage := Some( 6 | new URL( 7 | "https://github.com/yangbajing/scala-applications/tree/master/akka-rest-api" 8 | ) 9 | ) 10 | 11 | organization := "me.yangbajing" 12 | 13 | organizationHomepage := Some( 14 | new URL("https://github.com/yangbajing/scala-applications") 15 | ) 16 | 17 | startYear := Some(2015) 18 | 19 | scalaVersion := "2.11.12" 20 | 21 | scalacOptions ++= Seq( 22 | "-encoding", 23 | "utf8", 24 | "-unchecked", 25 | "-feature", 26 | "-deprecation" 27 | ) 28 | 29 | javacOptions ++= Seq( 30 | "-encoding", 31 | "utf8", 32 | "-Xlint:unchecked", 33 | "-Xlint:deprecation" 34 | ) 35 | 36 | javaOptions += "-Dproject.base=" + baseDirectory.value 37 | 38 | resolvers ++= Seq( 39 | "Typesafe Repository" at "http://repo.typesafe.com/typesafe/releases/", 40 | "Sonatype releases" at "http://oss.sonatype.org/content/repositories/releases", 41 | "Typesafe Snapshots" at "http://repo.typesafe.com/typesafe/snapshots/", 42 | "Sonatype snapshots" at "http://oss.sonatype.org/content/repositories/snapshots" 43 | ) 44 | 45 | assemblyJarName in assembly := "akka-rest-api.jar" 46 | 47 | mainClass in assembly := Some("me.yangbajing.akkarestapi.Main") 48 | 49 | libraryDependencies ++= Seq( 50 | _mongoScala, 51 | _json4sJackson, 52 | _redisclient, 53 | _akkaHttpCore, 54 | _akkaActor, 55 | _akkaSlf4j, 56 | _logback, 57 | _typesafeConfig, 58 | _scalaLogging, 59 | _scalatest 60 | ) 61 | 62 | lazy val _scalaXml = "org.scala-lang.modules" %% "scala-xml" % "1.0.4" 63 | lazy val _scalatest = "org.scalatest" %% "scalatest" % "2.2.5" % "test" 64 | lazy val _typesafeConfig = "com.typesafe" % "config" % "1.3.0" 65 | lazy val _scalaLogging = ("com.typesafe.scala-logging" %% "scala-logging" % "3.1.0") 66 | .exclude("org.scala-lang", "scala-library") 67 | .exclude("org.scala-lang", "scala-reflect") 68 | 69 | lazy val verAkka = "2.5.19" 70 | 71 | lazy val _akkaHttpCore = "com.typesafe.akka" %% "akka-http" % "10.1.6" 72 | 73 | lazy val _akkaActor = "com.typesafe.akka" %% "akka-actor" % verAkka 74 | lazy val _akkaSlf4j = "com.typesafe.akka" %% "akka-slf4j" % verAkka 75 | 76 | lazy val _redisclient = "net.debasishg" %% "redisclient" % "3.0" 77 | 78 | lazy val _json4sJackson = "org.json4s" %% "json4s-jackson" % "3.3.0" 79 | 80 | lazy val _mongoScala = "org.mongodb.scala" %% "mongo-scala-driver" % "1.0.0" 81 | 82 | lazy val _logback = "ch.qos.logback" % "logback-classic" % "1.1.3" 83 | lazy val _commonsEmail = "org.apache.commons" % "commons-email" % "1.4" 84 | lazy val _guava = "com.google.guava" % "guava" % "18.0" 85 | -------------------------------------------------------------------------------- /akka-rest-api/docs/scripts.txt: -------------------------------------------------------------------------------- 1 | # insert 2 | curl -XPOST -H 'content-type:application/json;charset=utf8' \ 3 | -d '{"key":"杭州誉存科技有限公司","tags":["大数据","企业信用"]}' \ 4 | http://localhost:44444/api/mongo/dbname/collname/_insert 5 | 6 | # query 7 | curl -XPOST -H 'content-type:application/json' \ 8 | -d '{"filter":{"key":"杭州誉存科技有限公司"}}' \ 9 | http://localhost:44444/api/mongo/dbname/collname/_query 10 | -------------------------------------------------------------------------------- /akka-rest-api/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.2.7 2 | -------------------------------------------------------------------------------- /akka-rest-api/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.9") 2 | -------------------------------------------------------------------------------- /akka-rest-api/project/sbt-launch.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yangbajing/scala-applications/a41bd2b1e5eef086cf54fe58630bb47d7c16df39/akka-rest-api/project/sbt-launch.jar -------------------------------------------------------------------------------- /akka-rest-api/sbt: -------------------------------------------------------------------------------- 1 | SCRIPT_DIR=`dirname $0` 2 | java -Dfile.encoding=UTF-8 -noverify -Xms512M -Xmx1024M -Xss1M -XX:+CMSClassUnloadingEnabled -jar "$SCRIPT_DIR/../_lib/sbt-launch.jar" $@ 3 | -------------------------------------------------------------------------------- /akka-rest-api/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | log-dead-letters = off 3 | log-dead-letters-during-shutdown = off 4 | http { 5 | server { 6 | socket-options { 7 | tcp-keep-alive = on 8 | } 9 | } 10 | } 11 | } 12 | 13 | api { 14 | network { 15 | server = "0.0.0.0" 16 | port = 44444 17 | } 18 | } -------------------------------------------------------------------------------- /akka-rest-api/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %date - [%level] - from %logger in %thread %n%message%n%xException%n 6 | 7 | 8 | 9 | 10 | logs/application.log 11 | 12 | 13 | logs/application-log-%d{yyyy-MM-dd}.gz 14 | 15 | 30 16 | 17 | 18 | %date{yyyy-MM-dd HH:mm:ss ZZZZ} [%level] from %logger in %thread - %n%message%n%xException%n 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /akka-rest-api/src/main/scala/me/yangbajing/akkarestapi/Main.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.akkarestapi 2 | 3 | import akka.http.scaladsl.Http 4 | import com.typesafe.config.ConfigFactory 5 | import com.typesafe.scalalogging.StrictLogging 6 | import me.yangbajing.akkarestapi.routes.ApiRoute 7 | import me.yangbajing.akkarestapi.service.ContextProps 8 | 9 | import scala.util.{Failure, Success} 10 | 11 | /** 12 | * Main 13 | * Created by Yang Jing (yangbajing@gmail.com) on 2015-11-16. 14 | */ 15 | object Main extends StrictLogging { 16 | 17 | def main(args: Array[String]): Unit = { 18 | import SystemUtils.{materializer, system} 19 | import system.dispatcher 20 | 21 | val conf = ConfigFactory.load() 22 | 23 | val props = new ContextProps() 24 | 25 | val bindingFuture = Http().bindAndHandle(ApiRoute(props), 26 | conf.getString("api.network.server"), conf.getInt("api.network.port")) 27 | 28 | bindingFuture.onComplete { 29 | case Success(binding) => 30 | logger.info(binding.toString) 31 | case Failure(e) => 32 | logger.error(e.getLocalizedMessage, e) 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /akka-rest-api/src/main/scala/me/yangbajing/akkarestapi/SystemUtils.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.akkarestapi 2 | 3 | import akka.actor.ActorSystem 4 | import akka.stream.ActorMaterializer 5 | 6 | /** 7 | * Created by Yang Jing (yangbajing@gmail.com) on 2015-11-16. 8 | */ 9 | object SystemUtils { 10 | implicit val system = ActorSystem() 11 | implicit val materializer = ActorMaterializer() 12 | } 13 | -------------------------------------------------------------------------------- /akka-rest-api/src/main/scala/me/yangbajing/akkarestapi/model/NewsResult.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.akkarestapi.model 2 | 3 | import java.time.LocalDateTime 4 | 5 | /** 6 | * News Result 7 | * Created by Yang Jing (yangbajing@gmail.com) on 2015-11-16. 8 | */ 9 | case class NewsResult(key: String, 10 | method: String, 11 | count: Int, 12 | items: Seq[NewsItem]) 13 | 14 | case class NewsItem(key: String, 15 | time: LocalDateTime, 16 | title: String, 17 | author: String, 18 | summary: String, 19 | content: String) 20 | -------------------------------------------------------------------------------- /akka-rest-api/src/main/scala/me/yangbajing/akkarestapi/model/NewsStatus.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.akkarestapi.model 2 | 3 | import java.time.LocalDateTime 4 | 5 | /** 6 | * 新闻服务状态 7 | * Created by Yang Jing (yangbajing@gmail.com) on 2015-11-16. 8 | */ 9 | case class NewsStatus(now: LocalDateTime, status: String) 10 | -------------------------------------------------------------------------------- /akka-rest-api/src/main/scala/me/yangbajing/akkarestapi/model/ResponseMessage.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.akkarestapi.model 2 | 3 | /** 4 | * HTTP 响应消息 5 | * Created by Yang Jing (yangbajing@gmail.com) on 2015-11-16. 6 | */ 7 | case class ResponseMessage(errcode: Int = 0, errmsg: String = "") extends RuntimeException(errmsg) 8 | -------------------------------------------------------------------------------- /akka-rest-api/src/main/scala/me/yangbajing/akkarestapi/repo/MongoRepo.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.akkarestapi.repo 2 | 3 | import me.yangbajing.akkarestapi.model.ResponseMessage 4 | import org.mongodb.scala.bson.{BsonDocument, BsonInt32, BsonObjectId} 5 | import org.mongodb.scala.{Document, MongoClient} 6 | 7 | import scala.concurrent.{ExecutionContext, Future} 8 | 9 | 10 | /** 11 | * Mongo Repo 12 | * Created by Yang Jing (yangbajing@gmail.com) on 2015-11-16. 13 | */ 14 | class MongoRepo private(mongoClient: MongoClient, dbName: String, collName: String)(implicit ec: ExecutionContext) { 15 | def update(doc: Document): Future[Document] = { 16 | val result = 17 | for { 18 | filter <- doc.get[BsonDocument]("filter") 19 | u <- doc.get[BsonDocument]("update") 20 | } yield { 21 | getColl(dbName, collName).updateOne(filter, u).toFuture().flatMap { results => 22 | if (results.isEmpty) Future.failed(ResponseMessage(-1, "insert error")) 23 | else Future.successful(Document(u)) 24 | } 25 | } 26 | 27 | result getOrElse Future.failed(ResponseMessage(-1, "filter或update不存在")) 28 | } 29 | 30 | def insert(doc: Document): Future[Document] = { 31 | val document = if (doc.contains("_id")) doc else (doc.newBuilder += ("_id" -> BsonObjectId())).result() 32 | getColl(dbName, collName).insertOne(document).toFuture().flatMap { results => 33 | if (results.isEmpty) Future.failed(ResponseMessage(-1, "insert error")) 34 | else Future.successful(document) 35 | } 36 | } 37 | 38 | def delete(filter: Document): Future[Document] = { 39 | getColl(dbName, collName).deleteOne(filter).toFuture().flatMap { results => 40 | if (results.isEmpty) Future.failed(ResponseMessage(-1, "delete error")) 41 | else Future.successful(filter) 42 | } 43 | } 44 | 45 | def query(doc: Document): Future[Seq[Document]] = { 46 | val result = 47 | for { 48 | filter <- doc.get[BsonDocument]("filter") 49 | } yield { 50 | val cursor = getColl(dbName, collName).find(filter) 51 | val sortCursor = doc.get[BsonDocument]("sort").map(sort => cursor.sort(sort)).getOrElse(cursor) 52 | val skipCursor = doc.get[BsonInt32]("skip").map(skip => sortCursor.skip(skip.intValue())).getOrElse(sortCursor) 53 | val limitCursor = doc.get[BsonInt32]("limit").map(limit => skipCursor.limit(limit.intValue())).getOrElse(skipCursor) 54 | limitCursor.toFuture() 55 | } 56 | 57 | result getOrElse Future.failed(ResponseMessage(-1, "filter或update不存在")) 58 | } 59 | 60 | def getColl(dbName: String, collName: String) = mongoClient.getDatabase(dbName).getCollection(collName) 61 | } 62 | 63 | object MongoRepo { 64 | def apply(dbName: String, collName: String)(implicit ec: ExecutionContext) = 65 | new MongoRepo(MongoSettings.mongoClient, dbName, collName) 66 | } 67 | -------------------------------------------------------------------------------- /akka-rest-api/src/main/scala/me/yangbajing/akkarestapi/repo/MongoSettings.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.akkarestapi.repo 2 | 3 | import org.mongodb.scala.MongoClient 4 | 5 | /** 6 | * Mongo Settings 7 | * Created by Yang Jing (yangbajing@gmail.com) on 2015-11-16. 8 | */ 9 | object MongoSettings { 10 | val mongoClient = MongoClient("mongodb://localhost") 11 | // val database = mongoClient.getDatabase("news_db") 12 | 13 | // def insert(dbName: String, collName: String, doc: Document) = { 14 | // getCollection(dbName, collName).insertOne(doc).toFuture() 15 | // } 16 | 17 | // def findOne(dbName: String, collName: String, query: Document) 18 | 19 | def getCollection(dbName: String, collName: String) = getDatabase(dbName).getCollection(collName) 20 | 21 | def getDatabase(dbName: String) = mongoClient.getDatabase(dbName) 22 | } 23 | -------------------------------------------------------------------------------- /akka-rest-api/src/main/scala/me/yangbajing/akkarestapi/repo/NewsRepo.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.akkarestapi.repo 2 | 3 | import me.yangbajing.akkarestapi.model.{NewsItem, ResponseMessage} 4 | import me.yangbajing.akkarestapi.utils.JsonSupport 5 | import org.mongodb.scala.bson.collection.immutable.Document 6 | 7 | import scala.concurrent.{ExecutionContext, Future} 8 | 9 | /** 10 | * News Repo 11 | * Created by Yang Jing (yangbajing@gmail.com) on 2015-11-16. 12 | */ 13 | class NewsRepo() { 14 | 15 | import JsonSupport._ 16 | 17 | val newsColl = MongoSettings.getCollection("news_db", "news") 18 | 19 | def insert(item: NewsItem)(implicit ec: ExecutionContext): Future[ResponseMessage] = { 20 | val doc = Document(serialization.write(item)) 21 | newsColl.insertOne(doc).toFuture().map { completes => 22 | if (completes.isEmpty) 23 | throw ResponseMessage(-1, "not found") 24 | ResponseMessage() 25 | } 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /akka-rest-api/src/main/scala/me/yangbajing/akkarestapi/routes/ApiRoute.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.akkarestapi.routes 2 | 3 | import akka.http.scaladsl.server.Directives._ 4 | 5 | import akka.stream.Materializer 6 | import me.yangbajing.akkarestapi.service.ContextProps 7 | 8 | import scala.concurrent.ExecutionContext 9 | 10 | /** 11 | * Api Route 12 | * Created by Yang Jing (yangbajing@gmail.com) on 2015-11-16. 13 | */ 14 | object ApiRoute { 15 | 16 | def apply(props: ContextProps)(implicit ec: ExecutionContext, mat: Materializer) = 17 | handleExceptions(props.myExceptionHandler) { 18 | pathPrefix("api") { 19 | NewsApiRoute(props) ~ 20 | MongoApiRoute(props) 21 | } 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /akka-rest-api/src/main/scala/me/yangbajing/akkarestapi/routes/MongoApiRoute.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.akkarestapi.routes 2 | 3 | import akka.http.scaladsl.model.StatusCodes 4 | import akka.stream.Materializer 5 | import me.yangbajing.akkarestapi.repo.MongoRepo 6 | import me.yangbajing.akkarestapi.service.ContextProps 7 | import org.mongodb.scala.bson.collection.immutable.Document 8 | 9 | import scala.concurrent.ExecutionContext 10 | import scala.util.{Failure, Success} 11 | 12 | /** 13 | * Mongo Api 14 | * Created by Yang Jing (yangbajing@gmail.com) on 2015-11-16. 15 | */ 16 | object MongoApiRoute { 17 | 18 | import akka.http.scaladsl.server.Directives._ 19 | import me.yangbajing.akkarestapi.utils.DocumentSupport._ 20 | 21 | def apply(props: ContextProps)(implicit ec: ExecutionContext, mat: Materializer) = { 22 | path("mongo" / Segment / Segment / Segment) { (dbName, collName, method) => 23 | post { 24 | entity(as[Document]) { doc => 25 | val mongoRepo = MongoRepo(dbName, collName) 26 | onComplete(method match { 27 | case "_update" => 28 | mongoRepo.update(doc).map(updateDoc => complete(updateDoc)) 29 | case "_insert" => 30 | mongoRepo.insert(doc).map(doc => complete(StatusCodes.Created, doc)) 31 | case "_query" => 32 | mongoRepo.query(doc).map(docs => complete(docs)) 33 | case "_delete" => 34 | mongoRepo.delete(doc).map(filterDoc => complete(filterDoc)) 35 | }) { 36 | case Success(response) => 37 | response 38 | case Failure(e) => 39 | e.printStackTrace() 40 | complete(StatusCodes.InternalServerError, e.getLocalizedMessage) 41 | } 42 | } 43 | } 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /akka-rest-api/src/main/scala/me/yangbajing/akkarestapi/routes/NewsApiRoute.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.akkarestapi.routes 2 | 3 | import akka.http.scaladsl.model.StatusCodes 4 | import akka.stream.Materializer 5 | import me.yangbajing.akkarestapi.model.NewsItem 6 | import me.yangbajing.akkarestapi.service.ContextProps 7 | 8 | import scala.concurrent.ExecutionContext 9 | 10 | /** 11 | * 新闻 API 12 | * Created by Yang Jing (yangbajing@gmail.com) on 2015-11-16. 13 | */ 14 | object NewsApiRoute { 15 | 16 | import akka.http.scaladsl.server.Directives._ 17 | import me.yangbajing.akkarestapi.utils.JsonSupport._ 18 | 19 | def apply(props: ContextProps)(implicit ec: ExecutionContext, mat: Materializer) = { 20 | pathPrefix("news") { 21 | pathEndOrSingleSlash { 22 | get { 23 | parameters( 24 | 'company, 25 | 'method ? "F") { (company, method) => 26 | onSuccess(props.newsService.findNews(company, method)) { result => 27 | complete(result) 28 | } 29 | } 30 | } ~ 31 | post { 32 | entity(as[NewsItem]) { item => 33 | onSuccess(props.newsService.persistNewsItem(item)) { result => 34 | complete(StatusCodes.Created, result) 35 | } 36 | } 37 | } 38 | } ~ 39 | path("status") { 40 | get { 41 | onSuccess(props.newsService.getStatus()) { result => 42 | complete(result) 43 | } 44 | } 45 | } 46 | } 47 | 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /akka-rest-api/src/main/scala/me/yangbajing/akkarestapi/service/ContextProps.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.akkarestapi.service 2 | 3 | import akka.http.scaladsl.model.StatusCodes 4 | import akka.http.scaladsl.server.{Directives, ExceptionHandler} 5 | 6 | import com.typesafe.scalalogging.StrictLogging 7 | import me.yangbajing.akkarestapi.model.ResponseMessage 8 | import me.yangbajing.akkarestapi.utils.JsonSupport 9 | 10 | /** 11 | * 上下文属性,所有服务都定义在此 12 | * Created by Yang Jing (yangbajing@gmail.com) on 2015-11-17. 13 | */ 14 | class ContextProps extends StrictLogging { 15 | 16 | import Directives._ 17 | import me.yangbajing.akkarestapi.utils.JsonSupport._ 18 | 19 | val myExceptionHandler = ExceptionHandler { 20 | case e: ResponseMessage => 21 | extractUri { uri => 22 | logger.error(s"Request to $uri could not be handled normally") 23 | complete(StatusCodes.InternalServerError, JsonSupport.serialization.write(e)) 24 | } 25 | // case _: ArithmeticException => 26 | // extractUri { uri => 27 | // logger.error(s"Request to $uri could not be handled normally", e) 28 | // complete(StatusCodes.InternalServerError, "Bad numbers, bad result!!!") 29 | // } 30 | } 31 | 32 | val newsService = NewsService() 33 | } 34 | -------------------------------------------------------------------------------- /akka-rest-api/src/main/scala/me/yangbajing/akkarestapi/service/NewsService.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.akkarestapi.service 2 | 3 | import me.yangbajing.akkarestapi.model.{NewsItem, NewsResult, NewsStatus, ResponseMessage} 4 | import me.yangbajing.akkarestapi.repo.NewsRepo 5 | import me.yangbajing.akkarestapi.utils.Utils 6 | 7 | import scala.concurrent.{ExecutionContext, Future} 8 | import scala.util.Random 9 | 10 | /** 11 | * News Service 12 | * Created by Yang Jing (yangbajing@gmail.com) on 2015-11-16. 13 | */ 14 | class NewsService private() { 15 | val newsRepo = new NewsRepo() 16 | 17 | def persistNewsItem(item: NewsItem)(implicit ec: ExecutionContext): Future[ResponseMessage] = { 18 | newsRepo.insert(item) 19 | } 20 | 21 | /** 22 | * 获取新闻 23 | * @param key 搜索关键词 24 | * @param method 搜索方式:S: 摘要,F: 全文 25 | * @return 26 | */ 27 | def findNews(key: String, 28 | method: String)(implicit ec: ExecutionContext): Future[NewsResult] = Future { 29 | NewsResult(key, method, Random.nextInt(), Nil) 30 | } 31 | 32 | /** 33 | * 获取当前新闻服务状态 34 | * @return 35 | */ 36 | def getStatus()(implicit ec: ExecutionContext): Future[NewsStatus] = Future { 37 | NewsStatus(Utils.now, "ok") 38 | } 39 | 40 | } 41 | 42 | object NewsService { 43 | def apply() = new NewsService() 44 | } 45 | -------------------------------------------------------------------------------- /akka-rest-api/src/main/scala/me/yangbajing/akkarestapi/utils/DocumentSupport.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.akkarestapi.utils 2 | 3 | import akka.http.scaladsl.marshalling.{Marshaller, ToEntityMarshaller} 4 | import akka.http.scaladsl.model.{ContentType, HttpCharsets, MediaTypes} 5 | import akka.http.scaladsl.unmarshalling.{FromEntityUnmarshaller, Unmarshaller} 6 | import akka.stream.Materializer 7 | import org.mongodb.scala.bson.collection.immutable.Document 8 | 9 | /** 10 | * MongoDB Document Akka Http (un)marshall支持 11 | * Created by Yang Jing (yangbajing@gmail.com) on 2015-11-16. 12 | */ 13 | trait DocumentSupport { 14 | implicit def documentUnmarshaller(implicit mat: Materializer): FromEntityUnmarshaller[Document] = 15 | Unmarshaller.byteStringUnmarshaller 16 | .forContentTypes(MediaTypes.`application/json`) 17 | .mapWithCharset { (data, charset) => 18 | val input = if (charset == HttpCharsets.`UTF-8`) data.utf8String else data.decodeString(charset.nioCharset.name) 19 | Document(input) 20 | } 21 | 22 | implicit val documentMarshaller: ToEntityMarshaller[Document] = 23 | Marshaller.StringMarshaller.wrap(MediaTypes.`application/json`)(v => v.toJson()) 24 | implicit val seqDocumentMarshaller: ToEntityMarshaller[Seq[Document]] = 25 | Marshaller.StringMarshaller.wrap(MediaTypes.`application/json`)(vs => vs.map(_.toJson()).mkString("[", ",", "]")) 26 | 27 | } 28 | 29 | object DocumentSupport extends DocumentSupport -------------------------------------------------------------------------------- /akka-rest-api/src/main/scala/me/yangbajing/akkarestapi/utils/JsonSupport.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.akkarestapi.utils 2 | 3 | import akka.http.scaladsl.marshalling.{Marshaller, ToEntityMarshaller} 4 | import akka.http.scaladsl.model.{ContentTypes, HttpCharsets, MediaTypes} 5 | import akka.http.scaladsl.unmarshalling.{FromEntityUnmarshaller, Unmarshaller} 6 | import akka.stream.Materializer 7 | import org.json4s.jackson.Serialization 8 | import org.json4s.{DefaultFormats, Formats, Serialization} 9 | 10 | /** 11 | * Json Support 12 | * Created by Yang Jing (yangbajing@gmail.com) on 2015-11-16. 13 | */ 14 | trait JsonSupport { 15 | implicit val formats = DefaultFormats 16 | implicit val serialization = Serialization 17 | 18 | implicit def json4sUnmarshallerConverter[A: Manifest](serialization: Serialization, formats: Formats)(implicit mat: Materializer): FromEntityUnmarshaller[A] = 19 | json4sUnmarshaller(manifest, serialization, formats, mat) 20 | 21 | implicit def json4sUnmarshaller[A: Manifest](implicit serialization: Serialization, formats: Formats, mat: Materializer): FromEntityUnmarshaller[A] = 22 | Unmarshaller.byteStringUnmarshaller 23 | .forContentTypes(MediaTypes.`application/json`) 24 | .mapWithCharset { (data, charset) => 25 | val input = if (charset == HttpCharsets.`UTF-8`) data.utf8String else data.decodeString(charset.nioCharset.name) 26 | serialization.read(input) 27 | } 28 | 29 | implicit def json4sMarshallerConverter[A <: AnyRef](serialization: Serialization, formats: Formats): ToEntityMarshaller[A] = 30 | json4sMarshaller(serialization, formats) 31 | 32 | implicit def json4sMarshaller[A <: AnyRef](implicit serialization: Serialization, formats: Formats): ToEntityMarshaller[A] = 33 | Marshaller.StringMarshaller.wrap(MediaTypes.`application/json`)(serialization.write[A]) 34 | 35 | } 36 | 37 | object JsonSupport extends JsonSupport 38 | -------------------------------------------------------------------------------- /akka-rest-api/src/main/scala/me/yangbajing/akkarestapi/utils/Utils.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.akkarestapi.utils 2 | 3 | import java.time.LocalDateTime 4 | 5 | /** 6 | * Utils 7 | * Created by Yang Jing (yangbajing@gmail.com) on 2015-11-16. 8 | */ 9 | object Utils { 10 | def now = LocalDateTime.now() 11 | } 12 | -------------------------------------------------------------------------------- /combine-request/README.md: -------------------------------------------------------------------------------- 1 | # Scala实战:使用Actor来控制集成API的并发请求 2 | 3 | [http://www.yangbajing.me/2016/06/29/scala实战:使用actor来控制集成api的并发请求](http://www.yangbajing.me/2016/06/29/scala%E5%AE%9E%E6%88%98%EF%BC%9A%E4%BD%BF%E7%94%A8actor%E6%9D%A5%E6%8E%A7%E5%88%B6%E9%9B%86%E6%88%90api%E7%9A%84%E5%B9%B6%E5%8F%91%E8%AF%B7%E6%B1%82/) 4 | 5 | - [CombineExample.scala](src/main/scala/combinerequest/CombineExample.scala): 合并请求示例 6 | - [RepetitionExample.scala](src/main/scala/combinerequest/RepetitionExample.scala): 重复请示示例 7 | -------------------------------------------------------------------------------- /combine-request/build.sbt: -------------------------------------------------------------------------------- 1 | scalaVersion := "2.12.6" 2 | 3 | scalacOptions ++= Seq( 4 | "-encoding", "utf8", 5 | "-unchecked", 6 | "-feature", 7 | "-deprecation" 8 | ) 9 | 10 | libraryDependencies ++= Seq( 11 | "com.typesafe.akka" %% "akka-actor" % "2.5.14" 12 | ) 13 | 14 | -------------------------------------------------------------------------------- /combine-request/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.1.6 2 | -------------------------------------------------------------------------------- /combine-request/sbt: -------------------------------------------------------------------------------- 1 | SCRIPT_DIR=`dirname $0` 2 | java -Dfile.encoding=UTF-8 -noverify -Xms512M -Xmx1024M -Xss1M -XX:+CMSClassUnloadingEnabled -jar "$SCRIPT_DIR/../_lib/sbt-launch.jar" $@ 3 | -------------------------------------------------------------------------------- /combine-request/src/main/scala/combinerequest/CombineExample.scala: -------------------------------------------------------------------------------- 1 | package combinerequest 2 | 3 | import akka.actor.ActorSystem 4 | import combinerequest.service.{EnterpriseService, InfraMongodbRepo, InfraResource} 5 | 6 | import scala.concurrent.Await 7 | import scala.concurrent.duration._ 8 | import scala.util.{Failure, Success} 9 | 10 | /** 11 | * 合并同一时间的3个请求 12 | * Created by Yang Jing (yangbajing@gmail.com) on 2016-06-29. 13 | */ 14 | object CombineExample { 15 | val system = ActorSystem() 16 | 17 | import system.dispatcher 18 | 19 | def main(args: Array[String]): Unit = { 20 | val infraResource = new InfraResource() 21 | val infraMongodbRepo = new InfraMongodbRepo() 22 | val enterpriseService = new EnterpriseService(system, infraResource, infraMongodbRepo) 23 | 24 | val company1Future = enterpriseService.getCorpDetail("科技公司") 25 | val company2Future = enterpriseService.getCorpDetail("科技公司") 26 | val company3Future = enterpriseService.getCorpDetail("科技公司") 27 | 28 | val companiesFuture = for { 29 | company1 <- company1Future 30 | company2 <- company2Future 31 | company3 <- company3Future 32 | } yield { 33 | List(company1, company2, company3) 34 | } 35 | 36 | Await.ready(companiesFuture, 60.seconds) 37 | .onComplete { 38 | case Success(c1 :: c2 :: c3 :: Nil) => 39 | // println(c1 eq c2) 40 | // println(c2 eq c3) 41 | println(s"$c1, $c2, $c3") 42 | case Success(unknown) => 43 | System.err.println(s"收到未期待的结果:$unknown") 44 | 45 | case Failure(e) => 46 | e.printStackTrace() 47 | } 48 | 49 | try { 50 | Await.result(system.terminate(), 60.seconds) 51 | } catch { 52 | case e: Throwable => 53 | e.printStackTrace() 54 | System.exit(3) 55 | } 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /combine-request/src/main/scala/combinerequest/RepetitionExample.scala: -------------------------------------------------------------------------------- 1 | package combinerequest 2 | 3 | import akka.actor.ActorSystem 4 | import combinerequest.domain.Company 5 | import combinerequest.service.{CompanyService, InfraMongodbRepo, InfraResource} 6 | 7 | import scala.concurrent.duration._ 8 | import scala.concurrent.{Await, ExecutionContext, Future} 9 | import scala.util.{Failure, Success} 10 | 11 | /** 12 | * Created by Yang Jing (yangbajing@gmail.com) on 2016-07-02. 13 | */ 14 | object RepetitionExample { 15 | val system = ActorSystem() 16 | 17 | import system.dispatcher 18 | 19 | def main(args: Array[String]): Unit = { 20 | val infraResource = new InfraResource() 21 | val infraMongodbRepo = new InfraMongodbRepo() 22 | val enterpriseService = new RepetitionService(infraResource, infraMongodbRepo) 23 | 24 | val company1Future = enterpriseService.getCorpDetail("科技公司") 25 | val company2Future = enterpriseService.getCorpDetail("科技公司") 26 | val company3Future = enterpriseService.getCorpDetail("科技公司") 27 | 28 | val companiesFuture = for { 29 | company1 <- company1Future 30 | company2 <- company2Future 31 | company3 <- company3Future 32 | } yield { 33 | List(company1, company2, company3) 34 | } 35 | 36 | Await.ready(companiesFuture, 60.seconds) 37 | .onComplete { 38 | case Success(c1 :: c2 :: c3 :: Nil) => 39 | // println(c1 eq c2) 40 | // println(c2 eq c3) 41 | println(s"$c1, $c2, $c3") 42 | case Success(unknown) => 43 | System.err.println(s"收到未期待的结果:$unknown") 44 | 45 | case Failure(e) => 46 | e.printStackTrace() 47 | } 48 | 49 | try { 50 | Await.result(system.terminate(), 60.seconds) 51 | } catch { 52 | case e: Throwable => 53 | e.printStackTrace() 54 | System.exit(3) 55 | } 56 | } 57 | 58 | } 59 | 60 | class RepetitionService(val infraResource: InfraResource, 61 | val infraMongodbRepo: InfraMongodbRepo) extends CompanyService { 62 | 63 | override def getCorpDetail(companyName: String)(implicit ec: ExecutionContext): Future[Option[Company]] = { 64 | infraMongodbRepo.findCorpDetail(companyName) 65 | .flatMap { 66 | case Some(company) => 67 | Future.successful(Some(company)) 68 | case None => 69 | infraResource.corpDetail(companyName) 70 | .flatMap { 71 | case Some(company) => 72 | infraMongodbRepo 73 | .saveCorpDetail(companyName, company) 74 | .map(_ => Some(company)) 75 | 76 | case None => 77 | Future.successful(None) 78 | } 79 | } 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /combine-request/src/main/scala/combinerequest/domain/Company.scala: -------------------------------------------------------------------------------- 1 | package combinerequest.domain 2 | 3 | /** 4 | * Created by Yang Jing (yangbajing@gmail.com) on 2016-06-30. 5 | */ 6 | case class Company(companyName: String) 7 | -------------------------------------------------------------------------------- /combine-request/src/main/scala/combinerequest/message/Messages.scala: -------------------------------------------------------------------------------- 1 | package combinerequest.message 2 | 3 | import akka.actor.{Actor, ActorRef} 4 | import combinerequest.domain.Company 5 | 6 | /** 7 | * Created by Yang Jing (yangbajing@gmail.com) on 2016-06-29. 8 | */ 9 | case class QueryCompany(companyName: String, doSender: ActorRef = Actor.noSender) 10 | 11 | case class ReceiveQueryCompanyResult(companyName: String, corpDetail: Option[Company]) 12 | 13 | sealed trait GetCompanyMessage { 14 | val companyName: String 15 | } 16 | 17 | /** 18 | * 获取工商消息 19 | * 20 | * @param companyName 公司名 21 | */ 22 | case class GetCorpDetail(companyName: String) extends GetCompanyMessage 23 | -------------------------------------------------------------------------------- /combine-request/src/main/scala/combinerequest/service/CompanyMaster.scala: -------------------------------------------------------------------------------- 1 | package combinerequest.service 2 | 3 | import akka.actor.{Actor, Props} 4 | import combinerequest.message.{GetCorpDetail, QueryCompany} 5 | 6 | /** 7 | * Created by Yang Jing (yangbajing@gmail.com) on 2016-06-29. 8 | */ 9 | class CompanyMaster(infraResource: InfraResource, 10 | infraMongodbRepo: InfraMongodbRepo) extends Actor { 11 | 12 | val actorCorpDetail = context.actorOf(CorpDetailActor.props(infraResource, infraMongodbRepo), "corpDetail") 13 | 14 | override def receive = { 15 | case GetCorpDetail(companyName) => 16 | actorCorpDetail ! QueryCompany(companyName, sender()) 17 | } 18 | 19 | } 20 | 21 | object CompanyMaster { 22 | 23 | def props(infraResource: InfraResource, 24 | infraMongodbRepo: InfraMongodbRepo) = 25 | Props(new CompanyMaster(infraResource, infraMongodbRepo)) 26 | 27 | } 28 | -------------------------------------------------------------------------------- /combine-request/src/main/scala/combinerequest/service/CorpDetailActor.scala: -------------------------------------------------------------------------------- 1 | package combinerequest.service 2 | 3 | import akka.actor.Props 4 | import combinerequest.service.ForwardCompanyActor.{ReadFromDB, ReadFromInfra} 5 | 6 | import scala.concurrent.Future 7 | 8 | /** 9 | * Created by Yang Jing (yangbajing@gmail.com) on 2016-06-29. 10 | */ 11 | class CorpDetailActor(infraResource: InfraResource, 12 | infraMongodbRepo: InfraMongodbRepo) extends ForwardCompanyActor { 13 | 14 | import context.dispatcher 15 | 16 | override val readFromDB: ReadFromDB = (companyName) => { 17 | infraMongodbRepo.findCorpDetail(companyName) 18 | } 19 | 20 | override val readFromInfra: ReadFromInfra = (companyName) => { 21 | infraResource.corpDetail(companyName) 22 | .flatMap { 23 | case Some(company) => 24 | infraMongodbRepo 25 | .saveCorpDetail(companyName, company) 26 | .map(_ => Some(company)) 27 | 28 | case None => 29 | Future.successful(None) 30 | } 31 | } 32 | 33 | } 34 | 35 | object CorpDetailActor { 36 | def props(infraResource: InfraResource, 37 | infraMongodbRepo: InfraMongodbRepo) = 38 | Props(new CorpDetailActor(infraResource, infraMongodbRepo)) 39 | } 40 | -------------------------------------------------------------------------------- /combine-request/src/main/scala/combinerequest/service/EnterpriseService.scala: -------------------------------------------------------------------------------- 1 | package combinerequest.service 2 | 3 | import akka.actor.ActorSystem 4 | import akka.pattern.ask 5 | import akka.util.Timeout 6 | import combinerequest.domain.Company 7 | import combinerequest.message.{GetCompanyMessage, GetCorpDetail} 8 | 9 | import scala.concurrent.{ExecutionContext, Future} 10 | import scala.concurrent.duration._ 11 | 12 | /** 13 | * Created by Yang Jing (yangbajing@gmail.com) on 2016-06-29. 14 | */ 15 | trait CompanyService { 16 | val infraResource: InfraResource 17 | val infraMongodbRepo: InfraMongodbRepo 18 | 19 | def getCorpDetail(companyName: String)(implicit ec: ExecutionContext): Future[Option[Company]] 20 | } 21 | 22 | class EnterpriseService(actorSystem: ActorSystem, 23 | val infraResource: InfraResource, 24 | val infraMongodbRepo: InfraMongodbRepo) extends CompanyService { 25 | 26 | private val master = actorSystem.actorOf(CompanyMaster.props(infraResource, infraMongodbRepo), "infra-company") 27 | private implicit val timeout = Timeout(60.seconds) 28 | 29 | @inline 30 | private def getResult(message: GetCompanyMessage)(implicit ec: ExecutionContext) = 31 | master.ask(message).mapTo[Option[Company]] 32 | 33 | def getCorpDetail(companyName: String)(implicit ec: ExecutionContext) = getResult(GetCorpDetail(companyName)) 34 | 35 | } 36 | -------------------------------------------------------------------------------- /combine-request/src/main/scala/combinerequest/service/ForwardCompanyActor.scala: -------------------------------------------------------------------------------- 1 | package combinerequest.service 2 | 3 | import akka.actor.{Actor, ActorRef} 4 | import combinerequest.domain.Company 5 | import combinerequest.message.{QueryCompany, ReceiveQueryCompanyResult} 6 | 7 | import scala.collection.mutable 8 | import scala.concurrent.Future 9 | 10 | /** 11 | * Created by Yang Jing (yangbajing@gmail.com) on 2016-06-29. 12 | */ 13 | trait ForwardCompanyActor extends Actor { 14 | 15 | import ForwardCompanyActor._ 16 | 17 | val companyListeners = mutable.Map.empty[String, Set[ActorRef]] 18 | 19 | override def receive = { 20 | case QueryCompany(companyName, doSender) => 21 | val listener = if (doSender == Actor.noSender) sender() else doSender 22 | registerListener(companyName, listener) 23 | 24 | case ReceiveQueryCompanyResult(companyName, maybeJsValue) => 25 | dispatchListeners(companyName, maybeJsValue) 26 | } 27 | 28 | def performReadTask(companyName: String): Unit = { 29 | import context.dispatcher 30 | readFromDB(companyName) 31 | .flatMap(maybe => if (maybe.isEmpty) readFromInfra(companyName) else Future.successful(maybe)) 32 | .foreach(maybe => self ! ReceiveQueryCompanyResult(companyName, maybe)) 33 | } 34 | 35 | def registerListener(companyName: String, listener: ActorRef): Unit = 36 | companyListeners.get(companyName) match { 37 | case Some(actors) => 38 | companyListeners.put(companyName, actors + listener) 39 | case None => 40 | companyListeners.put(companyName, Set(listener)) 41 | performReadTask(companyName) 42 | } 43 | 44 | def dispatchListeners(companyName: String, maybeJsValue: Option[Company]): Unit = { 45 | val maybeListener = companyListeners.get(companyName) 46 | maybeListener.foreach { listeners => 47 | for (listener <- listeners) { 48 | listener ! maybeJsValue 49 | } 50 | companyListeners -= companyName 51 | } 52 | } 53 | 54 | val readFromInfra: ReadFromInfra 55 | 56 | val readFromDB: ReadFromDB 57 | } 58 | 59 | object ForwardCompanyActor { 60 | 61 | type ReadFromDB = (String) => Future[Option[Company]] 62 | 63 | type ReadFromInfra = (String) => Future[Option[Company]] 64 | 65 | } 66 | -------------------------------------------------------------------------------- /combine-request/src/main/scala/combinerequest/service/InfraMongodbRepo.scala: -------------------------------------------------------------------------------- 1 | package combinerequest.service 2 | 3 | import java.time.LocalDateTime 4 | import java.util.concurrent.{ConcurrentHashMap, TimeUnit} 5 | 6 | import combinerequest.domain.Company 7 | 8 | import scala.concurrent.{ExecutionContext, Future} 9 | 10 | /** 11 | * Created by Yang Jing (yangbajing@gmail.com) on 2016-06-29. 12 | */ 13 | class InfraMongodbRepo { 14 | 15 | def saveCorpDetail(companyName: String, company: Company)(implicit ec: ExecutionContext): Future[Option[Company]] = 16 | Future { 17 | TimeUnit.MICROSECONDS.sleep(200) 18 | println(s"[${LocalDateTime.now()}] 保存公司: $company 成功") 19 | InfraCompanyDBMock.saveCompany(companyName, company) 20 | } 21 | 22 | def findCorpDetail(companyName: String)(implicit ec: ExecutionContext): Future[Option[Company]] = Future { 23 | TimeUnit.MILLISECONDS.sleep(100) 24 | InfraCompanyDBMock.getCompany(companyName) match { 25 | case some@Some(_) => 26 | println(s"[${LocalDateTime.now()}] 从本地数据库找到公司:$companyName") 27 | some 28 | case None => 29 | println(s"[${LocalDateTime.now()}] 本地数据库未找到公司:$companyName") 30 | None 31 | } 32 | } 33 | 34 | } 35 | 36 | object InfraCompanyDBMock { 37 | private val companies = new ConcurrentHashMap[String, Company]() 38 | 39 | def getCompany(companyName: String) = Option(companies.get(companyName)) 40 | 41 | def saveCompany(companyName: String, company: Company) = Option(companies.put(companyName, company)) 42 | } 43 | -------------------------------------------------------------------------------- /combine-request/src/main/scala/combinerequest/service/InfraResource.scala: -------------------------------------------------------------------------------- 1 | package combinerequest.service 2 | 3 | import java.time.LocalDateTime 4 | import java.util.concurrent.TimeUnit 5 | 6 | import combinerequest.domain.Company 7 | 8 | import scala.concurrent.{ExecutionContext, Future} 9 | 10 | /** 11 | * Created by Yang Jing (yangbajing@gmail.com) on 2016-06-29. 12 | */ 13 | class InfraResource { 14 | 15 | def corpDetail(companyName: String)(implicit ec: ExecutionContext): Future[Option[Company]] = Future { 16 | TimeUnit.SECONDS.sleep(1) 17 | println(s"[${LocalDateTime.now()}] 收到查询:$companyName 工商信息付费请求") 18 | Some(Company(companyName)) 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /email-server/README.md: -------------------------------------------------------------------------------- 1 | # Email服务器 2 | 3 | Email发送服务器 4 | 5 | ## 配置 6 | 7 | 推荐在启动时使用 `-Dapplication.file=/usr/app/etc/emailserver/application.conf` 指定配置文件 8 | 9 | 10 | ## 编译与运行 11 | 12 | ``` 13 | # 编译 14 | ./sbt assembly 15 | 16 | # 运行 17 | java -Dapplication.file=/usr/app/etc/emailserver/application.conf -jar target/scala-2.11/email-server.jar 18 | ``` 19 | 20 | 21 | ## 测试REST服务 22 | 23 | ``` 24 | # 查询存在的邮箱发送者: 25 | curl http://localhost:9999/email/users 26 | 27 | # 发送测试邮件 28 | curl -v -XPOST -H "Content-Type: application/json" \ 29 | -d '{"sender":"Info@email.cn", "subject":"测试邮件","to":["user1@email.cn", "user2@email.cn"],"content":"测试邮件内容咯~"}' \ 30 | http://localhost:9999/email/send 31 | ``` 32 | 33 | 34 | ## 使用JMS发送邮件 35 | 36 | **安装`activemq`** 37 | 38 | Downloads:[http://mirrors.hust.edu.cn/apache/activemq/5.11.1/apache-activemq-5.11.1-bin.tar.gz](http://mirrors.hust.edu.cn/apache/activemq/5.11.1/apache-activemq-5.11.1-bin.tar.gz) 39 | 40 | ``` 41 | tar zxf ~/Downloads/apache-activemq-5.11.1-bin.tar.gz 42 | cd apache-activemq-5.11.1/ 43 | ./bin/activemq-admin start 44 | ``` 45 | 46 | `activemq`管理控制台地址:[http://localhost:8161/admin/](http://localhost:8161/admin/),账号:`admin`,密码:`admin`。 47 | 48 | JMS TCP地址:`tcp://localhost:61616` 49 | 50 | **生产邮件发送请求** 51 | 52 | 修改[EmailProducers.scala](https://github.com/yangbajing/scala-applications/blob/master/email-server/src/main/scala/me/yangbajing/emailserver/demo/EmailProducers.scala)的`activeMqUrl`及`mapMessage`参数,执行`EmailProducers`生产一个邮件发送请求。 53 | 54 | 55 | ## changelog 56 | 57 | **ver: 0.0.2 2015-08-11** 58 | 59 | 实现消费`activemq jms`邮件发送消息 60 | 61 | **ver: 0.0.1 2015-08-11** 62 | 63 | 实现基于`REST API`的邮件发送功能。 64 | -------------------------------------------------------------------------------- /email-server/project/Build.scala: -------------------------------------------------------------------------------- 1 | import sbt.Keys._ 2 | import sbt._ 3 | import sbtassembly.AssemblyKeys 4 | 5 | object Build extends Build { 6 | 7 | override lazy val settings = super.settings :+ { 8 | shellPrompt := (s => Project.extract(s).currentProject.id + " > ") 9 | } 10 | 11 | lazy val root = Project("email-server", file(".")) 12 | .settings( 13 | description := "Email Server", 14 | version := "0.0.2", 15 | homepage := Some(new URL("http://github.com/yangbajing/scala-applications")), 16 | organization := "me.yangbajing", 17 | organizationHomepage := Some(new URL("https://github.com/yangbajing/scala-applications")), 18 | startYear := Some(2015), 19 | scalaVersion := "2.11.12", 20 | scalacOptions ++= Seq( 21 | "-encoding", "utf8", 22 | "-unchecked", 23 | "-feature", 24 | "-deprecation" 25 | ), 26 | javacOptions ++= Seq( 27 | "-encoding", "utf8", 28 | "-Xlint:unchecked", 29 | "-Xlint:deprecation" 30 | ), 31 | javaOptions += "-Dproject.base=" + baseDirectory.value, 32 | publish :=(), 33 | publishLocal :=(), 34 | publishTo := None, 35 | offline := true, 36 | resolvers ++= Seq( 37 | "Typesafe Repository" at "http://repo.typesafe.com/typesafe/releases/", 38 | "Sonatype releases" at "http://oss.sonatype.org/content/repositories/releases", 39 | "Typesafe Snapshots" at "http://repo.typesafe.com/typesafe/snapshots/", 40 | "Sonatype snapshots" at "http://oss.sonatype.org/content/repositories/snapshots"), 41 | AssemblyKeys.assemblyJarName in AssemblyKeys.assembly := "email-server.jar", 42 | mainClass in AssemblyKeys.assembly := Some("me.yangbajing.emailserver.Main"), 43 | libraryDependencies ++= Seq( 44 | _scalaReflect, 45 | _guice, 46 | _redisclient, 47 | _ssc, 48 | _commonsEmail, 49 | _akkaHttp, 50 | _akkaActor, 51 | _akkaSlf4j, 52 | _logback, 53 | _activemqClient, 54 | _typesafeConfig, 55 | _scalaLogging, 56 | _scalatest)) 57 | 58 | val _scalaReflect = "org.scala-lang" % "scala-reflect" % "2.11.12" 59 | val _scalaXml = "org.scala-lang.modules" %% "scala-xml" % "1.0.+" 60 | val _scalatest = "org.scalatest" %% "scalatest" % "2.2.+" % "test" 61 | val _typesafeConfig = "com.typesafe" % "config" % "1.3.+" 62 | val _scalaLogging = ("com.typesafe.scala-logging" %% "scala-logging" % "3.4.0").exclude("org.scala-lang", "scala-library").exclude("org.scala-lang", "scala-reflect") 63 | 64 | // val _akkaHttpPlayJson = "de.heikoseeberger" %% "akka-http-play-json" % "1.0.0" 65 | val verAkka = "2.4.19" 66 | 67 | val _akkaActor = "com.typesafe.akka" %% "akka-actor" % verAkka 68 | val _akkaSlf4j = "com.typesafe.akka" %% "akka-slf4j" % verAkka 69 | val _akkaStream = "com.typesafe.akka" %% "akka-stream" % verAkka 70 | val _akkaHttp = "com.typesafe.akka" %% "akka-http-experimental" % verAkka 71 | 72 | val _ssc = "com.elderresearch" %% "ssc" % "0.2.0" 73 | 74 | val _guice = "com.google.inject" % "guice" % "4.1.0" 75 | 76 | val _redisclient = "net.debasishg" %% "redisclient" % "3.0" 77 | 78 | val _logback = "ch.qos.logback" % "logback-classic" % "1.1.+" 79 | val _commonsEmail = "org.apache.commons" % "commons-email" % "1.4" 80 | val _reactivemongo = "org.reactivemongo" %% "reactivemongo" % "0.11.+" 81 | val _guava = "com.google.guava" % "guava" % "19.+" 82 | val _activemqClient = "org.apache.activemq" % "activemq-client" % "5.13.+" 83 | 84 | } 85 | 86 | -------------------------------------------------------------------------------- /email-server/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.1.6 2 | -------------------------------------------------------------------------------- /email-server/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.3.5") 2 | 3 | addSbtPlugin("com.thoughtworks.sbt-api-mappings" % "sbt-api-mappings" % "0.2.3") 4 | 5 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.3") 6 | -------------------------------------------------------------------------------- /email-server/project/sbt-launch.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yangbajing/scala-applications/a41bd2b1e5eef086cf54fe58630bb47d7c16df39/email-server/project/sbt-launch.jar -------------------------------------------------------------------------------- /email-server/sbt: -------------------------------------------------------------------------------- 1 | SCRIPT_DIR=`dirname $0` 2 | java -Dfile.encoding=UTF-8 -noverify -Xms512M -Xmx1024M -Xss1M -XX:+CMSClassUnloadingEnabled -jar "$SCRIPT_DIR/../_lib/sbt-launch.jar" $@ 3 | -------------------------------------------------------------------------------- /email-server/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | emailserver { 2 | server { 3 | interface = "0.0.0.0" 4 | port = 9999 5 | } 6 | 7 | emails { 8 | email1 { 9 | userName = "email1@email.com" 10 | password = "" 11 | smtp = "" 12 | smtpPort = 0 13 | ssl = true 14 | 15 | # 同一个组内的邮件会串行发送,此key可忽略 16 | group = "1" 17 | } 18 | 19 | email2 { 20 | userName = "email2@email.com" 21 | password = "" 22 | smtp = "" 23 | smtpPort = 0 24 | ssl = true 25 | group = "2" 26 | } 27 | } 28 | 29 | activemq { 30 | url = "tcp://127.0.0.1:61616" 31 | emailQueueName = "EmailQueue" 32 | } 33 | } -------------------------------------------------------------------------------- /email-server/src/main/scala/me/yangbajing/emailserver/ActorService.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.emailserver 2 | 3 | import akka.actor.ActorSystem 4 | import akka.stream.ActorMaterializer 5 | 6 | /** 7 | * Created by Yang Jing (yangbajing@gmail.com) on 2015-08-11. 8 | */ 9 | trait ActorService { 10 | implicit val system: ActorSystem 11 | 12 | implicit val materializer: ActorMaterializer 13 | } 14 | -------------------------------------------------------------------------------- /email-server/src/main/scala/me/yangbajing/emailserver/JsonImplicits.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.emailserver 2 | 3 | import java.time.LocalDateTime 4 | 5 | import me.yangbajing.emailserver.common.settings.{SettingActivemq, Setting, SettingServer} 6 | import me.yangbajing.emailserver.domain.{EmailSender, SendEmail} 7 | import me.yangbajing.emailserver.util.Utils 8 | import play.api.libs.json._ 9 | 10 | /** 11 | * Play Json trans Case Class 12 | * Created by Yang Jing (yangbajing@gmail.com) on 2015-08-10. 13 | */ 14 | trait JsonImplicits { 15 | implicit val __localDateTime = new Format[LocalDateTime] { 16 | override def writes(o: LocalDateTime): JsValue = JsString(o.format(Utils.formatterDateTime)) 17 | 18 | override def reads(json: JsValue): JsResult[LocalDateTime] = 19 | JsSuccess(LocalDateTime.parse(json.as[String], Utils.formatterDateTime)) 20 | } 21 | implicit val __sendEmailFormats = Json.format[SendEmail] 22 | implicit val __emailSenderFormats = Json.format[EmailSender] 23 | implicit val __settingServerFormats = Json.format[SettingServer] 24 | implicit val __settingActivemqFormats = Json.format[SettingActivemq] 25 | implicit val __settingFormats = Json.format[Setting] 26 | } 27 | 28 | object JsonImplicits extends JsonImplicits 29 | -------------------------------------------------------------------------------- /email-server/src/main/scala/me/yangbajing/emailserver/Main.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.emailserver 2 | 3 | import akka.http.scaladsl.Http 4 | import akka.http.scaladsl.Http.ServerBinding 5 | import me.yangbajing.emailserver.common.settings.Settings 6 | import me.yangbajing.emailserver.route.Routes 7 | import me.yangbajing.emailserver.service.{EmailService, MQConsumerService} 8 | import me.yangbajing.emailserver.util.Utils 9 | 10 | /** 11 | * Email Server Main 12 | * Created by Yang Jing (yangbajing@gmail.com) on 2015-08-10. 13 | */ 14 | object Main { 15 | 16 | import Utils.{materializer, system} 17 | import system.dispatcher 18 | 19 | def main(args: Array[String]): Unit = { 20 | val emailService = new EmailService(Settings.config.emails) 21 | val mqConsumerService = new MQConsumerService(Settings.config.activemq, emailService) 22 | val routes = new Routes(emailService, mqConsumerService) 23 | val bindingFuture = Http().bindAndHandle(routes.routes, Settings.config.server.interface, Settings.config.server.port) 24 | 25 | bindingFuture.onSuccess { 26 | case ServerBinding(inet) => 27 | println("binding to: " + inet) 28 | mqConsumerService.start() 29 | 30 | case other => 31 | println("biding other: " + other) 32 | mqConsumerService.close() 33 | system.shutdown() 34 | } 35 | bindingFuture.onFailure { 36 | case e: Exception => 37 | mqConsumerService.close() 38 | system.shutdown() 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /email-server/src/main/scala/me/yangbajing/emailserver/common/enums/MimeType.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.emailserver.common.enums 2 | 3 | import play.api.libs.json._ 4 | 5 | /** 6 | * 邮件格式 7 | * Created by Yang Jing (yangbajing@gmail.com) on 2015-08-11. 8 | */ 9 | object MimeType extends Enumeration { 10 | type MimeType = Value 11 | val Text = Value("text") 12 | val Html = Value("html") 13 | 14 | implicit val __mimeTypeFormats = new Format[MimeType] { 15 | override def writes(o: MimeType.Value): JsValue = JsString(o.toString) 16 | 17 | override def reads(json: JsValue): JsResult[MimeType.Value] = JsSuccess(MimeType.withName(json.as[String])) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /email-server/src/main/scala/me/yangbajing/emailserver/common/settings/Settings.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.emailserver.common.settings 2 | 3 | import java.io.File 4 | 5 | import com.typesafe.config.{Config, ConfigFactory} 6 | import me.yangbajing.emailserver.domain.EmailSender 7 | 8 | import scala.collection.JavaConverters._ 9 | import scala.util.Try 10 | 11 | /** 12 | * Email Server Setting 13 | * Created by Yang Jing (yangbajing@gmail.com) on 2015-08-11. 14 | */ 15 | object Settings { 16 | val config = { 17 | val c = 18 | Option(System.getProperty("application.file")).map(file => ConfigFactory.parseFile(new File(file))) 19 | .getOrElse(ConfigFactory.load()).getConfig("emailserver") 20 | _setting(c) 21 | } 22 | 23 | private def _setting(c: Config) = { 24 | val emailConf = c.getConfig("emails") 25 | val emails = emailConf.entrySet().asScala.toStream 26 | .filter(entry => entry.getKey.endsWith(".userName")) 27 | .map(_.getKey.replace(".userName", "")) 28 | .map(key => _emailSender(emailConf.getConfig(key))) 29 | .toVector 30 | Setting(_server(c.getConfig("server")), emails, _activemq(c.getConfig("activemq"))) 31 | } 32 | 33 | private def _emailSender(c: Config): EmailSender = { 34 | EmailSender(c.getString("userName"), c.getString("password"), c.getString("smtp"), c.getInt("smtpPort"), 35 | c.getBoolean("ssl"), Try(c.getBoolean("default")).getOrElse(false), Try(c.getString("group")).getOrElse("")) 36 | } 37 | 38 | private def _server(c: Config) = SettingServer(c.getString("interface"), c.getInt("port")) 39 | 40 | private def _activemq(c: Config) = SettingActivemq(c.getString("url"), 41 | Try(c.getString("userName")).toOption, Try(c.getString("password")).toOption, 42 | c.getString("emailQueueName")) 43 | } 44 | 45 | case class SettingActivemq(url: String, userName: Option[String], password: Option[String], emailQueueName: String) 46 | 47 | case class SettingServer(interface: String, port: Int) 48 | 49 | case class Setting(server: SettingServer, emails: Seq[EmailSender], activemq: SettingActivemq) 50 | -------------------------------------------------------------------------------- /email-server/src/main/scala/me/yangbajing/emailserver/demo/ConsumerSynchronous.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.emailserver.demo 2 | 3 | import javax.jms._ 4 | 5 | import org.apache.activemq.ActiveMQConnectionFactory 6 | 7 | /** 8 | * Created by Yang Jing (yangbajing@gmail.com) on 2015-08-11. 9 | */ 10 | object ConsumerSynchronous extends App { 11 | val activeMqUrl = "tcp://192.168.31.116:61616" 12 | 13 | val connFactory = new ActiveMQConnectionFactory(activeMqUrl) 14 | val conn = connFactory.createConnection() 15 | conn.setClientID("ConsumerSynchronous") 16 | conn.start() 17 | 18 | println("Started") 19 | 20 | val session = conn.createSession(false, Session.AUTO_ACKNOWLEDGE) 21 | val queue = session.createQueue("EmailQueue") 22 | val consumer = session.createConsumer(queue) 23 | 24 | val listener = new MessageListener { 25 | override def onMessage(message: Message): Unit = message match { 26 | case txt: TextMessage => { 27 | val replyProducer = session.createProducer(txt.getJMSReplyTo) 28 | replyProducer.setDeliveryMode(DeliveryMode.NON_PERSISTENT) 29 | 30 | println("Received message: " + txt.getText) 31 | 32 | val replyMessage = session.createTextMessage("Yes I received your message!") 33 | replyMessage.setJMSCorrelationID(txt.getJMSCorrelationID) 34 | 35 | println("Reply sent!") 36 | 37 | replyProducer.send(replyMessage) 38 | } 39 | 40 | case _ => 41 | throw new RuntimeException("Unhandled Message Type: " + message) 42 | } 43 | } 44 | 45 | consumer.setMessageListener(listener) 46 | } 47 | -------------------------------------------------------------------------------- /email-server/src/main/scala/me/yangbajing/emailserver/demo/EmailProducers.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.emailserver.demo 2 | 3 | import javax.jms.{DeliveryMode, Session, TextMessage} 4 | 5 | import org.apache.activemq.ActiveMQConnectionFactory 6 | 7 | import scala.util.Random 8 | 9 | /** 10 | * 邮件生产者 11 | * Created by Yang Jing (yangbajing@gmail.com) on 2015-08-11. 12 | */ 13 | object EmailProducers extends App { 14 | val activeMqUrl = "tcp://192.168.31.116:61616" 15 | 16 | val connFactory = new ActiveMQConnectionFactory(activeMqUrl) 17 | val conn = connFactory.createConnection() 18 | conn.setClientID("ProducerEmail") 19 | conn.start() 20 | 21 | val session = conn.createSession(false, Session.AUTO_ACKNOWLEDGE) 22 | val sendQueue = session.createQueue("EmailQueue") 23 | // val replyQueue = session.createQueue("ReplyToSynchronousMsgQueue") 24 | 25 | val producer = session.createProducer(sendQueue) 26 | producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT) 27 | 28 | val correlationId = new Random().nextString(32) 29 | // val replyConsumer = session.createConsumer(replyQueue, s"JMSCorrelationID = '$correlationId'") 30 | 31 | val mapMessage = session.createMapMessage() 32 | // mapMessage.setJMSReplyTo(replyQueue) 33 | mapMessage.setJMSCorrelationID(correlationId) 34 | 35 | mapMessage.setString("subject", "Activemq jms 邮件测试") 36 | mapMessage.setString("to", "yang.xunjing@qq.com;jing.yang@socialcredits.cn") 37 | mapMessage.setString("content", "Activemq jms 邮件测试的内容") 38 | mapMessage.setString("sender", "Info@socialcredits.cn") 39 | mapMessage.setString("mimeType", "text") 40 | 41 | println("Sending message...") 42 | 43 | producer.send(mapMessage) 44 | 45 | println("Waiting for reply...") 46 | 47 | // val reply = replyConsumer.receive(1000) 48 | // replyConsumer.close() 49 | 50 | // reply match { 51 | // case txt: TextMessage => println("Received reply: " + txt.getText) 52 | // case _ => throw new RuntimeException("Invalid Response: " + reply) 53 | // } 54 | 55 | conn.close() 56 | } 57 | -------------------------------------------------------------------------------- /email-server/src/main/scala/me/yangbajing/emailserver/demo/ProducerSynchronous.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.emailserver.demo 2 | 3 | import javax.jms.{TextMessage, DeliveryMode, Session} 4 | 5 | import org.apache.activemq.ActiveMQConnectionFactory 6 | 7 | import scala.util.Random 8 | 9 | /** 10 | * Active Producer 11 | * Created by Yang Jing (yangbajing@gmail.com) on 2015-08-11. 12 | */ 13 | object ProducerSynchronous extends App { 14 | val activeMqUrl = "tcp://192.168.31.116:61616" 15 | 16 | val connFactory = new ActiveMQConnectionFactory(activeMqUrl) 17 | val conn = connFactory.createConnection() 18 | conn.setClientID("ProducerSynchronous") 19 | conn.start() 20 | 21 | val session = conn.createSession(false, Session.AUTO_ACKNOWLEDGE) 22 | val sendQueue = session.createQueue("EmailQueue") 23 | val replyQueue = session.createQueue("ReplyToSynchronousMsgQueue") 24 | 25 | val producer = session.createProducer(sendQueue) 26 | producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT) 27 | 28 | val correlationId = new Random().nextString(32) 29 | val replyConsumer = session.createConsumer(replyQueue, s"JMSCorrelationID = '$correlationId'") 30 | 31 | val textMessage = session.createTextMessage("Hello, please reply immediately to my message!") 32 | textMessage.setJMSReplyTo(replyQueue) 33 | textMessage.setJMSCorrelationID(correlationId) 34 | 35 | println("Sending message...") 36 | 37 | producer.send(textMessage) 38 | 39 | println("Waiting for reply...") 40 | 41 | val reply = replyConsumer.receive(1000) 42 | replyConsumer.close() 43 | 44 | reply match { 45 | case txt: TextMessage => println("Received reply: " + txt.getText) 46 | case _ => throw new RuntimeException("Invalid Response: " + reply) 47 | } 48 | 49 | conn.close() 50 | } 51 | -------------------------------------------------------------------------------- /email-server/src/main/scala/me/yangbajing/emailserver/domain/Emails.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.emailserver.domain 2 | 3 | import me.yangbajing.emailserver.common.enums.MimeType 4 | 5 | /** 6 | * Email Message 7 | * Created by Yang Jing (yangbajing@gmail.com) on 2015-08-10. 8 | */ 9 | case class SendEmail(sender: String, subject: String, to: Seq[String], content: String, from: Option[String], 10 | mimeType: MimeType.MimeType) 11 | 12 | case object GetEmailSenders 13 | 14 | case class EmailSender(userName: String, password: String, smtp: String, smtpPort: Int, ssl: Boolean, group: String) 15 | -------------------------------------------------------------------------------- /email-server/src/main/scala/me/yangbajing/emailserver/route/Routes.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.emailserver.route 2 | 3 | import akka.actor.ActorSystem 4 | import akka.http.scaladsl.model.StatusCodes 5 | import akka.http.scaladsl.server.Directives._ 6 | import akka.stream.ActorMaterializer 7 | import akka.util.Timeout 8 | import de.heikoseeberger.akkahttpplayjson.PlayJsonSupport._ 9 | import me.yangbajing.emailserver.ActorService 10 | import me.yangbajing.emailserver.JsonImplicits._ 11 | import me.yangbajing.emailserver.domain.SendEmail 12 | import me.yangbajing.emailserver.service.{EmailService, MQConsumerService} 13 | import play.api.libs.json.{JsValue, Json} 14 | 15 | import scala.concurrent.duration._ 16 | import scala.util.{Failure, Success} 17 | 18 | /** 19 | * 路由 20 | * Created by Yang Jing (yangbajing@gmail.com) on 2015-08-10. 21 | */ 22 | class Routes(val emailService: EmailService, 23 | val mQConsumerService: MQConsumerService)(implicit val system: ActorSystem, val materializer: ActorMaterializer) extends ActorService { 24 | implicit val timeout = Timeout(60.seconds) 25 | 26 | val routes = 27 | pathPrefix("email") { 28 | path("send") { 29 | post { 30 | entity(as[JsValue].map(_.as[SendEmail])) { sendEmail => 31 | onComplete(emailService.sendEmail(sendEmail)) { 32 | case Success(value) => 33 | value match { 34 | case Right(msg) => complete(msg) 35 | case Left(msg) => complete(StatusCodes.Forbidden, msg) 36 | } 37 | 38 | case Failure(e) => complete(StatusCodes.InternalServerError, "SendEmail an error occurred: " + e.getMessage) 39 | } 40 | } 41 | } 42 | } ~ 43 | path("users") { 44 | get { 45 | onComplete(emailService.getEmailSenders) { 46 | case Success(emailSenders) => complete(Json.toJson(emailSenders)) 47 | 48 | case Failure(e) => complete(StatusCodes.InternalServerError, "An error occurred: " + e.getMessage) 49 | } 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /email-server/src/main/scala/me/yangbajing/emailserver/service/EmailService.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.emailserver.service 2 | 3 | import akka.actor.ActorRefFactory 4 | import akka.pattern.ask 5 | import akka.util.Timeout 6 | import me.yangbajing.emailserver.domain.{EmailSender, GetEmailSenders, SendEmail} 7 | import me.yangbajing.emailserver.service.actors.EmailMaster 8 | 9 | /** 10 | * 邮件服务 11 | * Created by Yang Jing (yangbajing@gmail.com) on 2015-08-10. 12 | */ 13 | class EmailService(initEmailSenders: Seq[EmailSender])(implicit actorRefFactory: ActorRefFactory) { 14 | val emailMaster = actorRefFactory.actorOf(EmailMaster.props(initEmailSenders), "email-master") 15 | 16 | def sendEmail(email: SendEmail)(implicit timeout: Timeout) = { 17 | (emailMaster ? email).mapTo[Either[String, String]] 18 | } 19 | 20 | def getEmailSenders(implicit timeout: Timeout) = { 21 | (emailMaster ? GetEmailSenders).mapTo[Seq[EmailSender]] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /email-server/src/main/scala/me/yangbajing/emailserver/service/MQConsumerService.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.emailserver.service 2 | 3 | import javax.jms._ 4 | 5 | import com.typesafe.scalalogging.StrictLogging 6 | import me.yangbajing.emailserver.common.enums.MimeType 7 | import me.yangbajing.emailserver.common.settings.SettingActivemq 8 | import me.yangbajing.emailserver.domain.SendEmail 9 | import me.yangbajing.emailserver.util.Utils 10 | import org.apache.activemq.ActiveMQConnectionFactory 11 | 12 | /** 13 | * 消息队列消费者 14 | * Created by Yang Jing (yangbajing@gmail.com) on 2015-08-11. 15 | */ 16 | class MQConsumerService(setting: SettingActivemq, emailService: EmailService) extends StrictLogging { 17 | 18 | import Utils.system.dispatcher 19 | import Utils.timeout 20 | 21 | val connFactory = new ActiveMQConnectionFactory(setting.url) 22 | val conn = 23 | (for { 24 | userName <- setting.userName 25 | password <- setting.password 26 | } yield connFactory.createConnection("admin", "admin")) getOrElse connFactory.createConnection() 27 | 28 | def start() { 29 | 30 | conn.setClientID("ConsumerSynchronous") 31 | conn.start() 32 | 33 | val session = conn.createSession(false, Session.AUTO_ACKNOWLEDGE) 34 | val queue = session.createQueue(setting.emailQueueName) 35 | val consumer = session.createConsumer(queue) 36 | 37 | val listener = new MessageListener { 38 | override def onMessage(message: Message): Unit = message match { 39 | case msg: MapMessage => { 40 | val subject = msg.getString("subject") 41 | val sender = msg.getString("sender") 42 | val content = msg.getString("content") 43 | val to = msg.getString("to").split(";") 44 | val mimeType = Option(msg.getString("mimeType")).map(MimeType.withName).getOrElse(MimeType.Text) 45 | val sendEmail = SendEmail(sender, subject, to, content, None, mimeType) 46 | emailService.sendEmail(sendEmail).onComplete(result => logger.debug(result.toString)) 47 | } 48 | 49 | case txt: TextMessage => { 50 | val replyProducer = session.createProducer(txt.getJMSReplyTo) 51 | replyProducer.setDeliveryMode(DeliveryMode.NON_PERSISTENT) 52 | 53 | logger.debug("Received message: " + txt.getText) 54 | 55 | val replyMessage = session.createTextMessage("Yes I received your message!") 56 | replyMessage.setJMSCorrelationID(txt.getJMSCorrelationID) 57 | 58 | logger.debug("Reply sent!") 59 | 60 | replyProducer.send(replyMessage) 61 | } 62 | 63 | case _ => 64 | logger.error("Unhandled Message Type: " + message) 65 | } 66 | } 67 | 68 | consumer.setMessageListener(listener) 69 | } 70 | 71 | def close() { 72 | conn.close() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /email-server/src/main/scala/me/yangbajing/emailserver/service/actors/EmailGroupActor.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.emailserver.service.actors 2 | 3 | import akka.actor.{Actor, ActorLogging, Props} 4 | import me.yangbajing.emailserver.common.enums.MimeType 5 | import me.yangbajing.emailserver.domain.{EmailSender, SendEmail} 6 | import org.apache.commons.mail.{HtmlEmail, SimpleEmail} 7 | 8 | /** 9 | * 邮箱发送组 10 | * 每个邮件发送组内串行发送邮件 11 | * Created by Yang Jing (yangbajing@gmail.com) on 2015-08-11. 12 | */ 13 | class EmailGroupActor(emailSenders: Map[String, EmailSender]) extends Actor with ActorLogging { 14 | 15 | override def receive: Receive = { 16 | case se: SendEmail => 17 | emailSenders.get(se.sender) match { 18 | case Some(emailSender) => 19 | sendEmail(emailSender, se) 20 | 21 | case None => 22 | log.error(s"userName: ${se.sender} not found. $se") 23 | } 24 | } 25 | 26 | 27 | /** 28 | * @param emailSender email sender 29 | * @param se 30 | */ 31 | private def sendEmail(emailSender: EmailSender, se: SendEmail): Unit = { 32 | val email = 33 | if (se.mimeType == MimeType.Text) generateSimpleEmail(emailSender, se) 34 | else generateHtmlEmail(emailSender, se) 35 | val sendMsg = email.send() 36 | log.debug(s"sendEmail $email: $sendMsg") 37 | } 38 | 39 | private def generateHtmlEmail(emailSender: EmailSender, se: SendEmail) = { 40 | val email = new HtmlEmail() 41 | email.setHostName(emailSender.smtp) 42 | email.setSmtpPort(emailSender.smtpPort) 43 | email.setAuthentication(emailSender.userName, emailSender.password) 44 | email.setSSLOnConnect(emailSender.ssl) 45 | email.addTo(se.to: _*) 46 | email.setFrom(se.from.getOrElse(emailSender.userName)) 47 | email.setSubject(se.subject) 48 | email.setHtmlMsg(se.content) 49 | email 50 | } 51 | 52 | private def generateSimpleEmail(emailSender: EmailSender, se: SendEmail) = { 53 | val email = new SimpleEmail() 54 | email.setHostName(emailSender.smtp) 55 | email.setSmtpPort(emailSender.smtpPort) 56 | email.setAuthentication(emailSender.userName, emailSender.password) 57 | email.setSSLOnConnect(emailSender.ssl) 58 | email.addTo(se.to: _*) 59 | email.setFrom(se.from.getOrElse(emailSender.userName)) 60 | email.setSubject(se.subject) 61 | email.setMsg(se.content) 62 | email 63 | } 64 | } 65 | 66 | object EmailGroupActor { 67 | val ACTOR_NAME_PREFIX = "group-" 68 | 69 | def props(emailSenders: Map[String, EmailSender]) = Props(new EmailGroupActor(emailSenders)) 70 | } 71 | -------------------------------------------------------------------------------- /email-server/src/main/scala/me/yangbajing/emailserver/service/actors/EmailMaster.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.emailserver.service.actors 2 | 3 | import akka.actor.{Actor, Props} 4 | import me.yangbajing.emailserver.domain.{EmailSender, GetEmailSenders, SendEmail} 5 | 6 | /** 7 | * 邮件发送Master 8 | * Created by Yang Jing (yangbajing@gmail.com) on 2015-08-11. 9 | */ 10 | class EmailMaster(initEmailSenders: Seq[EmailSender]) extends Actor { 11 | val groups = initEmailSenders.groupBy(_.group) 12 | val emailSenders = initEmailSenders.map(s => s.userName -> s).toMap 13 | 14 | // // TODO 自定义监管策略 15 | // override def supervisorStrategy: SupervisorStrategy = super.supervisorStrategy 16 | 17 | override def preStart(): Unit = { 18 | for ((group, emailSenders) <- groups) { 19 | context.actorOf( 20 | EmailGroupActor.props(emailSenders.map(s => s.userName -> s).toMap), 21 | EmailGroupActor.ACTOR_NAME_PREFIX + group) 22 | } 23 | } 24 | 25 | override def receive = { 26 | case sendEmail: SendEmail => { 27 | val userName = sendEmail.sender 28 | emailSenders.get(userName) match { 29 | case Some(emailSender) => 30 | context.child(EmailGroupActor.ACTOR_NAME_PREFIX + emailSender.group) match { 31 | case Some(ref) => 32 | ref ! sendEmail 33 | sender() ! Right("email added send queue") 34 | 35 | case None => 36 | sender() ! Left(s"actor: ${EmailGroupActor.ACTOR_NAME_PREFIX}-${emailSender.group} not found") 37 | } 38 | 39 | case None => 40 | sender() ! Left(s"userName: $userName not found") 41 | } 42 | } 43 | 44 | case GetEmailSenders => 45 | sender() ! initEmailSenders 46 | } 47 | } 48 | 49 | object EmailMaster { 50 | def props(initEmailSender: Seq[EmailSender]) = Props(new EmailMaster(initEmailSender)) 51 | } -------------------------------------------------------------------------------- /email-server/src/main/scala/me/yangbajing/emailserver/util/Utils.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.emailserver.util 2 | 3 | import java.time.format.DateTimeFormatter 4 | import java.util.concurrent.TimeUnit 5 | 6 | import akka.actor.ActorSystem 7 | import akka.stream.ActorMaterializer 8 | import akka.util.Timeout 9 | 10 | /** 11 | * 工具 12 | * Created by Yang Jing (yangbajing@gmail.com) on 2015-08-10. 13 | */ 14 | object Utils { 15 | implicit val system = ActorSystem("email-server") 16 | implicit val materializer = ActorMaterializer() 17 | implicit val timeout = Timeout(60, TimeUnit.SECONDS) 18 | 19 | val formatterDateTime = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") 20 | 21 | } 22 | -------------------------------------------------------------------------------- /email-server/src/test/scala/me/yangbajing/emailserver/common/settings/SettingsTest.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.emailserver.common.settings 2 | 3 | import org.scalatest.{Matchers, WordSpec} 4 | 5 | /** 6 | * Settings Test 7 | * Created by Yang Jing (yangbajing@gmail.com) on 2015-08-11. 8 | */ 9 | class SettingsTest extends WordSpec with Matchers { 10 | "Settings" should { 11 | "emails is nonEmpty" in { 12 | Settings.config.emails should have size 2 13 | } 14 | 15 | "server is nonEmpty" in { 16 | Settings.config.server.interface should not be empty 17 | Settings.config.server.port should be > 0 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /email-server/src/test/scala/sw.sc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yangbajing/scala-applications/a41bd2b1e5eef086cf54fe58630bb47d7c16df39/email-server/src/test/scala/sw.sc -------------------------------------------------------------------------------- /file-upload/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### macOS template 3 | # General 4 | .DS_Store 5 | .AppleDouble 6 | .LSOverride 7 | 8 | # Icon must end with two \r 9 | Icon 10 | 11 | # Thumbnails 12 | ._* 13 | 14 | # Files that might appear in the root of a volume 15 | .DocumentRevisions-V100 16 | .fseventsd 17 | .Spotlight-V100 18 | .TemporaryItems 19 | .Trashes 20 | .VolumeIcon.icns 21 | .com.apple.timemachine.donotpresent 22 | 23 | # Directories potentially created on remote AFP share 24 | .AppleDB 25 | .AppleDesktop 26 | Network Trash Folder 27 | Temporary Items 28 | .apdisk 29 | ### Vim template 30 | # Swap 31 | [._]*.s[a-v][a-z] 32 | [._]*.sw[a-p] 33 | [._]s[a-rt-v][a-z] 34 | [._]ss[a-gi-z] 35 | [._]sw[a-p] 36 | 37 | # Session 38 | Session.vim 39 | 40 | # Temporary 41 | .netrwhist 42 | *~ 43 | # Auto-generated tag files 44 | tags 45 | # Persistent undo 46 | [._]*.un~ 47 | ### NotepadPP template 48 | # Notepad++ backups # 49 | *.bak 50 | ### SBT template 51 | # Simple Build Tool 52 | # http://www.scala-sbt.org/release/docs/Getting-Started/Directories.html#configuring-version-control 53 | 54 | dist/* 55 | target/ 56 | lib_managed/ 57 | src_managed/ 58 | project/boot/ 59 | project/plugins/project/ 60 | .history 61 | .cache 62 | .lib/ 63 | ### Emacs template 64 | # -*- mode: gitignore; -*- 65 | \#*\# 66 | /.emacs.desktop 67 | /.emacs.desktop.lock 68 | *.elc 69 | auto-save-list 70 | tramp 71 | .\#* 72 | 73 | # Org-mode 74 | .org-id-locations 75 | *_archive 76 | 77 | # flymake-mode 78 | *_flymake.* 79 | 80 | # eshell files 81 | /eshell/history 82 | /eshell/lastdir 83 | 84 | # elpa packages 85 | /elpa/ 86 | 87 | # reftex files 88 | *.rel 89 | 90 | # AUCTeX auto folder 91 | /auto/ 92 | 93 | # cask packages 94 | .cask/ 95 | dist/ 96 | 97 | # Flycheck 98 | flycheck_*.el 99 | 100 | # server auth directory 101 | /server/ 102 | 103 | # projectiles files 104 | .projectile 105 | 106 | # directory configuration 107 | .dir-locals.el 108 | ### Eclipse template 109 | 110 | .metadata 111 | bin/ 112 | tmp/ 113 | *.tmp 114 | *.swp 115 | *~.nib 116 | local.properties 117 | .settings/ 118 | .loadpath 119 | .recommenders 120 | 121 | # External tool builders 122 | .externalToolBuilders/ 123 | 124 | # Locally stored "Eclipse launch configurations" 125 | *.launch 126 | 127 | # PyDev specific (Python IDE for Eclipse) 128 | *.pydevproject 129 | 130 | # CDT-specific (C/C++ Development Tooling) 131 | .cproject 132 | 133 | # CDT- autotools 134 | .autotools 135 | 136 | # Java annotation processor (APT) 137 | .factorypath 138 | 139 | # PDT-specific (PHP Development Tools) 140 | .buildpath 141 | 142 | # sbteclipse plugin 143 | .target 144 | 145 | # Tern plugin 146 | .tern-project 147 | 148 | # TeXlipse plugin 149 | .texlipse 150 | 151 | # STS (Spring Tool Suite) 152 | .springBeans 153 | 154 | # Code Recommenders 155 | .recommenders/ 156 | 157 | # Annotation Processing 158 | .apt_generated/ 159 | 160 | # Scala IDE specific (Scala & Java development for Eclipse) 161 | .cache-main 162 | .scala_dependencies 163 | .worksheet 164 | ### Windows template 165 | # Windows thumbnail cache files 166 | Thumbs.db 167 | ehthumbs.db 168 | ehthumbs_vista.db 169 | 170 | # Dump file 171 | *.stackdump 172 | 173 | # Folder config file 174 | [Dd]esktop.ini 175 | 176 | # Recycle Bin used on file shares 177 | $RECYCLE.BIN/ 178 | 179 | # Windows Installer files 180 | *.cab 181 | *.msi 182 | *.msix 183 | *.msm 184 | *.msp 185 | 186 | # Windows shortcuts 187 | *.lnk 188 | ### JetBrains template 189 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 190 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 191 | 192 | # User-specific stuff 193 | .idea/ 194 | 195 | # CMake 196 | cmake-build-*/ 197 | 198 | # Mongo Explorer plugin 199 | .idea/**/mongoSettings.xml 200 | 201 | # File-based project format 202 | *.iws 203 | 204 | # IntelliJ 205 | out/ 206 | 207 | # mpeltonen/sbt-idea plugin 208 | .idea_modules/ 209 | 210 | # JIRA plugin 211 | atlassian-ide-plugin.xml 212 | 213 | # Cursive Clojure plugin 214 | .idea/replstate.xml 215 | 216 | # Crashlytics plugin (for Android Studio and IntelliJ) 217 | com_crashlytics_export_strings.xml 218 | crashlytics.properties 219 | crashlytics-build.properties 220 | fabric.properties 221 | 222 | # Editor-based Rest Client 223 | .idea/httpRequests 224 | ### GitBook template 225 | # Node rules: 226 | ## Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 227 | .grunt 228 | 229 | ## Dependency directory 230 | ## Commenting this out is preferred by some people, see 231 | ## https://docs.npmjs.com/misc/faq#should-i-check-my-node_modules-folder-into-git 232 | node_modules 233 | 234 | # Book build output 235 | _book 236 | 237 | # eBook build output 238 | *.epub 239 | *.mobi 240 | *.pdf 241 | ### NetBeans template 242 | nbproject/private/ 243 | build/ 244 | nbbuild/ 245 | nbdist/ 246 | .nb-gradle/ 247 | ### Linux template 248 | 249 | # temporary files which can be created if a process still has a handle open of a deleted file 250 | .fuse_hidden* 251 | 252 | # KDE directory preferences 253 | .directory 254 | 255 | # Linux trash folder which might appear on any partition or disk 256 | .Trash-* 257 | 258 | # .nfs files are created when an open file is removed but is still being accessed 259 | .nfs* 260 | ### VisualStudioCode template 261 | .vscode/* 262 | !.vscode/settings.json 263 | !.vscode/tasks.json 264 | !.vscode/launch.json 265 | !.vscode/extensions.json 266 | -------------------------------------------------------------------------------- /file-upload/.scalafmt.conf: -------------------------------------------------------------------------------- 1 | //version = "2.0.0" 2 | style = defaultWithAlign 3 | lineEndings = unix 4 | encoding = "UTF-8" 5 | project.git = true 6 | docstrings = JavaDoc 7 | maxColumn = 120 8 | //indentOperator = spray 9 | unindentTopLevelOperators = true 10 | align.openParenDefnSite = false 11 | align.openParenCallSite = false 12 | optIn.breakChainOnFirstMethodDot = false 13 | optIn.configStyleArguments = false 14 | danglingParentheses = false 15 | spaces.inImportCurlyBraces = false 16 | newlines.alwaysBeforeTopLevelStatements = true 17 | rewrite.rules = [RedundantParens, AvoidInfix, SortModifiers, PreferCurlyFors, ExpandImportSelectors] 18 | rewrite.sortModifiers.order = [ 19 | "implicit", "final", "sealed", "abstract", 20 | "override", "private", "protected", "lazy" 21 | ] 22 | rewrite.neverInfix.excludeFilters = [ 23 | and 24 | min 25 | max 26 | until 27 | to 28 | by 29 | eq 30 | ne 31 | "should.*" 32 | "contain.*" 33 | "must.*" 34 | in 35 | ignore 36 | be 37 | taggedAs 38 | thrownBy 39 | synchronized 40 | have 41 | when 42 | size 43 | only 44 | noneOf 45 | oneElementOf 46 | noElementsOf 47 | atLeastOneElementOf 48 | atMostOneElementOf 49 | allElementsOf 50 | inOrderElementsOf 51 | theSameElementsAs 52 | ] 53 | -------------------------------------------------------------------------------- /file-upload/Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent { 3 | // 此处设定构建环境,目前可选有 4 | // default, java-8, python-3.5, ruby-2.3, go-1.11 等 5 | // 详情请阅 https://dev.tencent.com/help/knowledge-base/how-to-use-ci#agents 6 | label "java-8" 7 | } 8 | stages { 9 | 10 | stage("检出") { 11 | steps { 12 | sh 'ci-init' 13 | checkout( 14 | [$class: 'GitSCM', branches: [[name: env.GIT_BUILD_REF]], 15 | userRemoteConfigs: [[url: env.GIT_REPO_URL]]] 16 | ) 17 | } 18 | } 19 | 20 | stage("构建") { 21 | steps { 22 | echo "构建中..." 23 | // 请在这里放置您项目代码的单元测试调用过程,例如: 24 | // sh 'mvn package' // mvn 示例 25 | // sh 'make' // make 示例 26 | sh './sbt test assembly' 27 | echo "构建完成." 28 | // archiveArtifacts artifacts: '**/target/*.jar', fingerprint: true // 收集构建产物 29 | } 30 | } 31 | 32 | stage("测试") { 33 | steps { 34 | echo "单元测试中..." 35 | // 请在这里放置您项目代码的单元测试调用过程,例如: 36 | // sh 'mvn test' // mvn 示例 37 | // sh 'make test' // make 示例 38 | echo "单元测试完成." 39 | // junit 'target/surefire-reports/*.xml' // 收集单元测试报告的调用过程 40 | } 41 | } 42 | 43 | stage("部署") { 44 | steps { 45 | echo "部署中..." 46 | // 请在这里放置收集单元测试报告的调用过程,例如: 47 | // sh 'mvn tomcat7:deploy' // Maven tomcat7 插件示例: 48 | // sh './deploy.sh' // 自研部署脚本 49 | echo "部署完成" 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /file-upload/README.md: -------------------------------------------------------------------------------- 1 | # 大文件断点续传 2 | 3 | ***使用Akka HTTP与HTML 5实现*** 4 | 5 | ``` 6 | git clone https://github.com/yangbajing/scala-applications.git 7 | cd scala-applications/file-upload 8 | sbt "runMain me.yangbajing.fileupload.Main" 9 | ``` 10 | 11 | 打开浏览器访问:[http://localhost:33333/html/upload.html](http://localhost:33333/html/upload.html) 12 | -------------------------------------------------------------------------------- /file-upload/build.sbt: -------------------------------------------------------------------------------- 1 | name := "file-upload" 2 | 3 | description := "File Upload" 4 | 5 | version := "0.0.1" 6 | 7 | homepage := Some(new URL("https://github.com/yangbajing/scala-applications/tree/master/file-upload")) 8 | 9 | organization := "me.yangbajing" 10 | 11 | organizationHomepage := Some(new URL("https://github.com/yangbajing/scala-applications")) 12 | 13 | startYear := Some(2018) 14 | 15 | scalaVersion := "2.12.8" 16 | 17 | scalacOptions ++= Seq("-encoding", "utf8", "-unchecked", "-feature", "-deprecation") 18 | 19 | javacOptions ++= Seq("-encoding", "utf8", "-Xlint:unchecked", "-Xlint:deprecation") 20 | 21 | javaOptions += "-Dproject.base=" + baseDirectory.value 22 | 23 | scalafmtOnCompile in ThisBuild := true 24 | 25 | resolvers ++= Seq( 26 | "Typesafe Repository".at("http://repo.typesafe.com/typesafe/releases/"), 27 | "Sonatype releases".at("http://oss.sonatype.org/content/repositories/releases"), 28 | "Typesafe Snapshots".at("http://repo.typesafe.com/typesafe/snapshots/"), 29 | "Sonatype snapshots".at("http://oss.sonatype.org/content/repositories/snapshots")) 30 | 31 | assemblyJarName in assembly := "file-upload.jar" 32 | 33 | mainClass in assembly := Some("me.yangbajing.fileupload.Main") 34 | 35 | resolvers ++= Seq( 36 | "ihongka.cn sbt".at("https://artifactory.hongkazhijia.com/artifactory/sbt-release"), 37 | "ihongka.cn maven".at("https://artifactory.hongkazhijia.com/artifactory/libs-release")) 38 | 39 | libraryDependencies ++= Seq( 40 | "com.helloscala.fusion" %% "helloscala-common" % "1.0.0-alpha14", 41 | _typesafeConfig, 42 | _logback, 43 | _scalaLogging, 44 | _scalatest) ++ _akkas ++ _akkaHttps ++ _jsons 45 | 46 | lazy val _scalatest = "org.scalatest" %% "scalatest" % "3.0.7" % Test 47 | lazy val _typesafeConfig = "com.typesafe" % "config" % "1.3.3" 48 | lazy val _scalaLogging = ("com.typesafe.scala-logging" %% "scala-logging" % "3.9.2") 49 | .exclude("org.scala-lang", "scala-library") 50 | .exclude("org.scala-lang", "scala-reflect") 51 | 52 | lazy val _akkaHttps = 53 | Seq("com.typesafe.akka" %% "akka-http" % "10.1.8", "com.typesafe.akka" %% "akka-http-testkit" % "10.1.8" % Test) 54 | lazy val _logback = "ch.qos.logback" % "logback-classic" % "1.2.3" 55 | 56 | val verAkka = "2.5.22" 57 | lazy val _akkas = Seq( 58 | "com.typesafe.akka" %% "akka-actor" % verAkka, 59 | "com.typesafe.akka" %% "akka-stream" % verAkka, 60 | "com.typesafe.akka" %% "akka-slf4j" % verAkka, 61 | "com.typesafe.akka" %% "akka-stream-testkit" % verAkka % Test) 62 | 63 | val versionJackson = "2.9.6" 64 | lazy val _jsons = Seq( 65 | "com.fasterxml.jackson.datatype" % "jackson-datatype-jdk8" % versionJackson, 66 | "com.fasterxml.jackson.datatype" % "jackson-datatype-jsr310" % versionJackson, 67 | "com.fasterxml.jackson.module" % "jackson-module-parameter-names" % versionJackson, 68 | "com.fasterxml.jackson.module" %% "jackson-module-scala" % versionJackson) 69 | -------------------------------------------------------------------------------- /file-upload/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.2.7 2 | -------------------------------------------------------------------------------- /file-upload/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.9") 2 | 3 | addSbtPlugin("com.geirsson" % "sbt-scalafmt" % "1.5.1") 4 | -------------------------------------------------------------------------------- /file-upload/project/sbt-launch.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yangbajing/scala-applications/a41bd2b1e5eef086cf54fe58630bb47d7c16df39/file-upload/project/sbt-launch.jar -------------------------------------------------------------------------------- /file-upload/sbt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ./sbt-dist/bin/sbt "$@" -------------------------------------------------------------------------------- /file-upload/sbt-dist/bin/sbt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | ### ------------------------------- ### 5 | ### Helper methods for BASH scripts ### 6 | ### ------------------------------- ### 7 | 8 | realpath () { 9 | ( 10 | TARGET_FILE="$1" 11 | FIX_CYGPATH="$2" 12 | 13 | cd "$(dirname "$TARGET_FILE")" 14 | TARGET_FILE=$(basename "$TARGET_FILE") 15 | 16 | COUNT=0 17 | while [ -L "$TARGET_FILE" -a $COUNT -lt 100 ] 18 | do 19 | TARGET_FILE=$(readlink "$TARGET_FILE") 20 | cd "$(dirname "$TARGET_FILE")" 21 | TARGET_FILE=$(basename "$TARGET_FILE") 22 | COUNT=$(($COUNT + 1)) 23 | done 24 | 25 | # make sure we grab the actual windows path, instead of cygwin's path. 26 | if [[ "x$FIX_CYGPATH" != "x" ]]; then 27 | echo "$(cygwinpath "$(pwd -P)/$TARGET_FILE")" 28 | else 29 | echo "$(pwd -P)/$TARGET_FILE" 30 | fi 31 | ) 32 | } 33 | 34 | 35 | # Uses uname to detect if we're in the odd cygwin environment. 36 | is_cygwin() { 37 | local os=$(uname -s) 38 | case "$os" in 39 | CYGWIN*) return 0 ;; 40 | *) return 1 ;; 41 | esac 42 | } 43 | 44 | # TODO - Use nicer bash-isms here. 45 | CYGWIN_FLAG=$(if is_cygwin; then echo true; else echo false; fi) 46 | 47 | 48 | # This can fix cygwin style /cygdrive paths so we get the 49 | # windows style paths. 50 | cygwinpath() { 51 | local file="$1" 52 | if [[ "$CYGWIN_FLAG" == "true" ]]; then 53 | echo $(cygpath -w $file) 54 | else 55 | echo $file 56 | fi 57 | } 58 | 59 | . "$(dirname "$(realpath "$0")")/sbt-launch-lib.bash" 60 | 61 | 62 | declare -r noshare_opts="-Dsbt.global.base=project/.sbtboot -Dsbt.boot.directory=project/.boot -Dsbt.ivy.home=project/.ivy" 63 | declare -r sbt_opts_file=".sbtopts" 64 | declare -r etc_sbt_opts_file="${sbt_home}/conf/sbtopts" 65 | declare -r win_sbt_opts_file="${sbt_home}/conf/sbtconfig.txt" 66 | 67 | usage() { 68 | cat < path to global settings/plugins directory (default: ~/.sbt) 77 | -sbt-boot path to shared boot directory (default: ~/.sbt/boot in 0.11 series) 78 | -ivy path to local Ivy repository (default: ~/.ivy2) 79 | -mem set memory options (default: $sbt_mem, which is $(get_mem_opts $sbt_mem)) 80 | -no-share use all local caches; no sharing 81 | -no-global uses global caches, but does not use global ~/.sbt directory. 82 | -jvm-debug Turn on JVM debugging, open at the given port. 83 | -batch Disable interactive mode 84 | 85 | # sbt version (default: from project/build.properties if present, else latest release) 86 | -sbt-version use the specified version of sbt 87 | -sbt-jar use the specified jar as the sbt launcher 88 | -sbt-rc use an RC version of sbt 89 | -sbt-snapshot use a snapshot version of sbt 90 | 91 | # java version (default: java from PATH, currently $(java -version 2>&1 | grep version)) 92 | -java-home alternate JAVA_HOME 93 | 94 | # jvm options and output control 95 | JAVA_OPTS environment variable, if unset uses "$java_opts" 96 | SBT_OPTS environment variable, if unset uses "$default_sbt_opts" 97 | .sbtopts if this file exists in the current directory, it is 98 | prepended to the runner args 99 | /etc/sbt/sbtopts if this file exists, it is prepended to the runner args 100 | -Dkey=val pass -Dkey=val directly to the java runtime 101 | -J-X pass option -X directly to the java runtime 102 | (-J is stripped) 103 | -S-X add -X to sbt's scalacOptions (-S is stripped) 104 | 105 | In the case of duplicated or conflicting options, the order above 106 | shows precedence: JAVA_OPTS lowest, command line options highest. 107 | EOM 108 | } 109 | 110 | 111 | 112 | process_my_args () { 113 | while [[ $# -gt 0 ]]; do 114 | case "$1" in 115 | -no-colors) addJava "-Dsbt.log.noformat=true" && shift ;; 116 | -no-share) addJava "$noshare_opts" && shift ;; 117 | -no-global) addJava "-Dsbt.global.base=$(pwd)/project/.sbtboot" && shift ;; 118 | -sbt-boot) require_arg path "$1" "$2" && addJava "-Dsbt.boot.directory=$2" && shift 2 ;; 119 | -sbt-dir) require_arg path "$1" "$2" && addJava "-Dsbt.global.base=$2" && shift 2 ;; 120 | -debug-inc) addJava "-Dxsbt.inc.debug=true" && shift ;; 121 | -batch) exec &2 "$@" 20 | } 21 | vlog () { 22 | [[ $verbose || $debug ]] && echoerr "$@" 23 | } 24 | dlog () { 25 | [[ $debug ]] && echoerr "$@" 26 | } 27 | 28 | jar_file () { 29 | echo "$(cygwinpath "${sbt_home}/bin/sbt-launch.jar")" 30 | } 31 | 32 | acquire_sbt_jar () { 33 | sbt_jar="$(jar_file)" 34 | 35 | if [[ ! -f "$sbt_jar" ]]; then 36 | echoerr "Could not find launcher jar: $sbt_jar" 37 | exit 2 38 | fi 39 | } 40 | 41 | execRunner () { 42 | # print the arguments one to a line, quoting any containing spaces 43 | [[ $verbose || $debug ]] && echo "# Executing command line:" && { 44 | for arg; do 45 | if printf "%s\n" "$arg" | grep -q ' '; then 46 | printf "\"%s\"\n" "$arg" 47 | else 48 | printf "%s\n" "$arg" 49 | fi 50 | done 51 | echo "" 52 | } 53 | 54 | # THis used to be exec, but we loose the ability to re-hook stty then 55 | # for cygwin... Maybe we should flag the feature here... 56 | "$@" 57 | } 58 | 59 | addJava () { 60 | dlog "[addJava] arg = '$1'" 61 | java_args=( "${java_args[@]}" "$1" ) 62 | } 63 | addSbt () { 64 | dlog "[addSbt] arg = '$1'" 65 | sbt_commands=( "${sbt_commands[@]}" "$1" ) 66 | } 67 | addResidual () { 68 | dlog "[residual] arg = '$1'" 69 | residual_args=( "${residual_args[@]}" "$1" ) 70 | } 71 | addDebugger () { 72 | addJava "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$1" 73 | } 74 | 75 | get_mem_opts () { 76 | # if we detect any of these settings in ${JAVA_OPTS} or ${JAVA_TOOL_OPTIONS} we need to NOT output our settings. 77 | # The reason is the Xms/Xmx, if they don't line up, cause errors. 78 | if [[ "${JAVA_OPTS}" == *-Xmx* ]] || [[ "${JAVA_OPTS}" == *-Xms* ]] || [[ "${JAVA_OPTS}" == *-XX:MaxPermSize* ]] || [[ "${JAVA_OPTS}" == *-XX:MaxMetaspaceSize* ]] || [[ "${JAVA_OPTS}" == *-XX:ReservedCodeCacheSize* ]]; then 79 | echo "" 80 | elif [[ "${JAVA_TOOL_OPTIONS}" == *-Xmx* ]] || [[ "${JAVA_TOOL_OPTIONS}" == *-Xms* ]] || [[ "${JAVA_TOOL_OPTIONS}" == *-XX:MaxPermSize* ]] || [[ "${JAVA_TOOL_OPTIONS}" == *-XX:MaxMetaspaceSize* ]] || [[ "${JAVA_TOOL_OPTIONS}" == *-XX:ReservedCodeCacheSize* ]]; then 81 | echo "" 82 | elif [[ "${SBT_OPTS}" == *-Xmx* ]] || [[ "${SBT_OPTS}" == *-Xms* ]] || [[ "${SBT_OPTS}" == *-XX:MaxPermSize* ]] || [[ "${SBT_OPTS}" == *-XX:MaxMetaspaceSize* ]] || [[ "${SBT_OPTS}" == *-XX:ReservedCodeCacheSize* ]]; then 83 | echo "" 84 | else 85 | # a ham-fisted attempt to move some memory settings in concert 86 | # so they need not be messed around with individually. 87 | local mem=${1:-1024} 88 | local codecache=$(( $mem / 8 )) 89 | (( $codecache > 128 )) || codecache=128 90 | (( $codecache < 512 )) || codecache=512 91 | local class_metadata_size=$(( $codecache * 2 )) 92 | local class_metadata_opt=$([[ "$java_version" < "1.8" ]] && echo "MaxPermSize" || echo "MaxMetaspaceSize") 93 | 94 | local arg_xms=$([[ "${java_args[@]}" == *-Xms* ]] && echo "" || echo "-Xms${mem}m") 95 | local arg_xmx=$([[ "${java_args[@]}" == *-Xmx* ]] && echo "" || echo "-Xmx${mem}m") 96 | local arg_rccs=$([[ "${java_args[@]}" == *-XX:ReservedCodeCacheSize* ]] && echo "" || echo "-XX:ReservedCodeCacheSize=${codecache}m") 97 | local arg_meta=$([[ "${java_args[@]}" == *-XX:${class_metadata_opt}* ]] && echo "" || echo "-XX:${class_metadata_opt}=${class_metadata_size}m") 98 | 99 | echo "${arg_xms} ${arg_xmx} ${arg_rccs} ${arg_meta}" 100 | fi 101 | } 102 | 103 | require_arg () { 104 | local type="$1" 105 | local opt="$2" 106 | local arg="$3" 107 | if [[ -z "$arg" ]] || [[ "${arg:0:1}" == "-" ]]; then 108 | echo "$opt requires <$type> argument" 109 | exit 1 110 | fi 111 | } 112 | 113 | is_function_defined() { 114 | declare -f "$1" > /dev/null 115 | } 116 | 117 | process_args () { 118 | while [[ $# -gt 0 ]]; do 119 | case "$1" in 120 | -h|-help) usage; exit 1 ;; 121 | -v|-verbose) verbose=1 && shift ;; 122 | -d|-debug) debug=1 && shift ;; 123 | 124 | -ivy) require_arg path "$1" "$2" && addJava "-Dsbt.ivy.home=$2" && shift 2 ;; 125 | -mem) require_arg integer "$1" "$2" && sbt_mem="$2" && shift 2 ;; 126 | -jvm-debug) require_arg port "$1" "$2" && addDebugger $2 && shift 2 ;; 127 | -batch) exec &1 | awk -F '"' '/version/ {print $2}') 146 | vlog "[process_args] java_version = '$java_version'" 147 | } 148 | 149 | # Detect that we have java installed. 150 | checkJava() { 151 | local required_version="$1" 152 | # Now check to see if it's a good enough version 153 | if [[ "$java_version" == "" ]]; then 154 | echo 155 | echo No java installations was detected. 156 | echo Please go to http://www.java.com/getjava/ and download 157 | echo 158 | exit 1 159 | elif [[ ! "$java_version" > "$required_version" ]]; then 160 | echo 161 | echo The java installation you have is not up to date 162 | echo $script_name requires at least version $required_version+, you have 163 | echo version $java_version 164 | echo 165 | echo Please go to http://www.java.com/getjava/ and download 166 | echo a valid Java Runtime and install before running $script_name. 167 | echo 168 | exit 1 169 | fi 170 | } 171 | 172 | 173 | run() { 174 | # no jar? download it. 175 | [[ -f "$sbt_jar" ]] || acquire_sbt_jar "$sbt_version" || { 176 | # still no jar? uh-oh. 177 | echo "Download failed. Obtain the sbt-launch.jar manually and place it at $sbt_jar" 178 | exit 1 179 | } 180 | 181 | # process the combined args, then reset "$@" to the residuals 182 | process_args "$@" 183 | set -- "${residual_args[@]}" 184 | argumentCount=$# 185 | 186 | # TODO - java check should be configurable... 187 | checkJava "1.6" 188 | 189 | #If we're in cygwin, we should use the windows config, and terminal hacks 190 | if [[ "$CYGWIN_FLAG" == "true" ]]; then 191 | stty -icanon min 1 -echo > /dev/null 2>&1 192 | addJava "-Djline.terminal=jline.UnixTerminal" 193 | addJava "-Dsbt.cygwin=true" 194 | fi 195 | 196 | # run sbt 197 | execRunner "$java_cmd" \ 198 | $(get_mem_opts $sbt_mem) \ 199 | ${JAVA_OPTS} \ 200 | ${SBT_OPTS:-$default_sbt_opts} \ 201 | ${java_args[@]} \ 202 | -jar "$sbt_jar" \ 203 | "${sbt_commands[@]}" \ 204 | "${residual_args[@]}" 205 | 206 | exit_code=$? 207 | 208 | # Clean up the terminal from cygwin hacks. 209 | if [[ "$CYGWIN_FLAG" == "true" ]]; then 210 | stty icanon echo > /dev/null 2>&1 211 | fi 212 | exit $exit_code 213 | } 214 | -------------------------------------------------------------------------------- /file-upload/sbt-dist/bin/sbt-launch.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yangbajing/scala-applications/a41bd2b1e5eef086cf54fe58630bb47d7c16df39/file-upload/sbt-dist/bin/sbt-launch.jar -------------------------------------------------------------------------------- /file-upload/sbt-dist/bin/sbt.bat: -------------------------------------------------------------------------------- 1 | @REM SBT launcher script 2 | @REM 3 | @REM Envioronment: 4 | @REM JAVA_HOME - location of a JDK home dir (mandatory) 5 | @REM SBT_OPTS - JVM options (optional) 6 | @REM Configuration: 7 | @REM sbtconfig.txt found in the SBT_HOME. 8 | 9 | @REM ZOMG! We need delayed expansion to build up CFG_OPTS later 10 | @setlocal enabledelayedexpansion 11 | 12 | @echo off 13 | set SBT_HOME=%~dp0 14 | 15 | rem FIRST we load the config file of extra options. 16 | set FN=%SBT_HOME%\..\conf\sbtconfig.txt 17 | set CFG_OPTS= 18 | FOR /F "tokens=* eol=# usebackq delims=" %%i IN ("%FN%") DO ( 19 | set DO_NOT_REUSE_ME=%%i 20 | rem ZOMG (Part #2) WE use !! here to delay the expansion of 21 | rem CFG_OPTS, otherwise it remains "" for this loop. 22 | set CFG_OPTS=!CFG_OPTS! !DO_NOT_REUSE_ME! 23 | ) 24 | 25 | rem We use the value of the JAVACMD environment variable if defined 26 | set _JAVACMD=%JAVACMD% 27 | 28 | if "%_JAVACMD%"=="" ( 29 | if not "%JAVA_HOME%"=="" ( 30 | if exist "%JAVA_HOME%\bin\java.exe" set "_JAVACMD=%JAVA_HOME%\bin\java.exe" 31 | ) 32 | ) 33 | 34 | if "%_JAVACMD%"=="" set _JAVACMD=java 35 | 36 | rem We use the value of the JAVA_OPTS environment variable if defined, rather than the config. 37 | set _JAVA_OPTS=%JAVA_OPTS% 38 | if "%_JAVA_OPTS%"=="" set _JAVA_OPTS=%CFG_OPTS% 39 | 40 | :run 41 | 42 | "%_JAVACMD%" %_JAVA_OPTS% %SBT_OPTS% -cp "%SBT_HOME%sbt-launch.jar" xsbt.boot.Boot %* 43 | if ERRORLEVEL 1 goto error 44 | goto end 45 | 46 | :error 47 | @endlocal 48 | exit /B 1 49 | 50 | 51 | :end 52 | @endlocal 53 | exit /B 0 54 | -------------------------------------------------------------------------------- /file-upload/sbt-dist/conf/sbtconfig.txt: -------------------------------------------------------------------------------- 1 | # Set the java args to high 2 | 3 | -Xmx2048M 4 | 5 | -XX:MaxPermSize=256m 6 | 7 | -XX:ReservedCodeCacheSize=128m 8 | 9 | # Set the extra SBT options 10 | 11 | -Dsbt.log.format=true 12 | 13 | -Dsbt.override.build.repos=true 14 | -------------------------------------------------------------------------------- /file-upload/sbt-dist/conf/sbtopts: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------ # 2 | # The SBT Configuration file. # 3 | # ------------------------------------------------ # 4 | 5 | 6 | # Disable ANSI color codes 7 | # 8 | #-no-colors 9 | 10 | # Starts sbt even if the current directory contains no sbt project. 11 | # 12 | -sbt-create 13 | 14 | # Path to global settings/plugins directory (default: ~/.sbt) 15 | # 16 | #-sbt-dir /etc/sbt 17 | 18 | # Path to shared boot directory (default: ~/.sbt/boot in 0.11 series) 19 | # 20 | #-sbt-boot ~/.sbt/boot 21 | 22 | # Path to local Ivy repository (default: ~/.ivy2) 23 | # 24 | #-ivy ~/.ivy2 25 | 26 | # set memory options 27 | # 28 | #-mem 29 | 30 | # Use local caches for projects, no sharing. 31 | # 32 | #-no-share 33 | 34 | # Put SBT in offline mode. 35 | # 36 | #-offline 37 | 38 | # Sets the SBT version to use. 39 | #-sbt-version 0.11.3 40 | 41 | # Scala version (default: latest release) 42 | # 43 | #-scala-home 44 | #-scala-version 45 | 46 | # java version (default: java from PATH, currently $(java -version |& grep version)) 47 | # 48 | #-java-home 49 | 50 | -------------------------------------------------------------------------------- /file-upload/sbt.bat: -------------------------------------------------------------------------------- 1 | @REM SBT launcher script 2 | 3 | .\sbt-dist\bin\sbt.bat %* 4 | -------------------------------------------------------------------------------- /file-upload/src/main/scala/me/yangbajing/fileupload/Constants.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.fileupload 2 | 3 | object Constants { 4 | val HASH_LENGTH = 64 5 | val FILE_PART_MAX = 8 6 | } 7 | -------------------------------------------------------------------------------- /file-upload/src/main/scala/me/yangbajing/fileupload/Main.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.fileupload 2 | 3 | import akka.actor.ActorSystem 4 | import akka.http.scaladsl.Http 5 | import akka.stream.ActorMaterializer 6 | import com.typesafe.scalalogging.StrictLogging 7 | import me.yangbajing.fileupload.controller.FileRoute 8 | import me.yangbajing.fileupload.controller.HtmlRoute 9 | import me.yangbajing.fileupload.service.FileService 10 | import akka.http.scaladsl.server.Directives._ 11 | 12 | import scala.util.Failure 13 | import scala.util.Success 14 | 15 | object Main extends StrictLogging { 16 | 17 | def main(args: Array[String]): Unit = { 18 | implicit val system = ActorSystem() 19 | implicit val mat = ActorMaterializer() 20 | 21 | val host = "127.0.0.1" 22 | val port = 33333 23 | 24 | val fileService = FileService(system, mat) 25 | val handler = new FileRoute(fileService).route ~ 26 | new HtmlRoute().route 27 | 28 | val bindingFuture = Http().bindAndHandle(handler, host, port) 29 | 30 | bindingFuture.onComplete { 31 | case Success(binding) => 32 | logger.info(s"startup success, $binding") 33 | case Failure(cause) => 34 | logger.error("start failure", cause) 35 | system.terminate() 36 | }(system.dispatcher) 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /file-upload/src/main/scala/me/yangbajing/fileupload/controller/FileRoute.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.fileupload.controller 2 | 3 | import akka.http.scaladsl.model.Multipart 4 | import akka.http.scaladsl.model.StatusCodes 5 | import akka.http.scaladsl.server.Directives._ 6 | import akka.http.scaladsl.server.Directive0 7 | import akka.http.scaladsl.server.Route 8 | import com.typesafe.scalalogging.StrictLogging 9 | import me.yangbajing.fileupload.service.FileService 10 | import me.yangbajing.fileupload.util.FileUtils 11 | 12 | class FileRoute(fileService: FileService) extends StrictLogging { 13 | 14 | def route: Route = pathPrefix("file") { 15 | log { 16 | uploadRoute ~ 17 | downloadRoute ~ 18 | progressRoute 19 | } 20 | } 21 | 22 | private def uploadRoute: Route = path("upload") { 23 | post { 24 | withoutSizeLimit { 25 | entity(as[Multipart.FormData]) { formData => 26 | onSuccess(fileService.handleUpload(formData)) { results => 27 | import me.yangbajing.fileupload.util.JacksonSupport._ 28 | complete(results) 29 | } 30 | } 31 | } 32 | } 33 | } 34 | 35 | // 支持断点续传 36 | private def downloadRoute: Route = path("download" / Segment) { hash => 37 | getFromFile(FileUtils.getLocalPath(hash).toFile) 38 | } 39 | 40 | // 查询文件上传进度 41 | private def progressRoute: Route = path("progress" / Segment) { hash => 42 | onSuccess(fileService.progressByHash(hash)) { 43 | case Some(v) => 44 | import me.yangbajing.fileupload.util.JacksonSupport._ 45 | complete(v) 46 | case None => complete(StatusCodes.NotFound) 47 | } 48 | } 49 | 50 | private val log: Directive0 = extractRequest.flatMap { req => 51 | logger.debug(req.toString()) 52 | pass 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /file-upload/src/main/scala/me/yangbajing/fileupload/controller/HtmlRoute.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.fileupload.controller 2 | 3 | import akka.http.scaladsl.server.Directives._ 4 | import akka.http.scaladsl.server.Route 5 | 6 | class HtmlRoute { 7 | 8 | def route: Route = pathPrefix("html") { 9 | getFromBrowseableDirectory("web") 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /file-upload/src/main/scala/me/yangbajing/fileupload/io/CustomFileSource.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.fileupload.io 2 | 3 | import java.nio.ByteBuffer 4 | import java.nio.channels.FileChannel 5 | import java.nio.file.Files 6 | import java.nio.file.NoSuchFileException 7 | import java.nio.file.Path 8 | import java.nio.file.StandardOpenOption 9 | 10 | import akka.Done 11 | import akka.stream.Attributes.InputBuffer 12 | import akka.stream.Attributes 13 | import akka.stream.IOResult 14 | import akka.stream.Outlet 15 | import akka.stream.SourceShape 16 | import akka.stream.stage.GraphStageLogic 17 | import akka.stream.stage.GraphStageWithMaterializedValue 18 | import akka.stream.stage.OutHandler 19 | import akka.util.ByteString 20 | 21 | import scala.annotation.tailrec 22 | import scala.concurrent.Future 23 | import scala.concurrent.Promise 24 | import scala.util.Failure 25 | import scala.util.Success 26 | import scala.util.Try 27 | import scala.util.control.NonFatal 28 | 29 | final class CustomFileSource(path: Path, chunkSize: Int, startPosition: Long = 0, endPosition: Long = -1L) 30 | extends GraphStageWithMaterializedValue[SourceShape[ByteString], Future[IOResult]] { 31 | require(chunkSize > 0, "chunkSize must be greater than 0") 32 | val out = Outlet[ByteString]("FileSource.out") 33 | 34 | override val shape = SourceShape(out) 35 | 36 | override def createLogicAndMaterializedValue(inheritedAttributes: Attributes): (GraphStageLogic, Future[IOResult]) = { 37 | val ioResultPromise = Promise[IOResult]() 38 | 39 | val logic = new GraphStageLogic(shape) with OutHandler { 40 | handler => 41 | val buffer = ByteBuffer.allocate(chunkSize) 42 | val maxReadAhead = inheritedAttributes.get[InputBuffer](InputBuffer(16, 16)).max 43 | var channel: FileChannel = _ 44 | var position = startPosition 45 | var chunkCallback: Try[Int] => Unit = _ 46 | var eofEncountered = false 47 | var availableChunks: Vector[ByteString] = Vector.empty[ByteString] 48 | 49 | setHandler(out, this) 50 | 51 | override def preStart(): Unit = { 52 | try { 53 | // this is a bit weird but required to keep existing semantics 54 | if (!Files.exists(path)) throw new NoSuchFileException(path.toString) 55 | 56 | require(!Files.isDirectory(path), s"Path '$path' is a directory") 57 | require(Files.isReadable(path), s"Missing read permission for '$path'") 58 | 59 | channel = FileChannel.open(path, StandardOpenOption.READ) 60 | channel.position(position) 61 | // if (endPosition > 0) { 62 | // channel = channel.truncate(endPosition) 63 | // } 64 | } catch { 65 | case ex: Exception => 66 | ioResultPromise.trySuccess(IOResult(position, Failure(ex))) 67 | throw ex 68 | } 69 | } 70 | 71 | override def onPull(): Unit = { 72 | if (availableChunks.size < maxReadAhead && !eofEncountered) { 73 | availableChunks = readAhead(maxReadAhead, availableChunks) 74 | } 75 | 76 | //if already read something and try 77 | 78 | if (availableChunks.nonEmpty) { 79 | emitMultiple(out, availableChunks.iterator, () => if (eofEncountered) success() else setHandler(out, handler)) 80 | availableChunks = Vector.empty[ByteString] 81 | } else if (eofEncountered) success() 82 | } 83 | 84 | private def success(): Unit = { 85 | completeStage() 86 | ioResultPromise.trySuccess(IOResult(position, Success(Done))) 87 | } 88 | 89 | /** BLOCKING I/O READ */ 90 | @tailrec def readAhead(maxChunks: Int, chunks: Vector[ByteString]): Vector[ByteString] = { 91 | if (chunks.size < maxChunks && !eofEncountered) { 92 | val readBytes = try channel.read(buffer, position) 93 | catch { 94 | case NonFatal(ex) => 95 | failStage(ex) 96 | ioResultPromise.trySuccess(IOResult(position, Failure(ex))) 97 | throw ex 98 | } 99 | 100 | if (readBytes > 0) { 101 | buffer.flip() 102 | position += readBytes 103 | val newChunks = chunks :+ ByteString.fromByteBuffer(buffer) 104 | buffer.clear() 105 | 106 | if (readBytes < chunkSize || 107 | (endPosition > 0 && position >= endPosition)) { 108 | eofEncountered = true 109 | newChunks 110 | } else { 111 | readAhead(maxChunks, newChunks) 112 | } 113 | } else { 114 | eofEncountered = true 115 | chunks 116 | } 117 | } else { 118 | chunks 119 | } 120 | } 121 | 122 | override def onDownstreamFinish(): Unit = success() 123 | 124 | override def postStop(): Unit = { 125 | ioResultPromise.trySuccess(IOResult(position, Success(Done))) 126 | if ((channel ne null) && channel.isOpen) channel.close() 127 | } 128 | } 129 | 130 | (logic, ioResultPromise.future) 131 | } 132 | 133 | override def toString = s"FileSource($path, $chunkSize)" 134 | } 135 | -------------------------------------------------------------------------------- /file-upload/src/main/scala/me/yangbajing/fileupload/model/FileInfo.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.fileupload.model 2 | 3 | import java.nio.file.Path 4 | 5 | import akka.http.scaladsl.model.HttpHeader 6 | import akka.http.scaladsl.model.Multipart.FormData 7 | import me.yangbajing.fileupload.Constants 8 | 9 | import scala.collection.immutable 10 | 11 | case class FileInfo(bodyPart: FormData.BodyPart, hash: Option[String], contentLength: Long, startPosition: Long) { 12 | override def toString: String = 13 | s"FileInfo(${bodyPart.name}, $hash, $contentLength, $startPosition, ${bodyPart.filename}, ${bodyPart.headers})" 14 | } 15 | 16 | object FileInfo { 17 | val Empty = FileInfo(null, None, 0L, 0L) 18 | 19 | def apply(part: FormData.BodyPart): FileInfo = { 20 | val (hash, contentLength, startPosition) = part.name.split('.') match { 21 | case Array(a, b, c) => (a, b.toLong, c.toLong) 22 | case Array(a, b) => (a, b.toLong, 0L) 23 | case Array(a) => (a, 0L, 0L) 24 | case _ => throw new IllegalArgumentException(s"Multipart.FormData name格式不符合要求:${part.name}") 25 | } 26 | new FileInfo(part, if (Constants.HASH_LENGTH == hash.length) Some(hash) else None, contentLength, startPosition) 27 | } 28 | } 29 | 30 | /** 31 | * 文件元数据 32 | * @param hash 文件HASH(sha256) 33 | * @param size 已上传(bytes) 34 | * @param localPath 本地存储路径 35 | */ 36 | case class FileMeta(hash: String, size: Long, localPath: Path) 37 | 38 | case class FileBO( 39 | hash: Option[String], 40 | computedHash: Option[String], 41 | localPath: Path, 42 | contentLength: Long, 43 | filename: Option[String], 44 | headers: immutable.Seq[HttpHeader]) 45 | -------------------------------------------------------------------------------- /file-upload/src/main/scala/me/yangbajing/fileupload/service/FileService.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.fileupload.service 2 | 3 | import akka.actor.ActorSystem 4 | import akka.http.scaladsl.model.Multipart 5 | import akka.stream.ActorMaterializer 6 | import me.yangbajing.fileupload.model.FileBO 7 | import me.yangbajing.fileupload.model.FileMeta 8 | 9 | import scala.concurrent.Future 10 | 11 | trait FileService { 12 | 13 | implicit val system: ActorSystem 14 | implicit val mat: ActorMaterializer 15 | 16 | def progressByHash(hash: String): Future[Option[FileMeta]] 17 | 18 | def handleUpload(formData: Multipart.FormData): Future[Seq[FileBO]] 19 | 20 | } 21 | 22 | object FileService { 23 | 24 | def apply(system: ActorSystem, mat: ActorMaterializer) = new FileServiceImpl(system, mat) 25 | } 26 | -------------------------------------------------------------------------------- /file-upload/src/main/scala/me/yangbajing/fileupload/service/FileServiceImpl.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.fileupload.service 2 | 3 | import java.util.Objects 4 | 5 | import akka.actor.ActorSystem 6 | import akka.http.scaladsl.model.Multipart 7 | import akka.stream.scaladsl.Sink 8 | import akka.stream.ActorMaterializer 9 | import akka.stream.Materializer 10 | import com.typesafe.scalalogging.StrictLogging 11 | import me.yangbajing.fileupload.Constants 12 | import me.yangbajing.fileupload.model.FileBO 13 | import me.yangbajing.fileupload.model.FileInfo 14 | import me.yangbajing.fileupload.model.FileMeta 15 | import me.yangbajing.fileupload.util.FileUtils 16 | 17 | import scala.concurrent.Future 18 | import scala.concurrent.duration._ 19 | 20 | class FileServiceImpl(val system: ActorSystem, implicit val mat: ActorMaterializer) 21 | extends FileService 22 | with StrictLogging { 23 | 24 | override def progressByHash(hash: String): Future[Option[FileMeta]] = { 25 | require(Objects.nonNull(hash) && hash.nonEmpty, "hash 不能为空。") 26 | Future.successful(FileUtils.getFileMeta(hash)) 27 | } 28 | 29 | override def handleUpload(formData: Multipart.FormData): Future[Seq[FileBO]] = { 30 | formData.parts 31 | // .groupBy(Constants.FILE_PART_MAX, part => part.name.split('.').head) 32 | // .async 33 | // .foldAsync[FileInfo](FileInfo.Empty)((fileInfo, part) => mergeBodyPart(fileInfo, part)) 34 | // .mergeSubstreams 35 | .map(part => FileInfo(part)) 36 | .log("fileInfo", info => logger.debug(s"fileInfo: $info")) 37 | .mapAsync(Constants.FILE_PART_MAX)(processFile) 38 | .runWith(Sink.seq) 39 | } 40 | 41 | /** 42 | * sha存在,判断文件是否已上传? 43 | * startPosition存在,正从startPosition位置开始上传 44 | * @param fileInfo 45 | * @return 46 | */ 47 | private def processFile(fileInfo: FileInfo)(implicit mat: Materializer): Future[FileBO] = { 48 | import system.dispatcher 49 | val bodyPart = fileInfo.bodyPart 50 | fileInfo.hash.flatMap(FileUtils.getFileMeta) match { 51 | case Some(fileMeta) if fileInfo.contentLength == fileMeta.size => // 已上传完成 52 | Future.successful( 53 | FileBO( 54 | fileInfo.hash, 55 | Some(fileMeta.hash), 56 | fileMeta.localPath, 57 | fileMeta.size, 58 | bodyPart.filename, 59 | bodyPart.headers)) 60 | case _ => 61 | FileUtils.uploadFile(fileInfo) 62 | } 63 | } 64 | 65 | private def mergeBodyPart(fileInfo: FileInfo, part: Multipart.FormData.BodyPart): Future[FileInfo] = { 66 | import system.dispatcher 67 | part.name.split('.') match { 68 | case Array(_) => 69 | Future.successful(fileInfo.copy(bodyPart = part)) 70 | case Array(_, "hash") => 71 | part.entity.toStrict(1.second).flatMap { entity => 72 | val hash = entity.data.utf8String 73 | if (Constants.HASH_LENGTH != hash.length) 74 | Future.failed(new IllegalArgumentException("hash值应为64位sha256的16进制字符串")) 75 | else 76 | Future.successful(fileInfo.copy(hash = Some(hash.toLowerCase))) 77 | } 78 | case Array(_, "contentLength") => 79 | part.entity.toStrict(1.second).map(entity => fileInfo.copy(contentLength = entity.data.utf8String.toLong)) 80 | case Array(_, "startPosition") => 81 | part.entity.toStrict(1.second).map(entity => fileInfo.copy(startPosition = entity.data.utf8String.toLong)) 82 | case _ => 83 | Future.failed(new IllegalArgumentException(s"未知的FormData字段:${part.name}")) 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /file-upload/src/main/scala/me/yangbajing/fileupload/util/FileUtils.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.fileupload.util 2 | 3 | import java.nio.file.StandardOpenOption.APPEND 4 | import java.nio.file._ 5 | import java.security.MessageDigest 6 | 7 | import akka.stream.Materializer 8 | import akka.stream.scaladsl.FileIO 9 | import com.typesafe.scalalogging.StrictLogging 10 | import me.yangbajing.fileupload.Constants 11 | import me.yangbajing.fileupload.model.FileBO 12 | import me.yangbajing.fileupload.model.FileInfo 13 | import me.yangbajing.fileupload.model.FileMeta 14 | 15 | import scala.concurrent.ExecutionContext 16 | import scala.concurrent.Future 17 | 18 | object FileUtils extends StrictLogging { 19 | val TMP_DIR: Path = getOrCreateDirectories(Paths.get("/tmp/file-upload/tmp")) 20 | val LOCAL_PATH: String = getOrCreateDirectories(Paths.get("/tmp/file-upload")).toString 21 | 22 | def getOrCreateDirectories(path: Path): Path = { 23 | if (!Files.isDirectory(path)) { 24 | Files.createDirectories(path) 25 | } 26 | path 27 | } 28 | 29 | def getLocalPath(hash: String): Path = Paths.get(LOCAL_PATH, hash.take(2), hash) 30 | 31 | def getFileMeta(hash: String): Option[FileMeta] = { 32 | if (hash == null || Constants.HASH_LENGTH != hash.length) { 33 | None 34 | } else { 35 | val path = getLocalPath(hash) 36 | if (Files.exists(path) && Files.isReadable(path)) Some(FileMeta(hash, Files.size(path), path)) else None 37 | } 38 | } 39 | 40 | def uploadFile(fileInfo: FileInfo)(implicit mat: Materializer, ec: ExecutionContext): Future[FileBO] = { 41 | // TODO 需要校验上传完成文件的hash值与提交hash值是否匹配? 42 | val maybeMeta = fileInfo.hash.flatMap(FileUtils.getFileMeta) 43 | val beContinue = maybeMeta.isDefined && fileInfo.startPosition > 0L 44 | val f = if (beContinue) uploadContinue(fileInfo, maybeMeta.get) else uploadNewFile(fileInfo) 45 | f.andThen { 46 | case tryValue => 47 | logger.debug(s"文件上传完成:$tryValue") 48 | } 49 | } 50 | 51 | private def uploadContinue(fileInfo: FileInfo, meta: FileMeta)(implicit mat: Materializer, ec: ExecutionContext) = { 52 | val bodyPart = fileInfo.bodyPart 53 | val localPath = FileUtils.getLocalPath(fileInfo.hash.get) 54 | logger.debug(s"断点续传,startPosition:${fileInfo.startPosition},路径:$localPath") 55 | bodyPart.entity.dataBytes 56 | .runWith(FileIO.toPath(localPath, Set(APPEND), fileInfo.startPosition)) 57 | .map(ioResult => 58 | FileBO(fileInfo.hash, None, localPath, meta.size + ioResult.count, bodyPart.filename, bodyPart.headers)) 59 | } 60 | 61 | private def uploadNewFile(fileInfo: FileInfo)(implicit mat: Materializer, ec: ExecutionContext) = { 62 | val bodyPart = fileInfo.bodyPart 63 | val tmpPath = fileInfo.hash 64 | .map(h => FileUtils.getLocalPath(h)) 65 | .getOrElse(Files.createTempFile(FileUtils.TMP_DIR, bodyPart.filename.getOrElse(""), "")) 66 | val sha = MessageDigest.getInstance("SHA-256") 67 | logger.debug(s"新文件,路径:$tmpPath") 68 | bodyPart.entity.dataBytes 69 | .map { byteString => 70 | byteString.asByteBuffers.foreach(sha.update) 71 | byteString 72 | } 73 | .runWith(FileIO.toPath(tmpPath)) 74 | .map { ioResult => 75 | val computedHash = Utils.bytesToHex(sha.digest()) 76 | fileInfo.hash.foreach { h => 77 | require(h == computedHash, s"前端上传hash与服务端计算hash值不匹配,$h != $computedHash") 78 | } 79 | val localPath = fileInfo.hash match { 80 | case Some(_) => tmpPath 81 | case _ => move(computedHash, tmpPath, ioResult.count) 82 | } 83 | FileBO(fileInfo.hash, Some(computedHash), localPath, ioResult.count, bodyPart.filename, bodyPart.headers) 84 | } 85 | } 86 | 87 | def move(hash: String, tmpFile: Path, contentLength: Long): Path = { 88 | val targetDir = FileUtils.getOrCreateDirectories(Paths.get(FileUtils.LOCAL_PATH, hash.take(2))) 89 | val target = targetDir.resolve(hash) 90 | require(!Files.exists(target), s"目标文件已存在,$target") 91 | Files.move(tmpFile, target) 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /file-upload/src/main/scala/me/yangbajing/fileupload/util/Jackson.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.fileupload.util 2 | import com.fasterxml.jackson.annotation.JsonInclude 3 | import com.fasterxml.jackson.core.JsonParser 4 | import com.fasterxml.jackson.core.JsonProcessingException 5 | import com.fasterxml.jackson.core.TreeNode 6 | import com.fasterxml.jackson.databind._ 7 | import com.fasterxml.jackson.databind.node.ArrayNode 8 | import com.fasterxml.jackson.databind.node.ObjectNode 9 | import com.fasterxml.jackson.databind.ser.DefaultSerializerProvider 10 | import com.fasterxml.jackson.databind.ser.SerializerFactory 11 | 12 | import scala.reflect.ClassTag 13 | 14 | object Jackson { 15 | val defaultObjectMapper: ObjectMapper = createObjectMapper 16 | 17 | def createObjectNode: ObjectNode = defaultObjectMapper.createObjectNode 18 | 19 | def createArrayNode: ArrayNode = defaultObjectMapper.createArrayNode 20 | 21 | def readTree(jstr: String): JsonNode = defaultObjectMapper.readTree(jstr) 22 | 23 | def valueToTree(v: AnyRef): JsonNode = defaultObjectMapper.valueToTree(v) 24 | 25 | def treeToValue[T](tree: TreeNode)(implicit ev1: ClassTag[T]): T = 26 | defaultObjectMapper.treeToValue(tree, ev1.runtimeClass).asInstanceOf[T] 27 | 28 | def stringify(v: AnyRef): String = defaultObjectMapper.writeValueAsString(v) 29 | 30 | def prettyStringify(v: AnyRef): String = defaultObjectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(v) 31 | 32 | def extract[T](tree: TreeNode)(implicit ev1: ClassTag[T]): Either[JsonProcessingException, T] = 33 | try { 34 | Right(defaultObjectMapper.treeToValue(tree, ev1.runtimeClass).asInstanceOf[T]) 35 | } catch { 36 | case e: JsonProcessingException => 37 | Left(e) 38 | } 39 | 40 | @inline def extract[T](compare: Boolean, tree: TreeNode)(implicit ev1: ClassTag[T]): Either[Throwable, T] = 41 | if (compare) extract(tree) 42 | else Left(new IllegalStateException(s"compare比较结果为false,需要类型:${ev1.runtimeClass.getName}")) 43 | 44 | private def createObjectMapper: ObjectMapper = { 45 | new ObjectMapper() 46 | .findAndRegisterModules() 47 | .enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES) 48 | .enable(JsonParser.Feature.ALLOW_SINGLE_QUOTES) 49 | .enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) 50 | .enable(DeserializationFeature.USE_BIG_INTEGER_FOR_INTS) 51 | .disable(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES) 52 | .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) 53 | .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) 54 | // .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) 55 | // .disable(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS) 56 | .enable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) 57 | .disable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) 58 | .setSerializationInclusion(JsonInclude.Include.NON_NULL) 59 | // .setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")) 60 | // .enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY) 61 | } 62 | 63 | private class MassSerializerProvider(src: SerializerProvider, config: SerializationConfig, f: SerializerFactory) 64 | extends DefaultSerializerProvider(src, config, f) { 65 | def this() { 66 | this(null, null, null) 67 | } 68 | 69 | def this(src: MassSerializerProvider) { 70 | this(src, null, null) 71 | } 72 | 73 | override def copy: DefaultSerializerProvider = { 74 | if (getClass ne classOf[MassSerializerProvider]) return super.copy 75 | new MassSerializerProvider(this) 76 | } 77 | 78 | override def createInstance(config: SerializationConfig, jsf: SerializerFactory) = 79 | new MassSerializerProvider(this, config, jsf) 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /file-upload/src/main/scala/me/yangbajing/fileupload/util/JacksonSupport.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.fileupload.util 2 | import akka.http.scaladsl.marshalling.Marshaller 3 | import akka.http.scaladsl.marshalling.ToEntityMarshaller 4 | import akka.http.scaladsl.model.MediaTypes 5 | import akka.http.scaladsl.unmarshalling.FromEntityUnmarshaller 6 | import akka.http.scaladsl.unmarshalling.Unmarshaller 7 | import akka.util.ByteString 8 | import com.fasterxml.jackson.core.util.DefaultPrettyPrinter 9 | import com.fasterxml.jackson.databind.JsonNode 10 | import com.fasterxml.jackson.databind.ObjectMapper 11 | 12 | import scala.reflect.ClassTag 13 | 14 | /** 15 | * Created by yangbajing(yangbajing@gmail.com) on 2017-02-27. 16 | */ 17 | object JacksonSupport extends JacksonSupport { 18 | 19 | def stringify(value: AnyRef): String = 20 | Jackson.defaultObjectMapper.writeValueAsString(value) 21 | 22 | def prettyString(value: AnyRef): String = { 23 | val writer = Jackson.defaultObjectMapper.writer(new DefaultPrettyPrinter()) 24 | writer.writeValueAsString(value) 25 | } 26 | 27 | def readValue[A](content: String)( 28 | implicit 29 | ct: ClassTag[A], 30 | objectMapper: ObjectMapper = Jackson.defaultObjectMapper): A = 31 | objectMapper.readValue(content, ct.runtimeClass).asInstanceOf[A] 32 | 33 | def getJsonNode(content: String)(implicit objectMapper: ObjectMapper = Jackson.defaultObjectMapper): JsonNode = 34 | objectMapper.readTree(content) 35 | 36 | def isError(content: String)(implicit objectMapper: ObjectMapper = Jackson.defaultObjectMapper): Boolean = 37 | !isSuccess(content) 38 | 39 | def isSuccess(content: String)(implicit objectMapper: ObjectMapper = Jackson.defaultObjectMapper): Boolean = { 40 | val node = getJsonNode(content) 41 | val errCode = node.get("errCode") 42 | if (errCode eq null) true else errCode.asInt(0) == 0 43 | } 44 | } 45 | 46 | /** 47 | * JSON marshalling/unmarshalling using an in-scope Jackson's ObjectMapper 48 | */ 49 | trait JacksonSupport { 50 | 51 | private val jsonStringUnmarshaller = 52 | Unmarshaller.byteStringUnmarshaller.forContentTypes(MediaTypes.`application/json`).mapWithCharset { 53 | case (ByteString.empty, _) => throw Unmarshaller.NoContentException 54 | case (data, charset) => data.decodeString(charset.nioCharset.name) 55 | } 56 | 57 | private val jsonStringMarshaller = Marshaller.stringMarshaller(MediaTypes.`application/json`) 58 | 59 | /** 60 | * HTTP entity => `A` 61 | */ 62 | implicit def unmarshaller[A]( 63 | implicit 64 | ct: ClassTag[A], 65 | objectMapper: ObjectMapper = Jackson.defaultObjectMapper): FromEntityUnmarshaller[A] = 66 | jsonStringUnmarshaller.map { data => 67 | objectMapper.readValue(data, ct.runtimeClass).asInstanceOf[A] 68 | } 69 | 70 | /** 71 | * `A` => HTTP entity 72 | */ 73 | implicit def marshaller[A]( 74 | implicit objectMapper: ObjectMapper = Jackson.defaultObjectMapper): ToEntityMarshaller[A] = { 75 | jsonStringMarshaller.compose(objectMapper.writeValueAsString) 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /file-upload/src/main/scala/me/yangbajing/fileupload/util/Utils.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.fileupload.util 2 | import akka.http.scaladsl.coding.Encoder 3 | 4 | object Utils { 5 | private val hexArray = "0123456789abcdef".toCharArray 6 | 7 | def bytesToHex(bytes: Array[Byte]): String = { 8 | val hexChars = new Array[Char](bytes.length * 2) 9 | var j = 0 10 | while (j < bytes.length) { 11 | val v = bytes(j) & 0xFF 12 | hexChars(j * 2) = hexArray(v >>> 4) 13 | hexChars(j * 2 + 1) = hexArray(v & 0x0F) 14 | j += 1 15 | } 16 | new String(hexChars) 17 | } 18 | 19 | Encoder 20 | } 21 | -------------------------------------------------------------------------------- /file-upload/src/test/scala/me/yangbajing/fileupload/controller/FileRouteTest.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.fileupload.controller 2 | 3 | import java.nio.file.Files 4 | import java.nio.file.Paths 5 | 6 | import akka.actor.ActorSystem 7 | import akka.http.scaladsl.Http 8 | import akka.http.scaladsl.model.ContentType 9 | import akka.http.scaladsl.model.HttpEntity 10 | import akka.http.scaladsl.model.HttpMethods 11 | import akka.http.scaladsl.model.HttpRequest 12 | import akka.http.scaladsl.model.MediaTypes 13 | import akka.http.scaladsl.model.Multipart 14 | import akka.http.scaladsl.model.StatusCodes 15 | import akka.http.scaladsl.unmarshalling.Unmarshal 16 | import akka.stream.ActorMaterializer 17 | import akka.stream.Attributes 18 | import akka.stream.scaladsl.FileIO 19 | import akka.stream.scaladsl.Source 20 | import akka.testkit.TestKit 21 | import com.fasterxml.jackson.databind.node.ArrayNode 22 | import com.fasterxml.jackson.databind.node.ObjectNode 23 | import helloscala.common.util.DigestUtils 24 | import me.yangbajing.fileupload.io.CustomFileSource 25 | import org.scalatest.FunSuiteLike 26 | import org.scalatest.MustMatchers 27 | import org.scalatest.concurrent.ScalaFutures 28 | import org.scalatest.time.Milliseconds 29 | import org.scalatest.time.Seconds 30 | import org.scalatest.time.Span 31 | 32 | import scala.concurrent.Await 33 | import scala.concurrent.duration.Duration 34 | 35 | /** 36 | * 断点上传文件 37 | */ 38 | class FileRouteTest extends TestKit(ActorSystem("file-demo")) with FunSuiteLike with MustMatchers with ScalaFutures { 39 | private val BASE_URL = "http://118.89.226.54:8083" 40 | implicit private val mat = ActorMaterializer() 41 | 42 | val CHUNK_SIZE = 8192 43 | val file = Paths.get("/opt/Videos/VID_20190322_104457.mp4") 44 | val hash = DigestUtils.sha256HexFromPath(file) 45 | val fileLength = Files.size(file) 46 | var startPosition = 0L 47 | 48 | test("upload/continue 大文件") { 49 | // 只上传文件的一半内容 50 | val truncateSize = fileLength / 2 51 | 52 | val bodyPart = Multipart.FormData.BodyPart( 53 | s"$hash.$fileLength.0", 54 | HttpEntity.Default( 55 | ContentType(MediaTypes.`video/mp4`), 56 | fileLength, 57 | Source 58 | .fromGraph(new CustomFileSource(file, CHUNK_SIZE, 0L, truncateSize)) 59 | .withAttributes(Attributes(Attributes.Name("file")))), 60 | Map("filename" → file.getFileName.toString)) 61 | 62 | val formData = Multipart.FormData(bodyPart) 63 | val request = 64 | HttpRequest(HttpMethods.POST, s"$BASE_URL/file/ihongka_files/upload/continue", entity = formData.toEntity()) 65 | val responseF = Http().singleRequest(request) 66 | val response = Await.result(responseF, Duration.Inf) 67 | println(s"response: ${response.entity}") 68 | } 69 | 70 | test("upload/continue 大文件上传进度") { 71 | import me.yangbajing.fileupload.util.JacksonSupport._ 72 | val request = HttpRequest(HttpMethods.GET, s"$BASE_URL/file/ihongka_files/progress/$hash") 73 | val response = Http().singleRequest(request).futureValue 74 | response.status mustBe StatusCodes.OK 75 | val result = Unmarshal(response.entity).to[ObjectNode].futureValue 76 | println(result) 77 | startPosition = result.get("size").asLong() 78 | } 79 | 80 | test("upload/continue 断点续传") { 81 | import me.yangbajing.fileupload.util.JacksonSupport._ 82 | val bodyPart = Multipart.FormData.BodyPart( 83 | s"$hash.$fileLength.$startPosition", 84 | HttpEntity.Default( 85 | ContentType(MediaTypes.`video/mp4`), 86 | Files.size(file), 87 | FileIO.fromPath(file, CHUNK_SIZE, startPosition))) 88 | val formData = Multipart.FormData(bodyPart) 89 | val request = 90 | HttpRequest(HttpMethods.POST, s"$BASE_URL/file/ihongka_files/upload/continue", entity = formData.toEntity()) 91 | val responseF = Http().singleRequest(request) 92 | val response = Await.result(responseF, Duration.Inf) 93 | response.status mustBe StatusCodes.OK 94 | val result = Unmarshal(response.entity).to[ArrayNode].futureValue 95 | println(result) 96 | result.get(0).get("_id").asText() mustBe hash 97 | result.get(0).get("uploadSize").asLong() mustBe fileLength 98 | } 99 | 100 | implicit override def patienceConfig: PatienceConfig = PatienceConfig(Span(30, Seconds), Span(50, Milliseconds)) 101 | } 102 | -------------------------------------------------------------------------------- /file-upload/web/css/bootstrap-reboot.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v4.2.1 (https://getbootstrap.com/) 3 | * Copyright 2011-2018 The Bootstrap Authors 4 | * Copyright 2011-2018 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */ 8 | *, 9 | *::before, 10 | *::after { 11 | box-sizing: border-box; 12 | } 13 | 14 | html { 15 | font-family: sans-serif; 16 | line-height: 1.15; 17 | -webkit-text-size-adjust: 100%; 18 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 19 | } 20 | 21 | article, aside, figcaption, figure, footer, header, hgroup, main, nav, section { 22 | display: block; 23 | } 24 | 25 | body { 26 | margin: 0; 27 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 28 | font-size: 1rem; 29 | font-weight: 400; 30 | line-height: 1.5; 31 | color: #212529; 32 | text-align: left; 33 | background-color: #fff; 34 | } 35 | 36 | [tabindex="-1"]:focus { 37 | outline: 0 !important; 38 | } 39 | 40 | hr { 41 | box-sizing: content-box; 42 | height: 0; 43 | overflow: visible; 44 | } 45 | 46 | h1, h2, h3, h4, h5, h6 { 47 | margin-top: 0; 48 | margin-bottom: 0.5rem; 49 | } 50 | 51 | p { 52 | margin-top: 0; 53 | margin-bottom: 1rem; 54 | } 55 | 56 | abbr[title], 57 | abbr[data-original-title] { 58 | text-decoration: underline; 59 | -webkit-text-decoration: underline dotted; 60 | text-decoration: underline dotted; 61 | cursor: help; 62 | border-bottom: 0; 63 | text-decoration-skip-ink: none; 64 | } 65 | 66 | address { 67 | margin-bottom: 1rem; 68 | font-style: normal; 69 | line-height: inherit; 70 | } 71 | 72 | ol, 73 | ul, 74 | dl { 75 | margin-top: 0; 76 | margin-bottom: 1rem; 77 | } 78 | 79 | ol ol, 80 | ul ul, 81 | ol ul, 82 | ul ol { 83 | margin-bottom: 0; 84 | } 85 | 86 | dt { 87 | font-weight: 700; 88 | } 89 | 90 | dd { 91 | margin-bottom: .5rem; 92 | margin-left: 0; 93 | } 94 | 95 | blockquote { 96 | margin: 0 0 1rem; 97 | } 98 | 99 | b, 100 | strong { 101 | font-weight: bolder; 102 | } 103 | 104 | small { 105 | font-size: 80%; 106 | } 107 | 108 | sub, 109 | sup { 110 | position: relative; 111 | font-size: 75%; 112 | line-height: 0; 113 | vertical-align: baseline; 114 | } 115 | 116 | sub { 117 | bottom: -.25em; 118 | } 119 | 120 | sup { 121 | top: -.5em; 122 | } 123 | 124 | a { 125 | color: #007bff; 126 | text-decoration: none; 127 | background-color: transparent; 128 | } 129 | 130 | a:hover { 131 | color: #0056b3; 132 | text-decoration: underline; 133 | } 134 | 135 | a:not([href]):not([tabindex]) { 136 | color: inherit; 137 | text-decoration: none; 138 | } 139 | 140 | a:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus { 141 | color: inherit; 142 | text-decoration: none; 143 | } 144 | 145 | a:not([href]):not([tabindex]):focus { 146 | outline: 0; 147 | } 148 | 149 | pre, 150 | code, 151 | kbd, 152 | samp { 153 | font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 154 | font-size: 1em; 155 | } 156 | 157 | pre { 158 | margin-top: 0; 159 | margin-bottom: 1rem; 160 | overflow: auto; 161 | } 162 | 163 | figure { 164 | margin: 0 0 1rem; 165 | } 166 | 167 | img { 168 | vertical-align: middle; 169 | border-style: none; 170 | } 171 | 172 | svg { 173 | overflow: hidden; 174 | vertical-align: middle; 175 | } 176 | 177 | table { 178 | border-collapse: collapse; 179 | } 180 | 181 | caption { 182 | padding-top: 0.75rem; 183 | padding-bottom: 0.75rem; 184 | color: #6c757d; 185 | text-align: left; 186 | caption-side: bottom; 187 | } 188 | 189 | th { 190 | text-align: inherit; 191 | } 192 | 193 | label { 194 | display: inline-block; 195 | margin-bottom: 0.5rem; 196 | } 197 | 198 | button { 199 | border-radius: 0; 200 | } 201 | 202 | button:focus { 203 | outline: 1px dotted; 204 | outline: 5px auto -webkit-focus-ring-color; 205 | } 206 | 207 | input, 208 | button, 209 | select, 210 | optgroup, 211 | textarea { 212 | margin: 0; 213 | font-family: inherit; 214 | font-size: inherit; 215 | line-height: inherit; 216 | } 217 | 218 | button, 219 | input { 220 | overflow: visible; 221 | } 222 | 223 | button, 224 | select { 225 | text-transform: none; 226 | } 227 | 228 | button, 229 | [type="button"], 230 | [type="reset"], 231 | [type="submit"] { 232 | -webkit-appearance: button; 233 | } 234 | 235 | button::-moz-focus-inner, 236 | [type="button"]::-moz-focus-inner, 237 | [type="reset"]::-moz-focus-inner, 238 | [type="submit"]::-moz-focus-inner { 239 | padding: 0; 240 | border-style: none; 241 | } 242 | 243 | input[type="radio"], 244 | input[type="checkbox"] { 245 | box-sizing: border-box; 246 | padding: 0; 247 | } 248 | 249 | input[type="date"], 250 | input[type="time"], 251 | input[type="datetime-local"], 252 | input[type="month"] { 253 | -webkit-appearance: listbox; 254 | } 255 | 256 | textarea { 257 | overflow: auto; 258 | resize: vertical; 259 | } 260 | 261 | fieldset { 262 | min-width: 0; 263 | padding: 0; 264 | margin: 0; 265 | border: 0; 266 | } 267 | 268 | legend { 269 | display: block; 270 | width: 100%; 271 | max-width: 100%; 272 | padding: 0; 273 | margin-bottom: .5rem; 274 | font-size: 1.5rem; 275 | line-height: inherit; 276 | color: inherit; 277 | white-space: normal; 278 | } 279 | 280 | progress { 281 | vertical-align: baseline; 282 | } 283 | 284 | [type="number"]::-webkit-inner-spin-button, 285 | [type="number"]::-webkit-outer-spin-button { 286 | height: auto; 287 | } 288 | 289 | [type="search"] { 290 | outline-offset: -2px; 291 | -webkit-appearance: none; 292 | } 293 | 294 | [type="search"]::-webkit-search-decoration { 295 | -webkit-appearance: none; 296 | } 297 | 298 | ::-webkit-file-upload-button { 299 | font: inherit; 300 | -webkit-appearance: button; 301 | } 302 | 303 | output { 304 | display: inline-block; 305 | } 306 | 307 | summary { 308 | display: list-item; 309 | cursor: pointer; 310 | } 311 | 312 | template { 313 | display: none; 314 | } 315 | 316 | [hidden] { 317 | display: none !important; 318 | } 319 | /*# sourceMappingURL=bootstrap-reboot.css.map */ -------------------------------------------------------------------------------- /file-upload/web/upload.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 断点上传 7 | 8 | 9 | 10 |
11 |
12 |
13 |
14 |
15 | 16 | 17 |
18 |
19 | 20 | 21 |
22 |
23 | 24 | 25 |
26 |
27 | 34 |
35 |
36 |
37 |
38 | 39 | 40 |
41 |
42 | 43 | 44 |
45 |
46 | 47 | 48 |
49 |
50 | 57 |
58 |
59 | 60 |
61 |
62 |
    63 | 64 |
65 |
66 | 67 | 68 | 69 | 134 | 135 | -------------------------------------------------------------------------------- /jdbc-slick/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM postgresql:10 2 | 3 | RUN localedef -i zh_CN -c -f UTF-8 -A /usr/share/locale/locale.alias zh_CN.UTF-8 4 | 5 | ENV TZ Asia/Shanghai 6 | ENV LANG zh_CN.UTF-8 7 | 8 | COPY init.sql /docker-entrypoint-initdb.d/ 9 | 10 | EXPOSE 5432 11 | -------------------------------------------------------------------------------- /jdbc-slick/README.md: -------------------------------------------------------------------------------- 1 | # 在Scala里使用PostgreSQL 2 | 3 | 见:[https://www.yangbajing.me/2018/08/02/%E5%9C%A8scala%E9%87%8C%E4%BD%BF%E7%94%A8postgresql/](https://www.yangbajing.me/2018/08/02/%E5%9C%A8scala%E9%87%8C%E4%BD%BF%E7%94%A8postgresql/) 4 | -------------------------------------------------------------------------------- /jdbc-slick/build.sbt: -------------------------------------------------------------------------------- 1 | name := "jdbc-slick" 2 | 3 | organization := "me.yangbajing" 4 | 5 | organizationName := "Yangbajing's Garden" 6 | 7 | organizationHomepage := Some(url("https://www.yangbajing.me")) 8 | 9 | homepage := Some(url("https://github.com/yangbajing/scala-applications/tree/master/jdbc-slick")) 10 | 11 | startYear := Some(2018) 12 | 13 | licenses += ("Apache-2.0", new URL("https://www.apache.org/licenses/LICENSE-2.0.txt")) 14 | 15 | scalaVersion := "2.12.6" 16 | 17 | shellPrompt := { s => 18 | Project.extract(s).currentProject.id + " > " 19 | } 20 | 21 | scalacOptions ++= Seq( 22 | "-encoding", "utf8", 23 | "-unchecked", 24 | "-feature", 25 | "-deprecation" 26 | ) 27 | 28 | libraryDependencies ++= Seq( 29 | "com.fasterxml.jackson.datatype" % "jackson-datatype-jdk8" % "2.9.6", 30 | "com.fasterxml.jackson.datatype" % "jackson-datatype-jsr310" % "2.9.6", 31 | "com.fasterxml.jackson.module" % "jackson-module-parameter-names" % "2.9.6", 32 | "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.9.6", 33 | "org.postgresql" % "postgresql" % "42.2.2", 34 | "com.zaxxer" % "HikariCP" % "3.2.0", 35 | "com.typesafe.scala-logging" %% "scala-logging" % "3.9.0", 36 | "com.typesafe.slick" %% "slick" % "3.2.3", 37 | "com.github.tminglei" %% "slick-pg" % "0.16.2", 38 | "com.github.tminglei" %% "slick-pg_json4s" % "0.16.2", 39 | "com.typesafe.akka" %% "akka-stream" % "2.5.14", 40 | "com.typesafe.akka" %% "akka-http" % "10.1.3", 41 | "com.typesafe.akka" %% "akka-http-testkit" % "10.1.3" % Test, 42 | "com.typesafe.slick" %% "slick-testkit" % "3.2.3" % Test, 43 | "org.scalatest" %% "scalatest" % "3.0.5" % Test 44 | ) 45 | -------------------------------------------------------------------------------- /jdbc-slick/init.sql: -------------------------------------------------------------------------------- 1 | set timezone to 'Asia/Chongqing'; 2 | create user scala with nosuperuser 3 | replication 4 | encrypted password 'scala.2018'; 5 | create database scaladb owner = scala template = template0 encoding = 'UTF-8' lc_ctype = 'zh_CN.UTF-8' lc_collate = 'zh_CN.UTF-8'; 6 | 7 | -- create extension 8 | \c scaladb; 9 | create extension adminpack; 10 | create extension hstore; 11 | 12 | -- create tables .... 13 | 14 | create table book ( 15 | id bigserial primary key, 16 | isbn varchar(64) not null, 17 | title varchar(128) not null, 18 | author int [] not null, 19 | description text, 20 | created_at timestamptz, 21 | updated_at timestamptz 22 | ); 23 | create unique index book_uidx_isbn 24 | on book (isbn); 25 | 26 | create table author ( 27 | id serial primary key, 28 | name varchar(128) not null, 29 | age int, 30 | description text, 31 | created_at timestamptz, 32 | updated_at timestamptz 33 | ); 34 | 35 | -- change tables, views, sequences owner to scala 36 | DO $$DECLARE r record; 37 | BEGIN 38 | FOR r IN SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' 39 | LOOP 40 | EXECUTE 'alter table ' || r.table_name || ' owner to scala;'; 41 | END LOOP; 42 | END$$; 43 | 44 | DO $$DECLARE r record; 45 | BEGIN 46 | FOR r IN select sequence_name from information_schema.sequences where sequence_schema = 'public' 47 | LOOP 48 | EXECUTE 'alter sequence ' || r.sequence_name || ' owner to scala;'; 49 | END LOOP; 50 | END$$; 51 | 52 | DO $$DECLARE r record; 53 | BEGIN 54 | FOR r IN select table_name from information_schema.views where table_schema = 'public' 55 | LOOP 56 | EXECUTE 'alter table ' || r.table_name || ' owner to scala;'; 57 | END LOOP; 58 | END$$; 59 | -- grant all privileges on all tables in schema public to scala; 60 | -- grant all privileges on all sequences in schema public to scala; 61 | 62 | -------------------------------------------------------------------------------- /jdbc-slick/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.1.6 2 | -------------------------------------------------------------------------------- /jdbc-slick/sbt: -------------------------------------------------------------------------------- 1 | SCRIPT_DIR=`dirname $0` 2 | java -Dfile.encoding=UTF-8 -noverify -Xms512M -Xmx1024M -Xss1M -XX:+CMSClassUnloadingEnabled -jar "$SCRIPT_DIR/../_lib/sbt-launch.jar" $@ 3 | -------------------------------------------------------------------------------- /jdbc-slick/src/main/scala/combinerequest/CombineExample.scala: -------------------------------------------------------------------------------- 1 | package combinerequest 2 | 3 | import akka.actor.ActorSystem 4 | import combinerequest.service.{EnterpriseService, InfraMongodbRepo, InfraResource} 5 | 6 | import scala.concurrent.Await 7 | import scala.concurrent.duration._ 8 | import scala.util.{Failure, Success} 9 | 10 | /** 11 | * 合并同一时间的3个请求 12 | * Created by Yang Jing (yangbajing@gmail.com) on 2016-06-29. 13 | */ 14 | object CombineExample { 15 | val system = ActorSystem() 16 | 17 | import system.dispatcher 18 | 19 | def main(args: Array[String]): Unit = { 20 | val infraResource = new InfraResource() 21 | val infraMongodbRepo = new InfraMongodbRepo() 22 | val enterpriseService = new EnterpriseService(system, infraResource, infraMongodbRepo) 23 | 24 | val company1Future = enterpriseService.getCorpDetail("科技公司") 25 | val company2Future = enterpriseService.getCorpDetail("科技公司") 26 | val company3Future = enterpriseService.getCorpDetail("科技公司") 27 | 28 | val companiesFuture = for { 29 | company1 <- company1Future 30 | company2 <- company2Future 31 | company3 <- company3Future 32 | } yield { 33 | List(company1, company2, company3) 34 | } 35 | 36 | Await.ready(companiesFuture, 60.seconds) 37 | .onComplete { 38 | case Success(c1 :: c2 :: c3 :: Nil) => 39 | // println(c1 eq c2) 40 | // println(c2 eq c3) 41 | println(s"$c1, $c2, $c3") 42 | case Success(unknown) => 43 | System.err.println(s"收到未期待的结果:$unknown") 44 | 45 | case Failure(e) => 46 | e.printStackTrace() 47 | } 48 | 49 | try { 50 | Await.result(system.terminate(), 60.seconds) 51 | } catch { 52 | case e: Throwable => 53 | e.printStackTrace() 54 | System.exit(3) 55 | } 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /jdbc-slick/src/main/scala/combinerequest/RepetitionExample.scala: -------------------------------------------------------------------------------- 1 | package combinerequest 2 | 3 | import akka.actor.ActorSystem 4 | import combinerequest.domain.Company 5 | import combinerequest.service.{CompanyService, InfraMongodbRepo, InfraResource} 6 | 7 | import scala.concurrent.duration._ 8 | import scala.concurrent.{Await, ExecutionContext, Future} 9 | import scala.util.{Failure, Success} 10 | 11 | /** 12 | * Created by Yang Jing (yangbajing@gmail.com) on 2016-07-02. 13 | */ 14 | object RepetitionExample { 15 | val system = ActorSystem() 16 | 17 | import system.dispatcher 18 | 19 | def main(args: Array[String]): Unit = { 20 | val infraResource = new InfraResource() 21 | val infraMongodbRepo = new InfraMongodbRepo() 22 | val enterpriseService = new RepetitionService(infraResource, infraMongodbRepo) 23 | 24 | val company1Future = enterpriseService.getCorpDetail("科技公司") 25 | val company2Future = enterpriseService.getCorpDetail("科技公司") 26 | val company3Future = enterpriseService.getCorpDetail("科技公司") 27 | 28 | val companiesFuture = for { 29 | company1 <- company1Future 30 | company2 <- company2Future 31 | company3 <- company3Future 32 | } yield { 33 | List(company1, company2, company3) 34 | } 35 | 36 | Await.ready(companiesFuture, 60.seconds) 37 | .onComplete { 38 | case Success(c1 :: c2 :: c3 :: Nil) => 39 | // println(c1 eq c2) 40 | // println(c2 eq c3) 41 | println(s"$c1, $c2, $c3") 42 | case Success(unknown) => 43 | System.err.println(s"收到未期待的结果:$unknown") 44 | 45 | case Failure(e) => 46 | e.printStackTrace() 47 | } 48 | 49 | try { 50 | Await.result(system.terminate(), 60.seconds) 51 | } catch { 52 | case e: Throwable => 53 | e.printStackTrace() 54 | System.exit(3) 55 | } 56 | } 57 | 58 | } 59 | 60 | class RepetitionService(val infraResource: InfraResource, 61 | val infraMongodbRepo: InfraMongodbRepo) extends CompanyService { 62 | 63 | override def getCorpDetail(companyName: String)(implicit ec: ExecutionContext): Future[Option[Company]] = { 64 | infraMongodbRepo.findCorpDetail(companyName) 65 | .flatMap { 66 | case Some(company) => 67 | Future.successful(Some(company)) 68 | case None => 69 | infraResource.corpDetail(companyName) 70 | .flatMap { 71 | case Some(company) => 72 | infraMongodbRepo 73 | .saveCorpDetail(companyName, company) 74 | .map(_ => Some(company)) 75 | 76 | case None => 77 | Future.successful(None) 78 | } 79 | } 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /jdbc-slick/src/main/scala/combinerequest/domain/Company.scala: -------------------------------------------------------------------------------- 1 | package combinerequest.domain 2 | 3 | /** 4 | * Created by Yang Jing (yangbajing@gmail.com) on 2016-06-30. 5 | */ 6 | case class Company(companyName: String) 7 | -------------------------------------------------------------------------------- /jdbc-slick/src/main/scala/combinerequest/message/Messages.scala: -------------------------------------------------------------------------------- 1 | package combinerequest.message 2 | 3 | import akka.actor.{Actor, ActorRef} 4 | import combinerequest.domain.Company 5 | 6 | /** 7 | * Created by Yang Jing (yangbajing@gmail.com) on 2016-06-29. 8 | */ 9 | case class QueryCompany(companyName: String, doSender: ActorRef = Actor.noSender) 10 | 11 | case class ReceiveQueryCompanyResult(companyName: String, corpDetail: Option[Company]) 12 | 13 | sealed trait GetCompanyMessage { 14 | val companyName: String 15 | } 16 | 17 | /** 18 | * 获取工商消息 19 | * 20 | * @param companyName 公司名 21 | */ 22 | case class GetCorpDetail(companyName: String) extends GetCompanyMessage 23 | -------------------------------------------------------------------------------- /jdbc-slick/src/main/scala/combinerequest/service/CompanyMaster.scala: -------------------------------------------------------------------------------- 1 | package combinerequest.service 2 | 3 | import akka.actor.{Actor, Props} 4 | import combinerequest.message.{GetCorpDetail, QueryCompany} 5 | 6 | /** 7 | * Created by Yang Jing (yangbajing@gmail.com) on 2016-06-29. 8 | */ 9 | class CompanyMaster(infraResource: InfraResource, 10 | infraMongodbRepo: InfraMongodbRepo) extends Actor { 11 | 12 | val actorCorpDetail = context.actorOf(CorpDetailActor.props(infraResource, infraMongodbRepo), "corpDetail") 13 | 14 | override def receive = { 15 | case GetCorpDetail(companyName) => 16 | actorCorpDetail ! QueryCompany(companyName, sender()) 17 | } 18 | 19 | } 20 | 21 | object CompanyMaster { 22 | 23 | def props(infraResource: InfraResource, 24 | infraMongodbRepo: InfraMongodbRepo) = 25 | Props(new CompanyMaster(infraResource, infraMongodbRepo)) 26 | 27 | } 28 | -------------------------------------------------------------------------------- /jdbc-slick/src/main/scala/combinerequest/service/CorpDetailActor.scala: -------------------------------------------------------------------------------- 1 | package combinerequest.service 2 | 3 | import akka.actor.Props 4 | import combinerequest.service.ForwardCompanyActor.{ReadFromDB, ReadFromInfra} 5 | 6 | import scala.concurrent.Future 7 | 8 | /** 9 | * Created by Yang Jing (yangbajing@gmail.com) on 2016-06-29. 10 | */ 11 | class CorpDetailActor(infraResource: InfraResource, 12 | infraMongodbRepo: InfraMongodbRepo) extends ForwardCompanyActor { 13 | 14 | import context.dispatcher 15 | 16 | override val readFromDB: ReadFromDB = (companyName) => { 17 | infraMongodbRepo.findCorpDetail(companyName) 18 | } 19 | 20 | override val readFromInfra: ReadFromInfra = (companyName) => { 21 | infraResource.corpDetail(companyName) 22 | .flatMap { 23 | case Some(company) => 24 | infraMongodbRepo 25 | .saveCorpDetail(companyName, company) 26 | .map(_ => Some(company)) 27 | 28 | case None => 29 | Future.successful(None) 30 | } 31 | } 32 | 33 | } 34 | 35 | object CorpDetailActor { 36 | def props(infraResource: InfraResource, 37 | infraMongodbRepo: InfraMongodbRepo) = 38 | Props(new CorpDetailActor(infraResource, infraMongodbRepo)) 39 | } 40 | -------------------------------------------------------------------------------- /jdbc-slick/src/main/scala/combinerequest/service/EnterpriseService.scala: -------------------------------------------------------------------------------- 1 | package combinerequest.service 2 | 3 | import akka.actor.ActorSystem 4 | import akka.pattern.ask 5 | import akka.util.Timeout 6 | import combinerequest.domain.Company 7 | import combinerequest.message.{GetCompanyMessage, GetCorpDetail} 8 | 9 | import scala.concurrent.{ExecutionContext, Future} 10 | import scala.concurrent.duration._ 11 | 12 | /** 13 | * Created by Yang Jing (yangbajing@gmail.com) on 2016-06-29. 14 | */ 15 | trait CompanyService { 16 | val infraResource: InfraResource 17 | val infraMongodbRepo: InfraMongodbRepo 18 | 19 | def getCorpDetail(companyName: String)(implicit ec: ExecutionContext): Future[Option[Company]] 20 | } 21 | 22 | class EnterpriseService(actorSystem: ActorSystem, 23 | val infraResource: InfraResource, 24 | val infraMongodbRepo: InfraMongodbRepo) extends CompanyService { 25 | 26 | private val master = actorSystem.actorOf(CompanyMaster.props(infraResource, infraMongodbRepo), "infra-company") 27 | private implicit val timeout = Timeout(60.seconds) 28 | 29 | @inline 30 | private def getResult(message: GetCompanyMessage)(implicit ec: ExecutionContext) = 31 | master.ask(message).mapTo[Option[Company]] 32 | 33 | def getCorpDetail(companyName: String)(implicit ec: ExecutionContext) = getResult(GetCorpDetail(companyName)) 34 | 35 | } 36 | -------------------------------------------------------------------------------- /jdbc-slick/src/main/scala/combinerequest/service/ForwardCompanyActor.scala: -------------------------------------------------------------------------------- 1 | package combinerequest.service 2 | 3 | import akka.actor.{Actor, ActorRef} 4 | import combinerequest.domain.Company 5 | import combinerequest.message.{QueryCompany, ReceiveQueryCompanyResult} 6 | 7 | import scala.collection.mutable 8 | import scala.concurrent.Future 9 | 10 | /** 11 | * Created by Yang Jing (yangbajing@gmail.com) on 2016-06-29. 12 | */ 13 | trait ForwardCompanyActor extends Actor { 14 | 15 | import ForwardCompanyActor._ 16 | 17 | val companyListeners = mutable.Map.empty[String, Set[ActorRef]] 18 | 19 | override def receive = { 20 | case QueryCompany(companyName, doSender) => 21 | val listener = if (doSender == Actor.noSender) sender() else doSender 22 | registerListener(companyName, listener) 23 | 24 | case ReceiveQueryCompanyResult(companyName, maybeJsValue) => 25 | dispatchListeners(companyName, maybeJsValue) 26 | } 27 | 28 | def performReadTask(companyName: String): Unit = { 29 | import context.dispatcher 30 | readFromDB(companyName) 31 | .flatMap(maybe => if (maybe.isEmpty) readFromInfra(companyName) else Future.successful(maybe)) 32 | .foreach(maybe => self ! ReceiveQueryCompanyResult(companyName, maybe)) 33 | } 34 | 35 | def registerListener(companyName: String, listener: ActorRef): Unit = 36 | companyListeners.get(companyName) match { 37 | case Some(actors) => 38 | companyListeners.put(companyName, actors + listener) 39 | case None => 40 | companyListeners.put(companyName, Set(listener)) 41 | performReadTask(companyName) 42 | } 43 | 44 | def dispatchListeners(companyName: String, maybeJsValue: Option[Company]): Unit = { 45 | val maybeListener = companyListeners.get(companyName) 46 | maybeListener.foreach { listeners => 47 | for (listener <- listeners) { 48 | listener ! maybeJsValue 49 | } 50 | companyListeners -= companyName 51 | } 52 | } 53 | 54 | val readFromInfra: ReadFromInfra 55 | 56 | val readFromDB: ReadFromDB 57 | } 58 | 59 | object ForwardCompanyActor { 60 | 61 | type ReadFromDB = (String) => Future[Option[Company]] 62 | 63 | type ReadFromInfra = (String) => Future[Option[Company]] 64 | 65 | } 66 | -------------------------------------------------------------------------------- /jdbc-slick/src/main/scala/combinerequest/service/InfraMongodbRepo.scala: -------------------------------------------------------------------------------- 1 | package combinerequest.service 2 | 3 | import java.time.LocalDateTime 4 | import java.util.concurrent.{ConcurrentHashMap, TimeUnit} 5 | 6 | import combinerequest.domain.Company 7 | 8 | import scala.concurrent.{ExecutionContext, Future} 9 | 10 | /** 11 | * Created by Yang Jing (yangbajing@gmail.com) on 2016-06-29. 12 | */ 13 | class InfraMongodbRepo { 14 | 15 | def saveCorpDetail(companyName: String, company: Company)(implicit ec: ExecutionContext): Future[Option[Company]] = 16 | Future { 17 | TimeUnit.MICROSECONDS.sleep(200) 18 | println(s"[${LocalDateTime.now()}] 保存公司: $company 成功") 19 | InfraCompanyDBMock.saveCompany(companyName, company) 20 | } 21 | 22 | def findCorpDetail(companyName: String)(implicit ec: ExecutionContext): Future[Option[Company]] = Future { 23 | TimeUnit.MILLISECONDS.sleep(100) 24 | InfraCompanyDBMock.getCompany(companyName) match { 25 | case some@Some(_) => 26 | println(s"[${LocalDateTime.now()}] 从本地数据库找到公司:$companyName") 27 | some 28 | case None => 29 | println(s"[${LocalDateTime.now()}] 本地数据库未找到公司:$companyName") 30 | None 31 | } 32 | } 33 | 34 | } 35 | 36 | object InfraCompanyDBMock { 37 | private val companies = new ConcurrentHashMap[String, Company]() 38 | 39 | def getCompany(companyName: String) = Option(companies.get(companyName)) 40 | 41 | def saveCompany(companyName: String, company: Company) = Option(companies.put(companyName, company)) 42 | } 43 | -------------------------------------------------------------------------------- /jdbc-slick/src/main/scala/combinerequest/service/InfraResource.scala: -------------------------------------------------------------------------------- 1 | package combinerequest.service 2 | 3 | import java.time.LocalDateTime 4 | import java.util.concurrent.TimeUnit 5 | 6 | import combinerequest.domain.Company 7 | 8 | import scala.concurrent.{ExecutionContext, Future} 9 | 10 | /** 11 | * Created by Yang Jing (yangbajing@gmail.com) on 2016-06-29. 12 | */ 13 | class InfraResource { 14 | 15 | def corpDetail(companyName: String)(implicit ec: ExecutionContext): Future[Option[Company]] = Future { 16 | TimeUnit.SECONDS.sleep(1) 17 | println(s"[${LocalDateTime.now()}] 收到查询:$companyName 工商信息付费请求") 18 | Some(Company(companyName)) 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /jdbc-slick/src/main/scala/sample/model/Org.scala: -------------------------------------------------------------------------------- 1 | package sample.model 2 | 3 | import java.time.OffsetDateTime 4 | 5 | case class Org( 6 | id: Int, 7 | code: Option[String], 8 | name: String, 9 | contact: String, 10 | parent: Option[Int], 11 | parents: List[Int], 12 | status: Int = 1, 13 | createdAt: OffsetDateTime = OffsetDateTime.now(), 14 | updatedAt: Option[OffsetDateTime] = None 15 | ) 16 | 17 | case class OrgCreateReq( 18 | code: Option[String], 19 | name: String, 20 | contact: Option[String] 21 | ) 22 | 23 | case class OrgUpdateReq( 24 | code: Option[String], 25 | name: Option[String], 26 | contact: Option[String], 27 | status: Option[Int] 28 | ) 29 | 30 | case class OrgPageReq( 31 | code: Option[String], 32 | name: Option[String], 33 | status: Option[Int] 34 | ) 35 | 36 | case class OrgPageResp( 37 | content: Seq[Org], 38 | totalElements: Long, 39 | page: Int, 40 | size: Int 41 | ) 42 | -------------------------------------------------------------------------------- /jdbc-slick/src/main/scala/sample/respository/OrgRepository.scala: -------------------------------------------------------------------------------- 1 | package sample.respository 2 | 3 | import scala.concurrent.Future 4 | 5 | class OrgRepository(schema: Schema) { 6 | 7 | import schema.profile.api._ 8 | import schema._ 9 | 10 | def removeByIds(orgIds: Iterable[Int]): Future[Array[Int]] = { 11 | ??? 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /jdbc-slick/src/main/scala/sample/respository/Schema.scala: -------------------------------------------------------------------------------- 1 | package sample.respository 2 | 3 | import java.time.OffsetDateTime 4 | 5 | import SlickProfile.api._ 6 | import sample.model.Org 7 | 8 | object Schema { 9 | def apply() = new Schema() 10 | } 11 | 12 | class Schema { 13 | val profile = SlickProfile 14 | 15 | val db = Database.forConfig("sample.datasource") 16 | 17 | def tOrg = TableQuery[TableOrg] 18 | } 19 | 20 | class TableOrg(tag: Tag) extends Table[Org](tag, "t_org") { 21 | def id = column[Int]("id", O.PrimaryKey, O.AutoInc) 22 | 23 | def code = column[Option[String]]("code") 24 | 25 | def name = column[String]("name") 26 | 27 | def contact = column[String]("contact", O.SqlType("text")) 28 | 29 | def status = column[Int]("status", O.Default(1)) 30 | 31 | def createdAt = column[OffsetDateTime]("created_at") 32 | 33 | def updatedAt = column[Option[OffsetDateTime]]("update_at") 34 | 35 | def * = (id, code, name, contact, status, createdAt, updatedAt).mapTo[Org] 36 | } 37 | -------------------------------------------------------------------------------- /jdbc-slick/src/main/scala/sample/respository/SlickProfile.scala: -------------------------------------------------------------------------------- 1 | package sample.respository 2 | 3 | import com.github.tminglei.slickpg.{ExPostgresProfile, PgDate2Support} 4 | 5 | trait SlickProfile extends ExPostgresProfile with PgDate2Support { 6 | override val api: API = new super.API with DateTimeImplicits {} 7 | } 8 | 9 | object SlickProfile extends SlickProfile 10 | -------------------------------------------------------------------------------- /jdbc-slick/src/main/scala/sample/route/OrgRoute.scala: -------------------------------------------------------------------------------- 1 | package sample.route 2 | import akka.http.scaladsl.server.Directives._ 3 | import sample.model.OrgCreateReq 4 | import sample.service.OrgService 5 | 6 | class OrgRoute(orgService: OrgService) { 7 | 8 | def route= pathPrefix("route") { 9 | createRoute 10 | } 11 | 12 | def createRoute = (path("item") & post) { 13 | import sample.util.JacksonSupport._ 14 | entity(as[OrgCreateReq]) { req => 15 | onSuccess(orgService.create(req)) { resp => 16 | complete(resp) 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /jdbc-slick/src/main/scala/sample/service/OrgService.scala: -------------------------------------------------------------------------------- 1 | package sample.service 2 | 3 | import sample.model.{Org, OrgCreateReq} 4 | import sample.respository.{OrgRepository, Schema} 5 | 6 | import scala.concurrent.Future 7 | 8 | class OrgService(schema: Schema) { 9 | 10 | private val orgRepository= new OrgRepository(schema) 11 | 12 | def create(req: OrgCreateReq): Future[Org] = { 13 | ??? 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /jdbc-slick/src/main/scala/sample/util/Jackson.java: -------------------------------------------------------------------------------- 1 | package sample.util; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import com.fasterxml.jackson.core.JsonParser; 5 | import com.fasterxml.jackson.databind.DeserializationFeature; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import com.fasterxml.jackson.databind.SerializationFeature; 8 | import com.fasterxml.jackson.databind.node.ArrayNode; 9 | import com.fasterxml.jackson.databind.node.ObjectNode; 10 | 11 | import java.text.SimpleDateFormat; 12 | 13 | /** 14 | * Jackson全局配置 15 | * Created by yangbajing(yangbajing@gmail.com) on 2017-03-14. 16 | */ 17 | public class Jackson { 18 | public static final ObjectMapper defaultObjectMapper = getObjectMapper(); 19 | 20 | public static ObjectNode createObjectNode() { 21 | return defaultObjectMapper.createObjectNode(); 22 | } 23 | 24 | public static ArrayNode createArrayNode() { 25 | return defaultObjectMapper.createArrayNode(); 26 | } 27 | 28 | private static ObjectMapper getObjectMapper() { 29 | return new ObjectMapper() 30 | .findAndRegisterModules() 31 | .setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")) 32 | // .enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY) 33 | .enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES) 34 | .enable(JsonParser.Feature.ALLOW_SINGLE_QUOTES) 35 | .enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) 36 | .enable(DeserializationFeature.USE_BIG_INTEGER_FOR_INTS) 37 | .disable(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES) 38 | .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) 39 | .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) 40 | .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) 41 | .disable(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS) 42 | .setSerializationInclusion(JsonInclude.Include.NON_NULL); 43 | } 44 | } 45 | 46 | -------------------------------------------------------------------------------- /jdbc-slick/src/main/scala/sample/util/JacksonHelper.java: -------------------------------------------------------------------------------- 1 | package sample.util; 2 | 3 | import akka.Done; 4 | import akka.http.javadsl.marshalling.Marshaller; 5 | import akka.http.javadsl.model.HttpEntity; 6 | import akka.http.javadsl.model.MediaTypes; 7 | import akka.http.javadsl.model.RequestEntity; 8 | import akka.http.javadsl.unmarshalling.Unmarshaller; 9 | import akka.util.ByteString; 10 | import com.fasterxml.jackson.core.JsonProcessingException; 11 | import com.fasterxml.jackson.databind.ObjectMapper; 12 | 13 | import java.io.IOException; 14 | import java.util.HashMap; 15 | import java.util.Map; 16 | 17 | public class JacksonHelper { 18 | public static final Map empty = new HashMap<>(); 19 | 20 | public static Marshaller marshaller() { 21 | return marshaller(Jackson.defaultObjectMapper); 22 | } 23 | 24 | public static Marshaller marshaller(ObjectMapper mapper) { 25 | return Marshaller.wrapEntity( 26 | u -> toJSON(mapper, u), 27 | Marshaller.stringToEntity(), 28 | MediaTypes.APPLICATION_JSON 29 | ); 30 | } 31 | 32 | public static Unmarshaller byteStringUnmarshaller(Class expectedType) { 33 | return byteStringUnmarshaller(Jackson.defaultObjectMapper, expectedType); 34 | } 35 | 36 | public static Unmarshaller unmarshaller(Class expectedType) { 37 | return unmarshaller(Jackson.defaultObjectMapper, expectedType); 38 | } 39 | 40 | public static Unmarshaller unmarshaller(ObjectMapper mapper, Class expectedType) { 41 | return Unmarshaller.forMediaType(MediaTypes.APPLICATION_JSON, Unmarshaller.entityToString()) 42 | .thenApply(s -> fromJSON(mapper, s, expectedType)); 43 | } 44 | 45 | public static Unmarshaller byteStringUnmarshaller(ObjectMapper mapper, Class expectedType) { 46 | return Unmarshaller.sync(s -> fromJSON(mapper, s.utf8String(), expectedType)); 47 | } 48 | 49 | private static String toJSON(ObjectMapper mapper, Object object) { 50 | try { 51 | if (object instanceof Done) 52 | return mapper.writeValueAsString(empty); 53 | 54 | return mapper.writeValueAsString(object); 55 | } catch (JsonProcessingException e) { 56 | throw new IllegalArgumentException("Cannot marshal to JSON: " + object, e); 57 | } 58 | } 59 | 60 | private static T fromJSON(ObjectMapper mapper, String json, Class expectedType) { 61 | try { 62 | return mapper.readerFor(expectedType).readValue(json); 63 | } catch (IOException e) { 64 | throw new IllegalArgumentException("Cannot unmarshal JSON as " + expectedType.getSimpleName(), e); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /jdbc-slick/src/main/scala/sample/util/JacksonSupport.scala: -------------------------------------------------------------------------------- 1 | package sample.util 2 | 3 | import akka.http.scaladsl.marshalling.ToEntityMarshaller 4 | import akka.http.scaladsl.model.MediaTypes 5 | import akka.http.scaladsl.unmarshalling.{FromEntityUnmarshaller, Unmarshaller} 6 | import akka.util.ByteString 7 | import com.fasterxml.jackson.core.util.DefaultPrettyPrinter 8 | import com.fasterxml.jackson.databind.{JsonNode, ObjectMapper} 9 | 10 | import scala.reflect.ClassTag 11 | 12 | object JacksonSupport extends JacksonSupport { 13 | def stringify(value: AnyRef): String = { 14 | Jackson.defaultObjectMapper.writeValueAsString(value) 15 | } 16 | 17 | def prettyString(value: AnyRef): String = { 18 | val writer = Jackson.defaultObjectMapper.writer(new DefaultPrettyPrinter()) 19 | writer.writeValueAsString(value) 20 | } 21 | 22 | def readValue[A](content: String)(implicit 23 | ct: ClassTag[A], 24 | objectMapper: ObjectMapper = 25 | Jackson.defaultObjectMapper): A = { 26 | objectMapper.readValue(content, ct.runtimeClass).asInstanceOf[A] 27 | } 28 | 29 | def getJsonNode(content: String)(implicit objectMapper: ObjectMapper = 30 | Jackson.defaultObjectMapper): JsonNode = { 31 | objectMapper.readTree(content) 32 | } 33 | 34 | def isError(content: String)(implicit objectMapper: ObjectMapper = 35 | Jackson.defaultObjectMapper): Boolean = { 36 | !isSuccess(content) 37 | } 38 | 39 | def isSuccess(content: String)(implicit objectMapper: ObjectMapper = 40 | Jackson.defaultObjectMapper): Boolean = { 41 | val node = getJsonNode(content) 42 | val errCode = node.get("errCode") 43 | if (errCode eq null) true else errCode.asInt(0) == 0 44 | } 45 | } 46 | 47 | /** 48 | * JSON marshalling/unmarshalling using an in-scope Jackson's ObjectMapper 49 | */ 50 | trait JacksonSupport { 51 | 52 | private val jsonStringUnmarshaller = 53 | Unmarshaller.byteStringUnmarshaller 54 | .forContentTypes(MediaTypes.`application/json`) 55 | .mapWithCharset { 56 | case (ByteString.empty, _) => throw Unmarshaller.NoContentException 57 | case (data, charset) => data.decodeString(charset.nioCharset.name) 58 | } 59 | 60 | // private val jsonStringMarshaller = Marshaller.stringMarshaller(MediaTypes.`application/json`) 61 | 62 | /** 63 | * HTTP entity => `A` 64 | */ 65 | implicit def unmarshaller[A](implicit 66 | ct: ClassTag[A], 67 | objectMapper: ObjectMapper = 68 | Jackson.defaultObjectMapper) 69 | : FromEntityUnmarshaller[A] = 70 | jsonStringUnmarshaller.map { data => 71 | objectMapper.readValue(data, ct.runtimeClass).asInstanceOf[A] 72 | } 73 | 74 | /** 75 | * `A` => HTTP entity 76 | */ 77 | implicit def marshaller[A]( 78 | implicit objectMapper: ObjectMapper = Jackson.defaultObjectMapper) 79 | : ToEntityMarshaller[A] = { 80 | // jsonStringMarshaller.compose(objectMapper.writeValueAsString) 81 | JacksonHelper.marshaller[A](objectMapper) 82 | } 83 | 84 | } -------------------------------------------------------------------------------- /jdbc-slick/src/test/scala/sample/route/OrgRouteTest.scala: -------------------------------------------------------------------------------- 1 | package sample.route 2 | 3 | import akka.http.scaladsl.model.StatusCodes 4 | import akka.http.scaladsl.testkit.ScalatestRouteTest 5 | import org.scalatest.concurrent.ScalaFutures 6 | import org.scalatest.{BeforeAndAfterAll, MustMatchers, OptionValues, WordSpec} 7 | import sample.model.{Org, OrgCreateReq} 8 | import akka.http.scaladsl.server.Directives._ 9 | import sample.respository.OrgRepository 10 | import sample.service.OrgService 11 | 12 | class OrgRouteTest extends WordSpec with BeforeAndAfterAll with ScalatestRouteTest with MustMatchers with OptionValues with ScalaFutures { 13 | 14 | private var orgIds: Set[Int] = Set() 15 | private val schema = Schema() 16 | private val orgService = new OrgService() 17 | private val route = new OrgRoute(orgService).route 18 | 19 | "OrgRoute" should { 20 | import sample.util.JacksonSupport._ 21 | 22 | var org: Org = null 23 | 24 | "create" in { 25 | val req = OrgCreateReq(Some("000001"), "测试组织", None) 26 | Post("/org/item", req) ~> route ~> check { 27 | status mustBe StatusCodes.Created 28 | org = responseAs[Org] 29 | orgIds += org.id 30 | org.id must be > 0 31 | org.parent mustBe None 32 | org.updatedAt mustBe None 33 | } 34 | } 35 | 36 | "get" in { 37 | pending 38 | } 39 | 40 | "pageRoute" in { 41 | pending 42 | } 43 | 44 | "updateRoute" in { 45 | pending 46 | } 47 | 48 | "remoteRoute" in { 49 | pending 50 | } 51 | } 52 | 53 | private def cleanup() { 54 | val orgRepository = new OrgRepository() 55 | orgRepository.removeByIds(orgIds).futureValue 56 | } 57 | 58 | override def afterAll() { 59 | cleanup() 60 | super.afterAll() 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /scala-js/build.sbt: -------------------------------------------------------------------------------- 1 | enablePlugins(ScalaJSPlugin) 2 | 3 | name := "Scala.js Tutorial" 4 | 5 | scalaVersion := "2.11.8" 6 | 7 | scalaJSUseRhino in Global := false 8 | 9 | libraryDependencies += "org.scala-js" %%% "scalajs-dom" % "0.9.1" 10 | 11 | libraryDependencies += "be.doeraene" %%% "scalajs-jquery" % "0.9.0" 12 | 13 | libraryDependencies += "com.lihaoyi" %%% "utest" % "0.3.0" % "test" 14 | 15 | libraryDependencies += "com.thoughtworks.binding" %%% "dom" % "latest.release" 16 | 17 | addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full) 18 | 19 | skip in packageJSDependencies := false 20 | 21 | jsDependencies += 22 | "org.webjars" % "jquery" % "2.1.4" / "2.1.4/jquery.js" 23 | 24 | jsDependencies += RuntimeDOM 25 | 26 | 27 | testFrameworks += new TestFramework("utest.runner.Framework") 28 | 29 | scalaJSStage in Global := FullOptStage 30 | 31 | persistLauncher in Compile := true 32 | 33 | persistLauncher in Test := false 34 | -------------------------------------------------------------------------------- /scala-js/index-dev.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | The Scala.js Tutorial 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | -------------------------------------------------------------------------------- /scala-js/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.12 2 | -------------------------------------------------------------------------------- /scala-js/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.11") 2 | -------------------------------------------------------------------------------- /scala-js/scalajs-tutorial.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | The Scala.js Tutorial 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /scala-js/src/main/scala/tutorial/webapp/TutorialApp.scala: -------------------------------------------------------------------------------- 1 | package tutorial.webapp 2 | 3 | import org.scalajs.dom 4 | import org.scalajs.dom.document 5 | import org.scalajs.jquery.jQuery 6 | 7 | import scala.scalajs.js.JSApp 8 | import scala.scalajs.js.annotation.JSExport 9 | 10 | object TutorialApp extends JSApp { 11 | 12 | def appendPar(targetNode: dom.Node, text: String): Unit = { 13 | val parNode = document.createElement("p") 14 | val textNode = document.createTextNode(text) 15 | parNode.appendChild(textNode) 16 | targetNode.appendChild(parNode) 17 | } 18 | 19 | def setupUI(): Unit = { 20 | // jQuery("#click-me-button").click(addClickedMessage _) 21 | jQuery("body").append("

您好, 世界!

") 22 | jQuery("""""") 23 | .click(addClickedMessage _) 24 | .appendTo(jQuery("body")) 25 | } 26 | 27 | @JSExport 28 | def addClickedMessage(): Unit = { 29 | appendPar(document.body, "You clicked the button!") 30 | } 31 | 32 | def main() { 33 | // appendPar(document.body, "Hello world") 34 | // jQuery("body").append("

[message]

") 35 | jQuery(setupUI _) 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /scala-js/src/test/scala/tutorial/webapp/TutorialAppTest.scala: -------------------------------------------------------------------------------- 1 | package tutorial.webapp 2 | 3 | import org.scalajs.jquery.jQuery 4 | import utest._ 5 | 6 | /** 7 | * Created by Yang Jing (yangbajing@gmail.com) on 2016-08-09. 8 | */ 9 | object TutorialAppTest extends TestSuite { 10 | 11 | TutorialApp.setupUI() 12 | 13 | override def tests = TestSuite { 14 | 'HelloWorld { 15 | assert(jQuery("p:contains('您好, 世界!')").length >= 1) 16 | } 17 | 18 | 'ButtonClick { 19 | def messageCount = jQuery("p:contains('You clicked the button!')").length 20 | 21 | val button = jQuery("button:contains('Click me!')") 22 | 23 | assert(button.length == 1) 24 | assert(messageCount == 0) 25 | 26 | for (c <- 1 to 5) { 27 | button.click() 28 | assert(messageCount == c) 29 | } 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /scala-script/build.sbt: -------------------------------------------------------------------------------- 1 | scalaVersion := "2.11.8" 2 | 3 | scalacOptions ++= Seq( 4 | "-encoding", "utf8", 5 | "-unchecked", 6 | "-feature", 7 | "-deprecation" 8 | ) 9 | 10 | libraryDependencies += "com.typesafe.play" %% "play-ws" % "2.5.4" 11 | -------------------------------------------------------------------------------- /scala-script/sbt: -------------------------------------------------------------------------------- 1 | SCRIPT_DIR=`dirname $0` 2 | java -Dfile.encoding=UTF-8 -noverify -Xms512M -Xmx1024M -Xss1M -XX:+CMSClassUnloadingEnabled -jar "$SCRIPT_DIR/../_lib/sbt-launch.jar" $@ 3 | -------------------------------------------------------------------------------- /scala-script/src/main/scala/Demo.scala: -------------------------------------------------------------------------------- 1 | import java.nio.file._ 2 | 3 | import akka.actor.ActorSystem 4 | import akka.stream.ActorMaterializer 5 | import play.api.libs.ws.ahc.AhcWSClient 6 | 7 | import scala.concurrent.Await 8 | import scala.concurrent.duration._ 9 | 10 | object Demo extends App { 11 | implicit val system = ActorSystem() 12 | implicit val mat = ActorMaterializer() 13 | val client = AhcWSClient() 14 | 15 | val paramsList = 16 | """曹某某 4310231982xxxxxx38 158xxxx7233 6217xxxxxxxxxx95922 17 | |陈某 4503041989xxxxxx14 186xxxx1221 6229xxxxxxxxx79419 18 | |雷某某 6121021979xxxxxx19 186xxxxx758 6217xxxxxxxxxx75215 19 | | 季某某 3101151983xxxxxx39 139xxxx4665 6226xxxxxxx63987 20 | |何某某 4101811989xxxxxx14 131xxxx2515 6127xxxxxxxxxx01601 21 | |熊某某 5125281981xxxxxx70 188xxxx7391 6230xxxxxxxxxx61491 22 | | 罗某某 4305811990xxxxxx94 158xxxx6035 6212xxxxxxxxxx20908 23 | |吴某某 5002371986xxxxxx68 188xxxx8725 6228xxxxxxxxxx16476 24 | |罗某 4409811991xxxxxx11 135xxxx3967 6212xxxxxxxxxx62918 25 | |许某 3607351997xxxxxx37 181xxxx7357 6228xxxxxxxxxx41278""".stripMargin 26 | .split("\n") 27 | .map { line => 28 | //("external", "true") :: (List("personName", "idCard", "tel", "bankCard") zip line.trim.split("""[ ]+""")) 29 | Seq("personName", "idCard", "tel", "bankCard") zip line.trim.split("""[ ]+""") 30 | } 31 | //paramsList.foreach(println) 32 | 33 | def send(params: Seq[(String, String)]) = { 34 | val future = client 35 | .url("http://localhost:8080/api/integration/person/report") 36 | .withQueryString(params: _*) 37 | .get() 38 | val resp = Await.result(future, 10.seconds) 39 | resp.body 40 | } 41 | 42 | val results = paramsList.map(params => send(params)) 43 | Files.write(Paths.get("demo.txt"), results.mkString("\n").getBytes("UTF-8")) 44 | 45 | client.close() 46 | Await.result(system.terminate(), 10.seconds) 47 | } 48 | 49 | -------------------------------------------------------------------------------- /scala-script/src/main/scala/ScalaExample.scala: -------------------------------------------------------------------------------- 1 | import scala.annotation.tailrec 2 | 3 | /** 4 | * Created by Yang Jing (yangbajing@gmail.com) on 2016-05-30. 5 | */ 6 | object ScalaExample { 7 | 8 | @tailrec 9 | def factorial(n: Int, acc: Long): Long = { 10 | if (n <= 0) acc 11 | else factorial(n - 1, n * acc) 12 | } 13 | 14 | def main(args: Array[String]): Unit = { 15 | factorial(20, 1L) 16 | var i = 0 17 | var result = 0L 18 | val begin = System.nanoTime() 19 | while (i < 10) { 20 | result = factorial(20, 1L) 21 | i += 1 22 | } 23 | val end = System.nanoTime() 24 | println(s"result: $result, cost time: ${end - begin} ns") 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /scalatest/README.md: -------------------------------------------------------------------------------- 1 | # ScalaTest 示例 2 | 3 | 见:[https://www.yangbajing.me/2018/08/02/测试:使用scalatest/](https://www.yangbajing.me/2018/08/02/测试:使用scalatest/) 4 | -------------------------------------------------------------------------------- /scalatest/build.sbt: -------------------------------------------------------------------------------- 1 | name := "scalatest" 2 | 3 | organization := "me.yangbajing" 4 | 5 | organizationName := "Yangbajing's Garden" 6 | 7 | organizationHomepage := Some(url("https://www.yangbajing.me")) 8 | 9 | homepage := Some(url("https://github.com/yangbajing/scala-applications/tree/master/scalatest")) 10 | 11 | startYear := Some(2018) 12 | 13 | licenses += ("Apache-2.0", new URL("https://www.apache.org/licenses/LICENSE-2.0.txt")) 14 | 15 | scalaVersion := "2.12.6" 16 | 17 | shellPrompt := { s => 18 | Project.extract(s).currentProject.id + " > " 19 | } 20 | 21 | scalacOptions ++= Seq( 22 | "-encoding", "utf8", 23 | "-unchecked", 24 | "-feature", 25 | "-deprecation" 26 | ) 27 | 28 | libraryDependencies ++= Seq( 29 | "org.scalamock" %% "scalamock" % "4.1.0" % Test, 30 | "org.scalatest" %% "scalatest" % "3.0.5" % Test 31 | ) 32 | 33 | -------------------------------------------------------------------------------- /scalatest/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.1.6 2 | -------------------------------------------------------------------------------- /scalatest/sbt: -------------------------------------------------------------------------------- 1 | SCRIPT_DIR=`dirname $0` 2 | java -Dfile.encoding=UTF-8 -noverify -Xms512M -Xmx1024M -Xss1M -XX:+CMSClassUnloadingEnabled -jar "$SCRIPT_DIR/../_lib/sbt-launch.jar" $@ 3 | -------------------------------------------------------------------------------- /scalatest/src/test/scala/first/FirstTest.scala: -------------------------------------------------------------------------------- 1 | package first 2 | 3 | import org.scalamock.scalatest.MockFactory 4 | 5 | import scala.collection.mutable 6 | import org.scalatest._ 7 | import org.scalatest.concurrent.ScalaFutures 8 | import org.scalatest.time.{Millis, Seconds, Span} 9 | 10 | import scala.concurrent.Future 11 | 12 | class FirstTest extends WordSpec with MustMatchers with OptionValues with ScalaFutures with MockFactory { 13 | 14 | override implicit def patienceConfig = PatienceConfig(Span(60, Seconds), Span(50, Millis)) 15 | 16 | // "A Stack" should { 17 | // "pop values in last-in-first-out order" in { 18 | // val stack = mutable.Stack[Int]() 19 | // 20 | // stack.push(1) 21 | // stack.push(2) 22 | // stack.pop() mustBe 2 23 | // stack.pop() mustBe 1 24 | // } 25 | // 26 | // "throw NoSuchElementException if an empty stack is popped" in { 27 | // val emptyStack = mutable.Stack[Int]() 28 | // assertThrows[NoSuchElementException] { 29 | // emptyStack.pop() 30 | // } 31 | // } 32 | // 33 | // } 34 | 35 | // "option" should { 36 | // "value" in { 37 | // val v: Option[Int] = None 38 | // v.value mustBe 2 39 | //// v.get mustBe 1 40 | // } 41 | // 42 | // "other" in { 43 | // 3 mustBe 3 44 | // } 45 | // } 46 | 47 | // "future" should { 48 | // "await result === 3" in { 49 | // import scala.concurrent.ExecutionContext.Implicits.global 50 | // val f = Future{ 51 | // Thread.sleep(1000) 52 | // 3 53 | // } 54 | // val result = f.futureValue 55 | // result mustBe 3 56 | // } 57 | // } 58 | 59 | "scalamock" should { 60 | "function mock" in { 61 | val m = mockFunction[Int, String] 62 | m expects 42 returning "Forty two" 63 | m(42) mustBe "Forty two" 64 | } 65 | } 66 | 67 | } -------------------------------------------------------------------------------- /spark-startup/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### SBT template 3 | # Simple Build Tool 4 | # http://www.scala-sbt.org/release/docs/Getting-Started/Directories.html#configuring-version-control 5 | 6 | target/ 7 | lib_managed/ 8 | src_managed/ 9 | project/boot/ 10 | .history 11 | .cache 12 | ### Scala template 13 | *.class 14 | *.log 15 | 16 | # sbt specific 17 | .lib/ 18 | dist/* 19 | project/plugins/project/ 20 | 21 | # Scala-IDE specific 22 | .scala_dependencies 23 | .worksheet 24 | ### JetBrains template 25 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio 26 | 27 | *.iml 28 | 29 | ## Directory-based project format: 30 | .idea/ 31 | # if you remove the above rule, at least ignore the following: 32 | 33 | # User-specific stuff: 34 | # .idea/workspace.xml 35 | # .idea/tasks.xml 36 | # .idea/dictionaries 37 | 38 | # Sensitive or high-churn files: 39 | # .idea/dataSources.ids 40 | # .idea/dataSources.xml 41 | # .idea/sqlDataSources.xml 42 | # .idea/dynamic.xml 43 | # .idea/uiDesigner.xml 44 | 45 | # Gradle: 46 | # .idea/gradle.xml 47 | # .idea/libraries 48 | 49 | # Mongo Explorer plugin: 50 | # .idea/mongoSettings.xml 51 | 52 | ## File-based project format: 53 | *.ipr 54 | *.iws 55 | 56 | ## Plugin-specific files: 57 | 58 | # IntelliJ 59 | /out/ 60 | 61 | # mpeltonen/sbt-idea plugin 62 | .idea_modules/ 63 | 64 | # JIRA plugin 65 | atlassian-ide-plugin.xml 66 | 67 | # Crashlytics plugin (for Android Studio and IntelliJ) 68 | com_crashlytics_export_strings.xml 69 | crashlytics.properties 70 | crashlytics-build.properties 71 | 72 | -------------------------------------------------------------------------------- /spark-startup/build.sbt: -------------------------------------------------------------------------------- 1 | scalaVersion := "2.11.12" 2 | 3 | scalacOptions ++= Seq( 4 | "-encoding", "utf8", 5 | "-unchecked", 6 | "-feature", 7 | "-deprecation" 8 | ) 9 | 10 | assemblyJarName in assembly := "spark-startup.jar" 11 | 12 | assemblyOption in assembly := (assemblyOption in assembly).value.copy(includeScala = false) 13 | 14 | test in assembly := {} 15 | 16 | 17 | val verSpark = "2.3.1" 18 | val verHadoop = "2.7.5" 19 | 20 | libraryDependencies ++= Seq( 21 | "org.apache.spark" %% "spark-core" % "1.5.2" % "provided,test", 22 | "org.apache.spark" %% "spark-sql" % "1.5.2" % "provided,test", 23 | "org.scalatest" %% "scalatest" % "3.0.5" % "test" 24 | ) 25 | 26 | -------------------------------------------------------------------------------- /spark-startup/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.11 2 | -------------------------------------------------------------------------------- /spark-startup/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.2") 2 | -------------------------------------------------------------------------------- /spark-startup/sbt: -------------------------------------------------------------------------------- 1 | SCRIPT_DIR=`dirname $0` 2 | java -Dfile.encoding=UTF-8 -noverify -Xms512M -Xmx1024M -Xss1M -XX:+CMSClassUnloadingEnabled -jar "$SCRIPT_DIR/../_lib/sbt-launch.jar" $@ 3 | -------------------------------------------------------------------------------- /spark-startup/scripts/SparkApp.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | $SPARK_HOME/bin/spark-submit \ 4 | --class sample.SparkApp \ 5 | --master spark://sc-data-server-1:7077 \ 6 | --name "sample.SparkApp" \ 7 | --executor-memory 10G \ 8 | --driver-memory 2G \ 9 | --total-executor-cores 4 \ 10 | --conf spark.serializer=org.apache.spark.serializer.KryoSerializer \ 11 | ../target/scala-2.11/spark-startup.jar & 12 | 13 | -------------------------------------------------------------------------------- /spark-startup/src/main/scala/example/SparkApp.scala: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import org.apache.spark.sql.SQLContext 4 | import org.apache.spark.{SparkConf, SparkContext} 5 | 6 | /** 7 | * Created by Yang Jing (yangbajing@gmail.com) on 2016-03-12. 8 | */ 9 | class SparkApp(sc: SparkContext) extends Serializable { 10 | val sqlContext = new SQLContext(sc) 11 | 12 | def run() = { 13 | val jsonStrings = Seq( 14 | """{"name":"杨景","age":31}""", 15 | """{"name":"羊八井","age"31}""", 16 | """{"name":"yangbajing","age":31}""" 17 | ) 18 | val rdd = sc.parallelize(jsonStrings) 19 | val sql = sqlContext.read.json(rdd) 20 | sql.show() 21 | } 22 | 23 | } 24 | 25 | object SparkApp { 26 | 27 | def main(args: Array[String]): Unit = { 28 | val conf = new SparkConf() 29 | val sc = new SparkContext(conf) 30 | 31 | val app = new SparkApp(sc) 32 | app.run() 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /spark-startup/src/test/scala/example/SparkAppTest.scala: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import org.apache.spark.{SparkConf, SparkContext} 4 | import org.scalatest.WordSpec 5 | 6 | /** 7 | * Created by Yang Jing (yangbajing@gmail.com) on 2016-03-12. 8 | */ 9 | class SparkAppTest extends WordSpec { 10 | 11 | "SparkAppTest" should { 12 | 13 | "run" in { 14 | val conf = new SparkConf() 15 | .setAppName("SparkAppTest") 16 | .setMaster("local[*]") 17 | val sc = new SparkContext(conf) 18 | 19 | val app = new SparkApp(sc) 20 | app.run() 21 | } 22 | 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /springscala/.gitignore: -------------------------------------------------------------------------------- 1 | .gradle/ 2 | build/ 3 | classes/ 4 | -------------------------------------------------------------------------------- /springscala/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | springBootVersion = '1.4.0.RELEASE' 4 | scalaVersion = '2.11.12' 5 | scalaLibVersion = '2.11' 6 | } 7 | repositories { 8 | mavenCentral() 9 | jcenter() 10 | } 11 | dependencies { 12 | classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 13 | } 14 | } 15 | 16 | task wrapper(type: Wrapper) { 17 | description = 'Generates gradlew[.bat] scripts' 18 | gradleVersion = '2.14.1' 19 | } 20 | 21 | subprojects { 22 | apply plugin: 'java' 23 | apply plugin: 'scala' 24 | //apply plugin: 'eclipse' 25 | apply plugin: 'spring-boot' 26 | 27 | repositories { 28 | mavenCentral() 29 | jcenter() 30 | } 31 | 32 | group = 'me.yangbajing.demospringscala' 33 | version = '1.0' 34 | sourceCompatibility = 1.8 35 | targetCompatibility = 1.8 36 | 37 | compileJava { 38 | options.encoding = 'UTF-8' 39 | options.compilerArgs << "-Xlint:deprecation" << "-Xlint:-options" 40 | } 41 | 42 | dependencies { 43 | compile('org.springframework.boot:spring-boot-starter-actuator') 44 | //compile('org.springframework.boot:spring-boot-starter-data-elasticsearch') 45 | //compile('org.springframework.boot:spring-boot-starter-data-jpa') 46 | //compile('org.springframework.boot:spring-boot-starter-data-redis') 47 | compile('org.springframework.boot:spring-boot-devtools') 48 | //compile('org.springframework.boot:spring-boot-starter-jooq') 49 | compile('org.springframework.boot:spring-boot-starter-mail') 50 | compile('org.springframework.boot:spring-boot-starter-web') 51 | compile("org.scala-lang:scala-library:$scalaVersion") 52 | compile("org.scala-lang:scala-reflect:$scalaVersion") 53 | compile("com.fasterxml.jackson.module:jackson-module-scala_$scalaLibVersion:2.8.0.rc2") 54 | testCompile('org.springframework.boot:spring-boot-starter-test') 55 | } 56 | 57 | sourceSets { 58 | main { 59 | scala { 60 | srcDirs = ['src/main/scala', 'src/main/java'] 61 | } 62 | java { 63 | srcDirs = [] 64 | } 65 | } 66 | test { 67 | scala { 68 | srcDirs = ['src/main/scala', 'src/main/java'] 69 | } 70 | java { 71 | srcDirs = [] 72 | } 73 | } 74 | } 75 | 76 | //Java compiler settings 77 | tasks.withType(JavaCompile) { 78 | //ignore conjunction warning 79 | options.compilerArgs << '-Xlint:-options' 80 | } 81 | 82 | tasks.withType(ScalaCompile) { 83 | scalaCompileOptions.additionalParameters = ["-feature", "-language:implicitConversions", "-language:reflectiveCalls"] 84 | scalaCompileOptions.useAnt = false 85 | scalaCompileOptions.with { 86 | force = true 87 | } 88 | } 89 | 90 | jar { 91 | manifest.attributes provider: 'gradle' 92 | } 93 | 94 | //eclipse { 95 | // classpath { 96 | // containers.remove('org.eclipse.jdt.launching.JRE_CONTAINER') 97 | // containers 'org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8' 98 | // } 99 | //} 100 | 101 | } 102 | 103 | -------------------------------------------------------------------------------- /springscala/common/build.gradle: -------------------------------------------------------------------------------- 1 | 2 | description = 'springscala-common' 3 | 4 | -------------------------------------------------------------------------------- /springscala/common/src/main/java/me/yangbajing/demo/common/Constants.java: -------------------------------------------------------------------------------- 1 | package me.yangbajing.demo.common; 2 | 3 | /** 4 | * Created by yangbajing on 16-8-24. 5 | */ 6 | public class Constants { 7 | public static final String CHARSET = "UTF-8"; 8 | } 9 | -------------------------------------------------------------------------------- /springscala/common/src/main/scala/me/yangbajing/demo/common/Utils.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.demo.common 2 | 3 | import scala.util.Random 4 | 5 | /** 6 | * Created by yangbajing on 16-8-24. 7 | */ 8 | object Utils { 9 | 10 | def randomString(length: Int): String = { 11 | (0 until length).map(_ => Random.nextPrintableChar()).mkString 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /springscala/settings.gradle: -------------------------------------------------------------------------------- 1 | include "web", "common" 2 | -------------------------------------------------------------------------------- /springscala/web/build.gradle: -------------------------------------------------------------------------------- 1 | description = 'springscala-web' 2 | 3 | jar { 4 | baseName = 'springscala-web' 5 | } 6 | 7 | dependencies { 8 | compile project(':common') 9 | } 10 | 11 | -------------------------------------------------------------------------------- /springscala/web/src/main/java/me/yangbajing/demo/SpringscalaApplication.java: -------------------------------------------------------------------------------- 1 | package me.yangbajing.demo; 2 | 3 | import com.fasterxml.jackson.databind.Module; 4 | import com.fasterxml.jackson.module.scala.DefaultScalaModule; 5 | import me.yangbajing.demo.config.SpringscalaConfig; 6 | import org.springframework.boot.SpringApplication; 7 | import org.springframework.boot.autoconfigure.SpringBootApplication; 8 | import org.springframework.context.annotation.Bean; 9 | 10 | @SpringBootApplication 11 | public class SpringscalaApplication { 12 | 13 | public static void main(String[] args) { 14 | SpringApplication.run(SpringscalaApplication.class, args); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /springscala/web/src/main/java/me/yangbajing/demo/config/SpringscalaConfig.java: -------------------------------------------------------------------------------- 1 | package me.yangbajing.demo.config; 2 | 3 | import com.fasterxml.jackson.databind.Module; 4 | import com.fasterxml.jackson.module.scala.DefaultScalaModule; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | /** 9 | * Created by yangbajing on 16-8-24. 10 | */ 11 | @Configuration 12 | public class SpringscalaConfig { 13 | 14 | @Bean 15 | public Module jacksonModuleScala() { 16 | return new DefaultScalaModule(); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /springscala/web/src/main/java/me/yangbajing/demo/web/controllers/WebController.java: -------------------------------------------------------------------------------- 1 | package me.yangbajing.demo.web.controllers; 2 | 3 | import me.yangbajing.demo.data.domain.Message; 4 | import org.springframework.web.bind.annotation.RequestMapping; 5 | import org.springframework.web.bind.annotation.RequestMethod; 6 | import org.springframework.web.bind.annotation.RestController; 7 | 8 | /** 9 | * Java WebController 10 | * Created by yangbajing on 16-8-24. 11 | */ 12 | @RestController 13 | @RequestMapping("/web") 14 | public class WebController { 15 | 16 | @RequestMapping(path = "message", method = RequestMethod.GET) 17 | public Message message() { 18 | return new Message("Yang Jing", 30); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /springscala/web/src/main/resources/application.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yangbajing/scala-applications/a41bd2b1e5eef086cf54fe58630bb47d7c16df39/springscala/web/src/main/resources/application.properties -------------------------------------------------------------------------------- /springscala/web/src/main/scala/me/yangbajing/demo/data/domain/Message.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.demo.data.domain 2 | 3 | import me.yangbajing.demo.common.Utils 4 | 5 | import scala.beans.BeanProperty 6 | 7 | case class Message(name: String, age: Int) { 8 | @BeanProperty 9 | val sign: String = Utils.randomString(16) 10 | } 11 | 12 | -------------------------------------------------------------------------------- /springscala/web/src/main/scala/me/yangbajing/demo/web/controllers/ApiController.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.demo.web.controllers 2 | 3 | import me.yangbajing.demo.data.domain.Message 4 | import org.springframework.web.bind.annotation.{RequestMapping, RequestMethod, RestController} 5 | 6 | @RestController 7 | @RequestMapping(Array("/api")) 8 | class ApiController { 9 | 10 | @RequestMapping(path = Array("message"), method = Array(RequestMethod.GET)) 11 | def message(): Message = { 12 | Message("Yang Jing", 30) 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /springscala/web/src/test/java/me/yangbajing/demo/SpringscalaApplicationTests.java: -------------------------------------------------------------------------------- 1 | package me.yangbajing.demo; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | public class SpringscalaApplicationTests { 11 | 12 | @Test 13 | public void contextLoads() { 14 | } 15 | 16 | } 17 | --------------------------------------------------------------------------------