├── .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"
--------------------------------------------------------------------------------