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 9ca7dcc37..ad5f79f33 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ # hsweb4 基于spring-boot2,全响应式的后台管理框架 + [![Codecov](https://codecov.io/gh/hs-web/hsweb-framework/branch/4.0.x/graph/badge.svg)](https://codecov.io/gh/hs-web/hsweb-framework/branch/master) [![Build Status](https://api.travis-ci.com/hs-web/hsweb-framework.svg?branch=4.0.x)](https://travis-ci.com/hs-web/hsweb-framework) [![License](https://img.shields.io/badge/license-Apache%202-4EB1BA.svg?style=flat-square)](https://www.apache.org/licenses/LICENSE-2.0.html) # 功能,特性 + - [x] 基于[r2dbc](https://github.com/r2dbc) ,[easy-orm](https://github.com/hs-web/hsweb-easy-orm/tree/4.0.x) 的通用响应式CRUD - [x] H2,Mysql,SqlServer,PostgreSQL - [x] 响应式r2dbc事务控制 @@ -12,7 +14,7 @@ - [x] 数据权限控制 - [ ] 双因子验证 - [x] 多维度权限管理功能 -- [x] 响应式缓存 +- [x] 响应式缓存 - [ ] 非响应式支持(mvc,jdbc) - [ ] 内置业务功能 - [x] 权限管理 @@ -24,10 +26,111 @@ - [ ] 文件秒传 - [x] 数据字典 -# 文档 +# 示例 + +https://github.com/zhou-hao/hsweb4-examples + +## 应用场景 + +1. 完全开源的后台管理系统. +2. 模块化的后台管理系统. +3. 功能可拓展的后台管理系统. +4. 集成各种常用功能的后台管理系统. +5. 前后分离的后台管理系统. + +注意: +项目主要基于`spring-boot`,`spring-webflux`. 在使用`hsweb`之前,你应该对 [project-reactor](https://projectreactor.io/) , +[spring-boot](https://github.com/spring-projects/spring-boot) 有一定的了解. + +项目模块太多?不要被吓到.我们不推荐将本项目直接`clone`后修改,运行.而是使用maven依赖的方式使用`hsweb`. 选择自己需要的模块进行依赖,正式版发布后,所有模块都将发布到maven中央仓库. + +## 文档 + +各个模块的使用方式查看对应模块下的 `README.md`,在使用之前, 你可以先粗略浏览一下各个模块,对每个模块的作用有大致的了解. + +## 核心技术选型 + +1. Java 8 +2. Maven3 +3. Spring Boot 2.x +4. Project Reactor 响应式编程框架 +5. hsweb easy orm 对r2dbc的orm封装 + +## 模块简介 + +| 模块 | 说明 | +| ------------- |:----------:| +|[hsweb-authorization](hsweb-authorization)| 权限控制 | +|[hsweb-commons](hsweb-commons) | 基础通用功能 | +|[hsweb-concurrent](hsweb-concurrent)| 并发包,缓存,等 | +|[hsweb-core](hsweb-core)| 框架核心,基础工具类 | +|[hsweb-datasource](hsweb-datasource)| 数据源 | +|[hsweb-logging](hsweb-logging)| 日志 | +|[hsweb-starter](hsweb-starter)| 模块启动器 | +|[hsweb-system](hsweb-system)| **系统常用功能** | + +## 核心特性 + +1. 响应式,首个基于spring-webflux,r2dbc,从头到位的响应式. +2. DSL风格,可拓展的通用curd,支持前端直接传参数,无需担心任何sql注入. + +```java + //where name = #{name} + createQuery() + .where("name",name) + .fetch(); + + //update s_user set name = #{user.name} where id = #{user.id} + createUpdate() + .set(user::getName) + .where(user::getId) + .execute(); + +``` + +3. 类JPA增删改 + +```java + +@Table(name = "s_entity") +public class MyEntity { + + @Id + private String id; + + @Column + private String name; + + @Column + private Long createTime; +} + +``` + +直接注入即可实现增删改查 + +```java + +@Autowire +private ReactiveRepository repository; + +``` + +2. 灵活的权限控制 + +```java + +@PostMapping("/account") +@SaveAction +public Mono addAccount(@RequestBody Mono account){ + return accountService.doSave(account); +} + +``` + +## License -直接看代码: https://github.com/zhou-hao/hsweb4-examples +[Apache 2.0](https://github.com/spring-projects/spring-boot/blob/main/LICENSE.txt) -# 实践 -[JetLinks开源物联网平台](https://github.com/jetlinks) +[![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 474c3bf07..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.12-SNAPSHOT + 4.0.20-SNAPSHOT 4.0.0 + ${artifactId} 授权,权限管理API hsweb-authorization-api @@ -27,7 +28,6 @@ io.lettuce lettuce-core - 5.2.0.RELEASE test 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 3fc9d1c08..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,22 +54,34 @@ public final class AuthenticationHolder { private static final ReadWriteLock lock = new ReentrantReadWriteLock(); - private static Optional get(Function> function) { + private static final FastThreadLocal CURRENT = new FastThreadLocal<>(); + - return Flux.fromStream(suppliers.stream().map(function)) - .filter(Optional::isPresent) - .map(Optional::get) - .reduceWith(SimpleAuthentication::new, SimpleAuthentication::merge) - .filter(auth->auth.getUser()!=null) - .map(Authentication.class::cast) - .blockOptional(); + private static Optional get(Function> function) { + int size = suppliers.size(); + if (size == 0) { + return Optional.empty(); + } + if (size == 1) { + return function.apply(suppliers.get(0)); + } + ReactiveAuthenticationHolder.AuthenticationMerging merging + = new ReactiveAuthenticationHolder.AuthenticationMerging(); + for (AuthenticationSupplier supplier : suppliers) { + function.apply(supplier).ifPresent(merging::merge); + } + return Optional.ofNullable(merging.get()); } + /** * @return 当前登录的用户权限信息 */ public static Optional get() { - + Authentication current = CURRENT.get(); + if (current != null) { + return Optional.of(current); + } return get(AuthenticationSupplier::get); } @@ -82,7 +98,7 @@ public static Optional get(String userId) { /** * 初始化 {@link AuthenticationSupplier} * - * @param supplier + * @param supplier 认证信息提供者 */ public static void addSupplier(AuthenticationSupplier supplier) { lock.writeLock().lock(); @@ -93,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 de756e4aa..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,16 +26,15 @@ import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Function; -import java.util.stream.Collectors; /** - * 权限获取器,用于静态方式获取当前登录用户的权限信息. + * 响应式权限保持器,用于响应式方式获取当前登录用户的权限信息. * 例如: - *
- *     @RequestMapping("/example")
- *     public ResponseMessage example(){
- *         Authorization auth = AuthorizationHolder.get();
- *         return ResponseMessage.ok();
+ * 
{@code
+ *     @RequestMapping("/example")
+ *     public Mono example(){
+ *         return ReactiveAuthenticationHolder.get();
+ *     }
  *     }
  * 
* @@ -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/FieldDataAccess.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/FieldDataAccess.java index 5c945f3fc..70b129ebf 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/FieldDataAccess.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/FieldDataAccess.java @@ -4,10 +4,14 @@ import java.lang.annotation.*; +/** + * @deprecated 已弃用 + */ @DataAccessType(id = "FIELD_DENY", name = "字段权限") @Retention(RetentionPolicy.RUNTIME) @Documented @Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD}) +@Deprecated public @interface FieldDataAccess { @AliasFor(annotation = DataAccessType.class) 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 543704dbc..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 @@ -1,28 +1,101 @@ package org.hswebframework.web.authorization.annotation; +import org.hswebframework.web.authorization.Permission; import org.hswebframework.web.authorization.define.Phased; import java.lang.annotation.*; -@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.TYPE}) +/** + * 接口资源声明注解,声明Controller的资源相关信息,用于进行权限控制。 + *
+ * 在Controller进行注解,表示此接口需要有对应的权限{@link Permission#getId()}才能进行访问. + * 具体的操作权限控制,需要在方法上注解{@link ResourceAction}. + *
+ * + * + *
{@code
+ * @RestController
+ * //声明资源
+ * @Resource(id = "test", name = "测试功能")
+ * public class TestController implements ReactiveCrudController {
+ *
+ *     //声明操作,需要有 test:query 权限才能访问此接口
+ *     @QueryAction
+ *     public Mono getUser() {
+ *         return Authentication.currentReactive()
+ *                 .switchIfEmpty(Mono.error(new UnAuthorizedException()))
+ *                 .map(Authentication::getUser);
+ *     }
+ *
+ * }
+ * }
+ * 
+ * 如果接口不需要进行权限控制,可注解{@link Authorize#ignore()}来标识此接口不需要权限控制. + * 或者通过监听 {@link org.hswebframework.web.authorization.events.AuthorizingHandleBeforeEvent}来进行自定义处理 + *
{@code
+ *   @EventListener
+ *   public void handleAuthEvent(AuthorizingHandleBeforeEvent e) {
+ *      //admin用户可以访问全部操作
+ *      if ("admin".equals(e.getContext().getAuthentication().getUser().getUsername())) {
+ *         e.setAllow(true);
+ *       }
+ *    }
+ * }
+ * + * @author zhouhao + * @see ResourceAction + * @see Authorize + * @see org.hswebframework.web.authorization.events.AuthorizingHandleBeforeEvent + * @since 4.0 + */ +@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD,ElementType.FIELD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface Resource { + + /** + * 资源ID + * + * @return 资源ID + */ String id(); + /** + * @return 资源名称 + */ String name(); + /** + * @return 资源操作定义 + */ ResourceAction[] actions() default {}; + /** + * @return 多个操作控制逻辑 + */ Logical logical() default Logical.DEFAULT; + /** + * @return 权限控制阶段 + */ Phased phased() default Phased.before; + /** + * @return 资源描述 + */ String[] description() default {}; + /** + * @return 资源分组 + */ String[] group() default {}; + /** + * 如果在方法上设置此属性,表示是否合并类上注解的属性 + * + * @return 是否合并 + */ boolean merge() default true; } 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 335ef878d..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 @@ -1,24 +1,65 @@ package org.hswebframework.web.authorization.annotation; -import org.hswebframework.web.authorization.define.Phased; + +import org.hswebframework.web.authorization.Permission; import java.lang.annotation.*; /** + * 对资源操作的描述,通常用来进行权限控制. + *

+ * 在Controller方法上添加此注解,来声明根据权限操作{@link Permission#getActions()}进行权限控制. + *

+ * 可以使用注解继承的方式来统一定义操作: + *

{@code
+ * @Target(ElementType.METHOD)
+ * @Retention(RetentionPolicy.RUNTIME)
+ * @Inherited
+ * @Documented
+ * @ResourceAction(id = "create", name = "新增")
+ * public @interface CreateAction {
+ *
+ * }
+ * }
+ * 
+ * * @see CreateAction + * @see DeleteAction + * @see SaveAction + * @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 public @interface ResourceAction { + /** + * 操作标识 + * + * @return 操作标识 + * @see Permission#getActions() + */ String id(); + /** + * @return 操作名称 + */ String name(); + /** + * @return 操作说明 + */ String[] description() default {}; + /** + * @return 多个操作时的判断逻辑 + */ Logical logical() default Logical.DEFAULT; + /** + * @deprecated 已弃用, 4.1中移除 + */ + @Deprecated DataAccess[] dataAccess() default @DataAccess(ignore = true); } 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 cf7b7b339..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 @@ -5,13 +5,20 @@ import java.lang.annotation.*; -@Target(ElementType.METHOD) +/** + * 继承{@link ResourceAction},提供统一的id定义 + * + * @author zhouhao + * @since 4.0 + */ +@Target({ElementType.METHOD,ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented @ResourceAction(id = Permission.ACTION_SAVE, name = "保存") public @interface SaveAction { - @AliasFor(annotation = ResourceAction.class,attribute = "dataAccess") + @Deprecated + @AliasFor(annotation = ResourceAction.class, attribute = "dataAccess") DataAccess dataAccess() default @DataAccess(ignore = true); } diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/UserOwnData.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/UserOwnData.java index 7f4caa9f8..74dbb12f7 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/UserOwnData.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/UserOwnData.java @@ -4,12 +4,15 @@ /** * 声明某个操作支持用户查看自己的数据 + * + * @deprecated 已弃用 */ @Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented @DataAccessType(id = "user_own_data", name = "用户自己的数据") +@Deprecated public @interface UserOwnData { } 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/AuthorizeDefinitionContext.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/AuthorizeDefinitionContext.java new file mode 100644 index 000000000..8b69af611 --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/AuthorizeDefinitionContext.java @@ -0,0 +1,7 @@ +package org.hswebframework.web.authorization.define; + +public interface AuthorizeDefinitionContext { + + void addResource(ResourceDefinition def); + +} diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/AuthorizeDefinitionCustomizer.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/AuthorizeDefinitionCustomizer.java new file mode 100644 index 000000000..b0fe8d320 --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/AuthorizeDefinitionCustomizer.java @@ -0,0 +1,7 @@ +package org.hswebframework.web.authorization.define; + +public interface AuthorizeDefinitionCustomizer { + + void custom(AuthorizeDefinitionContext context); + +} diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/CompositeAuthorizeDefinitionCustomizer.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/CompositeAuthorizeDefinitionCustomizer.java new file mode 100644 index 000000000..54af14835 --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/CompositeAuthorizeDefinitionCustomizer.java @@ -0,0 +1,24 @@ +package org.hswebframework.web.authorization.define; + +import lombok.AllArgsConstructor; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +@AllArgsConstructor +public class CompositeAuthorizeDefinitionCustomizer implements AuthorizeDefinitionCustomizer{ + + private final List customizers; + + public CompositeAuthorizeDefinitionCustomizer(Iterable customizers){ + this(StreamSupport.stream(customizers.spliterator(),false).collect(Collectors.toList())); + } + + @Override + public void custom(AuthorizeDefinitionContext context) { + for (AuthorizeDefinitionCustomizer customizer : customizers) { + customizer.custom(context); + } + } +} 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/MergedAuthorizeDefinition.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/MergedAuthorizeDefinition.java index 94d32070d..b247c7a0c 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/MergedAuthorizeDefinition.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/MergedAuthorizeDefinition.java @@ -4,11 +4,11 @@ import java.util.Set; -public class MergedAuthorizeDefinition { +public class MergedAuthorizeDefinition implements AuthorizeDefinitionContext { - private ResourcesDefinition resources = new ResourcesDefinition(); + private final ResourcesDefinition resources = new ResourcesDefinition(); - private DimensionsDefinition dimensions = new DimensionsDefinition(); + private final DimensionsDefinition dimensions = new DimensionsDefinition(); public Set getResources() { return resources.getResources(); 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/AuthorizationBeforeEvent.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/events/AuthorizationBeforeEvent.java index 2222a4a5a..eb3d4d5e4 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/events/AuthorizationBeforeEvent.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/events/AuthorizationBeforeEvent.java @@ -18,6 +18,9 @@ package org.hswebframework.web.authorization.events; +import lombok.Getter; +import org.hswebframework.web.authorization.Authentication; + import java.util.function.Function; /** @@ -26,11 +29,29 @@ * @author zhouhao * @since 3.0 */ +@Getter public class AuthorizationBeforeEvent extends AbstractAuthorizationEvent { private static final long serialVersionUID = 5948747533500518524L; + private String userId; + + private Authentication authentication; + public AuthorizationBeforeEvent(String username, String password, Function parameterGetter) { super(username, password, parameterGetter); } + + public void setAuthorized(String userId) { + this.userId = userId; + } + + public void setAuthorized(Authentication authentication) { + this.authentication = authentication; + } + + public boolean isAuthorized() { + return userId != null || authentication != null; + } + } diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/events/AuthorizationInitializeEvent.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/events/AuthorizationInitializeEvent.java index 6144f6c5f..93f9bb261 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/events/AuthorizationInitializeEvent.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/events/AuthorizationInitializeEvent.java @@ -2,11 +2,14 @@ import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.Setter; import org.hswebframework.web.authorization.Authentication; +import org.hswebframework.web.event.DefaultAsyncEvent; @Getter +@Setter @AllArgsConstructor -public class AuthorizationInitializeEvent { +public class AuthorizationInitializeEvent extends DefaultAsyncEvent { private Authentication authentication; } 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 3eb6d2876..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,11 +2,25 @@ 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; -public class AuthorizingHandleBeforeEvent extends ApplicationEvent implements AuthorizationEvent { - - private static final long serialVersionUID = -1095765748533721998L; +/** + * 权限控制事件,在进行权限控制之前会推送此事件,用于自定义权限控制结果: + *
{@code
+ *   @EventListener
+ *   public void handleAuthEvent(AuthorizingHandleBeforeEvent e) {
+ *      //admin用户可以访问全部操作
+ *      if ("admin".equals(e.getContext().getAuthentication().getUser().getUsername())) {
+ *         e.setAllow(true);
+ *       }
+ *    }
+ * }
+ * + * @author zhouhao + * @since 4.0 + */ +public class AuthorizingHandleBeforeEvent extends DefaultAsyncEvent implements AuthorizationEvent { private boolean allow = false; @@ -14,15 +28,21 @@ public class AuthorizingHandleBeforeEvent extends ApplicationEvent implements Au private String message; - private HandleType handleType; + 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() { @@ -33,6 +53,11 @@ public boolean isAllow() { return allow; } + /** + * 设置通过当前请求 + * + * @param allow allow + */ public void setAllow(boolean allow) { execute = false; this.allow = allow; @@ -42,11 +67,18 @@ public String getMessage() { return message; } + /** + * 设置错误提示消息 + * + * @param message 消息 + */ public void setMessage(String message) { this.message = message; } - + /** + * @return 权限控制类型 + */ public HandleType getHandleType() { return handleType; } 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 7a1d5502e..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 @@ -1,7 +1,8 @@ package org.hswebframework.web.authorization.simple; import lombok.AllArgsConstructor; -import org.apache.commons.collections.CollectionUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; import org.hswebframework.web.authorization.*; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -11,6 +12,7 @@ import java.util.stream.Collectors; @AllArgsConstructor +@Slf4j public class CompositeReactiveAuthenticationManager implements ReactiveAuthenticationManager { private final List providers; @@ -21,7 +23,10 @@ public Mono authenticate(Mono request) { .stream() .map(manager -> manager .authenticate(request) - .onErrorResume((err) -> Mono.empty())) + .onErrorResume((err) -> { + log.warn("get user authenticate error", err); + return Mono.empty(); + })) .collect(Collectors.toList())) .take(1) .next(); @@ -34,7 +39,10 @@ public Mono getByUserId(String userId) { .stream() .map(manager -> manager .getByUserId(userId) - .onErrorResume((err) -> Mono.empty()) + .onErrorResume((err) -> { + log.warn("get user [{}] authentication error", userId, err); + return Mono.empty(); + }) )) .flatMap(Function.identity()) .collectList() 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 8d8ef7e7d..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 @@ -58,10 +58,16 @@ public Map getAttributes() { } public SimpleAuthentication merge(Authentication authentication) { - Map mePermissionGroup = permissions.stream() - .collect(Collectors.toMap(Permission::getId, Function.identity())); - user = authentication.getUser(); + Map mePermissionGroup = permissions + .stream() + .collect(Collectors.toMap(Permission::getId, Function.identity())); + + if (authentication.getUser() != null) { + user = authentication.getUser(); + } + attributes.putAll(authentication.getAttributes()); + for (Permission permission : authentication.getPermissions()) { Permission me = mePermissionGroup.get(permission.getId()); if (me == null) { @@ -72,7 +78,6 @@ public SimpleAuthentication merge(Authentication authentication) { me.getDataAccesses().addAll(permission.getDataAccesses()); } - for (Dimension dimension : authentication.getDimensions()) { if (!getDimension(dimension.getType(), dimension.getId()).isPresent()) { dimensions.add(dimension); @@ -81,18 +86,44 @@ 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(); - authentication.setUser(user); + 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; } + + public void setUser(User user) { + this.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/LocalUserToken.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/LocalUserToken.java index c562e1b0e..a157ee7f9 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/LocalUserToken.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/LocalUserToken.java @@ -74,9 +74,21 @@ public String getToken() { @Override public TokenState getState() { + if (state == TokenState.normal) { + checkExpired(); + } return state; } + @Override + public boolean checkExpired() { + if (UserToken.super.checkExpired()) { + setState(TokenState.expired); + return true; + } + return false; + } + public void setState(TokenState state) { this.state = state; } 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/ReactiveTokenAuthenticationSupplier.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/ReactiveTokenAuthenticationSupplier.java index 97efdb45a..2e1c205db 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/ReactiveTokenAuthenticationSupplier.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/ReactiveTokenAuthenticationSupplier.java @@ -20,15 +20,10 @@ public Mono get(String userId) { @Override public Mono get() { - return ContextUtils - .reactiveContext() - .flatMap(context -> context - .get(ContextKey.of(ParsedToken.class)) + return Mono + .deferContextual(context -> context + .getOrEmpty(ParsedToken.class) .map(t -> tokenManager.getByToken(t.getToken())) - .orElseGet(Mono::empty)) - .flatMap(auth -> ReactiveLogger - .mdc("userId", auth.getUser().getId(), - "username", auth.getUser().getName()) - .thenReturn(auth)); + .orElse(Mono.empty())); } } 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/UserTokenReactiveAuthenticationSupplier.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/UserTokenReactiveAuthenticationSupplier.java index 894a78f8c..e8fc342da 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/UserTokenReactiveAuthenticationSupplier.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/UserTokenReactiveAuthenticationSupplier.java @@ -68,25 +68,26 @@ protected Mono get(ReactiveAuthenticationManager authenticationM @Override public Mono get() { - return ContextUtils - .reactiveContext() - .flatMap(context -> context - .get(ContextKey.of(ParsedToken.class)) + return Mono + .deferContextual(context -> context + .getOrEmpty(ParsedToken.class) .map(t -> userTokenManager .getByToken(t.getToken()) - .filter(UserToken::validate) .flatMap(token -> { + //已过期则返回空 + if (token.isExpired()) { + return Mono.empty(); + } + if(!token.validate()){ + return Mono.empty(); + } Mono before = userTokenManager.touch(token.getToken()); if (token instanceof AuthenticationUserToken) { return before.thenReturn(((AuthenticationUserToken) token).getAuthentication()); } return before.then(get(thirdPartAuthenticationManager.get(token.getType()), token.getUserId())); })) - .orElseGet(Mono::empty)) - .flatMap(auth -> ReactiveLogger - .mdc("userId", auth.getUser().getId(), - "username", auth.getUser().getName()) - .thenReturn(auth)) + .orElse(Mono.empty())) ; } 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 e890de127..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,17 +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 -> operations - .expire(getTokenRedisKey(token.getToken()), 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(); } @@ -65,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() )); } @@ -83,6 +86,10 @@ public RedisUserTokenManager(ReactiveRedisConnectionFactory connectionFactory) { //异地登录模式,默认允许异地登录 private AllopatricLoginMode allopatricLoginMode = AllopatricLoginMode.allow; + @Getter + @Setter + private Duration maxTokenExpires = Duration.ofSeconds(1).negated(); + @Setter private ApplicationEventPublisher eventPublisher; @@ -101,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(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 @@ -182,94 +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.getToken()), 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) { - 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", maxInactiveInterval); - 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 (maxInactiveInterval > 0) { - return operations.expire(key, Duration.ofMillis(maxInactiveInterval)); - } - 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); - } + long expires = maxTokenExpires.isNegative() ? maxInactiveInterval : Math.min(maxInactiveInterval, maxTokenExpires.toMillis()); + return Mono + .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 -> { }); } @@ -280,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 @@ -297,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) { @@ -335,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) { @@ -344,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/java/org/hswebframework/web/authorization/token/redis/SimpleUserToken.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/redis/SimpleUserToken.java index d986bda34..6e931ba73 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/redis/SimpleUserToken.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/redis/SimpleUserToken.java @@ -35,18 +35,25 @@ public class SimpleUserToken implements UserToken { public static SimpleUserToken of(Map map) { Object authentication = map.get("authentication"); - if(authentication instanceof Authentication){ + if (authentication instanceof Authentication) { return FastBeanCopier.copy(map, new SimpleAuthenticationUserToken(((Authentication) authentication))); } return FastBeanCopier.copy(map, new SimpleUserToken()); } + public TokenState getState() { + if (state == TokenState.normal) { + checkExpired(); + } + return state; + } + @Override - public boolean isNormal() { - if (checkExpired()) { + public boolean checkExpired() { + if (UserToken.super.checkExpired()) { setState(TokenState.expired); - return false; + return true; } - return UserToken.super.isNormal(); + return false; } } 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 9e1bcb7d1..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,8 @@ 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 # message.token_state_normal=Normal 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 a9bd62303..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,8 @@ 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=用户已被禁用 # message.token_state_normal=正常 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 9e4df5c31..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,9 +9,11 @@ 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; +import reactor.util.context.Context; import java.util.Collections; import java.util.Set; @@ -114,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() { @@ -135,11 +141,8 @@ public String getType() { .currentReactive() .map(Authentication::getUser) .map(User::getId) - .doOnEach(ReactiveLogger.on(SignalType.ON_NEXT,(ctx,signal)->{ - System.out.println(ctx); - })) - .subscriberContext(acceptContext(ctx -> ctx.put(ContextKey.of(ParsedToken.class), parsedToken))) - .subscriberContext(ReactiveLogger.start("rid","1")) + .contextWrite(Context.of(ParsedToken.class, parsedToken)) + .contextWrite(ReactiveLogger.start("rid","1")) .as(StepVerifier::create) .expectNext("admin") .verifyComplete(); 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 9f0520387..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.12-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 d1d59ba53..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,42 +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(); } @@ -108,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()) { @@ -119,22 +127,17 @@ 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; - Phased dataAccessPhased = null; - dataAccessPhased = definition.getResources().getPhased(); + Phased dataAccessPhased = definition.getResources().getPhased(); if (definition.getPhased() == Phased.before) { //RDAC before authorizingHandler.handRBAC(context); @@ -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/aop/DefaultAopMethodAuthorizeDefinitionParser.java b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/aop/DefaultAopMethodAuthorizeDefinitionParser.java index 0a5f9efaf..6b3c96afe 100644 --- a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/aop/DefaultAopMethodAuthorizeDefinitionParser.java +++ b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/aop/DefaultAopMethodAuthorizeDefinitionParser.java @@ -6,6 +6,7 @@ import org.hswebframework.web.authorization.annotation.Authorize; import org.hswebframework.web.authorization.annotation.DataAccess; import org.hswebframework.web.authorization.annotation.Dimension; +import org.hswebframework.web.authorization.annotation.ResourceAction; import org.hswebframework.web.authorization.basic.define.DefaultBasicAuthorizeDefinition; import org.hswebframework.web.authorization.basic.define.EmptyAuthorizeDefinition; import org.hswebframework.web.authorization.define.AuthorizeDefinition; @@ -15,8 +16,10 @@ import org.springframework.core.type.AnnotationMetadata; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; +import org.springframework.web.bind.annotation.RequestMapping; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -63,7 +66,8 @@ public AuthorizeDefinition parse(Class target, Method method, MethodIntercept } //使用自定义 if (!CollectionUtils.isEmpty(parserCustomizers)) { - definition = parserCustomizers.stream() + definition = parserCustomizers + .stream() .map(customizer -> customizer.parse(target, method, context)) .filter(Objects::nonNull) .findAny().orElse(null); @@ -77,7 +81,7 @@ public AuthorizeDefinition parse(Class target, Method method, MethodIntercept Authorize annotation = AnnotationUtils.findAnnotation(target, method, Authorize.class); - if (annotation != null && annotation.ignore()) { + if (isIgnoreMethod(method) || (annotation != null && annotation.ignore())) { cache.put(key, EmptyAuthorizeDefinition.instance); return null; } @@ -107,4 +111,14 @@ public void destroy() { cache.clear(); } + static boolean isIgnoreMethod(Method method) { + //不是public的方法 + if(!Modifier.isPublic(method.getModifiers())){ + return true; + } + //没有以下注解 + return null == AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class) + && null == AnnotatedElementUtils.findMergedAnnotation(method, ResourceAction.class); + } + } 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 50e4510a9..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 { @@ -58,65 +46,6 @@ public UserTokenWebFilter userTokenWebFilter() { } - @Configuration - @ConditionalOnClass(name = "org.springframework.web.servlet.config.annotation.WebMvcConfigurer") - @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) - static class WebMvcAuthorizingConfiguration { - @Bean - @Order(Ordered.HIGHEST_PRECEDENCE) - public WebMvcConfigurer webUserTokenInterceptorConfigurer(UserTokenManager userTokenManager, - AopMethodAuthorizeDefinitionParser parser, - List userTokenParser) { - - return new WebMvcConfigurer() { - @Override - public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(new WebUserTokenInterceptor(userTokenManager, userTokenParser, parser)); - } - }; - } - - @Bean - public UserOnSignIn userOnSignIn(UserTokenManager userTokenManager) { - return new UserOnSignIn(userTokenManager); - } - - @Bean - public UserOnSignOut userOnSignOut(UserTokenManager userTokenManager) { - return new UserOnSignOut(userTokenManager); - } - - @SuppressWarnings("all") - @ConfigurationProperties(prefix = "hsweb.authorize.token.default") - public ServletUserTokenGenPar servletUserTokenGenPar(){ - return new ServletUserTokenGenPar(); - } - - @Bean - @ConditionalOnMissingBean(UserTokenParser.class) - public UserTokenParser userTokenParser() { - return new SessionIdUserTokenParser(); - } - - @Bean - public SessionIdUserTokenGenerator sessionIdUserTokenGenerator() { - return new SessionIdUserTokenGenerator(); - } - - @Bean - @ConditionalOnProperty(prefix = "hsweb.authorize.two-factor", name = "enable", havingValue = "true") - @Order(100) - public WebMvcConfigurer twoFactorHandlerConfigurer(TwoFactorValidatorManager manager) { - return new WebMvcConfigurer() { - @Override - public void addInterceptors(@Nonnull InterceptorRegistry registry) { - registry.addInterceptor(new TwoFactorHandlerInterceptorAdapter(manager)); - } - }; - } - - } - @Bean public ReactiveAuthenticationManagerProvider embedAuthenticationManager(EmbedAuthenticationProperties properties) { return new EmbedReactiveAuthenticationManager(properties); @@ -145,24 +74,10 @@ public ReactiveUserTokenController userTokenController() { return new ReactiveUserTokenController(); } - @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; - } + @Bean + @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) + public BearerTokenParser bearerTokenParser() { + return new BearerTokenParser(); } @@ -178,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 new file mode 100644 index 000000000..7ff7373e0 --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/configuration/WebMvcAuthorizingConfiguration.java @@ -0,0 +1,79 @@ +package org.hswebframework.web.authorization.basic.configuration; + +import org.hswebframework.web.authorization.basic.aop.AopMethodAuthorizeDefinitionParser; +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.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.*; +import org.springframework.boot.context.properties.ConfigurationProperties; +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 javax.annotation.Nonnull; +import java.util.List; + +@AutoConfiguration +@ConditionalOnClass(name = "org.springframework.web.servlet.config.annotation.WebMvcConfigurer") +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) +public class WebMvcAuthorizingConfiguration { + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + @ConditionalOnBean(AopMethodAuthorizeDefinitionParser.class) + public WebMvcConfigurer webUserTokenInterceptorConfigurer(UserTokenManager userTokenManager, + AopMethodAuthorizeDefinitionParser parser, + List userTokenParser) { + + return new WebMvcConfigurer() { + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new WebUserTokenInterceptor(userTokenManager, userTokenParser, parser)); + } + }; + } + + @Bean + public UserOnSignIn userOnSignIn(UserTokenManager userTokenManager) { + return new UserOnSignIn(userTokenManager); + } + + @Bean + public UserOnSignOut userOnSignOut(UserTokenManager userTokenManager) { + return new UserOnSignOut(userTokenManager); + } + + @SuppressWarnings("all") + @ConfigurationProperties(prefix = "hsweb.authorize.token.default") + public ServletUserTokenGenPar servletUserTokenGenPar() { + return new ServletUserTokenGenPar(); + } + + @Bean + @ConditionalOnMissingBean(UserTokenParser.class) + public UserTokenParser userTokenParser() { + return new SessionIdUserTokenParser(); + } + + @Bean + public SessionIdUserTokenGenerator sessionIdUserTokenGenerator() { + return new SessionIdUserTokenGenerator(); + } + + @Bean + @ConditionalOnProperty(prefix = "hsweb.authorize.two-factor", name = "enable", havingValue = "true") + @Order(100) + public WebMvcConfigurer twoFactorHandlerConfigurer(TwoFactorValidatorManager manager) { + return new WebMvcConfigurer() { + @Override + public void addInterceptors(@Nonnull InterceptorRegistry registry) { + registry.addInterceptor(new TwoFactorHandlerInterceptorAdapter(manager)); + } + }; + } + +} \ No newline at end of file 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 d5c556493..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; /** * 默认权限权限定义 @@ -38,20 +39,27 @@ public class DefaultBasicAuthorizeDefinition implements AopAuthorizeDefinition { private String message = "error.access_denied"; - private Phased phased; + 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) { @@ -65,6 +73,7 @@ public void putAnnotation(Authorize ann) { getResources().getResources().clear(); getDimensions().getDimensions().clear(); } + setPhased(ann.phased()); getResources().setPhased(ann.phased()); for (Resource resource : ann.resources()) { putAnnotation(resource); @@ -72,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) { @@ -97,6 +109,8 @@ public void putAnnotation(Resource ann) { putAnnotation(resource, action); } resource.setGroup(new ArrayList<>(Arrays.asList(ann.group()))); + setPhased(ann.phased()); + getResources().setPhased(ann.phased()); resources.addResource(resource, ann.merge()); } @@ -132,8 +146,8 @@ public void putAnnotation(ResourceActionDefinition definition, DataAccess ann) { return; } definition.getDataAccess() - .getDataAccessTypes() - .add(typeDefinition); + .getDataAccessTypes() + .add(typeDefinition); } public void putAnnotation(ResourceActionDefinition definition, DataAccessType dataAccessType) { @@ -147,8 +161,8 @@ public void putAnnotation(ResourceActionDefinition definition, DataAccessType da typeDefinition.setConfiguration(dataAccessType.configuration()); typeDefinition.setDescription(String.join("\n", dataAccessType.description())); definition.getDataAccess() - .getDataAccessTypes() - .add(typeDefinition); + .getDataAccessTypes() + .add(typeDefinition); } } diff --git a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/embed/EmbedAuthenticationProperties.java b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/embed/EmbedAuthenticationProperties.java index e4a09e3c0..1b35e4b1a 100644 --- a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/embed/EmbedAuthenticationProperties.java +++ b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/embed/EmbedAuthenticationProperties.java @@ -2,6 +2,7 @@ import lombok.Getter; import lombok.Setter; +import org.apache.commons.collections4.MapUtils; import org.hswebframework.web.authorization.Authentication; import org.hswebframework.web.authorization.AuthenticationRequest; import org.hswebframework.web.authorization.builder.DataAccessConfigBuilderFactory; @@ -69,7 +70,10 @@ public void afterPropertiesSet() { for (Map.Entry stringObjectEntry : objectMap.entrySet()) { if (stringObjectEntry.getValue() instanceof Map) { Map mapVal = ((Map) stringObjectEntry.getValue()); - boolean maybeIsList = mapVal.keySet().stream().allMatch(org.hswebframework.utils.StringUtils::isInt); + boolean maybeIsList = mapVal + .keySet() + .stream() + .allMatch(org.hswebframework.utils.StringUtils::isInt); if (maybeIsList) { stringObjectEntry.setValue(mapVal.values()); } @@ -82,20 +86,23 @@ public void afterPropertiesSet() { } public Authentication authenticate(AuthenticationRequest request) { - if(request instanceof PlainTextUsernamePasswordAuthenticationRequest){ + if (MapUtils.isEmpty(users)) { + return null; + } + if (request instanceof PlainTextUsernamePasswordAuthenticationRequest) { PlainTextUsernamePasswordAuthenticationRequest pwdReq = ((PlainTextUsernamePasswordAuthenticationRequest) request); - return users.values() - .stream() - .filter(user -> - pwdReq.getUsername().equals(user.getUsername()) - && pwdReq.getPassword().equals(user.getPassword())) - .findFirst() - .map(EmbedAuthenticationInfo::getId) - .map(authentications::get) - .orElseThrow(() -> new ValidationException("用户不存在")); + for (EmbedAuthenticationInfo user : users.values()) { + if (pwdReq.getUsername().equals(user.getUsername())) { + if (pwdReq.getPassword().equals(user.getPassword())) { + return user.toAuthentication(dataAccessConfigBuilderFactory); + } + return null; + } + } + return null; } - throw new UnsupportedOperationException("不支持的授权请求:"+request); + throw new UnsupportedOperationException("不支持的授权请求:" + request); } public Optional getAuthentication(String userId) { diff --git a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/embed/EmbedReactiveAuthenticationManager.java b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/embed/EmbedReactiveAuthenticationManager.java index 6aabf28d9..94fbd6739 100644 --- a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/embed/EmbedReactiveAuthenticationManager.java +++ b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/embed/EmbedReactiveAuthenticationManager.java @@ -1,6 +1,8 @@ package org.hswebframework.web.authorization.basic.embed; import lombok.AllArgsConstructor; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.MapUtils; import org.hswebframework.web.authorization.Authentication; import org.hswebframework.web.authorization.AuthenticationRequest; import org.hswebframework.web.authorization.ReactiveAuthenticationManager; @@ -22,7 +24,16 @@ public class EmbedReactiveAuthenticationManager implements ReactiveAuthenticatio @Override public Mono authenticate(Mono request) { - return request.map(properties::authenticate); + if (MapUtils.isEmpty(properties.getUsers())) { + return Mono.empty(); + } + return request. + handle((req, sink) -> { + Authentication auth = properties.authenticate(req); + if (auth != null) { + sink.next(auth); + } + }); } 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 a8f553f04..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 @@ -22,6 +22,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.SneakyThrows; import org.hswebframework.web.authorization.Authentication; +import org.hswebframework.web.authorization.ReactiveAuthenticationHolder; import org.hswebframework.web.authorization.ReactiveAuthenticationManager; import org.hswebframework.web.authorization.annotation.Authorize; import org.hswebframework.web.authorization.events.AuthorizationBeforeEvent; @@ -32,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.*; @@ -62,13 +65,13 @@ public class AuthorizationController { @Operation(summary = "当前登录用户权限信息") public Mono me() { return Authentication.currentReactive() - .switchIfEmpty(Mono.error(UnAuthorizedException::new)); + .switchIfEmpty(Mono.error(UnAuthorizedException::new)); } @PostMapping(value = "/login", consumes = MediaType.APPLICATION_JSON_VALUE) @Authorize(ignore = true) - @AccessLogger(ignore = true) - @Operation(summary = "登录",description = "必要参数:username,password.根据配置不同,其他参数也不同,如:验证码等.") + @AccessLogger(ignoreParameter = {"parameter"}) + @Operation(summary = "登录", description = "必要参数:username,password.根据配置不同,其他参数也不同,如:验证码等.") public Mono> authorizeByJson(@Parameter(example = "{\"username\":\"admin\",\"password\":\"admin\"}") @RequestBody Mono> parameter) { return doLogin(parameter); @@ -81,44 +84,61 @@ 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"); Function parameterGetter = parameters::get; - return Mono.defer(() -> { - AuthorizationDecodeEvent decodeEvent = new AuthorizationDecodeEvent(username_, password_, parameterGetter); - return decodeEvent - .publish(eventPublisher) - .then(Mono.defer(() -> { - String username = decodeEvent.getUsername(); - String password = decodeEvent.getPassword(); - AuthorizationBeforeEvent beforeEvent = new AuthorizationBeforeEvent(username, password, parameterGetter); - return beforeEvent - .publish(eventPublisher) - .then(authenticationManager - .authenticate(Mono.just(new PlainTextUsernamePasswordAuthenticationRequest(username, password))) - .switchIfEmpty(Mono.error(() -> new AuthenticationException(AuthenticationException.ILLEGAL_PASSWORD))) - .flatMap(auth -> { - //触发授权成功事件 - AuthorizationSuccessEvent event = new AuthorizationSuccessEvent(auth, parameterGetter); - event.getResult().put("userId", auth.getUser().getId()); - return event - .publish(eventPublisher) - .then(Mono.fromCallable(event::getResult)); - })); - })); - }).onErrorResume(err -> { - AuthorizationFailedEvent failedEvent = new AuthorizationFailedEvent(username_, password_, parameterGetter); - failedEvent.setException(err); - return failedEvent - .publish(eventPublisher) - .then(Mono.error(failedEvent.getException())); - }); + return Mono + .defer(() -> { + AuthorizationDecodeEvent decodeEvent = new AuthorizationDecodeEvent(username_, password_, parameterGetter); + return decodeEvent + .publish(eventPublisher) + .then(Mono.defer(() -> { + String username = decodeEvent.getUsername(); + String password = decodeEvent.getPassword(); + AuthorizationBeforeEvent beforeEvent = new AuthorizationBeforeEvent(username, password, parameterGetter); + return beforeEvent + .publish(eventPublisher) + .then(Mono.defer(() -> doAuthorize(beforeEvent) + .flatMap(auth -> { + //触发授权成功事件 + AuthorizationSuccessEvent event = new AuthorizationSuccessEvent(auth, parameterGetter); + event.getResult().put("userId", auth.getUser().getId()); + return event + .publish(eventPublisher) + .then(Mono.fromCallable(event::getResult)); + }))); + })); + }) + .onErrorResume(err -> { + AuthorizationFailedEvent failedEvent = new AuthorizationFailedEvent(username_, password_, parameterGetter); + failedEvent.setException(err); + return failedEvent + .publish(eventPublisher) + .then(Mono.error(failedEvent::getException)); + }); }); + } + private Mono doAuthorize(AuthorizationBeforeEvent event) { + Mono authenticationMono; + if (event.isAuthorized()) { + if (event.getAuthentication() != null) { + authenticationMono = Mono.just(event.getAuthentication()); + } else { + authenticationMono = ReactiveAuthenticationHolder + .get(event.getUserId()) + .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.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 new file mode 100644 index 000000000..6a848b14a --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/web/BearerTokenParser.java @@ -0,0 +1,22 @@ +package org.hswebframework.web.authorization.basic.web; + +import org.hswebframework.web.authorization.token.ParsedToken; +import org.springframework.http.HttpHeaders; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +public class BearerTokenParser implements ReactiveUserTokenParser { + @Override + public Mono parseToken(ServerWebExchange exchange) { + + String token = exchange + .getRequest() + .getHeaders() + .getFirst(HttpHeaders.AUTHORIZATION); + + if (token != null && token.startsWith("Bearer ")) { + 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 0e4c1810f..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,20 +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(new ParsedToken() { - @Override - public String getToken() { - return token; - } - - @Override - public String getType() { - return getTokenType(); - } - }); + 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/ReactiveUserTokenController.java b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/web/ReactiveUserTokenController.java index 1316853c2..916f9c46e 100644 --- a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/web/ReactiveUserTokenController.java +++ b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/web/ReactiveUserTokenController.java @@ -47,8 +47,8 @@ public void setAuthenticationManager(ReactiveAuthenticationManager authenticatio @Authorize(merge = false) @Operation(summary = "重置当前用户的令牌") public Mono resetToken() { - return ContextUtils.reactiveContext() - .map(context -> context.get(ContextKey.of(ParsedToken.class)).orElseThrow(UnAuthorizedException::new)) + return Mono + .deferContextual(ctx -> Mono.justOrEmpty(ctx.getOrEmpty(ParsedToken.class))) .flatMap(token -> userTokenManager.signOutByToken(token.getToken())) .thenReturn(true); } 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 d8b24a621..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; @@ -19,6 +20,7 @@ import org.springframework.web.server.WebFilterChain; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.util.context.Context; import java.util.ArrayList; import java.util.HashMap; @@ -28,6 +30,7 @@ @Component @Slf4j +@Order(1) public class UserTokenWebFilter implements WebFilter, BeanPostProcessor { private final List parsers = new ArrayList<>(); @@ -42,51 +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) - .subscriberContext( - ContextUtils.acceptContext( - context -> context.put(ParsedToken.class, token) - ) - )) - .defaultIfEmpty(chain.filter(exchange)) - .flatMap(Function.identity()) - .subscriberContext(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())); -// return chain.filter(exchange) -// .subscriberContext(ContextUtils.acceptContext(ctx -> -// Flux.fromIterable(parsers) -// .flatMap(parser -> parser.parseToken(exchange)) -// .subscribe(token -> ctx.put(ParsedToken.class, token))) -// ) -// .subscriberContext(ReactiveLogger.start("requestId", exchange.getRequest().getId())) } @EventListener public void handleUserSign(AuthorizationSuccessEvent event) { - ReactiveUserTokenGenerator generator = event.getParameter("tokenType") - .map(tokenGeneratorMap::get) - .orElseGet(() -> tokenGeneratorMap.get("default")); + ReactiveUserTokenGenerator generator = event + .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.factories deleted file mode 100644 index 47a5ff6e8..000000000 --- a/hsweb-authorization/hsweb-authorization-basic/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1,3 +0,0 @@ -# Auto Configure -org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -org.hswebframework.web.authorization.basic.configuration.AuthorizingHandlerAutoConfiguration \ No newline at end of file diff --git a/hsweb-authorization/hsweb-authorization-basic/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hsweb-authorization/hsweb-authorization-basic/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..42849de36 --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-basic/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +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 ac8384abf..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.12-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 e90793c6c..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, @@ -40,4 +40,21 @@ Mono createAccessToken(String clientId, */ Mono refreshAccessToken(String clientId, String refreshToken); + /** + * 移除token + * + * @param clientId clientId + * @param token token + * @return void + */ + Mono removeToken(String clientId, String token); + + /** + * 取消对用户的授权 + * + * @param clientId clientId + * @param userId 用户ID + * @return void + */ + Mono cancelGrant(String clientId, String userId); } 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/OAuth2Properties.java b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/OAuth2Properties.java new file mode 100644 index 000000000..8956781c1 --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/OAuth2Properties.java @@ -0,0 +1,20 @@ +package org.hswebframework.web.oauth2.server; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.time.Duration; + +@ConfigurationProperties(prefix = "hsweb.oauth2") +@Getter +@Setter +public class OAuth2Properties { + + //token有效期 + private Duration tokenExpireIn = Duration.ofSeconds(7200); + + //refreshToken有效期 + private Duration refreshTokenIn = Duration.ofDays(30); + +} 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 66228cc79..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,9 +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.oauth2.server.auth.ReactiveOAuth2AccessTokenParser; +import org.hswebframework.web.authorization.token.UserTokenManager; import org.hswebframework.web.oauth2.server.code.AuthorizationCodeGranter; import org.hswebframework.web.oauth2.server.code.DefaultAuthorizationCodeGranter; import org.hswebframework.web.oauth2.server.credential.ClientCredentialGranter; @@ -14,15 +13,20 @@ 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; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +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.data.redis.connection.ReactiveRedisConnectionFactory; +import org.springframework.data.redis.core.ReactiveRedisOperations; -@Configuration(proxyBeanMethods = false) +@AutoConfiguration +@EnableConfigurationProperties(OAuth2Properties.class) public class OAuth2ServerAutoConfiguration { @@ -30,13 +34,13 @@ public class OAuth2ServerAutoConfiguration { @ConditionalOnClass(ReactiveUserTokenParser.class) static class ReactiveOAuth2AccessTokenParserConfiguration { - @Bean - @ConditionalOnBean(AccessTokenManager.class) - public ReactiveOAuth2AccessTokenParser reactiveOAuth2AccessTokenParser(AccessTokenManager accessTokenManager) { - ReactiveOAuth2AccessTokenParser parser = new ReactiveOAuth2AccessTokenParser(accessTokenManager); - ReactiveAuthenticationHolder.addSupplier(parser); - return parser; - } +// @Bean +// @ConditionalOnBean(AccessTokenManager.class) +// public ReactiveOAuth2AccessTokenParser reactiveOAuth2AccessTokenParser(AccessTokenManager accessTokenManager) { +// ReactiveOAuth2AccessTokenParser parser = new ReactiveOAuth2AccessTokenParser(accessTokenManager); +// ReactiveAuthenticationHolder.addSupplier(parser); +// return parser; +// } } @Configuration(proxyBeanMethods = false) @@ -46,22 +50,30 @@ static class ReactiveOAuth2ServerAutoConfiguration { @Bean @ConditionalOnMissingBean - public AccessTokenManager accessTokenManager(ReactiveRedisConnectionFactory redisConnectionFactory) { - return new RedisAccessTokenManager(redisConnectionFactory); + public AccessTokenManager accessTokenManager(ReactiveRedisOperations redis, + UserTokenManager tokenManager, + OAuth2Properties properties) { + @SuppressWarnings("all") + RedisAccessTokenManager manager = new RedisAccessTokenManager((ReactiveRedisOperations) redis, tokenManager); + manager.setTokenExpireIn((int) properties.getTokenExpireIn().getSeconds()); + manager.setRefreshExpireIn((int) properties.getRefreshTokenIn().getSeconds()); + return manager; } @Bean @ConditionalOnMissingBean public ClientCredentialGranter clientCredentialGranter(ReactiveAuthenticationManager authenticationManager, - AccessTokenManager accessTokenManager) { - return new DefaultClientCredentialGranter(authenticationManager, accessTokenManager); + AccessTokenManager accessTokenManager, + ApplicationEventPublisher eventPublisher) { + return new DefaultClientCredentialGranter(authenticationManager, accessTokenManager,eventPublisher); } @Bean @ConditionalOnMissingBean public AuthorizationCodeGranter authorizationCodeGranter(AccessTokenManager tokenManager, + ApplicationEventPublisher eventPublisher, ReactiveRedisConnectionFactory redisConnectionFactory) { - return new DefaultAuthorizationCodeGranter(tokenManager, redisConnectionFactory); + return new DefaultAuthorizationCodeGranter(tokenManager,eventPublisher, redisConnectionFactory); } @Bean 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 dedcfe752..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("[ ]"); @@ -47,16 +44,11 @@ public Mono get(String userId) { @Override public Mono get() { - return ContextUtils - .reactiveContext() - .flatMap(context -> context - .get(ContextKey.of(ParsedToken.class)) + return Mono + .deferContextual(context -> context + .getOrEmpty(ParsedToken.class) .filter(token -> "oauth2".equals(token.getType())) .map(t -> accessTokenManager.getAuthenticationByToken(t.getToken())) - .orElse(Mono.empty())) - .flatMap(auth -> ReactiveLogger - .mdc("userId", auth.getUser().getId(), - "username", auth.getUser().getName()) - .thenReturn(auth)); + .orElse(Mono.empty())); } } 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 c3b53cabd..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 @@ -4,13 +4,16 @@ import org.hswebframework.web.authorization.Authentication; import org.hswebframework.web.id.IDGenerator; import org.hswebframework.web.oauth2.ErrorType; +import org.hswebframework.web.oauth2.GrantType; import org.hswebframework.web.oauth2.OAuth2Constants; import org.hswebframework.web.oauth2.OAuth2Exception; import org.hswebframework.web.oauth2.server.AccessToken; import org.hswebframework.web.oauth2.server.AccessTokenManager; import org.hswebframework.web.oauth2.server.OAuth2Client; import org.hswebframework.web.oauth2.server.ScopePredicate; +import org.hswebframework.web.oauth2.server.event.OAuth2GrantedEvent; import org.hswebframework.web.oauth2.server.utils.OAuth2ScopeUtils; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory; import org.springframework.data.redis.core.ReactiveRedisOperations; import org.springframework.data.redis.core.ReactiveRedisTemplate; @@ -25,11 +28,15 @@ public class DefaultAuthorizationCodeGranter implements AuthorizationCodeGranter private final AccessTokenManager accessTokenManager; + private final ApplicationEventPublisher eventPublisher; + private final ReactiveRedisOperations redis; @SuppressWarnings("all") - public DefaultAuthorizationCodeGranter(AccessTokenManager accessTokenManager, ReactiveRedisConnectionFactory connectionFactory) { - this(accessTokenManager, new ReactiveRedisTemplate<>(connectionFactory, RedisSerializationContext + public DefaultAuthorizationCodeGranter(AccessTokenManager accessTokenManager, + ApplicationEventPublisher eventPublisher, + ReactiveRedisConnectionFactory connectionFactory) { + this(accessTokenManager, eventPublisher, new ReactiveRedisTemplate<>(connectionFactory, RedisSerializationContext .newSerializationContext() .key((RedisSerializer) RedisSerializer.string()) .value(RedisSerializer.java()) @@ -48,9 +55,16 @@ public Mono requestCode(AuthorizationCodeRequest requ request.getParameter(OAuth2Constants.scope).map(String::valueOf).ifPresent(codeCache::setScope); codeCache.setCode(code); codeCache.setClientId(client.getClientId()); + ScopePredicate permissionPredicate = OAuth2ScopeUtils.createScopePredicate(codeCache.getScope()); - codeCache.setAuthentication(authentication.copy((permission, action) -> permissionPredicate.test(permission.getId(), action), dimension -> true)); + Authentication copy = authentication.copy( + (permission, action) -> permissionPredicate.test(permission.getId(), action), + dimension -> permissionPredicate.test(dimension.getType().getId(), dimension.getId())); + + copy.setAttribute("scope", codeCache.getScope()); + + codeCache.setAuthentication(copy); return redis @@ -72,16 +86,27 @@ public Mono requestToken(AuthorizationCodeTokenRequest request) { .map(this::getRedisKey) .flatMap(redis.opsForValue()::get) .switchIfEmpty(Mono.error(() -> new OAuth2Exception(ErrorType.ILLEGAL_CODE))) - .flatMap(cache -> redis - .opsForValue() - .delete(getRedisKey(cache.getCode())) - .thenReturn(cache)) + //移除code + .flatMap(cache -> redis.opsForValue().delete(getRedisKey(cache.getCode())).thenReturn(cache)) .flatMap(cache -> { if (!request.getClient().getClientId().equals(cache.getClientId())) { return Mono.error(new OAuth2Exception(ErrorType.ILLEGAL_CLIENT_ID)); } - return accessTokenManager.createAccessToken(cache.getClientId(), cache.getAuthentication(), false); - }); + return accessTokenManager + .createAccessToken(cache.getClientId(), cache.getAuthentication(), false) + .flatMap(token -> new OAuth2GrantedEvent(request.getClient(), + token, + cache.getAuthentication(), + cache.getScope(), + GrantType.authorization_code, + request.getParameters()) + .publish(eventPublisher) + .onErrorResume(err -> accessTokenManager + .removeToken(cache.getClientId(), token.getAccessToken()) + .then(Mono.error(err))) + .thenReturn(token)); + }) + ; } } diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/credential/DefaultClientCredentialGranter.java b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/credential/DefaultClientCredentialGranter.java index 74155bc44..08a78cf29 100644 --- a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/credential/DefaultClientCredentialGranter.java +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/credential/DefaultClientCredentialGranter.java @@ -2,9 +2,12 @@ import lombok.AllArgsConstructor; import org.hswebframework.web.authorization.ReactiveAuthenticationManager; +import org.hswebframework.web.oauth2.GrantType; import org.hswebframework.web.oauth2.server.AccessToken; import org.hswebframework.web.oauth2.server.AccessTokenManager; import org.hswebframework.web.oauth2.server.OAuth2Client; +import org.hswebframework.web.oauth2.server.event.OAuth2GrantedEvent; +import org.springframework.context.ApplicationEventPublisher; import reactor.core.publisher.Mono; @AllArgsConstructor @@ -14,6 +17,8 @@ public class DefaultClientCredentialGranter implements ClientCredentialGranter { private final AccessTokenManager accessTokenManager; + private final ApplicationEventPublisher eventPublisher; + @Override public Mono requestToken(ClientCredentialRequest request) { @@ -21,6 +26,19 @@ public Mono requestToken(ClientCredentialRequest request) { return authenticationManager .getByUserId(client.getUserId()) - .flatMap(auth -> accessTokenManager.createAccessToken(client.getClientId(), auth, true)); + .flatMap(auth -> accessTokenManager + .createAccessToken(client.getClientId(), auth, true) + .flatMap(token -> new OAuth2GrantedEvent(client, + token, + auth, + "*", + GrantType.client_credentials, + request.getParameters()) + .publish(eventPublisher) + .onErrorResume(err -> accessTokenManager + .removeToken(client.getClientId(), token.getAccessToken()) + .then(Mono.error(err))) + .thenReturn(token)) + ); } } diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/event/OAuth2GrantedEvent.java b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/event/OAuth2GrantedEvent.java new file mode 100644 index 000000000..38b341dcb --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/event/OAuth2GrantedEvent.java @@ -0,0 +1,32 @@ +package org.hswebframework.web.oauth2.server.event; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.hswebframework.web.authorization.Authentication; +import org.hswebframework.web.event.DefaultAsyncEvent; +import org.hswebframework.web.oauth2.server.AccessToken; +import org.hswebframework.web.oauth2.server.OAuth2Client; + +import java.util.Map; + +/** + * OAuth2授权成功事件 + * + * @author zhouhao + * @since 4.0.15 + */ +@Getter +@AllArgsConstructor +public class OAuth2GrantedEvent extends DefaultAsyncEvent { + private final OAuth2Client client; + + private final AccessToken accessToken; + + private final Authentication authentication; + + private final String scope; + + private final String grantType; + + private final Map parameters; +} 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 e6a0a0ac6..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 @@ -4,6 +4,9 @@ import lombok.Setter; import org.apache.commons.codec.digest.DigestUtils; import org.hswebframework.web.authorization.Authentication; +import org.hswebframework.web.authorization.token.AuthenticationUserToken; +import org.hswebframework.web.authorization.token.UserTokenManager; +import org.hswebframework.web.authorization.token.redis.RedisUserTokenManager; import org.hswebframework.web.oauth2.ErrorType; import org.hswebframework.web.oauth2.OAuth2Exception; import org.hswebframework.web.oauth2.server.AccessToken; @@ -13,6 +16,7 @@ import org.springframework.data.redis.core.ReactiveRedisTemplate; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.RedisSerializer; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.time.Duration; @@ -22,6 +26,8 @@ public class RedisAccessTokenManager implements AccessTokenManager { private final ReactiveRedisOperations tokenRedis; + private final UserTokenManager userTokenManager; + @Getter @Setter private int tokenExpireIn = 7200;//2小时 @@ -30,37 +36,49 @@ public class RedisAccessTokenManager implements AccessTokenManager { @Setter private int refreshExpireIn = 2592000; //30天 - public RedisAccessTokenManager(ReactiveRedisOperations tokenRedis) { + public RedisAccessTokenManager(ReactiveRedisOperations tokenRedis, + UserTokenManager userTokenManager) { this.tokenRedis = tokenRedis; + this.userTokenManager = userTokenManager; } @SuppressWarnings("all") public RedisAccessTokenManager(ReactiveRedisConnectionFactory connectionFactory) { - this(new ReactiveRedisTemplate<>(connectionFactory, RedisSerializationContext + ReactiveRedisTemplate redis = new ReactiveRedisTemplate<>(connectionFactory, RedisSerializationContext .newSerializationContext() .key((RedisSerializer) RedisSerializer.string()) .value(RedisSerializer.java()) .hashKey(RedisSerializer.string()) .hashValue(RedisSerializer.java()) - .build() - )); + .build()); + this.tokenRedis = redis; + this.userTokenManager = new RedisUserTokenManager(redis); } + @Override public Mono getAuthenticationByToken(String accessToken) { + return userTokenManager + .getByToken(accessToken) + .filter(token -> token instanceof AuthenticationUserToken) + .map(t -> ((AuthenticationUserToken) t).getAuthentication()); + } - return tokenRedis - .opsForValue() - .get(createTokenRedisKey(accessToken)) - .map(RedisAccessToken::getAuthentication); + private String createTokenRedisKey(String clientId, String token) { + return "oauth2-token:" + clientId + ":" + token; } - private String createTokenRedisKey(String token) { - return "oauth2-token:" + token; + private String createUserTokenRedisKey(RedisAccessToken token) { + return createUserTokenRedisKey(token.getClientId(), token.getAuthentication().getUser().getId()); } - private String createRefreshTokenRedisKey(String token) { - return "oauth2-refresh-token:" + token; + private String createUserTokenRedisKey(String clientId, String userId) { + return "oauth2-user-tokens:" + clientId + ":" + userId; + } + + + private String createRefreshTokenRedisKey(String clientId, String token) { + return "oauth2-refresh-token:" + clientId + ":" + token; } private String createSingletonTokenRedisKey(String clientId) { @@ -75,20 +93,51 @@ private Mono doCreateAccessToken(String clientId, Authenticati return storeToken(accessToken).thenReturn(accessToken); } + private Mono storeAuthToken(RedisAccessToken token) { + //保存独立的权限信息,通常是用户指定了特定的授权范围时生效. + if (token.storeAuth()) { + return userTokenManager + .signIn(token.getAccessToken(), + createTokenType(token.getClientId()), + token.getAuthentication().getUser().getId(), + tokenExpireIn * 1000L, + token.getAuthentication()) + .then(); + + } else { + return userTokenManager + .signIn(token.getAccessToken(), + createTokenType(token.getClientId()), + token.getAuthentication().getUser().getId(), + tokenExpireIn * 1000L) + .then(); + } + } + private Mono storeToken(RedisAccessToken token) { - return Mono - .zip( - tokenRedis.opsForValue().set(createTokenRedisKey(token.getAccessToken()), token, Duration.ofSeconds(tokenExpireIn)), - tokenRedis.opsForValue().set(createRefreshTokenRedisKey(token.getRefreshToken()), token, Duration.ofSeconds(refreshExpireIn)) - ).then(); + + return Flux + .merge(storeAuthToken(token), + tokenRedis + .opsForValue() + .set(createUserTokenRedisKey(token), token, Duration.ofSeconds(tokenExpireIn)), + tokenRedis + .opsForValue() + .set(createTokenRedisKey(token.getClientId(), + token.getAccessToken()), token, Duration.ofSeconds(tokenExpireIn)), + tokenRedis + .opsForValue() + .set(createRefreshTokenRedisKey(token.getClientId(), + token.getRefreshToken()), token, Duration.ofSeconds(refreshExpireIn))) + .then(); } 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)))) @@ -111,7 +160,7 @@ public Mono createAccessToken(String clientId, @Override public Mono refreshAccessToken(String clientId, String refreshToken) { - String redisKey = createRefreshTokenRedisKey(refreshToken); + String redisKey = createRefreshTokenRedisKey(clientId, refreshToken); return tokenRedis .opsForValue() @@ -129,10 +178,15 @@ public Mono refreshAccessToken(String clientId, String refreshToken .as(result -> { // 单例token if (token.isSingleton()) { - return tokenRedis - .opsForValue() - .set(createSingletonTokenRedisKey(clientId), token, Duration.ofSeconds(tokenExpireIn)) - .then(result); + return userTokenManager + .signOutByToken(token.getAccessToken()) + .then( + tokenRedis + .opsForValue() + .set(createSingletonTokenRedisKey(clientId), token, Duration.ofSeconds(tokenExpireIn)) + .then(result) + ) + ; } return result; }) @@ -140,4 +194,58 @@ public Mono refreshAccessToken(String clientId, String refreshToken }); } + + @Override + public Mono removeToken(String clientId, String token) { + + return Flux + .merge(userTokenManager.signOutByToken(token), + tokenRedis.delete(createSingletonTokenRedisKey(clientId)), + tokenRedis.delete(createTokenRedisKey(clientId, token))) + .then(); + } + + @Override + public Mono cancelGrant(String clientId, String userId) { + //删除最新的refresh_token + Mono removeRefreshToken = tokenRedis + .opsForValue() + .get(createUserTokenRedisKey(clientId, userId)) + .flatMap(t -> tokenRedis + .opsForValue() + .delete(createRefreshTokenRedisKey(t.getClientId(), t.getRefreshToken()))) + .then(); + + //删除access_token + Mono removeAccessToken = userTokenManager + .getByUserId(userId) + .flatMap(token -> { + //其他类型的token 忽略 + if (!(createTokenType(clientId)).equals(token.getType())) { + return Mono.empty(); + } + return tokenRedis + .opsForValue() + .get(createTokenRedisKey(clientId, token.getToken())) + .flatMap(t -> { + //移除token + return tokenRedis + .delete(createTokenRedisKey(t.getClientId(), t.getAccessToken())) + //移除token对应的refresh_token + .then(tokenRedis + .opsForValue() + .delete(createRefreshTokenRedisKey(t.getClientId(), t.getRefreshToken()))); + }) + .then(userTokenManager.signOutByToken(token.getToken())); + }) + .then(); + + return Flux + .merge(removeRefreshToken, removeAccessToken) + .then(); + } + + private String createTokenType(String clientId) { + return "oauth2-" + clientId; + } } diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/utils/OAuth2ScopeUtils.java b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/utils/OAuth2ScopeUtils.java index 4ac30fff0..40806f974 100644 --- a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/utils/OAuth2ScopeUtils.java +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/utils/OAuth2ScopeUtils.java @@ -6,6 +6,10 @@ import java.util.*; /** + *
{@code
+ *   role:* user:* device-manager:*
+ * }
+ * * @author zhouhao * @since 4.0.8 */ @@ -23,10 +27,13 @@ public static ScopePredicate createScopePredicate(String scopeStr) { Set acts = actions.computeIfAbsent(per, k -> new HashSet<>()); acts.addAll(Arrays.asList(permissions).subList(1, permissions.length)); } - + //全部授权 + if (actions.containsKey("*")) { + return ((permission, action) -> true); + } return ((permission, action) -> Optional .ofNullable(actions.get(permission)) - .map(acts -> action.length == 0 || acts.containsAll(Arrays.asList(action))) + .map(acts -> action.length == 0 || acts.contains("*") || acts.containsAll(Arrays.asList(action))) .orElse(false)); } } diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/web/OAuth2AuthorizeController.java b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/web/OAuth2AuthorizeController.java index b71ee6468..123ab20f9 100644 --- a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/web/OAuth2AuthorizeController.java +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/web/OAuth2AuthorizeController.java @@ -19,7 +19,9 @@ import org.hswebframework.web.oauth2.server.code.AuthorizationCodeRequest; import org.hswebframework.web.oauth2.server.code.AuthorizationCodeTokenRequest; import org.hswebframework.web.oauth2.server.credential.ClientCredentialRequest; +import org.hswebframework.web.oauth2.server.event.OAuth2GrantedEvent; import org.hswebframework.web.oauth2.server.refresh.RefreshTokenRequest; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; 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 4f9fdaacc..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,34 +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), 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 ee5932301..44cfa8aef 100644 --- a/hsweb-authorization/pom.xml +++ b/hsweb-authorization/pom.xml @@ -5,10 +5,11 @@ hsweb-framework org.hswebframework.web - 4.0.12-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 2dfc46220..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.12-SNAPSHOT + 4.0.20-SNAPSHOT 4.0.0 hsweb-commons-api + ${artifactId} @@ -41,14 +42,19 @@ com.google.code.findbugs jsr305 - 3.0.2 compile + commons-codec 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/Entity.java b/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/Entity.java index 52d100b81..b49ce9df9 100644 --- a/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/Entity.java +++ b/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/Entity.java @@ -19,6 +19,7 @@ package org.hswebframework.web.api.crud.entity; +import org.hswebframework.ezorm.core.StaticMethodReferenceColumn; import org.hswebframework.web.bean.FastBeanCopier; import org.hswebframework.web.validator.ValidatorUtils; @@ -32,18 +33,68 @@ */ public interface Entity extends Serializable { + /** + * 使用jsr303对当前实体类进行验证,如果未通过验证则会抛出{@link org.hswebframework.web.exception.ValidationException}异常 + * + * @param groups 分组 + * @see org.hswebframework.web.exception.ValidationException + */ default void tryValidate(Class... groups) { ValidatorUtils.tryValidate(this, groups); } + /** + * 使用jsr303对当前实体类的指定属性进行验证,如果未通过验证则会抛出{@link org.hswebframework.web.exception.ValidationException}异常 + * + * @param groups 分组 + * @see org.hswebframework.web.exception.ValidationException + */ + default void tryValidate(String property, Class... groups) { + ValidatorUtils.tryValidate(this, property, groups); + } + + /** + * 使用jsr303对当前实体类的指定属性进行验证,如果未通过验证则会抛出{@link org.hswebframework.web.exception.ValidationException}异常 + * + * @param groups 分组 + * @see org.hswebframework.web.exception.ValidationException + */ + default void tryValidate(StaticMethodReferenceColumn property, Class... groups) { + tryValidate(property.getColumn(), groups); + } + + /** + * 将当前实体类复制到指定其他类型中,类型将会被自动实例化,在类型明确时,建议使用{@link Entity#copyFrom(Object, String...)}. + * + * @param target 目标类型 + * @param ignoreProperties 忽略复制的属性 + * @param 类型 + * @return 复制结果 + */ default T copyTo(Class target, String... ignoreProperties) { return FastBeanCopier.copy(this, target, ignoreProperties); } + /** + * 将当前实体类复制到其他对象中 + * + * @param target 目标实体 + * @param ignoreProperties 忽略复制的属性 + * @param 类型 + * @return 复制结果 + */ default T copyTo(T target, String... ignoreProperties) { return FastBeanCopier.copy(this, target, ignoreProperties); } + /** + * 从其他对象复制属性到当前对象 + * + * @param target 其他对象 + * @param ignoreProperties 忽略复制的属性 + * @param 类型 + * @return 当前对象 + */ @SuppressWarnings("all") default T copyFrom(Object target, String... ignoreProperties) { return (T) FastBeanCopier.copy(target, this, ignoreProperties); diff --git a/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/EntityFactory.java b/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/EntityFactory.java index 5f0947795..2eea1163f 100644 --- a/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/EntityFactory.java +++ b/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/EntityFactory.java @@ -19,7 +19,7 @@ package org.hswebframework.web.api.crud.entity; -import javax.annotation.Nullable; +import java.util.function.Supplier; /** * 实体工厂接口,系统各个地方使用此接口来创建实体,在实际编码中也应该使用此接口来创建实体,而不是使用new方式来创建 @@ -58,6 +58,21 @@ public interface EntityFactory { */ T newInstance(Class entityClass, Class defaultClass); + /** + * 根据类型创建实例,如果类型无法创建,则使用默认类型进行创建 + *

+ * e.g. + *

+     *  entityFactory.newInstance(UserEntity.class,SimpleUserEntity::new);
+     * 
+ * + * @param entityClass 要创建的class + * @param defaultFactory 默认实体创建工厂 + * @param 类型 + * @return 实例 + */ + T newInstance(Class entityClass, Supplier defaultFactory); + /** * 创建实体并设置默认的属性 * @@ -68,6 +83,7 @@ public interface EntityFactory { * @return 创建结果 * @see EntityFactory#copyProperties(Object, Object) */ + @Deprecated default T newInstance(Class entityClass, S defaultProperties) { return copyProperties(defaultProperties, newInstance(entityClass)); } @@ -83,6 +99,7 @@ default T newInstance(Class entityClass, S defaultProperties) { * @return 创建结果 * @see EntityFactory#copyProperties(Object, Object) */ + @Deprecated default T newInstance(Class entityClass, Class defaultClass, S defaultProperties) { return copyProperties(defaultProperties, newInstance(entityClass, defaultClass)); } @@ -103,7 +120,6 @@ default Class getInstanceType(Class entityClass) { return getInstanceType(entityClass, false); } - @Nullable Class getInstanceType(Class entityClass, boolean autoRegister); /** @@ -115,5 +131,6 @@ default Class getInstanceType(Class entityClass) { * @param 被拷贝对象的类型 * @return 被拷贝的对象 */ + @Deprecated T copyProperties(S source, T target); } 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 4d893d4ca..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,10 +19,18 @@ 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) { - return FACTORY.newInstance(type); + return FACTORY.newInstance(type,mapper); } return mapper.get(); } 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 8dc3d6ceb..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,16 +1,24 @@ 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 { @Bean public ApplicationContextAware entityFactoryHolder() { - return context -> EntityFactoryHolder.FACTORY = context.getBean(EntityFactory.class); + return context -> { + try { + EntityFactoryHolder.FACTORY = context.getBean(EntityFactory.class); + } catch (BeansException ignore) { + + } + }; } } 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/GenericTreeSortSupportEntity.java b/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/GenericTreeSortSupportEntity.java index 6a37ebf61..8a79dbc5a 100644 --- a/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/GenericTreeSortSupportEntity.java +++ b/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/GenericTreeSortSupportEntity.java @@ -37,7 +37,7 @@ public abstract class GenericTreeSortSupportEntity extends GenericEntity /** * 父级类别 */ - @Column(name = "parent_id", length = 32) + @Column(name = "parent_id", length = 64) @Comment("父级ID") @Schema(description = "父节点ID") private PK parentId; 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 77d473772..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,19 +24,47 @@ 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 + * @since 4.0.0 + */ @Getter @Setter -public class PagerResult { +public class PagerResult implements Serializable { private static final long serialVersionUID = -6171751136953308027L; + /** + * 创建一个空结果 + * + * @param 结果类型 + * @return PagerResult + */ public static PagerResult empty() { return of(0, new ArrayList<>()); } + /** + * 创建一个分页结果 + * + * @param total 总数据量 + * @param list 当前页数据列表 + * @param 结果类型 + * @return PagerResult + */ @SuppressWarnings("all") public static PagerResult of(int total, List list) { PagerResult result; @@ -46,6 +74,15 @@ public static PagerResult of(int total, List list) { return result; } + /** + * 创建一个分页结果,并将查询参数中的分页索引等信息填充到分页结果中 + * + * @param total 总数据量 + * @param list 当前页数据列表 + * @param entity 查询参数 + * @param 结果类型 + * @return PagerResult + */ public static PagerResult of(int total, List list, QueryParam entity) { PagerResult pagerResult = of(total, list); pagerResult.setPageIndex(entity.getThinkPageIndex()); @@ -72,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/QueryNoPagingOperation.java b/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/QueryNoPagingOperation.java index cdf1ac6b7..f24aafb69 100644 --- a/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/QueryNoPagingOperation.java +++ b/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/QueryNoPagingOperation.java @@ -22,6 +22,24 @@ import static java.lang.annotation.ElementType.ANNOTATION_TYPE; import static java.lang.annotation.ElementType.METHOD; +/** + * 使用注解继承来对swagger接口文档注解的拓展,用来标识接口不支持分页查询参数. + * + * + *

{@code
+ * @GetMapping
+ * @QueryNoPagingOperation(summary="接口说明")
+ * public Flux handleRequest(@Parameter(hidden = true) QueryParamEntity query){
+ *  return service.query(query);
+ * }
+ *
+ * }
+ * + * 注意在参数上注解 {@code @Parameter(hidden=true)} + * @author zhouhao + * @since 4.0.5 + * @see QueryNoPagingOperation#parameters() + */ @Target({METHOD, ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited diff --git a/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/QueryOperation.java b/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/QueryOperation.java index 9552e72f5..3dd75ab47 100644 --- a/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/QueryOperation.java +++ b/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/QueryOperation.java @@ -22,6 +22,22 @@ import static java.lang.annotation.ElementType.ANNOTATION_TYPE; import static java.lang.annotation.ElementType.METHOD; +/** + * 使用注解继承来对swagger接口文档注解的拓展,用来标识接口支持分页查询参数. + * + *
{@code
+ * @GetMapping
+ * @QueryOperation(summary="接口说明")
+ * public Flux handleRequest(@Parameter(hidden = true) QueryParamEntity query){
+ *  return service.query(query);
+ * }
+ *
+ * }
+ * + * @author zhouhao + * @see QueryOperation#parameters() + * @since 4.0.5 + */ @Target({METHOD, ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited @@ -97,7 +113,7 @@ Parameter[] parameters() default { @Parameter(name = "where", description = "条件表达式,和terms参数冲突", example = "id = 1", schema = @Schema(implementation = String.class), in = ParameterIn.QUERY), @Parameter(name = "orderBy", description = "排序表达式,和sorts参数冲突", example = "id desc", schema = @Schema(implementation = String.class), in = ParameterIn.QUERY), @Parameter(name = "includes", description = "指定要查询的列,多列使用逗号分隔", example = "id", schema = @Schema(implementation = String.class), in = ParameterIn.QUERY), - @Parameter(name = "excludes", description = "指定不查询的列,多列使用逗号分隔", schema = @Schema(implementation = String.class), in = ParameterIn.QUERY), + @Parameter(name = "excludes", description = "指定不查询的列,多列使用逗号分隔", schema = @Schema(implementation = String.class), in = ParameterIn.QUERY), @Parameter(name = "terms[0].column", description = "指定条件字段", schema = @Schema(implementation = String.class), in = ParameterIn.QUERY), @Parameter(name = "terms[0].termType", description = "条件类型", schema = @Schema(implementation = String.class), example = "like", in = ParameterIn.QUERY), @Parameter(name = "terms[0].type", description = "多个条件组合方式", schema = @Schema(implementation = Term.Type.class), in = ParameterIn.QUERY), 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 b02fecca2..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,23 +1,25 @@ 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; import org.hswebframework.ezorm.core.param.QueryParam; import org.hswebframework.ezorm.core.param.Term; import org.hswebframework.ezorm.core.param.TermType; +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; @@ -25,34 +27,49 @@ * 查询参数实体,使用easyorm进行动态查询参数构建
* 可通过静态方法创建:
* 如: - * - * QueryParamEntity.of("id",id); - * + *
+ * {@code
+ *      QueryParamEntity.of("id",id);
+ * }
+ * 
+ *

+ * 或者使用DSL方式来构造: + *

{@code
+ *  QueryParamEntity
+ *  .newQuery()
+ *  .where("id",1)
+ *  .execute(service::query)
+ * }
* * @author zhouhao * @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; @@ -77,16 +94,32 @@ public int getPageIndexTmp() { @Override @Schema(description = "指定要查询的列") + @Nonnull public Set getIncludes() { return super.getIncludes(); } @Override @Schema(description = "指定不查询的列") + @Nonnull public Set getExcludes() { return super.getExcludes(); } + /** + * 基于另外一个条件参数来创建查询条件实体 + * + * @param param 参数 + * @return 新的查询条件 + * @since 4.0.14 + */ + public static QueryParamEntity of(Param param) { + if (param instanceof QueryParamEntity) { + return ((QueryParamEntity) param).clone(); + } + return FastBeanCopier.copy(param, new QueryParamEntity()); + } + /** * 创建一个空的查询参数实体,该实体无任何参数. * @@ -173,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; } - public QueryParamEntity doNotSort(){ + public QueryParamEntity doNotSort() { this.setSorts(new ArrayList<>()); return this; } diff --git a/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/RecordCreationEntity.java b/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/RecordCreationEntity.java index 930e841f6..69c9d7272 100644 --- a/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/RecordCreationEntity.java +++ b/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/RecordCreationEntity.java @@ -12,23 +12,55 @@ */ public interface RecordCreationEntity extends Entity { + /** + * @return 创建者ID + */ String getCreatorId(); + /** + * 设置创建者ID + * + * @param creatorId 创建者ID + */ void setCreatorId(String creatorId); + /** + * 创建时间,UTC时间戳 + * + * @return 创建时间 + * @see System#currentTimeMillis() + */ Long getCreateTime(); + /** + * 设置创建时间 ,UTC时间戳 + * + * @param createTime 创建时间 + * @see System#currentTimeMillis() + */ void setCreateTime(Long createTime); + /** + * 设置创建者名字,为了兼容,默认不支持记录创建者名字,由具体的实现类进行实现 + * + * @param name 创建者名字 + */ default void setCreatorName(String name) { } + /** + * 设置创建时间为当前时间 + */ default void setCreateTimeNow() { setCreateTime(System.currentTimeMillis()); } + /** + * @deprecated 已弃用, 在4.1版本中移除 + */ @JsonIgnore + @Deprecated default String getCreatorIdProperty() { return "creatorId"; } 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 e43c41801..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; /** * 记录修改信息的实体类,包括修改人和修改时间。 @@ -13,24 +15,76 @@ public interface RecordModifierEntity extends Entity { String modifierId = "modifierId"; String modifyTime = "modifyTime"; + /** + * 修改人ID + * + * @return 修改人ID + */ String getModifierId(); + /** + * 设置修改人ID + * + * @param modifierId 修改人ID + */ void setModifierId(String modifierId); + /** + * 设置修改人名字,为了兼容,默认不支持记录修改人名字,由具体的实现类进行实现 + * + * @param modifierName 修改人名字 + */ default void setModifierName(String modifierName) { } + /** + * @return 修改时间 + */ Long getModifyTime(); + /** + * 设置修改时间,UTC时间戳 + * + * @param modifyTime 修改时间 + * @see System#currentTimeMillis() + */ void setModifyTime(Long modifyTime); + /** + * 设置修改时间为当前时间 + */ default void setModifyTimeNow() { setModifyTime(System.currentTimeMillis()); } + /** + * @deprecated 已弃用, 4.1版本中移除 + */ @JsonIgnore 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/SortSupportEntity.java b/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/SortSupportEntity.java index 64dd3862e..657946c8e 100644 --- a/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/SortSupportEntity.java +++ b/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/SortSupportEntity.java @@ -18,18 +18,30 @@ package org.hswebframework.web.api.crud.entity; +import javax.annotation.Nonnull; + +/** + * 支持排序的实体 + * + * @author zhouhao + * @since 4.0.0 + */ public interface SortSupportEntity extends Comparable, Entity { + /** + * @return 排序序号 + */ Long getSortIndex(); + /** + * 设置排序序号 + * + * @param sortIndex 排序序号 + */ void setSortIndex(Long sortIndex); @Override - default int compareTo(SortSupportEntity support) { - if (support == null) { - return -1; - } - + default int compareTo(@Nonnull SortSupportEntity support) { return Long.compare(getSortIndex() == null ? 0 : getSortIndex(), support.getSortIndex() == null ? 0 : support.getSortIndex()); } } diff --git a/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/TermExpressionParser.java b/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/TermExpressionParser.java index af76310f1..3bbe57073 100644 --- a/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/TermExpressionParser.java +++ b/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/TermExpressionParser.java @@ -1,17 +1,15 @@ package org.hswebframework.web.api.crud.entity; import lombok.SneakyThrows; -import org.apache.commons.codec.net.URLCodec; +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.Sort; import org.hswebframework.ezorm.core.param.Term; import org.hswebframework.ezorm.core.param.TermType; -import org.hswebframework.web.api.crud.entity.QueryParamEntity; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; +import java.net.URLDecoder; +import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -24,12 +22,70 @@ */ public class TermExpressionParser { - static final URLCodec urlCodec = new URLCodec(); + /** + * 解析Map为动态条件,map中的key为条件列,value为条件值,如果列以$or$开头则表示or查询. + * + *
{@code
+     *   {
+     *       "name$like":"测试",
+     *       //OR
+     *       "$or$status$in":[1,2,3],
+     *       //嵌套
+     *       "$nest":{
+     *           "age$gt":10,
+     *       }
+     *   }
+     * }
+ * + * @param map map + * @return 条件 + */ + public static List parse(Map map) { + if (MapUtils.isEmpty(map)) { + return Collections.emptyList(); + } + + List terms = new ArrayList<>(map.size()); + for (Map.Entry entry : map.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + boolean isOr = false; + Term term = new Term(); + + //嵌套 + if (key.startsWith("$nest") || + (isOr = key.startsWith("$orNest"))) { + @SuppressWarnings("all") + List nest = value instanceof Map ? parse(((Map) value)) : parse(String.valueOf(value)); + term.setTerms(nest); + } + //普通 + else { + if (key.startsWith("$or$")) { + isOr = true; + key = key.substring(4); + } + term.setColumn(key); + term.setValue(value); + } + + if (isOr) { + term.setType(Term.Type.or); + } + terms.add(term); + } + + return terms; + + } @SneakyThrows public static List parse(String expression) { - expression = urlCodec.decode(expression); + try { + expression = URLDecoder.decode(expression, "utf-8"); + } catch (Throwable ignore) { + } Query conditional = QueryParamEntity.newQuery(); NestConditional nest = null; 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 3f35ddb6c..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 @@ -29,25 +29,82 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +/** + * 支持树结构的实体类 + * + * @param 主键类型 + * @author zhouhao + * @since 4.0 + */ @SuppressWarnings("all") public interface TreeSupportEntity extends Entity { + /** + * 获取主键 + * + * @return ID + */ PK getId(); + /** + * 设置主键 + * + * @param id ID + */ void setId(PK id); + /** + * 获取树路径,树路径表示当前节点所在位置 + * 格式通常为: aBcD-EfgH-iJkl,以-分割,一个分割表示一级. + * 比如: aBcD-EfgH-iJkl表示 当前节点在第三级,上一个节点为EfgH. + * + * @return 树路径 + */ String getPath(); + /** + * 设置路径,此值通常不需要手动设置,在进行保存时,由service自动进行分配. + * + * @param path 路径 + * @see TreeSupportEntity#expandTree2List(TreeSupportEntity, IDGenerator) + */ void setPath(String path); + /** + * 获取上级ID + * + * @return 上级ID + */ PK getParentId(); + /** + * 设置上级节点ID + * + * @param parentId + */ void setParentId(PK parentId); + /** + * 获取节点层级 + * + * @return 节点层级 + */ Integer getLevel(); + /** + * 设置节点层级 + * + * @return 节点层级 + */ void setLevel(Integer level); + /** + * 获取所有子节点,默认情况下此字段只会返回null.可以使用{@link TreeSupportEntity#list2tree(Collection, BiConsumer)}将 + * 列表结构转为树形结构 + * + * @param 当前实体类型 + * @return 自己节点 + */ > List getChildren(); @Override @@ -127,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())) { @@ -167,7 +222,9 @@ static , PK> void expandTree2List(T root, List, PK> List list2tree(Collection dat static , PK> List list2tree(final Collection dataList, final BiConsumer> childConsumer, final Function, Predicate> predicateFunction) { - 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"); - - Supplier> streamSupplier = () -> dataList.stream(); - // id,node - Map cache = new HashMap<>(); - // parentId,children - Map> treeCache = streamSupplier.get() - .peek(node -> cache.put(node.getId(), node)) - .filter(e -> e.getParentId() != null) - .collect(Collectors.groupingBy(TreeSupportEntity::getParentId)); - - Predicate rootNodePredicate = predicateFunction.apply(new TreeHelper() { - @Override - public List getChildren(PK parentId) { - return treeCache.get(parentId); - } - - @Override - public N getNode(PK id) { - return cache.get(id); - } - }); - - return streamSupplier.get() - //设置每个节点的子节点 - .peek(node -> childConsumer.accept(node, treeCache.get(node.getId()))) - //获取根节点 - .filter(rootNodePredicate) - .collect(Collectors.toList()); + return TreeUtils.list2tree(dataList, + TreeSupportEntity::getId, + TreeSupportEntity::getParentId, + childConsumer, + (helper, node) -> predicateFunction.apply(helper).test(node)); } /** 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 new file mode 100644 index 000000000..2c3e7f123 --- /dev/null +++ b/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/TreeUtils.java @@ -0,0 +1,143 @@ +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.*; +import java.util.function.*; +import java.util.stream.Collectors; + +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); + } + + } + + /** + * 列表结构转为树结构,并返回根节点集合. + *

+ * 根节点判断逻辑: parentId为空或者对应的节点数据没有在list中 + * + * @param dataList 数据集合 + * @param childConsumer 子节点消费接口,用于设置子节点 + * @param 元素类型 + * @param 主键类型 + * @return 根节点集合 + */ + public static List list2tree(Collection dataList, + Function idGetter, + Function parentIdGetter, + BiConsumer> childConsumer) { + return list2tree(dataList, + idGetter, + parentIdGetter, + childConsumer, + (helper, node) -> { + PK parentId = parentIdGetter.apply(node); + return ObjectUtils.isEmpty(parentId) + || helper.getNode(parentId) == null; + }); + } + + /** + * 列表结构转为树结构,并返回根节点集合 + * + * @param dataList 数据集合 + * @param childConsumer 子节点消费接口,用于设置子节点 + * @param rootPredicate 根节点判断函数,传入helper,获取一个判断是否为根节点的函数 + * @param 元素类型 + * @param 主键类型 + * @return 根节点集合 + */ + public static List list2tree(Collection dataList, + Function idGetter, + Function parentIdGetter, + BiConsumer> childConsumer, + BiPredicate, N> rootPredicate) { + Objects.requireNonNull(dataList, "source list can not be null"); + Objects.requireNonNull(childConsumer, "child consumer 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.newLinkedHashMapWithExpectedSize(size); + // parentId,children + Map> treeCache = dataList + .stream() + .peek(node -> cache.put(idGetter.apply(node), node)) + .filter(e -> parentIdGetter.apply(e) != null) + .collect(Collectors.groupingBy(parentIdGetter)); + + TreeSupportEntity.TreeHelper helper = new TreeSupportEntity.TreeHelper() { + @Override + public List getChildren(PK parentId) { + return treeCache.get(parentId); + } + + @Override + public N getNode(PK id) { + return cache.get(id); + } + }; + + 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/TermExpressionParserTest.java b/hsweb-commons/hsweb-commons-api/src/test/java/org/hswebframework/web/api/crud/entity/TermExpressionParserTest.java index a04ea1cd0..8d7eb276c 100644 --- a/hsweb-commons/hsweb-commons-api/src/test/java/org/hswebframework/web/api/crud/entity/TermExpressionParserTest.java +++ b/hsweb-commons/hsweb-commons-api/src/test/java/org/hswebframework/web/api/crud/entity/TermExpressionParserTest.java @@ -4,7 +4,9 @@ import org.hswebframework.ezorm.core.param.TermType; import org.junit.Test; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import static org.junit.Assert.*; @@ -23,6 +25,53 @@ public void testUrl(){ assertEquals(terms.get(1).getValue(), "test"); } + + @Test + public void testChinese() { + { + List terms = TermExpressionParser.parse("name = 我"); + + assertEquals(terms.get(0).getTermType(), TermType.eq); + assertEquals(terms.get(0).getValue(),"我"); + + } + + { + List terms = TermExpressionParser.parse("name like %我%"); + + assertEquals(terms.get(0).getTermType(), TermType.like); + assertEquals(terms.get(0).getValue(),"%我%"); + + } + } + @Test + public void testMap(){ + Map map = new LinkedHashMap<>(); + map.put("name$like","我"); + + map.put("$or$name","你"); + + map.put("$nest","age = 10"); + + + List terms = TermExpressionParser.parse(map); + + assertEquals(3,terms.size()); + assertEquals("like",terms.get(0).getTermType()); + assertEquals("name",terms.get(0).getColumn()); + assertEquals("我",terms.get(0).getValue()); + + assertEquals(Term.Type.or,terms.get(1).getType()); + assertEquals("name",terms.get(1).getColumn()); + assertEquals("你",terms.get(1).getValue()); + + assertEquals(1,terms.get(2).getTerms().size()); + + assertEquals("age",terms.get(2).getTerms().get(0).getColumn()); + + } + + @Test public void test() { { 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 70a5d4d87..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.12-SNAPSHOT + 4.0.20-SNAPSHOT 4.0.0 hsweb-commons-crud + ${artifactId} @@ -143,6 +144,12 @@ spring-webmvc true + + + com.github.jsqlparser + jsqlparser + 4.6 + \ No newline at end of file diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/annotation/DDL.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/annotation/DDL.java new file mode 100644 index 000000000..e3231652a --- /dev/null +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/annotation/DDL.java @@ -0,0 +1,13 @@ +package org.hswebframework.web.crud.annotation; + +import java.lang.annotation.*; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface DDL { + + boolean value() default true; + +} diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/annotation/EnableEasyormRepository.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/annotation/EnableEasyormRepository.java index c7c9a60f8..5e2e820e8 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/annotation/EnableEasyormRepository.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/annotation/EnableEasyormRepository.java @@ -7,6 +7,13 @@ import java.lang.annotation.*; /** + * 在启动类上注解,标识开启自动注册实体通用增删改查接口到spring上下文中. + * 在spring中,可直接进行泛型注入使用: + *

{@code
+ *   @Autowire
+ *   ReactiveRepository repository;
+ * }
+ * * @see org.hswebframework.ezorm.rdb.mapping.ReactiveRepository * @see org.hswebframework.ezorm.rdb.mapping.SyncRepository * @since 4.0.0 @@ -30,8 +37,17 @@ */ Class[] annotation() default Table.class; + /** + * @return 是否开启响应式, 默认开启 + */ boolean reactive() default true; + /** + * 是否开启非响应式操作,在使用WebFlux时,不建议开启 + * + * @return 开启非响应式 + * @see org.hswebframework.ezorm.rdb.mapping.SyncRepository + */ boolean nonReactive() default false; } diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/annotation/Reactive.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/annotation/Reactive.java index b535fe517..aa7982b48 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/annotation/Reactive.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/annotation/Reactive.java @@ -3,7 +3,11 @@ import java.lang.annotation.*; /** + * 在实体类上注解,标记是否开启响应式仓库 + * + * @author zhouhao * @see org.hswebframework.ezorm.rdb.mapping.ReactiveRepository + * @since 4.0.0 */ @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) 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 ce99ab329..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,19 +4,25 @@ 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; import org.hswebframework.web.crud.events.EntityDDLEvent; import org.hswebframework.web.event.GenericsPayloadApplicationEvent; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.core.annotation.AnnotatedElementUtils; import reactor.core.publisher.Flux; import reactor.core.scheduler.Schedulers; import java.time.Duration; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -49,41 +55,48 @@ public class AutoDDLProcessor implements InitializingBean { @Override @SneakyThrows public void afterPropertiesSet() { - if (entityFactory instanceof MapperEntityFactory) { - MapperEntityFactory factory = ((MapperEntityFactory) entityFactory); - for (EntityInfo entity : entities) { - factory.addMapping(entity.getEntityType(), MapperEntityFactory.defaultMapper(entity.getRealType())); + + List> readyToDDL = new ArrayList<>(this.entities.size()); + List> nonDDL = new ArrayList<>(); + + for (EntityInfo entity : this.entities) { + Class type = entityFactory.getInstanceType(entity.getRealType(), true); + DDL ddl = AnnotatedElementUtils.findMergedAnnotation(type, DDL.class); + if (properties.isAutoDdl() && (ddl == null || ddl.value())) { + readyToDDL.add(entity.getEntityType()); + } else { + nonDDL.add(entity.getEntityType()); } } - List entities = this.entities.stream().map(EntityInfo::getRealType).collect(Collectors.toList()); - if (properties.isAutoDdl()) { + + if (!readyToDDL.isEmpty()) { //加载全部表信息 if (reactive) { - Flux.fromIterable(entities) + Flux.fromIterable(readyToDDL) .doOnNext(type -> log.trace("auto ddl for {}", type)) .map(type -> { RDBTableMetadata metadata = resolver.resolve(type); - EntityDDLEvent event = new EntityDDLEvent(this,type,metadata); + EntityDDLEvent event = new EntityDDLEvent<>(this, type, metadata); eventPublisher.publishEvent(new GenericsPayloadApplicationEvent<>(this, event, type)); return metadata; }) .flatMap(meta -> operator - .ddl() - .createOrAlter(meta) - .autoLoad(false) - .commit() - .reactive() - .subscribeOn(Schedulers.elastic()) - ) + .ddl() + .createOrAlter(meta) + .autoLoad(false) + .commit() + .reactive() + .subscribeOn(Schedulers.boundedElastic()), + 8,8) .doOnError((err) -> log.error(err.getMessage(), err)) .then() .block(Duration.ofMinutes(5)); } else { - for (Class type : entities) { + for (Class type : readyToDDL) { log.trace("auto ddl for {}", type); try { RDBTableMetadata metadata = resolver.resolve(type); - EntityDDLEvent event = new EntityDDLEvent(this,type,metadata); + EntityDDLEvent event = new EntityDDLEvent<>(this, type, metadata); eventPublisher.publishEvent(new GenericsPayloadApplicationEvent<>(this, event, type)); operator.ddl() .createOrAlter(metadata) @@ -91,18 +104,24 @@ public void afterPropertiesSet() { .commit() .sync(); } catch (Exception e) { - log.error(e.getMessage(), e); + log.error(e.getLocalizedMessage(), e); throw e; } } } - } else { - for (Class entity : entities) { - RDBTableMetadata metadata = resolver.resolve(entity); - operator.getMetadata() - .getCurrentSchema() - .addTable(metadata); + } + + for (Class entity : nonDDL) { + RDBTableMetadata metadata = resolver.resolve(entity); + 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 28606bbe7..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 @@ -15,7 +15,7 @@ public class CompositeEntityTableMetadataResolver implements EntityTableMetadata private final List resolvers = new ArrayList<>(); - private final Map> cache = new ConcurrentHashMap<>(); + private final Map, AtomicReference> cache = new ConcurrentHashMap<>(); public void addParser(EntityTableMetadataParser resolver) { resolvers.add(resolver); @@ -28,14 +28,13 @@ 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) .reduce((t1, t2) -> { - for (RDBColumnMetadata column : t1.getColumns()) { - t2.addColumn(column.clone()); - } + t2.merge(t1); return t2; }).orElse(null); } diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/DefaultEntityResultWrapperFactory.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/DefaultEntityResultWrapperFactory.java index 5f9b42d7b..20fbec0e2 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/DefaultEntityResultWrapperFactory.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/DefaultEntityResultWrapperFactory.java @@ -5,6 +5,7 @@ import org.hswebframework.ezorm.rdb.executor.wrapper.ResultWrapper; import org.hswebframework.ezorm.rdb.mapping.EntityManager; import org.hswebframework.ezorm.rdb.mapping.wrapper.EntityResultWrapper; +import org.hswebframework.ezorm.rdb.mapping.wrapper.NestedEntityResultWrapper; @AllArgsConstructor public class DefaultEntityResultWrapperFactory implements EntityResultWrapperFactory { @@ -14,8 +15,7 @@ public class DefaultEntityResultWrapperFactory implements EntityResultWrapperFac @Override @SneakyThrows public ResultWrapper getWrapper(Class tClass) { - return new EntityResultWrapper<>(() -> entityManager.newInstance(tClass), - entityManager.getMapping(tClass)); + return new NestedEntityResultWrapper<>(entityManager.getMapping(tClass)); } } 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 new file mode 100644 index 000000000..26115ee33 --- /dev/null +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/DetectEntityColumnMapping.java @@ -0,0 +1,76 @@ +package org.hswebframework.web.crud.configuration; + +import org.hswebframework.ezorm.rdb.mapping.EntityColumnMapping; +import org.hswebframework.ezorm.rdb.mapping.MappingFeatureType; +import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata; +import org.hswebframework.ezorm.rdb.metadata.TableOrViewMetadata; +import org.hswebframework.web.api.crud.entity.EntityFactory; + +import java.util.Map; +import java.util.Optional; + +class DetectEntityColumnMapping implements EntityColumnMapping { + private final String id; + private final Class type; + private final EntityColumnMapping mapping; + private final EntityFactory entityFactory; + + public DetectEntityColumnMapping(Class type, + EntityColumnMapping mapping, + EntityFactory entityFactory) { + this.id = MappingFeatureType.columnPropertyMapping.createFeatureId(type); + this.type = type; + this.mapping = mapping; + this.entityFactory = entityFactory; + } + + @Override + public Class getEntityType() { + return type; + } + + @Override + public Optional getColumnByProperty(String property) { + return mapping.getColumnByProperty(property); + } + + @Override + public Optional getPropertyByColumnName(String columnName) { + return mapping.getPropertyByColumnName(columnName); + } + + @Override + public Optional getColumnByName(String columnName) { + return mapping.getColumnByName(columnName); + } + + @Override + public Map getColumnPropertyMapping() { + return mapping.getColumnPropertyMapping(); + } + + @Override + public TableOrViewMetadata getTable() { + return mapping.getTable(); + } + + @Override + public void reload() { + mapping.reload(); + } + + @Override + public Object newInstance() { + return entityFactory.newInstance(getEntityType()); + } + + @Override + public String getId() { + return id; + } + + @Override + public String getName() { + return getId(); + } +} 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 107f16be7..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 @@ -10,9 +10,12 @@ import org.hswebframework.ezorm.rdb.mapping.EntityManager; import org.hswebframework.ezorm.rdb.mapping.MappingFeatureType; import org.hswebframework.ezorm.rdb.mapping.jpa.JpaEntityTableMetadataParser; +import org.hswebframework.ezorm.rdb.mapping.jpa.JpaEntityTableMetadataParserProcessor; import org.hswebframework.ezorm.rdb.mapping.parser.EntityTableMetadataParser; +import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata; import org.hswebframework.ezorm.rdb.metadata.RDBDatabaseMetadata; 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.DefaultDatabaseOperator; import org.hswebframework.web.api.crud.entity.EntityFactory; @@ -20,14 +23,15 @@ import org.hswebframework.web.crud.entity.factory.EntityMappingCustomizer; import org.hswebframework.web.crud.entity.factory.MapperEntityFactory; import org.hswebframework.web.crud.events.*; -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.events.expr.SpelSqlExpressionInvoker; +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.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -35,18 +39,18 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +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 { } @@ -61,58 +65,16 @@ public EntityFactory entityFactory(ObjectProvider custo return factory; } - @Bean - @ConditionalOnMissingBean - public EntityManager entityManager(EntityTableMetadataResolver resolver, EntityFactory entityFactory) { - return new EntityManager() { - @Override - @SneakyThrows - public E newInstance(Class type) { - return entityFactory.newInstance(type); - } - - @Override - public EntityColumnMapping getMapping(Class entity) { - - return resolver.resolve(entityFactory.getInstanceType(entity, true)) - .getFeature(MappingFeatureType.columnPropertyMapping.createFeatureId(entity)) - .map(EntityColumnMapping.class::cast) - .orElse(null); - } - }; - } - - @Bean - public DefaultEntityResultWrapperFactory defaultEntityResultWrapperFactory(EntityManager entityManager) { - return new DefaultEntityResultWrapperFactory(entityManager); - } - - @Bean - @ConditionalOnMissingBean - public EntityTableMetadataResolver entityTableMappingResolver(List parsers) { - CompositeEntityTableMetadataResolver resolver = new CompositeEntityTableMetadataResolver(); - parsers.forEach(resolver::addParser); - return resolver; - } - - @Bean - @ConditionalOnMissingBean - public EntityTableMetadataParser jpaEntityTableMetadataParser(RDBDatabaseMetadata metadata) { - JpaEntityTableMetadataParser parser = new JpaEntityTableMetadataParser(); - parser.setDatabaseMetadata(metadata); - - return parser; - } - @Bean @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); - if (properties.isAutoDdl()) { + if (properties.isAutoDdl() && reactiveSqlExecutor.isPresent()) { for (RDBSchemaMetadata schema : metadata.getSchemas()) { schema.loadAllTableReactive() .block(Duration.ofSeconds(30)); @@ -128,6 +90,11 @@ public DatabaseOperator databaseOperator(RDBDatabaseMetadata metadata) { return DefaultDatabaseOperator.of(metadata); } + @Bean + public QueryHelper queryHelper(DatabaseOperator databaseOperator) { + return new DefaultQueryHelper(databaseOperator); + } + @Bean public BeanPostProcessor autoRegisterFeature(RDBDatabaseMetadata metadata) { CompositeEventListener eventListener = new CompositeEventListener(); @@ -147,19 +114,30 @@ public Object postProcessAfterInitialization(Object bean, String beanName) throw }; } + @Bean - public EntityEventListener entityEventListener(ApplicationEventPublisher eventPublisher, - ObjectProvider customizers) { - DefaultEntityEventListenerConfigure configure = new DefaultEntityEventListenerConfigure(); - customizers.forEach(customizer -> customizer.customize(configure)); - return new EntityEventListener(eventPublisher, configure); + public CreatorEventListener creatorEventListener() { + return new CreatorEventListener(); } + @Bean public ValidateEventListener validateEventListener() { return new ValidateEventListener(); } + @Bean + public EntityEventListener entityEventListener(ApplicationEventPublisher eventPublisher, + ObjectProvider invokers, + ObjectProvider customizers) { + DefaultEntityEventListenerConfigure configure = new DefaultEntityEventListenerConfigure(); + customizers.forEach(customizer -> customizer.customize(configure)); + EntityEventListener entityEventListener = new EntityEventListener(eventPublisher, configure); + entityEventListener.setExpressionInvoker(invokers.getIfAvailable(SpelSqlExpressionInvoker::new)); + + return entityEventListener; + } + @Bean @ConfigurationProperties(prefix = "easyorm.default-value-generator") public DefaultIdGenerator defaultIdGenerator() { @@ -177,9 +155,101 @@ public SnowFlakeStringIdGenerator snowFlakeStringIdGenerator() { return new SnowFlakeStringIdGenerator(); } + @Bean + public RandomIdGenerator randomIdGenerator() { + return new RandomIdGenerator(); + } + @Bean public CurrentTimeGenerator currentTimeGenerator() { return new CurrentTimeGenerator(); } + @Configuration + public static class EntityTableMetadataParserConfiguration { + + @Bean + public DefaultEntityResultWrapperFactory defaultEntityResultWrapperFactory(EntityManager entityManager) { + return new DefaultEntityResultWrapperFactory(entityManager); + } + + @Bean + @ConditionalOnMissingBean + public EntityManager entityManager(EntityTableMetadataResolver resolver, EntityFactory entityFactory) { + return new EntityManager() { + @Override + @SneakyThrows + public E newInstance(Class type) { + return entityFactory.newInstance(type); + } + + @Override + public EntityColumnMapping getMapping(Class entity) { + + return resolver.resolve(entity) + .getFeature(MappingFeatureType.columnPropertyMapping.createFeatureId(entity)) + .map(EntityColumnMapping.class::cast) + .orElse(null); + } + }; + } + + @Bean + @ConditionalOnMissingBean + public EntityTableMetadataResolver entityTableMappingResolver(ObjectProvider parsers) { + CompositeEntityTableMetadataResolver resolver = new CompositeEntityTableMetadataResolver(); + parsers.forEach(resolver::addParser); + return resolver; + } + + @Bean + @ConditionalOnMissingBean + public EntityTableMetadataParser jpaEntityTableMetadataParser(RDBDatabaseMetadata metadata, + EntityFactory factory, + ObjectProvider customizers) { + + JpaEntityTableMetadataParser parser = new JpaEntityTableMetadataParser() { + + @Override + 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(realType, columnMapping, factory)); + table.addFeature(columnMapping = new DetectEntityColumnMapping(entityType, columnMapping, factory)); + } + for (TableMetadataCustomizer customizer : customizers) { + customizer.customTable(realType, table); + } + columnMapping.reload(); + }); + return tableOpt; + } + + @Override + protected JpaEntityTableMetadataParserProcessor createProcessor(RDBTableMetadata table, Class type) { + Class realType = factory.getInstanceType(type, true); + return new JpaEntityTableMetadataParserProcessor(table, realType) { + @Override + protected void customColumn(PropertyDescriptor descriptor, + Field field, + RDBColumnMetadata column, + Set annotations) { + super.customColumn(descriptor, field, column, annotations); + for (TableMetadataCustomizer customizer : customizers) { + customizer.customColumn(realType, descriptor, field, annotations, column); + } + } + }; + } + }; + parser.setDatabaseMetadata(metadata); + + return parser; + } + } } 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 9fe25ff59..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,20 +17,25 @@ 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; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.core.io.support.ResourcePatternResolver; import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.classreading.CachingMetadataReaderFactory; 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; @@ -40,16 +45,71 @@ public class EasyormRepositoryRegistrar implements ImportBeanDefinitionRegistrar private final ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver(); - private final MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(); + private final MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(); + + private String getResourceClassName(Resource resource) { + try { + return metadataReaderFactory + .getMetadataReader(resource) + .getClassMetadata() + .getClassName(); + } catch (IOException e) { + return null; + } + } @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)); } + 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()) + .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 @SneakyThrows @SuppressWarnings("all") @@ -63,68 +123,28 @@ public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, B boolean nonReactiveEnabled = Boolean.TRUE.equals(attr.get("nonReactive")); String[] arr = (String[]) attr.get("value"); -// Set resources = Arrays -// .stream(arr) -// .flatMap(this::doGetResources) -// .collect(Collectors.toSet()); Class[] anno = (Class[]) attr.get("annotation"); - Set entityInfos = new HashSet<>(); + Set entityInfos = ConcurrentHashMap.newKeySet(); CandidateComponentsIndex index = CandidateComponentsIndexLoader.loadIndex(org.springframework.util.ClassUtils.getDefaultClassLoader()); - Set entities = Stream - .of(arr) - .flatMap(_package -> { - return index - .getCandidateTypes(_package, Table.class.getName()) - .stream(); - }) - .collect(Collectors.toSet()); - for (String className : entities) { -// MetadataReader reader = metadataReaderFactory.getMetadataReader(resource); -// String className = reader.getClassMetadata().getClassName(); + for (String className : scanEntities(arr)) { Class entityType = org.springframework.util.ClassUtils.forName(className, null); if (Arrays.stream(anno) - .noneMatch(ann -> AnnotationUtils.findAnnotation(entityType, ann) != null)) { + .noneMatch(ann -> AnnotationUtils.getAnnotation(entityType, ann) != null)) { continue; } - ImplementFor implementFor = AnnotationUtils.findAnnotation(entityType, ImplementFor.class); Reactive reactive = AnnotationUtils.findAnnotation(entityType, Reactive.class); - Class genericType = Optional - .ofNullable(implementFor) - .map(ImplementFor::value) - .orElseGet(() -> { - return Stream - .of(entityType.getInterfaces()) - .filter(e -> GenericEntity.class.isAssignableFrom(e)) - .findFirst() - .orElse(entityType); - }); - - Class idType = null; - if (implementFor == null || implementFor.idType() == Void.class) { - 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; - } - } else { - idType = implementFor.idType(); - } - EntityInfo entityInfo = new EntityInfo(genericType, + Class idType = findIdType(entityType); + + EntityInfo entityInfo = new EntityInfo(entityType, entityType, idType, reactiveEnabled, nonReactiveEnabled); - if (!entityInfos.contains(entityInfo) || implementFor != null) { + if (!entityInfos.contains(entityInfo)) { entityInfos.add(entityInfo); } @@ -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(); @@ -174,15 +205,6 @@ public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, B registry.registerBeanDefinition(AutoDDLProcessor.class.getName() + "_" + count.incrementAndGet(), definition); } -// try { -// BeanDefinition definition = registry.getBeanDefinition(AutoDDLProcessor.class.getName()); -// Set infos = (Set) definition.getPropertyValues().get("entities"); -// infos.addAll(entityInfos); -// } catch (NoSuchBeanDefinitionException e) { -// -// } - - } static AtomicInteger count = new AtomicInteger(); diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/EntityInfo.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/EntityInfo.java index 179dd4c3f..c9ca892f6 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/EntityInfo.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/EntityInfo.java @@ -10,11 +10,11 @@ @EqualsAndHashCode(of = "entityType") @AllArgsConstructor public class EntityInfo { - private Class entityType; + private Class entityType; - private Class realType; + private Class realType; - private Class idType; + private Class idType; private boolean reactive; 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/configuration/TableMetadataCustomizer.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/TableMetadataCustomizer.java new file mode 100644 index 000000000..f31786433 --- /dev/null +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/TableMetadataCustomizer.java @@ -0,0 +1,41 @@ +package org.hswebframework.web.crud.configuration; + +import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata; +import org.hswebframework.ezorm.rdb.metadata.RDBTableMetadata; + +import java.beans.PropertyDescriptor; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.util.Set; + +/** + * 表结构自定义器,实现此接口来自定义表结构. + * + * @author zhouhao + * @since 4.0.14 + */ +public interface TableMetadataCustomizer { + + /** + * 自定义列,在列被解析后调用. + * + * @param entityType 实体类型 + * @param descriptor 字段描述 + * @param field 字段 + * @param column 列定义 + * @param annotations 字段上的注解 + */ + void customColumn(Class entityType, + PropertyDescriptor descriptor, + Field field, + Set annotations, + RDBColumnMetadata column); + + /** + * 自定义表,在实体类被解析完成后调用. + * + * @param entityType 字段类型 + * @param table 表结构 + */ + void customTable(Class entityType, RDBTableMetadata table); +} diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/entity/factory/MapperEntityFactory.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/entity/factory/MapperEntityFactory.java index 1d2667584..ab998d404 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/entity/factory/MapperEntityFactory.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/entity/factory/MapperEntityFactory.java @@ -27,8 +27,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.lang.reflect.Constructor; import java.lang.reflect.Modifier; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Supplier; /** @@ -37,9 +39,11 @@ */ @SuppressWarnings("unchecked") public class MapperEntityFactory implements EntityFactory, BeanFactory { - private Map realTypeMapper = new HashMap<>(); - private Logger logger = LoggerFactory.getLogger(this.getClass()); - private Map copierCache = new HashMap<>(); + @SuppressWarnings("all") + private final Map, Mapper> realTypeMapper = new ConcurrentHashMap<>(); + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + @SuppressWarnings("all") + private final Map copierCache = new ConcurrentHashMap<>(); private static final DefaultMapperFactory DEFAULT_MAPPER_FACTORY = clazz -> { String simpleClassName = clazz.getPackage().getName().concat(".Simple").concat(clazz.getSimpleName()); @@ -68,11 +72,26 @@ public MapperEntityFactory(Map, Mapper> realTypeMapper) { this.realTypeMapper.putAll(realTypeMapper); } + public MapperEntityFactory addMapping(Class target, Supplier mapper) { + realTypeMapper.put(target, new Mapper(mapper.get().getClass(), mapper)); + return this; + } + + public MapperEntityFactory addMappingIfAbsent(Class target, Supplier mapper) { + realTypeMapper.putIfAbsent(target, new Mapper(mapper.get().getClass(), mapper)); + return this; + } + public MapperEntityFactory addMapping(Class target, Mapper mapper) { realTypeMapper.put(target, mapper); return this; } + public MapperEntityFactory addMappingIfAbsent(Class target, Mapper mapper) { + realTypeMapper.putIfAbsent(target, mapper); + return this; + } + public MapperEntityFactory addCopier(PropertyCopier copier) { Class source = (Class) ClassUtils.getGenericType(copier.getClass(), 0); Class target = (Class) ClassUtils.getGenericType(copier.getClass(), 1); @@ -106,13 +125,15 @@ public T copyProperties(S source, T target) { } return (T) defaultPropertyCopier.copyProperties(source, target); - } catch (Exception e) { + } catch (Throwable e) { logger.warn("copy properties error", e); } return target; } - protected Mapper initCache(Class beanClass) { + static final Mapper NON_MAPPER = new Mapper(null, null); + + protected Mapper createMapper(Class beanClass) { Mapper mapper = null; Class realType = null; ServiceLoader serviceLoader = ServiceLoader.load(beanClass, this.getClass().getClassLoader()); @@ -133,30 +154,36 @@ protected Mapper initCache(Class beanClass) { if (logger.isDebugEnabled() && realType != beanClass) { logger.debug("use instance {} for {}", realType, beanClass); } - mapper = new Mapper<>(realType, new DefaultInstanceGetter(realType)); - } - if (mapper != null) { - realTypeMapper.put(beanClass, mapper); + mapper = new Mapper<>(realType, new DefaultInstanceGetter<>(realType)); } - return mapper; + + return mapper == null ? NON_MAPPER : mapper; } @Override public T newInstance(Class beanClass) { - return newInstance(beanClass, null); + return newInstance(beanClass, (Class) null); } @Override - public T newInstance(Class beanClass, Class defaultClass) { - if (beanClass == null) { + public T newInstance(Class entityClass, Supplier defaultFactory) { + if (entityClass == null) { return null; } - Mapper mapper = realTypeMapper.get(beanClass); - if (mapper != null) { + Mapper mapper = realTypeMapper.computeIfAbsent(entityClass, this::createMapper); + if (mapper != null && mapper != NON_MAPPER) { return mapper.getInstanceGetter().get(); } - mapper = initCache(beanClass); - if (mapper != null) { + return defaultFactory.get(); + } + + @Override + public T newInstance(Class beanClass, Class defaultClass) { + if (beanClass == null) { + return null; + } + Mapper mapper = realTypeMapper.computeIfAbsent(beanClass, this::createMapper); + if (mapper != null && mapper != NON_MAPPER) { return mapper.getInstanceGetter().get(); } if (defaultClass != null) { @@ -184,21 +211,16 @@ public Class getInstanceType(Class beanClass, boolean autoRegister) { || beanClass.isEnum()) { return null; } - Mapper mapper = realTypeMapper.get(beanClass); - if (null != mapper) { - return mapper.getTarget(); - } - if (autoRegister) { - mapper = initCache(beanClass); - if (mapper != null) { - return mapper.getTarget(); - } + Mapper mapper = realTypeMapper.computeIfAbsent( + beanClass, + clazz -> autoRegister ? createMapper(clazz) : null); - return Modifier.isAbstract(beanClass.getModifiers()) - || Modifier.isInterface(beanClass.getModifiers()) - ? null : beanClass; + if (null != mapper && mapper != NON_MAPPER) { + return mapper.getTarget(); } - return null; + return Modifier.isAbstract(beanClass.getModifiers()) + || Modifier.isInterface(beanClass.getModifiers()) + ? null : beanClass; } public void setDefaultMapperFactory(DefaultMapperFactory defaultMapperFactory) { @@ -211,8 +233,8 @@ public void setDefaultPropertyCopier(DefaultPropertyCopier defaultPropertyCopier } public static class Mapper { - Class target; - Supplier instanceGetter; + final Class target; + final Supplier instanceGetter; public Mapper(Class target, Supplier instanceGetter) { this.target = target; @@ -237,16 +259,17 @@ public static Supplier defaultInstanceGetter(Class clazz) { } static class DefaultInstanceGetter implements Supplier { - Class type; + final Constructor constructor; + @SneakyThrows public DefaultInstanceGetter(Class type) { - this.type = type; + this.constructor = type.getConstructor(); } @Override @SneakyThrows public T get() { - return type.newInstance(); + return constructor.newInstance(); } } } diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/CompositeEventListener.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/CompositeEventListener.java index 638850b9f..f07ece5db 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/CompositeEventListener.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/CompositeEventListener.java @@ -5,7 +5,9 @@ import org.hswebframework.ezorm.rdb.events.EventContext; import org.hswebframework.ezorm.rdb.events.EventListener; import org.hswebframework.ezorm.rdb.events.EventType; +import org.springframework.core.Ordered; +import java.util.Comparator; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; @@ -24,5 +26,6 @@ public void onEvent(EventType type, EventContext context) { public void addListener(EventListener eventListener) { eventListeners.add(eventListener); + eventListeners.sort(Comparator.comparingLong(e -> e instanceof Ordered ? ((Ordered) e).getOrder() : Ordered.LOWEST_PRECEDENCE)); } } 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 new file mode 100644 index 000000000..772ca00cb --- /dev/null +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/CreatorEventListener.java @@ -0,0 +1,147 @@ +package org.hswebframework.web.crud.events; + +import org.hswebframework.ezorm.rdb.events.EventContext; +import org.hswebframework.ezorm.rdb.events.EventListener; +import org.hswebframework.ezorm.rdb.events.EventType; +import org.hswebframework.ezorm.rdb.mapping.events.MappingContextKeys; +import org.hswebframework.ezorm.rdb.mapping.events.MappingEventTypes; +import org.hswebframework.ezorm.rdb.mapping.events.ReactiveResultHolder; +import org.hswebframework.web.api.crud.entity.Entity; +import org.hswebframework.web.api.crud.entity.RecordCreationEntity; +import org.hswebframework.web.api.crud.entity.RecordModifierEntity; +import org.hswebframework.web.authorization.Authentication; +import org.hswebframework.web.validator.CreateGroup; +import org.hswebframework.web.validator.UpdateGroup; +import org.springframework.core.Ordered; +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; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; + +import static org.springframework.data.repository.util.ClassUtils.ifPresent; + +/** + * 自动填充创建人和修改人信息 + */ +public class CreatorEventListener implements EventListener, Ordered { + + @Override + public String getId() { + return "creator-listener"; + } + + @Override + public String getName() { + return "创建者监听器"; + } + + @Override + 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) { + if (resultHolder.isPresent()) { + 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(Context.empty(), type, context, 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,applyUpdate); + } else { + applyCreator(auth, context, instance, + type != MappingEventTypes.update_before,applyUpdate); + } + } + + context + .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 updateModifier) { + long now = System.currentTimeMillis(); + if (updateCreator) { + if (entity instanceof RecordCreationEntity) { + RecordCreationEntity e = (RecordCreationEntity) entity; + if (ObjectUtils.isEmpty(e.getCreatorId())) { + e.setCreatorId(auth.getUser().getId()); + e.setCreatorName(auth.getUser().getName()); + } + if (e.getCreateTime() == null) { + e.setCreateTime(now); + } + } else if (entity instanceof Map) { + @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 (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,boolean updateModifier) { + for (Object entity : entities) { + applyCreator(auth, context, entity, updateCreator,updateModifier); + } + + } + + @Override + public int getOrder() { + return Ordered.HIGHEST_PRECEDENCE; + } +} 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/EntityBeforeCreateEvent.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityBeforeCreateEvent.java index 73491fa2c..4cc2bbeee 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityBeforeCreateEvent.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityBeforeCreateEvent.java @@ -18,4 +18,9 @@ public class EntityBeforeCreateEvent extends DefaultAsyncEvent implements Ser private final List entity; private final Class entityType; + + @Override + public String toString() { + return "EntityBeforeCreateEvent<" + entityType.getSimpleName() + ">"+entity; + } } diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityBeforeDeleteEvent.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityBeforeDeleteEvent.java index 83bb172d1..c05f1ad87 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityBeforeDeleteEvent.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityBeforeDeleteEvent.java @@ -21,4 +21,8 @@ public class EntityBeforeDeleteEvent extends DefaultAsyncEvent implements Ser private final Class entityType; + @Override + public String toString() { + return "EntityBeforeDeleteEvent<" + entityType.getSimpleName() + ">"+entity; + } } diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityBeforeModifyEvent.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityBeforeModifyEvent.java index 30bbc15da..d94508536 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityBeforeModifyEvent.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityBeforeModifyEvent.java @@ -8,12 +8,12 @@ import java.util.List; /** - * @see org.hswebframework.web.crud.annotation.EnableEntityEvent * @param + * @see org.hswebframework.web.crud.annotation.EnableEntityEvent */ @AllArgsConstructor @Getter -public class EntityBeforeModifyEvent extends DefaultAsyncEvent implements Serializable{ +public class EntityBeforeModifyEvent extends DefaultAsyncEvent implements Serializable { private static final long serialVersionUID = -7158901204884303777L; @@ -23,4 +23,8 @@ public class EntityBeforeModifyEvent extends DefaultAsyncEvent implements Ser private final Class entityType; + @Override + public String toString() { + return "EntityBeforeModifyEvent<" + entityType.getSimpleName() + ">\n{\nbefore:" + before + "\nafter: " + after + "\n}"; + } } diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityBeforeQueryEvent.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityBeforeQueryEvent.java index 7c46f3cbd..f163f1834 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityBeforeQueryEvent.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityBeforeQueryEvent.java @@ -21,4 +21,8 @@ public class EntityBeforeQueryEvent extends DefaultAsyncEvent implements Seri private final Class entityType; + @Override + public String toString() { + return "EntityBeforeQueryEvent<" + entityType.getSimpleName() + ">"+param; + } } diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityBeforeSaveEvent.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityBeforeSaveEvent.java index 0df1b5d58..582e8778b 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityBeforeSaveEvent.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityBeforeSaveEvent.java @@ -18,4 +18,9 @@ public class EntityBeforeSaveEvent extends DefaultAsyncEvent implements Seria private final List entity; private final Class entityType; + + @Override + public String toString() { + return "EntityBeforeSaveEvent<" + entityType.getSimpleName() + ">"+entity; + } } diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityCreatedEvent.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityCreatedEvent.java index 9653026d9..11415b627 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityCreatedEvent.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityCreatedEvent.java @@ -18,4 +18,9 @@ public class EntityCreatedEvent extends DefaultAsyncEvent implements Serializ private final List entity; private final Class entityType; + + @Override + public String toString() { + return "EntityCreatedEvent<" + entityType.getSimpleName() + ">"+entity; + } } diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityDeletedEvent.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityDeletedEvent.java index 746948c52..7fcd6906e 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityDeletedEvent.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityDeletedEvent.java @@ -22,4 +22,9 @@ public class EntityDeletedEvent extends DefaultAsyncEvent implements Serializ private final Class entityType; + @Override + public String toString() { + return "EntityDeletedEvent<" + entityType.getSimpleName() + ">"+entity; + } + } 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 3a6ffd8d9..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 @@ -31,11 +31,20 @@ public class EntityEventHelper { */ public static Mono isDoFireEvent(boolean defaultIfEmpty) { return Mono - .subscriberContext() - .flatMap(ctx -> Mono.justOrEmpty(ctx.getOrEmpty(doEventContextKey))) + .deferContextual(ctx -> Mono.justOrEmpty(ctx.getOrEmpty(doEventContextKey))) .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不触发实体类事件 * @@ -49,7 +58,7 @@ public static Mono isDoFireEvent(boolean defaultIfEmpty) { * @return 流 */ public static Mono setDoNotFireEvent(Mono stream) { - return stream.subscriberContext(Context.of(doEventContextKey, false)); + return stream.contextWrite(Context.of(doEventContextKey, false)); } /** @@ -64,7 +73,7 @@ public static Mono setDoNotFireEvent(Mono stream) { * @return 流 */ public static Flux setDoNotFireEvent(Flux stream) { - return stream.subscriberContext(Context.of(doEventContextKey, false)); + return stream.contextWrite(Context.of(doEventContextKey, false)); } public static Mono publishSavedEvent(Object source, @@ -96,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 9595da9cc..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,26 +1,28 @@ package org.hswebframework.web.crud.events; -import lombok.AllArgsConstructor; -import org.apache.commons.beanutils.BeanUtilsBean; -import org.apache.commons.collections.CollectionUtils; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +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.*; import org.hswebframework.ezorm.rdb.events.EventListener; -import org.hswebframework.ezorm.rdb.events.EventType; +import org.hswebframework.ezorm.rdb.events.*; import org.hswebframework.ezorm.rdb.executor.NullValue; import org.hswebframework.ezorm.rdb.mapping.*; import org.hswebframework.ezorm.rdb.mapping.events.MappingContextKeys; import org.hswebframework.ezorm.rdb.mapping.events.MappingEventTypes; import org.hswebframework.ezorm.rdb.mapping.events.ReactiveResultHolder; import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata; -import org.hswebframework.ezorm.rdb.metadata.TableOrViewMetadata; +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; import org.hswebframework.web.event.GenericsPayloadApplicationEvent; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.core.Ordered; import reactor.core.publisher.Mono; import reactor.function.Function3; import reactor.util.function.Tuple2; @@ -31,16 +33,25 @@ import java.util.function.BiFunction; import java.util.function.Supplier; -import static org.hswebframework.web.crud.events.EntityEventHelper.*; +import static org.hswebframework.web.crud.events.EntityEventHelper.publishEvent; @SuppressWarnings("all") -@AllArgsConstructor -public class EntityEventListener implements EventListener { +@RequiredArgsConstructor +public class EntityEventListener implements EventListener, Ordered { + + public static final ContextKey> readyToDeleteContextKey = ContextKey.of("readyToDelete"); + //更新前的数据 + public static final ContextKey> readyToUpdateBeforeContextKey = ContextKey.of("readyToUpdateBefore"); + //更新后的数据 + public static final ContextKey> readyToUpdateAfterContextKey = ContextKey.of("readyToUpdateAfter"); private final ApplicationEventPublisher eventPublisher; private final EntityEventListenerConfigure listenerConfigure; + @Setter + private SqlExpressionInvoker expressionInvoker; + @Override public String getId() { return "entity-listener"; @@ -61,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; } @@ -80,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); } @@ -122,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() + ); }); }); } @@ -132,50 +143,64 @@ protected void handleQueryBefore(EntityColumnMapping mapping, EventContext conte protected List createAfterData(List olds, EventContext context) { List newValues = new ArrayList<>(olds.size()); + EntityColumnMapping mapping = context - .get(MappingContextKeys.columnMapping) - .orElseThrow(UnsupportedOperationException::new); - TableOrViewMetadata table = context.get(ContextKeys.table).orElseThrow(UnsupportedOperationException::new); - RDBColumnMetadata idColumn = table - .getColumns() - .stream() - .filter(RDBColumnMetadata::isPrimaryKey) - .findFirst() - .orElse(null); - if (idColumn == null) { - return Collections.emptyList(); - } + .get(MappingContextKeys.columnMapping) + .orElseThrow(UnsupportedOperationException::new); + + Map columns = context + .get(MappingContextKeys.updateColumnInstance) + .orElse(Collections.emptyMap()); + for (Object old : olds) { - Object newValue = context - .get(MappingContextKeys.instance) - .filter(Entity.class::isInstance) - .map(Entity.class::cast) - .orElseGet(() -> { - return context - .get(MappingContextKeys.updateColumnInstance) - .map(map -> { - Object data = FastBeanCopier.copy(map, FastBeanCopier.copy(old, mapping.getEntityType())); - //set null - for (Map.Entry stringObjectEntry : map.entrySet()) { - if (stringObjectEntry.getValue() == null || stringObjectEntry.getValue() instanceof NullValue) { - GlobalConfig - .getPropertyOperator() - .setProperty(data, stringObjectEntry.getKey(), null); - } - } - return data; - }) - .map(Entity.class::cast) - .orElse(null); - }); - if (newValue != null) { - FastBeanCopier.copy(old, newValue, FastBeanCopier.include(idColumn.getAlias())); + Map oldMap = null; + Object data = FastBeanCopier.copy(old, mapping.newInstance()); + for (Map.Entry entry : columns.entrySet()) { + + RDBColumnMetadata column = mapping.getColumnByName(entry.getKey()).orElse(null); + if (column == null) { + continue; + } + + Object value = entry.getValue(); + + //set null + if (value instanceof NullValue) { + value = null; + } + //原生sql + if (value instanceof NativeSql) { + value = expressionInvoker == null ? null : expressionInvoker.invoke( + ((NativeSql) value), + mapping, + oldMap == null ? oldMap = createFullMapping(old, mapping) : oldMap); + if (value == null) { + continue; + } + } + + GlobalConfig + .getPropertyOperator() + .setProperty(data, column.getAlias(), value); + } - newValues.add(newValue); + newValues.add(data); } return newValues; } + protected Map createFullMapping(Object old, EntityColumnMapping mapping) { + Map map = FastBeanCopier.copy(old, new HashMap<>()); + + for (RDBColumnMetadata column : mapping.getTable().getColumns()) { + if (map.containsKey(column.getAlias())) { + map.put(column.getName(), map.get(column.getAlias())); + } + } + + return map; + } + protected Mono sendUpdateEvent(List before, List after, Class type, @@ -196,71 +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) { - - 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(); - })); - } - - }); + 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(); + })); + } + + //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(); @@ -268,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(); } } } @@ -287,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); @@ -298,29 +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() - .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(); + })); } }); @@ -347,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) { @@ -429,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)) { @@ -468,10 +579,11 @@ 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 + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE - 100; } } diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityModifyEvent.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityModifyEvent.java index e15d55fef..ffece1438 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityModifyEvent.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityModifyEvent.java @@ -23,4 +23,8 @@ public class EntityModifyEvent extends DefaultAsyncEvent implements Serializa private final Class entityType; + @Override + public String toString() { + return "EntityModifyEvent<" + entityType.getSimpleName() + ">\n{\nbefore:" + before + "\nafter: " + after + "\n}"; + } } diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityPrepareCreateEvent.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityPrepareCreateEvent.java index c83d14a52..e0f39d6c2 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityPrepareCreateEvent.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityPrepareCreateEvent.java @@ -18,4 +18,9 @@ public class EntityPrepareCreateEvent extends DefaultAsyncEvent implements Se private final List entity; private final Class entityType; + + @Override + public String toString() { + return "EntityPrepareCreateEvent<" + entityType.getSimpleName() + ">"+entity; + } } diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityPrepareModifyEvent.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityPrepareModifyEvent.java index d59430d48..752cad145 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityPrepareModifyEvent.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityPrepareModifyEvent.java @@ -23,4 +23,8 @@ public class EntityPrepareModifyEvent extends DefaultAsyncEvent implements Se private final Class entityType; + @Override + public String toString() { + return "EntityPrepareModifyEvent<" + entityType.getSimpleName() + ">\n{\nbefore:" + before + "\nafter: " + after + "\n}"; + } } diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityPrepareSaveEvent.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityPrepareSaveEvent.java index 030379006..15c073a28 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityPrepareSaveEvent.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntityPrepareSaveEvent.java @@ -18,4 +18,9 @@ public class EntityPrepareSaveEvent extends DefaultAsyncEvent implements Seri private final List entity; private final Class entityType; + + @Override + public String toString() { + return "EntityPrepareSaveEvent<" + entityType.getSimpleName() + ">"+entity; + } } diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntitySavedEvent.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntitySavedEvent.java index 370f9dc09..222030058 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntitySavedEvent.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/EntitySavedEvent.java @@ -18,4 +18,9 @@ public class EntitySavedEvent extends DefaultAsyncEvent implements Serializab private final List entity; private final Class entityType; + + @Override + public String toString() { + return "EntitySavedEvent<" + entityType.getSimpleName() + ">"+entity; + } } diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/SqlExpressionInvoker.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/SqlExpressionInvoker.java new file mode 100644 index 000000000..8dcf2fb51 --- /dev/null +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/SqlExpressionInvoker.java @@ -0,0 +1,12 @@ +package org.hswebframework.web.crud.events; + +import org.hswebframework.ezorm.rdb.mapping.EntityColumnMapping; +import org.hswebframework.ezorm.rdb.operator.builder.fragments.NativeSql; + +import java.util.Map; + +public interface SqlExpressionInvoker { + + Object invoke(NativeSql sql, EntityColumnMapping mapping, Map object); + +} diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/ValidateEventListener.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/ValidateEventListener.java index be7e1fb00..4d52c1c14 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/ValidateEventListener.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/ValidateEventListener.java @@ -10,11 +10,12 @@ import org.hswebframework.web.i18n.LocaleUtils; import org.hswebframework.web.validator.CreateGroup; import org.hswebframework.web.validator.UpdateGroup; +import org.springframework.core.Ordered; import java.util.List; import java.util.Optional; -public class ValidateEventListener implements EventListener { +public class ValidateEventListener implements EventListener, Ordered { @Override public String getId() { @@ -35,9 +36,10 @@ public void onEvent(EventType type, EventContext context) { resultHolder .ifPresent(holder -> holder .invoke(LocaleUtils - .currentReactive() - .doOnNext(locale -> LocaleUtils.doWith(locale, (l) -> tryValidate(type, context))) - .then() + .doInReactive(() -> { + tryValidate(type, context); + return null; + }) )); } else { tryValidate(type, context); @@ -72,4 +74,9 @@ public void tryValidate(EventType type, EventContext context) { .ifPresent(entity -> entity.tryValidate(UpdateGroup.class)); } } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE - 1000; + } } 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 new file mode 100644 index 000000000..d235917b9 --- /dev/null +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/expr/AbstractSqlExpressionInvoker.java @@ -0,0 +1,26 @@ +package org.hswebframework.web.crud.events.expr; + +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; +import java.util.function.BiFunction; + +public abstract class AbstractSqlExpressionInvoker implements SqlExpressionInvoker { + + 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(mapping,sql.getParameters(), object); + } + + + 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 new file mode 100644 index 000000000..d038a8a8b --- /dev/null +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/expr/SpelSqlExpressionInvoker.java @@ -0,0 +1,196 @@ +package org.hswebframework.web.crud.events.expr; + +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.*; +import org.springframework.expression.spel.standard.SpelExpressionParser; +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.*; +import java.util.concurrent.atomic.AtomicLong; + +@Slf4j +public class SpelSqlExpressionInvoker extends AbstractSqlExpressionInvoker { + + protected static class SqlFunctions extends HashMap { + + 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) { + return String.valueOf(str).toLowerCase(); + } + + 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); + } + + public String trim(Object str) { + return String.valueOf(str).trim(); + } + + public String concat(Object... args) { + StringBuilder builder = new StringBuilder(); + for (Object arg : args) { + builder.append(arg); + } + return builder.toString(); + } + + public Object coalesce(Object... args) { + for (Object arg : args) { + if (arg != null) { + return arg; + } + } + return null; + } + } + + static final FastThreadLocal SHARED_CONTEXT = new FastThreadLocal() { + @Override + protected StandardEvaluationContext initialValue() { + StandardEvaluationContext context = new StandardEvaluationContext(); + context.addPropertyAccessor(accessor); + context.addMethodResolver(new ReflectiveMethodResolver() { + @Override + public MethodExecutor resolve(@Nonnull EvaluationContext context, + @Nonnull Object targetObject, + @Nonnull String name, + @Nonnull List argumentTypes) throws AccessException { + return super.resolve(context, targetObject, name.toLowerCase(), argumentTypes); + } + }); + context.setOperatorOverloader(new OperatorOverloader() { + @Override + public boolean overridesOperation(@Nonnull Operation operation, Object leftOperand, Object rightOperand) throws EvaluationException { + if (leftOperand instanceof Number || rightOperand instanceof Number) { + return leftOperand == null || rightOperand == null; + } + return leftOperand == null && rightOperand == null; + } + + @Override + public Object operate(@Nonnull Operation operation, Object leftOperand, Object rightOperand) throws EvaluationException { + return null; + } + }); + return context; + } + }; + + @Override + protected Function3, Object> compile(String sql) { + + StringBuilder builder = new StringBuilder(sql.length()); + int argIndex = 0; + for (int i = 0; i < sql.length(); i++) { + char c = sql.charAt(i); + if (c == '?') { + builder.append("_arg").append(argIndex++); + } else { + builder.append(c); + } + } + try { + SpelExpressionParser parser = new SpelExpressionParser(); + + Expression expression = parser.parseExpression(builder.toString()); + AtomicLong errorCount = new AtomicLong(); + + return (mapping, args, object) -> { + if (errorCount.get() > 1024) { + return null; + } + object = createArguments(mapping, object); + + if (args != null && args.length != 0) { + int index = 0; + for (Object parameter : args) { + object.put("_arg" + index, parameter); + } + } + StandardEvaluationContext context = SHARED_CONTEXT.get(); + try { + context.setRootObject(object); + Object val = expression.getValue(context); + errorCount.set(0); + return val; + } catch (Throwable err) { + log.warn("invoke native sql [{}] value error", + sql, + err); + errorCount.incrementAndGet(); + } finally { + context.setRootObject(null); + } + return null; + }; + } catch (Throwable error) { + return spelError(sql, error); + } + } + + protected SqlFunctions createArguments(EntityColumnMapping mapping, Map args) { + return new SqlFunctions(mapping, args); + } + + protected Function3, Object> spelError(String sql, Throwable error) { + log.warn("create sql expression [{}] parser error", sql, error); + return (mapping, args, data) -> null; + } + + static ExtMapAccessor accessor = new ExtMapAccessor(); + + static class ExtMapAccessor extends MapAccessor { + @Override + public boolean canRead(@Nonnull EvaluationContext context, Object target, @Nonnull String name) throws AccessException { + return target instanceof Map; + } + + @Override + @Nonnull + public TypedValue read(@Nonnull EvaluationContext context, Object target, @Nonnull String name) throws AccessException { + Assert.state(target instanceof Map, "Target must be of type Map"); + Map map = (Map) target; + Object value = map.get(name); + return new TypedValue(value); + } + } + +} 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/DefaultIdGenerator.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/generator/DefaultIdGenerator.java index b8dfee3a2..983e57bf4 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/generator/DefaultIdGenerator.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/generator/DefaultIdGenerator.java @@ -8,13 +8,14 @@ import org.hswebframework.ezorm.core.DefaultValueGenerator; import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata; import org.hswebframework.web.id.IDGenerator; +import org.springframework.util.StringUtils; import reactor.core.publisher.Mono; import java.util.HashMap; import java.util.Map; @Slf4j -public class DefaultIdGenerator implements DefaultValueGenerator { +public class DefaultIdGenerator implements DefaultValueGenerator { @Getter @Setter @@ -32,14 +33,10 @@ public String getSortId() { @Override @SneakyThrows public DefaultValue generate(RDBColumnMetadata metadata) { - return Mono.justOrEmpty(mappings.get(metadata.getOwner().getName())) - .switchIfEmpty(Mono.justOrEmpty(defaultId)) - .flatMap(id->Mono.justOrEmpty(metadata.findFeature(DefaultValueGenerator.createId(id)))) - .doOnNext(gen-> log.debug("use default id generator : {} for column : {}", gen.getSortId(), metadata.getFullName())) - .map(gen->gen.generate(metadata)) - .switchIfEmpty(Mono.error(()->new UnsupportedOperationException("不支持的生成器:" + defaultId))) - .toFuture() - .get(); + String genId = mappings.getOrDefault(metadata.getOwner().getName(), defaultId); + DefaultValueGenerator generator = metadata.findFeatureNow(DefaultValueGenerator.createId(genId)); + log.debug("use default id generator : {} for column : {}", generator.getSortId(), metadata.getFullName()); + return generator.generate(metadata); } @Override 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 new file mode 100644 index 000000000..25f7d541c --- /dev/null +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/DefaultQueryHelper.java @@ -0,0 +1,1368 @@ +package org.hswebframework.web.crud.query; + +import lombok.AllArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.hswebframework.ezorm.core.*; +import org.hswebframework.ezorm.core.dsl.Query; +import org.hswebframework.ezorm.core.param.Term; +import org.hswebframework.ezorm.core.param.TermType; +import org.hswebframework.ezorm.rdb.executor.SqlRequest; +import org.hswebframework.ezorm.rdb.executor.reactive.ReactiveSqlExecutor; +import org.hswebframework.ezorm.rdb.executor.wrapper.ColumnWrapperContext; +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; +import org.hswebframework.ezorm.rdb.metadata.RDBFeatureType; +import org.hswebframework.ezorm.rdb.metadata.TableOrViewMetadata; +import org.hswebframework.ezorm.rdb.operator.DatabaseOperator; +import org.hswebframework.ezorm.rdb.operator.builder.Paginator; +import org.hswebframework.ezorm.rdb.operator.builder.fragments.NativeSql; +import org.hswebframework.ezorm.rdb.operator.builder.fragments.PrepareSqlFragments; +import org.hswebframework.ezorm.rdb.operator.dml.Join; +import org.hswebframework.ezorm.rdb.operator.dml.JoinType; +import org.hswebframework.ezorm.rdb.operator.dml.QueryOperator; +import org.hswebframework.ezorm.rdb.operator.dml.SelectColumnSupplier; +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; +import org.slf4j.Logger; +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; +import reactor.util.context.Context; +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; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; + + +@AllArgsConstructor +public class DefaultQueryHelper implements QueryHelper { + + private final DatabaseOperator database; + + private final Map, Table> nameMapping = new ConcurrentHashMap<>(); + + private final Map analyzerCaches = new ConcurrentHashMap<>(); + + static final ResultWrapper countWrapper = + ResultWrappers.column("_total", i -> ((Number) i).intValue()); + + @Override + public QueryAnalyzer analysis(String selectSql) { + return analyzerCaches.computeIfAbsent(selectSql, sql -> new QueryAnalyzerImpl(database, sql)); + } + + @Override + public NativeQuerySpec select(String sql, Object... args) { + return new NativeQuerySpecImpl<>(this, sql, args, DefaultRecord::new, false); + } + + @Override + public NativeQuerySpec select(String sql, + Supplier newInstance, + Object... args) { + NativeQuerySpecImpl impl = new NativeQuerySpecImpl<>( + this, sql, args, map -> FastBeanCopier.copy(map, newInstance), true); + impl.setMapBuilder(ToHumpMap::new); + return impl; + } + + @Override + public SelectColumnMapperSpec select(Class resultType) { + return new QuerySpec<>(resultType, this); + } + + @Override + public SelectSpec select(Class resultType, Consumer> mapperSpec) { + QuerySpec querySpec = new QuerySpec<>(resultType, this); + + mapperSpec.accept(querySpec); + + return querySpec; + } + + 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")); + } + return database + .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() + "]")); + } + + Table parseTableName(Class type) { + Table table = AnnotatedElementUtils.findMergedAnnotation(type, Table.class); + if (null == table) { + throw new UnsupportedOperationException("type [" + type.getName() + "] not found @Table annotation"); + } + return table; + } + + @SafeVarargs + private static T[] toArray(T... arr) { + return arr; + } + + static class NativeQuerySpecImpl extends MapResultWrapper implements NativeQuerySpec { + + ContextView logContext = Context.empty(); + + private final DefaultQueryHelper parent; + private final QueryAnalyzer analyzer; + private final Object[] args; + + private final Function, R> mapper; + + + private QueryParamEntity param; + + + NativeQuerySpecImpl(DefaultQueryHelper parent, + String sql, + Object[] args, + Function, R> mapper, + boolean nest) { + this.parent = parent; + this.analyzer = parent.analysis(sql); + this.args = args; + this.mapper = mapper; + setWrapperNestObject(nest); + } + + @Override + public void wrapColumn(ColumnWrapperContext> context) { + Map instance = context.getRowInstance(); + String column = context.getColumnLabel(); + QueryAnalyzer.Column col = analyzer.findColumn(column).orElse(null); + + if (col != null && !analyzer.columnIsExpression(column, context.getColumnIndex())) { + 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())); + } + } + + + @Override + public NativeQuerySpec logger(Logger logger) { + this.logContext = Context.of(Logger.class, logger); + return this; + } + + @Override + 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); + } + + @Override + public ExecuteSpec where(QueryParamEntity param) { + this.param = param; + return this; + } + + @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(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 + public Mono> fetchPaged() { + if (param == null) { + return fetchPaged(0, 25); + } + 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()); + + return paginator.doPaging(sql, pageIndex, pageSize).toRequest(); + } + + @Override + public Mono> fetchPaged(int pageIndex, int pageSize) { + 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); + + ReactiveSqlExecutor sqlExecutor = parent.database.sql().reactive(); + + if (param.getTotal() != null) { + return sqlExecutor + .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), + sqlExecutor + .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 -> { + 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); + } + } + + static abstract class ColumnMapping { + final QuerySpec parent; + + public ColumnMapping(QuerySpec parent) { + this.parent = parent; + } + + abstract SelectColumnSupplier[] forSelect(); + + abstract boolean match(String[] column); + + abstract void applyValue(R result, String[] column, Object sqlValue); + + static class All extends ColumnMapping { + private final String table; + private final Class tableType; + private TableOrViewMetadata target; + + private final String alias; + + private final String targetProperty; + + private final ResolvableType propertyType; + + @SneakyThrows + public All(QuerySpec parent, + String table, + Class tableType, + Setter setter) { + super(parent); + this.table = table; + this.tableType = tableType; + this.targetProperty = setter == null ? null : MethodReferenceConverter.convertToColumn(setter); + if (this.targetProperty != null) { + 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; + } + String prefix = targetProperty == null ? "all" : targetProperty; + int size = parent.mappings.size(); + this.alias = size == 0 ? prefix : prefix + "_" + size; + } + + boolean propertyTypeIsCollection() { + return propertyType != null && Collection.class.isAssignableFrom(propertyType.toClass()); + } + + @Override + boolean match(String[] column) { + return column.length >= 2 && Objects.equals(alias, column[0]); + } + + @Override + void applyValue(R result, String[] column, Object sqlValue) { + + if (column.length > 1) { + RDBColumnMetadata metadata = target.getColumn(column[1]).orElse(null); + + if (metadata != null) { + ObjectPropertyOperator operator = GlobalConfig.getPropertyOperator(); + if (targetProperty == null) { + operator.setProperty(result, column[1], metadata.decode(sqlValue)); + } else { + Object val = operator.getPropertyOrNew(result, targetProperty); + operator.setProperty(val, column[1], metadata.decode(sqlValue)); + } + } + + } + } + + 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); + } + + JoinConditionalSpecImpl getJoin() { + if (this.table != null) { + return parent.getJoinByAlias(this.table); + } else { + return parent.getJoinByClass(tableType); + } + } + + @Override + SelectColumnSupplier[] forSelect() { + if (propertyTypeIsCollection()) { + return new SelectColumnSupplier[0]; + } + //查询主表 + if (tableType == parent.from) { + return toColumns(this.target = parent.table, null); + } + + //join表 + JoinConditionalSpecImpl join = getJoin(); + + this.target = join.main; + + return toColumns(this.target, join.alias); + } + } + + static class Default extends ColumnMapping { + private final String column; + private String alias; + private final Getter getter; + private final Setter setter; + RDBColumnMetadata metadata; + + public Default(QuerySpec parent, + String column, + Getter getter, + String alias, + Setter setter) { + super(parent); + this.column = column; + this.alias = alias; + this.getter = getter; + this.setter = setter; + } + + @Override + boolean match(String[] column) { + return column.length == 1 && Objects.equals(alias, column[0]); + } + + @Override + void applyValue(R result, String[] column, Object sqlValue) { + if (setter != null) { + setter.accept(result, (V) metadata.decode(sqlValue)); + return; + } + GlobalConfig.getPropertyOperator().setProperty(result, column[0], metadata.decode(sqlValue)); + } + + @Override + SelectColumnSupplier[] forSelect() { + this.alias = this.alias != null ? + this.alias : MethodReferenceConverter.convertToColumn(setter); + + if (column != null) { + String[] nestMaybe = column.split("[.]"); + if (nestMaybe.length == 2) { + JoinConditionalSpecImpl join = parent.getJoinByAlias(nestMaybe[0]); + + metadata = getColumn(join.main, nestMaybe[1]); + } else { + metadata = getColumn(parent.table, column); + } + return toArray(Selects.column(column).as(alias)); + + } else if (getter != null) { + + MethodReferenceInfo info = MethodReferenceConverter.parse(getter); + //查主表 + if (info.getOwner() == parent.from) { + metadata = getColumn(parent.table, info.getColumn()); + return toArray(Selects.column(info.getColumn()).as(alias)); + } else { + JoinConditionalSpecImpl join = parent.getJoinByClass(info.getOwner()); + metadata = getColumn(join.main, info.getColumn()); + return toArray(Selects.column(join.alias + "." + info.getColumn()).as(alias)); + } + + } + throw new IllegalArgumentException("column or getter can not be null"); + } + } + } + + @Slf4j + static class QuerySpec implements SelectSpec, FromSpec, SortSpec, ResultWrapper, SelectColumnMapperSpec { + + private final Class clazz; + + private final DefaultQueryHelper parent; + + private final List> mappings = new ArrayList<>(); + + private TableOrViewMetadata table; + + private Class from; + + private int joinIndex; + private QueryOperator query; + + private List joins; + + private QueryParamEntity param; + final ContextView logContext; + + private Function, Flux> resultHandler = Function.identity(); + + public QuerySpec(Class clazz, DefaultQueryHelper parent) { + this.clazz = EntityFactoryHolder.getMappedType(clazz); + this.parent = parent; + logContext = Context.of(Logger.class, LoggerFactory.getLogger(clazz)); + } + + private List joins() { + return joins == null ? joins = new ArrayList<>(3) : joins; + } + + private JoinConditionalSpecImpl getJoinByClass(Class clazz) { + + if (joins != null) { + for (JoinConditionalSpecImpl join : joins) { + if (Objects.equals(join.mainClass, clazz)) { + return join; + } + } + } + + throw new IllegalArgumentException("join class [" + clazz + "] not found!"); + } + + private JoinConditionalSpecImpl getJoinByAlias(String alias) { + if (joins != null) { + for (JoinConditionalSpecImpl join : joins) { + if (Objects.equals(join.alias, alias)) { + return join; + } + } + } + + throw new IllegalArgumentException("join alias [" + alias + "] not found!"); + } + + @Override + public FromSpec from(Class clazz) { + query = parent + .database + .dml() + .query(table = parent.getTable(from = clazz)); + return this; + } + + private QueryOperator createQuery() { + QueryOperator query = this.query.clone(); + for (ColumnMapping mapping : mappings) { + query.select(mapping.forSelect()); + } + return query; + + } + + @Override + public Mono count() { + BuildParameterQueryOperator operator = (BuildParameterQueryOperator) query.clone(); + operator.getParameter().setPageIndex(null); + operator.getParameter().setPageSize(null); + operator.getParameter().setOrderBy(new ArrayList<>()); + return operator + .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); + } + + @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); + } + 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.getTotal() != null) { + return createQuery() + .paging(param.getPageIndex(), param.getPageSize()) + .fetch(this) + .reactive() + .as(resultHandler) + .collectList() + .map(list -> PagerResult.of(param.getTotal(), list, param)) + .contextWrite(logContext); + } + + if (param.isParallelPager()) { + return Mono.zip(count(), + createQuery() + .paging(param.getPageIndex(), param.getPageSize()) + .fetch(this) + .reactive() + .as(resultHandler) + .collectList(), + (total, list) -> PagerResult.of(total, list, param)) + .contextWrite(logContext); + } + + + return this + .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 + public SortSpec where(QueryParamEntity param) { + query.setParam(this.param = refactorParam(param.clone())); + return this; + } + + private QueryParamEntity refactorParam(QueryParamEntity param) { + + for (Term term : param.getTerms()) { + refactorTerm(term); + } + + return param; + } + + private void refactorTerm(Term term) { + term.setColumn(refactorColumn(term.getColumn())); + } + + @Override + @SuppressWarnings("all") + public SortSpec where(Consumer> dsl) { + + query.where(c -> dsl.accept(new ConditionalImpl(this, c))); + + return this; + } + + private String createJoinAlias() { + return "j_" + (joinIndex++); + } + + public JoinSpec join(Class type, + String alias, + JoinType joinType, + Consumer> on) { + TableOrViewMetadata joinTable = parent.getTable(type); + + Query condition = QueryParamEntity.newQuery(); + + JoinConditionalSpecImpl spec = new JoinConditionalSpecImpl( + this, + type, + joinTable, + alias, + condition + ); + + joins().add(spec); + + on.accept(spec); + + QueryParamEntity param = condition.getParam(); + + for (ColumnMapping mapping : mappings) { + if (mapping instanceof ColumnMapping.All) { + // 1对多 + ColumnMapping.All all = (ColumnMapping.All) mapping; + if (all.propertyTypeIsCollection()) { + if (all.tableType == null) { + if (Objects.equals(all.table, spec.alias)) { + buildOnToMany(param, spec, all); + return this; + } + } else if (all.tableType == type) { + buildOnToMany(param, spec, all); + return this; + } + } + } + } + + Join join = new Join(); + join.setAlias(spec.alias); + join.setTerms(param.getTerms()); + join.setType(joinType); + join.setTarget(spec.main.getFullName()); + + query.join(join); + return this; + + } + + class Joiner { + private final List terms; + + private final List joinTerms = new ArrayList<>(); + + public Joiner(List terms) { + this.terms = terms; + prepare(terms); + } + + public void prepare(List terms) { + for (Term term : terms) { + if (Objects.equals(TermType.eq, term.getTermType()) + && term.getValue() instanceof JoinConditionalSpecImpl.ColumnRef) { + joinTerms.add(term); + } + if (term.getTerms() != null) { + prepare(term.getTerms()); + } + } + } + + + private Function, Flux> buildHandler(JoinConditionalSpecImpl join, + ColumnMapping.All mapping) { + if (joinTerms.size() == 1) { + 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); + } + + private List refactorTerms(R main) { + return refactorTerms(terms.stream().map(Term::clone).collect(Collectors.toList()), main); + } + + private List refactorTerms(List terms, R main) { + for (Term term : terms) { + refactorTerms(main, term); + if (CollectionUtils.isNotEmpty(term.getTerms())) { + refactorTerms(term.getTerms(), main); + } + } + return terms; + } + + 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(); + + Object value = FastBeanCopier.getProperty(main, mainProperty); + if (value == null) { + term.setTermType(TermType.isnull); + term.setValue(1); + } else { + term.setValue(value); + } + } + } + + private Function, Flux> buildBatchHandler(JoinConditionalSpecImpl join, + ColumnMapping.All mapping) { + Term term = joinTerms.get(0); + JoinConditionalSpecImpl.ColumnRef ref = (JoinConditionalSpecImpl.ColumnRef) term.getValue(); + + String joinProperty = term.getColumn(); + 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) + ); + } + + } + + private void buildOnToMany(QueryParamEntity param, JoinConditionalSpecImpl join, ColumnMapping.All mapping) { + + this.resultHandler = this.resultHandler.andThen(new Joiner(param.getTerms()).buildHandler(join, mapping)); + } + + @Override + public JoinSpec fullJoin(Class type, Consumer> on) { + return join(type, createJoinAlias(), JoinType.full, on); + } + + @Override + public JoinSpec leftJoin(Class type, Consumer> on) { + return join(type, createJoinAlias(), JoinType.left, on); + } + + @Override + public JoinSpec innerJoin(Class type, Consumer> on) { + return join(type, createJoinAlias(), JoinType.inner, on); + } + + @Override + public JoinSpec rightJoin(Class type, Consumer> on) { + return join(type, createJoinAlias(), JoinType.right, on); + } + + @SneakyThrows + public R newRowInstance0() { + return clazz.getConstructor().newInstance(); + } + + @Override + @SneakyThrows + public R newRowInstance() { + return EntityFactoryHolder.newInstance(clazz, this::newRowInstance0); + } + + @Override + public void wrapColumn(ColumnWrapperContext context) { + if (context.getResult() == null) { + return; + } + String[] column = context.getColumnLabel().split("[.]"); + ColumnMapping mapping = getMappingByColumn(column); + if (null == mapping) { + return; + } + + mapping.applyValue(context.getRowInstance(), column, context.getResult()); + + } + + @Override + public boolean completedWrapRow(R result) { + return true; + } + + @Override + public R getResult() { + throw new UnsupportedOperationException(); + } + + public ColumnMapping getMappingByColumn(String[] column) { + for (ColumnMapping mapping : mappings) { + if (mapping.match(column)) { + return mapping; + } + } + return null; + } + + + @Override + public SelectColumnMapperSpec all(Class joinType) { + mappings.add(new ColumnMapping.All<>(this, null, joinType, null)); + return this; + } + + @Override + public SelectColumnMapperSpec all(Class joinType, Setter setter) { + mappings.add(new ColumnMapping.All<>(this, null, joinType, setter)); + return this; + } + + @Override + public SelectColumnMapperSpec all(String table) { + mappings.add(new ColumnMapping.All<>(this, table, null, null)); + return this; + } + + @Override + public SelectColumnMapperSpec all(String table, Setter setter) { + mappings.add(new ColumnMapping.All<>(this, table, null, setter)); + return this; + } + + @Override + public SelectColumnMapperSpec as(Getter column, Setter target) { + + mappings.add(new ColumnMapping.Default<>(this, null, column, null, target)); + return this; + } + + @Override + public SelectColumnMapperSpec as(Getter getter, String target) { + mappings.add(new ColumnMapping.Default<>(this, null, getter, target, null)); + return this; + } + + @Override + public SelectColumnMapperSpec as(String column, Setter target) { + mappings.add(new ColumnMapping.Default<>(this, column, null, null, target)); + return this; + } + + @Override + public SelectColumnMapperSpec as(String column, String target) { + mappings.add(new ColumnMapping.Default<>(this, column, null, target, null)); + return this; + } + + + @Override + public SortSpec orderBy(String column, SortOrder.Order order) { + SortOrder sortOrder = new SortOrder(); + sortOrder.setColumn(column); + sortOrder.setOrder(order); + query.orderBy(sortOrder); + return this; + } + + @Override + public SortSpec orderBy(Getter column, SortOrder.Order order) { + + MethodReferenceInfo referenceInfo = MethodReferenceConverter.parse(column); + if (referenceInfo.getOwner() == from) { + return orderBy(referenceInfo.getColumn(), order); + } + JoinConditionalSpecImpl join = getJoinByClass(referenceInfo.getOwner()); + + return orderBy(join.alias + "." + referenceInfo.getColumn(), order); + } + + public String refactorColumn(String column) { + if (null == column) { + return null; + } + if (column.contains(".")) { + String[] joinColumn = column.split("[.]"); + for (ColumnMapping mapping : mappings) { + if (mapping instanceof ColumnMapping.All) { + //传递的是property + if (Objects.equals(joinColumn[0], ((ColumnMapping.All) mapping).targetProperty)) { + JoinConditionalSpecImpl join = ((ColumnMapping.All) mapping).getJoin(); + joinColumn[0] = join.alias; + return String.join(".", joinColumn); + } + } + } + } + return column; + } + } + + @AllArgsConstructor + static class JoinConditionalSpecImpl implements JoinConditionalSpec { + private final QuerySpec parent; + private final Class mainClass; + private final TableOrViewMetadata main; + private String alias; + private final Conditional target; + + @SuppressWarnings("all") + private Class mainClassSafe() { + return (Class) mainClass; + } + + @Override + public JoinConditionalSpecImpl applyColumn(StaticMethodReferenceColumn mainColumn, + String termType, + String alias, + StaticMethodReferenceColumn joinColumn) { + MethodReferenceInfo main = MethodReferenceConverter.parse(mainColumn); + MethodReferenceInfo join = MethodReferenceConverter.parse(joinColumn); + + //mainColumn是主表的列 + if (main.getOwner() == parent.from) { + return applyColumn(join.getColumn(), termType, parent.table, parent.table.getName(), mainColumn.getColumn()); + } + //join为主表 + if (join.getOwner() == parent.from) { + return applyColumn(mainColumn.getColumn(), termType, parent.table, parent.table.getName(), join.getColumn()); + } + + JoinConditionalSpecImpl spec = alias == null ? parent.getJoinByClass(join.getOwner()) : parent.getJoinByAlias(alias); + + return applyColumn(mainColumn.getColumn(), termType, spec.main, spec.alias, join.getColumn()); + } + + @Override + public JoinConditionalSpecImpl applyColumn(StaticMethodReferenceColumn mainColumn, + String termType, + StaticMethodReferenceColumn joinColumn) { + return applyColumn(mainColumn, termType, null, joinColumn); + } + + public JoinConditionalSpecImpl applyColumn(String mainColumn, + String termType, + TableOrViewMetadata join, + String alias, + String column) { + + RDBColumnMetadata columnMetadata = join + .getColumn(column) + .orElseThrow(() -> new IllegalArgumentException("column [" + column + "] not found")); + + getAccepter().accept(mainColumn, termType, new ColumnRef(columnMetadata, alias)); + + return this; + } + + @AllArgsConstructor + @lombok.Getter + public static class ColumnRef implements NativeSql { + private final RDBColumnMetadata column; + private final String alias; + + @Override + public String getSql() { + return column.getFullName(alias); + } + } + + @Override + public JoinNestConditionalSpec nest() { + Term term = new Term(); + term.setType(Term.Type.and); + target.accept(term); + + return new JoinNestConditionalSpecImpl<>(parent, this, term); + } + + @Override + public JoinNestConditionalSpec orNest() { + Term term = new Term(); + term.setType(Term.Type.or); + target.accept(term); + + return new JoinNestConditionalSpecImpl<>(parent, this, term); + } + + @Override + public JoinConditionalSpecImpl and() { + target.and(); + return this; + } + + @Override + public JoinConditionalSpecImpl or() { + target.or(); + return this; + } + + @Override + public JoinConditionalSpecImpl and(String column, String termType, Object value) { + target.and(column, termType, value); + return this; + } + + @Override + public JoinConditionalSpecImpl or(String column, String termType, Object value) { + target.or(column, termType, value); + return this; + } + + @Override + public Accepter getAccepter() { + return ((column, termType, value) -> { + target.getAccepter().accept(column, termType, value); + return this; + }); + } + + @Override + public JoinConditionalSpecImpl accept(Term term) { + target.accept(term); + return this; + } + + @Override + public JoinConditionalSpecImpl alias(String alias) { + this.alias = alias; + return this; + } + + + } + + static class JoinNestConditionalSpecImpl + extends SimpleNestConditional implements JoinNestConditionalSpec { + final QuerySpec parent; + + private final Term term; + + public JoinNestConditionalSpecImpl(QuerySpec parent, T target, Term term) { + super(target, term); + this.parent = parent; + this.term = term; + } + + @Override + public NestConditional accept(String column, String termType, Object value) { + return getAccepter().accept(parent.refactorColumn(column), termType, value); + } + + @Override + @SuppressWarnings("all") + public JoinNestConditionalSpecImpl nest() { + return new JoinNestConditionalSpecImpl<>(parent, this, term.nest()); + } + + @Override + @SuppressWarnings("all") + public JoinNestConditionalSpecImpl orNest() { + return new JoinNestConditionalSpecImpl<>(parent, this, term.orNest()); + } + + @Override + public JoinNestConditionalSpecImpl applyColumn(StaticMethodReferenceColumn joinColumn, + String termType, + String alias, + StaticMethodReferenceColumn mainOrJoinColumn) { + MethodReferenceInfo main = MethodReferenceConverter.parse(joinColumn); + MethodReferenceInfo join = MethodReferenceConverter.parse(joinColumn); + + //mainColumn是主表的列 + if (main.getOwner() == parent.from) { + return applyColumn(join.getColumn(), termType, parent.table, parent.table.getName(), joinColumn.getColumn()); + } + //join为主表 + if (join.getOwner() == parent.from) { + return applyColumn(joinColumn.getColumn(), termType, parent.table, parent.table.getName(), join.getColumn()); + } + + JoinConditionalSpecImpl spec = alias == null ? parent.getJoinByClass(join.getOwner()) : parent.getJoinByAlias(alias); + + return applyColumn(joinColumn.getColumn(), termType, spec.main, spec.alias, join.getColumn()); + } + + @Override + public JoinNestConditionalSpecImpl applyColumn(StaticMethodReferenceColumn mainColumn, + String termType, + StaticMethodReferenceColumn joinColumn) { + return applyColumn(joinColumn, termType, null, joinColumn); + } + + + 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")); + + getAccepter().accept(mainColumn, termType, new JoinConditionalSpecImpl.ColumnRef(columnMetadata, alias)); + + return this; + } + + @Override + public Accepter, Object> getAccepter() { + return (column, termType, value) -> { + super.getAccepter().accept(column, termType, value); + return this; + }; + } + } + + static class NestConditionalImpl extends SimpleNestConditional { + final QuerySpec parent; + + final Term term; + + public NestConditionalImpl(QuerySpec parent, T target, Term term) { + super(target, term); + this.parent = parent; + this.term = term; + } + + @Override + public NestConditional> nest() { + return new NestConditionalImpl<>(parent, this, term.nest()); + } + + @Override + public NestConditional> orNest() { + return new NestConditionalImpl<>(parent, this, term.orNest()); + } + + @Override + public NestConditional accept(String column, String termType, Object value) { + return super.accept(parent.refactorColumn(column), termType, value); + } + + @Override + public NestConditional accept(MethodReferenceColumn column, String termType) { + MethodReferenceInfo info = MethodReferenceConverter.parse(column); + if (info.getOwner() == parent.from) { + return super.accept(column, termType); + } + JoinConditionalSpecImpl join = parent.getJoinByClass(info.getOwner()); + return super.accept(join.alias + "." + info.getColumn(), termType, column.get()); + } + + @Override + public NestConditional accept(StaticMethodReferenceColumn column, String termType, Object value) { + MethodReferenceInfo info = MethodReferenceConverter.parse(column); + if (info.getOwner() == parent.from) { + return super.accept(column, termType, value); + } + JoinConditionalSpecImpl join = parent.getJoinByClass(info.getOwner()); + + super.accept(join.alias + "." + info.getColumn(), termType, value); + return this; + } + + } + + @AllArgsConstructor + static class ConditionalImpl> implements Conditional { + final QuerySpec parent; + + final Conditional real; + + @Override + public NestConditional nest() { + Term term = new Term(); + term.setType(Term.Type.and); + real.accept(term); + + return new NestConditionalImpl<>(parent, (T) this, term); + } + + @Override + public NestConditional orNest() { + Term term = new Term(); + term.setType(Term.Type.or); + real.accept(term); + return new NestConditionalImpl<>(parent, (T) this, term); + } + + @Override + public T and() { + real.and(); + return castSelf(); + } + + @Override + public T or() { + real.or(); + return castSelf(); + } + + @Override + public T and(String column, String termType, Object value) { + real.and(column, termType, value); + return castSelf(); + } + + @Override + public T or(String column, String termType, Object value) { + real.or(column, termType, value); + return castSelf(); + } + + @Override + public T accept(String column, String termType, Object value) { + return Conditional.super.accept(parent.refactorColumn(column), termType, value); + } + + @Override + public T accept(MethodReferenceColumn column, String termType) { + MethodReferenceInfo info = MethodReferenceConverter.parse(column); + if (info.getOwner() == parent.from) { + return Conditional.super.accept(column, termType); + } + JoinConditionalSpecImpl join = parent.getJoinByClass(info.getOwner()); + + return getAccepter().accept(join.alias + "." + info.getColumn(), termType, column.get()); + } + + @Override + public T accept(StaticMethodReferenceColumn column, String termType, Object value) { + MethodReferenceInfo info = MethodReferenceConverter.parse(column); + if (info.getOwner() == parent.from) { + return Conditional.super.accept(column, termType, value); + } + JoinConditionalSpecImpl join = parent.getJoinByClass(info.getOwner()); + + return getAccepter().accept(join.alias + "." + info.getColumn(), termType, value); + } + + @Override + public Accepter getAccepter() { + return (column, termType, value) -> { + real.getAccepter().accept(column, termType, value); + return castSelf(); + }; + } + + @Override + public T accept(Term term) { + real.accept(term); + return castSelf(); + } + } +} diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/JoinConditionalSpec.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/JoinConditionalSpec.java new file mode 100644 index 000000000..1458c8050 --- /dev/null +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/JoinConditionalSpec.java @@ -0,0 +1,38 @@ +package org.hswebframework.web.crud.query; + +import org.hswebframework.ezorm.core.Conditional; +import org.hswebframework.ezorm.core.StaticMethodReferenceColumn; + +public interface JoinConditionalSpec> extends JoinOnSpec, Conditional { + + @Override + JoinNestConditionalSpec nest(); + + @Override + JoinNestConditionalSpec orNest(); + + /** + * 使用方法引用定义join表别名。 + * + *
{@code
+     * // join t_detail detail ....
+     *  alias(MyEntity.getDetail)
+     * }
+ * + * @param alias 别名 + * @return this + */ + default C alias(StaticMethodReferenceColumn alias) { + return alias(alias.getColumn()); + } + + /** + * 定义join表别名,在后续列转换和条件中可以使用别名进行引用。 + * + * @param alias 别名 + * @return this + */ + C alias(String alias); + + +} 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 new file mode 100644 index 000000000..f1b7b5654 --- /dev/null +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/JoinNestConditionalSpec.java @@ -0,0 +1,16 @@ +package org.hswebframework.web.crud.query; + +import org.hswebframework.ezorm.core.NestConditional; +import org.hswebframework.ezorm.core.TermTypeConditionalSupport; + +public interface JoinNestConditionalSpec + extends JoinOnSpec>, NestConditional { + + @Override + JoinNestConditionalSpec> nest(); + + @Override + JoinNestConditionalSpec> orNest(); + + +} diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/JoinOnSpec.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/JoinOnSpec.java new file mode 100644 index 000000000..2ef41d620 --- /dev/null +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/JoinOnSpec.java @@ -0,0 +1,261 @@ +package org.hswebframework.web.crud.query; + +import org.hswebframework.ezorm.core.StaticMethodReferenceColumn; +import org.hswebframework.ezorm.core.TermTypeConditionalSupport; +import org.hswebframework.ezorm.core.param.TermType; + +public interface JoinOnSpec { + + /** + * 设置 join on = 条件 + *
{@code
+     *   // join detail d on d.id = t.id
+     *    is(DetailEntity::getId,MyEntity::getId)
+     * }
+ * + * @param joinColumn 关联表列 + * @param mainOrJoinColumn 主表或者其他关联表列 + * @param T + * @param T2 + * @return this + */ + default Self is(StaticMethodReferenceColumn joinColumn, StaticMethodReferenceColumn mainOrJoinColumn) { + return applyColumn(joinColumn, TermType.eq, mainOrJoinColumn); + } + + /** + * 设置 join on = 条件 + *
{@code
+     *   // join detail d on d.id = d2.id
+     *    is("id","d2",MyEntity::getId)
+     * }
+ * + * @param joinColumn 关联表列 + * @param mainOrJoinColumn 主表或者其他关联表列 + * @param alias 另外一个join表的别名 + * @param T + * @param T2 + * @return this + */ + default Self is(StaticMethodReferenceColumn joinColumn, + String alias, + StaticMethodReferenceColumn mainOrJoinColumn) { + return applyColumn(joinColumn, TermType.eq, alias, mainOrJoinColumn); + } + + /** + * 设置 join on != 条件 + *
{@code
+     *   // join detail d on d.id != t.id
+     *    not(DetailEntity::getId,MyEntity::getId)
+     * }
+ * + * @param joinColumn 关联表列 + * @param mainOrJoinColumn 主表或者其他关联表列 + * @param T + * @param T2 + * @return this + */ + default Self not(StaticMethodReferenceColumn joinColumn, StaticMethodReferenceColumn mainOrJoinColumn) { + return applyColumn(joinColumn, TermType.not, mainOrJoinColumn); + } + + /** + * 设置 join on != 条件 + *
{@code
+     *   // join detail d on d.id != d2.id
+     *    not("id","d2",MyEntity::getId)
+     * }
+ * + * @param joinColumn 关联表列 + * @param mainOrJoinColumn 主表或者其他关联表列 + * @param alias 另外一个join表的别名 + * @param T + * @param T2 + * @return this + */ + default Self not(StaticMethodReferenceColumn joinColumn, + String alias, + StaticMethodReferenceColumn mainOrJoinColumn) { + return applyColumn(joinColumn, TermType.not, alias, mainOrJoinColumn); + } + + /** + * 设置 join on > 条件 + *
{@code
+     *   // join detail d on d.max_age > t.age
+     *    gt(DetailEntity::getMaxAge,MyEntity::getAge)
+     * }
+ * + * @param joinColumn 关联表列 + * @param mainOrJoinColumn 主表或者其他关联表列 + * @param T + * @param T2 + * @return this + */ + default Self gt(StaticMethodReferenceColumn joinColumn, StaticMethodReferenceColumn mainOrJoinColumn) { + return applyColumn(joinColumn, TermType.gt, mainOrJoinColumn); + } + + /** + * 设置 join on > 条件 + *
{@code
+     *   // join detail d on d.max_age > t2.age
+     *    gt(DetailEntity::getMaxAge,"t2",MyEntity::getAge)
+     * }
+ * + * @param joinColumn 关联表列 + * @param mainOrJoinColumn 主表或者其他关联表列 + * @param alias 另外一个join表的别名 + * @param T + * @param T2 + * @return this + */ + default Self gt(StaticMethodReferenceColumn joinColumn, String alias, StaticMethodReferenceColumn mainOrJoinColumn) { + return applyColumn(joinColumn, TermType.gt, alias, mainOrJoinColumn); + } + + + /** + * 设置 join on >= 条件 + *
{@code
+     *   // join detail d on d.max_age >= t.age
+     *    gte(DetailEntity::getMaxAge,MyEntity::getAge)
+     * }
+ * + * @param joinColumn 关联表列 + * @param mainOrJoinColumn 主表或者其他关联表列 + * @param T + * @param T2 + * @return this + */ + default Self gte(StaticMethodReferenceColumn joinColumn, StaticMethodReferenceColumn mainOrJoinColumn) { + return applyColumn(joinColumn, TermType.gte, mainOrJoinColumn); + } + + /** + * 设置 join on >= 条件 + *
{@code
+     *   // join detail d on d.max_age >= t2.age
+     *    gte(DetailEntity::getMaxAge,"t2",MyEntity::getAge)
+     * }
+ * + * @param joinColumn 关联表列 + * @param mainOrJoinColumn 主表或者其他关联表列 + * @param alias 另外一个join表的别名 + * @param T + * @param T2 + * @return this + */ + default Self gte(StaticMethodReferenceColumn joinColumn, String alias, StaticMethodReferenceColumn mainOrJoinColumn) { + return applyColumn(joinColumn, TermType.gte, alias, mainOrJoinColumn); + } + + + /** + * 设置 join on < 条件 + *
{@code
+     *   // join detail d on d.max_age < t.age
+     *    lt(DetailEntity::getMaxAge,MyEntity::getAge)
+     * }
+ * + * @param joinColumn 关联表列 + * @param mainOrJoinColumn 主表或者其他关联表列 + * @param T + * @param T2 + * @return this + */ + default Self lt(StaticMethodReferenceColumn joinColumn, StaticMethodReferenceColumn mainOrJoinColumn) { + return applyColumn(joinColumn, TermType.lt, mainOrJoinColumn); + } + + /** + * 设置 join on < 条件 + *
{@code
+     *   // join detail d on d.max_age < t2.age
+     *    lt(DetailEntity::getMaxAge,"t2",MyEntity::getAge)
+     * }
+ * + * @param joinColumn 关联表列 + * @param mainOrJoinColumn 主表或者其他关联表列 + * @param alias 另外一个join表的别名 + * @param T + * @param T2 + * @return this + */ + default Self lt(StaticMethodReferenceColumn joinColumn, String alias, StaticMethodReferenceColumn mainOrJoinColumn) { + return applyColumn(joinColumn, TermType.lt, alias, mainOrJoinColumn); + } + + + /** + * 设置 join on <= 条件 + *
{@code
+     *   // join detail d on d.max_age <= t.age
+     *    lte(DetailEntity::getMaxAge,MyEntity::getAge)
+     * }
+ * + * @param joinColumn 关联表列 + * @param mainOrJoinColumn 主表或者其他关联表列 + * @param T + * @param T2 + * @return this + */ + default Self lte(StaticMethodReferenceColumn joinColumn, StaticMethodReferenceColumn mainOrJoinColumn) { + return applyColumn(joinColumn, TermType.lte, mainOrJoinColumn); + } + + /** + * 设置 join on <= 条件 + *
{@code
+     *   // join detail d on d.max_age <= t2.age
+     *    lte(DetailEntity::getMaxAge,"t2",MyEntity::getAge)
+     * }
+ * + * @param joinColumn 关联表列 + * @param mainOrJoinColumn 主表或者其他关联表列 + * @param alias 另外一个join表的别名 + * @param T + * @param T2 + * @return this + */ + default Self lte(StaticMethodReferenceColumn joinColumn, String alias, StaticMethodReferenceColumn mainOrJoinColumn) { + return applyColumn(joinColumn, TermType.lte, alias, mainOrJoinColumn); + } + + /** + * 设置 join on 字段关联条件 + *
{@code
+     *   // join on t.age > d.max_age
+     *    applyColumn(MyEntity::getAge,"gt",Detail::getMaxAge)
+     * }
+ * + * @param joinColumn 列名,可以为其他关联表的列名 + * @param termType 条件类型 {@link TermType} {@link org.hswebframework.ezorm.rdb.operator.builder.fragments.TermFragmentBuilder#getId() } + * @param mainOrJoinColumn 关联表列名 + * @return this + */ + Self applyColumn(StaticMethodReferenceColumn joinColumn, + String termType, + StaticMethodReferenceColumn mainOrJoinColumn); + + /** + * 设置 join on 字段关联条件 + *
{@code
+     *   // join detail d on d.age > d2.max_age
+     *    applyColumn(Detail::getAge,"gt","d2",Detail::getMaxAge)
+     * }
+ * + * @param joinColumn 列名,可以为其他关联表的列名 + * @param termType 条件类型 {@link TermType} {@link org.hswebframework.ezorm.rdb.operator.builder.fragments.TermFragmentBuilder#getId() } + * @param alias 另外一个join表别名 + * @param mainOrJoinColumn 关联表列名 + * @return this + */ + Self applyColumn(StaticMethodReferenceColumn joinColumn, + String termType, + String alias, + StaticMethodReferenceColumn mainOrJoinColumn); + + +} 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 new file mode 100644 index 000000000..d80733b1b --- /dev/null +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/QueryAnalyzer.java @@ -0,0 +1,208 @@ +package org.hswebframework.web.crud.query; + +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.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 查询分析器,用于分析SQL查询语句以及对SQL进行重构,追加查询条件等操作 + * + * @author zhouhao + * @since 4.0.16 + */ +public interface QueryAnalyzer { + + /** + * @return 原始SQL + */ + String originalSql(); + + /** + * 基于{@link QueryParamEntity}动态条件来重构SQL,将根据动态条件添加where条件,排序等操作. + * + * @param entity 查询条件 + * @param args 原始SQL中的预编译参数 + * @return 重构后的SQL + */ + SqlRequest refactor(QueryParamEntity entity, Object... args); + + /** + * 基于{@link QueryParamEntity}动态条件来重构用于查询count的SQL,通常用于分页时查询总数. + *
{@code
+     *  select count(1) _total from .....
+     * }
+ * + * @param entity 查询条件 + * @param args 原始SQL中的预编译参数 + * @return 重构后的SQL + */ + SqlRequest refactorCount(QueryParamEntity entity, Object... args); + + /** + * @return 查询信息 + */ + Select select(); + + /** + * 根据名称或者别名,查找查询语句中的列信息. + * + * @param name 列名、别名或者列全名 + * @return 列信息 + */ + Optional findColumn(String name); + + /** + * 判断查询的列是否为表达式,如使用了函数: sum(num) as num + * + * @param name 列名 + * @param index 列序号 + * @return 是否为表达式 + */ + boolean columnIsExpression(String name, int index); + + /** + * @return 关联表信息 + */ + List joins(); + + @AllArgsConstructor + @Getter + class Join { + + final String alias; + final Type type; + final Table table; + + // final List on; + + enum Type { + left, right, inner + } + } + + @RequiredArgsConstructor + @Getter + class Select { + private transient Map columns; + + final List columnList; + + 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; + } + } + + @Getter + @AllArgsConstructor + class Table { + final String alias; + + final TableOrViewMetadata metadata; + + public Table newAlias(String alias) { + return new Table(alias, metadata); + } + } + + @AllArgsConstructor + @Getter + class Column implements Feature { + static final FeatureId FEATURE_ID = FeatureId.of("AnalyzedColumn"); + + //列名 + String name; + //别名 + String alias; + //所有者 + 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 { + final Map columns; + + public SelectTable(String alias, + Map columns, + TableOrViewMetadata metadata) { + 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 new file mode 100644 index 000000000..cca2fb1a2 --- /dev/null +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/QueryAnalyzerImpl.java @@ -0,0 +1,1186 @@ +package org.hswebframework.web.crud.query; + +import lombok.Getter; +import lombok.SneakyThrows; +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; +import org.hswebframework.ezorm.core.meta.FeatureSupportedMetadata; +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.*; +import org.hswebframework.ezorm.rdb.metadata.dialect.Dialect; +import org.hswebframework.ezorm.rdb.operator.DatabaseOperator; +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; + +import java.util.*; + +import static net.sf.jsqlparser.statement.select.PlainSelect.getFormatedList; +import static org.hswebframework.ezorm.rdb.operator.builder.fragments.TermFragmentBuilder.createFeatureId; + + +class QueryAnalyzerImpl implements FromItemVisitor, SelectItemVisitor, SelectVisitor, QueryAnalyzer { + + private final DatabaseOperator database; + + private String sql; + + private final SelectBody parsed; + + private QueryAnalyzer.Select select; + + 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; + } + + @Override + public SqlRequest refactor(QueryParamEntity entity, Object... args) { + if (injector == null) { + initInjector(); + } + return injector.refactor(entity, args); + } + + @Override + public SqlRequest refactorCount(QueryParamEntity entity, Object... args) { + if (injector == null) { + initInjector(); + } + return injector.refactorCount(entity, args); + } + + @Override + public Select select() { + return select; + } + + @Override + public Optional findColumn(String name) { + return Optional.ofNullable(getColumnMappings().get(name)); + } + + @Override + public List joins() { + return new ArrayList<>(joins.values()); + } + + QueryAnalyzerImpl(DatabaseOperator database, String sql) { + this(database, parse(sql)); + this.sql = sql; + } + + + public boolean columnIsExpression(String name, int index) { + + if (index >= 0 && select.getColumnList().size() > index) { + return select.getColumnList().get(index) instanceof ExpressionColumn; + } + + return select.getColumns().get(name) instanceof ExpressionColumn; + } + + private Map getColumnMappings() { + if (columnMappings == null) { + 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); + if (null != column.getOwner()) { + columnMappings.put(column.getOwner() + "." + column.getName(), column); + columnMappings.put(column.getOwner() + "." + column.getAlias(), column); + } + } + } else { + // 主表 + for (RDBColumnMetadata column : select.table.metadata.getColumns()) { + Column col = new Column(column.getName(), column.getAlias(), select.table.alias, column); + columnMappings.put(column.getName(), col); + columnMappings.put(column.getAlias(), col); + columnMappings.put(select.table.alias + "." + column.getName(), col); + columnMappings.put(select.table.alias + "." + column.getAlias(), col); + } + } + + //关联表 + for (Join join : joins.values()) { + if (join.table instanceof SelectTable) { + for (Column column : select.getColumnList()) { + columnMappings.putIfAbsent(column.getName(), column); + columnMappings.putIfAbsent(column.getAlias(), column); + columnMappings.put(column.getOwner() + "." + column.getName(), column); + columnMappings.put(column.getOwner() + "." + column.getAlias(), column); + } + } else { + for (RDBColumnMetadata column : join.table.metadata.getColumns()) { + Column col = new Column(column.getName(), column.getAlias(), join.alias, column); + columnMappings.putIfAbsent(column.getName(), col); + columnMappings.putIfAbsent(column.getAlias(), col); + + columnMappings.put(join.alias + "." + column.getName(), col); + columnMappings.put(join.alias + "." + column.getAlias(), col); + } + } + + } + } + } + } + return columnMappings; + } + + private Column getColumnOrSelectColumn(String name) { + Column column = select.getColumns().get(name); + + if (column != null) { + return column; + } + column = select.getColumns().get(QueryHelperUtils.toSnake(name)); + if (column != null) { + return column; + } + + return getColumnMappings().get(name); + } + + @SneakyThrows + 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 parent) { + this.database = database; + this.virtualTable.putAll(parent.virtualTable); + if (null != selectBody) { + this.parsed = selectBody; + selectBody.accept(this); + } else { + this.parsed = null; + } + } + + 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.isEmpty()) { + return null; + } + char firstChar = name.charAt(0); + + if (firstChar == '`' || firstChar == '"' || firstChar == '[' || + name.startsWith(database.getMetadata().getDialect().getQuoteStart())) { + + return new String(name.toCharArray(), 1, name.length() - 2); + } + + return 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")); + } 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), + tableMetadata + ); + + select = new QueryAnalyzer.Select(new ArrayList<>(), table); + + } + + // select * from ( select a,b,c from table ) t + @Override + public void visit(SubSelect subSelect) { + 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.name, column.getAlias(), column.owner, column.metadata)); + } + + select = new QueryAnalyzer.Select( + new ArrayList<>(), + new QueryAnalyzer.SelectTable( + parsePlainName(alias), + columnMap, + sub.select.table.metadata + ) + ); + } + + @Override + public void visit(SubJoin subjoin) { + for (net.sf.jsqlparser.statement.select.Join join : subjoin.getJoinList()) { + join.getRightItem().accept(this); + } + } + + @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 + public void visit(AllColumns allColumns) { + putSelectColumns(select.table, select.columnList); + + for (QueryAnalyzer.Join value : new HashSet<>(joins.values())) { + putSelectColumns(value.table, select.columnList); + } + } + + private void putSelectColumns(QueryAnalyzer.Table table, List container) { + + if (table instanceof QueryAnalyzer.SelectTable) { + QueryAnalyzer.SelectTable selectTable = ((QueryAnalyzer.SelectTable) table); + + for (QueryAnalyzer.Column column : selectTable.columns.values()) { + String alias = table == select.table ? column.getAlias() : table.alias + "." + column.getAlias(); + container.add(new QueryAnalyzer.Column( + column.name, + alias, + table.alias, + column.metadata + )); + } + } else { + for (RDBColumnMetadata column : table.metadata.getColumns()) { + String alias = table == select.table ? column.getAlias() : table.alias + "." + column.getAlias(); + + container.add(new QueryAnalyzer.Column( + column.getRealName(), + alias, + table.alias, + column + )); + } + } + } + + @Override + public void visit(AllTableColumns allTableColumns) { + net.sf.jsqlparser.schema.Table table = allTableColumns.getTable(); + + String name = table.getName(); + + if (Objects.equals(select.table.alias, name)) { + putSelectColumns(select.table, select.columnList); + return; + } + + QueryAnalyzer.Join join = joins.get(parsePlainName(table.getName())); + + if (join == null) { + throw new IllegalStateException("table " + table.getName() + " not found in join"); + } + putSelectColumns(join.table, select.columnList); + } + + private QueryAnalyzer.Table getTable(net.sf.jsqlparser.schema.Table table) { + QueryAnalyzer.Table meta; + if (null == table) { + return select.table; + } + String tableName = parsePlainName(table.getName()); + + if (Objects.equals(tableName, select.table.alias)) { + meta = select.table; + } else { + QueryAnalyzer.Join join = joins.get(tableName); + if (join == null) { + throw new IllegalStateException("table " + table + " not found in from or join"); + } + meta = join.table; + } + return meta; + } + + + static class ExpressionColumn extends Column { + + private final SelectItem expr; + + public ExpressionColumn(String alias, String owner, RDBColumnMetadata metadata, SelectItem expr) { + super(alias, alias, owner, metadata); + this.expr = expr; + } + + @Override + public ExpressionColumn moveOwner(String owner) { + return new ExpressionColumn(alias, owner, metadata, expr); + } + } + + private void refactorAlias(Alias alias) { + if (alias != null) { + alias.setName( + database + .getMetadata() + .getDialect() + .quote(parsePlainName(alias.getName()), false) + ); + } + } + + @Override + public void visit(SelectExpressionItem selectExpressionItem) { + Expression expr = selectExpressionItem.getExpression(); + Alias alias = selectExpressionItem.getAlias(); + + if (!(expr instanceof net.sf.jsqlparser.schema.Column)) { + String aliasName = parsePlainName(alias == null ? expr.toString() : alias.getName()); + refactorAlias(alias); + select.columnList.add(new ExpressionColumn(aliasName, null, null, selectExpressionItem)); + + return; + } + net.sf.jsqlparser.schema.Column column = ((net.sf.jsqlparser.schema.Column) expr); + + String columnName = parsePlainName(column.getColumnName()); + + QueryAnalyzer.Table table = getTable(column.getTable()); + + String aliasName = alias == null ? columnName : parsePlainName(alias.getName()); + + RDBColumnMetadata metadata = table + .getMetadata() + .getColumn(columnName) + .orElse(null); + + if (metadata == null) { + if (table instanceof QueryAnalyzer.SelectTable) { + Column c = ((SelectTable) table).columns.get(columnName); + if (null != c) { + if (c.metadata == null) { + select.columnList.add(new QueryAnalyzer.Column(c.getName(), aliasName, table.alias, null)); + return; + } + metadata = c.metadata; + } + } + } + + if (metadata == null) { + throw new IllegalStateException("column [" + column.getColumnName() + "] not found in " + table.metadata.getName()); + } + + select.columnList.add(new QueryAnalyzer.Column(metadata.getRealName(), aliasName, table.alias, metadata)); + + + } + + @Override + public void visit(PlainSelect select) { + + FromItem from = select.getFromItem(); + + if (from == null) { + throw new IllegalArgumentException("select can not be without 'from'"); + } + from.accept(this); + + + List joinList = select.getJoins(); + + if (joinList != null) { + for (net.sf.jsqlparser.statement.select.Join join : joinList) { + FromItem fromItem = join.getRightItem(); + QueryAnalyzerImpl joinAn = new QueryAnalyzerImpl(database, (SelectBody) null, this); + fromItem.accept(joinAn); + + Join.Type type; + if (join.isLeft()) { + type = Join.Type.left; + } else if (join.isRight()) { + type = Join.Type.right; + } else if (join.isInner()) { + type = Join.Type.inner; + } else { + type = null; + } + joins.put(joinAn.select.table.alias, new Join(joinAn.select.table.alias, type, joinAn.select.table)); + } + } + + for (SelectItem selectItem : select.getSelectItems()) { + selectItem.accept(this); + } + } + + @Override + public void visit(SetOperationList setOpList) { + //union + + for (SelectBody body : setOpList.getSelects()) { + body.accept(this); + // break; + } + + + } + + @Override + public void visit(WithItem withItem) { + withItems.add(withItem); + + String name = withItem.getName(); + RDBViewMetadata view = new RDBViewMetadata(); + view.setName(name); + view.setSchema(database.getMetadata().getCurrentSchema()); + virtualTable.put(name, view); + if (withItem.getSubSelect() != null) { + QueryAnalyzerImpl analyzer = new QueryAnalyzerImpl(database, withItem.getSubSelect(), this); + for (Column column : analyzer.select.getColumnList()) { + RDBColumnMetadata metadata; + if (column.getMetadata() == null) { + metadata = new RDBColumnMetadata(); + } else { + metadata = column.metadata.clone(); + } + metadata.setName(column.getName()); + metadata.setAlias(column.getAlias()); + view.addColumn(metadata); + } + } + } + + @Override + public void visit(ValuesStatement aThis) { + + } + + private void initInjector() { + SimpleQueryRefactor injector = new SimpleQueryRefactor(); + parsed.accept(injector); + for (WithItem withItem : withItems) { + withItem.accept(injector); + } + this.injector = injector; + } + + static class QueryAnalyzerTermsFragmentBuilder extends AbstractTermsFragmentBuilder { + + @Override + public SqlFragments createTermFragments(QueryAnalyzerImpl parameter, List terms) { + return super.createTermFragments(parameter, terms); + } + + @Override + public SqlFragments createTermFragments(QueryAnalyzerImpl impl, Term term) { + Dialect dialect = impl.database.getMetadata().getDialect(); + + 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())) { + QueryAnalyzer.Join join = impl.joins.get(col.getOwner()); + if (null != join) { + table = join.table; + } else { + throw new IllegalArgumentException("undefined column [" + column + "]"); + } + } + + FeatureSupportedMetadata metadata = col.metadata; + 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( + 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; + + private String where; + private int prefixParameters; + private String orderBy; + + private String suffix; + private int suffixParameters; + + private boolean fastCount = true; + + private SqlFragments QUERY, SUFFIX, FAST_COUNT, SLOW_COUNT; + + SimpleQueryRefactor() { + + } + + + 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(","); + } + if (column instanceof ExpressionColumn) { + columns.append(((ExpressionColumn) column).expr); + fastCount = false; + continue; + } + + columns.append(column.owner) + .append('.') + .append(dialect.quote(column.name, column.metadata != null && !column.metadata.realNameDetected())) + .append(" as ") + .append(dialect.quote(column.alias, false)); + } + } + + @Override + public void visit(PlainSelect plainSelect) { + + StringBuilder from = new StringBuilder(); + StringBuilder columns = new StringBuilder(); + StringBuilder suffix = new StringBuilder(); + + + if (plainSelect.getDistinct() != null) { + columns.append(plainSelect.getDistinct()) + .append(' '); + fastCount = false; + } + + initColumns(columns); + + if (plainSelect.getFromItem() != null) { + from.append("FROM "); + + from.append(plainSelect.getFromItem()); + PrepareStatementVisitor visitor = new PrepareStatementVisitor(); + plainSelect.getFromItem().accept(visitor); + prefixParameters += visitor.parameterSize; + } + + if (plainSelect.getJoins() != null) { + PrepareStatementVisitor visitor = new PrepareStatementVisitor(); + for (net.sf.jsqlparser.statement.select.Join join : plainSelect.getJoins()) { + if (join.isSimple()) { + from.append(", ").append(join); + } 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); + } + } + } + prefixParameters += visitor.parameterSize; + } + + if (plainSelect.getWhere() != null) { + PrepareStatementVisitor visitor = new PrepareStatementVisitor(); + plainSelect.getWhere().accept(visitor); + prefixParameters += visitor.parameterSize; + where = plainSelect.getWhere().toString(); + } + + if (plainSelect.getOrderByElements() != null) { + orderBy = getFormatedList(plainSelect.getOrderByElements(), ""); + } + + if (plainSelect.getGroupBy() != null) { + fastCount = false; + suffix.append(' ').append(plainSelect.getGroupBy()); + } + suffix.append(' '); + + if (plainSelect.getHaving() != null) { + PrepareStatementVisitor visitor = new PrepareStatementVisitor(); + plainSelect.getHaving().accept(visitor); + suffixParameters = visitor.parameterSize; + suffix.append(" HAVING ").append(plainSelect.getHaving()); + } + + this.columns = columns.toString(); + this.from = from.toString(); + this.suffix = suffix.toString(); + + } + + @Override + public void visit(SetOperationList setOpList) { + StringBuilder from = new StringBuilder(); + StringBuilder columns = new StringBuilder(); + + initColumns(columns); + + from.append("FROM ("); + from.append(setOpList); + from.append(") "); + from.append(select.table.alias); + + this.from = from.toString(); + this.columns = columns.toString(); + this.suffix = ""; + + } + + @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) { + if (prefixParameters == 0) { + return new Object[0]; + } + Assert.isTrue(args.length >= prefixParameters, + "Illegal prepare statement parameter size, expect: " + prefixParameters + ", actual: " + args.length); + + return Arrays.copyOfRange(args, 0, prefixParameters); + } + + public Object[] getSuffixParameters(Object... args) { + if (suffixParameters == 0) { + return new Object[0]; + } + Assert.isTrue(args.length >= suffixParameters + prefixParameters, + "Illegal prepare statement parameter size, expect: " + suffixParameters + prefixParameters + ", actual: " + args.length); + + return Arrays.copyOfRange(args, prefixParameters, suffixParameters + prefixParameters); + } + + @Override + public SqlRequest refactor(QueryParamEntity param, Object... 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); + + sql.addSql(suffix) + .addParameter(getSuffixParameters(args)); + + appendOrderBy(sql, param); + + return sql.toRequest(); + } + + + @Override + public SqlRequest refactorCount(QueryParamEntity param, Object... args) { + BatchSqlFragments sql = new BatchSqlFragments( + StringUtils.hasText(where) ? 10 : 7, 2); + if (SUFFIX == null) { + SUFFIX = SqlFragments.of(suffix); + } + + if (fastCount) { + 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.add(SUFFIX); + } else { + 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.add(SUFFIX); + sql.addSql(") _t"); + } + + return sql + .addParameter(getSuffixParameters(args)) + .toRequest(); + } + + private void appendOrderBy(AppendableSqlFragments sql, QueryParamEntity param) { + + if (CollectionUtils.isNotEmpty(param.getSorts())) { + int index = 0; + BatchSqlFragments orderByValue = null; + BatchSqlFragments orderByColumn = null; + for (Sort sort : param.getSorts()) { + String name = sort.getName(); + Column column = getColumnOrSelectColumn(name); + + if (column == null) { + continue; + } + 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())); + //按固定值排序 + if (sort.getValue() != null) { + if (orderByValue == null) { + orderByValue = new BatchSqlFragments(); + orderByValue.addSql("case"); + } + orderByValue.addSql("when"); + orderByValue.addSql(columnName, "= ?").addParameter(sort.getValue()); + orderByValue.addSql("then").addSql(String.valueOf(desc ? 10000 + index++ : index++)); + } else { + if (orderByColumn == null) { + orderByColumn = new BatchSqlFragments(); + } else { + orderByColumn.addSql(","); + } + //todo function支持 + orderByColumn + .addSql(columnName) + .addSql(desc ? "DESC" : "ASC"); + } + } + + boolean customOrder = (orderByValue != null || orderByColumn != null); + + if (customOrder || orderBy != null) { + sql.addSql("ORDER BY"); + } + //按固定值 + if (orderByValue != null) { + orderByValue.addSql("else 10000 end"); + sql.addFragments(orderByValue); + } + //按列 + if (orderByColumn != null) { + if (orderByValue != null) { + sql.add(SqlFragments.COMMA); + } + sql.addFragments(orderByColumn); + } + if (orderBy != null) { + if (customOrder) { + sql.add(SqlFragments.COMMA); + } + sql.addSql(orderBy); + } + } else { + if (orderBy != null) { + sql.addSql("ORDER BY", orderBy); + } + } + + } + + private void appendWhere(AppendableSqlFragments sql, QueryParamEntity param) { + SqlFragments fragments = TERMS_BUILDER.createTermFragments(QueryAnalyzerImpl.this, param.getTerms()); + + if (fragments.isNotEmpty() || StringUtils.hasText(where)) { + sql.add(SqlFragments.WHERE); + } + + if (StringUtils.hasText(where)) { + sql.add(SqlFragments.LEFT_BRACKET); + sql.addSql(where); + sql.add(SqlFragments.RIGHT_BRACKET); + } + + if (fragments.isNotEmpty()) { + if (StringUtils.hasText(where)) { + sql.add(SqlFragments.AND); + } + sql.add(SqlFragments.LEFT_BRACKET); + sql.addFragments(fragments); + sql.add(SqlFragments.RIGHT_BRACKET); + } + } + + } + + + @Getter + 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 { + + SqlRequest refactor(QueryParamEntity param, Object... args); + + SqlRequest refactorCount(QueryParamEntity param, Object... args); + } + +} 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 new file mode 100644 index 000000000..1c109291c --- /dev/null +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/QueryHelper.java @@ -0,0 +1,800 @@ +package org.hswebframework.web.crud.query; + +import org.hswebframework.ezorm.core.Conditional; +import org.hswebframework.ezorm.core.MethodReferenceConverter; +import org.hswebframework.ezorm.core.dsl.Query; +import org.hswebframework.ezorm.rdb.mapping.ReactiveQuery; +import org.hswebframework.ezorm.rdb.mapping.defaults.record.Record; +import org.hswebframework.ezorm.rdb.operator.dml.query.SortOrder; +import org.hswebframework.web.api.crud.entity.PagerResult; +import org.hswebframework.web.api.crud.entity.QueryParamEntity; +import org.slf4j.Logger; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +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; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +/** + * 使用DSL方式链式调用来构建复杂查询 + * + *
{@code
+ *
+ * // select a.id as `a.id` ,b.name as b.name from table_a a
+ * // left join table_b b on a.id=b.id
+ * // where b.name like 'zhang%'
+ *
+ *   Flux =  helper
+ *      .select(R.class)
+ *      .as(A::getName,R::setName)
+ *      .as(A::getId,R::setAid)
+ *      .from(A.class)
+ *      .leftJoin(B.class,spec-> spec.is(A::id, B::id))
+ *      .where(dsl->dsl.like(B::getName,'zhang%'))
+ *      .fetch();
+ *
+ * }
+ *

+ * 使用原生SQL方式来构建动态条件查询 + *

{@code
+ *      helper
+ *       .select("select * from table_a a left join table_b b on a.id=b.id",R::new)
+ *       .where(dsl->dsl.like(R::getName,'zhang%'))
+ *       .fetch();
+ *  }
+ * + * @author zhouhao + * @see QueryHelper#select(String, Object...) + * @see QueryHelper#select(Class) + * @see QueryHelper#transformPageResult(Mono, Function) + * @see QueryHelper#combineOneToMany(Flux, Getter, ReactiveQuery, Getter, Setter) + * @see QueryHelper#combineOneToMany(Flux, Getter, Function, Getter, Setter) + * @since 4.0.16 + */ +public interface QueryHelper { + + /** + * 基于SQL创建分析器 + * + * @param selectSql SQL + * @return QueryAnalyzer + */ + QueryAnalyzer analysis(String selectSql); + + /** + * 逻辑和{@link QueryHelper#select(String, Object...)}相同,将查询结果转换为指定的实体类 + * + * @param sql SQL + * @param newInstance 实体类实例化方法 + * @param args 参数 + * @param 实体类型 + * @return NativeQuerySpec + */ + NativeQuerySpec select(String sql, + Supplier newInstance, + Object... args); + + /** + * 创建原生SQL查询器 + *

+ * 预编译参数仅支持?占位符,如果要使用模版,请使用{@link org.hswebframework.ezorm.rdb.executor.SqlRequests#template(String, Object)} + * 构造sql以及参数 + *

{@code
+     *
+     *  Flux records = helper
+     *        .select("select * from table where type = ?",type)
+     *         //注入动态查询条件
+     *        .where(param)
+     *        //或者编程式构造动态条件
+     *        .where(dsl->dsl.is("name",name))
+     *        //执行查询
+     *        .fetch();
+     * }
+ *

+ * join逻辑: + * + *

{@code
+     *
+     *  helper.select("select t1.id,t2.* from table t1"+
+     *                " left join table2 t2 on t1.id = t2.id") ...
+     *
+     *  将返回结构:
+     *   [
+     *     {
+     *     "id":"t1.id的值",
+     *     "t2.c1":"t2的字段"
+     *     }
+     *   ]
+     * }
+ * + *

+ * ⚠️注意:避免动态拼接SQL语句,应该使用预编译参数或者动态注入动态条件来进行条件处理. + * + * @param sql SQL查询语句 + * @param args 预编译参数 + * @return 查询构造器 + */ + NativeQuerySpec select(String sql, Object... args); + + + /** + * 创建一个查询构造器 + * + * @param resultType 实体类型,必须明确定义实体类,不能使用{@link java.util.Map}等类型 + * @param 类型 + * @return 查询构造器 + */ + SelectColumnMapperSpec select(Class resultType); + + /** + * 创建一个查询构造器,并返回指定的实体类型 + * + * @param resultType 实体类型,必须明确定义实体类,不能使用{@link java.util.Map}等类型 + * @param mapperSpec 实体映射配置 + * @param 类型 + * @return 查询构造器 + */ + SelectSpec select(Class resultType, + Consumer> mapperSpec); + + + interface NativeQuerySpec extends ExecuteSpec { + + /** + * 设置日志,在执行sql等操作时使用此日志进行日志打印. + * + * @param logger Logger + * @return this + */ + NativeQuerySpec logger(Logger logger); + + /** + * 以DSL方式构造查询条件 + *

{@code
+         *  helper
+         *  .select("select * from table t")
+         *  .where(dsl->dsl.is("type","device"))
+         * }
+ * + * @param dsl DSL + * @return this + */ + default ExecuteSpec where(Consumer> dsl) { + Query query = QueryParamEntity.newQuery().noPaging(); + dsl.accept(query); + return where(query.getParam()); + } + + /** + * 指定动态查询条件,通常用于前端动态传入查询条件 + *
{@code
+         *  helper
+         *  .select("select * from table t")
+         *  .where(param)
+         *  .fetch()
+         * }
+ * + * @param param DSL + * @return this + */ + ExecuteSpec where(QueryParamEntity param); + + } + + interface SelectSpec { + + /** + * 指定从哪个表查询 + * + * @param clazz 实体类型,类上需要注解{@link javax.persistence.Table},并使用{@link javax.persistence.Column}来描述列 + * @param 实体类型 + * @return 查询构造器 + * @see javax.persistence.Table + */ + FromSpec from(Class clazz); + + } + + + /** + * 查询条件构造器 + * + * @param 查询结果类型 + */ + interface WhereSpec extends ExecuteSpec { + + /** + * 使用动态查询参数来作为查询条件,用于通过参数传递查询条件的场景 + * + * @param param 查询参数 + * @return 排序描述 + * @see QueryParamEntity + */ + SortSpec where(QueryParamEntity param); + + /** + * 使用DSL方式来构造查询条件,用于编程式的构造查询条件 + *
{@code
+         *
+         *   // where t.name = ? or age > 18
+         *   where(dsl->dsl.is(MyEntity::getName,name).or().gt(MyEntity::getAge,18))
+         *
+         * }
+ * + * @param dsl DSL条件构造接收器 + * @return 排序描述 + */ + SortSpec where(Consumer> dsl); + } + + + /** + * 排序构造器 + * + * @param 查询结果类型 + */ + interface SortSpec extends ExecuteSpec { + + /** + * 使用指定的列名进行正序排序,多次执行将使用多列排序 + *
{@code
+         *  // order by a.index asc
+         *  orderByAsc("a.index");
+         * }
+ * + * @param column 列名 + * @return 排序构造器 + */ + default SortSpec orderByAsc(String column) { + return orderBy(column, SortOrder.Order.asc); + } + + /** + * 使用指定的列名进行倒序排序,多次执行将使用多列排序 + *
{@code
+         *  // order by a.index desc
+         *  orderByDesc("a.index");
+         * }
+ * + * @param column 列名 + * @return 排序构造器 + */ + default SortSpec orderByDesc(String column) { + return orderBy(column, SortOrder.Order.desc); + } + + /** + * 使用指定的列名进行排序,多次执行将使用多列排序 + *
{@code
+         *  // order by a.index asc
+         *  orderBy("a.index",SortOrder.Order.asc);
+         * }
+ * + * @param column 列名 + * @param order 排序方式 + * @return 排序构造器 + */ + SortSpec orderBy(String column, + SortOrder.Order order); + + + /** + * 对方法应用对应的列名进行正序排序,多次执行将使用多列排序 + *
{@code
+         *
+         *  // order by sort_order asc
+         *  orderByAsc(MyEntity::getSortOrder)
+         *
+         * }
+ * + * @param column 方法引用 + * @param S + * @return 排序构造器 + */ + default SortSpec orderByAsc(Getter column) { + return orderBy(column, SortOrder.Order.asc); + } + + /** + * 对方法应用对应的列名进行倒序排序,多次执行将使用多列排序 + *
{@code
+         *
+         *  // order by sort_order desc
+         *  orderByDesc(MyEntity::getSortOrder)
+         *
+         * }
+ * + * @param column 方法引用 + * @param S + * @return 排序构造器 + */ + default SortSpec orderByDesc(Getter column) { + return orderBy(column, SortOrder.Order.desc); + } + + /** + * 对方法应用对应的列名进行排序,多次执行将使用多列排序 + *
{@code
+         *
+         *  // order by sort_order desc
+         *  orderBy(MyEntity::getSortOrder,SortOrder.Order.desc)
+         *
+         * }
+ * + * @param column 方法引用 + * @param S + * @return 排序构造器 + */ + SortSpec orderBy(Getter column, + SortOrder.Order order); + + + } + + interface FromSpec extends JoinSpec, SortSpec { + + + } + + /** + * 表关联构造器 + * + * @param 查询结果类型 + */ + interface JoinSpec extends WhereSpec, SortSpec { + + + /** + * 对指定的实体类进行 left join + * + *
{@code
+         *   // left join detail on my.id = detail.id
+         *   leftJoin(DetailEntity.class,spec->spec.is(MyEntity::getId,DetailEntity::getId)
+         * }
+ * + * @param type 实体类型,需要注解{@link javax.persistence.Table} + * @param on 关联条件构造器 + * @param T + * @return 表关联构造器 + */ + JoinSpec leftJoin(Class type, Consumer> on); + + /** + * 对指定的实体类进行 right join + * + *
{@code
+         *   // left join detail on my.id = detail.id
+         *   rightJoin(DetailEntity.class,spec->spec.is(MyEntity::getId,DetailEntity::getId)
+         * }
+ * + * @param type 实体类型,需要注解{@link javax.persistence.Table} + * @param on 关联条件构造器 + * @param T + * @return 表关联构造器 + */ + JoinSpec rightJoin(Class type, Consumer> on); + + /** + * 对指定的实体类进行 inner join + * + *
{@code
+         *   // inner join detail on my.id = detail.id
+         *   innerJoin(DetailEntity.class,spec->spec.is(MyEntity::getId,DetailEntity::getId)
+         * }
+ * + * @param type 实体类型,需要注解{@link javax.persistence.Table} + * @param on 关联条件构造器 + * @param T + * @return 表关联构造器 + */ + JoinSpec innerJoin(Class type, Consumer> on); + + /** + * 对指定的实体类进行 full join + * + *
{@code
+         *   // join t1 on t1.id = t2.id
+         *   fullJoin(DetailEntity.class,spec->spec.is(MyEntity::getId,DetailEntity::getId)
+         * }
+ * + * @param type 实体类型,需要注解{@link javax.persistence.Table} + * @param on 关联条件构造器 + * @param T + * @return 表关联构造器 + */ + JoinSpec fullJoin(Class type, Consumer> on); + + + } + + /** + * 执行查询 + * + * @param + */ + interface ExecuteSpec { + + /** + * 执行count查询 + * + * @return count + */ + Mono count(); + + /** + * 执行查询,返回数据流 + * + * @return 数据流 + */ + Flux fetch(); + + /** + * 执行查询,返回数据流 + * + * @return 数据流 + */ + Flux fetch(int pageIndex,int pageSize); + + /** + * 执行分页查询,默认返回第一页的25条数据. + * + * @return 分页结果 + */ + Mono> fetchPaged(); + + /** + * 执行分页查询,并对结果进行转换 + * + * @param transfer 转换器 + * @param 转换后的数据类型 + * @return 转换后的分页结果 + */ + default Mono> fetchPaged(Function, Mono>> transfer) { + return transformPageResult(fetchPaged(), transfer); + } + + /** + * 指定分页执行查询 + * + * @param pageIndex 分页序号,从0开始 + * @param pageSize 每页数量 + * @return 分页结果 + */ + Mono> fetchPaged(int pageIndex, int pageSize); + + /** + * 指定分页执行查询,并对结果进行转换 + * + * @param pageIndex 分页序号,从0开始 + * @param pageSize 每页数量 + * @param transfer 转换器 + * @param 转换后的数据类型 + * @return 转换后的分页结果 + */ + default Mono> fetchPaged(int pageIndex, int pageSize, Function, Mono>> transfer) { + return transformPageResult(fetchPaged(pageIndex, pageSize), transfer); + } + } + + interface SelectColumnMapperSpec extends ColumnMapperSpec>, SelectSpec { + + } + + /** + * 列名映射构造器 + * + * @param 查询结果类型 + * @param Self + */ + interface ColumnMapperSpec> { + + /** + * 查询指定类型对应的表的全部字段. + * + * @param tableType 类型,只能是from或者join的类型. + * @return Self + */ + Self all(Class tableType); + + /** + * 查询指定类型对应的表的全部字段并映射到结果类型的一个字段中. + * + *
{@code
+         *   all(DetailEntity.class,MyEntity::setDetail)
+         * }
+ *

+ * 如果setter对应的属性类型为List,则自动进行一对多查询. + * 此时不支持按关联表进行条件查询主表的数据. + * + * @param tableType 类型,只能是from或者join的类型. + * @return Self + * @see QueryHelper#combineOneToMany(Flux, Getter, ReactiveQuery, Getter, Setter) + */ + Self all(Class tableType, Setter setter); + + /** + * 查询指定表的全部字段. + * + * @param tableOrAlias 表名或者join别名,只能是from或者join的表. + * @return Self + */ + Self all(String tableOrAlias); + + /** + * 查询指定类型对应的表的全部字段并映射到结果类型的一个字段中. + * + *

{@code
+         *   all("detail",MyEntity::setDetail)
+         * }
+ * + * @param tableOrAlias 表名或者join别名,只能是from或者join的表. + * @return Self + */ + Self all(String tableOrAlias, Setter setter); + + /** + * 指定查询的列名,以及映射到结果类型的字段. + *
{@code
+         *   as(DetailEntity::getName,MyEntity::setDetailName)
+         * }
+ * + * @param column 列名 + * @param target 结果类型字段 + * @param S + * @param V + * @return Self + */ + Self as(Getter column, Setter target); + + /** + * 指定查询的列名,以及映射到结果类型的字段. + *
{@code
+         *   as(DetailEntity::getName,"detail.name")
+         * }
+ * + * @param column 列名 + * @param target 结果类型字段 + * @param S + * @param V + * @return Self + */ + Self as(Getter column, String target); + + /** + * 指定查询的列名,以及映射到结果类型的字段. + * + *
{@code
+         *   as("_d.name",MyEntity::setDetailName)
+         * }
+ * + * @param column 列名 + * @param target 结果类型字段 + * @return Self + */ + Self as(String column, Setter target); + + /** + * 指定查询的列名,以及映射到结果类型的字段. + *
{@code
+         *   as("_d.name","detail.name")
+         * }
+ * + * @param column 列名 + * @param target 结果类型字段 + * @return Self + */ + Self as(String column, String target); + } + + /** + * Getter接口定义,只能使用方法引用实现此接口,如: + * + *
{@code
+     *   MyEntity::getId
+     * }
+ * + * @param + * @param + */ + interface Getter extends Function, Serializable { + + } + + /** + * Setter接口定义,只能使用方法引用实现此接口,如: + * + *
{@code
+     *   MyEntity::setId
+     * }
+ * + * @param + * @param + */ + interface Setter extends BiConsumer, Serializable { + + } + + /** + * 一对多数据组合,通常用于进行一对多的数据查询. + * + *
{@code
+     *
+     *  Flux flux = QueryHelper
+     *          .combineOneToMany(
+     *               myService.createQuery().fetch(),
+     *               MyEntity::getId,
+     *               infoService.createQuery(),
+     *               InfoEntity::getMyId,
+     *               MyEntity::setInfos
+     *           )
+     *
+     * }
+ * + * @param source 源数据 + * @param idMapper 主数据的ID获取器,如: MyEntity::getId + * @param fetcher 关联数据获取器,如: infoService.createQuery() + * @param mainIdGetter 关联数据的主数据ID获取器,如: InfoEntity::getMyId + * @param setter 主数据的关联数据设置器,如: MyEntity::setInfos + * @param 主数据类型 + * @param 主数据ID类型 + * @param 关联数据类型 + * @return Flux 组合后的数据流 + */ + static Flux combineOneToMany(Flux source, + Getter idMapper, + ReactiveQuery fetcher, + Getter mainIdGetter, + Setter> setter) { + return combineOneToMany(source, + idMapper, + list -> fetcher + .copy() + .in(MethodReferenceConverter.convertToColumn(mainIdGetter), list) + .fetch(), + mainIdGetter, + setter); + } + + /** + * 一对多数据组合,通常用于进行一对多的数据查询. + * + * @param source 源数据 + * @param idMapper 主数据的ID获取器,如: MyEntity::getId + * @param fetcher 关联数据获取器,如: ids->infoService.createQuery().in(InfoEntity::getMyId,ids).fetch() + * @param mainIdGetter 关联数据的主数据ID获取器,如: InfoEntity::getMyId + * @param setter 主数据的关联数据设置器,如: MyEntity::setInfos + * @param 主数据类型 + * @param 主数据ID类型 + * @param 关联数据类型 + * @return Flux 组合后的数据流 + */ + static Flux combineOneToMany(Flux source, + Getter idMapper, + Function, Flux> fetcher, + Getter mainIdGetter, + 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)); + }); + } + + /** + * 转换分页结果中的数据为另外一种数据 + * + * @param source 原始分页数据 + * @param transfer 转换器 + * @param + * @param + * @return 转换后的分页数据 + */ + @SuppressWarnings("all") + static Mono> transformPageResult(Mono> source, + Function, Mono>> transfer) { + 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; + }); + } + //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 new file mode 100644 index 000000000..220e0bbe9 --- /dev/null +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/QueryHelperUtils.java @@ -0,0 +1,78 @@ +package org.hswebframework.web.crud.query; + +import io.netty.util.concurrent.FastThreadLocal; +import org.hswebframework.web.exception.BusinessException; + +public class QueryHelperUtils { + + static final FastThreadLocal SHARE = new FastThreadLocal() { + @Override + protected StringBuilder initialValue() throws Exception { + return new StringBuilder(); + } + }; + + public static String toSnake(String col) { + StringBuilder builder = SHARE.get(); + builder.setLength(0); + for (int i = 0, len = col.length(); i < len; i++) { + char c = col.charAt(i); + if (Character.isUpperCase(c)) { + if (i != 0) { + builder.append('_'); + } + builder.append(Character.toLowerCase(c)); + } else { + builder.append(c); + } + } + return builder.toString(); + } + + public static String toHump(String col) { + StringBuilder builder = SHARE.get(); + builder.setLength(0); + boolean hasUpper = false, hasLower = false; + for (int i = 0, len = col.length(); i < len; i++) { + char c = col.charAt(i); + if (Character.isLowerCase(c)) { + hasLower = true; + } + if (Character.isUpperCase(c)) { + hasUpper = true; + } + if (hasUpper && hasLower) { + return col; + } + if (c == '_') { + if (i == len - 1) { + builder.append('_'); + } else { + builder.append(Character.toUpperCase(col.charAt(++i))); + } + } else { + builder.append(Character.toLowerCase(c)); + } + } + 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 d152b5e36..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,21 +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.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() - .mono("id:" + id) - .onCacheMissResume(ReactiveCrudService.super.findById(Mono.just(id))); + return this.getCache().getMono("id:" + id, () -> ReactiveCrudService.super.findById(id)); } @Override @@ -25,46 +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 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 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) - .doOnNext(id -> this.getCache().evict("id:" + id).subscribe()) - .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) -> s.doFinally((__) -> getCache().clear().subscribe())); + return ReactiveCrudService.super + .createUpdate() + .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) -> s.doFinally((__) -> getCache().clear().subscribe())); + return ReactiveCrudService.super + .createDelete() + .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/ReactiveCrudService.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/ReactiveCrudService.java index 67680a295..cec6e0ce8 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/ReactiveCrudService.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/ReactiveCrudService.java @@ -27,6 +27,8 @@ * @see GenericReactiveCrudService * @see GenericReactiveTreeSupportCrudService * @see EnableCacheReactiveCrudService + * @see org.hswebframework.web.crud.query.QueryHelper + * @since 4.0 */ public interface ReactiveCrudService { @@ -37,13 +39,13 @@ public interface ReactiveCrudService { /** * 创建一个DSL的动态查询接口,可使用DSL方式进行链式调用来构造动态查询条件.例如: - *
-     * Flux<MyEntity> flux=
-     *     service
+     * 
{@code
+     * Flux flux = service
      *     .createQuery()
      *     .where(MyEntity::getName,name)
      *     .in(MyEntity::getState,state1,state2)
      *     .fetch()
+     * }
      * 
* * @return 动态查询接口 @@ -54,14 +56,14 @@ default ReactiveQuery createQuery() { /** * 创建一个DSL动态更新接口,可使用DSL方式进行链式调用来构造动态更新条件.例如: - *
-     * Mono<Integer> flux=
-     *     service
+     * 
{@code
+     * Mono result = service
      *     .createUpdate()
      *     .set(entity::getState)
      *     .where(MyEntity::getName,name)
      *     .in(MyEntity::getState,state1,state2)
      *     .execute()
+     *     }
      * 
* * @return 动态更新接口 @@ -72,13 +74,13 @@ default ReactiveUpdate createUpdate() { /** * 创建一个DSL动态删除接口,可使用DSL方式进行链式调用来构造动态删除条件.例如: - *
-     * Mono<Integer> flux=
-     *     service
+     * 
{@code
+     * Mono result = service
      *     .createDelete()
      *     .where(MyEntity::getName,name)
      *     .in(MyEntity::getState,state1,state2)
      *     .execute()
+     * }
      * 
* * @return 动态更新接口 @@ -194,6 +196,8 @@ default Mono> queryPager(QueryParamEntity queryParamMono) { @Transactional(readOnly = true, transactionManager = TransactionManagers.reactiveTransactionManager) default Mono> queryPager(QueryParamEntity query, Function mapper) { + //如果查询参数指定了总数,表示不需要再进行count操作. + //建议前端在使用分页查询时,切换下一页时,将第一次查询到total结果传入查询参数,可以提升查询性能. if (query.getTotal() != null) { return getRepository() .createQuery() @@ -203,7 +207,7 @@ default Mono> queryPager(QueryParamEntity query, Function PagerResult.of(query.getTotal(), list, query)); } - //并行分页 + //并行分页,更快,所在页码无数据时,会返回空list. if (query.isParallelPager()) { return Mono .zip( @@ -220,10 +224,12 @@ default Mono> queryPager(QueryParamEntity query, Function(), query)); } - return query(query.clone().rePaging(total)) + //查询前根据数据总数进行重新分页:要跳转的页码没有数据则跳转到最后一页 + QueryParamEntity rePagingQuery = query.clone().rePaging(total); + return query(rePagingQuery) .map(mapper) .collectList() - .map(list -> PagerResult.of(total, list, query)); + .map(list -> PagerResult.of(total, list, rePagingQuery)); }); } 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 9b49472c1..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,99 +1,210 @@ 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; -import reactor.util.function.Tuple3; +import reactor.math.MathFlux; import java.util.*; -import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; /** + * 树形结构的通用增删改查服务 + * * @param TreeSortSupportEntity * @param ID * @see GenericReactiveTreeSupportCrudService */ public interface ReactiveTreeSortEntityService, K> - extends ReactiveCrudService { - + extends ReactiveCrudService { + + /** + * 动态查询并将查询结构转为树形结构 + * + * @param paramEntity 查询参数 + * @return 树形结构 + */ default Mono> queryResultToTree(Mono paramEntity) { return paramEntity.flatMap(this::queryResultToTree); } + /** + * 动态查询并将查询结构转为树形结构 + * + * @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)); } + /** + * 动态查询并将查询结构转为树形结构,包含所有子节点 + * + * @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 包含子节点的所有节点 + */ + @Transactional(readOnly = true, transactionManager = TransactionManagers.reactiveTransactionManager) default Flux queryIncludeChildren(Collection idList) { - return findById(idList) - .flatMap(e -> createQuery() - .where() - .like$("path", e.getPath()) - .fetch()); + return queryIncludeChildren(findById(idList)); + } + + /** + * 根据实体流查询全部子节点(包含原节点) + * + * @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 包含父节点的所有节点 + */ + @Transactional(readOnly = true, transactionManager = TransactionManagers.reactiveTransactionManager) default Flux queryIncludeParent(Collection idList) { - return findById(idList) - .flatMap(e -> createQuery() - .where() - .accept(Terms.Like.reversal("path", e.getPath(), false, true)) - .notEmpty("path") - .notNull("path") - .fetch()); + 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 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); } + /** + * 动态查询并将查询结构转为树形结构 + * + * @param queryParam 查询参数 + * @return 树形结构 + */ + @Transactional(readOnly = true, transactionManager = TransactionManagers.reactiveTransactionManager) default Flux queryIncludeChildren(QueryParamEntity queryParam) { + Set duplicateCheck = new HashSet<>(); + return query(queryParam) - .flatMap(e -> createQuery() - .where() - .like$("path", e.getPath()) - .fetch()); + .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); } @@ -103,183 +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)); } - 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) - .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())); - - 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())) - .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 findById(Flux.from(idPublisher)) - .flatMap(e -> createDelete() - .where() - .like$(e::getPath) - .execute()) - .collect(Collectors.summingInt(Integer::intValue)); + return this + .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(); @@ -296,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; @@ -304,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 17b6140a9..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 @@ -12,11 +12,14 @@ import org.hswebframework.web.exception.ValidationException; import org.hswebframework.web.i18n.LocaleUtils; import org.hswebframework.web.logger.ReactiveLogger; -import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; 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; import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; @@ -30,61 +33,83 @@ import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; +/** + * 统一错误处理 + * + * @author zhouhao + * @since 4.0 + */ @RestControllerAdvice -//@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) @Slf4j @Order public class CommonErrorControllerAdvice { @ExceptionHandler @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) - public Mono> handleException(BusinessException e) { + public Mono> handleException(TransactionException e) { + log.warn(e.getLocalizedMessage(), e); return LocaleUtils - .resolveThrowable(e, - (err, msg) -> ResponseMessage.error(err.getStatus(), err.getCode(), 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) { + return LocaleUtils + .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.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.getMessage(), 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 - .resolveThrowable(e, (err, msg) -> ResponseMessage - .>error(400, CodeConstants.Error.illegal_argument, msg) - .result(e.getDetails())) - ; + .currentReactive() + .map(locale -> ResponseMessage + .>error(400, + CodeConstants.Error.illegal_argument, + e.getLocalizedMessage(locale)) + .result(e.getDetails(locale))); } @ExceptionHandler @@ -95,39 +120,44 @@ public Mono>> handleException(C @ExceptionHandler @ResponseStatus(HttpStatus.BAD_REQUEST) + @SuppressWarnings("all") public Mono>> handleException(BindException e) { - return handleException(new ValidationException(e.getMessage(), e - .getBindingResult().getAllErrors() - .stream() - .filter(FieldError.class::isInstance) - .map(FieldError.class::cast) - .map(err -> new ValidationException.Detail(err.getField(), err.getDefaultMessage(), null)) - .collect(Collectors.toList()))); + return handleBindingResult(e.getBindingResult()); } @ExceptionHandler @ResponseStatus(HttpStatus.BAD_REQUEST) + @SuppressWarnings("all") public Mono>> handleException(WebExchangeBindException e) { - return handleException(new ValidationException(e.getMessage(), e - .getBindingResult().getAllErrors() - .stream() - .filter(FieldError.class::isInstance) - .map(FieldError.class::cast) - .map(err -> new ValidationException.Detail(err.getField(), err.getDefaultMessage(), null)) - .collect(Collectors.toList()))); + return handleBindingResult(e.getBindingResult()); } @ExceptionHandler @ResponseStatus(HttpStatus.BAD_REQUEST) + @SuppressWarnings("all") public Mono>> handleException(MethodArgumentNotValidException e) { - return handleException(new ValidationException(e.getMessage(), e - .getBindingResult().getAllErrors() - .stream() - .filter(FieldError.class::isInstance) - .map(FieldError.class::cast) - .map(err -> new ValidationException.Detail(err.getField(), err.getDefaultMessage(), null)) - .collect(Collectors.toList()))); + return handleBindingResult(e.getBindingResult()); + } + + private Mono>> handleBindingResult(BindingResult result) { + String message; + FieldError fieldError = result.getFieldError(); + ObjectError globalError = result.getGlobalError(); + + if (null != fieldError) { + message = fieldError.getDefaultMessage(); + } else if (null != globalError) { + message = globalError.getDefaultMessage(); + } else { + message = CodeConstants.Error.illegal_argument; + } + List details = result + .getFieldErrors() + .stream() + .map(err -> new ValidationException.Detail(err.getField(), err.getDefaultMessage(), null)) + .collect(Collectors.toList()); + return handleException(new ValidationException(message, details)); } @ExceptionHandler @@ -140,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.getMessage(), e))); + .resolveThrowable(e, (err, msg) -> ResponseMessage.error(504, CodeConstants.Error.timeout, msg)) + .doOnEach(ReactiveLogger.onNext(r -> log.warn(e.getLocalizedMessage(), e))); } @ExceptionHandler @@ -149,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.getMessage(), 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.getMessage(), e))); + log.warn(e.getLocalizedMessage(), e); + return Mono.just(ResponseMessage.error(e.getMessage())); } @ExceptionHandler @@ -166,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.getMessage(), 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.getMessage(), 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.getMessage(), e))); + .resolveMessageReactive("error.method_not_allowed") + .map(msg -> ResponseMessage + .error(406, "method_not_allowed", msg) + .result(e.getSupportedMethods())); } @@ -227,20 +260,19 @@ 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 @ResponseStatus(HttpStatus.BAD_REQUEST) public Mono> handleException(I18nSupportException e) { - return LocaleUtils - .resolveThrowable(e, - (err, msg) -> ResponseMessage.error(400, err.getI18nCode(), msg)); + return e.getLocalizedMessageReactive() + .map(msg -> ResponseMessage.error(400, e.getI18nCode(), msg)); } } 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 9a2c8e7b6..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,7 +1,8 @@ package org.hswebframework.web.crud.web; -import io.r2dbc.spi.R2dbcDataIntegrityViolationException; 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; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; @@ -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 { @@ -23,6 +24,12 @@ public CommonErrorControllerAdvice commonErrorControllerAdvice() { return new CommonErrorControllerAdvice(); } + @Bean + @ConditionalOnClass(name = "io.r2dbc.spi.R2dbcException") + @ConditionalOnMissingBean + public R2dbcErrorControllerAdvice r2dbcErrorControllerAdvice() { + return new R2dbcErrorControllerAdvice(); + } @Bean @ConditionalOnProperty(prefix = "hsweb.webflux.response-wrapper", name = "enabled", havingValue = "true", matchIfMissing = true) @@ -33,12 +40,6 @@ public ResponseMessageWrapper responseMessageWrapper(ServerCodecConfigurer codec return new ResponseMessageWrapper(codecConfigurer.getWriters(), resolver, registry); } - @Bean - public R2dbcDataIntegrityViolationException r2dbcDataIntegrityViolationException(){ - return new R2dbcDataIntegrityViolationException(); - } - - @Bean public WebFilter localeWebFilter() { return new WebFluxLocaleFilter(); 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 b310160e8..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,7 @@ 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; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; @@ -7,8 +9,9 @@ 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 { @Bean 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 d95770942..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.getMessage(), 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.getMessage(), 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.getMessage(), e); + log.warn(e.getLocalizedMessage(), e); return ResponseMessage.error(e.getMessage()); } @ExceptionHandler @ResponseStatus(HttpStatus.BAD_REQUEST) public ResponseMessage handleException(IllegalArgumentException e) { - log.error(e.getMessage(), 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.getMessage(), 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.getMessage(), 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/R2dbcErrorControllerAdvice.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/R2dbcErrorControllerAdvice.java index 50d3477e1..01c103dcc 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/R2dbcErrorControllerAdvice.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/R2dbcErrorControllerAdvice.java @@ -1,30 +1,33 @@ package org.hswebframework.web.crud.web; -import io.r2dbc.spi.R2dbcDataIntegrityViolationException; +import io.r2dbc.spi.R2dbcException; import lombok.extern.slf4j.Slf4j; import org.hswebframework.web.i18n.LocaleUtils; +import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; import reactor.core.publisher.Mono; -@Slf4j +/** + * 统一r2dbc错误处理 + * + * @author zhouhao + * @since 4.0 + */ @RestControllerAdvice +@Slf4j +@Order public class R2dbcErrorControllerAdvice { - @ExceptionHandler - @ResponseStatus(HttpStatus.BAD_REQUEST) - public Mono> handleException(R2dbcDataIntegrityViolationException e) { - String code; - if (e.getMessage().contains("Duplicate")) { - code = "error.duplicate_data"; - } else { - code = "error.data_error"; - log.warn(e.getMessage(), e); - } + @ExceptionHandler + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public Mono> handleException(R2dbcException e) { + log.error(e.getLocalizedMessage(), e); return LocaleUtils - .resolveMessageReactive(code) - .map(msg -> ResponseMessage.error(400, code, msg)); + .resolveMessageReactive("error.internal_server_error") + .map(msg -> ResponseMessage.error(500, "error." + e.getClass().getSimpleName(), msg)); } + } 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/ResponseMessageWrapperAdvice.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/ResponseMessageWrapperAdvice.java index 8728f13cf..d4f32299c 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/ResponseMessageWrapperAdvice.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/ResponseMessageWrapperAdvice.java @@ -13,7 +13,6 @@ import org.springframework.http.server.ServerHttpResponse; import org.springframework.util.CollectionUtils; import org.springframework.util.MimeType; -import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; @@ -21,6 +20,7 @@ import reactor.core.publisher.Mono; import javax.annotation.Nonnull; +import java.lang.reflect.Method; import java.util.HashSet; import java.util.Set; @@ -83,17 +83,21 @@ public Object beforeBodyWrite(Object body, if (body instanceof Mono) { return ((Mono) body) .map(ResponseMessage::ok) - .switchIfEmpty(Mono.just(ResponseMessage.ok())); + .switchIfEmpty(Mono.fromSupplier(ResponseMessage::ok)); } if (body instanceof Flux) { return ((Flux) body) .collectList() .map(ResponseMessage::ok) - .switchIfEmpty(Mono.just(ResponseMessage.ok())); + .switchIfEmpty(Mono.fromSupplier(ResponseMessage::ok)); } - if (body instanceof String) { + + Method method = returnType.getMethod(); + + if (method != null && returnType.getMethod().getReturnType() == String.class) { return JSON.toJSONString(ResponseMessage.ok(body)); } + return ResponseMessage.ok(body); } diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/reactive/ReactiveCrudController.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/reactive/ReactiveCrudController.java index 3437a0eab..16c31e46f 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/reactive/ReactiveCrudController.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/reactive/ReactiveCrudController.java @@ -1,5 +1,14 @@ package org.hswebframework.web.crud.web.reactive; +/** + * 通用响应式增删该查Controller,实现本接口来默认支持增删改查相关操作. + * + * @param 实体类型 + * @param 主键类型 + * @see ReactiveSaveController + * @see ReactiveQueryController + * @see ReactiveDeleteController + */ public interface ReactiveCrudController extends ReactiveSaveController, ReactiveQueryController, 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/ReactiveSaveController.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/reactive/ReactiveSaveController.java index c373a6323..cad2f6dcc 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/reactive/ReactiveSaveController.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/reactive/ReactiveSaveController.java @@ -15,6 +15,12 @@ import javax.validation.Valid; +/** + * 响应式保存接口,基于{@link ReactiveRepository}提供默认的新增,保存,修改接口. + * + * @param 实体类型 + * @param 主键类型 + */ public interface ReactiveSaveController { @Authorize(ignore = true) @@ -38,6 +44,14 @@ default E applyModifierEntity(Authentication authentication, E entity) { return entity; } + /** + * 尝试设置登陆用户信息到实体中 + * + * @param entity 实体 + * @param authentication 权限信息 + * @see RecordCreationEntity + * @see RecordModifierEntity + */ @Authorize(ignore = true) default E applyAuthentication(E entity, Authentication authentication) { if (entity instanceof RecordCreationEntity) { @@ -49,45 +63,123 @@ default E applyAuthentication(E entity, Authentication authentication) { return entity; } + /** + * 保存数据,如果传入了id,并且对应数据存在,则尝试覆盖,不存在则新增. + *

+ * 以类注解{@code @RequestMapping("/api/test")}为例: + *
{@code
+     *
+     * PATCH /api/test
+     * Content-Type: application/json
+     *
+     * [
+     *  {
+     *   "name":"value"
+     *  }
+     * ]
+     * }
+     * 
+ * + * @param payload payload + * @return 保存结果 + */ @PatchMapping @SaveAction @Operation(summary = "保存数据", description = "如果传入了id,并且对应数据存在,则尝试覆盖,不存在则新增.") default Mono save(@RequestBody Flux payload) { - return Authentication.currentReactive() + return Authentication + .currentReactive() .flatMapMany(auth -> payload.map(entity -> applyAuthentication(entity, auth))) .switchIfEmpty(payload) .as(getRepository()::save); } + /** + * 批量新增 + *

+ * 以类注解{@code @RequestMapping("/api/test")}为例: + *
{@code
+     *
+     * POST /api/test/_batch
+     * Content-Type: application/json
+     *
+     * [
+     *  {
+     *   "name":"value"
+     *  }
+     * ]
+     * }
+     * 
+ * + * @param payload payload + * @return 保存结果 + */ @PostMapping("/_batch") @SaveAction @Operation(summary = "批量新增数据") default Mono add(@RequestBody Flux payload) { - - return Authentication.currentReactive() + return Authentication + .currentReactive() .flatMapMany(auth -> payload.map(entity -> applyAuthentication(entity, auth))) .switchIfEmpty(payload) .collectList() .as(getRepository()::insertBatch); } + /** + * 新增单个数据,并返回新增后的数据. + *

+ * 以类注解{@code @RequestMapping("/api/test")}为例: + *
{@code
+     *
+     * POST /api/test
+     * Content-Type: application/json
+     *
+     *  {
+     *   "name":"value"
+     *  }
+     * }
+     * 
+ * + * @param payload payload + * @return 新增后的数据 + */ @PostMapping @SaveAction @Operation(summary = "新增单个数据,并返回新增后的数据.") default Mono add(@RequestBody Mono payload) { - return Authentication.currentReactive() + return Authentication + .currentReactive() .flatMap(auth -> payload.map(entity -> applyAuthentication(entity, auth))) .switchIfEmpty(payload) .flatMap(entity -> getRepository().insert(Mono.just(entity)).thenReturn(entity)); } + /** + * 根据ID修改数据 + *

+ * 以类注解{@code @RequestMapping("/api/test")}为例: + *
{@code
+     *
+     * PUT /api/test/{id}
+     * Content-Type: application/json
+     *
+     *  {
+     *   "name":"value"
+     *  }
+     * }
+     * 
+ * + * @param payload payload + * @return 是否成功 + */ @PutMapping("/{id}") @SaveAction @Operation(summary = "根据ID修改数据") default Mono update(@PathVariable K id, @RequestBody Mono payload) { - - return Authentication.currentReactive() + return Authentication + .currentReactive() .flatMap(auth -> payload.map(entity -> applyAuthentication(entity, auth))) .switchIfEmpty(payload) .flatMap(entity -> getRepository().updateById(id, Mono.just(entity))) 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 d1dd307e4..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(); } /** @@ -72,9 +74,9 @@ default Flux query(@Parameter(hidden = true) QueryParamEntity query) { */ @PostMapping("/_query/no-paging") @QueryAction - @QueryNoPagingOperation(summary = "使用POST方式分页动态查询(不返回总数)", - description = "此操作不返回分页总数,如果需要获取全部数据,请设置参数paging=false") - default Flux query(@Parameter(hidden = true)@RequestBody Mono query) { + @Operation(summary = "使用POST方式分页动态查询(不返回总数)", + description = "此操作不返回分页总数,如果需要获取全部数据,请设置参数paging=false") + default Flux query(@RequestBody Mono query) { return query.flatMapMany(this::query); } @@ -95,58 +97,155 @@ default Flux query(@Parameter(hidden = true)@RequestBody 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); } + /** + * POST方式动态查询. + * + *
+     *     POST /_query
+     *
+     *     {
+     *         "pageIndex":0,
+     *         "pageSize":20,
+     *         "where":"name like 张%", //放心使用,没有SQL注入
+     *         "orderBy":"id desc",
+     *         "terms":[ //高级条件
+     *             {
+     *                 "column":"name",
+     *                 "termType":"like",
+     *                 "value":"张%"
+     *             }
+     *         ]
+     *     }
+     * 
+ * + * @param query 查询条件 + * @return 结果流 + * @see QueryParamEntity + */ @PostMapping("/_query") @QueryAction @SuppressWarnings("all") - @QueryOperation(summary = "使用POST方式分页动态查询") - default Mono> queryPager(@Parameter(hidden = true) @RequestBody Mono query) { + @Operation(summary = "使用POST方式分页动态查询") + default Mono> queryPager(@RequestBody Mono query) { return query.flatMap(q -> queryPager(q)); } + /** + * POST方式动态查询数量. + * + *
+     *     POST /_count
+     *
+     *     {
+     *         "pageIndex":0,
+     *         "pageSize":20,
+     *         "where":"name like 张%", //放心使用,没有SQL注入
+     *         "orderBy":"id desc",
+     *         "terms":[ //高级条件
+     *             {
+     *                 "column":"name",
+     *                 "termType":"like",
+     *                 "value":"张%"
+     *             }
+     *         ]
+     *     }
+     * 
+ * + * @param query 查询条件 + * @return 查询结果 + * @see QueryParamEntity + */ @PostMapping("/_count") @QueryAction - @QueryNoPagingOperation(summary = "使用POST方式查询总数") - default Mono count(@Parameter(hidden = true) @RequestBody Mono query) { - return query.flatMap(this::count); + @Operation(summary = "使用POST方式查询总数") + default Mono count(@RequestBody Mono query) { + return getService().count(query); } /** - * 统计查询 + * GET方式动态查询数量. * *
-     *     GET /_count
+     *
+     *    GET /_count?pageIndex=0&pageSize=20&where=name is 张三&orderBy=id desc
+     *
      * 
* * @param query 查询条件 - * @return 统计结果 + * @return 查询结果 + * @see QueryParamEntity */ @GetMapping("/_count") @QueryAction @QueryNoPagingOperation(summary = "使用GET方式查询总数") default Mono count(@Parameter(hidden = true) QueryParamEntity query) { - return getService() + return Mono.defer(() -> getService().count(query)); + } + + @PostMapping("/_exists") + @QueryAction + @Operation(summary = "使用POST方式判断数据是否存在") + default Mono exists(@RequestBody Mono query) { + return query + .flatMap(param -> getService() .createQuery() - .setParam(query) - .count(); + .setParam(param) + .fetchOne() + .hasElement()); } + /** + * 使用GET方式判断数据是否存在. + * + *
+     *
+     *    GET /_exists?where=name is 张三
+     *
+     * 
+ * + * @param query 查询条件 + * @return 查询结果 + * @see QueryParamEntity + */ + @GetMapping("/_exists") + @QueryAction + @QueryNoPagingOperation(summary = "使用GET方式判断数据是否存在") + default Mono exists(@Parameter(hidden = true) QueryParamEntity query) { + return exists(Mono.just(query)); + } + + /** + * 根据ID查询. + *
+     * {@code
+     *     GET /{id}
+     * }
+     * 
+ * + * @param id ID + * @return 结果流 + * @see QueryParamEntity + */ @GetMapping("/{id:.+}") @QueryAction @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/java/org/hswebframework/web/crud/web/reactive/ReactiveServiceSaveController.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/reactive/ReactiveServiceSaveController.java index 581adfb7f..7d86b4510 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/reactive/ReactiveServiceSaveController.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/reactive/ReactiveServiceSaveController.java @@ -1,6 +1,7 @@ package org.hswebframework.web.crud.web.reactive; import io.swagger.v3.oas.annotations.Operation; +import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository; import org.hswebframework.ezorm.rdb.mapping.defaults.SaveResult; import org.hswebframework.web.api.crud.entity.RecordCreationEntity; import org.hswebframework.web.api.crud.entity.RecordModifierEntity; @@ -8,14 +9,21 @@ import org.hswebframework.web.authorization.annotation.Authorize; import org.hswebframework.web.authorization.annotation.SaveAction; import org.hswebframework.web.crud.service.ReactiveCrudService; +import org.hswebframework.web.exception.NotFoundException; import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -public interface ReactiveServiceSaveController { +/** + * 响应式保存接口,基于{@link ReactiveCrudService}提供默认的新增,保存,修改接口. + * + * @param 实体类型 + * @param 主键类型 + */ +public interface ReactiveServiceSaveController { @Authorize(ignore = true) - ReactiveCrudService getService(); + ReactiveCrudService getService(); @Authorize(ignore = true) default E applyCreationEntity(Authentication authentication, E entity) { @@ -46,48 +54,132 @@ default E applyAuthentication(E entity, Authentication authentication) { return entity; } + /** + * 保存数据,如果传入了id,并且对应数据存在,则尝试覆盖,不存在则新增. + *

+ * 以类注解{@code @RequestMapping("/api/test")}为例: + *
{@code
+     *
+     * PATCH /api/test
+     * Content-Type: application/json
+     *
+     * [
+     *  {
+     *   "name":"value"
+     *  }
+     * ]
+     * }
+     * 
+ * + * @param payload payload + * @return 保存结果 + */ @PatchMapping @SaveAction @Operation(summary = "保存数据", description = "如果传入了id,并且对应数据存在,则尝试覆盖,不存在则新增.") default Mono save(@RequestBody Flux payload) { - return Authentication.currentReactive() + return Authentication + .currentReactive() .flatMapMany(auth -> payload.map(entity -> applyAuthentication(entity, auth))) .switchIfEmpty(payload) .as(getService()::save); } + /** + * 批量新增 + *

+ * 以类注解{@code @RequestMapping("/api/test")}为例: + *
{@code
+     *
+     * POST /api/test/_batch
+     * Content-Type: application/json
+     *
+     * [
+     *  {
+     *   "name":"value"
+     *  }
+     * ]
+     * }
+     * 
+ * + * @param payload payload + * @return 保存结果 + */ @PostMapping("/_batch") @SaveAction @Operation(summary = "批量新增数据") default Mono add(@RequestBody Flux payload) { - return Authentication.currentReactive() + return Authentication + .currentReactive() .flatMapMany(auth -> payload.map(entity -> applyAuthentication(entity, auth))) .switchIfEmpty(payload) .collectList() .as(getService()::insertBatch); } + /** + * 新增单个数据,并返回新增后的数据. + *

+ * 以类注解{@code @RequestMapping("/api/test")}为例: + *
{@code
+     *
+     * POST /api/test
+     * Content-Type: application/json
+     *
+     *  {
+     *   "name":"value"
+     *  }
+     * }
+     * 
+ * + * @param payload payload + * @return 新增后的数据 + */ @PostMapping @SaveAction @Operation(summary = "新增单个数据,并返回新增后的数据.") default Mono add(@RequestBody Mono payload) { - return Authentication.currentReactive() + return Authentication + .currentReactive() .flatMap(auth -> payload.map(entity -> applyAuthentication(entity, auth))) .switchIfEmpty(payload) .flatMap(entity -> getService().insert(Mono.just(entity)).thenReturn(entity)); } - + /** + * 根据ID修改数据 + *

+ * 以类注解{@code @RequestMapping("/api/test")}为例: + *
{@code
+     *
+     * PUT /api/test/{id}
+     * Content-Type: application/json
+     *
+     *  {
+     *   "name":"value"
+     *  }
+     * }
+     * 
+ * + * @param payload payload + * @return 是否成功 + */ @PutMapping("/{id}") @SaveAction @Operation(summary = "根据ID修改数据") default Mono update(@PathVariable K id, @RequestBody Mono payload) { - return Authentication.currentReactive() + return Authentication + .currentReactive() .flatMap(auth -> payload.map(entity -> applyAuthentication(entity, auth))) .switchIfEmpty(payload) .flatMap(entity -> getService().updateById(id, Mono.just(entity))) + .doOnNext(i -> { + if (i == 0) { + throw new NotFoundException(); + } + }) .thenReturn(true); } 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 f35bfc889..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 @@ -2,4 +2,9 @@ error.unsupported_media_type=Unsupported media type 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 \ No newline at end of file +error.data_error=Data error +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 9b9331846..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,5 +1,10 @@ -error.unsupported_media_type=不支持的请求类型 -error.not_acceptable_media_type=不支持的媒体类型 -error.method_not_allowed=不支持的请求方法 -error.duplicate_data=重复的数据 -error.data_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 becf82e82..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 @@ -1,34 +1,55 @@ package org.hswebframework.web.crud; +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; @SpringBootTest @RunWith(SpringJUnit4ClassRunner.class) -public class CrudTests { +public class CrudTests { @Autowired private TestEntityService service; @Test - public void test(){ + public void test() { - TestEntity entity = TestEntity.of("test",100); + CustomTestEntity entity = new CustomTestEntity(); + entity.setExt("xxx"); + entity.setAge(1); + entity.setName("test"); - Mono.just(entity) - .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/TestApplication.java b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/TestApplication.java index 1b2c011f3..ebe8f483b 100644 --- a/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/TestApplication.java +++ b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/TestApplication.java @@ -1,7 +1,9 @@ package org.hswebframework.web.crud; import org.hswebframework.web.api.crud.entity.EntityFactory; +import org.hswebframework.web.crud.entity.factory.EntityMappingCustomizer; import org.hswebframework.web.crud.entity.factory.MapperEntityFactory; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.context.annotation.Bean; @@ -13,7 +15,9 @@ public class TestApplication { @Bean - public EntityFactory entityFactory(){ - return new MapperEntityFactory(); + public EntityFactory entityFactory(ObjectProvider customizers) { + MapperEntityFactory factory = new MapperEntityFactory(); + customizers.forEach(customizer -> customizer.custom(factory)); + return factory; } } 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 new file mode 100644 index 000000000..f9be2488d --- /dev/null +++ b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/entity/CustomTestEntity.java @@ -0,0 +1,28 @@ +package org.hswebframework.web.crud.entity; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +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; +import javax.persistence.GeneratedValue; +import javax.persistence.Table; + +@Getter +@Setter +@AllArgsConstructor(staticName = "of") +@NoArgsConstructor +@EnableEntityEvent +public class CustomTestEntity extends TestEntity { + + + @Column + @ToString.Ignore + private String ext; + +} 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 7ab89919d..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,11 +4,10 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.hswebframework.web.api.crud.entity.GenericEntity; -import org.hswebframework.web.crud.generator.Generators; +import org.hswebframework.web.api.crud.entity.ExtendableEntity; +import org.hswebframework.web.crud.annotation.EnableEntityEvent; import javax.persistence.Column; -import javax.persistence.GeneratedValue; import javax.persistence.Table; @Getter @@ -16,7 +15,8 @@ @Table(name = "s_test") @AllArgsConstructor(staticName = "of") @NoArgsConstructor -public class TestEntity extends GenericEntity { +@EnableEntityEvent +public class TestEntity extends ExtendableEntity { @Column(length = 32) private String name; @@ -24,9 +24,8 @@ public class TestEntity extends GenericEntity { @Column private Integer age; - @Override - @GeneratedValue(generator = Generators.DEFAULT_ID_GENERATOR) - public String getId() { - return super.getId(); - } + @Column + private String testName; + + } 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 85e9167b2..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 @@ -1,9 +1,11 @@ package org.hswebframework.web.crud.events; import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository; +import org.hswebframework.ezorm.rdb.operator.builder.fragments.NativeSql; 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; @@ -36,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)) @@ -48,6 +55,88 @@ 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); + reactiveRepository + .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(); + + Assert.assertEquals(1, listener.modified.getAndSet(0)); + + } + @Test public void testInsertBatch() { reactiveRepository.createQuery() @@ -69,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 new file mode 100644 index 000000000..3a9ea8172 --- /dev/null +++ b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/events/expr/SpelSqlExpressionInvokerTest.java @@ -0,0 +1,76 @@ +package org.hswebframework.web.crud.events.expr; + +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; +import java.util.Map; +import java.util.function.BiFunction; + +import static org.junit.jupiter.api.Assertions.*; + +class SpelSqlExpressionInvokerTest { + + + @Test + void test() { + SpelSqlExpressionInvoker invoker = new SpelSqlExpressionInvoker(); + + Function3, Object> func = invoker.compile("name + 1 + ?"); + + 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())); + + assertEquals(3, func.apply(mapping, null, Collections.singletonMap("name", 3))); + + } + + @Test + void testAddNull(){ + SpelSqlExpressionInvoker invoker = new SpelSqlExpressionInvoker(); + EntityColumnMapping mapping = Mockito.mock(EntityColumnMapping.class); + + 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 new file mode 100644 index 000000000..39b87d9a9 --- /dev/null +++ b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/query/DefaultQueryHelperTest.java @@ -0,0 +1,400 @@ +package org.hswebframework.web.crud.query; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.serializer.SerializerFeature; +import lombok.Getter; +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; +import org.hswebframework.web.crud.entity.TestEntity; +import org.junit.jupiter.api.Test; +import org.junit.runner.RunWith; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import reactor.test.StepVerifier; + +import java.math.BigDecimal; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.*; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +@RunWith(SpringJUnit4ClassRunner.class) +class DefaultQueryHelperTest { + + @Autowired + 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); + + database.dml() + .insert("s_test") + .value("id", "group-test") + .value("name", "group") + .value("age", 31) + .execute() + .sync(); + + 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("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 { + private String name; + private BigDecimal totalResult; + } + + @Test + public void testNative() { + database.dml() + .insert("s_test_event") + .value("id", "helper_testNative") + .value("name", "Ename2") + .execute() + .sync(); + + database.dml() + .insert("s_test") + .value("id", "helper_testNative") + .value("name", "main2") + .value("age", 20) + .execute() + .sync(); + + DefaultQueryHelper helper = new DefaultQueryHelper(database); + QueryParamEntity param = QueryParamEntity + .newQuery() + .is("e.id", "helper_testNative") + .is("t.age", "20") + .orderByAsc("t.age") + .getParam(); + + { + Sort sortByValue = new Sort(); + sortByValue.setName("t.id"); + sortByValue.setValue("1"); + param.getSorts().add(sortByValue); + } + { + Sort sortByValue = new Sort(); + sortByValue.setName("t.id"); + sortByValue.setValue("2"); + param.getSorts().add(sortByValue); + } + + + 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) + .logger(LoggerFactory.getLogger("org.hswebframework.test.native")) + .where(param) + .fetchPaged() + .doOnNext(v -> System.out.println(JSON.toJSONString(v, SerializerFeature.PrettyFormat))) + .as(StepVerifier::create) + .expectNextCount(1) + .verifyComplete(); + + helper.select("select id,name from s_test t " + + "union all select id,name from s_test_event") + .where(dsl -> dsl + .is("id", "helper_testNative") + .orderByAsc("name")) + .fetchPaged() + .doOnNext(v -> System.out.println(JSON.toJSONString(v, SerializerFeature.PrettyFormat))) + .as(StepVerifier::create) + .expectNextCount(1) + .verifyComplete(); + + + } + + @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() { + + database.dml() + .insert("s_test_event") + .value("id", "helper_test") + .value("name", "main") + .value("age", 10) + .execute() + .sync(); + + database.dml() + .insert("s_test") + .value("id", "helper_test") + .value("name", "main") + .value("testName", "testName") + .value("age", 10) + .execute() + .sync(); + + DefaultQueryHelper helper = new DefaultQueryHelper(database); + + helper.select(TestInfo.class) + // .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) + .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(0, 10) + .doOnNext(info -> System.out.println(JSON.toJSONString(info, SerializerFeature.PrettyFormat))) + .as(StepVerifier::create) + .expectNextCount(1) + .verifyComplete(); + + } + + @Getter + @Setter + @ToString + public static class TestInfo extends TestInfoSuper { + + private String id; + + private String name; + + private Integer age; + + private String testName; + + 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 new file mode 100644 index 000000000..31498cd96 --- /dev/null +++ b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/query/QueryAnalyzerImplTest.java @@ -0,0 +1,219 @@ +package org.hswebframework.web.crud.query; + +import org.hswebframework.ezorm.rdb.executor.SqlRequest; +import org.hswebframework.ezorm.rdb.executor.wrapper.ResultWrappers; +import org.hswebframework.ezorm.rdb.operator.DatabaseOperator; +import org.hswebframework.web.api.crud.entity.QueryParamEntity; +import org.junit.jupiter.api.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.test.StepVerifier; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +@RunWith(SpringJUnit4ClassRunner.class) +class QueryAnalyzerImplTest { + @Autowired + private DatabaseOperator database; + + + @Test + 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()); + + System.out.println(request); + + SqlRequest sql = analyzer.refactorCount( + QueryParamEntity + .newQuery() + .and("name", "123") + .getParam()); + System.out.println(sql); + + } + + + @Test + void testUnion() { + QueryAnalyzerImpl analyzer = new QueryAnalyzerImpl(database, + "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"); + + assertNotNull(analyzer.select().getColumns().get("n")); + + } + + @Test + void test() { + QueryAnalyzerImpl analyzer = new QueryAnalyzerImpl(database, + "select name n from s_test t"); + + assertNotNull(analyzer.select().table.alias, "t"); + assertNotNull(analyzer.select().table.metadata.getName(), "s_test"); + + assertNotNull(analyzer.select().getColumns().get("n")); + + + } + + @Test + void testSub() { + QueryAnalyzerImpl analyzer = new QueryAnalyzerImpl(database, + "select * from ( select distinct(name) as n from s_test ) t"); + + assertEquals(analyzer.select().table.alias, "t"); + + assertNotNull(analyzer.select().getColumns().get("n")); + + SqlRequest request = analyzer + .refactor(QueryParamEntity + .newQuery() + .where("n", "123") + .getParam()); + + System.out.println(request); + + database.sql() + .reactive() + .select(request, ResultWrappers.map()) + .as(StepVerifier::create) + .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 new file mode 100644 index 000000000..69ba5cddb --- /dev/null +++ b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/service/CustomTestCustom.java @@ -0,0 +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, TableMetadataCustomizer { + @Override + public void custom(MapperEntityFactory factory) { + 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 12bb085b4..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) @@ -25,7 +22,7 @@ public class GenericReactiveCacheSupportCrudServiceTest { @Test public void test() { - TestEntity entity = TestEntity.of("test2",100); + TestEntity entity = TestEntity.of("test2",100,"testName"); entityService.insert(Mono.just(entity)) .as(StepVerifier::create) @@ -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 236a8d8a4..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,10 +1,29 @@ 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 { + + @EventListener + 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 1aa8c684f..91ae1b6e9 100644 --- a/hsweb-commons/pom.xml +++ b/hsweb-commons/pom.xml @@ -23,9 +23,10 @@ hsweb-framework org.hswebframework.web - 4.0.12-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 1e20dc9d8..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.12-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 0411631ae..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 @@ -8,13 +8,23 @@ import java.util.Collection; import java.util.function.Function; +import java.util.function.Supplier; +/** + * 响应式缓存 + * + * @param 缓存元素类型 + */ public interface ReactiveCache { Flux getFlux(Object key); + Flux getFlux(Object key, Supplier> loader); + Mono getMono(Object key); + Mono getMono(Object key, Supplier> loader); + Mono put(Object key, Publisher data); Mono evict(Object key); @@ -25,20 +35,29 @@ public interface ReactiveCache { Mono clear(); + /** + * @deprecated https://github.com/reactor/reactor-addons/issues/237 + */ + @Deprecated default CacheFlux.FluxCacheBuilderMapMiss flux(Object key) { - return otherSupplier -> - Flux.defer(() -> - getFlux(key) - .switchIfEmpty(otherSupplier.get() - .collectList() - .flatMapMany(values -> put(key, Flux.fromIterable(values)) - .thenMany(Flux.fromIterable(values))))); + return otherSupplier -> Flux + .defer(() -> this + .getFlux(key) + .switchIfEmpty(otherSupplier.get() + .collectList() + .flatMapMany(values -> put(key, Flux.fromIterable(values)) + .thenMany(Flux.fromIterable(values))))); } + /** + * @deprecated https://github.com/reactor/reactor-addons/issues/237 + */ + @Deprecated default CacheMono.MonoCacheBuilderMapMiss mono(Object key) { - return otherSupplier -> - Mono.defer(() -> getMono(key) + return otherSupplier -> Mono + .defer(() -> this + .getMono(key) .switchIfEmpty(otherSupplier.get() - .flatMap(value -> put(key, Mono.just(value)).thenReturn(value)))); + .flatMap(value -> put(key, Mono.just(value)).thenReturn(value)))); } } 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 new file mode 100644 index 000000000..5bceac178 --- /dev/null +++ b/hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/supports/AbstractReactiveCache.java @@ -0,0 +1,175 @@ +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; +import reactor.core.Disposable; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.MonoOperator; +import reactor.core.publisher.Sinks; +import reactor.util.context.Context; +import reactor.util.context.ContextView; + +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; + +@Slf4j +public abstract class AbstractReactiveCache implements ReactiveCache { + static final Sinks.EmitFailureHandler emitFailureHandler = Sinks.EmitFailureHandler.busyLooping(Duration.ofSeconds(30)); + + private final Map cacheLoading = new ConcurrentHashMap<>(); + + protected static class CacheLoader extends MonoOperator { + + private final AbstractReactiveCache parent; + + private final Object key; + private Mono defaultValue; + + private final Sinks.One holder = Sinks.one(); + + private volatile Disposable loading; + + protected CacheLoader(AbstractReactiveCache parent, Object key, Mono source) { + super(source.cache()); + this.parent = parent; + this.key = key; + } + + protected void defaultValue(Mono defaultValue, ContextView context) { + if (this.defaultValue != null) { + return; + } + this.defaultValue = defaultValue; + tryLoad(context); + } + + + @SuppressWarnings("all") + private void tryLoad(ContextView context) { + if (holder.currentSubscriberCount() == 1 && loading == null) { + Mono source = this.source; + if (defaultValue != null) { + source = source + .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)); + } + } + + @Override + public void subscribe(CoreSubscriber actual) { + holder.asMono().subscribe(actual); + tryLoad(actual.currentContext()); + } + + private void complete() { + parent.cacheLoading.remove(key, this); + } + + + } + + protected abstract Mono getNow(Object key); + + public abstract Mono putNow(Object key, Object value); + + @Override + @SuppressWarnings("all") + public final Mono getMono(Object 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; + }) + .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)) + .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)); + }) + .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) { + + if (data instanceof Mono) { + return Mono.from(data) + .flatMap(e -> putNow(key, e)); + } + return Flux.from(data) + .collectList() + .flatMap(e -> putNow(key, e)); + } + + @Override + public abstract Mono evict(Object key); + + @Override + public Flux getAll(Object... keys) { + return Flux.just(keys) + .flatMap(this::getMono); + } + + @Override + public abstract Mono evictAll(Iterable key); + + @Override + public abstract Mono clear(); +} 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 237823c95..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 @@ -12,63 +12,36 @@ @SuppressWarnings("all") @AllArgsConstructor -public class CaffeineReactiveCache implements ReactiveCache { +public class CaffeineReactiveCache extends AbstractReactiveCache { private Cache cache; @Override - public Flux getFlux(Object key) { - return (Flux) Flux.defer(() -> { - Object v = cache.getIfPresent(key); - if (v == null) { - return Flux.empty(); - } - if (v instanceof Iterable) { - return Flux.fromIterable(((Iterable) v)); - } - return Flux.just(v); - }); - } - - @Override - public Mono getMono(Object key) { - return Mono.defer(() -> { - Object v = cache.getIfPresent(key); - if (v == null) { - return Mono.empty(); - } - return (Mono) Mono.just(v); - }); + public Mono evictAll(Iterable key) { + return Mono.fromRunnable(() -> cache.invalidateAll(key)); } @Override - public Mono put(Object key, Publisher data) { - return Mono.defer(() -> { - if (data instanceof Flux) { - return ((Flux) data).collectList() - .doOnNext(v -> cache.put(key, v)) - .then(); - } - if (data instanceof Mono) { - return ((Mono) data) - .doOnNext(v -> cache.put(key, v)) - .then(); + 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 Mono.error(new UnsupportedOperationException("unsupport publisher:" + data)); + return Flux.fromIterable(cache.getAllPresent(Arrays.asList(keys)).values()) + .map(e -> (E) e); }); } @Override - public Mono evictAll(Iterable key) { - return Mono.fromRunnable(() -> cache.invalidateAll(key)); + protected Mono getNow(Object key) { + return Mono.justOrEmpty(cache.getIfPresent(key)); } @Override - public Flux getAll(Object... keys) { - return Flux.defer(() -> { - return Flux.fromIterable(cache.getAllPresent(Arrays.asList(keys)).values()) - .map(e -> (E) e); - }); + public Mono putNow(Object key, Object value) { + cache.put(key, value); + return Mono.empty(); } @Override 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 eda0c8208..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 @@ -2,76 +2,49 @@ import com.google.common.cache.Cache; import lombok.AllArgsConstructor; -import org.hswebframework.web.cache.ReactiveCache; -import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.Arrays; -import java.util.Collection; @SuppressWarnings("all") @AllArgsConstructor -public class GuavaReactiveCache implements ReactiveCache { +public class GuavaReactiveCache extends AbstractReactiveCache { private Cache cache; - @Override - public Flux getFlux(Object key) { - return (Flux)Flux.defer(() -> { - Object v = cache.getIfPresent(key); - if (v == null) { - return Flux.empty(); - } - if (v instanceof Iterable) { - return Flux.fromIterable(((Iterable) v)); - } - return Flux.just(v); - }); - } @Override - public Mono getMono(Object key) { - return (Mono)Mono.defer(() -> { - Object v = cache.getIfPresent(key); - if (v == null) { - return Mono.empty(); - } - return (Mono) Mono.just(v); - }); + public Mono evictAll(Iterable key) { + return Mono.fromRunnable(() -> cache.invalidateAll(key)); } @Override - public Mono put(Object key, Publisher data) { - return Mono.defer(() -> { - if (data instanceof Flux) { - return ((Flux) data).collectList() - .doOnNext(v -> cache.put(key, v)) - .then(); - } - if (data instanceof Mono) { - return ((Mono) data) - .doOnNext(v -> cache.put(key, v)) - .then(); - } - return Mono.error(new UnsupportedOperationException("unsupport publisher:" + data)); - }); + protected Mono getNow(Object key) { + return Mono.justOrEmpty(cache.getIfPresent(key)); } @Override - public Mono evictAll(Iterable key) { - return Mono.fromRunnable(() -> cache.invalidateAll(key)); + public Mono putNow(Object key, Object value) { + cache.put(key, value); + return Mono.empty(); } @Override 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 df5b53a8c..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 @@ -16,7 +16,7 @@ @SuppressWarnings("all") @Slf4j -public class RedisReactiveCache implements ReactiveCache { +public class RedisReactiveCache extends AbstractReactiveCache { private ReactiveRedisOperations operations; @@ -30,7 +30,8 @@ public RedisReactiveCache(String redisKey, ReactiveRedisOperations { @@ -44,96 +45,49 @@ public RedisReactiveCache(String redisKey, ReactiveRedisOperations getFlux(Object key) { - return localCache - .getFlux(key) - .switchIfEmpty(Flux.defer(() -> { - return operations - .opsForHash() - .get(redisKey, key) - .flatMapIterable(r -> { - if (r instanceof Iterable) { - return ((Iterable) r); - } - return Collections.singletonList(r); - }) - .map(r -> (E) r); - })) - .onErrorResume(err -> this.handleError((Throwable) err)); - - } - - protected Mono handleError(Throwable error) { - return Mono.fromRunnable(() -> { - log.error(error.getMessage(), error); - }); + protected Mono getNow(Object key) { + return (Mono) localCache.getMono(key, () -> (Mono) operations.opsForHash().get(redisKey, key)); } @Override - public Mono getMono(Object key) { - return localCache.getMono(key) - .switchIfEmpty(operations.opsForHash() - .get(redisKey, key) - .map(v -> (E) v) - .flatMap(r -> localCache.put(key, Mono.just(r)) - .thenReturn(r))) - .onErrorResume(err -> this.handleError(err)); + public Mono putNow(Object key, Object value) { + return operations + .opsForHash() + .put(redisKey, key, value) + .then(localCache.evict(key)) + .then(operations.convertAndSend(topicName, key)) + .then(); } - @Override - public Mono put(Object key, Publisher data) { - if (data instanceof Mono) { - return ((Mono) data) - .flatMap(r -> { - return operations.opsForHash() - .put(redisKey, key, r) - .then(localCache.put(key, data)) - .then(operations.convertAndSend(topicName, key)); - - }) - .then() - .onErrorResume(err -> this.handleError(err)) - ; - } - if (data instanceof Flux) { - return ((Flux) data) - .collectList() - .flatMap(r -> { - return operations.opsForHash() - .put(redisKey, key, r) - .then(localCache.put(key, data)) - .then(operations.convertAndSend(topicName, key)); - - }) - .then() - .onErrorResume(err -> this.handleError(err)) - ; - } - return Mono.error(new UnsupportedOperationException("unsupport publisher:" + data)); - } + protected Mono handleError(Throwable error) { + log.error(error.getMessage(), error); + return Mono.empty(); + } @Override public Mono evictAll(Iterable key) { - return operations.opsForHash() + return operations + .opsForHash() .remove(redisKey, StreamSupport.stream(key.spliterator(), false).toArray()) .then(localCache.evictAll(key)) - .flatMap(nil -> Flux.fromIterable(key) + .flatMap(nil -> Flux + .fromIterable(key) .flatMap(k -> operations.convertAndSend(topicName, key)) - .then() - ) + .then()) .onErrorResume(err -> this.handleError(err)); } @Override public Flux getAll(Object... keys) { - if (keys.length == 0) { + if (keys == null || keys.length == 0) { return operations .opsForHash() .values(redisKey) .map(r -> (E) r); } - return operations.opsForHash() + return operations + .opsForHash() .multiGet(redisKey, Arrays.asList(keys)) .flatMapIterable(Function.identity()) .map(r -> (E) r) diff --git a/hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/supports/UnSupportedReactiveCache.java b/hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/supports/UnSupportedReactiveCache.java index 4200e2de7..f1d9f230c 100644 --- a/hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/supports/UnSupportedReactiveCache.java +++ b/hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/supports/UnSupportedReactiveCache.java @@ -15,10 +15,21 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public class UnSupportedReactiveCache implements ReactiveCache { - private static final UnSupportedReactiveCache INSTANCE = new UnSupportedReactiveCache(); + private static final UnSupportedReactiveCache INSTANCE = new UnSupportedReactiveCache<>(); + @SuppressWarnings("all") public static ReactiveCache getInstance() { - return INSTANCE; + return (UnSupportedReactiveCache) INSTANCE; + } + + @Override + public Flux getFlux(Object key, Supplier> loader) { + return loader.get(); + } + + @Override + public Mono getMono(Object key, Supplier> loader) { + return loader.get(); } @Override 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/hsweb-concurrent-cache/src/test/java/org/hswebframework/web/cache/RedisReactiveCacheManagerTest.java b/hsweb-concurrent/hsweb-concurrent-cache/src/test/java/org/hswebframework/web/cache/RedisReactiveCacheManagerTest.java index b98ec5999..34ee8177c 100644 --- a/hsweb-concurrent/hsweb-concurrent-cache/src/test/java/org/hswebframework/web/cache/RedisReactiveCacheManagerTest.java +++ b/hsweb-concurrent/hsweb-concurrent-cache/src/test/java/org/hswebframework/web/cache/RedisReactiveCacheManagerTest.java @@ -35,39 +35,37 @@ public void test() { ReactiveCache cache = cacheManager.getCache("test"); cache.clear() - .as(StepVerifier::create) - .verifyComplete(); + .as(StepVerifier::create) + .verifyComplete(); - cache.flux("test-flux") - .onCacheMissResume(Flux.just("1", "2", "3")) - .as(StepVerifier::create) - .expectNext("1", "2", "3") - .verifyComplete(); + cache.getFlux("test-flux", () -> Flux.just("1", "2", "3")) + .as(StepVerifier::create) + .expectNext("1", "2", "3") + .verifyComplete(); cache.put("test-flux", Flux.just("3", "2", "1")) - .as(StepVerifier::create) - .verifyComplete(); + .as(StepVerifier::create) + .verifyComplete(); cache.getFlux("test-flux") - .as(StepVerifier::create) - .expectNext("3", "2", "1") - .verifyComplete(); + .as(StepVerifier::create) + .expectNext("3", "2", "1") + .verifyComplete(); - cache.mono("test-mono") - .onCacheMissResume(Mono.just("1")) - .as(StepVerifier::create) - .expectNext("1") - .verifyComplete(); + cache.getMono("test-mono", () -> Mono.just("1")) + .as(StepVerifier::create) + .expectNext("1") + .verifyComplete(); cache.put("test-mono", Mono.just("2")) - .as(StepVerifier::create) - .verifyComplete(); + .as(StepVerifier::create) + .verifyComplete(); cache.getMono("test-mono") - .as(StepVerifier::create) - .expectNext("2") - .verifyComplete(); + .as(StepVerifier::create) + .expectNext("2") + .verifyComplete(); } diff --git a/hsweb-concurrent/pom.xml b/hsweb-concurrent/pom.xml index 78403e73d..9fd4855ea 100644 --- a/hsweb-concurrent/pom.xml +++ b/hsweb-concurrent/pom.xml @@ -5,11 +5,12 @@ hsweb-framework org.hswebframework.web - 4.0.12-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 6a0ad3521..e0b45fffd 100644 --- a/hsweb-core/pom.xml +++ b/hsweb-core/pom.xml @@ -5,12 +5,13 @@ hsweb-framework org.hswebframework.web - 4.0.12-SNAPSHOT + 4.0.20-SNAPSHOT ../pom.xml 4.0.0 hsweb-core + ${artifactId} 核心包 @@ -18,20 +19,24 @@ org.javassist javassist - 3.22.0-GA + ${javassist.version} + com.fasterxml.jackson.core jackson-databind + org.hswebframework hsweb-utils + org.springframework spring-context + org.springframework spring-web @@ -47,6 +52,7 @@ org.slf4j slf4j-api + commons-beanutils commons-beanutils @@ -92,8 +98,7 @@ org.glassfish - javax.el - 3.0.0 + jakarta.el @@ -106,5 +111,37 @@ reactor-extra + + com.google.guava + guava + + + + jctools-core + org.jctools + 4.0.1 + + + + io.netty + netty-common + + + + io.seruco.encoding + base62 + 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 4bfa511c4..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 @@ -18,20 +18,18 @@ package org.hswebframework.web.aop; +import com.google.common.collect.Maps; import lombok.AllArgsConstructor; import lombok.Getter; 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 org.springframework.util.DigestUtils; -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; @@ -45,28 +43,31 @@ public class MethodInterceptorHolder { /** * 参数名称获取器,用于获取方法参数的名称 */ - public static final ParameterNameDiscoverer nameDiscoverer = new LocalVariableTableParameterNameDiscoverer(); + public static final ParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer(); public static MethodInterceptorHolder create(MethodInvocation invocation) { - String id = DigestUtils.md5DigestAsHex(String.valueOf(invocation.getMethod().hashCode()).getBytes()); String[] argNames = nameDiscoverer.getParameterNames(invocation.getMethod()); Object[] args = invocation.getArguments(); - Map argMap = new LinkedHashMap<>(); - String[] names = new String[args.length]; - for (int i = 0, len = args.length; i < len; i++) { - names[i] = (argNames == null || argNames.length <= i || argNames[i] == null) ? "arg" + i : argNames[i]; - argMap.put(names[i], args[i]); - } - return new MethodInterceptorHolder(id, - invocation.getMethod(), - invocation.getThis(), - args, - names, - argMap); + String[] names; + //参数名与参数长度不一致,则填充argx来作为参数名 + if (argNames == null || argNames.length != args.length) { + names = new String[args.length]; + for (int i = 0, len = args.length; i < len; i++) { + names[i] = (argNames == null || argNames.length <= i || argNames[i] == null) ? "arg" + i : argNames[i]; + } + } else { + names = argNames; + } + return new MethodInterceptorHolder(null, + invocation.getMethod(), + invocation.getThis(), + args, + names, + null); } - private final String id; + private String id; private final Method method; @@ -76,8 +77,27 @@ public static MethodInterceptorHolder create(MethodInvocation invocation) { private final String[] argumentsNames; - private final Map namedArguments; + private Map namedArguments; + + public String getId() { + if (id == null) { + id = DigestUtils.md5Hex(method.toString()); + } + return id; + } + protected Map createNamedArguments() { + Map namedArguments = Maps.newLinkedHashMapWithExpectedSize(arguments.length); + for (int i = 0, len = arguments.length; i < len; i++) { + namedArguments.put(argumentsNames[i], arguments[i]); + } + return namedArguments; + + } + + public Map getNamedArguments() { + return namedArguments == null ? namedArguments = createNamedArguments() : namedArguments; + } public T findMethodAnnotation(Class annClass) { return AnnotationUtils.findMethodAnnotation(annClass, method, annClass); 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 new file mode 100644 index 000000000..29ce001f3 --- /dev/null +++ b/hsweb-core/src/main/java/org/hswebframework/web/bean/ClassDescription.java @@ -0,0 +1,45 @@ +package org.hswebframework.web.bean; + +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 { + private final Class type; + + private final boolean collectionType; + private final boolean arrayType; + 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; + collectionType = Collection.class.isAssignableFrom(type); + enumDict = EnumDict.class.isAssignableFrom(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/ClassDescriptions.java b/hsweb-core/src/main/java/org/hswebframework/web/bean/ClassDescriptions.java new file mode 100644 index 000000000..731a50b41 --- /dev/null +++ b/hsweb-core/src/main/java/org/hswebframework/web/bean/ClassDescriptions.java @@ -0,0 +1,16 @@ +package org.hswebframework.web.bean; + + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class ClassDescriptions { + + private static final Map, ClassDescription> CACHE = new ConcurrentHashMap<>(); + + public static ClassDescription getDescription(Class type) { + return CACHE.computeIfAbsent(type, ClassDescription::new); + } + + +} 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/DefaultToStringOperator.java b/hsweb-core/src/main/java/org/hswebframework/web/bean/DefaultToStringOperator.java index adca3b506..9ee75955a 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/bean/DefaultToStringOperator.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/bean/DefaultToStringOperator.java @@ -26,7 +26,7 @@ @Slf4j public class DefaultToStringOperator implements ToStringOperator { - private PropertyDescriptor[] descriptors; + private final PropertyDescriptor[] descriptors; private Set defaultIgnoreProperties; @@ -36,10 +36,9 @@ public class DefaultToStringOperator implements ToStringOperator { private Map> converts; - private Function coverStringConvert = (o) -> coverString(String.valueOf(o), 80); + private final Function coverStringConvert = (o) -> coverString(String.valueOf(o), 80); - - private Function> simpleConvertBuilder = type -> { + private final Function, BiFunction> simpleConvertBuilder = type -> { if (Date.class.isAssignableFrom(type)) { return (value, f) -> DateFormatter.toString(((Date) value), "yyyy-MM-dd HH:mm:ss"); } else { @@ -47,13 +46,14 @@ public class DefaultToStringOperator implements ToStringOperator { } }; - private Predicate simpleTypePredicate = ((Predicate) String.class::isAssignableFrom) + private final Predicate> simpleTypePredicate = ((Predicate>) String.class::isAssignableFrom) .or(Class::isEnum) .or(Class::isPrimitive) .or(Date.class::isAssignableFrom) .or(Number.class::isAssignableFrom) .or(Boolean.class::isAssignableFrom); - private Class targetType; + + private final Class targetType; public DefaultToStringOperator(Class targetType) { this.targetType = targetType; @@ -228,10 +228,9 @@ protected void init() { } } - class ConvertConfig { + static class ConvertConfig { long features; Set ignoreProperty; - } protected Map convertMap(Map obj, long features, Set ignoreProperty) { @@ -255,7 +254,7 @@ protected Map convertMap(Map obj, long features, } continue; } - Class type = value.getClass(); + Class type = value.getClass(); if (simpleTypePredicate.test(type)) { value = simpleConvertBuilder.apply(type).apply(value, null); if (ignoreProperty.contains(entry.getKey())) { @@ -292,7 +291,7 @@ protected Map toMap(T target, long features, Set ignoreP if (ToString.Feature.hasFeature(features, ToString.Feature.nullPropertyToEmpty)) { boolean isSimpleType = false; PropertyDescriptor propertyDescriptor = descriptorMap.get(entry.getKey()); - Class propertyType = null; + Class propertyType = null; if (propertyDescriptor != null) { propertyType = propertyDescriptor.getPropertyType(); isSimpleType = simpleTypePredicate.test(propertyType); 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 e00ba28d4..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 @@ -1,16 +1,22 @@ package org.hswebframework.web.bean; +import com.google.common.collect.Maps; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; 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.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,6 +40,8 @@ public final class FastBeanCopier { private static final PropertyUtilsBean propertyUtils = BeanUtilsBean.getInstance().getPropertyUtils(); + private static final ConvertUtilsBean convertUtils = BeanUtilsBean.getInstance().getConvertUtils(); + private static final Map, Class> wrapperClassMapping = new HashMap<>(); @SuppressWarnings("all") @@ -83,6 +91,15 @@ public boolean contains(Object o) { }; } + public static Object getProperty(Object source, String key) { + if (source instanceof Map) { + return ((Map) source).get(key); + } + SingleValueMap map = new SingleValueMap<>(); + copy(source, map, include(key)); + return map.getValue(); + } + public static T copy(S source, T target, String... ignore) { return copy(source, target, DEFAULT_CONVERT, ignore); } @@ -107,12 +124,21 @@ public static T copy(S source, T target, Set ignore) { @SuppressWarnings("all") public static T copy(S source, T target, Converter converter, Set ignore) { if (source instanceof Map && target instanceof Map) { - ((Map) target).putAll(((Map) source)); + if (CollectionUtils.isEmpty(ignore)) { + ((Map) target).putAll(((Map) source)); + } else { + ((Map) source) + .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; } @@ -123,7 +149,7 @@ static Class getUserClass(Object object) { Class type = ClassUtils.getUserClass(object); if (java.lang.reflect.Proxy.isProxyClass(type)) { - Class[] interfaces= type.getInterfaces(); + Class[] interfaces = type.getInterfaces(); return interfaces[0]; } @@ -135,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); } @@ -155,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); @@ -177,21 +218,28 @@ 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) - .map(BeanClassProperty::new) - //让字段有序 - .sorted(Comparator.comparing(property -> fieldNames.indexOf(property.name))) - .collect(Collectors.toMap(ClassProperty::getName, Function.identity(), (k, k2) -> k, LinkedHashMap::new)); + .filter(property -> !property + .getName() + .equals("class") && property.getReadMethod() != null && property.getWriteMethod() != null) + .map(BeanClassProperty::new) + //让字段有序 + .sorted(Comparator.comparing(property -> fieldNames.indexOf(property.name))) + .collect(Collectors.toMap(ClassProperty::getName, Function.identity(), (k, k2) -> k, LinkedHashMap::new)); } 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) { @@ -199,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); @@ -224,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"); @@ -231,13 +295,16 @@ private static String createCopierCode(Class source, Class target) { code.append("if($$__source.").append(sourceProperty.getReadMethod()).append("!=null){\n"); } code.append(targetProperty.generateVar(targetProperty.getName())).append("=") - .append(sourceProperty.generateGetter(target, targetProperty.getType())) - .append(";\n"); + .append(sourceProperty.generateGetter(target, targetProperty.getType())) + .append(";\n"); if (!targetProperty.isPrimitive()) { code.append("\tif(").append(sourceProperty.getName()).append("!=null){\n"); } - code.append("\t$$__target.").append(targetProperty.generateSetter(targetProperty.getType(), sourceProperty.getName())).append(";\n"); + code + .append("\t$$__target.") + .append(targetProperty.generateSetter(targetProperty.getType(), sourceProperty.getName())) + .append(";\n"); if (!targetProperty.isPrimitive()) { code.append("\t}\n"); } @@ -310,10 +377,10 @@ public boolean isWrapper(Class type) { protected Class getPrimitiveType(Class type) { return wrapperClassMapping.entrySet().stream() - .filter(entry -> entry.getValue() == type) - .map(Map.Entry::getKey) - .findFirst() - .orElse(null); + .filter(entry -> entry.getValue() == type) + .map(Map.Entry::getKey) + .findFirst() + .orElse(null); } protected Class getWrapperType() { @@ -334,18 +401,18 @@ public BiFunction, Class, String> createGetterFunction() { boolean hasGeneric = false; if (field != null) { String[] arr = Arrays.stream(ResolvableType.forField(field) - .getGenerics()) - .map(ResolvableType::getRawClass) - .filter(Objects::nonNull) - .map(t -> t.getName().concat(".class")) - .toArray(String[]::new); + .getGenerics()) + .map(ResolvableType::getRawClass) + .filter(Objects::nonNull) + .map(t -> t.getName().concat(".class")) + .toArray(String[]::new); if (arr.length > 0) { generic = "new Class[]{" + String.join(",", arr) + "}"; hasGeneric = true; } } String convert = "converter.convert((Object)(" + (isPrimitive() ? castWrapper(getterCode) : getterCode) + ")," - + getTypeName(targetType) + ".class," + generic + ")"; + + getTypeName(targetType) + ".class," + generic + ")"; StringBuilder convertCode = new StringBuilder(); if (targetType != getType()) { @@ -358,18 +425,18 @@ 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()) - .append(")") - .append(convert) - .append(").") - .append(targetType.getName()) - .append("Value()"); + .append(")") + .append(convert) + .append(").") + .append(targetType.getName()) + .append("Value()"); } } else if (isPrimitive()) { @@ -377,31 +444,36 @@ public BiFunction, Class, String> createGetterFunction() { //源字段类型为基本数据类型,目标字段为包装器类型 if (targetIsWrapper) { convertCode.append(targetType.getName()) - .append(".valueOf(") - .append(getterCode) - .append(")"); + .append(".valueOf(") + .append(getterCode) + .append(")"); } else { convertCode.append("(").append(targetType.getName()) - .append(")(") - .append(convert) - .append(")"); + .append(")(") + .append(convert) + .append(")"); } } else { convertCode.append("(").append(getTypeName(targetType)) - .append(")(") - .append(convert) - .append(")"); + .append(")(") + .append(convert) + .append(")"); } } else { if (Cloneable.class.isAssignableFrom(targetType)) { try { - convertCode.append("(").append(getTypeName()).append(")").append(getterCode).append(".clone()"); + convertCode + .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); @@ -480,6 +552,8 @@ public Collection newCollection(Class targetClass) { if (targetClass == List.class) { return new ArrayList<>(); + } else if (targetClass == ConcurrentHashMap.KeySetView.class) { + return ConcurrentHashMap.newKeySet(); } else if (targetClass == Set.class) { return new HashSet<>(); } else if (targetClass == Queue.class) { @@ -495,11 +569,14 @@ public Collection newCollection(Class targetClass) { @Override @SuppressWarnings("all") + @SneakyThrows public T convert(Object source, Class targetClass, Class[] genericType) { if (source == null) { return null; } - if (source.getClass().isEnum()) { + ClassDescription target = ClassDescriptions.getDescription(targetClass); + + if (target.isEnumType()) { if (source instanceof EnumDict) { Object val = (T) ((EnumDict) source).getValue(); if (targetClass.isInstance(val)) { @@ -520,7 +597,11 @@ public T convert(Object source, Class targetClass, Class[] genericType) { } if (targetClass == Date.class) { if (source instanceof String) { - return (T) DateFormatter.fromString((String) source); + T parsed = (T) DateFormatter.fromString((String) source); + if (parsed == null) { + return (T) converterByApache(Date.class, source); + } + return parsed; } if (source instanceof Number) { return (T) new Date(((Number) source).longValue()); @@ -529,13 +610,15 @@ public T convert(Object source, Class targetClass, Class[] genericType) { return (T) new Date(((Date) source).getTime()); } } - if (Collection.class.isAssignableFrom(targetClass)) { + if (target.isCollectionType()) { Collection collection = newCollection(targetClass); 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 { if (source instanceof String) { String stringValue = ((String) source); @@ -554,62 +637,130 @@ public T convert(Object source, Class targetClass, Class[] genericType) { } return (T) collection; } - - if (targetClass.isEnum()) { - if (EnumDict.class.isAssignableFrom(targetClass)) { + if (target.isEnumType()) { + if (target.isEnumDict()) { String strVal = String.valueOf(source); - - Object val = EnumDict.find((Class) targetClass, e -> { - return e.eq(source) || e.name().equalsIgnoreCase(strVal); - }).orElse(null); + Object val = null; + for (Object anEnum : target.getEnums()) { + EnumDict dic = ((EnumDict) anEnum); + Enum e = ((Enum) anEnum); + if (dic.eq(source) || e.name().equalsIgnoreCase(strVal)) { + val = (T) anEnum; + break; + } + } + if (val == null) { + return null; + } if (targetClass.isInstance(val)) { return ((T) val); } return convert(val, targetClass, genericType); } - String strSource=String.valueOf(source); - for (T t : targetClass.getEnumConstants()) { - if (((Enum) t).name().equalsIgnoreCase(strSource) - ||Objects.equals(String.valueOf(((Enum) t).ordinal()),strSource)) { - return t; + String strSource = String.valueOf(source); + for (Object e : target.getEnums()) { + Enum t = ((Enum) e); + if ((t.name().equalsIgnoreCase(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 (targetClass.isArray()) { + 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 = BeanUtilsBean - .getInstance() - .getConvertUtils() - .lookup(targetClass); + org.apache.commons.beanutils.Converter converter = convertUtils.lookup(targetClass); if (null != converter) { return converter.convert(targetClass, source); } + //快速复制map + if (targetClass == Map.class) { + if (source instanceof Map) { + return (T) copyMap(((Map) source)); + } + if (source instanceof Collection) { + Map map = new LinkedHashMap<>(); + int i = 0; + for (Object o : ((Collection) source)) { + if (genericType.length >= 2) { + map.put(convert(i++, genericType[0], EMPTY_CLASS_ARRAY), convert(o, genericType[1], EMPTY_CLASS_ARRAY)); + } else { + map.put(i++, o); + } + } + return (T) map; + + } + ClassDescription sourType = ClassDescriptions.getDescription(source.getClass()); + return (T) copy(source, Maps.newHashMapWithExpectedSize(sourType.getFieldSize())); + } + 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; } + + private Map copyMap(Map map) { + if (map instanceof TreeMap) { + return new TreeMap<>(map); + } + + if (map instanceof LinkedHashMap) { + return new LinkedHashMap<>(map); + } + + if (map instanceof ConcurrentHashMap) { + return new ConcurrentHashMap<>(map); + } + + return new HashMap<>(map); + } + + private Object converterByApache(Class targetClass, Object source) { + org.apache.commons.beanutils.Converter converter = convertUtils.lookup(targetClass); + if (null != converter) { + return converter.convert(targetClass, source); + } + return null; + } } @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/bean/SingleValueMap.java b/hsweb-core/src/main/java/org/hswebframework/web/bean/SingleValueMap.java new file mode 100644 index 000000000..b37221a38 --- /dev/null +++ b/hsweb-core/src/main/java/org/hswebframework/web/bean/SingleValueMap.java @@ -0,0 +1,108 @@ +package org.hswebframework.web.bean; + +import java.util.*; + +public class SingleValueMap implements Map { + private K key; + private V value; + + @Override + public int size() { + return value == null ? 0 : 1; + } + + @Override + public boolean isEmpty() { + return size() == 0; + } + + @Override + public boolean containsKey(Object key) { + return Objects.equals(this.key, key); + } + + @Override + public boolean containsValue(Object value) { + return Objects.equals(this.value, value); + } + + @Override + public V get(Object key) { + return Objects.equals(key, this.key) ? value : null; + } + + @Override + public V put(K key, V value) { + this.key = key; + V old = this.value; + this.value = value; + return old; + } + + @Override + public V remove(Object key) { + if (Objects.equals(key, this.key)) { + V old = this.value; + this.value = null; + return old; + } + return null; + } + + @Override + public void putAll(Map m) { + if (m.size() > 0) { + Map.Entry entry = m.entrySet().iterator().next(); + this.key = entry.getKey(); + this.value = entry.getValue(); + } + } + + @Override + public void clear() { + this.key = null; + this.value = null; + } + + @Override + public Set keySet() { + return key == null ? Collections.emptySet() : Collections.singleton(key); + } + + @Override + public Collection values() { + return value == null ? Collections.emptySet() : Collections.singleton(value); + } + + @Override + public Set> entrySet() { + return key == null ? Collections.emptySet() : Collections.singleton( + new Entry() { + @Override + public K getKey() { + return key; + } + + @Override + public V getValue() { + return value; + } + + @Override + public V setValue(V value) { + V old = SingleValueMap.this.value; + SingleValueMap.this.value = value; + return old; + } + } + ); + } + + public V getValue() { + return value; + } + + public K getKey() { + return key; + } +} 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/context/ContextUtils.java b/hsweb-core/src/main/java/org/hswebframework/web/context/ContextUtils.java index 256ee95ee..38eb71a9e 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/context/ContextUtils.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/context/ContextUtils.java @@ -17,21 +17,16 @@ public static Context currentContext() { return contextThreadLocal.get(); } + @Deprecated public static Mono reactiveContext() { return Mono - .subscriberContext() - .handle((context, sink) -> { - if (context.hasKey(Context.class)) { - sink.next(context.get(Context.class)); - } else { - sink.complete(); - } - }) - .subscriberContext(acceptContext(ctx -> { + .deferContextual(context->Mono.justOrEmpty(context.getOrEmpty(Context.class))) + .contextWrite(acceptContext(ctx -> { })); } + @Deprecated public static Function acceptContext(Consumer contextConsumer) { return context -> { if (!context.hasKey(Context.class)) { 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 ac5494c0b..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 @@ -19,12 +19,16 @@ import lombok.NoArgsConstructor; import lombok.SneakyThrows; 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; @@ -46,7 +50,7 @@ */ @JSONType(deserializer = EnumDict.EnumDictJSONDeserializer.class) @JsonDeserialize(contentUsing = EnumDict.EnumDictJSONDeserializer.class) -public interface EnumDict extends JSONSerializable { +public interface EnumDict extends JSONSerializable, Serializable { /** * 枚举选项的值,通常由字母或者数字组成,并且在同一个枚举中值唯一;对应数据库中的值通常也为此值 @@ -99,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) ); } @@ -135,20 +148,25 @@ default String getComments() { * @param 枚举类型 * @return 查找到的结果 */ - static Optional find(Class type, Predicate predicate) { - if (type.isEnum()) { - for (T enumDict : type.getEnumConstants()) { - if (predicate.test(enumDict)) { - return Optional.of(enumDict); + @SuppressWarnings("all") + static & EnumDict> Optional find(Class type, Predicate predicate) { + ClassDescription description = ClassDescriptions.getDescription(type); + if (description.isEnumType()) { + for (Object enumDict : description.getEnums()) { + if (predicate.test((T) enumDict)) { + return Optional.of((T) enumDict); } } } return Optional.empty(); } - static List findList(Class type, Predicate predicate) { - if (type.isEnum()) { - return Arrays.stream(type.getEnumConstants()) + @SuppressWarnings("all") + static & EnumDict> List findList(Class type, Predicate predicate) { + ClassDescription description = ClassDescriptions.getDescription(type); + if (description.isEnumType()) { + return Arrays.stream(description.getEnums()) + .map(v -> (T) v) .filter(predicate) .collect(Collectors.toList()); } @@ -160,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))); } /** @@ -171,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)); } @@ -180,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; } @@ -198,37 +219,40 @@ static long toMask(T... t) { @SafeVarargs - static boolean in(T target, T... t) { - Enum[] all = target.getClass().getEnumConstants(); + 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); } @@ -236,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); } @@ -303,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 @@ -317,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); @@ -334,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)); } } @@ -365,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(); @@ -376,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()); - - return new ValidationException(currentName,"validation.parameter_does_not_exist_in_enums", currentName); + .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/AsyncEvent.java b/hsweb-core/src/main/java/org/hswebframework/web/event/AsyncEvent.java index 100748047..19f78d79c 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/event/AsyncEvent.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/event/AsyncEvent.java @@ -4,6 +4,8 @@ import org.springframework.context.ApplicationEventPublisher; import reactor.core.publisher.Mono; +import java.util.function.Function; + /** * 异步事件,使用响应式编程进行事件监听时,请使用此事件接口 * @@ -27,6 +29,10 @@ public interface AsyncEvent { */ void first(Publisher publisher); + void transformFirst(Function,Publisher> mapper); + + void transform(Function,Publisher> mapper); + /** * 推送事件到 ApplicationEventPublisher * 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 new file mode 100644 index 000000000..71727f120 --- /dev/null +++ b/hsweb-core/src/main/java/org/hswebframework/web/event/AsyncEventHooks.java @@ -0,0 +1,62 @@ +package org.hswebframework.web.event; + +import io.netty.util.concurrent.FastThreadLocal; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.LinkedList; + +public class AsyncEventHooks { + + private static final FastThreadLocal> hooks = new FastThreadLocal>() { + @Override + protected LinkedList initialValue() { + return new LinkedList<>(); + } + }; + + public static AutoUnbindable bind(AsyncEventHook hook) { + LinkedList list = hooks.get(); + list.add(hook); + return () -> list.removeLastOccurrence(hook); + } + + static Mono hookFirst(AsyncEvent event, Mono publisher) { + LinkedList hooksList = hooks.getIfExists(); + if (hooksList == null) { + return publisher; + } + for (AsyncEventHook asyncEventHook : hooksList) { + publisher = asyncEventHook.hookFirst(event, publisher); + } + return publisher; + } + + static Mono hookAsync(AsyncEvent event, Mono publisher) { + LinkedList hooksList = hooks.getIfExists(); + if (hooksList == null) { + return publisher; + } + for (AsyncEventHook asyncEventHook : hooksList) { + publisher = asyncEventHook.hookAsync(event, publisher); + } + return publisher; + } + + + public interface AutoUnbindable extends AutoCloseable { + @Override + void close(); + } + + public interface AsyncEventHook { + default Mono hookAsync(AsyncEvent event, Mono publisher) { + return publisher; + } + + default Mono hookFirst(AsyncEvent event, Mono publisher) { + 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 0cc0d64a6..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 @@ -1,31 +1,44 @@ package org.hswebframework.web.event; -import lombok.Getter; import org.reactivestreams.Publisher; import org.springframework.context.ApplicationEventPublisher; import reactor.core.publisher.Mono; +import java.util.function.Function; + public class DefaultAsyncEvent implements AsyncEvent { - private Mono async = Mono.empty(); - private Mono first = Mono.empty(); + private transient Mono async = Mono.empty(); + private transient Mono first = Mono.empty(); - private boolean hasListener; + private transient boolean hasListener; public synchronized void async(Publisher publisher) { hasListener = true; - this.async = async.then(Mono.from(publisher).then()); + this.async = async.then(AsyncEventHooks.hookAsync(this, Mono.fromDirect(publisher))); } @Override public synchronized void first(Publisher publisher) { hasListener = true; - this.first = Mono.from(publisher).then(first); + this.first = AsyncEventHooks.hookFirst(this, Mono.fromDirect(publisher)).then(first); + } + + @Override + public synchronized void transformFirst(Function, Publisher> mapper) { + hasListener = true; + this.first = Mono.fromDirect(mapper.apply(this.first)); + } + + @Override + public synchronized void transform(Function, Publisher> mapper) { + hasListener = true; + this.async = Mono.fromDirect(mapper.apply(this.async)); } @Override public Mono getAsync() { - return this.first.then(this.async); + return this.first.then(this.async).then(); } @Override 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 ea77021c3..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 @@ -5,6 +5,10 @@ import lombok.Getter; import lombok.Setter; import org.hswebframework.web.i18n.LocaleUtils; +import org.springframework.util.StringUtils; +import reactor.core.publisher.Mono; + +import java.util.Locale; /** * 支持国际化消息的异常,code为 @@ -15,7 +19,7 @@ */ @Getter @Setter(AccessLevel.PROTECTED) -public class I18nSupportException extends RuntimeException { +public class I18nSupportException extends TraceSourceException { /** * 消息code,在message.properties文件中定义的key @@ -43,13 +47,70 @@ public I18nSupportException(String code, Throwable cause, Object... args) { this.i18nCode = code; } + public String getOriginalMessage() { + return super.getMessage() != null ? super.getMessage() : getI18nCode(); + } + @Override public String getMessage() { - return super.getMessage() != null ? super.getMessage() : getLocalizedMessage(); + return getLocalizedMessage(); } @Override - public String getLocalizedMessage() { - return LocaleUtils.resolveMessage(i18nCode, args); + public final String getLocalizedMessage() { + return getLocalizedMessage(LocaleUtils.current()); + } + + public String getLocalizedMessage(Locale locale) { + return LocaleUtils.resolveMessage(i18nCode, locale, getOriginalMessage(), args); + } + + public final Mono getLocalizedMessageReactive() { + return LocaleUtils + .currentReactive() + .map(this::getLocalizedMessage); + } + + public static String tryGetLocalizedMessage(Throwable error, Locale locale) { + if (error instanceof I18nSupportException) { + return ((I18nSupportException) error).getLocalizedMessage(locale); + } + String msg = error.getMessage(); + + if (!StringUtils.hasText(msg)) { + msg = "error." + error.getClass().getSimpleName(); + } + if (msg.contains(".")) { + return LocaleUtils.resolveMessage(msg, locale, msg); + } + return msg; + } + + public static String tryGetLocalizedMessage(Throwable error) { + return tryGetLocalizedMessage(error, LocaleUtils.current()); + } + + public static Mono tryGetLocalizedMessageReactive(Throwable error) { + return LocaleUtils + .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 new file mode 100644 index 000000000..edd0cd4e4 --- /dev/null +++ b/hsweb-core/src/main/java/org/hswebframework/web/exception/TraceSourceException.java @@ -0,0 +1,249 @@ +package org.hswebframework.web.exception; + +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; + +import javax.annotation.Nullable; +import java.util.Locale; +import java.util.Optional; +import java.util.function.Function; + +/** + * 支持溯源的异常,通过{@link TraceSourceException#withSource(Object) }来标识异常的源头. + * 在捕获异常的地方通过获取异常源来处理一些逻辑,比如判断是由哪条数据发生的错误等操作. + * + * @author zhouhao + * @since 4.0.15 + */ +public class TraceSourceException extends RuntimeException { + + private static final String deepTraceKey = TraceSourceException.class.getName() + "_deep"; + private static final Context deepTraceContext = Context.of(deepTraceKey, true); + + private String operation; + + private Object source; + + public TraceSourceException() { + + } + + public TraceSourceException(String message) { + super(message); + } + + public TraceSourceException(Throwable e) { + super(e.getMessage(), e); + } + + public TraceSourceException(String message, Throwable e) { + super(message, e); + } + + @Nullable + public Object getSource() { + return source; + } + + @Nullable + public String getOperation() { + return operation; + } + + public TraceSourceException withSource(Object source) { + this.source = source; + return self(); + } + + public TraceSourceException withSource(String operation, Object source) { + this.operation = operation; + this.source = source; + return self(); + } + + protected TraceSourceException self() { + return this; + } + + /** + * 深度溯源上下文,用来标识是否是深度溯源的异常.开启深度追踪后,会创建新的{@link TraceSourceException}对象. + * + * @return 上下文 + * @see Flux#contextWrite(ContextView) + * @see Mono#contextWrite(ContextView) + */ + @Deprecated + public static Context deepTraceContext() { + return deepTraceContext; + } + + public static Function> transfer(Object source) { + return transfer(null, source); + } + + + /** + * 溯源异常转换器.通常配合{@link Mono#onErrorResume(Function)}使用. + *

+ * 转换逻辑: + *

+ * 1. 如果捕获的异常不是TraceSourceException,则直接创建新的TraceSourceException并返回. + *

+ * 2. 如果捕获的异常是TraceSourceException,并且上下文没有指定{@link TraceSourceException#deepTraceContext()}, + * 则修改捕获的TraceSourceException异常中的source.如果上下文中指定了{@link TraceSourceException#deepTraceContext()} + * 则创建新的TraceSourceException + * + *

{@code
+     *
+     *  doSomething()
+     *  .onErrorResume(TraceSourceException.transfer(data))
+     *
+     * }
+ * + * @param operation 操作名称 + * @param source 源 + * @param 泛型 + * @return 转换器 + * @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 -> 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; + } + + 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; + } + + public static Mono tryGetOperationLocalizedReactive(Throwable err) { + return LocaleUtils + .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 fc62966ed..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 @@ -6,10 +6,13 @@ import lombok.Setter; import org.hswebframework.web.i18n.LocaleUtils; import org.springframework.http.HttpStatus; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.ResponseStatus; import javax.validation.ConstraintViolation; import java.util.*; +import java.util.stream.Collectors; @Getter @Setter @@ -31,9 +34,6 @@ public ValidationException(String property, String message, Object... args) { public ValidationException(String message, List details, Object... args) { super(message, args); this.details = details; - for (Detail detail : this.details) { - detail.translateI18n(args); - } } public ValidationException(Set> violations) { @@ -48,23 +48,44 @@ public ValidationException(Set> violations) { //{0} 属性 ,{1} 验证消息 //property也支持国际化? - String resolveMessage = propertyI18nEnabled ? - LocaleUtils.resolveMessage(first.getRootBeanClass().getName() + "." + property, property) - : property; + String propertyI18n = propertyI18nEnabled ? + first.getRootBeanClass().getName() + "." + property + : property; - setArgs(new Object[]{resolveMessage, first.getMessage()}); + setArgs(new Object[]{propertyI18n, first.getMessage()}); details = new ArrayList<>(violations.size()); for (ConstraintViolation violation : violations) { - details.add(new Detail(violation.getPropertyPath().toString(), violation.getMessage(), null)); + details.add(new Detail(violation.getPropertyPath().toString(), + violation.getMessage(), + null)); } } + public List getDetails(Locale locale) { + return CollectionUtils.isEmpty(details) + ? Collections.emptyList() + : details + .stream() + .map(detail -> detail.translateI18n(locale)) + .collect(Collectors.toList()); + } + + @Override + public String getLocalizedMessage(Locale locale) { + if (propertyI18nEnabled && "validation.property_validate_failed".equals(getI18nCode()) && getArgs().length > 0) { + Object[] args = getArgs().clone(); + args[0] = LocaleUtils.resolveMessage(String.valueOf(args[0]), locale, String.valueOf(args[0])); + return LocaleUtils.resolveMessage(getI18nCode(), locale, getOriginalMessage(), args); + } + return super.getLocalizedMessage(locale); + } @Getter @Setter @AllArgsConstructor public static class Detail { + @Schema(description = "字段") String property; @@ -74,10 +95,38 @@ public static class Detail { @Schema(description = "详情") Object detail; - public void translateI18n(Object... args) { - if (message.contains(".")) { - message = LocaleUtils.resolveMessage(message, message, args); + public Detail translateI18n(Locale locale) { + if (StringUtils.hasText(message) && message.contains(".")) { + return new Detail(property, LocaleUtils.resolveMessage(message, locale, message), detail); } + 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 d3c66ade3..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 @@ -1,19 +1,20 @@ package org.hswebframework.web.i18n; +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; import org.springframework.context.MessageSource; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.core.publisher.Signal; -import reactor.core.publisher.SignalType; +import reactor.core.CoreSubscriber; +import reactor.core.publisher.*; import reactor.util.context.Context; -import java.util.Locale; -import java.util.function.BiConsumer; -import java.util.function.BiFunction; -import java.util.function.Consumer; -import java.util.function.Function; +import javax.annotation.Nonnull; +import java.util.*; +import java.util.concurrent.Callable; +import java.util.function.*; /** * 用于进行国际化消息转换 @@ -23,7 +24,6 @@ *
  • {@link LocaleUtils#current()}
  • *
  • {@link LocaleUtils#currentReactive()}
  • *
  • {@link LocaleUtils#resolveMessageReactive(String, Object...)}
  • - *
  • {@link LocaleUtils#doOnNext(BiConsumer)}
  • * * * @author zhouhao @@ -33,10 +33,58 @@ public final class LocaleUtils { public static final Locale DEFAULT_LOCALE = Locale.getDefault(); - private static final ThreadLocal CONTEXT_THREAD_LOCAL = new ThreadLocal<>(); + private static final FastThreadLocal CONTEXT_THREAD_LOCAL = new FastThreadLocal<>(); 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; + } + /** * 获取当前的语言地区,如果没有设置则返回系统默认语言 * @@ -63,11 +111,12 @@ public static Locale current() { * @return 返回值 */ public static R doWith(T data, Locale locale, BiFunction mapper) { + Locale old = CONTEXT_THREAD_LOCAL.get(); try { CONTEXT_THREAD_LOCAL.set(locale); return mapper.apply(data, locale); } finally { - CONTEXT_THREAD_LOCAL.remove(); + CONTEXT_THREAD_LOCAL.set(old); } } @@ -78,11 +127,29 @@ public static R doWith(T data, Locale locale, BiFunction ma * @param consumer 任务 */ public static void doWith(Locale locale, Consumer consumer) { + Locale old = CONTEXT_THREAD_LOCAL.get(); try { CONTEXT_THREAD_LOCAL.set(locale); consumer.accept(locale); } finally { - CONTEXT_THREAD_LOCAL.remove(); + CONTEXT_THREAD_LOCAL.set(old); + } + } + + /** + * 使用指定的区域来执行某些操作 + * + * @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); } } @@ -91,7 +158,7 @@ public static void doWith(Locale locale, Consumer consumer) { *

    *

          * monoOrFlux
    -     * .subscriberContext(LocaleUtils.useLocale(locale))
    +     * .contextWrite(LocaleUtils.useLocale(locale))
          * 
    * * @param locale 区域 @@ -109,8 +176,25 @@ public static Function useLocale(Locale locale) { @SuppressWarnings("all") public static Mono currentReactive() { return Mono - .subscriberContext() - .map(ctx -> 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); + } + }); } /** @@ -251,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); + }); } /** @@ -268,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)); } /** @@ -283,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)); } /** @@ -371,7 +455,7 @@ public static Consumer> on(SignalType type, BiConsumer, if (signal.getType() != type) { return; } - Locale locale = signal.getContext().getOrDefault(Locale.class, DEFAULT_LOCALE); + Locale locale = signal.getContextView().getOrDefault(Locale.class, DEFAULT_LOCALE); doWith(locale, l -> operation.accept(signal, l)); }; @@ -397,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)); }; } @@ -450,4 +534,92 @@ public static > Function doOnError(BiConsumer operation.accept(s.getThrowable(), l)); } + public static Flux transform(Flux flux) { + return new LocaleFlux<>(flux); + } + + public static Mono transform(Mono mono) { + return new LocaleMono<>(mono); + } + + @AllArgsConstructor + static class LocaleMono extends Mono { + private final Mono source; + + @Override + public void subscribe(@Nonnull CoreSubscriber actual) { + doWith(actual, + actual.currentContext().getOrDefault(Locale.class, DEFAULT_LOCALE), + (a, l) -> { + source.subscribe( + new LocaleSwitchSubscriber<>(a) + ); + return null; + } + ); + } + } + + @AllArgsConstructor + static class LocaleFlux extends Flux { + private final Flux source; + + @Override + public void subscribe(@Nonnull CoreSubscriber actual) { + doWith(actual, + actual.currentContext().getOrDefault(Locale.class, DEFAULT_LOCALE), + (a, l) -> { + source.subscribe( + new LocaleSwitchSubscriber<>(a) + ); + return null; + } + ); + } + } + + @AllArgsConstructor + static class LocaleSwitchSubscriber extends BaseSubscriber { + private final CoreSubscriber actual; + + @Override + @Nonnull + public Context currentContext() { + return actual + .currentContext(); + } + + @Override + protected void hookOnSubscribe(@Nonnull Subscription subscription) { + actual.onSubscribe(this); + } + + private Locale current() { + return currentContext() + .getOrDefault(Locale.class, DEFAULT_LOCALE); + } + + @Override + protected void hookOnComplete() { + doWith(current(), (l) -> actual.onComplete()); + } + + @Override + protected void hookOnError(@Nonnull Throwable error) { + + doWith(error, current(), (v, l) -> { + actual.onError(v); + return null; + }); + } + + @Override + protected void hookOnNext(@Nonnull T value) { + + doWith(value, current(), (v, l) -> { + actual.onNext(v); + return null; + }); + } + } } 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/i18n/WebFluxLocaleFilter.java b/hsweb-core/src/main/java/org/hswebframework/web/i18n/WebFluxLocaleFilter.java index 748d7faee..fc6a0d0fe 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/i18n/WebFluxLocaleFilter.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/i18n/WebFluxLocaleFilter.java @@ -15,7 +15,8 @@ public class WebFluxLocaleFilter implements WebFilter { public Mono filter(@NonNull ServerWebExchange exchange, WebFilterChain chain) { return chain .filter(exchange) - .subscriberContext(LocaleUtils.useLocale(getLocaleContext(exchange))); + .as(LocaleUtils::transform) + .contextWrite(LocaleUtils.useLocale(getLocaleContext(exchange))); } public Locale getLocaleContext(ServerWebExchange exchange) { diff --git a/hsweb-core/src/main/java/org/hswebframework/web/id/IDGenerator.java b/hsweb-core/src/main/java/org/hswebframework/web/id/IDGenerator.java index bbb88dd8e..844039d8d 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/id/IDGenerator.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/id/IDGenerator.java @@ -18,11 +18,7 @@ package org.hswebframework.web.id; -import org.hswebframework.utils.RandomUtil; - -import java.math.BigInteger; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; +import org.hswebframework.web.utils.DigestUtils; /** * ID生成器,用于生成ID @@ -41,7 +37,7 @@ public interface IDGenerator { @SuppressWarnings("unchecked") static IDGenerator getNullGenerator() { - return (IDGenerator) NULL; + return (IDGenerator) NULL; } /** @@ -52,20 +48,12 @@ static IDGenerator getNullGenerator() { /** * 随机字符 */ - IDGenerator RANDOM = RandomUtil::randomChar; + IDGenerator RANDOM = RandomIdGenerator.GLOBAL; /** - * md5(uuid()+random()) + * md5(uuid()) */ - IDGenerator MD5 = () -> { - try { - MessageDigest md = MessageDigest.getInstance("MD5"); - md.update(UUID.generate().concat(RandomUtil.randomChar()).getBytes()); - return new BigInteger(1, md.digest()).toString(16); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } - }; + IDGenerator MD5 = () -> DigestUtils.md5Hex(UUID.generate()); /** * 雪花算法 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 new file mode 100644 index 000000000..01c66f302 --- /dev/null +++ b/hsweb-core/src/main/java/org/hswebframework/web/id/RandomIdGenerator.java @@ -0,0 +1,105 @@ +package org.hswebframework.web.id; + +import io.netty.util.concurrent.FastThreadLocal; +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) +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() + ); + + static final Base64.Encoder encoder = Base64.getUrlEncoder().withoutPadding(); + + private final static FastThreadLocal HOLDER = new FastThreadLocal() { + @Override + protected byte[] initialValue() { + return new byte[24]; + } + }; + + private final byte instanceId; + + public static RandomIdGenerator create(byte instanceId) { + return new RandomIdGenerator(instanceId); + } + + public String generate() { + long now = System.currentTimeMillis(); + byte[] value = HOLDER.get(); + value[0] = instanceId; + + value[1] = (byte) (now >>> 32); + value[2] = (byte) (now >>> 24); + value[3] = (byte) (now >>> 16); + value[4] = (byte) (now >>> 8); + value[5] = (byte) (now); + + nextBytes(value, 6, 8); + nextBytes(value, 8, 16); + nextBytes(value, 16, 24); + 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 void nextBytes(byte[] bytes, int offset, int len) { + Random random = random(); + + for (int i = offset; i < len; ) { + for (int rnd = random.nextInt(), + n = Math.min(len - i, Integer.SIZE / Byte.SIZE); + n-- > 0; rnd >>= Byte.SIZE) { + bytes[i++] = (byte) rnd; + } + } + + } + +} 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 373ac01a5..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,34 +4,36 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.security.SecureRandom; import java.util.*; +import java.util.concurrent.ThreadLocalRandom; @Slf4j public class SnowflakeIdGenerator { - private long workerId; - private long dataCenterId; + private final long workerId; + private final long dataCenterId; private long sequence = 0L; - private long twepoch = 1288834974657L; + private final long twepoch = 1288834974657L; - private long workerIdBits = 5L; - private long datacenterIdBits = 5L; - private long maxWorkerId = -1L ^ (-1L << workerIdBits); - private long maxDataCenterId = -1L ^ (-1L << datacenterIdBits); - private long sequenceBits = 12L; + private final long workerIdBits = 5L; + private final long datacenterIdBits = 5L; + private final long maxWorkerId = ~(-1L << workerIdBits); + private final long maxDataCenterId = ~(-1L << datacenterIdBits); + private final long sequenceBits = 12L; - private long workerIdShift = sequenceBits; - private long datacenterIdShift = sequenceBits + workerIdBits; - private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; - private long sequenceMask = -1L ^ (-1L << sequenceBits); + private final long workerIdShift = sequenceBits; + private final long datacenterIdShift = sequenceBits + workerIdBits; + private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; + private final long sequenceMask = ~(-1L << sequenceBits); private long lastTimestamp = -1L; 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); @@ -41,6 +43,14 @@ public static SnowflakeIdGenerator getInstance() { return generator; } + public static SnowflakeIdGenerator create(int workerId, int dataCenterId) { + return new SnowflakeIdGenerator(workerId, dataCenterId); + } + + public static SnowflakeIdGenerator create() { + return create(ThreadLocalRandom.current().nextInt(31), ThreadLocalRandom.current().nextInt(31)); + } + public SnowflakeIdGenerator(long workerId, long dataCenterId) { // sanity check for workerId if (workerId > maxWorkerId || workerId < 0) { @@ -56,10 +66,11 @@ public 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 cccc9aced..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,12 +1,15 @@ 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; import reactor.core.publisher.*; import reactor.util.context.Context; +import reactor.util.context.ContextView; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; @@ -27,29 +30,29 @@ public static Function start(String... keyAndValue) { public static Mono mdc(String key, String value) { return Mono .empty() - .subscriberContext(start(key, value)); + .contextWrite(start(key, value)); } public static Mono mdc(String... keyAndValue) { return Mono .empty() - .subscriberContext(start(keyAndValue)); + .contextWrite(start(keyAndValue)); } 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 LinkedHashMap<>(context)); + return ctx.put(CONTEXT_KEY, new ConcurrentHashMap<>(context)); } }; } - public static void log(Context context, Consumer> logger) { + public static void log(ContextView context, Consumer> logger) { Optional> maybeContextMap = context.getOrEmpty(CONTEXT_KEY); if (!maybeContextMap.isPresent()) { logger.accept(new HashMap<>()); @@ -70,7 +73,7 @@ public static Consumer> on(SignalType type, BiConsumer> maybeContextMap - = signal.getContext().getOrEmpty(CONTEXT_KEY); + = signal.getContextView().getOrEmpty(CONTEXT_KEY); if (!maybeContextMap.isPresent()) { logger.accept(new HashMap<>(), signal); } else { @@ -87,22 +90,21 @@ public static Consumer> on(SignalType type, BiConsumer mdc(Consumer> consumer) { return Mono - .subscriberContext() - .doOnNext(ctx -> { + .deferContextual(ctx -> { Optional> maybeContextMap = ctx.getOrEmpty(CONTEXT_KEY); if (maybeContextMap.isPresent()) { consumer.accept(maybeContextMap.get()); } else { consumer.accept(Collections.emptyMap()); - log.warn("logger context is empty,please call publisher.subscriberContext(ReactiveLogger.mdc()) first!"); + log.warn("logger context is empty,please call publisher.contextWrite(ReactiveLogger.mdc()) first!"); } - }) - .then(); + return Mono.empty(); + }); } public static BiConsumer> handle(BiConsumer> logger) { return (t, rFluxSink) -> { - log(rFluxSink.currentContext(), context -> { + log(rFluxSink.contextView(), context -> { logger.accept(t, rFluxSink); }); }; 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 cf6c1b3d0..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 = 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/DigestUtils.java b/hsweb-core/src/main/java/org/hswebframework/web/utils/DigestUtils.java new file mode 100644 index 000000000..4c863a9c8 --- /dev/null +++ b/hsweb-core/src/main/java/org/hswebframework/web/utils/DigestUtils.java @@ -0,0 +1,127 @@ +package org.hswebframework.web.utils; + +import io.netty.util.concurrent.FastThreadLocal; +import io.seruco.encoding.base62.Base62; +import org.apache.commons.codec.binary.Hex; +import org.hswebframework.web.id.RandomIdGenerator; + +import java.security.MessageDigest; +import java.util.function.Consumer; +import java.util.function.Supplier; + +public class DigestUtils { + + public static final FastThreadLocal md5 = new FastThreadLocal() { + @Override + protected MessageDigest initialValue() { + return org.apache.commons.codec.digest.DigestUtils.getMd5Digest(); + } + }; + + public static final FastThreadLocal sha256 = new FastThreadLocal() { + @Override + protected MessageDigest initialValue() { + return org.apache.commons.codec.digest.DigestUtils.getSha256Digest(); + } + }; + + public static final FastThreadLocal sha1 = new FastThreadLocal() { + @Override + protected MessageDigest initialValue() { + return org.apache.commons.codec.digest.DigestUtils.getSha1Digest(); + } + }; + + private static final Base62 base62 = Base62.createInstance(); + + + public static Base62 base62(){ + return base62; + } + public static byte[] md5(Consumer digestHandler) { + return digest(md5::get, digestHandler); + } + + public static String md5Hex(Consumer digestHandler) { + return digestHex(md5::get, digestHandler); + } + + public static byte[] sha1(Consumer digestHandler) { + return digest(sha1::get, digestHandler); + } + + public static String sha1Hex(Consumer digestHandler) { + return digestHex(sha1::get, digestHandler); + } + + public static byte[] sha256(Consumer digestHandler) { + return digest(sha256::get, digestHandler); + } + + public static String sha256Hex(Consumer digestHandler) { + return digestHex(sha1::get, digestHandler); + } + + public static byte[] md5(byte[] data) { + return org.apache.commons.codec.digest.DigestUtils.digest(md5.get(), data); + } + + public static byte[] md5(String str) { + return md5(str.getBytes()); + } + + public static String md5Hex(String str) { + return Hex.encodeHexString(md5(str.getBytes())); + } + public static String md5Base62(String str) { + return new String(base62.encode(md5(str.getBytes()))); + } + + public static byte[] sha256(byte[] data) { + return org.apache.commons.codec.digest.DigestUtils.digest(sha256.get(), data); + } + + public static byte[] sha256(String str) { + return sha256(str.getBytes()); + } + + public static String sha256Hex(String str) { + return Hex.encodeHexString(sha256(str.getBytes())); + } + + public static byte[] sha1(byte[] data) { + return org.apache.commons.codec.digest.DigestUtils.digest(sha1.get(), data); + } + + public static byte[] sha1(String str) { + return sha1(str.getBytes()); + } + + public static String sha1Hex(String str) { + return Hex.encodeHexString(sha1(str.getBytes())); + } + + public static byte[] digest(MessageDigest digest, byte[] data) { + return org.apache.commons.codec.digest.DigestUtils.digest(digest, data); + } + + public static byte[] digest(MessageDigest digest, String str) { + return digest(digest, str.getBytes()); + } + + public static String digestHex(MessageDigest digest, String str) { + return Hex.encodeHexString(digest(digest, str)); + } + + private static byte[] digest(Supplier digestSupplier, + Consumer digestHandler) { + MessageDigest digest = digestSupplier.get(); + digestHandler.accept(digest); + return digest.digest(); + } + + private static String digestHex(Supplier digestSupplier, + Consumer digestHandler) { + return Hex.encodeHexString(digest(digestSupplier, digestHandler)); + } +} 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/utils/ExpressionUtils.java b/hsweb-core/src/main/java/org/hswebframework/web/utils/ExpressionUtils.java index de2af2fb1..9ebceb7f6 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/utils/ExpressionUtils.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/utils/ExpressionUtils.java @@ -3,7 +3,6 @@ import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.apache.commons.beanutils.BeanUtilsBean2; -import org.apache.commons.codec.digest.DigestUtils; import org.hswebframework.expands.script.engine.DynamicScriptEngine; import org.hswebframework.expands.script.engine.DynamicScriptEngineFactory; import org.hswebframework.expands.script.engine.ExecuteResult; @@ -91,7 +90,10 @@ public static String analytical(String expression, Map vars, Str if (StringUtils.isEmpty(var)) { return ""; } - + Object val = vars.get(var); + if (val != null) { + return String.valueOf(val); + } if ("spel".equalsIgnoreCase(language) && !var.contains("#")) { try { Object fast = BeanUtilsBean2.getInstance().getPropertyUtils().getProperty(vars, var); @@ -116,7 +118,7 @@ public static String analytical(String expression, Map vars, Str try { return String.valueOf(engine.execute(id, vars).getIfSuccess()); } catch (Exception e) { - log.error(e.getMessage(), e); + log.error(e.getLocalizedMessage(), e); return ""; } diff --git a/hsweb-core/src/main/java/org/hswebframework/web/utils/FluxCache.java b/hsweb-core/src/main/java/org/hswebframework/web/utils/FluxCache.java new file mode 100644 index 000000000..a8c40c33a --- /dev/null +++ b/hsweb-core/src/main/java/org/hswebframework/web/utils/FluxCache.java @@ -0,0 +1,30 @@ +package org.hswebframework.web.utils; + +import org.reactivestreams.Publisher; +import reactor.core.Disposable; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.function.Function; + +public class FluxCache { + + + public static Flux cache(Flux source, Function, Publisher> handler) { + Disposable[] ref = new Disposable[1]; + Flux cache = source + .doFinally((s) -> ref[0] = null) + .replay() + .autoConnect(1, dis -> ref[0] = dis); + return Mono + .from(handler.apply(cache)) + .thenMany(cache) + .doFinally((s) -> { + if (ref[0] != null) { + ref[0].dispose(); + } + }); + + } + +} diff --git a/hsweb-core/src/main/java/org/hswebframework/web/utils/HttpParameterConverter.java b/hsweb-core/src/main/java/org/hswebframework/web/utils/HttpParameterConverter.java index 46257aa19..d61c1b0aa 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/utils/HttpParameterConverter.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/utils/HttpParameterConverter.java @@ -2,6 +2,7 @@ import org.apache.commons.beanutils.BeanMap; import org.hswebframework.utils.time.DateFormatter; +import org.hswebframework.web.bean.FastBeanCopier; import java.util.*; import java.util.function.Function; @@ -61,9 +62,7 @@ public HttpParameterConverter(Object bean) { if (bean instanceof Map) { beanMap = ((Map) bean); } else { - beanMap = new HashMap<>((Map) new BeanMap(bean)); - beanMap.remove("class"); - beanMap.remove("declaringClass"); + beanMap = FastBeanCopier.copy(bean,new HashMap<>()); } } diff --git a/hsweb-core/src/main/java/org/hswebframework/web/utils/ModuleUtils.java b/hsweb-core/src/main/java/org/hswebframework/web/utils/ModuleUtils.java index 2948bf367..46e92c6f2 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/utils/ModuleUtils.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/utils/ModuleUtils.java @@ -42,7 +42,7 @@ private ModuleUtils() { ModuleUtils.register(moduleInfo); } } catch (Exception e) { - log.error(e.getMessage(), e); + log.error(e.getLocalizedMessage(), e); } } diff --git a/hsweb-core/src/main/java/org/hswebframework/web/validator/ValidatorUtils.java b/hsweb-core/src/main/java/org/hswebframework/web/validator/ValidatorUtils.java index e578f1b4d..4c5a1a748 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/validator/ValidatorUtils.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/validator/ValidatorUtils.java @@ -17,6 +17,9 @@ private ValidatorUtils() { public static Validator getValidator() { if (validator == null) { synchronized (ValidatorUtils.class) { + if (validator != null) { + return validator; + } Configuration configuration = Validation .byDefaultProvider() .configure(); @@ -32,14 +35,29 @@ public static Validator getValidator() { return validator; } - @SuppressWarnings("all") - public static T tryValidate(T bean, Class... group) { + public static T tryValidate(T bean, Class... group) { Set> violations = getValidator().validate(bean, group); if (!violations.isEmpty()) { - throw new ValidationException(violations); + throw new ValidationException(violations).withSource(bean); } return bean; } + public static T tryValidate(T bean, String property, Class... group) { + Set> violations = getValidator().validateProperty(bean, property, group); + if (!violations.isEmpty()) { + throw new ValidationException(violations).withSource(bean); + } + + return bean; + } + + public static void tryValidate(Class bean, String property, Object value, Class... group) { + Set> violations = getValidator().validateValue(bean, property, value, group); + if (!violations.isEmpty()) { + throw new ValidationException(violations).withSource(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 f04d33f0f..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 @@ -1,14 +1,21 @@ package org.hswebframework.web.bean; +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.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; +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(); @@ -59,6 +139,43 @@ public void testMapArray() { } + @Test + public void testMapList() { + Map data = new HashMap<>(); + data.put("templates", new HashMap() { + { + put("0", Collections.singletonMap("name", "test")); + put("1", Collections.singletonMap("name", "test")); + } + }); + + Config config = FastBeanCopier.copy(data, new Config()); + + Assert.assertNotNull(config); + Assert.assertNotNull(config.templates); + System.out.println(config.templates); + Assert.assertEquals(2, config.templates.size()); + + + } + + @Getter + @Setter + public static class Config { + private List