diff --git a/CHANGELOG.md b/CHANGELOG.md index 773b244c..09dfe432 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,30 @@ -# [0.0.4](https://github.com/johnnymillergh/muscle-and-fitness-server/compare/0.0.3...0.0.4) (2021-08-18) +# [0.0.5](https://github.com/johnnymillergh/muscle-and-fitness-server/compare/0.0.4...0.0.5) (2021-08-26) + + +### Features + +* **$auth-center:** download and export role info ([b6640ce](https://github.com/johnnymillergh/muscle-and-fitness-server/commit/b6640cec6d45e826bb536e9293ca6cfaa790bb6c)) +* **$auth-center:** import role data from Excel ([3468082](https://github.com/johnnymillergh/muscle-and-fitness-server/commit/3468082f45d9d2dc7c18fb39f90fa7a13f9dbde4)) + + +### Performance Improvements + +* **$api-gateway:** parse user's ID from JWT ([a70434f](https://github.com/johnnymillergh/muscle-and-fitness-server/commit/a70434f3ad98932b457be926571202c968b438f0)) +* **$Druid:** dynamic connection pool size ([ea37259](https://github.com/johnnymillergh/muscle-and-fitness-server/commit/ea372596670366db47aec8439c1b12487e68ed30)) +* **$Druid:** set connection pool size equal to 2*processors ([a654323](https://github.com/johnnymillergh/muscle-and-fitness-server/commit/a654323b432cc1f7858a8bc07b568f2719642e4f)) +* **$OSS:** transform IO by NIO ([bce0cf4](https://github.com/johnnymillergh/muscle-and-fitness-server/commit/bce0cf4df5fa607996c8ef16808021f4ce635655)) +* **$starter:** refine Excel data abstract class ([9d5de27](https://github.com/johnnymillergh/muscle-and-fitness-server/commit/9d5de27077fba78014c458ebb4fa30b8c422a5b6)) +* **$starter:** simplify AbstractExcelImportController.java ([651615c](https://github.com/johnnymillergh/muscle-and-fitness-server/commit/651615c08c0e07aff27bf955ce3956794602b933)) + + +### BREAKING CHANGES + +* **$Druid:** set connection pool size equal to 2*processors +* **$Druid:** dynamic connection pool size, set by CPU core count + + + +# [0.0.4](https://github.com/johnnymillergh/muscle-and-fitness-server/compare/0.0.3...0.0.4) (2021-08-18) ### Features diff --git a/README.md b/README.md index 8aa64fda..ec7ce0fa 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,9 @@ Here is the highlights of **Muscle and Fitness Server**: 4. Secured API. [RBAC](https://en.wikipedia.org/wiki/Role-based_access_control) by API gateway and Auth Center. JWT authentication, and RBAC authorization. -5. [MySQL Replication for High Availability](https://severalnines.com/resources/database-management-tutorials/mysql-replication-high-availability-tutorial). Multi data source. [Dynamic SQL read-write isolation](https://baomidou.com/guide/dynamic-datasource.html). MyBatis-Plus is the integrated ORM library. +5. [MySQL Replication for High Availability](https://severalnines.com/resources/database-management-tutorials/mysql-replication-high-availability-tutorial). Multi data source. [Dynamic SQL read-write isolation](https://baomidou.com/guide/dynamic-datasource.html). [MyBatis-Plus](https://github.com/baomidou/mybatis-plus) is the integrated ORM library. [Druid](https://github.com/alibaba/druid) is the database connection pool. Dynamically enhance connection pool size by CPU count (logical processor count). Read more at [How to Find the Optimal Database Connection Pool Size](https://wiki.postgresql.org/wiki/Number_Of_Database_Connections#How_to_Find_the_Optimal_Database_Connection_Pool_Size), [Sizing the Connection Pool](https://dev.mysql.com/doc/connector-j/5.1/en/connector-j-usagenotes-j2ee-concepts-connection-pooling.html#idm46216069663472). -6. Redis 6.x support. [Master-slave replication for high availability](https://redis.io/topics/replication). Redis cluster +6. Redis 6.x support. [Master-slave replication for high availability](https://redis.io/topics/replication). Redis cluster. 7. Docker, Rancher Kubernetes support. Google JIB for building Docker container images. diff --git a/api-gateway/pom.xml b/api-gateway/pom.xml index b2e16085..a255bd7a 100644 --- a/api-gateway/pom.xml +++ b/api-gateway/pom.xml @@ -10,7 +10,7 @@ com.jmsoftware.maf muscle-and-fitness-server - 0.0.4 + 0.0.5 diff --git a/api-gateway/src/main/java/com/jmsoftware/maf/apigateway/ApiGatewayApplication.java b/api-gateway/src/main/java/com/jmsoftware/maf/apigateway/ApiGatewayApplication.java index 06ded326..5c415beb 100644 --- a/api-gateway/src/main/java/com/jmsoftware/maf/apigateway/ApiGatewayApplication.java +++ b/api-gateway/src/main/java/com/jmsoftware/maf/apigateway/ApiGatewayApplication.java @@ -2,6 +2,7 @@ import com.jmsoftware.maf.reactivespringcloudstarter.helper.SpringBootStartupHelper; import lombok.extern.slf4j.Slf4j; +import lombok.val; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @@ -18,16 +19,11 @@ @EnableDiscoveryClient @SpringBootApplication public class ApiGatewayApplication { - private static final StopWatch STOP_WATCH = new StopWatch(); - private static SpringBootStartupHelper springBootStartupHelper; - - public ApiGatewayApplication(SpringBootStartupHelper springBootStartupHelper) { - ApiGatewayApplication.springBootStartupHelper = springBootStartupHelper; - } - public static void main(String[] args) { - STOP_WATCH.start(); - SpringApplication.run(ApiGatewayApplication.class, args); - springBootStartupHelper.stop(STOP_WATCH); + val stopWatch = new StopWatch(); + stopWatch.start(); + val configurableApplicationContext = SpringApplication.run(ApiGatewayApplication.class, args); + val springBootStartupHelper = configurableApplicationContext.getBean(SpringBootStartupHelper.class); + springBootStartupHelper.stop(stopWatch); } } diff --git a/api-gateway/src/main/java/com/jmsoftware/maf/apigateway/security/impl/JwtReactiveServerSecurityContextRepositoryImpl.java b/api-gateway/src/main/java/com/jmsoftware/maf/apigateway/security/impl/JwtReactiveServerSecurityContextRepositoryImpl.java index 0f6e3a9c..bc9c9ac4 100644 --- a/api-gateway/src/main/java/com/jmsoftware/maf/apigateway/security/impl/JwtReactiveServerSecurityContextRepositoryImpl.java +++ b/api-gateway/src/main/java/com/jmsoftware/maf/apigateway/security/impl/JwtReactiveServerSecurityContextRepositoryImpl.java @@ -45,8 +45,8 @@ public Mono save(ServerWebExchange exchange, SecurityContext context) { public Mono load(ServerWebExchange exchange) { val request = exchange.getRequest(); // Ignore allowed URL - for (var ignoredUrl : mafConfiguration.flattenIgnoredUrls()) { - if (antPathMatcher.match(ignoredUrl, request.getURI().getPath())) { + for (var ignoredUrl : this.mafConfiguration.flattenIgnoredUrls()) { + if (this.antPathMatcher.match(ignoredUrl, request.getURI().getPath())) { return Mono.empty(); } } @@ -56,10 +56,11 @@ public Mono load(ServerWebExchange exchange) { HttpHeaders.AUTHORIZATION, request.getMethod(), request.getURI()); return Mono.error(new SecurityException(HttpStatus.NETWORK_AUTHENTICATION_REQUIRED, "JWT Required")); } - return authCenterRemoteApi.parse(authorization) + return this.authCenterRemoteApi.parse(authorization) .map(parseJwtResponse -> { log.info("parseJwtResponse: {}", parseJwtResponse); val userPrincipal = UserPrincipal.createByUsername(parseJwtResponse.getUsername()); + userPrincipal.setId(parseJwtResponse.getId()); val authentication = new UsernamePasswordAuthenticationToken(userPrincipal, null); log.warn("About to authenticate… Authentication is created. {}", authentication); return authentication; diff --git a/api-gateway/src/main/java/com/jmsoftware/maf/apigateway/security/impl/RbacReactiveAuthorizationManagerImpl.java b/api-gateway/src/main/java/com/jmsoftware/maf/apigateway/security/impl/RbacReactiveAuthorizationManagerImpl.java index 75235ee1..b8458821 100644 --- a/api-gateway/src/main/java/com/jmsoftware/maf/apigateway/security/impl/RbacReactiveAuthorizationManagerImpl.java +++ b/api-gateway/src/main/java/com/jmsoftware/maf/apigateway/security/impl/RbacReactiveAuthorizationManagerImpl.java @@ -111,8 +111,10 @@ public Mono check(Mono authentication, Au request.getMethod(), request.getURI(), userPrincipal.getUsername()); request .mutate() - .headers(httpHeaders -> httpHeaders.set(MafHttpHeader.X_USERNAME.getHeader(), - userPrincipal.getUsername())) + .headers(httpHeaders -> { + httpHeaders.set(MafHttpHeader.X_ID.getHeader(), String.valueOf(userPrincipal.getId())); + httpHeaders.set(MafHttpHeader.X_USERNAME.getHeader(), userPrincipal.getUsername()); + }) .build(); return new AuthorizationDecision(true); } diff --git a/auth-center/pom.xml b/auth-center/pom.xml index f9650d06..de64fb30 100644 --- a/auth-center/pom.xml +++ b/auth-center/pom.xml @@ -10,7 +10,7 @@ com.jmsoftware.maf muscle-and-fitness-server - 0.0.4 + 0.0.5 @@ -219,5 +219,27 @@ ${jjwt.version} runtime + + + + org.apache.poi + poi + ${poi.version} + provided + + + org.apache.poi + poi-ooxml + ${poi.version} + provided + + + + + io.minio + minio + ${minio.version} + provided + diff --git a/auth-center/src/main/java/com/jmsoftware/maf/authcenter/AuthCenterApplication.java b/auth-center/src/main/java/com/jmsoftware/maf/authcenter/AuthCenterApplication.java index b6bead0d..479e796a 100644 --- a/auth-center/src/main/java/com/jmsoftware/maf/authcenter/AuthCenterApplication.java +++ b/auth-center/src/main/java/com/jmsoftware/maf/authcenter/AuthCenterApplication.java @@ -2,6 +2,7 @@ import com.jmsoftware.maf.springcloudstarter.helper.SpringBootStartupHelper; import lombok.extern.slf4j.Slf4j; +import lombok.val; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @@ -13,24 +14,18 @@ *

