├── .github
└── workflows
│ ├── fullTest.yml
│ └── pullCi.yml
├── .gitignore
├── .scalafmt.conf
├── LICENSE
├── README.md
├── Snapshots.md
├── build.sbt
├── core
└── src
│ ├── main
│ ├── resources
│ │ └── reference.conf
│ ├── scala-2.12
│ │ └── io
│ │ │ └── altoo
│ │ │ └── serialization
│ │ │ └── kryo
│ │ │ └── scala
│ │ │ ├── ScalaVersionSerializers.scala
│ │ │ └── serializer
│ │ │ └── ScalaCollectionSerializer.scala
│ ├── scala-2.13
│ │ └── io
│ │ │ └── altoo
│ │ │ └── serialization
│ │ │ └── kryo
│ │ │ └── scala
│ │ │ ├── ScalaVersionSerializers.scala
│ │ │ └── serializer
│ │ │ └── ScalaCollectionSerializer.scala
│ ├── scala-3
│ │ └── io
│ │ │ └── altoo
│ │ │ └── serialization
│ │ │ └── kryo
│ │ │ └── scala
│ │ │ ├── ScalaVersionSerializers.scala
│ │ │ └── serializer
│ │ │ ├── LazyValSerializer.scala
│ │ │ ├── ScalaCollectionSerializer.scala
│ │ │ └── ScalaEnumNameSerializer.scala
│ └── scala
│ │ └── io
│ │ └── altoo
│ │ └── serialization
│ │ └── kryo
│ │ └── scala
│ │ ├── DefaultKeyProvider.scala
│ │ ├── DefaultKryoInitializer.scala
│ │ ├── DefaultQueueBuilder.scala
│ │ ├── KryoSerializer.scala
│ │ ├── KryoSerializerBackend.scala
│ │ ├── ReflectionHelper.scala
│ │ ├── ScalaKryoSerializer.scala
│ │ ├── SerializerPool.scala
│ │ ├── Transformer.scala
│ │ └── serializer
│ │ ├── EnumerationNameSerializer.scala
│ │ ├── KryoClassResolver.scala
│ │ ├── ScalaKryo.scala
│ │ ├── ScalaMapSerializers.scala
│ │ ├── ScalaObjectSerializer.scala
│ │ ├── ScalaSetSerializers.scala
│ │ ├── ScalaUnitSerializer.scala
│ │ └── SubclassResolver.scala
│ └── test
│ ├── scala-2.12
│ └── io
│ │ └── altoo
│ │ └── serialization
│ │ └── kryo
│ │ └── scala
│ │ └── serializer
│ │ └── ScalaVersionRegistry.scala
│ ├── scala-2.13
│ └── io
│ │ └── altoo
│ │ └── serialization
│ │ └── kryo
│ │ └── scala
│ │ └── serializer
│ │ └── ScalaVersionRegistry.scala
│ ├── scala-3
│ └── io
│ │ └── altoo
│ │ ├── external
│ │ └── ExternalEnum.scala
│ │ └── serialization
│ │ └── kryo
│ │ └── scala
│ │ └── serializer
│ │ ├── LazyValSpec.scala
│ │ ├── ScalaEnumSerializationTest.scala
│ │ └── ScalaVersionRegistry.scala
│ └── scala
│ └── io
│ └── altoo
│ └── serialization
│ └── kryo
│ └── scala
│ ├── BasicSerializationTest.scala
│ ├── CompressionEffectivenessSerializationTest.scala
│ ├── CryptoCustomKeySerializationTest.scala
│ ├── CryptoSerializationTest.scala
│ ├── EnumSerializationTest.scala
│ ├── ParallelActorSystemSerializationTest.scala
│ ├── TransformationSerializationTest.scala
│ ├── performance
│ └── EnumPerformanceTests.scala
│ ├── serializer
│ ├── EnumerationSerializerTest.scala
│ ├── MapSerializerTest.scala
│ ├── ScalaKryoTest.scala
│ ├── ScalaObjectSerializerTest.scala
│ ├── ScalaUnitSerializerTest.scala
│ ├── SubclassResolverTest.scala
│ └── TupleSerializationTest.scala
│ └── testkit
│ └── AbstractKryoTest.scala
├── project
├── build.properties
└── plugins.sbt
├── sonatype.sbt
└── version.sbt
/.github/workflows/fullTest.yml:
--------------------------------------------------------------------------------
1 | name: Full test prior to release
2 |
3 | on:
4 | workflow_dispatch
5 |
6 | jobs:
7 | build:
8 | runs-on: ubuntu-24.04
9 | strategy:
10 | matrix:
11 | java: [ 11, 17, 21 ]
12 | steps:
13 | - name: Checkout
14 | uses: actions/checkout@v4
15 |
16 | - name: Set up JDK ${{ matrix.java }}
17 | uses: actions/setup-java@v4
18 | with:
19 | java-version: '${{ matrix.java }}'
20 | distribution: 'temurin'
21 |
22 | - name: Install sbt
23 | uses: sbt/setup-sbt@v1
24 |
25 | - name: Cache Coursier cache
26 | uses: coursier/cache-action@v6
27 |
28 | - name: Run tests with Scala 2.12,2.13,3.3
29 | run: sbt +test +doc
30 |
--------------------------------------------------------------------------------
/.github/workflows/pullCi.yml:
--------------------------------------------------------------------------------
1 | name: Scala CI
2 |
3 | on:
4 | workflow_dispatch:
5 | pull_request:
6 | branches: [ main ]
7 | paths-ignore:
8 | - '**.md'
9 |
10 | jobs:
11 | build:
12 | runs-on: ubuntu-24.04
13 | steps:
14 | - name: Checkout
15 | uses: actions/checkout@v4
16 |
17 | - name: Set up JDK 17
18 | uses: actions/setup-java@v4
19 | with:
20 | java-version: '17'
21 | distribution: 'temurin'
22 |
23 | - name: Install sbt
24 | uses: sbt/setup-sbt@v1
25 |
26 | - name: Cache Coursier cache
27 | uses: coursier/cache-action@v6
28 |
29 | - name: Run tests with Scala 2.12,2.13,3.3
30 | run: sbt +test
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.class
2 | *.log
3 |
4 | # sbt specific
5 | dist/*
6 | target/
7 | lib_managed/
8 | src_managed/
9 | project/boot/
10 | project/plugins/project/
11 | .metals
12 | .bloop
13 | .bsp
14 |
15 | # Scala-IDE specific
16 | .scala_dependencies
17 |
18 | # Intellij IDEA specific
19 | .idea/
20 |
21 | # VS Code
22 | .history
23 |
--------------------------------------------------------------------------------
/.scalafmt.conf:
--------------------------------------------------------------------------------
1 | version = 3.6.1
2 | runner.dialect = scala212source3
3 | fileOverride { ".sbt" {
4 | runner.dialect = sbt1
5 | } }
6 | project.git = true
7 | style = defaultWithAlign
8 | docstrings.style = Asterisk
9 | docstrings.wrap = false
10 | indentOperator.preset = spray
11 | maxColumn = 192
12 | lineEndings = preserve
13 | rewrite.rules = [RedundantParens, AvoidInfix, Imports]
14 | indentOperator.exemptScope = all
15 | align.preset = some
16 | align.tokens."+" = [
17 | {
18 | code = "~>"
19 | owners = [
20 | { regex = "Term.ApplyInfix" }
21 | ]
22 | }
23 | ]
24 | literals.hexDigits = upper
25 | literals.hexPrefix = lower
26 | binPack.unsafeCallSite = always
27 | binPack.unsafeDefnSite = always
28 | binPack.indentCallSiteSingleArg = false
29 | binPack.indentCallSiteOnce = true
30 | newlines.avoidForSimpleOverflow = [slc]
31 | newlines.source = keep
32 | newlines.beforeMultiline = keep
33 | align.openParenDefnSite = false
34 | align.openParenCallSite = false
35 | align.allowOverflow = true
36 | optIn.breakChainOnFirstMethodDot = false
37 | optIn.configStyleArguments = false
38 | danglingParentheses.preset = false
39 | spaces.inImportCurlyBraces = false
40 | rewrite.imports.expand = false
41 | rewrite.imports.sort = scalastyle
42 | rewrite.scala3.convertToNewSyntax = true
43 | rewrite.scala3.removeOptionalBraces = false
44 | rewrite.neverInfix.excludeFilters = [
45 | forward
46 | orElse
47 | and
48 | min
49 | max
50 | until
51 | to
52 | by
53 | eq
54 | ne
55 | "should.*"
56 | "contain.*"
57 | "must.*"
58 | in
59 | ignore
60 | be
61 | taggedAs
62 | thrownBy
63 | synchronized
64 | have
65 | when
66 | size
67 | only
68 | noneOf
69 | oneElementOf
70 | noElementsOf
71 | atLeastOneElementOf
72 | atMostOneElementOf
73 | allElementsOf
74 | inOrderElementsOf
75 | theSameElementsAs
76 | theSameElementsInOrderAs
77 | behavior
78 | of
79 | ]
80 | rewriteTokens = {
81 | "⇒": "=>"
82 | "→": "->"
83 | "←": "<-"
84 | }
85 | project.layout = StandardConvention
86 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | scala-kryo-serialization - kryo-based serializers for Scala
2 | =====================================================================
3 |
4 | Scala Kryo Serialization provides a convenient way of using Kryo with Scala and is the base for [Pekko Kryo Serialization](https://github.com/altoo-ag/pekko-kryo-serialization) providing the same functionality to pekko.
5 |
6 | =====================================================================
7 | [](https://github.com/altoo-ag/scala-kryo-serialization/actions/workflows/fullTest.yml)
8 | [](https://index.scala-lang.org/altoo-ag/scala-kryo-serialization/scala-kryo-serialization)
9 |
10 | This library provides custom [Kryo](https://github.com/EsotericSoftware/kryo)-based serializers for Scala. See [Pekko Kryo Serialization](https://github.com/altoo-ag/pekko-kryo-serialization) for serialization in Pekko.
11 |
12 | It can also be used for a general purpose and very efficient Kryo-based serialization
13 | of such Scala types like Option, Tuple, Enumeration and most of Scala's collection types.
14 |
15 |
16 | Features
17 | --------
18 |
19 | * It is more efficient than Java serialization - both in size and speed
20 | * Does not require any additional build steps like compiling proto files, when using protobuf serialization
21 | * Almost any Scala and Java class can be serialized using it without any additional configuration or code changes
22 | * Efficient serialization of such Scala types like Option, Tuple, Enumeration, most of Scala's collection types
23 | * Supports transparent AES encryption and different modes of compression
24 | * Apache 2.0 license
25 |
26 | Note that this serializer is mainly intended to be used for remoting and not for (long term) persisted data.
27 | The underlying kryo serializer does not guarantee compatibility between major versions.
28 |
29 |
30 | How to use this library in your project
31 | ---------------------------------------
32 |
33 | To use this serializer, you need to do two things:
34 |
35 | * Include a dependency on this library into your project:
36 | `libraryDependencies += "io.altoo" %% "scala-kryo-serialization" % "? not yet released"`
37 |
38 | * Register and configure the serializer in your Typesafe Config configuration file, e.g. `application.conf`.
39 |
40 | We provide several versions of the library:
41 |
42 | | Version | Kryo Compatibility | Available Scala Versions | Tested with |
43 | |---------|--------------------|--------------------------|---------------------------------------------------------------------|
44 | | v1.3.x | Kryo-5.6 | 2.12,2.13,3 | JDK: OpenJdk11,OpenJdk17,OpenJdk21 Scala: 2.12.20,2.13.16,3.3.5 |
45 | | v1.2.x | Kryo-5.6 | 2.12,2.13,3 | JDK: OpenJdk11,OpenJdk17,OpenJdk21 Scala: 2.12.20,2.13.16,3.3.4 |
46 | | v1.1.x | Kryo-5.5 | 2.12,2.13,3 | JDK: OpenJdk11,OpenJdk17,OpenJdk21 Scala: 2.12.18,2.13.11,3.3.1 |
47 | | v1.0.x | Kryo-5.4 | 2.12,2.13,3 | JDK: OpenJdk11,OpenJdk17 Scala: 2.12.18,2.13.11,3.3.1 |
48 |
49 |
50 | Note that we use semantic versioning - see [semver.org](https://semver.org/).
51 |
52 |
53 | #### sbt projects
54 |
55 | To use the latest stable release of scala-kryo-serialization in sbt projects you just need to add
56 | this dependency:
57 |
58 | `libraryDependencies += "io.altoo" %% "scala-kryo-serialization" % "1.2.0"`
59 |
60 | #### maven projects
61 |
62 | To use the official release of scala-kryo-serialization in Maven projects, please use the following snippet in your pom.xml
63 |
64 | ```xml
65 |
146 | Serializes objects using direct field assignment. FieldSerializer is generic
147 | and can serialize most classes without any configuration. It is efficient
148 | and writes only the field data, without any extra information. It does not
149 | support adding, removing, or changing the type of fields without invalidating
150 | previously serialized bytes. This can be acceptable in many situations,
151 | such as when sending data over a network, but may not be a good choice for
152 | long term data storage because the Java classes cannot evolve.
153 |
154 | * `com.esotericsoftware.kryo.serializers.CompatibleFieldSerializer`
155 | Serializes objects using direct field assignment, providing both forward and
156 | backward compatibility. This means fields can be added or removed without
157 | invalidating previously serialized bytes. Changing the type of a field
158 | is not supported. The forward and backward compatibility comes at a cost: the
159 | first time the class is encountered in the serialized bytes, a simple
160 | schema is written containing the field name strings.
161 |
162 | * `com.esotericsoftware.kryo.serializers.VersionFieldSerializer`
163 | Serializes objects using direct field assignment, with versioning backward
164 | compatibility. Allows fields to have a @Since(int) annotation to indicate
165 | the version they were added. For a particular field, the value in @Since
166 | should never change once created. This is less flexible than FieldSerializer,
167 | which can handle most classes without needing annotations, but it provides
168 | backward compatibility. This means that new fields can be added, but
169 | removing, renaming or changing the type of any field will invalidate
170 | previous serialized bytes. VersionFieldSerializer has very little overhead
171 | (a single additional varint) compared to FieldSerializer. Forward
172 | compatibility is not supported.
173 |
174 | * `com.esotericsoftware.kryo.serializers.TaggedFieldSerializer`
175 | Serializes objects using direct field assignment for fields that have
176 | a @Tag(int) annotation. This provides backward compatibility so new
177 | fields can be added. TaggedFieldSerializer has two advantages over
178 | VersionFieldSerializer:
179 | 1) fields can be renamed
180 | 2) fields marked with the @Deprecated annotation will be ignored when
181 | reading old bytes and won't be written to new bytes.
182 |
183 | Deprecation effectively removes the field from serialization, though
184 | the field and @Tag annotation must remain in the class. The downside is that
185 | it has a small amount of additional overhead compared to
186 | VersionFieldSerializer (additional per field variant). Forward compatibility
187 | is not supported.
188 |
189 | ### Example for configuring a different field serializer
190 |
191 | Create a custom initializer
192 |
193 | ```scala
194 | class XyzKryoInitializer extends DefaultKryoInitializer {
195 | def preInit(kryo: ScalaKryo): Unit = {
196 | kryo.setDefaultSerializer(classOf[com.esotericsoftware.kryo.serializers.TaggedFieldSerializer[_]])
197 | }
198 | }
199 | ```
200 |
201 | And register the custom initializer in your `application.conf` by overriding
202 |
203 | scala-kryo-serialization.kryo-initializer = "com.example.XyzKryoInitializer"
204 |
205 | To configure the field serializer a serializer factory can be used as described here: https://github.com/EsotericSoftware/kryo#serializer-factories
206 |
207 | How to configure and customize encryption
208 | -----------------------------------------
209 |
210 | Using the `DefaultKeyProvider` an encryption key can statically be set by defining `encryption.aes.password` and `encryption.aes.salt`.
211 | Refere to the [reference.conf](https://github.com/altoo-ag/scala-kryo-serialization/blob/master/scala-kryo-serialization/src/main/resources/reference.conf) for an example configuration.
212 |
213 | Sometimes you need to pass a custom aes key, depending on the context you are in,
214 | instead of having a static key. For example, you might have the key in a data
215 | store, or provided by some other application. In such instances, you might want
216 | to provide the key dynamically to kryo serializer.
217 |
218 | You can override the
219 | ```hocon
220 | encryption.aes.key-provider = "CustomKeyProviderFQCN"
221 | ```
222 | Where `CustomKeyProviderFQCN` is a fully qualified class name of your custom aes key
223 | provider class. The key provider must extend the `DefaultKeyProvider` and can override the `aesKey` method.
224 |
225 | An example of such a custom aes-key supplier class could be something like this:
226 |
227 | ```scala
228 | class CustomKeyProvider extends DefaultKeyProvider {
229 | override def aesKey(config: Config): String = "ThisIsASecretKey"
230 | }
231 | ```
232 |
233 | The encryption transformer (selected for `aes` in post serialization transformations) only
234 | supports GCM modes (currently recommended default mode is `AES/GCM/NoPadding`).
235 |
236 | Important: The old encryption transformer only supported CBC modes without manual authentication which is
237 | deemed problematic. It is currently available for backwards compatibility by specifying `aesLegacy` in
238 | post serialization transformations instead of `aes`. Its usage is deprecated and will be removed in future versions.
239 |
240 |
241 | Resolving Subclasses
242 | --------------------
243 |
244 | If you are using `id-strategy="explicit"`, you may find that some of the standard Scala types are a bit hard to register properly.
245 | This is because these types are exposed in the API as simple traits or abstract classes, but they are actually implemented as many
246 | specialized subclasses that are used as necessary. Examples include:
247 |
248 | * scala.collection.immutable.Map
249 | * scala.collection.immutable.Set
250 |
251 | The problem is that Kryo thinks in terms of the *exact* class being serialized, but you are
252 | rarely working with the actual implementation class -- the application code only cares about
253 | the more abstract trait. The implementation class often isn't obvious, and is sometimes
254 | private to the library it comes from. This isn't an issue for idstrategies that add registrations
255 | when needed, or which use the class name, but in `explicit` you must register every class to be
256 | serialized, and that may turn out to be more than you expect.
257 |
258 | For cases like these, you can use the `SubclassResolver`. This is a variant of the standard
259 | Kryo ClassResolver, which is able to deal with subclasses of the registered types. You turn it
260 | on by setting
261 | ```hocon
262 | resolve-subclasses = true
263 | ```
264 | With that turned on, unregistered subclasses of a registered supertype are serialized as that
265 | supertype. So for example, if you have registered `immutable.Set`, and the object being serialized
266 | is actually an `immutable.Set.Set3` (the subclass used for Sets of 3 elements), it will serialize and
267 | deserialize that as an `immutable.Set`.
268 |
269 | If you register `immutable.Map`, you should use the `ScalaImmutableAbstractMapSerializer` with it.
270 | If you register `immutable.Set`, you should use the `ScalaImmutableAbstractSetSerializer`. These
271 | serializers are specifically designed to work with those traits.
272 |
273 | The `SubclassResolver` approach should only be used in cases where the implementation types are completely
274 | opaque, chosen by the implementation library, and not used explicitly in application code. If you have
275 | subclasses that have their own distinct semantics, such as `immutable.ListMap`, you should register
276 | those separately. You can register both a higher-level class like `immutable.Map` and a subclass
277 | like `immutable.ListMap` -- the resolver will choose the more-specific one when appropriate.
278 |
279 | `SubclassResolver` should be used with care -- even when it is turned on, you should define and
280 | register most of your classes explicitly, as usual. But it is a helpful way to tame the complexity
281 | of some class hierarchies, when that complexity can be treated as an implementation detail and all
282 | the subclasses can be serialized and deserialized identically.
283 |
284 |
285 | Using serializers with different configurations
286 | -----------------------------------------------
287 |
288 | There may be the need to use different configurations for different use cases.
289 | To support this the `KryoSerializer` can be extended to use a different configuration path.
290 |
291 | Define a custom configuration:
292 | ```hocon
293 | scala-kryo-serialization-xyz = ${scala-kryo-serialization} {
294 | # configuration overrides like...
295 | # id-strategy = "explicit"
296 | }
297 | ```
298 |
299 | Create new serializer subclass overriding the config key to the matching config section.
300 | ```scala
301 | package xyz
302 |
303 | class XyzKryoSerializer(config: Config, classLoader: ClassLoader) extends ScalaKryoSerializer(config, classLoader) {
304 | override def configKey: String = "scala-kryo-serialization-xyz"
305 | }
306 | ```
307 |
308 |
309 | Enum Serialization
310 | ------------------
311 |
312 | Serialization of Java and Scala 3 enums is done by name (and not by index) to avoid having reordering of enum values breaking serialization.
313 |
314 | Scala 3 `lazy val` Serialization Notice
315 | ---------------------------------------
316 |
317 | When serializing objects that contain `lazy val`s in Scala 3, please be aware of the following behavior:
318 |
319 | - Scala 3 implements `lazy val` using an internal state machine (`Uninitialized`, `Evaluating`, `Waiting`, `Initialized`).
320 | - Kryo will only serialize the `lazy val` value if it has been fully initialized. Intermediate states (`Evaluating` or `Waiting`) will be treated as uninitialized (`null`) during serialization.
321 | - As a result, after deserialization, the `lazy val` will be recomputed if it was not fully initialized during serialization.
322 |
323 | **Implication:** If your object contains expensive or side-effecting `lazy val`s, they might be re-evaluated after deserialization unless they were fully initialized before serialization.
324 |
325 | If this behavior is undesirable, consider explicitly evaluating such values before serialization, or avoid relying on `lazy val` in serialized objects.
326 |
327 |
328 | Using Kryo on JDK 17 and later
329 | ------------------------------
330 |
331 | Kryo needs modules to be opened for reflection when serializing basic JDK classes.
332 | Those options have to be passed to the JVM, for example in sbt:
333 | ```sbt
334 | javaOptions ++= Seq("--add-opens", "java.base/java.util=ALL-UNNAMED", "--add-opens", "java.base/java.util.concurrent=ALL-UNNAMED", "--add-opens", "java.base/java.lang=ALL-UNNAMED", "--add-opens", "java.base/java.lang.invoke=ALL-UNNAMED", "--add-opens", "java.base/java.math=ALL-UNNAMED")
335 | ```
336 |
337 | To use unsafe transformations, the following access must be granted:
338 | ```sbt
339 | javaOptions ++= Seq("--add-opens", "java.base/java.nio=ALL-UNNAMED", "--add-opens", "java.base/sun.nio.ch=ALL-UNNAMED")
340 | ```
341 |
342 | How do I build this library on my own?
343 | --------------------------------------
344 | If you wish to build the library on your own, you need to check out the project from GitHub and do
345 | ```
346 | sbt compile publishM2
347 | ```
348 |
349 |
--------------------------------------------------------------------------------
/Snapshots.md:
--------------------------------------------------------------------------------
1 | Snapshots
2 | ---------
3 |
4 | #### sbt projects
5 |
6 | For the latest snapshots you need to add the Sonatype's snapshot repository to your `plugins.sbt`
7 |
8 | `resolvers += Resolver.sonatypeRepo("snapshots")`
9 |
10 |
11 | And the snapshot dependency to your project
12 |
13 | `libraryDependencies += "io.altoo" %% "scala-kryo-serialization" % "1.0.0-SNAPSHOT"`
14 |
15 |
16 | #### maven projects
17 |
18 |
19 | For the latest snapshots use:
20 |
21 | ```xml
22 |
The C version of MurmurHash 2.0 found at that site was ported 78 | * to Java by Andrzej Bialecki (ab at getopt org).
79 | */ 80 | object MurmurHash { 81 | def hash(data: Array[Byte], seed: Int): Int = { 82 | val m: Int = 0x5BD1E995 83 | val r: Int = 24 84 | 85 | var h: Int = seed ^ data.length 86 | 87 | val len = data.length 88 | val len_4 = len >> 2 89 | 90 | var i = 0 91 | while (i < len_4) { 92 | val i_4 = i << 2 93 | var k: Int = data(i_4 + 3) 94 | k = k << 8 95 | k = k | (data(i_4 + 2) & 0xFF) 96 | k = k << 8 97 | k = k | (data(i_4 + 1) & 0xFF) 98 | k = k << 8 99 | k = k | (data(i_4 + 0) & 0xFF) 100 | k *= m 101 | k ^= k >>> r 102 | k *= m 103 | h *= m 104 | h ^= k 105 | i = i + 1 106 | } 107 | 108 | val len_m = len_4 << 2 109 | val left = len - len_m 110 | 111 | if (left != 0) { 112 | if (left >= 3) { 113 | h ^= (data(len - 3): Int) << 16 114 | } 115 | if (left >= 2) { 116 | h ^= (data(len - 2): Int) << 8 117 | } 118 | if (left >= 1) { 119 | h ^= (data(len - 1): Int) 120 | } 121 | 122 | h *= m 123 | } 124 | 125 | h ^= h >>> 13 126 | h *= m 127 | h ^= h >>> 15 128 | 129 | h 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /core/src/main/scala/io/altoo/serialization/kryo/scala/serializer/ScalaKryo.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * ***************************************************************************** 3 | * Copyright 2013 Roman Levenstein 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * **************************************************************************** 17 | */ 18 | package io.altoo.serialization.kryo.scala.serializer 19 | 20 | import com.esotericsoftware.kryo.kryo5.* 21 | import com.esotericsoftware.kryo.kryo5.serializers.FieldSerializer 22 | 23 | class ScalaKryo(classResolver: ClassResolver, referenceResolver: ReferenceResolver) 24 | extends Kryo(classResolver, referenceResolver) { 25 | 26 | lazy val objSer = new ScalaObjectSerializer[AnyRef] 27 | 28 | override def getDefaultSerializer(typ: Class[?]): Serializer[?] = { 29 | if (isSingleton(typ)) { 30 | objSer 31 | } else { 32 | super.getDefaultSerializer(typ) 33 | } 34 | } 35 | 36 | override def newDefaultSerializer(klass: Class[?]): Serializer[?] = { 37 | if (isSingleton(klass)) { 38 | objSer 39 | } else { 40 | super.newDefaultSerializer(klass) match { 41 | case fs: FieldSerializer[?] => 42 | // Scala has a lot of synthetic fields that must be serialized: 43 | // We also enable it by default in java since not wanting these fields 44 | // serialized looks like the exception rather than the rule. 45 | fs.getFieldSerializerConfig.setIgnoreSyntheticFields(false) 46 | fs.updateFields() 47 | fs 48 | case x: Serializer[?] => x 49 | } 50 | } 51 | } 52 | 53 | /** 54 | * return true if this class is a scala "object" 55 | */ 56 | def isSingleton(klass: Class[?]): Boolean = 57 | klass.getName.last == '$' && objSer.accepts(klass) 58 | } 59 | -------------------------------------------------------------------------------- /core/src/main/scala/io/altoo/serialization/kryo/scala/serializer/ScalaMapSerializers.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * ***************************************************************************** 3 | * Copyright 2012 Roman Levenstein 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * **************************************************************************** 17 | */ 18 | 19 | package io.altoo.serialization.kryo.scala.serializer 20 | 21 | import com.esotericsoftware.kryo.kryo5.io.{Input, Output} 22 | import com.esotericsoftware.kryo.kryo5.{Kryo, Serializer} 23 | 24 | import java.lang.reflect.Constructor 25 | import scala.collection.immutable.{Map as IMap, SortedMap} 26 | import scala.collection.mutable.Map as MMap 27 | 28 | /** 29 | * Module with specialized serializers for Scala Maps. 30 | * They are split in 3 different serializers in order: 31 | * 1. To not need reflection at runtime (find if it is SortedMap) 32 | * 2. Use inplace updates with mutable Maps 33 | * 34 | * @author luben 35 | */ 36 | 37 | class ScalaMutableMapSerializer() extends Serializer[MMap[?, ?]] { 38 | 39 | override def read(kryo: Kryo, input: Input, typ: Class[? <: MMap[?, ?]]): MMap[?, ?] = { 40 | val len = input.readInt(true) 41 | val coll = kryo.newInstance(typ).empty.asInstanceOf[MMap[Any, Any]] 42 | if (len != 0) { 43 | var i = 0 44 | while (i < len) { 45 | coll(kryo.readClassAndObject(input)) = kryo.readClassAndObject(input) 46 | i += 1 47 | } 48 | } 49 | coll 50 | } 51 | 52 | override def write(kryo: Kryo, output: Output, collection: MMap[?, ?]): Unit = { 53 | val len = collection.size 54 | output.writeInt(len, true) 55 | if (len != 0) { 56 | val it = collection.iterator 57 | while (it.hasNext) { 58 | val t = it.next() 59 | kryo.writeClassAndObject(output, t._1) 60 | kryo.writeClassAndObject(output, t._2) 61 | } 62 | } 63 | } 64 | } 65 | 66 | class ScalaImmutableMapSerializer() extends Serializer[IMap[?, ?]] { 67 | 68 | setImmutable(true) 69 | 70 | override def read(kryo: Kryo, input: Input, typ: Class[? <: IMap[?, ?]]): IMap[?, ?] = { 71 | val len = input.readInt(true) 72 | var coll: IMap[Any, Any] = kryo.newInstance(typ).asInstanceOf[IMap[Any, Any]].empty 73 | 74 | if (len != 0) { 75 | var i = 0 76 | while (i < len) { 77 | coll += kryo.readClassAndObject(input) -> kryo.readClassAndObject(input) 78 | i += 1 79 | } 80 | } 81 | coll 82 | } 83 | 84 | override def write(kryo: Kryo, output: Output, collection: IMap[?, ?]): Unit = { 85 | val len = collection.size 86 | output.writeInt(len, true) 87 | if (len != 0) { 88 | val it = collection.iterator 89 | while (it.hasNext) { 90 | val t = it.next() 91 | kryo.writeClassAndObject(output, t._1) 92 | kryo.writeClassAndObject(output, t._2) 93 | } 94 | } 95 | } 96 | } 97 | 98 | class ScalaImmutableAbstractMapSerializer() extends Serializer[IMap[?, ?]] { 99 | 100 | setImmutable(true) 101 | 102 | override def read(kryo: Kryo, input: Input, typ: Class[? <: IMap[?, ?]]): IMap[?, ?] = { 103 | val len = input.readInt(true) 104 | var coll: IMap[Any, Any] = IMap.empty 105 | 106 | if (len != 0) { 107 | var i = 0 108 | while (i < len) { 109 | coll += kryo.readClassAndObject(input) -> kryo.readClassAndObject(input) 110 | i += 1 111 | } 112 | } 113 | coll 114 | } 115 | 116 | override def write(kryo: Kryo, output: Output, collection: IMap[?, ?]): Unit = { 117 | val len = collection.size 118 | output.writeInt(len, true) 119 | if (len != 0) { 120 | val it = collection.iterator 121 | while (it.hasNext) { 122 | val t = it.next() 123 | kryo.writeClassAndObject(output, t._1) 124 | kryo.writeClassAndObject(output, t._2) 125 | } 126 | } 127 | } 128 | } 129 | 130 | class ScalaSortedMapSerializer() extends Serializer[SortedMap[?, ?]] { 131 | private var class2constuctor = IMap[Class[?], Constructor[?]]() 132 | 133 | // All sorted maps are immutable 134 | setImmutable(true) 135 | 136 | override def read(kryo: Kryo, input: Input, typ: Class[? <: SortedMap[?, ?]]): SortedMap[?, ?] = { 137 | val len = input.readInt(true) 138 | implicit val mapOrdering: Ordering[Any] = kryo.readClassAndObject(input).asInstanceOf[scala.math.Ordering[Any]] 139 | var coll: SortedMap[Any, Any] = 140 | try { 141 | val constructor = class2constuctor.getOrElse(typ, { 142 | val constr = typ.getDeclaredConstructor(classOf[scala.math.Ordering[?]]) 143 | class2constuctor += typ -> constr 144 | constr 145 | }) 146 | constructor.newInstance(mapOrdering).asInstanceOf[SortedMap[Any, Any]].empty 147 | } catch { 148 | case _: Throwable => kryo.newInstance(typ).asInstanceOf[SortedMap[Any, Any]].empty 149 | } 150 | 151 | var i = 0 152 | while (i < len) { 153 | coll += kryo.readClassAndObject(input) -> kryo.readClassAndObject(input) 154 | i += 1 155 | } 156 | coll 157 | } 158 | 159 | override def write(kryo: Kryo, output: Output, collection: SortedMap[?, ?]): Unit = { 160 | val len = collection.size 161 | output.writeInt(len, true) 162 | 163 | kryo.writeClassAndObject(output, collection.ordering) 164 | 165 | val it = collection.iterator 166 | while (it.hasNext) { 167 | val t = it.next() 168 | kryo.writeClassAndObject(output, t._1) 169 | kryo.writeClassAndObject(output, t._2) 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /core/src/main/scala/io/altoo/serialization/kryo/scala/serializer/ScalaObjectSerializer.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * ***************************************************************************** 3 | * Copyright 2014 Roman Levenstein 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * **************************************************************************** 17 | */ 18 | 19 | package io.altoo.serialization.kryo.scala.serializer 20 | 21 | import com.esotericsoftware.kryo.kryo5.io.{Input, Output} 22 | import com.esotericsoftware.kryo.kryo5.{Kryo, Serializer} 23 | 24 | import _root_.java.lang.reflect.Field 25 | import scala.collection.mutable.Map as MMap 26 | import scala.util.control.Exception.allCatch 27 | 28 | // Stolen with pride from Chill ;-) 29 | class ScalaObjectSerializer[T] extends Serializer[T] { 30 | private val cachedObj = MMap[Class[?], Option[T]]() 31 | 32 | // NOTE: even if a standalone or companion Scala object contains mutable 33 | // fields, the fact that there is only one of them in a process means that 34 | // we don't want to make a copy, so this serializer's type is treated as 35 | // always being immutable. 36 | override def isImmutable: Boolean = true 37 | 38 | // Does nothing 39 | override def write(kser: Kryo, out: Output, obj: T): Unit = () 40 | 41 | protected def createSingleton(cls: Class[?]): Option[T] = { 42 | moduleField(cls).map { _.get(null).asInstanceOf[T] } 43 | } 44 | 45 | protected def cachedRead(cls: Class[?]): Option[T] = { 46 | cachedObj.synchronized { cachedObj.getOrElseUpdate(cls, createSingleton(cls)) } 47 | } 48 | 49 | override def read(kser: Kryo, in: Input, cls: Class[? <: T]): T = cachedRead(cls).get 50 | 51 | def accepts(cls: Class[?]): Boolean = cachedRead(cls).isDefined 52 | 53 | protected def moduleField(klass: Class[?]): Option[Field] = 54 | Some(klass) 55 | .filter { _.getName.last == '$' } 56 | .flatMap { k => allCatch.opt(k.getDeclaredField("MODULE$")) } 57 | } 58 | -------------------------------------------------------------------------------- /core/src/main/scala/io/altoo/serialization/kryo/scala/serializer/ScalaSetSerializers.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * ***************************************************************************** 3 | * Copyright 2012 Roman Levenstein 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * **************************************************************************** 17 | */ 18 | 19 | package io.altoo.serialization.kryo.scala.serializer 20 | 21 | import com.esotericsoftware.kryo.kryo5.io.{Input, Output} 22 | import com.esotericsoftware.kryo.kryo5.{Kryo, Serializer} 23 | 24 | import java.lang.reflect.Constructor 25 | import scala.collection.immutable.{Set as imSet, SortedSet as imSSet} 26 | import scala.collection.mutable.{Set as mSet, SortedSet as mSSet} 27 | 28 | class ScalaImmutableSortedSetSerializer() extends Serializer[imSSet[?]] { 29 | 30 | setImmutable(true) 31 | 32 | private var class2constuctor = Map[Class[?], Constructor[?]]() 33 | 34 | override def read(kryo: Kryo, input: Input, typ: Class[? <: imSSet[?]]): imSSet[?] = { 35 | val len = input.readInt(true) 36 | 37 | var coll: imSSet[Any] = { 38 | // Read ordering and set it for this collection 39 | implicit val setOrdering: Ordering[Any] = kryo.readClassAndObject(input).asInstanceOf[scala.math.Ordering[Any]] 40 | try { 41 | val constructor = 42 | class2constuctor.getOrElse(typ, { 43 | val constr = typ.getDeclaredConstructor(classOf[scala.math.Ordering[?]]) 44 | class2constuctor += typ -> constr 45 | constr 46 | }) 47 | constructor.newInstance(setOrdering).asInstanceOf[imSSet[Any]].empty 48 | } catch { 49 | case _: Throwable => kryo.newInstance(typ).asInstanceOf[imSSet[Any]].empty 50 | } 51 | } 52 | 53 | var i = 0 54 | while (i < len) { 55 | coll += kryo.readClassAndObject(input) 56 | i += 1 57 | } 58 | coll 59 | } 60 | 61 | override def write(kryo: Kryo, output: Output, collection: imSSet[?]): Unit = { 62 | val len = collection.size 63 | output.writeInt(len, true) 64 | 65 | kryo.writeClassAndObject(output, collection.ordering) 66 | 67 | val it = collection.iterator 68 | while (it.hasNext) { 69 | kryo.writeClassAndObject(output, it.next()) 70 | } 71 | } 72 | } 73 | 74 | class ScalaImmutableSetSerializer() extends Serializer[imSet[?]] { 75 | 76 | setImmutable(true) 77 | 78 | override def read(kryo: Kryo, input: Input, typ: Class[? <: imSet[?]]): imSet[?] = { 79 | val len = input.readInt(true) 80 | var coll: imSet[Any] = kryo.newInstance(typ).asInstanceOf[imSet[Any]].empty 81 | var i = 0 82 | while (i < len) { 83 | coll += kryo.readClassAndObject(input) 84 | i += 1 85 | } 86 | coll 87 | } 88 | 89 | override def write(kryo: Kryo, output: Output, collection: imSet[?]): Unit = { 90 | output.writeInt(collection.size, true) 91 | val it = collection.iterator 92 | while (it.hasNext) { 93 | kryo.writeClassAndObject(output, it.next()) 94 | } 95 | } 96 | } 97 | 98 | class ScalaImmutableAbstractSetSerializer() extends Serializer[imSet[?]] { 99 | 100 | setImmutable(true) 101 | 102 | override def read(kryo: Kryo, input: Input, typ: Class[? <: imSet[?]]): imSet[?] = { 103 | val len = input.readInt(true) 104 | var coll: imSet[Any] = Set.empty 105 | var i = 0 106 | while (i < len) { 107 | coll += kryo.readClassAndObject(input) 108 | i += 1 109 | } 110 | coll 111 | } 112 | 113 | override def write(kryo: Kryo, output: Output, collection: imSet[?]): Unit = { 114 | output.writeInt(collection.size, true) 115 | val it = collection.iterator 116 | while (it.hasNext) { 117 | kryo.writeClassAndObject(output, it.next()) 118 | } 119 | } 120 | } 121 | 122 | class ScalaMutableSortedSetSerializer() extends Serializer[mSSet[?]] { 123 | private var class2constuctor = Map[Class[?], Constructor[?]]() 124 | 125 | override def read(kryo: Kryo, input: Input, typ: Class[? <: mSSet[?]]): mSSet[?] = { 126 | val len = input.readInt(true) 127 | 128 | val coll: mSSet[Any] = { 129 | // Read ordering and set it for this collection 130 | implicit val setOrdering: Ordering[Any] = kryo.readClassAndObject(input).asInstanceOf[scala.math.Ordering[Any]] 131 | try { 132 | val constructor = 133 | class2constuctor.getOrElse(typ, { 134 | val constr = typ.getDeclaredConstructor(classOf[scala.math.Ordering[?]]) 135 | class2constuctor += typ -> constr 136 | constr 137 | }) 138 | constructor.newInstance(setOrdering).asInstanceOf[mSSet[Any]].empty 139 | } catch { 140 | case _: Throwable => kryo.newInstance(typ).asInstanceOf[mSSet[Any]].empty 141 | } 142 | } 143 | 144 | var i = 0 145 | while (i < len) { 146 | coll += kryo.readClassAndObject(input) 147 | i += 1 148 | } 149 | coll 150 | } 151 | 152 | override def write(kryo: Kryo, output: Output, collection: mSSet[?]): Unit = { 153 | val len = collection.size 154 | output.writeInt(len, true) 155 | 156 | kryo.writeClassAndObject(output, collection.ordering) 157 | 158 | val it = collection.iterator 159 | while (it.hasNext) { 160 | kryo.writeClassAndObject(output, it.next()) 161 | } 162 | } 163 | } 164 | 165 | class ScalaMutableSetSerializer() extends Serializer[mSet[?]] { 166 | 167 | override def read(kryo: Kryo, input: Input, typ: Class[? <: mSet[?]]): mSet[?] = { 168 | val len = input.readInt(true) 169 | val coll: mSet[Any] = kryo.newInstance(typ).asInstanceOf[mSet[Any]].empty 170 | var i = 0 171 | while (i < len) { 172 | coll += kryo.readClassAndObject(input) 173 | i += 1 174 | } 175 | coll 176 | } 177 | 178 | override def write(kryo: Kryo, output: Output, collection: mSet[?]): Unit = { 179 | output.writeInt(collection.size, true) 180 | val it = collection.iterator 181 | while (it.hasNext) { 182 | kryo.writeClassAndObject(output, it.next()) 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /core/src/main/scala/io/altoo/serialization/kryo/scala/serializer/ScalaUnitSerializer.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * ***************************************************************************** 3 | * Copyright 2014 Roman Levenstein 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * **************************************************************************** 17 | */ 18 | 19 | package io.altoo.serialization.kryo.scala.serializer 20 | 21 | import com.esotericsoftware.kryo.kryo5.io.{Input, Output} 22 | import com.esotericsoftware.kryo.kryo5.{Kryo, Serializer} 23 | 24 | class ScalaUnitSerializer extends Serializer[Unit] { 25 | def write(kryo: Kryo, output: Output, obj: Unit): Unit = { 26 | // Write nothing, similarly to ScalaObjectSerializer 27 | } 28 | def read(kryo: Kryo, input: Input, typ: Class[? <: Unit]): Unit = { 29 | // Return the one true Unit 30 | () 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /core/src/main/scala/io/altoo/serialization/kryo/scala/serializer/SubclassResolver.scala: -------------------------------------------------------------------------------- 1 | package io.altoo.serialization.kryo.scala.serializer 2 | 3 | import com.esotericsoftware.kryo.kryo5.Registration 4 | import com.esotericsoftware.kryo.kryo5.util.DefaultClassResolver 5 | 6 | import java.util.Collections 7 | 8 | class SubclassResolver extends DefaultClassResolver { 9 | 10 | /** 11 | * We don't want to do subclass resolution during the Kryo.register() call, and unfortunately it 12 | * hits this a lot. So this doesn't get turned on until the KryoSerializer explicitly enables it, 13 | * at the end of Kryo setup. 14 | */ 15 | private var enabled = false 16 | 17 | def enable(): Unit = enabled = true 18 | 19 | /** 20 | * Keep track of the Types we've tried to look up and failed, to reduce wasted effort. 21 | */ 22 | private val unregisteredTypes = Collections.newSetFromMap[Class[?]](new java.util.WeakHashMap()) 23 | 24 | /** 25 | * Given Class clazz, this recursively walks up the reflection tree and collects all of its 26 | * ancestors, so we can check whether any of them are registered. 27 | */ 28 | def findRegistered(clazz: Class[?]): Option[Registration] = { 29 | if (clazz == null || unregisteredTypes.contains(clazz)) 30 | // Hit the top, so give up 31 | None 32 | else { 33 | val reg = classToRegistration.get(clazz) 34 | if (reg == null) { 35 | val result = 36 | findRegistered(clazz.getSuperclass) orElse 37 | clazz.getInterfaces.foldLeft(Option.empty[Registration]) { (res, interf) => 38 | res orElse findRegistered(interf) 39 | } 40 | if (result.isEmpty) { 41 | unregisteredTypes.add(clazz) 42 | } 43 | result 44 | } else { 45 | Some(reg) 46 | } 47 | } 48 | } 49 | 50 | override def getRegistration(tpe: Class[?]): Registration = { 51 | val found = super.getRegistration(tpe) 52 | if (enabled && found == null) { 53 | findRegistered(tpe) match { 54 | case Some(reg) => 55 | // Okay, we've found an ancestor registration. Add that registration for the current type, so 56 | // it'll be efficient later. (This isn't threadsafe, but a given Kryo instance isn't anyway.) 57 | classToRegistration.put(tpe, reg) 58 | reg 59 | 60 | case None => null 61 | } 62 | } else 63 | found 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /core/src/test/scala-2.12/io/altoo/serialization/kryo/scala/serializer/ScalaVersionRegistry.scala: -------------------------------------------------------------------------------- 1 | package io.altoo.serialization.kryo.scala.serializer 2 | 3 | import com.esotericsoftware.kryo.kryo5.Kryo 4 | 5 | object ScalaVersionRegistry { 6 | final val immutableHashMapImpl = "scala.collection.immutable.HashMap$HashTrieMap" 7 | final val immutableHashSetImpl = "scala.collection.immutable.HashSet$HashTrieSet" 8 | 9 | def registerHashMap(kryo: Kryo): Unit = { 10 | kryo.register(classOf[scala.collection.immutable.HashMap.HashTrieMap[_, _]], 40) 11 | } 12 | 13 | def registerHashSet(kryo: Kryo): Unit = { 14 | kryo.register(classOf[scala.collection.immutable.HashSet.HashTrieSet[_]], 41) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /core/src/test/scala-2.13/io/altoo/serialization/kryo/scala/serializer/ScalaVersionRegistry.scala: -------------------------------------------------------------------------------- 1 | package io.altoo.serialization.kryo.scala.serializer 2 | 3 | import com.esotericsoftware.kryo.kryo5.Kryo 4 | 5 | object ScalaVersionRegistry { 6 | final val immutableHashMapImpl = "scala.collection.immutable.HashMap" 7 | final val immutableHashSetImpl = "scala.collection.immutable.HashSet" 8 | 9 | def registerHashMap(kryo: Kryo): Unit = { 10 | kryo.register(classOf[scala.collection.immutable.HashMap[_, _]], 40) 11 | } 12 | 13 | def registerHashSet(kryo: Kryo): Unit = { 14 | kryo.register(classOf[scala.collection.immutable.HashSet[_]], 41) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /core/src/test/scala-3/io/altoo/external/ExternalEnum.scala: -------------------------------------------------------------------------------- 1 | package io.altoo.external 2 | 3 | import io.altoo.serialization.kryo.scala.serializer.ScalaEnumSerializationTest.Sample 4 | 5 | enum ExternalEnum(val name: String) { 6 | case A extends ExternalEnum("a") 7 | } -------------------------------------------------------------------------------- /core/src/test/scala-3/io/altoo/serialization/kryo/scala/serializer/LazyValSpec.scala: -------------------------------------------------------------------------------- 1 | package io.altoo.serialization.kryo.scala.serializer 2 | 3 | import io.altoo.serialization.kryo.scala.ScalaKryoSerializer 4 | import com.typesafe.config.ConfigFactory 5 | import org.scalatest.flatspec.AnyFlatSpec 6 | import org.scalatest.matchers.should.Matchers 7 | 8 | object LazyValSpec { 9 | case class Message(content: String) { 10 | lazy val mkContent: String = { 11 | Thread.sleep(200) 12 | s"Test string of LazyValSpec is $content." 13 | } 14 | } 15 | 16 | val ser = new ScalaKryoSerializer(ConfigFactory.defaultReference(), getClass.getClassLoader) 17 | 18 | def serialize(obj: Message): Array[Byte] = 19 | ser.serialize(obj).get 20 | 21 | def deserialize(bytes: Array[Byte]): Message = 22 | ser.deserialize[Message](bytes).get 23 | } 24 | 25 | class LazyValSpec extends AnyFlatSpec with Matchers { 26 | import LazyValSpec.* 27 | 28 | behavior of "Lazy val serialization" 29 | 30 | it should "be safe with Scala 3 `lazy val` intermediate states (`Evaluating` / `Waiting`)" in { 31 | val serializedWaitingStateMessage = locally { 32 | val msg = LazyValSpec.Message("Test if lazy val is safe with intermediate states") 33 | 34 | val evaluatingLazyVal = new Thread(() => { 35 | msg.mkContent // start evaluation before serialization 36 | () 37 | }) 38 | evaluatingLazyVal.start() 39 | 40 | Thread.sleep(50) // give some time for the fork to start lazy val rhs eval 41 | 42 | LazyValSpec.serialize(msg) // serialize in the meantime so that we capture Waiting state 43 | } 44 | 45 | val deserializedWaitingStateMsg = LazyValSpec.deserialize(serializedWaitingStateMessage) 46 | 47 | @volatile var content = "" 48 | @volatile var isStarted = false 49 | 50 | val read = new Thread(() => { 51 | isStarted = true 52 | content = deserializedWaitingStateMsg.mkContent 53 | () 54 | }) 55 | 56 | read.start() 57 | read.join(1000) 58 | 59 | assert(isStarted, "Lazy val was never accessed in the thread") 60 | assert(!content.isBlank, s"Lazy val content was blank") 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /core/src/test/scala-3/io/altoo/serialization/kryo/scala/serializer/ScalaEnumSerializationTest.scala: -------------------------------------------------------------------------------- 1 | package io.altoo.serialization.kryo.scala.serializer 2 | 3 | import com.esotericsoftware.kryo.kryo5.util.{DefaultClassResolver, ListReferenceResolver} 4 | import io.altoo.serialization.kryo.scala.testkit.KryoSerializationTesting 5 | import org.scalatest.flatspec.AnyFlatSpec 6 | import org.scalatest.matchers.should.Matchers 7 | 8 | object ScalaEnumSerializationTest { 9 | enum Sample(val name: String, val value: Int) { 10 | case A extends Sample("a", 1) 11 | case B extends Sample("b", 2) 12 | case C extends Sample("c", 3) 13 | } 14 | 15 | case class EmbeddedEnum(sample: Sample) { 16 | def this() = this(null) 17 | } 18 | 19 | enum SimpleADT { 20 | case A() 21 | case B 22 | } 23 | } 24 | 25 | class ScalaEnumSerializationTest extends AnyFlatSpec with Matchers with KryoSerializationTesting { 26 | import ScalaEnumSerializationTest.* 27 | 28 | val kryo = new ScalaKryo(new DefaultClassResolver(), new ListReferenceResolver()) 29 | kryo.setRegistrationRequired(false) 30 | kryo.addDefaultSerializer(classOf[scala.runtime.EnumValue], new ScalaEnumNameSerializer[scala.runtime.EnumValue]) 31 | 32 | behavior of "Kryo serialization" 33 | 34 | it should "round trip enum" in { 35 | kryo.setRegistrationRequired(false) 36 | 37 | testSerializationOf(Sample.B) 38 | } 39 | 40 | it should "round trip external enum" in { 41 | kryo.setRegistrationRequired(false) 42 | 43 | testSerializationOf(io.altoo.external.ExternalEnum.A) 44 | } 45 | 46 | it should "round trip embedded enum" in { 47 | kryo.setRegistrationRequired(false) 48 | kryo.register(classOf[EmbeddedEnum], 46) 49 | 50 | testSerializationOf(EmbeddedEnum(Sample.C)) 51 | } 52 | 53 | it should "round trip adt enum class using generic field serializer" in { 54 | kryo.setRegistrationRequired(false) 55 | kryo.register(classOf[SimpleADT], 47) 56 | 57 | testSerializationOf(SimpleADT.A) 58 | } 59 | 60 | it should "round trip adt enum object using enum serializer" in { 61 | kryo.setRegistrationRequired(false) 62 | kryo.register(classOf[SimpleADT], 47) 63 | 64 | testSerializationOf(SimpleADT.B) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /core/src/test/scala-3/io/altoo/serialization/kryo/scala/serializer/ScalaVersionRegistry.scala: -------------------------------------------------------------------------------- 1 | package io.altoo.serialization.kryo.scala.serializer 2 | 3 | import com.esotericsoftware.kryo.kryo5.Kryo 4 | 5 | object ScalaVersionRegistry { 6 | final val immutableHashMapImpl = "scala.collection.immutable.HashMap" 7 | final val immutableHashSetImpl = "scala.collection.immutable.HashSet" 8 | 9 | def registerHashMap(kryo: Kryo): Unit = { 10 | kryo.register(classOf[scala.collection.immutable.HashMap[_, _]], 40) 11 | } 12 | 13 | def registerHashSet(kryo: Kryo): Unit = { 14 | kryo.register(classOf[scala.collection.immutable.HashSet[_]], 41) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /core/src/test/scala/io/altoo/serialization/kryo/scala/BasicSerializationTest.scala: -------------------------------------------------------------------------------- 1 | package io.altoo.serialization.kryo.scala 2 | 3 | import com.typesafe.config.ConfigFactory 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | import java.nio.ByteBuffer 8 | 9 | object BasicSerializationTest { 10 | 11 | private val config = 12 | s""" 13 | |scala-kryo-serialization { 14 | | trace = true 15 | | id-strategy = "incremental" 16 | | implicit-registration-logging = true 17 | | post-serialization-transformations = off 18 | |} 19 | |""".stripMargin 20 | } 21 | 22 | class BasicSerializationTest extends AnyFlatSpec with Matchers { 23 | private val config = ConfigFactory.parseString(BasicSerializationTest.config).withFallback(ConfigFactory.defaultReference()) 24 | private val serializer = new ScalaKryoSerializer(config, getClass.getClassLoader) 25 | 26 | private val testList: List[Int] = List(1 to 40: _*) 27 | 28 | behavior of "KryoSerializer" 29 | 30 | it should "serialize and deserialize lists" in { 31 | // Check serialization/deserialization 32 | val serialized = serializer.serialize(testList).get 33 | 34 | val deserialized = serializer.deserialize[List[Int]](serialized) 35 | deserialized shouldBe util.Success(testList) 36 | 37 | // Check buffer serialization/deserialization 38 | val bb = ByteBuffer.allocate(testList.length * 8) 39 | 40 | serializer.serialize(testList, bb) 41 | bb.flip() 42 | val bufferDeserialized = serializer.deserialize[List[Int]](bb) 43 | bufferDeserialized shouldBe util.Success(testList) 44 | } 45 | 46 | it should "serialize and deserialize int" in { 47 | // Check serialization/deserialization 48 | val serialized = serializer.serialize(5).get 49 | 50 | val deserialized = serializer.deserialize[Int](serialized) 51 | deserialized shouldBe util.Success(5) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /core/src/test/scala/io/altoo/serialization/kryo/scala/CompressionEffectivenessSerializationTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 Roman Levenstein. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package io.altoo.serialization.kryo.scala 18 | 19 | import com.esotericsoftware.kryo.kryo5.minlog.Log 20 | import com.typesafe.config.ConfigFactory 21 | import org.scalatest.concurrent.ScalaFutures 22 | import org.scalatest.flatspec.AnyFlatSpec 23 | import org.scalatest.matchers.should.Matchers 24 | import org.scalatest.{BeforeAndAfterAll, Inside} 25 | 26 | import java.nio.ByteBuffer 27 | 28 | object CompressionEffectivenessSerializationTest { 29 | 30 | private val config = 31 | s""" 32 | |scala-kryo-serialization { 33 | | trace = true 34 | | id-strategy = "incremental" 35 | | implicit-registration-logging = true 36 | | post-serialization-transformations = "off" 37 | |} 38 | |""".stripMargin 39 | 40 | private val compressionConfig = 41 | s""" 42 | |scala-kryo-serialization { 43 | | post-serialization-transformations = "lz4" 44 | |} 45 | |""".stripMargin 46 | } 47 | 48 | class CompressionEffectivenessSerializationTest extends AnyFlatSpec with Matchers with ScalaFutures with Inside with BeforeAndAfterAll { 49 | Log.ERROR() 50 | 51 | private val hugeCollectionSize = 500 52 | 53 | // Long list for testing serializers and compression 54 | private val testList = 55 | List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 56 | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 57 | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 58 | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40) 59 | 60 | private val testSeq = Seq( 61 | "Rome", "Italy", "London", "England", "Paris", "France", "New York", "USA", "Tokio", "Japan", "Peking", "China", "Brussels", "Belgium", 62 | "Rome", "Italy", "London", "England", "Paris", "France", "New York", "USA", "Tokio", "Japan", "Peking", "China", "Brussels", "Belgium", 63 | "Rome", "Italy", "London", "England", "Paris", "France", "New York", "USA", "Tokio", "Japan", "Peking", "China", "Brussels", "Belgium", 64 | "Rome", "Italy", "London", "England", "Paris", "France", "New York", "USA", "Tokio", "Japan", "Peking", "China", "Brussels", "Belgium") 65 | 66 | // test systems 67 | private val serializer = new ScalaKryoSerializer( 68 | ConfigFactory.parseString(CompressionEffectivenessSerializationTest.config) 69 | .withFallback(ConfigFactory.defaultReference()), getClass.getClassLoader) 70 | 71 | private val serializerWithCompression = new ScalaKryoSerializer( 72 | ConfigFactory.parseString(CompressionEffectivenessSerializationTest.compressionConfig) 73 | .withFallback(ConfigFactory.parseString(CompressionEffectivenessSerializationTest.config)) 74 | .withFallback(ConfigFactory.defaultReference()), getClass.getClassLoader) 75 | 76 | behavior of "KryoSerializer compression" 77 | 78 | it should "produce smaller serialized List representation when compression is enabled" in { 79 | val uncompressedSize = serializeDeserialize(serializer, testList) 80 | val compressedSize = serializeDeserialize(serializerWithCompression, testList) 81 | (compressedSize.doubleValue() / uncompressedSize) should be < 0.4 82 | Console.println("Compressed Size = " + compressedSize) 83 | Console.println("Non-compressed Size = " + uncompressedSize) 84 | } 85 | 86 | it should "produce smaller serialized huge List representation when compression is enabled" in { 87 | var testList = List.empty[String] 88 | (0 until hugeCollectionSize).foreach { i => testList = ("k" + i) :: testList } 89 | val uncompressedSize = serializeDeserialize(serializer, testList) 90 | val compressedSize = serializeDeserialize(serializerWithCompression, testList) 91 | (compressedSize.doubleValue() / uncompressedSize) should be < 0.7 92 | Console.println("Compressed Size = " + compressedSize) 93 | Console.println("Non-compressed Size = " + uncompressedSize) 94 | } 95 | 96 | it should "produce smaller serialized huge Map representation when compression is enabled" in { 97 | var testMap: Map[String, String] = Map.empty[String, String] 98 | (0 until hugeCollectionSize).foreach { i => testMap += ("k" + i) -> ("v" + i) } 99 | val uncompressedSize = serializeDeserialize(serializer, testMap) 100 | val compressedSize = serializeDeserialize(serializerWithCompression, testMap) 101 | (compressedSize.doubleValue() / uncompressedSize) should be < 0.8 102 | Console.println("Compressed Size = " + compressedSize) 103 | Console.println("Non-compressed Size = " + uncompressedSize) 104 | } 105 | 106 | it should "produce smaller serialized Seq representation when compression is enabled" in { 107 | val uncompressedSize = serializeDeserialize(serializer, testSeq) 108 | val compressedSize = serializeDeserialize(serializerWithCompression, testSeq) 109 | (compressedSize.doubleValue() / uncompressedSize) should be < 0.8 110 | Console.println("Compressed Size = " + compressedSize) 111 | Console.println("Non-compressed Size = " + uncompressedSize) 112 | } 113 | 114 | it should "produce smaller serialized huge Seq representation when compression is enabled" in { 115 | var testSeq = Seq[String]() 116 | (0 until hugeCollectionSize).foreach { i => testSeq = testSeq :+ ("k" + i) } 117 | val uncompressedSize = serializeDeserialize(serializer, testSeq) 118 | val compressedSize = serializeDeserialize(serializerWithCompression, testSeq) 119 | (compressedSize.doubleValue() / uncompressedSize) should be < 0.8 120 | Console.println("Compressed Size = " + compressedSize) 121 | Console.println("Non-compressed Size = " + uncompressedSize) 122 | } 123 | 124 | it should "produce smaller serialized huge Set representation when compression is enabled" in { 125 | var testSet = Set.empty[String] 126 | (0 until hugeCollectionSize).foreach { i => testSet += ("k" + i) } 127 | val uncompressedSize = serializeDeserialize(serializer, testSet) 128 | val compressedSize = serializeDeserialize(serializerWithCompression, testSet) 129 | (compressedSize.doubleValue() / uncompressedSize) should be < 0.7 130 | Console.println("Compressed Size = " + compressedSize) 131 | Console.println("Non-compressed Size = " + uncompressedSize) 132 | } 133 | 134 | private def serializeDeserialize(serializer: ScalaKryoSerializer, obj: AnyRef): Int = { 135 | // Check serializer/deserializer 136 | val serialized = serializer.serialize(obj).get 137 | val deserialized = serializer.deserialize[AnyRef](serialized).get 138 | 139 | deserialized.equals(obj) shouldBe true 140 | 141 | // Check buffer serializer/deserializer 142 | val bb = ByteBuffer.allocate(2 * serialized.length) 143 | serializer.serialize(obj, bb) shouldBe a[util.Success[?]] 144 | bb.position() shouldBe serialized.length 145 | 146 | bb.flip() 147 | 148 | val bufferDeserialized = serializer.deserialize[AnyRef](bb).get 149 | bufferDeserialized.equals(obj) shouldBe true 150 | 151 | serialized.length 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /core/src/test/scala/io/altoo/serialization/kryo/scala/CryptoCustomKeySerializationTest.scala: -------------------------------------------------------------------------------- 1 | package io.altoo.serialization.kryo.scala 2 | 3 | import com.typesafe.config.{Config, ConfigFactory} 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | import java.nio.ByteBuffer 8 | import scala.collection.immutable.HashMap 9 | 10 | class KryoCryptoTestKey extends DefaultKeyProvider { 11 | override def aesKey(config: Config): Array[Byte] = "TheTestSecretKey".getBytes("UTF-8") 12 | } 13 | 14 | object CryptoCustomKeySerializationTest { 15 | private val config = { 16 | s""" 17 | |scala-kryo-serialization { 18 | | post-serialization-transformations = aes 19 | | encryption { 20 | | aes { 21 | | key-provider = "io.altoo.serialization.kryo.scala.KryoCryptoTestKey" 22 | | mode = "AES/GCM/NoPadding" 23 | | iv-length = 12 24 | | } 25 | | } 26 | |} 27 | |""".stripMargin 28 | } 29 | } 30 | 31 | class CryptoCustomKeySerializationTest extends AnyFlatSpec with Matchers { 32 | private val config = ConfigFactory.parseString(CryptoCustomKeySerializationTest.config) 33 | .withFallback(ConfigFactory.defaultReference()) 34 | 35 | private val encryptedSerializer = new ScalaKryoSerializer(config, getClass.getClassLoader) 36 | private val unencryptedSerializer = new ScalaKryoSerializer(ConfigFactory.defaultReference(), getClass.getClassLoader) 37 | 38 | private val crypto = new KryoCryptographer("TheTestSecretKey".getBytes("UTF-8"), "AES/GCM/NoPadding", 12) 39 | 40 | behavior of "Custom key encrypted serialization" 41 | 42 | it should "encrypt with custom aes key" in { 43 | val atm = List { 44 | HashMap[String, Any]( 45 | "foo" -> "foo", 46 | "bar" -> "foo,bar,baz", 47 | "baz" -> 124L) 48 | }.toArray 49 | 50 | val serialized = encryptedSerializer.serialize(atm).get 51 | 52 | val decrypted = crypto.fromBinary(serialized) 53 | val deserialized = unencryptedSerializer.deserialize[Array[HashMap[String, Any]]](decrypted) 54 | deserialized.get should contain theSameElementsInOrderAs atm 55 | 56 | val bb = ByteBuffer.allocate(serialized.length * 8) 57 | encryptedSerializer.serialize(atm, bb) shouldBe a[util.Success[?]] 58 | bb.flip() 59 | val unencrypted = crypto.fromBinary(bb) 60 | val bufferDeserialized = unencryptedSerializer.deserialize[Array[HashMap[String, Any]]](unencrypted) 61 | bufferDeserialized.get should contain theSameElementsInOrderAs atm 62 | } 63 | 64 | it should "decrypt with custom aes key" in { 65 | val atm = List { 66 | HashMap[String, Any]( 67 | "foo" -> "foo", 68 | "bar" -> "foo,bar,baz", 69 | "baz" -> 124L) 70 | }.toArray 71 | 72 | val serialized = unencryptedSerializer.serialize(atm).get 73 | val encrypted = crypto.toBinary(serialized) 74 | 75 | val deserialized = encryptedSerializer.deserialize[Array[HashMap[String, Any]]](encrypted) 76 | deserialized.get should contain theSameElementsInOrderAs atm 77 | 78 | val bufferDeserialized = encryptedSerializer.deserialize[Array[HashMap[String, Any]]](ByteBuffer.wrap(encrypted)) 79 | bufferDeserialized.get should contain theSameElementsInOrderAs atm 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /core/src/test/scala/io/altoo/serialization/kryo/scala/CryptoSerializationTest.scala: -------------------------------------------------------------------------------- 1 | package io.altoo.serialization.kryo.scala 2 | 3 | import com.typesafe.config.ConfigFactory 4 | import org.scalatest.BeforeAndAfterAll 5 | import org.scalatest.flatspec.AnyFlatSpec 6 | import org.scalatest.matchers.should.Matchers 7 | 8 | import java.nio.ByteBuffer 9 | import scala.collection.immutable.HashMap 10 | 11 | object CryptoSerializationTest { 12 | private val config = 13 | """ 14 | |scala-kryo-serialization { 15 | | post-serialization-transformations = aes 16 | | encryption { 17 | | aes { 18 | | key-provider = "io.altoo.serialization.kryo.scala.DefaultKeyProvider" 19 | | mode = "AES/GCM/NoPadding" 20 | | iv-length = 12 21 | | password = "j68KkRjq21ykRGAQ" 22 | | salt = "pepper" 23 | | } 24 | | } 25 | |} 26 | |""".stripMargin 27 | } 28 | 29 | class CryptoSerializationTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll { 30 | private val config = ConfigFactory.parseString(CryptoSerializationTest.config) 31 | .withFallback(ConfigFactory.defaultReference()) 32 | 33 | private val sourceSerializer = new ScalaKryoSerializer(config, getClass.getClassLoader) 34 | private val targetSerializer = new ScalaKryoSerializer(config, getClass.getClassLoader) 35 | 36 | behavior of "Encrypted serialization" 37 | 38 | it should "serialize and deserialize with encryption accross actor systems" in { 39 | val atm = List { 40 | HashMap[String, Any]( 41 | "foo" -> "foo", 42 | "bar" -> "foo,bar,baz", 43 | "baz" -> 124L) 44 | }.toArray 45 | 46 | val serialized = sourceSerializer.serialize(atm).get 47 | val deserialized = targetSerializer.deserialize[Array[HashMap[String, Any]]](serialized) 48 | deserialized.get should contain theSameElementsInOrderAs atm 49 | 50 | val bb = ByteBuffer.allocate(serialized.length * 2) 51 | sourceSerializer.serialize(atm, bb) 52 | bb.flip() 53 | val bufferDeserialized = targetSerializer.deserialize[Array[HashMap[String, Any]]](bb) 54 | bufferDeserialized.get should contain theSameElementsInOrderAs atm 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /core/src/test/scala/io/altoo/serialization/kryo/scala/EnumSerializationTest.scala: -------------------------------------------------------------------------------- 1 | package io.altoo.serialization.kryo.scala 2 | 3 | import com.typesafe.config.ConfigFactory 4 | import io.altoo.serialization.kryo.scala.performance.Time 5 | import io.altoo.serialization.kryo.scala.performance.Time.Time 6 | import org.scalatest.flatspec.AnyFlatSpec 7 | import org.scalatest.matchers.should.Matchers 8 | 9 | import scala.concurrent.duration.Duration 10 | import scala.concurrent.{Await, Future} 11 | 12 | object EnumSerializationTest { 13 | private val config = { 14 | """ 15 | |pekko-kryo-serialization { 16 | | id-strategy = "default" 17 | |} 18 | |""".stripMargin 19 | } 20 | } 21 | 22 | class EnumSerializationTest extends AnyFlatSpec with Matchers { 23 | private val serializer = new ScalaKryoSerializer(ConfigFactory.parseString(EnumSerializationTest.config).withFallback(ConfigFactory.defaultReference()), getClass.getClassLoader) 24 | 25 | behavior of "Enumeration serialization" 26 | 27 | it should "be threadsafe" in { 28 | import scala.concurrent.ExecutionContext.Implicits.global 29 | 30 | val listOfTimes = Time.values.toList 31 | val bytes = serializer.serialize(listOfTimes).get 32 | val futures = (1 to 2).map(_ => 33 | Future[List[Time]] { 34 | serializer.deserialize[List[Time]](bytes).get 35 | }) 36 | 37 | val result = Await.result(Future.sequence(futures), Duration.Inf) 38 | 39 | assert(result.forall { res => res == listOfTimes }) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /core/src/test/scala/io/altoo/serialization/kryo/scala/ParallelActorSystemSerializationTest.scala: -------------------------------------------------------------------------------- 1 | package io.altoo.serialization.kryo.scala 2 | 3 | import com.typesafe.config.ConfigFactory 4 | import org.scalatest.Inside 5 | import org.scalatest.flatspec.AnyFlatSpec 6 | import org.scalatest.matchers.should.Matchers 7 | 8 | import java.nio.ByteBuffer 9 | import scala.concurrent.{Await, Future} 10 | 11 | object ParallelActorSystemSerializationTest { 12 | private val config = 13 | s""" 14 | |scala-kryo-serialization { 15 | | use-unsafe = false 16 | | trace = true 17 | | id-strategy = "automatic" 18 | | implicit-registration-logging = true 19 | | post-serialization-transformations = off 20 | |} 21 | |""".stripMargin 22 | } 23 | 24 | final case class Sample(value: Option[String]) { 25 | override def toString: String = s"Sample()" 26 | } 27 | object Sample { 28 | def apply(value: String) = new Sample(Some(value)) 29 | } 30 | 31 | class ParallelActorSystemSerializationTest extends AnyFlatSpec with Matchers with Inside { 32 | import scala.concurrent.ExecutionContext.Implicits.global 33 | 34 | private val config = ConfigFactory.parseString(ParallelActorSystemSerializationTest.config).withFallback(ConfigFactory.defaultReference()) 35 | private val serializer1 = new ScalaKryoSerializer(config, getClass.getClassLoader) 36 | private val serializer2 = new ScalaKryoSerializer(config, getClass.getClassLoader) 37 | 38 | // regression test against https://github.com/altoo-ag/pekko-kryo-serialization/issues/237 39 | it should "be able to serialize/deserialize in highly concurrent load" in { 40 | val testClass = Sample("auth-store-syncer") 41 | 42 | val results: List[Future[Unit]] = (for (ser <- List(serializer1, serializer2)) 43 | yield List( 44 | Future(testSerialization(testClass, ser)), 45 | Future(testSerialization(testClass, ser)), 46 | Future(testSerialization(testClass, ser)), 47 | Future(testSerialization(testClass, ser)), 48 | Future(testSerialization(testClass, ser)), 49 | Future(testSerialization(testClass, ser)))).flatten 50 | 51 | import scala.concurrent.duration.* 52 | Await.result(Future.sequence(results), 10.seconds) 53 | } 54 | 55 | private def testSerialization(testClass: Sample, serializer: ScalaKryoSerializer): Unit = { 56 | // find the Serializer for it 57 | val serialized = serializer.serialize(testClass).get 58 | 59 | // check serialization/deserialization 60 | val deserialized = serializer.deserialize[AnyRef](serialized) 61 | deserialized shouldBe util.Success(testClass) 62 | 63 | // check buffer serialization/deserialization 64 | val bb = ByteBuffer.allocate(serialized.length * 2) 65 | serializer.serialize(testClass, bb) 66 | bb.flip() 67 | val bufferDeserialized = serializer.deserialize[AnyRef](bb) 68 | bufferDeserialized shouldBe util.Success(testClass) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /core/src/test/scala/io/altoo/serialization/kryo/scala/TransformationSerializationTest.scala: -------------------------------------------------------------------------------- 1 | package io.altoo.serialization.kryo.scala 2 | 3 | import com.typesafe.config.ConfigFactory 4 | import org.scalatest.Inside 5 | import org.scalatest.flatspec.AnyFlatSpec 6 | import org.scalatest.matchers.should.Matchers 7 | import scala.annotation.nowarn 8 | import java.nio.ByteBuffer 9 | import scala.collection.{immutable, mutable} 10 | import scala.collection.immutable.{HashMap, TreeMap} 11 | 12 | object TransformationSerializationTest { 13 | private val defaultConfig = 14 | """ 15 | |scala-kryo-serializer { 16 | | type = "nograph" 17 | | id-strategy = "incremental" 18 | | kryo-reference-map = false 19 | | buffer-size = 65536 20 | | post-serializer-transformations = off 21 | | implicit-registration-logging = true 22 | | encryption { 23 | | aes { 24 | | key-provider = "io.altoo.serializer.kryo.scala.DefaultKeyProvider" 25 | | mode = "AES/GCM/NoPadding" 26 | | iv-length = 12 27 | | password = "j68KkRjq21ykRGAQ" 28 | | salt = "pepper" 29 | | } 30 | | } 31 | |} 32 | |""".stripMargin 33 | } 34 | 35 | class ZipTransformationserializerTest extends TransformationSerializationTest("Zip", "scala-kryo-serializer.post-serializer-transformations = deflate") 36 | class Lz4TransformationserializerTest extends TransformationSerializationTest("LZ4", "scala-kryo-serializer.post-serializer-transformations = lz4") 37 | class AESTransformationserializerTest extends TransformationSerializationTest("AES", "scala-kryo-serializer.post-serializer-transformations = aes") 38 | class ZipAESTransformationserializerTest extends TransformationSerializationTest("ZipAES", """scala-kryo-serializer.post-serializer-transformations = "deflate,aes"""") 39 | class LZ4AESTransformationserializerTest extends TransformationSerializationTest("LZ4AES", """scala-kryo-serializer.post-serializer-transformations = "lz4,aes"""") 40 | class OffTransformationserializerTest extends TransformationSerializationTest("Off", "") 41 | class UnsafeTransformationserializerTest extends TransformationSerializationTest("Unsafe", "scala-kryo-serializer.use-unsafe = true") 42 | class UnsafeLZ4TransformationserializerTest extends TransformationSerializationTest("UnsafeLZ4", 43 | """ 44 | |scala-kryo-serializer.use-unsafe = true 45 | |scala-kryo-serializer.post-serializer-transformations = lz4 46 | """.stripMargin) 47 | 48 | //fails for scala 3 49 | @nowarn("cat=deprecation") 50 | @nowarn("msg=@nowarn annotation does not suppress any warnings") 51 | @nowarn("msg=object AnyRefMap in package mutable is deprecated (since 2.13.16): Use `scala.collection.mutable.HashMap` instead for better performance.") 52 | abstract class TransformationSerializationTest(name: String, testConfig: String) extends AnyFlatSpec with Matchers with Inside { 53 | private val config = ConfigFactory.parseString(testConfig) 54 | .withFallback(ConfigFactory.parseString(TransformationSerializationTest.defaultConfig)) 55 | .withFallback(ConfigFactory.defaultReference()) 56 | 57 | private val serializer = new ScalaKryoSerializer(config, getClass.getClassLoader) 58 | 59 | behavior of s"$name transformation serializer" 60 | 61 | it should "serialize and deserialize immutable TreeMap[String,Any] successfully" in { 62 | val tm = TreeMap[String, Any]( 63 | "foo" -> 123.3, 64 | "bar" -> "something as a text", 65 | "baz" -> null, 66 | "boom" -> true, 67 | "hash" -> HashMap[Int, Int](1 -> 200, 2 -> 300, 500 -> 3)) 68 | 69 | val serialized = serializer.serialize(tm).get 70 | val deserialized = serializer.deserialize[TreeMap[String, Any]](serialized) 71 | deserialized shouldBe util.Success(tm) 72 | 73 | val bb = ByteBuffer.allocate(serialized.length * 2) 74 | 75 | serializer.serialize(tm, bb) 76 | bb.flip() 77 | 78 | val bufferDeserialized = serializer.deserialize[TreeMap[String, Any]](bb) 79 | bufferDeserialized shouldBe util.Success(tm) 80 | } 81 | 82 | it should "serialize and deserialize immutable HashMap[String,Any] successfully" in { 83 | val tm = HashMap[String, Any]( 84 | "foo" -> 123.3, 85 | "bar" -> "something as a text", 86 | "baz" -> null, 87 | "boom" -> true, 88 | "hash" -> HashMap[Int, Int](1 -> 200, 2 -> 300, 500 -> 3)) 89 | 90 | val serialized = serializer.serialize(tm).get 91 | val deserialized = serializer.deserialize[HashMap[String, Any]](serialized) 92 | deserialized shouldBe util.Success(tm) 93 | 94 | val bb = ByteBuffer.allocate(serialized.length * 2) 95 | 96 | serializer.serialize(tm, bb) shouldBe a[util.Success[?]] 97 | bb.flip() 98 | 99 | val bufferDeserialized = serializer.deserialize[HashMap[String, Any]](bb) 100 | bufferDeserialized shouldBe util.Success(tm) 101 | } 102 | 103 | it should "serialize and deserialize mutable AnyRefMap[String,Any] successfully" in { 104 | val r = new scala.util.Random(0L) 105 | val tm = mutable.AnyRefMap[String, Any]( 106 | "foo" -> r.nextDouble(), 107 | "bar" -> "foo,bar,baz", 108 | "baz" -> 124L, 109 | "hash" -> HashMap[Int, Int](r.nextInt() -> r.nextInt(), 5 -> 500, 10 -> r.nextInt())) 110 | 111 | val serialized = serializer.serialize(tm).get 112 | val deserialized = serializer.deserialize[mutable.AnyRefMap[String, Any]](serialized) 113 | deserialized shouldBe util.Success(tm) 114 | 115 | val bb = ByteBuffer.allocate(serialized.length * 2) 116 | 117 | serializer.serialize(tm, bb) shouldBe a[util.Success[?]] 118 | bb.flip() 119 | 120 | val bufferDeserialized = serializer.deserialize[mutable.AnyRefMap[String, Any]](bb) 121 | bufferDeserialized shouldBe util.Success(tm) 122 | } 123 | 124 | it should "serialize and deserialize mutable HashMap[String,Any] successfully" in { 125 | val tm = scala.collection.mutable.HashMap[String, Any]( 126 | "foo" -> 123.3, 127 | "bar" -> "something as a text", 128 | "baz" -> null, 129 | "boom" -> true, 130 | "hash" -> HashMap[Int, Int](1 -> 200, 2 -> 300, 500 -> 3)) 131 | 132 | val serialized = serializer.serialize(tm).get 133 | val deserialized = serializer.deserialize[mutable.HashMap[String, Any]](serialized) 134 | deserialized shouldBe util.Success(tm) 135 | 136 | val bb = ByteBuffer.allocate(serialized.length * 2) 137 | 138 | serializer.serialize(tm, bb) shouldBe a[util.Success[?]] 139 | bb.flip() 140 | 141 | val bufferDeserialized = serializer.deserialize[mutable.HashMap[String, Any]](bb) 142 | bufferDeserialized shouldBe util.Success(tm) 143 | } 144 | 145 | // Sets 146 | it should "serialize and deserialize immutable HashSet[String] successfully" in { 147 | val tm = scala.collection.immutable.HashSet[String]("foo", "bar", "baz", "boom") 148 | 149 | val serialized = serializer.serialize(tm).get 150 | val deserialized = serializer.deserialize[immutable.HashSet[String]](serialized) 151 | deserialized shouldBe util.Success(tm) 152 | 153 | val bb = ByteBuffer.allocate(serialized.length * 2) 154 | 155 | serializer.serialize(tm, bb) shouldBe a[util.Success[?]] 156 | bb.flip() 157 | 158 | val bufferDeserialized = serializer.deserialize[immutable.HashSet[String]](bb) 159 | bufferDeserialized shouldBe util.Success(tm) 160 | } 161 | 162 | it should "serialize and deserialize immutable TreeSet[String] successfully" in { 163 | val tm = scala.collection.immutable.TreeSet[String]("foo", "bar", "baz", "boom") 164 | 165 | val serialized = serializer.serialize(tm).get 166 | val deserialized = serializer.deserialize[immutable.TreeSet[String]](serialized) 167 | deserialized shouldBe util.Success(tm) 168 | 169 | val bb = ByteBuffer.allocate(serialized.length * 2) 170 | 171 | serializer.serialize(tm, bb) shouldBe a[util.Success[?]] 172 | bb.flip() 173 | 174 | val bufferDeserialized = serializer.deserialize[immutable.TreeSet[String]](bb) 175 | bufferDeserialized shouldBe util.Success(tm) 176 | } 177 | 178 | it should "serialize and deserialize mutable HashSet[String] successfully" in { 179 | val tm = scala.collection.mutable.HashSet[String]("foo", "bar", "baz", "boom") 180 | 181 | val serialized = serializer.serialize(tm).get 182 | val deserialized = serializer.deserialize[mutable.HashSet[String]](serialized) 183 | deserialized shouldBe util.Success(tm) 184 | 185 | val bb = ByteBuffer.allocate(serialized.length * 2) 186 | 187 | serializer.serialize(tm, bb) shouldBe a[util.Success[?]] 188 | bb.flip() 189 | 190 | val bufferDeserialized = serializer.deserialize[mutable.HashSet[String]](bb) 191 | bufferDeserialized shouldBe util.Success(tm) 192 | } 193 | 194 | it should "serialize and deserialize mutable TreeSet[String] successfully" in { 195 | val tm = scala.collection.mutable.TreeSet[String]("foo", "bar", "baz", "boom") 196 | 197 | val serialized = serializer.serialize(tm).get 198 | val deserialized = serializer.deserialize[mutable.TreeSet[String]](serialized) 199 | deserialized shouldBe util.Success(tm) 200 | 201 | val bb = ByteBuffer.allocate(serialized.length * 2) 202 | 203 | serializer.serialize(tm, bb) shouldBe a[util.Success[?]] 204 | bb.flip() 205 | 206 | val bufferDeserialized = serializer.deserialize[mutable.TreeSet[String]](bb) 207 | bufferDeserialized shouldBe util.Success(tm) 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /core/src/test/scala/io/altoo/serialization/kryo/scala/performance/EnumPerformanceTests.scala: -------------------------------------------------------------------------------- 1 | package io.altoo.serialization.kryo.scala.performance 2 | 3 | import com.typesafe.config.ConfigFactory 4 | import io.altoo.serialization.kryo.scala.ScalaKryoSerializer 5 | import org.scalatest.* 6 | import org.scalatest.flatspec.AnyFlatSpec 7 | 8 | object Time extends Enumeration { 9 | type Time = Value 10 | val Second, Minute, Hour, Day, Month, Year = Value 11 | } 12 | 13 | object EnumPerformanceTests { 14 | 15 | def main(args: Array[String]): Unit = { 16 | (new PerformanceTests).execute() 17 | } 18 | 19 | private val defaultConfig = 20 | """ 21 | |scala-kryo-serialization { 22 | | id-strategy = "default" 23 | | } 24 | |""".stripMargin 25 | 26 | class PerformanceTests extends AnyFlatSpec with BeforeAndAfterAllConfigMap { 27 | import Time.* 28 | 29 | private val serializer = new ScalaKryoSerializer(ConfigFactory.parseString(EnumPerformanceTests.defaultConfig).withFallback(ConfigFactory.defaultReference()), getClass.getClassLoader) 30 | 31 | private def timeIt[A](name: String, loops: Int)(a: () => A): Unit = { 32 | val now = System.nanoTime 33 | var i = 0 34 | while (i < loops) { 35 | a() 36 | i += 1 37 | } 38 | val ms = (System.nanoTime - now) / 1000000.0 39 | println(f"$name%s:\t$ms%.1f\tms\t=\t${loops * 1000 / ms}%.0f\tops/s") 40 | } 41 | 42 | behavior of "Enumeration serialization" 43 | 44 | it should "be fast" in { 45 | val iterations = 10000 46 | 47 | val listOfTimes = (1 to 1000).flatMap { _ => Time.values.toList } 48 | timeIt("Enum Serialize: ", iterations) { () => serializer.serialize(listOfTimes).get } 49 | timeIt("Enum Serialize: ", iterations) { () => serializer.serialize(listOfTimes).get } 50 | timeIt("Enum Serialize: ", iterations) { () => serializer.serialize(listOfTimes).get } 51 | 52 | val bytes = serializer.serialize(listOfTimes).get 53 | 54 | timeIt("Enum Deserialize: ", iterations)(() => serializer.deserialize[List[Time]](bytes)) 55 | timeIt("Enum Deserialize: ", iterations)(() => serializer.deserialize[List[Time]](bytes)) 56 | timeIt("Enum Deserialize: ", iterations)(() => serializer.deserialize[List[Time]](bytes)) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /core/src/test/scala/io/altoo/serialization/kryo/scala/serializer/EnumerationSerializerTest.scala: -------------------------------------------------------------------------------- 1 | 2 | package io.altoo.serialization.kryo.scala.serializer 3 | 4 | import com.esotericsoftware.kryo.kryo5.Kryo 5 | import com.esotericsoftware.kryo.kryo5.io.{Input, Output} 6 | import org.scalatest.flatspec.AnyFlatSpec 7 | 8 | import scala.language.implicitConversions 9 | 10 | /** @author romix */ 11 | class EnumerationNameSerializerTest extends AnyFlatSpec { 12 | import Planet.* 13 | import Time.* 14 | import WeekDay.* 15 | 16 | 17 | behavior of "EnumerationSerializer" 18 | 19 | it should "serialize and deserialize" in { 20 | var kryo: Kryo = new Kryo() 21 | kryo.setRegistrationRequired(false) 22 | kryo.addDefaultSerializer(classOf[scala.Enumeration#Value], classOf[EnumerationNameSerializer]) 23 | kryo.register(Class.forName("scala.Enumeration$Val")) 24 | kryo.register(classOf[scala.Enumeration#Value]) 25 | kryo.register(WeekDay.getClass, 40) 26 | kryo.register(Time.getClass, 41) 27 | kryo.register(Planet.getClass, 42) 28 | 29 | val obuf1 = new Output(1024, 1024 * 1024) 30 | // Serialize 31 | kryo.writeClassAndObject(obuf1, Tue) 32 | kryo.writeClassAndObject(obuf1, Second) 33 | kryo.writeClassAndObject(obuf1, Earth) 34 | // Deserialize 35 | val bytes = obuf1.toBytes 36 | val ibuf1 = new Input(bytes) 37 | val enumObjWeekday1 = kryo.readClassAndObject(ibuf1) 38 | val enumObjTime1 = kryo.readClassAndObject(ibuf1) 39 | val enumObjPlanet1 = kryo.readClassAndObject(ibuf1) 40 | // Compare 41 | assert(Tue == enumObjWeekday1) 42 | assert(Second == enumObjTime1) 43 | assert(Earth == enumObjPlanet1) 44 | 45 | kryo = new Kryo() 46 | kryo.setRegistrationRequired(false) 47 | kryo.addDefaultSerializer(classOf[scala.Enumeration#Value], classOf[EnumerationNameSerializer]) 48 | kryo.register(Class.forName("scala.Enumeration$Val")) 49 | kryo.register(classOf[scala.Enumeration#Value]) 50 | kryo.register(WeekDay.getClass, 40) 51 | kryo.register(Time.getClass, 41) 52 | kryo.register(Planet.getClass, 42) 53 | val obuf2 = new Output(1024, 1024 * 1024) 54 | // Deserialize 55 | val ibuf2 = new Input(bytes) 56 | val enumObjWeekday2 = kryo.readClassAndObject(ibuf2) 57 | val enumObjTime2 = kryo.readClassAndObject(ibuf2) 58 | val enumObjPlanet2 = kryo.readClassAndObject(ibuf2) 59 | assert(Tue == enumObjWeekday2) 60 | assert(Second == enumObjTime2) 61 | assert(Earth == enumObjPlanet2) 62 | // Serialize 63 | kryo.writeClassAndObject(obuf2, Tue) 64 | kryo.writeClassAndObject(obuf2, Second) 65 | kryo.writeClassAndObject(obuf2, Earth) 66 | // Compare 67 | val ibuf3 = new Input(bytes) 68 | val enumObjWeekday3 = kryo.readClassAndObject(ibuf3) 69 | val enumObjTime3 = kryo.readClassAndObject(ibuf3) 70 | val enumObjPlanet3 = kryo.readClassAndObject(ibuf3) 71 | assert(Tue == enumObjWeekday3) 72 | assert(Second == enumObjTime3) 73 | assert(Earth == enumObjPlanet3) 74 | 75 | 76 | assert(WeekDay.withName(WeekDay.Fri.toString) == WeekDay.Fri) 77 | } 78 | 79 | 80 | behavior of "EnumerationNameSerializer" 81 | 82 | it should "serialize and deserialize" in { 83 | var kryo: Kryo = new Kryo() 84 | kryo.setRegistrationRequired(false) 85 | kryo.addDefaultSerializer(classOf[scala.Enumeration#Value], classOf[EnumerationNameSerializer]) 86 | kryo.register(Class.forName("scala.Enumeration$Val")) 87 | kryo.register(classOf[scala.Enumeration#Value]) 88 | kryo.register(WeekDay.getClass, 40) 89 | kryo.register(Time.getClass, 41) 90 | kryo.register(Planet.getClass, 42) 91 | 92 | val obuf1 = new Output(1024, 1024 * 1024) 93 | // Serialize 94 | kryo.writeClassAndObject(obuf1, Tue) 95 | kryo.writeClassAndObject(obuf1, Second) 96 | kryo.writeClassAndObject(obuf1, Earth) 97 | // Deserialize 98 | val bytes = obuf1.toBytes 99 | val ibuf1 = new Input(bytes) 100 | val enumObjWeekday1 = kryo.readClassAndObject(ibuf1) 101 | val enumObjTime1 = kryo.readClassAndObject(ibuf1) 102 | val enumObjPlanet1 = kryo.readClassAndObject(ibuf1) 103 | // Compare 104 | assert(Tue == enumObjWeekday1) 105 | assert(Second == enumObjTime1) 106 | assert(Earth == enumObjPlanet1) 107 | 108 | kryo = new Kryo() 109 | kryo.setRegistrationRequired(false) 110 | kryo.addDefaultSerializer(classOf[scala.Enumeration#Value], classOf[EnumerationNameSerializer]) 111 | kryo.register(Class.forName("scala.Enumeration$Val")) 112 | kryo.register(classOf[scala.Enumeration#Value]) 113 | kryo.register(WeekDay.getClass, 40) 114 | kryo.register(Time.getClass, 41) 115 | kryo.register(Planet.getClass, 42) 116 | val obuf2 = new Output(1024, 1024 * 1024) 117 | // Deserialize 118 | val ibuf2 = new Input(bytes) 119 | val enumObjWeekday2 = kryo.readClassAndObject(ibuf2) 120 | val enumObjTime2 = kryo.readClassAndObject(ibuf2) 121 | val enumObjPlanet2 = kryo.readClassAndObject(ibuf2) 122 | assert(Tue == enumObjWeekday2) 123 | assert(Second == enumObjTime2) 124 | assert(Earth == enumObjPlanet2) 125 | // Serialize 126 | kryo.writeClassAndObject(obuf2, Tue) 127 | kryo.writeClassAndObject(obuf2, Second) 128 | kryo.writeClassAndObject(obuf2, Earth) 129 | // Compare 130 | val ibuf3 = new Input(bytes) 131 | val enumObjWeekday3 = kryo.readClassAndObject(ibuf3) 132 | val enumObjTime3 = kryo.readClassAndObject(ibuf3) 133 | val enumObjPlanet3 = kryo.readClassAndObject(ibuf3) 134 | assert(Tue == enumObjWeekday3) 135 | assert(Second == enumObjTime3) 136 | assert(Earth == enumObjPlanet3) 137 | 138 | 139 | assert(WeekDay.withName(WeekDay.Fri.toString) == WeekDay.Fri) 140 | } 141 | } 142 | 143 | object WeekDay extends Enumeration { 144 | type WeekDay = Value 145 | val Mon, Tue, Wed, Thu, Fri, Sat, Sun = Value 146 | } 147 | 148 | object Time extends Enumeration { 149 | type Time = Value 150 | val Second, Minute, Hour, Day, Month, Year = Value 151 | } 152 | 153 | object Planet extends Enumeration { 154 | protected case class PlanetVal(mass: Double, radius: Double) extends super.Val { 155 | def surfaceGravity: Double = Planet.G * mass / (radius * radius) 156 | def surfaceWeight(otherMass: Double): Double = otherMass * surfaceGravity 157 | } 158 | implicit def valueToPlanetVal(x: Value): Val = x.asInstanceOf[Val] 159 | 160 | final val G: Double = 6.67300E-11 161 | final val Mercury = PlanetVal(3.303e+23, 2.4397e6) 162 | final val Venus = PlanetVal(4.869e+24, 6.0518e6) 163 | final val Earth = PlanetVal(5.976e+24, 6.37814e6) 164 | final val Mars = PlanetVal(6.421e+23, 3.3972e6) 165 | final val Jupiter = PlanetVal(1.9e+27, 7.1492e7) 166 | final val Saturn = PlanetVal(5.688e+26, 6.0268e7) 167 | final val Uranus = PlanetVal(8.686e+25, 2.5559e7) 168 | final val Neptune = PlanetVal(1.024e+26, 2.4746e7) 169 | } 170 | -------------------------------------------------------------------------------- /core/src/test/scala/io/altoo/serialization/kryo/scala/serializer/MapSerializerTest.scala: -------------------------------------------------------------------------------- 1 | package io.altoo.serialization.kryo.scala.serializer 2 | 3 | import com.esotericsoftware.kryo.kryo5.io.{Input, Output} 4 | import com.esotericsoftware.kryo.kryo5.serializers.MapSerializer 5 | import com.esotericsoftware.kryo.kryo5.{Kryo, Serializer} 6 | import io.altoo.serialization.kryo.scala.testkit.AbstractKryoTest 7 | import scala.annotation.nowarn 8 | import java.util 9 | import java.util.Random 10 | import java.util.concurrent.ConcurrentHashMap 11 | import scala.collection.immutable.{Map, Set, Vector} 12 | 13 | @nowarn("cat=deprecation") 14 | @nowarn("msg=object AnyRefMap in package mutable is deprecated (since 2.13.16): Use `scala.collection.mutable.HashMap` instead for better performance.") 15 | class MapSerializerTest extends AbstractKryoTest { 16 | 17 | private val hugeCollectionSize = 100 18 | 19 | 20 | behavior of "ScalaImmutableMapSerializer" 21 | 22 | it should "roundtrip immutable maps " in { 23 | kryo.setRegistrationRequired(true) 24 | kryo.addDefaultSerializer(classOf[scala.collection.Map[_, _]], classOf[ScalaImmutableMapSerializer]) 25 | ScalaVersionRegistry.registerHashMap(kryo) 26 | val map1 = Map("Rome" -> "Italy", "London" -> "England", "Paris" -> "France", "New York" -> "USA", "Tokio" -> "Japan", "Peking" -> "China", "Brussels" -> "Belgium") 27 | val map2 = map1 + ("Moscow" -> "Russia") 28 | val map3 = map2 + ("Berlin" -> "Germany") 29 | val map4 = map3 ++ Seq("Germany" -> "Berlin", "Russia" -> "Moscow") 30 | testSerializationOf(map1) 31 | testSerializationOf(map2) 32 | testSerializationOf(map3) 33 | testSerializationOf(map4) 34 | } 35 | 36 | 37 | behavior of "ScalaImmutableSetSerializer" 38 | 39 | it should "roundtrip immutable sets " in { 40 | kryo.setRegistrationRequired(true) 41 | kryo.addDefaultSerializer(classOf[scala.collection.immutable.Set[_]], classOf[ScalaImmutableSetSerializer]) 42 | ScalaVersionRegistry.registerHashSet(kryo) 43 | 44 | val set1 = Set("Rome", "Italy", "London", "England", "Paris", "France", "New York", "USA", "Tokio", "Japan", "Peking", "China", "Brussels", "Belgium") 45 | val set2 = set1 ++ Seq("Moscow", "Russia") 46 | val set3 = set2 ++ Seq("Berlin", "Germany") 47 | val set4 = set3 ++ Seq("Germany", "Berlin", "Russia", "Moscow") 48 | testSerializationOf(set1) 49 | testSerializationOf(set2) 50 | testSerializationOf(set3) 51 | testSerializationOf(set4) 52 | } 53 | 54 | 55 | behavior of "ScalaMutableMapSerializer" 56 | 57 | it should "roundtrip mutable maps " in { 58 | kryo.setRegistrationRequired(true) 59 | kryo.addDefaultSerializer(classOf[scala.collection.mutable.HashMap[_, _]], 60 | classOf[ScalaMutableMapSerializer]) 61 | kryo.register(classOf[scala.collection.mutable.HashMap[_, _]], 42) 62 | val map1 = scala.collection.mutable.Map("Rome" -> "Italy", "London" -> "England", "Paris" -> "France", 63 | "New York" -> "USA", "Tokio" -> "Japan", "Peking" -> "China", "Brussels" -> "Belgium") 64 | val map2 = map1 ++ Seq("Moscow" -> "Russia") 65 | val map3 = map2 ++ Seq("Berlin" -> "Germany") 66 | val map4 = map3 ++ Seq("Germany" -> "Berlin", "Russia" -> "Moscow") 67 | testSerializationOf(map1) 68 | testSerializationOf(map2) 69 | testSerializationOf(map3) 70 | testSerializationOf(map4) 71 | } 72 | 73 | it should "roundtrip mutable AnyRefMap" in { 74 | kryo.setRegistrationRequired(false) 75 | kryo.addDefaultSerializer(classOf[scala.collection.mutable.AnyRefMap[_, _]], classOf[ScalaMutableMapSerializer]) 76 | kryo.addDefaultSerializer(classOf[scala.collection.immutable.List[_]], classOf[ScalaCollectionSerializer]) 77 | kryo.register(classOf[scala.collection.mutable.AnyRefMap[AnyRef, Any]], 3040) 78 | 79 | val map1 = scala.collection.mutable.AnyRefMap[String, String]() 80 | 81 | 0 until hugeCollectionSize foreach { i => map1 += ("k" + i) -> ("v" + i) } 82 | val map2 = map1 ++ Seq("Moscow" -> "Russia") 83 | val map3 = map2 ++ Seq("Berlin" -> "Germany") 84 | val map4 = map3 ++ Seq("Germany" -> "Berlin") ++ Seq("Russia" -> "Moscow") 85 | testSerializationOf(map1) 86 | testSerializationOf(map2) 87 | testSerializationOf(map3) 88 | testSerializationOf(map4) 89 | testSerializationOf(List(scala.collection.mutable.AnyRefMap("Leo" -> "Romanoff"))) 90 | } 91 | 92 | it should "roundtrip muttable LongMap" in { 93 | kryo.setRegistrationRequired(false) 94 | kryo.addDefaultSerializer(classOf[scala.collection.mutable.LongMap[_]], classOf[ScalaMutableMapSerializer]) 95 | kryo.addDefaultSerializer(classOf[scala.collection.immutable.List[_]], classOf[ScalaCollectionSerializer]) 96 | kryo.register(classOf[scala.collection.mutable.LongMap[Any]], 3041) 97 | 98 | val map1 = scala.collection.mutable.LongMap[String]() 99 | 100 | 0 until hugeCollectionSize foreach { i => map1 += i.toLong -> ("v" + i) } 101 | val map2 = map1 ++ Seq(110L -> "Russia") 102 | val map3 = map2 ++ Seq(111L -> "Germany") 103 | val map4 = map3 ++ Seq(112L -> "Berlin") ++ Seq(113L -> "Moscow") 104 | testSerializationOf(map1) 105 | testSerializationOf(map2) 106 | testSerializationOf(map3) 107 | testSerializationOf(map4) 108 | testSerializationOf(List(map3)) 109 | } 110 | 111 | 112 | behavior of "Combined collection serializers" 113 | 114 | it should "roundtrip immuttable LongMap" in { 115 | kryo.setRegistrationRequired(false) 116 | kryo.addDefaultSerializer(classOf[scala.collection.immutable.LongMap[_]], classOf[ScalaImmutableMapSerializer]) 117 | kryo.addDefaultSerializer(classOf[scala.collection.immutable.List[_]], classOf[ScalaCollectionSerializer]) 118 | kryo.register(classOf[scala.collection.immutable.LongMap[Any]], 3042) 119 | 120 | var map1 = scala.collection.immutable.LongMap[String]() 121 | 122 | 0 until hugeCollectionSize foreach { i => map1 += i.toLong -> ("v" + i) } 123 | val map2 = map1 + (110L -> "Russia") 124 | val map3 = map2 + (111L -> "Germany") 125 | val map4 = map3 + (112L -> "Berlin") + (113L -> "Moscow") 126 | testSerializationOf(map1) 127 | testSerializationOf(map2) 128 | testSerializationOf(map3) 129 | testSerializationOf(map4) 130 | testSerializationOf(List(map3)) 131 | } 132 | 133 | it should "roundtrip custom classes and maps/vectors/lists of them" in { 134 | kryo.setRegistrationRequired(false) 135 | kryo.addDefaultSerializer(classOf[scala.Enumeration#Value], classOf[EnumerationNameSerializer]) 136 | kryo.addDefaultSerializer(classOf[scala.collection.immutable.Set[_]], classOf[ScalaImmutableSetSerializer]) 137 | kryo.addDefaultSerializer(classOf[scala.collection.Map[_, _]], classOf[ScalaImmutableMapSerializer]) 138 | kryo.addDefaultSerializer(classOf[scala.collection.immutable.Seq[_]], classOf[ScalaCollectionSerializer]) 139 | val scl1 = ScalaClass1() 140 | var map1: Map[String, String] = Map.empty[String, String] 141 | 142 | 0 until hugeCollectionSize foreach { i => map1 += ("k" + i) -> ("v" + i) } 143 | 144 | scl1.map11 = map1 145 | scl1.vector11 = Vector("LL", "ee", "oo") 146 | scl1.vector11 = null 147 | scl1.list11 = List("LL", "ee", "oo", "nn", "ii", "dd", "aa", "ss") 148 | testSerializationOf(scl1) 149 | 150 | val scl2 = ScalaClass1() 151 | scl2.map11 = map1 152 | scl2.vector11 = Vector("LL", "ee", "oo") 153 | scl2.list11 = List("LL", "ee", "oo", "nn") 154 | testSerializationOf(scl2) 155 | } 156 | 157 | it should "roundtrip big immutable maps" in { 158 | kryo.setRegistrationRequired(false) 159 | kryo.register(classOf[scala.collection.immutable.HashMap[_, _]], 40) 160 | kryo.register(classOf[Array[scala.collection.immutable.HashMap[Any, Any]]], 51) 161 | kryo.register(classOf[scala.Tuple1[Any]], 45) 162 | kryo.register(classOf[scala.Tuple2[Any, Any]], 46) 163 | kryo.register(classOf[scala.Tuple3[Any, Any, Any]], 47) 164 | kryo.register(classOf[scala.Tuple4[Any, Any, Any, Any]], 48) 165 | kryo.register(classOf[scala.Tuple5[Any, Any, Any, Any, Any]], 49) 166 | kryo.register(classOf[scala.Tuple6[Any, Any, Any, Any, Any, Any]], 50) 167 | kryo.addDefaultSerializer(classOf[scala.Enumeration#Value], classOf[EnumerationNameSerializer]) 168 | kryo.addDefaultSerializer(classOf[scala.collection.immutable.Set[_]], classOf[ScalaImmutableSetSerializer]) 169 | kryo.addDefaultSerializer(classOf[scala.collection.immutable.List[_]], classOf[ScalaCollectionSerializer]) 170 | 171 | var map1: Map[String, String] = Map.empty[String, String] 172 | 173 | 0 until hugeCollectionSize foreach { i => map1 += ("k" + i) -> ("v" + i) } 174 | val map2 = map1 + ("Moscow" -> "Russia") 175 | val map3 = map2 + ("Berlin" -> "Germany") 176 | val map4 = map3 + ("Germany" -> "Berlin") + ("Russia" -> "Moscow") 177 | testSerializationOf(map1) 178 | testSerializationOf(map2) 179 | testSerializationOf(map3) 180 | testSerializationOf(map4) 181 | testSerializationOf(List(Map("Leo" -> "Romanoff"))) 182 | } 183 | 184 | it should "roundtrip big immutable sets" in { 185 | kryo.setRegistrationRequired(false) 186 | kryo.register(classOf[Array[scala.collection.immutable.HashSet[_]]], 51) 187 | kryo.register(classOf[scala.Tuple1[Any]], 45) 188 | kryo.register(classOf[scala.Tuple2[Any, Any]], 46) 189 | kryo.register(classOf[scala.Tuple3[Any, Any, Any]], 47) 190 | kryo.register(classOf[scala.Tuple4[Any, Any, Any, Any]], 48) 191 | kryo.register(classOf[scala.Tuple5[Any, Any, Any, Any, Any]], 49) 192 | kryo.register(classOf[scala.Tuple6[Any, Any, Any, Any, Any, Any]], 50) 193 | kryo.addDefaultSerializer(classOf[scala.Enumeration#Value], classOf[EnumerationNameSerializer]) 194 | var map1 = Set.empty[String] 195 | 196 | 0 until hugeCollectionSize foreach { i => map1 += ("k" + i) } 197 | 198 | val map2 = map1 + "Moscow" 199 | val map3 = map2 + "Berlin" 200 | val map4 = map3 + "Germany" + "Russia" 201 | testSerializationOf(map1) 202 | testSerializationOf(map2) 203 | testSerializationOf(map3) 204 | testSerializationOf(map4) 205 | } 206 | 207 | it should "roundtrip big immutable lists" in { 208 | kryo.setRegistrationRequired(false) 209 | // Support serialization of Scala collections 210 | kryo.register(classOf[scala.collection.immutable.$colon$colon[_]], 60) 211 | kryo.addDefaultSerializer(classOf[scala.collection.immutable.List[_]], classOf[ScalaCollectionSerializer]) 212 | kryo.addDefaultSerializer(classOf[scala.Enumeration#Value], classOf[EnumerationNameSerializer]) 213 | 214 | var map1 = List.empty[String] 215 | 216 | 0 until 1000 foreach { i => map1 = ("k" + i) :: map1 } 217 | 218 | val map2 = "Moscow" :: "Russia" :: map1 219 | val map3 = "Berlin" :: "Germany" :: map2 220 | val map4 = "Germany" :: "Berlin" :: "Russia" :: "Moscow" :: map3 221 | testSerializationOf(map1) 222 | testSerializationOf(map2) 223 | testSerializationOf(map3) 224 | testSerializationOf(map4) 225 | } 226 | 227 | it should "roundtrip big immutable sequences" in { 228 | kryo.setRegistrationRequired(false) 229 | kryo.register(classOf[scala.collection.immutable.$colon$colon[_]], 40) 230 | kryo.addDefaultSerializer(classOf[scala.Enumeration#Value], classOf[EnumerationNameSerializer]) 231 | kryo.addDefaultSerializer(classOf[scala.collection.immutable.Set[_]], classOf[ScalaImmutableSetSerializer]) 232 | kryo.addDefaultSerializer(classOf[scala.collection.immutable.List[_]], classOf[ScalaCollectionSerializer]) 233 | val map1 = Seq("Rome", "Italy", "London", "England", "Paris", "France") 234 | val map2 = Seq("Moscow", "Russia") ++ map1 235 | val map3 = Seq("Berlin", "Germany") ++ map2 236 | val map4 = Seq("Germany", "Berlin", "Russia", "Moscow") ++ map3 237 | testSerializationOf(map1) 238 | testSerializationOf(map2) 239 | testSerializationOf(map3) 240 | testSerializationOf(map4) 241 | } 242 | 243 | it should "roundtrip empty java hash map" in { 244 | kryo.setRegistrationRequired(false) 245 | execute(new util.HashMap[Any, Any](), 0) 246 | } 247 | 248 | it should "roundtrip non-empty java hash map" in { 249 | kryo.setRegistrationRequired(false) 250 | execute(new util.HashMap[Any, Any](), 1000) 251 | } 252 | 253 | it should "roundtrip empty concurrent hash map" in { 254 | kryo.setRegistrationRequired(false) 255 | execute(new ConcurrentHashMap[Any, Any](), 0) 256 | } 257 | 258 | it should "roundtrip non-empty concurrent hash map" in { 259 | kryo.setRegistrationRequired(false) 260 | execute(new ConcurrentHashMap[Any, Any], 1000) 261 | } 262 | 263 | it should "roundtrip scala hash map" in { 264 | kryo.setRegistrationRequired(false) 265 | kryo.register(classOf[scala.collection.immutable.HashMap[_, _]]) 266 | var map = new scala.collection.immutable.HashMap[String, Int]() 267 | map ++= Seq("foo" -> 1, "bar" -> 2) 268 | testSerializationOf(map) 269 | } 270 | 271 | it should "roundtrip tree map" in { 272 | kryo.setRegistrationRequired(false) 273 | kryo.register(classOf[util.TreeMap[_, _]]) 274 | val map = new util.TreeMap[Any, Any]() 275 | map.put("123", "456") 276 | map.put("789", "abc") 277 | testSerializationOf(map) 278 | } 279 | 280 | 281 | private def execute(map: java.util.Map[Any, Any], inserts: Int) = { 282 | val random = new Random() 283 | 0 until inserts foreach { _ => map.put(random.nextLong(), random.nextBoolean()) } 284 | 285 | val kryo = new Kryo() 286 | kryo.register(classOf[util.HashMap[Any, Any]], new MapSerializer().asInstanceOf[Serializer[Map[Any, Any]]]) 287 | kryo.register(classOf[ConcurrentHashMap[Any, Any]], new MapSerializer().asInstanceOf[Serializer[Map[Any, Any]]]) 288 | 289 | val output = new Output(2048, -1) 290 | kryo.writeClassAndObject(output, map) 291 | output.close() 292 | 293 | val input = new Input(output.toBytes) 294 | val deserialized = kryo.readClassAndObject(input) 295 | input.close() 296 | 297 | assert(map == deserialized) 298 | } 299 | } 300 | 301 | case class ScalaClass1(var opt: Option[java.lang.Integer] = Some(3), var vector11: Vector[String] = Vector("LL", "ee", "oo"), var list11: List[String] = List("LL", "ee", "oo"), var map11: Map[String, String] = Map("Leo" -> "John", "Luke" -> "Lea")) 302 | -------------------------------------------------------------------------------- /core/src/test/scala/io/altoo/serialization/kryo/scala/serializer/ScalaKryoTest.scala: -------------------------------------------------------------------------------- 1 | package io.altoo.serialization.kryo.scala.serializer 2 | 3 | import com.esotericsoftware.kryo.kryo5.util.{DefaultClassResolver, ListReferenceResolver} 4 | import io.altoo.serialization.kryo.scala.testkit.KryoSerializationTesting 5 | import org.scalatest.flatspec.AnyFlatSpec 6 | 7 | class ScalaKryoTest extends AnyFlatSpec with KryoSerializationTesting { 8 | 9 | protected override val kryo: ScalaKryo = new ScalaKryo(new DefaultClassResolver(), new ListReferenceResolver()) 10 | kryo.setRegistrationRequired(false) 11 | 12 | behavior of "ScalaKryo" 13 | 14 | it should "preserve Nil equality" in { 15 | val deserializedNil = testSerializationOf(Nil) 16 | assert(deserializedNil eq Nil) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /core/src/test/scala/io/altoo/serialization/kryo/scala/serializer/ScalaObjectSerializerTest.scala: -------------------------------------------------------------------------------- 1 | package io.altoo.serialization.kryo.scala.serializer 2 | 3 | import io.altoo.serialization.kryo.scala.testkit.AbstractKryoTest 4 | 5 | import java.util.UUID 6 | 7 | trait Snowflake { 8 | val state: UUID = UUID.randomUUID() 9 | 10 | override def hashCode(): Int = state.hashCode() 11 | 12 | override def equals(another: Any): Boolean = another match { 13 | case anotherSnowflake: Snowflake => 14 | // NOTE: don't worry about respecting different flavours of 15 | // subclass, as all snowflakes are constructed different from 16 | // each other to start with. Only copies can be equal! 17 | this.state == anotherSnowflake.state 18 | case _ => false 19 | } 20 | } 21 | 22 | object standalone extends Snowflake 23 | 24 | object ScalaObjectSerializerTest extends Snowflake 25 | 26 | class ScalaObjectSerializerTest extends AbstractKryoTest { 27 | private def configureKryo(): Unit = { 28 | kryo.setRegistrationRequired(false) 29 | // NOTE: to support building under Scala 2.12, use the Java approach of obtaining 30 | // a singleton object's class at runtime, rather than `classOf[singleton.type]` 31 | kryo.addDefaultSerializer(standalone.getClass, classOf[ScalaObjectSerializer[Any]]) 32 | kryo.addDefaultSerializer(ScalaObjectSerializerTest.getClass, classOf[ScalaObjectSerializer[Any]]) 33 | } 34 | 35 | behavior of "ScalaObjectSerializer" 36 | 37 | it should "round trip standalone and companion objects" in { 38 | configureKryo() 39 | 40 | (testSerializationOf(standalone) should be).theSameInstanceAs(standalone) 41 | 42 | (testSerializationOf(ScalaObjectSerializerTest) should be).theSameInstanceAs(ScalaObjectSerializerTest) 43 | } 44 | 45 | it should "support copying of standalone and companion objects" in { 46 | configureKryo() 47 | 48 | (testCopyingOf(standalone) should be).theSameInstanceAs(standalone) 49 | 50 | (testCopyingOf(ScalaObjectSerializerTest) should be).theSameInstanceAs(ScalaObjectSerializerTest) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /core/src/test/scala/io/altoo/serialization/kryo/scala/serializer/ScalaUnitSerializerTest.scala: -------------------------------------------------------------------------------- 1 | package io.altoo.serialization.kryo.scala.serializer 2 | 3 | import io.altoo.serialization.kryo.scala.testkit.AbstractKryoTest 4 | 5 | 6 | class ScalaUnitSerializerTest extends AbstractKryoTest { 7 | 8 | behavior of "ScalaUnitSerializer" 9 | 10 | it should "roundtrip unit " in { 11 | kryo.setRegistrationRequired(true) 12 | kryo.addDefaultSerializer(classOf[scala.runtime.BoxedUnit], classOf[ScalaUnitSerializer]) 13 | kryo.register(classOf[scala.runtime.BoxedUnit], 50) 14 | testSerializationOf(()) 15 | } 16 | 17 | it should "roundtrip boxedUnit " in { 18 | kryo.setRegistrationRequired(true) 19 | kryo.addDefaultSerializer(classOf[scala.runtime.BoxedUnit], classOf[ScalaUnitSerializer]) 20 | kryo.register(classOf[scala.runtime.BoxedUnit], 50) 21 | testSerializationOf(scala.runtime.BoxedUnit.UNIT) 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /core/src/test/scala/io/altoo/serialization/kryo/scala/serializer/SubclassResolverTest.scala: -------------------------------------------------------------------------------- 1 | package io.altoo.serialization.kryo.scala.serializer 2 | 3 | import io.altoo.serialization.kryo.scala.testkit.AbstractKryoTest 4 | 5 | 6 | class SubclassResolverTest extends AbstractKryoTest { 7 | 8 | override val useSubclassResolver: Boolean = true 9 | 10 | 11 | behavior of "SubclassResolver" 12 | 13 | it should "work with normal Map" in { 14 | kryo.setRegistrationRequired(true) 15 | kryo.addDefaultSerializer(classOf[scala.collection.Map[_, _]], classOf[ScalaImmutableAbstractMapSerializer]) 16 | kryo.register(classOf[scala.collection.immutable.Map[_,_]], 40) 17 | kryo.getClassResolver match { 18 | case resolver:SubclassResolver => resolver.enable() 19 | case _ => // nothing to do 20 | } 21 | val map1 = Map("Rome" -> "Italy", "London" -> "England", "Paris" -> "France", "New York" -> "USA", "Tokio" -> "Japan", "Peking" -> "China", "Brussels" -> "Belgium") 22 | val map2 = map1 + ("Moscow" -> "Russia") 23 | val map3 = map2 + ("Berlin" -> "Germany") 24 | val map4 = map3 + ("Germany" -> "Berlin") + ("Russia" -> "Moscow") 25 | testSerializationOf(map1) 26 | testSerializationOf(map2) 27 | testSerializationOf(map3) 28 | testSerializationOf(map4) 29 | } 30 | 31 | it should "work with empty HashMap" in { 32 | kryo.setRegistrationRequired(true) 33 | kryo.addDefaultSerializer(classOf[scala.collection.Map[_, _]], classOf[ScalaImmutableAbstractMapSerializer]) 34 | kryo.register(classOf[scala.collection.immutable.Map[_,_]], 40) 35 | kryo.getClassResolver match { 36 | case resolver:SubclassResolver => resolver.enable() 37 | case _ => // nothing to do 38 | } 39 | val map1 = Map() 40 | testSerializationOf(map1) 41 | } 42 | 43 | it should "permit more-specific types to work when specified" in { 44 | import scala.collection.immutable.{HashMap, ListMap} 45 | 46 | kryo.setRegistrationRequired(true) 47 | // The usual generic case: 48 | kryo.addDefaultSerializer(classOf[scala.collection.immutable.Map[_, _]], classOf[ScalaImmutableAbstractMapSerializer]) 49 | kryo.register(classOf[scala.collection.immutable.Map[_,_]], 40) 50 | // The more-precise Map type that we want here: 51 | kryo.register(classOf[scala.collection.immutable.ListMap[_,_]], new ScalaImmutableMapSerializer, 41) 52 | kryo.getClassResolver match { 53 | case resolver:SubclassResolver => resolver.enable() 54 | case _ => // nothing to do 55 | } 56 | val map1 = Map("Rome" -> "Italy", "London" -> "England", "Paris" -> "France", "New York" -> "USA", "Tokio" -> "Japan", "Peking" -> "China", "Brussels" -> "Belgium") 57 | val map2 = ListMap("Rome" -> "Italy", "London" -> "England", "Paris" -> "France", "New York" -> "USA", "Tokio" -> "Japan", "Peking" -> "China", "Brussels" -> "Belgium") 58 | val map1Copy = testSerializationOf(map1) 59 | val map2Copy = testSerializationOf(map2) 60 | assert(map1Copy.isInstanceOf[HashMap[_, _]]) 61 | assert(map2Copy.isInstanceOf[ListMap[_,_]]) 62 | } 63 | 64 | it should "work with normal Set" in { 65 | kryo.setRegistrationRequired(true) 66 | kryo.addDefaultSerializer(classOf[scala.collection.Set[_]], classOf[ScalaImmutableAbstractSetSerializer]) 67 | kryo.register(classOf[scala.collection.immutable.Set[_]], 40) 68 | kryo.getClassResolver match { 69 | case resolver:SubclassResolver => resolver.enable() 70 | case _ => // nothing to do 71 | } 72 | 73 | val set1 = Set(83, 84, 959) 74 | testSerializationOf(set1) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /core/src/test/scala/io/altoo/serialization/kryo/scala/serializer/TupleSerializationTest.scala: -------------------------------------------------------------------------------- 1 | 2 | package io.altoo.serialization.kryo.scala.serializer 3 | 4 | import io.altoo.serialization.kryo.scala.testkit.AbstractKryoTest 5 | 6 | 7 | /** @author romix */ 8 | class TupleSerializationTest extends AbstractKryoTest { 9 | 10 | type IntTuple6 = (Int, Int, Int, Int, Int, Int) 11 | 12 | behavior of "Kryo serialization" 13 | 14 | it should "roundtrip tuples" in { 15 | kryo.setRegistrationRequired(false) 16 | kryo.register(classOf[scala.Tuple1[Any]], 45) 17 | kryo.register(classOf[scala.Tuple2[Any, Any]], 46) 18 | kryo.register(classOf[scala.Tuple3[Any, Any, Any]], 47) 19 | kryo.register(classOf[scala.Tuple4[Any, Any, Any, Any]], 48) 20 | kryo.register(classOf[scala.Tuple5[Any, Any, Any, Any, Any]], 49) 21 | kryo.register(classOf[scala.Tuple6[Any, Any, Any, Any, Any, Any]], 50) 22 | 23 | testSerializationOf((1, '2', "Three")) 24 | testSerializationOf((1, '2', "Three")) 25 | testSerializationOf((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17)) 26 | testSerializationOf((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17)) 27 | testSerializationOf((1, 2, 3, 4, 5, 6)) 28 | val intTuple6: IntTuple6 = (11, 22, 33, 44, 55, 66) 29 | testSerializationOf(intTuple6) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /core/src/test/scala/io/altoo/serialization/kryo/scala/testkit/AbstractKryoTest.scala: -------------------------------------------------------------------------------- 1 | package io.altoo.serialization.kryo.scala.testkit 2 | 3 | import com.esotericsoftware.kryo.kryo5.Kryo 4 | import com.esotericsoftware.kryo.kryo5.io.{Input, Output} 5 | import com.esotericsoftware.kryo.kryo5.objenesis.strategy.StdInstantiatorStrategy 6 | import com.esotericsoftware.kryo.kryo5.util.MapReferenceResolver 7 | import io.altoo.serialization.kryo.scala.serializer.SubclassResolver 8 | import org.scalatest.Outcome 9 | import org.scalatest.flatspec.AnyFlatSpec 10 | import org.scalatest.matchers.should.Matchers 11 | 12 | import java.io.{ByteArrayInputStream, ByteArrayOutputStream} 13 | 14 | /** 15 | * Testing directly with a configured Kryo instance. 16 | */ 17 | abstract class AbstractKryoTest extends AnyFlatSpec with KryoSerializationTesting with Matchers { 18 | protected var kryo: Kryo = _ 19 | 20 | protected val useSubclassResolver: Boolean = false 21 | 22 | override def withFixture(test: NoArgTest): Outcome = { 23 | val referenceResolver = new MapReferenceResolver() 24 | if (useSubclassResolver) 25 | kryo = new Kryo(new SubclassResolver(), referenceResolver) 26 | else 27 | kryo = new Kryo(referenceResolver) 28 | kryo.setReferences(true) 29 | kryo.setAutoReset(false) 30 | // Support deserialization of classes without no-arg constructors 31 | kryo.setInstantiatorStrategy(new StdInstantiatorStrategy()) 32 | super.withFixture(test) 33 | } 34 | } 35 | 36 | trait KryoSerializationTesting { 37 | protected def kryo: Kryo 38 | 39 | protected final def testSerializationOf[T](obj: T): T = { 40 | // todo: use Using once support for Scala 2.12 is dropped 41 | val outStream = new ByteArrayOutputStream() 42 | val output = new Output(outStream, 4096) 43 | kryo.writeClassAndObject(output, obj) 44 | output.flush() 45 | val serialized = outStream.toByteArray 46 | output.close() 47 | 48 | val input = new Input(new ByteArrayInputStream(serialized), 4096) 49 | val obj1 = kryo.readClassAndObject(input) 50 | input.close() 51 | 52 | assert(obj == obj1) 53 | 54 | obj1.asInstanceOf[T] 55 | } 56 | 57 | protected final def serialize[T](obj: T): Array[Byte] = { 58 | // todo: use Using once support for Scala 2.12 is dropped 59 | val output = new Output(4096) 60 | kryo.writeClassAndObject(output, obj) 61 | val serialized = output.toBytes 62 | output.close() 63 | serialized 64 | } 65 | 66 | protected final def deserialize[T](serialized: Array[Byte]): T = { 67 | // todo: use Using once support for Scala 2.12 is dropped 68 | val input = new Input(serialized) 69 | val obj1 = kryo.readClassAndObject(input) 70 | input.close() 71 | obj1.asInstanceOf[T] 72 | } 73 | 74 | protected final def testCopyingOf[T](obj: T): T = { 75 | val copy = kryo.copy(obj) 76 | 77 | assert(copy == obj) 78 | 79 | obj 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.11.2 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.4") 2 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.12.2") 3 | addSbtPlugin("com.github.sbt" % "sbt-release" % "1.4.0") 4 | addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.3.1") -------------------------------------------------------------------------------- /sonatype.sbt: -------------------------------------------------------------------------------- 1 | sonatypeProfileName := "io.altoo" 2 | ThisBuild / sonatypeCredentialHost := xerial.sbt.Sonatype.sonatypeCentralHost -------------------------------------------------------------------------------- /version.sbt: -------------------------------------------------------------------------------- 1 | ThisBuild / version := "1.3.1-SNAPSHOT" 2 | --------------------------------------------------------------------------------