sbglasius opened a new pull request, #16296:
URL: https://github.com/apache/grails-core/pull/16296

   Fixes #16294
   Fixes #16295
   
   `someObject as JSON` (and `as XML`) failed for two very common shapes of 
object. Both are fixed here, since fixing the first one only exposes the second.
   
   ## #16294 — `IllegalAccessException` on a non-public class
   
   A class that is not public — anonymous, local or package-private — cannot 
have its read methods invoked reflectively from another package, even though 
the methods themselves carry the `public` modifier. All four bean marshallers 
called `readMethod.invoke(...)` with no accessibility handling, so handing `as 
JSON` an anonymous implementation of a public interface (the usual shape of a 
Spring Security `UserDetails`) failed with:
   
   ```
   java.lang.IllegalAccessException: class 
org.grails.web.converters.marshaller.json.GroovyBeanMarshaller
       cannot access a member of class com.example.DemoController$1 with 
modifiers "public"
   ```
   
   The read method is now resolved to the interface method where one exists, 
and made accessible otherwise:
   
   ```java
   Method invokableMethod = ClassUtils.getInterfaceMethodIfPossible(readMethod, 
clazz);
   ReflectionUtils.makeAccessible(invokableMethod);
   Object value = invokableMethod.invoke(o, (Object[]) null);
   ```
   
   Resolving to the interface is not sufficient on its own — a package-private 
class with a public getter and no interface has nowhere to resolve to — so 
`makeAccessible` carries that case.
   
   The public *field* loops of both `GroovyBeanMarshaller`s failed identically 
on `field.get(o)` and are now made accessible too.
   
   ## #16295 — `IllegalArgumentException` on any `Serializable` bean
   
   `GenericJavaBeanMarshaller` evaluated `field.canAccess(o)` before the static 
check, and per its javadoc `canAccess` throws for a static member when the 
object is non-null. Any bean declaring `private static final long 
serialVersionUID` — nearly every `Serializable` bean — therefore failed:
   
   ```
   java.lang.IllegalArgumentException: non-null object for
       private static final long 
org.springframework.security.core.authority.SimpleGrantedAuthority.serialVersionUID
   ```
   
   The modifier checks now run first so they short-circuit before `canAccess`. 
This is a regression in the 8.x line from `9e60b8a4de`, which mechanically 
swapped the non-throwing `isAccessible()` for `canAccess(o)`.
   
   ## One change beyond the two issues
   
   Groovy compiles the variables captured by an anonymous class into 
`ACC_PUBLIC | ACC_SYNTHETIC` `groovy.lang.Reference` fields. Once the field 
loops could actually read them, they were emitted as duplicate keys with 
empty-object values (`{"name":{},"age":{}}`) — the new tests caught exactly 
that. Synthetic fields are compiler artifacts and never part of a bean's state, 
so they are now skipped in all four marshallers.
   
   ## Files changed
   
   - `grails-converters/.../marshaller/json/GroovyBeanMarshaller.java`
   - `grails-converters/.../marshaller/json/GenericJavaBeanMarshaller.java`
   - `grails-converters/.../marshaller/xml/GroovyBeanMarshaller.java`
   - `grails-converters/.../marshaller/xml/GenericJavaBeanMarshaller.java`
   
   ## Tests
   
   New fixtures under `org.grails.web.converters.beans`, deliberately in a 
different package from the marshallers — in the same package the JVM access 
check passes and neither bug reproduces. They cover a public interface with 
both abstract and `default` read methods (mirroring `UserDetails`), Groovy and 
Java anonymous / package-private / no-interface implementations, an anonymous 
class with a declared public field, and a `Serializable` bean carrying `private 
static final long serialVersionUID` alongside a public constant and a public 
instance field.
   
   Two new specs of 9 features each, driven through the public `new JSON(x)` / 
`new XML(x)` API: `json.NonPublicClassMarshallingSpec` and 
`xml.NonPublicClassMarshallingSpec`. Every non-public case first asserts 
`!Modifier.isPublic(person.getClass().modifiers)`, so the tests fail loudly 
rather than silently passing if a future compiler stops producing a non-public 
class.
   
   With the production change reverted, **18 of 18 fail** with the two reported 
exceptions verbatim; with it, 18/18 pass. Also green locally: full 
`:grails-converters:test`, `:grails-rest-transforms:test`, 
`:grails-test-suite-web:test`, `:grails-test-suite-uber:test`, 
`:grails-web-common:test` and `:grails-converters:codeStyle`.
   
   No documentation change: this restores the documented behaviour of `as JSON` 
/ `as XML` and adds no public API.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to