Sitelet https://github.com/KrailOrg/q3c-util/commit/b5b7f62fbe1d909f704a11d2fdf97b1756bc3343
Skip to content

Commit b5b7f62

Browse files
author
David Sowerby
committed
Migrated in SerializationSupport and InjectorLocator. Tests for it remain in krail-kotlin, see #10
1 parent 45df6cc commit b5b7f62

3 files changed

Lines changed: 160 additions & 1 deletion

File tree

‎build.gradle‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ group = 'uk.q3c.util'
2222

2323
kaytee {
2424
version {
25-
number = "0.13.2.0"
25+
number = "1.0.0.0"
2626
}
2727
}
2828

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package uk.q3c.util.guice
2+
3+
import com.google.inject.Injector
4+
import java.io.Serializable
5+
6+
/**
7+
* Created by David Sowerby on 26 Mar 2018
8+
*/
9+
interface InjectorLocator : Serializable {
10+
fun get(): Injector
11+
fun put(injector: Injector)
12+
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package uk.q3c.util.guice
2+
3+
4+
import com.google.inject.AbstractModule
5+
import com.google.inject.BindingAnnotation
6+
import com.google.inject.Inject
7+
import com.google.inject.Key
8+
import com.google.inject.spi.InjectionPoint
9+
import org.apache.commons.lang3.reflect.FieldUtils
10+
import org.slf4j.LoggerFactory
11+
import java.io.ObjectInputStream
12+
import java.io.Serializable
13+
import java.lang.reflect.Constructor
14+
import java.lang.reflect.Field
15+
import java.lang.reflect.Modifier
16+
17+
18+
/**
19+
* Created by David Sowerby on 16 Mar 2018
20+
*/
21+
@FunctionalInterface
22+
interface SerializationSupport : Serializable {
23+
var excludedFieldNames: List<String>
24+
fun injectTransientFields(target: Any)
25+
fun checkForNullTransients()
26+
fun deserialize(target: Any, inputStream: ObjectInputStream)
27+
}
28+
29+
30+
class DefaultSerializationSupport @Inject constructor(val injectorLocator: InjectorLocator) : SerializationSupport {
31+
32+
private var log = LoggerFactory.getLogger(this.javaClass.name)
33+
private val candidateFieldKeys: MutableMap<Field, Key<*>> = mutableMapOf()
34+
private val unResolvedFieldKeys: MutableMap<Field, Key<*>> = mutableMapOf()
35+
private val constructorParameterKeys = mutableListOf<Key<*>>()
36+
override var excludedFieldNames: List<String> = listOf()
37+
38+
private lateinit var target: Any
39+
40+
override fun injectTransientFields(target: Any) {
41+
this.target = target
42+
val injector = injectorLocator.get()
43+
44+
val candidateFields = collectCandidateFields()
45+
candidateFields.forEach({ f -> candidateFieldKeys[f] = createFieldKey(f) })
46+
val constructorInjectionPoint = InjectionPoint.forConstructorOf(target.javaClass)
47+
val constructorParams = (constructorInjectionPoint.member as Constructor<*>).parameterTypes
48+
for (i in 0 until constructorInjectionPoint.dependencies.size) {
49+
// ignore params which are Serializable - their associated fields will have been deserialised
50+
if (!Serializable::class.java.isAssignableFrom(constructorParams[i])) {
51+
constructorParameterKeys.add(constructorInjectionPoint.dependencies[i].key)
52+
}
53+
}
54+
55+
56+
for ((field, fieldKey) in candidateFieldKeys) {
57+
if (constructorParameterKeys.contains(fieldKey)) {
58+
field.isAccessible = true
59+
field.set(target, injector.getInstance(fieldKey))
60+
constructorParameterKeys.remove(fieldKey)
61+
log.debug("Injecting ${field.name} after deserialisation")
62+
} else {
63+
unResolvedFieldKeys[field] = fieldKey
64+
log.debug("${field.name} is not a Guice injected field, no injection after deserialisation")
65+
}
66+
}
67+
68+
}
69+
70+
private fun createFieldKey(field: Field): Key<*> {
71+
val genericType = field.genericType
72+
val fieldAnnotations = field.declaredAnnotations
73+
for (annotation in fieldAnnotations) {
74+
val annotationInterfaces = annotation.javaClass.annotatedInterfaces
75+
for (annInterface in annotationInterfaces) {
76+
val annotationType = annInterface.type
77+
if ((annotationType as Class<*>).isAnnotationPresent(BindingAnnotation::class.java)) {
78+
log.debug("$annotationType is a binding annotation")
79+
return Key.get(genericType, annotation)
80+
} else {
81+
log.debug("$annotationType is not a binding annotation, and is therefore ignored")
82+
}
83+
}
84+
}
85+
log.debug("Field ${field.name} has no binding annotations")
86+
return Key.get(genericType)
87+
}
88+
89+
90+
/**
91+
* Execute this method at the end of readObject() to make sure there are no transient fields left with value still at null
92+
*/
93+
override fun checkForNullTransients() {
94+
// we can't just use unresolvedFieldKeys, because the developer could set a transient to null after injectTransientFields was invoked
95+
val candidateFields = collectCandidateFields()
96+
if (candidateFields.isEmpty()) {
97+
// all transients have been populated
98+
log.debug("Deserialization complete, all fields resolved")
99+
if (unResolvedFieldKeys.isNotEmpty()) {
100+
// simply means that user code filled in a field
101+
log.debug("These fields were populated by user code: $unResolvedFieldKeys")
102+
}
103+
if (constructorParameterKeys.isNotEmpty()) {
104+
// somehow all the fields have been populated but not all the constructor params have been used. Warn the developer
105+
log.warn("All transient fields have been populated after deserialization, but these constructor parameters were not used: $constructorParameterKeys. This can occur if you have populated a Guice injected transient field without using SerializationSupport, or you have excluded a Guice injected transient field, or you have missed an annotation from a subclass constructor parameter")
106+
}
107+
} else {
108+
// a field has been missed - throw exception, suggest exclusion
109+
val nullFieldNames = mutableListOf<String>()
110+
for (field in candidateFields) {
111+
nullFieldNames.add(field.name)
112+
}
113+
throw SerializationSupportException("One or more transient fields is still null after deserialization. If this is required state, add them to the 'exclusions' parameter to prevent this exception. Null fields are: $nullFieldNames")
114+
}
115+
}
116+
117+
override fun deserialize(target: Any, inputStream: ObjectInputStream) {
118+
inputStream.defaultReadObject()
119+
injectTransientFields(target)
120+
checkForNullTransients()
121+
}
122+
123+
/**
124+
* Selects fields that need checking for injection. These are transient fields, with a null value, which are not listed in exclusions
125+
*/
126+
private fun collectCandidateFields(): List<Field> {
127+
val fields = FieldUtils.getAllFields(target.javaClass)
128+
// We only process transient fields that have null value - user code may have already set some values
129+
return fields.filter { f -> Modifier.isTransient(f.modifiers) }
130+
.filter { f ->
131+
f.isAccessible = true
132+
f.get(target) == null
133+
}.filter { f -> !excludedFieldNames.contains(f.name) }
134+
}
135+
136+
137+
}
138+
139+
140+
class SerializationSupportModule : AbstractModule() {
141+
override fun configure() {
142+
bind(SerializationSupport::class.java).to(DefaultSerializationSupport::class.java)
143+
}
144+
145+
}
146+
147+
class SerializationSupportException(msg: String) : RuntimeException(msg)

0 commit comments

Comments
 (0)