Динамическое добавление свойств в языке Java
Listener’ы и Interceptor’ы позволяют добавить в сущности дополнительные данные. В некоторых случаях их применение оправдано, но предпочитаю не засорять сущности уровня хранения структурами и данными, не имеющими к уровню хранения никакого отношения.
Mapping
Можно пронаследоваться от каждого класса, который требует расширения, и начинить его нужными данными уже на сервисном уровне. Это концептуально верный путь: слой хранения сохраняет чистоту, слой контроллеров не требует модификации. Однако, возникают проблемы с производительностью, ленивая загрузка перестает быть ленивой, т.к. маппер не знает, какие из полей нужны контроллеру, и вынужден перекладывать все. Управление маппером со стороны контроллера теоретически возможно, но это невозможно без модификации кода контроллера.
Dynamic proxy
package ru.bdm.reflection; //some imports omitted import static junit.framework.Assert.assertEquals; import static org.apache.commons.beanutils.PropertyUtils.getProperty; public class PropertyJoinerTest < public static class AnyType < public Object getAnyProperty() < return "anyPropertyValue"; >> @Test public void testWithPropertyExtractor() throws Exception < PropertyJoiner propertyJoiner = new PropertyJoiner(new PropertyExtractor() < @Override public Object get(Object o, String property) < return property + "Value"; >>, "first", "second"); AnyType src = new AnyType(); AnyType dst = propertyJoiner.joinProperties(src); assertEquals("firstValue", getProperty(dst, "first")); assertEquals("secondValue", getProperty(dst, "second")); assertEquals("anyPropertyValue", getProperty(dst, "anyProperty")); > >
Что под капотом?
public interface FirstHolder < Object getFirst(); >public interface SecondHolder
Динамически создается класс proxy, который наследует AnyType и реализует FirstHolder и SecondHolder .
Методы, определенные в AnyType , proxy перенаправляет к src , методы, определенные в FirstHolder и SecondHolder , перенаправляются в PropertyExtractor , который содержит логику вычисления добавочных свойств.
Таким образом мы получили возможность расширения представления, не меняя при этом код контроллеров и не засоряя сущности уровня хранения посторонними структурами и данными, не получая падения производительности из-за проблем с ленивой загрузкой.
Плата за это оказалась не очень велика: доступ к свойствам через прокси примерно в 150 раз медленнее, чем непосредственный. Это стоит учитывать при использовании инструмента.
Нагрузка нашего приложения была всего несколько запросов в секунду, за каждый запрос читалось максимум 50 сущностей (размер страницы), так что долей потерь в proxy можно было пренебречь.
Add property to object java
During the practice, I split and extracted the above code, and the main logic packaged in a tool class. The following is the code:
package com.yang.jmh.batch; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.yang.jmh.rest.PropertyAppender; import com.yang.jmh.rest.User0; import java.lang.reflect.InvocationTargetException; import java.util.HashMap; import java.util.Map; /** * @author: Yang * @date: 2020/11/4 00:12 * @description: */ public class Kill0 < private static final ObjectMapper MAPPER = new ObjectMapper(); public static void main(String[] args) throws JsonProcessingException, InvocationTargetException, IllegalAccessException < User0 user = new User0(); user.setName("Daisy"); System.out.println(MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(user)); System.out.println("====================================="); MappropertiesMap = new HashMap<>(1); propertiesMap.put("age", 18); Object obj = PropertyAppender.generate(user, propertiesMap); System.err.println(MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(obj)); > >
package com.yang.jmh.rest; /** * @author: Yang * @date: 2020/11/7 15:04 * @description: */ public class User0 < private String name; public String getName() < return name; >public void setName(String name) < this.name = name; >>
package com.yang.jmh.rest; import lombok.extern.slf4j.Slf4j; import org.apache.commons.beanutils.PropertyUtilsBean; import org.springframework.cglib.beans.BeanGenerator; import org.springframework.cglib.beans.BeanMap; import java.beans.PropertyDescriptor; import java.lang.reflect.InvocationTargetException; import java.util.HashMap; import java.util.Map; /** * @author: Yang * @date: 2020/11/7 14:47 * @description: */ @Slf4j public final class PropertyAppender < private static final class DynamicBean < private Object target; private BeanMap beanMap; private DynamicBean(Class superclass, MappropertyMap) < this.target = generateBean(superclass, propertyMap); this.beanMap = BeanMap.create(this.target); >private void setValue(String property, Object value) < beanMap.put(property, value); >private Object getValue(String property) < return beanMap.get(property); >private Object getTarget() < return this.target; >/** * Generate objects according to attribute */ private Object generateBean(Class superclass, Map propertyMap) < BeanGenerator generator = new BeanGenerator(); if (null != superclass) < generator.setSuperclass(superclass); >BeanGenerator.addProperties(generator, propertyMap); return generator.create(); > > public static Object generate(Object dest, Map newValueMap) throws InvocationTargetException, IllegalAccessException < PropertyUtilsBean propertyUtilsBean = new PropertyUtilsBean(); // 1. Get the array of the original object PropertyDescriptor[] descriptorArr = propertyUtilsBean.getPropertyDescriptors(dest); // 2. Traverse the field array of the original object, and package it to the MAP MapoldKeyMap = new HashMap<>(4); for (PropertyDescriptor it : descriptorArr) < if (!"class".equalsIgnoreCase(it.getName())) < oldKeyMap.put(it.getName(), it.getPropertyType()); newValueMap.put(it.getName(), it.getReadMethod().invoke(dest)); >> // 3. Merge the extension field MAP into the original field map newValueMap.forEach((k, v) -> oldKeyMap.put(k, v.getClass())); // 4. Based on the new field to generate a subclass object DynamicBean dynamicBean = new DynamicBean(dest.getClass(), oldKeyMap); // 5. Collection of after-merged property newValueMap.forEach((k, v) -> < try < dynamicBean.setValue(k, v); >catch (Exception e) < Log.Error ("Dynamic Add Field [Value] Error", E); >>); return dynamicBean.getTarget(); > >