├── .gitignore ├── LICENSE ├── README.md ├── build.sbt ├── project └── plugins.sbt ├── src ├── main │ ├── java │ │ └── com │ │ │ └── github │ │ │ └── scalaspring │ │ │ └── akka │ │ │ ├── ActorBean.java │ │ │ ├── ActorComponent.java │ │ │ └── config │ │ │ ├── AkkaConfigAutoConfiguration.java │ │ │ ├── AkkaConfigPropertySource.java │ │ │ ├── AkkaConfigPropertySourceAdapter.java │ │ │ └── AkkaConfigPropertySourceBeanFactoryPostProcessor.java │ └── scala │ │ └── com │ │ └── github │ │ └── scalaspring │ │ └── akka │ │ ├── ActorSystemBeanPostProcessor.scala │ │ ├── ActorSystemConfiguration.scala │ │ ├── ActorSystemLifecycle.scala │ │ ├── AkkaAutoConfiguration.scala │ │ ├── AkkaAutowiredImplicits.scala │ │ ├── SpringActor.scala │ │ ├── SpringActorRefFactory.scala │ │ ├── SpringExtension.scala │ │ ├── SpringIndirectActorProducer.scala │ │ ├── SpringLogging.scala │ │ └── SpringProps.scala └── test │ ├── resources │ ├── AkkaConfigAutoConfigurationSpec.properties │ └── application.yml │ └── scala │ └── com │ └── github │ └── scalaspring │ └── akka │ ├── AkkaAutoConfigurationSpec.scala │ └── config │ ├── AkkaConfigAutoConfigurationSpec.scala │ ├── AkkaConfigPropertySourceAdapterPatternSpec.scala │ └── AkkaConfigPropertySourceAdapterSpec.scala └── version.sbt /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | # sbt specific 5 | .cache 6 | .history 7 | .lib/ 8 | dist/* 9 | target/ 10 | lib_managed/ 11 | src_managed/ 12 | project/boot/ 13 | project/plugins/project/ 14 | 15 | # Scala-IDE specific 16 | .scala_dependencies 17 | .worksheet 18 | 19 | # IntelliJ IDEA specific 20 | .idea/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Akka Spring Boot Integration (akka-spring-boot) 2 | 3 | Scala-based integration of Akka with Spring Boot. 4 | Two-way Akka<->Spring configuration bindings and convention over configuration with sensible automatic defaults get your project running quickly. 5 | 6 | The goal of this project is to produce bootable, Scala-based Spring Boot applications with minimal configuration. 7 | 8 | #### Key Benefits 9 | 1. Full Spring dependency injection support 10 | * Autowire any dependency into your actors and leverage the full Spring ecosystem where it makes sense 11 | * Use existing Spring components to enable gradual migration or reuse of suitable existing enterprise components 12 | * Avoid the downsides of using Scala implicits or abstract types to implement dependency injection. While both are excellent features, they can also lead to tight coupling and less maintainable code. 13 | 2. Configure Akka via any Spring property source 14 | * Use your Spring Boot configuration (YAML, properties files, or any property source) to set Akka properties. Any property set via Spring is visible via Akka Config. 15 | * Seamless two-way integration of Akka and Spring configuration - any property defined in Akka configuration is accessible via Spring and vice versa. 16 | 3. Pre-configured default actor system that's managed for you 17 | * No need to create and manage an actor system for your actors. A default actor system will be created when your application starts and terminated when your application is stopped. 18 | 4. Easy creation of actor beans and actor references 19 | * Simple, standard annotations and familiar actorOf() methods are all that's required to create actors that integrate with Spring. 20 | 21 | #### Getting Started 22 | 23 | ##### build.sbt 24 | 25 | ````scala 26 | libraryDependencies ++= "com.github.scalaspring" %% "akka-spring-boot" % "0.3.1" 27 | ```` 28 | 29 | ##### Create an Actor and a Spring configuration 30 | 31 | ````scala 32 | @ActorComponent 33 | class EchoActor extends Actor { 34 | override def receive = { 35 | case message ⇒ sender() ! message 36 | } 37 | } 38 | 39 | @Configuration 40 | @ComponentScan 41 | @Import(Array(classOf[AkkaAutoConfiguration])) 42 | class EchoConfiguration extends ActorSystemConfiguration { 43 | 44 | @Bean 45 | def echoActor = actorOf[EchoActor] 46 | 47 | } 48 | ```` 49 | 50 | ###### Notes on the code 51 | 52 | * Actors 53 | * Annotate your actors with `@ActorComponent`, a [Spring meta-annotation](http://docs.spring.io/spring/docs/current/spring-framework-reference/html/beans.html#beans-meta-annotations). 54 | This is simply a more readable way of marking your actors as Spring prototype beans. 55 | * Configurations 56 | * Extend the `ActorSystemConfiguration` trait, which includes the helpful `actorOf()` methods 57 | * Import the `AkkaAutoConfiguration` configuration, which creates and manages the default actor system 58 | * Note that the `@ComponentScan` annotation will cause the EchoActor class to get picked up as a bean. 59 | * Configuration Properties 60 | * Use the `akka.actorSystem.lifecycle.phase` configuration property to control when the underlying `ActorSystem` is terminated. The default value is -10 to ensure its termination after any default beans. 61 | 62 | ##### Test the Configuration 63 | 64 | Create a ScalaTest-based test that uses the configuration (see the [scalatest-spring](https://github.com/scalaspring/scalatest-spring) project) 65 | 66 | ````scala 67 | @ContextConfiguration( 68 | loader = classOf[SpringApplicationContextLoader], 69 | classes = Array(classOf[EchoConfiguration]) 70 | ) 71 | class EchoConfigurationSpec extends FlatSpec with TestContextManagement with Matchers with AskSupport with ScalaFutures with StrictLogging { 72 | 73 | implicit val timeout: Timeout = (1 seconds) 74 | 75 | @Autowired val echoActor: ActorRef = null 76 | 77 | "Echo actor" should "receive and echo message" in { 78 | val message = "test message" 79 | val future = echoActor ? message 80 | 81 | whenReady(future) { result => 82 | logger.info(s"""received result "$result"""") 83 | result should equal(message) 84 | } 85 | } 86 | 87 | } 88 | ```` 89 | 90 | #### FAQ 91 | 92 | ##### How do I inject dependencies into my Scala classes? 93 | 94 | ###### Option 1: Constructor injection (Recommended) 95 | 96 | Use the standard Spring `@Autowired` (or Java's `@Inject`) annotation on your class constructor(s). Note that the parentheses on the `@Autowired` annotation are required. 97 | 98 | For example, assuming a bean of type MyService is defined in your configuration, the following actor will be injected with the appropriate dependency. 99 | Note that this technique works with any Scala class, not just Actors. Use one of the standard Spring annotations (`@Component`, `@Service`, etc.) instead of `@ActorComponent`. 100 | 101 | ````scala 102 | @Service 103 | class SomeService { 104 | def someMethod() = { ... } 105 | } 106 | 107 | @ActorComponent 108 | class SomeActor @Autowired() (val service: SomeService) extends Actor { 109 | override def receive = { 110 | // Call methods on service ... 111 | } 112 | } 113 | 114 | ```` 115 | 116 | ###### Option 2: Field injection 117 | 118 | Use the standard Spring `@Autowired` (or Java's `@Inject`) annotation on class fields. Note that Spring will set read-only (val) fields. 119 | 120 | ````scala 121 | @Component 122 | class SomeComponent { 123 | def someMethod() = { ... } 124 | } 125 | 126 | @ActorComponent 127 | class SomeActor extends Actor { 128 | 129 | @Autowired val component: SomeComponent = null 130 | 131 | override def receive = { 132 | // Call methods on component ... 133 | } 134 | } 135 | 136 | ```` 137 | 138 | ##### How is this project different than the spring-scala project from Pivotal Labs? 139 | 140 | The two projects have different purposes and approaches. 141 | The [scala-spring](https://github.com/spring-projects/spring-scala) project strives to make Spring accessible via functional configuration. 142 | This project uses a different approach, relying on standard Spring annotations, and more tightly integrates with Akka. 143 | Note that the scala-spring project is no longer maintained. 144 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import sbt.Keys._ 2 | 3 | // Common dependency versions 4 | val akkaVersion = "2.4.2" 5 | val springVersion = "4.2.5.RELEASE" 6 | val springBootVersion = "1.3.3.RELEASE" 7 | 8 | lazy val `akka-spring-boot` = (project in file(".")). 9 | settings(net.virtualvoid.sbt.graph.Plugin.graphSettings: _*). 10 | settings( 11 | organization := "com.github.scalaspring", 12 | name := "akka-spring-boot", 13 | description := "Scala-based integration of Akka with Spring Boot.\nTwo-way Akka<->Spring configuration bindings and convention over configuration with sensible automatic defaults get your project running quickly.", 14 | scalaVersion := "2.11.8", 15 | crossScalaVersions := Seq("2.10.6"), 16 | javacOptions := Seq("-source", "1.7", "-target", "1.7"), 17 | scalacOptions ++= Seq("-feature", "-deprecation"), 18 | resolvers += "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots", 19 | libraryDependencies ++= Seq( 20 | "com.typesafe.scala-logging" %% "scala-logging" % "3.+", 21 | "com.typesafe.akka" %% "akka-actor" % akkaVersion, 22 | "org.springframework" % "spring-context" % springVersion, 23 | "org.springframework.boot" % "spring-boot-starter" % springBootVersion 24 | // The following dependencies support configuration validation 25 | //"javax.validation" % "validation-api" % "1.1.0.Final", 26 | //"javax.el" % "javax.el-api" % "3.0.1-b04", 27 | //"org.hibernate" % "hibernate-validator" % "5.1.3.Final", 28 | ), 29 | // Runtime dependencies 30 | libraryDependencies ++= Seq( 31 | "ch.qos.logback" % "logback-classic" % "1.1.2" 32 | ).map { _ % "runtime" }, 33 | // Test dependencies 34 | libraryDependencies ++= Seq( 35 | "org.scalatest" %% "scalatest" % "2.2.6", 36 | "com.github.scalaspring" %% "scalatest-spring" % "0.3.1", 37 | "org.springframework" % "spring-test" % springVersion, 38 | "com.typesafe.akka" %% "akka-testkit" % akkaVersion 39 | ).map { _ % "test" }, 40 | // Publishing settings 41 | publishMavenStyle := true, 42 | publishArtifact in Test := false, 43 | pomIncludeRepository := { _ => false }, 44 | publishTo := { 45 | val nexus = "https://oss.sonatype.org/" 46 | if (isSnapshot.value) 47 | Some("snapshots" at nexus + "content/repositories/snapshots") 48 | else 49 | Some("releases" at nexus + "service/local/staging/deploy/maven2") 50 | }, 51 | pomExtra := 52 | http://github.com/scalaspring/akka-spring-boot 53 | 54 | 55 | Apache License, Version 2.0 56 | http://www.apache.org/licenses/LICENSE-2.0.html 57 | repo 58 | 59 | 60 | 61 | git@github.com:scalaspring/akka-spring-boot.git 62 | scm:git:git@github.com:scalaspring/akka-spring-boot.git 63 | 64 | 65 | 66 | lancearlaus 67 | Lance Arlaus 68 | http://lancearlaus.github.com 69 | 70 | 71 | ) 72 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | 2 | addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.7.5") 3 | 4 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0") 5 | 6 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "0.2.2") -------------------------------------------------------------------------------- /src/main/java/com/github/scalaspring/akka/ActorBean.java: -------------------------------------------------------------------------------- 1 | package com.github.scalaspring.akka; 2 | 3 | import org.springframework.beans.factory.annotation.Autowire; 4 | import org.springframework.beans.factory.config.ConfigurableBeanFactory; 5 | import org.springframework.beans.factory.support.AbstractBeanDefinition; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Scope; 8 | 9 | import java.lang.annotation.ElementType; 10 | import java.lang.annotation.Retention; 11 | import java.lang.annotation.RetentionPolicy; 12 | import java.lang.annotation.Target; 13 | 14 | /** 15 | * Annotate actor beans (not actor references!) within a Spring configuration. 16 | * 17 | * This annotation SHOULD only be required for Actor-based classes used from third-party 18 | * libraries, etc. Please use the {@code ActorComponent} annotation to annotate your 19 | * classes instead if you're creating your own actors for use with Spring. 20 | * 21 | * @see ActorComponent 22 | */ 23 | @Bean 24 | @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) 25 | @Target({ElementType.METHOD}) 26 | @Retention(RetentionPolicy.RUNTIME) 27 | public @interface ActorBean { 28 | /** 29 | * The name of this bean, or if plural, aliases for this bean. If left unspecified 30 | * the name of the bean is the name of the annotated method. If specified, the method 31 | * name is ignored. 32 | */ 33 | String[] name() default {}; 34 | 35 | /** 36 | * Are dependencies to be injected via convention-based autowiring by name or type? 37 | */ 38 | Autowire autowire() default Autowire.NO; 39 | 40 | /** 41 | * The optional name of a method to call on the bean instance during initialization. 42 | * Not commonly used, given that the method may be called programmatically directly 43 | * within the body of a Bean-annotated method. 44 | *

