├── samples ├── java │ ├── public │ │ ├── stylesheets │ │ │ └── main.css │ │ ├── javascripts │ │ │ └── hello.js │ │ └── images │ │ │ └── favicon.png │ ├── project │ │ ├── build.properties │ │ └── plugins.sbt │ ├── app │ │ ├── views │ │ │ ├── index.scala.html │ │ │ └── main.scala.html │ │ └── controllers │ │ │ └── Application.java │ ├── .gitignore │ ├── build.sbt │ ├── conf │ │ ├── routes │ │ ├── logback.xml │ │ └── application.conf │ ├── LICENSE │ └── test │ │ ├── IntegrationTest.java │ │ └── ApplicationTest.java └── scala │ ├── public │ ├── stylesheets │ │ └── main.css │ ├── javascripts │ │ └── hello.js │ └── images │ │ └── favicon.png │ ├── project │ ├── build.properties │ └── plugins.sbt │ ├── .gitignore │ ├── app │ ├── views │ │ ├── index.scala.html │ │ └── main.scala.html │ └── controllers │ │ └── Application.scala │ ├── build.sbt │ ├── LICENSE │ ├── test │ ├── IntegrationSpec.scala │ └── ApplicationSpec.scala │ └── conf │ ├── logback.xml │ ├── routes │ └── application.conf ├── project ├── build.properties └── plugins.sbt ├── sonatype.sbt ├── .gitignore ├── publish.sh ├── LICENSE ├── .travis.yml ├── plugin └── src │ ├── main │ ├── resources │ │ └── reference.conf │ └── com │ │ └── github │ │ └── mumoshu │ │ └── play2 │ │ └── memcached │ │ ├── Slf4JLogger.scala │ │ ├── api │ │ └── MemcachedComponents.scala │ │ ├── CustomSerializing.scala │ │ ├── MemcachedCacheApiProvider.scala │ │ ├── MemcachedComponents.java │ │ ├── MemcachedModule.scala │ │ ├── MemcachedClientProvider.scala │ │ └── MemcachedCacheApi.scala │ └── test │ └── MemcachedSpec.scala └── README.md /samples/java/public/stylesheets/main.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /samples/scala/public/stylesheets/main.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.9.7 2 | -------------------------------------------------------------------------------- /samples/java/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.9.7 2 | -------------------------------------------------------------------------------- /samples/scala/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.9.7 2 | -------------------------------------------------------------------------------- /sonatype.sbt: -------------------------------------------------------------------------------- 1 | import xerial.sbt.Sonatype._ 2 | 3 | sonatypeProfileName := "com.github.mumoshu" 4 | -------------------------------------------------------------------------------- /samples/java/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | // The Play plugin 2 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.9.0") 3 | 4 | -------------------------------------------------------------------------------- /samples/scala/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | // The Play plugin 2 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.9.0") 3 | 4 | -------------------------------------------------------------------------------- /samples/java/public/javascripts/hello.js: -------------------------------------------------------------------------------- 1 | if (window.console) { 2 | console.log("Welcome to your Play application's JavaScript!"); 3 | } -------------------------------------------------------------------------------- /samples/scala/public/javascripts/hello.js: -------------------------------------------------------------------------------- 1 | if (window.console) { 2 | console.log("Welcome to your Play application's JavaScript!"); 3 | } -------------------------------------------------------------------------------- /samples/java/public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playframework/play2-memcached/main/samples/java/public/images/favicon.png -------------------------------------------------------------------------------- /samples/scala/public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playframework/play2-memcached/main/samples/scala/public/images/favicon.png -------------------------------------------------------------------------------- /samples/java/app/views/index.scala.html: -------------------------------------------------------------------------------- 1 | @(message: String) 2 | 3 | @main("Welcome to Play") { 4 | 5 | Your new application is ready. 6 | 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | project/project 3 | project/target 4 | target 5 | tmp 6 | .history 7 | dist 8 | *~ 9 | .idea/ 10 | .settings/ 11 | .bsp/ 12 | -------------------------------------------------------------------------------- /samples/java/.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | target 3 | /.idea 4 | /.idea_modules 5 | /.classpath 6 | /.project 7 | /.settings 8 | /RUNNING_PID 9 | .bsp/ 10 | -------------------------------------------------------------------------------- /samples/scala/.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | target 3 | /.idea 4 | /.idea_modules 5 | /.classpath 6 | /.project 7 | /.settings 8 | /RUNNING_PID 9 | .bsp/ 10 | -------------------------------------------------------------------------------- /samples/scala/app/views/index.scala.html: -------------------------------------------------------------------------------- 1 | @(message: String) 2 | 3 | @main("Welcome to Play") { 4 | 5 | Your new application is ready. 6 | 7 | } 8 | -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PLAY_VERSION=2.8.0 sbt ++2.12.10 publishSigned sonatypeRelease 4 | PLAY_VERSION=2.8.0 sbt ++2.13.1 publishSigned sonatypeRelease 5 | -------------------------------------------------------------------------------- /samples/java/build.sbt: -------------------------------------------------------------------------------- 1 | libraryDependencies ++= Seq( 2 | javaJdbc, 3 | cacheApi, 4 | javaWs, 5 | "com.github.mumoshu" %% "play2-memcached-play29" % "0.12.0-SNAPSHOT" 6 | ) 7 | -------------------------------------------------------------------------------- /samples/scala/build.sbt: -------------------------------------------------------------------------------- 1 | libraryDependencies ++= Seq( 2 | jdbc, 3 | cacheApi, 4 | ws, 5 | "com.github.mumoshu" %% "play2-memcached-play29" % "0.12.0-SNAPSHOT", 6 | specs2 % Test 7 | ) 8 | 9 | resolvers += "scalaz-bintray" at "https://dl.bintray.com/scalaz/releases" 10 | -------------------------------------------------------------------------------- /samples/java/app/controllers/Application.java: -------------------------------------------------------------------------------- 1 | package controllers; 2 | 3 | import play.*; 4 | import play.mvc.*; 5 | 6 | import views.html.*; 7 | 8 | public class Application extends Controller { 9 | 10 | public Result index() { 11 | return ok(index.render("Your new application is ready.")); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | ivyLoggingLevel := UpdateLogging.Full 2 | 3 | val playVersion = scala.util.Properties.envOrElse("PLAY_VERSION", "2.9.0") 4 | 5 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % playVersion) 6 | 7 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.10.0") 8 | 9 | addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.2.1") 10 | -------------------------------------------------------------------------------- /samples/java/conf/routes: -------------------------------------------------------------------------------- 1 | # Routes 2 | # This file defines all application routes (Higher priority routes first) 3 | # ~~~~ 4 | 5 | # Home page 6 | GET / controllers.Application.index() 7 | 8 | # Map static resources from the /public folder to the /assets URL path 9 | GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset) 10 | -------------------------------------------------------------------------------- /samples/java/app/views/main.scala.html: -------------------------------------------------------------------------------- 1 | @(title: String)(content: Html) 2 | 3 | 4 | 5 | 6 | 7 | @title 8 | 9 | 10 | 11 | 12 | 13 | @content 14 | 15 | 16 | -------------------------------------------------------------------------------- /samples/scala/app/views/main.scala.html: -------------------------------------------------------------------------------- 1 | @(title: String)(content: Html) 2 | 3 | 4 | 5 | 6 | 7 | @title 8 | 9 | 10 | 11 | 12 | 13 | @content 14 | 15 | 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2012 KUOKA Yusuke 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /samples/java/LICENSE: -------------------------------------------------------------------------------- 1 | This software is licensed under the Apache 2 license, quoted below. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this project except in compliance with 4 | the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 5 | 6 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an 7 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific 8 | language governing permissions and limitations under the License. -------------------------------------------------------------------------------- /samples/scala/LICENSE: -------------------------------------------------------------------------------- 1 | This software is licensed under the Apache 2 license, quoted below. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this project except in compliance with 4 | the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 5 | 6 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an 7 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific 8 | language governing permissions and limitations under the License. -------------------------------------------------------------------------------- /samples/scala/test/IntegrationSpec.scala: -------------------------------------------------------------------------------- 1 | import org.specs2.mutable._ 2 | import org.specs2.runner._ 3 | import org.junit.runner._ 4 | 5 | import play.api.test._ 6 | import play.api.test.Helpers._ 7 | 8 | /** 9 | * add your integration spec here. 10 | * An integration test will fire up a whole play application in a real (or headless) browser 11 | */ 12 | @RunWith(classOf[JUnitRunner]) 13 | class IntegrationSpec extends Specification { 14 | 15 | "Application" should { 16 | 17 | "work from within a browser" in new WithBrowser { 18 | override def running() = { 19 | browser.goTo("http://localhost:" + port) 20 | 21 | browser.pageSource must contain("Your new application is ready.") 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /samples/java/test/IntegrationTest.java: -------------------------------------------------------------------------------- 1 | import org.junit.*; 2 | 3 | import play.mvc.*; 4 | import play.test.*; 5 | 6 | import static play.test.Helpers.*; 7 | import static org.junit.Assert.*; 8 | 9 | import static io.fluentlenium.core.filter.FilterConstructor.*; 10 | 11 | public class IntegrationTest { 12 | 13 | /** 14 | * add your integration test here 15 | * in this example we just check if the welcome page is being shown 16 | */ 17 | @Test 18 | public void test() { 19 | running(testServer(3333, fakeApplication(inMemoryDatabase())), HTMLUNIT, browser -> { 20 | browser.goTo("http://localhost:3333"); 21 | assertTrue(browser.pageSource().contains("Your new application is ready.")); 22 | }); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | scala: 3 | - 2.13.12 4 | - 3.3.1 5 | services: 6 | - memcache 7 | env: 8 | - PLAY_VERSION=2.9.0 TRAVIS_JDK=11 9 | # see https://github.com/travis-ci/travis-ci/issues/5227#issuecomment-165131913 10 | before_install: 11 | - curl -Ls https://git.io/jabba | bash && . ~/.jabba/jabba.sh 12 | - cat /etc/hosts 13 | - sudo hostname "$(hostname | cut -c1-63)" 14 | - sed -e "s/^\\(127\\.0\\.0\\.1.*\\)/\\1 $(hostname | cut -c1-63)/" /etc/hosts > /tmp/hosts 15 | - sudo mv /tmp/hosts /etc/hosts 16 | - cat /etc/hosts 17 | install: jabba install "adopt@~1.$TRAVIS_JDK.0-0" && jabba use "$_" && java -Xmx32m -version 18 | # see https://docs.travis-ci.com/user/languages/scala#Default-Test-Command about $TRAVIS_SCALA_VERSION 19 | script: 20 | - sbt ++$TRAVIS_SCALA_VERSION test 21 | -------------------------------------------------------------------------------- /samples/java/conf/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %coloredLevel - %logger - %message%n%xException 8 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /samples/scala/conf/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %coloredLevel - %logger - %message%n%xException 8 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /plugin/src/main/resources/reference.conf: -------------------------------------------------------------------------------- 1 | play { 2 | 3 | modules { 4 | enabled += "com.github.mumoshu.play2.memcached.MemcachedModule" 5 | } 6 | 7 | cache { 8 | # The caches to bind 9 | bindCaches = [] 10 | # The name of the default cache to use 11 | defaultCache = "play" 12 | # The dispatcher used for get, set, remove,... operations on the cache. By default Play's default dispatcher is used. 13 | dispatcher = null 14 | } 15 | 16 | } 17 | 18 | memcached { 19 | 20 | host = null 21 | #1.host = ... 22 | #2.host = ... 23 | 24 | user = null 25 | password = null 26 | 27 | hashkeys = "off" 28 | 29 | consistentHashing = false 30 | 31 | throwExceptionFromGetOnError = false 32 | 33 | max-timeout-exception-threshold = null 34 | 35 | } 36 | 37 | elasticache { 38 | 39 | config { 40 | 41 | endpoint = null 42 | 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /samples/scala/conf/routes: -------------------------------------------------------------------------------- 1 | # Routes 2 | # This file defines all application routes (Higher priority routes first) 3 | # ~~~~ 4 | 5 | # Home page 6 | GET / controllers.Application.index 7 | 8 | GET /cache/set controllers.Application.set 9 | GET /cache/get controllers.Application.get 10 | GET /session/set controllers.Application.setSessionCache 11 | GET /session/get controllers.Application.getSessionCache 12 | GET /cache/delete controllers.Application.delete 13 | GET /cacheInt controllers.Application.cacheInt 14 | GET /cacheString controllers.Application.cacheString 15 | GET /cacheBool controllers.Application.cacheBool 16 | 17 | # Map static resources from the /public folder to the /assets URL path 18 | GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset) 19 | -------------------------------------------------------------------------------- /plugin/src/main/com/github/mumoshu/play2/memcached/Slf4JLogger.scala: -------------------------------------------------------------------------------- 1 | package com.github.mumoshu.play2.memcached 2 | 3 | import net.spy.memcached.compat.log.{Level, AbstractLogger} 4 | import play.api.Logger 5 | 6 | class Slf4JLogger(name: String) extends AbstractLogger(name) { 7 | 8 | val logger = Logger("memcached") 9 | 10 | def isTraceEnabled = logger.isTraceEnabled 11 | 12 | def isDebugEnabled = logger.isDebugEnabled 13 | 14 | def isInfoEnabled = logger.isInfoEnabled 15 | 16 | def log(level: Level, msg: AnyRef, throwable: Throwable): Unit = { 17 | val message = msg.toString 18 | level match { 19 | case Level.TRACE => logger.trace(message, throwable) 20 | case Level.DEBUG => logger.debug(message, throwable) 21 | case Level.INFO => logger.info(message, throwable) 22 | case Level.WARN => logger.warn(message, throwable) 23 | case Level.ERROR => logger.error(message, throwable) 24 | case Level.FATAL => logger.error("[FATAL] " + message, throwable) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /plugin/src/main/com/github/mumoshu/play2/memcached/api/MemcachedComponents.scala: -------------------------------------------------------------------------------- 1 | package com.github.mumoshu.play2.memcached.api 2 | 3 | import play.api.Environment 4 | import play.api.cache.AsyncCacheApi 5 | import play.api.inject.ApplicationLifecycle 6 | 7 | import com.github.mumoshu.play2.memcached.{ MemcachedCacheApi, MemcachedClientProvider } 8 | import com.typesafe.config.Config 9 | 10 | import scala.concurrent.ExecutionContext 11 | 12 | /** 13 | * Memcached components for compile time injection 14 | */ 15 | trait MemcachedComponents { 16 | def config: Config 17 | def environment: Environment 18 | def applicationLifecycle: ApplicationLifecycle 19 | implicit def executionContext: ExecutionContext 20 | 21 | lazy val memcachedClientProvider: MemcachedClientProvider = new MemcachedClientProvider(config, applicationLifecycle) 22 | 23 | /** 24 | * Use this to create with the given name. 25 | */ 26 | def cacheApi(name: String, create: Boolean = true): AsyncCacheApi = { 27 | new MemcachedCacheApi(name, memcachedClientProvider.get, config, environment) 28 | } 29 | 30 | lazy val defaultCacheApi: AsyncCacheApi = cacheApi("play") 31 | } 32 | -------------------------------------------------------------------------------- /plugin/src/main/com/github/mumoshu/play2/memcached/CustomSerializing.scala: -------------------------------------------------------------------------------- 1 | package com.github.mumoshu.play2.memcached 2 | 3 | import java.io.{ObjectOutputStream, ByteArrayOutputStream, ObjectStreamClass} 4 | 5 | import net.spy.memcached.transcoders.SerializingTranscoder 6 | 7 | class CustomSerializing(private val classLoader: ClassLoader) extends SerializingTranscoder{ 8 | 9 | // You should not catch exceptions and return nulls here, 10 | // because you should cancel the future returned by asyncGet() on any exception. 11 | override protected def deserialize(data: Array[Byte]): java.lang.Object = { 12 | new java.io.ObjectInputStream(new java.io.ByteArrayInputStream(data)) { 13 | override protected def resolveClass(desc: ObjectStreamClass) = { 14 | Class.forName(desc.getName(), false, classLoader) 15 | } 16 | }.readObject() 17 | } 18 | 19 | // We don't catch exceptions here to make it corresponding to `deserialize`. 20 | override protected def serialize(obj: java.lang.Object) = { 21 | val bos: ByteArrayOutputStream = new ByteArrayOutputStream() 22 | // Allows serializing `null`. 23 | // See https://github.com/mumoshu/play2-memcached/issues/7 24 | new ObjectOutputStream(bos).writeObject(obj) 25 | bos.toByteArray() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /samples/java/test/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | import java.util.ArrayList; 2 | import java.util.HashMap; 3 | import java.util.List; 4 | import java.util.Map; 5 | 6 | import com.fasterxml.jackson.databind.JsonNode; 7 | import org.junit.*; 8 | 9 | import play.mvc.*; 10 | import play.test.*; 11 | import play.data.DynamicForm; 12 | import play.data.validation.ValidationError; 13 | import play.data.validation.Constraints.RequiredValidator; 14 | import play.i18n.Lang; 15 | import play.libs.F; 16 | import play.libs.F.*; 17 | import play.twirl.api.Content; 18 | 19 | import static play.test.Helpers.*; 20 | import static org.junit.Assert.*; 21 | 22 | 23 | /** 24 | * 25 | * Simple (JUnit) tests that can call all parts of a play app. 26 | * If you are interested in mocking a whole application, see the wiki for more details. 27 | * 28 | */ 29 | public class ApplicationTest { 30 | 31 | @Test 32 | public void simpleCheck() { 33 | int a = 1 + 1; 34 | assertEquals(2, a); 35 | } 36 | 37 | @Test 38 | public void renderTemplate() { 39 | Content html = views.html.index.render("Your new application is ready."); 40 | assertEquals("text/html", html.contentType()); 41 | assertTrue(html.body().contains("Your new application is ready.")); 42 | } 43 | 44 | 45 | } 46 | -------------------------------------------------------------------------------- /plugin/src/main/com/github/mumoshu/play2/memcached/MemcachedCacheApiProvider.scala: -------------------------------------------------------------------------------- 1 | package com.github.mumoshu.play2.memcached 2 | 3 | import akka.actor.ActorSystem 4 | 5 | import com.typesafe.config.Config 6 | import play.api.Environment 7 | import play.api.inject.{BindingKey, Injector} 8 | import play.api.cache.AsyncCacheApi 9 | 10 | import javax.inject.{Inject, Singleton, Provider} 11 | 12 | import net.spy.memcached.MemcachedClient 13 | 14 | import scala.concurrent.ExecutionContext 15 | 16 | @Singleton 17 | class MemcachedCacheApiProvider(namespace: String, client: BindingKey[MemcachedClient], config: Config, environment: Environment) extends Provider[AsyncCacheApi] { 18 | @Inject private var injector: Injector = _ 19 | @Inject private var actorSystem: ActorSystem = _ 20 | private lazy val ec: ExecutionContext = if(config.hasPath(MemcachedCacheApiProvider.PLAY_CACHE_DISPATCHER)) { 21 | actorSystem.dispatchers.lookup(config.getString(MemcachedCacheApiProvider.PLAY_CACHE_DISPATCHER)) 22 | } else { 23 | injector.instanceOf[ExecutionContext] 24 | } 25 | 26 | lazy val get: AsyncCacheApi = { 27 | new MemcachedCacheApi(namespace, injector.instanceOf(client), config, environment)(ec) 28 | } 29 | } 30 | 31 | object MemcachedCacheApiProvider { 32 | final val PLAY_CACHE_DISPATCHER = "play.cache.dispatcher" 33 | } 34 | -------------------------------------------------------------------------------- /samples/scala/app/controllers/Application.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import play.api._ 4 | import play.api.mvc._ 5 | import play.api.cache._ 6 | import javax.inject._ 7 | import scala.concurrent.duration._ 8 | 9 | class Application @Inject() (cache: SyncCacheApi, @NamedCache("session-cache") sessionCache: SyncCacheApi, val controllerComponents: ControllerComponents) extends BaseController { 10 | 11 | def index = Action { 12 | Ok(views.html.index("Your new application is ready.")) 13 | } 14 | 15 | def set = Action { 16 | cache.set("key", "cached value") 17 | Ok("Cached.") 18 | } 19 | 20 | def get = Action { 21 | Ok(cache.get[String]("key").toString) 22 | } 23 | 24 | def delete = Action { 25 | cache.set("key", "foooo", 0.seconds) 26 | Ok("Deleted.") 27 | } 28 | 29 | def cacheInt = Action { 30 | cache.set("key", 123) 31 | 32 | val a = cache.get[Int]("key").get 33 | val b = cache.get[java.lang.Integer]("key").get 34 | // 123 class java.lang.Integer 123 int 35 | val content: String = b.toString + b.getClass + " " + a + " " + a.getClass 36 | 37 | Ok(content) 38 | } 39 | 40 | def cacheString = Action { 41 | cache.set("key", "mikoto") 42 | 43 | // Some(mikoto) 44 | val content: String = cache.get[String]("key").toString 45 | 46 | Ok(content) 47 | } 48 | 49 | def cacheBool = Action { 50 | cache.set("key", true) 51 | 52 | val a = cache.get[Boolean]("key").get 53 | val b = cache.get[java.lang.Boolean]("key").get 54 | // true : class java.lang.Boolean, true : boolean 55 | val content: String = b.toString + " : " + b.getClass + ", " + a + " : " + a.getClass 56 | 57 | Ok(content) 58 | } 59 | 60 | def setSessionCache = Action { 61 | sessionCache.set("key", "session value") 62 | Ok("Cached.") 63 | } 64 | 65 | def getSessionCache = Action { 66 | Ok(sessionCache.get[String]("key").toString) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /samples/java/conf/application.conf: -------------------------------------------------------------------------------- 1 | # This is the main configuration file for the application. 2 | # ~~~~~ 3 | 4 | # Secret key 5 | # ~~~~~ 6 | # The secret key is used to secure cryptographics functions. 7 | # 8 | # This must be changed for production, but we recommend not changing it in this file. 9 | # 10 | # See http://www.playframework.com/documentation/latest/ApplicationSecret for more details. 11 | play.http.secret.key = "changeme" 12 | 13 | # The application languages 14 | # ~~~~~ 15 | play.i18n.langs = [ "en" ] 16 | 17 | # Router 18 | # ~~~~~ 19 | # Define the Router object to use for this application. 20 | # This router will be looked up first when the application is starting up, 21 | # so make sure this is the entry point. 22 | # Furthermore, it's assumed your route file is named properly. 23 | # So for an application router like `my.application.Router`, 24 | # you may need to define a router file `conf/my.application.routes`. 25 | # Default to Routes in the root package (and conf/routes) 26 | # play.http.router = my.application.Routes 27 | 28 | # Database configuration 29 | # ~~~~~ 30 | # You can declare as many datasources as you want. 31 | # By convention, the default datasource is named `default` 32 | # 33 | # db.default.driver=org.h2.Driver 34 | # db.default.url="jdbc:h2:mem:play" 35 | # db.default.user=sa 36 | # db.default.password="" 37 | 38 | # Evolutions 39 | # ~~~~~ 40 | # You can disable evolutions if needed 41 | # play.evolutions.enabled=false 42 | 43 | # You can disable evolutions for a specific datasource if necessary 44 | # play.evolutions.db.default.enabled=false 45 | 46 | play.modules.enabled+="com.github.mumoshu.play2.memcached.MemcachedModule" 47 | 48 | play.cache.defaultCache=default 49 | play.cache.bindCaches=["db-cache", "user-cache", "session-cache"] 50 | 51 | memcached.host="127.0.0.1:11211" 52 | 53 | # You may consider activating this option to enable support for long keys (more than 250 characters) 54 | # Available options are MD2, MD5, SHA-1, SHA-256, SHA-384, SHA-512 (see http://docs.oracle.com/javase/1.4.2/docs/guide/security/CryptoSpec.html#AppA) 55 | memcached.hashkeys=off 56 | 57 | play.allowGlobalApplication = false 58 | -------------------------------------------------------------------------------- /samples/scala/conf/application.conf: -------------------------------------------------------------------------------- 1 | # This is the main configuration file for the application. 2 | # ~~~~~ 3 | 4 | # Secret key 5 | # ~~~~~ 6 | # The secret key is used to secure cryptographics functions. 7 | # 8 | # This must be changed for production, but we recommend not changing it in this file. 9 | # 10 | # See http://www.playframework.com/documentation/latest/ApplicationSecret for more details. 11 | play.http.secret.key = "changeme" 12 | 13 | # The application languages 14 | # ~~~~~ 15 | play.i18n.langs = [ "en" ] 16 | 17 | # Router 18 | # ~~~~~ 19 | # Define the Router object to use for this application. 20 | # This router will be looked up first when the application is starting up, 21 | # so make sure this is the entry point. 22 | # Furthermore, it's assumed your route file is named properly. 23 | # So for an application router like `my.application.Router`, 24 | # you may need to define a router file `conf/my.application.routes`. 25 | # Default to Routes in the root package (and conf/routes) 26 | # play.http.router = my.application.Routes 27 | 28 | # Database configuration 29 | # ~~~~~ 30 | # You can declare as many datasources as you want. 31 | # By convention, the default datasource is named `default` 32 | # 33 | # db.default.driver=org.h2.Driver 34 | # db.default.url="jdbc:h2:mem:play" 35 | # db.default.user=sa 36 | # db.default.password="" 37 | 38 | # Evolutions 39 | # ~~~~~ 40 | # You can disable evolutions if needed 41 | # play.evolutions.enabled=false 42 | 43 | # You can disable evolutions for a specific datasource if necessary 44 | # play.evolutions.db.default.enabled=false 45 | 46 | play.modules.enabled+="com.github.mumoshu.play2.memcached.MemcachedModule" 47 | 48 | play.cache.defaultCache=default 49 | play.cache.bindCaches=["db-cache", "user-cache", "session-cache"] 50 | 51 | memcached.host="127.0.0.1:11211" 52 | 53 | # You may consider activating this option to enable support for long keys (more than 250 characters) 54 | # Available options are MD2, MD5, SHA-1, SHA-256, SHA-384, SHA-512 (see http://docs.oracle.com/javase/1.4.2/docs/guide/security/CryptoSpec.html#AppA) 55 | memcached.hashkeys=off 56 | 57 | play.allowGlobalApplication = false 58 | -------------------------------------------------------------------------------- /plugin/src/main/com/github/mumoshu/play2/memcached/MemcachedComponents.java: -------------------------------------------------------------------------------- 1 | package com.github.mumoshu.play2.memcached; 2 | 3 | import play.Environment; 4 | import play.cache.AsyncCacheApi; 5 | import play.cache.DefaultAsyncCacheApi; 6 | import play.components.AkkaComponents; 7 | import play.components.ConfigurationComponents; 8 | import play.inject.ApplicationLifecycle; 9 | 10 | /** 11 | * Memached Java Components for compile time injection. 12 | * 13 | *