* Change description here. * - * @author Johnny Miller (锺俊), email: johnnysviva@outlook.com - * @date 3/12/20 9:57 AM + * @author Johnny Miller (锺俊), email: johnnysviva@outlook.com, date: 3/12/20 9:57 AM **/ @Slf4j @EnableFeignClients @EnableDiscoveryClient @SpringBootApplication public class AuthCenterApplication { - private static final StopWatch STOP_WATCH = new StopWatch(); - private static SpringBootStartupHelper springBootStartupHelper; - - public AuthCenterApplication(SpringBootStartupHelper springBootStartupHelper) { - AuthCenterApplication.springBootStartupHelper = springBootStartupHelper; - } - public static void main(String[] args) { - STOP_WATCH.start(); - SpringApplication.run(AuthCenterApplication.class, args); - springBootStartupHelper.stop(STOP_WATCH); + val stopWatch = new StopWatch(); + stopWatch.start(); + val configurableApplicationContext = SpringApplication.run(AuthCenterApplication.class, args); + val springBootStartupHelper = configurableApplicationContext.getBean(SpringBootStartupHelper.class); + springBootStartupHelper.stop(stopWatch); } } diff --git a/auth-center/src/main/java/com/jmsoftware/maf/authcenter/permission/entity/persistence/Permission.java b/auth-center/src/main/java/com/jmsoftware/maf/authcenter/permission/entity/persistence/Permission.java index 0bb7187a..52f4462d 100644 --- a/auth-center/src/main/java/com/jmsoftware/maf/authcenter/permission/entity/persistence/Permission.java +++ b/auth-center/src/main/java/com/jmsoftware/maf/authcenter/permission/entity/persistence/Permission.java @@ -6,7 +6,7 @@ import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; -import java.util.Date; +import java.time.LocalDateTime; import static com.baomidou.mybatisplus.annotation.FieldFill.INSERT; import static com.baomidou.mybatisplus.annotation.FieldFill.UPDATE; @@ -21,109 +21,83 @@ @Data @TableName(value = Permission.TABLE_NAME) public class Permission { + public static final String TABLE_NAME = "permission"; + public static final String COL_ID = "id"; + public static final String COL_URL = "url"; + public static final String COL_DESCRIPTION = "description"; + public static final String COL_TYPE = "type"; + public static final String COL_PERMISSION_EXPRESSION = "permission_expression"; + public static final String COL_METHOD = "method"; + public static final String COL_SORT = "sort"; + public static final String COL_PARENT_ID = "parent_id"; + public static final String COL_CREATED_BY = "created_by"; + public static final String COL_CREATED_TIME = "created_time"; + public static final String COL_MODIFIED_BY = "modified_by"; + public static final String COL_MODIFIED_TIME = "modified_time"; + public static final String COL_DELETED = "deleted"; /** * Primary key */ @TableId(value = COL_ID, type = IdType.AUTO) private Long id; - /** * URL. If type of record is page (1), URL stands for route; if type of record is button (2), URL stands for API */ @TableField(value = COL_URL) private String url; - /** * Permission description */ @TableField(value = COL_DESCRIPTION) private String description; - /** * Permission type. 1 - page; 2 - button */ @TableField(value = COL_TYPE) private Byte type; - /** * Permission expression */ @TableField(value = COL_PERMISSION_EXPRESSION) private String permissionExpression; - /** * HTTP method of API */ @TableField(value = COL_METHOD) private Object method; - /** * Sort number */ @TableField(value = COL_SORT) private Integer sort; - /** * Primary key of parent */ @TableField(value = COL_PARENT_ID) private Long parentId; - /** * Created by */ @TableField(value = COL_CREATED_BY, fill = INSERT) private Long createdBy; - /** * Created time */ @TableField(value = COL_CREATED_TIME, fill = INSERT) - private Date createdTime; - + private LocalDateTime createdTime; /** * Modified by */ @TableField(value = COL_MODIFIED_BY, fill = UPDATE) private Long modifiedBy; - /** * Modified time */ @TableField(value = COL_MODIFIED_TIME, fill = UPDATE) - private Date modifiedTime; - + private LocalDateTime modifiedTime; /** * Deleted flag */ @TableField(value = COL_DELETED, fill = INSERT) private Byte deleted; - - public static final String TABLE_NAME = "permission"; - - public static final String COL_ID = "id"; - - public static final String COL_URL = "url"; - - public static final String COL_DESCRIPTION = "description"; - - public static final String COL_TYPE = "type"; - - public static final String COL_PERMISSION_EXPRESSION = "permission_expression"; - - public static final String COL_METHOD = "method"; - - public static final String COL_SORT = "sort"; - - public static final String COL_PARENT_ID = "parent_id"; - - public static final String COL_CREATED_BY = "created_by"; - - public static final String COL_CREATED_TIME = "created_time"; - - public static final String COL_MODIFIED_BY = "modified_by"; - - public static final String COL_MODIFIED_TIME = "modified_time"; - - public static final String COL_DELETED = "deleted"; } diff --git a/auth-center/src/main/java/com/jmsoftware/maf/authcenter/role/controller/RoleController.java b/auth-center/src/main/java/com/jmsoftware/maf/authcenter/role/controller/RoleController.java index 9fd55e81..9c24d0cd 100644 --- a/auth-center/src/main/java/com/jmsoftware/maf/authcenter/role/controller/RoleController.java +++ b/auth-center/src/main/java/com/jmsoftware/maf/authcenter/role/controller/RoleController.java @@ -1,16 +1,57 @@ package com.jmsoftware.maf.authcenter.role.controller; +import com.jmsoftware.maf.authcenter.role.entity.RoleExcelBean; import com.jmsoftware.maf.authcenter.role.service.RoleService; +import com.jmsoftware.maf.springcloudstarter.controller.AbstractExcelDataController; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.time.Instant; +import java.util.List; + /** * Description: RoleController, change description here. * * @author 钟俊(zhongjun), email: zhongjun@toguide.cn, date: 12/17/2020 4:44 PM **/ +@Slf4j @RestController @RequiredArgsConstructor -public class RoleController { +@RequestMapping("/roles") +public class RoleController extends AbstractExcelDataController { private final RoleService roleService; + + @Override + public void onExceptionOccurred() { + log.error("Exception occurred when uploading excel for role."); + this.fileName.set("role-stat" + Instant.now() + ".xlsx"); + } + + @Override + protected void beforeDatabaseOperation(List beanList) { + log.info("BeforeDatabaseOperation: {}", beanList); + } + + @Override + protected void executeDatabaseOperation(List beanList) throws Exception { + log.info("ExecuteDatabaseOperation: {}", beanList); + this.roleService.save(beanList); + } + + @Override + protected String getTemplateFileName() { + return RoleService.ROLE_TEMPLATE_EXCEL; + } + + @Override + protected List getListForExporting() { + return this.roleService.getListForExporting(); + } + + @Override + protected void validateBeforeAddToBeanList(List beanList, RoleExcelBean bean, int index) throws IllegalArgumentException { + this.roleService.validateBeforeAddToBeanList(beanList, bean, index); + } } diff --git a/auth-center/src/main/java/com/jmsoftware/maf/authcenter/role/entity/RoleExcelBean.java b/auth-center/src/main/java/com/jmsoftware/maf/authcenter/role/entity/RoleExcelBean.java new file mode 100644 index 00000000..47ff4b5f --- /dev/null +++ b/auth-center/src/main/java/com/jmsoftware/maf/authcenter/role/entity/RoleExcelBean.java @@ -0,0 +1,55 @@ +package com.jmsoftware.maf.authcenter.role.entity; + +import com.jmsoftware.maf.authcenter.role.entity.persistence.Role; +import com.jmsoftware.maf.springcloudstarter.annotation.ExcelColumn; +import lombok.Data; +import lombok.val; + +import javax.validation.constraints.NotBlank; + +/** + * Description: RoleExcelBean, change description here. + * + * @author Johnny Miller (锺俊), email: johnnysviva@outlook.com, date: 8/22/2021 10:52 AM + **/ +@Data +public class RoleExcelBean { + /** + * Role name + */ + @NotBlank + @ExcelColumn(name = "Name") + private String name; + /** + * Role description + */ + @NotBlank + @ExcelColumn(name = "Description") + private String description; + + /** + * Transform by role. + * + * @param role the role + * @return the role excel import + */ + public static RoleExcelBean transformBy(Role role) { + val roleExcelImport = new RoleExcelBean(); + roleExcelImport.setName(role.getName()); + roleExcelImport.setDescription(role.getDescription()); + return roleExcelImport; + } + + /** + * Transform by role. + * + * @param roleExcelBean the role excel import + * @return the role excel import + */ + public static Role transformTo(RoleExcelBean roleExcelBean) { + val role = new Role(); + role.setName(roleExcelBean.getName()); + role.setDescription(roleExcelBean.getDescription()); + return role; + } +} diff --git a/auth-center/src/main/java/com/jmsoftware/maf/authcenter/role/entity/persistence/Role.java b/auth-center/src/main/java/com/jmsoftware/maf/authcenter/role/entity/persistence/Role.java index c388c01b..3699ad2a 100644 --- a/auth-center/src/main/java/com/jmsoftware/maf/authcenter/role/entity/persistence/Role.java +++ b/auth-center/src/main/java/com/jmsoftware/maf/authcenter/role/entity/persistence/Role.java @@ -6,7 +6,7 @@ import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; -import java.util.Date; +import java.time.LocalDateTime; import static com.baomidou.mybatisplus.annotation.FieldFill.INSERT; import static com.baomidou.mybatisplus.annotation.FieldFill.UPDATE; @@ -21,69 +21,53 @@ @Data @TableName(value = Role.TABLE_NAME) public class Role { + public static final String TABLE_NAME = "role"; + public static final String COL_ID = "id"; + public static final String COL_NAME = "name"; + public static final String COL_DESCRIPTION = "description"; + public static final String COL_CREATED_BY = "created_by"; + public static final String COL_CREATED_TIME = "created_time"; + public static final String COL_MODIFIED_BY = "modified_by"; + public static final String COL_MODIFIED_TIME = "modified_time"; + public static final String COL_DELETED = "deleted"; /** * Primary key */ @TableId(value = COL_ID, type = IdType.AUTO) private Long id; - /** * Role name */ @TableField(value = COL_NAME) private String name; - /** * Role description */ @TableField(value = COL_DESCRIPTION) private String description; - /** * Created by */ @TableField(value = COL_CREATED_BY, fill = INSERT) private Long createdBy; - /** * Created time */ @TableField(value = COL_CREATED_TIME, fill = INSERT) - private Date createdTime; - + private LocalDateTime createdTime; /** * Modified by */ @TableField(value = COL_MODIFIED_BY, fill = UPDATE) private Long modifiedBy; - /** * Modified time */ @TableField(value = COL_MODIFIED_TIME, fill = UPDATE) - private Date modifiedTime; - + private LocalDateTime modifiedTime; /** * Deleted flag. */ @TableField(value = COL_DELETED, fill = INSERT) private Byte deleted; - - public static final String TABLE_NAME = "role"; - - public static final String COL_ID = "id"; - - public static final String COL_NAME = "name"; - - public static final String COL_DESCRIPTION = "description"; - - public static final String COL_CREATED_BY = "created_by"; - - public static final String COL_CREATED_TIME = "created_time"; - - public static final String COL_MODIFIED_BY = "modified_by"; - - public static final String COL_MODIFIED_TIME = "modified_time"; - - public static final String COL_DELETED = "deleted"; } diff --git a/auth-center/src/main/java/com/jmsoftware/maf/authcenter/role/service/RoleService.java b/auth-center/src/main/java/com/jmsoftware/maf/authcenter/role/service/RoleService.java index 50e5f388..a9ac9518 100644 --- a/auth-center/src/main/java/com/jmsoftware/maf/authcenter/role/service/RoleService.java +++ b/auth-center/src/main/java/com/jmsoftware/maf/authcenter/role/service/RoleService.java @@ -1,12 +1,14 @@ package com.jmsoftware.maf.authcenter.role.service; import com.baomidou.mybatisplus.extension.service.IService; +import com.jmsoftware.maf.authcenter.role.entity.RoleExcelBean; import com.jmsoftware.maf.authcenter.role.entity.persistence.Role; import com.jmsoftware.maf.common.domain.authcenter.role.GetRoleListByUserIdResponse; import com.jmsoftware.maf.common.domain.authcenter.role.GetRoleListByUserIdSingleResponse; import lombok.NonNull; import org.springframework.validation.annotation.Validated; +import javax.validation.Valid; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull; import java.util.List; @@ -21,6 +23,8 @@ */ @Validated public interface RoleService extends IService { + String ROLE_TEMPLATE_EXCEL = "role-stat.xlsx"; + /** * Gets role list by user id. * @@ -44,4 +48,28 @@ public interface RoleService extends IService { * @return the boolean */ boolean checkAdmin(@NotEmpty List<@NotNull Long> roleIdList); + + /** + * Gets list for exporting. + * + * @return the list for exporting + */ + List getListForExporting(); + + /** + * Validate before add to bean list boolean. + * + * @param beanList the bean list + * @param bean the bean + * @param index the index + * @throws IllegalArgumentException the illegal argument exception + */ + void validateBeforeAddToBeanList(List beanList, RoleExcelBean bean, int index) throws IllegalArgumentException; + + /** + * Save. + * + * @param beanList the bean list + */ + void save(@NotEmpty List<@Valid RoleExcelBean> beanList); } diff --git a/auth-center/src/main/java/com/jmsoftware/maf/authcenter/role/service/impl/RoleServiceImpl.java b/auth-center/src/main/java/com/jmsoftware/maf/authcenter/role/service/impl/RoleServiceImpl.java index fdc72e90..eb291221 100644 --- a/auth-center/src/main/java/com/jmsoftware/maf/authcenter/role/service/impl/RoleServiceImpl.java +++ b/auth-center/src/main/java/com/jmsoftware/maf/authcenter/role/service/impl/RoleServiceImpl.java @@ -4,10 +4,13 @@ import cn.hutool.core.util.BooleanUtil; import cn.hutool.core.util.RandomUtil; import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.validation.ValidationUtil; import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.jmsoftware.maf.authcenter.role.entity.RoleExcelBean; import com.jmsoftware.maf.authcenter.role.entity.constant.RoleRedisKey; import com.jmsoftware.maf.authcenter.role.entity.persistence.Role; import com.jmsoftware.maf.authcenter.role.mapper.RoleMapper; @@ -19,10 +22,13 @@ import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; import lombok.val; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import javax.validation.Valid; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull; import java.util.List; @@ -37,6 +43,7 @@ * @author Johnny Miller (锺俊) * @date 2020-05-10 22:39:50 */ +@Slf4j @Service @RequiredArgsConstructor public class RoleServiceImpl extends ServiceImpl implements RoleService { @@ -48,16 +55,18 @@ public class RoleServiceImpl extends ServiceImpl implements Ro @Override @SneakyThrows({JsonProcessingException.class}) public GetRoleListByUserIdResponse getRoleList(@NotNull Long userId) { - val key = String.format(mafProjectProperty.getProjectParentArtifactId() + val key = String.format(this.mafProjectProperty.getProjectParentArtifactId() + RoleRedisKey.GET_ROLE_LIST_BY_USER_ID.getKeyInfixFormat(), userId); - val hasKey = redisTemplate.hasKey(key); + val hasKey = this.redisTemplate.hasKey(key); if (BooleanUtil.isTrue(hasKey)) { - return objectMapper.readValue(redisTemplate.opsForValue().get(key), GetRoleListByUserIdResponse.class); + return this.objectMapper.readValue(this.redisTemplate.opsForValue().get(key), + GetRoleListByUserIdResponse.class); } val response = new GetRoleListByUserIdResponse(); response.setRoleList(this.getRoleListByUserId(userId)); - redisTemplate.opsForValue().set(key, objectMapper.writeValueAsString(response), RandomUtil.randomLong(1, 7), - TimeUnit.DAYS); + this.redisTemplate.opsForValue().set(key, this.objectMapper.writeValueAsString(response), + RandomUtil.randomLong(1, 7), + TimeUnit.DAYS); return response; } @@ -75,9 +84,41 @@ public boolean checkAdmin(@NotEmpty List<@NotNull Long> roleIdList) { val roleNameSet = roleList .stream() .map(Role::getName) - .filter(roleName -> StrUtil.equals(mafConfiguration.getSuperUserRole(), roleName)) + .filter(roleName -> StrUtil.equals(this.mafConfiguration.getSuperUserRole(), roleName)) .collect(Collectors.toSet()); // If roleNameSet is not empty (contains "admin") return CollUtil.isNotEmpty(roleNameSet); } + + @Override + public List getListForExporting() { + val rolePage = new Page(1, 500); + this.page(rolePage); + return rolePage + .getRecords() + .stream() + .map(RoleExcelBean::transformBy) + .collect(Collectors.toList()); + } + + @Override + public void validateBeforeAddToBeanList(List beanList, RoleExcelBean bean, int index) throws IllegalArgumentException { + val beanValidationResult = ValidationUtil.warpValidate(bean); + if (!beanValidationResult.isSuccess()) { + log.warn("Validation failed! beanList: {}, bean: {}, index: {}", beanList, bean, index); + val firstErrorMessage = CollUtil.getFirst(beanValidationResult.getErrorMessages()); + throw new IllegalArgumentException( + String.format("%s %s", firstErrorMessage.getPropertyName(), firstErrorMessage.getMessage())); + } + } + + @Override + @Transactional(rollbackFor = Throwable.class) + public void save(@NotEmpty List<@Valid RoleExcelBean> beanList) { + val roleList = beanList.stream().map(RoleExcelBean::transformTo).collect(Collectors.toList()); + val saved = this.saveBatch(roleList); + if (!saved) { + log.error("Cannot save batch role list. {}", roleList); + } + } } diff --git a/auth-center/src/main/java/com/jmsoftware/maf/authcenter/security/controller/JwtRemoteApiController.java b/auth-center/src/main/java/com/jmsoftware/maf/authcenter/security/controller/JwtRemoteApiController.java index f08559aa..c59c0d46 100644 --- a/auth-center/src/main/java/com/jmsoftware/maf/authcenter/security/controller/JwtRemoteApiController.java +++ b/auth-center/src/main/java/com/jmsoftware/maf/authcenter/security/controller/JwtRemoteApiController.java @@ -3,7 +3,6 @@ import com.jmsoftware.maf.authcenter.security.service.JwtService; import com.jmsoftware.maf.common.bean.ResponseBodyBean; import com.jmsoftware.maf.common.domain.authcenter.security.ParseJwtResponse; -import com.jmsoftware.maf.common.exception.SecurityException; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.RequiredArgsConstructor; @@ -36,8 +35,7 @@ public class JwtRemoteApiController { */ @GetMapping("/parse") @ApiOperation(value = "Parse JWT", notes = "Parse JWT (Remote API)") - public ResponseBodyBean parse(HttpServletRequest request) throws SecurityException { - return ResponseBodyBean.ofSuccess( - new ParseJwtResponse().setUsername(jwtService.getUsernameFromRequest(request))); + public ResponseBodyBean parse(HttpServletRequest request) { + return ResponseBodyBean.ofSuccess(this.jwtService.parse(request)); } } diff --git a/auth-center/src/main/java/com/jmsoftware/maf/authcenter/security/service/JwtService.java b/auth-center/src/main/java/com/jmsoftware/maf/authcenter/security/service/JwtService.java index ddc5d5fe..ed8dd38d 100644 --- a/auth-center/src/main/java/com/jmsoftware/maf/authcenter/security/service/JwtService.java +++ b/auth-center/src/main/java/com/jmsoftware/maf/authcenter/security/service/JwtService.java @@ -1,5 +1,6 @@ package com.jmsoftware.maf.authcenter.security.service; +import com.jmsoftware.maf.common.domain.authcenter.security.ParseJwtResponse; import com.jmsoftware.maf.common.exception.SecurityException; import io.jsonwebtoken.Claims; import org.springframework.security.core.Authentication; @@ -83,4 +84,12 @@ String createJwt(Boolean rememberMe, Long id, String subject, List roles * @return the jwt from request */ String getJwtFromRequest(HttpServletRequest request); + + /** + * Parse parse jwt response. + * + * @param request the request + * @return the parse jwt response + */ + ParseJwtResponse parse(HttpServletRequest request); } diff --git a/auth-center/src/main/java/com/jmsoftware/maf/authcenter/security/service/impl/JwtServiceImpl.java b/auth-center/src/main/java/com/jmsoftware/maf/authcenter/security/service/impl/JwtServiceImpl.java index e72259de..5697e3ce 100644 --- a/auth-center/src/main/java/com/jmsoftware/maf/authcenter/security/service/impl/JwtServiceImpl.java +++ b/auth-center/src/main/java/com/jmsoftware/maf/authcenter/security/service/impl/JwtServiceImpl.java @@ -4,12 +4,14 @@ import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import com.jmsoftware.maf.authcenter.security.service.JwtService; +import com.jmsoftware.maf.common.domain.authcenter.security.ParseJwtResponse; import com.jmsoftware.maf.common.domain.authcenter.security.UserPrincipal; import com.jmsoftware.maf.common.exception.SecurityException; import com.jmsoftware.maf.springcloudstarter.configuration.JwtConfiguration; import io.jsonwebtoken.*; import io.jsonwebtoken.security.Keys; import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import lombok.val; import org.springframework.data.redis.core.RedisTemplate; @@ -49,16 +51,16 @@ public class JwtServiceImpl implements JwtService { @PostConstruct private void init() { log.info("Start to init class members of {}.", this.getClass().getSimpleName()); - secretKey = Keys.hmacShaKeyFor(jwtConfiguration.getSigningKey().getBytes(StandardCharsets.UTF_8)); - log.warn("Secret key for JWT was generated. Algorithm: {}", secretKey.getAlgorithm()); - jwtParser = Jwts.parserBuilder().setSigningKey(secretKey).build(); + this.secretKey = Keys.hmacShaKeyFor(this.jwtConfiguration.getSigningKey().getBytes(StandardCharsets.UTF_8)); + log.warn("Secret key for JWT was generated. Algorithm: {}", this.secretKey.getAlgorithm()); + this.jwtParser = Jwts.parserBuilder().setSigningKey(this.secretKey).build(); } @Override public String createJwt(Authentication authentication, Boolean rememberMe) { val userPrincipal = (UserPrincipal) authentication.getPrincipal(); - return createJwt(rememberMe, userPrincipal.getId(), userPrincipal.getUsername(), userPrincipal.getRoles(), - userPrincipal.getAuthorities()); + return this.createJwt(rememberMe, userPrincipal.getId(), userPrincipal.getUsername(), userPrincipal.getRoles(), + userPrincipal.getAuthorities()); } @Override @@ -69,19 +71,20 @@ public String createJwt(Boolean rememberMe, Long id, String subject, List 0) { builder.setExpiration(DateUtil.offsetMillisecond(now, ttl.intValue())); } val jwt = builder.compact(); // Store new JWT in Redis - String redisKeyOfJwt = String.format("%s%s", jwtConfiguration.getJwtRedisKeyPrefix(), subject); - redisTemplate.opsForValue().set(redisKeyOfJwt, jwt, ttl, TimeUnit.MILLISECONDS); + String redisKeyOfJwt = String.format("%s%s", this.jwtConfiguration.getJwtRedisKeyPrefix(), subject); + this.redisTemplate.opsForValue().set(redisKeyOfJwt, jwt, ttl, TimeUnit.MILLISECONDS); log.info("Storing JWT in Redis. Key: {}, Value: {}", redisKeyOfJwt, jwt); return jwt; } @@ -90,7 +93,7 @@ public String createJwt(Boolean rememberMe, Long id, String subject, List new SecurityException(HttpStatus.INTERNAL_SERVER_ERROR, "The JWT Claims Set is null", null)); } catch (ExpiredJwtException e) { @@ -107,16 +110,16 @@ public Claims parseJwt(String jwt) throws SecurityException { throw new SecurityException(HttpStatus.UNAUTHORIZED, "The parameter of JWT is invalid"); } val username = claims.getSubject(); - val redisKeyOfJwt = jwtConfiguration.getJwtRedisKeyPrefix() + username; + val redisKeyOfJwt = this.jwtConfiguration.getJwtRedisKeyPrefix() + username; // Check if JWT exists - val expire = redisTemplate.opsForValue().getOperations().getExpire(redisKeyOfJwt, TimeUnit.MILLISECONDS); + val expire = this.redisTemplate.opsForValue().getOperations().getExpire(redisKeyOfJwt, TimeUnit.MILLISECONDS); if (ObjectUtil.isNull(expire) || expire <= 0) { throw new SecurityException(HttpStatus.UNAUTHORIZED, "JWT is expired (Redis expiration)"); } // Check if the current JWT is equal to the one in Redis. // If it's noe equal, that indicates current user has signed out or logged in before. // Both situations reveal the JWT has expired. - val jwtInRedis = (String) redisTemplate.opsForValue().get(redisKeyOfJwt); + val jwtInRedis = (String) this.redisTemplate.opsForValue().get(redisKeyOfJwt); if (!StrUtil.equals(jwt, jwtInRedis)) { throw new SecurityException(HttpStatus.UNAUTHORIZED, "JWT is expired (Not equaled)"); } @@ -125,17 +128,17 @@ public Claims parseJwt(String jwt) throws SecurityException { @Override public void invalidateJwt(HttpServletRequest request) throws SecurityException { - val jwt = getJwtFromRequest(request); - val username = getUsernameFromJwt(jwt); + val jwt = this.getJwtFromRequest(request); + val username = this.getUsernameFromJwt(jwt); // Delete JWT from redis - String redisKeyOfJwt = String.format("%s%s", jwtConfiguration.getJwtRedisKeyPrefix(), username); - val deletedKeyNumber = redisTemplate.opsForValue().getOperations().delete(redisKeyOfJwt); + String redisKeyOfJwt = String.format("%s%s", this.jwtConfiguration.getJwtRedisKeyPrefix(), username); + val deletedKeyNumber = this.redisTemplate.opsForValue().getOperations().delete(redisKeyOfJwt); log.error("Invalidate JWT. Redis key of JWT = {}, deleted = {}", redisKeyOfJwt, deletedKeyNumber); } @Override public String getUsernameFromJwt(String jwt) throws SecurityException { - val claims = parseJwt(jwt); + val claims = this.parseJwt(jwt); return claims.getSubject(); } @@ -153,4 +156,15 @@ public String getJwtFromRequest(HttpServletRequest request) { } return null; } + + @Override + @SneakyThrows + public ParseJwtResponse parse(HttpServletRequest request) { + val jwt = this.getJwtFromRequest(request); + val claims = this.parseJwt(jwt); + val parseJwtResponse = new ParseJwtResponse(); + parseJwtResponse.setId(Long.parseLong(claims.getId())); + parseJwtResponse.setUsername(claims.getSubject()); + return parseJwtResponse; + } } diff --git a/auth-center/src/main/java/com/jmsoftware/maf/authcenter/user/entity/persistence/User.java b/auth-center/src/main/java/com/jmsoftware/maf/authcenter/user/entity/persistence/User.java index 37bb3f7a..5955aa7f 100644 --- a/auth-center/src/main/java/com/jmsoftware/maf/authcenter/user/entity/persistence/User.java +++ b/auth-center/src/main/java/com/jmsoftware/maf/authcenter/user/entity/persistence/User.java @@ -1,9 +1,12 @@ package com.jmsoftware.maf.authcenter.user.entity.persistence; -import com.baomidou.mybatisplus.annotation.*; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; -import java.util.Date; +import java.time.LocalDateTime; import static com.baomidou.mybatisplus.annotation.FieldFill.INSERT; import static com.baomidou.mybatisplus.annotation.FieldFill.UPDATE; @@ -19,125 +22,95 @@ @SuppressWarnings("jol") @TableName(value = User.TABLE_NAME) public class User { + public static final String TABLE_NAME = "user"; + public static final String COL_ID = "id"; + public static final String COL_USERNAME = "username"; + public static final String COL_EMAIL = "email"; + public static final String COL_CELLPHONE = "cellphone"; + public static final String COL_PASSWORD = "password"; + public static final String COL_FULL_NAME = "full_name"; + public static final String COL_BIRTHDAY = "birthday"; + public static final String COL_GENDER = "gender"; + public static final String COL_AVATAR = "avatar"; + public static final String COL_STATUS = "status"; + public static final String COL_CREATED_BY = "created_by"; + public static final String COL_CREATED_TIME = "created_time"; + public static final String COL_MODIFIED_BY = "modified_by"; + public static final String COL_MODIFIED_TIME = "modified_time"; + public static final String COL_DELETED = "deleted"; /** * Primary key of user */ @TableId(value = COL_ID, type = IdType.AUTO) private Long id; - /** * Username */ @TableField(value = COL_USERNAME) private String username; - /** * Email */ @TableField(value = COL_EMAIL) private String email; - /** * Cellphone number */ @TableField(value = COL_CELLPHONE) private String cellphone; - /** * Password */ @TableField(value = COL_PASSWORD) private String password; - /** * Full name */ @TableField(value = COL_FULL_NAME) private String fullName; - /** * Birthday */ @TableField(value = COL_BIRTHDAY) - private Date birthday; - + private LocalDateTime birthday; /** * 26 gender options */ @TableField(value = COL_GENDER) private Object gender; - /** * User avatar full path on SFTP server */ @TableField(value = COL_AVATAR) private String avatar; - /** * Status. 1 - enabled, 2 - disabled */ @TableField(value = COL_STATUS) private Byte status; - /** * Created by */ @TableField(value = COL_CREATED_BY, fill = INSERT) private Long createdBy; - /** * Created time */ @TableField(value = COL_CREATED_TIME, fill = INSERT) - private Date createdTime; - + private LocalDateTime createdTime; /** * Modified by */ @TableField(value = COL_MODIFIED_BY, fill = UPDATE) private Long modifiedBy; - /** * Modified time */ @TableField(value = COL_MODIFIED_TIME, fill = UPDATE) - private Date modifiedTime; - + private LocalDateTime modifiedTime; /** * Delete flag. */ @TableField(value = COL_DELETED, fill = INSERT) private Byte deleted; - - public static final String TABLE_NAME = "user"; - - public static final String COL_ID = "id"; - - public static final String COL_USERNAME = "username"; - - public static final String COL_EMAIL = "email"; - - public static final String COL_CELLPHONE = "cellphone"; - - public static final String COL_PASSWORD = "password"; - - public static final String COL_FULL_NAME = "full_name"; - - public static final String COL_BIRTHDAY = "birthday"; - - public static final String COL_GENDER = "gender"; - - public static final String COL_AVATAR = "avatar"; - - public static final String COL_STATUS = "status"; - - public static final String COL_CREATED_BY = "created_by"; - - public static final String COL_CREATED_TIME = "created_time"; - - public static final String COL_MODIFIED_BY = "modified_by"; - - public static final String COL_MODIFIED_TIME = "modified_time"; - - public static final String COL_DELETED = "deleted"; } diff --git a/auth-center/src/main/java/com/jmsoftware/maf/authcenter/user/service/impl/UserRoleServiceImpl.java b/auth-center/src/main/java/com/jmsoftware/maf/authcenter/user/service/impl/UserRoleServiceImpl.java index cb6ec135..e0e65879 100644 --- a/auth-center/src/main/java/com/jmsoftware/maf/authcenter/user/service/impl/UserRoleServiceImpl.java +++ b/auth-center/src/main/java/com/jmsoftware/maf/authcenter/user/service/impl/UserRoleServiceImpl.java @@ -37,7 +37,7 @@ public void assignRoleByRoleName(@NonNull User user, @NotBlank String roleName) queryWrapper.select(Role::getId) .eq(Role::getName, roleName); val role = Optional.ofNullable(this.roleService.getOne(queryWrapper)) - .orElseThrow(() -> new BizException("")); + .orElseThrow(() -> new BizException("Cannot find the role: " + roleName)); val userRole = new UserRole(); userRole.setUserId(user.getId()); userRole.setRoleId(role.getId()); diff --git a/auth-center/src/main/java/com/jmsoftware/maf/authcenter/user/service/impl/UserServiceImpl.java b/auth-center/src/main/java/com/jmsoftware/maf/authcenter/user/service/impl/UserServiceImpl.java index 3fbcd77c..bf3defdf 100644 --- a/auth-center/src/main/java/com/jmsoftware/maf/authcenter/user/service/impl/UserServiceImpl.java +++ b/auth-center/src/main/java/com/jmsoftware/maf/authcenter/user/service/impl/UserServiceImpl.java @@ -24,7 +24,7 @@ import com.jmsoftware.maf.common.exception.SecurityException; import com.jmsoftware.maf.springcloudstarter.configuration.MafConfiguration; import com.jmsoftware.maf.springcloudstarter.configuration.MafProjectProperty; -import com.jmsoftware.maf.springcloudstarter.util.UsernameUtil; +import com.jmsoftware.maf.springcloudstarter.util.UserUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import lombok.val; @@ -131,7 +131,7 @@ public boolean logout(HttpServletRequest request) throws SecurityException { @Override public String getUserStatus(@Valid @NotNull GetUserStatusPayload payload) { - log.info("Current username: {}", UsernameUtil.getCurrentUsername()); + log.info("Current username: {}", UserUtil.getCurrentUsername()); return UserStatus.ofValue(payload.getStatus()).getDescription(); } diff --git a/auth-center/src/main/resources/application-development-docker.yml b/auth-center/src/main/resources/application-development-docker.yml index f071294d..aea1e370 100644 --- a/auth-center/src/main/resources/application-development-docker.yml +++ b/auth-center/src/main/resources/application-development-docker.yml @@ -40,3 +40,10 @@ redis: maf: configuration: swagger-enabled: true + +minio: + endpoint: http://maf-minio + port: 9000 + access-key: maf_minio_root_user + secret-key: jm@minio + bucket-name: maf diff --git a/auth-center/src/main/resources/application-development-local.yml b/auth-center/src/main/resources/application-development-local.yml index c48ed62b..d6dbfe95 100644 --- a/auth-center/src/main/resources/application-development-local.yml +++ b/auth-center/src/main/resources/application-development-local.yml @@ -37,11 +37,18 @@ redis: port: 6380 password: maf@redis - # Configure logging level for the environment logging: level: - com.baomidou: INFO + com.baomidou: DEBUG + com.jmsoftware: DEBUG maf: configuration: swagger-enabled: true + +minio: + endpoint: http://localhost + port: 9900 + access-key: maf_minio_root_user + secret-key: jm@minio + bucket-name: maf diff --git a/auth-center/src/main/resources/application-production.yml b/auth-center/src/main/resources/application-production.yml index 0736b0bc..5c808175 100644 --- a/auth-center/src/main/resources/application-production.yml +++ b/auth-center/src/main/resources/application-production.yml @@ -40,3 +40,10 @@ redis: maf: configuration: swagger-enabled: false + +minio: + endpoint: http://maf-minio + port: 9000 + access-key: maf_minio_root_user + secret-key: jm@minio + bucket-name: maf diff --git a/auth-center/src/main/resources/application-stage.yml b/auth-center/src/main/resources/application-stage.yml index 0736b0bc..5c808175 100644 --- a/auth-center/src/main/resources/application-stage.yml +++ b/auth-center/src/main/resources/application-stage.yml @@ -40,3 +40,10 @@ redis: maf: configuration: swagger-enabled: false + +minio: + endpoint: http://maf-minio + port: 9000 + access-key: maf_minio_root_user + secret-key: jm@minio + bucket-name: maf diff --git a/auth-center/src/main/resources/application-test.yml b/auth-center/src/main/resources/application-test.yml index ae3c624b..81332a9d 100644 --- a/auth-center/src/main/resources/application-test.yml +++ b/auth-center/src/main/resources/application-test.yml @@ -40,3 +40,10 @@ redis: maf: configuration: swagger-enabled: true + +minio: + endpoint: http://maf-minio + port: 9000 + access-key: maf_minio_root_user + secret-key: jm@minio + bucket-name: maf diff --git a/auth-center/src/main/resources/application.yml b/auth-center/src/main/resources/application.yml index 605a318b..5fb12cbd 100644 --- a/auth-center/src/main/resources/application.yml +++ b/auth-center/src/main/resources/application.yml @@ -44,9 +44,7 @@ spring: dynamic: primary: master_1 druid: - initial-size: 5 - max-active: 20 - min-idle: 2 + # connection pool size will be determined by DruidDataSourceCreatorPostProcessor.java max-wait: 60000 min-evictable-idle-time-millis: 600000 max-evictable-idle-time-millis: 900000 @@ -157,9 +155,6 @@ mybatis-plus: logging: config: classpath:logback-configuration/logback-${spring.profiles.active}.xml - level: - # Configure logging level for SFTP/JSCH - com.jcraft.jsch: INFO maf: project-property: @@ -196,3 +191,6 @@ maf: jwt: # an hour ttl: 3600000 + +minio: + enabled: true diff --git a/common/pom.xml b/common/pom.xml index ed121028..2819ad40 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -10,7 +10,7 @@ com.jmsoftware.maf muscle-and-fitness-server - 0.0.4 + 0.0.5 diff --git a/common/src/main/java/com/jmsoftware/maf/common/constant/MafHttpHeader.java b/common/src/main/java/com/jmsoftware/maf/common/constant/MafHttpHeader.java index d4451928..b29c5aad 100644 --- a/common/src/main/java/com/jmsoftware/maf/common/constant/MafHttpHeader.java +++ b/common/src/main/java/com/jmsoftware/maf/common/constant/MafHttpHeader.java @@ -9,10 +9,15 @@ **/ @Getter public enum MafHttpHeader { + /** + * X-Id + */ + X_ID("X-Id"), /** * X-Username */ - X_USERNAME("X-Username"); + X_USERNAME("X-Username"), + ; private final String header; diff --git a/common/src/main/java/com/jmsoftware/maf/common/domain/authcenter/security/ParseJwtResponse.java b/common/src/main/java/com/jmsoftware/maf/common/domain/authcenter/security/ParseJwtResponse.java index 42df767f..a3ef5bad 100644 --- a/common/src/main/java/com/jmsoftware/maf/common/domain/authcenter/security/ParseJwtResponse.java +++ b/common/src/main/java/com/jmsoftware/maf/common/domain/authcenter/security/ParseJwtResponse.java @@ -11,5 +11,6 @@ @Data @Accessors(chain = true) public class ParseJwtResponse { + private Long id; private String username; } diff --git a/maf-mis/pom.xml b/maf-mis/pom.xml index cf5d45e0..17ac69c8 100644 --- a/maf-mis/pom.xml +++ b/maf-mis/pom.xml @@ -10,7 +10,7 @@ com.jmsoftware.maf muscle-and-fitness-server - 0.0.4 + 0.0.5 diff --git a/maf-mis/src/main/java/com/jmsoftware/maf/mafmis/MafMisApplication.java b/maf-mis/src/main/java/com/jmsoftware/maf/mafmis/MafMisApplication.java index 2523c8cb..08bb0ca4 100644 --- a/maf-mis/src/main/java/com/jmsoftware/maf/mafmis/MafMisApplication.java +++ b/maf-mis/src/main/java/com/jmsoftware/maf/mafmis/MafMisApplication.java @@ -2,6 +2,7 @@ import com.jmsoftware.maf.springcloudstarter.helper.SpringBootStartupHelper; import lombok.extern.slf4j.Slf4j; +import lombok.val; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @@ -20,16 +21,11 @@ @EnableDiscoveryClient @SpringBootApplication public class MafMisApplication { - private static final StopWatch STOP_WATCH = new StopWatch(); - private static SpringBootStartupHelper springBootStartupHelper; - - public MafMisApplication(SpringBootStartupHelper springBootStartupHelper) { - MafMisApplication.springBootStartupHelper = springBootStartupHelper; - } - public static void main(String[] args) { - STOP_WATCH.start(); - SpringApplication.run(MafMisApplication.class, args); - springBootStartupHelper.stop(STOP_WATCH); + val stopWatch = new StopWatch(); + stopWatch.start(); + val configurableApplicationContext = SpringApplication.run(MafMisApplication.class, args); + val springBootStartupHelper = configurableApplicationContext.getBean(SpringBootStartupHelper.class); + springBootStartupHelper.stop(stopWatch); } } diff --git a/maf-mis/src/main/resources/application.yml b/maf-mis/src/main/resources/application.yml index dadc2793..5dcbb5f1 100644 --- a/maf-mis/src/main/resources/application.yml +++ b/maf-mis/src/main/resources/application.yml @@ -46,9 +46,7 @@ spring: dynamic: primary: master_1 druid: - initial-size: 5 - max-active: 20 - min-idle: 2 + # connection pool size will be determined by DruidDataSourceCreatorPostProcessor.java max-wait: 60000 min-evictable-idle-time-millis: 600000 max-evictable-idle-time-millis: 900000 @@ -156,9 +154,6 @@ mybatis-plus: logging: config: classpath:logback-configuration/logback-${spring.profiles.active}.xml - level: - # Configure logging level for SFTP/JSCH - com.jcraft.jsch: INFO maf: project-property: diff --git a/oss-center/pom.xml b/oss-center/pom.xml index dc10aeb5..3064e83b 100644 --- a/oss-center/pom.xml +++ b/oss-center/pom.xml @@ -10,7 +10,7 @@ com.jmsoftware.maf muscle-and-fitness-server - 0.0.4 + 0.0.5 diff --git a/oss-center/src/main/java/com/jmsoftware/maf/osscenter/OssCenterApplication.java b/oss-center/src/main/java/com/jmsoftware/maf/osscenter/OssCenterApplication.java index 182dc3cc..99dcc5c6 100644 --- a/oss-center/src/main/java/com/jmsoftware/maf/osscenter/OssCenterApplication.java +++ b/oss-center/src/main/java/com/jmsoftware/maf/osscenter/OssCenterApplication.java @@ -2,6 +2,7 @@ import com.jmsoftware.maf.springcloudstarter.helper.SpringBootStartupHelper; import lombok.extern.slf4j.Slf4j; +import lombok.val; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @@ -20,16 +21,11 @@ @EnableDiscoveryClient @SpringBootApplication public class OssCenterApplication { - private static final StopWatch STOP_WATCH = new StopWatch(); - private static SpringBootStartupHelper springBootStartupHelper; - - public OssCenterApplication(SpringBootStartupHelper springBootStartupHelper) { - OssCenterApplication.springBootStartupHelper = springBootStartupHelper; - } - public static void main(String[] args) { - STOP_WATCH.start(); - SpringApplication.run(OssCenterApplication.class, args); - springBootStartupHelper.stop(STOP_WATCH); + val stopWatch = new StopWatch(); + stopWatch.start(); + val configurableApplicationContext = SpringApplication.run(OssCenterApplication.class, args); + val springBootStartupHelper = configurableApplicationContext.getBean(SpringBootStartupHelper.class); + springBootStartupHelper.stop(stopWatch); } } diff --git a/oss-center/src/main/java/com/jmsoftware/maf/osscenter/read/service/impl/ReadResourceServiceImpl.java b/oss-center/src/main/java/com/jmsoftware/maf/osscenter/read/service/impl/ReadResourceServiceImpl.java index f577e223..80688aee 100644 --- a/oss-center/src/main/java/com/jmsoftware/maf/osscenter/read/service/impl/ReadResourceServiceImpl.java +++ b/oss-center/src/main/java/com/jmsoftware/maf/osscenter/read/service/impl/ReadResourceServiceImpl.java @@ -2,6 +2,7 @@ import cn.hutool.core.collection.CollUtil; import cn.hutool.core.io.IoUtil; +import cn.hutool.core.io.NioUtil; import com.jmsoftware.maf.osscenter.read.entity.SerializableStatObjectResponse; import com.jmsoftware.maf.osscenter.read.service.ReadResourceService; import com.jmsoftware.maf.springcloudstarter.minio.MinioHelper; @@ -49,7 +50,7 @@ public ResponseEntity asyncStreamSingleResource(@NotBlank .contentLength(statObjectResponse.size()) .contentType(MediaType.parseMediaType(statObjectResponse.contentType())) .body(outputStream -> { - IoUtil.copy(getObjectResponse, outputStream); + NioUtil.copyByNIO(getObjectResponse, outputStream, NioUtil.DEFAULT_BUFFER_SIZE, null); IoUtil.close(getObjectResponse); }); } @@ -75,7 +76,7 @@ public ResponseEntity asyncDownloadSingleResource(@NotBla .contentLength(statObjectResponse.size()) .contentType(MediaType.parseMediaType(statObjectResponse.contentType())) .body((outputStream -> { - IoUtil.copy(getObjectResponse, outputStream); + NioUtil.copyByNIO(getObjectResponse, outputStream, NioUtil.DEFAULT_LARGE_BUFFER_SIZE, null); IoUtil.close(getObjectResponse); })); } @@ -102,7 +103,7 @@ private ResponseEntity asyncGetResourceRegion(String buck .contentLength(rangeLength) .contentType(MediaType.parseMediaType(statObjectResponse.contentType())) .body(outputStream -> { - IoUtil.copy(getObjectResponse, outputStream); + NioUtil.copyByNIO(getObjectResponse, outputStream, NioUtil.DEFAULT_LARGE_BUFFER_SIZE, null); IoUtil.close(getObjectResponse); }); } diff --git a/oss-center/src/main/resources/application-development-local.yml b/oss-center/src/main/resources/application-development-local.yml index f2380dda..5a2e93c1 100644 --- a/oss-center/src/main/resources/application-development-local.yml +++ b/oss-center/src/main/resources/application-development-local.yml @@ -14,11 +14,6 @@ redis: port: 6380 password: maf@redis -logging: - # Configure logging level for SFTP/JSCH - level: - com.jcraft.jsch: INFO - maf: configuration: swagger-enabled: true diff --git a/oss-center/src/main/resources/application.yml b/oss-center/src/main/resources/application.yml index 51a7373d..734a07e2 100644 --- a/oss-center/src/main/resources/application.yml +++ b/oss-center/src/main/resources/application.yml @@ -79,9 +79,6 @@ management: logging: config: classpath:logback-configuration/logback-${spring.profiles.active}.xml - level: - # Configure logging level for SFTP/JSCH - com.jcraft.jsch: INFO maf: project-property: diff --git a/pom.xml b/pom.xml index f4be416d..da4aab18 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ com.jmsoftware.maf muscle-and-fitness-server - 0.0.4 + 0.0.5 Muscle and Fitness Server Muscle and Fitness Server (M&F, maf), a Spring Cloud microservice based, back-end server for managing data of muscle and fitness. diff --git a/reactive-spring-cloud-starter/pom.xml b/reactive-spring-cloud-starter/pom.xml index 01e03eeb..65f379ae 100644 --- a/reactive-spring-cloud-starter/pom.xml +++ b/reactive-spring-cloud-starter/pom.xml @@ -10,7 +10,7 @@ com.jmsoftware.maf muscle-and-fitness-server - 0.0.4 + 0.0.5 diff --git a/spring-boot-admin/pom.xml b/spring-boot-admin/pom.xml index 372988b7..e74ee001 100644 --- a/spring-boot-admin/pom.xml +++ b/spring-boot-admin/pom.xml @@ -10,7 +10,7 @@ com.jmsoftware.maf muscle-and-fitness-server - 0.0.4 + 0.0.5 diff --git a/spring-boot-admin/src/main/java/com/jmsoftware/maf/springbootadmin/SpringBootAdminApplication.java b/spring-boot-admin/src/main/java/com/jmsoftware/maf/springbootadmin/SpringBootAdminApplication.java index a27b1a5a..0f6c25ab 100644 --- a/spring-boot-admin/src/main/java/com/jmsoftware/maf/springbootadmin/SpringBootAdminApplication.java +++ b/spring-boot-admin/src/main/java/com/jmsoftware/maf/springbootadmin/SpringBootAdminApplication.java @@ -3,6 +3,7 @@ import com.jmsoftware.maf.springcloudstarter.helper.SpringBootStartupHelper; import de.codecentric.boot.admin.server.config.EnableAdminServer; import lombok.extern.slf4j.Slf4j; +import lombok.val; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @@ -13,24 +14,18 @@ *

* Change description here. * - * @author Johnny Miller (锺俊), email: johnnysviva@outlook.com - * @date 2/14/20 3:07 PM + * @author Johnny Miller (锺俊), email: johnnysviva@outlook.com,date: 2/14/20 3:07 PM **/ @Slf4j @EnableAdminServer @EnableDiscoveryClient @SpringBootApplication public class SpringBootAdminApplication { - private static final StopWatch STOP_WATCH = new StopWatch(); - private static SpringBootStartupHelper springBootStartupHelper; - - public SpringBootAdminApplication(SpringBootStartupHelper springBootStartupHelper) { - SpringBootAdminApplication.springBootStartupHelper = springBootStartupHelper; - } - public static void main(String[] args) { - STOP_WATCH.start(); - SpringApplication.run(SpringBootAdminApplication.class, args); - springBootStartupHelper.stop(STOP_WATCH); + val stopWatch = new StopWatch(); + stopWatch.start(); + val configurableApplicationContext = SpringApplication.run(SpringBootAdminApplication.class, args); + val springBootStartupHelper = configurableApplicationContext.getBean(SpringBootStartupHelper.class); + springBootStartupHelper.stop(stopWatch); } } diff --git a/spring-cloud-starter/pom.xml b/spring-cloud-starter/pom.xml index 0aa5612e..0a364192 100644 --- a/spring-cloud-starter/pom.xml +++ b/spring-cloud-starter/pom.xml @@ -10,7 +10,7 @@ com.jmsoftware.maf muscle-and-fitness-server - 0.0.4 + 0.0.5 diff --git a/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/annotation/ExcelColumn.java b/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/annotation/ExcelColumn.java index c82b803c..b8b1bf3f 100644 --- a/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/annotation/ExcelColumn.java +++ b/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/annotation/ExcelColumn.java @@ -14,11 +14,11 @@ @Retention(RetentionPolicy.RUNTIME) public @interface ExcelColumn { /** - * Description string. + * Column name. * * @return the string */ - String description() default ""; + String name() default "Default Column Name"; /** * Cell type cell type. diff --git a/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/aspect/WebRequestLogAspect.java b/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/aspect/WebRequestLogAspect.java index 3e7356c9..bedd4b43 100644 --- a/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/aspect/WebRequestLogAspect.java +++ b/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/aspect/WebRequestLogAspect.java @@ -4,7 +4,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.jmsoftware.maf.springcloudstarter.util.RequestUtil; -import com.jmsoftware.maf.springcloudstarter.util.UsernameUtil; +import com.jmsoftware.maf.springcloudstarter.util.UserUtil; import lombok.extern.slf4j.Slf4j; import lombok.val; import org.aspectj.lang.JoinPoint; @@ -89,7 +89,7 @@ public void beforeHandleRequest(JoinPoint joinPoint) { assert attributes != null; val request = attributes.getRequest(); log.info(BEFORE_TEMPLATE, request.getRequestURL().toString(), request.getMethod(), - RequestUtil.getRequestIpAndPort(request), UsernameUtil.getCurrentUsername(), + RequestUtil.getRequestIpAndPort(request), UserUtil.getCurrentUsername(), joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName(), LINE_SEPARATOR, JSONUtil.toJsonPrettyStr(joinPoint.getArgs())); } diff --git a/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/controller/AbstractExcelDataController.java b/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/controller/AbstractExcelDataController.java new file mode 100644 index 00000000..46a0e221 --- /dev/null +++ b/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/controller/AbstractExcelDataController.java @@ -0,0 +1,495 @@ +package com.jmsoftware.maf.springcloudstarter.controller; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.poi.excel.ExcelReader; +import cn.hutool.poi.excel.ExcelWriter; +import cn.hutool.poi.excel.WorkbookUtil; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.jmsoftware.maf.common.bean.ExcelImportResult; +import com.jmsoftware.maf.common.bean.ResponseBodyBean; +import com.jmsoftware.maf.springcloudstarter.annotation.ExcelColumn; +import com.jmsoftware.maf.springcloudstarter.configuration.ExcelImportConfiguration; +import com.jmsoftware.maf.springcloudstarter.minio.MinioHelper; +import com.jmsoftware.maf.springcloudstarter.util.CaseConversionUtil; +import io.swagger.annotations.ApiOperation; +import lombok.Cleanup; +import lombok.NonNull; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.apache.poi.ss.usermodel.Workbook; +import org.springframework.http.ContentDisposition; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; + +import javax.annotation.Resource; +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.lang.reflect.ParameterizedType; +import java.util.List; +import java.util.Map; + +/** + *

AbstractExcelDataController

+ *

Abstract controller for excel data import.

+ *

Features

+ *
    + *
  1. Row validation. If not valid, will generate error message at the last cell.
  2. + *
  3. Generalization support. AbstractExcelDataController<ExcelImportBeanType>
  4. + *
  5. Using annotation @ExcelColumn for fields can be generating Excel easier than ever.
  6. + *
  7. Support partial data import.
  8. + * + *
+ *

Understanding of the Lifecycle

+ *

Understanding of the lifecycle will help us make less mistakes during programming.

+ *

I. initContext()

+ *

Initialize context. Generate the field name array of ExcelImportBeanType by reflection.

+ *

II. registerBindHandlerMethods()

+ *

Register bind handler methods (starts with ‘“check”, like checkString()).

+ *

III. registerHandlerMethods()

+ *

Register default handler methods (starts with ‘“check”, like checkString()).

+ *

IV. beforeExecute()

+ *

Left black. Can be overridden by sub class.

+ *

V. initLocaleContext()

+ *

Initialize locale context for current thread. Main work below:

+ *
    + *
  • errorMessageList
  • + *
  • beanList
  • + *
  • returnMessageList
  • + *
  • rowLocation
  • + *
  • columnLocation
  • + *
  • sheetLocation
  • + * + *
+ *

After initializing, will upload Excel, read and bind data, validate data.

+ *

VI. beforeDatabaseOperation()

+ *

Left black. Can be overridden by sub class.

+ *

VII. executeDatabaseOperation()

+ *

Must be implemented by sub class.

+ *

VIII. onExceptionOccurred()

+ *

If any exceptions occur, this method will be called. Must be implemented by sub class.

+ *

Ⅸ. afterExecute()

+ *

Delete temporary file.

+ *

X. destroyLocaleContext()

+ *

Destroy locale context, in opposite of initLocaleContext().

+ * + * @param Excel import bean type + * @author Johnny Miller (锺俊), email: johnnysviva@outlook.com, date: 2/19/2021 10:06 AM + */ +@Slf4j +@SuppressWarnings({"unused"}) +public abstract class AbstractExcelDataController { + protected final ThreadLocal> beanList = ThreadLocal.withInitial(() -> null); + protected final ThreadLocal workbook = ThreadLocal.withInitial(() -> null); + protected final ThreadLocal excelFilePath = ThreadLocal.withInitial(() -> null); + protected final ThreadLocal exceptionOccurred = ThreadLocal.withInitial(() -> false); + protected final ThreadLocal> errorMessageList = ThreadLocal.withInitial(Lists::newLinkedList); + protected final ThreadLocal> returnMessageList = ThreadLocal.withInitial(Lists::newLinkedList); + protected final ThreadLocal fileName = ThreadLocal.withInitial(() -> null); + + /** + *
+ * + * + * + *
KeyValue
Excel Column NameexcelColumnName
Title + * NametitleName
+ */ + protected final Map importingFieldAliasMap = Maps.newHashMap(); + /** + *
+ * + * + * + *
KeyValue
excelColumnNameExcel Column Name
titleNameTitle + * Name
+ */ + protected final Map exportingFieldAliasMap = Maps.newHashMap(); + + @Resource + protected ExcelImportConfiguration excelImportConfiguration; + @Resource + protected MinioHelper minioHelper; + /** + * Deny all data when data validation fails. Default value is true. + */ + private boolean denyAll = true; + private Class beanClass; + private String[] fieldNameArray; + + /** + *

Constructor of AbstractExcelDataController

+ *
    + *
  1. Init context

    + *
  2. + *
  3. Register bind handler methods

    + *
  4. + *
  5. Register handler methods

    + *
  6. + *
+ */ + protected AbstractExcelDataController() { + this.initContext(); + } + + /** + *

Init context.

+ *
    + *
  1. Call getGenericClass() to set Excel import type;

    + *
  2. + *
  3. Set fieldNameArray

    + *
  4. + *
+ */ + protected void initContext() { + this.beanClass = this.getGenericClass(); + val declaredFields = this.beanClass.getDeclaredFields(); + val fieldNames = new String[declaredFields.length]; + for (var index = 0; index < declaredFields.length; index++) { + val declaredField = declaredFields[index]; + fieldNames[index] = declaredField.getName(); + final ExcelColumn excelColumn = declaredField.getAnnotation(ExcelColumn.class); + var columnName = excelColumn.name(); + if (StrUtil.isBlank(columnName)) { + columnName = CaseConversionUtil.convertToStartCase(StrUtil.toSymbolCase(declaredField.getName(), ' ')); + } + this.importingFieldAliasMap.put(columnName, declaredField.getName()); + this.exportingFieldAliasMap.put(declaredField.getName(), columnName); + } + this.fieldNameArray = fieldNames; + log.info("Generated {} field name array by reflection, fieldNames: {}", this.beanClass.getSimpleName(), + this.fieldNameArray); + } + + /** + * Init locale context. + */ + private void initLocaleContext() { + this.exceptionOccurred.set(false); + this.errorMessageList.set(Lists.newLinkedList()); + this.returnMessageList.set(Lists.newLinkedList()); + } + + /** + * Destroy locale context. + */ + private void destroyLocaleContext() { + this.beanList.remove(); + this.closeWorkbook(this.workbook.get()); + this.workbook.remove(); + this.excelFilePath.remove(); + this.exceptionOccurred.remove(); + this.errorMessageList.remove(); + this.returnMessageList.remove(); + this.fileName.remove(); + } + + /** + * Before execute. + */ + protected void beforeExecute() { + } + + /** + * On exception occurred. + */ + protected abstract void onExceptionOccurred(); + + /** + * Validate before adding to bean list boolean. + * + * @param beanList the bean list that contains validated bean + * @param bean the bean that needs to be validated + * @param index the index that is the reference to the row number of the Excel file + * @throws IllegalArgumentException the illegal argument exception + */ + protected abstract void validateBeforeAddToBeanList(List beanList, T bean, int index) throws IllegalArgumentException; + + /** + * Before database operation. + * + * @param beanList the bean list + */ + protected void beforeDatabaseOperation(List beanList) { + } + + /** + * Execute database operation. + * + * @param beanList the bean list that can be used for DB operations + * @throws Exception the exception + */ + protected abstract void executeDatabaseOperation(List beanList) throws Exception; + + /** + * After execute. Delete file. + */ + protected void afterExecute() { + } + + /** + * Gets template file name. + * + * @return the template file name + */ + protected abstract String getTemplateFileName(); + + /** + * Gets list for exporting. + * + * @return the list for exporting + */ + protected abstract List getListForExporting(); + + @GetMapping("/stat/excel-template") + @ApiOperation(value = "Download template excel file", notes = "Download template excel file") + public ResponseEntity downloadRoleStat() { + @Cleanup val excelWriter = new ExcelWriter(true); + excelWriter.setHeaderAlias(this.exportingFieldAliasMap); + excelWriter.write(this.getListForExporting()); + excelWriter.setFreezePane(1); + excelWriter.autoSizeColumnAll(); + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, + ContentDisposition.builder("attachment").filename( + this.getTemplateFileName()).build().toString()) + .body(outputStream -> excelWriter.flush(outputStream, true)); + } + + /** + * Upload excel file. Any exceptions happened in any lifecycle will not interrupt the whole process. + * + * @param multipartFile the multipart file + * @return the response body bean + */ + @PostMapping(value = "/upload/excel") + @ApiOperation(value = "Upload Excel file", notes = "Upload and import excel data") + public ResponseBodyBean upload(@RequestParam("file") MultipartFile multipartFile) { + this.beforeExecute(); + this.initLocaleContext(); + this.readMultipartFile(multipartFile); + this.bindData(); + this.beforeDatabaseOperation(this.beanList.get()); + this.exeDatabaseOperation(); + this.handlePreviousException(); + val excelImportResult = this.buildResult(); + this.afterExecute(); + this.destroyLocaleContext(); + return ResponseBodyBean.ofSuccess(excelImportResult); + } + + private ExcelImportResult buildResult() { + val excelImportResult = new ExcelImportResult(); + excelImportResult.setMessageList(this.returnMessageList.get()); + excelImportResult.setExcelFilePath(this.excelFilePath.get()); + return excelImportResult; + } + + private void handlePreviousException() { + if (Boolean.TRUE.equals(this.exceptionOccurred.get())) { + this.onExceptionOccurred(); + this.uploadWorkbook(); + } + } + + private void exeDatabaseOperation() { + // if no error message (errorMessageList if empty) or not denyAll, then execute DB operation + if (CollectionUtil.isEmpty(this.errorMessageList.get()) || !this.denyAll) { + if (CollectionUtil.isNotEmpty(this.beanList.get())) { + this.setReturnMessageList("Starting - Import data…"); + try { + this.executeDatabaseOperation(this.beanList.get()); + this.setReturnMessageList( + String.format("Finished - Import data. Imported count: %d", this.beanList.get().size())); + } catch (Exception e) { + this.exceptionOccurred.set(true); + log.error("Exception occurred when executing DB operation!", e); + this.setErrorMessage( + String.format("Exception occurred when executing DB operation! Exception message: %s", + e.getMessage())); + } + } else { + this.setReturnMessageList( + String.format("Finished - Import data. Empty list. Imported count: %d", + this.beanList.get().size())); + } + } else { + this.setReturnMessageList("[Warning] Found not valid data. Data import all failed!"); + } + } + + private void bindData() { + try { + this.setReturnMessageList("Starting - Validate and bind data…"); + this.bindData(this.workbook.get()); + this.setReturnMessageList("Finished - Validate and bind data"); + } catch (Exception e) { + log.error("Exception occurred when validating and binding data!", e); + this.setErrorMessage( + String.format("Exception occurred when validating and binding data! Exception message: %s", + e.getMessage())); + } + } + + private void readMultipartFile(MultipartFile multipartFile) { + try { + this.setReturnMessageList("Starting - Read Excel file…"); + this.workbook.set(this.readFile(multipartFile)); + this.setReturnMessageList("Finished - Read Excel file"); + } catch (IOException e) { + log.error("Exception occurred when reading Excel file!", e); + this.setErrorMessage( + String.format("Exception occurred when reading Excel file! Exception message: %s", e.getMessage())); + } + } + + /** + * Gets generic class. + * + * @return the generic class + */ + private Class getGenericClass() { + val type = this.getClass().getGenericSuperclass(); + log.info("Got type by reflection, typeName: {}", type.getTypeName()); + if (type instanceof ParameterizedType) { + val parameterizedType = (ParameterizedType) type; + val typeName = parameterizedType.getActualTypeArguments()[0].getTypeName(); + Class aClass; + try { + //noinspection unchecked + aClass = (Class) Class.forName(typeName); + } catch (ClassNotFoundException e) { + log.error("Exception occurred when looking for class!", e); + throw new IllegalArgumentException(e.getMessage()); + } + return aClass; + } + throw new IllegalArgumentException("Cannot find the type from the generic class!"); + } + + /** + * Sets field name array. + * + * @param fieldNameArray the field name array + */ + public void setFieldNameArray(String[] fieldNameArray) { + this.fieldNameArray = fieldNameArray; + } + + /** + * Read the file into workbook. + * + * @param multipartFile the multipart file + * @return the workbook + * @throws IOException the io exception + */ + private Workbook readFile(@NonNull MultipartFile multipartFile) throws IOException { + return WorkbookUtil.createBook(multipartFile.getInputStream()); + } + + @SneakyThrows + private void uploadWorkbook() { + @Cleanup val outputStream = new ByteArrayOutputStream(); + this.workbook.get().write(outputStream); + final var bufferedInputStream = new BufferedInputStream(new ByteArrayInputStream(outputStream.toByteArray())); + this.minioHelper.makeBucket("temp"); + this.minioHelper.putObject("temp", this.fileName.get(), bufferedInputStream, + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + this.excelFilePath.set("temp/temp.xlsx"); + } + + /** + *

Bind data from the workbook. Processes below:

+ *
    + *
  • Bind data to the instance of bindClass, according to the fieldNameArray.

    + *
  • + *
  • Validate the field by callback method

    + *
  • + *
+ * + * @param workbook the workbook + * @throws Exception the exception + */ + @SuppressWarnings("RedundantThrows") + private void bindData(Workbook workbook) throws Exception { + for (int index = 0; index < workbook.getNumberOfSheets(); index++) { + val sheet = workbook.getSheetAt(index); + val excelReader = new ExcelReader(workbook, index); + excelReader.setHeaderAlias(this.importingFieldAliasMap); + this.beanList.set(excelReader.readAll(this.beanClass)); + // The reason why validating bean by row is to generate error message behind the last column + for (var beanIndex = 0; beanIndex < this.beanList.get().size(); beanIndex++) { + this.validateBeanByRow(index, beanIndex); + } + } + } + + private void validateBeanByRow(int sheetIndex, int beanIndex) { + try { + this.validateBeforeAddToBeanList(this.beanList.get(), this.beanList.get().get(beanIndex), beanIndex); + } catch (Exception e) { + log.error("bindRowToBean method has encountered a problem!", e); + this.exceptionOccurred.set(true); + val errorMessage = String.format( + "Exception occurred when binding and validating the data of row number %d. " + + "Exception message: %s", beanIndex + 1, e.getMessage()); + this.setErrorMessage(errorMessage); + val errorRow = this.workbook.get().getSheetAt(sheetIndex).getRow(beanIndex + 1); + val errorInformationCell = errorRow.createCell(this.fieldNameArray.length); + errorInformationCell.setCellValue(errorMessage); + } + } + + /** + * Sets deny all. When exception occurred, deny all data or not + * + * @param denyAll the deny all + */ + public void setDenyAll(Boolean denyAll) { + this.denyAll = denyAll; + } + + /** + * Sets error message. + * + * @param errorInfo the error info + */ + protected void setErrorMessage(String errorInfo) { + this.errorMessageList.get().add(errorInfo); + this.setReturnMessageList(errorInfo); + } + + /** + * Sets return message list. + * + * @param message the message + */ + protected void setReturnMessageList(String message) { + this.returnMessageList.get().add(message); + } + + /** + * Close workbook. + * + * @param workbook the workbook + */ + private void closeWorkbook(Workbook workbook) { + if (workbook != null) { + try { + workbook.close(); + } catch (IOException e) { + log.error("Exception occurred when closing workbook! Exception message: {}, workbook: {}", + e.getMessage(), workbook); + } + } + } +} diff --git a/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/controller/AbstractExcelImportController.java b/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/controller/AbstractExcelImportController.java deleted file mode 100644 index 89a5707c..00000000 --- a/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/controller/AbstractExcelImportController.java +++ /dev/null @@ -1,1127 +0,0 @@ -package com.jmsoftware.maf.springcloudstarter.controller; - -import cn.hutool.core.collection.CollectionUtil; -import cn.hutool.core.util.ObjectUtil; -import cn.hutool.core.util.StrUtil; -import com.jmsoftware.maf.common.bean.ExcelImportResult; -import com.jmsoftware.maf.common.bean.ResponseBodyBean; -import com.jmsoftware.maf.springcloudstarter.configuration.ExcelImportConfiguration; -import com.jmsoftware.maf.springcloudstarter.sftp.SftpHelper; -import com.jmsoftware.maf.springcloudstarter.sftp.SftpUploadFile; -import com.jmsoftware.maf.springcloudstarter.util.PoiUtil; -import io.swagger.annotations.ApiOperation; -import lombok.*; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.FilenameUtils; -import org.apache.poi.hssf.usermodel.HSSFWorkbook; -import org.apache.poi.poifs.filesystem.POIFSFileSystem; -import org.apache.poi.ss.usermodel.*; -import org.apache.poi.xssf.usermodel.XSSFWorkbook; -import org.springframework.boot.system.ApplicationHome; -import org.springframework.integration.file.support.FileExistsMode; -import org.springframework.util.Assert; -import org.springframework.util.ReflectionUtils; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.multipart.MultipartHttpServletRequest; - -import javax.annotation.Resource; -import javax.servlet.http.HttpServletRequest; -import java.beans.IntrospectionException; -import java.beans.Introspector; -import java.beans.PropertyDescriptor; -import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.ParameterizedType; -import java.math.BigDecimal; -import java.text.SimpleDateFormat; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; -import java.util.*; - -/** - *

AbstractExcelImportController

- *

Abstract controller for excel data import.

- *

Features

- *
    - *
  1. Row validation. If not valid, will generate error message at the last cell.
  2. - *
  3. Generalization support. AbstractExcelImportController<ExcelImportBeanType>
  4. - *
  5. Using annotation @ExcelColumn for fields can be generating Excel easier than ever.
  6. - *
  7. Support partial data import.
  8. - * - *
- *

Understanding of the Lifecycle

- *

Understanding of the lifecycle will help us make less mistakes during programming.

- *

I. initContext()

- *

Initialize context. Generate the field name array of ExcelImportBeanType by reflection.

- *

II. registerBindHandlerMethods()

- *

Register bind handler methods (starts with ‘“check”, like checkString()).

- *

III. registerHandlerMethods()

- *

Register default handler methods (starts with ‘“check”, like checkString()).

- *

IV. beforeExecute()

- *

Left black. Can be overridden by sub class.

- *

V. initLocaleContext()

- *

Initialize locale context for current thread. Main work below:

- *
    - *
  • errorMessageList
  • - *
  • beanList
  • - *
  • returnMessageList
  • - *
  • rowLocation
  • - *
  • columnLocation
  • - *
  • sheetLocation
  • - *
  • readingRowCount
  • - * - *
- *

After initializing, will upload Excel, read and bind data, validate data.

- *

VI. beforeDatabaseOperation()

- *

Left black. Can be overridden by sub class.

- *

VII. executeDatabaseOperation()

- *

Must be implemented by sub class.

- *

VIII. onExceptionOccurred()

- *

If any exceptions occur, this method will be called. Must be implemented by sub class.

- *

Ⅸ. afterExecute()

- *

Delete temporary file.

- *

X. destroyLocaleContext()

- *

Destroy locale context, in opposite of initLocaleContext().

- * - * @param Excel import bean type - * @author Johnny Miller (锺俊), email: johnnysviva@outlook.com, date: 2/19/2021 10:06 AM - */ -@Slf4j -@SuppressWarnings({"unused"}) -public abstract class AbstractExcelImportController { - /** - * Temporary file path - */ - private static final String TEMP_FILE_PATH = - new ApplicationHome(AbstractExcelImportController.class).getSource() - .getParent() + "/temp-file/"; - /** - * SFTP upload directory - */ - private static final String SFTP_DIR = "excels/"; - /** - * File key in MultipartFile - * TODO: check if it's necessary - */ - private static final String FILE_KEY = "upload_file"; - /** - * Default check method prefix - */ - private static final String DEFAULT_CHECK_METHOD_PREFIX = "check"; - /** - * The constant XLS. - */ - private static final String XLS = "xls"; - /** - * The constant XLSX. - */ - private static final String XLSX = "xlsx"; - /** - * The User defined message. - */ - protected ThreadLocal userDefinedMessage = new ThreadLocal<>(); - /** - * Deny all data when data validation fails. Default value is true. - */ - private Boolean denyAll = Boolean.TRUE; - /** - * The class of ExcelImportBeanType - */ - private Class bindClass; - /** - * Data type and bind method - */ - private final Map, Method> bindMethodMap = new HashMap<>(); - /** - * The field names arrays of the ExcelImportBeanType - */ - private String[] fieldNameArray; - /** - * File name - */ - @Getter - @Setter - private String fileName; - /** - * Custom validation method - *

- * Start with `check`, parameter is List list, return value is String or Boolean. - *

- * e.g public String checkName(List list) - */ - private final Map checkMethodMap = new HashMap<>(); - /** - * Sheet location when binging data. - */ - private final ThreadLocal sheetLocation = new ThreadLocal<>(); - /** - * Row location - */ - private final ThreadLocal rowLocation = new ThreadLocal<>(); - /** - * Column location - */ - private final ThreadLocal columnLocation = new ThreadLocal<>(); - /** - * Reading row count - */ - private final ThreadLocal readingRowCount = new ThreadLocal<>(); - /** - * Error message list - */ - protected ThreadLocal> errorMessageList = new ThreadLocal<>(); - /** - * Bean list. After reading Excel - */ - protected final ThreadLocal> beanList = new ThreadLocal<>(); - /** - * Return message list - */ - private final ThreadLocal> returnMessageList = new ThreadLocal<>(); - /** - * The Workbook. - */ - protected final ThreadLocal workbook = new ThreadLocal<>(); - /** - * The Workbook with error message. - */ - protected final ThreadLocal workbookWithErrorMessage = new ThreadLocal<>(); - /** - * The Excel file path. - */ - protected final ThreadLocal excelFilePath = new ThreadLocal<>(); - /** - * The Exception occurred. - */ - protected final ThreadLocal exceptionOccurred = new ThreadLocal<>(); - /** - * The File. - */ - protected final ThreadLocal file = new ThreadLocal<>(); - - @Resource - protected SftpHelper sftpHelper; - @Resource - protected ExcelImportConfiguration excelImportConfiguration; - - /** - *

Constructor of AbstractExcelImportController

- *
    - *
  1. Init context

    - *
  2. - *
  3. Register bind handler methods

    - *
  4. - *
  5. Register handler methods

    - *
  6. - *
- */ - public AbstractExcelImportController() { - initContext(); - registerBindHandlerMethods(); - registerHandlerMethods(); - } - - /** - * Init locale context. - */ - private void initLocaleContext() { - errorMessageList.set(new LinkedList<>()); - beanList.set(new LinkedList<>()); - returnMessageList.set(new LinkedList<>()); - rowLocation.set(0); - columnLocation.set(0); - sheetLocation.set(0); - readingRowCount.set(0); - userDefinedMessage.set(""); - exceptionOccurred.set(false); - } - - /** - * On exception occurred. - */ - protected abstract void onExceptionOccurred(); - - /** - * Destroy locale context. - */ - private void destroyLocaleContext() { - errorMessageList.remove(); - beanList.remove(); - returnMessageList.remove(); - rowLocation.remove(); - columnLocation.remove(); - sheetLocation.remove(); - readingRowCount.remove(); - userDefinedMessage.remove(); - closeWorkbook(workbook.get()); - workbook.remove(); - closeWorkbook(workbookWithErrorMessage.get()); - workbookWithErrorMessage.remove(); - excelFilePath.remove(); - exceptionOccurred.remove(); - file.remove(); - } - - /** - * Close workbook. - * - * @param workbook the workbook - */ - private void closeWorkbook(Workbook workbook) { - if (workbook != null) { - try { - workbook.close(); - } catch (IOException e) { - log.error("Exception occurred when closing workbook! Exception message: {}, workbook: {}", - e.getMessage(), workbook); - } - } - } - - /** - * Upload excel file. Any exceptions happened in any lifecycle will not interrupt the whole process. - * - * @param request the request - * @return the response body bean - */ - @PostMapping(value = "/upload", headers = "content-type=multipart/form-data") - @ApiOperation(value = "Upload Excel file", notes = "Upload Excel file") - public ResponseBodyBean upload(HttpServletRequest request) { - beforeExecute(); - initLocaleContext(); - try { - setReturnMessageList("Starting - Upload Excel file…"); - file.set(uploadFile((MultipartHttpServletRequest) request)); - setReturnMessageList("Finished - Upload Excel file"); - } catch (IOException e) { - log.error("Exception occurred when uploading file!", e); - setErrorMessage( - String.format("Exception occurred when uploading file! Exception message: %s", e.getMessage())); - } - try { - setReturnMessageList("Starting - Read Excel file…"); - workbook.set(readFile(file.get())); - setReturnMessageList("Finished - Read Excel file"); - } catch (IOException e) { - log.error("Exception occurred when reading Excel file!", e); - setErrorMessage( - String.format("Exception occurred when reading Excel file! Exception message: %s", e.getMessage())); - } - try { - setReturnMessageList("Starting - Validate and bind data…"); - bindData(workbook.get()); - setReturnMessageList("Finished - Validate and bind data"); - } catch (Exception e) { - log.error("Exception occurred when validating and binding data!", e); - setErrorMessage(String.format("Exception occurred when validating and binding data! Exception message: %s", - e.getMessage())); - } - beforeDatabaseOperation(this.beanList.get()); - // if no error message (errorMessageList if empty) or not denyAll, then execute DB operation - if (CollectionUtil.isEmpty(this.errorMessageList.get()) || !denyAll) { - if (CollectionUtil.isNotEmpty(this.beanList.get())) { - setReturnMessageList("Starting - Import data…"); - try { - executeDatabaseOperation(this.beanList.get()); - setReturnMessageList( - String.format("Finished - Import data. Imported count: %d", beanList.get().size())); - } catch (Exception e) { - exceptionOccurred.set(true); - log.error("Exception occurred when executing DB operation!", e); - setErrorMessage( - String.format("Exception occurred when executing DB operation! Exception message: %s", - e.getMessage())); - } - } else { - setReturnMessageList( - String.format("Finished - Import data. Empty list. Imported count: %d", beanList.get().size())); - } - } else { - setReturnMessageList("[Warning] Found not valid data. Data import all failed!"); - } - if (exceptionOccurred.get()) { - onExceptionOccurred(); - } - ExcelImportResult excelImportResult = new ExcelImportResult(); - excelImportResult.setMessageList(this.returnMessageList.get()); - excelImportResult.setExcelFilePath(this.excelFilePath.get()); - afterExecute(); - destroyLocaleContext(); - return ResponseBodyBean.ofSuccess(excelImportResult); - } - - /** - * Before execute. - */ - protected void beforeExecute() { - } - - /** - * After execute. Delete file. - */ - protected void afterExecute() { - Optional optionalFile = Optional.ofNullable(file.get()); - optionalFile.ifPresent(file1 -> { - boolean deleted = file1.delete(); - if (deleted) { - log.warn("Deleted file. File absolute path: {}", file1.getAbsolutePath()); - } else { - log.warn("The file cannot be deleted!File absolute path: {}", file1.getAbsolutePath()); - } - }); - if (optionalFile.isEmpty()) { - log.warn("The file does not exist!"); - } - } - - /** - * Before database operation. - * - * @param beanList the bean list - */ - protected void beforeDatabaseOperation(List beanList) { - } - - /** - *

Register handler methods

- *
    - *
  • e.g: private Boolean checkXXX(String value,Integer index)
  • - *
  • e.g: private String checkXXX(String value,Integer index), the return value can be empty or - * “true”, or other error message which will be added to errorMessage
  • - *
- */ - private void registerHandlerMethods() { - this.checkMethodMap.clear(); - // Register valid methods for validation - Method[] methods = this.getClass().getDeclaredMethods(); - for (Method method : methods) { - if (isCheckMethod(method)) { - registerCheckMethod(method); - } - } - } - - /** - *

Check if method is valid for validation.

- *
    - *
  1. Method name starts with `check`

    - *
  2. - *
  3. Parameter is String value

    - *
  4. - *
  5. Return value is Boolean

    - *
  6. - *
- * - * @param method the method - * @return the boolean - */ - private Boolean isCheckMethod(Method method) { - val returnType = method.getReturnType(); - if (method.getName().startsWith(DEFAULT_CHECK_METHOD_PREFIX)) { - if (String.class.equals(returnType) || Boolean.class.equals(returnType) || boolean.class.equals( - returnType)) { - Class[] parameterTypes = method.getParameterTypes(); - return parameterTypes.length >= 2 && String.class.equals(parameterTypes[0]) - && (int.class.equals(parameterTypes[1]) || Integer.class.equals(parameterTypes[1])); - } - } - return false; - } - - /** - * Register check method. - * - * @param method the method - */ - private void registerCheckMethod(Method method) { - var fieldName = method.getName(). - substring(DEFAULT_CHECK_METHOD_PREFIX.length()); - fieldName = fieldName.substring(0, 1).toLowerCase() + fieldName.substring(1); - this.checkMethodMap.put(fieldName, method); - log.info("Registered check method [" + method.getName() + "]"); - } - - /** - *

Init context.

- *
    - *
  1. Call getGenericClass() to set Excel import type;

    - *
  2. - *
  3. Set fieldNameArray

    - *
  4. - *
- */ - protected void initContext() { - bindClass = this.getGenericClass(); - val declaredFields = bindClass.getDeclaredFields(); - val fieldNameArray = new String[declaredFields.length]; - for (var index = 0; index < declaredFields.length; index++) { - val declaredField = declaredFields[index]; - fieldNameArray[index] = declaredField.getName(); - } - log.info("Generated {} field name array by reflection, fieldNameArray: {}", bindClass.getSimpleName(), - fieldNameArray); - this.setFieldNameArray(fieldNameArray); - } - - /** - * Gets generic class. - * - * @return the generic class - */ - private Class getGenericClass() { - val type = getClass().getGenericSuperclass(); - log.info("Got type by reflection, typeName: {}", type.getTypeName()); - if (type instanceof ParameterizedType) { - val parameterizedType = (ParameterizedType) type; - val typeName = parameterizedType.getActualTypeArguments()[0].getTypeName(); - Class aClass; - try { - //noinspection unchecked - aClass = (Class) Class.forName(typeName); - } catch (ClassNotFoundException e) { - log.error("Exception occurred when looking for class!", e); - throw new RuntimeException( - "Exception occurred when looking for class! Exception message:" + e.getMessage()); - } - return aClass; - } - throw new RuntimeException("Cannot find the type from the generic class!"); - } - - /** - * Execute database operation. - * - * @param beanList the bean list that can be used for DB operations - * @throws Exception the exception - */ - protected abstract void executeDatabaseOperation(List beanList) throws Exception; - - /** - * Validate before adding to bean list boolean. - * - * @param beanList the bean list that contains validated bean - * @param bean the bean that needs to be validated - * @param index the index that is the reference to the row number of the Excel file - * @return the boolean - * @throws IllegalArgumentException the illegal argument exception - */ - protected abstract boolean validateBeforeAddToBeanList(List beanList, - ExcelImportBeanType bean, - int index) throws IllegalArgumentException; - - /** - * Sets field name array. - * - * @param fieldNameArray the field name array - */ - public void setFieldNameArray(String[] fieldNameArray) { - this.fieldNameArray = fieldNameArray; - } - - /** - * 上传文件到本地(暂时) - *

- * TODO: check the method if is necessary or not - * - * @param itemBytes the item bytes - * @param fileName the file name - * @return File file - */ - private File uploadTempFile(byte[] itemBytes, String fileName) { - val fileFullPath = TEMP_FILE_PATH + fileName; - log.info("上传文件到本地(暂时)。文件绝对路径:{}", fileFullPath); - // 新建文件 - val file = new File(fileFullPath); - if (!file.getParentFile().exists()) { - //noinspection ResultOfMethodCallIgnored - file.getParentFile().mkdir(); - } - // 上传 - // FileCopyUtils.copy(itemBytes, file); - return file; - } - - /** - * Upload file. - * - * @param request the request - * @return File file - * @throws IOException the io exception - * @see - * Upload large files : Spring Boot - */ - @SuppressWarnings("AlibabaRemoveCommentedCode") - private File uploadFile(MultipartHttpServletRequest request) throws IOException { - val multipartFile = request.getFileMap().get(FILE_KEY); - // Don't do this. - // it loads all of the bytes in java heap memory that leads to OutOfMemoryError. We'll use stream instead. - // byte[] fileBytes = multipartFile.getBytes(); - @Cleanup val fileStream = new BufferedInputStream(multipartFile.getInputStream()); - LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")); - val fileName = String.format("%s%s", - LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")), - multipartFile.getOriginalFilename()); - val targetFile = new File(TEMP_FILE_PATH + fileName); - FileUtils.copyInputStreamToFile(fileStream, targetFile); - uploadFileToSftp(targetFile); - return targetFile; - } - - /** - * Read the file into workbook. - * - * @param file the file - * @return the workbook - * @throws IOException the io exception - */ - private Workbook readFile(@NonNull File file) throws IOException { - Workbook workbook = null; - val extension = FilenameUtils.getExtension(file.getName()); - val bufferedInputStream = new BufferedInputStream(new FileInputStream(file)); - if (XLS.equals(extension)) { - POIFSFileSystem poifsFileSystem = new POIFSFileSystem(bufferedInputStream); - workbook = new HSSFWorkbook(poifsFileSystem); - bufferedInputStream.close(); - } else if (XLSX.equals(extension)) { - workbook = new XSSFWorkbook(bufferedInputStream); - } - return workbook; - } - - /** - * Upload file to SFTP - * - * @param file the file - */ - private void uploadFileToSftp(File file) { - val sftpUploadFile = SftpUploadFile.builder() - .fileToBeUploaded(file) - .fileExistsMode(FileExistsMode.REPLACE) - .subDirectory(SFTP_DIR).build(); - try { - sftpHelper.upload(sftpUploadFile); - } catch (Exception e) { - log.error("Exception occurred when uploading file to SFTP! Exception message:{}", e.getMessage()); - } - } - - /** - * Filter sheet. In default, will proceed all sheets. - * - * @param sheet the sheet - * @return the boolean - */ - protected boolean filterSheet(Sheet sheet) { - return true; - } - - /** - *

Bind data from the workbook. Processes below:

- *
    - *
  • Bind data to the instance of bindClass, according to the fieldNameArray.

    - *
  • - *
  • Validate the field by callback method

    - *
  • - *
- * - * @param workbook the workbook - * @throws Exception the exception - */ - @SuppressWarnings("RedundantThrows") - private void bindData(Workbook workbook) throws Exception { - for (int index = 0; index < workbook.getNumberOfSheets(); index++) { - val sheet = workbook.getSheetAt(index); - if (!filterSheet(sheet)) { - continue; - } - // Specify current sheet location - sheetLocation.set(index + 1); - sheet.getSheetName(); - // Set Reading row count - readingRowCount.set(readingRowCount.get() + sheet.getLastRowNum() - sheet.getFirstRowNum()); - // Check if exceeding the MAXIMUM_ROW_COUNT - if (readingRowCount.get() > excelImportConfiguration.getMaximumRowCount()) { - setErrorMessage(String.format( - "The amount of importing data cannot be greater than %d (Table head included)! " + - "Current reading row count: %d", excelImportConfiguration.getMaximumRowCount(), - readingRowCount.get())); - continue; - } - // Check if the readingRowCount is equal to zero - if (readingRowCount.get() == 0) { - setErrorMessage("Not available data to import. Check Excel again."); - continue; - } - // Check if the first row is equal to null - if (sheet.getRow(0) == null) { - setErrorMessage(String.format("Sheet [%s] (No. %d) format is invalid: no title! ", sheet.getSheetName(), - sheetLocation.get())); - // If the sheet is not valid, then skip it - continue; - } - val startIndex = sheet.getRow(0).getFirstCellNum(); - val endIndex = sheet.getRow(0).getLastCellNum(); - // Parse from the second row - for (var i = sheet.getFirstRowNum() + 1; i <= sheet.getLastRowNum(); i++) { - // Specify current row location - rowLocation.set(i + 1); - val row = sheet.getRow(i); - if (isBlankRow(row)) { - // Blank row will not be considered as a effective row, not be included in `readingRowCount` - readingRowCount.set(readingRowCount.get() - 1); - continue; - } - // Bind row to bean - bindRowToBean(row, startIndex, endIndex); - } - } - } - - /** - * Check if the row is blank. If there is one cell of the row is not blank, then the row is not blank. - * - * @param row the row - * @return true if the row is blank; or vice versa. - */ - private boolean isBlankRow(Row row) { - if (row == null) { - return true; - } - Cell cell; - var value = ""; - for (var i = row.getFirstCellNum(); i < row.getLastCellNum(); i++) { - cell = row.getCell(i, Row.MissingCellPolicy.RETURN_BLANK_AS_NULL); - if (cell != null) { - switch (cell.getCellType()) { - case STRING: - value = cell.getStringCellValue().trim(); - break; - case NUMERIC: - value = String.valueOf((int) cell.getNumericCellValue()); - break; - case BOOLEAN: - value = String.valueOf(cell.getBooleanCellValue()); - break; - case FORMULA: - value = String.valueOf(cell.getCellFormula()); - break; - default: - break; - } - if (StrUtil.isNotBlank(value)) { - return false; - } - } - } - return true; - } - - /** - * Bind row to bean. - * - * @param row the row - * @param startIndex the start index - * @param endIndex the end index - */ - private void bindRowToBean(Row row, int startIndex, int endIndex) { - Assert.notNull(this.bindClass, "bindClass must not be null!"); - Assert.notNull(this.fieldNameArray, "fieldNameArray must not be null!"); - var bindingResult = false; - try { - // New bean instance - val bean = this.bindClass.getDeclaredConstructor().newInstance(); - val beanInfo = Introspector.getBeanInfo(bean.getClass()); - val propertyDescriptors = beanInfo.getPropertyDescriptors(); - for (var index = startIndex; index < endIndex; index++) { - // If found more data, then binding failed - if (index >= fieldNameArray.length) { - bindingResult = false; - setErrorMessage( - String.format("Found redundant data on row number %d. Check again.", rowLocation.get())); - break; - } - // Get the field that needs binding - val fieldName = fieldNameArray[index]; - PropertyDescriptor propertyDescriptor = null; - for (val pd : propertyDescriptors) { - if (pd.getName().equals(fieldName)) { - propertyDescriptor = pd; - break; - } - } - if (ObjectUtil.isNull(propertyDescriptor)) { - throw new RuntimeException("Cannot find the field in the specify class!"); - } - val value = getCellValue2String(row.getCell(index)); - // Start to bind - // Specify current column location - columnLocation.set(index + 1); - // Execute custom validation method - val customValidationMethod = this.checkMethodMap.get(fieldName); - Object returnObj = null; - if (ObjectUtil.isNotNull(customValidationMethod)) { - returnObj = customValidationMethod.invoke(this, value, rowLocation.get(), bean); - } - val validationResult = returnObj == null ? null : returnObj.toString(); - // If `validationResult` is blank or equal to "true" - if (StrUtil.isBlank(validationResult) || Boolean.TRUE.toString().equals(validationResult)) { - bindingResult = bind(value, propertyDescriptor, bean); - } else { - bindingResult = false; - // If `validationResult` is not equal to "false" then add to error message list - if (!Boolean.FALSE.toString().equals(validationResult)) { - setErrorMessage(validationResult); - } - } - // Finished binding - if (!bindingResult) { - break; - } - } - if (bindingResult) { - bindingResult = validateBeforeAddToBeanList(this.beanList.get(), bean, rowLocation.get()); - } - if (bindingResult) { - this.beanList.get().add(bean); - } - } catch (IntrospectionException - | InvocationTargetException - | InstantiationException - | IllegalAccessException - | NoSuchMethodException e) { - log.error("bindRowToBean method has encountered a problem!", e); - exceptionOccurred.set(true); - val errorMessage = String.format( - "Exception occurred when binding and validating the data of row number %d. " + - "Exception message: %s", rowLocation.get(), e.getMessage()); - setErrorMessage(errorMessage); - val lastCellNum = row.getLastCellNum(); - val errorInformationCell = row.createCell(fieldNameArray.length); - errorInformationCell.setCellValue(errorMessage); - val firstSheet = workbookWithErrorMessage.get().getSheetAt(0); - val row1 = firstSheet.createRow(firstSheet.getLastRowNum() + 1); - PoiUtil.copyRow(true, workbookWithErrorMessage.get(), row, row1); - } - } - - /** - * Gets cell value 2 string. - * - * @param cell the cell - * @return the string - */ - private String getCellValue2String(Cell cell) { - var returnString = ""; - if (ObjectUtil.isNull(cell)) { - return returnString; - } - switch (cell.getCellType()) { - case BLANK: - return ""; - case NUMERIC: - // If it's date - if (DateUtil.isCellDateFormatted(cell)) { - val date = cell.getDateCellValue(); - val dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - returnString = dateFormat.format(date); - return returnString; - } - val bigDecimal = new BigDecimal(String.valueOf(cell.getNumericCellValue()).trim()); - // Keep decimal fraction parts which are not zero - val tempStr = bigDecimal.toPlainString(); - val dotIndex = tempStr.indexOf("."); - if ((bigDecimal.doubleValue() - bigDecimal.longValue()) == 0 && dotIndex > 0) { - returnString = tempStr.substring(0, dotIndex); - } else { - returnString = tempStr; - } - return returnString; - case STRING: - if (cell.getStringCellValue() != null) { - returnString = cell.getStringCellValue().trim(); - } - return returnString; - default: - return returnString; - } - } - - /** - * Register bind handler methods. - */ - private void registerBindHandlerMethods() { - try { - val bindStringField = ReflectionUtils.findMethod(this.getClass(), - "bindStringField", - String.class, - PropertyDescriptor.class, - Object.class); - val bindIntField = ReflectionUtils.findMethod(this.getClass(), - "bindIntField", - String.class, - PropertyDescriptor.class, - Object.class); - val bindLongField = ReflectionUtils.findMethod(this.getClass(), - "bindLongField", - String.class, - PropertyDescriptor.class, - Object.class); - val bindFloatField = ReflectionUtils.findMethod(this.getClass(), - "bindFloatField", - String.class, - PropertyDescriptor.class, - Object.class); - val bindDoubleField = ReflectionUtils.findMethod(this.getClass(), - "bindDoubleField", - String.class, - PropertyDescriptor.class, - Object.class); - val bindLocalDateTimeField = ReflectionUtils.findMethod(this.getClass(), - "bindLocalDateTimeField", - String.class, - PropertyDescriptor.class, - Object.class); - this.bindMethodMap.put(String.class, bindStringField); - this.bindMethodMap.put(Integer.class, bindIntField); - this.bindMethodMap.put(Long.class, bindLongField); - this.bindMethodMap.put(Float.class, bindFloatField); - this.bindMethodMap.put(Double.class, bindDoubleField); - this.bindMethodMap.put(LocalDateTime.class, bindLocalDateTimeField); - } catch (Exception e) { - log.error("The bindMethod required was not found in this class!", e); - } - } - - /** - * Real binding value to the bean's field. - *

- * - * @param value the string value of the excel cell - * @param propertyDescriptor the the field of the bean - * @param bean the bean - * @return true if the binding succeeded, or vice versa. - * @throws RuntimeException the runtime exception - */ - private Boolean bind(String value, PropertyDescriptor propertyDescriptor, Object bean) throws RuntimeException { - boolean result; - val fieldType = propertyDescriptor.getPropertyType(); - if (!this.bindMethodMap.containsKey(fieldType)) { - throw new RuntimeException( - String.format("The bindMethod required was not found in bindMethodMap! Field type: %s", - fieldType.getSimpleName())); - } - val bindMethod = this.bindMethodMap.get(fieldType); - value = StrUtil.isBlank(value) ? null : value; - try { - // Why is the parameter 'this' required? Because it's this class calling the 'bindMethod', - // and the 'bingMethod' belongs to this class object. - result = (Boolean) bindMethod.invoke(this, value, propertyDescriptor, bean); - } catch (IllegalAccessException - | IllegalArgumentException - | InvocationTargetException e) { - val exceptionMessage = new StringBuilder("Exception occurred when binding! "); - if (!StrUtil.isBlank(e.getMessage())) { - log.error("Exception occurred when invoking method!", e); - exceptionMessage.append(e.getMessage()).append(" "); - } - if (ObjectUtil.isNotNull(e.getCause()) && !StrUtil.isBlank(e.getCause().getMessage())) { - log.error("Exception occurred when invoking method!", e.getCause()); - exceptionMessage.append(e.getCause().getMessage()); - } - throw new RuntimeException(exceptionMessage.toString()); - } - return result; - } - - /** - * Bind int field boolean. - * - * @param value the value - * @param propertyDescriptor the property descriptor - * @param bean the bean - * @return the boolean - * @throws IllegalAccessException the illegal access exception - */ - private Boolean bindIntField(String value, PropertyDescriptor propertyDescriptor, Object bean) throws IllegalAccessException { - try { - var realValue = value == null ? null : Integer.parseInt(value); - if (ObjectUtil.isNull(realValue) && propertyDescriptor.getPropertyType() == int.class) { - realValue = 0; - } - propertyDescriptor.getWriteMethod().invoke(bean, realValue); - } catch (NumberFormatException | InvocationTargetException e) { - log.error("Exception occurred when binding int/Integer field! Exception message: {}, value: {}, field: {}", - e.getMessage(), value, propertyDescriptor.getName()); - val formattedMessage = String.format("Invalid data of the row %d, col %d, must be integer", - rowLocation.get(), columnLocation.get()); - setErrorMessage(formattedMessage); - throw new IllegalArgumentException(formattedMessage); - } - return true; - } - - /** - * Bind long field boolean. - * - * @param value the value - * @param propertyDescriptor the property descriptor - * @param bean the bean - * @return the boolean - * @throws IllegalAccessException the illegal access exception - */ - private Boolean bindLongField(String value, PropertyDescriptor propertyDescriptor, Object bean) throws IllegalAccessException { - try { - var realValue = value == null ? null : (long) Double.parseDouble(value); - if (ObjectUtil.isNull(realValue) && propertyDescriptor.getPropertyType() == long.class) { - realValue = 0L; - } - propertyDescriptor.getWriteMethod().invoke(bean, realValue); - } catch (NumberFormatException | InvocationTargetException e) { - log.error("Exception occurred when binding long/Long field! Exception message: {}, value: {}, field: {}", - e.getMessage(), value, propertyDescriptor.getName()); - val formattedMessage = String.format("Invalid data of the row %d, col %d, must be long integer", - rowLocation.get(), columnLocation.get()); - setErrorMessage(formattedMessage); - throw new IllegalArgumentException(formattedMessage); - } - return true; - } - - /** - * Bind float field boolean. - * - * @param value the value - * @param propertyDescriptor the property descriptor - * @param bean the bean - * @return the boolean - * @throws IllegalAccessException the illegal access exception - */ - private Boolean bindFloatField(String value, PropertyDescriptor propertyDescriptor, Object bean) throws IllegalAccessException { - try { - var realValue = value == null ? null : Float.parseFloat(value); - if (ObjectUtil.isNull(realValue) && propertyDescriptor.getPropertyType() == float.class) { - realValue = 0F; - } - propertyDescriptor.getWriteMethod().invoke(bean, realValue); - } catch (NumberFormatException | InvocationTargetException e) { - log.error("Exception occurred when binding float/Float field! Exception message: {}, value: {}, field: {}", - e.getMessage(), value, propertyDescriptor.getName()); - val formattedMessage = String.format("Invalid data of the row %d, col %d, must be float", - rowLocation.get(), columnLocation.get()); - setErrorMessage(formattedMessage); - throw new IllegalArgumentException(formattedMessage); - } - return true; - } - - /** - * Bind double field boolean. - * - * @param value the value - * @param propertyDescriptor the property descriptor - * @param bean the bean - * @return the boolean - * @throws IllegalAccessException the illegal access exception - */ - private Boolean bindDoubleField(String value, PropertyDescriptor propertyDescriptor, Object bean) throws IllegalAccessException { - try { - var realValue = value == null ? null : Double.parseDouble(value); - if (ObjectUtil.isNull(realValue) && propertyDescriptor.getPropertyType() == double.class) { - realValue = 0D; - } - propertyDescriptor.getWriteMethod().invoke(bean, realValue); - } catch (NumberFormatException | InvocationTargetException e) { - log.error( - "Exception occurred when binding double/Double field! Exception message: {}, value: {}, field: {}", - e.getMessage(), value, propertyDescriptor.getName()); - val formattedMessage = String.format("Invalid data of the row %d, col %d, must be double", - rowLocation.get(), columnLocation.get()); - setErrorMessage(formattedMessage); - throw new IllegalArgumentException(formattedMessage); - } - return true; - } - - /** - * Bind string field boolean. - * - * @param value the value - * @param propertyDescriptor the property descriptor - * @param bean the bean - * @return the boolean - * @throws IllegalAccessException the illegal access exception - */ - private Boolean bindStringField(String value, PropertyDescriptor propertyDescriptor, Object bean) throws IllegalAccessException { - try { - propertyDescriptor.getWriteMethod().invoke(bean, value); - } catch (InvocationTargetException e) { - log.error("Exception occurred when binding String field! Exception message: {}, value: {}, field: {}", - e.getMessage(), value, propertyDescriptor.getName()); - val formattedMessage = String.format("Invalid data of the row %d, col %d, must be string", - rowLocation.get(), columnLocation.get()); - setErrorMessage(formattedMessage); - throw new IllegalArgumentException(formattedMessage); - } - return true; - } - - /** - * Bind local date time field boolean. - * - * @param value the value - * @param propertyDescriptor the property descriptor - * @param bean the bean - * @return the boolean - * @throws IllegalAccessException the illegal access exception - */ - private Boolean bindLocalDateTimeField(String value, PropertyDescriptor propertyDescriptor, Object bean) throws IllegalAccessException { - try { - val date = value == null ? null : LocalDateTime.parse(value, - DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); - propertyDescriptor.getWriteMethod().invoke(bean, date); - } catch (DateTimeParseException | InvocationTargetException e) { - log.error( - "Exception occurred when binding LocalDateTime field! Exception message: {}, value: {}, field: {}", - e.getMessage(), value, propertyDescriptor.getName()); - val formattedMessage = String.format("Invalid data of the row %d, col %d, must be date", - rowLocation.get(), columnLocation.get()); - setErrorMessage(formattedMessage); - throw new IllegalArgumentException(formattedMessage); - } - return true; - } - - /** - * Sets deny all. When exception occurred, deny all data or not - * - * @param denyAll the deny all - */ - public void setDenyAll(Boolean denyAll) { - this.denyAll = denyAll; - } - - /** - * Sets error message. - * - * @param errorInfo the error info - */ - protected void setErrorMessage(String errorInfo) { - this.errorMessageList.get().add(errorInfo); - setReturnMessageList(errorInfo); - } - - /** - * Sets return message list. - * - * @param message the message - */ - protected void setReturnMessageList(String message) { - this.returnMessageList.get().add(message); - } -} diff --git a/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/database/MyBatisPlusMetaObjectHandler.java b/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/database/CommonMetaObjectHandler.java similarity index 52% rename from spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/database/MyBatisPlusMetaObjectHandler.java rename to spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/database/CommonMetaObjectHandler.java index dfccb609..75aae1a9 100644 --- a/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/database/MyBatisPlusMetaObjectHandler.java +++ b/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/database/CommonMetaObjectHandler.java @@ -1,24 +1,35 @@ package com.jmsoftware.maf.springcloudstarter.database; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import com.jmsoftware.maf.common.domain.DeletedField; -import com.jmsoftware.maf.springcloudstarter.util.UsernameUtil; +import com.jmsoftware.maf.springcloudstarter.util.UserUtil; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.reflection.MetaObject; import java.time.LocalDateTime; /** - *

MyBatisPlusConfiguration

- *

- * Change description here. - *

+ *

CommonMetaObjectHandler

+ *

CommonMetaObjectHandler will inject these fields automatically when executing INSERT or + * UPDATE.

+ *

This class is cooperating with the annotation @TableField. So the persistence Java class must be + * annotated by @TableField(value = COL_CREATED_BY, fill = INSERT) or @TableField(value = + * COL_MODIFIED_BY, fill = UPDATE).

+ *
+ * + * + * + *
MySQL Field NameField Name Java
created_bycreatedBy
created_timecreatedTime
modified_bymodifiedBy
modified_timemodifiedTime
deleteddeleted
+ *

 

* - * @author Johnny Miller (锺俊), email: johnnysviva@outlook.com - * @date 2019-05-02 11:57 + * @author Johnny Miller (锺俊), email: johnnysviva@outlook.com, date: 8/23/2021 10:43 AM + * @see TableField **/ @Slf4j -public class MyBatisPlusMetaObjectHandler implements MetaObjectHandler { +public class CommonMetaObjectHandler implements MetaObjectHandler { /** * The Java persistence object field name: createdBy */ @@ -46,18 +57,26 @@ public class MyBatisPlusMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { // 严格填充,只针对非主键的字段,只有该表注解了fill 并且 字段名和字段属性 能匹配到才会进行填充(null 值不填充) - log.info("Starting to insert fill metaObject: {}", metaObject.getOriginalObject()); - this.strictInsertFill(metaObject, CREATED_BY_FIELD_NAME, String.class, UsernameUtil.getCurrentUsername()) + if (log.isDebugEnabled()) { + log.debug("Starting to insert fill metaObject: {}", metaObject.getOriginalObject()); + } + this.strictInsertFill(metaObject, CREATED_BY_FIELD_NAME, Long.class, UserUtil.getCurrentId()) .strictInsertFill(metaObject, CREATED_TIME_FIELD_NAME, LocalDateTime.class, LocalDateTime.now()) .strictInsertFill(metaObject, DELETED_FIELD_NAME, Byte.class, DeletedField.NOT_DELETED.getValue()); - log.info("Finished to insert fill metaObject: {}", metaObject.getOriginalObject()); + if (log.isDebugEnabled()) { + log.debug("Finished to insert fill metaObject: {}", metaObject.getOriginalObject()); + } } @Override public void updateFill(MetaObject metaObject) { - log.info("Starting to update fill metaObject: {}", metaObject.getOriginalObject()); - this.strictUpdateFill(metaObject, MODIFIED_BY_FIELD_NAME, String.class, UsernameUtil.getCurrentUsername()) + if (log.isDebugEnabled()) { + log.debug("Starting to update fill metaObject: {}", metaObject.getOriginalObject()); + } + this.strictUpdateFill(metaObject, MODIFIED_BY_FIELD_NAME, Long.class, UserUtil.getCurrentId()) .strictUpdateFill(metaObject, MODIFIED_TIME_FIELD_NAME, LocalDateTime.class, LocalDateTime.now()); - log.info("Finished to update fill metaObject: {}", metaObject.getOriginalObject()); + if (log.isDebugEnabled()) { + log.debug("Finished to update fill metaObject: {}", metaObject.getOriginalObject()); + } } } diff --git a/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/database/DataSourceConfiguration.java b/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/database/DataSourceConfiguration.java index 8d87f23d..6e01a1a5 100644 --- a/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/database/DataSourceConfiguration.java +++ b/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/database/DataSourceConfiguration.java @@ -29,6 +29,12 @@ @Slf4j @ConditionalOnClass({MybatisPlusAutoConfiguration.class}) public class DataSourceConfiguration { + @Bean + public DruidDataSourceCreatorPostProcessor druidDataSourceCreatorPostProcessor() { + log.warn("Initial bean: '{}'", DruidDataSourceCreatorPostProcessor.class.getSimpleName()); + return new DruidDataSourceCreatorPostProcessor(); + } + /** * Primary data source. Had to configure DynamicRoutingDataSource as primary. Otherwise * MasterSlaveAutoRoutingPlugin will not be able to be injected datasource correctly. diff --git a/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/database/DruidDataSourceCreatorPostProcessor.java b/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/database/DruidDataSourceCreatorPostProcessor.java new file mode 100644 index 00000000..44d8b243 --- /dev/null +++ b/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/database/DruidDataSourceCreatorPostProcessor.java @@ -0,0 +1,65 @@ +package com.jmsoftware.maf.springcloudstarter.database; + +import com.baomidou.dynamic.datasource.creator.DruidDataSourceCreator; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; + +/** + * Description: DruidDataSourceCreatorPostProcessor, change description here. + * + * @author Johnny Miller (锺俊), email: johnnysviva@outlook.com, date: 8/25/2021 11:27 AM + **/ +@Slf4j +public class DruidDataSourceCreatorPostProcessor implements BeanPostProcessor { + @Nullable + @Override + public Object postProcessAfterInitialization(@NonNull Object bean, @NonNull String beanName) throws BeansException { + if (bean instanceof DruidDataSourceCreator) { + this.postProcessDynamicDataSourceProperties((DruidDataSourceCreator) bean); + } + return bean; + } + + /** + * Post process dynamic data source properties. Enhance connection pool size by available processor count + * (logical processor count) + * + *

A formula which has held up pretty well across a lot of benchmarks for years is that for optimal throughput + * the number of active connections should be somewhere near ((core_count * 2) + + * effective_spindle_count). Core count should not include HT threads, even if hyperthreading is enabled + * . Effective spindle count is zero if the active data set is fully cached, and approaches the actual number of + * spindles as the cache hit rate falls. Benchmarks of WIP for version 9.2 suggest that this formula will need + * adjustment on that release. There hasn't been any analysis so far regarding how well the formula works + * with SSDs.

+ *

However you choose a starting point for a connection pool size, you should probably try incremental + * adjustments with your production system to find the actual "sweet spot" for your hardware and + * workload.

+ *

Remember that this "sweet spot" is for the number of connections that are actively doing + * work. Ignore mostly-idle connections used for system monitoring and control when working out an + * appropriate pool size. You should always make max_connections a bit bigger than the number of + * connections you enable in your connection pool. That way there are always a few slots available for direct + * connections for system maintenance and monitoring.

+ * + * @param bean the bean + * @see + * How to Find the Optimal Database Connection Pool Size + * @see + * Sizing the Connection Pool + */ + private void postProcessDynamicDataSourceProperties(DruidDataSourceCreator bean) { + val cpuCoreCount = Runtime.getRuntime().availableProcessors(); + val minConnectionPoolSize = cpuCoreCount * 2 + 1; + val maxConnectionPoolSize = cpuCoreCount * 3; + bean.getGConfig() + .setInitialSize(minConnectionPoolSize) + .setMinIdle(minConnectionPoolSize) + .setMaxActive(maxConnectionPoolSize); + log.warn("Druid connection pool enhanced by current cpuCoreCount: {}, initial size: {}, min idle: {}" + + ", max active: {}", + cpuCoreCount, minConnectionPoolSize, minConnectionPoolSize, maxConnectionPoolSize); + } +} diff --git a/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/database/MyBatisPlusConfiguration.java b/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/database/MyBatisPlusConfiguration.java index 09c4bfb5..2bef11fc 100644 --- a/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/database/MyBatisPlusConfiguration.java +++ b/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/database/MyBatisPlusConfiguration.java @@ -70,8 +70,8 @@ public Interceptor masterSlaveAutoRoutingPlugin() { } @Bean - public MyBatisPlusMetaObjectHandler myBatisPlusConfiguration() { - log.warn("Initial bean: '{}'", MyBatisPlusMetaObjectHandler.class.getSimpleName()); - return new MyBatisPlusMetaObjectHandler(); + public CommonMetaObjectHandler myBatisPlusConfiguration() { + log.warn("Initial bean: '{}'", CommonMetaObjectHandler.class.getSimpleName()); + return new CommonMetaObjectHandler(); } } diff --git a/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/helper/IpHelper.java b/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/helper/IpHelper.java index cfa208d0..d6623b3b 100644 --- a/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/helper/IpHelper.java +++ b/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/helper/IpHelper.java @@ -43,7 +43,7 @@ public void onApplicationEvent(WebServerInitializedEvent event) { public String getPublicIp() { val jointProfiles = String.join(",", this.environment.getActiveProfiles()); if (StrUtil.containsIgnoreCase(jointProfiles, DEVELOPMENT_ENVIRONMENT)) { - log.debug("Current active profiles for environment contains: {}. U", DEVELOPMENT_ENVIRONMENT); + log.debug("Current active profiles for environment contains: {}", DEVELOPMENT_ENVIRONMENT); return this.getInternetIp(); } // An API provided by https://whatismyipaddress.com/api diff --git a/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/util/PoiUtil.java b/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/util/PoiUtil.java deleted file mode 100644 index 136583ec..00000000 --- a/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/util/PoiUtil.java +++ /dev/null @@ -1,214 +0,0 @@ -package com.jmsoftware.maf.springcloudstarter.util; - -import lombok.val; -import org.apache.poi.hssf.usermodel.*; -import org.apache.poi.ss.usermodel.*; -import org.apache.poi.ss.util.CellRangeAddress; - -/** - *

PoiUtil

- *

- * Change description here. - * - * @author @author Johnny Miller (锺俊), email: johnnysviva@outlook.com, date: 2/18/2021 5:37 PM - */ -@SuppressWarnings({"AlibabaRemoveCommentedCode", "unused"}) -public class PoiUtil { - private PoiUtil() { - } - - /** - * Copy cell style. - * - * @param sourceCellStyle the source cell style - * @param targetCellStyle the target cell style - */ - public static void copyCellStyle(HSSFCellStyle sourceCellStyle, HSSFCellStyle targetCellStyle) { - targetCellStyle.setAlignment(sourceCellStyle.getAlignment()); - // Boarder style - targetCellStyle.setBorderBottom(sourceCellStyle.getBorderBottom()); - targetCellStyle.setBorderLeft(sourceCellStyle.getBorderLeft()); - targetCellStyle.setBorderRight(sourceCellStyle.getBorderRight()); - targetCellStyle.setBorderTop(sourceCellStyle.getBorderTop()); - targetCellStyle.setTopBorderColor(sourceCellStyle.getTopBorderColor()); - targetCellStyle.setBottomBorderColor(sourceCellStyle.getBottomBorderColor()); - targetCellStyle.setRightBorderColor(sourceCellStyle.getRightBorderColor()); - targetCellStyle.setLeftBorderColor(sourceCellStyle.getLeftBorderColor()); - // Background and foreground - targetCellStyle.setFillBackgroundColor(sourceCellStyle.getFillBackgroundColor()); - targetCellStyle.setFillForegroundColor(sourceCellStyle.getFillForegroundColor()); - // Data format - targetCellStyle.setDataFormat(sourceCellStyle.getDataFormat()); - targetCellStyle.setFillPattern(sourceCellStyle.getFillPattern()); - // toStyle.setFont(fromStyle.getFont(null)); - targetCellStyle.setHidden(sourceCellStyle.getHidden()); - targetCellStyle.setIndention(sourceCellStyle.getIndention()); - targetCellStyle.setLocked(sourceCellStyle.getLocked()); - targetCellStyle.setRotation(sourceCellStyle.getRotation()); - targetCellStyle.setVerticalAlignment(sourceCellStyle.getVerticalAlignment()); - targetCellStyle.setWrapText(sourceCellStyle.getWrapText()); - } - - /** - * Copy sheet. - * - * @param workbook the workbook - * @param sourceSheet the source sheet - * @param targetSheet the target sheet - * @param copyValue the copy value - */ - public static void copySheet(HSSFWorkbook workbook, HSSFSheet sourceSheet, HSSFSheet targetSheet, - boolean copyValue) { - mergerRegion(sourceSheet, targetSheet); - sourceSheet.rowIterator().forEachRemaining(oldRow -> { - val newRow = targetSheet.createRow(oldRow.getRowNum()); - copyRow(workbook, (HSSFRow) oldRow, newRow, copyValue); - }); - } - - /** - * Copy row. - * - * @param workbook the workbook - * @param sourceRow the source row - * @param targetRow the target row - * @param copyValue the copy value - */ - public static void copyRow(HSSFWorkbook workbook, HSSFRow sourceRow, HSSFRow targetRow, boolean copyValue) { - sourceRow.cellIterator().forEachRemaining(oldCell -> { - val newCell = targetRow.createCell(oldCell.getColumnIndex()); - copyCell(workbook, oldCell, newCell, copyValue); - }); - } - - /** - * Merger region. - * - * @param sourceSheet the source sheet - * @param targetSheet the target sheet - */ - public static void mergerRegion(HSSFSheet sourceSheet, HSSFSheet targetSheet) { - val sheetMergerCount = sourceSheet.getNumMergedRegions(); - for (var i = 0; i < sheetMergerCount; i++) { - val mergedRegionAt = sourceSheet.getMergedRegion(i); - targetSheet.addMergedRegion(mergedRegionAt); - } - } - - /** - * Copy cell. - * - * @param workbook the workbook - * @param sourceCell the source cell - * @param targetCell the target cell - * @param copyValue the copy value - */ - public static void copyCell(HSSFWorkbook workbook, HSSFCell sourceCell, HSSFCell targetCell, boolean copyValue) { - val newStyle = workbook.createCellStyle(); - copyCellStyle(sourceCell.getCellStyle(), newStyle); - targetCell.setCellStyle(newStyle); - if (sourceCell.getCellComment() != null) { - targetCell.setCellComment(sourceCell.getCellComment()); - } - val srcCellType = sourceCell.getCellType(); - targetCell.setCellType(srcCellType); - if (copyValue) { - switch (srcCellType) { - case NUMERIC: - if (DateUtil.isCellDateFormatted(sourceCell)) { - targetCell.setCellValue(sourceCell.getDateCellValue()); - } else { - targetCell.setCellValue(sourceCell.getNumericCellValue()); - } - break; - case STRING: - targetCell.setCellValue(sourceCell.getRichStringCellValue()); - break; - case BLANK: - break; - case BOOLEAN: - targetCell.setCellValue(sourceCell.getBooleanCellValue()); - break; - case ERROR: - targetCell.setCellErrorValue(FormulaError.forInt(sourceCell.getErrorCellValue())); - break; - case FORMULA: - targetCell.setCellFormula(sourceCell.getCellFormula()); - break; - default: - } - } - } - - /** - * Copy row. - * - * @param copyValue the copy value - * @param workbook the workbook - * @param sourceRow the source row - * @param targetRow the target row - */ - public static void copyRow(boolean copyValue, Workbook workbook, Row sourceRow, Row targetRow) { - targetRow.setHeight(sourceRow.getHeight()); - - sourceRow.cellIterator().forEachRemaining(oldCell -> { - Cell newCell = targetRow.createCell(oldCell.getColumnIndex()); - copyCell(workbook, oldCell, newCell, copyValue); - }); - - val worksheet = sourceRow.getSheet(); - - for (var i = 0; i < worksheet.getNumMergedRegions(); i++) { - val cellRangeAddress = worksheet.getMergedRegion(i); - if (cellRangeAddress.getFirstRow() == sourceRow.getRowNum()) { - val newCellRangeAddress = - new CellRangeAddress(targetRow.getRowNum(), - (targetRow.getRowNum() + (cellRangeAddress.getLastRow() - cellRangeAddress.getFirstRow())), - cellRangeAddress.getFirstColumn(), - cellRangeAddress.getLastColumn()); - worksheet.addMergedRegion(newCellRangeAddress); - } - } - } - - /** - * Copy cell. - * - * @param workbook the workbook - * @param sourceCell the source cell - * @param targetCell the target cell - * @param copyValue the copy value - */ - public static void copyCell(Workbook workbook, Cell sourceCell, Cell targetCell, boolean copyValue) { - if (sourceCell.getCellComment() != null) { - targetCell.setCellComment(sourceCell.getCellComment()); - } - val srcCellType = sourceCell.getCellType(); - if (copyValue) { - switch (srcCellType) { - case NUMERIC: - if (DateUtil.isCellDateFormatted(sourceCell)) { - targetCell.setCellValue(sourceCell.getDateCellValue()); - } else { - targetCell.setCellValue(sourceCell.getNumericCellValue()); - } - break; - case STRING: - targetCell.setCellValue(sourceCell.getRichStringCellValue()); - break; - case BLANK: - break; - case BOOLEAN: - targetCell.setCellValue(sourceCell.getBooleanCellValue()); - break; - case ERROR: - targetCell.setCellErrorValue(sourceCell.getErrorCellValue()); - break; - case FORMULA: - targetCell.setCellFormula(sourceCell.getCellFormula()); - break; - default: - } - } - } -} diff --git a/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/util/UserUtil.java b/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/util/UserUtil.java new file mode 100644 index 00000000..a0b8a6a8 --- /dev/null +++ b/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/util/UserUtil.java @@ -0,0 +1,39 @@ +package com.jmsoftware.maf.springcloudstarter.util; + +import cn.hutool.core.util.StrUtil; +import com.jmsoftware.maf.common.constant.MafHttpHeader; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +/** + * Description: UserUtil, change description here. + * + * @author Johnny Miller (锺俊), email: johnnysviva@outlook.com, date: 6/28/2021 1:40 PM + **/ +@Slf4j +public class UserUtil { + private UserUtil() { + } + + public static String getCurrentUsername() { + val servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); + return servletRequestAttributes.getRequest().getHeader(MafHttpHeader.X_USERNAME.getHeader()); + } + + /** + * Gets current id. + * + * @return the current id + * @throws IllegalArgumentException if X-ID is blank in HTTP header + */ + public static Long getCurrentId() { + val servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); + val xid = servletRequestAttributes.getRequest().getHeader(MafHttpHeader.X_ID.getHeader()); + if (StrUtil.isBlank(xid)) { + throw new IllegalArgumentException("Invalid X-ID"); + } + return Long.valueOf(xid); + } +} diff --git a/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/util/UsernameUtil.java b/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/util/UsernameUtil.java deleted file mode 100644 index f85c94db..00000000 --- a/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/util/UsernameUtil.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.jmsoftware.maf.springcloudstarter.util; - -import com.jmsoftware.maf.common.constant.MafHttpHeader; -import org.springframework.web.context.request.RequestContextHolder; -import org.springframework.web.context.request.ServletRequestAttributes; - -/** - * Description: UsernameUtil, change description here. - * - * @author Johnny Miller (锺俊), email: johnnysviva@outlook.com, date: 6/28/2021 1:40 PM - **/ -public class UsernameUtil { - private UsernameUtil() { - } - - public static String getCurrentUsername() { - final ServletRequestAttributes servletRequestAttributes = - (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); - return servletRequestAttributes.getRequest().getHeader(MafHttpHeader.X_USERNAME.getHeader()); - } -} diff --git a/universal-ui/pom.xml b/universal-ui/pom.xml index 58ccdf8b..7058e2f3 100644 --- a/universal-ui/pom.xml +++ b/universal-ui/pom.xml @@ -11,7 +11,7 @@ muscle-and-fitness-server com.jmsoftware.maf - 0.0.4 + 0.0.5