The default value is {@code ""}, indicating no init method to be called. 45 | */ 46 | String initMethod() default ""; 47 | 48 | /** 49 | * The optional name of a method to call on the bean instance upon closing the 50 | * application context, for example a {@code close()} method on a JDBC 51 | * {@code DataSource} implementation, or a Hibernate {@code SessionFactory} object. 52 | * The method must have no arguments but may throw any exception. 53 | *

As a convenience to the user, the container will attempt to infer a destroy 54 | * method against an object returned from the {@code @Bean} method. For example, given 55 | * an {@code @Bean} method returning an Apache Commons DBCP {@code BasicDataSource}, 56 | * the container will notice the {@code close()} method available on that object and 57 | * automatically register it as the {@code destroyMethod}. This 'destroy method 58 | * inference' is currently limited to detecting only public, no-arg methods named 59 | * 'close' or 'shutdown'. The method may be declared at any level of the inheritance 60 | * hierarchy and will be detected regardless of the return type of the {@code @Bean} 61 | * method (i.e., detection occurs reflectively against the bean instance itself at 62 | * creation time). 63 | *

To disable destroy method inference for a particular {@code @Bean}, specify an 64 | * empty string as the value, e.g. {@code @Bean(destroyMethod="")}. Note that the 65 | * {@link org.springframework.beans.factory.DisposableBean} and the 66 | * {@link java.io.Closeable}/{@link java.lang.AutoCloseable} interfaces will 67 | * nevertheless get detected and the corresponding destroy/close method invoked. 68 | *

Note: Only invoked on beans whose lifecycle is under the full control of the 69 | * factory, which is always the case for singletons but not guaranteed for any 70 | * other scope. 71 | * @see org.springframework.context.ConfigurableApplicationContext#close() 72 | */ 73 | String destroyMethod() default AbstractBeanDefinition.INFER_METHOD; 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/com/github/scalaspring/akka/ActorComponent.java: -------------------------------------------------------------------------------- 1 | package com.github.scalaspring.akka; 2 | 3 | import org.springframework.beans.factory.config.ConfigurableBeanFactory; 4 | import org.springframework.context.annotation.Scope; 5 | import org.springframework.stereotype.Component; 6 | 7 | import java.lang.annotation.ElementType; 8 | import java.lang.annotation.Retention; 9 | import java.lang.annotation.RetentionPolicy; 10 | import java.lang.annotation.Target; 11 | 12 | /** 13 | * Annotate actor-specific beans classes. 14 | * 15 | * This is a Spring meta-annotation, the purpose of which is to make code more readable and to avoid 16 | * the common mistake of instantiating actors as singletons (the default for Spring beans). 17 | */ 18 | @Component 19 | @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) 20 | @Target({ElementType.TYPE}) 21 | @Retention(RetentionPolicy.RUNTIME) 22 | public @interface ActorComponent { 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/github/scalaspring/akka/config/AkkaConfigAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.scalaspring.akka.config; 2 | 3 | import com.typesafe.config.Config; 4 | import com.typesafe.config.ConfigFactory; 5 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 6 | import org.springframework.context.ConfigurableApplicationContext; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.ComponentScan; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; 11 | import org.springframework.core.env.ConfigurableEnvironment; 12 | 13 | import java.util.Map; 14 | 15 | import static com.github.scalaspring.akka.config.AkkaConfigPropertySourceAdapter.convertIndexedProperties; 16 | import static com.github.scalaspring.akka.config.AkkaConfigPropertySourceAdapter.getPropertyMap; 17 | 18 | /** 19 | * Links Spring environment (property sources) and Akka configuration (Config). 20 | * This allows Akka configuration to be specified via standard Spring-based property source(s) and also allows 21 | * Spring to access Akka-based defaults. 22 | * 23 | * This configuration provides the following: 24 | * 25 | * 1. An Akka configuration bean populated with Akka defaults, overridden by Spring defined properties. This allows 26 | * Akka configuration to be specified via any Spring supported property source (files, URLs, YAML, etc.) while 27 | * still leveraging Akka defaults when not specified. 28 | * 2. An optional property sources placeholder configurer. Created if not defined elsewhere, this bean processes @Value 29 | * annotations that reference properties. 30 | * 31 | * Note: This class is intentionally written in Java to enable use in Java-only projects and due to the fact that 32 | * a static factory method is required for the {@code PropertySourcesPlaceholderConfigurer} bean. Do not instantiate 33 | * this bean in a non-static method, especially in configurations that have autowired properties. You've been warned. 34 | */ 35 | @Configuration 36 | @ComponentScan 37 | public class AkkaConfigAutoConfiguration { 38 | 39 | /** 40 | * Creates an Akka configuration populated via Spring properties with fallback to Akka defined properties. 41 | */ 42 | @Bean 43 | public Config akkaConfig(ConfigurableApplicationContext applicationContext, ConfigurableEnvironment environment) { 44 | final Map converted = AkkaConfigPropertySourceAdapter.convertIndexedProperties(AkkaConfigPropertySourceAdapter.getPropertyMap(environment)); 45 | final Config defaultConfig = ConfigFactory.defaultReference(applicationContext.getClassLoader()); 46 | 47 | return ConfigFactory.parseMap(converted).withFallback(defaultConfig); 48 | } 49 | 50 | /** 51 | * Create placeholder configurer to support {@code @Value} annotations that reference environment properties. 52 | * 53 | * Only one placeholder configurer is required, hence the conditional 54 | */ 55 | @Bean @ConditionalOnMissingBean(PropertySourcesPlaceholderConfigurer.class) 56 | public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() { 57 | return new PropertySourcesPlaceholderConfigurer(); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/github/scalaspring/akka/config/AkkaConfigPropertySource.java: -------------------------------------------------------------------------------- 1 | package com.github.scalaspring.akka.config; 2 | 3 | import com.typesafe.config.Config; 4 | import com.typesafe.config.ConfigValue; 5 | import org.springframework.core.env.EnumerablePropertySource; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.Set; 11 | 12 | /** 13 | * Adapts Akka configuration properties to Spring by exposing them as a standard Spring EnumerablePropertySource. 14 | */ 15 | public class AkkaConfigPropertySource extends EnumerablePropertySource { 16 | 17 | public static final String PROPERTY_SOURCE_NAME = "akkaConfig"; 18 | 19 | public AkkaConfigPropertySource(Config config) { 20 | super(PROPERTY_SOURCE_NAME, config); 21 | } 22 | 23 | @Override 24 | public String[] getPropertyNames() { 25 | final Set> entries = source.entrySet(); 26 | final List names = new ArrayList<>(entries.size()); 27 | 28 | for (Map.Entry entry : entries) { 29 | names.add(entry.getKey()); 30 | } 31 | 32 | return names.toArray(new String[names.size()]); 33 | } 34 | 35 | @Override 36 | public Object getProperty(String name) { 37 | try { 38 | return source.hasPath(name) ? source.getAnyRef(name) : null; 39 | } catch (Throwable t) { 40 | // Catch bad path exceptions and return null 41 | return null; 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/github/scalaspring/akka/config/AkkaConfigPropertySourceAdapter.java: -------------------------------------------------------------------------------- 1 | package com.github.scalaspring.akka.config; 2 | 3 | import org.apache.commons.logging.Log; 4 | import org.apache.commons.logging.LogFactory; 5 | import org.springframework.core.env.ConfigurableEnvironment; 6 | import org.springframework.core.env.EnumerablePropertySource; 7 | import org.springframework.core.env.PropertySource; 8 | import org.springframework.core.env.StandardEnvironment; 9 | import org.springframework.util.CollectionUtils; 10 | 11 | import java.util.*; 12 | import java.util.regex.Matcher; 13 | import java.util.regex.Pattern; 14 | 15 | /** 16 | * Utility methods for adapting Spring properties to Akka Config. 17 | */ 18 | public class AkkaConfigPropertySourceAdapter { 19 | 20 | private static Log log = LogFactory.getLog(AkkaConfigPropertySourceAdapter.class); 21 | 22 | public static final Pattern INDEXED_PROPERTY_PATTERN = Pattern.compile("^\\s*(?\\w+(?:\\.\\w+)*)\\[(?\\d+)\\]\\s*$"); 23 | 24 | /** 25 | * Convert list properties specified in properties format into lists, replacing the original properties. 26 | * 27 | * Before: 28 | * list.property[0]=zero 29 | * list.property[1]=one 30 | * list.property[2]=two 31 | * After 32 | * list.property={@code List("zero", "one", "two")} 33 | * 34 | * @param propertyMap property map possibly containing indexed properties 35 | * @return property map (a copy) with indexed properties converted to lists 36 | */ 37 | @SuppressWarnings("unchecked") 38 | public static Map convertIndexedProperties(Map propertyMap) { 39 | 40 | if (CollectionUtils.isEmpty(propertyMap)) { 41 | return Collections.emptyMap(); 42 | } 43 | 44 | final Map converted = new LinkedHashMap<>(); 45 | 46 | for (Map.Entry entry: propertyMap.entrySet()) { 47 | final Matcher m = INDEXED_PROPERTY_PATTERN.matcher(entry.getKey()); 48 | final String path = m.matches() ? m.group("path") : entry.getKey(); 49 | final Object existing = converted.get(path); 50 | 51 | // Make sure properties are either list-based or normal 52 | if ((existing != null) && ((m.matches() && !(existing instanceof List)) || (!m.matches() && !(existing instanceof String)))) { 53 | throw new IllegalArgumentException("Invalid property hierarchy - property must be either a regular or a list-based property (path:" + path + ", existing: " + existing + ", key:" + entry.getKey() + ", value:" + entry.getValue()); 54 | } 55 | 56 | // Convert indexed properties to lists 57 | if (m.matches()) { 58 | 59 | final int index = Integer.parseInt(m.group("index")); 60 | final ArrayList list = converted.containsKey(path) ? (ArrayList) converted.get(path) : new ArrayList(); 61 | 62 | // Extend the list, if necessary 63 | list.ensureCapacity(index + 1); 64 | while (list.size() <= index) { 65 | list.add(null); 66 | } 67 | 68 | list.set(index, entry.getValue()); 69 | converted.put(path, list); 70 | 71 | } else { 72 | // Copy normal properties 73 | converted.put(entry.getKey(), entry.getValue()); 74 | } 75 | } 76 | 77 | // Make sure any contained lists are immutable 78 | for (Map.Entry entry : converted.entrySet()) { 79 | if (entry.getValue() instanceof List) { 80 | entry.setValue(Collections.unmodifiableList((List) entry.getValue())); 81 | } 82 | } 83 | 84 | return Collections.unmodifiableMap(converted); 85 | } 86 | 87 | public static Map getPropertyMap(ConfigurableEnvironment environment) { 88 | final Map propertyMap = new HashMap<>(); 89 | 90 | for (final PropertySource source : environment.getPropertySources()) { 91 | if (isEligiblePropertySource(source)) { 92 | final EnumerablePropertySource enumerable = (EnumerablePropertySource) source; 93 | 94 | log.debug("Adding properties from property source " + source.getName()); 95 | 96 | for (final String name : enumerable.getPropertyNames()) { 97 | if (isEligibleProperty(name) && !propertyMap.containsKey(name)) { 98 | if (log.isTraceEnabled()) log.trace("Adding property " + name); 99 | propertyMap.put(name, environment.getProperty(name)); 100 | } 101 | } 102 | } 103 | } 104 | 105 | return Collections.unmodifiableMap(propertyMap); 106 | } 107 | 108 | public static boolean isEligiblePropertySource(PropertySource source) { 109 | // Eliminate system environment properties and system properties sources 110 | // since these sources are already included in the default configuration 111 | final String name = source.getName(); 112 | return (source instanceof EnumerablePropertySource) && 113 | !( 114 | name.equalsIgnoreCase(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME) || 115 | name.equalsIgnoreCase(StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME) || 116 | name.equalsIgnoreCase(AkkaConfigPropertySource.PROPERTY_SOURCE_NAME) 117 | ); 118 | } 119 | 120 | public static boolean isEligibleProperty(String name) { 121 | return !name.startsWith("spring."); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/com/github/scalaspring/akka/config/AkkaConfigPropertySourceBeanFactoryPostProcessor.java: -------------------------------------------------------------------------------- 1 | package com.github.scalaspring.akka.config; 2 | 3 | import com.typesafe.config.ConfigFactory; 4 | import org.apache.commons.logging.Log; 5 | import org.apache.commons.logging.LogFactory; 6 | import org.springframework.beans.BeansException; 7 | import org.springframework.beans.factory.config.BeanFactoryPostProcessor; 8 | import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; 9 | import org.springframework.context.ApplicationContext; 10 | import org.springframework.context.ApplicationContextAware; 11 | import org.springframework.context.EnvironmentAware; 12 | import org.springframework.core.env.ConfigurableEnvironment; 13 | import org.springframework.core.env.Environment; 14 | import org.springframework.stereotype.Component; 15 | 16 | /** 17 | * Adds Akka configuration as a property source in the current application context. 18 | * This class is NOT for direct use by developers. 19 | * 20 | * Usage: Import the AkkaAutoConfiguration configuration to create an instance of this class via component scanning. 21 | * 22 | * @see com.github.scalaspring.akka.AkkaAutoConfiguration 23 | */ 24 | @Component 25 | public class AkkaConfigPropertySourceBeanFactoryPostProcessor implements BeanFactoryPostProcessor, ApplicationContextAware, EnvironmentAware { 26 | 27 | private static Log log = LogFactory.getLog(AkkaConfigPropertySourceBeanFactoryPostProcessor.class); 28 | 29 | protected ApplicationContext applicationContext = null; 30 | protected ConfigurableEnvironment environment = null; 31 | 32 | @Override 33 | public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { 34 | log.debug("Adding Akka Config property source to bean factory"); 35 | environment.getPropertySources().addLast(new AkkaConfigPropertySource(ConfigFactory.load(applicationContext.getClassLoader()))); 36 | } 37 | 38 | @Override 39 | public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 40 | this.applicationContext = applicationContext; 41 | } 42 | 43 | @Override 44 | public void setEnvironment(Environment environment) { 45 | this.environment = (ConfigurableEnvironment) environment; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/scala/com/github/scalaspring/akka/ActorSystemBeanPostProcessor.scala: -------------------------------------------------------------------------------- 1 | package com.github.scalaspring.akka 2 | 3 | import akka.actor.ActorSystem 4 | import org.springframework.beans.factory.config.BeanPostProcessor 5 | import org.springframework.context.ConfigurableApplicationContext 6 | 7 | /** 8 | * Registers {@code SpringExtension} on any actor system created in the application context. 9 | */ 10 | class ActorSystemBeanPostProcessor(applicationContext: ConfigurableApplicationContext) extends BeanPostProcessor with SpringLogging { 11 | 12 | override def postProcessBeforeInitialization(bean: AnyRef, beanName: String): AnyRef = { 13 | bean match { 14 | case system: ActorSystem => { 15 | log.info(s"Registering extension ${SpringExtension.getClass.getSimpleName} on actor system ${system.name}") 16 | system.registerExtension(SpringExtension).applicationContext = applicationContext 17 | bean 18 | } 19 | case _ => bean 20 | } 21 | } 22 | 23 | override def postProcessAfterInitialization(bean: AnyRef, beanName: String): AnyRef = bean 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/scala/com/github/scalaspring/akka/ActorSystemConfiguration.scala: -------------------------------------------------------------------------------- 1 | package com.github.scalaspring.akka 2 | 3 | import akka.actor.ActorSystem 4 | import org.springframework.beans.factory.annotation.Autowired 5 | 6 | /** 7 | * Extend this trait to add actor reference creation helper methods to any Spring configuration. 8 | */ 9 | trait ActorSystemConfiguration extends SpringActorRefFactory { 10 | 11 | @Autowired 12 | protected implicit val factory: ActorSystem = null 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/scala/com/github/scalaspring/akka/ActorSystemLifecycle.scala: -------------------------------------------------------------------------------- 1 | package com.github.scalaspring.akka 2 | 3 | import akka.actor.ActorSystem 4 | import org.springframework.beans.factory.annotation.Value 5 | import org.springframework.context.SmartLifecycle 6 | 7 | 8 | object ActorSystemLifecycle { 9 | def apply(actorSystem: ActorSystem) = new ActorSystemLifecycle(actorSystem) 10 | } 11 | 12 | /** 13 | * Shuts down the actor system when the application context is stopped. 14 | * 15 | * The lifecycle phase (default -10) can be adjusted by setting the akka.actorSystem.lifecycle.phase configuration 16 | * property. Note that the phase MUST be less than any beans that depend on the actor system to ensure that the 17 | * actor system is shut down after any dependent beans. 18 | */ 19 | class ActorSystemLifecycle(actorSystem: ActorSystem) extends SmartLifecycle with SpringLogging { 20 | 21 | override def isAutoStartup: Boolean = true 22 | 23 | @Value("${akka.actorSystem.lifecycle.phase:-10}") 24 | protected val phase: Int = -10 25 | override def getPhase: Int = phase 26 | 27 | override def isRunning: Boolean = !actorSystem.whenTerminated.isCompleted 28 | 29 | // Do nothing since the actor system is already started once created 30 | override def start() = {} 31 | 32 | override def stop(callback: Runnable): Unit = { 33 | if (actorSystem.whenTerminated.isCompleted) { 34 | log.warn(s"Actor system ${actorSystem.name} already terminated") 35 | callback.run() 36 | } else { 37 | log.info(s"Terminating actor system ${actorSystem.name}") 38 | actorSystem.registerOnTermination(callback) 39 | actorSystem.registerOnTermination { log.info(s"Actor system ${actorSystem.name} terminated") } 40 | actorSystem.terminate() 41 | } 42 | } 43 | 44 | // Leave this method not implemented since it shouldn't be called (the async stop() method should be called instead) 45 | final override def stop(): Unit = ??? 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/scala/com/github/scalaspring/akka/AkkaAutoConfiguration.scala: -------------------------------------------------------------------------------- 1 | package com.github.scalaspring.akka 2 | 3 | import akka.actor.{ActorSystem, Deploy, Props} 4 | import com.github.scalaspring.akka.config.AkkaConfigAutoConfiguration 5 | import com.typesafe.config.Config 6 | import org.springframework.beans.factory.annotation.Autowired 7 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean 8 | import org.springframework.context.ConfigurableApplicationContext 9 | import org.springframework.context.annotation.{Bean, Configuration, Import} 10 | 11 | import scala.concurrent.ExecutionContext 12 | 13 | /** 14 | * Configures Akka for use in a Spring application using reasonable defaults. 15 | * Most beans in this configuration are conditional and will only be defined if not already present in the context. 16 | * This allows user-defined configuration to override defaults. 17 | * 18 | * This configuration provides the following: 19 | * 1. A default actor system based on the supplied Akka Config. A single actor system is presumed sufficient 20 | * for most applications. 21 | * 2. A SmartLifecycle bean that shuts down the actor system when the application context is stopped. This matches 22 | * the lifetime of the actor system to the containing application context. 23 | * 3. A baseline Deploy instance used as the starting point for actors created within the application context. 24 | * 25 | * @see ActorRefFactory 26 | */ 27 | @Configuration 28 | @Import(Array(classOf[AkkaConfigAutoConfiguration])) 29 | class AkkaAutoConfiguration(actorSystemName: String) extends SpringLogging { 30 | 31 | def this() = this("default") 32 | 33 | @Autowired 34 | val applicationContext: ConfigurableApplicationContext = null 35 | 36 | @Autowired(required = false) 37 | val executionContext: ExecutionContext = null 38 | 39 | /** 40 | * Create a default actor system if none defined. 41 | * 42 | * Define a bean of type {@code ExecutionContext} to customize the actor system's execution context. If no such bean 43 | * is defined, the default Akka execution context will be used, which should be sufficient for most applications. 44 | * 45 | * Note that dispatchers can be customized on a per-actor basis, which should be used to segment workloads, as needed, 46 | * while maintaining a single actor system. 47 | * 48 | * Note: The {@code ActorSystem} trait includes a shutdown method that Spring would normally detect and call when 49 | * the application context is closed. The empty string destroyMethod {@code @Bean} annotation attribute disables this 50 | * detection to allow for explicit management of the actor system lifecycle via an {@code ActorSystemLifecycle} bean. 51 | * 52 | */ 53 | @Bean/*(destroyMethod = "")*/ @ConditionalOnMissingBean(Array(classOf[ActorSystem])) 54 | def actorSystem(config: Config): ActorSystem = { 55 | log.info(s"""Creating actor system "${actorSystemName}"""") 56 | ActorSystem(actorSystemName, Option(config), Option(applicationContext.getClassLoader), Option(executionContext)) 57 | } 58 | 59 | /** 60 | * Create a post processor to automatically register Spring extension on actor system(s). 61 | */ 62 | @Bean 63 | def actorSystemBeanPostProcessor = new ActorSystemBeanPostProcessor(applicationContext) 64 | 65 | /** 66 | * Create a lifecycle bean to shut down the actor system when the application context is stopped. 67 | */ 68 | @Bean @ConditionalOnMissingBean(Array(classOf[ActorSystemLifecycle])) 69 | def actorSystemLifecycle(actorSystem: ActorSystem): ActorSystemLifecycle = { 70 | log.info(s"""Creating lifecycle for actor system "${actorSystem.name}"""") 71 | ActorSystemLifecycle(actorSystem) 72 | } 73 | 74 | /** 75 | * Create a default actor Deploy instance if none defined. 76 | * 77 | * Define a bean of type {@code Deploy} to customize the deployment information used when creating actors. 78 | * 79 | * Note that deployment customization is supported at the following points. 80 | * For example, customizations defined during reference creation override those from a Deploy instance defined in the 81 | * application context. 82 | * 1. Application Context - Define a Deploy bean that will be used as the starting point for all actors 83 | * 2. Actor Component - Specify annotation properties, typically to allocate actors to specific dispatchers 84 | * 3. Actor Reference - Specify creation parameters to customize specific actor instances 85 | */ 86 | @Bean @ConditionalOnMissingBean(Array(classOf[Deploy])) 87 | def defaultDeploy = Props.defaultDeploy 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/main/scala/com/github/scalaspring/akka/AkkaAutowiredImplicits.scala: -------------------------------------------------------------------------------- 1 | package com.github.scalaspring.akka 2 | 3 | import akka.actor.ActorSystem 4 | import org.springframework.beans.factory.annotation.Autowired 5 | 6 | import scala.concurrent.ExecutionContextExecutor 7 | 8 | trait AkkaAutowiredImplicits { 9 | 10 | @Autowired implicit val system: ActorSystem = null 11 | @Autowired(required = false) private val _executor: ExecutionContextExecutor = null 12 | 13 | // executor property that defaults to the actor system's dispatcher if no executor bean defined in the application context 14 | implicit def executor: ExecutionContextExecutor = { 15 | _executor match { 16 | case null => system.dispatcher 17 | case _ => _executor 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/scala/com/github/scalaspring/akka/SpringActor.scala: -------------------------------------------------------------------------------- 1 | package com.github.scalaspring.akka 2 | 3 | import akka.actor.{Actor, ActorContext} 4 | 5 | /** 6 | * Extend this trait to add factory helper methods to actors. 7 | */ 8 | trait SpringActor extends Actor with SpringActorRefFactory { this: Actor => 9 | 10 | protected implicit val factory: ActorContext = context 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/scala/com/github/scalaspring/akka/SpringActorRefFactory.scala: -------------------------------------------------------------------------------- 1 | package com.github.scalaspring.akka 2 | 3 | import java.beans.Introspector 4 | 5 | import akka.actor._ 6 | 7 | import scala.reflect.ClassTag 8 | 9 | /** 10 | * This trait should NOT be used directly. Please use the configuration- ({@code ActorSystemConfiguration}) and 11 | * actor-specific ({@code SpringActor}) extensions instead. 12 | * 13 | * @see ActorSystemConfiguration 14 | * @see SpringActor 15 | */ 16 | private [akka] trait SpringActorRefFactory { 17 | 18 | /** 19 | * Creates a Spring-backed actor reference by type. 20 | * 21 | * Looks up the Spring bean of the given type, which is expected to be an Actor. 22 | * This factory method satisfies most use cases and is appropriate when a single actor of a given type is defined. 23 | * 24 | * Example: 25 | * 26 | * {{{ 27 | * @Bean 28 | * def myActor = actorOf[MyActor] 29 | * }}} 30 | * 31 | */ 32 | def actorOf[T <: Actor: ClassTag](implicit factory: ActorRefFactory): ActorRef = 33 | requireFactory(factory.actorOf(SpringProps[T])) 34 | 35 | /** 36 | * Creates a Spring-backed actor reference by type with constructor arguments. 37 | * 38 | * Looks up the Spring bean of the given type and constructor arguments (using Spring's {@code BeanFactory.getBean(class, args)}). 39 | * This factory method is typically used when the target actor needs to be customized per consumer during configuration. 40 | * 41 | * Example: 42 | * 43 | * {{{ 44 | * val someString = "some string" 45 | * 46 | * @Bean 47 | * def myActor = actorOf[MyActor](someString) 48 | * }}} 49 | * 50 | */ 51 | def actorOf[T <: Actor: ClassTag](args: Any*)(implicit factory: ActorRefFactory): ActorRef = 52 | requireFactory(factory.actorOf(SpringProps[T](args: _*))) 53 | 54 | /** 55 | * Creates a Spring-backed actor reference by name with optional constructor arguments. 56 | * 57 | * This factory method is typically used to disambiguate in the case of multiple beans defined for the same type. If 58 | * this is not the case, consider using the preferred type-based reference factory methods. 59 | * 60 | * Example: 61 | * 62 | * {{{ 63 | * @Bean 64 | * def myActor = actorOf("myActorBean2") 65 | * }}} 66 | * 67 | * @param beanName name of the underlying bean 68 | * @param args optional constructor arguments 69 | */ 70 | def actorOf(beanName: String, args: Any*)(implicit factory: ActorRefFactory): ActorRef = 71 | requireFactory(factory.actorOf(SpringProps(beanName, args: _*))) 72 | 73 | def actorOf(props: Props)(implicit factory: ActorRefFactory): ActorRef = 74 | requireFactory(factory.actorOf(props)) 75 | 76 | def actorOf(props: Props, name: String)(implicit factory: ActorRefFactory): ActorRef = 77 | requireFactory(factory.actorOf(props, name)) 78 | 79 | 80 | private def requireFactory(f: => ActorRef)(implicit factory: ActorRefFactory): ActorRef = { 81 | require(factory != null, "You cannot create actors from within a configuration constructor (using val or var). " + 82 | s"""Define a bean factory method annotated with the @Bean annotation instead. See the documentation for the ${classOf[SpringActorRefFactory].getSimpleName}.actorOf methods.""") 83 | f 84 | } 85 | 86 | protected def generateActorName[T <: Actor: ClassTag] = 87 | Introspector.decapitalize(implicitly[ClassTag[T]].runtimeClass.getSimpleName) 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/main/scala/com/github/scalaspring/akka/SpringExtension.scala: -------------------------------------------------------------------------------- 1 | package com.github.scalaspring.akka 2 | 3 | import akka.actor.{ExtendedActorSystem, Extension, ExtensionId, ExtensionIdProvider} 4 | import org.springframework.context.ConfigurableApplicationContext 5 | 6 | object SpringExtension extends ExtensionId[SpringExtension] with ExtensionIdProvider { 7 | 8 | override def lookup(): ExtensionId[_ <: Extension] = SpringExtension 9 | 10 | override def createExtension(system: ExtendedActorSystem): SpringExtension = new SpringExtension(system) 11 | 12 | } 13 | 14 | /** 15 | * Extension that holds the Spring application context. 16 | * 17 | * The applicationContext property is a write-once property that is set when creating the actor system bean 18 | * (via a bean post processor). 19 | * 20 | * @see ActorSystemBeanPostProcessor 21 | */ 22 | class SpringExtension(system: ExtendedActorSystem) extends Extension { 23 | 24 | private var _applicationContext: ConfigurableApplicationContext = null 25 | 26 | def applicationContext = _applicationContext 27 | 28 | def applicationContext_=(applicationContext: ConfigurableApplicationContext) { 29 | require(applicationContext != null, "applicationContext must not be null") 30 | if (_applicationContext != null) throw new IllegalStateException("application context already set") 31 | _applicationContext = applicationContext 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/scala/com/github/scalaspring/akka/SpringIndirectActorProducer.scala: -------------------------------------------------------------------------------- 1 | package com.github.scalaspring.akka 2 | 3 | import akka.actor.{Actor, IndirectActorProducer} 4 | import org.springframework.context.ConfigurableApplicationContext 5 | 6 | import scala.collection.immutable 7 | 8 | object SpringIndirectActorProducer { 9 | 10 | def getBeanNameForType(applicationContext: ConfigurableApplicationContext, clazz: Class[_]): String = { 11 | val beanNames = applicationContext.getBeanNamesForType(clazz) 12 | if (beanNames.length > 1) throw new IllegalArgumentException(s"Multiple beans found for actor class ${clazz.getName} (${beanNames}}). Please use name-based constructor to specify bean name to use.") 13 | beanNames.headOption.orElse(throw new IllegalArgumentException(s"No bean defined for actor class ${clazz.getName}")).get 14 | } 15 | 16 | def getTypeForBeanName(applicationContext: ConfigurableApplicationContext, beanName: String): Class[_ <: Actor] = { 17 | applicationContext.getBeanFactory.getType(beanName).asInstanceOf[Class[Actor]] 18 | } 19 | } 20 | 21 | import SpringIndirectActorProducer._ 22 | 23 | class SpringIndirectActorProducer(clazz: Class[_ <: Actor], applicationContext: ConfigurableApplicationContext, beanName: String, args: immutable.Seq[AnyRef]) extends IndirectActorProducer { 24 | 25 | def this(clazz: Class[_ <: Actor], applicationContext: ConfigurableApplicationContext, args: immutable.Seq[AnyRef]) = 26 | this(clazz, applicationContext, getBeanNameForType(applicationContext, clazz), args) 27 | 28 | def this(beanName: String, applicationContext: ConfigurableApplicationContext, args: immutable.Seq[AnyRef]) = 29 | this(getTypeForBeanName(applicationContext, beanName), applicationContext, beanName, args) 30 | 31 | validateActorBeanDefinition 32 | 33 | protected def validateActorBeanDefinition: Unit = { 34 | val beanClass = applicationContext.getBeanFactory.getType(beanName) 35 | val beanDefinition = applicationContext.getBeanFactory.getBeanDefinition(beanName) 36 | 37 | require(actorClass.isAssignableFrom(beanClass), s"""Invalid bean type. Bean "${beanName}" of type ${beanClass.getSimpleName} does not extend ${actorClass.getSimpleName}.""") 38 | require(!beanDefinition.isSingleton, s"""Actor beans must be non-singleton. Suggested fix: Annotate ${beanDefinition.getBeanClassName} with the @${classOf[ActorComponent].getSimpleName} annotation to create actor beans with prototype scope.""") 39 | // TODO: Validate actor constructor if arguments supplied to enable fail fast (see akka.util.Reflect.findConstructor) 40 | } 41 | 42 | override def actorClass: Class[_ <: Actor] = clazz 43 | 44 | override def produce(): Actor = { 45 | args match { 46 | case s if s.isEmpty => applicationContext.getBean(beanName).asInstanceOf[Actor] 47 | case _ => applicationContext.getBean(beanName, args: _*).asInstanceOf[Actor] 48 | } 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/scala/com/github/scalaspring/akka/SpringLogging.scala: -------------------------------------------------------------------------------- 1 | package com.github.scalaspring.akka 2 | 3 | import org.apache.commons.logging.{Log, LogFactory} 4 | import org.springframework.util.ClassUtils 5 | 6 | trait SpringLogging { 7 | // Remove any CGLIB gunk to clean up logging 8 | protected val log: Log = LogFactory.getLog(ClassUtils.getUserClass(getClass)) 9 | } 10 | -------------------------------------------------------------------------------- /src/main/scala/com/github/scalaspring/akka/SpringProps.scala: -------------------------------------------------------------------------------- 1 | package com.github.scalaspring.akka 2 | 3 | import akka.actor._ 4 | import org.springframework.context.ConfigurableApplicationContext 5 | 6 | import scala.reflect.ClassTag 7 | 8 | /** 9 | * Adapter class to create standard Akka Props backed by Spring beans. 10 | * Retrieves application context from the implicit ActorRefFactory (either the actor system or an actor). 11 | * 12 | * NOTE: This class is typically NOT used directly, but rather through the {@code SpringActorRefFactory} trait. 13 | * 14 | * @see SpringActorRefFactory 15 | * @see ActorSystemConfiguration 16 | * @see SpringActor 17 | */ 18 | object SpringProps { 19 | 20 | def apply[T <: Actor: ClassTag](implicit factory: ActorRefFactory): Props = apply(implicitly[ClassTag[T]].runtimeClass) 21 | 22 | def apply[T <: Actor: ClassTag](args: Any*)(implicit factory: ActorRefFactory): Props = apply(implicitly[ClassTag[T]].runtimeClass, args: _*) 23 | 24 | def apply(clazz: Class[_], args: Any*)(implicit factory: ActorRefFactory): Props = Props(classOf[SpringIndirectActorProducer], clazz, getApplicationContext(factory), args.toList) 25 | 26 | def apply(beanName: String, args: Any*)(implicit factory: ActorRefFactory): Props = Props(classOf[SpringIndirectActorProducer], beanName, getApplicationContext(factory), args.toList) 27 | 28 | private def getApplicationContext(factory: ActorRefFactory): ConfigurableApplicationContext = 29 | Option(getActorSystem(factory).extension(SpringExtension).applicationContext).getOrElse(throw new IllegalStateException(s"Extension ${SpringExtension.getClass.getSimpleName} not initialized; application context is null")) 30 | 31 | private def getActorSystem(factory: ActorRefFactory): ActorSystem = factory match { 32 | case system: ActorSystem => system 33 | case context: ActorContext => context.system 34 | case _ => throw new IllegalArgumentException("unexpected ActorRefFactory type") 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/test/resources/AkkaConfigAutoConfigurationSpec.properties: -------------------------------------------------------------------------------- 1 | # Single-valued properties 2 | single.string=one 3 | single.int=1 4 | single.double=1.1 5 | single.boolean=true 6 | single.address=1.1.1.1 7 | 8 | # List-valued properties 9 | list.string[0]=first 10 | list.string[1]=second 11 | 12 | list.address[0]=1.1.1.1 13 | list.address[1]=2.2.2.2 14 | 15 | akka.test.default-timeout=10s -------------------------------------------------------------------------------- /src/test/resources/application.yml: -------------------------------------------------------------------------------- 1 | 2 | 3 | http: 4 | server: 5 | port: 8081 6 | -------------------------------------------------------------------------------- /src/test/scala/com/github/scalaspring/akka/AkkaAutoConfigurationSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.scalaspring.akka 2 | 3 | import akka.actor.{Actor, ActorRef} 4 | import akka.pattern.AskSupport 5 | import akka.testkit.TestActors.EchoActor 6 | import akka.util.Timeout 7 | import com.github.scalaspring.akka.AkkaAutoConfigurationSpec.KeyValueProtocol 8 | import com.github.scalaspring.scalatest.TestContextManagement 9 | import com.typesafe.scalalogging.StrictLogging 10 | import org.scalatest.concurrent.ScalaFutures 11 | import org.scalatest.{FlatSpec, Matchers} 12 | import org.springframework.beans.factory.annotation.Autowired 13 | import org.springframework.boot.test.SpringApplicationContextLoader 14 | import org.springframework.context.annotation.{Bean, ComponentScan, Import} 15 | import org.springframework.stereotype.Component 16 | import org.springframework.test.context.ContextConfiguration 17 | 18 | import scala.concurrent.duration._ 19 | 20 | @ContextConfiguration( 21 | loader = classOf[SpringApplicationContextLoader], 22 | classes = Array(classOf[AkkaAutoConfigurationSpec.Configuration]) 23 | ) 24 | class AkkaAutoConfigurationSpec extends FlatSpec with TestContextManagement with Matchers with AskSupport with ScalaFutures with StrictLogging { 25 | 26 | import KeyValueProtocol._ 27 | 28 | import scala.concurrent.ExecutionContext.Implicits.global 29 | implicit val timeout: Timeout = (1.seconds) 30 | 31 | @Autowired val echoActor: ActorRef = null 32 | @Autowired val forwardingActor: ActorRef = null 33 | @Autowired val keyValueActor: ActorRef = null 34 | 35 | 36 | "Echo actor" should "receive and echo message" in { 37 | val message = "test message" 38 | val future = echoActor ? message 39 | 40 | whenReady(future) { result => 41 | logger.info(s"""received result "$result"""") 42 | result should equal(message) 43 | } 44 | } 45 | 46 | "Forwarding actor" should "receive and forward message" in { 47 | val message = "test message" 48 | val future = forwardingActor ? message 49 | 50 | whenReady(future) { result => 51 | logger.info(s"""received result "$result"""") 52 | result should equal(message) 53 | } 54 | } 55 | 56 | "Key value actor" should "put and get value by key" in { 57 | val (key, value) = ("someKey", "someValue") 58 | 59 | def checkResult(result: Any) = { 60 | result match { 61 | case Entry(k, v) => { 62 | logger.info(s"""received entry "$result"""") 63 | k shouldBe key 64 | v shouldBe Some(value) 65 | } 66 | } 67 | } 68 | 69 | // First put then get and verify same value retrieved 70 | val putFuture = keyValueActor ? Put(key, value) 71 | whenReady(putFuture)(checkResult _) 72 | 73 | val getFuture = putFuture.flatMap { case Entry(k, v) => keyValueActor ? Get(k) } 74 | whenReady(getFuture)(checkResult _) 75 | 76 | } 77 | 78 | } 79 | 80 | 81 | object AkkaAutoConfigurationSpec { 82 | 83 | @Configuration 84 | @ComponentScan 85 | @Import(Array(classOf[AkkaAutoConfiguration])) 86 | class Configuration extends ActorSystemConfiguration { 87 | 88 | @ActorBean 89 | def echoActorBean = new EchoActor() 90 | 91 | @Bean 92 | def echoActor = actorOf[EchoActor] 93 | 94 | @Bean 95 | def echoActor2 = actorOf[EchoActor] 96 | 97 | @Bean 98 | def forwardingActor(echoActor: ActorRef) = actorOf[ForwardingActor](echoActor) 99 | 100 | @Bean 101 | def parentActor = actorOf(SpringProps[ParentActor], "parent") 102 | 103 | @Bean 104 | def keyValueActor = actorOf[KeyValueActor] 105 | 106 | } 107 | 108 | /** 109 | * Demonstrates an actor with a constructor parameter. 110 | * 111 | * @param nextActor the forwarding destination for all received messages 112 | */ 113 | @ActorComponent 114 | class ForwardingActor(nextActor: ActorRef) extends Actor with StrictLogging { 115 | override def receive = { 116 | case message => { 117 | logger.info(s"Forwarding message $message to $nextActor") 118 | nextActor forward message 119 | } 120 | } 121 | } 122 | 123 | /** 124 | * Demonstrates a parent/child actor. 125 | */ 126 | @ActorComponent 127 | class ParentActor extends SpringActor with StrictLogging { 128 | 129 | val child = actorOf[EchoActor] 130 | 131 | logger.info(s"parent actor path: ${self.path}") 132 | logger.info(s"child actor path: ${child.path}") 133 | 134 | override def receive = { 135 | case message => { 136 | logger.info(s"Forwarding message $message to child") 137 | child forward message 138 | } 139 | } 140 | } 141 | 142 | @Component 143 | class KeyValueStore extends collection.mutable.HashMap[String, String] with StrictLogging 144 | 145 | object KeyValueProtocol { 146 | sealed trait Message 147 | case class Put(key: String, value: String) extends Message 148 | case class Get(key: String) extends Message 149 | case class Entry(key: String, value: Option[String]) extends Message 150 | } 151 | 152 | /** 153 | * Demonstrates constructor injection using Autowired annotation. 154 | */ 155 | @ActorComponent 156 | class KeyValueActor @Autowired() (val store: KeyValueStore) extends Actor with StrictLogging { 157 | 158 | import KeyValueProtocol._ 159 | 160 | logger.info("Starting up key value store actor...") 161 | 162 | override def receive = { 163 | case put: Put => { 164 | logger.info(s"Putting value into key value store ($put)") 165 | store.put(put.key, put.value) 166 | sender() ! Entry(put.key, Some(put.value)) 167 | } 168 | case get: Get => { 169 | logger.info(s"Getting value from key value store ($get)") 170 | sender() ! Entry(get.key, store.get(get.key)) 171 | } 172 | case _ => logger.info("Received unknown message") 173 | } 174 | } 175 | 176 | } 177 | 178 | -------------------------------------------------------------------------------- /src/test/scala/com/github/scalaspring/akka/config/AkkaConfigAutoConfigurationSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.scalaspring.akka.config 2 | 3 | import java.net.URL 4 | 5 | import com.github.scalaspring.scalatest.TestContextManagement 6 | import com.typesafe.config.{Config, ConfigFactory} 7 | import com.typesafe.scalalogging.StrictLogging 8 | import org.scalatest.{FlatSpec, Matchers} 9 | import org.springframework.beans.factory.annotation.{Autowired, Value} 10 | import org.springframework.boot.test.SpringApplicationContextLoader 11 | import org.springframework.context.annotation._ 12 | import org.springframework.core.env.ConfigurableEnvironment 13 | import org.springframework.test.context.ContextConfiguration 14 | 15 | @ContextConfiguration( 16 | loader = classOf[SpringApplicationContextLoader], 17 | classes = Array(classOf[AkkaConfigAutoConfigurationSpec.Configuration]) 18 | ) 19 | class AkkaConfigAutoConfigurationSpec extends FlatSpec with TestContextManagement with Matchers with StrictLogging { 20 | 21 | import AkkaConfigAutoConfigurationSpec._ 22 | 23 | @Autowired val environment: ConfigurableEnvironment = null 24 | 25 | @Autowired val config: Config = null 26 | 27 | @Autowired val testUrl: URL = null 28 | 29 | val defaultConfig: Config = ConfigFactory.defaultReference 30 | 31 | @Value("${single.string}") 32 | val singleString: String = null 33 | 34 | @Value("${java.vm.version}") 35 | val javaVmVersion: String = null 36 | 37 | "System properties" should "be accessible" in { 38 | //logger.info(System.getProperties.keySet.toString) 39 | //logger.info(defaultConfig.entrySet().asScala.map(_.getKey).toSeq.sorted.toString) 40 | javaVmVersion should not be null 41 | javaVmVersion shouldBe System.getProperty("java.vm.version") 42 | } 43 | 44 | "Autowired dependency" should "be injected" in { 45 | testUrl shouldBe TEST_URL 46 | } 47 | 48 | "Spring configuration" should "override Akka default configuration" in { 49 | val propertyName = "akka.test.default-timeout" 50 | 51 | val defaultTimeout = defaultConfig.getString(propertyName) 52 | val springTimeout = environment.getProperty(propertyName) 53 | val configTimeout = config.getString(propertyName) 54 | 55 | defaultTimeout should not be null 56 | springTimeout should not be null 57 | 58 | defaultTimeout should not equal springTimeout 59 | configTimeout shouldBe springTimeout 60 | } 61 | 62 | } 63 | 64 | 65 | object AkkaConfigAutoConfigurationSpec { 66 | 67 | val TEST_URL = new URL("http://google.com") 68 | 69 | @Import(Array(classOf[AkkaConfigAutoConfiguration])) 70 | @PropertySource(Array("classpath:AkkaConfigAutoConfigurationSpec.properties")) 71 | @Configuration 72 | class Configuration { 73 | 74 | @Bean 75 | def testUrl = TEST_URL 76 | 77 | } 78 | 79 | } 80 | 81 | -------------------------------------------------------------------------------- /src/test/scala/com/github/scalaspring/akka/config/AkkaConfigPropertySourceAdapterPatternSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.scalaspring.akka.config 2 | 3 | import java.util.regex.Matcher 4 | 5 | import com.typesafe.scalalogging.StrictLogging 6 | import org.scalatest.prop.TableDrivenPropertyChecks._ 7 | import org.scalatest.{FlatSpec, Matchers} 8 | 9 | class AkkaConfigPropertySourceAdapterPatternSpec extends FlatSpec with Matchers with StrictLogging { 10 | 11 | val indexed = Table( 12 | ("name", "path", "index"), 13 | ("x[0]", "x", 0), 14 | ("someProperty[0]", "someProperty", 0), 15 | ("some_property[1]", "some_property", 1), 16 | ("some.property[0]", "some.property", 0), 17 | (" some.property[0] ", "some.property", 0), 18 | ("some.other.property[893]", "some.other.property", 893) 19 | ) 20 | 21 | val nonIndexed = Table( 22 | ("name"), 23 | ("x"), 24 | ("someProperty"), 25 | ("some_property"), 26 | ("some.property"), 27 | ("some.other.property") 28 | ) 29 | 30 | "Indexed property regular expression" should "match indexed property names" in { 31 | forAll (indexed) { (name: String, path: String, index: Int) => 32 | val m: Matcher = AkkaConfigPropertySourceAdapter.INDEXED_PROPERTY_PATTERN.matcher(name) 33 | m.matches() shouldBe true 34 | m.group("path") shouldEqual path 35 | m.group("index") shouldEqual index.toString 36 | } 37 | } 38 | 39 | it should "not match non-indexed property names" in { 40 | forAll (nonIndexed) { (name: String) => 41 | val m: Matcher = AkkaConfigPropertySourceAdapter.INDEXED_PROPERTY_PATTERN.matcher(name) 42 | m.matches() shouldBe false 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/test/scala/com/github/scalaspring/akka/config/AkkaConfigPropertySourceAdapterSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.scalaspring.akka.config 2 | 3 | import java.util 4 | 5 | import com.typesafe.config.ConfigFactory 6 | import com.typesafe.scalalogging.StrictLogging 7 | import org.scalatest.{FlatSpec, Matchers} 8 | 9 | import scala.collection.JavaConverters._ 10 | 11 | class AkkaConfigPropertySourceAdapterSpec extends FlatSpec with Matchers with StrictLogging { 12 | 13 | val goodProperties = textToProperties( 14 | """|list[0]=zero 15 | |list[1]=one 16 | |list[2]=two 17 | |normal=normal""".stripMargin 18 | ) 19 | 20 | val badProperties = textToProperties( 21 | """|list[0]=zero 22 | |list[1]=one 23 | |list[2]=two 24 | |list=bad""".stripMargin 25 | ) 26 | 27 | def textToProperties(text: String): java.util.Map[String, String] = { 28 | text.lines.map { line => 29 | line.split('=') match { 30 | case Array(k, v) => (k, v) 31 | case _ => sys.error(s"invalid property format $line") 32 | } 33 | }.foldLeft(new java.util.LinkedHashMap[String, String]())((m, t) => { m.put(t._1, t._2); m }) 34 | } 35 | 36 | def validateListProperty(list: java.util.List[String]): Unit = { 37 | list should have size 3 38 | list.get(0) shouldBe "zero" 39 | list.get(1) shouldBe "one" 40 | list.get(2) shouldBe "two" 41 | } 42 | 43 | "Indexed properties" should "be converted to a list" in { 44 | 45 | val converted: java.util.Map[String, AnyRef] = AkkaConfigPropertySourceAdapter.convertIndexedProperties(goodProperties) 46 | val list = converted.get("list").asInstanceOf[java.util.List[String]] 47 | 48 | converted.keySet should have size 2 49 | converted.get("normal") shouldBe "normal" 50 | 51 | validateListProperty(list) 52 | } 53 | 54 | "Overlapping (bad) property hierarchy" should "throw exception" in { 55 | 56 | an [IllegalArgumentException] should be thrownBy { 57 | AkkaConfigPropertySourceAdapter.convertIndexedProperties(badProperties) 58 | } 59 | 60 | // Exception should be thrown regardless of property order 61 | val reversed = new util.LinkedHashMap[String, String]() 62 | badProperties.entrySet().asScala.foreach { e => reversed.put(e.getKey, e.getValue) } 63 | 64 | an [IllegalArgumentException] should be thrownBy { 65 | AkkaConfigPropertySourceAdapter.convertIndexedProperties(reversed) 66 | } 67 | } 68 | 69 | "Akka Config" should "parse converted property map" in { 70 | val converted = AkkaConfigPropertySourceAdapter.convertIndexedProperties(goodProperties) 71 | val config = ConfigFactory.parseMap(converted) 72 | 73 | config.entrySet should have size 2 74 | config.hasPath("list") shouldBe true 75 | config.hasPath("normal") shouldBe true 76 | 77 | validateListProperty(config.getStringList("list")) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /version.sbt: -------------------------------------------------------------------------------- 1 | version := "0.3.1" --------------------------------------------------------------------------------