Usage:

14 | * 15 | *
16 |  * public class MyComponents extends BuiltInComponentsFromContext implements MemcachedComponents {
17 |  *
18 |  *   public MyComponents(ApplicationLoader.Context context) {
19 |  *       super(context);
20 |  *   }
21 |  *
22 |  *   // A service class that depends on cache APIs
23 |  *   public CachedService someService() {
24 |  *       // defaultCacheApi is provided by MemcachedComponents
25 |  *       return new CachedService(defaultCacheApi());
26 |  *   }
27 |  *
28 |  *   // Another service that depends on a specific named cache
29 |  *   public AnotherService someService() {
30 |  *       // cacheApi provided by MemcachedComponents and
31 |  *       // "anotherService" is the name of the cache.
32 |  *       return new CachedService(cacheApi("anotherService"));
33 |  *   }
34 |  *
35 |  *   // other methods
36 |  * }
37 |  * 
38 | */ 39 | public interface MemcachedComponents extends ConfigurationComponents, AkkaComponents { 40 | 41 | Environment environment(); 42 | 43 | ApplicationLifecycle applicationLifecycle(); 44 | 45 | default MemcachedClientProvider memcachedClientProvider() { 46 | return new MemcachedClientProvider( 47 | config(), 48 | applicationLifecycle().asScala() 49 | ); 50 | } 51 | 52 | default AsyncCacheApi cacheApi(String name) { 53 | play.api.cache.AsyncCacheApi scalaAsyncCacheApi = new MemcachedCacheApi(name, memcachedClientProvider().get(), config(), environment().asScala(), executionContext()); 54 | return new DefaultAsyncCacheApi(scalaAsyncCacheApi); 55 | } 56 | 57 | default AsyncCacheApi defaultCacheApi() { 58 | return cacheApi("play"); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /plugin/src/test/MemcachedSpec.scala: -------------------------------------------------------------------------------- 1 | import com.github.mumoshu.play2.memcached._ 2 | import net.spy.memcached.MemcachedClient 3 | import org.specs2.mutable._ 4 | import play.api.cache.AsyncCacheApi 5 | import play.api.inject.guice.GuiceApplicationBuilder 6 | import play.cache.NamedCacheImpl 7 | import play.api.test.WithApplication 8 | 9 | import scala.jdk.CollectionConverters._ 10 | 11 | object MemcachedSpec extends Specification { 12 | 13 | sequential 14 | 15 | val memcachedHost = "127.0.0.1:11211" 16 | 17 | val environment = play.api.Environment.simple() 18 | 19 | val configurationMap: Map[String, Object] = Map( 20 | "play.modules.enabled" -> List( 21 | "play.api.i18n.I18nModule", 22 | "play.api.mvc.CookiesModule", 23 | "com.github.mumoshu.play2.memcached.MemcachedModule", 24 | "play.api.inject.BuiltinModule" 25 | ).asJava, 26 | "play.allowGlobalApplication" -> "false", 27 | "play.cache.defaultCache" -> "default", 28 | "play.cache.bindCaches" -> List("secondary").asJava, 29 | "memcached.1.host" -> memcachedHost 30 | ) 31 | val configuration = play.api.Configuration.from( 32 | configurationMap 33 | ) 34 | 35 | val modules = play.api.inject.Modules.locate(environment, configuration) 36 | 37 | "play2-memcached" should { 38 | "provide MemcachedModule" in { 39 | (modules.find { module => module.isInstanceOf[MemcachedModule] }.get).asInstanceOf[MemcachedModule] must beAnInstanceOf[MemcachedModule] 40 | } 41 | } 42 | 43 | "Module" should { 44 | "provide bindings" in { 45 | val memcachedModule = (modules.find { module => module.isInstanceOf[MemcachedModule] }.get).asInstanceOf[MemcachedModule] 46 | 47 | val bindings = memcachedModule.bindings(environment, configuration) 48 | 49 | bindings.size mustNotEqual (0) 50 | } 51 | } 52 | 53 | "Injector" should { 54 | 55 | def app = GuiceApplicationBuilder().configure(configurationMap).build() 56 | 57 | "provide memcached clients" in new WithApplication(app) { 58 | override def running() = { 59 | val memcachedClient = this.app.injector.instanceOf(play.api.inject.BindingKey(classOf[MemcachedClient])) 60 | 61 | memcachedClient must beAnInstanceOf[MemcachedClient] 62 | } 63 | } 64 | 65 | "provide a CacheApi implementation backed by memcached" in new WithApplication(app) { 66 | override def running() = { 67 | val cacheApi = this.app.injector.instanceOf(play.api.inject.BindingKey(classOf[AsyncCacheApi])) 68 | 69 | cacheApi must beAnInstanceOf[MemcachedCacheApi] 70 | cacheApi.asInstanceOf[MemcachedCacheApi].namespace must equalTo ("default") 71 | } 72 | } 73 | 74 | "provide a named CacheApi implementation backed by memcached" in new WithApplication(app) { 75 | override def running() = { 76 | val cacheApi = this.app.injector.instanceOf(play.api.inject.BindingKey(classOf[AsyncCacheApi]).qualifiedWith(new NamedCacheImpl("secondary"))) 77 | 78 | cacheApi must beAnInstanceOf[MemcachedCacheApi] 79 | cacheApi.asInstanceOf[MemcachedCacheApi].namespace must equalTo ("secondary") 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /samples/scala/test/ApplicationSpec.scala: -------------------------------------------------------------------------------- 1 | import org.specs2.mutable._ 2 | import org.specs2.runner._ 3 | import org.junit.runner._ 4 | import play.api.cache.SyncCacheApi 5 | 6 | import play.api.Application 7 | import play.api.mvc.Result 8 | import play.api.test._ 9 | import play.api.test.Helpers._ 10 | import play.cache.NamedCacheImpl 11 | import play.api.inject.guice.GuiceApplicationBuilder 12 | 13 | import scala.concurrent.Future 14 | 15 | /** 16 | * Add your spec here. 17 | * You can mock out a whole application including requests, plugins etc. 18 | * For more information, consult the wiki. 19 | */ 20 | @RunWith(classOf[JUnitRunner]) 21 | class ApplicationSpec extends Specification { 22 | 23 | sequential 24 | 25 | def app = GuiceApplicationBuilder().configure(Map( 26 | "memcached.host" -> "127.0.0.1:11211" 27 | )).build() 28 | 29 | "Application" should { 30 | 31 | "send 404 on a bad request" in new WithApplication(app){ 32 | override def running() = { 33 | route(this.app, FakeRequest(GET, "/boum")) must beSome[Future[Result]].which (status(_) == NOT_FOUND) 34 | } 35 | } 36 | 37 | "render the index page" in new WithApplication(app){ 38 | override def running() = { 39 | val home = route(this.app, FakeRequest(GET, "/")).get 40 | 41 | status(home) must equalTo(OK) 42 | contentType(home) must beSome[String].which(_ == "text/html") 43 | contentAsString(home) must contain ("Your new application is ready.") 44 | } 45 | } 46 | } 47 | 48 | def connectingLocalMemcached[T](app: Application)(block: => T):T = { 49 | 50 | app.injector.instanceOf[SyncCacheApi].remove("key") 51 | 52 | val bindingKey = play.api.inject.BindingKey(classOf[SyncCacheApi]).qualifiedWith(new NamedCacheImpl("session-cache")) 53 | app.injector.instanceOf(bindingKey).remove("key") 54 | 55 | block 56 | } 57 | 58 | def c(app: Application, url: String): String = contentAsString(route(app, FakeRequest(GET, url)).get) 59 | 60 | "The scala sample application" should { 61 | 62 | "return a cached data" in new WithApplication(app){ 63 | override def running() = { 64 | connectingLocalMemcached(this.app) { 65 | c(this.app, "/cache/get") must equalTo ("None") 66 | c(this.app, "/cache/set") must equalTo ("Cached.") 67 | c(this.app, "/cache/get") must equalTo ("Some(cached value)") 68 | c(this.app, "/session/get") must equalTo ("None") 69 | c(this.app, "/session/set") must equalTo ("Cached.") 70 | c(this.app, "/session/get") must equalTo ("Some(session value)") 71 | c(this.app, "/cache/get") must equalTo ("Some(cached value)") 72 | } 73 | } 74 | } 75 | 76 | "return expected results" in new WithApplication(app){ 77 | override def running() = { 78 | connectingLocalMemcached(this.app) { 79 | c(this.app, "/cacheInt") must equalTo ("123class java.lang.Integer 123 int") 80 | c(this.app, "/cacheString") must equalTo ("Some(mikoto)") 81 | c(this.app, "/cacheBool") must equalTo ("true : class java.lang.Boolean, true : boolean") 82 | } 83 | } 84 | } 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /plugin/src/main/com/github/mumoshu/play2/memcached/MemcachedModule.scala: -------------------------------------------------------------------------------- 1 | package com.github.mumoshu.play2.memcached 2 | 3 | import akka.stream.Materializer 4 | 5 | import play.api.cache._ 6 | import play.api.inject._ 7 | 8 | import play.cache.{ AsyncCacheApi => JavaAsyncCacheApi, SyncCacheApi => JavaSyncCacheApi, DefaultAsyncCacheApi => JavaDefaultAsyncCacheApi, DefaultSyncCacheApi => JavaDefaultSyncCacheApi, NamedCacheImpl } 9 | 10 | import javax.inject.{Inject, Singleton, Provider} 11 | 12 | import net.spy.memcached.MemcachedClient 13 | 14 | import scala.reflect.ClassTag 15 | 16 | class MemcachedModule extends SimpleModule((environment, configuration) => { 17 | 18 | import scala.jdk.CollectionConverters._ 19 | 20 | val defaultCacheName = configuration.underlying.getString("play.cache.defaultCache") 21 | val bindCaches = configuration.underlying.getStringList("play.cache.bindCaches").asScala 22 | 23 | // Creates a named cache qualifier 24 | def named(name: String): NamedCache = { 25 | new NamedCacheImpl(name) 26 | } 27 | 28 | // bind wrapper classes 29 | def wrapperBindings(cacheApiKey: BindingKey[AsyncCacheApi], namedCache: NamedCache): Seq[Binding[_]] = Seq( 30 | bind[JavaAsyncCacheApi].qualifiedWith(namedCache).to(new NamedJavaAsyncCacheApiProvider(cacheApiKey)), 31 | bind[Cached].qualifiedWith(namedCache).to(new NamedCachedProvider(cacheApiKey)), 32 | bind[SyncCacheApi].qualifiedWith(namedCache).to(new NamedSyncCacheApiProvider(cacheApiKey)), 33 | bind[JavaSyncCacheApi].qualifiedWith(namedCache).to(new NamedJavaSyncCacheApiProvider(cacheApiKey)) 34 | ) 35 | 36 | // bind a cache with the given name 37 | def bindCache(name: String) = { 38 | val namedCache = named(name) 39 | val cacheApiKey = bind[AsyncCacheApi].qualifiedWith(namedCache) 40 | Seq( 41 | cacheApiKey.to(new MemcachedCacheApiProvider(name, bind[MemcachedClient], configuration.underlying, environment)) 42 | ) ++ wrapperBindings(cacheApiKey, namedCache) 43 | } 44 | 45 | def bindDefault[T: ClassTag]: Binding[T] = { 46 | bind[T].to(bind[T].qualifiedWith(named(defaultCacheName))) 47 | } 48 | 49 | Seq( 50 | bind[MemcachedClient].toProvider[MemcachedClientProvider], 51 | // alias the default cache to the unqualified implementation 52 | bindDefault[AsyncCacheApi], 53 | bindDefault[JavaAsyncCacheApi], 54 | bindDefault[SyncCacheApi], 55 | bindDefault[JavaSyncCacheApi] 56 | ) ++ bindCache(defaultCacheName) ++ bindCaches.flatMap(bindCache) 57 | }) 58 | 59 | private class NamedSyncCacheApiProvider(key: BindingKey[AsyncCacheApi]) 60 | extends Provider[SyncCacheApi] { 61 | @Inject private var injector: Injector = _ 62 | lazy val get: SyncCacheApi = 63 | new DefaultSyncCacheApi(injector.instanceOf(key)) 64 | } 65 | 66 | private class NamedJavaAsyncCacheApiProvider(key: BindingKey[AsyncCacheApi]) extends Provider[JavaAsyncCacheApi] { 67 | @Inject private var injector: Injector = _ 68 | lazy val get: JavaAsyncCacheApi = 69 | new JavaDefaultAsyncCacheApi(injector.instanceOf(key)) 70 | } 71 | 72 | private class NamedJavaSyncCacheApiProvider(key: BindingKey[AsyncCacheApi]) 73 | extends Provider[JavaSyncCacheApi] { 74 | @Inject private var injector: Injector = _ 75 | lazy val get: JavaSyncCacheApi = 76 | new JavaDefaultSyncCacheApi(new JavaDefaultAsyncCacheApi(injector.instanceOf(key))) 77 | } 78 | 79 | private class NamedCachedProvider(key: BindingKey[AsyncCacheApi]) extends Provider[Cached] { 80 | @Inject private var injector: Injector = _ 81 | lazy val get: Cached = 82 | new Cached(injector.instanceOf(key))(injector.instanceOf[Materializer]) 83 | } 84 | -------------------------------------------------------------------------------- /plugin/src/main/com/github/mumoshu/play2/memcached/MemcachedClientProvider.scala: -------------------------------------------------------------------------------- 1 | package com.github.mumoshu.play2.memcached 2 | 3 | import com.typesafe.config.Config 4 | import net.spy.memcached.auth.{PlainCallbackHandler, AuthDescriptor} 5 | import play.api.Logger 6 | import play.api.inject.ApplicationLifecycle 7 | 8 | import scala.concurrent.Future 9 | 10 | import javax.inject.{Inject, Singleton, Provider} 11 | 12 | import net.spy.memcached.{KetamaConnectionFactory, ConnectionFactoryBuilder, AddrUtil, MemcachedClient, DefaultConnectionFactory} 13 | 14 | @Singleton 15 | class MemcachedClientProvider @Inject() (config: Config, lifecycle: ApplicationLifecycle) extends Provider[MemcachedClient] { 16 | lazy val logger = Logger("memcached.plugin") 17 | lazy val get: MemcachedClient = { 18 | val client = { 19 | System.setProperty("net.spy.log.LoggerImpl", "com.github.mumoshu.play2.memcached.Slf4JLogger") 20 | 21 | if(config.hasPath(MemcachedClientProvider.CONFIG_ELASTICACHE_CONFIG_ENDPOINT)) { 22 | new MemcachedClient(AddrUtil.getAddresses(config.getString(MemcachedClientProvider.CONFIG_ELASTICACHE_CONFIG_ENDPOINT))) 23 | } else { 24 | lazy val singleHost = if(config.hasPath(MemcachedClientProvider.CONFIG_HOST)) { 25 | Option(AddrUtil.getAddresses(config.getString(MemcachedClientProvider.CONFIG_HOST))) 26 | } else { 27 | None 28 | } 29 | lazy val multipleHosts = if(config.hasPath("memcached.1.host")) { 30 | def accumulate(nb: Int): String = { 31 | if(config.hasPath("memcached." + nb + ".host")) { 32 | val h = config.getString("memcached." + nb + ".host") 33 | h + " " + accumulate(nb + 1) 34 | } else { 35 | "" 36 | } 37 | } 38 | Option(AddrUtil.getAddresses(accumulate(1).trim())) 39 | } else { 40 | None 41 | } 42 | 43 | val addrs = singleHost.orElse(multipleHosts) 44 | .getOrElse(throw new RuntimeException("Bad configuration for memcached: missing host(s)")) 45 | 46 | if(config.hasPath(MemcachedClientProvider.CONFIG_USER)) { 47 | val memcacheUser = config.getString(MemcachedClientProvider.CONFIG_USER) 48 | val memcachePassword = if(config.hasPath(MemcachedClientProvider.CONFIG_PASSWORD)) { 49 | config.getString(MemcachedClientProvider.CONFIG_PASSWORD) 50 | } else { 51 | throw new RuntimeException("Bad configuration for memcached: missing password") 52 | } 53 | 54 | // Use plain SASL to connect to memcached 55 | val ad = new AuthDescriptor(Array("PLAIN"), 56 | new PlainCallbackHandler(memcacheUser, memcachePassword)) 57 | lazy val consistentHashing = config.getBoolean("memcached.consistentHashing") 58 | val cf = (if (consistentHashing) new ConnectionFactoryBuilder(new KetamaConnectionFactory()) else new ConnectionFactoryBuilder()) 59 | .setProtocol(ConnectionFactoryBuilder.Protocol.BINARY) 60 | .setTimeoutExceptionThreshold( 61 | if(config.hasPath(MemcachedClientProvider.CONFIG_MAX_TIMEOUT_EXCEPTION_THRESHOLD)) { 62 | config.getInt(MemcachedClientProvider.CONFIG_MAX_TIMEOUT_EXCEPTION_THRESHOLD) 63 | } else { 64 | DefaultConnectionFactory.DEFAULT_MAX_TIMEOUTEXCEPTION_THRESHOLD 65 | }) 66 | .setAuthDescriptor(ad) 67 | .build() 68 | 69 | new MemcachedClient(cf, addrs) 70 | } else { 71 | new MemcachedClient(addrs) 72 | } 73 | } 74 | } 75 | 76 | logger.info("Starting MemcachedPlugin.") 77 | 78 | lifecycle.addStopHook(() => Future.successful { 79 | logger.info("Stopping MemcachedPlugin.") 80 | client.shutdown() 81 | }) 82 | 83 | client 84 | } 85 | } 86 | 87 | object MemcachedClientProvider { 88 | final val CONFIG_ELASTICACHE_CONFIG_ENDPOINT = "elasticache.config.endpoint" 89 | final val CONFIG_MAX_TIMEOUT_EXCEPTION_THRESHOLD = "memcached.max-timeout-exception-threshold" 90 | final val CONFIG_HOST = "memcached.host" 91 | final val CONFIG_USER = "memcached.user" 92 | final val CONFIG_PASSWORD = "memcached.password" 93 | } 94 | -------------------------------------------------------------------------------- /plugin/src/main/com/github/mumoshu/play2/memcached/MemcachedCacheApi.scala: -------------------------------------------------------------------------------- 1 | package com.github.mumoshu.play2.memcached 2 | 3 | import akka.Done 4 | 5 | import com.typesafe.config.Config 6 | import net.spy.memcached.transcoders.Transcoder 7 | import net.spy.memcached.internal.{ GetCompletionListener, GetFuture, OperationCompletionListener, OperationFuture } 8 | import net.spy.memcached.ops.StatusCode 9 | import play.api.cache.AsyncCacheApi 10 | import play.api.{Environment, Logger} 11 | 12 | import javax.inject.{Inject, Singleton} 13 | 14 | import scala.concurrent.duration.Duration 15 | 16 | import net.spy.memcached.MemcachedClient 17 | 18 | import scala.concurrent.{ ExecutionContext, Future, Promise } 19 | import scala.reflect.ClassTag 20 | 21 | @Singleton 22 | class MemcachedCacheApi @Inject() (val namespace: String, val client: MemcachedClient, config: Config, environment: Environment)(implicit context: ExecutionContext) extends AsyncCacheApi { 23 | lazy val logger = Logger("memcached.plugin") 24 | lazy val tc = new CustomSerializing(environment.classLoader).asInstanceOf[Transcoder[Any]] 25 | lazy val hashkeys: String = config.getString("memcached.hashkeys") 26 | lazy val throwExceptionFromGetOnError: Boolean = config.getBoolean("memcached.throwExceptionFromGetOnError") 27 | 28 | def get[T: ClassTag](key: String): Future[Option[T]] = { 29 | if (key.isEmpty) { 30 | Future.successful(None) 31 | } else { 32 | val ct = implicitly[ClassTag[T]] 33 | 34 | logger.debug("Getting the cache for key " + namespace + key) 35 | val p = Promise[Option[T]]() // create incomplete promise/future 36 | client.asyncGet(namespace + hash(key), tc).addListener(new GetCompletionListener() { 37 | def onComplete(result: GetFuture[_]): Unit = { 38 | try { 39 | result.getStatus().getStatusCode() match { 40 | case StatusCode.SUCCESS => { 41 | val any = result.get 42 | logger.debug("any is " + any.getClass) 43 | p.success(Option( 44 | any match { 45 | case x if ct.runtimeClass.isInstance(x) => x.asInstanceOf[T] 46 | case x if ct == ClassTag.Nothing => x.asInstanceOf[T] 47 | case x => x.asInstanceOf[T] 48 | } 49 | )) 50 | } 51 | case StatusCode.ERR_NOT_FOUND => { 52 | logger.debug("Cache miss for " + namespace + key) 53 | p.success(None) 54 | } 55 | case _ => { 56 | fail(result, None) 57 | } 58 | } 59 | } catch { 60 | case e: Throwable => fail(result, Some(e)) 61 | } 62 | } 63 | 64 | def fail(result: GetFuture[_], exception: Option[Throwable]): Unit = { 65 | val msg = "An error has occured while getting the value from memcached. ct=" + ct + ". key=" + key + ". " + 66 | "spymemcached code: " + result.getStatus().getStatusCode() + " memcached code:" + result.getStatus().getMessage() 67 | if (throwExceptionFromGetOnError) { 68 | p.failure(exception.fold(new RuntimeException(msg))(new RuntimeException(msg, _))) 69 | } else { 70 | logger.error(msg, exception.orNull) 71 | p.success(None) 72 | } 73 | } 74 | }) 75 | p.future 76 | } 77 | } 78 | 79 | def getOrElseUpdate[A: ClassTag](key: String, expiration: Duration)(orElse: => Future[A]): Future[A] = { 80 | get[A](key).flatMap { 81 | case Some(value) => Future.successful(value) 82 | case None => orElse.flatMap(value => set(key, value, expiration).map(_ => value)) 83 | } 84 | } 85 | 86 | def set(key: String, value: Any, expiration: Duration = Duration.Inf): Future[Done] = { 87 | if (!key.isEmpty) { 88 | val p = Promise[Done]() // create incomplete promise/future 89 | val exp = if (expiration.isFinite) expiration.toSeconds.toInt else 0 90 | client.set(namespace + hash(key), exp, value, tc).addListener(new OperationCompletionListener() { 91 | def onComplete(result: OperationFuture[_]): Unit = { 92 | result.getStatus().getStatusCode() match { 93 | case StatusCode.SUCCESS => { 94 | p.success(Done) 95 | } 96 | case _ => { 97 | val msg = "An error has occured while setting the value in memcached. key=" + key + ". value=" + value + ". " + 98 | "spymemcached code: " + result.getStatus().getStatusCode() + " memcached code:" + result.getStatus().getMessage() 99 | if (throwExceptionFromGetOnError) { 100 | p.failure(new RuntimeException(msg)) 101 | } else { 102 | logger.error(msg) 103 | p.success(Done) 104 | } 105 | } 106 | } 107 | } 108 | }) 109 | p.future 110 | } else { 111 | Future.successful(Done) 112 | } 113 | } 114 | 115 | def remove(key: String): Future[Done] = { 116 | if (!key.isEmpty) { 117 | val p = Promise[Done]() // create incomplete promise/future 118 | client.delete(namespace + hash(key)).addListener(new OperationCompletionListener() { 119 | def onComplete(result: OperationFuture[_]): Unit = { 120 | result.getStatus().getStatusCode() match { 121 | case StatusCode.SUCCESS => { 122 | p.success(Done) 123 | } 124 | case StatusCode.ERR_NOT_FOUND => { 125 | logger.debug("Cache miss when removing " + namespace + key) 126 | p.success(Done) 127 | } 128 | case _ => { 129 | val msg = "An error has occured while removing the value in memcached. key=" + key + ". " + 130 | "spymemcached code: " + result.getStatus().getStatusCode() + " memcached code:" + result.getStatus().getMessage() 131 | if (throwExceptionFromGetOnError) { 132 | p.failure(new RuntimeException(msg)) 133 | } else { 134 | logger.error(msg) 135 | p.success(Done) 136 | } 137 | } 138 | } 139 | } 140 | }) 141 | p.future 142 | } else { 143 | Future.successful(Done) 144 | } 145 | } 146 | 147 | def removeAll(): Future[Done] = { 148 | val p = Promise[Done]() // create incomplete promise/future 149 | client.flush().addListener(new OperationCompletionListener() { 150 | def onComplete(result: OperationFuture[_]): Unit = { 151 | result.getStatus().getStatusCode() match { 152 | case StatusCode.SUCCESS => { 153 | p.success(Done) 154 | } 155 | case _ => { 156 | val msg = "An error has occured while removing all values from memcached. " + 157 | "spymemcached code: " + result.getStatus().getStatusCode() + " memcached code:" + result.getStatus().getMessage() 158 | if (throwExceptionFromGetOnError) { 159 | p.failure(new RuntimeException(msg)) 160 | } else { 161 | logger.error(msg) 162 | p.success(Done) 163 | } 164 | } 165 | } 166 | } 167 | }) 168 | p.future 169 | } 170 | 171 | // you may override hash implementation to use more sophisticated hashes, like xxHash for higher performance 172 | protected def hash(key: String): String = if(hashkeys == "off") key 173 | else java.security.MessageDigest.getInstance(hashkeys).digest(key.getBytes).map("%02x".format(_)).mkString 174 | } 175 | 176 | object MemcachedCacheApi { 177 | object ValFromJavaObject { 178 | def unapply(x: AnyRef): Option[AnyVal] = x match { 179 | case x: java.lang.Byte => Some(x.byteValue()) 180 | case x: java.lang.Short => Some(x.shortValue()) 181 | case x: java.lang.Integer => Some(x.intValue()) 182 | case x: java.lang.Long => Some(x.longValue()) 183 | case x: java.lang.Float => Some(x.floatValue()) 184 | case x: java.lang.Double => Some(x.doubleValue()) 185 | case x: java.lang.Character => Some(x.charValue()) 186 | case x: java.lang.Boolean => Some(x.booleanValue()) 187 | case _ => None 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Memcached Plugin for Play framework 2.x 2 | --------------------------------------- 3 | 4 | A Memcached implementation of Cache API for Play 2.x. 5 | Using spymemcached internally, which is the same as Play 1.x's default Cache implementation. 6 | 7 | ## Usage 8 | 9 | Add the following dependency to your Play project: 10 | 11 | ### Library dependencies 12 | 13 | For Play 2.6.x and newer: 14 | !!! Changed `play.modules.cache.*` config keys to `play.cache.*` !!! 15 | 16 | ```scala 17 | val appDependencies = Seq( 18 | play.PlayImport.cacheApi, 19 | "com.github.mumoshu" %% "play2-memcached-play26" % "0.9.2" 20 | ) 21 | val main = Project(appName).enablePlugins(play.PlayScala).settings( 22 | version := appVersion, 23 | libraryDependencies ++= appDependencies, 24 | resolvers += "Spy Repository" at "http://files.couchbase.com/maven2" // required to resolve `spymemcached`, the plugin's dependency. 25 | ) 26 | ``` 27 | 28 | For Play 2.5.x: 29 | 30 | ```scala 31 | val appDependencies = Seq( 32 | play.PlayImport.cache, 33 | "com.github.mumoshu" %% "play2-memcached-play25" % "0.8.0" 34 | ) 35 | val main = Project(appName).enablePlugins(play.PlayScala).settings( 36 | version := appVersion, 37 | libraryDependencies ++= appDependencies, 38 | resolvers += "Spy Repository" at "http://files.couchbase.com/maven2" // required to resolve `spymemcached`, the plugin's dependency. 39 | ) 40 | ``` 41 | 42 | For Play 2.4.x: 43 | 44 | ```scala 45 | val appDependencies = Seq( 46 | play.PlayImport.cache, 47 | "com.github.mumoshu" %% "play2-memcached-play24" % "0.7.0" 48 | ) 49 | val main = Project(appName).enablePlugins(play.PlayScala).settings( 50 | version := appVersion, 51 | libraryDependencies ++= appDependencies, 52 | resolvers += "Spy Repository" at "http://files.couchbase.com/maven2" // required to resolve `spymemcached`, the plugin's dependency. 53 | ) 54 | ``` 55 | 56 | For Play 2.3.x: 57 | 58 | ```scala 59 | val appDependencies = Seq( 60 | play.PlayImport.cache, 61 | "com.github.mumoshu" %% "play2-memcached-play23" % "0.7.0" 62 | ) 63 | val main = Project(appName).enablePlugins(play.PlayScala).settings( 64 | version := appVersion, 65 | libraryDependencies ++= appDependencies, 66 | resolvers += "Spy Repository" at "http://files.couchbase.com/maven2" // required to resolve `spymemcached`, the plugin's dependency. 67 | ) 68 | ``` 69 | 70 | For Play 2.2.0: 71 | 72 | ```scala 73 | val appDependencies = Seq( 74 | cache, // or play.Project.cache if not imported play.Project._ 75 | "com.github.mumoshu" %% "play2-memcached" % "0.4.0" // or 0.5.0-RC1 to try the latest improvements 76 | ) 77 | val main = play.Project(appName, appVersion, appDependencies).settings( 78 | resolvers += "Spy Repository" at "http://files.couchbase.com/maven2" // required to resolve `spymemcached`, the plugin's dependency. 79 | ) 80 | ``` 81 | 82 | 83 | For Play 2.1.0: 84 | 85 | ```scala 86 | val appDependencies = Seq( 87 | "com.github.mumoshu" %% "play2-memcached" % "0.3.0.3" 88 | ) 89 | val main = play.Project(appName, appVersion, appDependencies).settings( 90 | resolvers += "Spy Repository" at "http://files.couchbase.com/maven2" // required to resolve `spymemcached`, the plugin's dependency. 91 | ) 92 | ``` 93 | 94 | For Play 2.0: 95 | 96 | ```scala 97 | val appDependencies = Seq( 98 | "com.github.mumoshu" %% "play2-memcached" % "0.2.4.3" 99 | ) 100 | val main = PlayProject(appName, appVersion, appDependencies, mainLang = SCALA).settings( 101 | resolvers += "Spy Repository" at "http://files.couchbase.com/maven2" // required to resolve `spymemcached`, the plugin's dependency. 102 | ) 103 | ``` 104 | 105 | ### Configurations 106 | 107 | #### Starting with Play 2.6.x 108 | 109 | ``` 110 | play.modules.enabled+="com.github.mumoshu.play2.memcached.MemcachedModule" 111 | 112 | # Well-known configuration provided by Play 113 | play.cache.defaultCache=default 114 | play.cache.bindCaches=["db-cache", "user-cache", "session-cache"] 115 | 116 | # Tell play2-memcached where your memcached host is located at 117 | memcached.host="127.0.0.1:11211" 118 | ``` 119 | 120 | #### For Play 2.4.x and above 121 | 122 | ``` 123 | play.modules.enabled+="com.github.mumoshu.play2.memcached.MemcachedModule" 124 | 125 | # To avoid conflict with play2-memcached's Memcached-based cache module 126 | play.modules.disabled+="play.api.cache.EhCacheModule" 127 | 128 | # Well-known configuration provided by Play 129 | play.modules.cache.defaultCache=default 130 | play.modules.cache.bindCaches=["db-cache", "user-cache", "session-cache"] 131 | 132 | # Tell play2-memcached where your memcached host is located at 133 | memcached.host="127.0.0.1:11211" 134 | ``` 135 | 136 | #### Play 2.3.x or below 137 | 138 | Add a reference to the plugin in `play.plugins` file. 139 | `play.plugins` file must be put somewhere in the classpath. 140 | My recommendation is to put it in `conf/` directory. 141 | 142 | ``` 143 | 5000:com.github.mumoshu.play2.memcached.MemcachedPlugin 144 | ``` 145 | 146 | First of all, in `application.conf`, disable the EhCachePlugin - Play's default implementation of CacheAPI: 147 | 148 | ``` 149 | ehcacheplugin=disabled 150 | ``` 151 | 152 | Specify the host name or IP address of the memcached server, and the port number: 153 | 154 | ``` 155 | memcached.host="127.0.0.1:11211" 156 | ``` 157 | 158 | If you have multiple memcached instances over different host names or IP addresses, provide them like: 159 | 160 | ``` 161 | memcached.1.host="mumocached1:11211" 162 | memcached.2.host="mumocached2:11211" 163 | ``` 164 | 165 | ### Code examples 166 | 167 | #### For Play 2.4.x and above 168 | 169 | See the Play Framework documentation for the [Scala](https://www.playframework.com/documentation/2.6.x/ScalaCache) and [Java](https://www.playframework.com/documentation/2.6.x/JavaCache) API. 170 | 171 | #### For Play 2.3.x or below 172 | 173 | Then, you can use the `play.api.cache.Cache` object to store a value in memcached: 174 | 175 | ```scala 176 | Cache.set("key", "theValue") 177 | ``` 178 | 179 | This way, memcached tries to retain the stored value eternally. 180 | Of course Memcached does not guarantee eternity of the value, nor can it retain the value on restart. 181 | 182 | If you want the value expired after some time: 183 | 184 | ```scala 185 | Cache.set("key", "theValueWithExpirationTime", 3600) 186 | // The value expires after 3600 seconds. 187 | ``` 188 | 189 | To get the value for a key: 190 | 191 | ```scala 192 | val theValue = Cache.getAs[String]("key") 193 | ``` 194 | 195 | You can remove the value (It's not yet a part of Play 2.0's Cache API, though): 196 | 197 | ```scala 198 | play.api.Play.current.plugin[MemcachedPlugin].get.api.remove("keyToRemove") 199 | ``` 200 | 201 | ### Advanced configurations 202 | 203 | #### Disabling the plugin (For Play 2.3.x or below) 204 | 205 | You can disable the plugin in a similar manner to Play's build-in Ehcache Plugin. 206 | To disable the plugin in `application.conf`: 207 | 208 | ``` 209 | memcachedplugin=disabled 210 | ``` 211 | 212 | #### Authentication with SASL 213 | 214 | If you memcached requires the client an authentication with SASL, provide username/password like: 215 | 216 | ``` 217 | memcached.user=misaka 218 | memcached.password=mikoto 219 | ``` 220 | 221 | #### Configure logging 222 | 223 | By default, the plugin (or the spymemcached under the hood) does not output any logs at all. 224 | If you need to peek into what's going on, set the log level like: 225 | 226 | ##### For Play 2.4.x and above 227 | 228 | In your `logback.xml`: 229 | 230 | ``` 231 | 232 | ``` 233 | 234 | #### For Play 2.3.x or below 235 | 236 | ``` 237 | logger.memcached=DEBUG 238 | ``` 239 | 240 | #### Namespacing 241 | 242 | You can prefix every key to put/get/remove with a global namespace. 243 | 244 | ##### For Play 2.4.x and above 245 | 246 | You can inject an `(ASync)CacheApi` with @play.cache.NamedCache to prefix all the keys you get, set and remove with the given namespace. 247 | There is more documentation in the official Play Framework documentation. 248 | 249 | ``` 250 | @Inject @play.cache.NamedCache("user-cache") private AsyncCacheApi cacheApi; 251 | ``` 252 | 253 | ##### For Play 2.3.x or below 254 | 255 | By default, the namespace is an empty string, implying you don't use namespacing at all. 256 | To enable namespacing, configure it in "application.conf": 257 | 258 | ``` 259 | memcached.namespace=mikoto. 260 | ``` 261 | 262 | ### Key hashing 263 | 264 | **Requires play2-memcached 0.9.0 or later** 265 | 266 | You may consider activating this option to enable support for long keys which are composed of more than 250 characters. 267 | 268 | It is disabled by default like: 269 | 270 | ``` 271 | memcached.hashkeys=off 272 | ``` 273 | 274 | You can activate it by specifying the name of a message digest algorithm used for hashing: 275 | 276 | ``` 277 | memcached.hashkeys=SHA-1 278 | ``` 279 | 280 | Available options are MD2, MD5, SHA-1, SHA-256, SHA-384, SHA-512. 281 | Please refer to http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#MessageDigest for the complete list of available algorithms. 282 | 283 | ### Configuring timeouts 284 | 285 | Until Play version 2.6 you can specify timeouts for obtaining values from Memcached. 286 | This option isn't needed anymore since Play 2.6 because since that version Play's cache api is async per default. 287 | 288 | ``` 289 | # Timeout in 1 second (only until Play version 2.6) 290 | memcached.timeout=1 291 | ``` 292 | 293 | ### Using ElastiCache's Auto-Discovery feature 294 | 295 | At first, download the latest `AmazonElastiCacheClusterClient-*.jar` from the AWS console, 296 | or build it yourself as described in [The Amazon ElastiCache Cluster Client page in GitHub](https://github.com/amazonwebservices/aws-elasticache-cluster-client-memcached-for-java), 297 | and put it under `/lib`. 298 | 299 | Remove the SBT dependency on spymemcached by excluding it from play2-mamcached's transitive dependencies: 300 | 301 | ``` 302 | "com.github.mumoshu" %% "play2-memcached" % "0.5.0-RC1 exclude("net.spy", "spymemcached") 303 | ``` 304 | 305 | Configure your configuration endpoint in `application.conf`: 306 | 307 | ``` 308 | elasticache.config.endpoint="mycachename.asdfjk.cfg.use1.cache.amazonaws.com:11211". 309 | ``` 310 | 311 | ### Version history 312 | 313 | 0.2.2 Fixed the logging leak issue. You don't get a bunch of INFO messages to play app's default logger anymore. 314 | 315 | 0.2.3 Allow removing keys in both Java and Scala ways described in Play's documentation. See MemcachedIntegrationSpec.scala for how to remove keys in Scala. 316 | 317 | 0.2.4 Introduced "namespace" to prefix every key to put/get/remove with a global namespace configured in "application.conf" 318 | 319 | 0.2.4.1 Updated spymemcached to 2.8.12 320 | 321 | 0.2.4.3 Updated spymemcached to 2.9.0 which solves the authentication issues. 322 | 323 | 0.3.0 Built for Play 2.1.0 and available in the Maven Central. Also updated spymemcached to 2.8.4. 324 | 325 | 0.3.0.1 Updated spymemcached to 2.8.12 326 | 327 | 0.3.0.2 Reverted spymemcached to 2.8.9 to deal with authentication failures to various memcache servers caused by spymemcached 2.8.10+. See #17 and #20 for details. 328 | 329 | 0.3.0.3 Updated spymemcached to 2.9.0 which solves the authentication issues. 330 | 331 | 0.4.0 Build for Play 2.2.0 332 | 333 | 0.5.0-RC1 Improvements: 334 | #14 Adding support for Amazon Elasticache (thanks to @kamatsuoka) 335 | #23 Adding configurable timeouts on the future (thanks to @rmmeans) 336 | #24 Empty keys - kind of ehcache compilance, avoiding IllegalArgumentExceptions (thanks to @mkubala) 337 | 338 | 0.7.0 Cross built for Play 2.3.x, 2.4.x, Scala 2.10.5 and 2.11.6. Artifact IDs are renamed to `play2-memcached-play2{3,4}_2.1{0,1}` 339 | 340 | 0.8.0 Built for Play 2.5.x and Scala 2.11.11. Artifact ID for this build is `play2-memcached-play25_2.11` 341 | 342 | 0.9.0 Built for Play 2.6.x and Scala 2.11.11 and 2.12.3. Artifact ID for this build is `play2-memcached-play26_2.1{1,2}` 343 | !!! Changed `play.modules.cache.*` config keys to `play.cache.*` !!! 344 | 345 | 0.9.1 Remove global state by removing reference to deprecated Play.current 346 | 347 | 0.9.2 Fix frozen future in 2.6 API 348 | 349 | 0.10.0-M1 Built for Play 2.7.0-M1 and Scala 2.11.12 and 2.12.6. Artifact ID for this build is `play2-memcached-play27_2.1{1,2}` 350 | 351 | 0.10.0-M2 Built for Play 2.7.0-M2 and Scala 2.11.12 and 2.12.6. Artifact ID for this build is `play2-memcached-play27_2.1{1,2}` 352 | 353 | 0.10.0-RC3 Built for Play 2.7.0-RC8 and Scala 2.11.12 and 2.12.7. Artifact ID for this build is `play2-memcached-play27_2.1{1,2}` 354 | 355 | 0.10.0 Built for Play 2.7.0 and Scala 2.11.12 and 2.12.8. Artifact ID for this build is `play2-memcached-play27_2.1{1,2}` 356 | 357 | 0.10.1 Built for Play 2.7.3 and Scala 2.13.0, 2.11.12 and 2.12.8. Artifact ID for this build is `play2-memcached-play27_2.1{1,2,3}` 358 | 359 | 0.11.0 Built for Play 2.8.0 and Scala 2.13.1 and 2.12.10. Artifact ID for this build is `play2-memcached-play28_2.1{2,3}` 360 | 361 | 0.12.0 Built for Play 2.9.0 and Scala 2.13.12 and 3.3.1. Artifact ID for this build is `play2-memcached-play29_[2.13|3]` 362 | 363 | ### Publishing to the central 364 | 365 | ``` 366 | # Play 2.5 367 | PLAY_VERSION=2.5.0 sbt ++2.11.12 publishSigned sonatypeRelease 368 | 369 | # Play 2.6 370 | PLAY_VERSION=2.6.0 sbt ++2.12.8 publishSigned sonatypeRelease 371 | 372 | # Play 2.7 373 | PLAY_VERSION=2.7.3 sbt ++2.11.12 publishSigned sonatypeRelease 374 | PLAY_VERSION=2.7.3 sbt ++2.12.8 publishSigned sonatypeRelease 375 | PLAY_VERSION=2.7.3 sbt ++2.13.0 publishSigned sonatypeRelease 376 | 377 | # Play 2.8 378 | PLAY_VERSION=2.8.0 sbt ++2.12.10 publishSigned sonatypeRelease 379 | PLAY_VERSION=2.8.0 sbt ++2.13.1 publishSigned sonatypeRelease 380 | 381 | # Play 2.9 382 | PLAY_VERSION=2.9.0 sbt ++2.13.12 publishSigned sonatypeRelease 383 | PLAY_VERSION=2.9.0 sbt ++3.3.1 publishSigned sonatypeRelease 384 | ``` 385 | 386 | ### Acknowledgement 387 | 388 | Thanks to: 389 | @gakuzzzz for the original idea of "namespacing" and the initial pull request for it. 390 | @kamatsuoka for adding support for Amazon Elasticache. 391 | @rmmeans for adding configurable timeouts on the future. 392 | @mkubala for improving compliance with EhCache. 393 | 394 | ### Build status 395 | 396 | [![Build Status](https://secure.travis-ci.org/mumoshu/play2-memcached.png)](http://travis-ci.org/mumoshu/play2-memcached) 397 | --------------------------------------------------------------------------------