diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index 6c414f9e7..966184dca 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1 +1 @@ -distributionUrl=https://downloads.apache.org/maven/maven-3/3.3.9/binaries/apache-maven-3.3.9-bin.zip \ No newline at end of file +distributionUrl=https://archive.apache.org/dist/maven/maven-3/3.9.3/binaries/apache-maven-3.9.3-bin.zip \ No newline at end of file diff --git a/README.md b/README.md index deff62247..ad5f79f33 100644 --- a/README.md +++ b/README.md @@ -130,4 +130,7 @@ public Mono addAccount(@RequestBody Mono account){ ## License -[Apache 2.0](https://github.com/spring-projects/spring-boot/blob/main/LICENSE.txt) \ No newline at end of file +[Apache 2.0](https://github.com/spring-projects/spring-boot/blob/main/LICENSE.txt) + + +[![Stargazers over time](https://starchart.cc/hs-web/hsweb-framework.svg?variant=adaptive)](https://starchart.cc/hs-web/hsweb-framework) diff --git a/hsweb-authorization/hsweb-authorization-api/pom.xml b/hsweb-authorization/hsweb-authorization-api/pom.xml index 131f4d771..de505c863 100644 --- a/hsweb-authorization/hsweb-authorization-api/pom.xml +++ b/hsweb-authorization/hsweb-authorization-api/pom.xml @@ -5,10 +5,11 @@ hsweb-authorization org.hswebframework.web - 4.0.16-SNAPSHOT + 4.0.20-SNAPSHOT 4.0.0 + ${artifactId} 授权,权限管理API hsweb-authorization-api diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/Authentication.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/Authentication.java index 40ba06892..7e1d7f3c0 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/Authentication.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/Authentication.java @@ -93,8 +93,7 @@ static Optional current() { * @return 用户持有的权限集合 */ List getPermissions(); - - + default boolean hasDimension(String type, String... id) { return hasDimension(type, Arrays.asList(id)); } @@ -113,7 +112,7 @@ default boolean hasDimension(DimensionType type, String id) { } default Optional getDimension(String type, String id) { - if (StringUtils.isEmpty(type)) { + if (!StringUtils.hasText(type)) { return Optional.empty(); } return getDimensions() @@ -134,7 +133,7 @@ default Optional getDimension(DimensionType type, String id) { default List getDimensions(String type) { - if (StringUtils.isEmpty(type)) { + if (!StringUtils.hasText(type)) { return Collections.emptyList(); } return getDimensions() @@ -164,7 +163,8 @@ default Optional getPermission(String id) { if (null == id) { return Optional.empty(); } - return getPermissions().stream() + return getPermissions() + .stream() .filter(permission -> permission.getId().equals(id)) .findAny(); } @@ -173,17 +173,28 @@ default Optional getPermission(String id) { * 判断是否持有某权限以及对权限的可操作事件 * * @param permissionId 权限id {@link Permission#getId()} - * @param actions 可操作事件 {@link Permission#getActions()} 如果为空,则不判断action,只判断permissionId + * @param actions 可操作动作 {@link Permission#getActions()} 如果为空,则不判断action,只判断permissionId * @return 是否持有权限 */ default boolean hasPermission(String permissionId, String... actions) { - return hasPermission(permissionId, Arrays.asList(actions)); + return hasPermission(permissionId, + actions.length == 0 + ? Collections.emptyList() + : Arrays.asList(actions)); } default boolean hasPermission(String permissionId, Collection actions) { - return getPermission(permissionId) - .filter(permission -> actions.isEmpty() || permission.getActions().containsAll(actions)) - .isPresent(); + for (Permission permission : getPermissions()) { + if (Objects.equals(permission.getId(), "*")) { + return true; + } + if (Objects.equals(permissionId, permission.getId())) { + return actions.isEmpty() + || permission.getActions().containsAll(actions) + || permission.getActions().contains("*"); + } + } + return false; } /** @@ -201,6 +212,16 @@ default boolean hasPermission(String permissionId, Collection actions) { */ Map getAttributes(); + /** + * 设置属性,注意: 此属性可能并不会被持久化,仅用于临时传递信息. + * + * @param key key + * @param value value + */ + default void setAttribute(String key, Serializable value) { + getAttributes().put(key, value); + } + /** * 合并权限 * diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/AuthenticationHolder.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/AuthenticationHolder.java index c917ef15a..a8d9e9d5b 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/AuthenticationHolder.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/AuthenticationHolder.java @@ -18,13 +18,17 @@ package org.hswebframework.web.authorization; +import io.netty.util.concurrent.FastThreadLocal; +import lombok.SneakyThrows; import org.hswebframework.web.authorization.simple.SimpleAuthentication; +import reactor.core.Disposable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.concurrent.Callable; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.function.Function; @@ -50,6 +54,9 @@ public final class AuthenticationHolder { private static final ReadWriteLock lock = new ReentrantReadWriteLock(); + private static final FastThreadLocal CURRENT = new FastThreadLocal<>(); + + private static Optional get(Function> function) { int size = suppliers.size(); if (size == 0) { @@ -58,21 +65,23 @@ private static Optional get(Function get() { - + Authentication current = CURRENT.get(); + if (current != null) { + return Optional.of(current); + } return get(AuthenticationSupplier::get); } @@ -89,7 +98,7 @@ public static Optional get(String userId) { /** * 初始化 {@link AuthenticationSupplier} * - * @param supplier + * @param supplier 认证信息提供者 */ public static void addSupplier(AuthenticationSupplier supplier) { lock.writeLock().lock(); @@ -100,4 +109,23 @@ public static void addSupplier(AuthenticationSupplier supplier) { } } + /** + * 指定用户权限,执行一个任务。任务执行过程中可通过 {@link Authentication#current()}获取到当前权限. + * + * @param current 当前用户权限信息 + * @param callable 任务执行器 + * @param 任务执行结果类型 + * @return 任务执行结果 + */ + @SneakyThrows + public static T executeWith(Authentication current, Callable callable) { + Authentication previous = CURRENT.get(); + try { + CURRENT.set(current); + return callable.call(); + } finally { + CURRENT.set(previous); + } + } + } diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/Permission.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/Permission.java index ccc4f1d0e..d08b71f90 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/Permission.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/Permission.java @@ -119,6 +119,7 @@ default Optional getOption(String key) { * @see DataAccessConfig * @see org.hswebframework.web.authorization.access.DataAccessController */ + @Deprecated Set getDataAccesses(); diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/ReactiveAuthenticationHolder.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/ReactiveAuthenticationHolder.java index ce7d204ae..bf95951d0 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/ReactiveAuthenticationHolder.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/ReactiveAuthenticationHolder.java @@ -18,7 +18,7 @@ package org.hswebframework.web.authorization; -import org.apache.commons.collections.CollectionUtils; +import com.google.common.collect.Lists; import org.hswebframework.web.authorization.simple.SimpleAuthentication; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -26,7 +26,6 @@ import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Function; -import java.util.stream.Collectors; /** * 响应式权限保持器,用于响应式方式获取当前登录用户的权限信息. @@ -47,24 +46,10 @@ public final class ReactiveAuthenticationHolder { private static final List suppliers = new CopyOnWriteArrayList<>(); private static Mono get(Function> function) { - return Flux - .merge(suppliers - .stream() - .map(function) - .collect(Collectors.toList())) - .collectList() - .filter(CollectionUtils::isNotEmpty) - .map(all -> { - if (all.size() == 1) { - return all.get(0); - } - SimpleAuthentication authentication = new SimpleAuthentication(); - for (Authentication auth : all) { - authentication.merge(auth); - } - return authentication; - }); + .merge(Lists.transform(suppliers, function::apply)) + .collect(AuthenticationMerging::new, AuthenticationMerging::merge) + .mapNotNull(AuthenticationMerging::get); } /** @@ -99,4 +84,28 @@ public static void setSupplier(ReactiveAuthenticationSupplier supplier) { suppliers.add(supplier); } + + static class AuthenticationMerging { + + private Authentication auth; + private int count; + + public synchronized void merge(Authentication auth) { + if (this.auth == null || this.auth == auth) { + this.auth = auth; + } else { + if (count++ == 0) { + SimpleAuthentication newAuth = new SimpleAuthentication(); + newAuth.merge(this.auth); + this.auth = newAuth; + } + this.auth.merge(auth); + } + } + + Authentication get() { + return auth; + } + } + } diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/access/DimensionHelper.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/access/DimensionHelper.java index 24bc18002..9f3693295 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/access/DimensionHelper.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/access/DimensionHelper.java @@ -2,7 +2,7 @@ import lombok.AccessLevel; import lombok.NoArgsConstructor; -import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections4.CollectionUtils; import org.hswebframework.web.authorization.Authentication; import org.hswebframework.web.authorization.Dimension; import org.hswebframework.web.authorization.DimensionType; diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/Authorize.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/Authorize.java index b2dfd8edd..c8e647c08 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/Authorize.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/Authorize.java @@ -44,6 +44,14 @@ Dimension[] dimension() default {}; + /** + * 是否运行匿名访问,匿名访问时,直接允许执行,否则将进行权限验证. + * + * @return 是否允许匿名访问 + * @since 4.0.19 + */ + boolean anonymous() default false; + /** * 验证失败时返回的消息 * @@ -66,7 +74,7 @@ Logical logical() default Logical.DEFAULT; /** - * @return 验证时机,在方法调用前还是调用后s + * @return 验证时机,在方法调用前还是调用后 */ Phased phased() default Phased.before; diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/CreateAction.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/CreateAction.java index 3bc6b8a22..63b6a947f 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/CreateAction.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/CreateAction.java @@ -5,7 +5,7 @@ import java.lang.annotation.*; -@Target(ElementType.METHOD) +@Target({ElementType.METHOD,ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/DataAccess.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/DataAccess.java index 6fd9b696a..ba0b4852f 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/DataAccess.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/DataAccess.java @@ -31,10 +31,12 @@ * @see DataAccessController * @see ResourceAction#dataAccess() * @since 3.0 + * @deprecated 已弃用, 4.1中移除 */ -@Target({ElementType.ANNOTATION_TYPE,ElementType.METHOD}) +@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented +@Deprecated public @interface DataAccess { DataAccessType[] type() default {}; diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/DeleteAction.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/DeleteAction.java index 6df434c78..51dd95f64 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/DeleteAction.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/DeleteAction.java @@ -5,7 +5,7 @@ import java.lang.annotation.*; -@Target(ElementType.METHOD) +@Target({ElementType.METHOD,ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/QueryAction.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/QueryAction.java index 6ea8aa44d..a3194afce 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/QueryAction.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/QueryAction.java @@ -5,7 +5,7 @@ import java.lang.annotation.*; -@Target(ElementType.METHOD) +@Target({ElementType.METHOD,ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/Resource.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/Resource.java index 6b0efbe24..fa3597100 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/Resource.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/Resource.java @@ -49,7 +49,7 @@ * @see org.hswebframework.web.authorization.events.AuthorizingHandleBeforeEvent * @since 4.0 */ -@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.TYPE}) +@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD,ElementType.FIELD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/ResourceAction.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/ResourceAction.java index e8993be5d..864a8e163 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/ResourceAction.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/ResourceAction.java @@ -29,7 +29,7 @@ * @see org.hswebframework.web.authorization.Authentication * @see Permission#getActions() */ -@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD}) +@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD,ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/SaveAction.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/SaveAction.java index d878c52ae..44c8f4bef 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/SaveAction.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/SaveAction.java @@ -11,7 +11,7 @@ * @author zhouhao * @since 4.0 */ -@Target(ElementType.METHOD) +@Target({ElementType.METHOD,ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/AuthorizeDefinition.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/AuthorizeDefinition.java index b1c4e8b00..8ab805bd1 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/AuthorizeDefinition.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/AuthorizeDefinition.java @@ -21,6 +21,10 @@ public interface AuthorizeDefinition { boolean isEmpty(); + default boolean allowAnonymous() { + return false; + } + default String getDescription() { ResourcesDefinition res = getResources(); StringJoiner joiner = new StringJoiner(";"); diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/DimensionsDefinition.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/DimensionsDefinition.java index b238f1545..ed8b15b7d 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/DimensionsDefinition.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/DimensionsDefinition.java @@ -2,7 +2,7 @@ import lombok.Getter; import lombok.Setter; -import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections4.CollectionUtils; import org.hswebframework.web.authorization.Dimension; import org.hswebframework.web.authorization.annotation.Logical; diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/ResourceActionDefinition.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/ResourceActionDefinition.java index 2b257cbff..b09339916 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/ResourceActionDefinition.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/ResourceActionDefinition.java @@ -4,23 +4,44 @@ import lombok.Getter; import lombok.Setter; import org.hswebframework.web.bean.FastBeanCopier; +import org.hswebframework.web.i18n.I18nSupportUtils; +import org.hswebframework.web.i18n.MultipleI18nSupportEntity; -import java.util.List; +import java.util.Collection; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import static org.hswebframework.web.authorization.define.ResourceDefinition.supportLocale; @Getter @Setter @EqualsAndHashCode(of = "id") -public class ResourceActionDefinition { +public class ResourceActionDefinition implements MultipleI18nSupportEntity { private String id; private String name; private String description; + private Map> i18nMessages; + private DataAccessDefinition dataAccess = new DataAccessDefinition(); - public ResourceActionDefinition copy(){ - return FastBeanCopier.copy(this,ResourceActionDefinition::new); + + private final static String resolveActionPrefix = "hswebframework.web.system.action."; + + public ResourceActionDefinition copy() { + return FastBeanCopier.copy(this, ResourceActionDefinition::new); } + public Map> getI18nMessages() { + if (org.springframework.util.CollectionUtils.isEmpty(i18nMessages)) { + this.i18nMessages = I18nSupportUtils + .putI18nMessages( + resolveActionPrefix + this.id, "name", supportLocale, null, this.i18nMessages + ); + } + return i18nMessages; + } } diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/ResourceDefinition.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/ResourceDefinition.java index 065cbdc64..815116669 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/ResourceDefinition.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/ResourceDefinition.java @@ -5,18 +5,19 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; -import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections4.CollectionUtils; import org.hswebframework.web.authorization.annotation.Logical; import org.hswebframework.web.bean.FastBeanCopier; +import org.hswebframework.web.i18n.I18nSupportUtils; +import org.hswebframework.web.i18n.MultipleI18nSupportEntity; -import java.io.Serializable; import java.util.*; import java.util.stream.Collectors; @Getter @Setter @EqualsAndHashCode(of = "id") -public class ResourceDefinition { +public class ResourceDefinition implements MultipleI18nSupportEntity { private String id; private String name; @@ -27,6 +28,8 @@ public class ResourceDefinition { private List group; + private Map> i18nMessages; + @Setter(value = AccessLevel.PRIVATE) @JsonIgnore private volatile Set actionIds; @@ -35,6 +38,16 @@ public class ResourceDefinition { private Phased phased = Phased.before; + public final static List supportLocale = new ArrayList<>(); + + static { + supportLocale.add(Locale.CHINESE); + supportLocale.add(Locale.ENGLISH); + } + + + private final static String resolvePermissionPrefix = "hswebframework.web.system.permission."; + public static ResourceDefinition of(String id, String name) { ResourceDefinition definition = new ResourceDefinition(); definition.setId(id); @@ -42,6 +55,16 @@ public static ResourceDefinition of(String id, String name) { return definition; } + public Map> getI18nMessages() { + if (org.springframework.util.CollectionUtils.isEmpty(i18nMessages)) { + this.i18nMessages = I18nSupportUtils + .putI18nMessages( + resolvePermissionPrefix + this.id, "name", supportLocale, null, this.i18nMessages + ); + } + return i18nMessages; + } + public ResourceDefinition copy() { ResourceDefinition definition = FastBeanCopier.copy(this, ResourceDefinition::new); definition.setActions(actions.stream().map(ResourceActionDefinition::copy).collect(Collectors.toSet())); @@ -60,7 +83,7 @@ public synchronized ResourceDefinition addAction(ResourceActionDefinition action ResourceActionDefinition old = getAction(action.getId()).orElse(null); if (old != null) { old.getDataAccess().getDataAccessTypes() - .addAll(action.getDataAccess().getDataAccessTypes()); + .addAll(action.getDataAccess().getDataAccessTypes()); } actions.add(action); return this; @@ -68,8 +91,8 @@ public synchronized ResourceDefinition addAction(ResourceActionDefinition action public Optional getAction(String action) { return actions.stream() - .filter(act -> act.getId().equalsIgnoreCase(action)) - .findAny(); + .filter(act -> act.getId().equalsIgnoreCase(action)) + .findAny(); } public Set getActionIds() { @@ -85,13 +108,13 @@ public Set getActionIds() { @JsonIgnore public List getDataAccessAction() { return actions.stream() - .filter(act -> CollectionUtils.isNotEmpty(act.getDataAccess().getDataAccessTypes())) - .collect(Collectors.toList()); + .filter(act -> CollectionUtils.isNotEmpty(act.getDataAccess().getDataAccessTypes())) + .collect(Collectors.toList()); } public boolean hasDataAccessAction() { return actions.stream() - .anyMatch(act -> CollectionUtils.isNotEmpty(act.getDataAccess().getDataAccessTypes())); + .anyMatch(act -> CollectionUtils.isNotEmpty(act.getDataAccess().getDataAccessTypes())); } public boolean hasAction(Collection actions) { diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/ResourcesDefinition.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/ResourcesDefinition.java index 3aefa1c43..51a57a18a 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/ResourcesDefinition.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/ResourcesDefinition.java @@ -3,7 +3,8 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Getter; import lombok.Setter; -import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections4.CollectionUtils; +import org.hswebframework.web.authorization.Authentication; import org.hswebframework.web.authorization.Permission; import org.hswebframework.web.authorization.annotation.Logical; @@ -62,34 +63,24 @@ public boolean hasPermission(Permission permission) { .isPresent(); } - public boolean isEmpty(){ + public boolean isEmpty() { return resources.isEmpty(); } - public boolean hasPermission(Collection permissions) { + public boolean hasPermission(Authentication authentication) { if (CollectionUtils.isEmpty(resources)) { return true; } - if (CollectionUtils.isEmpty(permissions)) { - return false; - } - if (permissions.size() == 1) { - return hasPermission(permissions.iterator().next()); - } - - Map mappings = permissions.stream().collect(Collectors.toMap(Permission::getId, Function.identity())); if (logical == Logical.AND) { - return resources.stream() - .allMatch(resource -> Optional.ofNullable(mappings.get(resource.getId())) - .map(per -> resource.hasAction(per.getActions())) - .orElse(false)); + return resources + .stream() + .allMatch(resource -> authentication.hasPermission(resource.getId(), resource.getActionIds())); } - return resources.stream() - .anyMatch(resource -> Optional.ofNullable(mappings.get(resource.getId())) - .map(per -> resource.hasAction(per.getActions())) - .orElse(false)); + return resources + .stream() + .anyMatch(resource -> authentication.hasPermission(resource.getId(), resource.getActionIds())); } } diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/dimension/DimensionManager.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/dimension/DimensionManager.java index 5837635db..ccb56a2e5 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/dimension/DimensionManager.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/dimension/DimensionManager.java @@ -15,7 +15,6 @@ public interface DimensionManager { /** * 获取用户维度 * - * @param type 维度类型 * @param userId 用户ID * @return 用户维度信息 */ diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/dimension/DimensionUserBind.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/dimension/DimensionUserBind.java index f1f81bdd9..fba4d9c7f 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/dimension/DimensionUserBind.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/dimension/DimensionUserBind.java @@ -5,15 +5,35 @@ import lombok.NoArgsConstructor; import lombok.Setter; +import java.io.Externalizable; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; + @Getter @Setter @AllArgsConstructor(staticName = "of") @NoArgsConstructor -public class DimensionUserBind { - private String userId; +public class DimensionUserBind implements Externalizable { + private static final long serialVersionUID = -6849794470754667710L; + + private String userId; + + private String dimensionType; - private String dimensionType; + private String dimensionId; - private String dimensionId; + @Override + public void writeExternal(ObjectOutput out) throws IOException { + out.writeUTF(userId); + out.writeUTF(dimensionType); + out.writeUTF(dimensionId); + } + @Override + public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { + userId = in.readUTF(); + dimensionType = in.readUTF(); + dimensionId = in.readUTF(); + } } diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/dimension/DimensionUserDetail.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/dimension/DimensionUserDetail.java index 02fe2fe9e..0bd69e80c 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/dimension/DimensionUserDetail.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/dimension/DimensionUserDetail.java @@ -6,6 +6,7 @@ import lombok.Setter; import org.hswebframework.web.authorization.Dimension; +import java.io.Serializable; import java.util.ArrayList; import java.util.List; @@ -13,7 +14,10 @@ @Setter @AllArgsConstructor(staticName = "of") @NoArgsConstructor -public class DimensionUserDetail { +public class DimensionUserDetail implements Serializable { + private static final long serialVersionUID = -6849794470754667710L; + + private String userId; private List dimensions; diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/events/AuthorizingHandleBeforeEvent.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/events/AuthorizingHandleBeforeEvent.java index d2d189f26..680646cb5 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/events/AuthorizingHandleBeforeEvent.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/events/AuthorizingHandleBeforeEvent.java @@ -2,6 +2,7 @@ import org.hswebframework.web.authorization.define.AuthorizingContext; import org.hswebframework.web.authorization.define.HandleType; +import org.hswebframework.web.event.DefaultAsyncEvent; import org.springframework.context.ApplicationEvent; /** @@ -19,10 +20,7 @@ * @author zhouhao * @since 4.0 */ -// TODO: 2021/12/21 Reactive支持 -public class AuthorizingHandleBeforeEvent extends ApplicationEvent implements AuthorizationEvent { - - private static final long serialVersionUID = -1095765748533721998L; +public class AuthorizingHandleBeforeEvent extends DefaultAsyncEvent implements AuthorizationEvent { private boolean allow = false; @@ -30,15 +28,21 @@ public class AuthorizingHandleBeforeEvent extends ApplicationEvent implements Au private String message; + private final AuthorizingContext context; + + /** + * @deprecated 数据权限控制已取消,4.1版本后移除 + */ + @Deprecated private final HandleType handleType; public AuthorizingHandleBeforeEvent(AuthorizingContext context, HandleType handleType) { - super(context); + this.context = context; this.handleType = handleType; } public AuthorizingContext getContext() { - return ((AuthorizingContext) getSource()); + return context; } public boolean isExecute() { diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/exception/AccessDenyException.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/exception/AccessDenyException.java index bb774e45f..7461f4470 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/exception/AccessDenyException.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/exception/AccessDenyException.java @@ -14,11 +14,11 @@ * @since 3.0 */ @ResponseStatus(HttpStatus.FORBIDDEN) +@Getter public class AccessDenyException extends I18nSupportException { private static final long serialVersionUID = -5135300127303801430L; - @Getter private String code; public AccessDenyException() { @@ -42,7 +42,42 @@ public AccessDenyException(String message, Throwable cause) { } public AccessDenyException(String message, String code, Throwable cause) { - super(message, cause,code); + super(message, cause, code); this.code = code; } + + /** + * 不填充线程栈的异常,在一些对线程栈不敏感,且对异常不可控(如: 来自未认证请求产生的异常)的情况下不填充线程栈对性能有利。 + */ + public static class NoStackTrace extends AccessDenyException { + public NoStackTrace() { + super(); + } + + public NoStackTrace(String message) { + super(message); + } + + public NoStackTrace(String permission, Set actions) { + super(permission, actions); + } + + public NoStackTrace(String message, String code) { + super(message, code); + } + + public NoStackTrace(String message, Throwable cause) { + super(message, cause); + } + + public NoStackTrace(String message, String code, Throwable cause) { + super(message, code, cause); + } + + @Override + public final synchronized Throwable fillInStackTrace() { + return this; + } + } + } diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/exception/AuthenticationException.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/exception/AuthenticationException.java index 767166d5e..f5079ccf9 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/exception/AuthenticationException.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/exception/AuthenticationException.java @@ -27,4 +27,26 @@ public AuthenticationException(String code, String message, Throwable cause) { super(message, cause); this.code = code; } + + /** + * 不填充线程栈的异常,在一些对线程栈不敏感,且对异常不可控(如: 来自未认证请求产生的异常)的情况下不填充线程栈对性能有利。 + */ + public static class NoStackTrace extends AuthenticationException { + public NoStackTrace(String code) { + super(code); + } + + public NoStackTrace(String code, String message) { + super(code, message); + } + + public NoStackTrace(String code, String message, Throwable cause) { + super(code, message, cause); + } + + @Override + public final synchronized Throwable fillInStackTrace() { + return this; + } + } } diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/exception/UnAuthorizedException.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/exception/UnAuthorizedException.java index 75dd34fd6..e2bf61fe5 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/exception/UnAuthorizedException.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/exception/UnAuthorizedException.java @@ -18,6 +18,7 @@ package org.hswebframework.web.authorization.exception; +import lombok.Getter; import org.hswebframework.web.authorization.token.TokenState; import org.hswebframework.web.exception.I18nSupportException; import org.springframework.http.HttpStatus; @@ -29,6 +30,7 @@ * @author zhouhao * @since 3.0 */ +@Getter @ResponseStatus(HttpStatus.UNAUTHORIZED) public class UnAuthorizedException extends I18nSupportException { private static final long serialVersionUID = 2422918455013900645L; @@ -53,7 +55,29 @@ public UnAuthorizedException(String message, TokenState state, Throwable cause) this.state = state; } - public TokenState getState() { - return state; + /** + * 不填充线程栈的异常,在一些对线程栈不敏感,且对异常不可控(如: 来自未认证请求产生的异常)的情况下不填充线程栈对性能有利。 + */ + public static class NoStackTrace extends UnAuthorizedException { + public NoStackTrace() { + super(); + } + + public NoStackTrace(TokenState state) { + super(state); + } + + public NoStackTrace(String message, TokenState state) { + super(message, state); + } + + public NoStackTrace(String message, TokenState state, Throwable cause) { + super(message, state, cause); + } + + @Override + public final synchronized Throwable fillInStackTrace() { + return this; + } } } diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/CompositeReactiveAuthenticationManager.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/CompositeReactiveAuthenticationManager.java index c7a6dc643..4a14a0128 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/CompositeReactiveAuthenticationManager.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/CompositeReactiveAuthenticationManager.java @@ -2,7 +2,7 @@ import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections4.CollectionUtils; import org.hswebframework.web.authorization.*; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/DefaultAuthorizationAutoConfiguration.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/DefaultAuthorizationAutoConfiguration.java index 66fadf24e..704980582 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/DefaultAuthorizationAutoConfiguration.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/DefaultAuthorizationAutoConfiguration.java @@ -14,6 +14,7 @@ import org.hswebframework.web.convert.CustomMessageConverter; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -25,12 +26,9 @@ /** * @author zhouhao */ -@Configuration +@AutoConfiguration public class DefaultAuthorizationAutoConfiguration { - @Autowired(required = false) - private List dataAccessConfigConverters; - @Bean @ConditionalOnMissingBean(UserTokenManager.class) @ConfigurationProperties(prefix = "hsweb.user-token") @@ -67,11 +65,7 @@ public UserTokenAuthenticationSupplier userTokenAuthenticationSupplier(UserToken @ConditionalOnMissingBean(DataAccessConfigBuilderFactory.class) @ConfigurationProperties(prefix = "hsweb.authorization.data-access", ignoreInvalidFields = true) public SimpleDataAccessConfigBuilderFactory dataAccessConfigBuilderFactory() { - SimpleDataAccessConfigBuilderFactory factory = new SimpleDataAccessConfigBuilderFactory(); - if (null != dataAccessConfigConverters) { - dataAccessConfigConverters.forEach(factory::addConvert); - } - return factory; + return new SimpleDataAccessConfigBuilderFactory(); } @Bean diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/DefaultDimensionManager.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/DefaultDimensionManager.java index 25f842c19..9246b1223 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/DefaultDimensionManager.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/DefaultDimensionManager.java @@ -54,7 +54,7 @@ public Flux getUserDimension(Collection userId) { .flatMap(provider -> provider.getDimensionBindInfo(userId)) .groupBy(DimensionUserBind::getDimensionType) .flatMap(group -> { - String type = String.valueOf(group.key()); + String type = group.key(); Flux binds = group.cache(); DimensionProvider provider = providerMapping.get(type); if (null == provider) { @@ -70,7 +70,7 @@ public Flux getUserDimension(Collection userId) { .groupBy(DimensionUserBind::getUserId) .flatMap(userGroup -> Mono .zip( - Mono.just(String.valueOf(userGroup.key())), + Mono.just(userGroup.key()), userGroup .handle((bind, sink) -> { Dimension dimension = mapping.get(bind.getDimensionId()); diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/SimpleAuthentication.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/SimpleAuthentication.java index f4aac6b40..da6c2dc98 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/SimpleAuthentication.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/SimpleAuthentication.java @@ -59,8 +59,8 @@ public Map getAttributes() { public SimpleAuthentication merge(Authentication authentication) { Map mePermissionGroup = permissions - .stream() - .collect(Collectors.toMap(Permission::getId, Function.identity())); + .stream() + .collect(Collectors.toMap(Permission::getId, Function.identity())); if (authentication.getUser() != null) { user = authentication.getUser(); @@ -86,18 +86,23 @@ public SimpleAuthentication merge(Authentication authentication) { return this; } + protected SimpleAuthentication newInstance() { + return new SimpleAuthentication(); + } + @Override public Authentication copy(BiPredicate permissionFilter, Predicate dimension) { - SimpleAuthentication authentication = new SimpleAuthentication(); + SimpleAuthentication authentication = newInstance(); authentication.setDimensions(dimensions.stream().filter(dimension).collect(Collectors.toList())); authentication.setPermissions(permissions - .stream() - .map(permission -> permission.copy(action -> permissionFilter.test(permission, action), conf -> true)) - .filter(per -> !per.getActions().isEmpty()) - .collect(Collectors.toList()) + .stream() + .map(permission -> permission.copy(action -> permissionFilter.test(permission, action), conf -> true)) + .filter(per -> !per.getActions().isEmpty()) + .collect(Collectors.toList()) ); authentication.setUser(user); + authentication.setAttributes(new HashMap<>(attributes)); return authentication; } @@ -106,10 +111,18 @@ public void setUser(User user) { dimensions.add(user); } + protected void setUser0(User user) { + this.user = user; + } + public void setDimensions(List dimensions) { this.dimensions.addAll(dimensions); } + public void setDimensions(Collection dimensions) { + this.dimensions.addAll(dimensions); + } + public void addDimension(Dimension dimension) { this.dimensions.add(dimension); } diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/SimpleDimension.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/SimpleDimension.java index d8df4d036..7a2f1a152 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/SimpleDimension.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/SimpleDimension.java @@ -1,9 +1,6 @@ package org.hswebframework.web.authorization.simple; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.*; import org.hswebframework.web.authorization.Dimension; import org.hswebframework.web.authorization.DimensionType; @@ -13,6 +10,7 @@ @Setter @AllArgsConstructor(staticName = "of") @NoArgsConstructor +@EqualsAndHashCode public class SimpleDimension implements Dimension { private String id; diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/SimpleDimensionType.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/SimpleDimensionType.java index e7d80ff1f..9546cb98c 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/SimpleDimensionType.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/SimpleDimensionType.java @@ -1,9 +1,6 @@ package org.hswebframework.web.authorization.simple; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.*; import org.hswebframework.web.authorization.DimensionType; import java.io.Serializable; @@ -12,6 +9,7 @@ @Setter @AllArgsConstructor(staticName = "of") @NoArgsConstructor +@EqualsAndHashCode public class SimpleDimensionType implements DimensionType, Serializable { private static final long serialVersionUID = -6849794470754667710L; diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/SimplePermission.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/SimplePermission.java index c2d46908c..bba462060 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/SimplePermission.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/SimplePermission.java @@ -1,6 +1,7 @@ package org.hswebframework.web.authorization.simple; import lombok.*; +import org.apache.commons.collections4.CollectionUtils; import org.hswebframework.web.authorization.Permission; import org.hswebframework.web.authorization.access.DataAccessConfig; @@ -16,6 +17,7 @@ @Builder @NoArgsConstructor @AllArgsConstructor +@EqualsAndHashCode(exclude = "dataAccesses") public class SimplePermission implements Permission { private static final long serialVersionUID = 7587266693680162184L; @@ -62,4 +64,9 @@ public Permission copy(Predicate actionFilter, public Permission copy() { return copy(action -> true, conf -> true); } + + @Override + public String toString() { + return id + (CollectionUtils.isNotEmpty(actions) ? ":" + String.join(",", actions) : ""); + } } diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/SimpleRole.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/SimpleRole.java index 4e656911d..e06280ea8 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/SimpleRole.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/SimpleRole.java @@ -15,6 +15,7 @@ @Builder @NoArgsConstructor @AllArgsConstructor +@EqualsAndHashCode public class SimpleRole implements Role { private static final long serialVersionUID = 7460859165231311347L; diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/SimpleUser.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/SimpleUser.java index 46ab4bac6..da1a555fe 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/SimpleUser.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/SimpleUser.java @@ -14,6 +14,7 @@ @NoArgsConstructor @AllArgsConstructor @Builder +@EqualsAndHashCode public class SimpleUser implements User { private static final long serialVersionUID = 2194541828191869091L; diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/builder/SimpleAuthenticationBuilder.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/builder/SimpleAuthenticationBuilder.java index f2a38bd53..696738f3f 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/builder/SimpleAuthenticationBuilder.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/builder/SimpleAuthenticationBuilder.java @@ -3,6 +3,7 @@ import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; +import com.google.common.collect.Maps; import org.hswebframework.web.authorization.*; import org.hswebframework.web.authorization.builder.AuthenticationBuilder; import org.hswebframework.web.authorization.builder.DataAccessConfigBuilderFactory; @@ -44,11 +45,11 @@ public AuthenticationBuilder user(String user) { public AuthenticationBuilder user(Map user) { Objects.requireNonNull(user.get("id")); user(SimpleUser.builder() - .id(user.get("id")) - .username(user.get("username")) - .name(user.get("name")) - .userType(user.get("type")) - .build()); + .id(user.get("id")) + .username(user.get("username")) + .name(user.get("name")) + .userType(user.get("type")) + .build()); return this; } @@ -70,9 +71,7 @@ public AuthenticationBuilder permission(List permission) { return this; } - @Override - public AuthenticationBuilder permission(String permissionJson) { - JSONArray jsonArray = JSON.parseArray(permissionJson); + public AuthenticationBuilder permission(JSONArray jsonArray) { List permissions = new ArrayList<>(); for (int i = 0; i < jsonArray.size(); i++) { JSONObject jsonObject = jsonArray.getJSONObject(i); @@ -87,9 +86,12 @@ public AuthenticationBuilder permission(String permissionJson) { JSONArray dataAccess = jsonObject.getJSONArray("dataAccesses"); if (null != dataAccess) { permission.setDataAccesses(dataAccess.stream().map(JSONObject.class::cast) - .map(dataJson -> dataBuilderFactory.create().fromJson(dataJson.toJSONString()).build()) - .filter(Objects::nonNull) - .collect(Collectors.toSet())); + .map(dataJson -> dataBuilderFactory + .create() + .fromJson(dataJson.toJSONString()) + .build()) + .filter(Objects::nonNull) + .collect(Collectors.toSet())); } permissions.add(permission); } @@ -97,6 +99,11 @@ public AuthenticationBuilder permission(String permissionJson) { return this; } + @Override + public AuthenticationBuilder permission(String permissionJson) { + return permission(JSON.parseArray(permissionJson)); + } + @Override public AuthenticationBuilder attributes(String attributes) { authentication.getAttributes().putAll(JSON.>parseObject(attributes, Map.class)); @@ -119,12 +126,15 @@ public AuthenticationBuilder dimension(JSONArray json) { for (int i = 0; i < json.size(); i++) { JSONObject jsonObject = json.getJSONObject(i); Object type = jsonObject.get("type"); - - dimensions.add( SimpleDimension.of( - jsonObject.getString("id"), - jsonObject.getString("name"), - type instanceof String?SimpleDimensionType.of(String.valueOf(type)):jsonObject.getJSONObject("type").toJavaObject(SimpleDimensionType.class), - jsonObject.getJSONObject("options") + Map options = jsonObject.getJSONObject("options"); + + dimensions.add(SimpleDimension.of( + jsonObject.getString("id"), + jsonObject.getString("name"), + type instanceof String ? SimpleDimensionType.of(String.valueOf(type)) : jsonObject + .getJSONObject("type") + .toJavaObject(SimpleDimensionType.class), + options )); } authentication.setDimensions(dimensions); @@ -138,14 +148,17 @@ public AuthenticationBuilder json(String json) { JSONObject jsonObject = JSON.parseObject(json); user(jsonObject.getObject("user", SimpleUser.class)); if (jsonObject.containsKey("roles")) { - role(jsonObject.getJSONArray("roles").toJSONString()); + role((List) jsonObject.getJSONArray("roles").toJavaList(SimpleRole.class)); } if (jsonObject.containsKey("permissions")) { - permission(jsonObject.getJSONArray("permissions").toJSONString()); + permission(jsonObject.getJSONArray("permissions")); } if (jsonObject.containsKey("dimensions")) { dimension(jsonObject.getJSONArray("dimensions")); } + if (jsonObject.containsKey("attributes")) { + attributes(Maps.transformValues(jsonObject.getJSONObject("attributes"), Serializable.class::cast)); + } return this; } diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/DefaultUserTokenManager.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/DefaultUserTokenManager.java index 545589b84..350a73b13 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/DefaultUserTokenManager.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/DefaultUserTokenManager.java @@ -26,7 +26,6 @@ import org.hswebframework.web.authorization.token.event.UserTokenCreatedEvent; import org.hswebframework.web.authorization.token.event.UserTokenRemovedEvent; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEventPublisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -144,7 +143,7 @@ public Mono tokenIsLoggedIn(String token) { return Mono.just(false); } return getByToken(token) - .map(t -> !t.isExpired()) + .map(UserToken::isNormal) .defaultIfEmpty(false); } @@ -170,42 +169,39 @@ public Mono signOutByUserId(String userId) { } return Mono.defer(() -> { Set tokens = getUserToken(userId); - tokens.forEach(token -> signOutByToken(token, false)); - tokens.clear(); - userStorage.remove(userId); - return Mono.empty(); + return Flux + .fromIterable(tokens) + .flatMap(token -> signOutByToken(token, false)) + .then(Mono.fromRunnable(() -> { + tokens.clear(); + userStorage.remove(userId); + })); }); } - private void signOutByToken(String token, boolean removeUserToken) { - if (token == null) { - return; - } - LocalUserToken tokenObject = tokenStorage.remove(token); - if (tokenObject != null) { - String userId = tokenObject.getUserId(); - if (removeUserToken) { - Set tokens = getUserToken(userId); - if (!tokens.isEmpty()) { - tokens.remove(token); - } - if (tokens.isEmpty()) { - userStorage.remove(tokenObject.getUserId()); + private Mono signOutByToken(String token, boolean removeUserToken) { + if (token != null) { + LocalUserToken tokenObject = tokenStorage.remove(token); + if (tokenObject != null) { + String userId = tokenObject.getUserId(); + if (removeUserToken) { + Set tokens = getUserToken(userId); + if (!tokens.isEmpty()) { + tokens.remove(token); + } + if (tokens.isEmpty()) { + userStorage.remove(tokenObject.getUserId()); + } } + return new UserTokenRemovedEvent(tokenObject).publish(eventPublisher); } - publishEvent(new UserTokenRemovedEvent(tokenObject)); } + return Mono.empty(); } @Override public Mono signOutByToken(String token) { - return Mono.fromRunnable(() -> signOutByToken(token, true)); - } - - protected void publishEvent(ApplicationEvent event) { - if (null != eventPublisher) { - eventPublisher.publishEvent(event); - } + return signOutByToken(token, true); } public Mono changeTokenState(UserToken userToken, TokenState state) { @@ -216,7 +212,7 @@ public Mono changeTokenState(UserToken userToken, TokenState state) { token.setState(state); syncToken(userToken); - publishEvent(new UserTokenChangedEvent(copy, userToken)); + return new UserTokenChangedEvent(copy, userToken).publish(eventPublisher); } return Mono.empty(); } @@ -250,13 +246,13 @@ private Mono doSignIn(String token, String type, S detail.setType(type); detail.setMaxInactiveInterval(maxInactiveInterval); detail.setState(TokenState.normal); - Runnable doSign = () -> { + Mono doSign = Mono.defer(() -> { tokenStorage.put(token, detail); getUserToken(userId).add(token); - publishEvent(new UserTokenCreatedEvent(detail)); - }; + return new UserTokenCreatedEvent(detail).publish(eventPublisher); + }); AllopatricLoginMode mode = allopatricLoginModes.getOrDefault(type, allopatricLoginMode); if (mode == AllopatricLoginMode.deny) { return getByUserId(userId) @@ -268,17 +264,16 @@ private Mono doSignIn(String token, String type, S } return Mono.empty(); }) - .then(Mono.just(detail)) - .doOnNext(__ -> doSign.run()); + .then(doSign) + .thenReturn(detail); } else if (mode == AllopatricLoginMode.offlineOther) { return getByUserId(userId) .filter(userToken -> type.equals(userToken.getType())) .flatMap(userToken -> changeTokenState(userToken, TokenState.offline)) - .then(Mono.just(detail)) - .doOnNext(__ -> doSign.run()); + .then(doSign) + .thenReturn(detail); } - doSign.run(); - return Mono.just(detail); + return doSign.thenReturn(detail); }); } diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/ParsedToken.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/ParsedToken.java index e058775da..c8fa2458e 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/ParsedToken.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/ParsedToken.java @@ -1,5 +1,9 @@ package org.hswebframework.web.authorization.token; +import org.springframework.http.HttpHeaders; + +import java.util.function.BiConsumer; + /** * 令牌解析结果 * @@ -16,7 +20,25 @@ public interface ParsedToken { */ String getType(); + /** + * 将token应用到Http Header + * + * @param headers headers + * @since 4.0.17 + */ + default void apply(HttpHeaders headers) { + throw new UnsupportedOperationException("unsupported apply "+getType()+" token to headers"); + } + + static ParsedToken ofBearer(String token) { + return SimpleParsedToken.of("bearer", token, HttpHeaders::setBearerAuth); + } + static ParsedToken of(String type, String token) { - return SimpleParsedToken.of(type, token); + return of(type, token, (_header, _token) -> _header.set(HttpHeaders.AUTHORIZATION, type + " " + _token)); + } + + static ParsedToken of(String type, String token, BiConsumer headerSetter) { + return SimpleParsedToken.of(type, token, headerSetter); } } diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/SimpleParsedToken.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/SimpleParsedToken.java index cedcac0cd..a11d95f05 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/SimpleParsedToken.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/SimpleParsedToken.java @@ -3,15 +3,25 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; +import org.springframework.http.HttpHeaders; + +import java.util.function.BiConsumer; @Getter @Setter @AllArgsConstructor(staticName = "of") -public class SimpleParsedToken implements ParsedToken{ +public class SimpleParsedToken implements ParsedToken { private String type; private String token; + private BiConsumer headerSetter; + @Override + public void apply(HttpHeaders headers) { + if (headerSetter != null) { + headerSetter.accept(headers,token); + } + } } diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/UserTokenBeforeCreateEvent.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/UserTokenBeforeCreateEvent.java new file mode 100644 index 000000000..ddcaa8967 --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/UserTokenBeforeCreateEvent.java @@ -0,0 +1,19 @@ +package org.hswebframework.web.authorization.token; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import org.hswebframework.web.event.DefaultAsyncEvent; + +@Getter +@Setter +@AllArgsConstructor +public class UserTokenBeforeCreateEvent extends DefaultAsyncEvent { + private final UserToken token; + + /** + * 过期时间,单位毫秒,-1为不过期. + */ + private long expires; + +} diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/event/UserTokenChangedEvent.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/event/UserTokenChangedEvent.java index b78184dff..e9a7c412f 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/event/UserTokenChangedEvent.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/event/UserTokenChangedEvent.java @@ -2,13 +2,12 @@ import org.hswebframework.web.authorization.events.AuthorizationEvent; import org.hswebframework.web.authorization.token.UserToken; -import org.springframework.context.ApplicationEvent; +import org.hswebframework.web.event.DefaultAsyncEvent; -public class UserTokenChangedEvent extends ApplicationEvent implements AuthorizationEvent { +public class UserTokenChangedEvent extends DefaultAsyncEvent implements AuthorizationEvent { private final UserToken before, after; public UserTokenChangedEvent(UserToken before, UserToken after) { - super(after); this.before = before; this.after = after; } diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/event/UserTokenCreatedEvent.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/event/UserTokenCreatedEvent.java index ed7043cee..677e2355a 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/event/UserTokenCreatedEvent.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/event/UserTokenCreatedEvent.java @@ -1,14 +1,13 @@ package org.hswebframework.web.authorization.token.event; -import org.hswebframework.web.authorization.token.UserToken; import org.hswebframework.web.authorization.events.AuthorizationEvent; -import org.springframework.context.ApplicationEvent; +import org.hswebframework.web.authorization.token.UserToken; +import org.hswebframework.web.event.DefaultAsyncEvent; -public class UserTokenCreatedEvent extends ApplicationEvent implements AuthorizationEvent { +public class UserTokenCreatedEvent extends DefaultAsyncEvent implements AuthorizationEvent { private final UserToken detail; public UserTokenCreatedEvent(UserToken detail) { - super(detail); this.detail = detail; } diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/event/UserTokenRemovedEvent.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/event/UserTokenRemovedEvent.java index 00f977bf3..0d0f95809 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/event/UserTokenRemovedEvent.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/event/UserTokenRemovedEvent.java @@ -2,17 +2,19 @@ import org.hswebframework.web.authorization.events.AuthorizationEvent; import org.hswebframework.web.authorization.token.UserToken; -import org.springframework.context.ApplicationEvent; +import org.hswebframework.web.event.DefaultAsyncEvent; -public class UserTokenRemovedEvent extends ApplicationEvent implements AuthorizationEvent { +public class UserTokenRemovedEvent extends DefaultAsyncEvent implements AuthorizationEvent { private static final long serialVersionUID = -6662943150068863177L; + private final UserToken token; + public UserTokenRemovedEvent(UserToken token) { - super(token); + this.token=token; } public UserToken getDetail() { - return ((UserToken) getSource()); + return token; } } diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/redis/RedisUserTokenManager.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/redis/RedisUserTokenManager.java index 6271b80bf..ff2f00b89 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/redis/RedisUserTokenManager.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/redis/RedisUserTokenManager.java @@ -9,6 +9,7 @@ import org.hswebframework.web.authorization.token.event.UserTokenCreatedEvent; import org.hswebframework.web.authorization.token.event.UserTokenRemovedEvent; import org.hswebframework.web.bean.FastBeanCopier; +import org.hswebframework.web.event.AsyncEvent; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory; import org.springframework.data.redis.core.*; @@ -21,11 +22,9 @@ import java.time.Duration; import java.util.HashMap; import java.util.HashSet; -import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; -import java.util.function.Supplier; import java.util.stream.Collectors; public class RedisUserTokenManager implements UserTokenManager { @@ -46,21 +45,21 @@ public RedisUserTokenManager(ReactiveRedisOperations operations) this.userTokenStore = operations.opsForHash(); this.userTokenMapping = operations.opsForSet(); this.operations - .listenToChannel("_user_token_removed") - .subscribe(msg -> localCache.remove(String.valueOf(msg.getMessage()))); + .listenToChannel("_user_token_removed") + .subscribe(msg -> localCache.remove(String.valueOf(msg.getMessage()))); Flux.create(sink -> this.touchSink = sink) .buffer(Flux.interval(Duration.ofSeconds(10)), HashSet::new) .flatMap(list -> Flux - .fromIterable(list) - .flatMap(token -> { - String key = getTokenRedisKey(token.getToken()); - return Mono - .zip(this.userTokenStore.put(key, "lastRequestTime", token.getLastRequestTime()), - this.operations.expire(key, Duration.ofMillis(token.getMaxInactiveInterval()))) - .then(); - }) - .onErrorResume(err -> Mono.empty())) + .fromIterable(list) + .flatMap(token -> { + String key = getTokenRedisKey(token.getToken()); + return Mono + .zip(this.userTokenStore.put(key, "lastRequestTime", token.getLastRequestTime()), + this.operations.expire(key, Duration.ofMillis(token.getMaxInactiveInterval()))) + .then(); + }) + .onErrorResume(err -> Mono.empty())) .subscribe(); } @@ -69,12 +68,12 @@ public RedisUserTokenManager(ReactiveRedisOperations operations) public RedisUserTokenManager(ReactiveRedisConnectionFactory connectionFactory) { this(new ReactiveRedisTemplate<>(connectionFactory, RedisSerializationContext - .newSerializationContext() - .key((RedisSerializer) RedisSerializer.string()) - .value(RedisSerializer.java()) - .hashKey(RedisSerializer.string()) - .hashValue(RedisSerializer.java()) - .build() + .newSerializationContext() + .key((RedisSerializer) RedisSerializer.string()) + .value(RedisSerializer.java()) + .hashKey(RedisSerializer.string()) + .hashValue(RedisSerializer.java()) + .build() )); } @@ -109,80 +108,82 @@ public Mono getByToken(String token) { return Mono.just(inCache); } return userTokenStore - .entries(getTokenRedisKey(token)) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)) - .filter(map -> !map.isEmpty() && map.containsKey("token") && map.containsKey("userId")) - .map(SimpleUserToken::of) - .doOnNext(userToken -> localCache.put(userToken.getToken(), userToken)) - .cast(UserToken.class); + .entries(getTokenRedisKey(token)) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)) + .filter(map -> !map.isEmpty() && map.containsKey("token") && map.containsKey("userId")) + .map(SimpleUserToken::of) + .doOnNext(userToken -> localCache.put(userToken.getToken(), userToken)) + .cast(UserToken.class); } @Override public Flux getByUserId(String userId) { String redisKey = getUserRedisKey(userId); return userTokenMapping - .members(redisKey) - .map(String::valueOf) - .flatMap(token -> getByToken(token) - .switchIfEmpty(Mono.defer(() -> userTokenMapping - .remove(redisKey, token) - .then(Mono.empty())))); + .members(redisKey) + .map(String::valueOf) + .flatMap(token -> getByToken(token) + .switchIfEmpty(Mono.defer(() -> userTokenMapping + .remove(redisKey, token) + .then(Mono.empty())))); } @Override public Mono userIsLoggedIn(String userId) { return getByUserId(userId) - .hasElements(); + .any(UserToken::isNormal); } @Override public Mono tokenIsLoggedIn(String token) { - return operations.hasKey(getTokenRedisKey(token)); + return getByToken(token) + .map(UserToken::isNormal) + .defaultIfEmpty(false); } @Override public Mono totalUser() { return operations - .scan(ScanOptions - .scanOptions() - .match("*user-token-user:*") - .build()) - .count() - .map(Long::intValue); + .scan(ScanOptions + .scanOptions() + .match("*user-token-user:*") + .build()) + .count() + .map(Long::intValue); } @Override public Mono totalToken() { return operations - .scan(ScanOptions - .scanOptions() - .match("*user-token:*") - .build()) - .count() - .map(Long::intValue); + .scan(ScanOptions + .scanOptions() + .match("*user-token:*") + .build()) + .count() + .map(Long::intValue); } @Override public Flux allLoggedUser() { return operations - .scan(ScanOptions - .scanOptions() - .match("*user-token:*") - .build()) - .map(val -> String.valueOf(val).substring(11)) - .flatMap(this::getByToken); + .scan(ScanOptions + .scanOptions() + .match("*user-token:*") + .build()) + .map(val -> String.valueOf(val).substring(11)) + .flatMap(this::getByToken); } @Override public Mono signOutByUserId(String userId) { return this - .getByUserId(userId) - .flatMap(userToken -> operations - .delete(getTokenRedisKey(userToken.getToken())) - .then(onTokenRemoved(userToken))) - .then(operations.delete(getUserRedisKey(userId))) - .then(); + .getByUserId(userId) + .flatMap(userToken -> operations + .delete(getTokenRedisKey(userToken.getToken())) + .then(onTokenRemoved(userToken))) + .then(operations.delete(getUserRedisKey(userId))) + .then(); } @Override @@ -190,96 +191,124 @@ public Mono signOutByToken(String token) { //delete token //srem user token return getByToken(token) - .flatMap(t -> operations - .delete(getTokenRedisKey(t.getToken())) - .then(userTokenMapping.remove(getUserRedisKey(t.getUserId()), token)) - .then(onTokenRemoved(t)) - ) - .then(); + .flatMap(t -> operations + .delete(getTokenRedisKey(t.getToken())) + .then(userTokenMapping.remove(getUserRedisKey(t.getUserId()), token)) + .then(onTokenRemoved(t)) + ) + .then(); } @Override public Mono changeUserState(String userId, TokenState state) { return getByUserId(userId) - .flatMap(token -> changeTokenState(token.getToken(), state)) - .then(); + .flatMap(token -> changeTokenState(token.getToken(), state)) + .then(); } @Override public Mono changeTokenState(String token, TokenState state) { return getByToken(token) - .flatMap(old -> { - SimpleUserToken newToken = FastBeanCopier.copy(old, new SimpleUserToken()); - newToken.setState(state); - return userTokenStore - .put(getTokenRedisKey(token), "state", state.getValue()) - .then(onTokenChanged(old, newToken)); - }); + .flatMap(old -> { + SimpleUserToken newToken = FastBeanCopier.copy(old, new SimpleUserToken()); + newToken.setState(state); + return userTokenStore + .put(getTokenRedisKey(token), "state", state.getValue()) + .then(onTokenChanged(old, newToken)); + }); + } + + protected Mono sign0(String token, + String type, + String userId, + long expires, + boolean ignoreAllopatricLoginMode, + Consumer> cacheBuilder) { + return Mono.defer(() -> { + Map map = new HashMap<>(); + map.put("token", token); + map.put("type", type); + map.put("userId", userId); + map.put("maxInactiveInterval", expires); + map.put("state", TokenState.normal.getValue()); + map.put("signInTime", System.currentTimeMillis()); + map.put("lastRequestTime", System.currentTimeMillis()); + cacheBuilder.accept(map); + String key = getTokenRedisKey(token); + SimpleUserToken userToken = SimpleUserToken.of(map); + + // 推送事件,自定义过期时间等场景 + UserTokenBeforeCreateEvent event = new UserTokenBeforeCreateEvent(userToken, expires); + + return this + .publishEvent(event) + .then(Mono.defer(() -> { + map.put("maxInactiveInterval", event.getExpires()); + if (event.getExpires() > 0) { + return userTokenStore + .putAll(key, map) + .then(operations.expire(key, Duration.ofMillis(event.getExpires()))); + } + return userTokenStore.putAll(key, map); + })) + .then(userTokenMapping.add(getUserRedisKey(userId), token)) + .thenReturn(userToken); + }); } private Mono signIn(String token, String type, String userId, long maxInactiveInterval, + boolean ignoreAllopatricLoginMode, Consumer> cacheBuilder) { long expires = maxTokenExpires.isNegative() ? maxInactiveInterval : Math.min(maxInactiveInterval, maxTokenExpires.toMillis()); return Mono - .defer(() -> { - Mono doSign = Mono.defer(() -> { - Map map = new HashMap<>(); - map.put("token", token); - map.put("type", type); - map.put("userId", userId); - map.put("maxInactiveInterval", expires); - map.put("state", TokenState.normal.getValue()); - map.put("signInTime", System.currentTimeMillis()); - map.put("lastRequestTime", System.currentTimeMillis()); - cacheBuilder.accept(map); - String key = getTokenRedisKey(token); - return userTokenStore - .putAll(key, map) - .then(Mono.defer(() -> { - if (expires > 0) { - return operations.expire(key, Duration.ofMillis(expires)); - } - return Mono.empty(); - })) - .then(userTokenMapping.add(getUserRedisKey(userId), token)) - .thenReturn(SimpleUserToken.of(map)); - }); - - AllopatricLoginMode mode = allopatricLoginModes.getOrDefault(type, allopatricLoginMode); - if (mode == AllopatricLoginMode.deny) { - return userIsLoggedIn(userId) - .flatMap(r -> { - if (r) { - return Mono.error(new AccessDenyException("error.logged_in_elsewhere", TokenState.deny.getValue())); - } - return doSign; - }); - - } else if (mode == AllopatricLoginMode.offlineOther) { - return getByUserId(userId) - .flatMap(userToken -> { - if (type.equals(userToken.getType())) { - return this.changeTokenState(userToken.getToken(), TokenState.offline); - } - return Mono.empty(); - }) - .then(doSign); - } - + .defer(() -> { + Mono doSign = sign0( + token, + type, + userId, + expires, + ignoreAllopatricLoginMode, + cacheBuilder + ); + + if (ignoreAllopatricLoginMode) { return doSign; - }) - .flatMap(this::onUserTokenCreated); + } + AllopatricLoginMode mode = allopatricLoginModes.getOrDefault(type, allopatricLoginMode); + if (mode == AllopatricLoginMode.deny) { + return userIsLoggedIn(userId) + .flatMap(r -> { + if (r) { + return Mono.error(new AccessDenyException("error.logged_in_elsewhere", TokenState.deny.getValue())); + } + return doSign; + }); + + } else if (mode == AllopatricLoginMode.offlineOther) { + return getByUserId(userId) + .flatMap(userToken -> { + if (type.equals(userToken.getType())) { + return this.changeTokenState(userToken.getToken(), TokenState.offline); + } + return Mono.empty(); + }) + .then(doSign); + } + + return doSign; + }) + .flatMap(this::onUserTokenCreated); } @Override public Mono signIn(String token, String type, String userId, long maxInactiveInterval) { - return signIn(token, type, userId, maxInactiveInterval, ignore -> { + return signIn(token, type, userId, maxInactiveInterval, false, ignore -> { }); } @@ -290,9 +319,10 @@ public Mono signIn(String token, long maxInactiveInterval, Authentication authentication) { return this - .signIn(token, type, userId, maxInactiveInterval, - cache -> cache.put("authentication", authentication)) - .cast(AuthenticationUserToken.class); + .signIn(token, type, userId, maxInactiveInterval, + true, + cache -> cache.put("authentication", authentication)) + .cast(AuthenticationUserToken.class); } @Override @@ -307,32 +337,32 @@ public Mono touch(String token) { return Mono.empty(); } return getByToken(token) - .flatMap(userToken -> { - if (userToken.getMaxInactiveInterval() > 0) { - touchSink.next(userToken); - } - return Mono.empty(); - }); + .flatMap(userToken -> { + if (userToken.getMaxInactiveInterval() > 0) { + touchSink.next(userToken); + } + return Mono.empty(); + }); } @Override public Mono checkExpiredToken() { return operations - .scan(ScanOptions.scanOptions().match("*user-token-user:*").build()) + .scan(ScanOptions.scanOptions().match("*user-token-user:*").build()) + .map(String::valueOf) + .flatMap(key -> userTokenMapping + .members(key) .map(String::valueOf) - .flatMap(key -> userTokenMapping - .members(key) - .map(String::valueOf) - .flatMap(token -> operations - .hasKey(getTokenRedisKey(token)) - .flatMap(exists -> { - if (!exists) { - return userTokenMapping.remove(key, token); - } - return Mono.empty(); - }))) - .then(); + .flatMap(token -> operations + .hasKey(getTokenRedisKey(token)) + .flatMap(exists -> { + if (!exists) { + return userTokenMapping.remove(key, token); + } + return Mono.empty(); + }))) + .then(); } private Mono notifyTokenRemoved(String token) { @@ -345,8 +375,9 @@ private Mono onTokenRemoved(UserToken token) { if (eventPublisher == null) { return notifyTokenRemoved(token.getToken()); } - return Mono.fromRunnable(() -> eventPublisher.publishEvent(new UserTokenRemovedEvent(token))) - .then(notifyTokenRemoved(token.getToken())); + return new UserTokenRemovedEvent(token) + .publish(eventPublisher) + .then(notifyTokenRemoved(token.getToken())); } private Mono onTokenChanged(UserToken old, SimpleUserToken newToken) { @@ -354,19 +385,28 @@ private Mono onTokenChanged(UserToken old, SimpleUserToken newToken) { if (eventPublisher == null) { return notifyTokenRemoved(newToken.getToken()); } - return Mono.fromRunnable(() -> eventPublisher.publishEvent(new UserTokenChangedEvent(old, newToken))); + return new UserTokenChangedEvent(old, newToken) + .publish(eventPublisher) + .then(notifyTokenRemoved(newToken.getToken())); + } + + private Mono publishEvent(AsyncEvent event) { + if (eventPublisher != null) { + return event.publish(eventPublisher); + } + return Mono.empty(); } private Mono onUserTokenCreated(SimpleUserToken token) { localCache.put(token.getToken(), token); if (eventPublisher == null) { return notifyTokenRemoved(token.getToken()) - .thenReturn(token); - } - return Mono - .fromRunnable(() -> eventPublisher.publishEvent(new UserTokenCreatedEvent(token))) - .then(notifyTokenRemoved(token.getToken())) .thenReturn(token); + } + return new UserTokenCreatedEvent(token) + .publish(eventPublisher) + .then(notifyTokenRemoved(token.getToken())) + .thenReturn(token); } } diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/resources/META-INF/spring.factories b/hsweb-authorization/hsweb-authorization-api/src/main/resources/META-INF/spring.factories deleted file mode 100644 index cb2dcecb0..000000000 --- a/hsweb-authorization/hsweb-authorization-api/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1,3 +0,0 @@ -# Auto Configure -org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -org.hswebframework.web.authorization.simple.DefaultAuthorizationAutoConfiguration \ No newline at end of file diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hsweb-authorization/hsweb-authorization-api/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..1afa66b8e --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-api/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +org.hswebframework.web.authorization.simple.DefaultAuthorizationAutoConfiguration \ No newline at end of file diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/resources/i18n/authentication/messages_en.properties b/hsweb-authorization/hsweb-authorization-api/src/main/resources/i18n/authentication/messages_en.properties index a65a77de1..1f4814b9c 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/resources/i18n/authentication/messages_en.properties +++ b/hsweb-authorization/hsweb-authorization-api/src/main/resources/i18n/authentication/messages_en.properties @@ -1,7 +1,7 @@ error.access_denied=Access Denied error.permission_denied=Permission Denied [{0}]:{1} error.logged_in_elsewhere=User logged in elsewhere -error.illegal_password=Bad username or password +error.illegal_password=The username and password are incorrect or the user has been disabled error.illegal_user_password=Bad Password error.user_disabled=User is disabled # diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/resources/i18n/authentication/messages_zh.properties b/hsweb-authorization/hsweb-authorization-api/src/main/resources/i18n/authentication/messages_zh.properties index 7349e8bda..9d34f7188 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/resources/i18n/authentication/messages_zh.properties +++ b/hsweb-authorization/hsweb-authorization-api/src/main/resources/i18n/authentication/messages_zh.properties @@ -1,7 +1,7 @@ error.access_denied=权限不足,拒绝访问! error.permission_denied=当前用户无权限[{0}]:{1} error.logged_in_elsewhere=该用户已在其他地方登陆 -error.illegal_password=用户名或密码错误 +error.illegal_password=用户名密码错误或用户已被禁用 error.illegal_user_password=密码错误 error.user_disabled=用户已被禁用 # diff --git a/hsweb-authorization/hsweb-authorization-api/src/test/java/org/hswebframework/web/authorization/AuthenticationTests.java b/hsweb-authorization/hsweb-authorization-api/src/test/java/org/hswebframework/web/authorization/AuthenticationTests.java index d624d8473..0eb341d6f 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/test/java/org/hswebframework/web/authorization/AuthenticationTests.java +++ b/hsweb-authorization/hsweb-authorization-api/src/test/java/org/hswebframework/web/authorization/AuthenticationTests.java @@ -9,6 +9,7 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.springframework.context.support.StaticApplicationContext; import reactor.core.publisher.Mono; import reactor.core.publisher.SignalType; import reactor.test.StepVerifier; @@ -115,8 +116,12 @@ public Mono getByUserId(String userId) { }; //绑定用户token - UserTokenManager userTokenManager = new DefaultUserTokenManager(); - UserToken token = userTokenManager.signIn("test", "token-test", "admin", -1,authentication).block(); + DefaultUserTokenManager userTokenManager = new DefaultUserTokenManager(); + StaticApplicationContext ctx= new StaticApplicationContext(); + ctx.refresh(); + userTokenManager.setEventPublisher(ctx); + UserToken token = userTokenManager.signIn("test", "token-test", "admin", -1,authentication) + .block(); ReactiveAuthenticationHolder.addSupplier(new UserTokenReactiveAuthenticationSupplier(userTokenManager, authenticationManager)); ParsedToken parsedToken=new ParsedToken() { @@ -136,9 +141,6 @@ public String getType() { .currentReactive() .map(Authentication::getUser) .map(User::getId) - .doOnEach(ReactiveLogger.on(SignalType.ON_NEXT,(ctx,signal)->{ - System.out.println(ctx); - })) .contextWrite(Context.of(ParsedToken.class, parsedToken)) .contextWrite(ReactiveLogger.start("rid","1")) .as(StepVerifier::create) diff --git a/hsweb-authorization/hsweb-authorization-api/src/test/java/org/hswebframework/web/authorization/UserTokenManagerTests.java b/hsweb-authorization/hsweb-authorization-api/src/test/java/org/hswebframework/web/authorization/UserTokenManagerTests.java index 9331086cd..74a9e0953 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/test/java/org/hswebframework/web/authorization/UserTokenManagerTests.java +++ b/hsweb-authorization/hsweb-authorization-api/src/test/java/org/hswebframework/web/authorization/UserTokenManagerTests.java @@ -5,10 +5,19 @@ import org.hswebframework.web.authorization.token.*; import org.junit.Assert; import org.junit.Test; +import org.springframework.context.support.StaticApplicationContext; import reactor.test.StepVerifier; public class UserTokenManagerTests { + private DefaultUserTokenManager createUserTokenManager(){ + DefaultUserTokenManager userTokenManager = new DefaultUserTokenManager(); + StaticApplicationContext context=new StaticApplicationContext(); + context.refresh(); + + userTokenManager.setEventPublisher(context); + return userTokenManager; + } /** * 基本功能测试 @@ -17,7 +26,7 @@ public class UserTokenManagerTests { */ @Test public void testDefaultSetting() throws InterruptedException { - DefaultUserTokenManager userTokenManager = new DefaultUserTokenManager(); + DefaultUserTokenManager userTokenManager = createUserTokenManager(); userTokenManager.setAllopatricLoginMode(AllopatricLoginMode.allow); //允许异地登录 UserToken userToken = userTokenManager.signIn("test", "sessionId", "admin", 1000).block(); @@ -83,6 +92,7 @@ public void testDefaultSetting() throws InterruptedException { public void testDeny() throws InterruptedException { DefaultUserTokenManager userTokenManager = new DefaultUserTokenManager(); userTokenManager.setAllopatricLoginMode(AllopatricLoginMode.deny);//如果在其他地方登录,本地禁止登录 + userTokenManager.setEventPublisher(new StaticApplicationContext()); userTokenManager.signIn("test", "sessionId", "admin", 10000).subscribe(); @@ -102,7 +112,8 @@ public void testDeny() throws InterruptedException { */ @Test public void testOffline() { - DefaultUserTokenManager userTokenManager = new DefaultUserTokenManager(); + DefaultUserTokenManager userTokenManager = createUserTokenManager(); + userTokenManager.setAllopatricLoginMode(AllopatricLoginMode.offlineOther); //将其他地方登录的用户踢下线 userTokenManager.signIn("test", "sessionId", "admin", 1000).subscribe(); @@ -117,7 +128,7 @@ public void testOffline() { @Test public void testAuth() { - UserTokenManager userTokenManager = new DefaultUserTokenManager(); + DefaultUserTokenManager userTokenManager = createUserTokenManager(); Authentication authentication = new SimpleAuthentication(); userTokenManager.signIn("test", "test", "test", 1000, authentication) diff --git a/hsweb-authorization/hsweb-authorization-api/src/test/java/org/hswebframework/web/authorization/token/redis/RedisUserTokenManagerTest.java b/hsweb-authorization/hsweb-authorization-api/src/test/java/org/hswebframework/web/authorization/token/redis/RedisUserTokenManagerTest.java index 53fbcdeaf..bfea6787a 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/test/java/org/hswebframework/web/authorization/token/redis/RedisUserTokenManagerTest.java +++ b/hsweb-authorization/hsweb-authorization-api/src/test/java/org/hswebframework/web/authorization/token/redis/RedisUserTokenManagerTest.java @@ -7,6 +7,7 @@ import org.hswebframework.web.authorization.simple.SimpleAuthentication; import org.hswebframework.web.authorization.token.*; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.springframework.data.redis.connection.RedisStandaloneConfiguration; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; @@ -19,6 +20,7 @@ import static org.junit.Assert.*; +@Ignore public class RedisUserTokenManagerTest { UserTokenManager tokenManager; @@ -28,8 +30,8 @@ public void init() { LettuceConnectionFactory factory = new LettuceConnectionFactory(new RedisStandaloneConfiguration("127.0.0.1")); ReactiveRedisTemplate template = new ReactiveRedisTemplate<>( - factory, - RedisSerializationContext.java() + factory, + RedisSerializationContext.java() ); factory.afterPropertiesSet(); diff --git a/hsweb-authorization/hsweb-authorization-basic/pom.xml b/hsweb-authorization/hsweb-authorization-basic/pom.xml index 2ea98f896..8fc140d2c 100644 --- a/hsweb-authorization/hsweb-authorization-basic/pom.xml +++ b/hsweb-authorization/hsweb-authorization-basic/pom.xml @@ -5,10 +5,11 @@ hsweb-authorization org.hswebframework.web - 4.0.16-SNAPSHOT + 4.0.20-SNAPSHOT 4.0.0 + ${artifactId} hsweb-authorization-basic 实现hsweb-authorization-api的相关接口以及使用aop实现RBAC和数据权限的控制 diff --git a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/aop/AopAuthorizingController.java b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/aop/AopAuthorizingController.java index 23171ebd2..de642d936 100644 --- a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/aop/AopAuthorizingController.java +++ b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/aop/AopAuthorizingController.java @@ -19,6 +19,7 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -63,43 +64,44 @@ protected Publisher handleReactive0(AuthorizeDefinition definition, MethodInterceptorHolder holder, AuthorizingContext context, Supplier> invoker) { + MethodInterceptorContext interceptorContext = holder.createParamContext(invoker.get()); + context.setParamContext(interceptorContext); + return this + .invokeReactive( + Authentication + .currentReactive() + .switchIfEmpty( + context.getDefinition().allowAnonymous() + ? Mono.empty() + : Mono.error(UnAuthorizedException.NoStackTrace::new)) + .flatMap(auth -> { + context.setAuthentication(auth); + //响应式不再支持数据权限控制 + return authorizingHandler.handRBACAsync(context); + }), + (Publisher) interceptorContext.getInvokeResult()); + } + + private Publisher invokeReactive(Mono before, Publisher source) { + if (source instanceof Mono) { + return before.then((Mono) source); + } + return before.thenMany(source); + } - return Authentication - .currentReactive() - .switchIfEmpty(Mono.error(UnAuthorizedException::new)) - .flatMapMany(auth -> { - context.setAuthentication(auth); - Function afterRuner = runnable -> { - MethodInterceptorContext interceptorContext = holder.createParamContext(invoker.get()); - context.setParamContext(interceptorContext); - runnable.run(); - return (Publisher) interceptorContext.getInvokeResult(); - }; - if (context.getDefinition().getPhased() != Phased.after) { - authorizingHandler.handRBAC(context); - if (context.getDefinition().getResources().getPhased() != Phased.after) { - authorizingHandler.handleDataAccess(context); - return invoker.get(); - } else { - return afterRuner.apply(() -> authorizingHandler.handleDataAccess(context)); - } - - } else { - if (context.getDefinition().getResources().getPhased() != Phased.after) { - authorizingHandler.handleDataAccess(context); - return invoker.get(); - } else { - return afterRuner.apply(() -> { - authorizingHandler.handRBAC(context); - authorizingHandler.handleDataAccess(context); - }); - } - } - }); + private T invokeReactive(MethodInvocation invocation) { + if (Mono.class.isAssignableFrom(invocation.getMethod().getReturnType())) { + return (T) Mono.defer(() -> doProceed(invocation)); + } + if (Flux.class.isAssignableFrom(invocation.getMethod().getReturnType())) { + return (T) Flux.defer(() -> doProceed(invocation)); + } + return doProceed(invocation); } @SneakyThrows private T doProceed(MethodInvocation invocation) { + return (T) invocation.proceed(); } @@ -109,7 +111,12 @@ public Object invoke(MethodInvocation methodInvocation) throws Throwable { MethodInterceptorContext paramContext = holder.createParamContext(); - AuthorizeDefinition definition = aopMethodAuthorizeDefinitionParser.parse(methodInvocation.getThis().getClass(), methodInvocation.getMethod(), paramContext); + AuthorizeDefinition definition = aopMethodAuthorizeDefinitionParser + .parse(methodInvocation + .getThis() + .getClass(), + methodInvocation.getMethod(), + paramContext); Object result = null; boolean isControl = false; if (null != definition && !definition.isEmpty()) { @@ -120,16 +127,12 @@ public Object invoke(MethodInvocation methodInvocation) throws Throwable { Class returnType = methodInvocation.getMethod().getReturnType(); //handle reactive method if (Publisher.class.isAssignableFrom(returnType)) { - Publisher publisher = handleReactive0(definition, holder, context, () -> doProceed(methodInvocation)); - if (Mono.class.isAssignableFrom(returnType)) { - return Mono.from(publisher); - } else if (Flux.class.isAssignableFrom(returnType)) { - return Flux.from(publisher); - } - throw new UnsupportedOperationException("unsupported reactive type:" + returnType); + return handleReactive0(definition, holder, context, () -> invokeReactive(methodInvocation)); } - Authentication authentication = Authentication.current().orElseThrow(UnAuthorizedException::new); + Authentication authentication = Authentication + .current() + .orElseThrow(UnAuthorizedException.NoStackTrace::new); context.setAuthentication(authentication); isControl = true; @@ -185,8 +188,9 @@ public AopAuthorizingController(AuthorizingHandler authorizingHandler, AopMethod public boolean matches(Method method, Class aClass) { Authorize authorize; boolean support = AnnotationUtils.findAnnotation(aClass, Controller.class) != null - || AnnotationUtils.findAnnotation(aClass, RestController.class) != null - || ((authorize = AnnotationUtils.findAnnotation(aClass, method, Authorize.class)) != null && !authorize.ignore() + || AnnotationUtils.findAnnotation(aClass, RestController.class) != null + || AnnotationUtils.findAnnotation(aClass, RequestMapping.class) != null + || ((authorize = AnnotationUtils.findAnnotation(aClass, method, Authorize.class)) != null && !authorize.ignore() ); if (support && autoParse) { @@ -198,10 +202,11 @@ public boolean matches(Method method, Class aClass) { @Override public void run(String... args) throws Exception { if (autoParse) { - List definitions = aopMethodAuthorizeDefinitionParser.getAllParsed() - .stream() - .filter(def -> !def.isEmpty()) - .collect(Collectors.toList()); + List definitions = aopMethodAuthorizeDefinitionParser + .getAllParsed() + .stream() + .filter(def -> !def.isEmpty()) + .collect(Collectors.toList()); log.info("publish AuthorizeDefinitionInitializedEvent,definition size:{}", definitions.size()); eventPublisher.publishEvent(new AuthorizeDefinitionInitializedEvent(definitions)); diff --git a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/configuration/AuthorizingHandlerAutoConfiguration.java b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/configuration/AuthorizingHandlerAutoConfiguration.java index e542d8c63..83dd6e592 100644 --- a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/configuration/AuthorizingHandlerAutoConfiguration.java +++ b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/configuration/AuthorizingHandlerAutoConfiguration.java @@ -3,32 +3,20 @@ import org.hswebframework.web.authorization.AuthenticationManager; import org.hswebframework.web.authorization.ReactiveAuthenticationManagerProvider; import org.hswebframework.web.authorization.access.DataAccessController; -import org.hswebframework.web.authorization.access.DataAccessHandler; -import org.hswebframework.web.authorization.basic.aop.AopMethodAuthorizeDefinitionParser; import org.hswebframework.web.authorization.basic.embed.EmbedAuthenticationProperties; import org.hswebframework.web.authorization.basic.embed.EmbedReactiveAuthenticationManager; +import org.hswebframework.web.authorization.basic.handler.AuthorizationLoginLoggerInfoHandler; import org.hswebframework.web.authorization.basic.handler.DefaultAuthorizingHandler; import org.hswebframework.web.authorization.basic.handler.UserAllowPermissionHandler; import org.hswebframework.web.authorization.basic.handler.access.DefaultDataAccessController; -import org.hswebframework.web.authorization.basic.twofactor.TwoFactorHandlerInterceptorAdapter; import org.hswebframework.web.authorization.basic.web.*; import org.hswebframework.web.authorization.token.UserTokenManager; -import org.hswebframework.web.authorization.twofactor.TwoFactorValidatorManager; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.*; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.Ordered; -import org.springframework.core.annotation.Order; -import org.springframework.web.servlet.config.annotation.InterceptorRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; - -import javax.annotation.Nonnull; -import java.util.List; /** * 权限控制自动配置类 @@ -36,7 +24,7 @@ * @author zhouhao * @since 3.0 */ -@Configuration +@AutoConfiguration @EnableConfigurationProperties(EmbedAuthenticationProperties.class) public class AuthorizingHandlerAutoConfiguration { @@ -92,26 +80,6 @@ public BearerTokenParser bearerTokenParser() { return new BearerTokenParser(); } - @Configuration - public static class DataAccessHandlerProcessor implements BeanPostProcessor { - - @Autowired - private DefaultDataAccessController defaultDataAccessController; - - @Override - public Object postProcessBeforeInitialization(Object bean, String beanName) { - return bean; - } - - @Override - public Object postProcessAfterInitialization(Object bean, String beanName) { - if (bean instanceof DataAccessHandler) { - defaultDataAccessController.addHandler(((DataAccessHandler) bean)); - } - return bean; - } - } - @Configuration @ConditionalOnProperty(prefix = "hsweb.authorize", name = "basic-authorization", havingValue = "true") @@ -125,4 +93,11 @@ public BasicAuthorizationTokenParser basicAuthorizationTokenParser(Authenticatio } } + + + @Bean + @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) + public AuthorizationLoginLoggerInfoHandler authorizationLoginLoggerInfoHandler() { + return new AuthorizationLoginLoggerInfoHandler(); + } } diff --git a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/configuration/WebMvcAuthorizingConfiguration.java b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/configuration/WebMvcAuthorizingConfiguration.java index e7b97facc..7ff7373e0 100644 --- a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/configuration/WebMvcAuthorizingConfiguration.java +++ b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/configuration/WebMvcAuthorizingConfiguration.java @@ -5,6 +5,7 @@ import org.hswebframework.web.authorization.basic.web.*; import org.hswebframework.web.authorization.token.UserTokenManager; import org.hswebframework.web.authorization.twofactor.TwoFactorValidatorManager; +import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.*; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; @@ -17,7 +18,7 @@ import javax.annotation.Nonnull; import java.util.List; -@Configuration +@AutoConfiguration @ConditionalOnClass(name = "org.springframework.web.servlet.config.annotation.WebMvcConfigurer") @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) public class WebMvcAuthorizingConfiguration { diff --git a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/define/DefaultBasicAuthorizeDefinition.java b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/define/DefaultBasicAuthorizeDefinition.java index dd334acf8..d41ce68c6 100644 --- a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/define/DefaultBasicAuthorizeDefinition.java +++ b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/define/DefaultBasicAuthorizeDefinition.java @@ -4,15 +4,16 @@ import lombok.*; import org.hswebframework.web.authorization.annotation.*; import org.hswebframework.web.authorization.define.*; -import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.util.StringUtils; import java.lang.annotation.Annotation; import java.lang.reflect.Method; -import java.util.*; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import static org.hswebframework.web.authorization.define.ResourceDefinition.supportLocale; /** * 默认权限权限定义 @@ -40,18 +41,25 @@ public class DefaultBasicAuthorizeDefinition implements AopAuthorizeDefinition { private Phased phased = Phased.before; + private boolean allowAnonymous = false; + @Override public boolean isEmpty() { return false; } + @Override + public boolean allowAnonymous() { + return allowAnonymous; + } + private static final Set> types = new HashSet<>(Arrays.asList( - Authorize.class, - DataAccess.class, - Dimension.class, - Resource.class, - ResourceAction.class, - DataAccessType.class + Authorize.class, + DataAccess.class, + Dimension.class, + Resource.class, + ResourceAction.class, + DataAccessType.class )); public static AopAuthorizeDefinition from(Class targetClass, Method method) { @@ -73,6 +81,9 @@ public void putAnnotation(Authorize ann) { for (Dimension dimension : ann.dimension()) { putAnnotation(dimension); } + if (ann.anonymous()) { + allowAnonymous = true; + } } public void putAnnotation(Dimension ann) { diff --git a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/handler/AuthorizationLoginLoggerInfoHandler.java b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/handler/AuthorizationLoginLoggerInfoHandler.java new file mode 100644 index 000000000..13c08cff9 --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/handler/AuthorizationLoginLoggerInfoHandler.java @@ -0,0 +1,33 @@ +package org.hswebframework.web.authorization.basic.handler; + +import org.hswebframework.web.authorization.Authentication; +import org.hswebframework.web.authorization.events.AuthorizationSuccessEvent; +import org.hswebframework.web.logging.AccessLoggerInfo; +import org.springframework.context.event.EventListener; +import reactor.core.publisher.Mono; + +/** + * @author gyl + * @since 2.2 + */ +public class AuthorizationLoginLoggerInfoHandler { + + @EventListener + public void fillLoggerInfoAuth(AuthorizationSuccessEvent event) { + event.async( + //填充操作日志用户认证信息 + Mono.deferContextual(ctx -> { + ctx.getOrEmpty(AccessLoggerInfo.class) + .ifPresent(loggerInfo -> { + Authentication auth = event.getAuthentication(); + loggerInfo.putContext("userId", auth.getUser().getId()); + loggerInfo.putContext("username", auth.getUser().getUsername()); + loggerInfo.putContext("userName", auth.getUser().getName()); + }); + // FIXME: 2024/3/26 未传递用户维度信息,如有需要也可通过上下文传递 + return Mono.empty(); + }) + ); + + } +} diff --git a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/handler/AuthorizingHandler.java b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/handler/AuthorizingHandler.java index 8d35de635..8a630a303 100644 --- a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/handler/AuthorizingHandler.java +++ b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/handler/AuthorizingHandler.java @@ -1,6 +1,7 @@ package org.hswebframework.web.authorization.basic.handler; import org.hswebframework.web.authorization.define.AuthorizingContext; +import reactor.core.publisher.Mono; /** * aop方式权限控制处理器 @@ -8,10 +9,17 @@ * @author zhouhao */ public interface AuthorizingHandler { + void handRBAC(AuthorizingContext context); + default Mono handRBACAsync(AuthorizingContext context) { + return Mono.fromRunnable(() -> handRBAC(context)); + } + + @Deprecated void handleDataAccess(AuthorizingContext context); + @Deprecated default void handle(AuthorizingContext context) { handRBAC(context); handleDataAccess(context); diff --git a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/handler/DefaultAuthorizingHandler.java b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/handler/DefaultAuthorizingHandler.java index 66057c732..bfac89095 100644 --- a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/handler/DefaultAuthorizingHandler.java +++ b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/handler/DefaultAuthorizingHandler.java @@ -1,5 +1,7 @@ package org.hswebframework.web.authorization.basic.handler; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; import org.hswebframework.web.authorization.Authentication; import org.hswebframework.web.authorization.Permission; import org.hswebframework.web.authorization.access.DataAccessController; @@ -13,16 +15,18 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; +import reactor.core.publisher.Mono; + +import java.util.concurrent.TimeUnit; /** * @author zhouhao */ +@Slf4j public class DefaultAuthorizingHandler implements AuthorizingHandler { private DataAccessController dataAccessController; - private Logger logger = LoggerFactory.getLogger(this.getClass()); - private ApplicationEventPublisher eventPublisher; public DefaultAuthorizingHandler(DataAccessController dataAccessController) { @@ -51,15 +55,54 @@ public void handRBAC(AuthorizingContext context) { } + @Override + public Mono handRBACAsync(AuthorizingContext context) { + return this + .handleEventAsync(context, HandleType.RBAC) + .doOnNext(handled -> { + //没有自定义事件处理 + if (!handled) { + handleRBAC(context.getAuthentication(), context.getDefinition()); + } + }) + .then(); + } + + private Mono handleEventAsync(AuthorizingContext context, HandleType type) { + if (null != eventPublisher) { + AuthorizingHandleBeforeEvent event = new AuthorizingHandleBeforeEvent(context, type); + return event + .publish(eventPublisher) + .then(Mono.fromCallable(() -> { + if (!event.isExecute()) { + if (event.isAllow()) { + return true; + } else { + throw new AccessDenyException.NoStackTrace(event.getMessage()); + } + } + return false; + })); + } + return Mono.just(false); + } + + @SneakyThrows private boolean handleEvent(AuthorizingContext context, HandleType type) { if (null != eventPublisher) { AuthorizingHandleBeforeEvent event = new AuthorizingHandleBeforeEvent(context, type); eventPublisher.publishEvent(event); + if (event.hasListener()) { + event + .getAsync() + .toFuture() + .get(10, TimeUnit.SECONDS); + } if (!event.isExecute()) { if (event.isAllow()) { return true; } else { - throw new AccessDenyException(event.getMessage()); + throw new AccessDenyException.NoStackTrace(event.getMessage()); } } } @@ -69,7 +112,7 @@ private boolean handleEvent(AuthorizingContext context, HandleType type) { public void handleDataAccess(AuthorizingContext context) { if (dataAccessController == null) { - logger.warn("dataAccessController is null,skip result access control!"); + log.warn("dataAccessController is null,skip result access control!"); return; } if (context.getDefinition().getResources() == null) { @@ -82,21 +125,26 @@ public void handleDataAccess(AuthorizingContext context) { DataAccessController finalAccessController = dataAccessController; Authentication autz = context.getAuthentication(); - boolean isAccess = context.getDefinition() - .getResources() - .getDataAccessResources() - .stream() - .allMatch(resource -> { - Permission permission = autz.getPermission(resource.getId()).orElseThrow(AccessDenyException::new); - return resource.getDataAccessAction() - .stream() - .allMatch(act -> permission.getDataAccesses(act.getId()) - .stream() - .allMatch(dataAccessConfig -> finalAccessController.doAccess(dataAccessConfig, context))); - - }); + boolean isAccess = context + .getDefinition() + .getResources() + .getDataAccessResources() + .stream() + .allMatch(resource -> { + Permission permission = autz + .getPermission(resource.getId()) + .orElseThrow(AccessDenyException.NoStackTrace::new); + return resource + .getDataAccessAction() + .stream() + .allMatch(act -> permission + .getDataAccesses(act.getId()) + .stream() + .allMatch(dataAccessConfig -> finalAccessController.doAccess(dataAccessConfig, context))); + + }); if (!isAccess) { - throw new AccessDenyException(context.getDefinition().getMessage()); + throw new AccessDenyException.NoStackTrace(context.getDefinition().getMessage()); } } @@ -105,8 +153,8 @@ protected void handleRBAC(Authentication authentication, AuthorizeDefinition def ResourcesDefinition resources = definition.getResources(); - if (!resources.hasPermission(authentication.getPermissions())) { - throw new AccessDenyException(definition.getMessage(),definition.getDescription()); + if (!resources.hasPermission(authentication)) { + throw new AccessDenyException.NoStackTrace(definition.getMessage(), definition.getDescription()); } } } diff --git a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/handler/access/DimensionDataAccessHandler.java b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/handler/access/DimensionDataAccessHandler.java index 2c3638975..09aa2ed85 100644 --- a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/handler/access/DimensionDataAccessHandler.java +++ b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/handler/access/DimensionDataAccessHandler.java @@ -5,7 +5,7 @@ import lombok.Setter; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections4.CollectionUtils; import org.hswebframework.ezorm.core.param.Param; import org.hswebframework.ezorm.core.param.QueryParam; import org.hswebframework.web.api.crud.entity.Entity; diff --git a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/web/AuthorizationController.java b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/web/AuthorizationController.java index a10024680..8ac82b20b 100644 --- a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/web/AuthorizationController.java +++ b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/web/AuthorizationController.java @@ -33,8 +33,10 @@ import org.hswebframework.web.authorization.exception.UnAuthorizedException; import org.hswebframework.web.authorization.simple.PlainTextUsernamePasswordAuthenticationRequest; import org.hswebframework.web.logging.AccessLogger; +import org.hswebframework.web.logging.AccessLoggerInfo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.event.EventListener; import org.springframework.http.MediaType; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.*; @@ -68,7 +70,7 @@ public Mono me() { @PostMapping(value = "/login", consumes = MediaType.APPLICATION_JSON_VALUE) @Authorize(ignore = true) - @AccessLogger(ignore = true) + @AccessLogger(ignoreParameter = {"parameter"}) @Operation(summary = "登录", description = "必要参数:username,password.根据配置不同,其他参数也不同,如:验证码等.") public Mono> authorizeByJson(@Parameter(example = "{\"username\":\"admin\",\"password\":\"admin\"}") @RequestBody Mono> parameter) { @@ -82,8 +84,8 @@ public Mono> authorizeByJson(@Parameter(example = "{\"userna private Mono> doLogin(Mono> parameter) { return parameter.flatMap(parameters -> { - String username_ = (String) parameters.get("username"); - String password_ = (String) parameters.get("password"); + String username_ = String.valueOf(parameters.getOrDefault("username", "")); + String password_ = String.valueOf(parameters.getOrDefault("password", "")); Assert.hasLength(username_, "validation.username_must_not_be_empty"); Assert.hasLength(password_, "validation.password_must_not_be_empty"); @@ -116,7 +118,7 @@ private Mono> doLogin(Mono> parameter) { failedEvent.setException(err); return failedEvent .publish(eventPublisher) - .then(Mono.error(failedEvent.getException())); + .then(Mono.error(failedEvent::getException)); }); }); } @@ -129,12 +131,12 @@ private Mono doAuthorize(AuthorizationBeforeEvent event) { } else { authenticationMono = ReactiveAuthenticationHolder .get(event.getUserId()) - .switchIfEmpty(Mono.error(() -> new AuthenticationException(AuthenticationException.USER_DISABLED))); + .switchIfEmpty(Mono.error(() -> new AuthenticationException.NoStackTrace(AuthenticationException.USER_DISABLED))); } } else { authenticationMono = authenticationManager .authenticate(Mono.just(new PlainTextUsernamePasswordAuthenticationRequest(event.getUsername(), event.getPassword()))) - .switchIfEmpty(Mono.error(() -> new AuthenticationException(AuthenticationException.ILLEGAL_PASSWORD))); + .switchIfEmpty(Mono.error(() -> new AuthenticationException.NoStackTrace(AuthenticationException.ILLEGAL_PASSWORD))); } return authenticationMono; } diff --git a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/web/AuthorizedToken.java b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/web/AuthorizedToken.java index f53a74868..af9db38e3 100644 --- a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/web/AuthorizedToken.java +++ b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/web/AuthorizedToken.java @@ -1,5 +1,6 @@ package org.hswebframework.web.authorization.basic.web; +import org.hswebframework.web.authorization.Authentication; import org.hswebframework.web.authorization.token.ParsedToken; /** @@ -14,6 +15,16 @@ public interface AuthorizedToken extends ParsedToken { */ String getUserId(); + /** + * 获取认证权限信息 + * + * @return Authentication + * @since 4.0.17 + */ + default Authentication getAuthentication() { + return null; + } + /** * @return 令牌有效期,单位毫秒,-1为长期有效 */ diff --git a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/web/BearerTokenParser.java b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/web/BearerTokenParser.java index d7c49d176..6a848b14a 100644 --- a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/web/BearerTokenParser.java +++ b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/web/BearerTokenParser.java @@ -15,7 +15,7 @@ public Mono parseToken(ServerWebExchange exchange) { .getFirst(HttpHeaders.AUTHORIZATION); if (token != null && token.startsWith("Bearer ")) { - return Mono.just(ParsedToken.of("bearer", token.substring(7))); + return Mono.just(ParsedToken.ofBearer(token.substring(7))); } return Mono.empty(); } diff --git a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/web/DefaultUserTokenGenPar.java b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/web/DefaultUserTokenGenPar.java index b1948ef42..685498503 100644 --- a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/web/DefaultUserTokenGenPar.java +++ b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/web/DefaultUserTokenGenPar.java @@ -19,8 +19,11 @@ public class DefaultUserTokenGenPar implements ReactiveUserTokenGenerator, React private long timeout = TimeUnit.MINUTES.toMillis(30); + @SuppressWarnings("all") private String headerName = "X-Access-Token"; + private String parameterName = ":X_Access_Token"; + @Override public String getTokenType() { return "default"; @@ -58,10 +61,10 @@ public Mono parseToken(ServerWebExchange exchange) { String token = Optional.ofNullable(exchange.getRequest() .getHeaders() .getFirst(headerName)) - .orElseGet(() -> exchange.getRequest().getQueryParams().getFirst(":X_Access_Token")); + .orElseGet(() -> exchange.getRequest().getQueryParams().getFirst(parameterName)); if (token == null) { return Mono.empty(); } - return Mono.just(ParsedToken.of(getTokenType(),token)); + return Mono.just(ParsedToken.of(getTokenType(),token,(_header,_token)->_header.set(headerName,_token))); } } diff --git a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/web/UserTokenWebFilter.java b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/web/UserTokenWebFilter.java index 711b32d73..82c9bd134 100644 --- a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/web/UserTokenWebFilter.java +++ b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/web/UserTokenWebFilter.java @@ -11,6 +11,7 @@ import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.event.EventListener; +import org.springframework.core.annotation.Order; import org.springframework.lang.NonNull; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; @@ -29,6 +30,7 @@ @Component @Slf4j +@Order(1) public class UserTokenWebFilter implements WebFilter, BeanPostProcessor { private final List parsers = new ArrayList<>(); @@ -43,41 +45,46 @@ public class UserTokenWebFilter implements WebFilter, BeanPostProcessor { public Mono filter(@NonNull ServerWebExchange exchange, WebFilterChain chain) { return Flux - .fromIterable(parsers) - .flatMap(parser -> parser.parseToken(exchange)) - .next() - .map(token -> chain - .filter(exchange) - .contextWrite(Context.of(ParsedToken.class, token))) - .defaultIfEmpty(chain.filter(exchange)) - .flatMap(Function.identity()) - .contextWrite(ReactiveLogger.start("requestId", exchange.getRequest().getId())); + .fromIterable(parsers) + .flatMap(parser -> parser.parseToken(exchange)) + .next() + .map(token -> chain + .filter(exchange) + .contextWrite(Context.of(ParsedToken.class, token))) + .defaultIfEmpty(chain.filter(exchange)) + .flatMap(Function.identity()) + .contextWrite(ReactiveLogger.start("requestId", exchange.getRequest().getId())); } @EventListener public void handleUserSign(AuthorizationSuccessEvent event) { ReactiveUserTokenGenerator generator = event - .getParameter("tokenType") - .map(tokenGeneratorMap::get) - .orElseGet(() -> tokenGeneratorMap.get("default")); + .getParameter("tokenType") + .map(tokenGeneratorMap::get) + .orElseGet(() -> tokenGeneratorMap.get("default")); if (generator != null) { GeneratedToken token = generator.generate(event.getAuthentication()); event.getResult().putAll(token.getResponse()); if (StringUtils.hasText(token.getToken())) { event.getResult().put("token", token.getToken()); - long expires = event.getParameter("expires") - .map(String::valueOf) - .map(Long::parseLong) - .orElse(token.getTimeout()); - event.getResult().put("expires", expires); - event.async(userTokenManager - .signIn(token.getToken(), token.getType(), event - .getAuthentication() - .getUser() - .getId(), expires) - .doOnNext(t -> log.debug("user [{}] sign in", t.getUserId())) - .then()); + long expires = event + .getParameter("expires") + .map(String::valueOf) + .map(Long::parseLong) + .orElse(token.getTimeout()); + + event.async( + userTokenManager + .signIn(token.getToken(), token.getType(), event + .getAuthentication() + .getUser() + .getId(), expires) + .doOnNext(t -> { + event.getResult().put("expires", t.getMaxInactiveInterval()); + log.debug("user [{}] sign in", t.getUserId()); + }) + .then()); } } diff --git a/hsweb-authorization/hsweb-authorization-basic/src/main/resources/META-INF/spring.factories b/hsweb-authorization/hsweb-authorization-basic/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports similarity index 57% rename from hsweb-authorization/hsweb-authorization-basic/src/main/resources/META-INF/spring.factories rename to hsweb-authorization/hsweb-authorization-basic/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index e8d9b2d64..42849de36 100644 --- a/hsweb-authorization/hsweb-authorization-basic/src/main/resources/META-INF/spring.factories +++ b/hsweb-authorization/hsweb-authorization-basic/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,4 +1,2 @@ -# Auto Configure -org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -org.hswebframework.web.authorization.basic.configuration.AuthorizingHandlerAutoConfiguration,\ +org.hswebframework.web.authorization.basic.configuration.AuthorizingHandlerAutoConfiguration org.hswebframework.web.authorization.basic.configuration.WebMvcAuthorizingConfiguration \ No newline at end of file diff --git a/hsweb-authorization/hsweb-authorization-basic/src/test/java/org/hswebframework/web/authorization/basic/aop/AopAuthorizingControllerTest.java b/hsweb-authorization/hsweb-authorization-basic/src/test/java/org/hswebframework/web/authorization/basic/aop/AopAuthorizingControllerTest.java index 31ed54700..5c930e119 100644 --- a/hsweb-authorization/hsweb-authorization-basic/src/test/java/org/hswebframework/web/authorization/basic/aop/AopAuthorizingControllerTest.java +++ b/hsweb-authorization/hsweb-authorization-basic/src/test/java/org/hswebframework/web/authorization/basic/aop/AopAuthorizingControllerTest.java @@ -1,5 +1,6 @@ package org.hswebframework.web.authorization.basic.aop; +import org.hswebframework.ezorm.core.CastUtil; import org.hswebframework.ezorm.core.param.Param; import org.hswebframework.ezorm.core.param.QueryParam; import org.hswebframework.ezorm.core.param.Term; @@ -59,114 +60,114 @@ public Mono get() { .expectNext("403") .verifyComplete(); } - - @Test - public void testFiledDeny() { - SimpleAuthentication authentication = new SimpleAuthentication(); - - SimpleFieldFilterDataAccessConfig config = new SimpleFieldFilterDataAccessConfig(); - config.setAction("query"); - config.setFields(new HashSet<>(Arrays.asList("name"))); - - authentication.setUser(SimpleUser.builder().id("test").username("test").build()); - authentication.setPermissions(Arrays.asList(SimplePermission.builder() - .actions(Collections.singleton("query")) - .dataAccesses(Collections.singleton(config)) - .id("test").build())); - - ReactiveAuthenticationHolder.setSupplier(new ReactiveAuthenticationSupplier() { - @Override - public Mono get(String userId) { - return Mono.empty(); - } - - @Override - public Mono get() { - return Mono.just(authentication); - } - }); - - testController.queryUser(new QueryParam()) - .map(Param::getExcludes) - .as(StepVerifier::create) - .expectNextMatches(f -> f.contains("name")) - .verifyComplete(); - - testController.queryUser(Mono.just(new QueryParam())) - .map(Param::getExcludes) - .as(StepVerifier::create) - .expectNextMatches(f -> f.contains("name")) - .verifyComplete(); - } - - @Test - public void testDimensionDataAccess() { - SimpleAuthentication authentication = new SimpleAuthentication(); - - DimensionDataAccessConfig config = new DimensionDataAccessConfig(); - config.setAction("query"); - config.setScopeType("role"); - - DimensionDataAccessConfig config2 = new DimensionDataAccessConfig(); - config2.setAction("save"); - config2.setScopeType("role"); - ReactiveAuthenticationHolder.setSupplier(new ReactiveAuthenticationSupplier() { - @Override - public Mono get(String userId) { - return Mono.empty(); - } - - @Override - public Mono get() { - return Mono.just(authentication); - } - }); - - authentication.setUser(SimpleUser.builder().id("test").username("test").build()); - authentication.setPermissions(Arrays.asList(SimplePermission.builder() - .actions(new HashSet<>(Arrays.asList("query", "save"))) - .dataAccesses(new HashSet<>(Arrays.asList(config, config2))) - .id("test").build())); - authentication.setDimensions(Collections.singletonList(Dimension.of("test", "test", DefaultDimensionType.role))); - - testController.queryUserByDimension(Mono.just(new QueryParam())) - .map(Param::getTerms) - .flatMapIterable(Function.identity()) - .next() - .map(Term::getValue) - .>map(Collection.class::cast) - .flatMapIterable(Function.identity()) - .next() - .as(StepVerifier::create) - .expectNextMatches("test"::equals) - .verifyComplete(); - - TestEntity testEntity = new TestEntity(); - testEntity.setRoleId("123"); - - testController.save(Mono.just(testEntity)) - .as(StepVerifier::create) - .expectError(AccessDenyException.class) - .verify(); - - testController.add(Mono.just(testEntity)) - .as(StepVerifier::create) - .expectNextCount(1) - .verifyComplete(); - - testController.update(testEntity.getId(),Mono.just(testEntity)) - .as(StepVerifier::create) - .expectError(AccessDenyException.class) - .verify(); - - testEntity = new TestEntity(); - testEntity.setRoleId("test"); - - testController.save(Mono.just(testEntity)) - .as(StepVerifier::create) - .expectNextCount(1) - .verifyComplete(); - - - } +// +// @Test +// public void testFiledDeny() { +// SimpleAuthentication authentication = new SimpleAuthentication(); +// +// SimpleFieldFilterDataAccessConfig config = new SimpleFieldFilterDataAccessConfig(); +// config.setAction("query"); +// config.setFields(new HashSet<>(Arrays.asList("name"))); +// +// authentication.setUser(SimpleUser.builder().id("test").username("test").build()); +// authentication.setPermissions(Arrays.asList(SimplePermission.builder() +// .actions(Collections.singleton("query")) +// .dataAccesses(Collections.singleton(config)) +// .id("test").build())); +// +// ReactiveAuthenticationHolder.setSupplier(new ReactiveAuthenticationSupplier() { +// @Override +// public Mono get(String userId) { +// return Mono.empty(); +// } +// +// @Override +// public Mono get() { +// return Mono.just(authentication); +// } +// }); +// +// testController.queryUser(new QueryParam()) +// .map(Param::getExcludes) +// .as(StepVerifier::create) +// .expectNextMatches(f -> f.contains("name")) +// .verifyComplete(); +// +// testController.queryUser(Mono.just(new QueryParam())) +// .map(Param::getExcludes) +// .as(StepVerifier::create) +// .expectNextMatches(f -> f.contains("name")) +// .verifyComplete(); +// } +// +// @Test +// public void testDimensionDataAccess() { +// SimpleAuthentication authentication = new SimpleAuthentication(); +// +// DimensionDataAccessConfig config = new DimensionDataAccessConfig(); +// config.setAction("query"); +// config.setScopeType("role"); +// +// DimensionDataAccessConfig config2 = new DimensionDataAccessConfig(); +// config2.setAction("save"); +// config2.setScopeType("role"); +// ReactiveAuthenticationHolder.setSupplier(new ReactiveAuthenticationSupplier() { +// @Override +// public Mono get(String userId) { +// return Mono.empty(); +// } +// +// @Override +// public Mono get() { +// return Mono.just(authentication); +// } +// }); +// +// authentication.setUser(SimpleUser.builder().id("test").username("test").build()); +// authentication.setPermissions(Arrays.asList(SimplePermission.builder() +// .actions(new HashSet<>(Arrays.asList("query", "save"))) +// .dataAccesses(new HashSet<>(Arrays.asList(config, config2))) +// .id("test").build())); +// authentication.setDimensions(Collections.singletonList(Dimension.of("test", "test", DefaultDimensionType.role))); +// +// testController.queryUserByDimension(Mono.just(new QueryParam())) +// .map(Param::getTerms) +// .flatMapIterable(Function.identity()) +// .next() +// .map(Term::getValue) +// .map(CastUtil::>cast) +// .flatMapIterable(Function.identity()) +// .next() +// .as(StepVerifier::create) +// .expectNextMatches("test"::equals) +// .verifyComplete(); +// +// TestEntity testEntity = new TestEntity(); +// testEntity.setRoleId("123"); +// +// testController.save(Mono.just(testEntity)) +// .as(StepVerifier::create) +// .expectError(AccessDenyException.class) +// .verify(); +// +// testController.add(Mono.just(testEntity)) +// .as(StepVerifier::create) +// .expectNextCount(1) +// .verifyComplete(); +// +// testController.update(testEntity.getId(),Mono.just(testEntity)) +// .as(StepVerifier::create) +// .expectError(AccessDenyException.class) +// .verify(); +// +// testEntity = new TestEntity(); +// testEntity.setRoleId("test"); +// +// testController.save(Mono.just(testEntity)) +// .as(StepVerifier::create) +// .expectNextCount(1) +// .verifyComplete(); +// +// +// } } \ No newline at end of file diff --git a/hsweb-authorization/hsweb-authorization-oauth2/pom.xml b/hsweb-authorization/hsweb-authorization-oauth2/pom.xml index b35b99b87..5c59f00ec 100644 --- a/hsweb-authorization/hsweb-authorization-oauth2/pom.xml +++ b/hsweb-authorization/hsweb-authorization-oauth2/pom.xml @@ -5,10 +5,11 @@ hsweb-authorization org.hswebframework.web - 4.0.16-SNAPSHOT + 4.0.20-SNAPSHOT 4.0.0 + ${artifactId} hsweb-authorization-oauth2 diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/OAuth2Exception.java b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/OAuth2Exception.java index 41d8a62a5..8a87b07b6 100644 --- a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/OAuth2Exception.java +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/OAuth2Exception.java @@ -2,6 +2,7 @@ import lombok.Getter; import org.hswebframework.web.exception.BusinessException; +import org.hswebframework.web.exception.I18nSupportException; @Getter public class OAuth2Exception extends BusinessException { @@ -16,4 +17,22 @@ public OAuth2Exception(String message, Throwable cause, ErrorType type) { super(message, cause); this.type = type; } + + /** + * 不填充线程栈的异常,在一些对线程栈不敏感,且对异常不可控(如: 来自未认证请求产生的异常)的情况下不填充线程栈对性能有利。 + */ + public static class NoStackTrace extends OAuth2Exception { + public NoStackTrace(ErrorType type) { + super(type); + } + + public NoStackTrace(String message, Throwable cause, ErrorType type) { + super(message, cause, type); + } + + @Override + public final synchronized Throwable fillInStackTrace() { + return this; + } + } } diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/AccessTokenManager.java b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/AccessTokenManager.java index 7762b4e4c..d6669c6e2 100644 --- a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/AccessTokenManager.java +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/AccessTokenManager.java @@ -25,7 +25,7 @@ public interface AccessTokenManager { * @param clientId clientId {@link OAuth2Client#getClientId()} * @param authentication 权限信息 * @param singleton 是否单例,如果为true,重复创建token将返回首次创建的token - * @return + * @return AccessToken */ Mono createAccessToken(String clientId, Authentication authentication, diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/OAuth2Client.java b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/OAuth2Client.java index d231171a5..c3205c28c 100644 --- a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/OAuth2Client.java +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/OAuth2Client.java @@ -4,6 +4,7 @@ import lombok.Setter; import org.hswebframework.web.oauth2.ErrorType; import org.hswebframework.web.oauth2.OAuth2Exception; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import javax.validation.constraints.NotBlank; @@ -30,13 +31,13 @@ public class OAuth2Client { private String userId; public void validateRedirectUri(String redirectUri) { - if (StringUtils.isEmpty(redirectUri) || (!redirectUri.startsWith(this.redirectUrl))) { + if (ObjectUtils.isEmpty(redirectUri) || (!redirectUri.startsWith(this.redirectUrl))) { throw new OAuth2Exception(ErrorType.ILLEGAL_REDIRECT_URI); } } public void validateSecret(String secret) { - if (StringUtils.isEmpty(secret) || (!secret.equals(this.clientSecret))) { + if (ObjectUtils.isEmpty(secret) || (!secret.equals(this.clientSecret))) { throw new OAuth2Exception(ErrorType.ILLEGAL_CLIENT_SECRET); } } diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/OAuth2ServerAutoConfiguration.java b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/OAuth2ServerAutoConfiguration.java index 42f1a575c..4fdfa28e0 100644 --- a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/OAuth2ServerAutoConfiguration.java +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/OAuth2ServerAutoConfiguration.java @@ -1,11 +1,8 @@ package org.hswebframework.web.oauth2.server; -import org.hswebframework.web.authorization.ReactiveAuthenticationHolder; import org.hswebframework.web.authorization.ReactiveAuthenticationManager; import org.hswebframework.web.authorization.basic.web.ReactiveUserTokenParser; -import org.hswebframework.web.authorization.token.UserToken; import org.hswebframework.web.authorization.token.UserTokenManager; -import org.hswebframework.web.oauth2.server.auth.ReactiveOAuth2AccessTokenParser; import org.hswebframework.web.oauth2.server.code.AuthorizationCodeGranter; import org.hswebframework.web.oauth2.server.code.DefaultAuthorizationCodeGranter; import org.hswebframework.web.oauth2.server.credential.ClientCredentialGranter; @@ -16,6 +13,7 @@ import org.hswebframework.web.oauth2.server.refresh.RefreshTokenGranter; import org.hswebframework.web.oauth2.server.web.OAuth2AuthorizeController; import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -27,7 +25,7 @@ import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory; import org.springframework.data.redis.core.ReactiveRedisOperations; -@Configuration(proxyBeanMethods = false) +@AutoConfiguration @EnableConfigurationProperties(OAuth2Properties.class) public class OAuth2ServerAutoConfiguration { diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/auth/ReactiveOAuth2AccessTokenParser.java b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/auth/ReactiveOAuth2AccessTokenParser.java index 152d0b525..b25b5025d 100644 --- a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/auth/ReactiveOAuth2AccessTokenParser.java +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/auth/ReactiveOAuth2AccessTokenParser.java @@ -5,9 +5,6 @@ import org.hswebframework.web.authorization.ReactiveAuthenticationSupplier; import org.hswebframework.web.authorization.basic.web.ReactiveUserTokenParser; import org.hswebframework.web.authorization.token.ParsedToken; -import org.hswebframework.web.context.ContextKey; -import org.hswebframework.web.context.ContextUtils; -import org.hswebframework.web.logger.ReactiveLogger; import org.hswebframework.web.oauth2.server.AccessTokenManager; import org.springframework.http.HttpHeaders; import org.springframework.util.StringUtils; @@ -23,7 +20,7 @@ public class ReactiveOAuth2AccessTokenParser implements ReactiveUserTokenParser, public Mono parseToken(ServerWebExchange exchange) { String token = exchange.getRequest().getQueryParams().getFirst("access_token"); - if (StringUtils.isEmpty(token)) { + if (!StringUtils.hasText(token)) { token = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION); if (StringUtils.hasText(token)) { String[] typeAndToken = token.split("[ ]"); diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/code/DefaultAuthorizationCodeGranter.java b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/code/DefaultAuthorizationCodeGranter.java index 38e0341f6..d63b56883 100644 --- a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/code/DefaultAuthorizationCodeGranter.java +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/code/DefaultAuthorizationCodeGranter.java @@ -58,9 +58,13 @@ public Mono requestCode(AuthorizationCodeRequest requ ScopePredicate permissionPredicate = OAuth2ScopeUtils.createScopePredicate(codeCache.getScope()); - codeCache.setAuthentication(authentication.copy( + Authentication copy = authentication.copy( (permission, action) -> permissionPredicate.test(permission.getId(), action), - dimension -> permissionPredicate.test(dimension.getType().getId(), dimension.getId()))); + dimension -> permissionPredicate.test(dimension.getType().getId(), dimension.getId())); + + copy.setAttribute("scope", codeCache.getScope()); + + codeCache.setAuthentication(copy); return redis diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/impl/RedisAccessToken.java b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/impl/RedisAccessToken.java index 262116926..d5eaeaac8 100644 --- a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/impl/RedisAccessToken.java +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/impl/RedisAccessToken.java @@ -27,8 +27,18 @@ public class RedisAccessToken implements Serializable { private boolean singleton; - public AccessToken toAccessToken(int expiresIn){ - AccessToken token=new AccessToken(); + public boolean storeAuth() { + boolean allowAllScope = authentication + .getAttribute("scope") + .map("*"::equals) + .orElse(false); + + //不是单例,并且没有授予全部权限 + return !singleton && !allowAllScope; + } + + public AccessToken toAccessToken(int expiresIn) { + AccessToken token = new AccessToken(); token.setAccessToken(accessToken); token.setRefreshToken(refreshToken); token.setExpiresIn(expiresIn); diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/impl/RedisAccessTokenManager.java b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/impl/RedisAccessTokenManager.java index 412cb00b8..ff4dc98ae 100644 --- a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/impl/RedisAccessTokenManager.java +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/impl/RedisAccessTokenManager.java @@ -20,8 +20,6 @@ import reactor.core.publisher.Mono; import java.time.Duration; -import java.util.HashSet; -import java.util.Set; import java.util.UUID; public class RedisAccessTokenManager implements AccessTokenManager { @@ -96,20 +94,22 @@ private Mono doCreateAccessToken(String clientId, Authenticati } private Mono storeAuthToken(RedisAccessToken token) { - if (token.isSingleton()) { + //保存独立的权限信息,通常是用户指定了特定的授权范围时生效. + if (token.storeAuth()) { return userTokenManager .signIn(token.getAccessToken(), createTokenType(token.getClientId()), token.getAuthentication().getUser().getId(), - tokenExpireIn * 1000L) + tokenExpireIn * 1000L, + token.getAuthentication()) .then(); + } else { return userTokenManager .signIn(token.getAccessToken(), createTokenType(token.getClientId()), token.getAuthentication().getUser().getId(), - tokenExpireIn * 1000L, - token.getAuthentication()) + tokenExpireIn * 1000L) .then(); } } @@ -134,10 +134,10 @@ private Mono storeToken(RedisAccessToken token) { private Mono doCreateSingletonAccessToken(String clientId, Authentication authentication) { String redisKey = createSingletonTokenRedisKey(clientId); - return tokenRedis .opsForValue() .get(redisKey) + .filterWhen(token -> userTokenManager.tokenIsLoggedIn(token.getAccessToken())) .flatMap(token -> tokenRedis .getExpire(redisKey) .map(duration -> token.toAccessToken((int) (duration.toMillis() / 1000)))) diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/resources/META-INF/spring.factories b/hsweb-authorization/hsweb-authorization-oauth2/src/main/resources/META-INF/spring.factories deleted file mode 100644 index 8e76d8af6..000000000 --- a/hsweb-authorization/hsweb-authorization-oauth2/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1,3 +0,0 @@ -# Auto Configure -org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -org.hswebframework.web.oauth2.server.OAuth2ServerAutoConfiguration \ No newline at end of file diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hsweb-authorization/hsweb-authorization-oauth2/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..42c207559 --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +org.hswebframework.web.oauth2.server.OAuth2ServerAutoConfiguration \ No newline at end of file diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/test/java/org/hswebframework/web/oauth2/server/code/DefaultAuthorizationCodeGranterTest.java b/hsweb-authorization/hsweb-authorization-oauth2/src/test/java/org/hswebframework/web/oauth2/server/code/DefaultAuthorizationCodeGranterTest.java index 6e0545441..286d06eb6 100644 --- a/hsweb-authorization/hsweb-authorization-oauth2/src/test/java/org/hswebframework/web/oauth2/server/code/DefaultAuthorizationCodeGranterTest.java +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/test/java/org/hswebframework/web/oauth2/server/code/DefaultAuthorizationCodeGranterTest.java @@ -1,35 +1,42 @@ package org.hswebframework.web.oauth2.server.code; -import org.hswebframework.web.authorization.Permission; import org.hswebframework.web.authorization.simple.SimpleAuthentication; -import org.hswebframework.web.authorization.simple.SimplePermission; +import org.hswebframework.web.authorization.simple.SimpleUser; import org.hswebframework.web.oauth2.server.OAuth2Client; import org.hswebframework.web.oauth2.server.RedisHelper; import org.hswebframework.web.oauth2.server.impl.RedisAccessTokenManager; +import org.junit.Ignore; import org.junit.Test; import org.springframework.context.support.StaticApplicationContext; import reactor.test.StepVerifier; import java.util.Collections; -import java.util.function.BiPredicate; - -import static org.junit.Assert.*; +@Ignore public class DefaultAuthorizationCodeGranterTest { @Test public void testRequestToken() { + StaticApplicationContext context = new StaticApplicationContext(); + context.refresh(); + context.start(); + DefaultAuthorizationCodeGranter codeGranter = new DefaultAuthorizationCodeGranter( - new RedisAccessTokenManager(RedisHelper.factory), new StaticApplicationContext(), RedisHelper.factory + new RedisAccessTokenManager(RedisHelper.factory), context, RedisHelper.factory ); OAuth2Client client = new OAuth2Client(); client.setClientId("test"); client.setClientSecret("test"); + SimpleAuthentication authentication = new SimpleAuthentication(); + authentication.setUser(SimpleUser + .builder() + .id("test") + .build()); codeGranter - .requestCode(new AuthorizationCodeRequest(client, new SimpleAuthentication(), Collections.emptyMap())) + .requestCode(new AuthorizationCodeRequest(client, authentication, Collections.emptyMap())) .doOnNext(System.out::println) .flatMap(response -> codeGranter .requestToken(new AuthorizationCodeTokenRequest(client, Collections.singletonMap("code", response.getCode())))) diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/test/java/org/hswebframework/web/oauth2/server/impl/RedisAccessTokenManagerTest.java b/hsweb-authorization/hsweb-authorization-oauth2/src/test/java/org/hswebframework/web/oauth2/server/impl/RedisAccessTokenManagerTest.java index 75f65ac82..a118207ef 100644 --- a/hsweb-authorization/hsweb-authorization-oauth2/src/test/java/org/hswebframework/web/oauth2/server/impl/RedisAccessTokenManagerTest.java +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/test/java/org/hswebframework/web/oauth2/server/impl/RedisAccessTokenManagerTest.java @@ -1,7 +1,9 @@ package org.hswebframework.web.oauth2.server.impl; import org.hswebframework.web.authorization.simple.SimpleAuthentication; +import org.hswebframework.web.authorization.simple.SimpleUser; import org.hswebframework.web.oauth2.server.RedisHelper; +import org.junit.Ignore; import org.junit.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -9,6 +11,7 @@ import static org.junit.Assert.*; +@Ignore public class RedisAccessTokenManagerTest { @Test @@ -16,12 +19,14 @@ public void testCreateAccessToken() { RedisAccessTokenManager tokenManager = new RedisAccessTokenManager(RedisHelper.factory); SimpleAuthentication authentication = new SimpleAuthentication(); - + authentication.setUser(SimpleUser.builder() + .id("test") + .build()); tokenManager.createAccessToken("test", authentication, false) - .doOnNext(System.out::println) - .as(StepVerifier::create) - .expectNextCount(1) - .verifyComplete(); + .doOnNext(System.out::println) + .as(StepVerifier::create) + .expectNextCount(1) + .verifyComplete(); } @@ -30,14 +35,16 @@ public void testRefreshToken() { RedisAccessTokenManager tokenManager = new RedisAccessTokenManager(RedisHelper.factory); SimpleAuthentication authentication = new SimpleAuthentication(); - + authentication.setUser(SimpleUser.builder() + .id("test") + .build()); tokenManager - .createAccessToken("test", authentication, false) - .zipWhen(token -> tokenManager.refreshAccessToken("test", token.getRefreshToken())) - .as(StepVerifier::create) - .expectNextMatches(tp2 -> { - return tp2.getT1().getRefreshToken().equals(tp2.getT2().getRefreshToken()); - }) + .createAccessToken("test", authentication, false) + .zipWhen(token -> tokenManager.refreshAccessToken("test", token.getRefreshToken())) + .as(StepVerifier::create) + .expectNextMatches(tp2 -> { + return tp2.getT1().getRefreshToken().equals(tp2.getT2().getRefreshToken()); + }) ; } @@ -47,16 +54,18 @@ public void testCreateSingletonAccessToken() { RedisAccessTokenManager tokenManager = new RedisAccessTokenManager(RedisHelper.factory); SimpleAuthentication authentication = new SimpleAuthentication(); - + authentication.setUser(SimpleUser.builder() + .id("test") + .build()); Flux - .concat(tokenManager - .createAccessToken("test", authentication, true), - tokenManager - .createAccessToken("test", authentication, true)) - .doOnNext(System.out::println) - .as(StepVerifier::create) - .expectNextCount(2) - .verifyComplete(); + .concat(tokenManager + .createAccessToken("test", authentication, true), + tokenManager + .createAccessToken("test", authentication, true)) + .doOnNext(System.out::println) + .as(StepVerifier::create) + .expectNextCount(2) + .verifyComplete(); } } \ No newline at end of file diff --git a/hsweb-authorization/pom.xml b/hsweb-authorization/pom.xml index 0e1e86fc6..44cfa8aef 100644 --- a/hsweb-authorization/pom.xml +++ b/hsweb-authorization/pom.xml @@ -5,10 +5,11 @@ hsweb-framework org.hswebframework.web - 4.0.16-SNAPSHOT + 4.0.20-SNAPSHOT 4.0.0 + ${artifactId} hsweb-authorization pom diff --git a/hsweb-commons/hsweb-commons-api/pom.xml b/hsweb-commons/hsweb-commons-api/pom.xml index 73cde322e..0d16a40a9 100644 --- a/hsweb-commons/hsweb-commons-api/pom.xml +++ b/hsweb-commons/hsweb-commons-api/pom.xml @@ -5,11 +5,12 @@ hsweb-commons org.hswebframework.web - 4.0.16-SNAPSHOT + 4.0.20-SNAPSHOT 4.0.0 hsweb-commons-api + ${artifactId} @@ -49,6 +50,11 @@ commons-codec + + org.springframework.boot + spring-boot-autoconfigure + + \ No newline at end of file diff --git a/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/EntityFactoryHolder.java b/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/EntityFactoryHolder.java index cd704e572..2f19399e0 100644 --- a/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/EntityFactoryHolder.java +++ b/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/EntityFactoryHolder.java @@ -19,6 +19,14 @@ public static EntityFactory get() { return FACTORY; } + + public static Class getMappedType(Class type) { + if (FACTORY != null) { + return FACTORY.getInstanceType(type); + } + return type; + } + public static T newInstance(Class type, Supplier mapper) { if (FACTORY != null) { diff --git a/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/EntityFactoryHolderConfiguration.java b/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/EntityFactoryHolderConfiguration.java index 84074bfcc..a39b2e534 100644 --- a/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/EntityFactoryHolderConfiguration.java +++ b/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/EntityFactoryHolderConfiguration.java @@ -1,11 +1,12 @@ package org.hswebframework.web.api.crud.entity; import org.springframework.beans.BeansException; +import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.context.ApplicationContextAware; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -@Configuration(proxyBeanMethods = false) +@AutoConfiguration public class EntityFactoryHolderConfiguration { diff --git a/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/ExtendableEntity.java b/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/ExtendableEntity.java new file mode 100644 index 000000000..146c99b18 --- /dev/null +++ b/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/ExtendableEntity.java @@ -0,0 +1,69 @@ +package org.hswebframework.web.api.crud.entity; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import lombok.Setter; +import org.hswebframework.ezorm.core.Extendable; + +import java.util.Collections; +import java.util.Map; + +/** + * 可扩展的实体类 + *

+ *

    + *
  • + * 实体类继承此类,或者实现{@link Extendable}接口. + *
  • + *
  • + * 使用{@link org.hswebframework.web.crud.configuration.TableMetadataCustomizer}自定义表结构 + *
  • + *
  • + * json序列化时,默认会将拓展字段平铺到json中. + *
  • + *
+ * + * @param 主键类型 + * @see JsonAnySetter + * @see JsonAnyGetter + * @since 4.0.18 + */ +@Getter +@Setter +public class ExtendableEntity extends GenericEntity implements Extendable { + + private Map extensions; + + /** + * 默认不序列化扩展属性,会由{@link ExtendableEntity#extensions()},{@link JsonAnyGetter}平铺到json中. + * + * @return 扩展属性 + */ + @JsonIgnore + public Map getExtensions() { + return extensions; + } + + @Override + @JsonAnyGetter + public Map extensions() { + return extensions == null ? Collections.emptyMap() : extensions; + } + + @Override + public Object getExtension(String property) { + Map ext = this.extensions; + return ext == null ? null : ext.get(property); + } + + @Override + @JsonAnySetter + public synchronized void setExtension(String property, Object value) { + if (extensions == null) { + extensions = new java.util.HashMap<>(); + } + extensions.put(property, value); + } +} diff --git a/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/ExtendableTreeSortSupportEntity.java b/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/ExtendableTreeSortSupportEntity.java new file mode 100644 index 000000000..b67580e47 --- /dev/null +++ b/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/ExtendableTreeSortSupportEntity.java @@ -0,0 +1,69 @@ +/* + * + * * Copyright 2020 http://www.hswebframework.org + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.hswebframework.web.api.crud.entity; + + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.validator.constraints.Length; +import org.hswebframework.ezorm.rdb.mapping.annotation.Comment; + +import javax.persistence.Column; + +/** + * 支持树形结构,排序的实体类,要使用树形结构,排序功能的实体类直接继承该类 + */ +@Getter +@Setter +public abstract class ExtendableTreeSortSupportEntity extends ExtendableEntity + implements TreeSortSupportEntity { + /** + * 父级类别 + */ + @Column(name = "parent_id", length = 64) + @Comment("父级ID") + @Schema(description = "父节点ID") + private PK parentId; + + /** + * 树结构编码,用于快速查找, 每一层由4位字符组成,用-分割 + * 如第一层:0001 第二层:0001-0001 第三层:0001-0001-0001 + */ + @Column(name = "path", length = 128) + @Comment("树路径") + @Schema(description = "树结构路径") + @Length(max = 128, message = "目录层级太深") + private String path; + + /** + * 排序索引 + */ + @Column(name = "sort_index", precision = 32) + @Comment("排序序号") + @Schema(description = "排序序号") + private Long sortIndex; + + @Column(name = "_level", precision = 32) + @Comment("树层级") + @Schema(description = "树层级") + private Integer level; + + +} \ No newline at end of file diff --git a/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/GenericEntity.java b/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/GenericEntity.java index f030c5df6..84dc9b87a 100644 --- a/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/GenericEntity.java +++ b/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/GenericEntity.java @@ -26,6 +26,7 @@ import javax.persistence.Column; import javax.persistence.GeneratedValue; import javax.persistence.Id; +import java.util.Map; /** diff --git a/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/GenericI18nEntity.java b/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/GenericI18nEntity.java new file mode 100644 index 000000000..40f60eaf9 --- /dev/null +++ b/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/GenericI18nEntity.java @@ -0,0 +1,43 @@ +package org.hswebframework.web.api.crud.entity; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import org.apache.commons.collections4.MapUtils; +import org.hswebframework.ezorm.rdb.mapping.annotation.ColumnType; +import org.hswebframework.ezorm.rdb.mapping.annotation.JsonCodec; +import org.hswebframework.web.i18n.I18nSupportEntity; + +import javax.persistence.Column; +import java.sql.JDBCType; +import java.util.Collections; +import java.util.Map; + +@Getter +@Setter +public class GenericI18nEntity extends GenericEntity implements I18nSupportEntity { + + /** + * map key为标识,如: name , description. value为国际化信息 + * + *
{@code
+     *   {
+     *       "name":{"zh":"名称","en":"name"},
+     *       "description":{"zh":"描述","en":"description"}
+     *   }
+     * }
+ */ + @Schema(title = "国际化信息定义") + @Column + @JsonCodec + @ColumnType(jdbcType = JDBCType.LONGVARCHAR, javaType = String.class) + private Map> i18nMessages; + + @Override + public Map getI18nMessages(String key) { + if (MapUtils.isEmpty(i18nMessages)) { + return Collections.emptyMap(); + } + return i18nMessages.getOrDefault(key, Collections.emptyMap()); + } +} diff --git a/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/PagerResult.java b/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/PagerResult.java index 9ff27518b..17992f567 100644 --- a/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/PagerResult.java +++ b/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/PagerResult.java @@ -24,18 +24,19 @@ import lombok.Setter; import org.hswebframework.ezorm.core.param.QueryParam; +import java.io.*; import java.util.ArrayList; import java.util.List; -import java.util.Map; /** * 分页查询结果,用于在分页查询时,定义查询结果.如果需要拓展此类,例如自定义json序列化,请使用spi方式定义拓展实现类型: - *
+ * 
{@code
  * ---resources
  * -----|--META-INF
  * -----|----services
  * -----|------org.hswebframework.web.api.crud.entity.PagerResult
- * 
+ * }
+ *

* * @param 结果类型 * @author zhouhao @@ -43,7 +44,7 @@ */ @Getter @Setter -public class PagerResult { +public class PagerResult implements Serializable { private static final long serialVersionUID = -6171751136953308027L; /** @@ -108,5 +109,4 @@ public PagerResult(int total, List data) { this.total = total; this.data = data; } - } diff --git a/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/QueryParamEntity.java b/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/QueryParamEntity.java index fadebf1d1..edaa3e755 100644 --- a/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/QueryParamEntity.java +++ b/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/QueryParamEntity.java @@ -1,13 +1,12 @@ package org.hswebframework.web.api.crud.entity; import io.swagger.v3.oas.annotations.Hidden; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.MapUtils; import org.hswebframework.ezorm.core.NestConditional; import org.hswebframework.ezorm.core.dsl.Query; import org.hswebframework.ezorm.core.param.Param; @@ -17,9 +16,10 @@ import org.hswebframework.web.bean.FastBeanCopier; import org.springframework.util.StringUtils; - +import javax.annotation.Nonnull; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.function.Consumer; @@ -45,26 +45,31 @@ * @see QueryParam * @since 3.0 */ +@Getter @Slf4j public class QueryParamEntity extends QueryParam { private static final long serialVersionUID = 8097500947924037523L; - @Getter @Schema(description = "where条件表达式,与terms参数不能共存.语法: name = 张三 and age > 16") private String where; - @Getter @Schema(description = "orderBy条件表达式,与sorts参数不能共存.语法: age asc,createTime desc") private String orderBy; //总数,设置了此值时,在分页查询的时候将不执行count. - @Getter @Setter @Schema(description = "设置了此值后将不重复执行count查询总数") private Integer total; + /** + * @see TermExpressionParser#parse(Map) + * @since 4.0.17 + */ @Getter + @Schema(description = "使用map方式传递查询条件.与terms参数不能共存.格式: {\"name$like\":\"张三\"}") + private Map filter; + @Setter @Schema(description = "是否进行并行分页") private boolean parallelPager = false; @@ -89,12 +94,14 @@ public int getPageIndexTmp() { @Override @Schema(description = "指定要查询的列") + @Nonnull public Set getIncludes() { return super.getIncludes(); } @Override @Schema(description = "指定不查询的列") + @Nonnull public Set getExcludes() { return super.getExcludes(); } @@ -199,7 +206,7 @@ public Query toNestQuery(Consumer filter) { + this.filter = filter; + if (MapUtils.isNotEmpty(filter)) { + setTerms(TermExpressionParser.parse(filter)); + } + } + @Override + @Nonnull public List getTerms() { List terms = super.getTerms(); if (CollectionUtils.isEmpty(terms) && StringUtils.hasText(where)) { setTerms(terms = TermExpressionParser.parse(where)); } + if (CollectionUtils.isEmpty(terms) && MapUtils.isNotEmpty(filter)) { + setTerms(terms = TermExpressionParser.parse(filter)); + } return terms; } + @SuppressWarnings("unchecked") public QueryParamEntity noPaging() { setPaging(false); return this; diff --git a/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/RecordModifierEntity.java b/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/RecordModifierEntity.java index 4811967a5..685758856 100644 --- a/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/RecordModifierEntity.java +++ b/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/RecordModifierEntity.java @@ -1,6 +1,8 @@ package org.hswebframework.web.api.crud.entity; import com.fasterxml.jackson.annotation.JsonIgnore; +import reactor.util.context.Context; +import reactor.util.context.ContextView; /** * 记录修改信息的实体类,包括修改人和修改时间。 @@ -63,4 +65,26 @@ default void setModifyTimeNow() { default String getModifierIdProperty() { return modifierId; } + + /** + * 标记不自动更新修改人相关内容 + * + * @param ctx 上下文 + * @return 上下文 + */ + static Context markDoNotUpdate(Context ctx) { + return ctx.put(RecordModifierEntity.class, true); + } + + /** + * 判断上下文是否不更新修改人相关内容 + * + * @param ctx 上下文 + * @return 上下文 + */ + static boolean isDoNotUpdate(ContextView ctx) { + return Boolean.TRUE.equals( + ctx.getOrDefault(RecordModifierEntity.class, false) + ); + } } diff --git a/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/TreeSupportEntity.java b/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/TreeSupportEntity.java index 9d01bca8f..c38a8e6b3 100644 --- a/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/TreeSupportEntity.java +++ b/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/TreeSupportEntity.java @@ -184,11 +184,6 @@ static , PK> void expandTree2List(T root, List, PK> void expandTree2List(T root, List queue = new LinkedList<>(); queue.add(root); //已经处理过的节点过滤器 - Set filter = new HashSet<>(); + Set filter = new HashSet<>(); for (T parent = queue.poll(); parent != null; parent = queue.poll()) { - long hash = System.identityHashCode(parent); - if (filter.contains(hash)) { + if (!filter.add(parent)) { continue; } - filter.add(hash); //处理子节点 if (!CollectionUtils.isEmpty(parent.getChildren())) { diff --git a/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/TreeUtils.java b/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/TreeUtils.java index fc0ca0093..2c3e7f123 100644 --- a/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/TreeUtils.java +++ b/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/TreeUtils.java @@ -1,6 +1,7 @@ package org.hswebframework.web.api.crud.entity; import com.google.common.collect.Maps; +import org.apache.commons.collections4.CollectionUtils; import org.springframework.util.ObjectUtils; import java.util.*; @@ -9,6 +10,51 @@ public class TreeUtils { + /** + * 树结构转为List + * + * @param nodeList List + * @param children 子节点获取函数 + * @param 节点类型 + * @return List + */ + public static List treeToList(Collection nodeList, + Function> children) { + List list = new ArrayList<>(nodeList.size()); + flatTree(nodeList, children, list::add); + return list; + } + + /** + * 平铺树结构 + * + * @param nodeList 树结构list + * @param children 子节点获取函数 + * @param handler 平铺节点接收函数 + * @param 节点类型 + */ + public static void flatTree(Collection nodeList, + Function> children, + Consumer handler) { + Queue queue = new LinkedList<>(nodeList); + Set distinct = new HashSet<>(); + + while (!queue.isEmpty()) { + N node = queue.poll(); + + if (!distinct.add(node)) { + continue; + } + + Collection childrenList = children.apply(node); + if (CollectionUtils.isNotEmpty(childrenList)) { + queue.addAll(childrenList); + } + + handler.accept(node); + } + + } /** * 列表结构转为树结构,并返回根节点集合. @@ -32,31 +78,34 @@ public static List list2tree(Collection dataList, (helper, node) -> { PK parentId = parentIdGetter.apply(node); return ObjectUtils.isEmpty(parentId) - || helper.getNode(parentId) == null; + || helper.getNode(parentId) == null; }); } /** * 列表结构转为树结构,并返回根节点集合 * - * @param dataList 数据集合 - * @param childConsumer 子节点消费接口,用于设置子节点 - * @param predicateFunction 根节点判断函数,传入helper,获取一个判断是否为跟节点的函数 - * @param 元素类型 - * @param 主键类型 + * @param dataList 数据集合 + * @param childConsumer 子节点消费接口,用于设置子节点 + * @param rootPredicate 根节点判断函数,传入helper,获取一个判断是否为根节点的函数 + * @param 元素类型 + * @param 主键类型 * @return 根节点集合 */ public static List list2tree(Collection dataList, Function idGetter, Function parentIdGetter, BiConsumer> childConsumer, - BiPredicate, N> predicateFunction) { + BiPredicate, N> rootPredicate) { Objects.requireNonNull(dataList, "source list can not be null"); Objects.requireNonNull(childConsumer, "child consumer can not be null"); - Objects.requireNonNull(predicateFunction, "root predicate function can not be null"); - + Objects.requireNonNull(rootPredicate, "root predicate function can not be null"); + int size = dataList.size(); + if (size == 0) { + return new ArrayList<>(0); + } // id,node - Map cache = Maps.newHashMapWithExpectedSize(dataList.size()); + Map cache = Maps.newLinkedHashMapWithExpectedSize(size); // parentId,children Map> treeCache = dataList .stream() @@ -76,13 +125,19 @@ public N getNode(PK id) { } }; - return dataList - .stream() - //设置每个节点的子节点 - .peek(node -> childConsumer.accept(node, treeCache.get(idGetter.apply(node)))) - //获取根节点 - .filter(node -> predicateFunction.test(helper, node)) - .collect(Collectors.toList()); + List list = new ArrayList<>(treeCache.size()); + + for (N node : cache.values()) { + //设置每个节点的子节点 + childConsumer.accept(node, treeCache.get(idGetter.apply(node))); + + //获取根节点 + if (rootPredicate.test(helper, node)) { + list.add(node); + } + } + return list; } + } diff --git a/hsweb-commons/hsweb-commons-api/src/main/resources/META-INF/spring.factories b/hsweb-commons/hsweb-commons-api/src/main/resources/META-INF/spring.factories deleted file mode 100644 index 90971163a..000000000 --- a/hsweb-commons/hsweb-commons-api/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1,3 +0,0 @@ -# Auto Configure -org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -org.hswebframework.web.api.crud.entity.EntityFactoryHolderConfiguration \ No newline at end of file diff --git a/hsweb-commons/hsweb-commons-api/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hsweb-commons/hsweb-commons-api/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..8e62f1085 --- /dev/null +++ b/hsweb-commons/hsweb-commons-api/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +org.hswebframework.web.api.crud.entity.EntityFactoryHolderConfiguration \ No newline at end of file diff --git a/hsweb-commons/hsweb-commons-api/src/test/java/org/hswebframework/web/api/crud/entity/ExtendableEntityTest.java b/hsweb-commons/hsweb-commons-api/src/test/java/org/hswebframework/web/api/crud/entity/ExtendableEntityTest.java new file mode 100644 index 000000000..82634d3f8 --- /dev/null +++ b/hsweb-commons/hsweb-commons-api/src/test/java/org/hswebframework/web/api/crud/entity/ExtendableEntityTest.java @@ -0,0 +1,33 @@ +package org.hswebframework.web.api.crud.entity; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.SneakyThrows; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class ExtendableEntityTest { + + + @Test + @SneakyThrows + public void testJson() { + ExtendableEntity entity = new ExtendableEntity<>(); + entity.setId("test"); + entity.setExtension("extName", "test"); + + ObjectMapper mapper = new ObjectMapper(); + + String json = mapper.writerFor(ExtendableEntity.class).writeValueAsString(entity); + + System.out.println(json); + ExtendableEntity decoded = mapper.readerFor(ExtendableEntity.class).readValue(json); + assertNotNull(decoded.getId()); + + assertEquals(entity.getId(), decoded.getId()); + + assertNotNull(decoded.getExtension("extName")); + + assertEquals(entity.getExtension("extName"), decoded.getExtension("extName")); + } +} \ No newline at end of file diff --git a/hsweb-commons/hsweb-commons-api/src/test/java/org/hswebframework/web/api/crud/entity/TreeUtilsTest.java b/hsweb-commons/hsweb-commons-api/src/test/java/org/hswebframework/web/api/crud/entity/TreeUtilsTest.java new file mode 100644 index 000000000..3f6fc995c --- /dev/null +++ b/hsweb-commons/hsweb-commons-api/src/test/java/org/hswebframework/web/api/crud/entity/TreeUtilsTest.java @@ -0,0 +1,80 @@ +package org.hswebframework.web.api.crud.entity; + +import com.google.common.collect.Collections2; +import lombok.Getter; +import lombok.Setter; +import org.apache.commons.collections4.CollectionUtils; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class TreeUtilsTest { + + + @Test + public void testTreeToList() { + + Node node1 = new Node(); + node1.setChildren(Arrays.asList(new Node(), new Node())); + + List nodes = TreeUtils.treeToList(Collections.singletonList(node1), + Node::getChildren); + + + assertNotNull(nodes); + assertEquals(3, nodes.size()); + + } + + @Test + public void testListToTree() { + int size = 5; + List nodes = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + Node node = new Node(); + node.setId(String.valueOf(i)); + node.setParenTId(i == 0 ? null : String.valueOf(i - 1)); + nodes.add(node); + } + // 打乱顺序 + Collections.shuffle(nodes); + + // 并发执行,并且创建新的节点 + List tree = TreeUtils + .list2tree(Collections2.transform(nodes, e -> { + Node copy = new Node(); + copy.setId(e.id); + copy.setParenTId(e.parenTId); + copy.setChildren(e.children); + return copy; + }), + Node::getId, + Node::getParenTId, + Node::setChildren, + // 自定义根节点判断 + (helper, e) -> "2".contains(e.getId())); + assertNotNull(tree); + Node children = tree.get(0); + assertNotNull(children); + while (CollectionUtils.isNotEmpty(children.getChildren())) { + children = children.getChildren().get(0); + } + assertEquals("4", children.getId()); + } + + @Getter + @Setter + static class Node { + private String id; + + private String parenTId; + + private List children; + } +} \ No newline at end of file diff --git a/hsweb-commons/hsweb-commons-crud/pom.xml b/hsweb-commons/hsweb-commons-crud/pom.xml index bb3c9e839..8869c7277 100644 --- a/hsweb-commons/hsweb-commons-crud/pom.xml +++ b/hsweb-commons/hsweb-commons-crud/pom.xml @@ -5,11 +5,12 @@ hsweb-commons org.hswebframework.web - 4.0.16-SNAPSHOT + 4.0.20-SNAPSHOT 4.0.0 hsweb-commons-crud + ${artifactId} diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/AutoDDLProcessor.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/AutoDDLProcessor.java index 626d0b470..65f4fd4eb 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/AutoDDLProcessor.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/AutoDDLProcessor.java @@ -4,8 +4,11 @@ import lombok.Setter; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import org.hswebframework.ezorm.rdb.executor.SqlRequest; +import org.hswebframework.ezorm.rdb.metadata.RDBSchemaMetadata; import org.hswebframework.ezorm.rdb.metadata.RDBTableMetadata; import org.hswebframework.ezorm.rdb.operator.DatabaseOperator; +import org.hswebframework.ezorm.rdb.operator.builder.fragments.ddl.CreateTableSqlBuilder; import org.hswebframework.web.api.crud.entity.EntityFactory; import org.hswebframework.web.crud.annotation.DDL; import org.hswebframework.web.crud.entity.factory.MapperEntityFactory; @@ -60,9 +63,9 @@ public void afterPropertiesSet() { Class type = entityFactory.getInstanceType(entity.getRealType(), true); DDL ddl = AnnotatedElementUtils.findMergedAnnotation(type, DDL.class); if (properties.isAutoDdl() && (ddl == null || ddl.value())) { - readyToDDL.add(type); + readyToDDL.add(entity.getEntityType()); } else { - nonDDL.add(type); + nonDDL.add(entity.getEntityType()); } } @@ -78,13 +81,13 @@ public void afterPropertiesSet() { return metadata; }) .flatMap(meta -> operator - .ddl() - .createOrAlter(meta) - .autoLoad(false) - .commit() - .reactive() - .subscribeOn(Schedulers.boundedElastic()), - 8) + .ddl() + .createOrAlter(meta) + .autoLoad(false) + .commit() + .reactive() + .subscribeOn(Schedulers.boundedElastic()), + 8,8) .doOnError((err) -> log.error(err.getMessage(), err)) .then() .block(Duration.ofMinutes(5)); @@ -110,10 +113,15 @@ public void afterPropertiesSet() { for (Class entity : nonDDL) { RDBTableMetadata metadata = resolver.resolve(entity); - operator - .getMetadata() - .getCurrentSchema() - .addTable(metadata); + RDBSchemaMetadata schema = metadata.getSchema(); + RDBTableMetadata table = schema + .getTable(metadata.getName()) + .orElse(null); + if (table == null) { + SqlRequest request = schema.findFeatureNow(CreateTableSqlBuilder.ID).build(metadata); + log.info("DDL SQL for {} \n{}", entity, request.toNativeSql()); + } + schema.addTable(metadata); } } } diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/CompositeEntityTableMetadataResolver.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/CompositeEntityTableMetadataResolver.java index c19e21585..6f496600c 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/CompositeEntityTableMetadataResolver.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/CompositeEntityTableMetadataResolver.java @@ -28,7 +28,8 @@ public RDBTableMetadata resolve(Class entityClass) { } private RDBTableMetadata doResolve(Class entityClass) { - return resolvers.stream() + return resolvers + .stream() .map(resolver -> resolver.parseTableMetadata(entityClass)) .filter(Optional::isPresent) .map(Optional::get) diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/DetectEntityColumnMapping.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/DetectEntityColumnMapping.java index d2ddc0bc6..26115ee33 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/DetectEntityColumnMapping.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/DetectEntityColumnMapping.java @@ -54,6 +54,11 @@ public TableOrViewMetadata getTable() { return mapping.getTable(); } + @Override + public void reload() { + mapping.reload(); + } + @Override public Object newInstance() { return entityFactory.newInstance(getEntityType()); diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/DialectProvider.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/DialectProvider.java new file mode 100644 index 000000000..02d6f0efd --- /dev/null +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/DialectProvider.java @@ -0,0 +1,57 @@ +package org.hswebframework.web.crud.configuration; + +import org.hswebframework.ezorm.rdb.metadata.RDBSchemaMetadata; +import org.hswebframework.ezorm.rdb.metadata.dialect.Dialect; + +/** + * 数据库方言提供商, 通过实现此接口拓展数据库方言. + *

+ * 实现此接口,并使用jdk SPI暴露实现. + *

{@code
+ *   META-INF/services/org.hswebframework.web.crud.configuration.DialectProvider
+ * }
+ * + * @author zhouhao + * @see java.util.ServiceLoader + * @since 4.0.17 + */ +public interface DialectProvider { + + /** + * 方言名称 + * + * @return 方言名称 + */ + String name(); + + /** + * 获取方言实例 + * + * @return 方言实例 + */ + Dialect getDialect(); + + /** + * 获取sql预编译参数绑定符号,如: ? + * + * @return 参数绑定符号 + */ + String getBindSymbol(); + + /** + * 创建一个schema + * + * @param name schema名称 + * @return schema + */ + RDBSchemaMetadata createSchema(String name); + + /** + * 获取验证连接的sql + * + * @return sql + */ + default String getValidationSql() { + return "select 1"; + } +} diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/DialectProviders.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/DialectProviders.java new file mode 100644 index 000000000..894e9c444 --- /dev/null +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/DialectProviders.java @@ -0,0 +1,37 @@ +package org.hswebframework.web.crud.configuration; + +import lombok.SneakyThrows; + +import java.util.*; + +public class DialectProviders { + private static final Map allSupportedDialect = new HashMap<>(); + + static { + for (EasyormProperties.DialectEnum value : EasyormProperties.DialectEnum.values()) { + allSupportedDialect.put(value.name(), value); + } + + for (DialectProvider dialectProvider : ServiceLoader.load(DialectProvider.class)) { + allSupportedDialect.put(dialectProvider.name(), dialectProvider); + } + } + + public static List all(){ + return new ArrayList<>(allSupportedDialect.values()); + } + + @SneakyThrows + public static DialectProvider lookup(String dialect) { + DialectProvider provider = allSupportedDialect.get(dialect); + if (provider == null) { + if (dialect.contains(".")) { + provider = (DialectProvider) Class.forName(dialect).newInstance(); + allSupportedDialect.put(dialect, provider); + } else { + throw new UnsupportedOperationException("unsupported dialect : " + dialect + ",all alive dialect :" + allSupportedDialect.keySet()); + } + } + return provider; + } +} diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/EasyormConfiguration.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/EasyormConfiguration.java index 4dc32b3e8..43069c441 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/EasyormConfiguration.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/EasyormConfiguration.java @@ -6,7 +6,6 @@ import org.hswebframework.ezorm.rdb.events.EventListener; import org.hswebframework.ezorm.rdb.executor.SyncSqlExecutor; import org.hswebframework.ezorm.rdb.executor.reactive.ReactiveSqlExecutor; -import org.hswebframework.ezorm.rdb.mapping.DefaultEntityColumnMapping; import org.hswebframework.ezorm.rdb.mapping.EntityColumnMapping; import org.hswebframework.ezorm.rdb.mapping.EntityManager; import org.hswebframework.ezorm.rdb.mapping.MappingFeatureType; @@ -25,42 +24,33 @@ import org.hswebframework.web.crud.entity.factory.MapperEntityFactory; import org.hswebframework.web.crud.events.*; import org.hswebframework.web.crud.events.expr.SpelSqlExpressionInvoker; -import org.hswebframework.web.crud.generator.CurrentTimeGenerator; -import org.hswebframework.web.crud.generator.DefaultIdGenerator; -import org.hswebframework.web.crud.generator.MD5Generator; -import org.hswebframework.web.crud.generator.SnowFlakeStringIdGenerator; +import org.hswebframework.web.crud.generator.*; import org.hswebframework.web.crud.query.DefaultQueryHelper; import org.hswebframework.web.crud.query.QueryHelper; import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.ObjectProvider; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanPostProcessor; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.Ordered; -import org.springframework.core.annotation.Order; import java.beans.PropertyDescriptor; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.time.Duration; -import java.util.List; import java.util.Optional; import java.util.Set; -@Configuration +@AutoConfiguration @EnableConfigurationProperties(EasyormProperties.class) @EnableEasyormRepository("org.hswebframework.web.**.entity") public class EasyormConfiguration { - @Autowired - private EasyormProperties properties; - static { } @@ -79,7 +69,8 @@ public EntityFactory entityFactory(ObjectProvider custo @ConditionalOnMissingBean @SuppressWarnings("all") public RDBDatabaseMetadata databaseMetadata(Optional syncSqlExecutor, - Optional reactiveSqlExecutor) { + Optional reactiveSqlExecutor, + EasyormProperties properties) { RDBDatabaseMetadata metadata = properties.createDatabaseMetadata(); syncSqlExecutor.ifPresent(metadata::addFeature); reactiveSqlExecutor.ifPresent(metadata::addFeature); @@ -164,6 +155,11 @@ public SnowFlakeStringIdGenerator snowFlakeStringIdGenerator() { return new SnowFlakeStringIdGenerator(); } + @Bean + public RandomIdGenerator randomIdGenerator() { + return new RandomIdGenerator(); + } + @Bean public CurrentTimeGenerator currentTimeGenerator() { return new CurrentTimeGenerator(); @@ -219,16 +215,17 @@ public Optional parseTableMetadata(Class entityType) { Class realType = factory.getInstanceType(entityType, true); Optional tableOpt = super.parseTableMetadata(realType); tableOpt.ifPresent(table -> { + EntityColumnMapping columnMapping = table.findFeatureNow( + MappingFeatureType.columnPropertyMapping.createFeatureId(realType) + ); if (realType != entityType) { - table.addFeature(new DetectEntityColumnMapping( - entityType, - table.findFeatureNow( - MappingFeatureType.columnPropertyMapping.createFeatureId(realType) - ), factory)); + table.addFeature(new DetectEntityColumnMapping(realType, columnMapping, factory)); + table.addFeature(columnMapping = new DetectEntityColumnMapping(entityType, columnMapping, factory)); } for (TableMetadataCustomizer customizer : customizers) { customizer.customTable(realType, table); } + columnMapping.reload(); }); return tableOpt; } diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/EasyormProperties.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/EasyormProperties.java index ee4e0d2b3..5f49d51c3 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/EasyormProperties.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/EasyormProperties.java @@ -1,9 +1,6 @@ package org.hswebframework.web.crud.configuration; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.Getter; -import lombok.SneakyThrows; +import lombok.*; import org.hswebframework.ezorm.rdb.metadata.RDBDatabaseMetadata; import org.hswebframework.ezorm.rdb.metadata.RDBSchemaMetadata; import org.hswebframework.ezorm.rdb.metadata.dialect.Dialect; @@ -14,15 +11,13 @@ import org.hswebframework.ezorm.rdb.supports.postgres.PostgresqlSchemaMetadata; import org.springframework.boot.context.properties.ConfigurationProperties; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; +import java.util.*; @ConfigurationProperties(prefix = "easyorm") @Data public class EasyormProperties { - private String defaultSchema="PUBLIC"; + private String defaultSchema = "PUBLIC"; private String[] schemas = {}; @@ -32,12 +27,22 @@ public class EasyormProperties { private boolean allowTypeAlter = true; - private DialectEnum dialect = DialectEnum.h2; + /** + * @see DialectProvider + */ + private DialectProvider dialect = DialectEnum.h2; + @Deprecated private Class dialectType; + @Deprecated private Class schemaType; + @SneakyThrows + public void setDialect(String dialect) { + this.dialect = DialectProviders.lookup(dialect); + } + public RDBDatabaseMetadata createDatabaseMetadata() { RDBDatabaseMetadata metadata = new RDBDatabaseMetadata(createDialect()); @@ -46,8 +51,8 @@ public RDBDatabaseMetadata createDatabaseMetadata() { schemaSet.add(defaultSchema); } schemaSet.stream() - .map(this::createSchema) - .forEach(metadata::addSchema); + .map(this::createSchema) + .forEach(metadata::addSchema); metadata.getSchema(defaultSchema) .ifPresent(metadata::setCurrentSchema); @@ -57,24 +62,17 @@ public RDBDatabaseMetadata createDatabaseMetadata() { @SneakyThrows public RDBSchemaMetadata createSchema(String name) { - if (schemaType == null) { - return dialect.createSchema(name); - } - return schemaType.getConstructor(String.class).newInstance(name); + return dialect.createSchema(name); } @SneakyThrows public Dialect createDialect() { - if (dialectType == null) { - return dialect.getDialect(); - } - - return dialectType.newInstance(); + return dialect.getDialect(); } @Getter @AllArgsConstructor - public enum DialectEnum { + public enum DialectEnum implements DialectProvider { mysql(Dialect.MYSQL, "?") { @Override public RDBSchemaMetadata createSchema(String name) { @@ -92,6 +90,11 @@ public RDBSchemaMetadata createSchema(String name) { public RDBSchemaMetadata createSchema(String name) { return new OracleSchemaMetadata(name); } + + @Override + public String getValidationSql() { + return "select 1 from dual"; + } }, postgres(Dialect.POSTGRES, "$") { @Override @@ -107,8 +110,8 @@ public RDBSchemaMetadata createSchema(String name) { }, ; - private Dialect dialect; - private String bindSymbol; + private final Dialect dialect; + private final String bindSymbol; public abstract RDBSchemaMetadata createSchema(String name); } diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/EasyormRepositoryRegistrar.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/EasyormRepositoryRegistrar.java index bd6575bb7..ba21b8c41 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/EasyormRepositoryRegistrar.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/EasyormRepositoryRegistrar.java @@ -17,6 +17,7 @@ import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; import org.springframework.context.index.CandidateComponentsIndex; import org.springframework.context.index.CandidateComponentsIndexLoader; +import org.springframework.core.GenericTypeResolver; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.io.Resource; @@ -27,12 +28,14 @@ import org.springframework.core.type.classreading.MetadataReader; import org.springframework.core.type.classreading.MetadataReaderFactory; import org.springframework.core.type.classreading.SimpleMetadataReaderFactory; +import org.springframework.util.ReflectionUtils; import javax.persistence.Table; import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -47,9 +50,9 @@ public class EasyormRepositoryRegistrar implements ImportBeanDefinitionRegistrar private String getResourceClassName(Resource resource) { try { return metadataReaderFactory - .getMetadataReader(resource) - .getClassMetadata() - .getClassName(); + .getMetadataReader(resource) + .getClassMetadata() + .getClassName(); } catch (IOException e) { return null; } @@ -58,8 +61,8 @@ private String getResourceClassName(Resource resource) { @SneakyThrows private Stream doGetResources(String packageStr) { String path = ResourcePatternResolver - .CLASSPATH_ALL_URL_PREFIX - .concat(packageStr.replace(".", "/")).concat("/**/*.class"); + .CLASSPATH_ALL_URL_PREFIX + .concat(packageStr.replace(".", "/")).concat("/**/*.class"); return Arrays.stream(resourcePatternResolver.getResources(path)); } @@ -67,16 +70,44 @@ protected Set scanEntities(String[] packageStr) { CandidateComponentsIndex index = CandidateComponentsIndexLoader.loadIndex(org.springframework.util.ClassUtils.getDefaultClassLoader()); if (null == index) { return Stream - .of(packageStr) - .flatMap(this::doGetResources) - .map(this::getResourceClassName) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); - } - return Stream .of(packageStr) - .flatMap(pkg -> index.getCandidateTypes(pkg, Table.class.getName()).stream()) + .flatMap(this::doGetResources) + .map(this::getResourceClassName) + .filter(Objects::nonNull) .collect(Collectors.toSet()); + } + return Stream + .of(packageStr) + .flatMap(pkg -> index.getCandidateTypes(pkg, Table.class.getName()).stream()) + .collect(Collectors.toSet()); + } + + private Class findIdType(Class entityType) { + Class idType; + try { + if (GenericEntity.class.isAssignableFrom(entityType)) { + return GenericTypeResolver.resolveTypeArgument(entityType, GenericEntity.class); + } + + Class[] ref = new Class[1]; + ReflectionUtils.doWithFields(entityType, field -> { + if (field.isAnnotationPresent(javax.persistence.Id.class)) { + ref[0] = field.getType(); + } + }); + idType = ref[0]; + + if (idType == null) { + Method getId = org.springframework.util.ClassUtils.getMethod(entityType, "getId"); + idType = getId.getReturnType(); + } + } catch (Throwable e) { + log.warn("unknown id type of entity:{}", entityType); + idType = String.class; + } + + return idType; + } @Override @@ -95,7 +126,7 @@ public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, B Class[] anno = (Class[]) attr.get("annotation"); - Set entityInfos = new HashSet<>(); + Set entityInfos = ConcurrentHashMap.newKeySet(); CandidateComponentsIndex index = CandidateComponentsIndexLoader.loadIndex(org.springframework.util.ClassUtils.getDefaultClassLoader()); for (String className : scanEntities(arr)) { Class entityType = org.springframework.util.ClassUtils.forName(className, null); @@ -106,18 +137,7 @@ public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, B Reactive reactive = AnnotationUtils.findAnnotation(entityType, Reactive.class); - Class idType = null; - try { - if (GenericEntity.class.isAssignableFrom(entityType)) { - idType = ClassUtils.getGenericType(entityType); - } - if (idType == null) { - Method getId = org.springframework.util.ClassUtils.getMethod(entityType, "getId"); - idType = getId.getReturnType(); - } - } catch (Exception e) { - idType = String.class; - } + Class idType = findIdType(entityType); EntityInfo entityInfo = new EntityInfo(entityType, entityType, @@ -134,7 +154,8 @@ public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, B Class idType = entityInfo.getIdType(); Class realType = entityInfo.getRealType(); if (entityInfo.isReactive()) { - log.trace("register ReactiveRepository<{},{}>", entityType.getName(), idType.getSimpleName()); + String beanName = entityType.getSimpleName().concat("ReactiveRepository"); + log.trace("Register bean ReactiveRepository<{},{}> {}", entityType.getName(), idType.getSimpleName(), beanName); ResolvableType repositoryType = ResolvableType.forClassWithGenerics(DefaultReactiveRepository.class, entityType, idType); @@ -142,25 +163,35 @@ public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, B definition.setTargetType(repositoryType); definition.setBeanClass(ReactiveRepositoryFactoryBean.class); definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); - definition.getPropertyValues().add("entityType", realType); - registry.registerBeanDefinition(realType.getSimpleName().concat("ReactiveRepository"), definition); + definition.getPropertyValues().add("entityType", entityType); + if (!registry.containsBeanDefinition(beanName)) { + registry.registerBeanDefinition(beanName, definition); + } else { + entityInfos.remove(entityInfo); + } } if (entityInfo.isNonReactive()) { - log.trace("register SyncRepository<{},{}>", entityType.getName(), idType.getSimpleName()); + String beanName = entityType.getSimpleName().concat("SyncRepository"); + log.trace("Register bean SyncRepository<{},{}> {}", entityType.getName(), idType.getSimpleName(), beanName); + ResolvableType repositoryType = ResolvableType.forClassWithGenerics(DefaultSyncRepository.class, entityType, idType); RootBeanDefinition definition = new RootBeanDefinition(); definition.setTargetType(repositoryType); definition.setBeanClass(SyncRepositoryFactoryBean.class); definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); - definition.getPropertyValues().add("entityType", realType); - registry.registerBeanDefinition(realType.getSimpleName().concat("SyncRepository"), definition); + definition.getPropertyValues().add("entityType", entityType); + if (!registry.containsBeanDefinition(beanName)) { + registry.registerBeanDefinition(beanName, definition); + } else { + entityInfos.remove(entityInfo); + } } } Map> group = entityInfos - .stream() - .collect(Collectors.groupingBy(EntityInfo::isReactive, Collectors.toSet())); + .stream() + .collect(Collectors.groupingBy(EntityInfo::isReactive, Collectors.toSet())); for (Map.Entry> entry : group.entrySet()) { RootBeanDefinition definition = new RootBeanDefinition(); diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/JdbcSqlExecutorConfiguration.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/JdbcSqlExecutorConfiguration.java index 3205e05dc..f7ebe67bd 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/JdbcSqlExecutorConfiguration.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/JdbcSqlExecutorConfiguration.java @@ -4,6 +4,7 @@ import org.hswebframework.ezorm.rdb.executor.reactive.ReactiveSqlExecutor; import org.hswebframework.web.crud.sql.DefaultJdbcExecutor; import org.hswebframework.web.crud.sql.DefaultJdbcReactiveExecutor; +import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -13,7 +14,7 @@ import javax.sql.DataSource; -@Configuration +@AutoConfiguration @AutoConfigureAfter(DataSourceAutoConfiguration.class) @ConditionalOnBean(DataSource.class) public class JdbcSqlExecutorConfiguration { diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/R2dbcSqlExecutorConfiguration.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/R2dbcSqlExecutorConfiguration.java index 5275b6075..a317a7a3c 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/R2dbcSqlExecutorConfiguration.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/R2dbcSqlExecutorConfiguration.java @@ -5,6 +5,7 @@ import org.hswebframework.ezorm.rdb.executor.reactive.ReactiveSqlExecutor; import org.hswebframework.ezorm.rdb.executor.reactive.ReactiveSyncSqlExecutor; import org.hswebframework.web.crud.sql.DefaultR2dbcExecutor; +import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -12,7 +13,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -@Configuration +@AutoConfiguration @AutoConfigureAfter(name = "org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration") @ConditionalOnBean(ConnectionFactory.class) public class R2dbcSqlExecutorConfiguration { diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/ReactiveRepositoryFactoryBean.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/ReactiveRepositoryFactoryBean.java index 291898a24..381f2f945 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/ReactiveRepositoryFactoryBean.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/ReactiveRepositoryFactoryBean.java @@ -4,6 +4,8 @@ import lombok.Setter; import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository; import org.hswebframework.ezorm.rdb.mapping.defaults.DefaultReactiveRepository; +import org.hswebframework.ezorm.rdb.metadata.RDBSchemaMetadata; +import org.hswebframework.ezorm.rdb.metadata.RDBTableMetadata; import org.hswebframework.ezorm.rdb.operator.DatabaseOperator; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.annotation.Autowired; @@ -11,7 +13,7 @@ @Getter @Setter public class ReactiveRepositoryFactoryBean - implements FactoryBean> { + implements FactoryBean> { @Autowired private DatabaseOperator operator; @@ -26,11 +28,12 @@ public class ReactiveRepositoryFactoryBean @Override public ReactiveRepository getObject() { - - return new DefaultReactiveRepository<>(operator, - resolver.resolve(entityType), - entityType, - wrapperFactory.getWrapper(entityType)); + RDBTableMetadata table = resolver.resolve(entityType); + return new DefaultReactiveRepository<>( + operator, + table.getName(), + entityType, + wrapperFactory.getWrapper(entityType)); } @Override diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/CreatorEventListener.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/CreatorEventListener.java index 54f1f14af..772ca00cb 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/CreatorEventListener.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/CreatorEventListener.java @@ -16,6 +16,8 @@ import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import reactor.core.publisher.Mono; +import reactor.util.context.Context; +import reactor.util.context.ContextView; import java.util.Collection; import java.util.List; @@ -23,6 +25,8 @@ import java.util.Optional; import java.util.function.Consumer; +import static org.springframework.data.repository.util.ClassUtils.ifPresent; + /** * 自动填充创建人和修改人信息 */ @@ -42,45 +46,50 @@ public String getName() { public void onEvent(EventType type, EventContext context) { Optional resultHolder = context.get(MappingContextKeys.reactiveResultHolder); if (type == MappingEventTypes.insert_before - || type == MappingEventTypes.save_before - || type == MappingEventTypes.update_before) { + || type == MappingEventTypes.save_before + || type == MappingEventTypes.update_before) { if (resultHolder.isPresent()) { - resultHolder - .ifPresent(holder -> holder - .before( - Authentication - .currentReactive() - .doOnNext(auth -> doApplyCreator(type, context, auth)) - .then() - )); + ReactiveResultHolder holder = resultHolder.get(); + + holder + .before( + Mono.deferContextual(ctx -> Authentication + .currentReactive() + .doOnNext(auth -> doApplyCreator(ctx, type, context, auth)) + .then()) + ); } else { Authentication - .current() - .ifPresent(auth -> doApplyCreator(type, context, auth)); + .current() + .ifPresent(auth -> doApplyCreator(Context.empty(), type, context, auth)); } } } - protected void doApplyCreator(EventType type, EventContext context, Authentication auth) { + protected void doApplyCreator(ContextView ctx, EventType type, EventContext context, Authentication auth) { Object instance = context.get(MappingContextKeys.instance).orElse(null); + boolean applyUpdate = !RecordModifierEntity.isDoNotUpdate(ctx); if (instance != null) { if (instance instanceof Collection) { - applyCreator(auth, context, ((Collection) instance), type != MappingEventTypes.update_before); + applyCreator(auth, context, ((Collection) instance), + type != MappingEventTypes.update_before,applyUpdate); } else { - applyCreator(auth, context, instance, type != MappingEventTypes.update_before); + applyCreator(auth, context, instance, + type != MappingEventTypes.update_before,applyUpdate); } } context - .get(MappingContextKeys.updateColumnInstance) - .ifPresent(map -> applyCreator(auth, context, map, type != MappingEventTypes.update_before)); + .get(MappingContextKeys.updateColumnInstance) + .ifPresent(map -> applyCreator(auth, context, map, type != MappingEventTypes.update_before,applyUpdate)); } public void applyCreator(Authentication auth, EventContext context, Object entity, - boolean updateCreator) { + boolean updateCreator, + boolean updateModifier) { long now = System.currentTimeMillis(); if (updateCreator) { if (entity instanceof RecordCreationEntity) { @@ -93,35 +102,40 @@ public void applyCreator(Authentication auth, e.setCreateTime(now); } } else if (entity instanceof Map) { - Map map = ((Map) entity); - map.putIfAbsent("creatorId", auth.getUser().getId()); - map.putIfAbsent("creatorName", auth.getUser().getName()); - map.putIfAbsent("createTime", now); + @SuppressWarnings("all") + Map map = ((Map) entity); + map.putIfAbsent("creator_id", auth.getUser().getId()); + map.putIfAbsent("creator_name", auth.getUser().getName()); + map.putIfAbsent("create_time", now); } } - if (entity instanceof RecordModifierEntity) { - RecordModifierEntity e = (RecordModifierEntity) entity; - if (ObjectUtils.isEmpty(e.getModifierId())) { - e.setModifierId(auth.getUser().getId()); - e.setModifierName(auth.getUser().getName()); - } - if (e.getModifyTime() == null) { - e.setModifyTime(now); - } - } else if (entity instanceof Map) { - Map map = ((Map) entity); - map.putIfAbsent("modifierId", auth.getUser().getId()); - map.putIfAbsent("modifierName", auth.getUser().getName()); - map.putIfAbsent("modifyTime", now); + if (updateModifier){ + if (entity instanceof RecordModifierEntity) { + RecordModifierEntity e = (RecordModifierEntity) entity; + if (ObjectUtils.isEmpty(e.getModifierId())) { + e.setModifierId(auth.getUser().getId()); + e.setModifierName(auth.getUser().getName()); + } + if (e.getModifyTime() == null) { + e.setModifyTime(now); + } + } else if (entity instanceof Map) { + @SuppressWarnings("all") + Map map = ((Map) entity); + map.putIfAbsent("modifier_id", auth.getUser().getId()); + map.putIfAbsent("modifier_name", auth.getUser().getName()); + map.putIfAbsent("modify_time", now); + } } + } - public void applyCreator(Authentication auth, EventContext context, Collection entities, boolean updateCreator) { + public void applyCreator(Authentication auth, EventContext context, Collection entities, boolean updateCreator,boolean updateModifier) { for (Object entity : entities) { - applyCreator(auth, context, entity, updateCreator); + applyCreator(auth, context, entity, updateCreator,updateModifier); } } diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/DefaultEntityEventListenerConfigure.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/DefaultEntityEventListenerConfigure.java index 04a6460b6..1a8ee5002 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/DefaultEntityEventListenerConfigure.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/DefaultEntityEventListenerConfigure.java @@ -73,20 +73,15 @@ protected void initByEntity(Class type, @Override public boolean isEnabled(Class entityType) { - if (!enabledFeatures.containsKey(entityType)) { - initByEntity(entityType, getOrCreateTypeMap(entityType, enabledFeatures), false); - } - return MapUtils.isNotEmpty(enabledFeatures.get(entityType)); + Map> enabled = initByEntityType(entityType); + return MapUtils.isNotEmpty(enabled); } @Override public boolean isEnabled(Class entityType, EntityEventType type, EntityEventPhase phase) { - if (!enabledFeatures.containsKey(entityType)) { - initByEntity(entityType, getOrCreateTypeMap(entityType, enabledFeatures), false); - } - Map> enabled = enabledFeatures.get(entityType); + Map> enabled = initByEntityType(entityType); if (MapUtils.isEmpty(enabled)) { return false; } @@ -102,4 +97,16 @@ public boolean isEnabled(Class entityType, return false; } + + private Map> initByEntityType(Class entityType) { + return enabledFeatures + .compute(entityType, (k, v) -> { + if (v != null) { + return v; + } + v = new EnumMap<>(EntityEventType.class); + initByEntity(k, v, false); + return v; + }); + } } diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityEventHelper.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityEventHelper.java index cd7d52ff1..8de961632 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityEventHelper.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityEventHelper.java @@ -35,6 +35,16 @@ public static Mono isDoFireEvent(boolean defaultIfEmpty) { .defaultIfEmpty(defaultIfEmpty); } + public static Mono tryFireEvent(Supplier> task) { + return Mono + .deferContextual(ctx -> { + if (Boolean.TRUE.equals(ctx.getOrDefault(doEventContextKey, true))) { + return task.get(); + } + return Mono.empty(); + }); + } + /** * 设置Mono不触发实体类事件 * @@ -95,6 +105,10 @@ public static Mono publishModifyEvent(Object source, List before, List after, Consumer>> publisher) { + //没有数据被更新则不触发事件 + if (before.isEmpty()) { + return Mono.empty(); + } return publishEvent(source, entityType, () -> new EntityModifyEvent<>(before, after, entityType), publisher); } diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityEventListener.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityEventListener.java index 3549bcb2d..8dae712ff 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityEventListener.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityEventListener.java @@ -1,10 +1,10 @@ package org.hswebframework.web.crud.events; -import lombok.AllArgsConstructor; import lombok.RequiredArgsConstructor; import lombok.Setter; -import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.MapUtils; import org.hswebframework.ezorm.core.GlobalConfig; import org.hswebframework.ezorm.core.param.QueryParam; import org.hswebframework.ezorm.rdb.events.EventListener; @@ -16,6 +16,7 @@ import org.hswebframework.ezorm.rdb.mapping.events.ReactiveResultHolder; import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata; import org.hswebframework.ezorm.rdb.operator.builder.fragments.NativeSql; +import org.hswebframework.ezorm.rdb.operator.dml.update.UpdateOperator; import org.hswebframework.web.api.crud.entity.Entity; import org.hswebframework.web.bean.FastBeanCopier; import org.hswebframework.web.event.AsyncEvent; @@ -32,7 +33,6 @@ import java.util.function.BiFunction; import java.util.function.Supplier; -import static org.hswebframework.web.crud.events.EntityEventHelper.isDoFireEvent; import static org.hswebframework.web.crud.events.EntityEventHelper.publishEvent; @SuppressWarnings("all") @@ -40,7 +40,10 @@ public class EntityEventListener implements EventListener, Ordered { public static final ContextKey> readyToDeleteContextKey = ContextKey.of("readyToDelete"); - public static final ContextKey> readyToUpdateContextKey = ContextKey.of("readyToUpdate"); + //更新前的数据 + public static final ContextKey> readyToUpdateBeforeContextKey = ContextKey.of("readyToUpdateBefore"); + //更新后的数据 + public static final ContextKey> readyToUpdateAfterContextKey = ContextKey.of("readyToUpdateAfter"); private final ApplicationEventPublisher eventPublisher; @@ -69,8 +72,8 @@ public void onEvent(EventType type, EventContext context) { Class entityType; if (mapping == null || - !Entity.class.isAssignableFrom(entityType = (Class) mapping.getEntityType()) || - !listenerConfigure.isEnabled(entityType)) { + !Entity.class.isAssignableFrom(entityType = (Class) mapping.getEntityType()) || + !listenerConfigure.isEnabled(entityType)) { return; } @@ -88,9 +91,9 @@ public void onEvent(EventType type, EventContext context) { EntityCreatedEvent::new); } else { handleBatchOperation(mapping.getEntityType(), - EntityEventType.save, + EntityEventType.create, context, - EntityPrepareSaveEvent::new, + EntityPrepareCreateEvent::new, EntityBeforeCreateEvent::new, EntityCreatedEvent::new); } @@ -130,9 +133,9 @@ protected void handleQueryBefore(EntityColumnMapping mapping, EventContext conte EntityBeforeQueryEvent event = new EntityBeforeQueryEvent<>(queryParam, mapping.getEntityType()); eventPublisher.publishEvent(new GenericsPayloadApplicationEvent<>(this, event, mapping.getEntityType())); holder - .before( - event.getAsync() - ); + .before( + event.getAsync() + ); }); }); } @@ -142,12 +145,12 @@ protected List createAfterData(List olds, List newValues = new ArrayList<>(olds.size()); EntityColumnMapping mapping = context - .get(MappingContextKeys.columnMapping) - .orElseThrow(UnsupportedOperationException::new); + .get(MappingContextKeys.columnMapping) + .orElseThrow(UnsupportedOperationException::new); Map columns = context - .get(MappingContextKeys.updateColumnInstance) - .orElse(Collections.emptyMap()); + .get(MappingContextKeys.updateColumnInstance) + .orElse(Collections.emptyMap()); for (Object old : olds) { Map oldMap = null; @@ -168,17 +171,17 @@ protected List createAfterData(List olds, //原生sql if (value instanceof NativeSql) { value = expressionInvoker == null ? null : expressionInvoker.invoke( - ((NativeSql) value), - mapping, - oldMap == null ? oldMap = createFullMapping(old, mapping) : oldMap); + ((NativeSql) value), + mapping, + oldMap == null ? oldMap = createFullMapping(old, mapping) : oldMap); if (value == null) { continue; } } GlobalConfig - .getPropertyOperator() - .setProperty(data, column.getAlias(), value); + .getPropertyOperator() + .setProperty(data, column.getAlias(), value); } newValues.add(data); @@ -218,72 +221,148 @@ protected Mono sendDeleteEvent(List olds, eventPublisher::publishEvent); } + // 回填修改后的字段到准备更新的数据中 + // 用于实现通过事件来修改即将被修改的数据 + protected void prepareUpdateInstance(List before, List after, EventContext ctx) { + Map instance = ctx + .get(MappingContextKeys.updateColumnInstance) + .orElse(null); + if (before.size() != 1 || after.size() != 1 || instance == null) { + //不支持一次性更新多条数据时设置. + return; + } + EntityColumnMapping mapping = ctx + .get(MappingContextKeys.columnMapping) + .orElseThrow(UnsupportedOperationException::new); + + Object afterEntity = after.get(0); + Object beforeEntity = before.get(0); + Map copy = new HashMap<>(instance); + + Map afterMap = FastBeanCopier.copy(afterEntity, new HashMap<>()); + Map beforeMap = FastBeanCopier.copy(beforeEntity, new HashMap<>()); + + //设置实体类中指定的字段值 + for (Map.Entry entry : afterMap.entrySet()) { + RDBColumnMetadata column = mapping.getColumnByProperty(entry.getKey()).orElse(null); + if (column == null || !column.isUpdatable()) { + continue; + } + + //原始值 + Object origin = copy.remove(column.getAlias()); + if (origin == null) { + origin = copy.remove(column.getName()); + } + //没有指定原始值,说明是通过事件指定的. + if (origin == null) { + //值相同忽略更新,可能是事件并没有修改这个字段. + if (Objects.equals(beforeMap.get(column.getAlias()), entry.getValue()) || + Objects.equals(beforeMap.get(column.getName()), entry.getValue())) { + continue; + } + } + + //按sql更新 忽略 + if (origin instanceof NativeSql) { + continue; + } + //设置新的值 + instance.put(column.getAlias(), entry.getValue()); + } + + DSLUpdate operator = ctx + .get(ContextKeys.>source()) + .orElse(null); + + if (operator != null && MapUtils.isNotEmpty(copy)) { + for (Map.Entry entry : copy.entrySet()) { + Object val = entry.getValue(); + if (val instanceof NullValue || val instanceof NativeSql) { + continue; + } + operator.excludes(entry.getKey()); + } + + } + + } + protected void handleUpdateBefore(DSLUpdate update, EventContext context) { Object repo = context.get(MappingContextKeys.repository).orElse(null); EntityColumnMapping mapping = context - .get(MappingContextKeys.columnMapping) - .orElseThrow(UnsupportedOperationException::new); + .get(MappingContextKeys.columnMapping) + .orElseThrow(UnsupportedOperationException::new); Class entityType = (Class) mapping.getEntityType(); if (repo instanceof ReactiveRepository) { + ReactiveResultHolder holder = context.get(MappingContextKeys.reactiveResultHolder).orElse(null); + if (holder != null) { + AtomicReference, List>> updated = new AtomicReference<>(); + //prepare + if (isEnabled(entityType, + EntityEventType.modify, + EntityEventPhase.prepare, + EntityEventPhase.before, + EntityEventPhase.after)) { + holder.before( + this.doAsyncEvent(() -> ((ReactiveRepository) repo) + .createQuery() + .setParam(update.toQueryParam()) + .fetch() + .collectList() + .flatMap((list) -> { + //没有数据被修改则不触发事件 + if (list.isEmpty()) { + return Mono.empty(); + } + List after = createAfterData(list, context); + updated.set(Tuples.of(list, after)); + context.set(readyToUpdateBeforeContextKey, list); + context.set(readyToUpdateAfterContextKey, after); + EntityPrepareModifyEvent event = new EntityPrepareModifyEvent(list, after, entityType); + + return sendUpdateEvent(list, + after, + entityType, + (_list, _after, _type) -> event) + .then(Mono.fromRunnable(() -> { + if (event.hasListener()) { + prepareUpdateInstance(list, after, context); + } + })); + + }).then()) + ); + } + //before + if (isEnabled(entityType, EntityEventType.modify, EntityEventPhase.before)) { + holder.invoke(this.doAsyncEvent(() -> { + Tuple2, List> _tmp = updated.get(); + if (_tmp != null) { + return sendUpdateEvent(_tmp.getT1(), + _tmp.getT2(), + entityType, + EntityBeforeModifyEvent::new); + } + return Mono.empty(); + })); + } - context.get(MappingContextKeys.reactiveResultHolder) - .ifPresent(holder -> { - AtomicReference, List>> updated = new AtomicReference<>(); - //prepare - if (isEnabled(entityType, - EntityEventType.modify, - EntityEventPhase.prepare, - EntityEventPhase.before, - EntityEventPhase.after)) { - holder.before( - this.doAsyncEvent(() -> ((ReactiveRepository) repo) - .createQuery() - .setParam(update.toQueryParam()) - .fetch() - .collectList() - .flatMap((list) -> { - List after = createAfterData(list, context); - updated.set(Tuples.of(list, after)); - return sendUpdateEvent(list, - after, - entityType, - EntityPrepareModifyEvent::new); - - }).then() - ) - ); - } - //before - if (isEnabled(entityType, EntityEventType.modify, EntityEventPhase.before)) { - holder.invoke(this.doAsyncEvent(() -> { - Tuple2, List> _tmp = updated.get(); - if (_tmp != null) { - - return sendUpdateEvent(_tmp.getT1(), - _tmp.getT2(), - entityType, - EntityBeforeModifyEvent::new); - } - return Mono.empty(); - })); - } - - //after - if (isEnabled(entityType, EntityEventType.modify, EntityEventPhase.after)) { - holder.after(v -> this - .doAsyncEvent(() -> { - Tuple2, List> _tmp = updated.getAndSet(null); - if (_tmp != null) { - return sendUpdateEvent(_tmp.getT1(), - _tmp.getT2(), - entityType, - EntityModifyEvent::new); - } - return Mono.empty(); - })); - } - - }); + //after + if (isEnabled(entityType, EntityEventType.modify, EntityEventPhase.after)) { + holder.after(v -> this + .doAsyncEvent(() -> { + Tuple2, List> _tmp = updated.getAndSet(null); + if (_tmp != null) { + return sendUpdateEvent(_tmp.getT1(), + _tmp.getT2(), + entityType, + EntityModifyEvent::new); + } + return Mono.empty(); + })); + } + } } else if (repo instanceof SyncRepository) { if (isEnabled(entityType, EntityEventType.modify, EntityEventPhase.before)) { QueryParam param = update.toQueryParam(); @@ -291,11 +370,14 @@ protected void handleUpdateBefore(DSLUpdate update, EventContext context) List list = syncRepository.createQuery() .setParam(param) .fetch(); + if (list.isEmpty()) { + return; + } sendUpdateEvent(list, createAfterData(list, context), (Class) mapping.getEntityType(), EntityBeforeModifyEvent::new) - .block(); + .block(); } } } @@ -310,8 +392,8 @@ protected void handleUpdateBefore(EventContext context) { protected void handleDeleteBefore(Class entityType, EventContext context) { EntityColumnMapping mapping = context - .get(MappingContextKeys.columnMapping) - .orElseThrow(UnsupportedOperationException::new); + .get(MappingContextKeys.columnMapping) + .orElseThrow(UnsupportedOperationException::new); context.get(ContextKeys.source()) .ifPresent(dslUpdate -> { Object repo = context.get(MappingContextKeys.repository).orElse(null); @@ -321,32 +403,32 @@ protected void handleDeleteBefore(Class entityType, EventContext context AtomicReference> deleted = new AtomicReference<>(); if (isEnabled(entityType, EntityEventType.delete, EntityEventPhase.before, EntityEventPhase.after)) { holder.before( - this.doAsyncEvent(() -> ((ReactiveRepository) repo) - .createQuery() - .setParam(dslUpdate.toQueryParam()) - .fetch() - .collectList() - .doOnNext(list -> { - context.set(readyToDeleteContextKey, list); - }) - .filter(CollectionUtils::isNotEmpty) - .flatMap(list -> { - deleted.set(list); - return this - .sendDeleteEvent(list, (Class) mapping.getEntityType(), EntityBeforeDeleteEvent::new); - }) - ) + this.doAsyncEvent(() -> ((ReactiveRepository) repo) + .createQuery() + .setParam(dslUpdate.toQueryParam()) + .fetch() + .collectList() + .doOnNext(list -> { + context.set(readyToDeleteContextKey, list); + }) + .filter(CollectionUtils::isNotEmpty) + .flatMap(list -> { + deleted.set(list); + return this + .sendDeleteEvent(list, (Class) mapping.getEntityType(), EntityBeforeDeleteEvent::new); + }) + ) ); } if (isEnabled(entityType, EntityEventType.delete, EntityEventPhase.after)) { holder.after(v -> this - .doAsyncEvent(() -> { - List _tmp = deleted.getAndSet(null); - if (CollectionUtils.isNotEmpty(_tmp)) { - return sendDeleteEvent(_tmp, (Class) mapping.getEntityType(), EntityDeletedEvent::new); - } - return Mono.empty(); - })); + .doAsyncEvent(() -> { + List _tmp = deleted.getAndSet(null); + if (CollectionUtils.isNotEmpty(_tmp)) { + return sendDeleteEvent(_tmp, (Class) mapping.getEntityType(), EntityDeletedEvent::new); + } + return Mono.empty(); + })); } }); @@ -373,56 +455,59 @@ protected void handleBatchOperation(Class clazz, BiFunction, Class, AsyncEvent> execute, BiFunction, Class, AsyncEvent> after) { - context.get(MappingContextKeys.instance) - .filter(List.class::isInstance) - .map(List.class::cast) - .ifPresent(lst -> { - AsyncEvent prepareEvent = before.apply(lst, clazz); - AsyncEvent afterEvent = after.apply(lst, clazz); - AsyncEvent beforeEvent = execute.apply(lst, clazz); - Object repo = context.get(MappingContextKeys.repository).orElse(null); - if (repo instanceof ReactiveRepository) { - Optional resultHolder = context.get(MappingContextKeys.reactiveResultHolder); - if (resultHolder.isPresent()) { - ReactiveResultHolder holder = resultHolder.get(); - if (null != prepareEvent && isEnabled(clazz, entityEventType, EntityEventPhase.prepare)) { - holder.before( - this.doAsyncEvent(() -> { - return publishEvent(this, - clazz, - () -> prepareEvent, - eventPublisher::publishEvent); - }) - ); - } + List lst = context.get(MappingContextKeys.instance) + .filter(List.class::isInstance) + .map(List.class::cast) + .orElse(null); + if (lst == null) { + return; + } - if (null != beforeEvent && isEnabled(clazz, entityEventType, EntityEventPhase.before)) { - holder.invoke( - this.doAsyncEvent(() -> { - return publishEvent(this, - clazz, - () -> beforeEvent, - eventPublisher::publishEvent); - }) - ); - } - if (null != afterEvent && isEnabled(clazz, entityEventType, EntityEventPhase.after)) { - holder.after(v -> { - return this.doAsyncEvent(() -> { - return publishEvent(this, - clazz, - () -> afterEvent, - eventPublisher::publishEvent); - }); - }); - } - return; - } - } - eventPublisher.publishEvent(new GenericsPayloadApplicationEvent<>(this, afterEvent, clazz)); - //block非响应式的支持 - afterEvent.getAsync().block(); - }); + AsyncEvent prepareEvent = before.apply(lst, clazz); + AsyncEvent afterEvent = after.apply(lst, clazz); + AsyncEvent beforeEvent = execute.apply(lst, clazz); + Object repo = context.get(MappingContextKeys.repository).orElse(null); + if (repo instanceof ReactiveRepository) { + Optional resultHolder = context.get(MappingContextKeys.reactiveResultHolder); + if (resultHolder.isPresent()) { + ReactiveResultHolder holder = resultHolder.get(); + if (null != prepareEvent && isEnabled(clazz, entityEventType, EntityEventPhase.prepare)) { + holder.before( + this.doAsyncEvent(() -> { + return publishEvent(this, + clazz, + () -> prepareEvent, + eventPublisher::publishEvent); + }) + ); + } + + if (null != beforeEvent && isEnabled(clazz, entityEventType, EntityEventPhase.before)) { + holder.invoke( + this.doAsyncEvent(() -> { + return publishEvent(this, + clazz, + () -> beforeEvent, + eventPublisher::publishEvent); + }) + ); + } + if (null != afterEvent && isEnabled(clazz, entityEventType, EntityEventPhase.after)) { + holder.after(v -> { + return this.doAsyncEvent(() -> { + return publishEvent(this, + clazz, + () -> afterEvent, + eventPublisher::publishEvent); + }); + }); + } + return; + } + } + eventPublisher.publishEvent(new GenericsPayloadApplicationEvent<>(this, afterEvent, clazz)); + //block非响应式的支持 + afterEvent.getAsync().block(); } boolean isEnabled(Class clazz, EntityEventType entityEventType, EntityEventPhase... phase) { @@ -455,23 +540,23 @@ protected void handleSingleOperation(Class clazz, ReactiveResultHolder holder = resultHolder.get(); if (null != prepareEvent && isEnabled(clazz, entityEventType, EntityEventPhase.prepare)) { holder.before( - this.doAsyncEvent(() -> { - return publishEvent(this, - clazz, - () -> prepareEvent, - eventPublisher::publishEvent); - }) + this.doAsyncEvent(() -> { + return publishEvent(this, + clazz, + () -> prepareEvent, + eventPublisher::publishEvent); + }) ); } if (null != beforeEvent && isEnabled(clazz, entityEventType, EntityEventPhase.before)) { holder.invoke( - this.doAsyncEvent(() -> { - return publishEvent(this, - clazz, - () -> beforeEvent, - eventPublisher::publishEvent); - }) + this.doAsyncEvent(() -> { + return publishEvent(this, + clazz, + () -> beforeEvent, + eventPublisher::publishEvent); + }) ); } if (null != afterEvent && isEnabled(clazz, entityEventType, EntityEventPhase.after)) { @@ -494,11 +579,7 @@ protected void handleSingleOperation(Class clazz, } protected Mono doAsyncEvent(Supplier> eventSupplier) { - return isDoFireEvent(true) - .filter(Boolean::booleanValue) - .flatMap(ignore -> { - return eventSupplier.get(); - }); + return EntityEventHelper.tryFireEvent(eventSupplier); } @Override diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/expr/AbstractSqlExpressionInvoker.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/expr/AbstractSqlExpressionInvoker.java index ef078803b..d235917b9 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/expr/AbstractSqlExpressionInvoker.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/expr/AbstractSqlExpressionInvoker.java @@ -3,6 +3,7 @@ import org.hswebframework.ezorm.rdb.mapping.EntityColumnMapping; import org.hswebframework.ezorm.rdb.operator.builder.fragments.NativeSql; import org.hswebframework.web.crud.events.SqlExpressionInvoker; +import reactor.function.Function3; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -10,16 +11,16 @@ public abstract class AbstractSqlExpressionInvoker implements SqlExpressionInvoker { - private final Map, Object>> compiled = + private final Map, Object>> compiled = new ConcurrentHashMap<>(); @Override public Object invoke(NativeSql sql, EntityColumnMapping mapping, Map object) { return compiled.computeIfAbsent(sql.getSql(), this::compile) - .apply(sql.getParameters(), object); + .apply(mapping,sql.getParameters(), object); } - protected abstract BiFunction, Object> compile(String sql); + protected abstract Function3, Object> compile(String sql); } diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/expr/SpelSqlExpressionInvoker.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/expr/SpelSqlExpressionInvoker.java index 291d87781..d038a8a8b 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/expr/SpelSqlExpressionInvoker.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/expr/SpelSqlExpressionInvoker.java @@ -2,6 +2,8 @@ import io.netty.util.concurrent.FastThreadLocal; import lombok.extern.slf4j.Slf4j; +import org.hswebframework.ezorm.rdb.mapping.EntityColumnMapping; +import org.hswebframework.web.crud.query.QueryHelperUtils; import org.springframework.context.expression.MapAccessor; import org.springframework.core.convert.TypeDescriptor; import org.springframework.expression.*; @@ -9,21 +11,37 @@ import org.springframework.expression.spel.support.ReflectiveMethodResolver; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.util.Assert; +import reactor.function.Function3; import javax.annotation.Nonnull; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.atomic.AtomicLong; -import java.util.function.BiFunction; @Slf4j public class SpelSqlExpressionInvoker extends AbstractSqlExpressionInvoker { protected static class SqlFunctions extends HashMap { - public SqlFunctions(Map map) { + private final EntityColumnMapping mapping; + + public SqlFunctions(EntityColumnMapping mapping, Map map) { super(map); + this.mapping = mapping; + } + + @Override + public Object get(Object key) { + Object val = super.get(key); + if (val == null) { + val = super.get(QueryHelperUtils.toHump(String.valueOf(key))); + } + if (val == null) { + val = mapping + .getPropertyByColumnName(String.valueOf(key)) + .map(super::get) + .orElse(null); + } + return val; } public String lower(Object str) { @@ -34,6 +52,10 @@ public String upper(Object str) { return String.valueOf(str).toUpperCase(); } + public Object ifnull(Object nullable, Object val) { + return nullable == null ? val : nullable; + } + public String substring(Object str, int start, int length) { return String.valueOf(str).substring(start, length); } @@ -93,7 +115,7 @@ public Object operate(@Nonnull Operation operation, Object leftOperand, Object r }; @Override - protected BiFunction, Object> compile(String sql) { + protected Function3, Object> compile(String sql) { StringBuilder builder = new StringBuilder(sql.length()); int argIndex = 0; @@ -111,11 +133,11 @@ protected BiFunction, Object> compile(String sql) Expression expression = parser.parseExpression(builder.toString()); AtomicLong errorCount = new AtomicLong(); - return (args, object) -> { + return (mapping, args, object) -> { if (errorCount.get() > 1024) { return null; } - object = createArguments(object); + object = createArguments(mapping, object); if (args != null && args.length != 0) { int index = 0; @@ -144,13 +166,13 @@ protected BiFunction, Object> compile(String sql) } } - protected SqlFunctions createArguments(Map args) { - return new SqlFunctions(args); + protected SqlFunctions createArguments(EntityColumnMapping mapping, Map args) { + return new SqlFunctions(mapping, args); } - protected BiFunction, Object> spelError(String sql, Throwable error) { + protected Function3, Object> spelError(String sql, Throwable error) { log.warn("create sql expression [{}] parser error", sql, error); - return (args, data) -> null; + return (mapping, args, data) -> null; } static ExtMapAccessor accessor = new ExtMapAccessor(); diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/exception/DatabaseExceptionAnalyzerReporter.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/exception/DatabaseExceptionAnalyzerReporter.java new file mode 100644 index 000000000..5ccf62594 --- /dev/null +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/exception/DatabaseExceptionAnalyzerReporter.java @@ -0,0 +1,69 @@ +package org.hswebframework.web.crud.exception; + +import lombok.extern.slf4j.Slf4j; +import org.hswebframework.web.crud.configuration.DialectProvider; +import org.hswebframework.web.crud.configuration.DialectProviders; +import org.hswebframework.web.exception.analyzer.ExceptionAnalyzerReporter; + +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +@Slf4j +public class DatabaseExceptionAnalyzerReporter extends ExceptionAnalyzerReporter { + + public DatabaseExceptionAnalyzerReporter() { + init(); + } + + void init() { + addSimpleReporter( + Pattern.compile("^Binding.*"), + error -> log + .warn(wrapLog("请在application.yml中正确配置`easyorm.dialect`,可选项为:{}"), + DialectProviders + .all() + .stream() + .map(DialectProvider::name) + .collect(Collectors.toList()) + , error)); + + addSimpleReporter( + Pattern.compile("^Unknown database.*"), + error -> log + .warn(wrapLog("请先手动创建数据库或者配置`easyorm.default-schema`,数据库名不能包含只能由`数字字母下划线`组成."), error)); + + addSimpleReporter( + Pattern.compile("^Timeout on blocking.*"), + error -> log + .warn(wrapLog("操作超时,请检查数据库连接是否正确,数据库是否能正常访问."), error)); + + + initForPgsql(); + + initRedis(); + } + + void initRedis(){ + addReporter( + err->err.getClass().getCanonicalName().contains("RedisConnectionException"), + error -> log + .warn(wrapLog("请检查redis连接配置."), error)); + } + + void initForPgsql() { + addSimpleReporter( + Pattern.compile(".*\\[3D000].*"), + error -> log + .warn(wrapLog("请先手动创建数据库,数据库名不能包含只能由`数字字母下划线`组成."), error)); + + addSimpleReporter( + Pattern.compile(".*\\[3F000].*"), + error -> log + .warn(wrapLog("请正确配置`easyorm.default-schema`为pgsql数据库中对应的schema."), error)); + + addReporter( + err->err.getClass().getCanonicalName().contains("PostgresConnectionException"), + error -> log + .warn(wrapLog("请检查数据库连接配置是否正确."), error)); + } +} diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/generator/Generators.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/generator/Generators.java index f1002ce64..e03e2facb 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/generator/Generators.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/generator/Generators.java @@ -23,5 +23,9 @@ public interface Generators { */ String CURRENT_TIME = "current_time"; + /** + * @see org.hswebframework.web.id.RandomIdGenerator + */ + String RANDOM = "random"; } diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/generator/RandomIdGenerator.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/generator/RandomIdGenerator.java new file mode 100644 index 000000000..f5b1dedfa --- /dev/null +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/generator/RandomIdGenerator.java @@ -0,0 +1,23 @@ +package org.hswebframework.web.crud.generator; + +import org.hswebframework.ezorm.core.DefaultValueGenerator; +import org.hswebframework.ezorm.core.RuntimeDefaultValue; +import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata; +import org.hswebframework.web.id.IDGenerator; + +public class RandomIdGenerator implements DefaultValueGenerator { + @Override + public String getSortId() { + return Generators.RANDOM; + } + + @Override + public RuntimeDefaultValue generate(RDBColumnMetadata metadata) { + return IDGenerator.RANDOM::generate; + } + + @Override + public String getName() { + return "Random"; + } +} diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/DefaultQueryHelper.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/DefaultQueryHelper.java index 58f817229..25f7d541c 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/DefaultQueryHelper.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/DefaultQueryHelper.java @@ -14,6 +14,7 @@ import org.hswebframework.ezorm.rdb.executor.wrapper.MapResultWrapper; import org.hswebframework.ezorm.rdb.executor.wrapper.ResultWrapper; import org.hswebframework.ezorm.rdb.executor.wrapper.ResultWrappers; +import org.hswebframework.ezorm.rdb.mapping.EntityPropertyDescriptor; import org.hswebframework.ezorm.rdb.mapping.defaults.record.DefaultRecord; import org.hswebframework.ezorm.rdb.mapping.defaults.record.Record; import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata; @@ -30,6 +31,8 @@ import org.hswebframework.ezorm.rdb.operator.dml.query.BuildParameterQueryOperator; import org.hswebframework.ezorm.rdb.operator.dml.query.Selects; import org.hswebframework.ezorm.rdb.operator.dml.query.SortOrder; +import org.hswebframework.ezorm.rdb.utils.PropertyUtils; +import org.hswebframework.web.api.crud.entity.EntityFactoryHolder; import org.hswebframework.web.api.crud.entity.PagerResult; import org.hswebframework.web.api.crud.entity.QueryParamEntity; import org.hswebframework.web.bean.FastBeanCopier; @@ -37,6 +40,7 @@ import org.slf4j.LoggerFactory; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -44,6 +48,7 @@ import reactor.util.context.ContextView; import javax.persistence.Table; +import java.lang.reflect.Field; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; @@ -61,7 +66,8 @@ public class DefaultQueryHelper implements QueryHelper { private final Map analyzerCaches = new ConcurrentHashMap<>(); - static final ResultWrapper countWrapper = ResultWrappers.column("_total", i -> ((Number) i).intValue()); + static final ResultWrapper countWrapper = + ResultWrappers.column("_total", i -> ((Number) i).intValue()); @Override public QueryAnalyzer analysis(String selectSql) { @@ -77,7 +83,10 @@ public NativeQuerySpec select(String sql, Object... args) { public NativeQuerySpec select(String sql, Supplier newInstance, Object... args) { - return new NativeQuerySpecImpl<>(this, sql, args, map -> FastBeanCopier.copy(map, newInstance), true); + NativeQuerySpecImpl impl = new NativeQuerySpecImpl<>( + this, sql, args, map -> FastBeanCopier.copy(map, newInstance), true); + impl.setMapBuilder(ToHumpMap::new); + return impl; } @Override @@ -98,22 +107,22 @@ TableOrViewMetadata getTable(Class type) { Table table = nameMapping.computeIfAbsent(type, this::parseTableName); if (StringUtils.hasText(table.schema())) { return database - .getMetadata() - .getSchema(table.schema()) - .flatMap(schema -> schema.getTableOrView(table.name(), false)) - .orElseThrow(() -> new UnsupportedOperationException("table [" + table.schema() + "." + table.name() + "] not found")); + .getMetadata() + .getSchema(table.schema()) + .flatMap(schema -> schema.getTableOrView(table.name(), false)) + .orElseThrow(() -> new UnsupportedOperationException("table [" + table.schema() + "." + table.name() + "] not found")); } return database - .getMetadata() - .getCurrentSchema() - .getTableOrView(table.name(), false) - .orElseThrow(() -> new UnsupportedOperationException("table [" + table.name() + "] not found")); + .getMetadata() + .getCurrentSchema() + .getTableOrView(table.name(), false) + .orElseThrow(() -> new UnsupportedOperationException("table [" + table.name() + "] not found")); } static RDBColumnMetadata getColumn(TableOrViewMetadata table, String column) { return table - .getColumn(column) - .orElseThrow(() -> new UnsupportedOperationException("column [" + column + "] not found in [" + table.getName() + "]")); + .getColumn(column) + .orElseThrow(() -> new UnsupportedOperationException("column [" + column + "] not found in [" + table.getName() + "]")); } Table parseTableName(Class type) { @@ -162,7 +171,10 @@ public void wrapColumn(ColumnWrapperContext> context) { QueryAnalyzer.Column col = analyzer.findColumn(column).orElse(null); if (col != null && !analyzer.columnIsExpression(column, context.getColumnIndex())) { - doWrap(instance, column, col.metadata.decode(context.getResult())); + Object val = col.metadata == null + ? getCodec().decode(context.getResult()) + : col.metadata.decode(context.getResult()); + doWrap(instance, column, val); } else { doWrap(instance, col == null ? QueryHelperUtils.toHump(column) : col.alias, getCodec().decode(context.getResult())); } @@ -181,12 +193,12 @@ public Mono count() { SqlRequest countSql = analyzer.refactorCount(param == null ? new QueryParamEntity() : param, args); return parent - .database - .sql() - .reactive() - .select(countSql, countWrapper) - .single(0) - .contextWrite(logContext); + .database + .sql() + .reactive() + .select(countSql, countWrapper) + .single(0) + .contextWrite(logContext); } @Override @@ -197,13 +209,27 @@ public ExecuteSpec where(QueryParamEntity param) { @Override public Flux fetch() { + QueryParamEntity _param = param == null ? QueryParamEntity.of().noPaging() : param; + SqlRequest request = analyzer.refactor(_param, args); + if (_param.isPaging()) { + request = createPagingSql(request, param.getPageIndex(), param.getPageSize()); + } return parent - .database - .sql() - .reactive() - .select(analyzer.refactor(param, args), this) - .map(mapper) - .contextWrite(logContext); + .database + .sql() + .reactive() + .select(request, this) + .map(mapper) + .contextWrite(logContext); + } + + @Override + public Flux fetch(int pageIndex, int pageSize) { + if (param == null) { + param = new QueryParamEntity(); + } + param.doPaging(pageIndex, pageSize); + return fetch(); } @Override @@ -211,24 +237,29 @@ public Mono> fetchPaged() { if (param == null) { return fetchPaged(0, 25); } - return fetchPaged(param.getPageIndex(), param.getPageSize()); + return fetchPaged(param); } private SqlRequest createPagingSql(SqlRequest request, int pageIndex, int pageSize) { PrepareSqlFragments sql = PrepareSqlFragments.of(request.getSql(), request.getParameters()); Paginator paginator = parent - .database - .getMetadata() - .getCurrentSchema() - .findFeatureNow(RDBFeatureType.paginator.getId()); + .database + .getMetadata() + .getCurrentSchema() + .findFeatureNow(RDBFeatureType.paginator.getId()); return paginator.doPaging(sql, pageIndex, pageSize).toRequest(); } @Override public Mono> fetchPaged(int pageIndex, int pageSize) { - QueryParamEntity param = this.param == null ? new QueryParamEntity().doPaging(pageIndex, pageSize) : this.param.clone(); + return fetchPaged(this.param == null + ? new QueryParamEntity().doPaging(pageIndex, pageSize) + : this.param.clone().doPaging(pageIndex, pageSize)); + } + + public Mono> fetchPaged(QueryParamEntity param) { SqlRequest listSql = analyzer.refactor(param, args); @@ -236,43 +267,43 @@ public Mono> fetchPaged(int pageIndex, int pageSize) { if (param.getTotal() != null) { return sqlExecutor - .select(createPagingSql(listSql, pageIndex, pageSize), this).map(mapper) - .collectList() - .map(list -> PagerResult.of(param.getTotal(), list, param)) - .contextWrite(logContext); + .select(createPagingSql(listSql, param.getPageIndex(), param.getPageSize()), this).map(mapper) + .collectList() + .map(list -> PagerResult.of(param.getTotal(), list, param)) + .contextWrite(logContext); } SqlRequest countSql = analyzer.refactorCount(param, args); if (param.isParallelPager()) { return Mono.zip(sqlExecutor - .select(countSql, countWrapper) - .single(0), + .select(countSql, countWrapper) + .single(0), sqlExecutor - .select(createPagingSql(listSql, param.getPageIndex(), param.getPageSize()), this) - .map(mapper) - .collectList(), + .select(createPagingSql(listSql, param.getPageIndex(), param.getPageSize()), this) + .map(mapper) + .collectList(), (total, list) -> PagerResult.of(total, list, param)) .contextWrite(logContext); } return sqlExecutor - .select(countSql, countWrapper) - .single(0) - .>flatMap(total -> { - if (total == 0) { - return Mono.just(PagerResult.of(0, new ArrayList<>(), param.clone())); - } - QueryParamEntity copy = param.clone(); - copy.rePaging(total); - return sqlExecutor - .select(createPagingSql(listSql, copy.getPageIndex(), copy.getPageSize()), this) - .map(mapper) - .collectList() - .map(list -> PagerResult.of(total, list, copy)); - - }) - .contextWrite(logContext); + .select(countSql, countWrapper) + .single(0) + .>flatMap(total -> { + QueryParamEntity copy = param.clone(); + copy.rePaging(total); + if (total == 0) { + return Mono.just(PagerResult.of(0, new ArrayList<>(), copy)); + } + return sqlExecutor + .select(createPagingSql(listSql, copy.getPageIndex(), copy.getPageSize()), this) + .map(mapper) + .collectList() + .map(list -> PagerResult.of(total, list, copy)); + + }) + .contextWrite(logContext); } } @@ -310,7 +341,11 @@ public All(QuerySpec parent, this.tableType = tableType; this.targetProperty = setter == null ? null : MethodReferenceConverter.convertToColumn(setter); if (this.targetProperty != null) { - propertyType = ResolvableType.forField(parent.clazz.getDeclaredField(targetProperty), parent.clazz); + Field field = ReflectionUtils.findField(parent.clazz, targetProperty); + if (field == null) { + throw new NoSuchFieldException(parent.clazz.getName() + "." + targetProperty); + } + propertyType = ResolvableType.forField(field, parent.clazz); } else { propertyType = null; } @@ -351,12 +386,12 @@ SelectColumnSupplier[] toColumns(TableOrViewMetadata table, String owner) { return table - .getColumns() - .stream() - .map(column -> Selects - .column(owner == null ? column.getName() : owner + "." + column.getName()) - .as(alias + "." + column.getAlias())) - .toArray(SelectColumnSupplier[]::new); + .getColumns() + .stream() + .map(column -> Selects + .column(owner == null ? column.getName() : owner + "." + column.getName()) + .as(alias + "." + column.getAlias())) + .toArray(SelectColumnSupplier[]::new); } JoinConditionalSpecImpl getJoin() { @@ -369,7 +404,7 @@ JoinConditionalSpecImpl getJoin() { @Override SelectColumnSupplier[] forSelect() { - if(propertyTypeIsCollection()){ + if (propertyTypeIsCollection()) { return new SelectColumnSupplier[0]; } //查询主表 @@ -422,7 +457,7 @@ void applyValue(R result, String[] column, Object sqlValue) { @Override SelectColumnSupplier[] forSelect() { this.alias = this.alias != null ? - this.alias : MethodReferenceConverter.convertToColumn(setter); + this.alias : MethodReferenceConverter.convertToColumn(setter); if (column != null) { String[] nestMaybe = column.split("[.]"); @@ -478,7 +513,7 @@ static class QuerySpec implements SelectSpec, FromSpec, SortSpec, Re private Function, Flux> resultHandler = Function.identity(); public QuerySpec(Class clazz, DefaultQueryHelper parent) { - this.clazz = clazz; + this.clazz = EntityFactoryHolder.getMappedType(clazz); this.parent = parent; logContext = Context.of(Logger.class, LoggerFactory.getLogger(clazz)); } @@ -515,9 +550,9 @@ private JoinConditionalSpecImpl getJoinByAlias(String alias) { @Override public FromSpec from(Class clazz) { query = parent - .database - .dml() - .query(table = parent.getTable(from = clazz)); + .database + .dml() + .query(table = parent.getTable(from = clazz)); return this; } @@ -537,75 +572,91 @@ public Mono count() { operator.getParameter().setPageSize(null); operator.getParameter().setOrderBy(new ArrayList<>()); return operator - .select(Selects.count1().as("_total")) - .fetch(countWrapper) - .reactive() - .single(0) - .contextWrite(logContext); + .select(Selects.count1().as("_total")) + .fetch(countWrapper) + .reactive() + .single(0) + .contextWrite(logContext); } @Override public Flux fetch() { return createQuery() - .fetch(this) - .reactive() - .contextWrite(logContext) - .as(resultHandler); + .fetch(this) + .reactive() + .contextWrite(logContext) + .as(resultHandler); + } + + @Override + public Flux fetch(int pageIndex, int pageSize) { + return createQuery() + .paging(pageIndex, pageSize) + .fetch(this) + .reactive() + .contextWrite(logContext) + .as(resultHandler); } @Override public Mono> fetchPaged() { if (param != null) { - return fetchPaged(param.getPageIndex(), param.getPageSize()); + return fetchPaged(param); } return fetchPaged(0, 25); } @Override public Mono> fetchPaged(int pageIndex, int pageSize) { + return fetchPaged(param != null + ? param.clone().doPaging(pageIndex, pageSize) + : new QueryParamEntity().doPaging(pageIndex, pageSize)); + } + + private Mono> fetchPaged(QueryParamEntity param) { - if (param != null && param.getTotal() != null) { + if (param.getTotal() != null) { return createQuery() - .paging(pageIndex, pageSize) - .fetch(this) - .reactive() - .as(resultHandler) - .collectList() - .map(list -> PagerResult.of(param.getTotal(), list, param)) - .contextWrite(logContext); + .paging(param.getPageIndex(), param.getPageSize()) + .fetch(this) + .reactive() + .as(resultHandler) + .collectList() + .map(list -> PagerResult.of(param.getTotal(), list, param)) + .contextWrite(logContext); } - if (param != null && param.isParallelPager()) { + if (param.isParallelPager()) { return Mono.zip(count(), createQuery() - .paging(pageIndex, pageSize) - .fetch(this) - .reactive() - .as(resultHandler) - .collectList(), + .paging(param.getPageIndex(), param.getPageSize()) + .fetch(this) + .reactive() + .as(resultHandler) + .collectList(), (total, list) -> PagerResult.of(total, list, param)) .contextWrite(logContext); } - QueryParamEntity copy = param != null ? param.clone() : new QueryParamEntity().doPaging(pageIndex, pageSize); return this - .count() - .flatMap(i -> { - if (i == 0) { - return Mono.just(PagerResult.of(0, new ArrayList<>(), copy)); - } - copy.rePaging(i); - return createQuery() - .paging(copy.getPageIndex(), copy.getPageSize()) - .fetch(this) - .reactive() - .as(resultHandler) - .collectList() - .map(list -> PagerResult.of(i, list, copy)) - .contextWrite(logContext); - }); + .count() + .flatMap(i -> { + QueryParamEntity copy = param.clone(); + copy.rePaging(i); + if (i == 0) { + return Mono.just(PagerResult.of(0, new ArrayList<>(), copy)); + } + return createQuery() + .paging(copy.getPageIndex(), copy.getPageSize()) + .fetch(this) + .reactive() + .as(resultHandler) + .collectList() + .map(list -> PagerResult.of(i, list, copy)) + .contextWrite(logContext); + }); } @Override @@ -649,11 +700,11 @@ public JoinSpec join(Class type, Query condition = QueryParamEntity.newQuery(); JoinConditionalSpecImpl spec = new JoinConditionalSpecImpl( - this, - type, - joinTable, - alias, - condition + this, + type, + joinTable, + alias, + condition ); joins().add(spec); @@ -704,7 +755,7 @@ public Joiner(List terms) { public void prepare(List terms) { for (Term term : terms) { if (Objects.equals(TermType.eq, term.getTermType()) - && term.getValue() instanceof JoinConditionalSpecImpl.ColumnRef) { + && term.getValue() instanceof JoinConditionalSpecImpl.ColumnRef) { joinTerms.add(term); } if (term.getTerms() != null) { @@ -720,18 +771,18 @@ private Function, Flux> buildHandler(JoinConditionalSpecImpl join, return buildBatchHandler(join, mapping); } return flux -> flux - .flatMap(data -> { - QueryParamEntity param = new QueryParamEntity(); - param.setTerms(refactorTerms(data)); - return parent - .select(join.mainClassSafe()) - .all(join.mainClass) - .from(join.mainClass) - .where(param.noPaging()) - .fetch() - .collectList() - .map(list -> FastBeanCopier.copy(Collections.singletonMap(mapping.targetProperty, list), data)); - }, 16); + .flatMap(data -> { + QueryParamEntity param = new QueryParamEntity(); + param.setTerms(refactorTerms(data)); + return parent + .select(join.mainClassSafe()) + .all(join.mainClass) + .from(join.mainClass) + .where(param.noPaging()) + .fetch() + .collectList() + .map(list -> FastBeanCopier.copy(Collections.singletonMap(mapping.targetProperty, list), data)); + }, 16); } private List refactorTerms(R main) { @@ -751,7 +802,7 @@ private List refactorTerms(List terms, R main) { private void refactorTerms(R main, Term term) { if (term.getValue() instanceof JoinConditionalSpecImpl.ColumnRef) { JoinConditionalSpecImpl.ColumnRef ref = (JoinConditionalSpecImpl.ColumnRef) term.getValue(); - String mainProperty = ref.getColumn().getAlias(); + String mainProperty = ref.getColumn().getAlias(); Object value = FastBeanCopier.getProperty(main, mainProperty); if (value == null) { @@ -772,26 +823,26 @@ private Function, Flux> buildBatchHandler(JoinConditionalSpecImpl joi String mainProperty = ref.getColumn().getAlias(); return flux -> QueryHelper - .combineOneToMany( - flux, - t -> FastBeanCopier.getProperty(t, mainProperty), - idList -> { - term.setColumn(joinProperty); - term.setTermType(TermType.in); - term.setValue(idList); - - QueryParamEntity param = new QueryParamEntity(); - param.setTerms(terms); - return parent - .select(join.mainClassSafe()) - .all(join.mainClass) - .from(join.mainClass) - .where(param.noPaging()) - .fetch(); - }, - r -> FastBeanCopier.getProperty(r, joinProperty), - (t, list) -> FastBeanCopier.copy(Collections.singletonMap(mapping.targetProperty, list), t) - ); + .combineOneToMany( + flux, + t -> FastBeanCopier.getProperty(t, mainProperty), + idList -> { + term.setColumn(joinProperty); + term.setTermType(TermType.in); + term.setValue(idList); + + QueryParamEntity param = new QueryParamEntity(); + param.setTerms(terms); + return parent + .select(join.mainClassSafe()) + .all(join.mainClass) + .from(join.mainClass) + .where(param.noPaging()) + .fetch(); + }, + r -> FastBeanCopier.getProperty(r, joinProperty), + (t, list) -> FastBeanCopier.copy(Collections.singletonMap(mapping.targetProperty, list), t) + ); } } @@ -821,10 +872,15 @@ public JoinSpec rightJoin(Class type, Consumer> return join(type, createJoinAlias(), JoinType.right, on); } + @SneakyThrows + public R newRowInstance0() { + return clazz.getConstructor().newInstance(); + } + @Override @SneakyThrows public R newRowInstance() { - return clazz.newInstance(); + return EntityFactoryHolder.newInstance(clazz, this::newRowInstance0); } @Override @@ -963,9 +1019,10 @@ static class JoinConditionalSpecImpl implements JoinConditionalSpec target; @SuppressWarnings("all") - private Class mainClassSafe(){ + private Class mainClassSafe() { return (Class) mainClass; } + @Override public JoinConditionalSpecImpl applyColumn(StaticMethodReferenceColumn mainColumn, String termType, @@ -1002,8 +1059,8 @@ public JoinConditionalSpecImpl applyColumn(String mainColumn, String column) { RDBColumnMetadata columnMetadata = join - .getColumn(column) - .orElseThrow(() -> new IllegalArgumentException("column [" + column + "] not found")); + .getColumn(column) + .orElseThrow(() -> new IllegalArgumentException("column [" + column + "] not found")); getAccepter().accept(mainColumn, termType, new ColumnRef(columnMetadata, alias)); @@ -1087,7 +1144,8 @@ public JoinConditionalSpecImpl alias(String alias) { } - static class JoinNestConditionalSpecImpl extends SimpleNestConditional implements JoinNestConditionalSpec { + static class JoinNestConditionalSpecImpl + extends SimpleNestConditional implements JoinNestConditionalSpec { final QuerySpec parent; private final Term term; @@ -1104,17 +1162,19 @@ public NestConditional accept(String column, String termType, Object value) { } @Override - public JoinNestConditionalSpec> nest() { + @SuppressWarnings("all") + public JoinNestConditionalSpecImpl nest() { return new JoinNestConditionalSpecImpl<>(parent, this, term.nest()); } @Override - public JoinNestConditionalSpec> orNest() { + @SuppressWarnings("all") + public JoinNestConditionalSpecImpl orNest() { return new JoinNestConditionalSpecImpl<>(parent, this, term.orNest()); } @Override - public T applyColumn(StaticMethodReferenceColumn joinColumn, + public JoinNestConditionalSpecImpl applyColumn(StaticMethodReferenceColumn joinColumn, String termType, String alias, StaticMethodReferenceColumn mainOrJoinColumn) { @@ -1136,26 +1196,26 @@ public T applyColumn(StaticMethodReferenceColumn joinColumn, } @Override - public T applyColumn(StaticMethodReferenceColumn mainColumn, + public JoinNestConditionalSpecImpl applyColumn(StaticMethodReferenceColumn mainColumn, String termType, StaticMethodReferenceColumn joinColumn) { return applyColumn(joinColumn, termType, null, joinColumn); } - public T applyColumn(String mainColumn, + public JoinNestConditionalSpecImpl applyColumn(String mainColumn, String termType, TableOrViewMetadata join, String alias, String column) { RDBColumnMetadata columnMetadata = join - .getColumn(column) - .orElseThrow(() -> new IllegalArgumentException("column [" + column + "] not found")); + .getColumn(column) + .orElseThrow(() -> new IllegalArgumentException("column [" + column + "] not found")); - getAccepter().accept(mainColumn, termType, new JoinConditionalSpecImpl.ColumnRef(columnMetadata,alias)); + getAccepter().accept(mainColumn, termType, new JoinConditionalSpecImpl.ColumnRef(columnMetadata, alias)); - return (T) this; + return this; } @Override diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/JoinNestConditionalSpec.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/JoinNestConditionalSpec.java index fbc7368ef..f1b7b5654 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/JoinNestConditionalSpec.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/JoinNestConditionalSpec.java @@ -1,19 +1,16 @@ package org.hswebframework.web.crud.query; import org.hswebframework.ezorm.core.NestConditional; -import org.hswebframework.ezorm.core.StaticMethodReferenceColumn; import org.hswebframework.ezorm.core.TermTypeConditionalSupport; -import org.hswebframework.ezorm.core.param.TermType; -import org.hswebframework.ezorm.rdb.operator.builder.fragments.term.AbstractTermFragmentBuilder; public interface JoinNestConditionalSpec - extends JoinOnSpec, NestConditional { + extends JoinOnSpec>, NestConditional { @Override - JoinNestConditionalSpec> nest(); + JoinNestConditionalSpec> nest(); @Override - JoinNestConditionalSpec> orNest(); + JoinNestConditionalSpec> orNest(); } diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/QueryAnalyzer.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/QueryAnalyzer.java index 7eb627d2e..d80733b1b 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/QueryAnalyzer.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/QueryAnalyzer.java @@ -3,15 +3,16 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.RequiredArgsConstructor; +import org.hswebframework.ezorm.core.FeatureId; +import org.hswebframework.ezorm.core.FeatureType; +import org.hswebframework.ezorm.core.meta.Feature; import org.hswebframework.ezorm.rdb.executor.SqlRequest; import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata; import org.hswebframework.ezorm.rdb.metadata.TableOrViewMetadata; import org.hswebframework.ezorm.rdb.operator.DatabaseOperator; import org.hswebframework.web.api.crud.entity.QueryParamEntity; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; @@ -100,12 +101,20 @@ class Select { final Table table; + public Select newSelectAlias(String alias) { + return new Select(columnList + .stream() + .map(col -> col.moveOwner(alias)) + .collect(Collectors.toList()), + table.newAlias(alias)); + } + public Map getColumns() { return columns == null - ? columns = columnList - .stream() - .collect(Collectors.toMap(Column::getAlias, Function.identity(), (a, b) -> b)) - : columns; + ? columns = columnList + .stream() + .collect(Collectors.toMap(Column::getAlias, Function.identity(), (a, b) -> b)) + : columns; } } @@ -115,11 +124,17 @@ class Table { final String alias; final TableOrViewMetadata metadata; + + public Table newAlias(String alias) { + return new Table(alias, metadata); + } } @AllArgsConstructor @Getter - class Column { + class Column implements Feature { + static final FeatureId FEATURE_ID = FeatureId.of("AnalyzedColumn"); + //列名 String name; //别名 @@ -128,6 +143,20 @@ class Column { String owner; //元数据信息 RDBColumnMetadata metadata; + + public Column moveOwner(String owner) { + return new Column(name, alias, owner, metadata); + } + + @Override + public String getId() { + return FEATURE_ID.getId(); + } + + @Override + public FeatureType getType() { + return AnalyzerFeatureType.AnalyzedCol; + } } class SelectTable extends Table { @@ -139,7 +168,41 @@ public SelectTable(String alias, super(alias, metadata); this.columns = columns; } + + @Override + public Table newAlias(String alias) { + return new SelectTable( + alias, + columns + .entrySet() + .stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + e -> e.getValue().moveOwner(alias), + (l, r) -> r, + LinkedHashMap::new + )) + , metadata); + } + + public Map getColumns() { + return Collections.unmodifiableMap(columns); + } } + enum AnalyzerFeatureType implements FeatureType { + AnalyzedCol; + + @Override + public String getId() { + return name(); + } + + @Override + public String getName() { + return name(); + } + } + } diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/QueryAnalyzerImpl.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/QueryAnalyzerImpl.java index 38c333cb9..cca2fb1a2 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/QueryAnalyzerImpl.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/QueryAnalyzerImpl.java @@ -2,11 +2,10 @@ import lombok.Getter; import lombok.SneakyThrows; -import net.sf.jsqlparser.expression.Alias; -import net.sf.jsqlparser.expression.Expression; -import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; -import net.sf.jsqlparser.expression.JdbcParameter; +import net.sf.jsqlparser.expression.*; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.select.*; import net.sf.jsqlparser.statement.values.ValuesStatement; import org.apache.commons.collections4.CollectionUtils; @@ -14,14 +13,10 @@ import org.hswebframework.ezorm.core.param.Sort; import org.hswebframework.ezorm.core.param.Term; import org.hswebframework.ezorm.rdb.executor.SqlRequest; -import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata; -import org.hswebframework.ezorm.rdb.metadata.RDBSchemaMetadata; +import org.hswebframework.ezorm.rdb.metadata.*; import org.hswebframework.ezorm.rdb.metadata.dialect.Dialect; import org.hswebframework.ezorm.rdb.operator.DatabaseOperator; -import org.hswebframework.ezorm.rdb.operator.builder.fragments.AbstractTermsFragmentBuilder; -import org.hswebframework.ezorm.rdb.operator.builder.fragments.EmptySqlFragments; -import org.hswebframework.ezorm.rdb.operator.builder.fragments.PrepareSqlFragments; -import org.hswebframework.ezorm.rdb.operator.builder.fragments.SqlFragments; +import org.hswebframework.ezorm.rdb.operator.builder.fragments.*; import org.hswebframework.web.api.crud.entity.QueryParamEntity; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -44,10 +39,13 @@ class QueryAnalyzerImpl implements FromItemVisitor, SelectItemVisitor, SelectVis private final Map joins = new LinkedHashMap<>(); + private final List withItems = new ArrayList<>(); private QueryRefactor injector; private volatile Map columnMappings; + private final Map virtualTable = new HashMap<>(); + @Override public String originalSql() { return sql; @@ -104,12 +102,31 @@ private Map getColumnMappings() { synchronized (this) { if (columnMappings == null) { columnMappings = new HashMap<>(); + if (select.table instanceof SelectTable) { + + for (Map.Entry entry : + ((SelectTable) select.getTable()).getColumns().entrySet()) { + Column column = entry.getValue(); + Column col = new Column(column.getName(), column.getAlias(), select.table.alias, column.metadata); + columnMappings.put(entry.getKey(), col); + columnMappings.put(select.table.alias + "." + entry.getKey(), col); + + if (!(column instanceof ExpressionColumn) && column.metadata != null) { + columnMappings.put(column.metadata.getName(), col); + columnMappings.put(select.table.alias + "." + column.metadata.getName(), col); + columnMappings.put(column.metadata.getAlias(), col); + columnMappings.put(select.table.alias + "." + column.metadata.getAlias(), col); + } + } + for (Column column : select.getColumnList()) { columnMappings.put(column.getName(), column); columnMappings.put(column.getAlias(), column); - columnMappings.put(column.getOwner() + "." + column.getName(), column); - columnMappings.put(column.getOwner() + "." + column.getAlias(), column); + if (null != column.getOwner()) { + columnMappings.put(column.getOwner() + "." + column.getName(), column); + columnMappings.put(column.getOwner() + "." + column.getAlias(), column); + } } } else { // 主表 @@ -164,12 +181,13 @@ private Column getColumnOrSelectColumn(String name) { } @SneakyThrows - private static SelectBody parse(String sql) { - return ((net.sf.jsqlparser.statement.select.Select) CCJSqlParserUtil.parse(sql)).getSelectBody(); + private static net.sf.jsqlparser.statement.select.Select parse(String sql) { + return ((net.sf.jsqlparser.statement.select.Select) CCJSqlParserUtil.parse(sql)); } - QueryAnalyzerImpl(DatabaseOperator database, SelectBody selectBody) { + QueryAnalyzerImpl(DatabaseOperator database, SelectBody selectBody, QueryAnalyzerImpl parent) { this.database = database; + this.virtualTable.putAll(parent.virtualTable); if (null != selectBody) { this.parsed = selectBody; selectBody.accept(this); @@ -178,14 +196,44 @@ private static SelectBody parse(String sql) { } } + QueryAnalyzerImpl(DatabaseOperator database, SubSelect select, QueryAnalyzerImpl parent) { + this.parsed = select.getSelectBody(); + this.database = database; + this.virtualTable.putAll(parent.virtualTable); + //with ... + if (CollectionUtils.isNotEmpty(select.getWithItemsList())) { + for (WithItem withItem : select.getWithItemsList()) { + withItem.accept(this); + } + } + if (this.parsed != null) { + this.parsed.accept(this); + } + } + + QueryAnalyzerImpl(DatabaseOperator database, net.sf.jsqlparser.statement.select.Select select) { + this.parsed = select.getSelectBody(); + this.database = database; + //with ... + if (CollectionUtils.isNotEmpty(select.getWithItemsList())) { + for (WithItem withItem : select.getWithItemsList()) { + withItem.accept(this); + } + } + + if (this.parsed != null) { + this.parsed.accept(this); + } + } + private String parsePlainName(String name) { - if (name == null || name.length() == 0) { + if (name == null || name.isEmpty()) { return null; } char firstChar = name.charAt(0); if (firstChar == '`' || firstChar == '"' || firstChar == '[' || - name.startsWith(database.getMetadata().getDialect().getQuoteStart())) { + name.startsWith(database.getMetadata().getDialect().getQuoteStart())) { return new String(name.toCharArray(), 1, name.length() - 2); } @@ -196,23 +244,35 @@ private String parsePlainName(String name) { @Override public void visit(net.sf.jsqlparser.schema.Table tableName) { String schema = parsePlainName(tableName.getSchemaName()); + + String name = parsePlainName(tableName.getName()); + RDBSchemaMetadata schemaMetadata; if (schema != null) { schemaMetadata = database - .getMetadata() - .getSchema(schema) - .orElseThrow(() -> new IllegalStateException("schema " + schema + " not initialized")); + .getMetadata() + .getSchema(schema) + .orElseThrow(() -> new IllegalStateException("schema " + schema + " not initialized")); } else { schemaMetadata = database.getMetadata().getCurrentSchema(); + if (!virtualTable.containsKey(name)) { + tableName.setSchemaName(schemaMetadata.getQuoteName()); + } } String alias = tableName.getAlias() == null ? tableName.getName() : tableName.getAlias().getName(); + TableOrViewMetadata tableMetadata = schemaMetadata + .getTableOrView(name, false) + .orElseGet(() -> virtualTable.get(name)); + + if (tableMetadata == null) { + throw new IllegalStateException("table or view " + tableName.getName() + " not found in " + schemaMetadata.getName()); + } + tableName.setName(tableMetadata.getRealName()); QueryAnalyzer.Table table = new QueryAnalyzer.Table( - parsePlainName(alias), - schemaMetadata - .getTableOrView(parsePlainName(tableName.getName()), false) - .orElseThrow(() -> new IllegalStateException("table or view " + tableName.getName() + " not found in " + schemaMetadata.getName())) + parsePlainName(alias), + tableMetadata ); select = new QueryAnalyzer.Select(new ArrayList<>(), table); @@ -222,24 +282,26 @@ public void visit(net.sf.jsqlparser.schema.Table tableName) { // select * from ( select a,b,c from table ) t @Override public void visit(SubSelect subSelect) { - SelectBody body = subSelect.getSelectBody(); - QueryAnalyzerImpl sub = new QueryAnalyzerImpl(database, body); - String alias = subSelect.getAlias() == null ? null : subSelect.getAlias().getName(); + visit(subSelect, subSelect.getAlias() == null ? null : subSelect.getAlias().getName()); + } + public void visit(SubSelect subSelect, String alias) { + SelectBody body = subSelect.getSelectBody(); + QueryAnalyzerImpl sub = new QueryAnalyzerImpl(database, body, this); Map columnMap = new LinkedHashMap<>(); for (Column column : sub.select.getColumnList()) { columnMap.put(column.getAlias(), - new Column(column.alias, column.getAlias(), column.owner, column.metadata)); + new Column(column.name, column.getAlias(), column.owner, column.metadata)); } select = new QueryAnalyzer.Select( - new ArrayList<>(), - new QueryAnalyzer.SelectTable( - parsePlainName(alias), - columnMap, - sub.select.table.metadata - ) + new ArrayList<>(), + new QueryAnalyzer.SelectTable( + parsePlainName(alias), + columnMap, + sub.select.table.metadata + ) ); } @@ -252,22 +314,66 @@ public void visit(SubJoin subjoin) { @Override public void visit(LateralSubSelect lateralSubSelect) { - + this.visit(lateralSubSelect.getSubSelect(), + lateralSubSelect.getAlias() == null ? null : lateralSubSelect.getAlias().getName()); } @Override public void visit(ValuesList valuesList) { + if (valuesList.getAlias() == null) { + throw new IllegalArgumentException("valuesList[" + valuesList + "] must have alias"); + } + String name = parsePlainName(valuesList.getAlias().getName()); + FakeTable view = new FakeTable(); + if (valuesList.getColumnNames() != null) { + //获取会自动创建列 + for (String columnName : valuesList.getColumnNames()) { + RDBColumnMetadata ignore = view.getColumn(parsePlainName(columnName)).orElse(null); + } + } + + if (valuesList.getAlias().getAliasColumns() != null) { + for (Alias.AliasColumn alias : valuesList.getAlias().getAliasColumns()) { + RDBColumnMetadata ignore = view.getColumn(parsePlainName(alias.name)).orElse(null); + } + } + view.setName(name); + view.setRealName(name); + view.setSchema(database.getMetadata().getCurrentSchema()); + view.setAlias(name); + + Table table = new Table(name, view); + + select = new QueryAnalyzer.Select(new ArrayList<>(), table); } @Override public void visit(TableFunction tableFunction) { + if (tableFunction.getAlias() == null) { + throw new IllegalArgumentException("table function[" + tableFunction + "] must have alias"); + } + String name = parsePlainName(tableFunction.getAlias().getName()); + + FakeTable view = new FakeTable(); + + view.setName(name); + view.setSchema(database.getMetadata().getCurrentSchema()); + view.setAlias(name); + + Table table = new Table(name, view); + + select = new QueryAnalyzer.Select(new ArrayList<>(), table); } @Override public void visit(ParenthesisFromItem aThis) { - + aThis.getFromItem().accept(this); + String alias = parsePlainName(aThis.getAlias() == null ? null : aThis.getAlias().getName()); + if (alias != null) { + this.select = select.newSelectAlias(alias); + } } @Override @@ -287,10 +393,10 @@ private void putSelectColumns(QueryAnalyzer.Table table, List public SqlFragments createTermFragments(QueryAnalyzerImpl impl, Term term) { Dialect dialect = impl.database.getMetadata().getDialect(); - Table table; + Table table = impl.select.table; String column = term.getColumn(); Column col = impl.getColumnMappings().get(column); - +// +// if (col == null) { +// if (column.contains(".")) { +// String[] split = column.split("\\."); +// if (split.length == 2) { +// QueryAnalyzer.Join join = impl.joins.get(split[0]); +// if (null != join) { +// table = join.table; +// column = split[1]; +// } else { +// throw new IllegalArgumentException("undefined column [" + column + "]"); +// } +// } +// } +// RDBColumnMetadata columnMetadata = table +// .getMetadata() +// .getColumn(column) +// .orElse(null); +// if (columnMetadata != null) { +// col = new Column(column, column, table.alias, columnMetadata); +// } else { +// throw new IllegalArgumentException("undefined column [" + column + "]"); +// } +// } if (col == null) { throw new IllegalArgumentException("undefined column [" + column + "]"); } - if (Objects.equals(impl.select.table.alias, col.getOwner())) { - table = impl.select.table; - } else { + if (!Objects.equals(impl.select.table.alias, col.getOwner())) { QueryAnalyzer.Join join = impl.joins.get(col.getOwner()); if (null != join) { table = join.table; @@ -512,17 +670,24 @@ public SqlFragments createTermFragments(QueryAnalyzerImpl impl, Term term) { if (col.metadata == null) { metadata = table.metadata; } + + String colName = col.metadata != null ? col.metadata.getRealName() : col.name; + String fullName = col.metadata != null + ? col.getMetadata().getFullName(table.alias) + : table.alias + "." + dialect.quote(colName, false); + return metadata - .findFeature(createFeatureId(term.getTermType())) - .map(feature -> feature.createFragments( - table.alias + "." + dialect.quote(col.name, col.metadata != null), col.metadata, term)) - .orElse(EmptySqlFragments.INSTANCE); + .findFeature(createFeatureId(term.getTermType())) + .map(feature -> feature.createFragments( + fullName, col.metadata, term)) + .orElse(EmptySqlFragments.INSTANCE); } } static QueryAnalyzerTermsFragmentBuilder TERMS_BUILDER = new QueryAnalyzerTermsFragmentBuilder(); class SimpleQueryRefactor implements QueryRefactor, SelectVisitor { + private String prefix = ""; private String from; private String columns; @@ -536,6 +701,8 @@ class SimpleQueryRefactor implements QueryRefactor, SelectVisitor { private boolean fastCount = true; + private SqlFragments QUERY, SUFFIX, FAST_COUNT, SLOW_COUNT; + SimpleQueryRefactor() { } @@ -545,7 +712,15 @@ private void initColumns(StringBuilder columns) { int idx = 0; Dialect dialect = database.getMetadata().getDialect(); + if (select.columnList.size() == 1 && "*".equals(select.columnList.get(0).name)) { + columns.append(select.columnList.get(0).owner).append('.').append('*'); + return; + } for (Column column : select.columnList) { + if ("*".equals(column.name)) { + continue; + } + if (idx++ > 0) { columns.append(","); } @@ -555,7 +730,9 @@ private void initColumns(StringBuilder columns) { continue; } - columns.append(column.owner).append('.').append(dialect.quote(column.name, column.metadata != null)) + columns.append(column.owner) + .append('.') + .append(dialect.quote(column.name, column.metadata != null && !column.metadata.realNameDetected())) .append(" as ") .append(dialect.quote(column.alias, false)); } @@ -570,7 +747,8 @@ public void visit(PlainSelect plainSelect) { if (plainSelect.getDistinct() != null) { - columns.append(plainSelect.getDistinct()); + columns.append(plainSelect.getDistinct()) + .append(' '); fastCount = false; } @@ -580,6 +758,9 @@ public void visit(PlainSelect plainSelect) { from.append("FROM "); from.append(plainSelect.getFromItem()); + PrepareStatementVisitor visitor = new PrepareStatementVisitor(); + plainSelect.getFromItem().accept(visitor); + prefixParameters += visitor.parameterSize; } if (plainSelect.getJoins() != null) { @@ -590,6 +771,9 @@ public void visit(PlainSelect plainSelect) { } else { from.append(" ").append(join); } + if (null != join.getRightItem()) { + join.getRightItem().accept(visitor); + } if (null != join.getOnExpressions()) { for (Expression onExpression : join.getOnExpressions()) { onExpression.accept(visitor); @@ -649,12 +833,19 @@ public void visit(SetOperationList setOpList) { @Override public void visit(WithItem withItem) { - + if (!StringUtils.hasText(prefix)) { + prefix += "WITH "; + } + prefix += withItem; + PrepareStatementVisitor visitor = new PrepareStatementVisitor(); + withItem.accept(visitor); + prefixParameters += visitor.parameterSize; } @Override public void visit(ValuesStatement aThis) { - + PrepareStatementVisitor visitor = new PrepareStatementVisitor(); + aThis.accept(visitor); } public Object[] getPrefixParameters(Object... args) { @@ -679,11 +870,13 @@ public Object[] getSuffixParameters(Object... args) { @Override public SqlRequest refactor(QueryParamEntity param, Object... args) { - PrepareSqlFragments sql = PrepareSqlFragments - .of("SELECT") - .addSql(columns) - .addSql(from) - .addParameter(getPrefixParameters(args)); + if (QUERY == null) { + QUERY = SqlFragments.of(prefix, "SELECT", columns, from); + } + BatchSqlFragments sql = new BatchSqlFragments( + StringUtils.hasText(where) ? 10 : 6, 2); + sql.add(QUERY) + .addParameter(getPrefixParameters(args)); appendWhere(sql, param); @@ -695,40 +888,58 @@ public SqlRequest refactor(QueryParamEntity param, Object... args) { return sql.toRequest(); } + @Override public SqlRequest refactorCount(QueryParamEntity param, Object... args) { - PrepareSqlFragments sql = PrepareSqlFragments - .of("SELECT", getPrefixParameters(args)); + BatchSqlFragments sql = new BatchSqlFragments( + StringUtils.hasText(where) ? 10 : 7, 2); + if (SUFFIX == null) { + SUFFIX = SqlFragments.of(suffix); + } if (fastCount) { - sql.addSql("count(1) as _total"); - - sql.addSql(from); + if (FAST_COUNT == null) { + FAST_COUNT = SqlFragments.of( + prefix, "SELECT count(1) as", + database.getMetadata().getDialect().quote("_total"), + from); + } + //SELECT count(1) as _total from + sql.add(FAST_COUNT); + sql.addParameter(getPrefixParameters(args)); appendWhere(sql, param); - sql.addSql(suffix); + sql.add(SUFFIX); } else { - sql.addSql("count(1) as _total from (SELECT") - .addSql(columns, from); + if (SLOW_COUNT == null) { + SLOW_COUNT = SqlFragments + .of(prefix, + "SELECT count(1) as", + database.getMetadata().getDialect().quote("_total"), + "from (SELECT", columns, from); + } + + sql.add(SLOW_COUNT); + sql.addParameter(getPrefixParameters(args)); appendWhere(sql, param); - sql.addSql(suffix) - .addSql(") _t"); + sql.add(SUFFIX); + sql.addSql(") _t"); } return sql - .addParameter(getSuffixParameters(args)) - .toRequest(); + .addParameter(getSuffixParameters(args)) + .toRequest(); } - private void appendOrderBy(PrepareSqlFragments sql, QueryParamEntity param) { + private void appendOrderBy(AppendableSqlFragments sql, QueryParamEntity param) { if (CollectionUtils.isNotEmpty(param.getSorts())) { int index = 0; - PrepareSqlFragments orderByValue = null; - PrepareSqlFragments orderByColumn = null; + BatchSqlFragments orderByValue = null; + BatchSqlFragments orderByColumn = null; for (Sort sort : param.getSorts()) { String name = sort.getName(); Column column = getColumnOrSelectColumn(name); @@ -738,15 +949,15 @@ private void appendOrderBy(PrepareSqlFragments sql, QueryParamEntity param) { } boolean desc = "desc".equalsIgnoreCase(sort.getOrder()); String columnName = column.getOwner() == null ? - database.getMetadata().getDialect().quote(column.getName(), false) - : org.hswebframework.ezorm.core.utils.StringUtils - .concat(column.getOwner(), - ".", - database.getMetadata().getDialect().quote(column.getName())); + database.getMetadata().getDialect().quote(column.getName(), false) + : org.hswebframework.ezorm.core.utils.StringUtils + .concat(column.getOwner(), + ".", + database.getMetadata().getDialect().quote(column.getName())); //按固定值排序 if (sort.getValue() != null) { if (orderByValue == null) { - orderByValue = PrepareSqlFragments.of(); + orderByValue = new BatchSqlFragments(); orderByValue.addSql("case"); } orderByValue.addSql("when"); @@ -754,14 +965,14 @@ private void appendOrderBy(PrepareSqlFragments sql, QueryParamEntity param) { orderByValue.addSql("then").addSql(String.valueOf(desc ? 10000 + index++ : index++)); } else { if (orderByColumn == null) { - orderByColumn = PrepareSqlFragments.of(); + orderByColumn = new BatchSqlFragments(); } else { orderByColumn.addSql(","); } //todo function支持 orderByColumn - .addSql(columnName) - .addSql(desc ? "DESC" : "ASC"); + .addSql(columnName) + .addSql(desc ? "DESC" : "ASC"); } } @@ -778,13 +989,13 @@ private void appendOrderBy(PrepareSqlFragments sql, QueryParamEntity param) { //按列 if (orderByColumn != null) { if (orderByValue != null) { - sql.addSql(","); + sql.add(SqlFragments.COMMA); } sql.addFragments(orderByColumn); } if (orderBy != null) { if (customOrder) { - sql.addSql(","); + sql.add(SqlFragments.COMMA); } sql.addSql(orderBy); } @@ -796,26 +1007,26 @@ private void appendOrderBy(PrepareSqlFragments sql, QueryParamEntity param) { } - private void appendWhere(PrepareSqlFragments sql, QueryParamEntity param) { + private void appendWhere(AppendableSqlFragments sql, QueryParamEntity param) { SqlFragments fragments = TERMS_BUILDER.createTermFragments(QueryAnalyzerImpl.this, param.getTerms()); if (fragments.isNotEmpty() || StringUtils.hasText(where)) { - sql.addSql(" WHERE "); + sql.add(SqlFragments.WHERE); } if (StringUtils.hasText(where)) { - sql.addSql("("); + sql.add(SqlFragments.LEFT_BRACKET); sql.addSql(where); - sql.addSql(")"); + sql.add(SqlFragments.RIGHT_BRACKET); } if (fragments.isNotEmpty()) { if (StringUtils.hasText(where)) { - sql.addSql("AND"); + sql.add(SqlFragments.AND); } - sql.addSql("("); + sql.add(SqlFragments.LEFT_BRACKET); sql.addFragments(fragments); - sql.addSql(")"); + sql.add(SqlFragments.RIGHT_BRACKET); } } @@ -823,14 +1034,146 @@ private void appendWhere(PrepareSqlFragments sql, QueryParamEntity param) { @Getter - static class PrepareStatementVisitor extends ExpressionVisitorAdapter { + static class PrepareStatementVisitor extends ExpressionVisitorAdapter implements FromItemVisitor, SelectVisitor { private int parameterSize; + public PrepareStatementVisitor() { + setSelectVisitor(this); + } + @Override public void visit(JdbcParameter parameter) { parameterSize++; super.visit(parameter); } + + @Override + public void visit(net.sf.jsqlparser.schema.Table tableName) { + + } + + @Override + public void visit(SubJoin subjoin) { + if (subjoin.getLeft() != null) { + subjoin.getLeft().accept(this); + } + if (CollectionUtils.isNotEmpty(subjoin.getJoinList())) { + for (net.sf.jsqlparser.statement.select.Join join : subjoin.getJoinList()) { + if (join.getRightItem() != null) { + join.getRightItem().accept(this); + } + if (join.getOnExpressions() != null) { + join.getOnExpressions().forEach(expr -> expr.accept(this)); + } + } + } + } + + @Override + public void visit(LateralSubSelect lateralSubSelect) { + if (lateralSubSelect.getSubSelect() != null) { + lateralSubSelect.getSubSelect().accept((ExpressionVisitor) this); + } + } + + @Override + public void visit(ValuesList valuesList) { + if (valuesList.getMultiExpressionList() != null) { + for (ExpressionList expressionList : valuesList.getMultiExpressionList().getExpressionLists()) { + expressionList.getExpressions().forEach(expr -> expr.accept(this)); + } + } + } + + @Override + public void visit(TableFunction tableFunction) { + tableFunction.getFunction().accept(this); + } + + @Override + public void visit(ParenthesisFromItem aThis) { + aThis.getFromItem().accept(this); + } + + @Override + public void visit(PlainSelect plainSelect) { + plainSelect.getFromItem().accept(this); + if (plainSelect.getJoins() != null) { + for (net.sf.jsqlparser.statement.select.Join join : plainSelect.getJoins()) { + join.getRightItem().accept(this); + } + } + if (plainSelect.getSelectItems() != null) { + for (SelectItem selectItem : plainSelect.getSelectItems()) { + selectItem.accept(this); + } + } + if (plainSelect.getWhere() != null) { + plainSelect.getWhere().accept(this); + } + if (plainSelect.getHaving() != null) { + plainSelect.getHaving().accept(this); + } + + if (plainSelect.getGroupBy() != null) { + for (Expression expression : plainSelect.getGroupBy().getGroupByExpressionList().getExpressions()) { + expression.accept(this); + } + } + } + + @Override + public void visit(SetOperationList setOpList) { + if (CollectionUtils.isNotEmpty(setOpList.getSelects())) { + for (SelectBody select : setOpList.getSelects()) { + select.accept(this); + } + } + if (setOpList.getOffset() != null) { + setOpList.getOffset().getOffset().accept(this); + } + if (setOpList.getLimit() != null) { + if (setOpList.getLimit().getRowCount() != null) { + setOpList.getLimit().getRowCount().accept(this); + } + if (setOpList.getLimit().getOffset() != null) { + setOpList.getLimit().getOffset().accept(this); + } + } + } + + @Override + public void visit(WithItem withItem) { + if (CollectionUtils.isNotEmpty(withItem.getWithItemList())) { + for (SelectItem selectItem : withItem.getWithItemList()) { + selectItem.accept(this); + } + } + if (withItem.getSubSelect() != null) { + withItem.getSubSelect().accept((ExpressionVisitor) this); + } + } + + @Override + public void visit(ValuesStatement aThis) { + if (aThis.getExpressions() != null) { + aThis.getExpressions().accept(this); + } + } + } + + static class FakeTable extends RDBViewMetadata { + @Override + public Optional getColumn(String name) { + //sql中声明的列都可以使用 + + QueryHelperUtils.assertLegalColumn(name); + + RDBColumnMetadata fake = new RDBColumnMetadata(); + fake.setName(name); + addColumn(fake); + return Optional.of(fake); + } } private interface QueryRefactor { diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/QueryHelper.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/QueryHelper.java index cea7cd8dc..1c109291c 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/QueryHelper.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/QueryHelper.java @@ -14,6 +14,7 @@ import javax.validation.constraints.NotNull; import java.io.Serializable; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; @@ -149,7 +150,7 @@ SelectSpec select(Class resultType, interface NativeQuerySpec extends ExecuteSpec { /** - * 设置日志,在执行sql等操作时使用次日志进行日志打印. + * 设置日志,在执行sql等操作时使用此日志进行日志打印. * * @param logger Logger * @return this @@ -168,7 +169,7 @@ interface NativeQuerySpec extends ExecuteSpec { * @return this */ default ExecuteSpec where(Consumer> dsl) { - Query query = QueryParamEntity.newQuery(); + Query query = QueryParamEntity.newQuery().noPaging(); dsl.accept(query); return where(query.getParam()); } @@ -436,6 +437,13 @@ interface ExecuteSpec { */ Flux fetch(); + /** + * 执行查询,返回数据流 + * + * @return 数据流 + */ + Flux fetch(int pageIndex,int pageSize); + /** * 执行分页查询,默认返回第一页的25条数据. * @@ -649,8 +657,9 @@ static Flux combineOneToMany(Flux source, return combineOneToMany(source, idMapper, list -> fetcher - .in(MethodReferenceConverter.convertToColumn(mainIdGetter), list) - .fetch(), + .copy() + .in(MethodReferenceConverter.convertToColumn(mainIdGetter), list) + .fetch(), mainIdGetter, setter); } @@ -675,23 +684,23 @@ static Flux combineOneToMany(Flux source, Setter> setter) { return source - .buffer(200) - .concatMap(buffer -> { - Map mapping = buffer - .stream() - .collect(Collectors.toMap(idMapper, Function.identity(), (a, b) -> b)); - return fetcher - .apply(mapping.keySet()) - .collect(Collectors.groupingBy(mainIdGetter)) - .flatMapIterable(Map::entrySet) - .doOnNext(e -> { - T main = mapping.get(e.getKey()); - if (main != null) { - setter.accept(main, e.getValue()); - } - }) - .thenMany(Flux.fromIterable(buffer)); - }); + .buffer(200) + .concatMap(buffer -> { + Map mapping = buffer + .stream() + .collect(Collectors.toMap(idMapper, Function.identity(), (a, b) -> b)); + return fetcher + .apply(mapping.keySet()) + .collect(Collectors.groupingBy(mainIdGetter)) + .flatMapIterable(Map::entrySet) + .doOnNext(e -> { + T main = mapping.get(e.getKey()); + if (main != null) { + setter.accept(main, e.getValue()); + } + }) + .thenMany(Flux.fromIterable(buffer)); + }); } /** @@ -709,17 +718,83 @@ static Mono> transformPageResult(Mono> sour return source.flatMap(result -> { if (result.getTotal() > 0) { return transfer - .apply(result.getData()) - .map(newDataList -> { - PagerResult pagerResult = PagerResult.of(result.getTotal(), newDataList); - pagerResult.setPageIndex(result.getPageIndex()); - pagerResult.setPageSize(result.getPageSize()); - return pagerResult; - }); + .apply(result.getData()) + .map(newDataList -> { + PagerResult pagerResult = PagerResult.of(result.getTotal(), newDataList); + pagerResult.setPageIndex(result.getPageIndex()); + pagerResult.setPageSize(result.getPageSize()); + return pagerResult; + }); } //empty return Mono.just((PagerResult) result); }); } + /** + * 指定ReactiveQuery和QueryParamEntity,执行查询并封装为分页查询结果. + * + * @param param QueryParamEntity + * @param query ReactiveQuery + * @param T + * @return PagerResult + */ + static Mono> queryPager(QueryParamEntity param, + Supplier> query) { + + return queryPager(param, query, Function.identity()); + } + + /** + * 指定ReactiveQuery和QueryParamEntity,执行查询并封装为分页查询结果. + * + * @param param QueryParamEntity + * @param query ReactiveQuery + * @param mapper 转换结果类型 + * @param T + * @return PagerResult + */ + static Mono> queryPager(QueryParamEntity param, + Supplier> query, + Function mapper) { + //如果查询参数指定了总数,表示不需要再进行count操作. + //建议前端在使用分页查询时,切换下一页时,将第一次查询到total结果传入查询参数,可以提升查询性能. + if (param.getTotal() != null) { + return query + .get() + .setParam(param.rePaging(param.getTotal())) + .fetch() + .map(mapper) + .collectList() + .map(list -> PagerResult.of(param.getTotal(), list, param)); + } + //并行分页,更快,所在页码无数据时,会返回空list. + if (param.isParallelPager()) { + return Mono + .zip( + query.get().setParam(param.clone()).count(), + query.get().setParam(param.clone()).fetch().map(mapper).collectList(), + (total, data) -> PagerResult.of(total, data, param) + ); + } + return query + .get() + .setParam(param.clone()) + .count() + .flatMap(total -> { + if (total == 0) { + return Mono.just(PagerResult.of(0, new ArrayList<>(), param)); + } + //查询前根据数据总数进行重新分页:要跳转的页码没有数据则跳转到最后一页 + QueryParamEntity rePagingQuery = param.clone().rePaging(total); + return query + .get() + .setParam(rePagingQuery) + .fetch() + .map(mapper) + .collectList() + .map(list -> PagerResult.of(total, list, rePagingQuery)); + }); + } + } diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/QueryHelperUtils.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/QueryHelperUtils.java index c7df7b5bd..220e0bbe9 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/QueryHelperUtils.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/QueryHelperUtils.java @@ -1,6 +1,7 @@ package org.hswebframework.web.crud.query; import io.netty.util.concurrent.FastThreadLocal; +import org.hswebframework.web.exception.BusinessException; public class QueryHelperUtils { @@ -17,7 +18,10 @@ public static String toSnake(String col) { for (int i = 0, len = col.length(); i < len; i++) { char c = col.charAt(i); if (Character.isUpperCase(c)) { - builder.append('_').append(Character.toLowerCase(c)); + if (i != 0) { + builder.append('_'); + } + builder.append(Character.toLowerCase(c)); } else { builder.append(c); } @@ -41,7 +45,11 @@ public static String toHump(String col) { return col; } if (c == '_') { - builder.append(Character.toUpperCase(col.charAt(++i))); + if (i == len - 1) { + builder.append('_'); + } else { + builder.append(Character.toUpperCase(col.charAt(++i))); + } } else { builder.append(Character.toLowerCase(c)); } @@ -49,4 +57,22 @@ public static String toHump(String col) { return builder.toString(); } + + public static void assertLegalColumn(String col) { + if (!isLegalColumn(col)) { + throw new BusinessException.NoStackTrace("error.illegal_column_name", col); + } + } + + public static boolean isLegalColumn(String col) { + int len = col.length(); + for (int i = 0; i < len; i++) { + char c = col.charAt(i); + if (c == '_' || c == '$' || Character.isLetterOrDigit(c)) { + continue; + } + return false; + } + return true; + } } diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/ToHumpMap.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/ToHumpMap.java new file mode 100644 index 000000000..0f2dab926 --- /dev/null +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/ToHumpMap.java @@ -0,0 +1,18 @@ +package org.hswebframework.web.crud.query; + +import java.util.LinkedHashMap; + +public class ToHumpMap extends LinkedHashMap { + + @Override + public V put(String key, V value) { + V val = super.put(key, value); + + String humpKey = QueryHelperUtils.toHump(key); + if (!humpKey.equals(key)) { + super.put(humpKey, value); + } + return val; + } + +} diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/CrudService.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/CrudService.java index 7ffd1b106..97477f941 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/CrudService.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/CrudService.java @@ -1,6 +1,6 @@ package org.hswebframework.web.crud.service; -import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections4.CollectionUtils; import org.hswebframework.ezorm.core.param.QueryParam; import org.hswebframework.ezorm.rdb.mapping.SyncDelete; import org.hswebframework.ezorm.rdb.mapping.SyncQuery; diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/EnableCacheReactiveCrudService.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/EnableCacheReactiveCrudService.java index 1036fb16e..dbe2f7110 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/EnableCacheReactiveCrudService.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/EnableCacheReactiveCrudService.java @@ -3,22 +3,29 @@ import org.hswebframework.ezorm.rdb.mapping.ReactiveDelete; import org.hswebframework.ezorm.rdb.mapping.ReactiveUpdate; import org.hswebframework.ezorm.rdb.mapping.defaults.SaveResult; +import org.hswebframework.web.api.crud.entity.TransactionManagers; import org.hswebframework.web.cache.ReactiveCache; +import org.hswebframework.web.crud.utils.TransactionUtils; import org.reactivestreams.Publisher; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.reactive.TransactionSynchronization; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import javax.annotation.Nonnull; import java.util.Collection; -import java.util.function.Function; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; public interface EnableCacheReactiveCrudService extends ReactiveCrudService { ReactiveCache getCache(); + String ALL_DATA_KEY = "@all"; + default Mono findById(K id) { - return this - .getCache() - .getMono("id:" + id, () -> ReactiveCrudService.super.findById(id)); + return this.getCache().getMono("id:" + id, () -> ReactiveCrudService.super.findById(id)); } @Override @@ -26,67 +33,115 @@ default Mono findById(Mono publisher) { return publisher.flatMap(this::findById); } + @Override + @Transactional(transactionManager = TransactionManagers.reactiveTransactionManager) + default Mono updateById(K id, E data) { + return updateById(id, Mono.just(data)); + } + @Override default Mono updateById(K id, Mono entityPublisher) { - return ReactiveCrudService.super - .updateById(id, entityPublisher) - .doFinally(i -> getCache().evict("id:" + id).subscribe()); + return registerClearCache(Collections.singleton("id:" + id)) + .then(ReactiveCrudService.super.updateById(id, entityPublisher)); + } + + @Override + default Mono save(Collection collection) { + return registerClearCache() + .then(ReactiveCrudService.super.save(collection)); } @Override default Mono save(E data) { - return ReactiveCrudService.super - .save(data) - .doFinally(i -> getCache().clear().subscribe()); + return registerClearCache() + .then(ReactiveCrudService.super.save(data)); } @Override default Mono save(Publisher entityPublisher) { - return ReactiveCrudService.super - .save(entityPublisher) - .doFinally(i -> getCache().clear().subscribe()); + return registerClearCache() + .then(ReactiveCrudService.super.save(entityPublisher)); } @Override default Mono insert(E data) { - return ReactiveCrudService.super - .insert(data) - .doFinally(i -> getCache().clear().subscribe()); + return registerClearCache() + .then(ReactiveCrudService.super.insert(data)); } @Override default Mono insert(Publisher entityPublisher) { - return ReactiveCrudService.super - .insert(entityPublisher) - .doFinally(i -> getCache().clear().subscribe()); + return registerClearCache() + .then(ReactiveCrudService.super.insert(entityPublisher)); } @Override default Mono insertBatch(Publisher> entityPublisher) { - return ReactiveCrudService.super - .insertBatch(entityPublisher) - .doFinally(i -> getCache().clear().subscribe()); + return registerClearCache() + .then(ReactiveCrudService.super.insertBatch(entityPublisher)); + } + + default Mono registerClearCache() { + return TransactionUtils.registerSynchronization(new TransactionSynchronization() { + @Override + @Nonnull + public Mono afterCommit() { + return getCache().clear(); + } + }, TransactionSynchronization::afterCommit); + } + + default Mono registerClearCache(Collection keys) { + return TransactionUtils.registerSynchronization(new TransactionSynchronization() { + @Override + @Nonnull + public Mono afterCommit() { + Set set = new HashSet<>(keys); + //同步删除全量数据的缓存 + set.add(ALL_DATA_KEY); + return getCache().evictAll(set); + } + }, TransactionSynchronization::afterCommit); + } + + + @Override + @Transactional(transactionManager = TransactionManagers.reactiveTransactionManager) + default Mono deleteById(K id) { + return deleteById(Mono.just(id)); } @Override default Mono deleteById(Publisher idPublisher) { - return Flux - .from(idPublisher) - .flatMap(id -> this.getCache().evict("id:" + id).thenReturn(id)) - .as(ReactiveCrudService.super::deleteById); + Flux cache = Flux.from(idPublisher).cache(); + return cache + .map(id -> "id:" + id) + .collectList() + .flatMap(this::registerClearCache) + .then(ReactiveCrudService.super.deleteById(cache)); } @Override default ReactiveUpdate createUpdate() { return ReactiveCrudService.super .createUpdate() - .onExecute((update, s) -> getCache().clear().then(s)); + .onExecute((update, s) -> s.flatMap(i -> { + if (i > 0) { + return getCache().clear().thenReturn(i); + } + return Mono.just(i); + })); } @Override default ReactiveDelete createDelete() { return ReactiveCrudService.super .createDelete() - .onExecute((update, s) -> getCache().clear().then(s)); + .onExecute((update, s) -> s.flatMap(i -> { + if (i > 0) { + return getCache().clear().thenReturn(i); + } + return Mono.just(i); + })); } } diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/GenericReactiveCacheSupportCrudService.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/GenericReactiveCacheSupportCrudService.java index 9afc3a081..b4c45b950 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/GenericReactiveCacheSupportCrudService.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/GenericReactiveCacheSupportCrudService.java @@ -5,7 +5,7 @@ import org.hswebframework.web.cache.ReactiveCacheManager; import org.hswebframework.web.cache.supports.UnSupportedReactiveCache; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.PostMapping; +import reactor.core.publisher.Flux; public abstract class GenericReactiveCacheSupportCrudService implements EnableCacheReactiveCrudService { @@ -37,4 +37,9 @@ public ReactiveCache getCache() { public String getCacheName() { return this.getClass().getSimpleName(); } + + + public Flux getCacheAll() { + return getCache().getFlux(ALL_DATA_KEY, () -> EnableCacheReactiveCrudService.super.createQuery().fetch()); + } } diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/GenericReactiveTreeSupportCrudService.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/GenericReactiveTreeSupportCrudService.java index 708cdf490..684d511c0 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/GenericReactiveTreeSupportCrudService.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/GenericReactiveTreeSupportCrudService.java @@ -6,6 +6,8 @@ public abstract class GenericReactiveTreeSupportCrudService, K> implements ReactiveTreeSortEntityService { + private static final int SAVE_BUFFER_SIZE = Integer.getInteger("tree.save.buffer.size", 200); + @Autowired private ReactiveRepository repository; @@ -14,4 +16,8 @@ public ReactiveRepository getRepository() { return repository; } + @Override + public int getBufferSize() { + return SAVE_BUFFER_SIZE; + } } diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/ReactiveTreeSortEntityService.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/ReactiveTreeSortEntityService.java index 66de7ca0e..97bd0187a 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/ReactiveTreeSortEntityService.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/ReactiveTreeSortEntityService.java @@ -1,14 +1,19 @@ package org.hswebframework.web.crud.service; +import org.apache.commons.collections4.CollectionUtils; +import org.hswebframework.ezorm.core.MethodReferenceColumn; +import org.hswebframework.ezorm.core.StaticMethodReferenceColumn; +import org.hswebframework.ezorm.rdb.mapping.ReactiveDelete; import org.hswebframework.ezorm.rdb.mapping.defaults.SaveResult; import org.hswebframework.ezorm.rdb.operator.dml.Terms; import org.hswebframework.utils.RandomUtil; -import org.hswebframework.web.api.crud.entity.QueryParamEntity; -import org.hswebframework.web.api.crud.entity.TreeSortSupportEntity; -import org.hswebframework.web.api.crud.entity.TreeSupportEntity; +import org.hswebframework.web.api.crud.entity.*; +import org.hswebframework.web.exception.ValidationException; import org.hswebframework.web.id.IDGenerator; import org.hswebframework.web.validator.CreateGroup; import org.reactivestreams.Publisher; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -28,7 +33,7 @@ * @see GenericReactiveTreeSupportCrudService */ public interface ReactiveTreeSortEntityService, K> - extends ReactiveCrudService { + extends ReactiveCrudService { /** * 动态查询并将查询结构转为树形结构 @@ -46,12 +51,13 @@ default Mono> queryResultToTree(Mono paramEn * @param paramEntity 查询参数 * @return 树形结构 */ + @Transactional(readOnly = true, transactionManager = TransactionManagers.reactiveTransactionManager) default Mono> queryResultToTree(QueryParamEntity paramEntity) { return query(paramEntity) - .collectList() - .map(list -> TreeSupportEntity.list2tree(list, - this::setChildren, - this::createRootNodePredicate)); + .collectList() + .map(list -> TreeSupportEntity.list2tree(list, + this::setChildren, + this::createRootNodePredicate)); } /** @@ -60,56 +66,81 @@ default Mono> queryResultToTree(QueryParamEntity paramEntity) { * @param paramEntity 查询参数 * @return 树形结构 */ + @Transactional(readOnly = true, transactionManager = TransactionManagers.reactiveTransactionManager) default Mono> queryIncludeChildrenTree(QueryParamEntity paramEntity) { return queryIncludeChildren(paramEntity) - .collectList() - .map(list -> TreeSupportEntity.list2tree(list, - this::setChildren, - this::createRootNodePredicate)); + .collectList() + .map(list -> TreeSupportEntity.list2tree(list, + this::setChildren, + this::createRootNodePredicate)); } /** * 查询指定ID的实体以及对应的全部子节点 * * @param idList ID集合 - * @return 树形结构 + * @return 包含子节点的所有节点 */ + @Transactional(readOnly = true, transactionManager = TransactionManagers.reactiveTransactionManager) default Flux queryIncludeChildren(Collection idList) { - Set duplicateCheck = new HashSet<>(); + return queryIncludeChildren(findById(idList)); + } - return findById(idList) - .concatMap(e -> StringUtils - .isEmpty(e.getPath()) || !duplicateCheck.add(e.getPath()) - ? Mono.just(e) - : createQuery() - .where() - //使用path快速查询 - .like$("path", e.getPath()) - .fetch()) - .distinct(TreeSupportEntity::getId); + /** + * 根据实体流查询全部子节点(包含原节点) + * + * @param entities 实体流 + * @return 包含子节点的所有节点 + * @since 4.0.18 + */ + @Transactional(readOnly = true, transactionManager = TransactionManagers.reactiveTransactionManager) + default Flux queryIncludeChildren(Flux entities) { + Set duplicateCheck = new HashSet<>(); + return entities + .concatMap(e -> !StringUtils.hasText(e.getPath()) || !duplicateCheck.add(e.getPath()) + ? Mono.just(e) + : createQuery() + .where() + //使用path快速查询 + .like$("path", e.getPath()) + .fetch(), + Integer.MAX_VALUE) + .distinct(TreeSupportEntity::getId); } /** * 查询指定ID的实体以及对应的全部父节点 * * @param idList ID集合 - * @return 树形结构 + * @return 包含父节点的所有节点 */ + @Transactional(readOnly = true, transactionManager = TransactionManagers.reactiveTransactionManager) default Flux queryIncludeParent(Collection idList) { + return queryIncludeParent(findById(idList)); + } + + /** + * 根据实体流查询全部父节点(包含原节点) + * + * @param entities 实体流 + * @return 包含父节点的所有节点 + * @since 4.0.18 + */ + @Transactional(readOnly = true, transactionManager = TransactionManagers.reactiveTransactionManager) + default Flux queryIncludeParent(Flux entities) { Set duplicateCheck = new HashSet<>(); - return findById(idList) - .concatMap(e -> StringUtils - .isEmpty(e.getPath()) || !duplicateCheck.add(e.getPath()) - ? Mono.just(e) - : createQuery() - .where() - //where ? like path and path !='' and path not null - .accept(Terms.Like.reversal("path", e.getPath(), false, true)) - .notEmpty("path") - .notNull("path") - .fetch()) - .distinct(TreeSupportEntity::getId); + return entities + .concatMap(e -> !StringUtils.hasText(e.getPath()) || !duplicateCheck.add(e.getPath()) + ? Mono.just(e) + : createQuery() + .where() + //where ? like path and path !='' and path not null + .accept(Terms.Like.reversal("path", e.getPath(), false, true)) + .notEmpty("path") + .notNull("path") + .fetch(), Integer.MAX_VALUE) + .distinct(TreeSupportEntity::getId); } /** @@ -118,39 +149,62 @@ default Flux queryIncludeParent(Collection idList) { * @param queryParam 查询参数 * @return 树形结构 */ + @Transactional(readOnly = true, transactionManager = TransactionManagers.reactiveTransactionManager) default Flux queryIncludeChildren(QueryParamEntity queryParam) { Set duplicateCheck = new HashSet<>(); return query(queryParam) - .concatMap(e -> StringUtils - .isEmpty(e.getPath()) || !duplicateCheck.add(e.getPath()) - ? Mono.just(e) - : createQuery() - .where() - .like$("path", e.getPath()) - .fetch() - ) - .distinct(TreeSupportEntity::getId); + .concatMap(e -> !StringUtils.hasText(e.getPath()) || !duplicateCheck.add(e.getPath()) + ? Mono.just(e) + : createQuery() + .as(q -> { + if (CollectionUtils.isNotEmpty(queryParam.getIncludes())) { + q.select(queryParam.getIncludes().toArray(new String[0])); + } + if (CollectionUtils.isNotEmpty(queryParam.getExcludes())) { + q.selectExcludes(queryParam.getExcludes().toArray(new String[0])); + } + return q; + }) + .where() + .like$("path", e.getPath()) + .fetch() + , Integer.MAX_VALUE) + .distinct(TreeSupportEntity::getId); } @Override + @Transactional(transactionManager = TransactionManagers.reactiveTransactionManager) default Mono insert(Publisher entityPublisher) { return insertBatch(Flux.from(entityPublisher).collectList()); } @Override + @Transactional(transactionManager = TransactionManagers.reactiveTransactionManager) + default Mono insert(E data) { + return this.insertBatch(Flux.just(Collections.singletonList(data))); + } + + @Override + @Transactional(transactionManager = TransactionManagers.reactiveTransactionManager) default Mono insertBatch(Publisher> entityPublisher) { - return this.getRepository() - .insertBatch(Flux.from(entityPublisher) - .flatMap(Flux::fromIterable) - .flatMap(this::applyTreeProperty) - .flatMap(e -> Flux.fromIterable(TreeSupportEntity.expandTree2List(e, getIDGenerator()))) - .collectList()); + return this + .getRepository() + .insertBatch(new TreeSortServiceHelper<>(this) + .prepare(Flux.from(entityPublisher) + .flatMapIterable(Function.identity())) + // .doOnNext(e -> e.tryValidate(CreateGroup.class)) + .buffer(getBufferSize())); } + default int getBufferSize() { + return 200; + } + + @Deprecated default Mono applyTreeProperty(E ele) { if (StringUtils.hasText(ele.getPath()) || - StringUtils.isEmpty(ele.getParentId())) { + ObjectUtils.isEmpty(ele.getParentId())) { return Mono.just(ele); } @@ -160,188 +214,144 @@ default Mono applyTreeProperty(E ele) { .thenReturn(ele); } + @Deprecated //校验是否有循环依赖,修改父节点为自己的子节点? default Mono checkCyclicDependency(K id, E ele) { - if (StringUtils.isEmpty(id)) { + if (ObjectUtils.isEmpty(id)) { return Mono.empty(); } return this - .queryIncludeChildren(Collections.singletonList(id)) - .doOnNext(e -> { - if (Objects.equals(ele.getParentId(), e.getId())) { - throw new IllegalArgumentException("不能修改父节点为自己或者自己的子节点"); - } - }) - .then(Mono.just(ele)); + .queryIncludeChildren(Collections.singletonList(id)) + .doOnNext(e -> { + if (Objects.equals(ele.getParentId(), e.getId())) { + throw new ValidationException.NoStackTrace("parentId", "error.tree_entity_cyclic_dependency"); + } + }) + .then(Mono.just(ele)); } - //重构子节点的path - default Mono refactorChildPath(K id, String path, Consumer pathAccepter) { + @Deprecated + default Mono> checkParentId(Collection source) { + + Set idSet = source + .stream() + .map(TreeSupportEntity::getId) + .filter(e -> !ObjectUtils.isEmpty(e)) + .collect(Collectors.toSet()); + + if (idSet.isEmpty()) { + return Mono.just(source); + } + + Set readyToCheck = source + .stream() + .map(TreeSupportEntity::getParentId) + .filter(e -> !ObjectUtils.isEmpty(e) && !idSet.contains(e)) + .collect(Collectors.toSet()); + + if (readyToCheck.isEmpty()) { + return Mono.just(source); + } + return this - .createQuery() - .where("parentId", id) - .fetch() - .flatMap(e -> { - if (StringUtils.isEmpty(path)) { - e.setPath(RandomUtil.randomChar(4)); - } else { - e.setPath(path + "-" + RandomUtil.randomChar(4)); - } - pathAccepter.accept(e); - if (e.getParentId() != null) { - return this - .refactorChildPath(e.getId(), e.getPath(), pathAccepter) - .thenReturn(e); - } - return Mono.just(e); - }) - .as(getRepository()::save) - .then(); + .createQuery() + .select("id") + .in("id", readyToCheck) + .fetch() + .doOnNext(e -> readyToCheck.remove(e.getId())) + .then(Mono.fromSupplier(() -> { + if (!readyToCheck.isEmpty()) { + throw new ValidationException( + "error.tree_entity_parent_id_not_exist", + Collections.singletonList( + new ValidationException.Detail( + "parentId", + "error.tree_entity_parent_id_not_exist", + readyToCheck)) + ); + } + return source; + })); + + } + + @Deprecated + //重构子节点的path + default void refactorChildPath(K id, Function> childGetter, String path, Consumer pathAccepter) { + + Collection children = childGetter.apply(id); + if (CollectionUtils.isEmpty(children)) { + return; + } + for (E child : children) { + if (ObjectUtils.isEmpty(path)) { + child.setPath(RandomUtil.randomChar(4)); + } else { + child.setPath(path + "-" + RandomUtil.randomChar(4)); + } + pathAccepter.accept(child); + this.refactorChildPath(child.getId(), childGetter, child.getPath(), pathAccepter); + } + } @Override + @Transactional(rollbackFor = Throwable.class, + transactionManager = TransactionManagers.reactiveTransactionManager) default Mono save(Publisher entityPublisher) { - return Flux - .from(entityPublisher) - //1.先平铺 - .flatMapIterable(e -> TreeSupportEntity.expandTree2List(e, getIDGenerator())) - .collectList() - .flatMapIterable(list -> { - Map map = list - .stream() - .filter(e -> e.getId() != null) - .collect(Collectors.toMap(TreeSupportEntity::getId, Function.identity())); - //2. 重新组装树结构 - return TreeSupportEntity.list2tree(list, - this::setChildren, - (Predicate) e -> this.isRootNode(e) || map.get(e.getParentId()) == null); - - }) - //执行验证 - .doOnNext(e -> e.tryValidate(CreateGroup.class)) - //再次平铺为 - .flatMapIterable(e -> TreeSupportEntity.expandTree2List(e, getIDGenerator())) - //重构path - .as(this::tryRefactorPath) - .as(this.getRepository()::save); + return new TreeSortServiceHelper<>(this) + .prepare(Flux.from(entityPublisher)) +// .doOnNext(e -> e.tryValidate(CreateGroup.class)) + .buffer(getBufferSize()) + .concatMap(this.getRepository()::save) + .reduce(SaveResult::merge); } + @Deprecated default Flux tryRefactorPath(Flux stream) { - Flux cache = stream.cache(); - Mono> mapping = cache - .filter(e -> null != e.getId()) - .collectMap(TreeSupportEntity::getId, Function.identity()) - .defaultIfEmpty(Collections.emptyMap()); - - //查询出旧数据 - Mono> olds = cache - .filter(e -> null != e.getId()) - .map(TreeSupportEntity::getId) - .as(this::findById) - .collectMap(TreeSupportEntity::getId, Function.identity()) - .defaultIfEmpty(Collections.emptyMap()); - - - return Mono - .zip(mapping, olds) - .flatMapMany(tp2 -> { - Map map = tp2.getT1(); - Map oldMap = tp2.getT2(); - - return cache - .flatMap(data -> { - E old = data.getId() == null ? null : oldMap.get(data.getId()); - K parentId = old != null ? old.getParentId() : data.getParentId(); - E oldParent = parentId == null ? null : oldMap.get(parentId); - if (old != null) { - K newParentId = data.getParentId(); - //父节点发生变化,更新所有子节点path - if (!Objects.equals(parentId, newParentId)) { - List> jobs = new ArrayList<>(); - Consumer childConsumer = child -> { - //更新了父节点,但是同时也传入的对应的子节点 - E readyToUpdate = map.get(child.getId()); - if (null != readyToUpdate) { - readyToUpdate.setPath(child.getPath()); - } - }; - - //变更到了顶级节点 - if (isRootNode(data)) { - data.setPath(RandomUtil.randomChar(4)); - jobs.add(this.refactorChildPath(old.getId(), data.getPath(), childConsumer)); - } else { - if (null != oldParent) { - data.setPath(oldParent.getPath() + "-" + RandomUtil.randomChar(4)); - jobs.add(this.refactorChildPath(old.getId(), data.getPath(), childConsumer)); - } else { - jobs.add(this.findById(newParentId) - .flatMap(parent -> { - data.setPath(parent.getPath() + "-" + RandomUtil.randomChar(4)); - return this.refactorChildPath(data.getId(), data.getPath(), childConsumer); - }) - ); - } - } - return Flux.merge(jobs) - .then(Mono.just(data)); - } else { - //父节点未变化则使用原始的path - Consumer pathRefactor = (parent) -> { - if (old.getPath().startsWith(parent.getPath())) { - data.setPath(old.getPath()); - } else { - data.setPath(parent.getPath() + "-" + RandomUtil.randomChar(4)); - } - }; - if (oldParent != null) { - pathRefactor.accept(oldParent); - } else if (parentId != null) { - return findById(parentId) - .switchIfEmpty(Mono.fromRunnable(() -> { - data.setParentId(null); - data.setLevel(1); - data.setPath(old.getPath()); - })) - .doOnNext(pathRefactor) - .thenReturn(data); - } else { - data.setPath(old.getPath()); - } - - } - } - return Mono.just(data); - }); - }); + return new TreeSortServiceHelper<>(this).prepare(stream); } @Override + @Transactional(transactionManager = TransactionManagers.reactiveTransactionManager) default Mono save(Collection collection) { return save(Flux.fromIterable(collection)); } @Override + @Transactional(transactionManager = TransactionManagers.reactiveTransactionManager) default Mono save(E data) { return save(Flux.just(data)); } @Override + @Transactional(transactionManager = TransactionManagers.reactiveTransactionManager) default Mono updateById(K id, Mono entityPublisher) { return this - .save(entityPublisher.doOnNext(e -> e.setId(id))) - .map(SaveResult::getTotal); + .findById(id) + .map(e -> this + .save(entityPublisher.doOnNext(data -> data.setId(id))) + .map(SaveResult::getTotal)) + .defaultIfEmpty(Mono.just(0)) + .flatMap(Function.identity()); } @Override + @Transactional(transactionManager = TransactionManagers.reactiveTransactionManager) + default Mono deleteById(K id) { + return this.deleteById(Flux.just(id)); + } + + @Override + @Transactional(transactionManager = TransactionManagers.reactiveTransactionManager) default Mono deleteById(Publisher idPublisher) { return this - .findById(Flux.from(idPublisher)) - .concatMap(e -> StringUtils.hasText(e.getPath()) - ? createDelete().where().like$(e::getPath).execute() - : getRepository().deleteById(e.getId())) - .as(MathFlux::sumInt); + .findById(Flux.from(idPublisher)) + .concatMap(e -> StringUtils.hasText(e.getPath()) + ? getRepository().createDelete().where().like$(e::getPath).execute() + : getRepository().deleteById(e.getId()), Integer.MAX_VALUE) + .as(MathFlux::sumInt); } IDGenerator getIDGenerator(); @@ -358,7 +368,7 @@ default Predicate createRootNodePredicate(TreeSupportEntity.TreeHelper return true; } //有父节点,但是父节点不存在 - if (!StringUtils.isEmpty(node.getParentId())) { + if (!ObjectUtils.isEmpty(node.getParentId())) { return helper.getNode(node.getParentId()) == null; } return false; @@ -366,6 +376,25 @@ default Predicate createRootNodePredicate(TreeSupportEntity.TreeHelper } default boolean isRootNode(E entity) { - return StringUtils.isEmpty(entity.getParentId()) || "-1".equals(String.valueOf(entity.getParentId())); + return ObjectUtils.isEmpty(entity.getParentId()) || "-1".equals(String.valueOf(entity.getParentId())); + } + + @Override + @SuppressWarnings("all") + default ReactiveDelete createDelete() { + return ReactiveCrudService.super + .createDelete() + .onExecute((delete, executor) -> this + .queryIncludeChildren(delete.toQueryParam(QueryParamEntity::new) + .includes("id", "path", "parentId")) + .map(TreeSupportEntity::getId) + .buffer(200) + .concatMap(list -> getRepository() + .createDelete() + .where() + .in("id", list) + .execute(), Integer.MAX_VALUE) + //.concatWith(executor) + .reduce(0, Math::addExact)); } } diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/TreeSortServiceHelper.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/TreeSortServiceHelper.java new file mode 100644 index 000000000..0da90387b --- /dev/null +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/TreeSortServiceHelper.java @@ -0,0 +1,274 @@ +package org.hswebframework.web.crud.service; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.MapUtils; +import org.hswebframework.utils.RandomUtil; +import org.hswebframework.web.api.crud.entity.TreeSortSupportEntity; +import org.hswebframework.web.api.crud.entity.TreeSupportEntity; +import org.hswebframework.web.exception.ValidationException; +import org.springframework.util.ObjectUtils; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.*; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +public class TreeSortServiceHelper, PK> { + + //包含子节点的数据 + private Map allData; + + private Map oldData; + + private Map thisTime; + + private Map readyToSave; + + private final Map> childrenMapping = new LinkedHashMap<>(); + + private final ReactiveTreeSortEntityService service; + + TreeSortServiceHelper(ReactiveTreeSortEntityService service) { + this.service = service; + } + + Flux prepare(Flux source) { + Flux cache = source + .flatMapIterable(e -> TreeSupportEntity.expandTree2List(e, service.getIDGenerator())) + .collectList() + .flatMapIterable(list -> { + + Map map = list + .stream() + .filter(e -> e.getId() != null) + .collect(Collectors.toMap( + TreeSupportEntity::getId, + Function.identity(), + (a, b) -> a + )); + //重新组装树结构 + TreeSupportEntity.list2tree(list, + service::setChildren, + (Predicate) e -> service.isRootNode(e) || map.get(e.getParentId()) == null); + + return list; + }) + .cache(); + + return init(cache) + .then(Mono.defer(this::checkParentId)) + .then(Mono.fromRunnable(this::checkCyclicDependency)) + .then(Mono.fromRunnable(this::refactorPath)) + .thenMany(Flux.defer(() -> Flux.fromIterable(readyToSave.values()))) + .doOnNext(this::refactor); + } + + private Mono init(Flux source) { + oldData = new LinkedHashMap<>(); + thisTime = new LinkedHashMap<>(); + allData = new LinkedHashMap<>(); + readyToSave = new LinkedHashMap<>(); + + Mono> allDataFetcher = + source + .mapNotNull(e -> { + + if (e.getId() != null) { + thisTime.put(e.getId(), e); + } + + return e.getId(); + }) + .collect(Collectors.toSet()) + .flatMap(list -> service + .queryIncludeChildren(list) + .collectMap(TreeSupportEntity::getId, Function.identity())); + return allDataFetcher + .doOnNext(includeChildren -> { + //旧的数据 + for (E value : thisTime.values()) { + E old = includeChildren.get(value.getId()); + if (null != old) { + this.oldData.put(value.getId(), old); + } + } + + readyToSave.putAll(thisTime); + + allData.putAll(includeChildren); + allData.putAll(this.thisTime); + initChildren(); + + }) + .then(); + } + + private void initChildren() { + childrenMapping.clear(); + + for (E value : allData.values()) { + if (service.isRootNode(value) || value.getId() == null) { + continue; + } + childrenMapping + .computeIfAbsent(value.getParentId(), ignore -> new LinkedHashMap<>()) + .put(value.getId(), value); + } + } + + private void checkCyclicDependency() { + for (E value : readyToSave.values()) { + checkCyclicDependency(value, new LinkedHashSet<>()); + } + } + + private void checkCyclicDependency(E val, Set container) { + if (!container.add(val.getId())) { + throw new ValidationException("parentId", "error.tree_entity_cyclic_dependency"); + } + Map children = childrenMapping.get(val.getId()); + if (MapUtils.isNotEmpty(children)) { + for (Map.Entry entry : children.entrySet()) { + checkCyclicDependency(entry.getValue(), container); + } + } + } + + private Mono checkParentId() { + + if (allData.isEmpty()) { + return Mono.empty(); + } + + Set readyToCheck = thisTime + .values() + .stream() + .map(TreeSupportEntity::getParentId) + .filter(e -> !ObjectUtils.isEmpty(e) && !allData.containsKey(e)) + .collect(Collectors.toSet()); + + if (readyToCheck.isEmpty()) { + return Mono.empty(); + } + return service + .createQuery() + .in("id", readyToCheck) + .fetch() + .doOnNext(e -> { + allData.put(e.getId(), e); + readyToCheck.remove(e.getId()); + }) + .then(Mono.fromRunnable(() -> { + if (!readyToCheck.isEmpty()) { + throw new ValidationException( + "error.tree_entity_parent_id_not_exist", + Collections.singletonList( + new ValidationException.Detail( + "parentId", + "error.tree_entity_parent_id_not_exist", + readyToCheck)) + ); + } + initChildren(); + })); + } + + private void refactorPath() { + Function> childGetter + = id -> childrenMapping + .getOrDefault(id, Collections.emptyMap()) + .values(); + + for (E data : thisTime.values()) { + E old = data.getId() == null ? null : oldData.get(data.getId()); + PK parentId = old != null ? old.getParentId() : data.getParentId(); + E oldParent = parentId == null ? null : allData.get(parentId); + //编辑节点 + if (old != null) { + PK newParentId = data.getParentId(); + //父节点发生变化,更新所有子节点path + if (newParentId != null && !newParentId.equals(parentId)) { + Consumer childConsumer = child -> { + //更新了父节点,但是同时也传入的对应的子节点 + E readyToUpdate = thisTime.get(child.getId()); + if (null != readyToUpdate) { + readyToUpdate.setPath(child.getPath()); + } + }; + + //变更到了顶级节点 + if (service.isRootNode(data)) { + data.setPath(RandomUtil.randomChar(4)); + this.refactorChildPath(old.getId(), data.getPath(), childConsumer); + //重新保存所有子节点 + putChildToReadyToSave(childGetter, old); + + } else { + E newParent = allData.get(newParentId); + if (null != newParent) { + data.setPath(newParent.getPath() + "-" + RandomUtil.randomChar(4)); + this.refactorChildPath(data.getId(), data.getPath(), childConsumer); + //重新保存所有子节点 + putChildToReadyToSave(childGetter, data); + } + } + } else { + if (oldParent != null) { + if (old.getPath().startsWith(oldParent.getPath())) { + data.setPath(old.getPath()); + } else { + data.setPath(oldParent.getPath() + "-" + RandomUtil.randomChar(4)); + } + } else { + data.setPath(old.getPath()); + } + } + } + + //新增节点 + else if (parentId != null) { + if (oldParent != null) { + data.setPath(oldParent.getPath() + "-" + RandomUtil.randomChar(4)); + } + } + } + + } + + private void putChildToReadyToSave(Function> childGetter, E data) { + childGetter + .apply(data.getId()) + .forEach(e -> { + readyToSave.put(e.getId(), e); + putChildToReadyToSave(childGetter, e); + }); + } + + private void refactor(E e) { + if (e.getPath() != null) { + e.setLevel(e.getPath().split("-").length); + } + } + + //重构子节点的path + private void refactorChildPath(PK id, String path, Consumer pathAccepter) { + + Collection children = childrenMapping.getOrDefault(id, Collections.emptyMap()).values(); + if (CollectionUtils.isEmpty(children)) { + return; + } + for (E child : children) { + if (ObjectUtils.isEmpty(path)) { + child.setPath(RandomUtil.randomChar(4)); + } else { + child.setPath(path + "-" + RandomUtil.randomChar(4)); + } + pathAccepter.accept(child); + this.refactorChildPath(child.getId(), child.getPath(), pathAccepter); + } + + } +} diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/sql/DefaultR2dbcExecutor.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/sql/DefaultR2dbcExecutor.java index 6e8ab1444..166d0645b 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/sql/DefaultR2dbcExecutor.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/sql/DefaultR2dbcExecutor.java @@ -10,9 +10,10 @@ import org.hswebframework.web.api.crud.entity.TransactionManagers; import org.hswebframework.web.datasource.DataSourceHolder; import org.hswebframework.web.datasource.R2dbcDataSource; +import org.hswebframework.web.exception.I18nSupportException; import org.reactivestreams.Publisher; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.r2dbc.connectionfactory.ConnectionFactoryUtils; +import org.springframework.r2dbc.connection.ConnectionFactoryUtils; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import reactor.core.publisher.Flux; @@ -27,6 +28,7 @@ public class DefaultR2dbcExecutor extends R2dbcReactiveSqlExecutor { @Autowired + @Setter private ConnectionFactory defaultFactory; @Setter @@ -48,6 +50,17 @@ protected SqlRequest convertRequest(SqlRequest sqlRequest) { return sqlRequest; } + @Override + protected Statement prepareStatement(Statement statement, SqlRequest request) { + try { + return super.prepareStatement(statement, request); + } catch (Throwable e) { + throw new I18nSupportException + .NoStackTrace("error.sql.prepare", e) + .withSource("sql.prepare", request); + } + } + protected void bindNull(Statement statement, int index, Class type) { if (type == Date.class) { type = LocalDateTime.class; @@ -60,11 +73,12 @@ protected void bindNull(Statement statement, int index, Class type) { } protected void bind(Statement statement, int index, Object value) { + if (value instanceof Date) { value = ((Date) value) - .toInstant() - .atZone(ZoneOffset.systemDefault()) - .toLocalDateTime(); + .toInstant() + .atZone(ZoneOffset.systemDefault()) + .toLocalDateTime(); } if (bindCustomSymbol) { statement.bind(getBindSymbol() + (index + getBindFirstIndex()), value); @@ -77,8 +91,8 @@ protected void bind(Statement statement, int index, Object value) { protected Mono getConnection() { if (DataSourceHolder.isDynamicDataSourceReady()) { return DataSourceHolder.currentR2dbc() - .flatMap(R2dbcDataSource::getNative) - .flatMap(ConnectionFactoryUtils::getConnection); + .flatMap(R2dbcDataSource::getNative) + .flatMap(ConnectionFactoryUtils::getConnection); } else { return ConnectionFactoryUtils.getConnection(defaultFactory); } @@ -116,7 +130,7 @@ public Mono update(SqlRequest request) { @Override @Transactional(transactionManager = TransactionManagers.reactiveTransactionManager) public Mono update(String sql, Object... args) { - return super.update(sql,args); + return super.update(sql, args); } @Override @@ -128,18 +142,18 @@ public Flux select(Publisher request, ResultWrapper wra @Override @Transactional(readOnly = true, transactionManager = TransactionManagers.reactiveTransactionManager) public Flux> select(String sql, Object... args) { - return super.select(sql,args); + return super.select(sql, args); } @Override @Transactional(readOnly = true, transactionManager = TransactionManagers.reactiveTransactionManager) public Flux select(String sql, ResultWrapper wrapper) { - return super.select(sql,wrapper); + return super.select(sql, wrapper); } @Override @Transactional(readOnly = true, transactionManager = TransactionManagers.reactiveTransactionManager) public Flux select(SqlRequest sqlRequest, ResultWrapper wrapper) { - return super.select(sqlRequest,wrapper); + return super.select(sqlRequest, wrapper); } } diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/sql/terms/TreeChildTermBuilder.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/sql/terms/TreeChildTermBuilder.java new file mode 100644 index 000000000..99dac1107 --- /dev/null +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/sql/terms/TreeChildTermBuilder.java @@ -0,0 +1,65 @@ +package org.hswebframework.web.crud.sql.terms; + +import org.hswebframework.ezorm.core.param.Term; +import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata; +import org.hswebframework.ezorm.rdb.operator.builder.fragments.BatchSqlFragments; +import org.hswebframework.ezorm.rdb.operator.builder.fragments.PrepareSqlFragments; +import org.hswebframework.ezorm.rdb.operator.builder.fragments.SqlFragments; +import org.hswebframework.ezorm.rdb.operator.builder.fragments.term.AbstractTermFragmentBuilder; + +import java.util.Arrays; +import java.util.List; + +/** + * 树结构相关数据查询条件构造器,用于构造根据树结构数据以及子节点查询相关联的数据, + * 如查询某个地区以及下级地区的数据. + * + * @author zhouhao + * @since 4.0.17 + */ +public abstract class TreeChildTermBuilder extends AbstractTermFragmentBuilder { + public TreeChildTermBuilder(String termType, String name) { + super(termType, name); + } + + protected abstract String tableName(); + + @Override + public SqlFragments createFragments(String columnFullName, RDBColumnMetadata column, Term term) { + List id = convertList(column, term); + + String tableName = getTableName(tableName(), column); + + String[] args = new String[id.size()]; + Arrays.fill(args, "?"); + + RDBColumnMetadata pathColumn = column + .getOwner() + .getSchema() + .getTable(tableName) + .flatMap(t -> t.getColumn("path")) + .orElseThrow(() -> new IllegalArgumentException("not found 'path' column")); + + RDBColumnMetadata idColumn = column + .getOwner() + .getSchema() + .getTable(tableName) + .flatMap(t -> t.getColumn("id")) + .orElseThrow(() -> new IllegalArgumentException("not found 'id' column")); + + BatchSqlFragments fragments = new BatchSqlFragments(2, 1); + if (term.getOptions().contains("not")) { + fragments.add(SqlFragments.NOT); + } + + return fragments + .addSql( + "exists(select 1 from", tableName, "_p join", tableName, + "_c on", idColumn.getFullName("_c"), "in(", String.join(",", args), ")", + "and", pathColumn.getFullName("_p"), "like concat(" + pathColumn.getFullName("_c") + ",'%')", + "where", columnFullName, "=", idColumn.getFullName("_p"), ")" + ) + .addParameter(id); + + } +} diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/utils/TransactionUtils.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/utils/TransactionUtils.java new file mode 100644 index 000000000..84e38bb16 --- /dev/null +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/utils/TransactionUtils.java @@ -0,0 +1,49 @@ +package org.hswebframework.web.crud.utils; + +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.transaction.NoTransactionException; +import org.springframework.transaction.reactive.TransactionContextManager; +import org.springframework.transaction.reactive.TransactionSynchronization; +import org.springframework.transaction.reactive.TransactionSynchronizationManager; +import reactor.core.publisher.Mono; + +import java.util.function.Function; + +@Slf4j +public class TransactionUtils { + + public static Mono afterCommit(Mono task) { + return TransactionUtils.registerSynchronization( + new TransactionSynchronization() { + @Override + @NonNull + public Mono afterCommit() { + return task; + } + }, + TransactionSynchronization::afterCommit + ); + } + + public static Mono registerSynchronization(TransactionSynchronization synchronization, + Function> whenNoTransaction) { + return TransactionSynchronizationManager + .forCurrentTransaction() + .flatMap(manager -> { + if (manager.isSynchronizationActive()) { + try { + manager.registerSynchronization(synchronization); + } catch (Throwable err) { + log.warn("register TransactionSynchronization [{}] error", synchronization, err); + return whenNoTransaction.apply(synchronization); + } + return Mono.empty(); + } else { + log.info("transaction is not active,execute TransactionSynchronization [{}] immediately.", synchronization); + return whenNoTransaction.apply(synchronization); + } + }) + .onErrorResume(NoTransactionException.class, err -> whenNoTransaction.apply(synchronization)); + } +} diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonErrorControllerAdvice.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonErrorControllerAdvice.java index fef9f27e7..070cf27af 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonErrorControllerAdvice.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonErrorControllerAdvice.java @@ -14,6 +14,7 @@ import org.hswebframework.web.logger.ReactiveLogger; import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.transaction.TransactionException; import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; @@ -46,63 +47,69 @@ public class CommonErrorControllerAdvice { @ExceptionHandler @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public Mono> handleException(TransactionException e) { - log.error(e.getLocalizedMessage(), e); + log.warn(e.getLocalizedMessage(), e); return LocaleUtils - .resolveMessageReactive("error.internal_server_error") - .map(msg -> ResponseMessage.error(500, "error." + e.getClass().getSimpleName(), msg)); + .resolveMessageReactive("error.internal_server_error") + .map(msg -> ResponseMessage.error(500, "error." + e.getClass().getSimpleName(), msg)); } @ExceptionHandler - @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) - public Mono> handleException(BusinessException e) { + public Mono>> handleException(BusinessException e) { return LocaleUtils - .resolveThrowable(e, - (err, msg) -> ResponseMessage.error(err.getStatus(), err.getCode(), msg)); + .resolveThrowable(e, + (err, msg) -> ResponseMessage.error(err.getStatus(), err.getCode(), msg)) + .map(msg -> { + HttpStatus status = HttpStatus.resolve(msg.getStatus()); + return ResponseEntity + .status(status == null ? HttpStatus.BAD_REQUEST : status) + .body(msg); + }); } @ExceptionHandler - @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + @ResponseStatus(HttpStatus.BAD_REQUEST) public Mono> handleException(UnsupportedOperationException e) { + log.warn(e.getLocalizedMessage(), e); return LocaleUtils - .resolveThrowable(e, (err, msg) -> (ResponseMessage.error(500, CodeConstants.Error.unsupported, msg))) - .doOnEach(ReactiveLogger.onNext(r -> log.error(e.getLocalizedMessage(), e))); + .resolveThrowable(e, (err, msg) -> + (ResponseMessage.error(400, CodeConstants.Error.unsupported, msg))); } @ExceptionHandler @ResponseStatus(HttpStatus.UNAUTHORIZED) public Mono> handleException(UnAuthorizedException e) { return LocaleUtils - .resolveThrowable(e, (err, msg) -> (ResponseMessage - .error(401, CodeConstants.Error.unauthorized, msg) - .result(e.getState()))); + .resolveThrowable(e, (err, msg) -> (ResponseMessage + .error(401, CodeConstants.Error.unauthorized, msg) + .result(e.getState()))); } @ExceptionHandler @ResponseStatus(HttpStatus.FORBIDDEN) public Mono> handleException(AccessDenyException e) { return LocaleUtils - .resolveThrowable(e, (err, msg) -> ResponseMessage.error(403, e.getCode(), msg)) - ; + .resolveThrowable(e, (err, msg) -> ResponseMessage.error(403, e.getCode(), msg)) + ; } @ExceptionHandler @ResponseStatus(HttpStatus.NOT_FOUND) public Mono> handleException(NotFoundException e) { return LocaleUtils - .resolveThrowable(e, (err, msg) -> ResponseMessage.error(404, CodeConstants.Error.not_found, msg)) - ; + .resolveThrowable(e, (err, msg) -> ResponseMessage.error(404, CodeConstants.Error.not_found, msg)) + ; } @ExceptionHandler @ResponseStatus(HttpStatus.BAD_REQUEST) public Mono>> handleException(ValidationException e) { return LocaleUtils - .currentReactive() - .map(locale -> ResponseMessage - .>error(400, - CodeConstants.Error.illegal_argument, - e.getLocalizedMessage(locale)) - .result(e.getDetails(locale))); + .currentReactive() + .map(locale -> ResponseMessage + .>error(400, + CodeConstants.Error.illegal_argument, + e.getLocalizedMessage(locale)) + .result(e.getDetails(locale))); } @ExceptionHandler @@ -146,10 +153,10 @@ private Mono>> handleBindingRes message = CodeConstants.Error.illegal_argument; } List details = result - .getFieldErrors() - .stream() - .map(err -> new ValidationException.Detail(err.getField(), err.getDefaultMessage(), null)) - .collect(Collectors.toList()); + .getFieldErrors() + .stream() + .map(err -> new ValidationException.Detail(err.getField(), err.getDefaultMessage(), null)) + .collect(Collectors.toList()); return handleException(new ValidationException(message, details)); } @@ -163,8 +170,8 @@ public Mono> handleException(javax.validation.ValidationExcep @ResponseStatus(HttpStatus.GATEWAY_TIMEOUT) public Mono> handleException(TimeoutException e) { return LocaleUtils - .resolveThrowable(e, (err, msg) -> ResponseMessage.error(504, CodeConstants.Error.timeout, msg)) - .doOnEach(ReactiveLogger.onNext(r -> log.error(e.getLocalizedMessage(), e))); + .resolveThrowable(e, (err, msg) -> ResponseMessage.error(504, CodeConstants.Error.timeout, msg)) + .doOnEach(ReactiveLogger.onNext(r -> log.warn(e.getLocalizedMessage(), e))); } @ExceptionHandler @@ -172,16 +179,17 @@ public Mono> handleException(TimeoutException e) { @Order public Mono> handleException(RuntimeException e) { return LocaleUtils - .resolveThrowable(e, (err, msg) -> ResponseMessage.error(msg)) - .doOnEach(ReactiveLogger.onNext(r -> log.error(e.getLocalizedMessage(), e))); + .resolveThrowable(e, (err, msg) -> { + log.warn(msg, e); + return ResponseMessage.error(msg); + }); } @ExceptionHandler @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public Mono> handleException(NullPointerException e) { - - return Mono.just(ResponseMessage.error(e.getMessage())) - .doOnEach(ReactiveLogger.onNext(r -> log.error(e.getLocalizedMessage(), e))); + log.warn(e.getLocalizedMessage(), e); + return Mono.just(ResponseMessage.error(e.getMessage())); } @ExceptionHandler @@ -189,51 +197,53 @@ public Mono> handleException(NullPointerException e) { public Mono> handleException(IllegalArgumentException e) { return LocaleUtils - .resolveThrowable(e, (err, msg) -> ResponseMessage.error(400, CodeConstants.Error.illegal_argument, msg)) - .doOnEach(ReactiveLogger.onNext(r -> log.error(e.getLocalizedMessage(), e))) - ; + .resolveThrowable(e, (err, msg) -> { + log.warn(msg, e); + return ResponseMessage.error(400, CodeConstants.Error.illegal_argument, msg); + }); } @ExceptionHandler @ResponseStatus(HttpStatus.BAD_REQUEST) public Mono> handleException(AuthenticationException e) { return LocaleUtils - .resolveThrowable(e, (err, msg) -> ResponseMessage.error(400, err.getCode(), msg)) - ; + .resolveThrowable(e, (err, msg) -> ResponseMessage.error(400, err.getCode(), msg)); } @ExceptionHandler @ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE) public Mono> handleException(UnsupportedMediaTypeStatusException e) { + log.warn(e.getLocalizedMessage(), e); + return LocaleUtils - .resolveMessageReactive("error.unsupported_media_type") - .map(msg -> ResponseMessage - .error(415, "unsupported_media_type", msg) - .result(e.getSupportedMediaTypes())) - .doOnEach(ReactiveLogger.onNext(r -> log.error(e.getLocalizedMessage(), e))); + .resolveMessageReactive("error.unsupported_media_type") + .map(msg -> ResponseMessage + .error(415, "unsupported_media_type", msg) + .result(e.getSupportedMediaTypes())); } @ExceptionHandler @ResponseStatus(HttpStatus.NOT_ACCEPTABLE) public Mono> handleException(NotAcceptableStatusException e) { + log.warn(e.getLocalizedMessage(), e); return LocaleUtils - .resolveMessageReactive("error.not_acceptable_media_type") - .map(msg -> ResponseMessage - .error(406, "not_acceptable_media_type", msg) - .result(e.getSupportedMediaTypes())) - .doOnEach(ReactiveLogger.onNext(r -> log.error(e.getLocalizedMessage(), e))); + .resolveMessageReactive("error.not_acceptable_media_type") + .map(msg -> ResponseMessage + .error(406, "not_acceptable_media_type", msg) + .result(e.getSupportedMediaTypes())); } @ExceptionHandler @ResponseStatus(HttpStatus.NOT_ACCEPTABLE) public Mono> handleException(MethodNotAllowedException e) { + log.warn(e.getLocalizedMessage(), e); + return LocaleUtils - .resolveMessageReactive("error.method_not_allowed") - .map(msg -> ResponseMessage - .error(406, "method_not_allowed", msg) - .result(e.getSupportedMethods())) - .doOnEach(ReactiveLogger.onNext(r -> log.error(e.getLocalizedMessage(), e))); + .resolveMessageReactive("error.method_not_allowed") + .map(msg -> ResponseMessage + .error(406, "method_not_allowed", msg) + .result(e.getSupportedMethods())); } @@ -250,12 +260,12 @@ public Mono>> handleException(S } while (exception != null && exception != e); if (exception == null) { return Mono.just( - ResponseMessage.error(400, CodeConstants.Error.illegal_argument, e.getMessage()) + ResponseMessage.error(400, CodeConstants.Error.illegal_argument, e.getMessage()) ); } return LocaleUtils - .resolveThrowable(exception, - (err, msg) -> ResponseMessage.error(400, CodeConstants.Error.illegal_argument, msg)); + .resolveThrowable(exception, + (err, msg) -> ResponseMessage.error(400, CodeConstants.Error.illegal_argument, msg)); } @ExceptionHandler diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonWebFluxConfiguration.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonWebFluxConfiguration.java index bcbdd9c84..562cef16a 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonWebFluxConfiguration.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonWebFluxConfiguration.java @@ -1,6 +1,7 @@ package org.hswebframework.web.crud.web; import org.hswebframework.web.i18n.WebFluxLocaleFilter; +import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -13,7 +14,7 @@ import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import org.springframework.web.server.WebFilter; -@Configuration +@AutoConfiguration @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) public class CommonWebFluxConfiguration { diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonWebMvcConfiguration.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonWebMvcConfiguration.java index cb72e93e4..7268e4f67 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonWebMvcConfiguration.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonWebMvcConfiguration.java @@ -1,5 +1,6 @@ package org.hswebframework.web.crud.web; +import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -8,7 +9,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -@Configuration +@AutoConfiguration @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) @ConditionalOnClass(org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice.class) public class CommonWebMvcConfiguration { diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonWebMvcErrorControllerAdvice.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonWebMvcErrorControllerAdvice.java index 731ee3acb..f18dcb352 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonWebMvcErrorControllerAdvice.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonWebMvcErrorControllerAdvice.java @@ -54,7 +54,7 @@ public ResponseMessage handleException(BusinessException err) { @ExceptionHandler @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ResponseMessage handleException(UnsupportedOperationException e) { - log.error(e.getLocalizedMessage(), e); + log.warn(e.getLocalizedMessage(), e); String msg = resolveMessage(e); return ResponseMessage.error(500, CodeConstants.Error.unsupported, msg); } @@ -149,21 +149,21 @@ public ResponseMessage handleException(TimeoutException e) { @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @Order public ResponseMessage handleException(RuntimeException e) { - log.error(e.getLocalizedMessage(), e); + log.warn(e.getLocalizedMessage(), e); return ResponseMessage.error(resolveMessage(e)); } @ExceptionHandler @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ResponseMessage handleException(NullPointerException e) { - log.error(e.getLocalizedMessage(), e); + log.warn(e.getLocalizedMessage(), e); return ResponseMessage.error(e.getMessage()); } @ExceptionHandler @ResponseStatus(HttpStatus.BAD_REQUEST) public ResponseMessage handleException(IllegalArgumentException e) { - log.error(e.getLocalizedMessage(), e); + log.warn(e.getLocalizedMessage(), e); return ResponseMessage.error(400, CodeConstants.Error.illegal_argument, resolveMessage(e)); } @@ -171,7 +171,7 @@ public ResponseMessage handleException(IllegalArgumentException e) { @ExceptionHandler @ResponseStatus(HttpStatus.BAD_REQUEST) public ResponseMessage handleException(AuthenticationException e) { - log.error(e.getLocalizedMessage(), e); + log.warn(e.getLocalizedMessage(), e); return ResponseMessage.error(400, e.getCode(), resolveMessage(e)); } @@ -179,7 +179,7 @@ public ResponseMessage handleException(AuthenticationException e) { @ExceptionHandler @ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE) public ResponseMessage handleException(UnsupportedMediaTypeStatusException e) { - log.error(e.getLocalizedMessage(), e); + log.warn(e.getLocalizedMessage(), e); return ResponseMessage .error(415, "unsupported_media_type", LocaleUtils.resolveMessage("error.unsupported_media_type")) @@ -189,7 +189,7 @@ public ResponseMessage handleException(UnsupportedMediaTypeStatusExcepti @ExceptionHandler @ResponseStatus(HttpStatus.NOT_ACCEPTABLE) public ResponseMessage handleException(NotAcceptableStatusException e) { - log.error(e.getLocalizedMessage(), e); + log.warn(e.getLocalizedMessage(), e); return ResponseMessage .error(406, "not_acceptable_media_type", LocaleUtils @@ -200,7 +200,7 @@ public ResponseMessage handleException(NotAcceptableStatusException e) { @ExceptionHandler @ResponseStatus(HttpStatus.NOT_ACCEPTABLE) public ResponseMessage handleException(MethodNotAllowedException e) { - log.error(e.getLocalizedMessage(), e); + log.warn(e.getLocalizedMessage(), e); return ResponseMessage .error(406, "method_not_allowed", LocaleUtils.resolveMessage("error.method_not_allowed")) diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/QueryController.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/QueryController.java index 98ec2cc13..5bb2b4f3a 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/QueryController.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/QueryController.java @@ -169,7 +169,7 @@ default int count(@Parameter(hidden = true) QueryParamEntity query) { default E getById(@PathVariable K id) { return getRepository() .findById(id) - .orElseThrow(NotFoundException::new); + .orElseThrow(NotFoundException.NoStackTrace::new); } } diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/ResponseMessageWrapper.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/ResponseMessageWrapper.java index 37e058d48..9610bc1a5 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/ResponseMessageWrapper.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/ResponseMessageWrapper.java @@ -37,7 +37,7 @@ public ResponseMessageWrapper(List> writers, static { try { param = new MethodParameter(ResponseMessageWrapper.class - .getDeclaredMethod("methodForParams"), -1); + .getDeclaredMethod("methodForParams"), -1); } catch (NoSuchMethodException e) { e.printStackTrace(); } @@ -69,24 +69,24 @@ public boolean supports(@NonNull HandlerResult result) { boolean isAlreadyResponse = gen == ResponseMessage.class || gen == ResponseEntity.class; boolean isStream = result.getReturnType().resolve() == Mono.class - || result.getReturnType().resolve() == Flux.class; + || result.getReturnType().resolve() == Flux.class; RequestMapping mapping = result.getReturnTypeSource() - .getMethodAnnotation(RequestMapping.class); + .getMethodAnnotation(RequestMapping.class); if (mapping == null) { return false; } for (String produce : mapping.produces()) { MimeType mimeType = MimeType.valueOf(produce); if (MediaType.TEXT_EVENT_STREAM.includes(mimeType) || - MediaType.APPLICATION_STREAM_JSON.includes(mimeType)) { + MediaType.APPLICATION_NDJSON.includes(mimeType)) { return false; } } return isStream - && super.supports(result) - && !isAlreadyResponse; + && super.supports(result) + && !isAlreadyResponse; } @Override @@ -94,24 +94,31 @@ public boolean supports(@NonNull HandlerResult result) { public Mono handleResult(ServerWebExchange exchange, HandlerResult result) { Object body = result.getReturnValue(); - if (exchange - .getRequest() - .getHeaders() - .getAccept() - .contains(MediaType.TEXT_EVENT_STREAM)) { - return writeBody(body, param, exchange); + List accept = exchange.getRequest().getHeaders().getAccept(); + + if (accept.contains(MediaType.TEXT_EVENT_STREAM)|| + accept.contains(MediaType.APPLICATION_NDJSON)) { + return writeBody(body, result.getReturnTypeSource(), exchange); + } + + String ignoreWrapper = exchange + .getRequest() + .getHeaders() + .getFirst("X-Response-Wrapper"); + if ("Ignore".equals(ignoreWrapper)) { + return writeBody(body, result.getReturnTypeSource(), exchange); } if (body instanceof Mono) { body = ((Mono) body) - .map(ResponseMessage::ok) - .switchIfEmpty(Mono.just(ResponseMessage.ok())); + .map(ResponseMessage::ok) + .switchIfEmpty(Mono.just(ResponseMessage.ok())); } if (body instanceof Flux) { body = ((Flux) body) - .collectList() - .map(ResponseMessage::ok) - .switchIfEmpty(Mono.just(ResponseMessage.ok())); + .collectList() + .map(ResponseMessage::ok) + .switchIfEmpty(Mono.just(ResponseMessage.ok())); } if (body == null) { diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/reactive/ReactiveDeleteController.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/reactive/ReactiveDeleteController.java index 9bfa0672c..75f06da11 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/reactive/ReactiveDeleteController.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/reactive/ReactiveDeleteController.java @@ -19,7 +19,7 @@ public interface ReactiveDeleteController { default Mono delete(@PathVariable K id) { return getRepository() .findById(Mono.just(id)) - .switchIfEmpty(Mono.error(NotFoundException::new)) + .switchIfEmpty(Mono.error(NotFoundException.NoStackTrace::new)) .flatMap(e -> getRepository() .deleteById(Mono.just(id)) .thenReturn(e)); diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/reactive/ReactiveQueryController.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/reactive/ReactiveQueryController.java index b568cad64..6f9e2b5cc 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/reactive/ReactiveQueryController.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/reactive/ReactiveQueryController.java @@ -160,7 +160,7 @@ default Mono count(QueryParamEntity query) { default Mono getById(@PathVariable K id) { return getRepository() .findById(Mono.just(id)) - .switchIfEmpty(Mono.error(NotFoundException::new)); + .switchIfEmpty(Mono.error(NotFoundException.NoStackTrace::new)); } } diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/reactive/ReactiveServiceDeleteController.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/reactive/ReactiveServiceDeleteController.java index 7908d9659..ba3074f0e 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/reactive/ReactiveServiceDeleteController.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/reactive/ReactiveServiceDeleteController.java @@ -19,7 +19,7 @@ public interface ReactiveServiceDeleteController { default Mono delete(@PathVariable K id) { return getService() .findById(Mono.just(id)) - .switchIfEmpty(Mono.error(NotFoundException::new)) + .switchIfEmpty(Mono.error(NotFoundException.NoStackTrace::new)) .flatMap(e -> getService() .deleteById(Mono.just(id)) .thenReturn(e)); diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/reactive/ReactiveServiceQueryController.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/reactive/ReactiveServiceQueryController.java index d798dacad..227849213 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/reactive/ReactiveServiceQueryController.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/reactive/ReactiveServiceQueryController.java @@ -10,6 +10,8 @@ import org.hswebframework.web.authorization.annotation.QueryAction; import org.hswebframework.web.crud.service.ReactiveCrudService; import org.hswebframework.web.exception.NotFoundException; +import org.hswebframework.web.exception.TraceSourceException; +import org.springframework.util.ClassUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -37,12 +39,12 @@ public interface ReactiveServiceQueryController { @GetMapping("/_query/no-paging") @QueryAction @QueryNoPagingOperation(summary = "使用GET方式分页动态查询(不返回总数)", - description = "此操作不返回分页总数,如果需要获取全部数据,请设置参数paging=false") + description = "此操作不返回分页总数,如果需要获取全部数据,请设置参数paging=false") default Flux query(@Parameter(hidden = true) QueryParamEntity query) { return getService() - .createQuery() - .setParam(query) - .fetch(); + .createQuery() + .setParam(query) + .fetch(); } /** @@ -73,7 +75,7 @@ default Flux query(@Parameter(hidden = true) QueryParamEntity query) { @PostMapping("/_query/no-paging") @QueryAction @Operation(summary = "使用POST方式分页动态查询(不返回总数)", - description = "此操作不返回分页总数,如果需要获取全部数据,请设置参数paging=false") + description = "此操作不返回分页总数,如果需要获取全部数据,请设置参数paging=false") default Flux query(@RequestBody Mono query) { return query.flatMapMany(this::query); } @@ -95,11 +97,11 @@ default Flux query(@RequestBody Mono query) { default Mono> queryPager(@Parameter(hidden = true) QueryParamEntity query) { if (query.getTotal() != null) { return getService() - .createQuery() - .setParam(query.rePaging(query.getTotal())) - .fetch() - .collectList() - .map(list -> PagerResult.of(query.getTotal(), list, query)); + .createQuery() + .setParam(query.rePaging(query.getTotal())) + .fetch() + .collectList() + .map(list -> PagerResult.of(query.getTotal(), list, query)); } return getService().queryPager(query); @@ -195,11 +197,11 @@ default Mono count(@Parameter(hidden = true) QueryParamEntity query) { @Operation(summary = "使用POST方式判断数据是否存在") default Mono exists(@RequestBody Mono query) { return query - .flatMap(param -> getService() - .createQuery() - .setParam(param) - .fetchOne() - .hasElement()); + .flatMap(param -> getService() + .createQuery() + .setParam(param) + .fetchOne() + .hasElement()); } /** @@ -239,8 +241,11 @@ default Mono exists(@Parameter(hidden = true) QueryParamEntity query) { @Operation(summary = "根据ID查询") default Mono getById(@PathVariable K id) { return getService() - .findById(id) - .switchIfEmpty(Mono.error(NotFoundException::new)); + .findById(id) + .switchIfEmpty(Mono.error(() -> new NotFoundException + .NoStackTrace("error.data.find.not_found", id) + .withSource(ClassUtils.getUserClass(this).getCanonicalName() + ".getById", id))); } + } diff --git a/hsweb-commons/hsweb-commons-crud/src/main/resources/META-INF/services/org.hswebframework.web.exception.analyzer.ExceptionAnalyzer b/hsweb-commons/hsweb-commons-crud/src/main/resources/META-INF/services/org.hswebframework.web.exception.analyzer.ExceptionAnalyzer new file mode 100644 index 000000000..bc8746972 --- /dev/null +++ b/hsweb-commons/hsweb-commons-crud/src/main/resources/META-INF/services/org.hswebframework.web.exception.analyzer.ExceptionAnalyzer @@ -0,0 +1 @@ +org.hswebframework.web.crud.exception.DatabaseExceptionAnalyzerReporter \ No newline at end of file diff --git a/hsweb-commons/hsweb-commons-crud/src/main/resources/META-INF/spring.factories b/hsweb-commons/hsweb-commons-crud/src/main/resources/META-INF/spring.factories deleted file mode 100644 index 0fe7a61e4..000000000 --- a/hsweb-commons/hsweb-commons-crud/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1,7 +0,0 @@ -# Auto Configure -org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -org.hswebframework.web.crud.configuration.EasyormConfiguration,\ -org.hswebframework.web.crud.configuration.JdbcSqlExecutorConfiguration,\ -org.hswebframework.web.crud.configuration.R2dbcSqlExecutorConfiguration,\ -org.hswebframework.web.crud.web.CommonWebFluxConfiguration,\ -org.hswebframework.web.crud.web.CommonWebMvcConfiguration \ No newline at end of file diff --git a/hsweb-commons/hsweb-commons-crud/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hsweb-commons/hsweb-commons-crud/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..b54296e48 --- /dev/null +++ b/hsweb-commons/hsweb-commons-crud/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,5 @@ +org.hswebframework.web.crud.configuration.EasyormConfiguration +org.hswebframework.web.crud.configuration.JdbcSqlExecutorConfiguration +org.hswebframework.web.crud.configuration.R2dbcSqlExecutorConfiguration +org.hswebframework.web.crud.web.CommonWebFluxConfiguration +org.hswebframework.web.crud.web.CommonWebMvcConfiguration \ No newline at end of file diff --git a/hsweb-commons/hsweb-commons-crud/src/main/resources/i18n/commons/messages_en.properties b/hsweb-commons/hsweb-commons-crud/src/main/resources/i18n/commons/messages_en.properties index 8e5ffa046..e8142ce63 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/resources/i18n/commons/messages_en.properties +++ b/hsweb-commons/hsweb-commons-crud/src/main/resources/i18n/commons/messages_en.properties @@ -3,4 +3,8 @@ error.not_acceptable_media_type=Not acceptable media type error.method_not_allowed=Method not allowed error.duplicate_data=Duplicate data error.data_error=Data error -error.internal_server_error = Internal server error \ No newline at end of file +error.internal_server_error = Internal server error +error.tree_entity_cyclic_dependency=Cannot modify parent node as oneself or one's own child node +error.tree_entity_parent_id_not_exist=Parent node does not exist or has been deleted +error.data.find.not_found=Data not found +error.sql.prepare.failed.IndexOutOfBoundsException=Execute SQL failed, try check config: `easyorm.dialect`. \ No newline at end of file diff --git a/hsweb-commons/hsweb-commons-crud/src/main/resources/i18n/commons/messages_zh.properties b/hsweb-commons/hsweb-commons-crud/src/main/resources/i18n/commons/messages_zh.properties index 946a0c0eb..28b2e85ae 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/resources/i18n/commons/messages_zh.properties +++ b/hsweb-commons/hsweb-commons-crud/src/main/resources/i18n/commons/messages_zh.properties @@ -1,6 +1,10 @@ -error.unsupported_media_type=不支持的请求类型 -error.not_acceptable_media_type=不支持的媒体类型 -error.method_not_allowed=不支持的请求方法 -error.duplicate_data=重复的数据 -error.data_error=数据错误 -error.internal_server_error=服务器内部错误 \ No newline at end of file +error.unsupported_media_type=\u4E0D\u652F\u6301\u7684\u8BF7\u6C42\u7C7B\u578B +error.not_acceptable_media_type=\u4E0D\u652F\u6301\u7684\u5A92\u4F53\u7C7B\u578B +error.method_not_allowed=\u4E0D\u652F\u6301\u7684\u8BF7\u6C42\u65B9\u6CD5 +error.duplicate_data=\u91CD\u590D\u7684\u6570\u636E +error.data_error=\u6570\u636E\u9519\u8BEF +error.internal_server_error=\u670D\u52A1\u5668\u5185\u90E8\u9519\u8BEF +error.tree_entity_cyclic_dependency=\u4E0D\u80FD\u4FEE\u6539\u7236\u8282\u70B9\u4E3A\u81EA\u5DF1\u6216\u8005\u81EA\u5DF1\u7684\u5B50\u8282\u70B9 +error.tree_entity_parent_id_not_exist=\u7236\u8282\u70B9\u4E0D\u5B58\u5728\u6216\u5DF2\u88AB\u5220\u9664 +error.data.find.not_found=\u6570\u636E\u4E0D\u5B58\u5728 +error.sql.prepare.failed.IndexOutOfBoundsException=SQL\u6267\u884C\u5931\u8D25,\u8BF7\u5C1D\u8BD5\u68C0\u67E5`easyorm.dialect`\u914D\u7F6E. \ No newline at end of file diff --git a/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/CrudTests.java b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/CrudTests.java index e172ea4ab..697f7eeff 100644 --- a/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/CrudTests.java +++ b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/CrudTests.java @@ -2,12 +2,14 @@ import org.hswebframework.web.crud.entity.CustomTestEntity; import org.hswebframework.web.crud.entity.TestEntity; +import org.hswebframework.web.crud.events.EntityBeforeModifyEvent; import org.hswebframework.web.crud.service.TestEntityService; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.event.EventListener; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -27,18 +29,27 @@ public void test() { entity.setExt("xxx"); entity.setAge(1); entity.setName("test"); - - Mono.just(entity) - .cast(TestEntity.class) - .as(service::insert) - .as(StepVerifier::create) - .expectNext(1) - .verifyComplete(); + + entity.setExtension("extName", "test"); + + service.insert(entity) + .as(StepVerifier::create) + .expectNext(1) + .verifyComplete(); Assert.assertNotNull(entity.getId()); service.findById(entity.getId()) + .doOnNext(System.out::println) .as(StepVerifier::create) .expectNextMatches(e -> e instanceof CustomTestEntity) .verifyComplete(); + + service.createUpdate() + .set("name", "test2") + .where("id", entity.getId()) + .execute() + .as(StepVerifier::create) + .expectNext(1) + .verifyComplete(); } } diff --git a/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/entity/CustomTestEntity.java b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/entity/CustomTestEntity.java index a66d07edb..f9be2488d 100644 --- a/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/entity/CustomTestEntity.java +++ b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/entity/CustomTestEntity.java @@ -6,6 +6,7 @@ import lombok.Setter; import org.hswebframework.web.api.crud.entity.GenericEntity; import org.hswebframework.web.bean.ToString; +import org.hswebframework.web.crud.annotation.EnableEntityEvent; import org.hswebframework.web.crud.generator.Generators; import javax.persistence.Column; @@ -16,6 +17,7 @@ @Setter @AllArgsConstructor(staticName = "of") @NoArgsConstructor +@EnableEntityEvent public class CustomTestEntity extends TestEntity { diff --git a/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/entity/TestEntity.java b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/entity/TestEntity.java index b7e942ad3..ee36be473 100644 --- a/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/entity/TestEntity.java +++ b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/entity/TestEntity.java @@ -4,12 +4,10 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.hswebframework.web.api.crud.entity.GenericEntity; +import org.hswebframework.web.api.crud.entity.ExtendableEntity; import org.hswebframework.web.crud.annotation.EnableEntityEvent; -import org.hswebframework.web.crud.generator.Generators; import javax.persistence.Column; -import javax.persistence.GeneratedValue; import javax.persistence.Table; @Getter @@ -18,7 +16,7 @@ @AllArgsConstructor(staticName = "of") @NoArgsConstructor @EnableEntityEvent -public class TestEntity extends GenericEntity { +public class TestEntity extends ExtendableEntity { @Column(length = 32) private String name; diff --git a/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/entity/TestTreeSortEntity.java b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/entity/TestTreeSortEntity.java index 77bbf1106..62c8ac40f 100644 --- a/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/entity/TestTreeSortEntity.java +++ b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/entity/TestTreeSortEntity.java @@ -2,24 +2,35 @@ import lombok.Getter; import lombok.Setter; +import org.hswebframework.ezorm.rdb.mapping.annotation.DefaultValue; import org.hswebframework.web.api.crud.entity.GenericTreeSortSupportEntity; import org.hswebframework.web.api.crud.entity.TreeSupportEntity; +import org.hswebframework.web.validator.CreateGroup; import javax.persistence.Column; import javax.persistence.Table; +import javax.validation.constraints.NotBlank; import java.util.List; @Getter @Setter @Table(name = "test_tree_sort") -public class TestTreeSortEntity extends GenericTreeSortSupportEntity { +public class TestTreeSortEntity extends GenericTreeSortSupportEntity { @Column private String name; + @Column(nullable = false) + @NotBlank(groups = CreateGroup.class) + @DefaultValue("test") + private String defaultTest; private List children; + @Override + public String toString() { + return "TestTreeSortEntity{}"; + } } diff --git a/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/events/EntityEventListenerTest.java b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/events/EntityEventListenerTest.java index 4f85284a6..3fe9c75da 100644 --- a/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/events/EntityEventListenerTest.java +++ b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/events/EntityEventListenerTest.java @@ -5,6 +5,7 @@ import org.hswebframework.web.crud.TestApplication; import org.hswebframework.web.crud.entity.EventTestEntity; import org.junit.Assert; +import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; @@ -37,6 +38,11 @@ public class EntityEventListenerTest { @Autowired private TestEntityListener listener; + @Before + public void before() { + listener.reset(); + } + @Test public void test() { Mono.just(EventTestEntity.of("test", 1)) @@ -49,25 +55,83 @@ public void test() { } + @Test + public void testPrepareModifySetNull() { + EventTestEntity entity = EventTestEntity.of("prepare-setNull", 20); + reactiveRepository + .insert(entity) + .as(StepVerifier::create) + .expectNext(1) + .verifyComplete(); + Assert.assertEquals(listener.created.getAndSet(0), 1); + + reactiveRepository + .createUpdate() + .set("name", "prepare-setNull-set") + .setNull("age") + .where("id", entity.getId()) + .execute() + .as(StepVerifier::create) + .expectNextCount(1) + .verifyComplete(); + + reactiveRepository + .findById(entity.getId()) + .mapNotNull(EventTestEntity::getAge) + .as(StepVerifier::create) + .expectComplete() + .verify(); + + } + + @Test + public void testPrepareModify() { + EventTestEntity entity = EventTestEntity.of("prepare", 10); + reactiveRepository + .insert(entity) + .as(StepVerifier::create) + .expectNext(1) + .verifyComplete(); + Assert.assertEquals(listener.created.getAndSet(0), 1); + + reactiveRepository + .createUpdate() + .set("name", "prepare-xx") + .set("age", 20) + .where("id", entity.getId()) + .execute() + .as(StepVerifier::create) + .expectNextCount(1) + .verifyComplete(); + + reactiveRepository + .findById(entity.getId()) + .map(EventTestEntity::getName) + .as(StepVerifier::create) + .expectNext("prepare-0") + .verifyComplete(); + + } + @Test public void testUpdateNative() { - EventTestEntity entity =EventTestEntity.of("test-update-native", null); + EventTestEntity entity = EventTestEntity.of("test-update-native", null); reactiveRepository - .insert(entity) - .as(StepVerifier::create) - .expectNext(1) - .verifyComplete(); + .insert(entity) + .as(StepVerifier::create) + .expectNext(1) + .verifyComplete(); Assert.assertEquals(listener.created.getAndSet(0), 1); reactiveRepository - .createUpdate() - .set(EventTestEntity::getAge, NativeSql.of("coalesce(age+1,?)",10)) - .where() - .is(entity::getName) - .execute() - .as(StepVerifier::create) - .expectNext(1) - .verifyComplete(); + .createUpdate() + .set(EventTestEntity::getAge, NativeSql.of("coalesce(age+1,?)", 10)) + .where() + .is(entity::getName) + .execute() + .as(StepVerifier::create) + .expectNext(1) + .verifyComplete(); Assert.assertEquals(1, listener.modified.getAndSet(0)); @@ -94,10 +158,10 @@ public void testInsertBatch() { Assert.assertEquals(listener.beforeCreate.getAndSet(0), 2); reactiveRepository - .createUpdate().set("age", 3).where().in("name", "test2", "test3").execute() - .as(StepVerifier::create) - .expectNext(2) - .verifyComplete(); + .createUpdate().set("age", 3).where().in("name", "test2", "test3").execute() + .as(StepVerifier::create) + .expectNext(2) + .verifyComplete(); Assert.assertEquals(listener.modified.getAndSet(0), 2); Assert.assertEquals(listener.beforeModify.getAndSet(0), 2); diff --git a/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/events/TestEntityListener.java b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/events/TestEntityListener.java index d141667d7..33f097fa0 100644 --- a/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/events/TestEntityListener.java +++ b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/events/TestEntityListener.java @@ -23,6 +23,19 @@ public class TestEntityListener { AtomicInteger beforeSave = new AtomicInteger(); AtomicInteger beforeQuery = new AtomicInteger(); + void reset(){ + beforeCreate.set(0); + beforeDelete.set(0); + created.set(0); + deleted.set(0); + modified.set(0); + beforeModify.set(0); + saved.set(0); + beforeSave.set(0); + beforeQuery.set(0); + + } + @EventListener public void handleBeforeQuery(EntityBeforeQueryEvent event){ event.async(Mono.fromRunnable(() -> { @@ -79,6 +92,18 @@ public void handleCreated(EntityDeletedEvent event) { })); } + @EventListener + public void handlePrepareModify(EntityPrepareModifyEvent event) { + event.async(Mono.fromRunnable(() -> { + System.out.println(event); + for (EventTestEntity eventTestEntity : event.getAfter()) { + if(eventTestEntity.getName().equals("prepare-xx")){ + eventTestEntity.setName("prepare-0"); + eventTestEntity.setAge(null); + } + } + })); + } @EventListener public void handleModify(EntityModifyEvent event) { event.async(Mono.fromRunnable(() -> { diff --git a/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/events/expr/SpelSqlExpressionInvokerTest.java b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/events/expr/SpelSqlExpressionInvokerTest.java index 6a5a63ba2..3a9ea8172 100644 --- a/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/events/expr/SpelSqlExpressionInvokerTest.java +++ b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/events/expr/SpelSqlExpressionInvokerTest.java @@ -3,6 +3,7 @@ import org.hswebframework.ezorm.rdb.mapping.EntityColumnMapping; import org.hswebframework.ezorm.rdb.operator.builder.fragments.NativeSql; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import reactor.function.Function3; import java.util.Collections; @@ -18,21 +19,58 @@ class SpelSqlExpressionInvokerTest { void test() { SpelSqlExpressionInvoker invoker = new SpelSqlExpressionInvoker(); - BiFunction, Object> func = invoker.compile("name + 1 + ?"); + Function3, Object> func = invoker.compile("name + 1 + ?"); - assertEquals(13, func.apply(new Object[]{2}, Collections.singletonMap("name", 10))); + EntityColumnMapping mapping = Mockito.mock(EntityColumnMapping.class); + + assertEquals(13, func.apply(mapping, new Object[]{2}, Collections.singletonMap("name", 10))); } @Test void testFunction() { SpelSqlExpressionInvoker invoker = new SpelSqlExpressionInvoker(); + EntityColumnMapping mapping = Mockito.mock(EntityColumnMapping.class); + + Function3, Object> func = invoker.compile("coalesce(name,?)"); + + assertEquals(2, func.apply(mapping, new Object[]{2}, Collections.emptyMap())); - BiFunction, Object> func = invoker.compile("coalesce(name,?)"); + assertEquals(3, func.apply(mapping, null, Collections.singletonMap("name", 3))); + + } - assertEquals(2, func.apply(new Object[]{2}, Collections.emptyMap())); + @Test + void testAddNull(){ + SpelSqlExpressionInvoker invoker = new SpelSqlExpressionInvoker(); + EntityColumnMapping mapping = Mockito.mock(EntityColumnMapping.class); - assertEquals(3, func.apply(null, Collections.singletonMap("name",3))); + Function3, Object> func = invoker.compile("IFNULL(test,0) + ?"); + assertEquals(2, func.apply(mapping, new Object[]{2}, Collections.emptyMap())); } + + @Test + void testSnake() { + SpelSqlExpressionInvoker invoker = new SpelSqlExpressionInvoker(); + EntityColumnMapping mapping = Mockito.mock(EntityColumnMapping.class); + + { + Function3, Object> func = invoker.compile("count_value + ?"); + + assertEquals(12, func.apply(mapping,new Object[]{2}, Collections.singletonMap("countValue", 10))); + + } + { + Mockito.when(mapping.getPropertyByColumnName("_count_v")) + .thenReturn(java.util.Optional.of("countValue")); + Function3, Object> func = invoker.compile("_count_v + ?"); + + assertEquals(12, func.apply(mapping,new Object[]{2}, Collections.singletonMap("countValue", 10))); + + } + + + } + } \ No newline at end of file diff --git a/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/exception/DatabaseExceptionAnalyzerReporterTest.java b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/exception/DatabaseExceptionAnalyzerReporterTest.java new file mode 100644 index 000000000..54e4d84f6 --- /dev/null +++ b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/exception/DatabaseExceptionAnalyzerReporterTest.java @@ -0,0 +1,52 @@ +package org.hswebframework.web.crud.exception; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static org.junit.Assert.*; + +public class DatabaseExceptionAnalyzerReporterTest { + + DatabaseExceptionAnalyzerReporter reporter=new DatabaseExceptionAnalyzerReporter(); + + @Test + void testTimeout(){ + + Assertions.assertTrue(reporter.doReportException( + new IllegalStateException("Timeout on blocking read for 10000 MILLISECONDS") + )); + + } + + @Test + void testBinding(){ + Assertions.assertTrue(reporter.doReportException( + new IndexOutOfBoundsException("Binding index 0 when only 0 parameters are expected ") + )); + + Assertions.assertTrue(reporter.doReportException( + new IndexOutOfBoundsException("Binding parameters is not supported for simple statement") + )); + } + + @Test + void testUnknownDatabase(){ + Assertions.assertTrue(reporter.doReportException( + new IndexOutOfBoundsException("Unknown database 'jetlinks' ") + )); + } + + + @Test + void testPgsqlUnknownDatabase(){ + Assertions.assertTrue(reporter.doReportException( + new IndexOutOfBoundsException("[3D000] database \"jetlinks22\" does not exist") + )); + } + @Test + void testPgsqlUnknownSchema(){ + Assertions.assertTrue(reporter.doReportException( + new IndexOutOfBoundsException("[3F000] schema \"jetlinks22\" does not exist") + )); + } +} \ No newline at end of file diff --git a/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/query/DefaultQueryHelperTest.java b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/query/DefaultQueryHelperTest.java index cda9fdf29..39b87d9a9 100644 --- a/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/query/DefaultQueryHelperTest.java +++ b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/query/DefaultQueryHelperTest.java @@ -6,6 +6,8 @@ import lombok.Setter; import lombok.ToString; import org.hswebframework.ezorm.core.param.Sort; +import org.hswebframework.ezorm.rdb.executor.SqlRequest; +import org.hswebframework.ezorm.rdb.executor.SqlRequests; import org.hswebframework.ezorm.rdb.operator.DatabaseOperator; import org.hswebframework.web.api.crud.entity.QueryParamEntity; import org.hswebframework.web.crud.entity.EventTestEntity; @@ -34,6 +36,90 @@ class DefaultQueryHelperTest { private DatabaseOperator database; + @Test + public void testLoadTable() { + database + .sql() + .reactive() + .execute(SqlRequests.of("create table \"NATIVE_TEST\"( " + + "\"id\" varchar(32) primary key" + + ",name varchar(32)" + + ",\"testName\" varchar(32)" + + ")")) + .as(StepVerifier::create) + .expectComplete() + .verify(); + + DefaultQueryHelper helper = new DefaultQueryHelper(database); + + database + .dml() + .insert("native_test") + .value("id", "test") + .value("NAME", "test") + .value("testName", "test") + .execute() + .sync(); + + helper.select("select id,name,testName from native_test") + .fetch() + .doOnNext(System.out::println) + .as(StepVerifier::create) + .expectNextCount(1) + .verifyComplete(); + } + + @Test + public void testPage() { + DefaultQueryHelper helper = new DefaultQueryHelper(database); + + database.dml() + .insert("s_test") + .value("id", "page-test") + .value("name", "page") + .value("age", 22) + .execute() + .sync(); + + database.dml() + .insert("s_test") + .value("id", "page-test2") + .value("name", "page") + .value("age", 22) + .execute() + .sync(); + + helper.select("select * from s_test") + .where(dsl -> { + dsl.doPaging(0, 1); + }) + .fetch() + .as(StepVerifier::create) + .expectNextCount(1) + .verifyComplete(); + } + + @Test + public void testAgg() { + DefaultQueryHelper helper = new DefaultQueryHelper(database); + + database.dml() + .insert("s_test") + .value("id", "agg-test") + .value("name", "agg") + .value("age", 111) + .execute() + .sync(); + + helper.select("select sum(age) num from s_test t") + .where(dsl -> dsl.is("name", "agg")) + .fetch() + .doOnNext(v -> System.out.println(JSON.toJSONString(v, SerializerFeature.PrettyFormat))) + .as(StepVerifier::create) + .expectNextCount(1) + .verifyComplete(); + } + @Test public void testGroup() { DefaultQueryHelper helper = new DefaultQueryHelper(database); @@ -46,20 +132,92 @@ public void testGroup() { .execute() .sync(); - helper.select("select name as \"name\",count(1) totalResult from s_test group by name having count(1) > ? ", GroupResult::new,0) + helper + .select("select name as \"name\",count(1) totalResult from s_test group by name having count(1) > ? ", GroupResult::new, 0) + .where(dsl -> dsl + .is("age", "31") + .orderByAsc(GroupResult::getTotalResult)) + .fetch() + .doOnNext(v -> System.out.println(JSON.toJSONString(v, SerializerFeature.PrettyFormat))) + .as(StepVerifier::create) + .expectNextCount(1) + .verifyComplete(); + } + + @Test + public void testDistinct() { + DefaultQueryHelper helper = new DefaultQueryHelper(database); + + database.dml() + .insert("s_test") + .value("id", "distinct-test") + .value("name", "testDistinct") + .value("testName", "distinct") + .value("age", 33) + .execute() + .sync(); + + + helper.select("select distinct name from s_test ", 0) + .fetchPaged(0, 10) + .doOnNext(v -> System.out.println(JSON.toJSONString(v, SerializerFeature.PrettyFormat))) + .as(StepVerifier::create) + .expectNextCount(1) + .verifyComplete(); + } + + @Test + public void testInner() { + DefaultQueryHelper helper = new DefaultQueryHelper(database); + + database.dml() + .insert("s_test") + .value("id", "inner-test") + .value("name", "inner") + .value("testName", "inner") + .value("age", 31) + .execute() + .sync(); + + + helper.select("select age,count(1) c from ( select *,'1' as x from s_test ) a group by age ", 0) .where(dsl -> dsl - .is("age", "31") - .orderByAsc(GroupResult::getTotalResult)) - .fetch() + .is("x", "1") + .is("name", "inner") + .is("a.testName", "inner") + .is("age", 31)) + .fetchPaged(0, 10) .doOnNext(v -> System.out.println(JSON.toJSONString(v, SerializerFeature.PrettyFormat))) .as(StepVerifier::create) .expectNextCount(1) .verifyComplete(); } + @Test + public void testJoinSubQuery() { + DefaultQueryHelper helper = new DefaultQueryHelper(database); + + database.dml() + .insert("s_test") + .value("id", "join_sub") + .value("name", "join_sub") + .value("testName", "join_sub") + .value("age", 31) + .execute() + .sync(); + + helper + .select("select * from s_test t1 join (select * from s_test s where name = ? ) t2 on t2.id = t1.id ", "join_sub") + .fetch() + .doOnNext(v -> System.out.println(JSON.toJSONString(v, SerializerFeature.PrettyFormat))) + .as(StepVerifier::create) + .expectNextCount(1) + .verifyComplete(); + } + @Getter @Setter - public static class GroupResult{ + public static class GroupResult { private String name; private BigDecimal totalResult; } @@ -83,11 +241,11 @@ public void testNative() { DefaultQueryHelper helper = new DefaultQueryHelper(database); QueryParamEntity param = QueryParamEntity - .newQuery() - .is("e.id", "helper_testNative") - .is("t.age", "20") - .orderByAsc("t.age") - .getParam(); + .newQuery() + .is("e.id", "helper_testNative") + .is("t.age", "20") + .orderByAsc("t.age") + .getParam(); { Sort sortByValue = new Sort(); @@ -104,8 +262,8 @@ public void testNative() { helper.select("select t.*,e.*,e.name ename,e.id `x.id` from s_test t " + - "left join s_test_event e on e.id = t.id " + - "where t.age = ?", 20) + "left join s_test_event e on e.id = t.id " + + "where t.age = ?", 20) .logger(LoggerFactory.getLogger("org.hswebframework.test.native")) .where(param) .fetchPaged() @@ -115,10 +273,10 @@ public void testNative() { .verifyComplete(); helper.select("select id,name from s_test t " + - "union all select id,name from s_test_event") + "union all select id,name from s_test_event") .where(dsl -> dsl - .is("id", "helper_testNative") - .orderByAsc("name")) + .is("id", "helper_testNative") + .orderByAsc("name")) .fetchPaged() .doOnNext(v -> System.out.println(JSON.toJSONString(v, SerializerFeature.PrettyFormat))) .as(StepVerifier::create) @@ -128,6 +286,36 @@ public void testNative() { } + @Test + public void testCustomFirstPageIndex() { + DefaultQueryHelper helper = new DefaultQueryHelper(database); + QueryParamEntity e = new QueryParamEntity(); + e.and("id", "eq", "testCustomFirstPageIndex"); + e.setFirstPageIndex(1); + e.setPageIndex(2); + + { + helper.select(TestInfo.class) + .from(TestEntity.class) + .where(e) + .fetchPaged() + .doOnNext(info -> System.out.println(JSON.toJSONString(info, SerializerFeature.PrettyFormat))) + .as(StepVerifier::create) + .expectNextMatches(p -> p.getPageIndex() == 1) + .verifyComplete(); + } + + { + helper.select("select * from s_test") + .where(e) + .fetchPaged() + .doOnNext(info -> System.out.println(JSON.toJSONString(info, SerializerFeature.PrettyFormat))) + .as(StepVerifier::create) + .expectNextMatches(p -> p.getPageIndex() == 1) + .verifyComplete(); + } + } + @Test public void test() { @@ -151,31 +339,35 @@ public void test() { DefaultQueryHelper helper = new DefaultQueryHelper(database); helper.select(TestInfo.class) - .all(EventTestEntity.class, TestInfo::setEventList) -// .all("e2", TestInfo::setEvent) + // .all(EventTestEntity.class, TestInfo::setEventList) + .all("e2", TestInfo::setEvent) .all(TestEntity.class) .from(TestEntity.class) - .leftJoin(EventTestEntity.class, - join -> join - .alias("e1") - .is(EventTestEntity::getId, TestEntity::getId) -// .is(EventTestEntity::getName, TestEntity::getId) - .notNull(EventTestEntity::getAge)) // .leftJoin(EventTestEntity.class, // join -> join -// .alias("e2") -// .is(EventTestEntity::getId, TestEntity::getId)) - -// .where(dsl -> dsl.is(EventTestEntity::getName, "Ename") -// .is("e1.name", "Ename") -// .orNest() -// .is(TestEntity::getName, "main") -// .is("e1.name", "Ename") -// .end() -// ) +// .alias("e1") +// .is(EventTestEntity::getId, TestEntity::getId) +//// .is(EventTestEntity::getName, TestEntity::getId) +// .notNull(EventTestEntity::getAge)) + .leftJoin(EventTestEntity.class, + join -> join + .alias("e2") + .is(EventTestEntity::getId, TestEntity::getId) + .nest() + .is(EventTestEntity::getId,TestEntity::getId) + .is(EventTestEntity::getAge,10) + .end() + ) + .where(dsl -> dsl.is(EventTestEntity::getName, "Ename") + .is("e1.name", "Ename") + .orNest() + .is(TestEntity::getName, "main") + .is("e1.name", "Ename") + .end() + ) .orderByAsc(TestEntity::getAge) .orderByDesc(EventTestEntity::getAge) - .fetchPaged() + .fetchPaged(0, 10) .doOnNext(info -> System.out.println(JSON.toJSONString(info, SerializerFeature.PrettyFormat))) .as(StepVerifier::create) .expectNextCount(1) @@ -186,7 +378,7 @@ public void test() { @Getter @Setter @ToString - public static class TestInfo { + public static class TestInfo extends TestInfoSuper { private String id; @@ -198,6 +390,11 @@ public static class TestInfo { private EventTestEntity event; + } + + @Getter + @Setter + public static class TestInfoSuper { private List eventList; } } \ No newline at end of file diff --git a/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/query/QueryAnalyzerImplTest.java b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/query/QueryAnalyzerImplTest.java index 3e9de6424..31498cd96 100644 --- a/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/query/QueryAnalyzerImplTest.java +++ b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/query/QueryAnalyzerImplTest.java @@ -25,18 +25,18 @@ void testInject() { QueryAnalyzerImpl analyzer = new QueryAnalyzerImpl(database, "select count(distinct time) t2, \"name\" n from \"s_test\" t"); SqlRequest request = analyzer.refactor( - QueryParamEntity - .newQuery() - .and("name", "123") - .getParam()); + QueryParamEntity + .newQuery() + .and("name", "123") + .getParam()); System.out.println(request); SqlRequest sql = analyzer.refactorCount( - QueryParamEntity - .newQuery() - .and("name", "123") - .getParam()); + QueryParamEntity + .newQuery() + .and("name", "123") + .getParam()); System.out.println(sql); } @@ -46,7 +46,7 @@ void testInject() { void testUnion() { QueryAnalyzerImpl analyzer = new QueryAnalyzerImpl(database, "select name n from s_test t " + - "union select name n from s_test t"); + "union select name n from s_test t"); assertNotNull(analyzer.select().table.alias, "t"); assertNotNull(analyzer.select().table.metadata.getName(), "s_test"); @@ -78,10 +78,10 @@ void testSub() { assertNotNull(analyzer.select().getColumns().get("n")); SqlRequest request = analyzer - .refactor(QueryParamEntity - .newQuery() - .where("n", "123") - .getParam()); + .refactor(QueryParamEntity + .newQuery() + .where("n", "123") + .getParam()); System.out.println(request); @@ -92,4 +92,128 @@ void testSub() { .expectComplete() .verify(); } + + @Test + void testJoin() { + QueryAnalyzerImpl analyzer = new QueryAnalyzerImpl( + database, + "select *,t2.c from s_test t " + + "left join (select z.id id, count(1) c from s_test z) t2 on t2.id = t.id"); + + SqlRequest request = analyzer + .refactor(QueryParamEntity + .of() + .and("t2.c", "is", "xyz")); + + System.out.println(request); + + } + + @Test + void testPrepare() { + QueryAnalyzerImpl analyzer = new QueryAnalyzerImpl( + database, + "select * from (select substring(id,9) id from s_test where left(id,1) = ?) t"); + + SqlRequest request = analyzer + .refactor(QueryParamEntity.of(), 33); + + System.out.println(request); + } + + @Test + void testWith() { + + QueryAnalyzerImpl analyzer = new QueryAnalyzerImpl( + database, + "WITH RECURSIVE Tree AS (\n" + + "\n" + + " SELECT id\n" + + " FROM s_test\n" + + " WHERE id = ? \n" + + "\t\n" + + " UNION ALL\n" + + "\t\n" + + " SELECT ai.id\n" + + " FROM s_test AS ai\n" + + " INNER JOIN Tree AS tr ON ai.id = tr.id\n" + + ")\n" + + "SELECT t1.id\n" + + "FROM Tree AS t1;"); + + SqlRequest request = analyzer + .refactor(QueryParamEntity.of().and("id", "eq", "test"), 1); + + System.out.println(request); + } + + @Test + void testTableFunction() { + QueryAnalyzerImpl analyzer = new QueryAnalyzerImpl( + database, + "select t.key from json_each_text('{\"name\":\"test\"}') t"); + + SqlRequest request = analyzer + .refactor(QueryParamEntity.of().and("key", "like", "test%"), 1); + System.out.println(request); + } + + @Test + void testTableFunctionJoin() { + QueryAnalyzerImpl analyzer = new QueryAnalyzerImpl( + database, + "select t1.*,t2.key from s_test t1 left join json_each_text('{\"name\":\"test\"}') t2 on t2.key='test' and t2.value='test1'"); + + SqlRequest request = analyzer + .refactor(QueryParamEntity.of().and("t2.key", "like", "test%"), 1); + System.out.println(request); + } + + @Test + void testValues() { + QueryAnalyzerImpl analyzer = new QueryAnalyzerImpl( + database, + "select * from (values (1,2),(3,4)) t(\"a\",b)"); + + SqlRequest request = analyzer + .refactor(QueryParamEntity.of().and("a", "eq", 1), 1); + System.out.println(request); + } + + @Test + void testLateralSubSelect() { + QueryAnalyzerImpl analyzer = new QueryAnalyzerImpl( + database, + "select * from s_test t, lateral(select * from s_test where id = t.id) t2"); + + SqlRequest request = analyzer + .refactor(QueryParamEntity.of().and("t2.id", "eq", "test"), 1); + System.out.println(request); + } + + @Test + void testParenthesisFrom() { + QueryAnalyzerImpl analyzer = new QueryAnalyzerImpl( + database, + "select * from (s_test) t"); + + SqlRequest request = analyzer + .refactor(QueryParamEntity.of().and("t.id", "eq", "test"), 1); + System.out.println(request); + } + + + @Test + void testDistinct() { + QueryAnalyzerImpl analyzer = new QueryAnalyzerImpl( + database, + "select distinct upper(t.id) v from s_test t group by t.name"); + + SqlRequest request = analyzer + .refactor(QueryParamEntity.of().and("t.id", "eq", "test"), 1); + + System.out.println(request); + + System.out.println(analyzer.refactorCount(QueryParamEntity.of())); + } } \ No newline at end of file diff --git a/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/query/QueryHelperUtilsTest.java b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/query/QueryHelperUtilsTest.java new file mode 100644 index 000000000..3d875c0b3 --- /dev/null +++ b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/query/QueryHelperUtilsTest.java @@ -0,0 +1,42 @@ +package org.hswebframework.web.crud.query; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class QueryHelperUtilsTest { + + + @Test + void testToHump(){ + + assertEquals("testName",QueryHelperUtils.toHump("test_name")); + + + assertEquals("ruownum_",QueryHelperUtils.toHump("RUOWNUM_")); + + } + + @Test + void testToSnake(){ + + assertEquals("test_name",QueryHelperUtils.toSnake("testName")); + + assertEquals("test_name",QueryHelperUtils.toSnake("TestName")); + + + + } + + + @Test + void testLegal(){ + + assertTrue(QueryHelperUtils.isLegalColumn("test_name")); + assertFalse(QueryHelperUtils.isLegalColumn("test-name")); + + assertFalse(QueryHelperUtils.isLegalColumn("test\nname")); + + + } +} \ No newline at end of file diff --git a/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/service/CustomTestCustom.java b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/service/CustomTestCustom.java index 0f60ec701..69ba5cddb 100644 --- a/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/service/CustomTestCustom.java +++ b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/service/CustomTestCustom.java @@ -1,15 +1,48 @@ package org.hswebframework.web.crud.service; +import org.hswebframework.ezorm.rdb.metadata.DataType; +import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata; +import org.hswebframework.ezorm.rdb.metadata.RDBTableMetadata; +import org.hswebframework.web.crud.configuration.TableMetadataCustomizer; import org.hswebframework.web.crud.entity.CustomTestEntity; import org.hswebframework.web.crud.entity.TestEntity; import org.hswebframework.web.crud.entity.factory.EntityMappingCustomizer; import org.hswebframework.web.crud.entity.factory.MapperEntityFactory; import org.springframework.stereotype.Component; +import java.beans.PropertyDescriptor; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.sql.JDBCType; +import java.util.Set; + @Component -public class CustomTestCustom implements EntityMappingCustomizer { +public class CustomTestCustom implements EntityMappingCustomizer, TableMetadataCustomizer { @Override public void custom(MapperEntityFactory factory) { - factory.addMapping(TestEntity.class, new MapperEntityFactory.Mapper<>(CustomTestEntity.class,CustomTestEntity::new)); + factory.addMapping(TestEntity.class, new MapperEntityFactory.Mapper<>(CustomTestEntity.class, CustomTestEntity::new)); + } + + @Override + public void customColumn(Class entityType, + PropertyDescriptor descriptor, + Field field, + Set annotations, + RDBColumnMetadata column) { + + } + + @Override + public void customTable(Class entityType, RDBTableMetadata table) { + if (TestEntity.class.isAssignableFrom(entityType)) { + + RDBColumnMetadata col = table.newColumn(); + col.setName("ext_name"); + col.setAlias("extName"); + col.setLength(32); + col.setType(DataType.jdbc(JDBCType.VARCHAR, String.class)); + table.addColumn(col); + + } } } diff --git a/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/service/GenericReactiveCacheSupportCrudServiceTest.java b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/service/GenericReactiveCacheSupportCrudServiceTest.java index 2eadc8afc..d13c9bed5 100644 --- a/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/service/GenericReactiveCacheSupportCrudServiceTest.java +++ b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/service/GenericReactiveCacheSupportCrudServiceTest.java @@ -1,6 +1,5 @@ package org.hswebframework.web.crud.service; -import org.hswebframework.web.cache.ReactiveCacheManager; import org.hswebframework.web.crud.TestApplication; import org.hswebframework.web.crud.entity.TestEntity; import org.junit.Test; @@ -12,8 +11,6 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; -import static org.junit.Assert.*; - @SpringBootTest(classes = TestApplication.class, args = "--hsweb.cache.type=guava") @RunWith(SpringRunner.class) @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS) @@ -59,6 +56,71 @@ public void test() { .as(StepVerifier::create) .expectError(NullPointerException.class) .verify(); + + + } + + @Test + public void test2() { + + TestEntity entity = TestEntity.of("test1",100,"testName"); + + entityService + .createDelete() + .notNull(TestEntity::getId) + .execute() + .block(); + + entityService + .insert(Mono.just(entity)) + .as(StepVerifier::create) + .expectNext(1) + .verifyComplete(); + + entityService + .getCacheAll() + .as(StepVerifier::create) + .expectNextCount(1) + .verifyComplete(); + + entity.setAge(120); + entityService + .updateById(entity.getId(), entity) + .as(StepVerifier::create) + .expectNext(1) + .verifyComplete(); + + entityService + .getCacheAll() + .switchIfEmpty(Mono.error(NullPointerException::new)) + .as(StepVerifier::create) + .expectNextMatches(t -> t.getAge().equals(120)) + .verifyComplete(); + + entity.setId(null); + entityService + .insert(Mono.just(entity)) + .as(StepVerifier::create) + .expectNext(1) + .verifyComplete(); + + entityService + .getCacheAll() + .as(StepVerifier::create) + .expectNextCount(2) + .verifyComplete(); + + entityService + .deleteById(entity.getId()) + .as(StepVerifier::create) + .expectNextCount(1) + .verifyComplete(); + + entityService + .getCacheAll() + .as(StepVerifier::create) + .expectNextCount(1) + .verifyComplete(); } } \ No newline at end of file diff --git a/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/service/ReactiveTreeSortEntityServiceTest.java b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/service/ReactiveTreeSortEntityServiceTest.java index ca3d3404d..4d0f262cf 100644 --- a/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/service/ReactiveTreeSortEntityServiceTest.java +++ b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/service/ReactiveTreeSortEntityServiceTest.java @@ -4,11 +4,13 @@ import org.hswebframework.ezorm.rdb.mapping.defaults.SaveResult; import org.hswebframework.web.api.crud.entity.QueryParamEntity; import org.hswebframework.web.crud.entity.TestTreeSortEntity; +import org.hswebframework.web.exception.ValidationException; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -25,14 +27,26 @@ public class ReactiveTreeSortEntityServiceTest { private TestTreeSortEntityService sortEntityService; + @Test + public void testCreateDefaultId() { + TestTreeSortEntity entity = new TestTreeSortEntity(); + entity.setName("Simple-test"); + + sortEntityService + .insert(Mono.just(entity)) + .as(StepVerifier::create) + .expectNext(1) + .verifyComplete(); + } + @Test public void testCrud() { TestTreeSortEntity entity = new TestTreeSortEntity(); - entity.setId("test"); - entity.setName("test"); + entity.setId("Crud-test"); + entity.setName("Crud-test"); TestTreeSortEntity entity2 = new TestTreeSortEntity(); - entity2.setName("test2"); + entity2.setName("Crud-test2"); entity.setChildren(Arrays.asList(entity2)); @@ -47,7 +61,7 @@ public void testCrud() { .expectNext(2) .verifyComplete(); - sortEntityService.queryResultToTree(QueryParamEntity.of()) + sortEntityService.queryResultToTree(QueryParamEntity.of().and("id", "like", "Crud-%")) .map(List::size) .as(StepVerifier::create) .expectNext(1) @@ -59,7 +73,7 @@ public void testCrud() { .verifyComplete(); - sortEntityService.deleteById(Mono.just("test")) + sortEntityService.deleteById(Mono.just(entity.getId())) .as(StepVerifier::create) .expectNext(2) .verifyComplete(); @@ -86,26 +100,27 @@ public void testChangeParent() { entity3.setParentId(entity2.getId()); sortEntityService - .save(Arrays.asList(entity, entity_0, entity2, entity3)) - .then() - .as(StepVerifier::create) - .expectComplete() - .verify(); + .save(Arrays.asList(entity, entity_0, entity2, entity3)) + .then() + .as(StepVerifier::create) + .expectComplete() + .verify(); entity2.setChildren(null); entity2.setParentId(entity_0.getId()); sortEntityService - .save(Arrays.asList(entity2)) - .then() - .as(StepVerifier::create) - .expectComplete() - .verify(); + .save(Arrays.asList(entity2)) + .then() + .as(StepVerifier::create) + .expectComplete() + .verify(); - sortEntityService.queryIncludeChildren(Arrays.asList(entity_0.getId())) - .as(StepVerifier::create) - .expectNextCount(3) - .verifyComplete(); + sortEntityService + .queryIncludeChildren(Arrays.asList(entity_0.getId())) + .as(StepVerifier::create) + .expectNextCount(3) + .verifyComplete(); } @@ -116,27 +131,171 @@ public void testSave() { entity.setName("test-path"); sortEntityService - .save(entity) - .then() - .as(StepVerifier::create) - .expectComplete() - .verify(); + .save(entity) + .then() + .as(StepVerifier::create) + .expectComplete() + .verify(); String firstPath = entity.getPath(); assertNotNull(firstPath); entity.setPath(null); sortEntityService - .save(entity) - .then() - .as(StepVerifier::create) - .expectComplete() - .verify(); + .save(entity) + .then() + .as(StepVerifier::create) + .expectComplete() + .verify(); sortEntityService - .findById(entity.getId()) - .map(TestTreeSortEntity::getPath) - .as(StepVerifier::create) - .expectNext(firstPath) - .verifyComplete(); + .findById(entity.getId()) + .map(TestTreeSortEntity::getPath) + .as(StepVerifier::create) + .expectNext(firstPath) + .verifyComplete(); } + + @Test + public void testNotExistParentId() { + TestTreeSortEntity entity = new TestTreeSortEntity(); + entity.setId("NotExistParentIdTest"); + entity.setName("NotExistParentIdTest"); + entity.setParentId("NotExistParentId"); + + sortEntityService + .insert(entity) + .then() + .as(StepVerifier::create) + .expectError(ValidationException.class) + .verify(); + + TestTreeSortEntity entity2 = new TestTreeSortEntity(); + entity2.setId("NotExistParentId"); + entity2.setName("NotExistParentId"); + + sortEntityService + .save(Flux.just(entity, entity2)) + .then() + .as(StepVerifier::create) + .expectComplete() + .verify(); + } + + + @Test + public void testCyclicDependency() { + + TestTreeSortEntity root = new TestTreeSortEntity(); + root.setId("testCyclicDependency-root"); + root.setName("testCyclicDependency"); + + + TestTreeSortEntity node1 = new TestTreeSortEntity(); + node1.setId("testCyclicDependency-node1"); + node1.setName("testCyclicDependency-node1"); + node1.setParentId(root.getId()); + + root.setParentId(node1.getId()); + sortEntityService + .insert(Flux.just(root, node1)) + .as(StepVerifier::create) + .expectErrorMatches(err -> err.getMessage().contains("tree_entity_cyclic_dependency")) + .verify(); + + root.setParentId(null); + root.setChildren(null); + node1.setChildren(null); + + sortEntityService + .insert(Flux.just(root, node1)) + .as(StepVerifier::create) + .expectNext(2) + .verifyComplete(); + + root.setParentId(node1.getId()); + root.setChildren(null); + node1.setChildren(null); + + sortEntityService + .save(Flux.just(root)) + .as(StepVerifier::create) + .expectErrorMatches(err -> err.getMessage().contains("tree_entity_cyclic_dependency")) + .verify(); + } + + + @Test + public void testDelete() { + TestTreeSortEntity root = new TestTreeSortEntity(); + root.setId("delete-root"); + root.setName("deleteRoot"); + + + TestTreeSortEntity node1 = new TestTreeSortEntity(); + node1.setId("delete-node1"); + node1.setName("delete-node1"); + node1.setParentId(root.getId()); + + sortEntityService + .save(Flux.just(root, node1)) + .map(SaveResult::getTotal) + .as(StepVerifier::create) + .expectNext(2) + .verifyComplete(); + + sortEntityService + .createDelete() + .where(TestTreeSortEntity::getId, "delete-root") + .execute() + .as(StepVerifier::create) + .expectNext(2) + .verifyComplete(); + + sortEntityService + .save(Flux.just(root, node1)) + .map(SaveResult::getTotal) + .as(StepVerifier::create) + .expectNext(2) + .verifyComplete(); + + sortEntityService + .deleteById(root.getId()) + .as(StepVerifier::create) + .expectNext(2) + .verifyComplete(); + + } + + @Test + public void testChild() { + TestTreeSortEntity entity = new TestTreeSortEntity(); + entity.setId("ChildQuery"); + entity.setName("ChildQuery"); + + TestTreeSortEntity entity2 = new TestTreeSortEntity(); + entity2.setId("ChildQuery2"); + entity2.setName("ChildQuery2"); + entity2.setParentId(entity.getId()); + + TestTreeSortEntity entity3 = new TestTreeSortEntity(); + entity3.setId("ChildQuery3"); + entity3.setName("ChildQuery3"); + + + sortEntityService + .save(Flux.just(entity, entity2, entity3)) + .then() + .as(StepVerifier::create) + .expectComplete() + .verify(); + + sortEntityService + .createQuery() + .accept("id", "test-child", entity.getId()) + .fetch() + .as(StepVerifier::create) + .expectNextCount(2) + .verifyComplete(); + } + } \ No newline at end of file diff --git a/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/service/TestEntityService.java b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/service/TestEntityService.java index e813fa099..e5e24711d 100644 --- a/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/service/TestEntityService.java +++ b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/service/TestEntityService.java @@ -1,11 +1,14 @@ package org.hswebframework.web.crud.service; import org.hswebframework.web.crud.entity.TestEntity; +import org.hswebframework.web.crud.events.EntityBeforeModifyEvent; import org.hswebframework.web.crud.events.EntityCreatedEvent; +import org.hswebframework.web.crud.events.EntityPrepareModifyEvent; import org.hswebframework.web.id.IDGenerator; import org.junit.jupiter.api.Test; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; @Service public class TestEntityService extends GenericReactiveCrudService { @@ -16,4 +19,11 @@ public void handleEvent(EntityCreatedEvent event){ System.out.println(event.getEntity()); } + + + @EventListener + public void listener(EntityPrepareModifyEvent event){ + System.out.println(event); + event.async(Mono.empty()); + } } diff --git a/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/service/TestTreeChildTermBuilder.java b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/service/TestTreeChildTermBuilder.java new file mode 100644 index 000000000..f234c6f94 --- /dev/null +++ b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/service/TestTreeChildTermBuilder.java @@ -0,0 +1,16 @@ +package org.hswebframework.web.crud.service; + +import org.hswebframework.web.crud.sql.terms.TreeChildTermBuilder; +import org.springframework.stereotype.Component; + +@Component +public class TestTreeChildTermBuilder extends TreeChildTermBuilder { + public TestTreeChildTermBuilder() { + super("test-child", "测试子节点"); + } + + @Override + protected String tableName() { + return "test_tree_sort"; + } +} diff --git a/hsweb-commons/pom.xml b/hsweb-commons/pom.xml index 758535b00..91ae1b6e9 100644 --- a/hsweb-commons/pom.xml +++ b/hsweb-commons/pom.xml @@ -23,9 +23,10 @@ hsweb-framework org.hswebframework.web - 4.0.16-SNAPSHOT + 4.0.20-SNAPSHOT ../pom.xml + ${artifactId} 4.0.0 通用模块 diff --git a/hsweb-concurrent/hsweb-concurrent-cache/pom.xml b/hsweb-concurrent/hsweb-concurrent-cache/pom.xml index 70e5b0787..4b1b65820 100644 --- a/hsweb-concurrent/hsweb-concurrent-cache/pom.xml +++ b/hsweb-concurrent/hsweb-concurrent-cache/pom.xml @@ -5,11 +5,12 @@ hsweb-concurrent org.hswebframework.web - 4.0.16-SNAPSHOT + 4.0.20-SNAPSHOT 4.0.0 hsweb-concurrent-cache + ${artifactId} diff --git a/hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/ReactiveCache.java b/hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/ReactiveCache.java index 35b087224..dafa7b648 100644 --- a/hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/ReactiveCache.java +++ b/hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/ReactiveCache.java @@ -10,6 +10,11 @@ import java.util.function.Function; import java.util.function.Supplier; +/** + * 响应式缓存 + * + * @param 缓存元素类型 + */ public interface ReactiveCache { Flux getFlux(Object key); diff --git a/hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/configuration/ReactiveCacheManagerConfiguration.java b/hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/configuration/ReactiveCacheManagerConfiguration.java index a8256a9d5..5b764fa74 100644 --- a/hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/configuration/ReactiveCacheManagerConfiguration.java +++ b/hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/configuration/ReactiveCacheManagerConfiguration.java @@ -1,13 +1,13 @@ package org.hswebframework.web.cache.configuration; import org.hswebframework.web.cache.ReactiveCacheManager; +import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -@Configuration +@AutoConfiguration @ConditionalOnMissingBean(ReactiveCacheManager.class) @EnableConfigurationProperties(ReactiveCacheProperties.class) public class ReactiveCacheManagerConfiguration { diff --git a/hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/supports/AbstractReactiveCache.java b/hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/supports/AbstractReactiveCache.java index ebdaafe43..5bceac178 100644 --- a/hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/supports/AbstractReactiveCache.java +++ b/hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/supports/AbstractReactiveCache.java @@ -1,5 +1,6 @@ package org.hswebframework.web.cache.supports; +import lombok.extern.slf4j.Slf4j; import org.hswebframework.web.cache.ReactiveCache; import org.reactivestreams.Publisher; import reactor.core.CoreSubscriber; @@ -17,8 +18,10 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.function.Supplier; +@Slf4j public abstract class AbstractReactiveCache implements ReactiveCache { - static Sinks.EmitFailureHandler emitFailureHandler = Sinks.EmitFailureHandler.busyLooping(Duration.ofSeconds(30)); + static final Sinks.EmitFailureHandler emitFailureHandler = Sinks.EmitFailureHandler.busyLooping(Duration.ofSeconds(30)); + private final Map cacheLoading = new ConcurrentHashMap<>(); protected static class CacheLoader extends MonoOperator { @@ -53,25 +56,25 @@ private void tryLoad(ContextView context) { Mono source = this.source; if (defaultValue != null) { source = source - .switchIfEmpty((Mono) defaultValue - .flatMap(val -> { - return parent.putNow(key, val).thenReturn(val); - })); + .switchIfEmpty((Mono) defaultValue + .flatMap(val -> { + return parent.putNow(key, val).thenReturn(val); + })); } loading = source.subscribe( - val -> { - complete(); - holder.emitValue(val, emitFailureHandler); - }, - err -> { - complete(); - holder.emitError(err, emitFailureHandler); - }, - () -> { - complete(); - holder.emitEmpty(emitFailureHandler); - }, - Context.of(context)); + val -> { + complete(); + holder.emitValue(val, emitFailureHandler); + }, + err -> { + complete(); + holder.emitError(err, emitFailureHandler); + }, + () -> { + complete(); + holder.emitEmpty(emitFailureHandler); + }, + Context.of(context)); } } @@ -95,42 +98,53 @@ private void complete() { @Override @SuppressWarnings("all") public final Mono getMono(Object key) { - return (Mono) cacheLoading.computeIfAbsent(key, _key -> new CacheLoader(this, _key, getNow(_key))); + return (Mono) cacheLoading + .computeIfAbsent(key, _key -> new CacheLoader(this, _key, getNow(_key))) + .onErrorResume(err -> handleLoaderError(key, err)); } @Override @SuppressWarnings("all") public final Mono getMono(Object key, Supplier> loader) { - return Mono.deferContextual(ctx -> { - CacheLoader cacheLoader = cacheLoading.compute(key, (_key, old) -> { - CacheLoader cl = new CacheLoader(this, _key, getNow(_key)); - cl.defaultValue(loader.get(), ctx); - return cl; - }); - return (Mono) cacheLoader; - }); + return Mono + .deferContextual(ctx -> { + CacheLoader cacheLoader = cacheLoading.compute(key, (_key, old) -> { + CacheLoader cl = new CacheLoader(this, _key, getNow(_key)); + cl.defaultValue(loader.get(), ctx); + return cl; + }); + return (Mono) cacheLoader; + }) + .onErrorResume(err -> handleLoaderError(key, err)); } @Override public final Flux getFlux(Object key) { return (cacheLoading.computeIfAbsent(key, _key -> new CacheLoader(this, _key, getNow(_key)))) - .flatMapIterable(e -> ((List) e)); + .flatMapIterable(e -> ((List) e)) + .onErrorResume(err -> handleLoaderError(key, err)); } @Override public final Flux getFlux(Object key, Supplier> loader) { return Flux.deferContextual(ctx -> { - CacheLoader cacheLoader = cacheLoading.compute(key, (_key, old) -> { - CacheLoader cl = new CacheLoader(this, _key, getNow(_key)); - cl.defaultValue(loader.get().collectList(), ctx); - return cl; - }); - return cacheLoader.flatMapIterable(e -> ((List) e)); - }); + CacheLoader cacheLoader = cacheLoading.compute(key, (_key, old) -> { + CacheLoader cl = new CacheLoader(this, _key, getNow(_key)); + cl.defaultValue(loader.get().collectList(), ctx); + return cl; + }); + return cacheLoader.flatMapIterable(e -> ((List) e)); + }) + .onErrorResume(err -> handleLoaderError(key, err)); } + protected Mono handleLoaderError(Object key, Throwable err) { + log.warn("load cache error,key:{},evict it.", key, err); + return evict(key) + .then(Mono.empty()); + } @Override public final Mono put(Object key, Publisher data) { diff --git a/hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/supports/CaffeineReactiveCache.java b/hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/supports/CaffeineReactiveCache.java index 8acbb1eca..84b425213 100644 --- a/hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/supports/CaffeineReactiveCache.java +++ b/hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/supports/CaffeineReactiveCache.java @@ -24,6 +24,10 @@ public Mono evictAll(Iterable key) { @Override public Flux getAll(Object... keys) { return Flux.defer(() -> { + if (keys == null || keys.length == 0) { + return Flux.fromIterable(cache.asMap().values()) + .map(e -> (E) e); + } return Flux.fromIterable(cache.getAllPresent(Arrays.asList(keys)).values()) .map(e -> (E) e); }); diff --git a/hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/supports/GuavaReactiveCache.java b/hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/supports/GuavaReactiveCache.java index 3fd0591ef..fa4245283 100644 --- a/hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/supports/GuavaReactiveCache.java +++ b/hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/supports/GuavaReactiveCache.java @@ -9,7 +9,7 @@ @SuppressWarnings("all") @AllArgsConstructor -public class GuavaReactiveCache extends AbstractReactiveCache{ +public class GuavaReactiveCache extends AbstractReactiveCache { private Cache cache; @@ -34,11 +34,17 @@ public Mono putNow(Object key, Object value) { public Mono evict(Object key) { return Mono.fromRunnable(() -> cache.invalidate(key)); } + @Override public Flux getAll(Object... keys) { return Flux.defer(() -> { + if (keys == null || keys.length == 0) { + return Flux + .fromIterable(cache.asMap().values()) + .map(e -> (E) e); + } return Flux.fromIterable(cache.getAllPresent(Arrays.asList(keys)).values()) - .map(e -> (E) e); + .map(e -> (E) e); }); } diff --git a/hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/supports/RedisReactiveCache.java b/hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/supports/RedisReactiveCache.java index 7a5836418..c72c4dda0 100644 --- a/hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/supports/RedisReactiveCache.java +++ b/hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/supports/RedisReactiveCache.java @@ -80,7 +80,7 @@ public Mono evictAll(Iterable key) { @Override public Flux getAll(Object... keys) { - if (keys.length == 0) { + if (keys == null || keys.length == 0) { return operations .opsForHash() .values(redisKey) diff --git a/hsweb-concurrent/hsweb-concurrent-cache/src/main/resources/META-INF/spring.factories b/hsweb-concurrent/hsweb-concurrent-cache/src/main/resources/META-INF/spring.factories deleted file mode 100644 index 0fd0e2620..000000000 --- a/hsweb-concurrent/hsweb-concurrent-cache/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1,3 +0,0 @@ -# Auto Configure -org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -org.hswebframework.web.cache.configuration.ReactiveCacheManagerConfiguration \ No newline at end of file diff --git a/hsweb-concurrent/hsweb-concurrent-cache/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hsweb-concurrent/hsweb-concurrent-cache/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..459a3e025 --- /dev/null +++ b/hsweb-concurrent/hsweb-concurrent-cache/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +org.hswebframework.web.cache.configuration.ReactiveCacheManagerConfiguration \ No newline at end of file diff --git a/hsweb-concurrent/pom.xml b/hsweb-concurrent/pom.xml index 0aff40bc6..9fd4855ea 100644 --- a/hsweb-concurrent/pom.xml +++ b/hsweb-concurrent/pom.xml @@ -5,11 +5,12 @@ hsweb-framework org.hswebframework.web - 4.0.16-SNAPSHOT + 4.0.20-SNAPSHOT 4.0.0 hsweb-concurrent + ${artifactId} pom hsweb-concurrent-cache diff --git a/hsweb-core/pom.xml b/hsweb-core/pom.xml index f13e68c4b..e0b45fffd 100644 --- a/hsweb-core/pom.xml +++ b/hsweb-core/pom.xml @@ -5,12 +5,13 @@ hsweb-framework org.hswebframework.web - 4.0.16-SNAPSHOT + 4.0.20-SNAPSHOT ../pom.xml 4.0.0 hsweb-core + ${artifactId} 核心包 @@ -18,7 +19,7 @@ org.javassist javassist - 3.29.0-GA + ${javassist.version} @@ -132,5 +133,15 @@ 0.1.3 + + org.apache.commons + commons-collections4 + + + + org.hswebframework + hsweb-easy-orm-core + + \ No newline at end of file diff --git a/hsweb-core/src/main/java/org/hswebframework/web/aop/MethodInterceptorHolder.java b/hsweb-core/src/main/java/org/hswebframework/web/aop/MethodInterceptorHolder.java index 57b1b0304..184775977 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/aop/MethodInterceptorHolder.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/aop/MethodInterceptorHolder.java @@ -21,19 +21,15 @@ import com.google.common.collect.Maps; import lombok.AllArgsConstructor; import lombok.Getter; -import lombok.Setter; import org.aopalliance.intercept.MethodInvocation; import org.hswebframework.web.utils.AnnotationUtils; import org.hswebframework.web.utils.DigestUtils; import org.reactivestreams.Publisher; -import org.springframework.core.LocalVariableTableParameterNameDiscoverer; +import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.ParameterNameDiscoverer; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; import java.lang.annotation.Annotation; import java.lang.reflect.Method; -import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; import java.util.function.Function; @@ -47,7 +43,7 @@ public class MethodInterceptorHolder { /** * 参数名称获取器,用于获取方法参数的名称 */ - public static final ParameterNameDiscoverer nameDiscoverer = new LocalVariableTableParameterNameDiscoverer(); + public static final ParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer(); public static MethodInterceptorHolder create(MethodInvocation invocation) { String[] argNames = nameDiscoverer.getParameterNames(invocation.getMethod()); diff --git a/hsweb-core/src/main/java/org/hswebframework/web/bean/ClassDescription.java b/hsweb-core/src/main/java/org/hswebframework/web/bean/ClassDescription.java index df1893ed3..29ce001f3 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/bean/ClassDescription.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/bean/ClassDescription.java @@ -3,7 +3,12 @@ import lombok.Getter; import org.hswebframework.web.dict.EnumDict; +import java.lang.reflect.Field; +import java.util.Arrays; import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; @Getter public class ClassDescription { @@ -14,8 +19,10 @@ public class ClassDescription { private final boolean enumType; private final boolean enumDict; private final int fieldSize; + private final boolean number; private final Object[] enums; + private final Map fields; public ClassDescription(Class type) { this.type = type; @@ -24,11 +31,15 @@ public ClassDescription(Class type) { arrayType = type.isArray(); enumType = type.isEnum(); fieldSize = type.getDeclaredFields().length; + number = Number.class.isAssignableFrom(type); if (enumType) { enums = type.getEnumConstants(); } else { enums = null; } + fields = Arrays + .stream(type.getDeclaredFields()) + .collect(Collectors.toMap(Field::getName, f -> f, (a, b) -> b)); } } diff --git a/hsweb-core/src/main/java/org/hswebframework/web/bean/CompareUtils.java b/hsweb-core/src/main/java/org/hswebframework/web/bean/CompareUtils.java index 736920d0e..85e2d807e 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/bean/CompareUtils.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/bean/CompareUtils.java @@ -63,11 +63,11 @@ public static boolean compare(Object source, Object target) { return compare(((Map) target), source); } - if (source.getClass().isEnum()) { + if (source.getClass().isEnum() || source instanceof Enum) { return compare(((Enum) source), target); } - if (target.getClass().isEnum()) { + if (target.getClass().isEnum() || source instanceof Enum) { return compare(((Enum) target), source); } @@ -178,9 +178,9 @@ public static boolean compare(Number number, Object target) { if (target instanceof String) { //日期格式的字符串? String stringValue = String.valueOf(target); - if (DateFormatter.isSupport(stringValue)) { + DateFormatter dateFormatter = DateFormatter.getFormatter(stringValue); + if (dateFormatter != null) { //格式化为相同格式的字符串进行对比 - DateFormatter dateFormatter = DateFormatter.getFormatter(stringValue); return (dateFormatter.toString(new Date(number.longValue())).equals(stringValue)); } try { @@ -260,9 +260,9 @@ public static boolean compare(Date date, Object target) { if (target instanceof String) { //日期格式的字符串? String stringValue = String.valueOf(target); - if (DateFormatter.isSupport(stringValue)) { + DateFormatter dateFormatter = DateFormatter.getFormatter(stringValue); + if (dateFormatter != null) { //格式化为相同格式的字符串进行对比 - DateFormatter dateFormatter = DateFormatter.getFormatter(stringValue); return (dateFormatter.toString(date).equals(stringValue)); } } 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 84f985290..34ae21536 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 @@ -1,14 +1,22 @@ package org.hswebframework.web.bean; +import com.google.common.collect.Sets; +import reactor.core.Disposable; + import java.util.Arrays; import java.util.HashSet; import java.util.Set; -public interface Copier { +public interface Copier extends Disposable { void copy(Object source, Object target, Set ignore, Converter converter); - default void copy(Object source, Object target, String... ignore){ - copy(source,target,new HashSet<>(Arrays.asList(ignore)),FastBeanCopier.DEFAULT_CONVERT); + default void copy(Object source, Object target, String... ignore) { + copy(source, target, Sets.newHashSet(ignore), FastBeanCopier.DEFAULT_CONVERT); + } + + @Override + default void dispose() { + } } diff --git a/hsweb-core/src/main/java/org/hswebframework/web/bean/Diff.java b/hsweb-core/src/main/java/org/hswebframework/web/bean/Diff.java index 08a9a2f37..6a40bb1f7 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/bean/Diff.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/bean/Diff.java @@ -1,16 +1,13 @@ package org.hswebframework.web.bean; import com.alibaba.fastjson.JSON; +import com.google.common.collect.Sets; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import lombok.ToString; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; @Getter @Setter @@ -24,13 +21,18 @@ public class Diff { private Object after; - public static List of(Object before, Object after) { + + public static List of(Object before, Object after, String... ignoreProperty) { List diffs = new ArrayList<>(); + Set ignores = Sets.newHashSet(ignoreProperty); Map beforeMap = FastBeanCopier.copy(before, HashMap::new); Map afterMap = FastBeanCopier.copy(after, HashMap::new); for (Map.Entry entry : afterMap.entrySet()) { + if (ignores.contains(entry.getKey())) { + continue; + } Object afterValue = entry.getValue(); String key = entry.getKey(); Object beforeValue = beforeMap.get(key); diff --git a/hsweb-core/src/main/java/org/hswebframework/web/bean/ExtendableToBeanCopier.java b/hsweb-core/src/main/java/org/hswebframework/web/bean/ExtendableToBeanCopier.java new file mode 100644 index 000000000..d154f328d --- /dev/null +++ b/hsweb-core/src/main/java/org/hswebframework/web/bean/ExtendableToBeanCopier.java @@ -0,0 +1,20 @@ +package org.hswebframework.web.bean; + +import lombok.AllArgsConstructor; +import org.hswebframework.ezorm.core.Extendable; + +import java.util.Set; + +@AllArgsConstructor +class ExtendableToBeanCopier implements Copier { + + private final Copier copier; + + @Override + public void copy(Object source, Object target, Set ignore, Converter converter) { + copier.copy(source, target, ignore, converter); + FastBeanCopier.copy(((Extendable) source).extensions(), target); + } + + +} diff --git a/hsweb-core/src/main/java/org/hswebframework/web/bean/ExtendableToMapCopier.java b/hsweb-core/src/main/java/org/hswebframework/web/bean/ExtendableToMapCopier.java new file mode 100644 index 000000000..f2dd5ddbc --- /dev/null +++ b/hsweb-core/src/main/java/org/hswebframework/web/bean/ExtendableToMapCopier.java @@ -0,0 +1,23 @@ +package org.hswebframework.web.bean; + +import lombok.AllArgsConstructor; +import org.hswebframework.ezorm.core.Extendable; + +import java.util.Map; +import java.util.Set; + +@AllArgsConstructor +class ExtendableToMapCopier implements Copier { + + private final Copier copier; + + @Override + public void copy(Object source, Object target, Set ignore, Converter converter) { + copier.copy(source, target, ignore, converter); + ExtendableUtils.copyToMap((Extendable) source, ignore, (Map) target); + //移除map中的extensions + ((Map) target).remove("extensions"); + } + + +} diff --git a/hsweb-core/src/main/java/org/hswebframework/web/bean/ExtendableUtils.java b/hsweb-core/src/main/java/org/hswebframework/web/bean/ExtendableUtils.java new file mode 100644 index 000000000..e61659cfa --- /dev/null +++ b/hsweb-core/src/main/java/org/hswebframework/web/bean/ExtendableUtils.java @@ -0,0 +1,41 @@ +package org.hswebframework.web.bean; + +import com.google.common.collect.Maps; +import org.apache.commons.collections4.CollectionUtils; +import org.hswebframework.ezorm.core.Extendable; + +import java.util.Map; +import java.util.Set; + +public class ExtendableUtils { + + public static void copyFromMap(Map source, + Set ignore, + Extendable target) { + ClassDescription def = ClassDescriptions.getDescription(target.getClass()); + + for (Map.Entry entry : source.entrySet()) { + //只copy没有定义的数据 + if (!ignore.contains(entry.getKey()) && !def.getFields().containsKey(entry.getKey())) { + target.setExtension(entry.getKey(), entry.getValue()); + } + } + + } + + public static void copyToMap(Extendable target, + Set ignore, + Map source) { + if (CollectionUtils.isNotEmpty(ignore)) { + source.putAll( + Maps.filterKeys(target.extensions(), key -> !ignore.contains(key)) + ); + } else { + source.putAll( + target.extensions() + ); + } + + } + +} diff --git a/hsweb-core/src/main/java/org/hswebframework/web/bean/FastBeanCopier.java b/hsweb-core/src/main/java/org/hswebframework/web/bean/FastBeanCopier.java index 909c99343..45d7ab842 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/bean/FastBeanCopier.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/bean/FastBeanCopier.java @@ -8,13 +8,15 @@ import org.apache.commons.beanutils.BeanUtilsBean; import org.apache.commons.beanutils.ConvertUtilsBean; import org.apache.commons.beanutils.PropertyUtilsBean; +import org.hswebframework.ezorm.core.Extendable; import org.hswebframework.utils.time.DateFormatter; import org.hswebframework.web.dict.EnumDict; import org.hswebframework.web.proxy.Proxy; -import org.jctools.maps.NonBlockingHashMap; +import org.hswebframework.web.utils.DynamicArrayList; import org.springframework.core.ResolvableType; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; +import org.springframework.util.NumberUtils; import org.springframework.util.ReflectionUtils; import java.beans.PropertyDescriptor; @@ -34,7 +36,7 @@ */ @Slf4j public final class FastBeanCopier { - private static final Map CACHE = new NonBlockingHashMap<>(); + private static final Map CACHE = new ConcurrentHashMap<>(); private static final PropertyUtilsBean propertyUtils = BeanUtilsBean.getInstance().getPropertyUtils(); @@ -126,17 +128,17 @@ public static T copy(S source, T target, Converter converter, Set ((Map) target).putAll(((Map) source)); } else { ((Map) source) - .forEach((k, v) -> { - if (!ignore.contains(k)) { - ((Map) target).put(k, v); - } - }); + .forEach((k, v) -> { + if (!ignore.contains(k)) { + ((Map) target).put(k, v); + } + }); } return target; } getCopier(source, target, true) - .copy(source, target, ignore, converter); + .copy(source, target, ignore, converter); return target; } @@ -159,7 +161,7 @@ public static Copier getCopier(Object source, Object target, boolean autoCreate) Class targetType = getUserClass(target); CacheKey key = createCacheKey(sourceType, targetType); if (autoCreate) { - return CACHE.computeIfAbsent(key, k -> createCopier(sourceType, targetType)); + return CACHE.computeIfAbsent(key, k -> createCopier(k.sourceType, k.targetType)); } else { return CACHE.get(key); } @@ -179,20 +181,35 @@ public static Copier createCopier(Class source, Class target) { if (tartName.startsWith("package ")) { tartName = tartName.substring("package ".length()); } + boolean targetIsExtendable = Extendable.class.isAssignableFrom(target); + boolean sourceIsExtendable = Extendable.class.isAssignableFrom(source); + boolean targetIsMap = Map.class.isAssignableFrom(target); + boolean sourceIsMap = Map.class.isAssignableFrom(source); + String method = "public void copy(Object s, Object t, java.util.Set ignore, " + - "org.hswebframework.web.bean.Converter converter){\n" + - "try{\n\t" + - sourceName + " $$__source=(" + sourceName + ")s;\n\t" + - tartName + " $$__target=(" + tartName + ")t;\n\t" + - createCopierCode(source, target) + - "}catch(Exception e){\n" + - "\tthrow new RuntimeException(e.getMessage(),e);" + - "\n}\n" + - "\n}"; + "org.hswebframework.web.bean.Converter converter){\n" + + "try{\n\t" + + sourceName + " $$__source=(" + sourceName + ")s;\n\t" + + tartName + " $$__target=(" + tartName + ")t;\n\t" + + createCopierCode(source, target) + + "}catch(Throwable e){\n" + + "\tthrow e;" + + "\n}\n" + + "\n}"; try { - return Proxy.create(Copier.class) - .addMethod(method) - .newInstance(); + @SuppressWarnings("all") + Proxy proxy = Proxy + .create(Copier.class, new Class[]{source, target}) + .addMethod(method); + Copier copier = proxy.newInstance(); + if (sourceIsExtendable && targetIsMap) { + copier = new ExtendableToMapCopier(copier); + } else if (sourceIsMap && targetIsExtendable) { + copier = new MapToExtendableCopier(copier); + } else if (sourceIsExtendable) { + copier = new ExtendableToBeanCopier(copier); + } + return copier; } catch (Exception e) { log.error("创建bean copy 代理对象失败:\n{}", method, e); throw new UnsupportedOperationException(e.getMessage(), e); @@ -201,13 +218,15 @@ public static Copier createCopier(Class source, Class target) { private static Map createProperty(Class type) { - List fieldNames = Arrays.stream(type.getDeclaredFields()) - .map(Field::getName).collect(Collectors.toList()); + List fieldNames = Arrays + .stream(type.getDeclaredFields()) + .map(Field::getName) + .collect(Collectors.toList()); return Stream.of(propertyUtils.getPropertyDescriptors(type)) .filter(property -> !property - .getName() - .equals("class") && property.getReadMethod() != null && property.getWriteMethod() != null) + .getName() + .equals("class") && property.getReadMethod() != null && property.getWriteMethod() != null) .map(BeanClassProperty::new) //让字段有序 .sorted(Comparator.comparing(property -> fieldNames.indexOf(property.name))) @@ -216,8 +235,11 @@ private static Map createProperty(Class type) { } private static Map createMapProperty(Map template) { - return template.values().stream().map(classProperty -> new MapClassProperty(classProperty.name)) - .collect(Collectors.toMap(ClassProperty::getName, Function.identity(), (k, k2) -> k, LinkedHashMap::new)); + return template + .values() + .stream() + .map(classProperty -> new MapClassProperty(classProperty.name)) + .collect(Collectors.toMap(ClassProperty::getName, Function.identity(), (k, k2) -> k, LinkedHashMap::new)); } private static String createCopierCode(Class source, Class target) { @@ -225,19 +247,20 @@ private static String createCopierCode(Class source, Class target) { Map targetProperties = null; + boolean targetIsExtendable = Extendable.class.isAssignableFrom(target); + boolean sourceIsExtendable = Extendable.class.isAssignableFrom(source); + boolean targetIsMap = Map.class.isAssignableFrom(target); + boolean sourceIsMap = Map.class.isAssignableFrom(source); //源类型为Map - if (Map.class.isAssignableFrom(source)) { - if (!Map.class.isAssignableFrom(target)) { + if (sourceIsMap) { + if (!targetIsMap) { targetProperties = createProperty(target); sourceProperties = createMapProperty(targetProperties); } - } else if (Map.class.isAssignableFrom(target)) { - if (!Map.class.isAssignableFrom(source)) { - sourceProperties = createProperty(source); - targetProperties = createMapProperty(sourceProperties); - - } + } else if (targetIsMap) { + sourceProperties = createProperty(source); + targetProperties = createMapProperty(sourceProperties); } else { targetProperties = createProperty(target); sourceProperties = createProperty(source); @@ -250,6 +273,21 @@ private static String createCopierCode(Class source, Class target) { for (ClassProperty sourceProperty : sourceProperties.values()) { ClassProperty targetProperty = targetProperties.get(sourceProperty.getName()); if (targetProperty == null) { + //复制到拓展对象 + if (targetIsExtendable && !sourceIsExtendable && !sourceIsMap) { + code.append("if(!ignore.contains(\"").append(sourceProperty.getName()).append("\")){\n\t"); + if (!sourceProperty.isPrimitive()) { + code.append("if($$__source.").append(sourceProperty.getReadMethod()).append("!=null){\n"); + } + code.append("\t\t((org.hswebframework.ezorm.core.Extendable)$$__target).setExtension(") + .append("\"").append(sourceProperty.name).append("\",") + .append("$$__source.").append(sourceProperty.getReadMethod()) + .append(");"); + if (!sourceProperty.isPrimitive()) { + code.append("\n\t}"); + } + code.append("\n}\n"); + } continue; } code.append("if(!ignore.contains(\"").append(sourceProperty.getName()).append("\")){\n\t"); @@ -264,9 +302,9 @@ private static String createCopierCode(Class source, Class target) { code.append("\tif(").append(sourceProperty.getName()).append("!=null){\n"); } code - .append("\t$$__target.") - .append(targetProperty.generateSetter(targetProperty.getType(), sourceProperty.getName())) - .append(";\n"); + .append("\t$$__target.") + .append(targetProperty.generateSetter(targetProperty.getType(), sourceProperty.getName())) + .append(";\n"); if (!targetProperty.isPrimitive()) { code.append("\t}\n"); } @@ -374,7 +412,7 @@ public BiFunction, Class, String> createGetterFunction() { } } String convert = "converter.convert((Object)(" + (isPrimitive() ? castWrapper(getterCode) : getterCode) + ")," - + getTypeName(targetType) + ".class," + generic + ")"; + + getTypeName(targetType) + ".class," + generic + ")"; StringBuilder convertCode = new StringBuilder(); if (targetType != getType()) { @@ -387,10 +425,10 @@ public BiFunction, Class, String> createGetterFunction() { // source.getField().intValue(); if (sourceIsWrapper) { convertCode - .append(getterCode) - .append(".") - .append(sourcePrimitive.getName()) - .append("Value()"); + .append(getterCode) + .append(".") + .append(sourcePrimitive.getName()) + .append("Value()"); } else { //类型不一致,调用convert转换 convertCode.append("((").append(targetWrapperClass.getName()) @@ -425,17 +463,17 @@ public BiFunction, Class, String> createGetterFunction() { if (Cloneable.class.isAssignableFrom(targetType)) { try { convertCode - .append("(") - .append(getTypeName()) - .append(")") - .append(getterCode) - .append(".clone()"); + .append("(") + .append(getTypeName()) + .append(")") + .append(getterCode) + .append(".clone()"); } catch (Exception e) { convertCode.append(getterCode); } } else { if ((Map.class.isAssignableFrom(targetType) - || Collection.class.isAssignableFrom(type)) && hasGeneric) { + || Collection.class.isAssignableFrom(type)) && hasGeneric) { convertCode.append("(").append(getTypeName()).append(")").append(convert); } else { convertCode.append("(").append(getTypeName()).append(")").append(getterCode); @@ -531,6 +569,7 @@ public Collection newCollection(Class targetClass) { @Override @SuppressWarnings("all") + @SneakyThrows public T convert(Object source, Class targetClass, Class[] genericType) { if (source == null) { return null; @@ -576,8 +615,8 @@ public T convert(Object source, Class targetClass, Class[] genericType) { Collection sourceCollection; if (source instanceof Collection) { sourceCollection = (Collection) source; - } else if (source instanceof Object[]) { - sourceCollection = Arrays.asList((Object[]) source); + } else if (source.getClass().isArray()) { + sourceCollection = new DynamicArrayList(source); } else if (source instanceof Map) { sourceCollection = ((Map) source).values(); } else { @@ -598,7 +637,6 @@ public T convert(Object source, Class targetClass, Class[] genericType) { } return (T) collection; } - if (target.isEnumType()) { if (target.isEnumDict()) { String strVal = String.valueOf(source); @@ -623,21 +661,38 @@ public T convert(Object source, Class targetClass, Class[] genericType) { for (Object e : target.getEnums()) { Enum t = ((Enum) e); if ((t.name().equalsIgnoreCase(strSource) - || Objects.equals(String.valueOf(t.ordinal()), strSource))) { + || Objects.equals(String.valueOf(t.ordinal()), strSource))) { return (T) e; } } - log.warn("无法将:{}转为枚举:{}", source, targetClass); + log.warn("无法将:{}转为枚举:{}", + source, + targetClass, + new ClassCastException(source + "=>" + targetClass)); return null; } //转换为数组 if (target.isArrayType()) { Class componentType = targetClass.getComponentType(); + List val = convert(source, List.class, new Class[]{componentType}); - return (T) val.toArray((Object[]) Array.newInstance(componentType, val.size())); - } + int size = val.size(); + Object array = Array.newInstance(componentType, size); + for (int i = 0; i < size; i++) { + Array.set(array, i, val.get(i)); + } + return (T) array; + } + if (target.isNumber()) { + if (source instanceof String) { + return (T) NumberUtils.parseNumber(String.valueOf(source), (Class) targetClass); + } + if (source instanceof Date) { + source = ((Date) source).getTime(); + } + } try { org.apache.commons.beanutils.Converter converter = convertUtils.lookup(targetClass); if (null != converter) { @@ -668,8 +723,8 @@ public T convert(Object source, Class targetClass, Class[] genericType) { return copy(source, beanFactory.newInstance(targetClass), this); } catch (Exception e) { - log.warn("复制类型{}->{}失败", source, targetClass, e); - throw new UnsupportedOperationException(e.getMessage(), e); + log.warn("复制类型{}->{}失败", targetClass, e); + throw e; } // return null; } @@ -702,10 +757,10 @@ private Object converterByApache(Class targetClass, Object source) { @AllArgsConstructor public static class CacheKey { - private final Class targetType; - private final Class sourceType; + private final Class targetType; + @Override public boolean equals(Object obj) { if (!(obj instanceof CacheKey)) { diff --git a/hsweb-core/src/main/java/org/hswebframework/web/bean/MapToExtendableCopier.java b/hsweb-core/src/main/java/org/hswebframework/web/bean/MapToExtendableCopier.java new file mode 100644 index 000000000..9bbab361b --- /dev/null +++ b/hsweb-core/src/main/java/org/hswebframework/web/bean/MapToExtendableCopier.java @@ -0,0 +1,22 @@ +package org.hswebframework.web.bean; + +import lombok.AllArgsConstructor; +import org.hswebframework.ezorm.core.Extendable; + +import java.util.Map; +import java.util.Set; + +@AllArgsConstructor +class MapToExtendableCopier implements Copier { + + private final Copier copier; + + @Override + public void copy(Object source, Object target, Set ignore, Converter converter) { + copier.copy(source, target, ignore, converter); + + ExtendableUtils.copyFromMap((Map) source, ignore, (Extendable) target); + } + + +} diff --git a/hsweb-core/src/main/java/org/hswebframework/web/context/ContextKey.java b/hsweb-core/src/main/java/org/hswebframework/web/context/ContextKey.java index e2386df63..00ee46095 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/context/ContextKey.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/context/ContextKey.java @@ -4,9 +4,9 @@ import lombok.Getter; @AllArgsConstructor +@Getter public final class ContextKey { - @Getter private final String key; public static ContextKey of(String key) { diff --git a/hsweb-core/src/main/java/org/hswebframework/web/dict/EnumDict.java b/hsweb-core/src/main/java/org/hswebframework/web/dict/EnumDict.java index 5d4e1137a..5cb43158f 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/dict/EnumDict.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/dict/EnumDict.java @@ -21,12 +21,14 @@ import lombok.extern.slf4j.Slf4j; import org.hswebframework.web.bean.ClassDescription; import org.hswebframework.web.bean.ClassDescriptions; +import org.hswebframework.web.dict.defaults.DefaultItemDefine; import org.hswebframework.web.exception.ValidationException; import org.hswebframework.web.i18n.LocaleUtils; import org.springframework.beans.BeanUtils; import org.springframework.util.StringUtils; import java.io.IOException; +import java.io.Serializable; import java.lang.reflect.Type; import java.util.*; import java.util.function.Function; @@ -48,7 +50,7 @@ */ @JSONType(deserializer = EnumDict.EnumDictJSONDeserializer.class) @JsonDeserialize(contentUsing = EnumDict.EnumDictJSONDeserializer.class) -public interface EnumDict extends JSONSerializable { +public interface EnumDict extends JSONSerializable, Serializable { /** * 枚举选项的值,通常由字母或者数字组成,并且在同一个枚举中值唯一;对应数据库中的值通常也为此值 @@ -101,13 +103,22 @@ default boolean eq(Object v) { if (v instanceof Map) { v = ((Map) v).getOrDefault("value", ((Map) v).get("text")); } + if (v instanceof Number) { + v = ((Number) v).intValue(); + } + if (v instanceof EnumDict) { + EnumDict dict = ((EnumDict) v); + v = dict.getValue(); + if (v == null) { + v = dict.getText(); + } + } return this == v - || getValue() == v - || getValue().equals(v) -// || (v instanceof Number ? in(((Number) v).longValue()) : false) - || String.valueOf(getValue()).equalsIgnoreCase(String.valueOf(v)) -// || v.equals(getMask()) - || getText().equalsIgnoreCase(String.valueOf(v) + || getValue() == v + || Objects.equals(getValue(), v) + || Objects.equals(ordinal(), v) + || String.valueOf(getValue()).equalsIgnoreCase(String.valueOf(v)) + || getText().equalsIgnoreCase(String.valueOf(v) ); } @@ -138,7 +149,7 @@ default String getComments() { * @return 查找到的结果 */ @SuppressWarnings("all") - static Optional find(Class type, Predicate predicate) { + static & EnumDict> Optional find(Class type, Predicate predicate) { ClassDescription description = ClassDescriptions.getDescription(type); if (description.isEnumType()) { for (Object enumDict : description.getEnums()) { @@ -151,7 +162,7 @@ static Optional find(Class type, Predicate } @SuppressWarnings("all") - static List findList(Class type, Predicate predicate) { + static & EnumDict> List findList(Class type, Predicate predicate) { ClassDescription description = ClassDescriptions.getDescription(type); if (description.isEnumType()) { return Arrays.stream(description.getEnums()) @@ -167,10 +178,13 @@ static List findList(Class type, Predicate * * @see EnumDict#find(Class, Predicate) */ - static > Optional findByValue(Class type, Object value) { + static & EnumDict> Optional findByValue(Class type, Object value) { + if (value == null) { + return Optional.empty(); + } return find(type, e -> e.getValue() == value || e.getValue().equals(value) || String - .valueOf(e.getValue()) - .equalsIgnoreCase(String.valueOf(value))); + .valueOf(e.getValue()) + .equalsIgnoreCase(String.valueOf(value))); } /** @@ -178,7 +192,7 @@ static > Optional findByValue(Class type, Obj * * @see EnumDict#find(Class, Predicate) */ - static Optional findByText(Class type, String text) { + static & EnumDict> Optional findByText(Class type, String text) { return find(type, e -> e.getText().equalsIgnoreCase(text)); } @@ -187,12 +201,12 @@ static Optional findByText(Class type, String * * @see EnumDict#find(Class, Predicate) */ - static Optional find(Class type, Object target) { + static & EnumDict> Optional find(Class type, Object target) { return find(type, v -> v.eq(target)); } @SafeVarargs - static long toMask(T... t) { + static > long toMask(T... t) { if (t == null) { return 0L; } @@ -205,38 +219,40 @@ static long toMask(T... t) { @SafeVarargs - static boolean in(T target, T... t) { - ClassDescription description= ClassDescriptions.getDescription(target.getClass()); + static & EnumDict> boolean in(T target, T... t) { + ClassDescription description = ClassDescriptions.getDescription(target.getClass()); Object[] all = description.getEnums(); if (all.length >= 64) { - List list = Arrays.asList(t); - return Arrays.stream(all) - .map(EnumDict.class::cast) - .anyMatch(list::contains); + Set allSet = new HashSet<>(Arrays.asList(all)); + for (T t1 : t) { + if (allSet.contains(t1)) { + return true; + } + } + return false; } return maskIn(toMask(t), target); } @SafeVarargs - static boolean maskIn(long mask, T... t) { + static > boolean maskIn(long mask, T... t) { long value = toMask(t); return (mask & value) == value; } @SafeVarargs - static boolean maskInAny(long mask, T... t) { + static > boolean maskInAny(long mask, T... t) { long value = toMask(t); return (mask & value) != 0; } - static List getByMask(List allOptions, long mask) { + static > List getByMask(List allOptions, long mask) { if (allOptions.size() >= 64) { throw new UnsupportedOperationException("不支持选项超过64个数据字典!"); } List arr = new ArrayList<>(); - List all = allOptions; - for (T t : all) { + for (T t : allOptions) { if (t.in(mask)) { arr.add(t); } @@ -244,12 +260,12 @@ static List getByMask(List allOptions, long mask) { return arr; } - static List getByMask(Supplier> allOptionsSupplier, long mask) { + static > List getByMask(Supplier> allOptionsSupplier, long mask) { return getByMask(allOptionsSupplier.get(), mask); } - static List getByMask(Class tClass, long mask) { + static & EnumDict> List getByMask(Class tClass, long mask) { return getByMask(Arrays.asList(tClass.getEnumConstants()), mask); } @@ -311,7 +327,7 @@ default void write(JSONSerializer jsonSerializer, Object o, Type type, int i) { @Slf4j @AllArgsConstructor @NoArgsConstructor - class EnumDictJSONDeserializer extends JsonDeserializer implements ObjectDeserializer { + class EnumDictJSONDeserializer extends JsonDeserializer implements ObjectDeserializer { private Function mapper; @Override @@ -325,7 +341,7 @@ public T deserialze(DefaultJSONParser parser, Type type, Object fieldName) { int intValue = lexer.intValue(); lexer.nextToken(JSONToken.COMMA); - return (T) EnumDict.find((Class) type, intValue); + return (T) EnumDict.find((Class) type, intValue).orElse(null); } else if (token == JSONToken.LITERAL_STRING) { String name = lexer.stringVal(); lexer.nextToken(JSONToken.COMMA); @@ -342,9 +358,9 @@ public T deserialze(DefaultJSONParser parser, Type type, Object fieldName) { if (value instanceof Map) { return (T) EnumDict.find(((Class) type), ((Map) value).get("value")) .orElseGet(() -> - EnumDict - .find(((Class) type), ((Map) value).get("text")) - .orElse(null)); + EnumDict + .find(((Class) type), ((Map) value).get("text")) + .orElse(null)); } } @@ -373,6 +389,15 @@ public Object deserialize(JsonParser jp, DeserializationContext ctxt) throws IOE if (node.isNumber()) { return mapper.apply(node.asLong()); } + if (node.isObject()) { + JsonNode value = node.get("value"); + if (value == null) { + value = node.get("text"); + } + if (value != null) { + return mapper.apply(value.asText()); + } + } } String currentName = jp.currentName(); Object currentValue = jp.getCurrentValue(); @@ -384,54 +409,87 @@ public Object deserialize(JsonParser jp, DeserializationContext ctxt) throws IOE } Supplier exceptionSupplier = () -> { List values = Stream - .of(findPropertyType.getEnumConstants()) - .map(Enum.class::cast) - .map(e -> { - if (e instanceof EnumDict) { - return ((EnumDict) e).getValue(); - } - return e.name(); - }).collect(Collectors.toList()); + .of(findPropertyType.getEnumConstants()) + .map(Enum.class::cast) + .map(e -> { + if (e instanceof EnumDict) { + return ((EnumDict) e).getValue(); + } + return e.name(); + }).collect(Collectors.toList()); return new ValidationException(currentName, "validation.parameter_does_not_exist_in_enums", currentName); }; if (EnumDict.class.isAssignableFrom(findPropertyType) && findPropertyType.isEnum()) { if (node.isObject()) { + JsonNode valueNode = node.get("value"); + Object value = null; + if (valueNode != null) { + if (valueNode.isTextual()) { + value = valueNode.textValue(); + } else if (valueNode.isNumber()) { + value = valueNode.numberValue(); + } + } return (EnumDict) EnumDict - .findByValue(findPropertyType, node.get("value").textValue()) - .orElseThrow(exceptionSupplier); + .findByValue(findPropertyType, value) + .orElseThrow(exceptionSupplier); } if (node.isNumber()) { return (EnumDict) EnumDict - .find(findPropertyType, node.numberValue()) - .orElseThrow(exceptionSupplier); + .find(findPropertyType, node.numberValue()) + .orElseThrow(exceptionSupplier); } if (node.isTextual()) { return (EnumDict) EnumDict - .find(findPropertyType, node.textValue()) - .orElseThrow(exceptionSupplier); + .find(findPropertyType, node.textValue()) + .orElseThrow(exceptionSupplier); } return exceptionSupplier.get(); } if (findPropertyType.isEnum()) { return Stream - .of(findPropertyType.getEnumConstants()) - .filter(o -> { - if (node.isTextual()) { - return node.textValue().equalsIgnoreCase(((Enum) o).name()); - } - if (node.isNumber()) { - return node.intValue() == ((Enum) o).ordinal(); - } - return false; - }) - .findAny() - .orElseThrow(exceptionSupplier); + .of(findPropertyType.getEnumConstants()) + .filter(o -> { + if (node.isTextual()) { + return node.textValue().equalsIgnoreCase(((Enum) o).name()); + } + if (node.isNumber()) { + return node.intValue() == ((Enum) o).ordinal(); + } + return false; + }) + .findAny() + .orElseThrow(exceptionSupplier); } - log.warn("unsupported deserialize enum json : {}", node); + log.warn("unsupported deserialize enum json : {} for: {}@{}", node, currentName, currentValue); return null; } } + /** + * 创建动态的字典选项 + * + * @param value 值 + * @return 字典选项 + */ + static EnumDict create(String value) { + return create(value, null); + } + + /** + * 创建动态的字典选项 + * + * @param value 值 + * @param text 说明 + * @return 字典选项 + */ + static EnumDict create(String value, String text) { + return DefaultItemDefine + .builder() + .value(value) + .text(text) + .build(); + } } diff --git a/hsweb-core/src/main/java/org/hswebframework/web/dict/ItemDefine.java b/hsweb-core/src/main/java/org/hswebframework/web/dict/ItemDefine.java index 45f147bb4..e3dee490d 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/dict/ItemDefine.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/dict/ItemDefine.java @@ -1,6 +1,5 @@ package org.hswebframework.web.dict; -import java.util.List; /** * @author zhouhao diff --git a/hsweb-core/src/main/java/org/hswebframework/web/dict/defaults/DefaultDictDefineRepository.java b/hsweb-core/src/main/java/org/hswebframework/web/dict/defaults/DefaultDictDefineRepository.java index d81024ac9..0a29e2506 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/dict/defaults/DefaultDictDefineRepository.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/dict/defaults/DefaultDictDefineRepository.java @@ -3,18 +3,11 @@ import lombok.extern.slf4j.Slf4j; import org.hswebframework.utils.StringUtils; import org.hswebframework.web.dict.*; -import org.springframework.core.io.Resource; -import org.springframework.core.io.support.PathMatchingResourcePatternResolver; -import org.springframework.core.io.support.ResourcePatternResolver; -import org.springframework.core.type.classreading.MetadataReader; -import org.springframework.core.type.classreading.SimpleMetadataReaderFactory; -import org.springframework.util.ReflectionUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import java.lang.reflect.Field; import java.util.*; -import java.util.stream.Collectors; +import java.util.concurrent.ConcurrentHashMap; /** * @author zhouhao @@ -22,10 +15,13 @@ */ @Slf4j public class DefaultDictDefineRepository implements DictDefineRepository { - protected static final Map parsedDict = new HashMap<>(); + protected final Map parsedDict = new ConcurrentHashMap<>(); - public static void registerDefine(DictDefine define) { - if (define == null) { + public DefaultDictDefineRepository() { + } + + public void registerDefine(DictDefine define) { + if (define == null || define.getId() == null) { return; } parsedDict.put(define.getId(), define); @@ -34,42 +30,52 @@ public static void registerDefine(DictDefine define) { @SuppressWarnings("all") public static DictDefine parseEnumDict(Class type) { - Dict dict = type.getAnnotation(Dict.class); - if (!type.isEnum()) { - throw new UnsupportedOperationException("unsupported type " + type); - } - List> items = new ArrayList<>(); - for (Object enumConstant : type.getEnumConstants()) { - if (enumConstant instanceof EnumDict) { - items.add((EnumDict) enumConstant); - } else { - Enum e = ((Enum) enumConstant); - items.add(DefaultItemDefine.builder() - .value(e.name()) - .text(e.name()) - .ordinal(e.ordinal()) - .build()); + try { + Dict dict = type.getAnnotation(Dict.class); + if (!type.isEnum()) { + return null; } - } - DefaultDictDefine define = new DefaultDictDefine(); - if (dict != null) { - define.setId(dict.value()); - define.setComments(dict.comments()); - define.setAlias(dict.alias()); - } else { + Object[] constants = type.getEnumConstants(); + List> items = new ArrayList<>(constants.length); - String id = StringUtils.camelCase2UnderScoreCase(type.getSimpleName()).replace("_", "-"); - if (id.startsWith("-")) { - id = id.substring(1); + for (Object enumConstant : constants) { + if (enumConstant instanceof EnumDict) { + items.add((EnumDict) enumConstant); + } else { + Enum e = ((Enum) enumConstant); + items.add( + DefaultItemDefine + .builder() + .value(e.name()) + .text(e.name()) + .ordinal(e.ordinal()) + .build()); + } } - define.setId(id); - define.setAlias(type.getSimpleName()); + + DefaultDictDefine define = new DefaultDictDefine(); + if (dict != null) { + define.setId(dict.value()); + define.setComments(dict.comments()); + define.setAlias(dict.alias()); + } else { + + String id = StringUtils.camelCase2UnderScoreCase(type.getSimpleName()).replace("_", "-"); + if (id.startsWith("-")) { + id = id.substring(1); + } + define.setId(id); + define.setAlias(type.getSimpleName()); // define.setComments(); + } + define.setItems(items); + log.trace("parse enum dict : {} as : {}", type, define.getId()); + return define; + } catch (Throwable e) { + log.warn("parse enum class [{}] error", type, e); + return null; } - define.setItems(items); - log.trace("parse enum dict : {} as : {}", type, define.getId()); - return define; } diff --git a/hsweb-core/src/main/java/org/hswebframework/web/dict/defaults/DefaultItemDefine.java b/hsweb-core/src/main/java/org/hswebframework/web/dict/defaults/DefaultItemDefine.java index 94df21045..ff73093c9 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/dict/defaults/DefaultItemDefine.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/dict/defaults/DefaultItemDefine.java @@ -5,8 +5,10 @@ import lombok.Data; import lombok.NoArgsConstructor; import org.hswebframework.web.dict.ItemDefine; +import org.hswebframework.web.i18n.MultipleI18nSupportEntity; -import java.util.List; +import java.util.Locale; +import java.util.Map; /** * @author zhouhao @@ -16,9 +18,24 @@ @Builder @NoArgsConstructor @AllArgsConstructor -public class DefaultItemDefine implements ItemDefine { +public class DefaultItemDefine implements ItemDefine, MultipleI18nSupportEntity { + private static final long serialVersionUID = 1L; + private String text; private String value; private String comments; private int ordinal; + private Map> i18nMessages; + + public DefaultItemDefine(String text, String value, String comments, int ordinal) { + this.text = text; + this.value = value; + this.comments = comments; + this.ordinal = ordinal; + } + + @Override + public String getI18nMessage(Locale locale) { + return getI18nMessage("text", locale, text); + } } diff --git a/hsweb-core/src/main/java/org/hswebframework/web/event/AsyncEventHooks.java b/hsweb-core/src/main/java/org/hswebframework/web/event/AsyncEventHooks.java index 4b729b53f..71727f120 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/event/AsyncEventHooks.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/event/AsyncEventHooks.java @@ -26,12 +26,10 @@ static Mono hookFirst(AsyncEvent event, Mono publisher) { if (hooksList == null) { return publisher; } - if (hooksList.size() == 1) { - return hooksList.getFirst().hookFirst(event, publisher); + for (AsyncEventHook asyncEventHook : hooksList) { + publisher = asyncEventHook.hookFirst(event, publisher); } - return Flux.fromIterable(hooksList) - .flatMap(hook -> hook.hookFirst(event, publisher)) - .then(); + return publisher; } static Mono hookAsync(AsyncEvent event, Mono publisher) { @@ -39,12 +37,10 @@ static Mono hookAsync(AsyncEvent event, Mono publisher) { if (hooksList == null) { return publisher; } - if (hooksList.size() == 1) { - return hooksList.getFirst().hookAsync(event, publisher); + for (AsyncEventHook asyncEventHook : hooksList) { + publisher = asyncEventHook.hookAsync(event, publisher); } - return Flux.fromIterable(hooksList) - .flatMap(hook -> hook.hookAsync(event, publisher)) - .then(); + return publisher; } diff --git a/hsweb-core/src/main/java/org/hswebframework/web/event/DefaultAsyncEvent.java b/hsweb-core/src/main/java/org/hswebframework/web/event/DefaultAsyncEvent.java index 9d7360d11..8307a4922 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/event/DefaultAsyncEvent.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/event/DefaultAsyncEvent.java @@ -25,12 +25,14 @@ public synchronized void first(Publisher publisher) { } @Override - public void transformFirst(Function, Publisher> mapper) { + public synchronized void transformFirst(Function, Publisher> mapper) { + hasListener = true; this.first = Mono.fromDirect(mapper.apply(this.first)); } @Override - public void transform(Function, Publisher> mapper) { + public synchronized void transform(Function, Publisher> mapper) { + hasListener = true; this.async = Mono.fromDirect(mapper.apply(this.async)); } diff --git a/hsweb-core/src/main/java/org/hswebframework/web/exception/BusinessException.java b/hsweb-core/src/main/java/org/hswebframework/web/exception/BusinessException.java index 16a09d7a7..a74b541de 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/exception/BusinessException.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/exception/BusinessException.java @@ -26,12 +26,11 @@ * @author zhouhao * @since 2.0 */ +@Getter public class BusinessException extends I18nSupportException { private static final long serialVersionUID = 5441923856899380112L; - @Getter private int status = 500; - @Getter private String code; public BusinessException(String message) { @@ -62,4 +61,40 @@ public BusinessException(String message, Throwable cause, int status) { super(message, cause); this.status = status; } + + /** + * 不填充线程栈的异常,在一些对线程栈不敏感,且对异常不可控(如: 来自未认证请求产生的异常)的情况下不填充线程栈对性能有利。 + */ + public static class NoStackTrace extends BusinessException { + public NoStackTrace(String message) { + this(message, 500); + } + + public NoStackTrace(String message, int status, Object... args) { + this(message, null, status, args); + } + + public NoStackTrace(String message, String code) { + this(message, code, 500); + } + + public NoStackTrace(String message, String code, int status, Object... args) { + super(message, code, status, args); + + } + + public NoStackTrace(String message, Throwable cause) { + super(message, cause); + } + + public NoStackTrace(String message, Throwable cause, int status) { + super(message, cause, status); + } + + + @Override + public final synchronized Throwable fillInStackTrace() { + return this; + } + } } diff --git a/hsweb-core/src/main/java/org/hswebframework/web/exception/I18nSupportException.java b/hsweb-core/src/main/java/org/hswebframework/web/exception/I18nSupportException.java index 2478ab72b..9a5e8a787 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/exception/I18nSupportException.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/exception/I18nSupportException.java @@ -9,7 +9,6 @@ import reactor.core.publisher.Mono; import java.util.Locale; -import java.util.Objects; /** * 支持国际化消息的异常,code为 @@ -54,7 +53,7 @@ public String getOriginalMessage() { @Override public String getMessage() { - return super.getMessage() != null ? super.getMessage() : getLocalizedMessage(); + return getLocalizedMessage(); } @Override @@ -68,8 +67,8 @@ public String getLocalizedMessage(Locale locale) { public final Mono getLocalizedMessageReactive() { return LocaleUtils - .currentReactive() - .map(this::getLocalizedMessage); + .currentReactive() + .map(this::getLocalizedMessage); } public static String tryGetLocalizedMessage(Throwable error, Locale locale) { @@ -93,7 +92,25 @@ public static String tryGetLocalizedMessage(Throwable error) { public static Mono tryGetLocalizedMessageReactive(Throwable error) { return LocaleUtils - .currentReactive() - .map(locale -> tryGetLocalizedMessage(error, locale)); + .currentReactive() + .map(locale -> tryGetLocalizedMessage(error, locale)); + } + + /** + * 不填充线程栈的异常,在一些对线程栈不敏感,且对异常不可控(如: 来自未认证请求产生的异常)的情况下不填充线程栈对性能有利。 + */ + public static class NoStackTrace extends I18nSupportException { + public NoStackTrace(String code, Object... args) { + super(code, args); + } + + public NoStackTrace(String code, Throwable cause, Object... args) { + super(code, cause, args); + } + + @Override + public final synchronized Throwable fillInStackTrace() { + return this; + } } } diff --git a/hsweb-core/src/main/java/org/hswebframework/web/exception/NotFoundException.java b/hsweb-core/src/main/java/org/hswebframework/web/exception/NotFoundException.java index b005303e4..bcfabe713 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/exception/NotFoundException.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/exception/NotFoundException.java @@ -31,4 +31,22 @@ public NotFoundException(String message, Object... args) { public NotFoundException() { this("error.not_found"); } + + /** + * 不填充线程栈的异常,在一些对线程栈不敏感,且对异常不可控(如: 来自未认证请求产生的异常)的情况下不填充线程栈对性能有利。 + */ + public static class NoStackTrace extends NotFoundException { + public NoStackTrace(String code, Object... args) { + super(code, args); + } + + public NoStackTrace() { + super(); + } + + @Override + public final synchronized Throwable fillInStackTrace() { + return this; + } + } } diff --git a/hsweb-core/src/main/java/org/hswebframework/web/exception/TraceSourceException.java b/hsweb-core/src/main/java/org/hswebframework/web/exception/TraceSourceException.java index 91bf44ab1..edd0cd4e4 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/exception/TraceSourceException.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/exception/TraceSourceException.java @@ -2,6 +2,7 @@ import org.hswebframework.web.i18n.LocaleUtils; import org.springframework.util.StringUtils; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.context.Context; import reactor.util.context.ContextView; @@ -36,7 +37,7 @@ public TraceSourceException(String message) { } public TraceSourceException(Throwable e) { - super(e.getMessage(),e); + super(e.getMessage(), e); } public TraceSourceException(String message, Throwable e) { @@ -72,9 +73,10 @@ protected TraceSourceException self() { * 深度溯源上下文,用来标识是否是深度溯源的异常.开启深度追踪后,会创建新的{@link TraceSourceException}对象. * * @return 上下文 - * @see reactor.core.publisher.Flux#contextWrite(ContextView) + * @see Flux#contextWrite(ContextView) * @see Mono#contextWrite(ContextView) */ + @Deprecated public static Context deepTraceContext() { return deepTraceContext; } @@ -106,32 +108,50 @@ public static Function> transfer(Object source) { * @param source 源 * @param 泛型 * @return 转换器 - * @see reactor.core.publisher.Flux#onErrorResume(Function) + * @see Flux#onErrorResume(Function) * @see Mono#onErrorResume(Function) */ public static Function> transfer(String operation, Object source) { if (source == null && operation == null) { return Mono::error; } - return err -> { - if (err instanceof TraceSourceException) { - return Mono - .deferContextual(ctx -> { - if (ctx.hasKey(deepTraceKey)) { - return Mono.error(new TraceSourceException(err).withSource(operation,source)); - } else { - return Mono.error(((TraceSourceException) err).withSource(operation,source)); - } - }); - } - return Mono.error(new TraceSourceException(err).withSource(operation,source)); - }; + return err -> Mono.error(transform(err, operation, source)); + } + + /** + * 填充溯源信息到异常中 + * + * @param error 异常 + * @param operation 操作名称 + * @param source 源数据 + * @return 填充后的异常 + */ + public static Throwable transform(Throwable error, String operation, Object source) { + error.addSuppressed( + new StacklessTraceSourceException().withSource(operation, source) + ); + return error; } public static Object tryGetSource(Throwable err) { + if (err instanceof TraceSourceException) { return ((TraceSourceException) err).getSource(); } + + for (Throwable throwable : err.getSuppressed()) { + Object source = tryGetSource(throwable); + if (source != null) { + return source; + } + } + + Throwable cause = err.getCause(); + + if (cause != null) { + return tryGetSource(cause); + } + return null; } @@ -139,9 +159,55 @@ public static String tryGetOperation(Throwable err) { if (err instanceof TraceSourceException) { return ((TraceSourceException) err).getOperation(); } + + for (Throwable throwable : err.getSuppressed()) { + String operation = tryGetOperation(throwable); + if (operation != null) { + return operation; + } + } + + Throwable cause = err.getCause(); + if (cause != null) { + return tryGetOperation(cause); + } return null; } + protected String getExceptionName() { + return this.getClass().getCanonicalName(); + } + + @Override + public String toString() { + String className = getExceptionName(); + String message = this.getLocalizedMessage(); + String operation = this.operation; + String source = Optional + .ofNullable(this.source) + .map(Object::toString) + .orElse(null); + + StringBuilder builder = new StringBuilder( + className.length() + + (message == null ? 0 : message.length()) + + (operation == null ? 0 : operation.length()) + + (source == null ? 0 : source.length())); + + builder.append(className); + if (message != null) { + builder.append(':').append(message); + } + if (operation != null) { + builder.append("\n\t[Operation] ⇢ ").append(operation); + } + if (source != null) { + builder.append("\n\t [Source] ⇢ ").append(source); + } + + return builder.toString(); + } + public static String tryGetOperationLocalized(Throwable err, Locale locale) { String opt = tryGetOperation(err); return StringUtils.hasText(opt) ? LocaleUtils.resolveMessage(opt, locale, opt) : opt; @@ -149,12 +215,35 @@ public static String tryGetOperationLocalized(Throwable err, Locale locale) { public static Mono tryGetOperationLocalizedReactive(Throwable err) { return LocaleUtils - .currentReactive() - .handle((locale, sink) -> { - String opt = tryGetOperationLocalized(err, locale); - if (opt != null) { - sink.next(opt); - } - }); + .currentReactive() + .handle((locale, sink) -> { + String opt = tryGetOperationLocalized(err, locale); + if (opt != null) { + sink.next(opt); + } + }); + } + + public static class StacklessTraceSourceException extends TraceSourceException { + public StacklessTraceSourceException() { + super(); + } + + public StacklessTraceSourceException(String message) { + super(message); + } + + public StacklessTraceSourceException(Throwable e) { + super(e.getMessage(), e); + } + + public StacklessTraceSourceException(String message, Throwable e) { + super(message, e); + } + + @Override + public synchronized Throwable fillInStackTrace() { + return this; + } } } diff --git a/hsweb-core/src/main/java/org/hswebframework/web/exception/ValidationException.java b/hsweb-core/src/main/java/org/hswebframework/web/exception/ValidationException.java index 97d61f2c7..370b974bc 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/exception/ValidationException.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/exception/ValidationException.java @@ -49,8 +49,8 @@ public ValidationException(Set> violations) { //{0} 属性 ,{1} 验证消息 //property也支持国际化? String propertyI18n = propertyI18nEnabled ? - first.getRootBeanClass().getName() + "." + property - : property; + first.getRootBeanClass().getName() + "." + property + : property; setArgs(new Object[]{propertyI18n, first.getMessage()}); @@ -64,11 +64,11 @@ public ValidationException(Set> violations) { public List getDetails(Locale locale) { return CollectionUtils.isEmpty(details) - ? Collections.emptyList() - : details - .stream() - .map(detail -> detail.translateI18n(locale)) - .collect(Collectors.toList()); + ? Collections.emptyList() + : details + .stream() + .map(detail -> detail.translateI18n(locale)) + .collect(Collectors.toList()); } @Override @@ -102,4 +102,31 @@ public Detail translateI18n(Locale locale) { return this; } } + + /** + * 不填充线程栈的异常,在一些对线程栈不敏感,且对异常不可控(如: 来自未认证请求产生的异常)的情况下不填充线程栈对性能有利。 + */ + public static class NoStackTrace extends ValidationException { + public NoStackTrace(String message) { + super(message); + } + + public NoStackTrace(String property, String message, Object... args) { + super(property, message, args); + } + + public NoStackTrace(String message, List details, Object... args) { + super(message, details, args); + + } + + public NoStackTrace(Set> violations) { + super(violations); + } + + @Override + public final synchronized Throwable fillInStackTrace() { + return this; + } + } } diff --git a/hsweb-core/src/main/java/org/hswebframework/web/exception/analyzer/ExceptionAnalyzer.java b/hsweb-core/src/main/java/org/hswebframework/web/exception/analyzer/ExceptionAnalyzer.java new file mode 100644 index 000000000..f7dcfd60c --- /dev/null +++ b/hsweb-core/src/main/java/org/hswebframework/web/exception/analyzer/ExceptionAnalyzer.java @@ -0,0 +1,27 @@ +package org.hswebframework.web.exception.analyzer; + +/** + * 异常分析器,用于分析异常信息. 实现此接口,并使用SPI进行拓展. + * + *
{@code
+ *
+ *  META-INF/services/org.hswebframework.web.exception.analyzer.ExceptionAnalyzer
+ *
+ * }
+ * + * @author zhouhao + * @since 4.0.18 + * @see ExceptionAnalyzerReporter + */ +public interface ExceptionAnalyzer { + + /** + * 执行分析 + * + * @param error 异常信息 + * @return 是否被处理 + */ + boolean analyze(Throwable error); + + +} diff --git a/hsweb-core/src/main/java/org/hswebframework/web/exception/analyzer/ExceptionAnalyzerReporter.java b/hsweb-core/src/main/java/org/hswebframework/web/exception/analyzer/ExceptionAnalyzerReporter.java new file mode 100644 index 000000000..d2bd60afc --- /dev/null +++ b/hsweb-core/src/main/java/org/hswebframework/web/exception/analyzer/ExceptionAnalyzerReporter.java @@ -0,0 +1,84 @@ +package org.hswebframework.web.exception.analyzer; + +import lombok.extern.slf4j.Slf4j; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +/** + * 提供基础的异常分析器实现 + * + * @author zhouhao + * @since 4.0.18 + */ +@Slf4j +public class ExceptionAnalyzerReporter implements ExceptionAnalyzer { + + private final List reporter = new CopyOnWriteArrayList<>(); + + + public static String wrapLog(String message) { + char[] arr = new char[message.length() + 2]; + Arrays.fill(arr, '='); + arr[0] = '\n'; + arr[arr.length - 1] = '\n'; + String line = new String(arr); + return line + message + line; + } + + protected void addReporter(Predicate predicate, + Consumer reporter) { + this.reporter.add(new Reporter() { + @Override + public boolean predicate(Throwable error) { + return predicate.test(error); + } + + @Override + public void report(Throwable error) { + reporter.accept(error); + } + }); + } + + protected void addSimpleReporter(Pattern pattern, Consumer reporter) { + + addReporter((error) -> { + if (error.getMessage() == null) { + return pattern.matcher(error.toString()).matches(); + } + return pattern.matcher(error.getMessage()).matches() || pattern.matcher(error.toString()).matches(); + }, reporter); + } + + public boolean doReportException(Throwable failure) { + Throwable cause = failure; + while (cause != null) { + for (Reporter _reporter : this.reporter) { + if (_reporter.predicate(cause)) { + _reporter.report(cause); + return true; + } + } + cause = cause.getCause(); + } + return false; + } + + @Override + public boolean analyze(Throwable error) { + return doReportException(error); + } + + interface Reporter { + + boolean predicate(Throwable error); + + void report(Throwable error); + + } +} diff --git a/hsweb-core/src/main/java/org/hswebframework/web/exception/analyzer/ExceptionAnalyzers.java b/hsweb-core/src/main/java/org/hswebframework/web/exception/analyzer/ExceptionAnalyzers.java new file mode 100644 index 000000000..626b3b753 --- /dev/null +++ b/hsweb-core/src/main/java/org/hswebframework/web/exception/analyzer/ExceptionAnalyzers.java @@ -0,0 +1,47 @@ +package org.hswebframework.web.exception.analyzer; + +import lombok.extern.slf4j.Slf4j; + +import java.util.List; +import java.util.ServiceLoader; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * 异常分析器,用于分析异常信息.使用{@link ExceptionAnalyzer}进行分析拓展. + * + * @author zhouhao + * @see ExceptionAnalyzer + * @since 4.0.18 + */ +@Slf4j +public class ExceptionAnalyzers { + + private static final List ANALYZER = new CopyOnWriteArrayList<>(); + + private ExceptionAnalyzers() { + + } + + static { + ServiceLoader.load(ExceptionAnalyzer.class).forEach(ANALYZER::add); + } + + public static void addAnalyzer(ExceptionAnalyzer analyzer) { + log.debug("add ExceptionAnalyzer:{}", analyzer); + ANALYZER.add(analyzer); + } + + public static boolean analyze(Throwable failure) { + Throwable cause = failure; + while (cause != null) { + for (ExceptionAnalyzer _analyzer : ANALYZER) { + if (_analyzer.analyze(cause)) { + return true; + } + } + cause = cause.getCause(); + } + return false; + } + +} diff --git a/hsweb-core/src/main/java/org/hswebframework/web/i18n/I18nSupportEntity.java b/hsweb-core/src/main/java/org/hswebframework/web/i18n/I18nSupportEntity.java new file mode 100644 index 000000000..d86781d2f --- /dev/null +++ b/hsweb-core/src/main/java/org/hswebframework/web/i18n/I18nSupportEntity.java @@ -0,0 +1,73 @@ +package org.hswebframework.web.i18n; + +import org.apache.commons.collections4.MapUtils; + +import java.util.Locale; +import java.util.Map; + +/** + * 国际化支持实体,实现此接口,提供基础的国际化支持.如:针对实体类某些字段的国际化支持. + * + * @author zhouhao + * @since 4.0.18 + * @see SingleI18nSupportEntity + * @see MultipleI18nSupportEntity + */ +public interface I18nSupportEntity { + + /** + * 根据key获取全部国际化信息,key为地区标识,value为国际化消息. + *
{@code
+     *
+     *    {"zh":"你好","en":"hello"}
+     *
+     *  }
+ * + * @param key key + * @return 国际化信息 + */ + Map getI18nMessages(String key); + + /** + * 根据当前地区获取,指定key的国际化信息. + *
{@code
+     *
+     *    public String getI18nName(){
+     *        return getI18nMessages("name",this.name);
+     *    }
+     *
+     * }
+ * + * @param key key + * @return 国际化信息 + * @see LocaleUtils#transform + */ + default String getI18nMessage(String key, String defaultMessage) { + return getI18nMessage(key, LocaleUtils.current(), defaultMessage); + } + + /** + * 根据指定的语言地区,获取指定key的国际化信息. + *
{@code
+     *
+     *    public String getI18nName(){
+     *        return getI18nMessages("name",Locale.US,this.name);
+     *    }
+     *
+     * }
+ * + * @param key key + * @return 国际化信息 + */ + default String getI18nMessage(String key, Locale locale, String defaultMessage) { + + Map entries = getI18nMessages(key); + + if (MapUtils.isEmpty(entries)) { + return defaultMessage; + } + + return LocaleUtils.getMessage(entries::get, locale, () -> defaultMessage); + } + +} diff --git a/hsweb-core/src/main/java/org/hswebframework/web/i18n/I18nSupportUtils.java b/hsweb-core/src/main/java/org/hswebframework/web/i18n/I18nSupportUtils.java new file mode 100644 index 000000000..e5b48f1ae --- /dev/null +++ b/hsweb-core/src/main/java/org/hswebframework/web/i18n/I18nSupportUtils.java @@ -0,0 +1,52 @@ +package org.hswebframework.web.i18n; + +import org.apache.commons.collections4.MapUtils; +import org.springframework.util.StringUtils; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +public class I18nSupportUtils { + + public static Map> putI18nMessages(String i18nKey, + String property, + Collection locales, + String defaultMsg, + Map> container) { + if (container == null) { + container = new HashMap<>(); + } + + container.compute( + property, + (p, c) -> { + Map msg = putI18nMessages(i18nKey, locales, defaultMsg, c); + //为空不存储 + return MapUtils.isEmpty(msg) ? null : msg; + }); + + return container; + } + + public static Map putI18nMessages(String i18nKey, + Collection locales, + String defaultMsg, + Map container) { + if (container == null) { + container = new HashMap<>(); + } + + for (Locale locale : locales) { + String msg = LocaleUtils.resolveMessage(i18nKey, locale, defaultMsg); + if (StringUtils.hasText(msg)) { + container.put(locale.toString(), msg); + } + } + + return container; + } + + +} diff --git a/hsweb-core/src/main/java/org/hswebframework/web/i18n/LocaleUtils.java b/hsweb-core/src/main/java/org/hswebframework/web/i18n/LocaleUtils.java index e41cd5928..ec79c63b3 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/i18n/LocaleUtils.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/i18n/LocaleUtils.java @@ -2,6 +2,7 @@ import io.netty.util.concurrent.FastThreadLocal; import lombok.AllArgsConstructor; +import lombok.SneakyThrows; import org.hswebframework.web.exception.I18nSupportException; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; @@ -11,12 +12,9 @@ import reactor.util.context.Context; import javax.annotation.Nonnull; -import java.util.Locale; +import java.util.*; import java.util.concurrent.Callable; -import java.util.function.BiConsumer; -import java.util.function.BiFunction; -import java.util.function.Consumer; -import java.util.function.Function; +import java.util.function.*; /** * 用于进行国际化消息转换 @@ -39,6 +37,54 @@ public final class LocaleUtils { static MessageSource messageSource = UnsupportedMessageSource.instance(); + static Set supportsLocales; + + static { + supportsLocales = new HashSet<>(); + supportsLocales.add(Locale.CHINESE); + supportsLocales.add(Locale.ENGLISH); + String prop = System.getProperty("hsweb.locale.supports"); + if (prop != null) { + try { + for (String locale : prop.split(",")) { + if (locale.isEmpty()) { + continue; + } + supportsLocales.add(Locale.forLanguageTag(locale)); + } + } catch (Throwable e) { + System.err.println("error parse hsweb.locale.supports :" + prop); + } + } + } + + /** + * 获取支持的语言地区,默认支持中文和英文,可通过jvm参数: -Dhsweb.locale.supports=zh,en 来指定支持的语言地区 + * + * @return 支持的语言地区 + */ + public static Set getSupportLocales() { + return Collections.unmodifiableSet(supportsLocales); + } + + /** + * 从指定数据源中获取国际化消息 + * + * @param messageSource 消息源 + * @param locale 语言地区 + * @param defaultMessage 默认消息 + */ + public static String getMessage(Function messageSource, + Locale locale, + Supplier defaultMessage) { + String str = locale.toString(); + String msg = messageSource.apply(str); + if (msg == null) { + msg = messageSource.apply(locale.getLanguage()); + } + return msg == null ? defaultMessage.get() : msg; + } + /** * 获取当前的语言地区,如果没有设置则返回系统默认语言 * @@ -90,6 +136,23 @@ public static void doWith(Locale locale, Consumer consumer) { } } + /** + * 使用指定的区域来执行某些操作 + * + * @param locale 区域 + * @param callable 任务 + */ + @SneakyThrows + public static T doWith(Locale locale, Callable callable) { + Locale old = CONTEXT_THREAD_LOCAL.get(); + try { + CONTEXT_THREAD_LOCAL.set(locale); + return callable.call(); + } finally { + CONTEXT_THREAD_LOCAL.set(old); + } + } + /** * 在响应式作用,使用指定的区域作为语言环境,在下游则可以使用{@link LocaleUtils#currentReactive()}来获取 *

@@ -113,24 +176,25 @@ public static Function useLocale(Locale locale) { @SuppressWarnings("all") public static Mono currentReactive() { return Mono - .deferContextual(ctx -> Mono.just(ctx.getOrDefault(Locale.class, DEFAULT_LOCALE))); + .deferContextual(ctx -> Mono.just(ctx.getOrDefault(Locale.class, DEFAULT_LOCALE))); } + public static Mono doInReactive(Callable call) { return currentReactive() - .handle((locale, sink) -> { - Locale old = CONTEXT_THREAD_LOCAL.get(); - try { - CONTEXT_THREAD_LOCAL.set(locale); - T data = call.call(); - if (data != null) { - sink.next(data); - } - } catch (Throwable e) { - sink.error(e); - } finally { - CONTEXT_THREAD_LOCAL.set(old); + .handle((locale, sink) -> { + Locale old = CONTEXT_THREAD_LOCAL.get(); + try { + CONTEXT_THREAD_LOCAL.set(locale); + T data = call.call(); + if (data != null) { + sink.next(data); } - }); + } catch (Throwable e) { + sink.error(e); + } finally { + CONTEXT_THREAD_LOCAL.set(old); + } + }); } /** @@ -271,11 +335,11 @@ public static Mono doWithReactive(MessageSource messageSource, BiFunction mapper, Object... args) { return currentReactive() - .map(locale -> { - String msg = message.apply(source); - String newMsg = resolveMessage(messageSource, locale, msg, msg, args); - return mapper.apply(source, newMsg); - }); + .map(locale -> { + String msg = message.apply(source); + String newMsg = resolveMessage(messageSource, locale, msg, msg, args); + return mapper.apply(source, newMsg); + }); } /** @@ -288,7 +352,7 @@ public static Mono doWithReactive(MessageSource messageSource, public static Mono resolveMessageReactive(String code, Object... args) { return currentReactive() - .map(locale -> resolveMessage(messageSource, locale, code, code, args)); + .map(locale -> resolveMessage(messageSource, locale, code, code, args)); } /** @@ -303,7 +367,7 @@ public static Mono resolveMessageReactive(MessageSource messageSource, String code, Object... args) { return currentReactive() - .map(locale -> resolveMessage(messageSource, locale, code, code, args)); + .map(locale -> resolveMessage(messageSource, locale, code, code, args)); } /** @@ -417,12 +481,12 @@ public static > Function doOn(SignalType type, B return publisher -> { if (publisher instanceof Mono) { return (T) Mono - .from(publisher) - .doOnEach(on(type, operation)); - } - return (T) Flux .from(publisher) .doOnEach(on(type, operation)); + } + return (T) Flux + .from(publisher) + .doOnEach(on(type, operation)); }; } @@ -488,7 +552,7 @@ public void subscribe(@Nonnull CoreSubscriber actual) { actual.currentContext().getOrDefault(Locale.class, DEFAULT_LOCALE), (a, l) -> { source.subscribe( - new LocaleSwitchSubscriber<>(a) + new LocaleSwitchSubscriber<>(a) ); return null; } @@ -506,7 +570,7 @@ public void subscribe(@Nonnull CoreSubscriber actual) { actual.currentContext().getOrDefault(Locale.class, DEFAULT_LOCALE), (a, l) -> { source.subscribe( - new LocaleSwitchSubscriber<>(a) + new LocaleSwitchSubscriber<>(a) ); return null; } @@ -522,7 +586,7 @@ static class LocaleSwitchSubscriber extends BaseSubscriber { @Nonnull public Context currentContext() { return actual - .currentContext(); + .currentContext(); } @Override @@ -532,7 +596,7 @@ protected void hookOnSubscribe(@Nonnull Subscription subscription) { private Locale current() { return currentContext() - .getOrDefault(Locale.class, DEFAULT_LOCALE); + .getOrDefault(Locale.class, DEFAULT_LOCALE); } @Override diff --git a/hsweb-core/src/main/java/org/hswebframework/web/i18n/MultipleI18nSupportEntity.java b/hsweb-core/src/main/java/org/hswebframework/web/i18n/MultipleI18nSupportEntity.java new file mode 100644 index 000000000..7e4c6806b --- /dev/null +++ b/hsweb-core/src/main/java/org/hswebframework/web/i18n/MultipleI18nSupportEntity.java @@ -0,0 +1,51 @@ +package org.hswebframework.web.i18n; + +import io.swagger.v3.oas.annotations.media.Schema; +import org.apache.commons.collections4.MapUtils; + +import java.util.Collections; +import java.util.Map; + +/** + * 支持多个国际化信息的实体类,用于多个字段的国际化支持. + * + * @author zhouhao + * @since 4.0.18 + * @see I18nSupportUtils + */ +public interface MultipleI18nSupportEntity extends I18nSupportEntity { + + /** + * 全部国际化信息,key为字段名,value为国际化信息. + *

{@code
+     *  {
+     *      "name":{"zh":"中文","en":"english"},
+     *      "desc":{"zh":"描述","en":"description"}
+     *  }
+     * }
+ * + * @return 国际化信息 + */ + @Schema(description = "国际化配置", example = "{\"name\":{\"zh\":\"名称\",\"en\":\"Name\"}}") + Map> getI18nMessages(); + + /** + * 根据key获取全部国际化信息,key为地区标识,value为国际化消息. + *
{@code
+     *
+     *    {"zh":"你好","en":"hello"}
+     *
+     *  }
+ * + * @param key key + * @return 国际化信息 + */ + @Override + default Map getI18nMessages(String key) { + Map> source = getI18nMessages(); + if (MapUtils.isEmpty(source)) { + return Collections.emptyMap(); + } + return source.get(key); + } +} diff --git a/hsweb-core/src/main/java/org/hswebframework/web/i18n/SingleI18nSupportEntity.java b/hsweb-core/src/main/java/org/hswebframework/web/i18n/SingleI18nSupportEntity.java new file mode 100644 index 000000000..54b19ebff --- /dev/null +++ b/hsweb-core/src/main/java/org/hswebframework/web/i18n/SingleI18nSupportEntity.java @@ -0,0 +1,13 @@ +package org.hswebframework.web.i18n; + +import java.util.Map; + +public interface SingleI18nSupportEntity extends I18nSupportEntity { + + Map getI18nMessages(); + + default Map getI18nMessages(String key) { + return getI18nMessages(); + } + +} diff --git a/hsweb-core/src/main/java/org/hswebframework/web/id/RandomIdGenerator.java b/hsweb-core/src/main/java/org/hswebframework/web/id/RandomIdGenerator.java index 93a87046a..01c66f302 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/id/RandomIdGenerator.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/id/RandomIdGenerator.java @@ -4,7 +4,9 @@ import lombok.AccessLevel; import lombok.AllArgsConstructor; +import java.time.Duration; import java.util.Base64; +import java.util.Random; import java.util.concurrent.ThreadLocalRandom; @AllArgsConstructor(access = AccessLevel.PRIVATE) @@ -12,7 +14,7 @@ public class RandomIdGenerator implements IDGenerator { // java -Dgenerator.random.instance-id=8 static final RandomIdGenerator GLOBAL = new RandomIdGenerator( - Integer.getInteger("generator.random.instance-id", ThreadLocalRandom.current().nextInt(1,127)).byteValue() + Integer.getInteger("generator.random.instance-id", ThreadLocalRandom.current().nextInt(1, 127)).byteValue() ); static final Base64.Encoder encoder = Base64.getUrlEncoder().withoutPadding(); @@ -47,9 +49,48 @@ public String generate() { return encoder.encodeToString(value); } + public static boolean isRandomId(String id) { + if (id.length() < 16 || id.length() > 48) { + return false; + } + return org.apache.commons.codec.binary.Base64.isBase64(id); + } + + public static boolean timestampRangeOf(String id, Duration duration) { + try { + if (!isRandomId(id)) { + return false; + } + long now = System.currentTimeMillis(); + long ts = getTimestampInId(id); + return Math.abs(now - ts) <= duration.toMillis(); + } catch (IllegalArgumentException e) { + return false; + } + } + + public static long getTimestampInId(String id) { + byte[] bytes = Base64.getUrlDecoder().decode(id); + if (bytes.length < 6) { + return -1; + } + long now = System.currentTimeMillis(); + return ((now >>> 56) & 0xff) << 56 | + ((now >>> 48) & 0xff) << 48 | + ((now >>> 40) & 0xff) << 40 | + ((long) bytes[1] & 0xff) << 32 | + ((long) bytes[2] & 0xff) << 24 | + ((long) bytes[3] & 0xff) << 16 | + ((long) bytes[4] & 0xff) << 8 | + (long) bytes[5] & 0xff; + } + + protected Random random() { + return io.netty.util.internal.ThreadLocalRandom.current(); + } - private static void nextBytes(byte[] bytes, int offset, int len) { - ThreadLocalRandom random = ThreadLocalRandom.current(); + private void nextBytes(byte[] bytes, int offset, int len) { + Random random = random(); for (int i = offset; i < len; ) { for (int rnd = random.nextInt(), diff --git a/hsweb-core/src/main/java/org/hswebframework/web/id/SnowflakeIdGenerator.java b/hsweb-core/src/main/java/org/hswebframework/web/id/SnowflakeIdGenerator.java index 18328f602..b0fcd6655 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/id/SnowflakeIdGenerator.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/id/SnowflakeIdGenerator.java @@ -4,6 +4,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.security.SecureRandom; import java.util.*; import java.util.concurrent.ThreadLocalRandom; @@ -32,7 +33,7 @@ public class SnowflakeIdGenerator { private static final SnowflakeIdGenerator generator; static { - Random random = new Random(); + Random random = new SecureRandom(); long workerId = Long.getLong("id-worker", random.nextInt(31)); long dataCenterId = Long.getLong("id-datacenter", random.nextInt(31)); generator = new SnowflakeIdGenerator(workerId, dataCenterId); @@ -50,7 +51,7 @@ public static SnowflakeIdGenerator create() { return create(ThreadLocalRandom.current().nextInt(31), ThreadLocalRandom.current().nextInt(31)); } - private SnowflakeIdGenerator(long workerId, long dataCenterId) { + public SnowflakeIdGenerator(long workerId, long dataCenterId) { // sanity check for workerId if (workerId > maxWorkerId || workerId < 0) { throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId)); @@ -65,10 +66,11 @@ private SnowflakeIdGenerator(long workerId, long dataCenterId) { public synchronized long nextId() { long timestamp = timeGen(); - + //时间回退 if (timestamp < lastTimestamp) { - log.error("clock is moving backwards. Rejecting requests until {}.", lastTimestamp); - throw new UnsupportedOperationException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp)); + //发生回退时不拒绝,有可能出现重复数据? + log.warn("clock is moving backwards {}.", lastTimestamp); +// throw new UnsupportedOperationException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp)); } if (lastTimestamp == timestamp) { diff --git a/hsweb-core/src/main/java/org/hswebframework/web/logger/ReactiveLogger.java b/hsweb-core/src/main/java/org/hswebframework/web/logger/ReactiveLogger.java index 17cda62ab..5c1231c86 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/logger/ReactiveLogger.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/logger/ReactiveLogger.java @@ -1,5 +1,6 @@ package org.hswebframework.web.logger; +import com.google.common.collect.Maps; import lombok.extern.slf4j.Slf4j; import org.hswebframework.web.utils.CollectionUtils; import org.slf4j.MDC; @@ -42,7 +43,7 @@ public static Function start(Map context) { return ctx -> { Optional> maybeContextMap = ctx.getOrEmpty(CONTEXT_KEY); if (maybeContextMap.isPresent()) { - maybeContextMap.get().putAll(context); + maybeContextMap.get().putAll(Maps.filterValues(context,Objects::nonNull)); return ctx; } else { return ctx.put(CONTEXT_KEY, new ConcurrentHashMap<>(context)); diff --git a/hsweb-core/src/main/java/org/hswebframework/web/proxy/Proxy.java b/hsweb-core/src/main/java/org/hswebframework/web/proxy/Proxy.java index f6a453e0c..4811e6632 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/proxy/Proxy.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/proxy/Proxy.java @@ -4,16 +4,15 @@ import javassist.bytecode.AnnotationsAttribute; import javassist.bytecode.ConstPool; import javassist.bytecode.annotation.*; -import javassist.scopedpool.*; import lombok.Getter; import lombok.SneakyThrows; -import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.core.type.AnnotationMetadata; -import org.springframework.core.type.StandardAnnotationMetadata; import org.springframework.util.ClassUtils; -import java.util.Arrays; -import java.util.Map; +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.*; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; @@ -21,7 +20,7 @@ * @author zhouhao * @since 3.0 */ -public class Proxy { +public class Proxy extends URLClassLoader { private static final AtomicLong counter = new AtomicLong(1); private final CtClass ctClass; @@ -32,31 +31,114 @@ public class Proxy { @Getter private final String classFullName; + private final Set loaders = new HashSet<>(); private Class targetClass; @SneakyThrows - public static Proxy create(Class superClass, String... classPathString) { - return new Proxy<>(superClass, classPathString); + public static Proxy create(Class superClass, Class[] classPaths, String... classPathString) { + return new Proxy<>(superClass, classPaths, classPathString); } @SneakyThrows + public static Proxy create(Class superClass, String... classPathString) { + return new Proxy<>(superClass, null, classPathString); + } + public Proxy(Class superClass, String... classPathString) { + this(superClass, null, classPathString); + } + + @Override + public Class loadClass(String name) throws ClassNotFoundException { + for (ClassLoader loader : loaders) { + try { + return loader.loadClass(name); + } catch (ClassNotFoundException ignore) { + } + } + return super.loadClass(name); + } + + @Override + public URL getResource(String name) { + for (ClassLoader loader : loaders) { + URL resource = loader.getResource(name); + if (resource != null) { + return resource; + } + } + return super.getResource(name); + } + + @Override + public Enumeration getResources(String name) throws IOException { + @SuppressWarnings("all") + Enumeration[] tmp = (Enumeration[]) new Enumeration[loaders.size()]; + + return new Enumeration() { + + @Override + public boolean hasMoreElements() { + for (Enumeration urlEnumeration : tmp) { + if (urlEnumeration.hasMoreElements()) { + return true; + } + } + return false; + } + + @Override + public URL nextElement() { + for (Enumeration urlEnumeration : tmp) { + if (urlEnumeration.hasMoreElements()) { + return urlEnumeration.nextElement(); + } + } + return null; + } + }; + } + + @SneakyThrows + private static URL[] toUrl(String[] str) { + if (str == null || str.length == 0) { + return new URL[0]; + } + URL[] arr = new URL[str.length]; + for (int i = 0; i < str.length; i++) { + arr[i] = URI.create(str[i]).toURL(); + } + return arr; + } + + @SneakyThrows + public Proxy(Class superClass, Class[] classPaths, String... classPathString) { + super(toUrl(classPathString)); if (superClass == null) { throw new NullPointerException("superClass can not be null"); } this.superClass = superClass; ClassPool classPool = ClassPool.getDefault(); - classPool.insertClassPath(new ClassClassPath(this.getClass())); - classPool.insertClassPath(new LoaderClassPath(ClassUtils.getDefaultClassLoader())); - - if (classPathString != null) { - for (String path : classPathString) { - classPool.insertClassPath(path); + if (classPaths != null) { + for (Class classPath : classPaths) { + if (classPath.getClassLoader() != null && + classPath.getClassLoader() != this.getClass().getClassLoader()) { + loaders.add(classPath.getClassLoader()); + } } } - className = superClass.getSimpleName() + "FastBeanCopier" + counter.getAndAdd(1); - classFullName = superClass.getPackage() + "." + className; + + loaders.add(ClassUtils.getDefaultClassLoader()); + loaders.add(Proxy.class.getClassLoader()); + classPool.insertClassPath(new LoaderClassPath(this)); + + className = superClass.getSimpleName() + "$Proxy" + counter.getAndIncrement(); + String packageName = superClass.getPackage().getName(); + if (packageName.startsWith("java")) { + packageName = "proxy." + packageName; + } + classFullName = packageName + "." + className; ctClass = classPool.makeClass(classFullName); if (superClass != Object.class) { @@ -100,8 +182,11 @@ public static MemberValue createMemberValue(Object value, ConstPool constPool) { memberValue = new ClassMemberValue(((Class) value).getName(), constPool); } else if (value instanceof Object[]) { Object[] arr = ((Object[]) value); - ArrayMemberValue arrayMemberValue = new ArrayMemberValue(new ClassMemberValue(arr[0].getClass().getName(), constPool), constPool); - arrayMemberValue.setValue(Arrays.stream(arr) + ArrayMemberValue arrayMemberValue = new ArrayMemberValue( + new ClassMemberValue(arr[0].getClass().getName(), constPool), constPool); + arrayMemberValue.setValue( + Arrays + .stream(arr) .map(o -> createMemberValue(o, constPool)) .toArray(MemberValue[]::new)); memberValue = arrayMemberValue; @@ -147,13 +232,15 @@ private Proxy handleException(Task task) { @SneakyThrows public I newInstance() { - return getTargetClass().newInstance(); + return getTargetClass().getConstructor().newInstance(); } @SneakyThrows + @SuppressWarnings("all") public Class getTargetClass() { if (targetClass == null) { - targetClass = (Class)ctClass.toClass(ClassUtils.getDefaultClassLoader(), null); + byte[] code = ctClass.toBytecode(); + targetClass = (Class) defineClass(null, code, 0, code.length); } return targetClass; } diff --git a/hsweb-core/src/main/java/org/hswebframework/web/utils/DynamicArrayList.java b/hsweb-core/src/main/java/org/hswebframework/web/utils/DynamicArrayList.java new file mode 100644 index 000000000..e5992b2d6 --- /dev/null +++ b/hsweb-core/src/main/java/org/hswebframework/web/utils/DynamicArrayList.java @@ -0,0 +1,22 @@ +package org.hswebframework.web.utils; + +import lombok.AllArgsConstructor; + +import java.lang.reflect.Array; +import java.util.AbstractList; + +@AllArgsConstructor +public class DynamicArrayList extends AbstractList { + + private final Object value; + + @Override + public E get(int index) { + return (E) Array.get(value, index); + } + + @Override + public int size() { + return Array.getLength(value); + } +} diff --git a/hsweb-core/src/main/java/org/hswebframework/web/warn/Warning.java b/hsweb-core/src/main/java/org/hswebframework/web/warn/Warning.java new file mode 100644 index 000000000..9147d537f --- /dev/null +++ b/hsweb-core/src/main/java/org/hswebframework/web/warn/Warning.java @@ -0,0 +1,66 @@ +package org.hswebframework.web.warn; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.context.Context; +import reactor.util.context.ContextView; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; + +@Getter +@AllArgsConstructor +public class Warning { + + private static final Object CONTEXT_KEY = Warning.class; + + private final String code; + + private final Object[] args; + + + public static Context addWarnToContext(ContextView context, Supplier warning) { + Context ctx = createWarning(context); + List warnings = ctx.get(CONTEXT_KEY); + warnings.add(warning.get()); + return ctx; + } + + public static Context createWarning(ContextView context) { + Context ctx = Context.of(context); + if (!ctx.hasKey(CONTEXT_KEY)) { + ctx = ctx.put(CONTEXT_KEY, new CopyOnWriteArrayList<>()); + } + return ctx; + } + + + public static Function> resumeFluxError( + Throwable error, + Function builder) { + return err -> Flux.deferContextual(ctx -> { + Warning warning = builder.apply(err); + if (warning != null && ctx.hasKey(CONTEXT_KEY)) { + ctx.>get(CONTEXT_KEY).add(warning); + } + return Mono.empty(); + }); + } + + public static Function> resumeMonoError( + Throwable error, + Function builder) { + return err -> Mono.deferContextual(ctx -> { + Warning warning = builder.apply(err); + if (warning != null && ctx.hasKey(CONTEXT_KEY)) { + ctx.>get(CONTEXT_KEY).add(warning); + } + return Mono.empty(); + }); + } +} diff --git a/hsweb-core/src/test/java/org/hswebframework/web/bean/CompareUtilsTest.java b/hsweb-core/src/test/java/org/hswebframework/web/bean/CompareUtilsTest.java index e15bc23c7..f6b090a4c 100644 --- a/hsweb-core/src/test/java/org/hswebframework/web/bean/CompareUtilsTest.java +++ b/hsweb-core/src/test/java/org/hswebframework/web/bean/CompareUtilsTest.java @@ -11,8 +11,6 @@ import java.math.BigDecimal; import java.util.*; -import static org.junit.Assert.*; - public class CompareUtilsTest { @Test @@ -40,8 +38,8 @@ public void numberTest() { Assert.assertTrue(CompareUtils.compare(1, 1)); Assert.assertTrue(CompareUtils.compare(1, 1D)); Assert.assertTrue(CompareUtils.compare(1, 1.0D)); - Assert.assertTrue(CompareUtils.compare(1e3,"1e3")); - Assert.assertTrue(CompareUtils.compare(1e3,"1000")); + Assert.assertTrue(CompareUtils.compare(1e3, "1e3")); + Assert.assertTrue(CompareUtils.compare(1e3, "1000")); Assert.assertTrue(CompareUtils.compare(1, "1")); Assert.assertTrue(CompareUtils.compare("1.0", 1)); @@ -65,6 +63,10 @@ public void enumTest() { Assert.assertFalse(CompareUtils.compare(TestEnumDic.RED, "蓝色")); + Assert.assertFalse(CompareUtils.compare((Object) TestEnumDic.RED, TestEnumDic.BLUE)); + + Assert.assertTrue(CompareUtils.compare((Object) TestEnumDic.RED, TestEnumDic.RED)); + } @@ -73,7 +75,7 @@ public void stringTest() { Assert.assertTrue(CompareUtils.compare("20180101", DateFormatter.fromString("20180101"))); - Assert.assertTrue(CompareUtils.compare(1,"1")); + Assert.assertTrue(CompareUtils.compare(1, "1")); Assert.assertTrue(CompareUtils.compare("1", 1)); @@ -169,10 +171,19 @@ enum TestEnum { @Getter @AllArgsConstructor enum TestEnumDic implements EnumDict { - RED("RED", "红色"), BLUE("BLUE", "蓝色"); + RED("RED", "红色") { + public void function() { + + } + }, + BLUE("BLUE", "蓝色") { + public void function() { + + } + }; - private String value; - private String text; + private final String value; + private final String text; } 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 0c73a2b45..c12dba4c6 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 @@ -3,11 +3,18 @@ import com.google.common.collect.ImmutableMap; import lombok.Getter; import lombok.Setter; +import lombok.SneakyThrows; +import org.hswebframework.ezorm.core.DefaultExtendable; import org.junit.Assert; import org.junit.Test; +import org.springframework.util.ClassUtils; +import java.io.File; +import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Proxy; +import java.net.URL; +import java.net.URLClassLoader; import java.util.*; import java.util.concurrent.atomic.AtomicReference; @@ -17,6 +24,79 @@ */ public class FastBeanCopierTest { + @Test + public void testExtendableToExtendable() { + ExtendableEntity source = new ExtendableEntity(); + source.setName("test"); + source.setExtension("age", 123); + source.setExtension("color", Color.RED); + + ExtendableEntity e = FastBeanCopier.copy(source, new ExtendableEntity()); + + Assert.assertEquals(source.getName(), e.getName()); + Assert.assertEquals(source.getExtension("age"), e.getExtension("age")); + Assert.assertEquals(source.getExtension("color"), e.getExtension("color")); + + } + @Test + public void testToExtendable() { + Source source = new Source(); + source.setName("test"); + source.setAge(123); + source.setColor(Color.RED); + ExtendableEntity e = FastBeanCopier.copy(source, new ExtendableEntity()); + + Assert.assertEquals(source.getName(), e.getName()); + Assert.assertEquals(source.getAge(), e.getExtension("age")); + Assert.assertEquals(source.getColor(), e.getExtension("color")); + + Map map = FastBeanCopier.copy(e, new HashMap<>()); + System.out.println(map); + + ExtendableEntity t = FastBeanCopier.copy(map, new ExtendableEntity()); + Assert.assertEquals(e.getName(), t.getName()); + + System.out.println(e.extensions()); + System.out.println(t.extensions()); + Assert.assertEquals(e.extensions(), t.extensions()); + + } + + @Test + public void testFromExtendable() { + Source source = new Source(); + ExtendableEntity e = FastBeanCopier.copy(source, new ExtendableEntity()); + e.setName("test"); + e.setExtension("age",123); + FastBeanCopier.copy(e, source); + Assert.assertEquals(e.getName(), source.getName()); + Assert.assertEquals(e.getExtension("age"), source.getAge()); + + + } + @Test + public void testMapToExtendable() { + Source source = new Source(); + source.setName("test"); + source.setAge(123); + source.setColor(Color.RED); + Map map = FastBeanCopier.copy(source, new HashMap<>()); + ExtendableEntity e = FastBeanCopier.copy(map, new ExtendableEntity()); + Assert.assertEquals(source.getName(), e.getName()); + Assert.assertEquals(source.getAge(), e.getExtension("age")); + Assert.assertEquals(source.getColor(), e.getExtension("color")); + } + + + @Getter + @Setter + public static class ExtendableEntity extends DefaultExtendable { + + private String name; + + private boolean boy2; + } + @Test public void test() throws InvocationTargetException, IllegalAccessException { Source source = new Source(); @@ -62,7 +142,7 @@ public void testMapArray() { @Test public void testMapList() { Map data = new HashMap<>(); - data.put("templates", new HashMap() { + data.put("templates", new HashMap() { { put("0", Collections.singletonMap("name", "test")); put("1", Collections.singletonMap("name", "test")); @@ -74,7 +154,7 @@ public void testMapList() { Assert.assertNotNull(config); Assert.assertNotNull(config.templates); System.out.println(config.templates); - Assert.assertEquals(2,config.templates.size()); + Assert.assertEquals(2, config.templates.size()); } @@ -92,7 +172,7 @@ public static class Template { @Override public String toString() { - return "name:"+name; + return "name:" + name; } } @@ -119,24 +199,85 @@ public void testCopyMap() { System.out.println(FastBeanCopier.copy(target, new Target())); } + @Test + @SneakyThrows + public void testCrossClassLoader() { + URL clazz = new File("target/test-classes").getAbsoluteFile().toURI().toURL(); + + System.out.println(clazz); + URLClassLoader loader = new URLClassLoader(new URL[]{ + clazz + }, ClassUtils.getDefaultClassLoader()) { + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + try { + Class clazz = loadSelfClass(name); + if (null != clazz) { + if (resolve) { + resolveClass(clazz); + } + return clazz; + } + } catch (Throwable ignore) { + + } + return super.loadClass(name, resolve); + } + + @SneakyThrows + public synchronized Class loadSelfClass(String name) { + Class clazz = super.findLoadedClass(name); + if (clazz == null) { + clazz = super.findClass(name); + resolveClass(clazz); + } + return clazz; + } + + @Override + public Enumeration getResources(String name) throws IOException { + return findResources(name); + } + + @Override + public URL getResource(String name) { + return findResource(name); + } + }; + Class sourceClass = loader.loadClass(Source.class.getName()); + Assert.assertNotSame(sourceClass, Source.class); + + Object source = sourceClass.newInstance(); + FastBeanCopier.copy(Collections.singletonMap("name", "测试"), source); + + Map map = FastBeanCopier.copy(source, new HashMap<>()); + System.out.println(map); + + loader.close(); + map = FastBeanCopier.copy(source, new HashMap<>()); + + System.out.println(map); + + } + @Test public void testProxy() { AtomicReference reference = new AtomicReference<>(); - ProxyTest test = (ProxyTest) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), + ProxyTest test = (ProxyTest) Proxy.newProxyInstance(ClassUtils.getDefaultClassLoader(), new Class[]{ProxyTest.class}, (proxy, method, args) -> { - if (method.getName().equals("getName")) { - return "test"; - } - - if (method.getName().equals("setName")) { - reference.set(args[0]); - return null; - } + if (method.getName().equals("getName")) { + return "test"; + } + if (method.getName().equals("setName")) { + reference.set(args[0]); return null; - }); + } + + return null; + }); Target source = new Target(); 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 67aa5fb5d..cc02629a2 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 @@ -55,6 +55,9 @@ public class Source { private String[] arr6 = {"1", "2"}; + private int[] arr7={1,2}; + private int[] arr8={1,2}; + private Color[] colors ={Color.BLUE,Color.RED}; private String source; 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 8b50678c9..150596228 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 @@ -17,13 +17,13 @@ @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; @@ -37,7 +37,7 @@ public class Target { private Date updateTime; - @ToString.Features({coverIgnoreProperty,jsonFormat}) + @ToString.Features({coverIgnoreProperty, jsonFormat}) @ToString.Ignore(value = "password") private NestObject nestObject; @@ -66,6 +66,8 @@ public class Target { private Color[] colors; + private long[] arr7; + private List arr8; @Override public String toString() { diff --git a/hsweb-core/src/test/java/org/hswebframework/web/dict/EnumDictTest.java b/hsweb-core/src/test/java/org/hswebframework/web/dict/EnumDictTest.java index f4c2be887..645e59a87 100644 --- a/hsweb-core/src/test/java/org/hswebframework/web/dict/EnumDictTest.java +++ b/hsweb-core/src/test/java/org/hswebframework/web/dict/EnumDictTest.java @@ -57,6 +57,11 @@ public JsonDeserializer findEnumDeserializer(Class type, .readValue("{\"testEnum\":\"e1\"}"); Assert.assertEquals(testEntity.testEnum, TestEnum.E1); + testEntity = mapper.readerFor(TestEntity.class) + .readValue("{\"testEnum\":0}"); + Assert.assertEquals(testEntity.testEnum, TestEnum.E1); + + System.out.println((Object) mapper.readerFor(TestEnum.class).readValue("\"E1\"")); testEntity = mapper.readerFor(TestEntity.class) @@ -68,7 +73,7 @@ public JsonDeserializer findEnumDeserializer(Class type, @Test public void testEq() { - assertFalse(EnumDict.find(TestEnum.class, 1) + assertTrue(EnumDict.find(TestEnum.class, 1) .isPresent()); assertTrue(EnumDict.find(TestEnum.class, "e1") @@ -85,6 +90,8 @@ public void testEq() { public static class TestEntity { private TestEnum testEnum = TestEnum.E1; + private TestEnumInteger testEnumInteger = TestEnumInteger.E1; + private SimpleEnum simpleEnum = SimpleEnum.A; private TestEnum[] testEnums; diff --git a/hsweb-core/src/test/java/org/hswebframework/web/dict/TestEnumInteger.java b/hsweb-core/src/test/java/org/hswebframework/web/dict/TestEnumInteger.java new file mode 100644 index 000000000..c1caf8ca3 --- /dev/null +++ b/hsweb-core/src/test/java/org/hswebframework/web/dict/TestEnumInteger.java @@ -0,0 +1,21 @@ +package org.hswebframework.web.dict; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author Qingsheng.Ning + */ +@Getter +@AllArgsConstructor +public enum TestEnumInteger implements EnumDict { + E1(1), E2(2) + ; + + private final Integer value; + + @Override + public String getText() { + return name(); + } +} \ No newline at end of file diff --git a/hsweb-core/src/test/java/org/hswebframework/web/exception/TraceSourceExceptionTest.java b/hsweb-core/src/test/java/org/hswebframework/web/exception/TraceSourceExceptionTest.java new file mode 100644 index 000000000..b889e13c1 --- /dev/null +++ b/hsweb-core/src/test/java/org/hswebframework/web/exception/TraceSourceExceptionTest.java @@ -0,0 +1,45 @@ +package org.hswebframework.web.exception; + +import org.junit.Test; + +import static org.junit.Assert.*; + +public class TraceSourceExceptionTest { + + + @Test + public void test() { + + TraceSourceException exp = new TraceSourceException() + .withSource("test","testSource"); + + + { + assertEquals("test", TraceSourceException.tryGetOperation(exp)); + + assertEquals("testSource", TraceSourceException.tryGetSource(exp)); + + } + { + RuntimeException e = new RuntimeException(); + e.addSuppressed(exp); + assertEquals("test", TraceSourceException.tryGetOperation(e)); + + assertEquals("testSource", TraceSourceException.tryGetSource(e)); + e.printStackTrace(); + } + + { + assertEquals("test", TraceSourceException + .tryGetOperation( + new RuntimeException(exp) + )); + assertEquals("testSource", TraceSourceException + .tryGetSource( + new RuntimeException(exp) + )); + + } + + } +} \ No newline at end of file diff --git a/hsweb-core/src/test/java/org/hswebframework/web/i18n/I18nSupportUtilsTest.java b/hsweb-core/src/test/java/org/hswebframework/web/i18n/I18nSupportUtilsTest.java new file mode 100644 index 000000000..eede8958f --- /dev/null +++ b/hsweb-core/src/test/java/org/hswebframework/web/i18n/I18nSupportUtilsTest.java @@ -0,0 +1,31 @@ +package org.hswebframework.web.i18n; + +import org.junit.Test; + +import java.util.Arrays; +import java.util.Locale; +import java.util.Map; + +import static org.junit.Assert.*; + +public class I18nSupportUtilsTest { + + + @Test + public void test() { + Map> container = I18nSupportUtils + .putI18nMessages("message.test.a", + "a", + Arrays.asList(Locale.CHINESE, Locale.ENGLISH), + "Test", + null + ); + System.out.println(container); + assertNotNull(container); + assertNotNull(container.get("a")); + assertTrue(container.get("a").containsKey("zh")); + assertTrue(container.get("a").containsKey("en")); + + } + +} \ No newline at end of file diff --git a/hsweb-core/src/test/java/org/hswebframework/web/i18n/LocaleUtilsTest.java b/hsweb-core/src/test/java/org/hswebframework/web/i18n/LocaleUtilsTest.java index 4bc2be7ff..4d663fbc7 100644 --- a/hsweb-core/src/test/java/org/hswebframework/web/i18n/LocaleUtilsTest.java +++ b/hsweb-core/src/test/java/org/hswebframework/web/i18n/LocaleUtilsTest.java @@ -11,6 +11,15 @@ public class LocaleUtilsTest { + @Test + public void testSupports(){ + + assertNotNull(LocaleUtils.getSupportLocales()); + + System.out.println(LocaleUtils.getSupportLocales()); + + + } @Test public void testFlux() { diff --git a/hsweb-core/src/test/java/org/hswebframework/web/i18n/MultipleI18nSupportEntityTest.java b/hsweb-core/src/test/java/org/hswebframework/web/i18n/MultipleI18nSupportEntityTest.java new file mode 100644 index 000000000..74969318a --- /dev/null +++ b/hsweb-core/src/test/java/org/hswebframework/web/i18n/MultipleI18nSupportEntityTest.java @@ -0,0 +1,44 @@ +package org.hswebframework.web.i18n; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Getter; +import lombok.Setter; +import lombok.SneakyThrows; +import org.junit.Test; + +import java.util.Collections; +import java.util.Locale; +import java.util.Map; + +import static org.junit.Assert.*; + +public class MultipleI18nSupportEntityTest { + + + @Test + @SneakyThrows + public void testJson() { + MultipleI18nSupportEntityTestEntity entity = new MultipleI18nSupportEntityTestEntity(); + + entity.setI18nMessages(Collections.singletonMap("name", Collections.singletonMap("zh", "名称"))); + + String msg = LocaleUtils.doWith( + entity, + Locale.CHINA, + (e, l) -> e.getI18nMessage("name", "123")); + + assertNotEquals("123", msg); + } + + @Getter + @Setter + public static class MultipleI18nSupportEntityTestEntity implements MultipleI18nSupportEntity { + + private Map> i18nMessages; + + @Override + public Map> getI18nMessages() { + return i18nMessages; + } + } +} \ No newline at end of file diff --git a/hsweb-core/src/test/java/org/hswebframework/web/id/RandomIdGeneratorTest.java b/hsweb-core/src/test/java/org/hswebframework/web/id/RandomIdGeneratorTest.java new file mode 100644 index 000000000..f156f1c0b --- /dev/null +++ b/hsweb-core/src/test/java/org/hswebframework/web/id/RandomIdGeneratorTest.java @@ -0,0 +1,26 @@ +package org.hswebframework.web.id; + +import org.junit.Test; + +import java.time.Duration; + +import static org.junit.Assert.*; + +public class RandomIdGeneratorTest { + + + @Test + public void test() { + RandomIdGenerator.GLOBAL.generate(); + + long now = System.currentTimeMillis(); + String id = RandomIdGenerator.GLOBAL.generate(); + System.out.println(id + "-->" + id.length()); + long ts = RandomIdGenerator.getTimestampInId(id); + + System.out.println(now + ">" + ts); + assertTrue(RandomIdGenerator.isRandomId(id)); + assertTrue(RandomIdGenerator.timestampRangeOf(id, Duration.ofMillis(100))); + assertTrue(ts >= now); + } +} \ No newline at end of file diff --git a/hsweb-core/src/test/java/org/hswebframework/web/id/SnowflakeIdGeneratorTest.java b/hsweb-core/src/test/java/org/hswebframework/web/id/SnowflakeIdGeneratorTest.java new file mode 100644 index 000000000..9ef490c4a --- /dev/null +++ b/hsweb-core/src/test/java/org/hswebframework/web/id/SnowflakeIdGeneratorTest.java @@ -0,0 +1,35 @@ +package org.hswebframework.web.id; + +import org.junit.Test; + +import java.util.concurrent.atomic.AtomicLong; + +import static org.junit.Assert.*; + +public class SnowflakeIdGeneratorTest { + + + @Test + public void test(){ + + AtomicLong time = new AtomicLong(System.currentTimeMillis()); + + + SnowflakeIdGenerator generator = new SnowflakeIdGenerator(0,1){ + @Override + protected long timeGen() { + return time.get(); + } + }; + + System.out.println(generator.nextId()); + //回退1秒 + time.addAndGet(-1000); + System.out.println(generator.nextId()); + + time.addAndGet(2000); + System.out.println(generator.nextId()); + + } + +} \ No newline at end of file diff --git a/hsweb-datasource/hsweb-datasource-api/pom.xml b/hsweb-datasource/hsweb-datasource-api/pom.xml index d2794fc36..3218e2f21 100644 --- a/hsweb-datasource/hsweb-datasource-api/pom.xml +++ b/hsweb-datasource/hsweb-datasource-api/pom.xml @@ -5,13 +5,14 @@ hsweb-datasource org.hswebframework.web - 4.0.16-SNAPSHOT + 4.0.20-SNAPSHOT ../pom.xml 4.0.0 hsweb-datasource-api + ${artifactId} 数据源管理API,以及简单的多数据源实现,支持aop,表达式等多种方式切换数据源 diff --git a/hsweb-datasource/hsweb-datasource-api/src/main/java/org/hswebframework/web/datasource/DynamicDataSourceAutoConfiguration.java b/hsweb-datasource/hsweb-datasource-api/src/main/java/org/hswebframework/web/datasource/DynamicDataSourceAutoConfiguration.java index acdac629b..f95973c48 100644 --- a/hsweb-datasource/hsweb-datasource-api/src/main/java/org/hswebframework/web/datasource/DynamicDataSourceAutoConfiguration.java +++ b/hsweb-datasource/hsweb-datasource-api/src/main/java/org/hswebframework/web/datasource/DynamicDataSourceAutoConfiguration.java @@ -3,6 +3,7 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; @@ -11,7 +12,7 @@ /** * @author zhouhao */ -@Configuration +@AutoConfiguration @ImportAutoConfiguration(AopDataSourceSwitcherAutoConfiguration.class) public class DynamicDataSourceAutoConfiguration { diff --git a/hsweb-datasource/hsweb-datasource-api/src/main/resources/META-INF/spring.factories b/hsweb-datasource/hsweb-datasource-api/src/main/resources/META-INF/spring.factories deleted file mode 100644 index 2e3dfc8b4..000000000 --- a/hsweb-datasource/hsweb-datasource-api/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1,3 +0,0 @@ -# Auto Configure -org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -org.hswebframework.web.datasource.DynamicDataSourceAutoConfiguration \ No newline at end of file diff --git a/hsweb-datasource/hsweb-datasource-api/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hsweb-datasource/hsweb-datasource-api/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..bc57e3d89 --- /dev/null +++ b/hsweb-datasource/hsweb-datasource-api/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +org.hswebframework.web.datasource.DynamicDataSourceAutoConfiguration \ No newline at end of file diff --git a/hsweb-datasource/hsweb-datasource-jta/pom.xml b/hsweb-datasource/hsweb-datasource-jta/pom.xml index 0f1d07836..4e6e3c60f 100644 --- a/hsweb-datasource/hsweb-datasource-jta/pom.xml +++ b/hsweb-datasource/hsweb-datasource-jta/pom.xml @@ -5,13 +5,14 @@ hsweb-datasource org.hswebframework.web - 4.0.16-SNAPSHOT + 4.0.20-SNAPSHOT ../pom.xml 4.0.0 hsweb-datasource-jta + ${artifactId} 基于atomikos的多数据源实现,支持事务中切换数据源 diff --git a/hsweb-datasource/hsweb-datasource-jta/src/main/java/org/hswebframework/web/datasource/jta/AtomikosDataSourceAutoConfiguration.java b/hsweb-datasource/hsweb-datasource-jta/src/main/java/org/hswebframework/web/datasource/jta/AtomikosDataSourceAutoConfiguration.java index 15b29ee84..7c904097d 100644 --- a/hsweb-datasource/hsweb-datasource-jta/src/main/java/org/hswebframework/web/datasource/jta/AtomikosDataSourceAutoConfiguration.java +++ b/hsweb-datasource/hsweb-datasource-jta/src/main/java/org/hswebframework/web/datasource/jta/AtomikosDataSourceAutoConfiguration.java @@ -3,6 +3,7 @@ import org.hswebframework.web.datasource.DynamicDataSourceAutoConfiguration; import org.hswebframework.web.datasource.DynamicDataSourceService; import org.hswebframework.web.datasource.config.DynamicDataSourceConfigRepository; +import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean; @@ -16,7 +17,7 @@ /** * @author zhouhao */ -@Configuration +@AutoConfiguration @AutoConfigureBefore(DynamicDataSourceAutoConfiguration.class) public class AtomikosDataSourceAutoConfiguration { diff --git a/hsweb-datasource/hsweb-datasource-jta/src/main/resources/META-INF/spring.factories b/hsweb-datasource/hsweb-datasource-jta/src/main/resources/META-INF/spring.factories deleted file mode 100644 index 23ff1b58d..000000000 --- a/hsweb-datasource/hsweb-datasource-jta/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1,4 +0,0 @@ -# Auto Configure -org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -org.hswebframework.web.datasource.jta.AtomikosDataSourceAutoConfiguration,\ -org.hswebframework.web.datasource.jta.JtaJdbcSqlExecutorAutoConfiguration \ No newline at end of file diff --git a/hsweb-datasource/hsweb-datasource-jta/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hsweb-datasource/hsweb-datasource-jta/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..c8c233c38 --- /dev/null +++ b/hsweb-datasource/hsweb-datasource-jta/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +org.hswebframework.web.datasource.jta.AtomikosDataSourceAutoConfiguration \ No newline at end of file diff --git a/hsweb-datasource/hsweb-datasource-web/pom.xml b/hsweb-datasource/hsweb-datasource-web/pom.xml index 6ea0cff4d..6b4cd6b24 100644 --- a/hsweb-datasource/hsweb-datasource-web/pom.xml +++ b/hsweb-datasource/hsweb-datasource-web/pom.xml @@ -5,13 +5,14 @@ hsweb-datasource org.hswebframework.web - 4.0.16-SNAPSHOT + 4.0.20-SNAPSHOT ../pom.xml 4.0.0 hsweb-datasource-web + ${artifactId} 暴露多数据源的web接口 diff --git a/hsweb-datasource/hsweb-datasource-web/src/main/java/org/hswebframework/web/datasource/web/DatasourceWebApiAutoConfiguration.java b/hsweb-datasource/hsweb-datasource-web/src/main/java/org/hswebframework/web/datasource/web/DatasourceWebApiAutoConfiguration.java index 9f8d899f7..633aa79bf 100644 --- a/hsweb-datasource/hsweb-datasource-web/src/main/java/org/hswebframework/web/datasource/web/DatasourceWebApiAutoConfiguration.java +++ b/hsweb-datasource/hsweb-datasource-web/src/main/java/org/hswebframework/web/datasource/web/DatasourceWebApiAutoConfiguration.java @@ -1,9 +1,10 @@ package org.hswebframework.web.datasource.web; +import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -@Configuration +@AutoConfiguration public class DatasourceWebApiAutoConfiguration { @Bean diff --git a/hsweb-datasource/hsweb-datasource-web/src/main/resources/META-INF/spring.factories b/hsweb-datasource/hsweb-datasource-web/src/main/resources/META-INF/spring.factories deleted file mode 100644 index b72459b53..000000000 --- a/hsweb-datasource/hsweb-datasource-web/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1,3 +0,0 @@ -# Auto Configure -org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -org.hswebframework.web.datasource.web.DatasourceWebApiAutoConfiguration \ No newline at end of file diff --git a/hsweb-datasource/hsweb-datasource-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hsweb-datasource/hsweb-datasource-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..66b9b289b --- /dev/null +++ b/hsweb-datasource/hsweb-datasource-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +org.hswebframework.web.datasource.web.DatasourceWebApiAutoConfiguration \ No newline at end of file diff --git a/hsweb-datasource/pom.xml b/hsweb-datasource/pom.xml index a72f59fbe..8f471dcd9 100644 --- a/hsweb-datasource/pom.xml +++ b/hsweb-datasource/pom.xml @@ -5,7 +5,7 @@ hsweb-framework org.hswebframework.web - 4.0.16-SNAPSHOT + 4.0.20-SNAPSHOT ../pom.xml @@ -14,6 +14,7 @@ 数据源,多数据源,动态数据源 hsweb-datasource + ${artifactId} pom hsweb-datasource-api diff --git a/hsweb-logging/hsweb-access-logging-aop/pom.xml b/hsweb-logging/hsweb-access-logging-aop/pom.xml index f41764314..9690a4b31 100644 --- a/hsweb-logging/hsweb-access-logging-aop/pom.xml +++ b/hsweb-logging/hsweb-access-logging-aop/pom.xml @@ -5,12 +5,13 @@ hsweb-logging org.hswebframework.web - 4.0.16-SNAPSHOT + 4.0.20-SNAPSHOT ../pom.xml 4.0.0 hsweb-access-logging-aop + ${artifactId} 基于AOP实现访问日志解析,使用spring event发布日志事件. diff --git a/hsweb-logging/hsweb-access-logging-aop/src/main/java/org/hswebframework/web/logging/aop/AccessLoggerParser.java b/hsweb-logging/hsweb-access-logging-aop/src/main/java/org/hswebframework/web/logging/aop/AccessLoggerParser.java index 584ba6f85..1434193be 100644 --- a/hsweb-logging/hsweb-access-logging-aop/src/main/java/org/hswebframework/web/logging/aop/AccessLoggerParser.java +++ b/hsweb-logging/hsweb-access-logging-aop/src/main/java/org/hswebframework/web/logging/aop/AccessLoggerParser.java @@ -5,9 +5,18 @@ import org.hswebframework.web.logging.LoggerDefine; import java.lang.reflect.Method; +import java.util.function.Predicate; public interface AccessLoggerParser { boolean support(Class clazz, Method method); LoggerDefine parse(MethodInterceptorHolder holder); + + /** + * @param holder MethodInterceptorHolder + * @return 是否忽略支持记录当前参数 + */ + default Predicate ignoreParameter(MethodInterceptorHolder holder) { + return p -> false; + } } diff --git a/hsweb-logging/hsweb-access-logging-aop/src/main/java/org/hswebframework/web/logging/aop/AopAccessLoggerSupport.java b/hsweb-logging/hsweb-access-logging-aop/src/main/java/org/hswebframework/web/logging/aop/AopAccessLoggerSupport.java index 452cc9741..87ce9fa69 100644 --- a/hsweb-logging/hsweb-access-logging-aop/src/main/java/org/hswebframework/web/logging/aop/AopAccessLoggerSupport.java +++ b/hsweb-logging/hsweb-access-logging-aop/src/main/java/org/hswebframework/web/logging/aop/AopAccessLoggerSupport.java @@ -1,5 +1,6 @@ package org.hswebframework.web.logging.aop; +import com.google.common.collect.Maps; import org.aopalliance.intercept.MethodInterceptor; import org.hswebframework.web.aop.MethodInterceptorHolder; import org.hswebframework.web.id.IDGenerator; @@ -13,12 +14,16 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; import org.springframework.core.Ordered; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.util.ClassUtils; +import org.springframework.web.bind.annotation.RequestMapping; import javax.servlet.http.HttpServletRequest; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.function.Predicate; /** * 使用AOP记录访问日志,并触发{@link AccessLoggerListener#onLogger(AccessLoggerInfo)} @@ -61,7 +66,8 @@ protected AccessLoggerInfo createLogger(MethodInterceptorHolder holder) { info.setId(IDGenerator.MD5.generate()); info.setRequestTime(System.currentTimeMillis()); - LoggerDefine define = loggerParsers.stream() + LoggerDefine define = loggerParsers + .stream() .filter(parser -> parser.support(ClassUtils.getUserClass(holder.getTarget()), holder.getMethod())) .findAny() .map(parser -> parser.parse(holder)) @@ -71,7 +77,7 @@ protected AccessLoggerInfo createLogger(MethodInterceptorHolder holder) { info.setAction(define.getAction()); info.setDescribe(define.getDescribe()); } - info.setParameters(holder.getNamedArguments()); + info.setParameters(parseParameter(holder)); info.setTarget(holder.getTarget().getClass()); info.setMethod(holder.getMethod()); @@ -86,6 +92,16 @@ protected AccessLoggerInfo createLogger(MethodInterceptorHolder holder) { } + private Map parseParameter(MethodInterceptorHolder holder) { + Predicate ignoreParameter = loggerParsers + .stream() + .map(l -> l.ignoreParameter(holder)) + .reduce(Predicate::or) + .orElseGet(() -> p -> false); + + return Maps.filterKeys(holder.getNamedArguments(), ignoreParameter::test); + } + @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; @@ -93,6 +109,9 @@ public int getOrder() { @Override public boolean matches(Method method, Class aClass) { + if(null == AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class)){ + return false; + } return loggerParsers.stream().anyMatch(parser -> parser.support(aClass, method)); } } diff --git a/hsweb-logging/hsweb-access-logging-aop/src/main/java/org/hswebframework/web/logging/aop/DefaultAccessLoggerParser.java b/hsweb-logging/hsweb-access-logging-aop/src/main/java/org/hswebframework/web/logging/aop/DefaultAccessLoggerParser.java index a0fc433fc..2f7157e17 100644 --- a/hsweb-logging/hsweb-access-logging-aop/src/main/java/org/hswebframework/web/logging/aop/DefaultAccessLoggerParser.java +++ b/hsweb-logging/hsweb-access-logging-aop/src/main/java/org/hswebframework/web/logging/aop/DefaultAccessLoggerParser.java @@ -7,7 +7,11 @@ import org.springframework.core.annotation.AnnotationUtils; import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.HashSet; import java.util.Objects; +import java.util.Set; +import java.util.function.Predicate; import java.util.stream.Stream; @@ -24,16 +28,33 @@ public LoggerDefine parse(MethodInterceptorHolder holder) { AccessLogger methodAnn = holder.findMethodAnnotation(AccessLogger.class); AccessLogger classAnn = holder.findClassAnnotation(AccessLogger.class); String action = Stream.of(classAnn, methodAnn) - .filter(Objects::nonNull) - .map(AccessLogger::value) - .reduce((c, m) -> c.concat("-").concat(m)) - .orElse(""); + .filter(Objects::nonNull) + .map(AccessLogger::value) + .reduce((c, m) -> c.concat("-").concat(m)) + .orElse(""); String describe = Stream.of(classAnn, methodAnn) - .filter(Objects::nonNull) - .map(AccessLogger::describe) - .flatMap(Stream::of) - .reduce((c, s) -> c.concat("\n").concat(s)) - .orElse(""); - return new LoggerDefine(action,describe); + .filter(Objects::nonNull) + .map(AccessLogger::describe) + .flatMap(Stream::of) + .reduce((c, s) -> c.concat("\n").concat(s)) + .orElse(""); + return new LoggerDefine(action, describe); + + } + + @Override + public Predicate ignoreParameter(MethodInterceptorHolder holder) { + AccessLogger methodAnn = holder.findMethodAnnotation(AccessLogger.class); + AccessLogger classAnn = holder.findClassAnnotation(AccessLogger.class); + + Set ignoreParameter = new HashSet<>(); + if (methodAnn != null) { + ignoreParameter.addAll(Arrays.asList(methodAnn.ignoreParameter())); + } + if (classAnn != null) { + ignoreParameter.addAll(Arrays.asList(classAnn.ignoreParameter())); + } + return parameter -> ignoreParameter.contains("*") || ignoreParameter.contains(parameter); } + } diff --git a/hsweb-logging/hsweb-access-logging-aop/src/main/java/org/hswebframework/web/logging/aop/ReactiveAopAccessLoggerSupport.java b/hsweb-logging/hsweb-access-logging-aop/src/main/java/org/hswebframework/web/logging/aop/ReactiveAopAccessLoggerSupport.java index b1f94673b..e5060fb7a 100644 --- a/hsweb-logging/hsweb-access-logging-aop/src/main/java/org/hswebframework/web/logging/aop/ReactiveAopAccessLoggerSupport.java +++ b/hsweb-logging/hsweb-access-logging-aop/src/main/java/org/hswebframework/web/logging/aop/ReactiveAopAccessLoggerSupport.java @@ -11,14 +11,17 @@ import org.hswebframework.web.logging.events.AccessLoggerAfterEvent; import org.hswebframework.web.logging.events.AccessLoggerBeforeEvent; import org.hswebframework.web.utils.ReactiveWebUtils; +import org.reactivestreams.Publisher; import org.springframework.aop.support.StaticMethodMatcherPointcutAdvisor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; import org.springframework.core.Ordered; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.util.ClassUtils; import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; @@ -31,6 +34,7 @@ import java.lang.reflect.Method; import java.util.*; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; /** * 使用AOP记录访问日志,并触发{@link AccessLoggerListener#onLogger(AccessLoggerInfo)} @@ -47,6 +51,7 @@ public class ReactiveAopAccessLoggerSupport extends StaticMethodMatcherPointcutA private ApplicationEventPublisher eventPublisher; private final Map defineCache = new ConcurrentReferenceHashMap<>(); + private final Map> ignoreParameterCache = new ConcurrentReferenceHashMap<>(); private static final LoggerDefine UNSUPPORTED = new LoggerDefine(); @@ -78,12 +83,12 @@ private Mono currentRequestInfo(ContextView context) { protected Flux wrapFluxResponse(Flux flux, AccessLoggerInfo loggerInfo) { return Flux.deferContextual(ctx -> this - .currentRequestInfo(ctx) - .doOnNext(loggerInfo::putAccessInfo) - .then(beforeRequest(loggerInfo)) - .thenMany(flux) - .doOnError(loggerInfo::setException) - .doFinally(signal -> completeRequest(loggerInfo, ctx))); + .currentRequestInfo(ctx) + .doOnNext(loggerInfo::putAccessInfo) + .then(beforeRequest(loggerInfo)) + .thenMany(flux) + .doOnError(loggerInfo::setException) + .doFinally(signal -> completeRequest(loggerInfo, ctx))); } private Mono beforeRequest(AccessLoggerInfo loggerInfo) { @@ -125,6 +130,14 @@ private LoggerDefine createDefine(MethodInterceptorHolder holder) { .orElse(UNSUPPORTED); } + private Predicate ignoreParameter(MethodInterceptorHolder holder) { + return loggerParsers + .stream() + .map(l -> l.ignoreParameter(holder)) + .reduce(Predicate::or) + .orElseGet(() -> p -> false); + } + @SuppressWarnings("all") protected AccessLoggerInfo createLogger(MethodInterceptorHolder holder) { AccessLoggerInfo info = new AccessLoggerInfo(); @@ -140,6 +153,18 @@ protected AccessLoggerInfo createLogger(MethodInterceptorHolder holder) { info.setDescribe(define.getDescribe()); } + info.setParameters(parseParameter(holder)); + info.setTarget(holder.getTarget().getClass()); + info.setMethod(holder.getMethod()); + return info; + + } + + private Map parseParameter(MethodInterceptorHolder holder) { + Predicate ignoreParameter = ignoreParameterCache.computeIfAbsent(new CacheKey( + ClassUtils.getUserClass(holder.getTarget()), + holder.getMethod()), method -> ignoreParameter(holder)); + Map value = new ConcurrentHashMap<>(); String[] names = holder.getArgumentsNames(); @@ -148,6 +173,9 @@ protected AccessLoggerInfo createLogger(MethodInterceptorHolder holder) { for (int i = 0; i < args.length; i++) { String name = names[i]; + if (ignoreParameter.test(name)) { + continue; + } Object val = args[i]; if (val == null) { value.put(name, "null"); @@ -169,12 +197,7 @@ protected AccessLoggerInfo createLogger(MethodInterceptorHolder holder) { value.put(name, val); } } - - info.setParameters(value); - info.setTarget(holder.getTarget().getClass()); - info.setMethod(holder.getMethod()); - return info; - + return value; } @Override @@ -183,7 +206,15 @@ public int getOrder() { } @Override - public boolean matches(@Nonnull Method method,@Nonnull Class aClass) { + public boolean matches(@Nonnull Method method, @Nonnull Class aClass) { + //仅支持响应式 + if (!Publisher.class.isAssignableFrom(method.getReturnType())) { + return false; + } + // 只记录API请求 + if(null == AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class)){ + return false; + } AccessLogger ann = AnnotationUtils.findAnnotation(method, AccessLogger.class); if (ann != null && ann.ignore()) { return false; @@ -195,7 +226,7 @@ public boolean matches(@Nonnull Method method,@Nonnull Class aClass) { @Override @Nonnull - public Mono filter(@Nonnull ServerWebExchange exchange,@Nonnull WebFilterChain chain) { + public Mono filter(@Nonnull ServerWebExchange exchange, @Nonnull WebFilterChain chain) { return chain .filter(exchange) .contextWrite(Context.of(RequestInfo.class, createAccessInfo(exchange))); @@ -206,7 +237,7 @@ private RequestInfo createAccessInfo(ServerWebExchange exchange) { ServerHttpRequest request = exchange.getRequest(); info.setRequestId(request.getId()); info.setPath(request.getPath().value()); - info.setRequestMethod(request.getMethodValue()); + info.setRequestMethod(request.getMethod() == null ? "UNKNOWN" : request.getMethod().name()); info.setHeaders(request.getHeaders().toSingleValueMap()); Optional.ofNullable(ReactiveWebUtils.getIpAddr(request)) diff --git a/hsweb-logging/hsweb-access-logging-api/pom.xml b/hsweb-logging/hsweb-access-logging-api/pom.xml index 568461e57..8c0de1e5d 100644 --- a/hsweb-logging/hsweb-access-logging-api/pom.xml +++ b/hsweb-logging/hsweb-access-logging-api/pom.xml @@ -5,12 +5,13 @@ hsweb-logging org.hswebframework.web - 4.0.16-SNAPSHOT + 4.0.20-SNAPSHOT ../pom.xml 4.0.0 hsweb-access-logging-api + ${artifactId} 访问日志API模块 diff --git a/hsweb-logging/hsweb-access-logging-api/src/main/java/org/hswebframework/web/logging/AccessLogger.java b/hsweb-logging/hsweb-access-logging-api/src/main/java/org/hswebframework/web/logging/AccessLogger.java index e4e79b4a2..ac353ae8f 100644 --- a/hsweb-logging/hsweb-access-logging-api/src/main/java/org/hswebframework/web/logging/AccessLogger.java +++ b/hsweb-logging/hsweb-access-logging-api/src/main/java/org/hswebframework/web/logging/AccessLogger.java @@ -45,4 +45,10 @@ * @return 是否取消日志记录, 如果不想记录某些方法或者类, 设置为true即可 */ boolean ignore() default false; + + /** + * @return 忽略记载方法的请求参数 + *

如果不想记录方法全部或某些参数,则可以配置返回*或者对应参数名(多个用逗号分割)

+ */ + String[] ignoreParameter() default ""; } diff --git a/hsweb-logging/pom.xml b/hsweb-logging/pom.xml index 0de6902aa..43296dfee 100644 --- a/hsweb-logging/pom.xml +++ b/hsweb-logging/pom.xml @@ -23,7 +23,7 @@ hsweb-framework org.hswebframework.web - 4.0.16-SNAPSHOT + 4.0.20-SNAPSHOT ../pom.xml 4.0.0 @@ -31,6 +31,7 @@ 日志模块 hsweb-logging + ${artifactId} pom hsweb-access-logging-api diff --git a/hsweb-starter/pom.xml b/hsweb-starter/pom.xml index da77ed8f9..0843eb777 100644 --- a/hsweb-starter/pom.xml +++ b/hsweb-starter/pom.xml @@ -5,11 +5,12 @@ hsweb-framework org.hswebframework.web - 4.0.16-SNAPSHOT + 4.0.20-SNAPSHOT 4.0.0 hsweb-starter + ${artifactId} diff --git a/hsweb-starter/src/main/java/org/hswebframework/web/starter/CorsAutoConfiguration.java b/hsweb-starter/src/main/java/org/hswebframework/web/starter/CorsAutoConfiguration.java index f0a9fa45c..dcac2c1cb 100644 --- a/hsweb-starter/src/main/java/org/hswebframework/web/starter/CorsAutoConfiguration.java +++ b/hsweb-starter/src/main/java/org/hswebframework/web/starter/CorsAutoConfiguration.java @@ -1,5 +1,6 @@ package org.hswebframework.web.starter; +import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; @@ -61,7 +62,7 @@ * @author Jia * @since 1.0 */ -@Configuration +@AutoConfiguration @ConditionalOnProperty(prefix = "hsweb.cors", name = "enable", havingValue = "true") @EnableConfigurationProperties(CorsProperties.class) public class CorsAutoConfiguration { diff --git a/hsweb-starter/src/main/java/org/hswebframework/web/starter/CorsProperties.java b/hsweb-starter/src/main/java/org/hswebframework/web/starter/CorsProperties.java index 7a9e636e1..5ea92aa4b 100644 --- a/hsweb-starter/src/main/java/org/hswebframework/web/starter/CorsProperties.java +++ b/hsweb-starter/src/main/java/org/hswebframework/web/starter/CorsProperties.java @@ -3,7 +3,7 @@ import lombok.Getter; import lombok.Setter; import lombok.ToString; -import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections4.CollectionUtils; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.http.HttpMethod; import org.springframework.web.reactive.config.CorsRegistration; diff --git a/hsweb-starter/src/main/java/org/hswebframework/web/starter/HswebAutoConfiguration.java b/hsweb-starter/src/main/java/org/hswebframework/web/starter/HswebAutoConfiguration.java index f78873797..add60606d 100644 --- a/hsweb-starter/src/main/java/org/hswebframework/web/starter/HswebAutoConfiguration.java +++ b/hsweb-starter/src/main/java/org/hswebframework/web/starter/HswebAutoConfiguration.java @@ -12,6 +12,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; @@ -24,7 +25,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -@Configuration +@AutoConfiguration @EnableConfigurationProperties(AppProperties.class) public class HswebAutoConfiguration { diff --git a/hsweb-starter/src/main/java/org/hswebframework/web/starter/i18n/I18nConfiguration.java b/hsweb-starter/src/main/java/org/hswebframework/web/starter/i18n/I18nConfiguration.java index 419efd887..c18e19d3c 100644 --- a/hsweb-starter/src/main/java/org/hswebframework/web/starter/i18n/I18nConfiguration.java +++ b/hsweb-starter/src/main/java/org/hswebframework/web/starter/i18n/I18nConfiguration.java @@ -4,6 +4,7 @@ import lombok.extern.slf4j.Slf4j; import org.hswebframework.web.i18n.MessageSourceInitializer; import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigureOrder; import org.springframework.context.MessageSource; import org.springframework.context.annotation.Bean; @@ -11,6 +12,7 @@ import org.springframework.context.annotation.Primary; import org.springframework.context.support.ResourceBundleMessageSource; import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.core.io.Resource; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.util.StringUtils; @@ -18,7 +20,7 @@ import java.util.Arrays; import java.util.stream.Collectors; -@Configuration(proxyBeanMethods = false) +@AutoConfiguration @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) @Slf4j public class I18nConfiguration { diff --git a/hsweb-starter/src/main/java/org/hswebframework/web/starter/initialize/SystemInitialize.java b/hsweb-starter/src/main/java/org/hswebframework/web/starter/initialize/SystemInitialize.java index 2c8dc32e9..ad6b9da94 100644 --- a/hsweb-starter/src/main/java/org/hswebframework/web/starter/initialize/SystemInitialize.java +++ b/hsweb-starter/src/main/java/org/hswebframework/web/starter/initialize/SystemInitialize.java @@ -27,6 +27,7 @@ /** * @author zhouhao */ +@Deprecated public class SystemInitialize { private final Logger logger = LoggerFactory.getLogger(SystemInitialize.class); diff --git a/hsweb-starter/src/main/java/org/hswebframework/web/starter/jackson/CustomCodecsAutoConfiguration.java b/hsweb-starter/src/main/java/org/hswebframework/web/starter/jackson/CustomCodecsAutoConfiguration.java index 056454b0d..137c9de76 100644 --- a/hsweb-starter/src/main/java/org/hswebframework/web/starter/jackson/CustomCodecsAutoConfiguration.java +++ b/hsweb-starter/src/main/java/org/hswebframework/web/starter/jackson/CustomCodecsAutoConfiguration.java @@ -1,85 +1,37 @@ package org.hswebframework.web.starter.jackson; -import com.fasterxml.jackson.core.JacksonException; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.*; -import com.fasterxml.jackson.databind.deser.BeanDeserializerBuilder; -import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; -import com.fasterxml.jackson.databind.module.SimpleDeserializers; +import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.databind.type.ClassKey; -import com.fasterxml.jackson.databind.type.ReferenceType; import org.hswebframework.web.api.crud.entity.EntityFactory; -import org.hswebframework.web.dict.EnumDict; -import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.web.codec.CodecCustomizer; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.http.codec.CodecConfigurer; -import java.io.IOException; - -@Configuration(proxyBeanMethods = false) -@AutoConfigureAfter(JacksonAutoConfiguration.class) +@AutoConfiguration(after = JacksonAutoConfiguration.class) public class CustomCodecsAutoConfiguration { - @Configuration(proxyBeanMethods = false) + @AutoConfiguration @ConditionalOnClass(ObjectMapper.class) static class JacksonDecoderConfiguration { + + @Bean + SimpleModule entityAndEnumDictModule(EntityFactory entityFactory) { + SimpleModule module = new SimpleModule(); + module.setDeserializers(new CustomDeserializers(entityFactory)); + return module; + } + @Bean @Order(1) @ConditionalOnBean(ObjectMapper.class) @SuppressWarnings("all") CodecCustomizer jacksonDecoderCustomizer(EntityFactory entityFactory, ObjectMapper objectMapper) { - // objectMapper.setTypeFactory(new CustomTypeFactory(entityFactory)); - SimpleModule module = new SimpleModule(); - module.setDeserializers(new SimpleDeserializers() { - - @Override - public JsonDeserializer findBeanDeserializer(JavaType type, - DeserializationConfig config, - BeanDescription beanDesc) throws JsonMappingException { - JsonDeserializer deserializer = super.findBeanDeserializer(type, config, beanDesc); - - if (deserializer == null) { - - Class clazz = entityFactory.getInstanceType(type.getRawClass(), false); - - if (clazz == null || clazz == type.getRawClass()) { - return null; - } - - addDeserializer((Class) type.getRawClass(), new JsonDeserializer() { - @Override - public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException { - return p.readValueAs(clazz); - } - }); - } - - return super.findBeanDeserializer(type, config, beanDesc); - } - - @Override - public JsonDeserializer findEnumDeserializer(Class type, - DeserializationConfig config, - BeanDescription beanDesc) { - JsonDeserializer deser = null; - if (type.isEnum() && EnumDict.class.isAssignableFrom(type)) { - deser = new EnumDict.EnumDictJSONDeserializer(val -> EnumDict - .find((Class) type, val) - .orElse(null)); - } - return deser; - } - }); - objectMapper.registerModule(module); - return (configurer) -> { CodecConfigurer.DefaultCodecs defaults = configurer.defaultCodecs(); defaults.jackson2JsonDecoder(new CustomJackson2JsonDecoder(entityFactory, objectMapper)); diff --git a/hsweb-starter/src/main/java/org/hswebframework/web/starter/jackson/CustomDeserializers.java b/hsweb-starter/src/main/java/org/hswebframework/web/starter/jackson/CustomDeserializers.java new file mode 100644 index 000000000..802c99645 --- /dev/null +++ b/hsweb-starter/src/main/java/org/hswebframework/web/starter/jackson/CustomDeserializers.java @@ -0,0 +1,54 @@ +package org.hswebframework.web.starter.jackson; + +import com.fasterxml.jackson.core.JacksonException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.module.SimpleDeserializers; +import lombok.AllArgsConstructor; +import org.hswebframework.web.api.crud.entity.EntityFactory; +import org.hswebframework.web.api.crud.entity.EntityFactoryHolder; +import org.hswebframework.web.dict.EnumDict; + +import java.io.IOException; + +@AllArgsConstructor +@SuppressWarnings("all") +public class CustomDeserializers extends SimpleDeserializers { + private final EntityFactory entityFactory; + @Override + public JsonDeserializer findBeanDeserializer(JavaType type, + DeserializationConfig config, + BeanDescription beanDesc) throws JsonMappingException { + JsonDeserializer deserializer = super.findBeanDeserializer(type, config, beanDesc); + + if (deserializer == null) { + + Class clazz =entityFactory.getInstanceType(type.getRawClass(), false); + + if (clazz == null || clazz == type.getRawClass()) { + return null; + } + addDeserializer((Class) type.getRawClass(), new JsonDeserializer() { + @Override + public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException { + return p.readValueAs(clazz); + } + }); + } + + return super.findBeanDeserializer(type, config, beanDesc); + } + + @Override + public JsonDeserializer findEnumDeserializer(Class type, + DeserializationConfig config, + BeanDescription beanDesc) { + JsonDeserializer deser = null; + if (type.isEnum() && EnumDict.class.isAssignableFrom(type)) { + deser = new EnumDict.EnumDictJSONDeserializer(val -> EnumDict + .find((Class) type, val) + .orElse(null)); + } + return deser; + } +} diff --git a/hsweb-starter/src/main/java/org/hswebframework/web/starter/jackson/CustomJackson2jsonEncoder.java b/hsweb-starter/src/main/java/org/hswebframework/web/starter/jackson/CustomJackson2jsonEncoder.java index 27c42a42d..6a6490a53 100644 --- a/hsweb-starter/src/main/java/org/hswebframework/web/starter/jackson/CustomJackson2jsonEncoder.java +++ b/hsweb-starter/src/main/java/org/hswebframework/web/starter/jackson/CustomJackson2jsonEncoder.java @@ -1,5 +1,9 @@ package org.hswebframework.web.starter.jackson; +import lombok.AllArgsConstructor; +import org.hswebframework.web.authorization.Authentication; +import org.hswebframework.web.authorization.AuthenticationHolder; +import org.hswebframework.web.authorization.simple.SimpleAuthentication; import org.hswebframework.web.i18n.LocaleUtils; import org.springframework.http.codec.json.Jackson2CodecSupport; @@ -7,6 +11,8 @@ import java.lang.annotation.Annotation; import java.nio.charset.Charset; import java.util.*; +import java.util.concurrent.Callable; +import java.util.function.Function; import com.fasterxml.jackson.core.JsonEncoding; import com.fasterxml.jackson.core.JsonGenerator; @@ -37,6 +43,8 @@ import org.springframework.util.Assert; import org.springframework.util.MimeType; +import javax.annotation.Nonnull; + /** * Base class providing support methods for Jackson 2.9 encoding. For non-streaming use * cases, {@link Flux} elements are collected into a {@link List} before serialization for @@ -56,7 +64,7 @@ public class CustomJackson2jsonEncoder extends Jackson2CodecSupport implements H static { STREAM_SEPARATORS = new HashMap<>(4); - STREAM_SEPARATORS.put(MediaType.APPLICATION_STREAM_JSON, NEWLINE_SEPARATOR); + STREAM_SEPARATORS.put(MediaType.APPLICATION_NDJSON, NEWLINE_SEPARATOR); STREAM_SEPARATORS.put(MediaType.parseMediaType("application/stream+x-jackson-smile"), new byte[0]); ENCODINGS = new HashMap<>(JsonEncoding.values().length + 1); @@ -75,6 +83,7 @@ public class CustomJackson2jsonEncoder extends Jackson2CodecSupport implements H */ protected CustomJackson2jsonEncoder(ObjectMapper mapper, MimeType... mimeTypes) { super(mapper, mimeTypes); + streamingMediaTypes.add(MediaType.APPLICATION_NDJSON); } @@ -105,71 +114,80 @@ public boolean canEncode(ResolvableType elementType, @Nullable MimeType mimeType } } return (Object.class == clazz || - (!String.class.isAssignableFrom(elementType.resolve(clazz)) && getObjectMapper().canSerialize(clazz))); + (!String.class.isAssignableFrom(elementType.resolve(clazz)) && getObjectMapper().canSerialize(clazz))); } + @Override - public Flux encode(Publisher inputStream, DataBufferFactory bufferFactory, - ResolvableType elementType, @Nullable MimeType mimeType, @Nullable Map hints) { + @Nonnull + public Flux encode(@Nonnull Publisher inputStream, @Nonnull DataBufferFactory bufferFactory, + @Nonnull ResolvableType elementType, @Nullable MimeType mimeType, @Nullable Map hints) { Assert.notNull(inputStream, "'inputStream' must not be null"); Assert.notNull(bufferFactory, "'bufferFactory' must not be null"); Assert.notNull(elementType, "'elementType' must not be null"); + if (inputStream instanceof Mono) { - return Mono.from(inputStream) - .as(LocaleUtils::transform) - .map(value -> encodeValue(value, bufferFactory, elementType, mimeType, hints)) - .flux(); + return Mono + .zip( + currentContext(hints), + Mono.from(inputStream), + (ctx, value) -> ctx + .execute(() -> encodeValue(value, bufferFactory, elementType, mimeType, hints)) + ) + .flux(); } else { byte[] separator = streamSeparator(mimeType); if (separator != null) { // streaming try { ObjectWriter writer = createObjectWriter(elementType, mimeType, hints); ByteArrayBuilder byteBuilder = new ByteArrayBuilder(writer - .getFactory() - ._getBufferRecycler()); + .getFactory() + ._getBufferRecycler()); JsonEncoding encoding = getJsonEncoding(mimeType); JsonGenerator generator = getObjectMapper() - .getFactory() - .createGenerator(byteBuilder, encoding); + .getFactory() + .createGenerator(byteBuilder, encoding); SequenceWriter sequenceWriter = writer.writeValues(generator); - return Flux - .from(inputStream) - .as(LocaleUtils::transform) - .map(value -> this.encodeStreamingValue(value, - bufferFactory, - hints, - sequenceWriter, - byteBuilder, - separator)) - .doAfterTerminate(() -> { - try { - byteBuilder.release(); - generator.close(); - } catch (IOException ex) { - logger.error("Could not close Encoder resources", ex); - } - }); + return currentContext(hints) + .flatMapMany(ctx -> ctx + .transform(inputStream, + value -> this + .encodeStreamingValue(value, + bufferFactory, + hints, + sequenceWriter, + byteBuilder, + separator))) + + .doAfterTerminate(() -> { + try { + byteBuilder.release(); + generator.close(); + } catch (IOException ex) { + logger.error("Could not close Encoder resources", ex); + } + }); } catch (IOException ex) { return Flux.error(ex); } } else { // non-streaming ResolvableType listType = ResolvableType.forClassWithGenerics(List.class, elementType); - return Flux.from(inputStream) - .collectList() - .as(LocaleUtils::transform) - .map(value -> encodeValue(value, bufferFactory, listType, mimeType, hints)) - .flux(); + return currentContext(hints) + .flatMapMany(ctx -> ctx + .transform(Flux.from(inputStream).collectList(), + value -> encodeValue(value, bufferFactory, listType, mimeType, hints))); } } } @Override - public DataBuffer encodeValue(Object value, DataBufferFactory bufferFactory, - ResolvableType valueType, @Nullable MimeType mimeType, @Nullable Map hints) { + @Nonnull + public DataBuffer encodeValue(@Nonnull Object value,@Nonnull DataBufferFactory bufferFactory, + @Nonnull ResolvableType valueType, @Nullable MimeType mimeType, @Nullable Map hints) { ObjectWriter writer = createObjectWriter(valueType, mimeType, hints); ByteArrayBuilder byteBuilder = new ByteArrayBuilder(writer.getFactory()._getBufferRecycler()); @@ -250,7 +268,7 @@ private ObjectWriter createObjectWriter(ResolvableType valueType, @Nullable Mime JavaType javaType = getJavaType(valueType.getType(), null); Class jsonView = (hints != null ? (Class) hints.get(Jackson2CodecSupport.JSON_VIEW_HINT) : null); ObjectWriter writer = (jsonView != null ? - getObjectMapper().writerWithView(jsonView) : getObjectMapper().writer()); + getObjectMapper().writerWithView(jsonView) : getObjectMapper().writer()); if (javaType.isContainerType()) { writer = writer.forType(javaType); @@ -320,4 +338,32 @@ public Map getEncodeHints(@Nullable ResolvableType actualType, R protected A getAnnotation(MethodParameter parameter, Class annotType) { return parameter.getMethodAnnotation(annotType); } + + static final SimpleAuthentication ANONYMOUS = new SimpleAuthentication(); + + static Mono currentContext(Map hints) { + return Mono + .zip(Authentication.currentReactive().defaultIfEmpty(ANONYMOUS), + LocaleUtils.currentReactive(), EncodingContext::new); + } + + @AllArgsConstructor + static class EncodingContext { + private final Authentication authentication; + private final Locale locale; + + private Flux transform(Publisher source, Function transformer) { + return Flux + .from(source) + .map((val) -> execute(() -> transformer.apply(val))); + } + + private T execute(Callable callable) { + if (authentication == null || authentication == ANONYMOUS) { + return LocaleUtils.doWith(locale, callable); + } + return AuthenticationHolder + .executeWith(authentication, () -> LocaleUtils.doWith(locale, callable)); + } + } } \ No newline at end of file diff --git a/hsweb-starter/src/main/java/org/hswebframework/web/starter/reporter/GenericExceptionReport.java b/hsweb-starter/src/main/java/org/hswebframework/web/starter/reporter/GenericExceptionReport.java new file mode 100644 index 000000000..c9c34af98 --- /dev/null +++ b/hsweb-starter/src/main/java/org/hswebframework/web/starter/reporter/GenericExceptionReport.java @@ -0,0 +1,22 @@ +package org.hswebframework.web.starter.reporter; + +import lombok.extern.slf4j.Slf4j; +import org.hswebframework.web.exception.analyzer.ExceptionAnalyzers; +import org.springframework.beans.BeansException; +import org.springframework.boot.SpringBootExceptionReporter; +import org.springframework.context.ConfigurableApplicationContext; + +@Slf4j +public class GenericExceptionReport implements SpringBootExceptionReporter { + + + public GenericExceptionReport(ConfigurableApplicationContext context) { + } + + + @Override + public boolean reportException(Throwable failure) { + return ExceptionAnalyzers.analyze(failure); + } + +} diff --git a/hsweb-starter/src/main/resources/META-INF/spring.factories b/hsweb-starter/src/main/resources/META-INF/spring.factories index 43f2e42db..5ad0657ab 100644 --- a/hsweb-starter/src/main/resources/META-INF/spring.factories +++ b/hsweb-starter/src/main/resources/META-INF/spring.factories @@ -1,6 +1,2 @@ -# Auto Configure -org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -org.hswebframework.web.starter.jackson.CustomCodecsAutoConfiguration,\ -org.hswebframework.web.starter.HswebAutoConfiguration,\ -org.hswebframework.web.starter.CorsAutoConfiguration,\ -org.hswebframework.web.starter.i18n.I18nConfiguration \ No newline at end of file +org.springframework.boot.SpringBootExceptionReporter=\ +org.hswebframework.web.starter.reporter.GenericExceptionReport \ No newline at end of file diff --git a/hsweb-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hsweb-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..f9f4af520 --- /dev/null +++ b/hsweb-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,5 @@ +org.hswebframework.web.starter.jackson.CustomCodecsAutoConfiguration +org.hswebframework.web.starter.jackson.CustomCodecsAutoConfiguration.JacksonDecoderConfiguration +org.hswebframework.web.starter.HswebAutoConfiguration +org.hswebframework.web.starter.CorsAutoConfiguration +org.hswebframework.web.starter.i18n.I18nConfiguration \ No newline at end of file diff --git a/hsweb-starter/src/test/java/org/hswebframework/web/starter/reporter/GenericExceptionReportTest.java b/hsweb-starter/src/test/java/org/hswebframework/web/starter/reporter/GenericExceptionReportTest.java new file mode 100644 index 000000000..1776c54e1 --- /dev/null +++ b/hsweb-starter/src/test/java/org/hswebframework/web/starter/reporter/GenericExceptionReportTest.java @@ -0,0 +1,24 @@ +package org.hswebframework.web.starter.reporter; + +import org.hswebframework.web.exception.analyzer.ExceptionAnalyzers; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.context.support.GenericApplicationContext; + + +public class GenericExceptionReportTest { + + + @Test + void test(){ + GenericExceptionReport report = new GenericExceptionReport( + new GenericApplicationContext() + ); + + Assertions.assertTrue( + report.reportException(new IndexOutOfBoundsException("Binding index 0 when only 0 parameters are expected ")) + ); + + } + +} \ No newline at end of file diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/pom.xml b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/pom.xml index 22267f807..ef1ce8045 100644 --- a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/pom.xml +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/pom.xml @@ -5,11 +5,12 @@ hsweb-system-authorization org.hswebframework.web - 4.0.16-SNAPSHOT + 4.0.20-SNAPSHOT 4.0.0 hsweb-system-authorization-api + ${artifactId} diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/system/authorization/api/entity/ActionEntity.java b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/system/authorization/api/entity/ActionEntity.java index f468dd118..415565f07 100644 --- a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/system/authorization/api/entity/ActionEntity.java +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/system/authorization/api/entity/ActionEntity.java @@ -2,8 +2,14 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; +import org.hswebframework.ezorm.rdb.mapping.annotation.ColumnType; +import org.hswebframework.ezorm.rdb.mapping.annotation.JsonCodec; import org.hswebframework.web.api.crud.entity.Entity; +import org.hswebframework.web.i18n.MultipleI18nSupportEntity; +import org.hswebframework.web.i18n.SingleI18nSupportEntity; +import javax.persistence.Column; +import java.sql.JDBCType; import java.util.Map; @Getter @@ -12,7 +18,7 @@ @NoArgsConstructor @AllArgsConstructor @EqualsAndHashCode(of = "action") -public class ActionEntity implements Entity { +public class ActionEntity implements Entity, MultipleI18nSupportEntity { @Schema(description = "操作标识,如: add,query") private String action; @@ -24,5 +30,15 @@ public class ActionEntity implements Entity { private String describe; @Schema(description = "其他配置") - private Map properties; + private Map properties; + + @Schema(description = "国际化信息") + private Map> i18nMessages; + + public String getI18nName() { + return getI18nMessage("name", name); + } + public String getI18nDescribe() { + return getI18nMessage("describe", describe); + } } diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/system/authorization/api/entity/DimensionUserEntity.java b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/system/authorization/api/entity/DimensionUserEntity.java index c4cb157ba..5364aec17 100644 --- a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/system/authorization/api/entity/DimensionUserEntity.java +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/system/authorization/api/entity/DimensionUserEntity.java @@ -5,9 +5,11 @@ import lombok.Setter; import org.hswebframework.ezorm.rdb.mapping.annotation.ColumnType; import org.hswebframework.ezorm.rdb.mapping.annotation.Comment; +import org.hswebframework.ezorm.rdb.mapping.annotation.DefaultValue; import org.hswebframework.ezorm.rdb.mapping.annotation.EnumCodec; import org.hswebframework.web.api.crud.entity.GenericEntity; import org.hswebframework.web.crud.annotation.EnableEntityEvent; +import org.hswebframework.web.crud.generator.Generators; import org.hswebframework.web.dict.EnumDict; import org.hswebframework.web.system.authorization.api.enums.DimensionUserFeature; import org.springframework.util.DigestUtils; @@ -74,6 +76,11 @@ public class DimensionUserEntity extends GenericEntity { @Schema(description = "其他功能") private DimensionUserFeature[] features; + @Column(updatable = false) + @DefaultValue(generator = Generators.CURRENT_TIME) + @Schema(description = "关联时间", accessMode = Schema.AccessMode.READ_ONLY) + private Long relationTime; + public void generateId() { if (StringUtils.isEmpty(getId())) { String id = DigestUtils diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/system/authorization/api/entity/PermissionEntity.java b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/system/authorization/api/entity/PermissionEntity.java index dd596d6fa..f60c25634 100644 --- a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/system/authorization/api/entity/PermissionEntity.java +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/system/authorization/api/entity/PermissionEntity.java @@ -11,6 +11,7 @@ import org.hswebframework.web.api.crud.entity.RecordModifierEntity; import org.hswebframework.web.bean.FastBeanCopier; import org.hswebframework.web.crud.generator.Generators; +import org.hswebframework.web.i18n.MultipleI18nSupportEntity; import org.hswebframework.web.validator.CreateGroup; import org.springframework.util.CollectionUtils; @@ -31,7 +32,7 @@ @Builder @NoArgsConstructor @AllArgsConstructor -public class PermissionEntity extends GenericEntity implements RecordCreationEntity, RecordModifierEntity { +public class PermissionEntity extends GenericEntity implements RecordCreationEntity, RecordModifierEntity, MultipleI18nSupportEntity { @Override @Pattern(regexp = "^[0-9a-zA-Z_\\-]+$", message = "ID只能由数字,字母,下划线和中划线组成", groups = CreateGroup.class) @@ -102,6 +103,19 @@ public String getId() { @Column(length = 64, updatable = false) private String modifierId; + @Schema(title = "国际化信息定义") + @Column + @JsonCodec + @ColumnType(jdbcType = JDBCType.LONGVARCHAR, javaType = String.class) + private Map> i18nMessages; + + + public String getI18nName() { + return getI18nMessage("name", name); + } + public String getI18nDescribe() { + return getI18nMessage("describe", describe); + } public PermissionEntity copy(Predicate actionFilter, Predicate fieldFilter) { PermissionEntity entity = FastBeanCopier.copy(this, new PermissionEntity()); diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/system/authorization/api/entity/UserEntity.java b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/system/authorization/api/entity/UserEntity.java index 2e831be27..ce79a6ef2 100644 --- a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/system/authorization/api/entity/UserEntity.java +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/system/authorization/api/entity/UserEntity.java @@ -1,7 +1,5 @@ package org.hswebframework.web.system.authorization.api.entity; -import com.alibaba.fastjson.JSON; -import com.alibaba.fastjson.serializer.SerializerFeature; import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.media.Schema; @@ -11,18 +9,15 @@ import org.hswebframework.ezorm.rdb.mapping.annotation.Comment; import org.hswebframework.ezorm.rdb.mapping.annotation.DefaultValue; import org.hswebframework.web.api.crud.entity.GenericEntity; -import org.hswebframework.web.api.crud.entity.QueryParamEntity; import org.hswebframework.web.api.crud.entity.RecordCreationEntity; import org.hswebframework.web.bean.ToString; import org.hswebframework.web.validator.CreateGroup; import org.springframework.util.StringUtils; import javax.persistence.Column; -import javax.persistence.GeneratedValue; import javax.persistence.Index; import javax.persistence.Table; import javax.validation.constraints.NotBlank; -import java.util.Objects; /** * 系统用户实体 @@ -94,6 +89,10 @@ public void generateId() { if (StringUtils.hasText(getId())) { return; } - setId(DigestUtils.md5Hex(username)); + setId(generateId(username)); + } + + public static String generateId(String username) { + return DigestUtils.md5Hex(username); } } diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/system/authorization/api/event/UserModifiedEvent.java b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/system/authorization/api/event/UserModifiedEvent.java index 510df0151..733a3a54f 100644 --- a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/system/authorization/api/event/UserModifiedEvent.java +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/system/authorization/api/event/UserModifiedEvent.java @@ -6,7 +6,7 @@ import org.hswebframework.web.system.authorization.api.entity.UserEntity; /** - * 用户密码发生修改时事件 + * 用户修改事件,修改用户时将触发此事件. * * @author zhouhao * @see org.springframework.context.event.EventListener @@ -16,9 +16,20 @@ @AllArgsConstructor @Getter public class UserModifiedEvent extends DefaultAsyncEvent { + //修改前信息 private UserEntity before; + //修改后信息 private UserEntity userEntity; + //用户是否修改了密码 private boolean passwordModified; + + //新密码原始文本, passwordModified 为 true 时有值 + private String newPassword; + + @Deprecated + public UserModifiedEvent(UserEntity before, UserEntity after, boolean passwordModified) { + this(before, after, passwordModified, null); + } } diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/system/authorization/api/service/reactive/ReactiveUserService.java b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/system/authorization/api/service/reactive/ReactiveUserService.java index 209196cde..430d4bdea 100644 --- a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/system/authorization/api/service/reactive/ReactiveUserService.java +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/system/authorization/api/service/reactive/ReactiveUserService.java @@ -1,6 +1,8 @@ package org.hswebframework.web.system.authorization.api.service.reactive; import org.hswebframework.ezorm.core.param.QueryParam; +import org.hswebframework.web.api.crud.entity.PagerResult; +import org.hswebframework.web.api.crud.entity.QueryParamEntity; import org.hswebframework.web.system.authorization.api.entity.UserEntity; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -29,6 +31,12 @@ public interface ReactiveUserService { */ Mono saveUser(Mono userEntity); + /** + * 新增用户 + * + * @param userEntity 用户实体 + * @return 新增的用户实体 + */ Mono addUser(UserEntity userEntity); /** @@ -77,6 +85,7 @@ public interface ReactiveUserService { /** * 根据查询条件查询用户 + * * @param queryParam 动态查询条件 * @return 用户列表 */ @@ -84,6 +93,7 @@ public interface ReactiveUserService { /** * 根据查询条件查询用户数量 + * * @param queryParam 查询条件 * @return 用户数量 */ @@ -91,10 +101,19 @@ public interface ReactiveUserService { /** * 删除用户 + * * @param userId 用户ID * @return 是否成功 * @see org.hswebframework.web.system.authorization.api.event.UserDeletedEvent */ Mono deleteUser(String userId); + /** + * 分页查询用户 + * + * @param param 动态查询条件 + * @return 查询结果 + * @since 4.0.17 + */ + Mono> queryPager(QueryParamEntity param); } diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/pom.xml b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/pom.xml index 23c3074d7..09eba9838 100644 --- a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/pom.xml +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/pom.xml @@ -5,11 +5,12 @@ hsweb-system-authorization org.hswebframework.web - 4.0.16-SNAPSHOT + 4.0.20-SNAPSHOT 4.0.0 hsweb-system-authorization-default + ${artifactId} diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/configuration/AuthorizationServiceAutoConfiguration.java b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/configuration/AuthorizationServiceAutoConfiguration.java index 47f501d6f..a5a13c444 100644 --- a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/configuration/AuthorizationServiceAutoConfiguration.java +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/configuration/AuthorizationServiceAutoConfiguration.java @@ -14,18 +14,20 @@ import org.hswebframework.web.system.authorization.defaults.service.terms.DimensionTerm; import org.hswebframework.web.system.authorization.defaults.service.terms.UserDimensionTerm; import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -@Configuration +@AutoConfiguration public class AuthorizationServiceAutoConfiguration { - @Configuration(proxyBeanMethods = false) - @AutoConfigureBefore(DefaultAuthorizationAutoConfiguration.class) - static class ReactiveAuthorizationServiceAutoConfiguration { + @AutoConfiguration + public static class ReactiveAuthorizationServiceAutoConfiguration { + @ConditionalOnBean(ReactiveRepository.class) @Bean public ReactiveUserService reactiveUserService() { @@ -51,6 +53,7 @@ public PermissionSynchronization permissionSynchronization(ReactiveRepository getUserIdByDimensionId(String dimensionId) { @EventListener public void handleDimensionChanged(EntitySavedEvent event) { - eventPublisher.publishEvent(ClearUserAuthorizationCacheEvent.all()); + event.async( + ClearUserAuthorizationCacheEvent.all().publish(eventPublisher) + ); } @EventListener public void handleDimensionChanged(EntityModifyEvent event) { - eventPublisher.publishEvent(ClearUserAuthorizationCacheEvent.all()); + event.async( + ClearUserAuthorizationCacheEvent.all().publish(eventPublisher) + ); } @EventListener diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/DefaultReactiveAuthenticationInitializeService.java b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/DefaultReactiveAuthenticationInitializeService.java index be5cf9489..a93979f9c 100644 --- a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/DefaultReactiveAuthenticationInitializeService.java +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/DefaultReactiveAuthenticationInitializeService.java @@ -1,7 +1,7 @@ package org.hswebframework.web.system.authorization.defaults.service; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections4.CollectionUtils; import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository; import org.hswebframework.web.authorization.Authentication; import org.hswebframework.web.authorization.Dimension; @@ -26,6 +26,7 @@ import org.springframework.util.StringUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.util.function.Tuples; import java.util.*; import java.util.function.Function; @@ -33,7 +34,7 @@ @Slf4j public class DefaultReactiveAuthenticationInitializeService - implements ReactiveAuthenticationInitializeService { + implements ReactiveAuthenticationInitializeService { @Autowired private ReactiveUserService userService; @@ -63,25 +64,25 @@ public Mono doInit(Mono userEntityMono) { return userEntityMono.flatMap(user -> { SimpleAuthentication authentication = new SimpleAuthentication(); authentication.setUser(SimpleUser - .builder() - .id(user.getId()) - .name(user.getName()) - .username(user.getUsername()) - .userType(user.getType()) - .build()); + .builder() + .id(user.getId()) + .name(user.getName()) + .username(user.getUsername()) + .userType(user.getType()) + .build()); return initPermission(authentication) - .defaultIfEmpty(authentication) - .onErrorResume(err -> { - log.warn(err.getMessage(), err); - return Mono.just(authentication); - }) - .flatMap(auth -> { - AuthorizationInitializeEvent event = new AuthorizationInitializeEvent(auth); - return event - .publish(eventPublisher) - .then(Mono.fromSupplier(event::getAuthentication)); - }); + .defaultIfEmpty(authentication) + .onErrorResume(err -> { + log.warn(err.getMessage(), err); + return Mono.just(authentication); + }) + .flatMap(auth -> { + AuthorizationInitializeEvent event = new AuthorizationInitializeEvent(auth); + return event + .publish(eventPublisher) + .then(Mono.fromSupplier(event::getAuthentication)); + }); }); } @@ -90,26 +91,28 @@ protected Flux getSettings(List dimension .filter(dimension -> dimension.getType() != null) .groupBy(d -> d.getType().getId(), (Function) Dimension::getId) .flatMap(group -> - group.collectList() - .flatMapMany(list -> settingRepository - .createQuery() - .where(AuthorizationSettingEntity::getState, 1) - .and(AuthorizationSettingEntity::getDimensionType, group.key()) - .in(AuthorizationSettingEntity::getDimensionTarget, list) - .fetch())); + group.collectList() + .flatMapMany(list -> settingRepository + .createQuery() + .where(AuthorizationSettingEntity::getState, 1) + .and(AuthorizationSettingEntity::getDimensionType, group.key()) + .in(AuthorizationSettingEntity::getDimensionTarget, list) + .fetch())); } protected Mono initPermission(SimpleAuthentication authentication) { return Flux.fromIterable(dimensionProviders) .flatMap(provider -> provider.getDimensionByUserId(authentication.getUser().getId())) .cast(Dimension.class) + //去重?还是合并? + .distinct(dis -> Tuples.of(dis.getType().getId(), dis.getId())) .doOnNext(authentication::addDimension) .collectList() .then(Mono.defer(() -> Mono - .zip(getAllPermission(), - getSettings(authentication.getDimensions()).collect(Collectors.groupingBy(AuthorizationSettingEntity::getPermission)), - (_p, _s) -> handlePermission(authentication, _p, _s) - ))); + .zip(getAllPermission(), + getSettings(authentication.getDimensions()).collect(Collectors.groupingBy(AuthorizationSettingEntity::getPermission)), + (_p, _s) -> handlePermission(authentication, _p, _s) + ))); } @@ -145,9 +148,9 @@ protected SimpleAuthentication handlePermission(SimpleAuthentication authenticat .stream() .map(conf -> { DataAccessConfig config = builderFactory - .create() - .fromMap(conf.toMap()) - .build(); + .create() + .fromMap(conf.toMap()) + .build(); if (config == null) { log.warn("unsupported data access:{}", conf.toMap()); } @@ -205,11 +208,11 @@ protected SimpleAuthentication handlePermission(SimpleAuthentication authenticat protected Mono> getAllPermission() { return permissionRepository - .createQuery() - .where(PermissionEntity::getStatus, 1) - .fetch() - .collect(Collectors.toMap(PermissionEntity::getId, Function.identity())) - .switchIfEmpty(Mono.just(Collections.emptyMap())); + .createQuery() + .where(PermissionEntity::getStatus, 1) + .fetch() + .collect(Collectors.toMap(PermissionEntity::getId, Function.identity())) + .switchIfEmpty(Mono.just(Collections.emptyMap())); } } diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/DefaultReactiveAuthenticationManager.java b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/DefaultReactiveAuthenticationManager.java index 20f134621..9d5d64955 100644 --- a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/DefaultReactiveAuthenticationManager.java +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/DefaultReactiveAuthenticationManager.java @@ -58,7 +58,6 @@ public void handleClearAuthCache(ClearUserAuthorizationCacheEvent event) { public Mono authenticate(Mono request) { return request .filter(PlainTextUsernamePasswordAuthenticationRequest.class::isInstance) - .switchIfEmpty(Mono.error(() -> new UnsupportedOperationException("不支持的请求类型"))) .map(PlainTextUsernamePasswordAuthenticationRequest.class::cast) .flatMap(pwdRequest -> reactiveUserService.findByUsernameAndPassword(pwdRequest.getUsername(), pwdRequest.getPassword())) .filter(user -> Byte.valueOf((byte) 1).equals(user.getStatus())) diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/DefaultReactiveUserService.java b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/DefaultReactiveUserService.java index b56d81193..27f95e8a5 100644 --- a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/DefaultReactiveUserService.java +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/DefaultReactiveUserService.java @@ -5,6 +5,8 @@ import org.hswebframework.ezorm.core.param.QueryParam; import org.hswebframework.ezorm.rdb.exception.DuplicateKeyException; import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository; +import org.hswebframework.web.api.crud.entity.PagerResult; +import org.hswebframework.web.api.crud.entity.QueryParamEntity; import org.hswebframework.web.api.crud.entity.TransactionManagers; import org.hswebframework.web.crud.service.GenericReactiveCrudService; import org.hswebframework.web.exception.NotFoundException; @@ -57,16 +59,12 @@ public Mono newUserInstance() { public Mono saveUser(Mono request) { return request .flatMap(userEntity -> { - if (StringUtils.isEmpty(userEntity.getId())) { + if (!StringUtils.hasText(userEntity.getId())) { return doAdd(userEntity); } return findById(userEntity.getId()) .flatMap(old -> doUpdate(old, userEntity)) - .switchIfEmpty( - Objects.equals(userEntity.getId(), userEntity.getUsername()) ? - doAdd(userEntity) : - Mono.error(NotFoundException::new) - ); + .switchIfEmpty(doAdd(userEntity)); }).thenReturn(true); } @@ -115,18 +113,22 @@ protected Mono doUpdate(UserEntity old, UserEntity newer) { old.getPassword() ); + String newPassword = passwordChanged ? newer.getPassword() : null; if (updatePassword) { newer.setSalt(IDGenerator.RANDOM.generate()); passwordValidator.validate(newer.getPassword()); newer.setPassword(passwordEncoder.encode(newer.getPassword(), newer.getSalt())); } + UserEntity copyEntity = old.copyTo(new UserEntity()); + UserEntity newEntity = newer.copyTo(copyEntity); return getRepository() .createUpdate() .set(newer) .where(newer::getId) .execute() - .flatMap(__ -> new UserModifiedEvent(old, newer, passwordChanged).publish(eventPublisher)) - .thenReturn(newer) + .flatMap(__ -> new UserModifiedEvent(old, newEntity, passwordChanged, newPassword) + .publish(eventPublisher) + .thenReturn(newEntity)) .flatMap(e -> ClearUserAuthorizationCacheEvent .of(e.getId()) .publish(eventPublisher) @@ -190,7 +192,7 @@ public Mono changeState(Publisher userId, byte state) { public Mono changePassword(String userId, String oldPassword, String newPassword) { passwordValidator.validate(newPassword); return findById(userId) - .switchIfEmpty(Mono.error(NotFoundException::new)) + .switchIfEmpty(Mono.error(NotFoundException.NoStackTrace::new)) .filter(user -> passwordEncoder.encode(oldPassword, user.getSalt()).equals(user.getPassword())) .switchIfEmpty(Mono.error(() -> new ValidationException("error.illegal_user_password"))) .flatMap(old -> { @@ -204,7 +206,7 @@ public Mono changePassword(String userId, String oldPassword, String ne .set(newer::getPassword) .where(newer::getId) .execute() - .flatMap(e -> new UserModifiedEvent(old, newer, passwordChanged) + .flatMap(e -> new UserModifiedEvent(old, newer, passwordChanged, newPassword) .publish(eventPublisher) .thenReturn(e)); }) @@ -230,7 +232,7 @@ public Mono countUser(QueryParam queryParam) { } @Override - @Transactional(readOnly = true, transactionManager = TransactionManagers.reactiveTransactionManager) + @Transactional(transactionManager = TransactionManagers.reactiveTransactionManager) public Mono deleteUser(String userId) { return this .findById(userId) @@ -239,4 +241,9 @@ public Mono deleteUser(String userId) { .flatMap(i -> new UserDeletedEvent(user).publish(eventPublisher)) .thenReturn(true)); } + + @Override + public Mono> queryPager(QueryParamEntity queryParamMono) { + return super.queryPager(queryParamMono); + } } diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/PermissionSynchronization.java b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/PermissionSynchronization.java index bd8baa237..a091a9720 100644 --- a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/PermissionSynchronization.java +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/PermissionSynchronization.java @@ -8,10 +8,10 @@ import org.hswebframework.web.authorization.define.*; import org.hswebframework.web.crud.web.reactive.ReactiveQueryController; import org.hswebframework.web.crud.web.reactive.ReactiveServiceQueryController; +import org.hswebframework.web.i18n.LocaleUtils; import org.hswebframework.web.system.authorization.api.entity.ActionEntity; import org.hswebframework.web.system.authorization.api.entity.OptionalField; import org.hswebframework.web.system.authorization.api.entity.PermissionEntity; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.context.event.EventListener; import org.springframework.util.CollectionUtils; @@ -75,15 +75,15 @@ public void handleResourceParseEvent(AuthorizeDefinitionInitializedEvent event) } } - protected PermissionEntity convert(Map old, ResourceDefinition definition) { + public static PermissionEntity convert(Map old, ResourceDefinition definition, Map> entityFieldsMapping) { PermissionEntity entity = old.getOrDefault(definition.getId(), PermissionEntity.builder() - .name(definition.getName()) - .describe(definition.getDescription()) - .status((byte) 1) - .build()); + .name(definition.getName()) + .describe(definition.getDescription()) + .i18nMessages(definition.getI18nMessages()) + .status((byte) 1) + .build()); entity.setId(definition.getId()); - if (CollectionUtils.isEmpty(entity.getOptionalFields())) { entity.setOptionalFields(entityFieldsMapping.get(entity.getId())); } @@ -100,23 +100,31 @@ protected PermissionEntity convert(Map old, ResourceDe .name(definitionAction.getName()) .describe(definitionAction.getName()) .build()); + action.setI18nMessages(definitionAction.getI18nMessages()); Map properties = Optional.ofNullable(action.getProperties()).orElse(new HashMap<>()); - Set types = Optional.of(properties.computeIfAbsent("supportDataAccessTypes", t -> new HashSet<>())) + @SuppressWarnings("all") + Set types = (Set) Optional + .of(properties.computeIfAbsent("supportDataAccessTypes", t -> new HashSet<>())) .filter(Collection.class::isInstance) - .>map(Collection.class::cast) - .>map(HashSet::new) + .map(Collection.class::cast) + .map(HashSet::new) .orElseGet(HashSet::new); - types.addAll(definitionAction.getDataAccess().getDataAccessTypes().stream().map(DataAccessTypeDefinition::getId).collect(Collectors.toSet())); + types.addAll(definitionAction + .getDataAccess() + .getDataAccessTypes() + .stream() + .map(DataAccessTypeDefinition::getId) + .collect(Collectors.toSet())); action.setProperties(properties); oldAction.put(action.getAction(), action); } entity.setActions(new ArrayList<>(oldAction.values())); - return entity; } + @Override public void run(String... args) throws Exception { if (definition.getResources().isEmpty()) { @@ -129,8 +137,8 @@ public void run(String... args) throws Exception { .fetch() .collect(Collectors.toMap(PermissionEntity::getId, Function.identity())) .flatMap(group -> Flux.fromIterable(definition.getResources()) - .map(d -> this.convert(group, d)) - .as(permissionRepository::save)) + .map(d -> PermissionSynchronization.convert(group, d, entityFieldsMapping)) + .as(permissionRepository::save)) .doOnError(err -> log.warn("sync permission error", err)) .subscribe(l -> { log.info("sync permission success:{}", l); diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/terms/DimensionTerm.java b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/terms/DimensionTerm.java index 11b447e7d..b23455152 100644 --- a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/terms/DimensionTerm.java +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/terms/DimensionTerm.java @@ -6,10 +6,12 @@ import org.hswebframework.ezorm.core.param.QueryParam; import org.hswebframework.ezorm.core.param.Term; import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata; +import org.hswebframework.ezorm.rdb.operator.builder.fragments.BatchSqlFragments; import org.hswebframework.ezorm.rdb.operator.builder.fragments.EmptySqlFragments; import org.hswebframework.ezorm.rdb.operator.builder.fragments.PrepareSqlFragments; import org.hswebframework.ezorm.rdb.operator.builder.fragments.SqlFragments; import org.hswebframework.ezorm.rdb.operator.builder.fragments.term.AbstractTermFragmentBuilder; +import org.hswebframework.ezorm.rdb.utils.SqlUtils; import java.util.List; import java.util.StringJoiner; @@ -27,6 +29,8 @@ public DimensionTerm() { super("dimension", "和维度关联的数据"); } + private static final SqlFragments USER_ID_IN = SqlFragments.of("and d.user_id in("); + public static > T inject(T query, String column, String dimensionType, @@ -34,6 +38,7 @@ public static > T inject(T query, return inject(query, column, dimensionType, false, false, userId); } + @SuppressWarnings("all") public static > T inject(T query, String column, String dimensionType, @@ -67,21 +72,26 @@ public SqlFragments createFragments(String columnFullName, RDBColumnMetadata col if (CollectionUtils.isEmpty(options)) { throw new IllegalArgumentException("查询条件错误,正确格式:" + column.getAlias() + "$dimension${type}$[not]"); } - PrepareSqlFragments fragments = PrepareSqlFragments.of(); + BatchSqlFragments fragments = new BatchSqlFragments(6, 2); if (options.contains("not")) { - fragments.addSql("not "); + fragments.add(SqlFragments.NOT); } fragments - .addSql("exists(select 1 from", getTableName("s_dimension_user", column), "d where d.dimension_type_id = ? and d.dimension_id =", columnFullName) - .addParameter(options.get(0)); + .addSql("exists(select 1 from", + getTableName("s_dimension_user", column), + "d where d.dimension_type_id = ? and d.dimension_id =", columnFullName) + .addParameter(options.get(0)); if (!options.contains("any")) { - fragments.addSql("and d.user_id in(", values.stream().map(r -> "?").collect(Collectors.joining(",")), ")") - .addParameter(values); + fragments + .add(USER_ID_IN) + .add(SqlUtils.createQuestionMarks(values.size())) + .add(SqlFragments.RIGHT_BRACKET) + .addParameter(values); } - fragments.addSql(")"); + fragments.add(SqlFragments.RIGHT_BRACKET); return fragments; } } \ No newline at end of file diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/terms/UserDimensionTerm.java b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/terms/UserDimensionTerm.java index b63fc2b4f..2fd42ec45 100644 --- a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/terms/UserDimensionTerm.java +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/terms/UserDimensionTerm.java @@ -2,13 +2,13 @@ import org.hswebframework.ezorm.core.param.Term; import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata; +import org.hswebframework.ezorm.rdb.operator.builder.fragments.BatchSqlFragments; import org.hswebframework.ezorm.rdb.operator.builder.fragments.EmptySqlFragments; -import org.hswebframework.ezorm.rdb.operator.builder.fragments.PrepareSqlFragments; import org.hswebframework.ezorm.rdb.operator.builder.fragments.SqlFragments; import org.hswebframework.ezorm.rdb.operator.builder.fragments.term.AbstractTermFragmentBuilder; +import org.hswebframework.ezorm.rdb.utils.SqlUtils; import java.util.List; -import java.util.stream.Collectors; /** * 查询和用户维度绑定的数据,如: 查询机构下的用户 @@ -22,6 +22,9 @@ public UserDimensionTerm() { super("in-dimension", "在维度中的用户数据"); } + static SqlFragments DIMENSION_ID_IN = SqlFragments.of("and d.dimension_id in("); + static SqlFragments DIMENSION_TYPE_ID = SqlFragments.of("and d.dimension_type_id = ?"); + @Override public SqlFragments createFragments(String columnFullName, RDBColumnMetadata column, Term term) { @@ -30,28 +33,32 @@ public SqlFragments createFragments(String columnFullName, RDBColumnMetadata col return EmptySqlFragments.INSTANCE; } - PrepareSqlFragments fragments = PrepareSqlFragments.of(); + BatchSqlFragments fragments = new BatchSqlFragments(7,2); List options = term.getOptions(); if (options.contains("not")) { - fragments.addSql("not"); + fragments.add(SqlFragments.NOT); } - fragments.addSql("exists(select 1 from ",getTableName("s_dimension_user",column)," d where d.user_id =", columnFullName); + fragments.addSql("exists(select 1 from", + getTableName("s_dimension_user", column), + "d where d.user_id =", columnFullName); - if (options.size() > 0) { + if (!options.isEmpty()) { String typeId = options.get(0); if (!"not".equals(typeId) && !"any".equals(typeId)) { - fragments.addSql("and d.dimension_type_id = ?").addParameter(typeId); + fragments.add(DIMENSION_TYPE_ID).addParameter(typeId); } } if (!options.contains("any")) { - fragments.addSql("and d.dimension_id in(", - values.stream().map(r -> "?").collect(Collectors.joining(",")), ")") - .addParameter(values); + fragments + .add(DIMENSION_ID_IN) + .add(SqlUtils.createQuestionMarks(values.size())) + .add(SqlFragments.RIGHT_BRACKET) + .addParameter(values); } - fragments.addSql(")"); + fragments.add(SqlFragments.RIGHT_BRACKET); return fragments; } diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/webflux/WebFluxPermissionController.java b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/webflux/WebFluxPermissionController.java index b2137c937..cbc6d5fb3 100644 --- a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/webflux/WebFluxPermissionController.java +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/webflux/WebFluxPermissionController.java @@ -3,7 +3,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; -import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections4.CollectionUtils; import org.hswebframework.ezorm.rdb.operator.dml.query.SortOrder; import org.hswebframework.web.api.crud.entity.QueryNoPagingOperation; import org.hswebframework.web.api.crud.entity.QueryOperation; diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/resources/META-INF/spring.factories b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/resources/META-INF/spring.factories deleted file mode 100644 index 67134b550..000000000 --- a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1,4 +0,0 @@ -# Auto Configure -org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -org.hswebframework.web.system.authorization.defaults.configuration.AuthorizationServiceAutoConfiguration,\ -org.hswebframework.web.system.authorization.defaults.configuration.AuthorizationWebAutoConfiguration \ No newline at end of file diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..554ed7245 --- /dev/null +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ +org.hswebframework.web.system.authorization.defaults.configuration.AuthorizationServiceAutoConfiguration +org.hswebframework.web.system.authorization.defaults.configuration.AuthorizationServiceAutoConfiguration.ReactiveAuthorizationServiceAutoConfiguration +org.hswebframework.web.system.authorization.defaults.configuration.AuthorizationWebAutoConfiguration +org.hswebframework.web.system.authorization.defaults.configuration.AuthorizationWebAutoConfiguration.WebFluxAuthorizationConfiguration \ No newline at end of file diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/resources/i18n/authentication-default/messages_en.properties b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/resources/i18n/authentication-default/messages_en.properties index 280214ac6..b2f495f82 100644 --- a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/resources/i18n/authentication-default/messages_en.properties +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/resources/i18n/authentication-default/messages_en.properties @@ -1,2 +1,3 @@ error.duplicate_key=Duplicate Data -error.user_already_exists=User already exists \ No newline at end of file +error.user_already_exists=User already exists +error.user_not_found=The user does not exist or the id does not meet the rule \ No newline at end of file diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/resources/i18n/authentication-default/messages_zh.properties b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/resources/i18n/authentication-default/messages_zh.properties index 2ace1f08f..43724899f 100644 --- a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/resources/i18n/authentication-default/messages_zh.properties +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/resources/i18n/authentication-default/messages_zh.properties @@ -1,2 +1,3 @@ error.duplicate_key=已存在重复的数据 -error.user_already_exists=用户已存在 \ No newline at end of file +error.user_already_exists=用户已存在 +error.user_not_found=用户不存在或ID不符合规则:[{0}] \ No newline at end of file diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/test/java/org/hswebframework/web/system/authorization/defaults/service/reactive/DefaultReactiveUserServiceTest.java b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/test/java/org/hswebframework/web/system/authorization/defaults/service/reactive/DefaultReactiveUserServiceTest.java index 533539ea8..25ad3ae1b 100644 --- a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/test/java/org/hswebframework/web/system/authorization/defaults/service/reactive/DefaultReactiveUserServiceTest.java +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/test/java/org/hswebframework/web/system/authorization/defaults/service/reactive/DefaultReactiveUserServiceTest.java @@ -41,10 +41,10 @@ public void testParallel() { .zip( userService .saveUser(Mono.just(userBuilder.get())) - .subscribeOn(Schedulers.newSingle("newSingle")), + .subscribeOn(Schedulers.boundedElastic()), userService .saveUser(Mono.just(userBuilder.get())) - .subscribeOn(Schedulers.newSingle("newSingle")) + .subscribeOn(Schedulers.boundedElastic()) ) .as(StepVerifier::create) .expectError(ValidationException.class) diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/test/java/org/hswebframework/web/system/authorization/defaults/service/reactive/ReactiveTestApplication.java b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/test/java/org/hswebframework/web/system/authorization/defaults/service/reactive/ReactiveTestApplication.java index 42859fb4f..470afec4d 100644 --- a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/test/java/org/hswebframework/web/system/authorization/defaults/service/reactive/ReactiveTestApplication.java +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/test/java/org/hswebframework/web/system/authorization/defaults/service/reactive/ReactiveTestApplication.java @@ -1,20 +1,25 @@ package org.hswebframework.web.system.authorization.defaults.service.reactive; import org.hswebframework.web.authorization.simple.DefaultAuthorizationAutoConfiguration; +import org.hswebframework.web.crud.annotation.EnableEasyormRepository; +import org.hswebframework.web.crud.configuration.EasyormConfiguration; import org.hswebframework.web.crud.configuration.JdbcSqlExecutorConfiguration; +import org.hswebframework.web.system.authorization.defaults.configuration.AuthorizationServiceAutoConfiguration; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.r2dbc.R2dbcTransactionManagerAutoConfiguration; @SpringBootApplication(exclude = { - //TransactionAutoConfiguration.class, - JdbcSqlExecutorConfiguration.class, - DataSourceAutoConfiguration.class + //TransactionAutoConfiguration.class, + JdbcSqlExecutorConfiguration.class, + DataSourceAutoConfiguration.class }) @ImportAutoConfiguration({ - R2dbcTransactionManagerAutoConfiguration.class, - DefaultAuthorizationAutoConfiguration.class + R2dbcTransactionManagerAutoConfiguration.class, + DefaultAuthorizationAutoConfiguration.class, + AuthorizationServiceAutoConfiguration.class, + AuthorizationServiceAutoConfiguration.ReactiveAuthorizationServiceAutoConfiguration.class }) public class ReactiveTestApplication { diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/pom.xml b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/pom.xml index c09476f56..497e3045f 100644 --- a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/pom.xml +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/pom.xml @@ -5,12 +5,13 @@ hsweb-system-authorization org.hswebframework.web - 4.0.16-SNAPSHOT + 4.0.20-SNAPSHOT ../pom.xml 4.0.0 hsweb-system-authorization-oauth2 + ${artifactId} diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/configuration/OAuth2ClientManagerAutoConfiguration.java b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/configuration/OAuth2ClientManagerAutoConfiguration.java index c9121c58f..b0a81c2f0 100644 --- a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/configuration/OAuth2ClientManagerAutoConfiguration.java +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/configuration/OAuth2ClientManagerAutoConfiguration.java @@ -4,15 +4,16 @@ import org.hswebframework.web.oauth2.service.InDBOAuth2ClientManager; import org.hswebframework.web.oauth2.service.OAuth2ClientService; import org.hswebframework.web.oauth2.web.WebFluxOAuth2ClientController; +import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -@Configuration(proxyBeanMethods = false) +@AutoConfiguration public class OAuth2ClientManagerAutoConfiguration { - @Configuration(proxyBeanMethods = false) + @AutoConfiguration @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) static class ReactiveOAuth2ClientManagerAutoConfiguration { diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/src/main/resources/META-INF/spring.factories b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/src/main/resources/META-INF/spring.factories deleted file mode 100644 index 4f804d6fb..000000000 --- a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1,3 +0,0 @@ -# Auto Configure -org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -org.hswebframework.web.oauth2.configuration.OAuth2ClientManagerAutoConfiguration \ No newline at end of file diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..176e89837 --- /dev/null +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +org.hswebframework.web.oauth2.configuration.OAuth2ClientManagerAutoConfiguration +org.hswebframework.web.oauth2.configuration.OAuth2ClientManagerAutoConfiguration.ReactiveOAuth2ClientManagerAutoConfiguration \ No newline at end of file diff --git a/hsweb-system/hsweb-system-authorization/pom.xml b/hsweb-system/hsweb-system-authorization/pom.xml index 314ce1a9e..c2ba8d6a6 100644 --- a/hsweb-system/hsweb-system-authorization/pom.xml +++ b/hsweb-system/hsweb-system-authorization/pom.xml @@ -5,7 +5,7 @@ hsweb-system org.hswebframework.web - 4.0.16-SNAPSHOT + 4.0.20-SNAPSHOT 4.0.0 pom @@ -17,6 +17,7 @@ hsweb-system-authorization-oauth2 hsweb-system-authorization + ${artifactId} \ No newline at end of file diff --git a/hsweb-system/hsweb-system-dictionary/pom.xml b/hsweb-system/hsweb-system-dictionary/pom.xml index 60b1c6d6a..a1b6d4a6f 100644 --- a/hsweb-system/hsweb-system-dictionary/pom.xml +++ b/hsweb-system/hsweb-system-dictionary/pom.xml @@ -5,11 +5,12 @@ hsweb-system org.hswebframework.web - 4.0.16-SNAPSHOT + 4.0.20-SNAPSHOT 4.0.0 hsweb-system-dictionary + ${artifactId} @@ -17,6 +18,29 @@ hsweb-commons-crud ${project.version} + + org.springframework.boot + spring-boot-starter-test + test + + + + io.r2dbc + r2dbc-h2 + test + + + + com.h2database + h2 + test + + + + org.springframework.boot + spring-boot-starter-data-r2dbc + test + \ No newline at end of file diff --git a/hsweb-system/hsweb-system-dictionary/src/main/java/org/hswebframework/web/dictionary/configuration/DictionaryAutoConfiguration.java b/hsweb-system/hsweb-system-dictionary/src/main/java/org/hswebframework/web/dictionary/configuration/DictionaryAutoConfiguration.java index e7ae2b0e6..071086309 100644 --- a/hsweb-system/hsweb-system-dictionary/src/main/java/org/hswebframework/web/dictionary/configuration/DictionaryAutoConfiguration.java +++ b/hsweb-system/hsweb-system-dictionary/src/main/java/org/hswebframework/web/dictionary/configuration/DictionaryAutoConfiguration.java @@ -1,21 +1,23 @@ package org.hswebframework.web.dictionary.configuration; +import org.hswebframework.web.cache.ReactiveCacheManager; import org.hswebframework.web.dictionary.service.CompositeDictDefineRepository; import org.hswebframework.web.dictionary.service.DefaultDictionaryItemService; import org.hswebframework.web.dictionary.service.DefaultDictionaryService; import org.hswebframework.web.dictionary.webflux.WebfluxDictionaryController; import org.hswebframework.web.dictionary.webflux.WebfluxDictionaryItemController; +import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -@Configuration +@AutoConfiguration @EnableConfigurationProperties(DictionaryProperties.class) public class DictionaryAutoConfiguration { - @Configuration(proxyBeanMethods = false) + @AutoConfiguration static class DictionaryServiceConfiguration { @Bean @@ -29,28 +31,30 @@ public DefaultDictionaryService defaultDictionaryService() { } @Bean - public CompositeDictDefineRepository compositeDictDefineRepository(DictionaryProperties properties) { - CompositeDictDefineRepository repository = new CompositeDictDefineRepository(); - properties.doScanEnum() - .stream() - .map(CompositeDictDefineRepository::parseEnumDict) - .forEach(repository::addDefine); - return repository; + public CompositeDictDefineRepository compositeDictDefineRepository(DictionaryProperties properties, + DefaultDictionaryService service, + ReactiveCacheManager cacheManager) { + + return new CompositeDictDefineRepository( + service, + cacheManager, + properties + ); } } - @Configuration + @AutoConfiguration @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) static class DictionaryWebFluxConfiguration { @Bean - public WebfluxDictionaryController webfluxDictionaryController(){ + public WebfluxDictionaryController webfluxDictionaryController() { return new WebfluxDictionaryController(); } @Bean - public WebfluxDictionaryItemController webfluxDictionaryItemController(){ + public WebfluxDictionaryItemController webfluxDictionaryItemController() { return new WebfluxDictionaryItemController(); } } diff --git a/hsweb-system/hsweb-system-dictionary/src/main/java/org/hswebframework/web/dictionary/configuration/DictionaryProperties.java b/hsweb-system/hsweb-system-dictionary/src/main/java/org/hswebframework/web/dictionary/configuration/DictionaryProperties.java index 53219ad47..7f73ed524 100644 --- a/hsweb-system/hsweb-system-dictionary/src/main/java/org/hswebframework/web/dictionary/configuration/DictionaryProperties.java +++ b/hsweb-system/hsweb-system-dictionary/src/main/java/org/hswebframework/web/dictionary/configuration/DictionaryProperties.java @@ -11,13 +11,11 @@ import org.springframework.core.io.support.ResourcePatternResolver; import org.springframework.core.type.classreading.CachingMetadataReaderFactory; import org.springframework.core.type.classreading.MetadataReader; -import org.springframework.core.type.classreading.MetadataReaderFactory; import org.springframework.util.ClassUtils; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import java.io.IOException; +import java.util.*; +import java.util.stream.Stream; @ConfigurationProperties(prefix = "hsweb.dict") @Getter @@ -27,30 +25,40 @@ public class DictionaryProperties { private Set enumPackages = new HashSet<>(); + public DictionaryProperties() { + } + @SneakyThrows - public List doScanEnum() { + public Stream> doScanEnum() { Set packages = new HashSet<>(enumPackages); packages.add("org.hswebframework.web"); CachingMetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(); ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver(); - List classes = new ArrayList<>(); + + List> classes = new ArrayList<>(); for (String enumPackage : packages) { String path = "classpath*:" + ClassUtils.convertClassNameToResourcePath(enumPackage) + "/**/*.class"; - Resource[] resources = resourcePatternResolver.getResources(path); + log.info("scan enum dict package:{}", path); + Resource[] resources; + try { + resources = resourcePatternResolver.getResources(path); + } catch (IOException e) { + log.warn("scan enum dict package:{} error:", path, e); + return Stream.empty(); + } for (Resource resource : resources) { try { MetadataReader reader = metadataReaderFactory.getMetadataReader(resource); String name = reader.getClassMetadata().getClassName(); - Class clazz = ClassUtils.forName(name,null); + Class clazz = ClassUtils.forName(name, null); if (clazz.isEnum() && EnumDict.class.isAssignableFrom(clazz)) { classes.add(clazz); } - } catch (Throwable e) { + } catch (Throwable ignore) { } } } - metadataReaderFactory.clearCache(); - return classes; + return classes.stream(); } } diff --git a/hsweb-system/hsweb-system-dictionary/src/main/java/org/hswebframework/web/dictionary/entity/DictionaryEntity.java b/hsweb-system/hsweb-system-dictionary/src/main/java/org/hswebframework/web/dictionary/entity/DictionaryEntity.java index 7930d759f..537092a40 100644 --- a/hsweb-system/hsweb-system-dictionary/src/main/java/org/hswebframework/web/dictionary/entity/DictionaryEntity.java +++ b/hsweb-system/hsweb-system-dictionary/src/main/java/org/hswebframework/web/dictionary/entity/DictionaryEntity.java @@ -20,17 +20,28 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.Setter; +import org.hswebframework.ezorm.rdb.mapping.annotation.ColumnType; import org.hswebframework.ezorm.rdb.mapping.annotation.Comment; import org.hswebframework.ezorm.rdb.mapping.annotation.DefaultValue; +import org.hswebframework.ezorm.rdb.mapping.annotation.JsonCodec; import org.hswebframework.web.api.crud.entity.GenericEntity; import org.hswebframework.web.api.crud.entity.RecordCreationEntity; +import org.hswebframework.web.crud.generator.Generators; import org.hswebframework.web.dict.DictDefine; import org.hswebframework.web.dict.defaults.DefaultDictDefine; +import org.hswebframework.web.i18n.I18nSupportUtils; +import org.hswebframework.web.i18n.LocaleUtils; +import org.hswebframework.web.i18n.MultipleI18nSupportEntity; +import org.hswebframework.web.validator.CreateGroup; import javax.persistence.Column; import javax.persistence.Table; import javax.validation.constraints.NotBlank; +import java.sql.JDBCType; +import java.util.Collection; import java.util.List; +import java.util.Locale; +import java.util.Map; /** * 数据字典 @@ -41,10 +52,10 @@ @Comment("数据字典") @Getter @Setter -public class DictionaryEntity extends GenericEntity implements RecordCreationEntity { +public class DictionaryEntity extends GenericEntity implements RecordCreationEntity, MultipleI18nSupportEntity { //字典名称 @Column(nullable = false) - @NotBlank(message = "名称不能为空") + @NotBlank(message = "名称不能为空", groups = CreateGroup.class) @Schema(description = "字典名称") private String name; //分类 @@ -56,11 +67,12 @@ public class DictionaryEntity extends GenericEntity implements RecordCre @Schema(description = "说明") private String describe; //创建时间 - @Column(name = "create_time") + @Column(name = "create_time", updatable = false) @Schema(description = "创建时间") + @DefaultValue(generator = Generators.CURRENT_TIME) private Long createTime; //创建人id - @Column(name = "creator_id") + @Column(name = "creator_id", updatable = false) @Schema(description = "创建人ID") private String creatorId; //状态 @@ -69,17 +81,41 @@ public class DictionaryEntity extends GenericEntity implements RecordCre @Schema(description = "状态,0禁用,1启用") private Byte status; + @Column + @JsonCodec + @ColumnType(javaType = String.class, jdbcType = JDBCType.LONGVARCHAR) + private Map> i18nMessages; + //字段选项 private List items; + public String getI18nName() { + return getI18nMessage("name", this.name); + } + + public String getI18nDescribe() { + return getI18nMessage("describe", this.describe); + } + + public void putI18nName(String i18nKey) { + putI18nName(i18nKey, LocaleUtils.getSupportLocales()); + } + + public void putI18nName(String i18nKey, + Collection locales) { + this.i18nMessages = I18nSupportUtils + .putI18nMessages( + i18nKey, "name", locales, null, this.i18nMessages + ); + } public DictDefine toDictDefine() { return DefaultDictDefine - .builder() - .id(this.getId()) - .alias(this.getName()) - .comments(this.getDescribe()) - .items(this.getItems()) - .build(); + .builder() + .id(this.getId()) + .alias(this.getName()) + .comments(this.getDescribe()) + .items(this.getItems()) + .build(); } } \ No newline at end of file diff --git a/hsweb-system/hsweb-system-dictionary/src/main/java/org/hswebframework/web/dictionary/entity/DictionaryItemEntity.java b/hsweb-system/hsweb-system-dictionary/src/main/java/org/hswebframework/web/dictionary/entity/DictionaryItemEntity.java index cdd4b3e11..ac765cf9a 100644 --- a/hsweb-system/hsweb-system-dictionary/src/main/java/org/hswebframework/web/dictionary/entity/DictionaryItemEntity.java +++ b/hsweb-system/hsweb-system-dictionary/src/main/java/org/hswebframework/web/dictionary/entity/DictionaryItemEntity.java @@ -20,28 +20,37 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.Setter; +import org.hswebframework.ezorm.rdb.mapping.annotation.ColumnType; import org.hswebframework.ezorm.rdb.mapping.annotation.Comment; import org.hswebframework.ezorm.rdb.mapping.annotation.DefaultValue; +import org.hswebframework.ezorm.rdb.mapping.annotation.JsonCodec; import org.hswebframework.web.api.crud.entity.GenericTreeSortSupportEntity; import org.hswebframework.web.dict.EnumDict; +import org.hswebframework.web.i18n.I18nSupportUtils; +import org.hswebframework.web.i18n.LocaleUtils; +import org.hswebframework.web.i18n.MultipleI18nSupportEntity; +import org.hswebframework.web.utils.DigestUtils; +import org.springframework.util.StringUtils; import javax.persistence.Column; import javax.persistence.Index; import javax.persistence.Table; -import java.util.List; +import java.sql.JDBCType; +import java.util.*; /** * 数据字典选项 */ @Getter @Setter -@Table(name = "s_dictionary_item",indexes = { - @Index(name = "idx_dic_item_dic_id",columnList = "dict_id"), - @Index(name = "idx_dic_item_ordinal",columnList = "ordinal"), - @Index(name = "idx_dic_item_path",columnList = "path") +@Table(name = "s_dictionary_item", indexes = { + @Index(name = "idx_dic_item_dic_id", columnList = "dict_id"), + @Index(name = "idx_dic_item_ordinal", columnList = "ordinal"), + @Index(name = "idx_dic_item_path", columnList = "path") }) @Comment("数据字典选项") -public class DictionaryItemEntity extends GenericTreeSortSupportEntity implements EnumDict { +public class DictionaryItemEntity extends GenericTreeSortSupportEntity + implements EnumDict, MultipleI18nSupportEntity { //字典id @Column(name = "dict_id", length = 64, updatable = false, nullable = false) @Schema(description = "数据字典ID") @@ -89,14 +98,66 @@ public int ordinal() { @Schema(description = "子节点") private List children; + @Schema(description = "国际化配置") + @Column + @JsonCodec + @ColumnType(javaType = String.class, jdbcType = JDBCType.LONGVARCHAR) + private Map> i18nMessages; + + public String getI18nText() { + return getI18nMessage("text", this.text); + } + + /** + * 根据消息key生成默认的的语言并填充到i18nMessages中 + * + * @param i18nKey key,在国际化文件中定义. + */ + public void putI18nText(String i18nKey) { + putI18nText(i18nKey, LocaleUtils.getSupportLocales()); + } + + /** + * 根据消息key生成对应的语言并填充到i18nMessages中 + * + * @param i18nKey key,在国际化文件中定义. + * @param locales 要生成的语言. + */ + public void putI18nText(String i18nKey, + Collection locales) { + this.i18nMessages = I18nSupportUtils + .putI18nMessages( + i18nKey, "text", locales, null, this.i18nMessages + ); + } + + public String getI18nName() { + return getI18nMessage("name", this.name); + } + + public String getI18nDescribe() { + return getI18nMessage("describe", this.describe); + } + + public void generateId() { + if (StringUtils.hasText(this.getId())) { + return; + } + this.setId(generateId(this.getDictId(), this.getOrdinal())); + } + + public static String generateId(String dictId, Integer ordinal) { + return DigestUtils.md5Hex(String.join("|", dictId, ordinal.toString())); + } + @Override public Object getWriteJSONObject() { JSONObject jsonObject = new JSONObject(); jsonObject.put("id", getId()); - jsonObject.put("name", getName()); + jsonObject.put("name", getI18nName()); jsonObject.put("dictId", getDictId()); jsonObject.put("value", getValue()); - jsonObject.put("text", getText()); + jsonObject.put("text", getI18nText()); jsonObject.put("ordinal", getOrdinal()); jsonObject.put("sortIndex", getSortIndex()); jsonObject.put("parentId", getParentId()); @@ -104,7 +165,7 @@ public Object getWriteJSONObject() { jsonObject.put("mask", getMask()); jsonObject.put("searchCode", getSearchCode()); jsonObject.put("status", getStatus()); - jsonObject.put("describe", getDescribe()); + jsonObject.put("describe", getI18nDescribe()); return jsonObject; } } \ No newline at end of file diff --git a/hsweb-system/hsweb-system-dictionary/src/main/java/org/hswebframework/web/dictionary/service/CompositeDictDefineRepository.java b/hsweb-system/hsweb-system-dictionary/src/main/java/org/hswebframework/web/dictionary/service/CompositeDictDefineRepository.java index 0e0c70083..916996412 100644 --- a/hsweb-system/hsweb-system-dictionary/src/main/java/org/hswebframework/web/dictionary/service/CompositeDictDefineRepository.java +++ b/hsweb-system/hsweb-system-dictionary/src/main/java/org/hswebframework/web/dictionary/service/CompositeDictDefineRepository.java @@ -1,12 +1,15 @@ package org.hswebframework.web.dictionary.service; +import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.hswebframework.web.api.crud.entity.QueryParamEntity; import org.hswebframework.web.cache.ReactiveCacheManager; import org.hswebframework.web.dict.DictDefine; import org.hswebframework.web.dict.defaults.DefaultDictDefineRepository; +import org.hswebframework.web.dictionary.configuration.DictionaryProperties; import org.hswebframework.web.dictionary.entity.DictionaryEntity; import org.hswebframework.web.dictionary.event.ClearDictionaryCacheEvent; +import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.event.EventListener; import org.springframework.util.StringUtils; @@ -14,25 +17,27 @@ import reactor.core.publisher.Mono; @Slf4j -public class CompositeDictDefineRepository extends DefaultDictDefineRepository { +@AllArgsConstructor +public class CompositeDictDefineRepository extends DefaultDictDefineRepository implements SmartInitializingSingleton { - @Autowired - private DefaultDictionaryService dictionaryService; - @Autowired - private ReactiveCacheManager cacheManager; + private final DefaultDictionaryService dictionaryService; + + private final ReactiveCacheManager cacheManager; + + private final DictionaryProperties properties; @EventListener public void handleClearCacheEvent(ClearDictionaryCacheEvent event) { - if (StringUtils.isEmpty(event.getDictionaryId())) { + if (StringUtils.hasText(event.getDictionaryId())) { cacheManager.getCache("dic-define") - .clear() - .doOnSuccess(r -> log.info("clear all dic cache success")) + .evict(event.getDictionaryId()) + .doOnSuccess(r -> log.info("clear dict [{}] cache success", event.getDictionaryId())) .subscribe(); } else { cacheManager.getCache("dic-define") - .evict(event.getDictionaryId()) - .doOnSuccess(r -> log.info("clear dict [{}] cache success", event.getDictionaryId())) + .clear() + .doOnSuccess(r -> log.info("clear all dic cache success")) .subscribe(); } @@ -42,25 +47,34 @@ public void handleClearCacheEvent(ClearDictionaryCacheEvent event) { public Mono getDefine(String id) { return super.getDefine(id) .switchIfEmpty(Mono.defer(() -> cacheManager - .getCache("dic-define") - .getMono(id, () -> getFromDb(id)))); + .getCache("dic-define") + .getMono(id, () -> getFromDb(id)))); } @Override public Flux getAllDefine() { return Flux.concat(super.getAllDefine(), QueryParamEntity - .newQuery() - .noPaging() - .execute(dictionaryService::findAllDetail) - .map(DictionaryEntity::toDictDefine)); + .newQuery() + .noPaging() + .execute(paramEntity -> dictionaryService.findAllDetail(paramEntity, false)) + .map(DictionaryEntity::toDictDefine)); } private Mono getFromDb(String id) { return dictionaryService - .findDetailById(id) - .filter(e -> Byte.valueOf((byte) 1).equals(e.getStatus())) - .map(DictionaryEntity::toDictDefine); + .findDetailById(id) + .filter(e -> Byte.valueOf((byte) 1).equals(e.getStatus())) + .map(DictionaryEntity::toDictDefine); } + @Override + public void afterSingletonsInstantiated() { + + properties + .doScanEnum() + .map(CompositeDictDefineRepository::parseEnumDict) + .forEach(this::addDefine); + + } } diff --git a/hsweb-system/hsweb-system-dictionary/src/main/java/org/hswebframework/web/dictionary/service/DefaultDictionaryItemService.java b/hsweb-system/hsweb-system-dictionary/src/main/java/org/hswebframework/web/dictionary/service/DefaultDictionaryItemService.java index 193e30482..e8ed1b874 100644 --- a/hsweb-system/hsweb-system-dictionary/src/main/java/org/hswebframework/web/dictionary/service/DefaultDictionaryItemService.java +++ b/hsweb-system/hsweb-system-dictionary/src/main/java/org/hswebframework/web/dictionary/service/DefaultDictionaryItemService.java @@ -3,14 +3,19 @@ import org.hswebframework.ezorm.rdb.mapping.ReactiveDelete; import org.hswebframework.ezorm.rdb.mapping.ReactiveUpdate; import org.hswebframework.ezorm.rdb.mapping.defaults.SaveResult; +import org.hswebframework.ezorm.rdb.operator.dml.query.SortOrder; import org.hswebframework.web.crud.service.GenericReactiveCrudService; import org.hswebframework.web.crud.service.ReactiveTreeSortEntityService; import org.hswebframework.web.dictionary.entity.DictionaryItemEntity; import org.hswebframework.web.dictionary.event.ClearDictionaryCacheEvent; +import org.hswebframework.web.exception.BusinessException; import org.hswebframework.web.id.IDGenerator; +import org.hswebframework.web.utils.DigestUtils; import org.reactivestreams.Publisher; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.util.StringUtils; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.Collection; @@ -34,43 +39,96 @@ public void setChildren(DictionaryItemEntity entity, List @Override public Mono insert(Publisher entityPublisher) { - return super.insert(entityPublisher) - .doOnSuccess(r -> eventPublisher.publishEvent(ClearDictionaryCacheEvent.of())); + return super.insert(this.fillOrdinal(entityPublisher)) + .doOnSuccess(r -> eventPublisher.publishEvent(ClearDictionaryCacheEvent.of())); } @Override public Mono insertBatch(Publisher> entityPublisher) { - return super.insertBatch(entityPublisher) - .doOnSuccess(r -> eventPublisher.publishEvent(ClearDictionaryCacheEvent.of())); + return super.insertBatch(this.fillCollectionOrdinal(entityPublisher)) + .doOnSuccess(r -> eventPublisher.publishEvent(ClearDictionaryCacheEvent.of())); } @Override public Mono updateById(String id, Mono entityPublisher) { return super.updateById(id, entityPublisher) - .doOnSuccess(r -> eventPublisher.publishEvent(ClearDictionaryCacheEvent.of())); + .doOnSuccess(r -> eventPublisher.publishEvent(ClearDictionaryCacheEvent.of())); } @Override public Mono deleteById(Publisher idPublisher) { return super.deleteById(idPublisher) - .doOnSuccess(r -> eventPublisher.publishEvent(ClearDictionaryCacheEvent.of())); + .doOnSuccess(r -> eventPublisher.publishEvent(ClearDictionaryCacheEvent.of())); } @Override public Mono save(Publisher entityPublisher) { - return super.save(entityPublisher) - .doOnSuccess(r -> eventPublisher.publishEvent(ClearDictionaryCacheEvent.of())); + return super.save(this.fillOrdinal(entityPublisher)) + .doOnSuccess(r -> eventPublisher.publishEvent(ClearDictionaryCacheEvent.of())); } @Override public ReactiveUpdate createUpdate() { return super.createUpdate() - .onExecute((ignore, r) -> r.doOnSuccess(l -> eventPublisher.publishEvent(ClearDictionaryCacheEvent.of()))); + .onExecute((ignore, r) -> r.doOnSuccess(l -> eventPublisher.publishEvent(ClearDictionaryCacheEvent.of()))); } @Override public ReactiveDelete createDelete() { return super.createDelete() - .onExecute((ignore, r) -> r.doOnSuccess(l -> eventPublisher.publishEvent(ClearDictionaryCacheEvent.of()))); + .onExecute((ignore, r) -> r.doOnSuccess(l -> eventPublisher.publishEvent(ClearDictionaryCacheEvent.of()))); } + + public Publisher> fillCollectionOrdinal(Publisher> entityPublisher) { + return Flux + .from(entityPublisher) + .flatMap(collection -> fillOrdinal(Flux.fromIterable(collection)).collectList()); + } + + + public Flux fillOrdinal(Publisher publisher) { + return Flux + .from(publisher) + .groupBy(DictionaryItemEntity::getDictId) + .flatMap(group -> group + .collectList() + .flatMapMany(list -> { + boolean isAllNull = list.stream().allMatch(item -> item.getOrdinal() == null); + boolean notNull = list.stream().allMatch(item -> item.getOrdinal() != null); + if (notNull) { + return Flux + .fromIterable(list) + .doOnNext(DictionaryItemEntity::generateId); + } + if (isAllNull) { + return fillOrdinal(group.key(), list); + } + return Mono.error(() -> new BusinessException("error.ordinal_can_not_null")); + + })); + } + + private Flux fillOrdinal(String dictId, List list) { + return this + .createQuery() + .select(DictionaryItemEntity::getOrdinal) + .where(DictionaryItemEntity::getDictId, dictId) + .orderBy(SortOrder.desc(DictionaryItemEntity::getOrdinal)) + .fetchOne() + .map(DictionaryItemEntity::getOrdinal) + .defaultIfEmpty(-1) + .flatMapMany(maxOrdinal -> Flux + .fromIterable(list) + .index() + .map(tp2 -> { + DictionaryItemEntity item = tp2.getT2(); + int ordinal = tp2.getT1().intValue() + maxOrdinal + 1; + item.setOrdinal(ordinal); + item.generateId(); + return item; + })); + } + + + } diff --git a/hsweb-system/hsweb-system-dictionary/src/main/java/org/hswebframework/web/dictionary/service/DefaultDictionaryService.java b/hsweb-system/hsweb-system-dictionary/src/main/java/org/hswebframework/web/dictionary/service/DefaultDictionaryService.java index 659a0907b..df46719ec 100644 --- a/hsweb-system/hsweb-system-dictionary/src/main/java/org/hswebframework/web/dictionary/service/DefaultDictionaryService.java +++ b/hsweb-system/hsweb-system-dictionary/src/main/java/org/hswebframework/web/dictionary/service/DefaultDictionaryService.java @@ -1,10 +1,12 @@ package org.hswebframework.web.dictionary.service; +import org.apache.commons.collections4.CollectionUtils; import org.hswebframework.ezorm.rdb.mapping.ReactiveDelete; import org.hswebframework.ezorm.rdb.mapping.ReactiveUpdate; import org.hswebframework.ezorm.rdb.mapping.defaults.SaveResult; +import org.hswebframework.ezorm.rdb.operator.dml.query.SortOrder; import org.hswebframework.web.api.crud.entity.QueryParamEntity; -import org.hswebframework.web.api.crud.entity.SortSupportEntity; +import org.hswebframework.web.crud.query.QueryHelper; import org.hswebframework.web.crud.service.GenericReactiveCrudService; import org.hswebframework.web.dictionary.entity.DictionaryEntity; import org.hswebframework.web.dictionary.entity.DictionaryItemEntity; @@ -16,8 +18,6 @@ import reactor.core.publisher.Mono; import java.util.Collection; -import java.util.function.Function; -import java.util.stream.Collectors; public class DefaultDictionaryService extends GenericReactiveCrudService { @@ -75,6 +75,7 @@ public Mono findDetailById(String id) { .zipWith(itemService .createQuery() .where(DictionaryItemEntity::getDictId, id) + .orderBy(SortOrder.asc(DictionaryItemEntity::getOrdinal)) .fetch() .collectList(), (dic, items) -> { @@ -83,29 +84,31 @@ public Mono findDetailById(String id) { }); } - public Flux findAllDetail(QueryParamEntity paramEntity) { - /* - 1. 查询出所有字典并以ID为key转为map - 2. 查询出所有字段选项并按dicId分组 - 3. 根据分组后的key(dictId)获取字段 - 4. 将2的分组结果放到字典里 - */ + public Flux findAllDetail(QueryParamEntity paramEntity, boolean allowEmptyItem) { return createQuery() .setParam(paramEntity) .fetch() - .collect(Collectors.toMap(DictionaryEntity::getId, Function.identity())) //.1 - .flatMapMany(dicMap -> - itemService.createQuery() - .fetch() - .groupBy(DictionaryItemEntity::getDictId)//.2 - .flatMap(group -> Mono - .justOrEmpty(dicMap.get(group.key())) //.3 - .zipWhen(dict -> group.collectList(), - (dict, items) -> { - items.sort(SortSupportEntity::compareTo); - dict.setItems(items); //.4 - return dict; - }))); + .as(flux -> fillDetail(flux, allowEmptyItem)); + } + + /** + * 查询字典详情 + * + * @param dictionary 源数据 + * @param allowEmptyItem 是否允许item为空 + */ + public Flux fillDetail(Flux dictionary, boolean allowEmptyItem) { + return QueryHelper + .combineOneToMany( + dictionary, + DictionaryEntity::getId, + itemService.createQuery(), + DictionaryItemEntity::getDictId, + DictionaryEntity::setItems + ) + //根据条件过滤是否允许返回item为空的 + .filter(dict -> allowEmptyItem || CollectionUtils.isNotEmpty(dict.getItems())); } + } diff --git a/hsweb-system/hsweb-system-dictionary/src/main/java/org/hswebframework/web/dictionary/webflux/WebfluxDictionaryController.java b/hsweb-system/hsweb-system-dictionary/src/main/java/org/hswebframework/web/dictionary/webflux/WebfluxDictionaryController.java index 639a15fe2..9a49db050 100644 --- a/hsweb-system/hsweb-system-dictionary/src/main/java/org/hswebframework/web/dictionary/webflux/WebfluxDictionaryController.java +++ b/hsweb-system/hsweb-system-dictionary/src/main/java/org/hswebframework/web/dictionary/webflux/WebfluxDictionaryController.java @@ -3,6 +3,9 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import org.hswebframework.web.api.crud.entity.QueryParamEntity; +import org.hswebframework.web.api.crud.entity.QueryNoPagingOperation; import org.hswebframework.web.authorization.annotation.Authorize; import org.hswebframework.web.authorization.annotation.Resource; import org.hswebframework.web.crud.service.ReactiveCrudService; @@ -13,11 +16,9 @@ import org.hswebframework.web.dictionary.entity.DictionaryEntity; import org.hswebframework.web.dictionary.service.DefaultDictionaryService; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; @RestController @@ -37,11 +38,27 @@ public ReactiveCrudService getService() { return dictionaryService; } + @GetMapping("/detail/_query") + @QueryNoPagingOperation(summary = "使用GET方式获取数据字典详情") + public Flux getItemDefineById(@Parameter(hidden = true) QueryParamEntity query) { + return dictionaryService + .findAllDetail(query, true); + } + + @PostMapping("/detail/_query") + @Operation(summary = "使用POST方式获取数据字典详情") + public Flux getItemDefineById(@RequestBody Mono query) { + return query + .flatMapMany(param -> dictionaryService + .findAllDetail(param, true)); + } + @GetMapping("/{id:.+}/items") @Authorize(merge = false) @Operation(summary = "获取数据字段的所有选项") public Flux> getItemDefineById(@PathVariable String id) { - return repository.getDefine(id) + return repository + .getDefine(id) .flatMapIterable(DictDefine::getItems); } diff --git a/hsweb-system/hsweb-system-dictionary/src/main/resources/META-INF/spring.factories b/hsweb-system/hsweb-system-dictionary/src/main/resources/META-INF/spring.factories deleted file mode 100644 index c50a45022..000000000 --- a/hsweb-system/hsweb-system-dictionary/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1,3 +0,0 @@ -# Auto Configure -org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -org.hswebframework.web.dictionary.configuration.DictionaryAutoConfiguration \ No newline at end of file diff --git a/hsweb-system/hsweb-system-dictionary/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hsweb-system/hsweb-system-dictionary/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..af7326ed4 --- /dev/null +++ b/hsweb-system/hsweb-system-dictionary/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,3 @@ +org.hswebframework.web.dictionary.configuration.DictionaryAutoConfiguration +org.hswebframework.web.dictionary.configuration.DictionaryAutoConfiguration.DictionaryServiceConfiguration +org.hswebframework.web.dictionary.configuration.DictionaryAutoConfiguration.DictionaryWebFluxConfiguration \ No newline at end of file diff --git a/hsweb-system/hsweb-system-dictionary/src/main/resources/i18n/dictionary/messages_en.properties b/hsweb-system/hsweb-system-dictionary/src/main/resources/i18n/dictionary/messages_en.properties new file mode 100644 index 000000000..56a352b8b --- /dev/null +++ b/hsweb-system/hsweb-system-dictionary/src/main/resources/i18n/dictionary/messages_en.properties @@ -0,0 +1 @@ +error.ordinal_can_not_null=The serial number cannot be empty \ No newline at end of file diff --git a/hsweb-system/hsweb-system-dictionary/src/main/resources/i18n/dictionary/messages_zh.properties b/hsweb-system/hsweb-system-dictionary/src/main/resources/i18n/dictionary/messages_zh.properties new file mode 100644 index 000000000..655a1f2df --- /dev/null +++ b/hsweb-system/hsweb-system-dictionary/src/main/resources/i18n/dictionary/messages_zh.properties @@ -0,0 +1 @@ +error.ordinal_can_not_null=序号不能为空 \ No newline at end of file diff --git a/hsweb-system/hsweb-system-dictionary/src/test/java/org/hswebframework/web/dictionary/ReactiveTestApplication.java b/hsweb-system/hsweb-system-dictionary/src/test/java/org/hswebframework/web/dictionary/ReactiveTestApplication.java new file mode 100644 index 000000000..271f68cb6 --- /dev/null +++ b/hsweb-system/hsweb-system-dictionary/src/test/java/org/hswebframework/web/dictionary/ReactiveTestApplication.java @@ -0,0 +1,19 @@ +package org.hswebframework.web.dictionary; + +import org.hswebframework.web.crud.configuration.JdbcSqlExecutorConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.r2dbc.R2dbcTransactionManagerAutoConfiguration; + +@SpringBootApplication(exclude = { + JdbcSqlExecutorConfiguration.class, + DataSourceAutoConfiguration.class +}) +@ImportAutoConfiguration({ + R2dbcTransactionManagerAutoConfiguration.class +}) +public class ReactiveTestApplication { + + +} diff --git a/hsweb-system/hsweb-system-dictionary/src/test/java/org/hswebframework/web/dictionary/service/DefaultDictionaryItemServiceTest.java b/hsweb-system/hsweb-system-dictionary/src/test/java/org/hswebframework/web/dictionary/service/DefaultDictionaryItemServiceTest.java new file mode 100644 index 000000000..81118c199 --- /dev/null +++ b/hsweb-system/hsweb-system-dictionary/src/test/java/org/hswebframework/web/dictionary/service/DefaultDictionaryItemServiceTest.java @@ -0,0 +1,133 @@ +package org.hswebframework.web.dictionary.service; + +import io.r2dbc.spi.R2dbcDataIntegrityViolationException; +import org.hswebframework.ezorm.rdb.exception.DuplicateKeyException; +import org.hswebframework.web.api.crud.entity.QueryParamEntity; +import org.hswebframework.web.dictionary.entity.DictionaryEntity; +import org.hswebframework.web.dictionary.entity.DictionaryItemEntity; +import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + +@SpringBootTest +@RunWith(SpringJUnit4ClassRunner.class) +public class DefaultDictionaryItemServiceTest { + + @Autowired + private DefaultDictionaryItemService defaultDictionaryItemService; + @Autowired + private DefaultDictionaryService defaultDictionaryService; + + @BeforeEach + void init() { + DictionaryEntity dictionary = new DictionaryEntity(); + dictionary.setName("demo"); + dictionary.setStatus((byte) 1); + dictionary.setId("demo"); + + defaultDictionaryService + .save(dictionary) + .then() + .as(StepVerifier::create) + .expectComplete() + .verify(); + } + + public DictionaryItemEntity createItem(String value) { + DictionaryItemEntity itemEntity = new DictionaryItemEntity(); + itemEntity.setDictId("demo"); + itemEntity.setName(value); + itemEntity.setValue(value); + itemEntity.setText(value); + itemEntity.setStatus((byte) 1); + return itemEntity; + } + + @Test + public void save() { + DictionaryItemEntity itemEntity = createItem("test1"); + itemEntity.setOrdinal(0); + DictionaryItemEntity itemEntity2 = createItem("test2"); + itemEntity2.setOrdinal(0); + + defaultDictionaryItemService + .save(Flux.just(itemEntity, itemEntity2)) + .then() + .as(StepVerifier::create) + .expectComplete() + .verify(); + + defaultDictionaryItemService + .query(new QueryParamEntity().noPaging()) + .doOnNext(System.out::println) + .count() + .as(StepVerifier::create) + .expectNext(1L) + .verifyComplete(); + + itemEntity2.setOrdinal(null); + + defaultDictionaryItemService + .save(Flux.just(itemEntity, itemEntity2)) + .then() + .as(StepVerifier::create) + .expectErrorMessage("error.ordinal_can_not_null") + .verify(); + + } + + @Test + public void testErrorOrdinal() { + DictionaryItemEntity itemEntity = createItem("test-error"); + itemEntity.setOrdinal(0); + + defaultDictionaryItemService + .save(itemEntity) + .then() + .as(StepVerifier::create) + .expectComplete() + .verify(); + + DictionaryItemEntity itemEntity2 = createItem("test-error"); + itemEntity2.setOrdinal(0); + + defaultDictionaryItemService + .insert(itemEntity2) + .then() + .as(StepVerifier::create) + .expectError(DuplicateKeyException.class) + .verify(); + + } + + @Test + public void testAutoOrdinal() { + //自动填充ordinal + DictionaryItemEntity itemEntity = createItem("test-auto"); + itemEntity.setOrdinal(null); + DictionaryItemEntity itemEntity2 = createItem("test-auto"); + itemEntity2.setOrdinal(null); + + + defaultDictionaryItemService + .save(Flux.just(itemEntity, itemEntity2)) + .then() + .as(StepVerifier::create) + .expectComplete() + .verify(); + + defaultDictionaryItemService + .query(QueryParamEntity.of("value","test-auto").noPaging()) + .doOnNext(System.out::println) + .count() + .as(StepVerifier::create) + .expectNext(2L) + .verifyComplete(); + } + +} \ No newline at end of file diff --git a/hsweb-system/hsweb-system-file/pom.xml b/hsweb-system/hsweb-system-file/pom.xml index 92dacbb26..1e4dada2d 100644 --- a/hsweb-system/hsweb-system-file/pom.xml +++ b/hsweb-system/hsweb-system-file/pom.xml @@ -5,11 +5,12 @@ hsweb-system org.hswebframework.web - 4.0.16-SNAPSHOT + 4.0.20-SNAPSHOT 4.0.0 hsweb-system-file + ${artifactId} diff --git a/hsweb-system/hsweb-system-file/src/main/java/org/hswebframework/web/file/FileServiceConfiguration.java b/hsweb-system/hsweb-system-file/src/main/java/org/hswebframework/web/file/FileServiceConfiguration.java index ab1b0085a..082c8cf86 100644 --- a/hsweb-system/hsweb-system-file/src/main/java/org/hswebframework/web/file/FileServiceConfiguration.java +++ b/hsweb-system/hsweb-system-file/src/main/java/org/hswebframework/web/file/FileServiceConfiguration.java @@ -3,13 +3,14 @@ import org.hswebframework.web.file.service.FileStorageService; import org.hswebframework.web.file.service.LocalFileStorageService; import org.hswebframework.web.file.web.ReactiveFileController; +import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -@Configuration +@AutoConfiguration @EnableConfigurationProperties(FileUploadProperties.class) public class FileServiceConfiguration { diff --git a/hsweb-system/hsweb-system-file/src/main/java/org/hswebframework/web/file/FileUploadProperties.java b/hsweb-system/hsweb-system-file/src/main/java/org/hswebframework/web/file/FileUploadProperties.java index 88eb89afe..4cd67adfa 100644 --- a/hsweb-system/hsweb-system-file/src/main/java/org/hswebframework/web/file/FileUploadProperties.java +++ b/hsweb-system/hsweb-system-file/src/main/java/org/hswebframework/web/file/FileUploadProperties.java @@ -2,7 +2,7 @@ import lombok.Getter; import lombok.Setter; -import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections4.CollectionUtils; import org.hswebframework.utils.time.DateFormatter; import org.hswebframework.web.id.IDGenerator; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -113,15 +113,17 @@ public StaticFileInfo createStaticSavePath(String name) { info.location = staticLocation + "/" + filePath + "/" + fileName; info.savePath = absPath + "/" + fileName; - + info.relativeLocation = filePath + "/" + fileName; return info; } @Getter @Setter public static class StaticFileInfo { + private String savePath; + private String relativeLocation; private String location; } } diff --git a/hsweb-system/hsweb-system-file/src/main/java/org/hswebframework/web/file/service/LocalFileStorageService.java b/hsweb-system/hsweb-system-file/src/main/java/org/hswebframework/web/file/service/LocalFileStorageService.java index e30b34db5..233ce0ecb 100644 --- a/hsweb-system/hsweb-system-file/src/main/java/org/hswebframework/web/file/service/LocalFileStorageService.java +++ b/hsweb-system/hsweb-system-file/src/main/java/org/hswebframework/web/file/service/LocalFileStorageService.java @@ -24,12 +24,14 @@ public class LocalFileStorageService implements FileStorageService { @Override public Mono saveFile(FilePart filePart) { FileUploadProperties.StaticFileInfo info = properties.createStaticSavePath(filePart.filename()); - File file = new File(info.getSavePath()); - - return (filePart) - .transferTo(file) - .then(Mono.fromRunnable(()->properties.applyFilePermission(file))) - .thenReturn(info.getLocation()); + return createStaticFileInfo(filePart.filename()) + .flatMap(into -> { + File file = new File(info.getSavePath()); + return (filePart) + .transferTo(file) + .then(Mono.fromRunnable(() -> properties.applyFilePermission(file))) + .thenReturn(info.getLocation()); + }); } private static final OpenOption[] FILE_CHANNEL_OPTIONS = { @@ -37,30 +39,33 @@ public Mono saveFile(FilePart filePart) { StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE}; + protected Mono createStaticFileInfo(String fileName) { + return Mono.just(properties.createStaticSavePath(fileName)); + } + @Override @SneakyThrows public Mono saveFile(InputStream inputStream, String fileType) { String fileName = "_temp" + (fileType.startsWith(".") ? fileType : "." + fileType); - FileUploadProperties.StaticFileInfo info = properties.createStaticSavePath(fileName); - - return Mono - .fromCallable(() -> { - try (ReadableByteChannel input = Channels.newChannel(inputStream); - FileChannel output = FileChannel.open(Paths.get(info.getSavePath()), FILE_CHANNEL_OPTIONS)) { - long size = (input instanceof FileChannel ? ((FileChannel) input).size() : Long.MAX_VALUE); - long totalWritten = 0; - while (totalWritten < size) { - long written = output.transferFrom(input, totalWritten, size - totalWritten); - if (written <= 0) { - break; + return createStaticFileInfo(fileName) + .flatMap(info -> Mono + .fromCallable(() -> { + try (ReadableByteChannel input = Channels.newChannel(inputStream); + FileChannel output = FileChannel.open(Paths.get(info.getSavePath()), FILE_CHANNEL_OPTIONS)) { + long size = (input instanceof FileChannel ? ((FileChannel) input).size() : Long.MAX_VALUE); + long totalWritten = 0; + while (totalWritten < size) { + long written = output.transferFrom(input, totalWritten, size - totalWritten); + if (written <= 0) { + break; + } + totalWritten += written; + } + return info.getLocation(); } - totalWritten += written; - } - return info.getLocation(); - } - }) - .doOnSuccess((ignore)-> properties.applyFilePermission(new File(info.getSavePath()))) - .subscribeOn(Schedulers.boundedElastic()); + }) + .doOnSuccess((ignore) -> properties.applyFilePermission(new File(info.getSavePath()))) + .subscribeOn(Schedulers.boundedElastic())); } } diff --git a/hsweb-system/hsweb-system-file/src/main/resources/META-INF/spring.factories b/hsweb-system/hsweb-system-file/src/main/resources/META-INF/spring.factories deleted file mode 100644 index 98aeb609e..000000000 --- a/hsweb-system/hsweb-system-file/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1,3 +0,0 @@ -# Auto Configure -org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -org.hswebframework.web.file.FileServiceConfiguration \ No newline at end of file diff --git a/hsweb-system/hsweb-system-file/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hsweb-system/hsweb-system-file/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..b7ff56290 --- /dev/null +++ b/hsweb-system/hsweb-system-file/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +org.hswebframework.web.file.FileServiceConfiguration \ No newline at end of file diff --git a/hsweb-system/pom.xml b/hsweb-system/pom.xml index 01eac079e..5dd2aed34 100644 --- a/hsweb-system/pom.xml +++ b/hsweb-system/pom.xml @@ -5,7 +5,7 @@ hsweb-framework org.hswebframework.web - 4.0.16-SNAPSHOT + 4.0.20-SNAPSHOT ../pom.xml 4.0.0 @@ -17,5 +17,6 @@ hsweb-system-dictionary hsweb-system + ${artifactId} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 318935c4b..ed58377c0 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ org.hswebframework.web hsweb-framework - 4.0.16-SNAPSHOT + 4.0.20-SNAPSHOT hsweb-starter hsweb-core @@ -79,22 +79,21 @@ 1.8 ${java.version} - 2.7.11 + 2.7.18 - 3.20.0-GA - 5.19.0.2 + 3.29.2-GA 1.2.83 - 2.1.212 + 2.2.220 5.1.39 3.2.2 1.6.12 - 4.1.1-SNAPSHOT - 3.0.2 + 4.1.4 + 3.0.4 3.0.2 2.7.0 - + 4.1.111.Final Borca-SR2 @@ -104,27 +103,18 @@ - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.3 + org.sonatype.central + central-publishing-maven-plugin + 0.8.0 true - sonatype-releases - https://oss.sonatype.org/ - true - 120 - - - - org.apache.maven.plugins - maven-release-plugin - - true - false - release - deploy + central + true + validated + hsweb-framework:${project.version} + org.apache.maven.plugins maven-gpg-plugin @@ -139,6 +129,7 @@ + org.apache.maven.plugins maven-javadoc-plugin @@ -158,15 +149,10 @@ - - sonatype-releases - sonatype repository - https://oss.sonatype.org/service/local/staging/deploy/maven2 - - sonatype-snapshots - Nexus Snapshot Repository - https://oss.sonatype.org/content/repositories/snapshots + central + Central Snapshot Repository + https://central.sonatype.com/repository/maven-snapshots @@ -183,6 +169,9 @@ prepare-agent + + jacocoArgLine + report @@ -220,11 +209,14 @@ org.apache.maven.plugins maven-compiler-plugin - 3.10.1 + 3.11.0 ${project.build.jdk} ${project.build.jdk} ${project.build.sourceEncoding} + + -parameters + @@ -245,22 +237,20 @@ org.codehaus.groovy groovy-all - 2.4.15 + 2.4.21 org.apache.maven.plugins maven-surefire-plugin - 2.17 + 2.22.0 **/*Test.java - **/*Test.groovy **/*Tests.java - **/*Test.groovy - **/*Spec.java + -Dfile.encoding=UTF-8 ${jacocoArgLine} @@ -282,7 +272,7 @@ org.codehaus.groovy groovy-all - 2.4.15 + 2.4.21 pom @@ -325,6 +315,15 @@ + + + io.netty + netty-bom + ${netty.version} + pom + import + + com.google.code.findbugs jsr305 @@ -360,7 +359,7 @@ org.projectlombok lombok - 1.16.22 + 1.18.30 @@ -403,7 +402,19 @@ commons-beanutils commons-beanutils - 1.9.4 + 1.11.0 + + + + + + + + + + org.apache.commons + commons-collections4 + 4.4 @@ -470,12 +481,18 @@ aliyun-nexus aliyun https://maven.aliyun.com/nexus/content/groups/public/ + + false + hsweb-nexus Nexus Release Repository - https://nexus.hsweb.me/content/groups/public/ + https://nexus.jetlinks.cn/content/groups/public/ + + false + true @@ -487,15 +504,10 @@ - - releases - Nexus Release Repository - https://nexus.hsweb.me/content/repositories/releases/ - snapshots Nexus Snapshot Repository - https://nexus.hsweb.me/content/repositories/snapshots/ + https://nexus.jetlinks.cn/content/repositories/snapshots/