|
| 1 | +== Stringify Object for Java |
| 2 | + |
| 3 | +https://travis-ci.org/wavesoftware/java-stringify-object[image:https://travis-ci.org/wavesoftware/java-stringify-object.svg?branch=master[Build |
| 4 | +Status]] |
| 5 | +https://sonar.wavesoftware.pl/dashboard/index/pl.wavesoftware.utils:stringify-object[image:https://sonar.wavesoftware.pl/api/badges/gate?key=pl.wavesoftware.utils:stringify-object[Quality |
| 6 | +Gate]] |
| 7 | +https://coveralls.io/github/wavesoftware/java-stringify-object?branch=master[image:https://coveralls.io/repos/github/wavesoftware/java-stringify-object/badge.svg?branch=master[Coverage |
| 8 | +Status]] |
| 9 | +https://bintray.com/bintray/jcenter/pl.wavesoftware.utils%3Astringify-object[image:https://img.shields.io/maven-central/v/pl.wavesoftware.utils/stringify-object.svg[Maven |
| 10 | +Central]] |
| 11 | + |
| 12 | +A utility to safely inspect any Java Object as String representation. |
| 13 | +It's best to be used with domain model (also with JPA entities) with |
| 14 | +intention to dump those entities as text to log files. |
| 15 | + |
| 16 | +It runs in two modes: `+PROMISCUOUS+` (by default) and `+QUIET+`. In |
| 17 | +`+PROMISCUOUS+` mode every defined field is automatically inspected, |
| 18 | +unless the field is annotated with `+@DoNotInspect+` annotation. In |
| 19 | +`+QUIET+` mode only fields annotated with `+@Inspect+` will gets |
| 20 | +inspected. |
| 21 | + |
| 22 | +This library has proper support for object graph cycles, and JPA |
| 23 | +(Hibernate) lazy loaded elements. |
| 24 | + |
| 25 | +=== Usage |
| 26 | + |
| 27 | +==== In Promiscuous mode |
| 28 | + |
| 29 | +[source,java] |
| 30 | +---- |
| 31 | +// In PROMISCUOUS mode define fields to exclude |
| 32 | +class Person { |
| 33 | + private int id; |
| 34 | + @DisplayNull |
| 35 | + private Person parent; |
| 36 | + private List<Person> childs; |
| 37 | + private Account account; |
| 38 | + @Inspect(conditionally = IsInDevelopment.class) |
| 39 | + private String password; |
| 40 | + @DoNotInspect |
| 41 | + private String ignored; |
| 42 | +} |
| 43 | + |
| 44 | +// inspect an object |
| 45 | +List<Person> people = query.getResultList(); |
| 46 | +Stringify stringify = Stringify.of(people); |
| 47 | +stringify.mode(Mode.PROMISCUOUS); |
| 48 | +// stringify.beanFactory(...); |
| 49 | +assert "<Person id=15, parent=<Person id=16, parent=null, " |
| 50 | + + "childs=[(↻)], account=⁂Lazy>, childs=[], " |
| 51 | + + "account=⁂Lazy>".equals(stringify.toString()); |
| 52 | +---- |
| 53 | + |
| 54 | +==== In Quiet mode |
| 55 | + |
| 56 | +[source,java] |
| 57 | +---- |
| 58 | +// In QUIET mode define fields to inspect |
| 59 | +class Person { |
| 60 | + @Inspect private int id; |
| 61 | + @Inspect @DisplayNull private Person parent; |
| 62 | + @Inspect private List<Person> childs; |
| 63 | + @Inspect private Account account; |
| 64 | + private String ignored; |
| 65 | +} |
| 66 | + |
| 67 | +// inspect an object |
| 68 | +List<Person> people = query.getResultList(); |
| 69 | +Stringify stringify = Stringify.of(people); |
| 70 | +stringify.mode(Mode.QUIET); |
| 71 | +assert "<Person id=15, parent=<Person id=16, parent=null, " |
| 72 | + + "childs=[(↻)], account=⁂Lazy>, childs=[], " |
| 73 | + + "account=⁂Lazy>".equals(stringify.toString()); |
| 74 | +---- |
| 75 | + |
| 76 | +=== Features |
| 77 | + |
| 78 | +* String representation of any Java class in two modes `+PROMISCUOUS+` |
| 79 | +and `+QUIET+` |
| 80 | +* Fine tuning of which fields to display |
| 81 | +* Support for cycles in object graph - `+(↻)+` is displayed instead |
| 82 | +* Support for Hibernate lazy loaded entities - `+⁂Lazy+` is displayed |
| 83 | +instead |
| 84 | + |
| 85 | +[[vs-lombok-tostring]] |
| 86 | +=== vs. Lombok @ToString |
| 87 | + |
| 88 | +Stringify Object for Java is designed for *slightly different* use case |
| 89 | +then Lombok. |
| 90 | + |
| 91 | +Lombok `+@ToString+` is designed to quickly inspect fields of simple |
| 92 | +objects by generating static simple implementation of this mechanism. |
| 93 | + |
| 94 | +Stringify Object for Java is designed to inspect complex objects that |
| 95 | +can have cycles and can be managed by JPA provider like Hibernate |
| 96 | +(introducing Lazy Loading problems). |
| 97 | + |
| 98 | +==== Pros of Lombok vs Stringify Object |
| 99 | + |
| 100 | +* Lombok is *fast* - it's statically generated code without using |
| 101 | +Reflection API. |
| 102 | +* Lombok is *easy* - it's zero configuration in most cases. |
| 103 | + |
| 104 | +==== Cons of Lombok vs Stringify Object |
| 105 | + |
| 106 | +* Lombok can't *detect cycles* is object graph, which implies |
| 107 | +`+StackOverflowException+` being thrown in that case |
| 108 | +* Lombok can't detect a *lazy loaded entities*, which leads to force |
| 109 | +loading it from JPA by invoking SQL statements. It's typical *n+1 |
| 110 | +problem*, but with nasty consequences - your `+toString()+` method is |
| 111 | +invoking SQL without your knowledge!! |
| 112 | + |
| 113 | +=== Configuration |
| 114 | + |
| 115 | +Configuration is done in two ways: declarative - using Java's service |
| 116 | +loader mechanism, and programmatic. |
| 117 | + |
| 118 | +==== Configuration using Service Loader |
| 119 | + |
| 120 | +A `+Configurator+` interface is intended to be implemented in user code, |
| 121 | +and assigned to https://www.baeldung.com/java-spi[Service Loader] |
| 122 | +mechanism. |
| 123 | + |
| 124 | +To do that, create on your classpath, a file: |
| 125 | + |
| 126 | +`+/META-INF/services/pl.wavesoftware.utils.stringify.spi.Configurator+` |
| 127 | + |
| 128 | +In that file, place a fully qualified class name of your class that |
| 129 | +implements `+Configurator+` interface. It should be called first time |
| 130 | +you use an Stringify to inspect an object: |
| 131 | + |
| 132 | +.... |
| 133 | +# classpath:/META-INF/services/pl.wavesoftware.utils.stringify.spi.Configurator |
| 134 | +org.acmecorp.StringifyConfigurator |
| 135 | +.... |
| 136 | + |
| 137 | +Then implement that class in your code: |
| 138 | + |
| 139 | +[source,java] |
| 140 | +---- |
| 141 | +package org.acmecorp; |
| 142 | +
|
| 143 | +import pl.wavesoftware.utils.stringify.api.Configuration; |
| 144 | +import pl.wavesoftware.utils.stringify.spi.Configurator; |
| 145 | +
|
| 146 | +public final class StringifyConfigurator implements Configurator { |
| 147 | + |
| 148 | + @Override |
| 149 | + public void configure(Configuration configuration) { |
| 150 | + configuration.beanFactory(new SpringBeanFactory()); |
| 151 | + } |
| 152 | +} |
| 153 | +---- |
| 154 | + |
| 155 | +with example Spring based BeanFactory: |
| 156 | + |
| 157 | +[source,java] |
| 158 | +---- |
| 159 | +package org.acmecorp; |
| 160 | +
|
| 161 | +import org.springframework.context.event.ContextRefreshedEvent; |
| 162 | +import org.springframework.context.ApplicationContext; |
| 163 | +import org.springframework.context.annotation.Configuration; |
| 164 | +
|
| 165 | +import pl.wavesoftware.utils.stringify.spi.BeanFactory; |
| 166 | +import pl.wavesoftware.utils.stringify.spi.BootingAware; |
| 167 | +
|
| 168 | +@Configuration |
| 169 | +class SpringBeanFactory implements BeanFactory, BootingAware { |
| 170 | + private static ApplicationContext context; |
| 171 | + |
| 172 | + @EventListener(ContextRefreshedEvent.class) |
| 173 | + void onRefresh(ContextRefreshedEvent event) { |
| 174 | + SpringBeanFactory.context = event.getApplicationContext(); |
| 175 | + } |
| 176 | + |
| 177 | + @Override |
| 178 | + public <T> T create(Class<T> contractClass) { |
| 179 | + return SpringBeanFactory.context.getBean(contractClass); |
| 180 | + } |
| 181 | +
|
| 182 | + @Override |
| 183 | + public boolean isReady() { |
| 184 | + return SpringBeanFactory.context != null; |
| 185 | + } |
| 186 | +} |
| 187 | +---- |
| 188 | + |
| 189 | +==== Programmatic configuration |
| 190 | + |
| 191 | +You can also fine tune you configuration on instance level - using |
| 192 | +methods available at `+Stringify+` interface: |
| 193 | + |
| 194 | +[source,java] |
| 195 | +---- |
| 196 | +// given |
| 197 | +BeanFactory beanFactory = createBeanFactory(); |
| 198 | +Person person = createPerson(); |
| 199 | +
|
| 200 | +// then |
| 201 | +Stringify stringifier = Stringify.of(person); |
| 202 | +stringifier |
| 203 | + .beanFactory(beanFactory) |
| 204 | + .mode(Mode.QUIET) |
| 205 | + .stringify(); |
| 206 | +---- |
| 207 | + |
| 208 | +=== Dependencies |
| 209 | + |
| 210 | +* Java >= 8 |
| 211 | +* https://github.com/wavesoftware/java-eid-exceptions[EID Exceptions] |
| 212 | +library |
| 213 | + |
| 214 | +==== Contributing |
| 215 | + |
| 216 | +Contributions are welcome! |
| 217 | + |
| 218 | +To contribute, follow the standard |
| 219 | +http://danielkummer.github.io/git-flow-cheatsheet/[git flow] of: |
| 220 | + |
| 221 | +. Fork it |
| 222 | +. Create your feature branch |
| 223 | +(`+git checkout -b feature/my-new-feature+`) |
| 224 | +. Commit your changes (`+git commit -am 'Add some feature'+`) |
| 225 | +. Push to the branch (`+git push origin feature/my-new-feature+`) |
| 226 | +. Create new Pull Request |
| 227 | + |
| 228 | +Even if you can't contribute code, if you have an idea for an |
| 229 | +improvement please open an |
| 230 | +https://github.com/wavesoftware/java-stringify-object/issues[issue]. |
0 commit comments