├── .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 | [![Full test prior to release](https://github.com/altoo-ag/scala-kryo-serialization/actions/workflows/fullTest.yml/badge.svg)](https://github.com/altoo-ag/scala-kryo-serialization/actions/workflows/fullTest.yml) 8 | [![Latest version](https://index.scala-lang.org/altoo-ag/scala-kryo-serialization/scala-kryo-serialization/latest.svg)](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 | 66 | 67 | false 68 | 69 | central 70 | Maven Central Repository 71 | https://repo1.maven.org/maven2 72 | 73 | 74 | 75 | io.altoo 76 | scala-kryo-serialization_2.13 77 | 1.2.0 78 | 79 | ``` 80 | 81 | For snapshots see [Snapshots.md](Snapshots.md) 82 | 83 | 84 | Configuration of scala-kryo-serialization 85 | ---------------------------------------------- 86 | 87 | The following options are available for configuring this serializer: 88 | 89 | * You can add a new `scala-kryo-serialization` section to the configuration to customize the serializer. 90 | Consult the supplied [reference.conf](https://github.com/altoo-ag/scala-kryo-serialization/blob/master/core/src/main/resources/reference.conf) for a detailed explanation of all the options available. 91 | 92 | * Then you can create an instance of `ScalaKryoSerializer` and use it to serialize data. 93 | The serializer implements pooling to perform serialization across multiple threads. 94 | 95 | 96 | How do you create mappings or classes sections with proper content? 97 | ------------------------------------------------------------------- 98 | 99 | One of the easiest ways to understand which classes you need to register in those 100 | sections is to leave both sections first empty and then set 101 | 102 | implicit-registration-logging = true 103 | 104 | As a result, you'll eventually see log messages about implicit registration of 105 | some classes. By default, they will receive some random default ids. Once you see 106 | the names of implicitly registered classes, you can copy them into your mappings 107 | or classes sections and assign an id of your choice to each of those classes. 108 | 109 | You may need to repeat the process several times until you see no further log 110 | messages about implicitly registered classes. 111 | 112 | Another useful trick is to provide your own custom initializer for Kryo (see 113 | below) and inside it, you registerclasses of a few objects that are typically 114 | used by your application, for example: 115 | 116 | ```scala 117 | kryo.register(myObj1.getClass) 118 | kryo.register(myObj2.getClass) 119 | ``` 120 | 121 | Obviously, you can also explicitly assign IDs to your classes in the initializer, 122 | if you wish: 123 | 124 | ```scala 125 | kryo.register(myObj3.getClass, 123) 126 | ``` 127 | 128 | If you use this library as an alternative serialization method when sending messages 129 | between actors, it is extremely important that the order of class registration and 130 | the assigned class IDs are the same for senders and for receivers! 131 | 132 | 133 | How to customize kryo initialization 134 | ------------------------------------ 135 | 136 | To further customize kryo you can extend the `io.altoo.serialization.kryo.scala.DefaultKryoInitializer` and 137 | configure the FQCN under `scala-kryo-serialization.kryo-initializer`. 138 | 139 | #### Configuring default field serializers 140 | In `preInit` a different default serializer can be configured 141 | as it will be picked up by serializers added afterward. 142 | By default, the `com.esotericsoftware.kryo.serializers.FieldSerializer` will be used. 143 | 144 | The available options are: 145 | * `com.esotericsoftware.kryo.serializers.FieldSerializer`
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 | 23 | sonatype-snapshots 24 | sonatype snapshots repo 25 | https://oss.sonatype.org/content/repositories/snapshots 26 | 27 | 28 | 29 | io.altoo 30 | scala-kryo-serialization_2.13 31 | 1.0.0-SNAPSHOT 32 | 33 | ``` -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | import sbtrelease.ReleasePlugin.autoImport.ReleaseTransformations._ 3 | 4 | // Basics 5 | 6 | // note: keep in sync to pekko https://github.com/apache/incubator-pekko/blob/main/project/Dependencies.scala 7 | val mainScalaVersion = "3.3.5" 8 | val secondaryScalaVersions = Seq("2.12.20", "2.13.16") 9 | 10 | val kryoVersion = "5.6.2" 11 | enablePlugins(ReleasePlugin) 12 | addCommandAlias("validatePullRequest", ";+test") 13 | 14 | lazy val root: Project = project.in(file(".")) 15 | .settings(Test / parallelExecution := false) 16 | .settings(commonSettings) 17 | .settings(name := "scala-kryo-serialization") 18 | .settings(releaseProcess := releaseSettings) 19 | .settings(publish / skip := true) 20 | .aggregate(core) 21 | 22 | lazy val core: Project = project.in(file("core")) 23 | .settings(moduleSettings) 24 | .settings(description := "pekko-serialization implementation using kryo - core implementation") 25 | .settings(name := "scala-kryo-serialization") 26 | .settings(libraryDependencies ++= coreDeps ++ testingDeps) 27 | .settings(Compile / unmanagedSourceDirectories += { 28 | scalaBinaryVersion.value match { 29 | case "2.12" => baseDirectory.value / "src" / "main" / "scala-2.12" 30 | case "2.13" => baseDirectory.value / "src" / "main" / "scala-2.13" 31 | case _ => baseDirectory.value / "src" / "main" / "scala-3" 32 | } 33 | }) 34 | .settings(Test / unmanagedSourceDirectories += { 35 | scalaBinaryVersion.value match { 36 | case "2.12" => baseDirectory.value / "src" / "test" / "scala-2.12" 37 | case "2.13" => baseDirectory.value / "src" / "test" / "scala-2.13" 38 | case _ => baseDirectory.value / "src" / "test" / "scala-3" 39 | } 40 | }) 41 | 42 | // Dependencies 43 | lazy val coreDeps = Seq( 44 | "com.esotericsoftware.kryo" % "kryo5" % kryoVersion, 45 | "com.typesafe" % "config" % "1.4.3", 46 | "org.lz4" % "lz4-java" % "1.8.0", 47 | "org.agrona" % "agrona" % "1.22.0", // should match pekko-remote/aeron inherited version 48 | "org.scala-lang.modules" %% "scala-collection-compat" % "2.12.0", 49 | "org.slf4j" % "slf4j-api" % "2.0.16", 50 | "org.slf4j" % "log4j-over-slf4j" % "2.0.16") 51 | 52 | lazy val testingDeps = Seq( 53 | "org.scalatest" %% "scalatest" % "3.2.19" % Test, 54 | "ch.qos.logback" % "logback-classic" % "1.5.16" % Test) 55 | 56 | // Settings 57 | lazy val commonSettings: Seq[Setting[?]] = Seq( 58 | organization := "io.altoo", 59 | ) 60 | 61 | lazy val moduleSettings: Seq[Setting[?]] = commonSettings ++ noReleaseInSubmoduleSettings ++ scalacBasicOptions ++ scalacStrictOptions ++ scalacLintOptions ++ Seq( 62 | scalaVersion := mainScalaVersion, 63 | versionScheme := Some("early-semver"), 64 | crossScalaVersions := (scalaVersion.value +: secondaryScalaVersions), 65 | fork := true, 66 | testForkedParallel := false, 67 | classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.ScalaLibrary, 68 | run / javaOptions += "-XX:+UseAES -XX:+UseAESIntrinsics", // Enabling hardware AES support if available 69 | // required to run serialization with JDK 17 70 | Test / 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", 71 | "--add-opens", "java.base/java.lang.invoke=ALL-UNNAMED", "--add-opens", "java.base/java.math=ALL-UNNAMED"), 72 | // required to run unsafe with JDK 17 73 | Test / javaOptions ++= Seq("--add-opens", "java.base/java.nio=ALL-UNNAMED", "--add-opens", "java.base/sun.nio.ch=ALL-UNNAMED"), 74 | pomExtra := pomExtras, 75 | publishTo := sonatypePublishToBundle.value, 76 | publishMavenStyle := true, 77 | Test / publishArtifact := false, 78 | pomIncludeRepository := { _ => false }) 79 | 80 | lazy val scalacBasicOptions = Seq( 81 | scalacOptions ++= { 82 | scalaBinaryVersion.value match { 83 | case "2.12" | "2.13" => 84 | Seq( 85 | "-encoding", "utf8", 86 | "-feature", 87 | "-unchecked", 88 | "-deprecation", 89 | "-language:existentials", 90 | "-Xlog-reflective-calls", 91 | "-Ywarn-unused:-nowarn", 92 | "-Xsource:3", 93 | "-opt:l:inline", 94 | "-opt-inline-from:io.altoo.pekko.serialization.kryo.*") 95 | case "3" => 96 | Seq( 97 | "-encoding", "utf8", 98 | "-feature", 99 | "-unchecked", 100 | "-deprecation", 101 | "-language:existentials") 102 | } 103 | }) 104 | 105 | // strict options 106 | lazy val scalacStrictOptions = Seq( 107 | scalacOptions ++= { 108 | scalaBinaryVersion.value match { 109 | case "2.12" => 110 | Seq( 111 | "-Xfatal-warnings", 112 | "-Yno-adapted-args", 113 | "-Ywarn-adapted-args", 114 | "-Ywarn-dead-code", 115 | "-Ywarn-extra-implicit", 116 | "-Ywarn-inaccessible", 117 | "-Ywarn-nullary-override", 118 | "-Ywarn-nullary-unit", 119 | "-Ywarn-unused:-explicits,-implicits,_") 120 | case "2.13" => 121 | Seq( 122 | "-Werror", 123 | "-Wdead-code", 124 | "-Wextra-implicit", 125 | "-Wunused:imports", 126 | "-Wunused:patvars", 127 | "-Wunused:privates", 128 | "-Wunused:locals", 129 | // "-Wunused:params", enable once 2.12 support is dropped 130 | ) 131 | case "3" => 132 | Seq( 133 | // "-Xfatal-warnings", enable once dotty supports @nowarn 134 | "-Ycheck-all-patmat" 135 | ) 136 | } 137 | }) 138 | 139 | // lint options 140 | lazy val scalacLintOptions = Seq( 141 | scalacOptions ++= { 142 | scalaBinaryVersion.value match { 143 | case "2.12" => 144 | Seq( 145 | "-Xlint:private-shadow", 146 | "-Xlint:type-parameter-shadow", 147 | "-Xlint:adapted-args", 148 | "-Xlint:unsound-match", 149 | "-Xlint:option-implicit") 150 | case "2.13" => 151 | Seq( 152 | "-Xlint:inaccessible", 153 | "-Xlint:nullary-unit", 154 | "-Xlint:private-shadow", 155 | "-Xlint:type-parameter-shadow", 156 | "-Xlint:adapted-args", 157 | "-Xlint:option-implicit", 158 | "-Xlint:missing-interpolator", 159 | "-Xlint:poly-implicit-overload", 160 | "-Xlint:option-implicit", 161 | "-Xlint:package-object-classes", 162 | "-Xlint:constant", 163 | "-Xlint:nonlocal-return", 164 | "-Xlint:valpattern", 165 | "-Xlint:eta-zero", 166 | "-Xlint:deprecation") 167 | case "3" => 168 | Seq() 169 | } 170 | }) 171 | 172 | lazy val noReleaseInSubmoduleSettings: Seq[Setting[?]] = Seq( 173 | releaseProcess := Seq[ReleaseStep](ReleaseStep(_ => sys.error("cannot release a submodule!")))) 174 | 175 | // Configure cross builds. 176 | lazy val releaseSettings = Seq[ReleaseStep]( 177 | checkSnapshotDependencies, 178 | inquireVersions, 179 | runClean, 180 | runTest, 181 | setReleaseVersion, 182 | commitReleaseVersion, 183 | tagRelease, 184 | // do these manually on checked out tag... verify on https://oss.sonatype.org/#stagingRepositories 185 | // releaseStepCommandAndRemaining("+publishSigned"), 186 | // releaseStepCommand("sonatypeBundleRelease"), 187 | setNextVersion, 188 | commitNextVersion, 189 | pushChanges) 190 | releaseCrossBuild := true 191 | 192 | lazy val pomExtras = https://github.com/altoo-ag/scala-kryo-serialization 193 | 194 | 195 | The Apache Software License, Version 2.0 196 | http://www.apache.org/licenses/LICENSE-2.0.txt 197 | repo 198 | 199 | 200 | 201 | git@github.com:altoo-ag/scala-kryo-serialization.git 202 | scm:git:git@github.com:altoo-ag/scala-kryo-serialization.git 203 | 204 | 205 | 206 | danischroeter 207 | Daniel Schröter 208 | dsc@scaling.ch 209 | 210 | 211 | nvollmar 212 | Nicolas Vollmar 213 | nvo@scaling.ch 214 | 215 | 216 | -------------------------------------------------------------------------------- /core/src/main/resources/reference.conf: -------------------------------------------------------------------------------- 1 | ######################################################### 2 | # Akka akka-kryo-serializer Reference Config File # 3 | ######################################################### 4 | 5 | # This is the reference config file that contains all the default settings. 6 | # Make your edits/overrides in your application.conf. 7 | 8 | scala-kryo-serialization { 9 | # Possibles values for type are: graph or nograph 10 | # graph supports serialization of object graphs with shared nodes 11 | # and cyclic references, but this comes at the expense of a small overhead 12 | # nograph does not support object graphs with shared nodes, but is usually faster 13 | type = "graph" 14 | 15 | # Possible values for id-strategy are: 16 | # default, explicit, incremental, automatic 17 | # 18 | # default - slowest and produces bigger serialized representation. Contains fully- 19 | # qualified class names (FQCNs) for each class 20 | # 21 | # explicit - fast and produces compact serialized representation. Requires that all 22 | # classes that will be serialized are pre-registered using the "mappings" and "classes" 23 | # sections. To guarantee that both sender and receiver use the same numeric ids for the same 24 | # classes it is advised to provide exactly the same entries in the "mappings" section 25 | # 26 | # incremental - fast and produces compact serialized representation. Support optional 27 | # pre-registering of classes using the "mappings" and "classes" sections. If class is 28 | # not pre-registered, it will be registered dynamically by picking a next available id 29 | # To guarantee that both sender and receiver use the same numeric ids for the same 30 | # classes it is advised to pre-register them using at least the "classes" section 31 | # 32 | # automatic - Contains fully-qualified class names (FQCNs) for each class that is not 33 | # pre-registered in the "mappings" and "classes" section 34 | id-strategy = "default" 35 | 36 | # Define a default size for byte buffers used during serialization 37 | buffer-size = 4096 38 | 39 | # The serialization byte buffers are doubled as needed until they exceed 40 | # maxBufferSize and an exception is thrown. Can be -1 for no maximum. 41 | # must be < akka.remote.artery.advanced.maximum-frame-size 42 | max-buffer-size = -1 43 | 44 | # To use a custom queue the [[io.altoo.pekko.serialization.kryo.DefaultQueueBuilder]] 45 | # can be extended and registered here. 46 | queue-builder = "io.altoo.serialization.kryo.scala.DefaultQueueBuilder" 47 | 48 | # If set it will use the UnsafeInput and UnsafeOutput 49 | # Kyro IO instances. Please note that there is no guarantee 50 | # for backward/forward compatibility of unsafe serialization. 51 | # It is also not compatible with the safe-serialized values 52 | use-unsafe = false 53 | 54 | # The transformations that have be done while serialization 55 | # Supported transformations: compression and encryption 56 | # accepted values(comma separated if multiple): off | lz4 | deflate | aes 57 | # Transformations occur in the order they are specified on serialization 58 | # and reverse order on deserialization. For example: "lz4,aes" 59 | # lz4 usually offers a good middle ground between size and performance. 60 | post-serialization-transformations = "off" 61 | 62 | # Settings for aes encryption, if included in transformations AES 63 | # algo mode, key and custom key class can be specified AES algo mode. 64 | # The configured key provider class `io.altoo.pekko.serialization.kryo.DefaultKeyProvider` 65 | # derives a key from the configured password and salt. 66 | # To dynamically provide an aes key extend the `io.altoo.pekko.serialization.kryo.DefaultKeyProvider` 67 | # and configure it here. 68 | # 69 | # Example configuration: 70 | # encryption { 71 | # aes { 72 | # key-provider = "io.altoo.pekko.serialization.kryo.DefaultKeyProvider" 73 | # mode = "AES/GCM/NoPadding" 74 | # iv-length = 12 75 | # # password/salt properties are only required when using the default key provider 76 | # password = j68KkRjq21ykRGAQ 77 | # salt = pepper 78 | # } 79 | # } 80 | 81 | # Log implicitly registered classes. Useful, if you want to know all classes 82 | # which are serialized 83 | implicit-registration-logging = false 84 | 85 | # If enabled, Kryo logs a lot of information about serialization process. 86 | # Useful for debugging and low-level tweaking 87 | kryo-trace = false 88 | 89 | # If enabled, Kryo uses internally a map detecting shared nodes. 90 | # This is a preferred mode for big object graphs with a lot of nodes. 91 | # For small object graphs (e.g. below 10 nodes) set it to false for 92 | # better performance. 93 | kryo-reference-map = true 94 | 95 | # For more advanced customizations the [[io.altoo.pekko.serialization.kryo.DefaultKryoInitializer]] 96 | # can be subclassed and configured here. 97 | # The preInit can be used to change the default field serializer. 98 | # The postInit can be used to register additional serializers and classes. 99 | kryo-initializer = "io.altoo.serialization.kryo.scala.DefaultKryoInitializer" 100 | 101 | # If enabled, allows Kryo to resolve subclasses of registered Types. 102 | # 103 | # This is primarily useful when id-strategy is set to "explicit". In this 104 | # case, all classes to be serialized must be explicitly registered. The 105 | # problem is that a large number of common Scala and Akka types (such as 106 | # Map and ActorRef) are actually traits that mask a large number of 107 | # specialized classes that deal with various situations and optimizations. 108 | # It isn't straightforward to register all of these, so you can instead 109 | # register a single supertype, with a serializer that can handle *all* of 110 | # the subclasses, and the subclasses get serialized with that. 111 | # 112 | # Use this with care: you should only rely on this when you are confident 113 | # that the superclass serializer covers all of the special cases properly. 114 | resolve-subclasses = false 115 | 116 | # Define mappings from a fully qualified class name to a numeric id. 117 | # Using ids instead of FQCN leads to smaller sizes of serialized representations 118 | # and faster serialization. 119 | # 120 | # This section is mandatory for idstartegy=explicit 121 | # This section is optional for idstartegy=incremental 122 | # This section is ignored for idstartegy=default 123 | # 124 | # The smallest possible id should start at 20 (or even higher), because 125 | # ids below it are used by Kryo internally e.g. for built-in Java and 126 | # Scala types. 127 | # 128 | # Some helpful mappings are provided through `supplied-basic-mappings` 129 | # and can be added/extended by: 130 | # 131 | # mappings = ${pekko-kryo-serialization.optional-basic-mappings} { 132 | # fully.qualified.classname1 = id1 133 | # fully.qualified.classname2 = id2 134 | # } 135 | # 136 | mappings { 137 | # fully.qualified.classname1 = id1 138 | # fully.qualified.classname2 = id2 139 | } 140 | 141 | # Define a set of fully qualified class names for 142 | # classes to be used for serialization. 143 | # The ids for those classes will be assigned automatically, 144 | # but respecting the order of declaration in this section 145 | # 146 | # This section is optional for idstartegy=incremental 147 | # This section is ignored for idstartegy=default 148 | # This section is optional for idstartegy=explicit 149 | classes = [ 150 | # fully.qualified.classname1 151 | # fully.qualified.classname2 152 | ] 153 | 154 | # Note: even though only to be helpful, these mappings are considered 155 | # part of the api and changes are to be considered breaking the api 156 | optional-basic-mappings { 157 | // java 158 | "java.util.UUID" = 30 159 | 160 | "java.time.LocalDate" = 31 161 | "java.time.LocalDateTime" = 32 162 | "java.time.LocalTime" = 33 163 | "java.time.ZoneOffset" = 34 164 | "java.time.ZoneRegion" = 35 165 | "java.time.ZonedDateTime" = 36 166 | "java.time.Instant" = 37 167 | "java.time.Duration" = 38 168 | 169 | // scala 170 | "scala.Some" = 50 171 | "scala.None$" = 51 172 | "scala.util.Left" = 52 173 | "scala.util.Right" = 53 174 | "scala.util.Success" = 54 175 | "scala.util.Failure" = 55 176 | 177 | "scala.Tuple2" = 60 178 | "scala.Tuple3" = 61 179 | "scala.Tuple4" = 62 180 | "scala.Tuple5" = 63 181 | "scala.Tuple6" = 64 182 | "scala.Tuple7" = 65 183 | "scala.Tuple8" = 66 184 | } 185 | 186 | optional-scala2_12-mappings = { 187 | "scala.collection.immutable.Nil$" = 70 188 | "scala.collection.immutable.$colon$colon" = 71 189 | "scala.collection.immutable.Map$EmptyMap$" = 72 190 | "scala.collection.immutable.Map$Map1" = 73 191 | "scala.collection.immutable.Map$Map2" = 74 192 | "scala.collection.immutable.Map$Map3" = 75 193 | "scala.collection.immutable.Map$Map4" = 76 194 | "scala.collection.immutable.Set$EmptySet$" = 77 195 | "scala.collection.immutable.Set$Set1" = 78 196 | "scala.collection.immutable.Set$Set2" = 79 197 | "scala.collection.immutable.Set$Set3" = 80 198 | "scala.collection.immutable.Set$Set4" = 81 199 | "scala.collection.immutable.ArraySeq$ofRef" = 82 200 | "scala.collection.immutable.ArraySeq$ofInt" = 83 201 | "scala.collection.immutable.ArraySeq$ofDouble" = 84 202 | "scala.collection.immutable.ArraySeq$ofLong" = 85 203 | "scala.collection.immutable.ArraySeq$ofFloat" = 86 204 | "scala.collection.immutable.ArraySeq$ofChar" = 87 205 | "scala.collection.immutable.ArraySeq$ofByte" = 88 206 | "scala.collection.immutable.ArraySeq$ofShort" = 89 207 | "scala.collection.immutable.ArraySeq$ofBoolean" = 90 208 | "scala.collection.immutable.ArraySeq$ofUnit" = 91 209 | } 210 | 211 | # note: Vector is only available from 2.13.2 and above - for 2.13.0 or 2.13.1 use the optional-scala2_12-mappings 212 | optional-scala2_13-mappings = { 213 | "scala.collection.immutable.Nil$" = 70 214 | "scala.collection.immutable.$colon$colon" = 71 215 | "scala.collection.immutable.Map$EmptyMap$" = 72 216 | "scala.collection.immutable.Map$Map1" = 73 217 | "scala.collection.immutable.Map$Map2" = 74 218 | "scala.collection.immutable.Map$Map3" = 75 219 | "scala.collection.immutable.Map$Map4" = 76 220 | "scala.collection.immutable.Set$EmptySet$" = 77 221 | "scala.collection.immutable.Set$Set1" = 78 222 | "scala.collection.immutable.Set$Set2" = 79 223 | "scala.collection.immutable.Set$Set3" = 80 224 | "scala.collection.immutable.Set$Set4" = 81 225 | "scala.collection.immutable.ArraySeq$ofRef" = 82 226 | "scala.collection.immutable.ArraySeq$ofInt" = 83 227 | "scala.collection.immutable.ArraySeq$ofDouble" = 84 228 | "scala.collection.immutable.ArraySeq$ofLong" = 85 229 | "scala.collection.immutable.ArraySeq$ofFloat" = 86 230 | "scala.collection.immutable.ArraySeq$ofChar" = 87 231 | "scala.collection.immutable.ArraySeq$ofByte" = 88 232 | "scala.collection.immutable.ArraySeq$ofShort" = 89 233 | "scala.collection.immutable.ArraySeq$ofBoolean" = 90 234 | "scala.collection.immutable.ArraySeq$ofUnit" = 91 235 | "scala.collection.immutable.Vector0$" = 92 236 | "scala.collection.immutable.Vector1" = 93 237 | "scala.collection.immutable.Vector2" = 94 238 | "scala.collection.immutable.Vector3" = 95 239 | "scala.collection.immutable.Vector4" = 96 240 | "scala.collection.immutable.Vector5" = 97 241 | "scala.collection.immutable.Vector6" = 98 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /core/src/main/scala-2.12/io/altoo/serialization/kryo/scala/ScalaVersionSerializers.scala: -------------------------------------------------------------------------------- 1 | package io.altoo.serialization.kryo.scala 2 | 3 | import com.esotericsoftware.kryo.kryo5.Kryo 4 | import io.altoo.serialization.kryo.scala.serializer.{ScalaCollectionSerializer, ScalaImmutableMapSerializer, ScalaImmutableSetSerializer} 5 | 6 | private[kryo] object ScalaVersionSerializers { 7 | def mapAndSet(kryo: Kryo) = { 8 | kryo.addDefaultSerializer(classOf[scala.collection.generic.MapFactory[scala.collection.Map]], classOf[ScalaImmutableMapSerializer]) 9 | kryo.addDefaultSerializer(classOf[scala.collection.generic.SetFactory[scala.collection.Set]], classOf[ScalaImmutableSetSerializer]) 10 | } 11 | 12 | def iterable(kryo: Kryo) = { 13 | kryo.addDefaultSerializer(classOf[scala.collection.Traversable[_]], classOf[ScalaCollectionSerializer]) 14 | } 15 | 16 | def enums(kryo: Kryo): Unit = () // Scala 3 only 17 | 18 | def lazyVal(kryo: Kryo): Unit = () // Scala 3 only 19 | } 20 | -------------------------------------------------------------------------------- /core/src/main/scala-2.12/io/altoo/serialization/kryo/scala/serializer/ScalaCollectionSerializer.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 scala.collection.Traversable 22 | 23 | import com.esotericsoftware.kryo.kryo5.Kryo 24 | import com.esotericsoftware.kryo.kryo5.Serializer 25 | import com.esotericsoftware.kryo.kryo5.io.Input 26 | import com.esotericsoftware.kryo.kryo5.io.Output 27 | 28 | /** 29 | * Generic serializer for traversable collections 30 | * 31 | * @author romix 32 | */ 33 | class ScalaCollectionSerializer() extends Serializer[Traversable[_]] { 34 | 35 | override def read(kryo: Kryo, input: Input, typ: Class[_ <: Traversable[_]]): Traversable[_] = { 36 | val len = input.readInt(true) 37 | val inst = kryo.newInstance(typ) 38 | val coll = inst.asInstanceOf[Traversable[Any]].genericBuilder[Any] 39 | 40 | var i = 0 41 | while (i < len) { 42 | coll += kryo.readClassAndObject(input) 43 | i += 1 44 | } 45 | coll.result 46 | } 47 | 48 | override def write(kryo: Kryo, output: Output, obj: Traversable[_]) = { 49 | val collection: Traversable[_] = obj 50 | val len = collection.size 51 | output.writeInt(len, true) 52 | collection.foreach { e: Any => kryo.writeClassAndObject(output, e) } 53 | } 54 | } 55 | 56 | -------------------------------------------------------------------------------- /core/src/main/scala-2.13/io/altoo/serialization/kryo/scala/ScalaVersionSerializers.scala: -------------------------------------------------------------------------------- 1 | package io.altoo.serialization.kryo.scala 2 | 3 | import com.esotericsoftware.kryo.kryo5.Kryo 4 | import io.altoo.serialization.kryo.scala.serializer.{ScalaCollectionSerializer, ScalaImmutableMapSerializer} 5 | 6 | private[kryo] object ScalaVersionSerializers { 7 | def mapAndSet(kryo: Kryo): Unit = { 8 | kryo.addDefaultSerializer(classOf[scala.collection.MapFactory[_root_.scala.collection.Map]], classOf[ScalaImmutableMapSerializer]) 9 | } 10 | 11 | def iterable(kryo: Kryo): Unit = { 12 | kryo.addDefaultSerializer(classOf[scala.collection.Iterable[_]], classOf[ScalaCollectionSerializer]) 13 | } 14 | 15 | def enums(kryo: Kryo): Unit = () // Scala 3 only 16 | 17 | def lazyVal(kryo: Kryo): Unit = () // Scala 3 only 18 | } 19 | -------------------------------------------------------------------------------- /core/src/main/scala-2.13/io/altoo/serialization/kryo/scala/serializer/ScalaCollectionSerializer.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 | /** 25 | * Generic serializer for traversable collections 26 | * 27 | * @author romix 28 | */ 29 | class ScalaCollectionSerializer() extends Serializer[Iterable[_]] { 30 | 31 | override def read(kryo: Kryo, input: Input, typ: Class[_ <: Iterable[_]]): Iterable[_] = { 32 | val len = input.readInt(true) 33 | val inst = kryo.newInstance(typ) 34 | val coll = inst.iterableFactory.newBuilder[Any] 35 | 36 | var i = 0 37 | while (i < len) { 38 | coll += kryo.readClassAndObject(input) 39 | i += 1 40 | } 41 | coll.result() 42 | } 43 | 44 | override def write(kryo: Kryo, output: Output, obj: Iterable[_]): Unit = { 45 | val collection: Iterable[_] = obj 46 | val len = collection.size 47 | output.writeInt(len, true) 48 | collection.foreach { (e: Any) => kryo.writeClassAndObject(output, e) } 49 | } 50 | } 51 | 52 | -------------------------------------------------------------------------------- /core/src/main/scala-3/io/altoo/serialization/kryo/scala/ScalaVersionSerializers.scala: -------------------------------------------------------------------------------- 1 | package io.altoo.serialization.kryo.scala 2 | 3 | import com.esotericsoftware.kryo.kryo5.Kryo 4 | import io.altoo.serialization.kryo.scala.serializer.{LazyValSerializer, ScalaCollectionSerializer, ScalaEnumNameSerializer, ScalaImmutableMapSerializer} 5 | 6 | private[kryo] object ScalaVersionSerializers { 7 | def mapAndSet(kryo: Kryo): Unit = { 8 | kryo.addDefaultSerializer(classOf[scala.collection.MapFactory[_root_.scala.collection.Map]], classOf[ScalaImmutableMapSerializer]) 9 | } 10 | 11 | def iterable(kryo: Kryo): Unit = { 12 | kryo.addDefaultSerializer(classOf[scala.collection.Iterable[?]], classOf[ScalaCollectionSerializer]) 13 | } 14 | 15 | def enums(kryo: Kryo): Unit = { 16 | kryo.addDefaultSerializer(classOf[scala.runtime.EnumValue], classOf[ScalaEnumNameSerializer[scala.runtime.EnumValue]]) 17 | } 18 | 19 | def lazyVal(kryo: Kryo): Unit = { 20 | kryo.register(classOf[scala.runtime.LazyVals.Waiting], new LazyValSerializer) 21 | kryo.register(classOf[scala.runtime.LazyVals.Evaluating.type], new LazyValSerializer) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /core/src/main/scala-3/io/altoo/serialization/kryo/scala/serializer/LazyValSerializer.scala: -------------------------------------------------------------------------------- 1 | package io.altoo.serialization.kryo.scala.serializer 2 | 3 | import scala.runtime.LazyVals.LazyValControlState 4 | 5 | import com.esotericsoftware.kryo.kryo5.{Kryo, Serializer} 6 | import com.esotericsoftware.kryo.kryo5.io.{Input, Output} 7 | 8 | class LazyValSerializer extends Serializer[LazyValControlState] { 9 | override def write(kryo: Kryo, output: Output, obj: LazyValControlState): Unit = 10 | kryo.writeClassAndObject(output, null) 11 | 12 | override def read(kryo: Kryo, input: Input, `type`: Class[? <: LazyValControlState]): LazyValControlState = 13 | kryo.readClassAndObject(input).asInstanceOf[LazyValControlState] 14 | } 15 | -------------------------------------------------------------------------------- /core/src/main/scala-3/io/altoo/serialization/kryo/scala/serializer/ScalaCollectionSerializer.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 | /** 25 | * Generic serializer for traversable collections 26 | * 27 | * @author romix 28 | */ 29 | class ScalaCollectionSerializer() extends Serializer[Iterable[?]] { 30 | 31 | override def read(kryo: Kryo, input: Input, typ: Class[? <: Iterable[?]]): Iterable[?] = { 32 | val len = input.readInt(true) 33 | val inst = kryo.newInstance(typ) 34 | val coll = inst.iterableFactory.newBuilder[Any] 35 | 36 | var i = 0 37 | while i < len do { 38 | coll += kryo.readClassAndObject(input) 39 | i += 1 40 | } 41 | coll.result() 42 | } 43 | 44 | override def write(kryo: Kryo, output: Output, obj: Iterable[?]): Unit = { 45 | val collection: Iterable[?] = obj 46 | val len = collection.size 47 | output.writeInt(len, true) 48 | collection.foreach { (e: Any) => kryo.writeClassAndObject(output, e) } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /core/src/main/scala-3/io/altoo/serialization/kryo/scala/serializer/ScalaEnumNameSerializer.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.{Kryo, Serializer} 5 | 6 | import scala.runtime.EnumValue 7 | 8 | /** Serializes enums using the enum's name. This prevents invalidating previously serialized bytes when the enum order changes */ 9 | class ScalaEnumNameSerializer[T <: EnumValue] extends Serializer[T] { 10 | 11 | def read(kryo: Kryo, input: Input, typ: Class[? <: T]): T = { 12 | val clazz = kryo.readClass(input).getType 13 | val name = input.readString() 14 | 15 | try { 16 | // using value instead of ordinal to make serialization more stable, e.g. allowing reordering without breaking compatibility 17 | clazz.getDeclaredMethod("valueOf", classOf[String]).invoke(null, name).asInstanceOf[T] 18 | } catch { 19 | case _: java.lang.NoSuchMethodException => 20 | // work around Scala 3 ADT-like enums missing valueOf method 21 | val objectClazz = Class.forName(clazz.getName + "$") 22 | objectClazz.getDeclaredField(name).get(null).asInstanceOf[T] 23 | } 24 | } 25 | 26 | def write(kryo: Kryo, output: Output, obj: T): Unit = { 27 | val enumClass = obj.getClass.getSuperclass 28 | val productPrefixMethod = obj.getClass.getDeclaredMethod("productPrefix") 29 | if !productPrefixMethod.canAccess(obj) then productPrefixMethod.setAccessible(true) 30 | val name = productPrefixMethod.invoke(obj).asInstanceOf[String] 31 | kryo.writeClass(output, enumClass) 32 | output.writeString(name) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /core/src/main/scala/io/altoo/serialization/kryo/scala/DefaultKeyProvider.scala: -------------------------------------------------------------------------------- 1 | package io.altoo.serialization.kryo.scala 2 | 3 | import com.typesafe.config.Config 4 | 5 | import javax.crypto.SecretKeyFactory 6 | import javax.crypto.spec.PBEKeySpec 7 | 8 | /** 9 | * Default encryption key provider that can be extended to provide encryption key differently. 10 | */ 11 | class DefaultKeyProvider { 12 | 13 | /** 14 | * @param config The config scope of the serializer 15 | */ 16 | def aesKey(config: Config): Array[Byte] = { 17 | val password = config.getString("encryption.aes.password") 18 | val salt = config.getString("encryption.aes.salt") 19 | deriveKey(password, salt) 20 | } 21 | 22 | protected final def deriveKey(password: String, salt: String): Array[Byte] = { 23 | val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") 24 | val spec = new PBEKeySpec(password.toCharArray, salt.getBytes("UTF-8"), 65536, 256) 25 | factory.generateSecret(spec).getEncoded 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /core/src/main/scala/io/altoo/serialization/kryo/scala/DefaultKryoInitializer.scala: -------------------------------------------------------------------------------- 1 | package io.altoo.serialization.kryo.scala 2 | 3 | import com.esotericsoftware.kryo.kryo5.{ClassResolver, ReferenceResolver} 4 | import com.esotericsoftware.kryo.kryo5.serializers.FieldSerializer 5 | import com.esotericsoftware.kryo.kryo5.util.{DefaultClassResolver, ListReferenceResolver, MapReferenceResolver} 6 | import io.altoo.serialization.kryo.scala.serializer.* 7 | 8 | import scala.util.{Failure, Success} 9 | 10 | /** 11 | * Extensible strategy to configure and customize kryo instance. 12 | * Create a subclass of [[DefaultKryoInitializer]] and configure the FQCN under key kryo-initializer. 13 | */ 14 | class DefaultKryoInitializer { 15 | 16 | /** 17 | * Can be overridden to provide a custom reference resolver - override only if you know what you are doing! 18 | */ 19 | def createReferenceResolver(settings: KryoSerializationSettings): ReferenceResolver = { 20 | if (settings.kryoReferenceMap) new MapReferenceResolver() else new ListReferenceResolver() 21 | } 22 | 23 | /** 24 | * Can be overridden to provide a custom class resolver - override only if you know what you are doing! 25 | */ 26 | def createClassResolver(settings: KryoSerializationSettings): ClassResolver = { 27 | if (settings.idStrategy == "incremental") new KryoClassResolver(settings.implicitRegistrationLogging) 28 | else if (settings.resolveSubclasses) new SubclassResolver() 29 | else new DefaultClassResolver() 30 | } 31 | 32 | /** 33 | * Can be overridden to set a different field serializer before other serializer are initialized. 34 | * Note: register custom classes/serializer in `postInit`, otherwise default order might break. 35 | */ 36 | def preInit(kryo: ScalaKryo): Unit = { 37 | kryo.setDefaultSerializer(classOf[com.esotericsoftware.kryo.kryo5.serializers.FieldSerializer[?]]) 38 | } 39 | 40 | /** 41 | * Registers serializer for standard/often used scala classes - override only if you know what you are doing! 42 | */ 43 | def init(kryo: ScalaKryo): Unit = { 44 | initScalaSerializer(kryo) 45 | } 46 | 47 | /** 48 | * Can be overridden to register additional serializer and classes explicitly or reconfigure kryo. 49 | */ 50 | def postInit(kryo: ScalaKryo): Unit = () 51 | 52 | protected def initScalaSerializer(kryo: ScalaKryo): Unit = { 53 | // Support serialization of some standard or often used Scala classes 54 | kryo.addDefaultSerializer(classOf[scala.Enumeration#Value], classOf[EnumerationNameSerializer]) 55 | ReflectionHelper.getClassFor("scala.Enumeration$Val", classOf[Enumeration].getClassLoader) match { 56 | case Success(clazz) => kryo.register(clazz) 57 | case Failure(e) => throw e 58 | } 59 | kryo.register(classOf[scala.Enumeration#Value]) 60 | 61 | // identity preserving serializers for Unit and BoxedUnit 62 | kryo.addDefaultSerializer(classOf[scala.runtime.BoxedUnit], classOf[ScalaUnitSerializer]) 63 | 64 | // mutable maps 65 | kryo.addDefaultSerializer(classOf[scala.collection.mutable.Map[?, ?]], classOf[ScalaMutableMapSerializer]) 66 | 67 | // immutable maps - specialized by mutable, immutable and sortable 68 | kryo.addDefaultSerializer(classOf[scala.collection.immutable.SortedMap[?, ?]], classOf[ScalaSortedMapSerializer]) 69 | kryo.addDefaultSerializer(classOf[scala.collection.immutable.Map[?, ?]], classOf[ScalaImmutableMapSerializer]) 70 | 71 | // Sets - specialized by mutability and sortability 72 | kryo.addDefaultSerializer(classOf[scala.collection.immutable.BitSet], classOf[FieldSerializer[scala.collection.immutable.BitSet]]) 73 | kryo.addDefaultSerializer(classOf[scala.collection.immutable.SortedSet[?]], classOf[ScalaImmutableSortedSetSerializer]) 74 | kryo.addDefaultSerializer(classOf[scala.collection.immutable.Set[?]], classOf[ScalaImmutableSetSerializer]) 75 | 76 | kryo.addDefaultSerializer(classOf[scala.collection.mutable.BitSet], classOf[FieldSerializer[scala.collection.mutable.BitSet]]) 77 | kryo.addDefaultSerializer(classOf[scala.collection.mutable.SortedSet[?]], classOf[ScalaMutableSortedSetSerializer]) 78 | kryo.addDefaultSerializer(classOf[scala.collection.mutable.Set[?]], classOf[ScalaMutableSetSerializer]) 79 | 80 | // Map/Set Factories 81 | ScalaVersionSerializers.mapAndSet(kryo) 82 | ScalaVersionSerializers.iterable(kryo) 83 | ScalaVersionSerializers.enums(kryo) 84 | // Scala 3 LazyVal Serializer 85 | ScalaVersionSerializers.lazyVal(kryo) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /core/src/main/scala/io/altoo/serialization/kryo/scala/DefaultQueueBuilder.scala: -------------------------------------------------------------------------------- 1 | package io.altoo.serialization.kryo.scala 2 | 3 | import org.agrona.concurrent.ManyToManyConcurrentArrayQueue 4 | 5 | import java.util 6 | import scala.reflect.ClassTag 7 | 8 | /** 9 | * Default queue builder that can be extended to use another type of queue. 10 | * Notice that it must be a multiple producer and multiple consumer queue type, 11 | * you could use for example a bounded non-blocking queue. 12 | */ 13 | class DefaultQueueBuilder { 14 | 15 | /** 16 | * Override to use a different queue. 17 | */ 18 | def build[T: ClassTag]: util.Queue[T] = new ManyToManyConcurrentArrayQueue[T](Runtime.getRuntime.availableProcessors * 4) 19 | } 20 | -------------------------------------------------------------------------------- /core/src/main/scala/io/altoo/serialization/kryo/scala/KryoSerializer.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 20 | 21 | import com.esotericsoftware.kryo.kryo5.Kryo 22 | import com.esotericsoftware.kryo.kryo5.minlog.Log as MiniLog 23 | import com.esotericsoftware.kryo.kryo5.objenesis.strategy.StdInstantiatorStrategy 24 | import com.esotericsoftware.kryo.kryo5.util.* 25 | import com.typesafe.config.Config 26 | import io.altoo.serialization.kryo.scala.serializer.* 27 | import org.slf4j.LoggerFactory 28 | 29 | import java.nio.ByteBuffer 30 | import scala.jdk.CollectionConverters.* 31 | import scala.util.* 32 | 33 | /** 34 | * INTERNAL API - api may change at any point in time 35 | * without any warning. 36 | */ 37 | class EncryptionSettings(val config: Config) { 38 | val keyProvider: String = config.getString("encryption.aes.key-provider") 39 | val aesMode: String = config.getString("encryption.aes.mode") 40 | val aesIvLength: Int = config.getInt("encryption.aes.iv-length") 41 | } 42 | 43 | /** 44 | * INTERNAL API - api may change at any point in time 45 | * without any warning. 46 | */ 47 | class KryoSerializationSettings(val config: Config) { 48 | val serializerType: String = config.getString("type") 49 | 50 | val bufferSize: Int = config.getInt("buffer-size") 51 | val maxBufferSize: Int = config.getInt("max-buffer-size") 52 | 53 | // Each entry should be: FQCN -> integer id 54 | val classNameMappings: Map[String, String] = configToMap(config.getConfig("mappings")) 55 | val classNames: java.util.List[String] = config.getStringList("classes") 56 | 57 | // Strategy: default, explicit, incremental, automatic 58 | val idStrategy: String = config.getString("id-strategy") 59 | val implicitRegistrationLogging: Boolean = config.getBoolean("implicit-registration-logging") 60 | 61 | val kryoTrace: Boolean = config.getBoolean("kryo-trace") 62 | val kryoReferenceMap: Boolean = config.getBoolean("kryo-reference-map") 63 | val kryoInitializer: String = config.getString("kryo-initializer") 64 | 65 | val useUnsafe: Boolean = config.getBoolean("use-unsafe") 66 | 67 | val encryptionSettings: Option[EncryptionSettings] = if (config.hasPath("encryption")) Some(new EncryptionSettings(config)) else None 68 | 69 | val postSerTransformations: String = config.getString("post-serialization-transformations") 70 | val queueBuilder: String = config.getString("queue-builder") 71 | val resolveSubclasses: Boolean = config.getBoolean("resolve-subclasses") 72 | 73 | private def configToMap(cfg: Config): Map[String, String] = 74 | cfg.root.unwrapped.asScala.toMap.map { case (k, v) => (k, v.toString) } 75 | } 76 | 77 | private[kryo] abstract class KryoSerializer(config: Config, classLoader: ClassLoader) { 78 | protected def configKey: String 79 | 80 | private val log = LoggerFactory.getLogger(getClass) 81 | private val settings = new KryoSerializationSettings(config.getConfig(configKey)) 82 | 83 | locally { 84 | log.debug("Got mappings: {}", settings.classNameMappings) 85 | log.debug("Got classnames for incremental strategy: {}", settings.classNames) 86 | log.debug("Got buffer-size: {}", settings.bufferSize) 87 | log.debug("Got max-buffer-size: {}", settings.maxBufferSize) 88 | log.debug("Got id strategy: {}", settings.idStrategy) 89 | log.debug("Got serializer type: {}", settings.serializerType) 90 | log.debug("Got implicit registration logging: {}", settings.implicitRegistrationLogging) 91 | log.debug("Got use unsafe: {}", settings.useUnsafe) 92 | log.debug("Got serializer configuration class: {}", settings.kryoInitializer) 93 | log.debug("Got encryption settings: {}", settings.encryptionSettings) 94 | log.debug("Got transformations: {}", settings.postSerTransformations) 95 | log.debug("Got queue builder: {}", settings.queueBuilder) 96 | log.debug("Got resolveSubclasses: {}", settings.resolveSubclasses) 97 | } 98 | 99 | if (settings.kryoTrace) 100 | MiniLog.TRACE() 101 | 102 | private val kryoInitializerClass: Class[? <: DefaultKryoInitializer] = 103 | ReflectionHelper.getClassFor(settings.kryoInitializer, classLoader) match { 104 | case Success(clazz) if classOf[DefaultKryoInitializer].isAssignableFrom(clazz) => clazz.asSubclass(classOf[DefaultKryoInitializer]) 105 | case Success(clazz) => 106 | log.error("Configured class {} does not extend DefaultKryoInitializer", clazz) 107 | throw new IllegalStateException(s"Configured class $clazz does not extend DefaultKryoInitializer") 108 | case Failure(e) => 109 | log.error("Class could not be loaded: {} ", settings.kryoInitializer) 110 | throw e 111 | } 112 | 113 | private val aesKeyProviderClass: Option[Class[? <: DefaultKeyProvider]] = 114 | settings.encryptionSettings.map(c => 115 | ReflectionHelper.getClassFor(c.keyProvider, classLoader) match { 116 | case Success(clazz) if classOf[DefaultKeyProvider].isAssignableFrom(clazz) => clazz.asSubclass(classOf[DefaultKeyProvider]) 117 | case Success(clazz) => 118 | log.error("Configured class {} does not extend DefaultKeyProvider", clazz) 119 | throw new IllegalStateException(s"Configured class $clazz does not extend DefaultKryoInitializer") 120 | case Failure(e) => 121 | log.error("Class could not be loaded: {} ", c.keyProvider) 122 | throw e 123 | }) 124 | 125 | protected[kryo] def useManifest: Boolean 126 | protected[kryo] def prepareKryoInitializer(initializer: DefaultKryoInitializer): Unit 127 | 128 | private val transform: String => Option[Transformer] = { 129 | case "off" => None 130 | case "lz4" => Some(new LZ4KryoCompressor) 131 | case "deflate" => Some(new ZipKryoCompressor) 132 | case "aes" => settings.encryptionSettings match { 133 | case Some(es) if es.aesMode.contains("GCM") => 134 | Some(new KryoCryptographer(aesKeyProviderClass.get.getDeclaredConstructor().newInstance().aesKey(config.getConfig(configKey)), es.aesMode, es.aesIvLength)) 135 | case Some(es) => 136 | throw new Exception(s"Mode ${es.aesMode} is not supported for 'aes'") 137 | case None => 138 | throw new Exception("Encryption transformation selected but encryption has not been configured") 139 | } 140 | case x => throw new Exception(s"Could not recognise the transformer: [$x]") 141 | } 142 | 143 | private val kryoTransformer = new KryoTransformer(settings.postSerTransformations.split(",").toList.flatMap(transform(_).toList)) 144 | 145 | private val queueBuilderClass: Class[? <: DefaultQueueBuilder] = 146 | ReflectionHelper.getClassFor(settings.queueBuilder, classLoader) match { 147 | case Success(clazz) if classOf[DefaultQueueBuilder].isAssignableFrom(clazz) => clazz.asSubclass(classOf[DefaultQueueBuilder]) 148 | case Success(clazz) => 149 | log.error("Configured class {} does not extend DefaultQueueBuilder", clazz) 150 | throw new IllegalStateException(s"Configured class $clazz does not extend DefaultQueueBuilder") 151 | case Failure(e) => 152 | log.error("Class could not be loaded: {} ", settings.queueBuilder) 153 | throw e 154 | } 155 | 156 | // serializer pool to delegate actual serialization 157 | private val serializerPool = new SerializerPool(queueBuilderClass.getDeclaredConstructor().newInstance(), 158 | () => new KryoSerializerBackend(getKryo(settings.idStrategy, settings.serializerType), settings.bufferSize, settings.maxBufferSize, useManifest, settings.useUnsafe)(log, classLoader)) 159 | 160 | // Delegate to a serializer backend 161 | protected def toBinaryInternal(obj: Any): Array[Byte] = { 162 | val ser = serializerPool.fetch() 163 | try 164 | kryoTransformer.toBinary(ser.toBinary(obj)) 165 | finally 166 | serializerPool.release(ser) 167 | } 168 | 169 | protected def toBinaryInternal(obj: Any, buf: ByteBuffer): Unit = { 170 | val ser = serializerPool.fetch() 171 | try { 172 | if (kryoTransformer.isIdentity) 173 | ser.toBinary(obj, buf) 174 | else 175 | kryoTransformer.toBinary(ser.toBinary(obj), buf) 176 | } finally 177 | serializerPool.release(ser) 178 | } 179 | 180 | protected def fromBinaryInternal(bytes: Array[Byte], clazz: Option[Class[?]]): AnyRef = { 181 | val ser = serializerPool.fetch() 182 | try 183 | ser.fromBinary(kryoTransformer.fromBinary(bytes), clazz) 184 | finally 185 | serializerPool.release(ser) 186 | } 187 | 188 | protected def fromBinaryInternal(buf: ByteBuffer, manifest: Option[String]): AnyRef = { 189 | val ser = serializerPool.fetch() 190 | try { 191 | if (kryoTransformer.isIdentity) 192 | ser.fromBinary(buf, manifest) 193 | else 194 | ser.fromBinary(kryoTransformer.fromBinary(buf), manifest.flatMap(ReflectionHelper.getClassFor(_, classLoader).toOption)) 195 | } finally 196 | serializerPool.release(ser) 197 | } 198 | 199 | // Initialization 200 | private def getKryo(strategy: String, serializerType: String): Kryo = { 201 | val initializer = kryoInitializerClass.getDeclaredConstructor().newInstance() 202 | prepareKryoInitializer(initializer) 203 | val referenceResolver = initializer.createReferenceResolver(settings) 204 | val classResolver = initializer.createClassResolver(settings) 205 | val kryo = new ScalaKryo(classResolver, referenceResolver) 206 | kryo.setClassLoader(classLoader) 207 | // support deserialization of classes without no-arg constructors 208 | val instStrategy = kryo.getInstantiatorStrategy.asInstanceOf[DefaultInstantiatorStrategy] 209 | instStrategy.setFallbackInstantiatorStrategy(new StdInstantiatorStrategy()) 210 | kryo.setInstantiatorStrategy(instStrategy) 211 | kryo.setOptimizedGenerics(false) // causes issue serializing classes extending generic base classes 212 | 213 | serializerType match { 214 | case "graph" => kryo.setReferences(true) 215 | case "nograph" => kryo.setReferences(false) 216 | case o => throw new IllegalStateException("Unknown serializer type: " + o) 217 | } 218 | 219 | initializer.preInit(kryo) 220 | initializer.init(kryo) 221 | 222 | // if explicit we require all classes to be registered explicitely 223 | kryo.setRegistrationRequired(strategy == "explicit") 224 | 225 | // register configured class mappings and classes 226 | if (strategy != "default") { 227 | for ((fqcn: String, idNum: String) <- settings.classNameMappings) { 228 | val id = idNum.toInt 229 | ReflectionHelper.getClassFor(fqcn, classLoader) match { 230 | case Success(clazz) => kryo.register(clazz, id) 231 | case Failure(e) => 232 | log.error("Class could not be loaded and/or registered: {} ", fqcn) 233 | throw e 234 | } 235 | } 236 | 237 | for (classname <- settings.classNames.asScala) { 238 | ReflectionHelper.getClassFor(classname, classLoader) match { 239 | case Success(clazz) => kryo.register(clazz) 240 | case Failure(e) => 241 | log.warn("Class could not be loaded and/or registered: {} ", classname) 242 | throw e 243 | } 244 | } 245 | } 246 | 247 | initializer.postInit(kryo) 248 | 249 | classResolver match { 250 | // Now that we're done with registration, turn on the SubclassResolver: 251 | case resolver: SubclassResolver => resolver.enable() 252 | case _ => 253 | } 254 | 255 | kryo 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /core/src/main/scala/io/altoo/serialization/kryo/scala/KryoSerializerBackend.scala: -------------------------------------------------------------------------------- 1 | package io.altoo.serialization.kryo.scala 2 | 3 | import com.esotericsoftware.kryo.kryo5.Kryo 4 | import com.esotericsoftware.kryo.kryo5.io.{ByteBufferInput, ByteBufferOutput, Input, Output} 5 | import com.esotericsoftware.kryo.kryo5.unsafe.{UnsafeInput, UnsafeOutput} 6 | import org.slf4j.Logger 7 | 8 | import java.nio.ByteBuffer 9 | 10 | private[kryo] class KryoSerializerBackend(val kryo: Kryo, val bufferSize: Int, val maxBufferSize: Int, val useManifest: Boolean, val useUnsafe: Boolean)(log: Logger, 11 | classLoader: ClassLoader) { 12 | 13 | // "toBinary" serializes the given object to an Array of Bytes 14 | // Implements Serializer 15 | def toBinary(obj: Any): Array[Byte] = { 16 | val buffer = output 17 | try { 18 | if (useManifest) 19 | kryo.writeObject(buffer, obj) 20 | else 21 | kryo.writeClassAndObject(buffer, obj) 22 | buffer.toBytes 23 | } catch { 24 | case e: StackOverflowError if !kryo.getReferences => // when configured with "nograph" serialization can fail with stack overflow 25 | log.error(s"Could not serialize class with potentially circular references: $classLoader", e) 26 | throw new RuntimeException("Could not serialize class with potential circular references: " + obj) 27 | } finally { 28 | buffer.reset() 29 | } 30 | } 31 | 32 | // Implements ByteBufferSerializer 33 | def toBinary(obj: Any, buf: ByteBuffer): Unit = { 34 | val buffer = getOutput(buf) 35 | try { 36 | if (useManifest) 37 | kryo.writeObject(buffer, obj) 38 | else 39 | kryo.writeClassAndObject(buffer, obj) 40 | buffer.toBytes 41 | } catch { 42 | case e: StackOverflowError if !kryo.getReferences => // when configured with "nograph" serialization can fail with stack overflow 43 | log.error(s"Could not serialize class with potentially circular references: $obj", e) 44 | throw new RuntimeException("Could not serialize class with potential circular references: " + obj) 45 | } 46 | } 47 | 48 | // "fromBinary" deserializes the given array, 49 | // using the type hint (if any, see "includeManifest" above) 50 | // into the optionally provided classLoader. 51 | // Implements Serializer 52 | def fromBinary(bytes: Array[Byte], clazz: Option[Class[?]]): AnyRef = { 53 | val buffer = getInput(bytes) 54 | try { 55 | if (useManifest) 56 | clazz match { 57 | case Some(c) => kryo.readObject(buffer, c).asInstanceOf[AnyRef] 58 | case _ => throw new RuntimeException("Object of unknown class cannot be deserialized") 59 | } 60 | else 61 | kryo.readClassAndObject(buffer) 62 | } finally { 63 | buffer.close() 64 | } 65 | } 66 | 67 | // Implements ByteBufferSerializer 68 | def fromBinary(buf: ByteBuffer, manifest: Option[String]): AnyRef = { 69 | val buffer = getInput(buf) 70 | if (useManifest) { 71 | val clazz = manifest.flatMap(ReflectionHelper.getClassFor(_, classLoader).toOption) 72 | clazz match { 73 | case Some(c) => kryo.readObject(buffer, c) 74 | case _ => throw new RuntimeException("Object of unknown class cannot be deserialized") 75 | } 76 | } else 77 | kryo.readClassAndObject(buffer) 78 | } 79 | 80 | // Used by Serializer implementation 81 | private val output = 82 | if (useUnsafe) 83 | new UnsafeOutput(bufferSize, maxBufferSize) 84 | else 85 | new Output(bufferSize, maxBufferSize) 86 | 87 | // Used by ByteBufferSerializer implementation 88 | private def getOutput(buffer: ByteBuffer): Output = 89 | new ByteBufferOutput(buffer) 90 | 91 | // Used by Serializer implementation 92 | private def getInput(bytes: Array[Byte]): Input = 93 | if (useUnsafe) 94 | new UnsafeInput(bytes) 95 | else 96 | new Input(bytes) 97 | 98 | // Used by ByteBufferSerializer implementation 99 | private def getInput(buffer: ByteBuffer): Input = 100 | new ByteBufferInput(buffer) 101 | 102 | } 103 | -------------------------------------------------------------------------------- /core/src/main/scala/io/altoo/serialization/kryo/scala/ReflectionHelper.scala: -------------------------------------------------------------------------------- 1 | package io.altoo.serialization.kryo.scala 2 | 3 | import scala.util.Try 4 | 5 | object ReflectionHelper { 6 | def getClassFor(fqcn: String, classLoader: ClassLoader): Try[Class[? <: AnyRef]] = 7 | Try[Class[? <: AnyRef]] { 8 | Class.forName(fqcn, false, classLoader).asInstanceOf[Class[? <: AnyRef]] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /core/src/main/scala/io/altoo/serialization/kryo/scala/ScalaKryoSerializer.scala: -------------------------------------------------------------------------------- 1 | package io.altoo.serialization.kryo.scala 2 | 3 | import com.typesafe.config.Config 4 | 5 | import java.nio.ByteBuffer 6 | import scala.util.Try 7 | 8 | /** 9 | * Plain Scala serializer backed by Kryo. 10 | * Implements pooling of serialization backends and only one instance should be held. 11 | */ 12 | class ScalaKryoSerializer(config: Config, classLoader: ClassLoader) extends KryoSerializer(config, classLoader) { 13 | override protected def configKey: String = "scala-kryo-serialization" 14 | override protected[kryo] final def useManifest: Boolean = false 15 | 16 | protected[kryo] def prepareKryoInitializer(initializer: DefaultKryoInitializer): Unit = () 17 | 18 | // serialization api 19 | def serialize(obj: Any): Try[Array[Byte]] = Try(toBinaryInternal(obj)) 20 | 21 | def serialize(obj: Any, buf: ByteBuffer): Try[Unit] = Try(toBinaryInternal(obj, buf)) 22 | 23 | def deserialize[T](bytes: Array[Byte]): Try[T] = Try(fromBinaryInternal(bytes, None).asInstanceOf[T]) 24 | 25 | def deserialize[T](buf: ByteBuffer): Try[T] = Try(fromBinaryInternal(buf, None).asInstanceOf[T]) 26 | } 27 | -------------------------------------------------------------------------------- /core/src/main/scala/io/altoo/serialization/kryo/scala/SerializerPool.scala: -------------------------------------------------------------------------------- 1 | package io.altoo.serialization.kryo.scala 2 | 3 | /** 4 | * Returns a SerializerPool, useful to reduce GC overhead. 5 | * 6 | * @param queueBuilder queue builder. 7 | * @param newInstance Serializer instance builder. 8 | */ 9 | private[kryo] class SerializerPool(queueBuilder: DefaultQueueBuilder, newInstance: () => KryoSerializerBackend) { 10 | 11 | private val pool = queueBuilder.build[KryoSerializerBackend] 12 | 13 | def fetch(): KryoSerializerBackend = { 14 | pool.poll() match { 15 | case null => newInstance() 16 | case o => o 17 | } 18 | } 19 | 20 | def release(o: KryoSerializerBackend): Unit = { 21 | pool.offer(o) 22 | } 23 | 24 | def add(o: KryoSerializerBackend): Unit = { 25 | pool.add(o) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /core/src/main/scala/io/altoo/serialization/kryo/scala/Transformer.scala: -------------------------------------------------------------------------------- 1 | package io.altoo.serialization.kryo.scala 2 | 3 | import java.nio.{ByteBuffer, ByteOrder} 4 | import java.security.SecureRandom 5 | import java.util.zip.{Deflater, Inflater} 6 | import javax.crypto.Cipher 7 | import javax.crypto.spec.{GCMParameterSpec, SecretKeySpec} 8 | import net.jpountz.lz4.{LZ4Exception, LZ4Factory} 9 | 10 | import scala.collection.mutable 11 | 12 | private[kryo] class KryoTransformer(transformations: List[Transformer]) { 13 | private[this] val toPipeLine = transformations.map(x => x.toBinary(_: Array[Byte])).reduceLeftOption(_.andThen(_)).getOrElse(identity(_: Array[Byte])) 14 | private[this] val fromPipeLine = transformations.map(x => x.fromBinary(_: Array[Byte])).reverse.reduceLeftOption(_.andThen(_)).getOrElse(identity(_: Array[Byte])) 15 | 16 | private[this] val toBufferPipeline: (Array[Byte], ByteBuffer) => Unit = transformations match { 17 | case Nil => (_, _) => throw new UnsupportedOperationException("Should be optimized away") 18 | case transformer :: Nil => transformer.toBinary 19 | case transformations => 20 | val pipeline = transformations.init.map(x => x.toBinary(_: Array[Byte])).reduceLeft(_.andThen(_)) 21 | val lastTransformation = transformations.last 22 | (in, out) => lastTransformation.toBinary(pipeline(in), out) 23 | } 24 | 25 | private[this] val fromBufferPipeline: ByteBuffer => Array[Byte] = transformations match { 26 | case Nil => _ => throw new UnsupportedOperationException("Should be optimized away") 27 | case transformer :: Nil => transformer.fromBinary 28 | case transformations => 29 | val pipeline = transformations.init.reverse.map(x => x.fromBinary(_: Array[Byte])).reduceLeft(_.andThen(_)) 30 | val lastTransformation = transformations.last 31 | in => pipeline(lastTransformation.fromBinary(in)) 32 | } 33 | 34 | val isIdentity: Boolean = transformations.isEmpty 35 | 36 | def toBinary(inputBuff: Array[Byte]): Array[Byte] = { 37 | toPipeLine(inputBuff) 38 | } 39 | 40 | def toBinary(inputBuff: Array[Byte], outputBuff: ByteBuffer): Unit = { 41 | toBufferPipeline(inputBuff, outputBuff) 42 | } 43 | 44 | def fromBinary(inputBuff: Array[Byte]): Array[Byte] = { 45 | fromPipeLine(inputBuff) 46 | } 47 | 48 | def fromBinary(inputBuff: ByteBuffer): Array[Byte] = { 49 | fromBufferPipeline(inputBuff) 50 | } 51 | } 52 | 53 | trait Transformer { 54 | def toBinary(inputBuff: Array[Byte]): Array[Byte] 55 | 56 | def toBinary(inputBuff: Array[Byte], outputBuff: ByteBuffer): Unit = outputBuff.put(toBinary(inputBuff)) 57 | 58 | def fromBinary(inputBuff: Array[Byte]): Array[Byte] 59 | 60 | def fromBinary(inputBuff: ByteBuffer): Array[Byte] = { 61 | val in = new Array[Byte](inputBuff.remaining()) 62 | inputBuff.put(in) 63 | fromBinary(in) 64 | } 65 | } 66 | 67 | class LZ4KryoCompressor extends Transformer { 68 | private lazy val lz4factory = LZ4Factory.fastestInstance 69 | 70 | override def toBinary(inputBuff: Array[Byte]): Array[Byte] = { 71 | val inputSize = inputBuff.length 72 | val lz4 = lz4factory.fastCompressor 73 | val maxOutputSize = lz4.maxCompressedLength(inputSize) 74 | val outputBuff = new Array[Byte](maxOutputSize + 4) 75 | val outputSize = lz4.compress(inputBuff, 0, inputSize, outputBuff, 4, maxOutputSize) 76 | 77 | // encode 32 bit length in the first bytes 78 | outputBuff(0) = (inputSize & 0xFF).toByte 79 | outputBuff(1) = (inputSize >> 8 & 0xFF).toByte 80 | outputBuff(2) = (inputSize >> 16 & 0xFF).toByte 81 | outputBuff(3) = (inputSize >> 24 & 0xFF).toByte 82 | outputBuff.take(outputSize + 4) 83 | } 84 | 85 | override def toBinary(inputBuff: Array[Byte], outputBuff: ByteBuffer): Unit = { 86 | val inputSize = inputBuff.length 87 | val lz4 = lz4factory.fastCompressor 88 | // encode 32 bit length in the first bytes 89 | outputBuff.order(ByteOrder.LITTLE_ENDIAN).putInt(inputSize) 90 | try { 91 | lz4.compress(ByteBuffer.wrap(inputBuff), outputBuff) 92 | } catch { 93 | case e: LZ4Exception => 94 | throw new RuntimeException(s"Compression failed for input buffer size: ${inputBuff.length} and output buffer size: ${outputBuff.capacity()}", e) 95 | } 96 | } 97 | 98 | override def fromBinary(inputBuff: Array[Byte]): Array[Byte] = { 99 | fromBinary(ByteBuffer.wrap(inputBuff)) 100 | } 101 | 102 | override def fromBinary(inputBuff: ByteBuffer): Array[Byte] = { 103 | // the first 4 bytes are the original size 104 | val size = inputBuff.order(ByteOrder.LITTLE_ENDIAN).getInt 105 | val lz4 = lz4factory.fastDecompressor() 106 | val outputBuff = new Array[Byte](size) 107 | lz4.decompress(inputBuff, ByteBuffer.wrap(outputBuff)) 108 | outputBuff 109 | } 110 | } 111 | 112 | class ZipKryoCompressor extends Transformer { 113 | 114 | override def toBinary(inputBuff: Array[Byte]): Array[Byte] = { 115 | val deflater = new Deflater(Deflater.BEST_SPEED) 116 | val inputSize = inputBuff.length 117 | val outputBuff = new mutable.ArrayBuilder.ofByte 118 | outputBuff += (inputSize & 0xFF).toByte 119 | outputBuff += (inputSize >> 8 & 0xFF).toByte 120 | outputBuff += (inputSize >> 16 & 0xFF).toByte 121 | outputBuff += (inputSize >> 24 & 0xFF).toByte 122 | 123 | deflater.setInput(inputBuff) 124 | deflater.finish() 125 | val buff = new Array[Byte](4096) 126 | 127 | while (!deflater.finished) { 128 | val n = deflater.deflate(buff) 129 | outputBuff ++= buff.take(n) 130 | } 131 | deflater.end() 132 | outputBuff.result() 133 | } 134 | 135 | override def toBinary(inputBuff: Array[Byte], outputBuff: ByteBuffer): Unit = { 136 | val deflater = new Deflater(Deflater.BEST_SPEED) 137 | val inputSize = inputBuff.length 138 | outputBuff.order(ByteOrder.LITTLE_ENDIAN).putInt(inputSize) 139 | 140 | deflater.setInput(inputBuff) 141 | deflater.finish() 142 | deflater.deflate(outputBuff) 143 | deflater.end() 144 | } 145 | 146 | override def fromBinary(inputBuff: Array[Byte]): Array[Byte] = { 147 | fromBinary(ByteBuffer.wrap(inputBuff)) 148 | } 149 | 150 | override def fromBinary(inputBuff: ByteBuffer): Array[Byte] = { 151 | val inflater = new Inflater() 152 | val size = inputBuff.order(ByteOrder.LITTLE_ENDIAN).getInt() 153 | val outputBuff = new Array[Byte](size) 154 | inflater.setInput(inputBuff) 155 | inflater.inflate(ByteBuffer.wrap(outputBuff)) 156 | inflater.end() 157 | outputBuff 158 | } 159 | } 160 | 161 | class KryoCryptographer(key: Array[Byte], mode: String, ivLength: Int) extends Transformer { 162 | private final val AuthTagLength = 128 163 | 164 | if (ivLength < 12 || ivLength >= 16) { 165 | throw new IllegalStateException("invalid iv length") 166 | } 167 | 168 | private[this] val keySpec = new SecretKeySpec(key, "AES") 169 | private lazy val random = new SecureRandom() 170 | 171 | override def toBinary(plaintext: Array[Byte]): Array[Byte] = { 172 | val cipher = Cipher.getInstance(mode) 173 | // fill randomized IV 174 | val iv = new Array[Byte](ivLength) 175 | random.nextBytes(iv) 176 | // set up encryption 177 | val parameterSpec = new GCMParameterSpec(AuthTagLength, iv) 178 | cipher.init(Cipher.ENCRYPT_MODE, keySpec, parameterSpec) 179 | val ciphertext = cipher.doFinal(plaintext) 180 | // concat IV length, IV and ciphertext 181 | val byteBuffer = ByteBuffer.allocate(4 + iv.length + ciphertext.length) 182 | byteBuffer.putInt(iv.length) 183 | byteBuffer.put(iv) 184 | byteBuffer.put(ciphertext) 185 | byteBuffer.array // output 186 | } 187 | 188 | override def toBinary(inputBuff: Array[Byte], outputBuff: ByteBuffer): Unit = { 189 | val cipher = Cipher.getInstance(mode) 190 | // fill randomized IV 191 | val iv = new Array[Byte](ivLength) 192 | random.nextBytes(iv) 193 | // set up encryption 194 | val parameterSpec = new GCMParameterSpec(AuthTagLength, iv) 195 | cipher.init(Cipher.ENCRYPT_MODE, keySpec, parameterSpec) 196 | // concat IV length, IV and ciphertext 197 | outputBuff.putInt(iv.length) 198 | outputBuff.put(iv) 199 | cipher.doFinal(ByteBuffer.wrap(inputBuff), outputBuff) 200 | } 201 | 202 | override def fromBinary(input: Array[Byte]): Array[Byte] = { 203 | fromBinary(ByteBuffer.wrap(input)) 204 | } 205 | 206 | override def fromBinary(inputBuff: ByteBuffer): Array[Byte] = { 207 | val cipher = Cipher.getInstance(mode) 208 | // extract IV length, IV and ciphertext 209 | val ivLength = inputBuff.getInt() 210 | if (ivLength < 12 || ivLength >= 16) { // check input parameter to protect against attacks 211 | throw new IllegalStateException("invalid iv length") 212 | } 213 | val iv = new Array[Byte](ivLength) 214 | inputBuff.get(iv) 215 | val ciphertext = new Array[Byte](inputBuff.remaining()) 216 | inputBuff.get(ciphertext) 217 | // set up decryption 218 | val parameterSpec = new GCMParameterSpec(AuthTagLength, iv) 219 | cipher.init(Cipher.DECRYPT_MODE, keySpec, parameterSpec) 220 | cipher.doFinal(ciphertext) // plaintext 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /core/src/main/scala/io/altoo/serialization/kryo/scala/serializer/EnumerationNameSerializer.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.{Kryo, Serializer} 5 | 6 | import java.lang.reflect.Field 7 | 8 | /** 9 | * Serializes enumeration by name. 10 | */ 11 | class EnumerationNameSerializer extends Serializer[Enumeration#Value] { 12 | 13 | def read(kryo: Kryo, input: Input, typ: Class[? <: Enumeration#Value]): Enumeration#Value = { 14 | val clazz = kryo.readClass(input).getType 15 | val name = input.readString() 16 | clazz.getDeclaredField("MODULE$").get(null).asInstanceOf[Enumeration].withName(name) 17 | } 18 | 19 | def write(kryo: Kryo, output: Output, obj: Enumeration#Value): Unit = { 20 | val parentEnum = parent(obj.getClass.getSuperclass) 21 | .getOrElse(throw new NoSuchElementException(s"Enumeration not found for $obj")) 22 | val enumClass = parentEnum.get(obj).getClass 23 | kryo.writeClass(output, enumClass) 24 | output.writeString(obj.toString) 25 | } 26 | 27 | private def parent(typ: Class[?]): Option[Field] = 28 | if (typ == null) None 29 | else typ.getDeclaredFields.find(_.getName == "$outer").orElse(parent(typ.getSuperclass)) 30 | } 31 | -------------------------------------------------------------------------------- /core/src/main/scala/io/altoo/serialization/kryo/scala/serializer/KryoClassResolver.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.Registration 22 | import com.esotericsoftware.kryo.kryo5.util.DefaultClassResolver 23 | 24 | class KryoClassResolver(val logImplicits: Boolean) extends DefaultClassResolver { 25 | override def registerImplicit(typ: Class[?]): Registration = { 26 | if (kryo.isRegistrationRequired) { 27 | throw new IllegalArgumentException("Class is not registered: " + typ.getName 28 | + "\nNote: To register this class use: kryo.register(" + typ.getName + ".class);") 29 | } 30 | // registerInternal(new Registration(typ, kryo.getDefaultSerializer(typ), DefaultClassResolver.NAME)) 31 | /* TODO: This does not work if sender and receiver are 32 | * initialized independently and using different order of classes 33 | * Try to ensure that the same ID is assigned to the same classname 34 | * by every Kryo instance: 35 | */ 36 | // Take a next available ID 37 | // register(typ, kryo.getDefaultSerializer(typ)) 38 | // Use typename hashCode as an ID. It is pretty unique and is independent of the order of class registration 39 | // and node that performs it. The disadvantage is: it takes more bytes to encode and it is still dependent 40 | // on the order in which messages arrive on the deserializer side, because only the first message will contain 41 | // the ID->FQCN mapping. 42 | // val implicitRegistration = kryo.register(new Registration(typ, kryo.getDefaultSerializer(typ), typ.getName.hashCode()>>>1)) 43 | val implicitRegistration = kryo.register(new Registration(typ, kryo.getDefaultSerializer(typ), MurmurHash.hash(typ.getName.getBytes("UTF-8"), 0) >>> 1)) 44 | if (logImplicits) { 45 | val registration = kryo.getRegistration(typ) 46 | if (registration.getId == DefaultClassResolver.NAME) 47 | println("Implicitly registered class " + typ.getName) 48 | else 49 | println("Implicitly registered class with id: " + typ.getName + "=" + registration.getId) 50 | } 51 | implicitRegistration 52 | } 53 | } 54 | 55 | /** 56 | * Licensed to the Apache Software Foundation (ASF) under one 57 | * or more contributor license agreements. See the NOTICE file 58 | * distributed with this work for additional information 59 | * regarding copyright ownership. The ASF licenses this file 60 | * to you under the Apache License, Version 2.0 (the 61 | * "License"); you may not use this file except in compliance 62 | * with the License. You may obtain a copy of the License at 63 | * 64 | * http://www.apache.org/licenses/LICENSE-2.0 65 | * 66 | * Unless required by applicable law or agreed to in writing, software 67 | * distributed under the License is distributed on an "AS IS" BASIS, 68 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 69 | * See the License for the specific language governing permissions and 70 | * limitations under the License. 71 | */ 72 | 73 | /** 74 | * This is a very fast, non-cryptographic hash suitable for general hash-based 75 | * lookup. See http://murmurhash.googlepages.com/ for more details. 76 | * 77 | *

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 | --------------------------------------------------------------------------------