1.项目需求
通讯协议: HTTP(HyperText Transfer Protocol)
数据格式: post请求传输json数据并返还json格式结果
服务性能:高可用,高并发
图像格式:Base64编码
开发方式:敏捷开发,成熟框架
开发语言:Scala
2.技术知识:
Scala: http://docs.scala-lang.org/
Playframework: https://www.playframework.com/documentation/2.6.x/ScalaHome
Akka: https://akka.io/
3.开发环境:
Jdk安装(1.8.0版本及以上)
Scala安装(2.11.8版本及以上)
Sbt安装 https://www.scala-sbt.org/download.html
4.编译方法
下载源码地址: 待传
执行如下命令:
sbt clean
sbt compile
sbt dist
5.关键代码:
编译配置build.sbt
name := """image-project""" version := "1.0" lazy val root = (project in file(".")).enablePlugins(PlayScala) PlayKeys.devSettings := Seq("play.server.http.port" -> "8080") scalaVersion := "2.11.8" libraryDependencies += guice libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "3.0.0" % Test libraryDependencies += "com.netaporter" %% "scala-uri" % "0.4.16" libraryDependencies ++= Seq( "com.alibaba" % "fastjson" % "1.2.34", "commons-io" % "commons-io" % "2.5" ) //addSbtPlugin("tv.cntt" % "xitrum-package" % "3.28.1") //XitrumPackage.copy("dirToCopy", "fileToCopy")
系统配置application.conf
akka { # "akka.log-config-on-start" is extraordinarly useful because it log the complete # configuration at INFO level, including defaults and overrides, so it s worth # putting at the very top. # # Put the following in your conf/logback.xml file: # # <logger name="akka.actor" level="INFO" /> # # And then uncomment this line to debug the configuration. # #log-config-on-start = true } ## Secret key # http://www.playframework.com/documentation/latest/ApplicationSecret # ~~~~~ # The secret key is used to sign Play's session cookie. # This must be changed for production, but we don't recommend you change it in this file. #play.http.secret.key = "changeme" play.http.secret.key = "hongyanwang" ## Modules # https://www.playframework.com/documentation/latest/Modules # ~~~~~ # Control which modules are loaded when Play starts. Note that modules are # the replacement for "GlobalSettings", which are deprecated in 2.5.x. # Please see https://www.playframework.com/documentation/latest/GlobalSettings # for more information. # # You can also extend Play functionality by using one of the publically available # Play modules: https://playframework.com/documentation/latest/ModuleDirectory play.modules { # By default, Play will load any class called Module that is defined # in the root package (the "app" directory), or you can define them # explicitly below. # If there are any built-in modules that you want to disable, you can list them here. #enabled += my.application.Module #enabled += Module #enabled += "tasks.TasksModule" # If there are any built-in modules that you want to disable, you can list them here. #disabled += "" } play.i18n { # The application languages langs = [ "en" ] # Whether the language cookie should be secure or not #langCookieSecure = true # Whether the HTTP only attribute of the cookie should be set to true #langCookieHttpOnly = true } ## Play HTTP settings # ~~~~~ play.http { ## Router # https://www.playframework.com/documentation/latest/JavaRouting # https://www.playframework.com/documentation/latest/ScalaRouting # ~~~~~ # Define the Router object to use for this application. # This router will be looked up first when the application is starting up, # so make sure this is the entry point. # Furthermore, it's assumed your route file is named properly. # So for an application router like `my.application.Router`, # you may need to define a router file `conf/my.application.routes`. # Default to Routes in the root package (aka "apps" folder) (and conf/routes) #router = my.application.Router ## Action Creator # https://www.playframework.com/documentation/latest/JavaActionCreator # ~~~~~ #actionCreator = null ## ErrorHandler # https://www.playframework.com/documentation/latest/JavaRouting # https://www.playframework.com/documentation/latest/ScalaRouting # ~~~~~ # If null, will attempt to load a class called ErrorHandler in the root package, #errorHandler = null ## Session & Flash # https://www.playframework.com/documentation/latest/JavaSessionFlash # https://www.playframework.com/documentation/latest/ScalaSessionFlash # ~~~~~ session { # Sets the cookie to be sent only over HTTPS. #secure = true # Sets the cookie to be accessed only by the server. #httpOnly = true # Sets the max-age field of the cookie to 5 minutes. # NOTE: this only sets when the browser will discard the cookie. Play will consider any # cookie value with a valid signature to be a valid session forever. To implement a server side session timeout, # you need to put a timestamp in the session and check it at regular intervals to possibly expire it. #maxAge = 300 # Sets the domain on the session cookie. #domain = "example.com" } flash { # Sets the cookie to be sent only over HTTPS. #secure = true # Sets the cookie to be accessed only by the server. #httpOnly = true } } ## Netty Provider # https://www.playframework.com/documentation/latest/SettingsNetty # ~~~~~ play.server.netty { # Whether the Netty wire should be logged #log.wire = true # If you run Play on Linux, you can use Netty's native socket transport # for higher performance with less garbage. #transport = "native" } ## Cache # https://www.playframework.com/documentation/latest/JavaCache # https://www.playframework.com/documentation/latest/ScalaCache # ~~~~~ # Play comes with an integrated cache API that can reduce the operational # overhead of repeated requests. You must enable this by adding to build.sbt: # # libraryDependencies += cache # play.cache { # If you want to bind several caches, you can bind the individually #bindCaches = ["db-cache", "user-cache", "session-cache"] } ## Filter Configuration # https://www.playframework.com/documentation/latest/Filters # ~~~~~ # There are a number of built-in filters that can be enabled and configured # to give Play greater security. # play.filters { # Enabled filters are run automatically against Play. # CSRFFilter, AllowedHostFilters, and SecurityHeadersFilters are enabled by default. enabled += filters.ElapseFilter # Disabled filters remove elements from the enabled list. #disabled += filters.ExampleFilters ## CORS filter configuration # https://www.playframework.com/documentation/latest/CorsFilter # ~~~~~ # CORS is a protocol that allows web applications to make requests from the browser # across different domains. # NOTE: You MUST apply the CORS configuration before the CSRF filter, as CSRF has # dependencies on CORS settings. cors { # Filter paths by a whitelist of path prefixes pathPrefixes = ["/", "/image/save.php", "/image/save"] # The allowed origins. If null, all origins are allowed. #allowedOrigins = ["http://www.example.com"] # The allowed HTTP methods. If null, all methods are allowed allowedHttpMethods = ["GET", "POST"] } hosts { # Allow requests to example.com, its subdomains, and localhost:9000. #allowed = [".example.com", "localhost:9000"] allowed = ["."] } } ## Evolutions # https://www.playframework.com/documentation/latest/Evolutions # ~~~~~ # Evolutions allows database scripts to be automatically run on startup in dev mode # for database migrations. You must enable this by adding to build.sbt: # # libraryDependencies += evolutions # play.evolutions { # You can disable evolutions for a specific datasource if necessary #db.default.enabled = false } ## Database Connection Pool # https://www.playframework.com/documentation/latest/SettingsJDBC # ~~~~~ # Play doesn't require a JDBC database to run, but you can easily enable one. # # libraryDependencies += jdbc # include "image.conf"
应用配置image.conf
#支持最大请求数据包 play.http.parser.maxMemoryBuffer = 15M play.http.parser.maxDishBuffer = 15M #端口 play.server.http.port = 8080 #图片存储路径 image.path = "d:\\" #调度任务 task.enable = true task.interval = 10
路由routers
# Routes # This file defines all application routes (Higher priority routes first) # ~~~~ # Map static resources from the /public folder to the /assets URL path GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset) # An example controller showing a sample home page GET / controllers.HomeController.index # An example controller showing how to write asynchronous code POST /image/save.php controllers.ImageController.post
模块Module
/** * 模块实例化 * author hongyan.wang * Email why000007@163.com * Date 03/21/2017 */ class Module extends AbstractModule with AkkaGuiceSupport { override def configure() = { bind(classOf[ImageService]).to(classOf[ImageServiceImpl]).asEagerSingleton() bind(classOf[ImageTask]).asEagerSingleton() bindActor[ImageActor]("imageActor") } }
返回结果Response
import com.alibaba.fastjson.annotation.JSONField /** * 返回结果 * author hongyan.wang * Email why000007@163.com * Date 03/21/2017 */ sealed case class Response(@beans.BeanProperty @JSONField(ordinal = 1) val result: Int = 0) { def this(result: Int, imagePath: String) { this(result); this.imagePath = imagePath} @JSONField(ordinal = 2) @beans.BeanProperty var imagePath: String = "" @JSONField(ordinal = 3) @beans.BeanProperty var message: String = if( result == 1 ) "处理成功" else "处理失败" /** * * 省略业务字段 */ }
控制层ImageController
/** * 数据接收 * author hongyan.wang * Email why000007@163.com * Date 03/19/2017 */ @Singleton class ImageController @Inject()(cc: ControllerComponents, actorSystem: ActorSystem, imageService: ImageService)(implicit exec: ExecutionContext) extends AbstractController(cc) { implicit def Response2String(response: Response): String = com.alibaba.fastjson.JSON.toJSONString(response, SerializerFeature.PrettyFormat) private[ImageController] val logger = Logger(classOf[ImageController]) def post() = Action { request => /** * 省略数据校验 */ //val jsonBody: Option[JsValue] = request.body.asJson val jsonObj: JSONObject = com.alibaba.fastjson.JSON.parseObject(request.body.asText.getOrElse("{}")) val response: String = imageService.call(jsonObj.getString("image_cont"), jsonObj.getString("image_ext")) Result( header = ResponseHeader(200, Map.empty), body = HttpEntity.Strict(ByteString(response), Some("application/json;charset=utf-8")) ) } }过滤器ElapseFilter
/** * 过滤器 * author hongyan.wang * Email why000007@163.com * Date 03/17/2017 */ @Singleton class ElapseFilter @Inject()(implicit ec: ExecutionContext) extends EssentialFilter { private[filters] val logger = Logger(getClass) override def apply(next: EssentialAction) = EssentialAction { request => val start = Instant.ofEpochMilli(System.currentTimeMillis()) logger.info("http[method=%s, source=%s, url=%s?%s]".format(request.method, request.remoteAddress, request.path, request.rawQueryString)) next(request).map { result => val duration = Duration.between(start, Instant.now()) logger.info("http[elapse=%s]".format(duration.toMillis)) result.withHeaders("Elapse-Time" -> duration.toMillis.toString) } } }
业务层Services
/** * 业务处理 * author hongyan.wang * Email why000007@163.com * Date 03/21/2017 */ package object Services { implicit def RichFileUtil(fileUtil: FileUtils) = new RichFileUtil(fileUtil) protected[Services] val fileUtils = new FileUtils lazy private[Services] val config = Configuration(ConfigFactory.load()) val path = config.getString("image.path").getOrElse(".") trait ImageService extends { /***************************/ } with utils.Logger { def call(img_cont:String, img_ext:String): Response } class ImageServiceImpl extends ImageService { @throws(classOf[Exception]) override def call(img_cont: String, img_ext: String): Response = { /** * 此处省略关键业务处理部分 */ val fname = s"${path}${File.separator}${DateUtil.fTime()}-${new StringFormat(Random.nextInt(99)).formatted("%4s").replaceAll(" ","0")}.${img_ext}" fileUtils.save64(fname, img_cont) new Response(1, fname) } } }
调度任务Task
/** * 消息接收 * author hongyan.wang * Email why000007@163.com * Date 03/21/2017 */ class ImageActor @Inject()(config: Configuration) extends Actor with utils.Logger { self ! "Message From Self" override def receive: Receive = { case Some("delete") => { logger.info("收到一条删除任务") /** * 此处任务处理部分 */ } case str: String => logger.info(str) case _ => } }
/** * 调度任务 * author hongyan.wang * Email why000007@163.com * Date 03/21/2017 */ class ImageTask @Inject()(config: Configuration, actorSystem: ActorSystem, @Named("imageActor") actor: ActorRef)(implicit executionContext: ExecutionContext) { private val logger = Logger(getClass) if(config.getBoolean("task.enable").getOrElse(false)) { val interval = config.getInt("task.interval").getOrElse(5) actorSystem.scheduler.schedule ( initialDelay = Duration(60, TimeUnit.SECONDS), //interval = Duration(interval, TimeUnit.HOURS), interval = Duration(interval, TimeUnit.SECONDS), receiver = actor, message = Some("delete") ) logger.info(s"调度任务启动") }else { logger.warn("调度任务取消") } }
工具类
/** * 文件工具 * author hongyan.wang * Email why000007@163.com * Date 04/06/2017 */ class RichFileUtil(fileUtils: FileUtils) { def exists(fname: String): Boolean = new File(fname).exists def mkdirs(fname : String): Boolean = new File(fname).mkdirs() def length(fname: String): Long = new File(fname).length() def save64(fname: String, text64:String): Boolean = { try { FileUtils.writeByteArrayToFile(new File(fname),StringUtil.decode64(text64)) true } catch { case ex:Exception => ex.printStackTrace(); false } } def read64(fname: String): Option[String] = { if(exists(fname)) { Some(StringUtil.encode64(FileUtils.readFileToByteArray(new File(fname)))) } else { None } } }
/** * 字符串工具 * author hongyan.wang * Email why000007@163.com * Date 03/23/2017 */ object StringUtil { def encode64(content:String, charset: String="UTF-8"): String = Base64.getEncoder().encodeToString(content.getBytes(charset)) def decode64(content:String): Array[Byte] = Base64.getDecoder().decode(content) def encode64(bytes: Array[Byte]): String = Base64.getEncoder().encodeToString(bytes) def toString(entity: AnyRef): String = JSON.toJSONString(entity,SerializerFeature.NotWriteRootClassName) }
/** * 资源关闭 * author hongyan.wang * Email why000007@163.com * Date 03/27/2017 */ object Using { def close[A <: { def close(): Unit }, B](resource: A)(f: A => B): B = try { f(resource) } finally { if (resource != null) { resource.close() } } }
编译结果
sbt distJava HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=256m; support was removed in 8.0
[warn] Executing in batch mode.
[warn] For better performance, hit [ENTER] to switch to interactive mode, or
[warn] consider launching sbt without any commands, or explicitly passing 'shell'
[info] Loading project definition from E:\worspaceS\image_project\project
[info] Set current project to image-project (in build file:/E:/worspaceS/image_project/)
[info] Packaging E:\worspaceS\image_project\target\scala-2.11\image-project_2.11-1.0-sources.jar ...
[info] Updating {file:/E:/worspaceS/image_project/}root...
[info] Done packaging.
[info]
[info] Your package is ready in E:\worspaceS\image_project\target\universal\image-project-1.0.zip (解压该文件,运行/bin/image-project可执行文件)
[info]
[success] Total time: 12 s, completed 2018-4-25 17:55:38
运行与测试
1.解压image_project\target\universal\image-project-1.0.zip 压缩包,运行bin/image-project可执行文件,启动服务端程序
2.客户端调用
>>>> HttpPost URL: http://127.0.0.1:8080/image/save.php
>>>> HttpResponse status[200:OK], cost: 1790 ms.
{
"result":1,
"imagePath":"d:\\\\2018-0425-1804-0002-0790-0070.jpg",
"message":"处理成功"
}
==============================快乐编程=========================