diff --git a/hsweb-commons/hsweb-commons-entity/src/main/java/org/hswebframework/web/commons/entity/GenericEntity.java b/hsweb-commons/hsweb-commons-entity/src/main/java/org/hswebframework/web/commons/entity/GenericEntity.java index d17dd262f..e7699b870 100644 --- a/hsweb-commons/hsweb-commons-entity/src/main/java/org/hswebframework/web/commons/entity/GenericEntity.java +++ b/hsweb-commons/hsweb-commons-entity/src/main/java/org/hswebframework/web/commons/entity/GenericEntity.java @@ -17,6 +17,9 @@ */ package org.hswebframework.web.commons.entity; + +import org.hswebframework.web.bean.ToString; + import java.util.LinkedHashMap; import java.util.Map; @@ -35,6 +38,10 @@ public interface GenericEntity extends CloneableEntity { void setId(PK id); + default String toString(String... ignoreProperty) { + return ToString.toString(this, ignoreProperty); + } + default Map getProperties() { return null; } diff --git a/hsweb-commons/hsweb-commons-entity/src/main/java/org/hswebframework/web/commons/entity/SimpleGenericEntity.java b/hsweb-commons/hsweb-commons-entity/src/main/java/org/hswebframework/web/commons/entity/SimpleGenericEntity.java index c4675e69d..41fff5418 100644 --- a/hsweb-commons/hsweb-commons-entity/src/main/java/org/hswebframework/web/commons/entity/SimpleGenericEntity.java +++ b/hsweb-commons/hsweb-commons-entity/src/main/java/org/hswebframework/web/commons/entity/SimpleGenericEntity.java @@ -35,6 +35,11 @@ public abstract class SimpleGenericEntity implements GenericEntity { private Map properties; + @Override + public String toString() { + return toString((String[]) null); + } + @Override public PK getId() { return this.id; diff --git a/hsweb-core/src/main/java/org/hswebframework/web/bean/Copier.java b/hsweb-core/src/main/java/org/hswebframework/web/bean/Copier.java index 28743e2fd..84f985290 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/bean/Copier.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/bean/Copier.java @@ -10,5 +10,6 @@ public interface Copier { default void copy(Object source, Object target, String... ignore){ copy(source,target,new HashSet<>(Arrays.asList(ignore)),FastBeanCopier.DEFAULT_CONVERT); } + } diff --git a/hsweb-core/src/main/java/org/hswebframework/web/bean/DefaultToStringOperator.java b/hsweb-core/src/main/java/org/hswebframework/web/bean/DefaultToStringOperator.java new file mode 100644 index 000000000..75abc3028 --- /dev/null +++ b/hsweb-core/src/main/java/org/hswebframework/web/bean/DefaultToStringOperator.java @@ -0,0 +1,342 @@ +package org.hswebframework.web.bean; + +import com.alibaba.fastjson.JSON; +import lombok.extern.slf4j.Slf4j; +import org.hswebframework.utils.time.DateFormatter; +import org.springframework.beans.BeanUtils; +import org.springframework.core.annotation.AnnotationUtils; + +import java.beans.PropertyDescriptor; +import java.lang.reflect.Field; +import java.util.*; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import static org.hswebframework.web.bean.ToString.Feature.coverIgnoreProperty; +import static org.hswebframework.web.bean.ToString.Feature.disableNestProperty; +import static org.hswebframework.web.bean.ToString.Feature.nullPropertyToEmpty; + +/** + * @author zhouhao + * @since 3.0.0-RC + */ +@Slf4j +public class DefaultToStringOperator implements ToStringOperator { + + private PropertyDescriptor[] descriptors; + + private Set defaultIgnoreProperties; + + private long defaultFeatures = ToString.DEFAULT_FEATURE; + + private Map descriptorMap; + + private Map> converts; + + private Function coverStringConvert = (o) -> coverString(String.valueOf(o), 50); + + private Function> simpleConvertBuilder = type -> { + if (Date.class.isAssignableFrom(type)) { + return (value, f) -> DateFormatter.toString(((Date) value), "yyyy-MM-dd HH:mm:ss"); + } else { + return (value, f) -> value; + } + }; + + Predicate simpleTypePredicate = ((Predicate) String.class::isAssignableFrom) + .or(Class::isEnum) + .or(Class::isPrimitive) + .or(Date.class::isAssignableFrom) + .or(Number.class::isAssignableFrom) + .or(Boolean.class::isAssignableFrom); + private Class targetType; + + public DefaultToStringOperator(Class targetType) { + this.targetType = targetType; + descriptors = BeanUtils.getPropertyDescriptors(targetType); + init(); + } + + public static String coverString(String str, double percent) { + if (str.length() == 1) { + return "*"; + } + + if (percent > 1) { + percent = percent / 100d; + } + percent = 1 - percent; + long size = Math.round(str.length() * percent); + + long end = (str.length() - size / 2); + + long start = str.length() - end; + start = start == 0 && percent > 0 ? 1 : start; + char[] chars = str.toCharArray(); + for (int i = 0; i < chars.length; i++) { + if (i >= start && i <= end - 1) { + chars[i] = '*'; + } + } + return new String(chars); + } + + @SuppressWarnings("all") + protected void init() { + converts = new HashMap<>(); + descriptorMap = Arrays.stream(descriptors).collect(Collectors.toMap(PropertyDescriptor::getName, Function.identity())); + //获取类上的注解 + ToString.Ignore classIgnore = AnnotationUtils.getAnnotation(targetType, ToString.Ignore.class); + ToString.Features features = AnnotationUtils.getAnnotation(targetType, ToString.Features.class); + if (null != features && features.value().length > 0) { + defaultFeatures = ToString.Feature.createFeatures(features.value()); + } else { + defaultFeatures = ToString.DEFAULT_FEATURE; + } + defaultIgnoreProperties = classIgnore == null ? + new HashSet<>(Collections.emptySet()) + : new HashSet<>(Arrays.asList(classIgnore.value())); + + //是否打码 + boolean defaultCover = classIgnore != null && classIgnore.cover(); + + for (PropertyDescriptor descriptor : descriptors) { + if ("class".equals(descriptor.getName())) { + continue; + } + Class propertyType = descriptor.getPropertyType(); + String propertyName = descriptor.getName(); + BiFunction convert; + ToString.Ignore propertyIgnore = null; + long propertyFeature = 0; + try { + Field field = targetType.getDeclaredField(descriptor.getName()); + propertyIgnore = field.getAnnotation(ToString.Ignore.class); + features = AnnotationUtils.getAnnotation(field, ToString.Features.class); + if (propertyIgnore != null) { + for (String val : propertyIgnore.value()) { + defaultIgnoreProperties.add(field.getName().concat(".").concat(val)); + } + } + if (null != features && features.value().length > 0) { + propertyFeature = ToString.Feature.createFeatures(features.value()); + } + } catch (NoSuchFieldException e) { + log.warn("无法获取字段{},该字段将不会被打码!", descriptor.getName()); + + } + //是否设置了打码 + boolean cover = (propertyIgnore == null && defaultCover) || (propertyIgnore != null && propertyIgnore.cover()); + //是否注解了ignore + boolean hide = propertyIgnore != null; + + long finalPropertyFeature = propertyFeature; + + if (simpleTypePredicate.test(propertyType)) { + BiFunction simpleConvert = simpleConvertBuilder.apply(propertyType); + convert = (value, f) -> { + long feature = finalPropertyFeature == 0 ? f.features : finalPropertyFeature; + + value = simpleConvert.apply(value, f); + if (hide || f.ignoreProperty.contains(propertyName)) { + if (ToString.Feature.hasFeature(feature, ToString.Feature.coverIgnoreProperty)) { + return coverStringConvert.apply(value); + } else { + return null; + } + } + return value; + }; + + } else { + boolean toStringOverride = false; + try { + toStringOverride = propertyType.getMethod("toString").getDeclaringClass() != Object.class; + } catch (NoSuchMethodException ignore) { + } + boolean finalToStringOverride = toStringOverride; + boolean justReturn = propertyType.isArray() + || Collection.class.isAssignableFrom(propertyType) + || Map.class.isAssignableFrom(propertyType); + + convert = (value, f) -> { + if (f.ignoreProperty.contains(propertyName)) { + return null; + } + long feature = finalPropertyFeature == 0 ? f.features : finalPropertyFeature; + + boolean jsonFormat = ToString.Feature.hasFeature(feature, ToString.Feature.jsonFormat); + boolean propertyJsonFormat = ToString.Feature.hasFeature(finalPropertyFeature, ToString.Feature.jsonFormat); + + if (ToString.Feature.hasFeature(f.features, disableNestProperty)) { + return null; + } + if (!jsonFormat && finalToStringOverride) { + return String.valueOf(value); + } + + Set newIgnoreProperty = f.ignoreProperty + .stream() + .filter(property -> property.startsWith(propertyName.concat("."))) + .map(property -> property.substring(propertyName.length() + 1)) + .collect(Collectors.toSet()); + + if (justReturn) { + if (value instanceof Object[]) { + value = Arrays.asList(((Object[]) value)); + } + if (value instanceof Map) { + value = convertMap(((Map) value), feature, newIgnoreProperty); + } + if (value instanceof Collection) { + value = ((Collection) value).stream() + .map((val) -> { + if (val instanceof Map) { + return convertMap(((Map) val), feature, newIgnoreProperty); + } + if (simpleTypePredicate.test(val.getClass())) { + return val; + } + ToStringOperator operator = ToString.getOperator(val.getClass()); + if (operator instanceof DefaultToStringOperator) { + return ((DefaultToStringOperator) operator).toMap(val, feature, newIgnoreProperty); + } + return operator.toString(val, feature, newIgnoreProperty); + }).collect(Collectors.toList()); + + } + if (value instanceof Map) { + value = convertMap(((Map) value), feature, newIgnoreProperty); + } + if (propertyJsonFormat) { + return JSON.toJSONString(value); + } + return value; + } + + ToStringOperator operator = ToString.getOperator(value.getClass()); + if (!propertyJsonFormat && operator instanceof DefaultToStringOperator) { + return ((DefaultToStringOperator) operator).toMap(value, feature, newIgnoreProperty); + } else { + return operator.toString(value, feature, newIgnoreProperty); + } + }; + } + converts.put(descriptor.getName(), convert); + } + } + + class ConvertConfig { + long features; + Set ignoreProperty; + + } + + protected Map convertMap(Map obj, long features, Set ignoreProperty) { + if (ignoreProperty.isEmpty()) { + return obj; + } + boolean cover = ToString.Feature.hasFeature(features, coverIgnoreProperty); + boolean isNullPropertyToEmpty = ToString.Feature.hasFeature(features, nullPropertyToEmpty); + boolean isDisableNestProperty = ToString.Feature.hasFeature(features, disableNestProperty); + + Map newMap = new HashMap<>(obj); + Set ignore = new HashSet<>(ignoreProperty.size()); + ignore.addAll(defaultIgnoreProperties); + + for (Map.Entry entry : newMap.entrySet()) { + Object value = entry.getValue(); + + if (value == null) { + if (isNullPropertyToEmpty) { + entry.setValue(""); + } + continue; + } + Class type = value.getClass(); + if (simpleTypePredicate.test(type)) { + value = simpleConvertBuilder.apply(type).apply(value, null); + if (ignoreProperty.contains(entry.getKey())) { + if (cover) { + value = coverStringConvert.apply(value); + } else { + ignore.add(entry.getKey()); + } + entry.setValue(value); + } + + } else { + if (isDisableNestProperty) { + ignore.add(entry.getKey()); + } + } + } + ignore.forEach(newMap::remove); + return newMap; + } + + protected Map toMap(T target, long features, Set ignoreProperty) { + Map map = target instanceof Map ? ((Map) target) : FastBeanCopier.copy(target, new LinkedHashMap<>()); + + Set ignore = ignoreProperty == null || ignoreProperty.isEmpty() ? defaultIgnoreProperties : ignoreProperty; + ConvertConfig convertConfig = new ConvertConfig(); + convertConfig.ignoreProperty = ignore; + convertConfig.features = features == -1 ? defaultFeatures : features; + Set realIgnore = new HashSet<>(); + + for (Map.Entry entry : map.entrySet()) { + Object value = entry.getValue(); + if (value == null) { + if (ToString.Feature.hasFeature(features, ToString.Feature.nullPropertyToEmpty)) { + boolean isSimpleType = false; + PropertyDescriptor propertyDescriptor = descriptorMap.get(entry.getKey()); + Class propertyType = null; + if (propertyDescriptor != null) { + propertyType = propertyDescriptor.getPropertyType(); + isSimpleType = simpleTypePredicate.test(propertyType); + } + if (isSimpleType || propertyType == null) { + entry.setValue(""); + } else if (propertyType.isArray() || Collection.class.isAssignableFrom(propertyType)) { + entry.setValue(Collections.emptyList()); + } else { + entry.setValue(Collections.emptyMap()); + } + } + continue; + } + BiFunction converter = converts.get(entry.getKey()); + if (null != converter) { + entry.setValue(converter.apply(value, convertConfig)); + } + if (entry.getValue() == null) { + realIgnore.add(entry.getKey()); + } + } + realIgnore.forEach(map::remove); + + return map; + } + + @Override + public String toString(T target, long features, Set ignoreProperty) { + if (target == null) { + return ""; + } + if (features == -1) { + features = defaultFeatures; + } + + Map mapValue = toMap(target, features, ignoreProperty); + if (ToString.Feature.hasFeature(features, ToString.Feature.jsonFormat)) { + return JSON.toJSONString(mapValue); + } + StringJoiner joiner = new StringJoiner(", ", target.getClass().getSimpleName() + "(", ")"); + + mapValue.forEach((key, value) -> joiner.add(key.concat("=").concat(String.valueOf(value)))); + + return joiner.toString(); + } +} diff --git a/hsweb-core/src/main/java/org/hswebframework/web/bean/ToString.java b/hsweb-core/src/main/java/org/hswebframework/web/bean/ToString.java new file mode 100644 index 000000000..d25459764 --- /dev/null +++ b/hsweb-core/src/main/java/org/hswebframework/web/bean/ToString.java @@ -0,0 +1,134 @@ +package org.hswebframework.web.bean; + +import org.springframework.util.ClassUtils; + +import java.lang.annotation.*; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @author zhouhao + * @since 3.0.0-RC + */ +public class ToString { + + public static long DEFAULT_FEATURE = Feature.createFeatures( + Feature.coverIgnoreProperty + , Feature.nullPropertyToEmpty +// , Feature.jsonFormat + ); + + public static final Map cache = new ConcurrentHashMap<>(); + + @SuppressWarnings("all") + public static ToStringOperator getOperator(Class type) { + return cache.computeIfAbsent(type, DefaultToStringOperator::new); + } + + @SuppressWarnings("all") + public static String toString(T target) { + return getOperator((Class) ClassUtils.getUserClass(target)).toString(target); + } + + @SuppressWarnings("all") + public static String toString(T target, String... ignoreProperty) { + return getOperator((Class) ClassUtils.getUserClass(target)).toString(target, ignoreProperty); + } + + @Target({ElementType.TYPE, ElementType.FIELD}) + @Retention(RetentionPolicy.RUNTIME) + @Documented + public @interface Ignore { + + String[] value() default {}; + + boolean cover() default true; + + } + + @Target({ElementType.TYPE, ElementType.FIELD}) + @Retention(RetentionPolicy.RUNTIME) + @Documented + public @interface Features { + Feature[] value() default {}; + } + + public enum Feature { + + /** + * 什么也不配置 + * + * @since 3.0.0-RC + */ + empty, + + /** + * 忽略为null的字段 + * + * @since 3.0.0-RC + */ + ignoreNullProperty, + + /** + * null的字段转为空,如null字符串转为"", null的list转为[] + * + * @since 3.0.0-RC + */ + nullPropertyToEmpty, + + /** + * 排除的字段使用*进行遮盖,如: 张三 =? 张* , 18502314087 => 185****087 + * + * @since 3.0.0-RC + */ + coverIgnoreProperty, + + /** + * 是否关闭嵌套属性toString + * + * @since 3.0.0-RC + */ + disableNestProperty, + + /** + * 以json方式进行格式化 + * + * @since 3.0.0-RC + */ + jsonFormat; + + + public long getMask() { + return 1L << ordinal(); + } + + public static boolean hasFeature(long features, Feature feature) { + long mast = feature.getMask(); + return (features & mast) == mast; + } + + public static long removeFeatures(long oldFeature, Feature... features) { + if (features == null) { + return 0L; + } + long value = oldFeature; + for (Feature feature : features) { + value &= ~feature.getMask(); + } + return value; + } + + public static long createFeatures(Feature... features) { + if (features == null) { + return 0L; + } + long value = 0L; + for (Feature feature : features) { + value |= feature.getMask(); + } + + return value; + } + } + +} diff --git a/hsweb-core/src/main/java/org/hswebframework/web/bean/ToStringOperator.java b/hsweb-core/src/main/java/org/hswebframework/web/bean/ToStringOperator.java new file mode 100644 index 000000000..90b08780d --- /dev/null +++ b/hsweb-core/src/main/java/org/hswebframework/web/bean/ToStringOperator.java @@ -0,0 +1,17 @@ +package org.hswebframework.web.bean; + + +import java.util.*; + +/** + * @author zhouhao + * @since 3.0.0-RC + */ +public interface ToStringOperator { + + default String toString(T target, String... ignoreProperty) { + return toString(target, -1, ignoreProperty == null ? Collections.emptySet() : new HashSet<>(Arrays.asList(ignoreProperty))); + } + + String toString(T target, long features, Set ignoreProperty); +} diff --git a/hsweb-core/src/test/java/org/hswebframework/web/bean/FastBeanCopierTest.java b/hsweb-core/src/test/java/org/hswebframework/web/bean/FastBeanCopierTest.java index 677f896ae..32c969102 100644 --- a/hsweb-core/src/test/java/org/hswebframework/web/bean/FastBeanCopierTest.java +++ b/hsweb-core/src/test/java/org/hswebframework/web/bean/FastBeanCopierTest.java @@ -26,6 +26,7 @@ public class FastBeanCopierTest { source.setNestObject2(Collections.singletonMap("name","mapTest")); NestObject nestObject = new NestObject(); nestObject.setAge(10); + nestObject.setPassword("1234567"); nestObject.setName("测试2"); source.setNestObject(nestObject); source.setNestObject3(nestObject); @@ -35,9 +36,9 @@ public class FastBeanCopierTest { long t = System.currentTimeMillis(); - for (int i = 10_0000; i > 0; i--) { - FastBeanCopier.copy(source, target); - } +// for (int i = 10_0000; i > 0; i--) { +// FastBeanCopier.copy(source, target); +// } System.out.println(System.currentTimeMillis() - t); System.out.println(source); diff --git a/hsweb-core/src/test/java/org/hswebframework/web/bean/NestObject.java b/hsweb-core/src/test/java/org/hswebframework/web/bean/NestObject.java index d00482ffa..a6b5acb20 100644 --- a/hsweb-core/src/test/java/org/hswebframework/web/bean/NestObject.java +++ b/hsweb-core/src/test/java/org/hswebframework/web/bean/NestObject.java @@ -1,15 +1,13 @@ package org.hswebframework.web.bean; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; +import lombok.*; /** * @author zhouhao * @since */ -@Data +@Getter +@Setter @Builder @NoArgsConstructor @AllArgsConstructor @@ -19,8 +17,16 @@ public class NestObject implements Cloneable { private int age; + @ToString.Ignore + private String password ; + @Override public NestObject clone() throws CloneNotSupportedException { - return (NestObject)super.clone(); + return (NestObject) super.clone(); + } + + @Override + public String toString() { + return ToString.toString(this); } } diff --git a/hsweb-core/src/test/java/org/hswebframework/web/bean/Source.java b/hsweb-core/src/test/java/org/hswebframework/web/bean/Source.java index 18355c223..44b5ed20e 100644 --- a/hsweb-core/src/test/java/org/hswebframework/web/bean/Source.java +++ b/hsweb-core/src/test/java/org/hswebframework/web/bean/Source.java @@ -31,11 +31,11 @@ public class Source { private NestObject nestObject; - private List nestObjects = Arrays.asList(new NestObject("test", 1),new NestObject("test", 1)); + private List nestObjects = Arrays.asList(new NestObject("test", 1, "1234567"), new NestObject("test", 1, "1234567")); private Map nestObject2 = new HashMap<>(); - private NestObject nestObject3; + private NestObject nestObject3 = new NestObject("test", 1, "1234567"); private Color color = Color.RED; diff --git a/hsweb-core/src/test/java/org/hswebframework/web/bean/Target.java b/hsweb-core/src/test/java/org/hswebframework/web/bean/Target.java index 2a233cbdb..f41a3f667 100644 --- a/hsweb-core/src/test/java/org/hswebframework/web/bean/Target.java +++ b/hsweb-core/src/test/java/org/hswebframework/web/bean/Target.java @@ -1,6 +1,8 @@ package org.hswebframework.web.bean; import lombok.Data; +import lombok.Getter; +import lombok.Setter; import org.springframework.validation.annotation.Validated; import java.util.ArrayList; @@ -8,15 +10,20 @@ import java.util.Date; import java.util.List; import java.util.Map; -@Data +import static org.hswebframework.web.bean.ToString.Feature.coverIgnoreProperty; +import static org.hswebframework.web.bean.ToString.Feature.jsonFormat; +import static org.hswebframework.web.bean.ToString.Feature.nullPropertyToEmpty; + +@Getter +@Setter public class Target { - private String name; + private String name; private String[] ids; private Boolean boy; private boolean boy2; - private String boy3; + private String boy3; private int age; @@ -24,21 +31,24 @@ public class Target { private String age3; - private Date deleteTime=new Date(); + private Date deleteTime = new Date(); private String createTime; private Date updateTime; + @ToString.Features({coverIgnoreProperty,jsonFormat}) + @ToString.Ignore(value = "password") private NestObject nestObject; private NestObject nestObject2; - private List> nestObjects; + @ToString.Ignore(value = "password") + private List> nestObjects; + @ToString.Ignore("password") private Map nestObject3; - private int color; private Color color2; @@ -55,4 +65,8 @@ public class Target { private Integer[] arr4; + @Override + public String toString() { + return ToString.toString(this); + } } \ No newline at end of file diff --git a/hsweb-starter/hsweb-spring-boot-starter/src/main/java/org/hswebframework/web/starter/HswebAutoConfiguration.java b/hsweb-starter/hsweb-spring-boot-starter/src/main/java/org/hswebframework/web/starter/HswebAutoConfiguration.java index 8b5e6353b..af72e90d6 100644 --- a/hsweb-starter/hsweb-spring-boot-starter/src/main/java/org/hswebframework/web/starter/HswebAutoConfiguration.java +++ b/hsweb-starter/hsweb-spring-boot-starter/src/main/java/org/hswebframework/web/starter/HswebAutoConfiguration.java @@ -115,7 +115,6 @@ public class HswebAutoConfiguration { if (type instanceof Class) { Class classType = ((Class) type); if (classType.isEnum()) { - // TODO: 2018/4/12 支持EnumDict枚举的反序列化 return super.getDeserializer(type); } checkAutoType(type.getTypeName(), ((Class) type)); diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/entity/authorization/SimpleUserEntity.java b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/entity/authorization/SimpleUserEntity.java index d6a354c30..798e73ed7 100644 --- a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/entity/authorization/SimpleUserEntity.java +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/entity/authorization/SimpleUserEntity.java @@ -3,6 +3,7 @@ package org.hswebframework.web.entity.authorization; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.hswebframework.web.bean.ToString; import org.hswebframework.web.commons.entity.SimpleGenericEntity; /** @@ -18,8 +19,10 @@ public class SimpleUserEntity extends SimpleGenericEntity implements Use private String username; + @ToString.Ignore private String password; + @ToString.Ignore(cover = false) private String salt; private Long createTime;